Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions docs/DESKTOP.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
40 changes: 39 additions & 1 deletion src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
@@ -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())
Expand Down
Loading