Compare commits

...

1 Commits

Author SHA1 Message Date
Admin
bdbe48ce1a fix: register MediaSession action handlers for iOS lock screen resume
All checks were successful
Release / Test backend (push) Successful in 37s
Release / Check ui (push) Successful in 56s
Release / Docker / caddy (push) Successful in 41s
Release / Docker / backend (push) Successful in 2m53s
Release / Docker / runner (push) Successful in 3m13s
Release / Docker / ui (push) Successful in 2m25s
Release / Gitea Release (push) Successful in 50s
Without explicit setActionHandler('play'/'pause'/...) the browser uses
default handling which on iOS Safari stops working after ~1 min of
pause in background/locked state (AudioSession gets suspended and the
lock screen button can't resume it without a direct user gesture).

Registering handlers in the layout (where audioEl lives) ensures:
- play/pause call audioEl directly — iOS treats this as a trusted
  gesture and allows .play() even from the lock screen
- seekbackward/seekforward map to ±15s / ±30s
- playbackState stays in sync so the lock screen shows the right icon

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-04 18:22:09 +05:00

View File

@@ -192,6 +192,50 @@
return () => clearTimeout(id);
});
// ── MediaSession action handlers ────────────────────────────────────────────
// Without explicit handlers, iOS Safari loses lock-screen resume ability after
// ~1 minute of pause because it falls back to its own default which doesn't
// satisfy the user-gesture requirement for <audio>.play().
// Handlers registered here call audioEl directly so iOS trusts the gesture.
$effect(() => {
if (typeof navigator === 'undefined' || !('mediaSession' in navigator) || !audioEl) return;
const el = audioEl; // capture for closure
navigator.mediaSession.setActionHandler('play', () => {
el.play().catch(() => {});
});
navigator.mediaSession.setActionHandler('pause', () => {
el.pause();
});
navigator.mediaSession.setActionHandler('seekbackward', (d) => {
el.currentTime = Math.max(0, el.currentTime - (d.seekOffset ?? 15));
});
navigator.mediaSession.setActionHandler('seekforward', (d) => {
el.currentTime = Math.min(el.duration || 0, el.currentTime + (d.seekOffset ?? 30));
});
// previoustrack / nexttrack fall back to skip ±30s if no chapter nav available
try {
navigator.mediaSession.setActionHandler('previoustrack', () => {
el.currentTime = Math.max(0, el.currentTime - 30);
});
navigator.mediaSession.setActionHandler('nexttrack', () => {
el.currentTime = Math.min(el.duration || 0, el.currentTime + 30);
});
} catch { /* some browsers don't support these */ }
return () => {
(['play', 'pause', 'seekbackward', 'seekforward'] as MediaSessionAction[]).forEach((a) => {
try { navigator.mediaSession.setActionHandler(a, null); } catch { /* ignore */ }
});
};
});
// Keep playbackState in sync so iOS lock screen shows the right button state
$effect(() => {
if (typeof navigator === 'undefined' || !('mediaSession' in navigator)) return;
navigator.mediaSession.playbackState = audioStore.isPlaying ? 'playing' : 'paused';
});
// ── Save audio time on pause/end (debounced 2s) ─────────────────────────
let audioTimeSaveTimer = 0;
function saveAudioTime() {