diff --git a/public/js/components/CaptionTracks.js b/public/js/components/CaptionTracks.js index 97564e0..e86f7a8 100644 --- a/public/js/components/CaptionTracks.js +++ b/public/js/components/CaptionTracks.js @@ -57,15 +57,27 @@ class CaptionTracks { } prepareMediaRecovery() { - if (!this.remoteTracks.length) return; + const tracks = Array.from(this.hlsTracks, ([key, entry]) => ({ key, entry, + cues: Array.from(entry.element.track.cues || []), mode: entry.element.track.mode })); + const hasRemoteTracks = this.remoteTracks.length > 0; + if (!hasRemoteTracks && !tracks.length) return; const selected = this.remoteTracks.findIndex(element => element.track.mode === 'showing'); const generation = this.generation, revision = this.selectionRevision; return () => { - if (this.generation !== generation || this.selectionRevision !== revision) return; - // HLS detachment clears native cues, including extracted remote subtitles. - this.removeRemoteTracks(); - this.addRemoteTracks(); - if (selected >= 0 && this.remoteTracks[selected]) this.remoteTracks[selected].track.mode = 'showing'; + if (this.generation !== generation) return; + // HLS detachment clears all native cues, including our owned caption tracks. + for (const { key, entry, cues, mode } of tracks) { + if (this.hlsTracks.get(key) !== entry) continue; + this.appendHlsCues(entry, cues); + if (this.selectionRevision === revision) entry.element.track.mode = mode; + } + if (hasRemoteTracks) { + const remoteSelection = this.selectionRevision === revision ? selected : + this.remoteTracks.findIndex(element => element.track.mode === 'showing'); + this.removeRemoteTracks(); + this.addRemoteTracks(); + if (remoteSelection >= 0 && this.remoteTracks[remoteSelection]) this.remoteTracks[remoteSelection].track.mode = 'showing'; + } this.onChange(); }; } diff --git a/test/caption-tracks.test.js b/test/caption-tracks.test.js index 7261c9d..5f66c41 100644 --- a/test/caption-tracks.test.js +++ b/test/caption-tracks.test.js @@ -104,6 +104,11 @@ test('both subtitle menus select real HLS cues, discard stale tracks and preserv }); assert.equal(await page.locator('#list button.active').count(), 1); } + await page.evaluate(() => window.player.audioRecovery.requestRecovery('caption recovery fixture')); + await page.waitForFunction(() => window.player.audioRecovery.lastRecovery > 0 && !window.player.audioRecovery.onAttached); + await page.waitForFunction(() => Array.from(window.player.video.textTracks).some(track => + track.mode === 'showing' && Array.from(track.activeCues || []).some(cue => cue.text === 'Cue from /sub2.vtt'))); + assert.equal(await page.evaluate(() => window.player.video.paused), true); await page.locator('#list button').first().click(); assert.equal(await page.evaluate(() => window.player.hls.subtitleTrack), -1); assert.equal(await page.evaluate(() => Array.from(window.player.video.textTracks).every(track => track.mode === 'disabled')), true); @@ -162,6 +167,11 @@ test('both subtitle menus select real HLS cues, discard stale tracks and preserv return track.mode === 'showing' && track.cues?.length === 1 && track.cues[0].text === 'CEA caption'; }); + await page.evaluate(() => window.player.audioRecovery.requestRecovery('CEA recovery fixture')); + await page.waitForFunction(() => window.player.audioRecovery.lastRecovery > 0 && !window.player.audioRecovery.onAttached); + await page.waitForFunction(() => Array.from(window.player.video.textTracks).some(track => + track.label === 'English CC' && track.mode === 'showing' && track.cues?.length === 1 && track.cues[0].text === 'CEA caption')); + await page.evaluate(() => { const player = window.player; player.stop();