fix iOS background play

This commit is contained in:
Eduard Prigoana 2026-02-03 13:46:34 +00:00
parent 18b82e793e
commit 377adc8f0a
2 changed files with 70 additions and 7 deletions

View file

@ -98,6 +98,17 @@ class AudioContextManager {
if (this.isInitialized) return;
if (!audioElement) return;
// Detect iOS - skip Web Audio initialization on iOS to avoid lock screen audio issues
// iOS suspends AudioContext when screen locks, and MediaSession controls don't count
// as user gestures to resume it, causing audio to play silently
const ua = navigator.userAgent.toLowerCase();
const isIOS = /iphone|ipad|ipod/.test(ua) || (ua.includes('mac') && navigator.maxTouchPoints > 1);
if (isIOS) {
console.log('[AudioContext] Skipping Web Audio initialization on iOS for lock screen compatibility');
this.isInitialized = true; // Mark as initialized to prevent repeated attempts
return;
}
try {
this.audio = audioElement;
@ -177,11 +188,28 @@ class AudioContextManager {
/**
* Resume audio context (required after user interaction)
* @returns {Promise<boolean>} - Returns true if context is running
*/
resume() {
if (this.audioContext && this.audioContext.state === 'suspended') {
this.audioContext.resume();
async resume() {
if (!this.audioContext) return false;
console.log('[AudioContext] Current state:', this.audioContext.state);
if (this.audioContext.state === 'suspended') {
try {
await this.audioContext.resume();
console.log('[AudioContext] Resumed successfully, state:', this.audioContext.state);
} catch (e) {
console.warn('[AudioContext] Failed to resume:', e);
}
}
// Ensure graph is connected after resuming (iOS may disconnect when suspended)
if (this.isInitialized && this.audioContext.state === 'running') {
this._connectGraph();
}
return this.audioContext.state === 'running';
}
/**

View file

@ -9,6 +9,7 @@ import {
createQualityBadgeHTML,
} from './utils.js';
import { queueManager, replayGainSettings } from './storage.js';
import { audioContextManager } from './audio-context.js';
export class Player {
constructor(audioElement, api, quality = 'HI_RES_LOSSLESS') {
@ -50,6 +51,17 @@ export class Player {
window.addEventListener('beforeunload', () => {
this.saveQueueState();
});
// Handle visibility change for iOS - AudioContext gets suspended when screen locks
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'visible' && !this.audio.paused) {
// Ensure audio context is resumed when user returns to the app
if (!audioContextManager.isReady()) {
audioContextManager.init(this.audio);
}
audioContextManager.resume();
}
});
}
setVolume(value) {
@ -176,19 +188,42 @@ export class Player {
setupMediaSession() {
if (!('mediaSession' in navigator)) return;
navigator.mediaSession.setActionHandler('play', () => {
this.audio.play().catch(console.error);
navigator.mediaSession.setActionHandler('play', async () => {
// Initialize and resume audio context first (required for iOS lock screen)
// Must happen before audio.play() or audio won't route through Web Audio
if (!audioContextManager.isReady()) {
audioContextManager.init(this.audio);
}
await audioContextManager.resume();
try {
await this.audio.play();
} catch (e) {
console.error('MediaSession play failed:', e);
// If play fails, try to handle it like a regular play/pause
this.handlePlayPause();
}
});
navigator.mediaSession.setActionHandler('pause', () => {
this.audio.pause();
});
navigator.mediaSession.setActionHandler('previoustrack', () => {
navigator.mediaSession.setActionHandler('previoustrack', async () => {
// Ensure audio context is active for iOS lock screen controls
if (!audioContextManager.isReady()) {
audioContextManager.init(this.audio);
}
await audioContextManager.resume();
this.playPrev();
});
navigator.mediaSession.setActionHandler('nexttrack', () => {
navigator.mediaSession.setActionHandler('nexttrack', async () => {
// Ensure audio context is active for iOS lock screen controls
if (!audioContextManager.isReady()) {
audioContextManager.init(this.audio);
}
await audioContextManager.resume();
this.playNext();
});