style: apply current rustfmt to the tree
A newer rustfmt wraps over-long match arms and call expressions that the version main was last formatted with left on one line. Pure formatting, no semantic change — split out so the friends-list feature commits stay focused on real changes. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -84,7 +84,6 @@ pub enum BandwidthStatus {
|
|||||||
Failed,
|
Failed,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fn default_status() -> BandwidthStatus {
|
fn default_status() -> BandwidthStatus {
|
||||||
BandwidthStatus::Unmeasured
|
BandwidthStatus::Unmeasured
|
||||||
}
|
}
|
||||||
@@ -116,11 +115,9 @@ pub fn save(cfg: &Config) -> Result<()> {
|
|||||||
let parent = path
|
let parent = path
|
||||||
.parent()
|
.parent()
|
||||||
.context("config path has no parent directory")?;
|
.context("config path has no parent directory")?;
|
||||||
fs::create_dir_all(parent)
|
fs::create_dir_all(parent).with_context(|| format!("failed to create {}", parent.display()))?;
|
||||||
.with_context(|| format!("failed to create {}", parent.display()))?;
|
|
||||||
|
|
||||||
let serialized =
|
let serialized = toml::to_string_pretty(cfg).context("failed to serialize config to TOML")?;
|
||||||
toml::to_string_pretty(cfg).context("failed to serialize config to TOML")?;
|
|
||||||
|
|
||||||
let tmp = parent.join(format!(".config.toml.tmp.{}", std::process::id()));
|
let tmp = parent.join(format!(".config.toml.tmp.{}", std::process::id()));
|
||||||
{
|
{
|
||||||
|
|||||||
+39
-13
@@ -85,7 +85,9 @@ fn install_hint_for_bin(bin: &str) -> String {
|
|||||||
let distro = detect_distro();
|
let distro = detect_distro();
|
||||||
let pkg = match bin {
|
let pkg = match bin {
|
||||||
"gst-launch-1.0" | "gst-inspect-1.0" => match distro.as_deref() {
|
"gst-launch-1.0" | "gst-inspect-1.0" => match distro.as_deref() {
|
||||||
Some("arch" | "cachyos" | "manjaro" | "endeavouros" | "artix" | "garuda") => "gstreamer gst-plugins-base",
|
Some("arch" | "cachyos" | "manjaro" | "endeavouros" | "artix" | "garuda") => {
|
||||||
|
"gstreamer gst-plugins-base"
|
||||||
|
}
|
||||||
Some("debian" | "ubuntu" | "pop" | "linuxmint") => "gstreamer1.0-tools",
|
Some("debian" | "ubuntu" | "pop" | "linuxmint") => "gstreamer1.0-tools",
|
||||||
Some("fedora" | "nobara") => "gstreamer1 gstreamer1-plugins-base-tools",
|
Some("fedora" | "nobara") => "gstreamer1 gstreamer1-plugins-base-tools",
|
||||||
_ => "gstreamer + tools",
|
_ => "gstreamer + tools",
|
||||||
@@ -98,7 +100,9 @@ fn install_hint_for_bin(bin: &str) -> String {
|
|||||||
_ => "pulseaudio-utils (provides `pactl`)",
|
_ => "pulseaudio-utils (provides `pactl`)",
|
||||||
},
|
},
|
||||||
"xwininfo" => match distro.as_deref() {
|
"xwininfo" => match distro.as_deref() {
|
||||||
Some("arch" | "cachyos" | "manjaro" | "endeavouros" | "artix" | "garuda") => "xorg-xwininfo",
|
Some("arch" | "cachyos" | "manjaro" | "endeavouros" | "artix" | "garuda") => {
|
||||||
|
"xorg-xwininfo"
|
||||||
|
}
|
||||||
Some("debian" | "ubuntu" | "pop" | "linuxmint") => "x11-utils",
|
Some("debian" | "ubuntu" | "pop" | "linuxmint") => "x11-utils",
|
||||||
Some("fedora" | "nobara") => "xorg-x11-utils",
|
Some("fedora" | "nobara") => "xorg-x11-utils",
|
||||||
Some("opensuse" | "opensuse-tumbleweed" | "opensuse-leap") => "xwininfo",
|
Some("opensuse" | "opensuse-tumbleweed" | "opensuse-leap") => "xwininfo",
|
||||||
@@ -113,56 +117,74 @@ fn install_hint_for_gst_element(name: &str) -> String {
|
|||||||
let distro = detect_distro();
|
let distro = detect_distro();
|
||||||
let pkg = match name {
|
let pkg = match name {
|
||||||
"pipewiresrc" => match distro.as_deref() {
|
"pipewiresrc" => match distro.as_deref() {
|
||||||
Some("arch" | "cachyos" | "manjaro" | "endeavouros" | "artix" | "garuda") => "gst-plugin-pipewire",
|
Some("arch" | "cachyos" | "manjaro" | "endeavouros" | "artix" | "garuda") => {
|
||||||
|
"gst-plugin-pipewire"
|
||||||
|
}
|
||||||
Some("debian" | "ubuntu" | "pop" | "linuxmint") => "gstreamer1.0-pipewire",
|
Some("debian" | "ubuntu" | "pop" | "linuxmint") => "gstreamer1.0-pipewire",
|
||||||
Some("fedora" | "nobara") => "pipewire-gstreamer",
|
Some("fedora" | "nobara") => "pipewire-gstreamer",
|
||||||
Some("opensuse" | "opensuse-tumbleweed" | "opensuse-leap") => "pipewire-gstreamer",
|
Some("opensuse" | "opensuse-tumbleweed" | "opensuse-leap") => "pipewire-gstreamer",
|
||||||
_ => "the GStreamer PipeWire plugin",
|
_ => "the GStreamer PipeWire plugin",
|
||||||
},
|
},
|
||||||
"vah264enc" => match distro.as_deref() {
|
"vah264enc" => match distro.as_deref() {
|
||||||
Some("arch" | "cachyos" | "manjaro" | "endeavouros" | "artix" | "garuda") => "gst-plugin-va",
|
Some("arch" | "cachyos" | "manjaro" | "endeavouros" | "artix" | "garuda") => {
|
||||||
|
"gst-plugin-va"
|
||||||
|
}
|
||||||
Some("debian" | "ubuntu" | "pop" | "linuxmint") => "gstreamer1.0-plugins-bad",
|
Some("debian" | "ubuntu" | "pop" | "linuxmint") => "gstreamer1.0-plugins-bad",
|
||||||
Some("fedora" | "nobara") => "gstreamer1-plugins-bad-free",
|
Some("fedora" | "nobara") => "gstreamer1-plugins-bad-free",
|
||||||
Some("opensuse" | "opensuse-tumbleweed" | "opensuse-leap") => "gstreamer-plugins-bad",
|
Some("opensuse" | "opensuse-tumbleweed" | "opensuse-leap") => "gstreamer-plugins-bad",
|
||||||
_ => "the GStreamer VA-API plugin (requires an H.264-capable GPU; almost all modern GPUs)",
|
_ => {
|
||||||
|
"the GStreamer VA-API plugin (requires an H.264-capable GPU; almost all modern GPUs)"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"x264enc" => match distro.as_deref() {
|
"x264enc" => match distro.as_deref() {
|
||||||
Some("arch" | "cachyos" | "manjaro" | "endeavouros" | "artix" | "garuda") => "gst-plugins-ugly",
|
Some("arch" | "cachyos" | "manjaro" | "endeavouros" | "artix" | "garuda") => {
|
||||||
|
"gst-plugins-ugly"
|
||||||
|
}
|
||||||
Some("debian" | "ubuntu" | "pop" | "linuxmint") => "gstreamer1.0-plugins-ugly",
|
Some("debian" | "ubuntu" | "pop" | "linuxmint") => "gstreamer1.0-plugins-ugly",
|
||||||
Some("fedora" | "nobara") => "gstreamer1-plugins-ugly",
|
Some("fedora" | "nobara") => "gstreamer1-plugins-ugly",
|
||||||
Some("opensuse" | "opensuse-tumbleweed" | "opensuse-leap") => "gstreamer-plugins-ugly",
|
Some("opensuse" | "opensuse-tumbleweed" | "opensuse-leap") => "gstreamer-plugins-ugly",
|
||||||
_ => "the GStreamer x264 plugin (plugins-ugly)",
|
_ => "the GStreamer x264 plugin (plugins-ugly)",
|
||||||
},
|
},
|
||||||
"ximagesrc" => match distro.as_deref() {
|
"ximagesrc" => match distro.as_deref() {
|
||||||
Some("arch" | "cachyos" | "manjaro" | "endeavouros" | "artix" | "garuda") => "gst-plugins-good",
|
Some("arch" | "cachyos" | "manjaro" | "endeavouros" | "artix" | "garuda") => {
|
||||||
|
"gst-plugins-good"
|
||||||
|
}
|
||||||
Some("debian" | "ubuntu" | "pop" | "linuxmint") => "gstreamer1.0-plugins-good",
|
Some("debian" | "ubuntu" | "pop" | "linuxmint") => "gstreamer1.0-plugins-good",
|
||||||
Some("fedora" | "nobara") => "gstreamer1-plugins-good",
|
Some("fedora" | "nobara") => "gstreamer1-plugins-good",
|
||||||
Some("opensuse" | "opensuse-tumbleweed" | "opensuse-leap") => "gstreamer-plugins-good",
|
Some("opensuse" | "opensuse-tumbleweed" | "opensuse-leap") => "gstreamer-plugins-good",
|
||||||
_ => "the GStreamer X11 plugin (plugins-good)",
|
_ => "the GStreamer X11 plugin (plugins-good)",
|
||||||
},
|
},
|
||||||
"videoscale" => match distro.as_deref() {
|
"videoscale" => match distro.as_deref() {
|
||||||
Some("arch" | "cachyos" | "manjaro" | "endeavouros" | "artix" | "garuda") => "gst-plugins-base",
|
Some("arch" | "cachyos" | "manjaro" | "endeavouros" | "artix" | "garuda") => {
|
||||||
|
"gst-plugins-base"
|
||||||
|
}
|
||||||
Some("debian" | "ubuntu" | "pop" | "linuxmint") => "gstreamer1.0-plugins-base",
|
Some("debian" | "ubuntu" | "pop" | "linuxmint") => "gstreamer1.0-plugins-base",
|
||||||
Some("fedora" | "nobara") => "gstreamer1-plugins-base",
|
Some("fedora" | "nobara") => "gstreamer1-plugins-base",
|
||||||
Some("opensuse" | "opensuse-tumbleweed" | "opensuse-leap") => "gstreamer-plugins-base",
|
Some("opensuse" | "opensuse-tumbleweed" | "opensuse-leap") => "gstreamer-plugins-base",
|
||||||
_ => "the GStreamer plugins-base set",
|
_ => "the GStreamer plugins-base set",
|
||||||
},
|
},
|
||||||
"h264parse" | "mpegtsmux" | "aacparse" => match distro.as_deref() {
|
"h264parse" | "mpegtsmux" | "aacparse" => match distro.as_deref() {
|
||||||
Some("arch" | "cachyos" | "manjaro" | "endeavouros" | "artix" | "garuda") => "gst-plugins-bad",
|
Some("arch" | "cachyos" | "manjaro" | "endeavouros" | "artix" | "garuda") => {
|
||||||
|
"gst-plugins-bad"
|
||||||
|
}
|
||||||
Some("debian" | "ubuntu" | "pop" | "linuxmint") => "gstreamer1.0-plugins-bad",
|
Some("debian" | "ubuntu" | "pop" | "linuxmint") => "gstreamer1.0-plugins-bad",
|
||||||
Some("fedora" | "nobara") => "gstreamer1-plugins-bad-free",
|
Some("fedora" | "nobara") => "gstreamer1-plugins-bad-free",
|
||||||
Some("opensuse" | "opensuse-tumbleweed" | "opensuse-leap") => "gstreamer-plugins-bad",
|
Some("opensuse" | "opensuse-tumbleweed" | "opensuse-leap") => "gstreamer-plugins-bad",
|
||||||
_ => "the GStreamer plugins-bad set",
|
_ => "the GStreamer plugins-bad set",
|
||||||
},
|
},
|
||||||
"pulsesrc" => match distro.as_deref() {
|
"pulsesrc" => match distro.as_deref() {
|
||||||
Some("arch" | "cachyos" | "manjaro" | "endeavouros" | "artix" | "garuda") => "gst-plugins-good",
|
Some("arch" | "cachyos" | "manjaro" | "endeavouros" | "artix" | "garuda") => {
|
||||||
|
"gst-plugins-good"
|
||||||
|
}
|
||||||
Some("debian" | "ubuntu" | "pop" | "linuxmint") => "gstreamer1.0-pulseaudio",
|
Some("debian" | "ubuntu" | "pop" | "linuxmint") => "gstreamer1.0-pulseaudio",
|
||||||
Some("fedora" | "nobara") => "gstreamer1-plugins-good",
|
Some("fedora" | "nobara") => "gstreamer1-plugins-good",
|
||||||
Some("opensuse" | "opensuse-tumbleweed" | "opensuse-leap") => "gstreamer-plugins-good",
|
Some("opensuse" | "opensuse-tumbleweed" | "opensuse-leap") => "gstreamer-plugins-good",
|
||||||
_ => "the GStreamer PulseAudio plugin",
|
_ => "the GStreamer PulseAudio plugin",
|
||||||
},
|
},
|
||||||
"avenc_aac" => match distro.as_deref() {
|
"avenc_aac" => match distro.as_deref() {
|
||||||
Some("arch" | "cachyos" | "manjaro" | "endeavouros" | "artix" | "garuda") => "gst-libav",
|
Some("arch" | "cachyos" | "manjaro" | "endeavouros" | "artix" | "garuda") => {
|
||||||
|
"gst-libav"
|
||||||
|
}
|
||||||
Some("debian" | "ubuntu" | "pop" | "linuxmint") => "gstreamer1.0-libav",
|
Some("debian" | "ubuntu" | "pop" | "linuxmint") => "gstreamer1.0-libav",
|
||||||
Some("fedora" | "nobara") => "gstreamer1-libav",
|
Some("fedora" | "nobara") => "gstreamer1-libav",
|
||||||
Some("opensuse" | "opensuse-tumbleweed" | "opensuse-leap") => "gstreamer-libav",
|
Some("opensuse" | "opensuse-tumbleweed" | "opensuse-leap") => "gstreamer-libav",
|
||||||
@@ -175,10 +197,14 @@ fn install_hint_for_gst_element(name: &str) -> String {
|
|||||||
|
|
||||||
fn install_command(distro: &Option<String>, pkg: &str) -> String {
|
fn install_command(distro: &Option<String>, pkg: &str) -> String {
|
||||||
let cmd = match distro.as_deref() {
|
let cmd = match distro.as_deref() {
|
||||||
Some("arch" | "cachyos" | "manjaro" | "endeavouros" | "artix" | "garuda") => format!("sudo pacman -S {pkg}"),
|
Some("arch" | "cachyos" | "manjaro" | "endeavouros" | "artix" | "garuda") => {
|
||||||
|
format!("sudo pacman -S {pkg}")
|
||||||
|
}
|
||||||
Some("debian" | "ubuntu" | "pop" | "linuxmint") => format!("sudo apt install {pkg}"),
|
Some("debian" | "ubuntu" | "pop" | "linuxmint") => format!("sudo apt install {pkg}"),
|
||||||
Some("fedora" | "nobara") => format!("sudo dnf install {pkg}"),
|
Some("fedora" | "nobara") => format!("sudo dnf install {pkg}"),
|
||||||
Some("opensuse" | "opensuse-tumbleweed" | "opensuse-leap") => format!("sudo zypper install {pkg}"),
|
Some("opensuse" | "opensuse-tumbleweed" | "opensuse-leap") => {
|
||||||
|
format!("sudo zypper install {pkg}")
|
||||||
|
}
|
||||||
_ => format!("install the `{pkg}` package via your distro's package manager"),
|
_ => format!("install the `{pkg}` package via your distro's package manager"),
|
||||||
};
|
};
|
||||||
format!("Install hint: {cmd}")
|
format!("Install hint: {cmd}")
|
||||||
|
|||||||
+12
-4
@@ -261,12 +261,20 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn capture_state_round_trips() {
|
fn capture_state_round_trips() {
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
parse(Event::Capture { state: EmitState::Started }),
|
parse(Event::Capture {
|
||||||
ChildEvent::Capture { state: CaptureState::Started }
|
state: EmitState::Started
|
||||||
|
}),
|
||||||
|
ChildEvent::Capture {
|
||||||
|
state: CaptureState::Started
|
||||||
|
}
|
||||||
));
|
));
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
parse(Event::Capture { state: EmitState::Stopped }),
|
parse(Event::Capture {
|
||||||
ChildEvent::Capture { state: CaptureState::Stopped }
|
state: EmitState::Stopped
|
||||||
|
}),
|
||||||
|
ChildEvent::Capture {
|
||||||
|
state: CaptureState::Stopped
|
||||||
|
}
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+23
-19
@@ -52,9 +52,8 @@ impl Routing {
|
|||||||
let pid = std::process::id();
|
let pid = std::process::id();
|
||||||
let sink_name = format!("pixelpass_capture_{pid}");
|
let sink_name = format!("pixelpass_capture_{pid}");
|
||||||
|
|
||||||
let sink_module =
|
let sink_module = load_module(&["module-null-sink", &format!("sink_name={sink_name}")])
|
||||||
load_module(&["module-null-sink", &format!("sink_name={sink_name}")])
|
.context("failed to load module-null-sink")?;
|
||||||
.context("failed to load module-null-sink")?;
|
|
||||||
|
|
||||||
// 20ms loopback latency keeps the mirrored audio tight; pactl's
|
// 20ms loopback latency keeps the mirrored audio tight; pactl's
|
||||||
// default of 200ms is enough to be perceptible.
|
// default of 200ms is enough to be perceptible.
|
||||||
@@ -205,7 +204,9 @@ fn parse_sink_inputs(stdout: &[u8]) -> Result<Vec<App>> {
|
|||||||
serde_json::from_slice(stdout).context("pactl returned unparseable JSON")?;
|
serde_json::from_slice(stdout).context("pactl returned unparseable JSON")?;
|
||||||
let mut counts: BTreeMap<String, u32> = BTreeMap::new();
|
let mut counts: BTreeMap<String, u32> = BTreeMap::new();
|
||||||
for entry in entries {
|
for entry in entries {
|
||||||
let Some(name) = entry.properties.application_name else { continue };
|
let Some(name) = entry.properties.application_name else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
let trimmed = name.trim();
|
let trimmed = name.trim();
|
||||||
if trimmed.is_empty() {
|
if trimmed.is_empty() {
|
||||||
continue;
|
continue;
|
||||||
@@ -359,16 +360,14 @@ fn run_router(
|
|||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
use pipewire::{self as pw, types::ObjectType};
|
use pipewire::{self as pw, types::ObjectType};
|
||||||
|
|
||||||
let main_loop = pw::main_loop::MainLoopRc::new(None)
|
let main_loop =
|
||||||
.context("pw main loop construction failed")?;
|
pw::main_loop::MainLoopRc::new(None).context("pw main loop construction failed")?;
|
||||||
let context = pw::context::ContextRc::new(&main_loop, None)
|
let context =
|
||||||
.context("pw context construction failed")?;
|
pw::context::ContextRc::new(&main_loop, None).context("pw context construction failed")?;
|
||||||
let core = context
|
let core = context
|
||||||
.connect_rc(None)
|
.connect_rc(None)
|
||||||
.context("pw core connect failed (is the daemon running?)")?;
|
.context("pw core connect failed (is the daemon running?)")?;
|
||||||
let registry = core
|
let registry = core.get_registry_rc().context("pw get_registry failed")?;
|
||||||
.get_registry_rc()
|
|
||||||
.context("pw get_registry failed")?;
|
|
||||||
|
|
||||||
let state = Rc::new(RefCell::new(RouterState {
|
let state = Rc::new(RefCell::new(RouterState {
|
||||||
sink_serial: None,
|
sink_serial: None,
|
||||||
@@ -409,20 +408,21 @@ fn run_router(
|
|||||||
let _reg_listener = registry
|
let _reg_listener = registry
|
||||||
.add_listener_local()
|
.add_listener_local()
|
||||||
.global(move |obj| {
|
.global(move |obj| {
|
||||||
let Some(reg) = registry_weak.upgrade() else { return };
|
let Some(reg) = registry_weak.upgrade() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
match obj.type_ {
|
match obj.type_ {
|
||||||
ObjectType::Node => {
|
ObjectType::Node => {
|
||||||
let Some(props) = obj.props.as_ref() else { return };
|
let Some(props) = obj.props.as_ref() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
if props.get("node.name") == Some(sink_name_owned.as_str()) {
|
if props.get("node.name") == Some(sink_name_owned.as_str()) {
|
||||||
if let Some(serial) = props
|
if let Some(serial) = props
|
||||||
.get("object.serial")
|
.get("object.serial")
|
||||||
.and_then(|s| s.parse::<u32>().ok())
|
.and_then(|s| s.parse::<u32>().ok())
|
||||||
{
|
{
|
||||||
state_for_reg.borrow_mut().sink_serial = Some(serial);
|
state_for_reg.borrow_mut().sink_serial = Some(serial);
|
||||||
tracing::info!(
|
tracing::info!(serial, "audio routing: pixelpass sink registered");
|
||||||
serial,
|
|
||||||
"audio routing: pixelpass sink registered"
|
|
||||||
);
|
|
||||||
try_flush(&state_for_reg, &event_tx_for_reg);
|
try_flush(&state_for_reg, &event_tx_for_reg);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
@@ -430,7 +430,9 @@ fn run_router(
|
|||||||
if props.get("media.class") != Some("Stream/Output/Audio") {
|
if props.get("media.class") != Some("Stream/Output/Audio") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let Some(app) = props.get("application.name") else { return };
|
let Some(app) = props.get("application.name") else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
if !app.eq_ignore_ascii_case(&filter_lower) {
|
if !app.eq_ignore_ascii_case(&filter_lower) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -443,7 +445,9 @@ fn run_router(
|
|||||||
try_flush(&state_for_reg, &event_tx_for_reg);
|
try_flush(&state_for_reg, &event_tx_for_reg);
|
||||||
}
|
}
|
||||||
ObjectType::Metadata => {
|
ObjectType::Metadata => {
|
||||||
let Some(props) = obj.props.as_ref() else { return };
|
let Some(props) = obj.props.as_ref() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
if props.get("metadata.name") != Some("default") {
|
if props.get("metadata.name") != Some("default") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
+34
-7
@@ -256,7 +256,9 @@ fn spawn_kick_listener(sup_tx: mpsc::Sender<SupervisorMsg>) {
|
|||||||
let Some(id) = line.trim().strip_prefix("kick ") else {
|
let Some(id) = line.trim().strip_prefix("kick ") else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
let msg = SupervisorMsg::KickViewer { id: id.trim().to_string() };
|
let msg = SupervisorMsg::KickViewer {
|
||||||
|
id: id.trim().to_string(),
|
||||||
|
};
|
||||||
// blocking_send is valid here: this is a plain thread, not inside
|
// blocking_send is valid here: this is a plain thread, not inside
|
||||||
// the tokio runtime. An Err means the supervisor closed — stop.
|
// the tokio runtime. An Err means the supervisor closed — stop.
|
||||||
if sup_tx.blocking_send(msg).is_err() {
|
if sup_tx.blocking_send(msg).is_err() {
|
||||||
@@ -315,7 +317,11 @@ async fn supervise(
|
|||||||
viewers.insert(id.clone(), cancel);
|
viewers.insert(id.clone(), cancel);
|
||||||
let active = viewers.len() as u32;
|
let active = viewers.len() as u32;
|
||||||
let _ = reply.send(Ok(port));
|
let _ = reply.send(Ok(port));
|
||||||
output::emit(output::Event::ViewerJoined { id: &id, active, max: max_viewers });
|
output::emit(output::Event::ViewerJoined {
|
||||||
|
id: &id,
|
||||||
|
active,
|
||||||
|
max: max_viewers,
|
||||||
|
});
|
||||||
tracing::info!(active, cap = max_viewers, "viewer joined");
|
tracing::info!(active, cap = max_viewers, "viewer joined");
|
||||||
}
|
}
|
||||||
SupervisorMsg::RemoveViewer { id } => {
|
SupervisorMsg::RemoveViewer { id } => {
|
||||||
@@ -325,7 +331,11 @@ async fn supervise(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let active = viewers.len() as u32;
|
let active = viewers.len() as u32;
|
||||||
output::emit(output::Event::ViewerLeft { id: &id, active, max: max_viewers });
|
output::emit(output::Event::ViewerLeft {
|
||||||
|
id: &id,
|
||||||
|
active,
|
||||||
|
max: max_viewers,
|
||||||
|
});
|
||||||
tracing::info!(active, cap = max_viewers, "viewer left");
|
tracing::info!(active, cap = max_viewers, "viewer left");
|
||||||
if active == 0
|
if active == 0
|
||||||
&& let Some(h) = handle.take()
|
&& let Some(h) = handle.take()
|
||||||
@@ -372,10 +382,25 @@ fn print_host_banner(
|
|||||||
eprintln!("┌─ PixelPass · host ─────────────────────────────────────────");
|
eprintln!("┌─ PixelPass · host ─────────────────────────────────────────");
|
||||||
eprintln!("│ display server : {display:?}");
|
eprintln!("│ display server : {display:?}");
|
||||||
eprintln!("│ capture : {}", capture_summary(opts));
|
eprintln!("│ capture : {}", capture_summary(opts));
|
||||||
eprintln!("│ quality : {} — {}", quality.label, quality.dimensions_summary());
|
eprintln!(
|
||||||
|
"│ quality : {} — {}",
|
||||||
|
quality.label,
|
||||||
|
quality.dimensions_summary()
|
||||||
|
);
|
||||||
eprintln!("│ ({})", quality.note);
|
eprintln!("│ ({})", quality.note);
|
||||||
eprintln!("│ hw encode : {}", if opts.no_hwencode { "off (software x264)" } else { "on (VAAPI H.264)" });
|
eprintln!(
|
||||||
eprintln!("│ max viewers : {} ({})", resolution.value, resolution.source.label());
|
"│ hw encode : {}",
|
||||||
|
if opts.no_hwencode {
|
||||||
|
"off (software x264)"
|
||||||
|
} else {
|
||||||
|
"on (VAAPI H.264)"
|
||||||
|
}
|
||||||
|
);
|
||||||
|
eprintln!(
|
||||||
|
"│ max viewers : {} ({})",
|
||||||
|
resolution.value,
|
||||||
|
resolution.source.label()
|
||||||
|
);
|
||||||
eprintln!("│");
|
eprintln!("│");
|
||||||
if clipboard_ok {
|
if clipboard_ok {
|
||||||
eprintln!("│ Your share code has been copied to your clipboard.");
|
eprintln!("│ Your share code has been copied to your clipboard.");
|
||||||
@@ -439,7 +464,9 @@ fn resolve_max_viewers(opts: &HostOpts, effective_bitrate: u32) -> MaxViewersRes
|
|||||||
let n = bandwidth::recommended_max_viewers(upstream, effective_bitrate);
|
let n = bandwidth::recommended_max_viewers(upstream, effective_bitrate);
|
||||||
return MaxViewersResolution {
|
return MaxViewersResolution {
|
||||||
value: n,
|
value: n,
|
||||||
source: MaxViewersSource::BandwidthMeasurement { safe_mbps: upstream },
|
source: MaxViewersSource::BandwidthMeasurement {
|
||||||
|
safe_mbps: upstream,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
MaxViewersResolution {
|
MaxViewersResolution {
|
||||||
|
|||||||
@@ -308,7 +308,9 @@ async fn default_audio_monitor() -> Result<String> {
|
|||||||
.arg("get-default-sink")
|
.arg("get-default-sink")
|
||||||
.output()
|
.output()
|
||||||
.await
|
.await
|
||||||
.context("failed to run `pactl get-default-sink` (install pulseaudio-utils or pipewire-pulse)")?;
|
.context(
|
||||||
|
"failed to run `pactl get-default-sink` (install pulseaudio-utils or pipewire-pulse)",
|
||||||
|
)?;
|
||||||
if !output.status.success() {
|
if !output.status.success() {
|
||||||
bail!(
|
bail!(
|
||||||
"pactl get-default-sink failed: {}",
|
"pactl get-default-sink failed: {}",
|
||||||
|
|||||||
+31
-7
@@ -27,10 +27,26 @@ impl Quality {
|
|||||||
/// values and resolves to one of the others at runtime (see [`resolve_auto`]).
|
/// values and resolves to one of the others at runtime (see [`resolve_auto`]).
|
||||||
fn preset(self) -> Option<Preset> {
|
fn preset(self) -> Option<Preset> {
|
||||||
let p = match self {
|
let p = match self {
|
||||||
Quality::Source => Preset { max_height: None, bitrate: 6000, framerate: 30 },
|
Quality::Source => Preset {
|
||||||
Quality::High => Preset { max_height: Some(1080), bitrate: 4000, framerate: 30 },
|
max_height: None,
|
||||||
Quality::Medium => Preset { max_height: Some(720), bitrate: 2500, framerate: 30 },
|
bitrate: 6000,
|
||||||
Quality::Low => Preset { max_height: Some(480), bitrate: 1000, framerate: 30 },
|
framerate: 30,
|
||||||
|
},
|
||||||
|
Quality::High => Preset {
|
||||||
|
max_height: Some(1080),
|
||||||
|
bitrate: 4000,
|
||||||
|
framerate: 30,
|
||||||
|
},
|
||||||
|
Quality::Medium => Preset {
|
||||||
|
max_height: Some(720),
|
||||||
|
bitrate: 2500,
|
||||||
|
framerate: 30,
|
||||||
|
},
|
||||||
|
Quality::Low => Preset {
|
||||||
|
max_height: Some(480),
|
||||||
|
bitrate: 1000,
|
||||||
|
framerate: 30,
|
||||||
|
},
|
||||||
Quality::Auto => return None,
|
Quality::Auto => return None,
|
||||||
};
|
};
|
||||||
Some(p)
|
Some(p)
|
||||||
@@ -49,7 +65,12 @@ impl Quality {
|
|||||||
|
|
||||||
/// Fixed presets in descending quality order — Auto walks this to find the
|
/// Fixed presets in descending quality order — Auto walks this to find the
|
||||||
/// best one whose per-viewer bitrate fits the measured upstream budget.
|
/// best one whose per-viewer bitrate fits the measured upstream budget.
|
||||||
const AUTO_LADDER: [Quality; 4] = [Quality::Source, Quality::High, Quality::Medium, Quality::Low];
|
const AUTO_LADDER: [Quality; 4] = [
|
||||||
|
Quality::Source,
|
||||||
|
Quality::High,
|
||||||
|
Quality::Medium,
|
||||||
|
Quality::Low,
|
||||||
|
];
|
||||||
|
|
||||||
/// Auto's fallback when there is no usable bandwidth measurement.
|
/// Auto's fallback when there is no usable bandwidth measurement.
|
||||||
const AUTO_FALLBACK: Quality = Quality::Medium;
|
const AUTO_FALLBACK: Quality = Quality::Medium;
|
||||||
@@ -146,7 +167,9 @@ fn resolve_auto(safe_mbps: Option<f64>, sizing_viewers: u32) -> (Preset, String,
|
|||||||
(
|
(
|
||||||
preset,
|
preset,
|
||||||
format!("Auto → {}", chosen.name()),
|
format!("Auto → {}", chosen.name()),
|
||||||
format!("auto: {safe_mbps:.1} Mbps safe ÷ {n} viewer(s) = {budget_mbps:.1} Mbps each"),
|
format!(
|
||||||
|
"auto: {safe_mbps:.1} Mbps safe ÷ {n} viewer(s) = {budget_mbps:.1} Mbps each"
|
||||||
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
@@ -154,7 +177,8 @@ fn resolve_auto(safe_mbps: Option<f64>, sizing_viewers: u32) -> (Preset, String,
|
|||||||
(
|
(
|
||||||
preset,
|
preset,
|
||||||
format!("Auto → {}", AUTO_FALLBACK.name()),
|
format!("Auto → {}", AUTO_FALLBACK.name()),
|
||||||
"auto fallback — no bandwidth measurement (run `pixelpass --reconfigure`)".to_string(),
|
"auto fallback — no bandwidth measurement (run `pixelpass --reconfigure`)"
|
||||||
|
.to_string(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+15
-5
@@ -26,7 +26,11 @@ pub async fn start(opts: &HostOpts, quality: &EffectiveQuality) -> Result<Captur
|
|||||||
.context("could not reach the xdg-desktop-portal ScreenCast interface")?;
|
.context("could not reach the xdg-desktop-portal ScreenCast interface")?;
|
||||||
let session = proxy.create_session().await?;
|
let session = proxy.create_session().await?;
|
||||||
|
|
||||||
let source = if opts.window { SourceType::Window } else { SourceType::Monitor };
|
let source = if opts.window {
|
||||||
|
SourceType::Window
|
||||||
|
} else {
|
||||||
|
SourceType::Monitor
|
||||||
|
};
|
||||||
proxy
|
proxy
|
||||||
.select_sources(
|
.select_sources(
|
||||||
&session,
|
&session,
|
||||||
@@ -70,10 +74,16 @@ pub async fn start(opts: &HostOpts, quality: &EffectiveQuality) -> Result<Captur
|
|||||||
"do-timestamp=true".to_string(),
|
"do-timestamp=true".to_string(),
|
||||||
];
|
];
|
||||||
|
|
||||||
pipeline::spawn(opts, quality, Some((w as u32, h as u32)), source_args, move || {
|
pipeline::spawn(
|
||||||
// Parent no longer needs the pipewire fd — gst inherited its own copy.
|
opts,
|
||||||
let _ = close(raw_fd);
|
quality,
|
||||||
})
|
Some((w as u32, h as u32)),
|
||||||
|
source_args,
|
||||||
|
move || {
|
||||||
|
// Parent no longer needs the pipewire fd — gst inherited its own copy.
|
||||||
|
let _ = close(raw_fd);
|
||||||
|
},
|
||||||
|
)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+6
-5
@@ -180,10 +180,7 @@ async fn preflight_if_needed(theme: &ColorfulTheme) {
|
|||||||
eprintln!();
|
eprintln!();
|
||||||
let Ok(choice) = Select::with_theme(theme)
|
let Ok(choice) = Select::with_theme(theme)
|
||||||
.with_prompt("Last bandwidth test failed. Try again?")
|
.with_prompt("Last bandwidth test failed. Try again?")
|
||||||
.items([
|
.items(["Yes — retry now", "No — use the conservative default"])
|
||||||
"Yes — retry now",
|
|
||||||
"No — use the conservative default",
|
|
||||||
])
|
|
||||||
.default(0)
|
.default(0)
|
||||||
.interact()
|
.interact()
|
||||||
else {
|
else {
|
||||||
@@ -344,5 +341,9 @@ pub fn prompt_player() -> Result<Player> {
|
|||||||
.items(["mpv", "VLC"])
|
.items(["mpv", "VLC"])
|
||||||
.default(0)
|
.default(0)
|
||||||
.interact()?;
|
.interact()?;
|
||||||
Ok(if choice == 0 { Player::Mpv } else { Player::Vlc })
|
Ok(if choice == 0 {
|
||||||
|
Player::Mpv
|
||||||
|
} else {
|
||||||
|
Player::Vlc
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-1
@@ -73,7 +73,11 @@ async fn main() -> Result<()> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn init_tracing(verbose: bool) {
|
fn init_tracing(verbose: bool) {
|
||||||
let default = if verbose { "pixelpass=trace,iroh=info" } else { "pixelpass=info,iroh=warn" };
|
let default = if verbose {
|
||||||
|
"pixelpass=trace,iroh=info"
|
||||||
|
} else {
|
||||||
|
"pixelpass=info,iroh=warn"
|
||||||
|
};
|
||||||
let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(default));
|
let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(default));
|
||||||
// Tracing MUST write to stderr. `tracing_subscriber::fmt()` defaults its
|
// Tracing MUST write to stderr. `tracing_subscriber::fmt()` defaults its
|
||||||
// writer to stdout, but with `--output json` stdout carries the JSON event
|
// writer to stdout, but with `--output json` stdout carries the JSON event
|
||||||
|
|||||||
+4
-4
@@ -110,9 +110,7 @@ pub async fn run() -> Result<()> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if live_skipped > 0 {
|
if live_skipped > 0 {
|
||||||
println!(
|
println!("[pixelpass] --repair: left {live_skipped} live pixelpass host(s) alone.");
|
||||||
"[pixelpass] --repair: left {live_skipped} live pixelpass host(s) alone."
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if failed > 0 {
|
if failed > 0 {
|
||||||
@@ -154,7 +152,9 @@ fn list_modules() -> Result<Vec<Module>> {
|
|||||||
for line in text.lines() {
|
for line in text.lines() {
|
||||||
let mut parts = line.splitn(4, '\t');
|
let mut parts = line.splitn(4, '\t');
|
||||||
let Some(id_str) = parts.next() else { continue };
|
let Some(id_str) = parts.next() else { continue };
|
||||||
let Ok(id) = id_str.parse::<u32>() else { continue };
|
let Ok(id) = id_str.parse::<u32>() else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
let Some(name) = parts.next() else { continue };
|
let Some(name) = parts.next() else { continue };
|
||||||
let args = parts.next().unwrap_or("").to_string();
|
let args = parts.next().unwrap_or("").to_string();
|
||||||
modules.push(Module {
|
modules.push(Module {
|
||||||
|
|||||||
Reference in New Issue
Block a user