diff --git a/docs/DESKTOP.md b/docs/DESKTOP.md index 02a3b78..1b30fc6 100644 --- a/docs/DESKTOP.md +++ b/docs/DESKTOP.md @@ -61,6 +61,27 @@ sudo apt install gstreamer1.0-plugins-good gstreamer1.0-libav # Debian Check with `gst-inspect-1.0 autoaudiosink`. If that says "No such element", the app will be silent. +**And `pulsesink` is silent on Bluetooth.** Having the plugins is not the end of it. WebKit asks +`autoaudiosink` to choose the output element; autoaudiosink prefers `pulsesink`; and pulsesink +talking to PipeWire's PulseAudio compatibility layer plays nothing at all on a Bluetooth sink. The +failure is invisible from inside the page — the stream opens, unmuted, at full volume, the +AudioContext says `running`, `currentTime` advances, and the samples go nowhere. Measured on the +same page with the same packages, same Bluetooth device: + +| sink element | peak | +|---|---| +| `autoaudiosink` → `pulsesink` | 0.0000 | +| `pipewiresink` ranked MAX | 0.3349 | + +So `src-tauri/src/lib.rs` sets `GST_PLUGIN_FEATURE_RANK=pipewiresink:MAX` before the webview +starts, which makes the sink follow the desktop's default output. Set that variable yourself to +override it. Browsers never use GStreamer, which is why chopdeck.com plays on a machine where the +desktop app is mute — a difference worth remembering before blaming the machine. + +One more, not a bug: WebKit reports the AudioContext state as `interrupted` (not a standard state) +whenever the window does not have focus, and freezes `currentTime`. An unfocused window is silent +by design; it resumes on focus. + The builds are not code-signed yet. macOS will refuse to open the app until you right-click it and choose Open once; Windows SmartScreen asks for confirmation. Signing (an Apple Developer ID and a Windows certificate) is the step to take before listing on itch.io or Steam, and it is only secrets diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index bff60cb..db07cc3 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1,8 +1,46 @@ // Chop Deck desktop. Everything the machine does lives in the web build under ../dist; this shell only // adds what a browser tab cannot: a native save dialog for SAVE mode, opening links in the system -// browser, and a second window for the owner's manual. +// browser, a second window for the owner's manual, and on Linux one nudge to GStreamer so the machine +// is audible. + +/// WebKitGTK has no audio stack of its own: Web Audio leaves through GStreamer, and WebKit asks +/// `autoaudiosink` to choose the output element. autoaudiosink prefers `pulsesink`, and pulsesink +/// talking to PipeWire's PulseAudio compatibility layer is **silent on Bluetooth outputs** -- the +/// stream opens, unmuted, at full volume, the AudioContext reports "running", and not one sample +/// arrives. Measured on one machine, same page, same packages, same Bluetooth sink: +/// +/// | sink element | peak | +/// |---------------------------|--------| +/// | autoaudiosink -> pulsesink| 0.0000 | +/// | pipewiresink ranked MAX | 0.3349 | +/// +/// Ranking the native `pipewiresink` top fixes it and costs nothing on wired outputs, where +/// pulsesink happened to work anyway. Browsers are unaffected because they never use GStreamer, +/// which is why chopdeck.com plays on a machine where this app did not. +/// +/// An existing `GST_PLUGIN_FEATURE_RANK` is respected: we append rather than replace, and do +/// nothing at all if the user has already said something about pipewiresink. This must run before +/// the webview starts, because GStreamer reads the variable when it builds its registry. +#[cfg(target_os = "linux")] +fn prefer_pipewire_audio_sink() { + const KEY: &str = "GST_PLUGIN_FEATURE_RANK"; + let existing = std::env::var(KEY).unwrap_or_default(); + if existing.contains("pipewiresink") { + return; + } + let next = if existing.is_empty() { + "pipewiresink:MAX".to_owned() + } else { + format!("{existing},pipewiresink:MAX") + }; + std::env::set_var(KEY, next); +} + #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { + #[cfg(target_os = "linux")] + prefer_pipewire_audio_sink(); + tauri::Builder::default() .plugin(tauri_plugin_dialog::init()) .plugin(tauri_plugin_fs::init())