Skip to content

[Bug] Linux: notification sounds never play, WebKitGTK can't load media from Tauri's custom URI scheme (MEDIA_ERR_SRC_NOT_SUPPORTED) #2562

Description

@JoeMattie

Describe the bug

On Linux, notification sounds never play. playNotificationSound() builds new Audio("/sounds/<name>.mp3") (desktop/src/features/notifications/lib/sound.ts), which resolves against the page origin. On Linux, Tauri serves frontendDist through a custom URI scheme registered with webkit_web_context_register_uri_scheme (tauri://localhost), and WebKitGTK's media backend cannot load media from a custom URI scheme. The element fails with MEDIA_ERR_SRC_NOT_SUPPORTED (code 4) at readyState 0, and both error paths in playNotificationSound swallow it:

audio.play().catch(() => {
  // Best-effort — user may not have interacted with the page yet.
});

so there is no console output, no toast, and no telemetry. The desktop notification itself still appears, which is why this reads as "sound settings do nothing" rather than "notifications are broken". On GNOME the notification daemon plays its own default alert, so users can hear a sound while never hearing the sound they picked in Settings.

This is Linux-only. WKWebView and WebView2 both serve media from their custom scheme handlers.

To Reproduce:

  1. Run Buzz Desktop on Linux (AppImage or a local pnpm tauri dev build).
  2. Settings > Notifications: pick any sound for @Mentions, confirm alerts are on.
  3. Have someone @mention you in a channel.
  4. The desktop notification appears. The selected .mp3 never plays.

Minimal standalone repro, no Buzz needed. Requires gjs and gir1.2-webkit2-4.1 (Debian/Ubuntu: gjs libwebkit2gtk-4.1-0 gir1.2-webkit2-4.1). Put any mp3 at /tmp/flutter.mp3:

// repro.js (run: gjs repro.js)
imports.gi.versions.Gtk = '3.0';
const { Gtk, WebKit2, GLib, Gio } = imports.gi;
Gtk.init(null);

const HTML = `<!doctype html><html><body><script>
const a = new Audio('/sounds/flutter.mp3');
a.addEventListener('ended', () => document.title = 'RESULT|ENDED');
a.addEventListener('error', () => document.title =
  'RESULT|MEDIA_ERROR|readyState=' + a.readyState + '|code=' + a.error.code);
a.play().catch(e => document.title = 'RESULT|PLAY_REJECTED|' + e.name);
<\/script></body></html>`;

WebKit2.WebContext.get_default().register_uri_scheme('tauri', (req) => {
  const path = req.get_path();
  const [mime, data] = path === '/sounds/flutter.mp3'
    ? ['audio/mpeg', GLib.file_get_contents('/tmp/flutter.mp3')[1]]
    : ['text/html', new TextEncoder().encode(HTML)];
  const bytes = new GLib.Bytes(data);
  req.finish(Gio.MemoryInputStream.new_from_bytes(bytes), bytes.get_size(), mime);
});

const w = new Gtk.Window({ default_width: 200, default_height: 100 });
// wry sets AutoplayPolicy.Allow by default, so match that here.
const v = new WebKit2.WebView({
  website_policies: new WebKit2.WebsitePolicies({ autoplay: WebKit2.AutoplayPolicy.ALLOW }),
});
w.add(v);
v.connect('notify::title', () => {
  if ((v.title || '').startsWith('RESULT|')) { print(v.title); Gtk.main_quit(); }
});
v.load_uri('tauri://localhost/index.html');
w.show_all();
Gtk.main();

Expected behavior:

The selected notification sound plays. Failing that, the failure is visible somewhere instead of being swallowed by an empty catch.

Supporting Material

Four runs, same machine, same desktop/public/sounds/flutter.mp3, isolated GStreamer registry each time. Only the delivery URL changes:

source URL WebKitGTK 2.52.3 (system) WebKitGTK bundled in Buzz_0.4.23_amd64.AppImage
file:///tmp/.../flutter.mp3 ENDED, readyState=4 ENDED, readyState=3
tauri://localhost/sounds/flutter.mp3 MEDIA_ERROR, readyState=0, err=4 MEDIA_ERROR, readyState=0, err=4
fetch() then blob: URL ENDED, readyState=4 ENDED, readyState=3

fetch('/sounds/flutter.mp3') over the custom scheme returns 200 with the full 15597 bytes, so the scheme handler and the asset itself are fine. Only the media element's loader rejects the URL.

The bundled AppImage WebKit behaves identically to the system one, so this is not an AppImage packaging problem. It reproduces on a plain distro WebKitGTK.

Two related notes while in this code:

  1. sound.ts swallows both failure modes (try/catch around getAudio and .catch(() => {}) on play()). Even a console.warn in the reject path would have made this self-diagnosing.
  2. In useAppShellDesktopNotifications.ts and use-feed-desktop-notifications.ts the sound is gated on sendDesktopNotification() resolving true. If OS notification delivery fails, the sound is suppressed too. These are separate user-facing features and probably should not be coupled.

Suggested fix

Load the sound bytes once and play from a blob: URL. Verified working on both WebKit builds above:

const cache = new Map<SoundName, HTMLAudioElement>();

async function getAudio(name: SoundName): Promise<HTMLAudioElement> {
  let audio = cache.get(name);
  if (!audio) {
    // WebKitGTK's media backend cannot load from Tauri's custom URI scheme,
    // so fetch the asset and hand the element a blob: URL instead.
    const response = await fetch(`/sounds/${name}.mp3`);
    audio = new Audio(URL.createObjectURL(await response.blob()));
    cache.set(name, audio);
  }
  return audio;
}

Inlining the mp3s as data: URIs at build time would also work, at the cost of bundle size. Either way, the .catch() should log rather than discard.

Environment (please complete the following information):

  • OS: Ubuntu 26.04, GNOME on Wayland, x86_64
  • Version: Buzz 0.4.23 (Buzz_0.4.23_amd64.AppImage)
  • System WebKitGTK: libwebkit2gtk-4.1-0 2.52.3-0ubuntu0.26.04.2
  • GStreamer: 1.28.2

Additional context

Unrelated to #2560, which covers AppImage library bundling and GStreamer registry pollution on the same machine. Those are real, but the table above shows they are not what silences the notification sounds: the bundled WebKit plays the same mp3 fine over file:// and blob:.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions