Keep iPhone audio playing through background handoffs
This commit is contained in:
@@ -2495,6 +2495,19 @@ const Player = {
|
||||
}
|
||||
},
|
||||
|
||||
// Use one uninterrupted audio decoder on iPhone from the first play. Locking
|
||||
// then promotes that existing clock; it never loads/seeks a new sound source.
|
||||
prepareBackgroundAudio() {
|
||||
if (!IS_IOS || this.mode !== 'progressive') return;
|
||||
const source = AudioContinuity.progressiveSource(current, els.video.getAttribute('src'));
|
||||
if (!source) return;
|
||||
this.mode = 'dual';
|
||||
this.secondary = els.audio;
|
||||
els.video.muted = true;
|
||||
els.audio.src = source;
|
||||
els.audio.load();
|
||||
},
|
||||
|
||||
attach(quality) {
|
||||
const V = els.video, A = els.audio;
|
||||
// An uploaded audio file has no picture track: play it like audio-only and
|
||||
@@ -2508,6 +2521,7 @@ const Player = {
|
||||
const audioOnly = data.settings.audioOnly || (current && current.kind === 'audio') || bgStart;
|
||||
|
||||
// Reset
|
||||
audioContinuity.reset(V); audioContinuity.reset(A);
|
||||
V.pause(); A.pause();
|
||||
this.stopDrift();
|
||||
this._bgHandoff = false;
|
||||
@@ -2551,6 +2565,7 @@ const Player = {
|
||||
A.removeAttribute('src'); A.load();
|
||||
els.art.classList.add('hidden');
|
||||
}
|
||||
this.prepareBackgroundAudio();
|
||||
this.applyVolume();
|
||||
this.applySpeed();
|
||||
this.master.load();
|
||||
@@ -2596,6 +2611,7 @@ const Player = {
|
||||
return;
|
||||
}
|
||||
|
||||
this.prepareBackgroundAudio();
|
||||
this.applyVolume();
|
||||
this.applySpeed();
|
||||
this.master.load();
|
||||
@@ -2639,15 +2655,14 @@ const Player = {
|
||||
|
||||
play() {
|
||||
this._wantsPlaying = true;
|
||||
this.master.play().catch(() => {});
|
||||
if (this.secondary) {
|
||||
this.secondary.currentTime = this.master.currentTime;
|
||||
this.secondary.play().catch(() => {});
|
||||
}
|
||||
audioContinuity.play(this.master);
|
||||
audioContinuity.resume(this.secondary, this.master);
|
||||
},
|
||||
pause() {
|
||||
this._wantsPlaying = false;
|
||||
this.clearBufferGrace();
|
||||
audioContinuity.reset(this.master);
|
||||
if (this.secondary) audioContinuity.reset(this.secondary);
|
||||
this.master.pause();
|
||||
if (this.secondary) this.secondary.pause();
|
||||
},
|
||||
@@ -2743,8 +2758,7 @@ const Player = {
|
||||
// The element that's actually producing sound in the current mode.
|
||||
const sounder = this.soundEl;
|
||||
if (sounder && sounder.paused) {
|
||||
sounder.currentTime = this.master.currentTime;
|
||||
sounder.play().catch(() => {});
|
||||
audioContinuity.resume(sounder, this.master);
|
||||
}
|
||||
}, 1000);
|
||||
},
|
||||
@@ -2785,25 +2799,26 @@ const Player = {
|
||||
if (!src) return; // nothing to continue with — leave playback alone
|
||||
const V = els.video, A = els.audio;
|
||||
try {
|
||||
A.src = src;
|
||||
A.currentTime = V.currentTime || 0;
|
||||
this._bgHandoff = true;
|
||||
this.master = A;
|
||||
if (A.getAttribute('src') !== src) A.src = src;
|
||||
if (AudioContinuity.needsAlignment(A.currentTime, V.currentTime)) A.currentTime = V.currentTime || 0;
|
||||
A.volume = V.volume;
|
||||
A.muted = V.muted;
|
||||
const p = A.play();
|
||||
if (p && p.catch) p.catch(() => {});
|
||||
V.pause();
|
||||
// Making <audio> the master keeps soundEl, the drift watchdog and the
|
||||
// progress UI pointed at whatever is actually producing sound.
|
||||
this.master = A;
|
||||
this._bgHandoff = true;
|
||||
audioContinuity.play(A).then(() => {
|
||||
if (this._bgHandoff && this.master === A && !A.paused) V.pause();
|
||||
});
|
||||
// Master ownership is set before play/pause can dispatch another handler.
|
||||
} catch {
|
||||
this._bgHandoff = false;
|
||||
this.master = V;
|
||||
}
|
||||
},
|
||||
|
||||
// Undo handoffToBackgroundAudio, putting the video back in charge at the
|
||||
// position the audio reached while the app was away.
|
||||
restoreFromBackgroundAudio() {
|
||||
if (document.visibilityState !== 'visible') return;
|
||||
// A track that STARTED in the background is audio-only: re-attach its
|
||||
// picture at the position the audio reached.
|
||||
if (this._bgAudioMode && current && !data.settings.audioOnly) {
|
||||
@@ -2823,8 +2838,8 @@ const Player = {
|
||||
this.master = V;
|
||||
this.secondary = A;
|
||||
try {
|
||||
V.currentTime = t;
|
||||
if (this._wantsPlaying) { const p = V.play(); if (p && p.catch) p.catch(() => {}); }
|
||||
if (AudioContinuity.needsAlignment(V.currentTime, t, 0.12)) { this._internalSeek = true; V.currentTime = t; }
|
||||
if (this._wantsPlaying) audioContinuity.play(V);
|
||||
} catch { /* the watchdog picks up whatever is left */ }
|
||||
return;
|
||||
}
|
||||
@@ -2832,14 +2847,11 @@ const Player = {
|
||||
this._bgHandoff = false;
|
||||
this.master = V;
|
||||
try {
|
||||
V.currentTime = t;
|
||||
A.pause();
|
||||
A.removeAttribute('src');
|
||||
A.load();
|
||||
if (this._wantsPlaying) {
|
||||
const p = V.play();
|
||||
if (p && p.catch) p.catch(() => {});
|
||||
}
|
||||
this.mode = 'dual';
|
||||
this.secondary = A;
|
||||
V.muted = true;
|
||||
if (AudioContinuity.needsAlignment(V.currentTime, t, 0.12)) { this._internalSeek = true; V.currentTime = t; }
|
||||
if (this._wantsPlaying) audioContinuity.play(V);
|
||||
} catch { /* the watchdog picks up whatever is left */ }
|
||||
},
|
||||
|
||||
@@ -2851,12 +2863,10 @@ const Player = {
|
||||
if (!this._wantsPlaying) return;
|
||||
const sounder = this.soundEl;
|
||||
if (sounder && sounder.paused) {
|
||||
sounder.currentTime = this.master.currentTime;
|
||||
sounder.play().catch(() => {});
|
||||
audioContinuity.resume(sounder, this.master);
|
||||
}
|
||||
if (this.mode === 'dual' && this.secondary && this.secondary.paused && !this.master.paused) {
|
||||
this.secondary.currentTime = this.master.currentTime;
|
||||
this.secondary.play().catch(() => {});
|
||||
audioContinuity.resume(this.secondary, this.master);
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -2933,7 +2943,7 @@ function wirePlayerEvents() {
|
||||
const masterIs = (el) => Player.master === el;
|
||||
|
||||
function bind(el) {
|
||||
el.addEventListener('play', () => { if (masterIs(el) && Player.secondary && Player.secondary.paused) { Player.secondary.currentTime = el.currentTime; Player.secondary.play().catch(() => {}); } updatePlayBtn(); });
|
||||
el.addEventListener('play', () => { if (masterIs(el)) audioContinuity.resume(Player.secondary, el); updatePlayBtn(); });
|
||||
el.addEventListener('pause', () => {
|
||||
// iOS paused the master video because the app went to the background
|
||||
// (sometimes before visibilitychange arrives): hand the sound over now.
|
||||
@@ -2983,8 +2993,7 @@ function wirePlayerEvents() {
|
||||
if (Player.secondary && !el.paused && Player.secondary.paused) {
|
||||
// Resume-only path (audio silently paused by iOS or a real stall):
|
||||
// seeking a *paused* audio element is inaudible, so sync then play.
|
||||
Player.secondary.currentTime = el.currentTime;
|
||||
Player.secondary.play().catch(() => {});
|
||||
audioContinuity.resume(Player.secondary, el);
|
||||
} else if (Player.secondary && !el.paused) {
|
||||
// Audio kept playing through the video stall — realign the video.
|
||||
Player.correctDrift();
|
||||
|
||||
Reference in New Issue
Block a user