diff --git a/libs/ui/playback/src/lib/vjs-player/vjs-mpegts-session.spec.ts b/libs/ui/playback/src/lib/vjs-player/vjs-mpegts-session.spec.ts index 82fc6c33c..d472e0e80 100644 --- a/libs/ui/playback/src/lib/vjs-player/vjs-mpegts-session.spec.ts +++ b/libs/ui/playback/src/lib/vjs-player/vjs-mpegts-session.spec.ts @@ -64,6 +64,24 @@ describe('VjsMpegTsSession', () => { expect(mpegTsPlayer.play).toHaveBeenCalledTimes(1); }); + it('handles an asynchronous autoplay rejection', () => { + const catchRejection = jest.fn(); + const mpegTsPlayer = createMpegTsPlayer(); + mpegTsPlayer.play.mockReturnValue({ + catch: catchRejection, + } as unknown as Promise); + createPlayerMock.mockReturnValue(mpegTsPlayer); + const video = document.createElement('video'); + const { session } = createSession(); + + session.start('https://example.test/live/stream.ts', video); + + expect(catchRejection).toHaveBeenCalledWith(expect.any(Function)); + expect( + catchRejection.mock.calls[0][0](new Error('autoplay blocked')) + ).toBeUndefined(); + }); + it('normalizes VOD duration from seekable and then buffered ranges', async () => { const mpegTsPlayer = createMpegTsPlayer(); createPlayerMock.mockReturnValue(mpegTsPlayer); diff --git a/libs/ui/playback/src/lib/vjs-player/vjs-mpegts-session.ts b/libs/ui/playback/src/lib/vjs-player/vjs-mpegts-session.ts index eff4fd714..6ee7e4fc0 100644 --- a/libs/ui/playback/src/lib/vjs-player/vjs-mpegts-session.ts +++ b/libs/ui/playback/src/lib/vjs-player/vjs-mpegts-session.ts @@ -80,7 +80,10 @@ export class VjsMpegTsSession { } engine.on(mpegts.Events.ERROR, this.errorListener); engine.load(); - void engine.play(); + const playResult = engine.play(); + if (playResult) { + void playResult.catch(() => undefined); + } } syncDuration(): void { diff --git a/libs/ui/playback/src/lib/vjs-player/vjs-player-reset-coordinator.spec.ts b/libs/ui/playback/src/lib/vjs-player/vjs-player-reset-coordinator.spec.ts index 63900ad00..893f58b29 100644 --- a/libs/ui/playback/src/lib/vjs-player/vjs-player-reset-coordinator.spec.ts +++ b/libs/ui/playback/src/lib/vjs-player/vjs-player-reset-coordinator.spec.ts @@ -21,6 +21,18 @@ describe('VjsPlayerResetCoordinator', () => { expect(harness.reset).toHaveBeenCalledTimes(1); }); + it('captures the latest engine volume after an asynchronous pause', () => { + const harness = createHarness(false, 0.3); + const coordinator = createCoordinator(harness); + + coordinator.requestReset(); + harness.player.volume(0.8); + harness.paused = true; + coordinator.handlePause(); + + expect(coordinator.handlePlayerReset()).toBe(0.8); + }); + it('snapshots actual engine volume and suppresses reset-generated changes', () => { const harness = createHarness(true, 0.3); const coordinator = createCoordinator(harness, 0.8); diff --git a/libs/ui/playback/src/lib/vjs-player/vjs-player-reset-coordinator.ts b/libs/ui/playback/src/lib/vjs-player/vjs-player-reset-coordinator.ts index ea7567e9f..40245ca74 100644 --- a/libs/ui/playback/src/lib/vjs-player/vjs-player-reset-coordinator.ts +++ b/libs/ui/playback/src/lib/vjs-player/vjs-player-reset-coordinator.ts @@ -24,7 +24,6 @@ export class VjsPlayerResetCoordinator { } this.requested = true; - this.captureVolume(); try { this.config.player().pause(); } catch { @@ -99,6 +98,7 @@ export class VjsPlayerResetCoordinator { return; } + this.captureVolume(); this.requested = false; this.inFlight = true; try { diff --git a/libs/ui/playback/src/lib/vjs-player/vjs-player.component.reset.spec.ts b/libs/ui/playback/src/lib/vjs-player/vjs-player.component.reset.spec.ts index 4bd1c1b0e..eb0e74379 100644 --- a/libs/ui/playback/src/lib/vjs-player/vjs-player.component.reset.spec.ts +++ b/libs/ui/playback/src/lib/vjs-player/vjs-player.component.reset.spec.ts @@ -32,6 +32,7 @@ describe('VjsPlayerComponent reset lifecycle', () => { }); beforeEach(async () => { + localStorage.removeItem('volume'); playerHarness = createVideoJsPlayerHarness(); videoJsMock .mockReset() @@ -150,6 +151,34 @@ describe('VjsPlayerComponent reset lifecycle', () => { expect(playerHarness.reset).toHaveBeenCalledTimes(1); }); + it('restores a volume input that changes while reset waits for pause', () => { + localStorage.setItem('volume', '0.3'); + fixture.componentRef.setInput('options', { + sources: [{ src: 'https://example.test/movie.mp4' }], + }); + fixture.componentRef.setInput('volume', 0.3); + fixture.detectChanges(); + playerHarness.ready(); + playerHarness.paused = false; + playerHarness.pauseCompletesImmediately = false; + + fixture.componentRef.setInput('options', { sources: [] }); + fixture.detectChanges(); + fixture.componentRef.setInput('volume', 0.8); + fixture.detectChanges(); + playerHarness.emit('volumechange'); + + playerHarness.paused = true; + playerHarness.emit('pause'); + playerHarness.volume(1); + playerHarness.emit('volumechange'); + playerHarness.currentVideo = document.createElement('video'); + playerHarness.emit('playerreset'); + + expect(localStorage.getItem('volume')).toBe('0.8'); + expect(playerHarness.volume).toHaveBeenLastCalledWith(0.8); + }); + it('restores the latest normal source after an already-started reset', () => { fixture.componentRef.setInput('options', { sources: [{ src: 'https://example.test/movie.mp4' }], @@ -259,6 +288,7 @@ describe('VjsPlayerComponent reset lifecycle', () => { function createVideoJsPlayerHarness() { const listeners = new Map void>>(); + let volume = 0.5; const harness = { currentVideo: document.createElement('video'), paused: true, @@ -272,6 +302,12 @@ function createVideoJsPlayerHarness() { }), reset: jest.fn(), src: jest.fn(), + volume: jest.fn((value?: number) => { + if (value !== undefined) { + volume = value; + } + return volume; + }), emit(event: string) { for (const listener of listeners.get(event) ?? []) { listener(); @@ -300,7 +336,7 @@ function createVideoJsPlayerHarness() { reset: harness.reset, src: harness.src, tech: jest.fn(() => ({ el: () => harness.currentVideo })), - volume: jest.fn(() => 0.5), + volume: harness.volume, dispose: jest.fn(), qualitySelectorHls: jest.fn(), aspectRatioPanel: jest.fn(),