From 82ddb5b7cc8cb9f1e3f707b59c7457b5f4f623a1 Mon Sep 17 00:00:00 2001 From: eJosR-Coding Date: Thu, 17 Sep 2026 13:07:43 -0500 Subject: [PATCH] fix(sounds): play through blob URLs so packaged Linux builds have sound WebKitGTK's media element can't stream from Tauri's custom protocols (tauri://, asset://) in packaged builds, although fetch() over them works. Sounds are fetched once into a blob: URL and played from there; CSP media-src allows blob:. Found on the installed 0.2.0 rpm: toast without sound, dev build fine. --- src-tauri/tauri.conf.json | 2 +- src/platform/sounds.ts | 30 +++++++++++++++++++++++++++--- 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index c476ca7..3d47fde 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -25,7 +25,7 @@ } ], "security": { - "csp": "default-src 'self'; connect-src 'self' ipc: http://ipc.localhost https://api.github.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: https://avatars.githubusercontent.com; media-src 'self' asset: http://asset.localhost", + "csp": "default-src 'self'; connect-src 'self' ipc: http://ipc.localhost https://api.github.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: https://avatars.githubusercontent.com; media-src 'self' blob: asset: http://asset.localhost", "assetProtocol": { "enable": true, "scope": [ diff --git a/src/platform/sounds.ts b/src/platform/sounds.ts index 464c0b1..628b7a0 100644 --- a/src/platform/sounds.ts +++ b/src/platform/sounds.ts @@ -44,12 +44,36 @@ function urlFor(ref: SoundRef): string | null { return path ? convertFileSrc(path) : null; } +/** + * WebKitGTK's media element can't stream from Tauri's custom protocols + * (tauri://, asset://) in packaged builds, even though fetch() over them + * works fine. So: fetch the bytes once, hand the element a blob: URL. + */ +const blobCache = new Map(); + +async function playableUrl(url: string): Promise { + const cached = blobCache.get(url); + if (cached) return cached; + const res = await fetch(url); + if (!res.ok) throw new Error(`sound ${res.status}`); + const blobUrl = URL.createObjectURL(await res.blob()); + blobCache.set(url, blobUrl); + return blobUrl; +} + /** Fire and forget. Resolves when playback ends so callers can stagger toasts. */ -export function playSound(ref: SoundRef, volume: number): Promise { +export async function playSound(ref: SoundRef, volume: number): Promise { const url = urlFor(ref); - if (!url) return Promise.resolve(); + if (!url) return; + let src: string; + try { + src = await playableUrl(url); + } catch (e) { + console.warn("sound load failed", ref, e); + return; + } return new Promise((resolve) => { - const a = new Audio(url); + const a = new Audio(src); a.volume = Math.min(1, Math.max(0, volume)); a.addEventListener("ended", () => resolve(), { once: true }); a.addEventListener("error", () => {