Merge branch 'main' into fix/title-date
This commit is contained in:
commit
0def21e3ca
2 changed files with 70 additions and 7 deletions
|
|
@ -98,6 +98,17 @@ class AudioContextManager {
|
||||||
if (this.isInitialized) return;
|
if (this.isInitialized) return;
|
||||||
if (!audioElement) 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 {
|
try {
|
||||||
this.audio = audioElement;
|
this.audio = audioElement;
|
||||||
|
|
||||||
|
|
@ -177,11 +188,28 @@ class AudioContextManager {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resume audio context (required after user interaction)
|
* Resume audio context (required after user interaction)
|
||||||
|
* @returns {Promise<boolean>} - Returns true if context is running
|
||||||
*/
|
*/
|
||||||
resume() {
|
async resume() {
|
||||||
if (this.audioContext && this.audioContext.state === 'suspended') {
|
if (!this.audioContext) return false;
|
||||||
this.audioContext.resume();
|
|
||||||
|
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';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
43
js/player.js
43
js/player.js
|
|
@ -10,6 +10,7 @@ import {
|
||||||
createQualityBadgeHTML,
|
createQualityBadgeHTML,
|
||||||
} from './utils.js';
|
} from './utils.js';
|
||||||
import { queueManager, replayGainSettings, trackDateSettings } from './storage.js';
|
import { queueManager, replayGainSettings, trackDateSettings } from './storage.js';
|
||||||
|
import { audioContextManager } from './audio-context.js';
|
||||||
|
|
||||||
export class Player {
|
export class Player {
|
||||||
constructor(audioElement, api, quality = 'HI_RES_LOSSLESS') {
|
constructor(audioElement, api, quality = 'HI_RES_LOSSLESS') {
|
||||||
|
|
@ -51,6 +52,17 @@ export class Player {
|
||||||
window.addEventListener('beforeunload', () => {
|
window.addEventListener('beforeunload', () => {
|
||||||
this.saveQueueState();
|
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) {
|
setVolume(value) {
|
||||||
|
|
@ -174,19 +186,42 @@ export class Player {
|
||||||
setupMediaSession() {
|
setupMediaSession() {
|
||||||
if (!('mediaSession' in navigator)) return;
|
if (!('mediaSession' in navigator)) return;
|
||||||
|
|
||||||
navigator.mediaSession.setActionHandler('play', () => {
|
navigator.mediaSession.setActionHandler('play', async () => {
|
||||||
this.audio.play().catch(console.error);
|
// 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', () => {
|
navigator.mediaSession.setActionHandler('pause', () => {
|
||||||
this.audio.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();
|
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();
|
this.playNext();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue