diff --git a/js/audio-context.js b/js/audio-context.js index ef1375f..5f1c2e6 100644 --- a/js/audio-context.js +++ b/js/audio-context.js @@ -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} - 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'; } /** diff --git a/js/player.js b/js/player.js index 9618bf1..bea2daf 100644 --- a/js/player.js +++ b/js/player.js @@ -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(); });