From eaee68295705337127aa382576240af7c1aa3c54 Mon Sep 17 00:00:00 2001 From: Steven Vandevelde Date: Fri, 10 Jul 2026 00:46:20 +0200 Subject: [PATCH] chore: audio engine ios improvements --- src/components/engine/audio/element.js | 54 +++++++++++++++++++++++++- 1 file changed, 52 insertions(+), 2 deletions(-) diff --git a/src/components/engine/audio/element.js b/src/components/engine/audio/element.js index 28b1f4a4..796857ae 100644 --- a/src/components/engine/audio/element.js +++ b/src/components/engine/audio/element.js @@ -40,6 +40,14 @@ class AudioEngine extends BroadcastableDiffuseElement { #items = signal(/** @type {AudioUrl[]} */ ([])); #volume = signal(0.75); + /** + * True while a play has been requested but the audio hasn't actually started + * yet. Keeps `isPlaying` from briefly dropping to `false` during track + * transitions, which on iOS can cause the Media Session to report "paused" + * and interrupt the audio session. + */ + #pendingPlay = signal(false); + /** @type {Map} Streams pending MediaSource setup */ #streams = new Map(); @@ -52,6 +60,8 @@ class AudioEngine extends BroadcastableDiffuseElement { volume = this.#volume.get; isPlaying = computed(() => { + if (this.#pendingPlay.value) return true; + const item = this.items()?.[0]; if (!item) return false; @@ -201,10 +211,34 @@ class AudioEngine extends BroadcastableDiffuseElement { const promise = audio.play() || Promise.resolve(); item.$state.isPlaying.set(true); + promise.then(() => { + this.#pendingPlay.set(false); + }); + promise.catch((e) => { if (!audio.isConnected) { - return; /* The node was removed from the DOM, we can ignore this error */ + /* The node was removed from the DOM, we can ignore this error */ + return; + } + + // On iOS Safari `preload="auto"` is downgraded to `metadata` (or + // `none` on cellular), so a freshly-created