diff --git a/libs/ui/playback/src/lib/embedded-mpv-player/embedded-mpv-controls.adapter.spec.ts b/libs/ui/playback/src/lib/embedded-mpv-player/embedded-mpv-controls.adapter.spec.ts index 56ce7ab49..a08d6e3d1 100644 --- a/libs/ui/playback/src/lib/embedded-mpv-player/embedded-mpv-controls.adapter.spec.ts +++ b/libs/ui/playback/src/lib/embedded-mpv-player/embedded-mpv-controls.adapter.spec.ts @@ -198,6 +198,7 @@ describe('EmbeddedMpvControlsAdapter', () => { playbackSpeed: true, aspectRatio: true, recording: true, + pictureInPicture: false, fullscreen: true, seriesNavigation: true, }); @@ -222,6 +223,7 @@ describe('EmbeddedMpvControlsAdapter', () => { playbackSpeed: false, aspectRatio: false, recording: false, + pictureInPicture: false, fullscreen: true, seriesNavigation: true, }); @@ -263,6 +265,8 @@ describe('EmbeddedMpvControlsAdapter', () => { speedPresets: DEFAULT_SPEED_PRESETS, aspectRatio: '16:9', aspectPresets: DEFAULT_ASPECT_PRESETS, + pictureInPictureActive: false, + canPictureInPicture: false, canPreviousEpisode: true, canNextEpisode: false, }); @@ -374,6 +378,7 @@ describe('EmbeddedMpvControlsAdapter', () => { it('delegates every non-recording shared-controls command', () => { configure(); + expect(() => adapter.commands.togglePictureInPicture()).not.toThrow(); adapter.commands.togglePlay(); adapter.commands.seekTo(45); adapter.commands.seekBy(-10); diff --git a/libs/ui/playback/src/lib/embedded-mpv-player/embedded-mpv-controls.adapter.ts b/libs/ui/playback/src/lib/embedded-mpv-player/embedded-mpv-controls.adapter.ts index 03361d485..cfed72188 100644 --- a/libs/ui/playback/src/lib/embedded-mpv-player/embedded-mpv-controls.adapter.ts +++ b/libs/ui/playback/src/lib/embedded-mpv-player/embedded-mpv-controls.adapter.ts @@ -114,6 +114,7 @@ export class EmbeddedMpvControlsAdapter implements PlayerController { playbackSpeed: optionalCapabilities?.playbackSpeed ?? false, aspectRatio: optionalCapabilities?.aspectOverride ?? false, recording: optionalCapabilities?.recording ?? false, + pictureInPicture: false, fullscreen: true, seriesNavigation: !isLive && context.seriesNavigation() !== null, }; @@ -190,6 +191,8 @@ export class EmbeddedMpvControlsAdapter implements PlayerController { ), transitionKey: this.recordingTransitionKey(), }, + pictureInPictureActive: false, + canPictureInPicture: false, canPreviousEpisode: hasSeriesNavigation && seriesNavigation?.canPrevious === true, canNextEpisode: @@ -207,6 +210,7 @@ export class EmbeddedMpvControlsAdapter implements PlayerController { setPlaybackSpeed: (speed) => void this.controller.setSpeed(speed), setAspectRatio: (value) => void this.controller.setAspect(value), toggleRecording: () => void this.toggleRecording(), + togglePictureInPicture: () => undefined, }; constructor() { diff --git a/libs/ui/playback/src/lib/player-controls/controls-menu-selection.spec.ts b/libs/ui/playback/src/lib/player-controls/controls-menu-selection.spec.ts index bff239af4..791b81a87 100644 --- a/libs/ui/playback/src/lib/player-controls/controls-menu-selection.spec.ts +++ b/libs/ui/playback/src/lib/player-controls/controls-menu-selection.spec.ts @@ -21,6 +21,7 @@ describe('ControlsMenuSelection', () => { setPlaybackSpeed: jest.fn(), setAspectRatio: jest.fn(), toggleRecording: jest.fn(), + togglePictureInPicture: jest.fn(), }; menus = new ControlsMenuState(); visibility = new ControlsVisibility(() => false); diff --git a/libs/ui/playback/src/lib/player-controls/player-controls-defaults.spec.ts b/libs/ui/playback/src/lib/player-controls/player-controls-defaults.spec.ts new file mode 100644 index 000000000..53aaf4381 --- /dev/null +++ b/libs/ui/playback/src/lib/player-controls/player-controls-defaults.spec.ts @@ -0,0 +1,14 @@ +import { + DEFAULT_PLAYER_CAPABILITIES, + createEmptyControlsState, +} from './player-controls-defaults'; + +describe('player-controls defaults', () => { + it('defaults Picture-in-Picture to unsupported and inactive', () => { + expect(DEFAULT_PLAYER_CAPABILITIES.pictureInPicture).toBe(false); + expect(createEmptyControlsState()).toMatchObject({ + pictureInPictureActive: false, + canPictureInPicture: false, + }); + }); +}); diff --git a/libs/ui/playback/src/lib/player-controls/player-controls-defaults.ts b/libs/ui/playback/src/lib/player-controls/player-controls-defaults.ts index 6073458dd..ed003689a 100644 --- a/libs/ui/playback/src/lib/player-controls/player-controls-defaults.ts +++ b/libs/ui/playback/src/lib/player-controls/player-controls-defaults.ts @@ -12,6 +12,7 @@ export const DEFAULT_PLAYER_CAPABILITIES: PlayerControlsCapabilities = { playbackSpeed: false, aspectRatio: false, recording: false, + pictureInPicture: false, fullscreen: false, seriesNavigation: false, }; @@ -51,6 +52,8 @@ export function createEmptyControlsState(): PlayerControlsState { aspectRatio: 'no', aspectPresets: DEFAULT_ASPECT_PRESETS, recording: { active: false, elapsedSeconds: 0, message: null }, + pictureInPictureActive: false, + canPictureInPicture: false, canPreviousEpisode: false, canNextEpisode: false, }; diff --git a/libs/ui/playback/src/lib/player-controls/player-controls.component.contract.spec.ts b/libs/ui/playback/src/lib/player-controls/player-controls.component.contract.spec.ts index dab25f611..c71123976 100644 --- a/libs/ui/playback/src/lib/player-controls/player-controls.component.contract.spec.ts +++ b/libs/ui/playback/src/lib/player-controls/player-controls.component.contract.spec.ts @@ -33,6 +33,7 @@ function createFakeController(): FakeController { setPlaybackSpeed: jest.fn(), setAspectRatio: jest.fn(), toggleRecording: jest.fn(), + togglePictureInPicture: jest.fn(), }; return { controller: { capabilities, state, commands }, diff --git a/libs/ui/playback/src/lib/player-controls/player-controls.component.interactions.spec.ts b/libs/ui/playback/src/lib/player-controls/player-controls.component.interactions.spec.ts index 70a1f8917..06e9e13b0 100644 --- a/libs/ui/playback/src/lib/player-controls/player-controls.component.interactions.spec.ts +++ b/libs/ui/playback/src/lib/player-controls/player-controls.component.interactions.spec.ts @@ -30,6 +30,7 @@ function createFakeController() { setPlaybackSpeed: jest.fn(), setAspectRatio: jest.fn(), toggleRecording: jest.fn(), + togglePictureInPicture: jest.fn(), }; const controller: PlayerController = { capabilities, state, commands }; return { controller, capabilities, state, commands }; diff --git a/libs/ui/playback/src/lib/player-controls/player-controls.component.spec.ts b/libs/ui/playback/src/lib/player-controls/player-controls.component.spec.ts index f6c707629..33d776090 100644 --- a/libs/ui/playback/src/lib/player-controls/player-controls.component.spec.ts +++ b/libs/ui/playback/src/lib/player-controls/player-controls.component.spec.ts @@ -28,6 +28,7 @@ function createFakeController() { setPlaybackSpeed: jest.fn(), setAspectRatio: jest.fn(), toggleRecording: jest.fn(), + togglePictureInPicture: jest.fn(), }; const controller: PlayerController = { capabilities, state, commands }; return { controller, capabilities, state, commands }; diff --git a/libs/ui/playback/src/lib/player-controls/player-controls.component.surface.spec.ts b/libs/ui/playback/src/lib/player-controls/player-controls.component.surface.spec.ts index ed4bb8d1f..b461092fe 100644 --- a/libs/ui/playback/src/lib/player-controls/player-controls.component.surface.spec.ts +++ b/libs/ui/playback/src/lib/player-controls/player-controls.component.surface.spec.ts @@ -30,6 +30,7 @@ function createFakeController() { setPlaybackSpeed: jest.fn(), setAspectRatio: jest.fn(), toggleRecording: jest.fn(), + togglePictureInPicture: jest.fn(), }; const controller: PlayerController = { capabilities, state, commands }; return { controller, capabilities, state, commands }; @@ -298,23 +299,17 @@ describe('PlayerControlsComponent surface, fullscreen and shortcuts', () => { }); describe('keyboard shortcuts', () => { - it('does not consume or toggle playback while loading', () => { - setState({ status: 'loading' }); - fixture.detectChanges(); + it.each(['loading', 'error'] as const)( + 'does not consume or toggle playback while status is %s', + (status) => { + setState({ status }); + fixture.detectChanges(); - expect(pressKey(' ')).toBe(false); - expect(pressKey('k')).toBe(false); - expect(fake.commands.togglePlay).not.toHaveBeenCalled(); - }); - - it('does not consume or toggle playback after an error', () => { - setState({ status: 'error' }); - fixture.detectChanges(); - - expect(pressKey(' ')).toBe(false); - expect(pressKey('k')).toBe(false); - expect(fake.commands.togglePlay).not.toHaveBeenCalled(); - }); + expect(pressKey(' ')).toBe(false); + expect(pressKey('k')).toBe(false); + expect(fake.commands.togglePlay).not.toHaveBeenCalled(); + } + ); it('Escape closes an open menu', () => { component.toggleMenu('subtitle'); diff --git a/libs/ui/playback/src/lib/player-controls/player-controls.component.timeline.spec.ts b/libs/ui/playback/src/lib/player-controls/player-controls.component.timeline.spec.ts index fccff8128..ddb2f71be 100644 --- a/libs/ui/playback/src/lib/player-controls/player-controls.component.timeline.spec.ts +++ b/libs/ui/playback/src/lib/player-controls/player-controls.component.timeline.spec.ts @@ -27,6 +27,7 @@ function createFakeController() { setPlaybackSpeed: jest.fn(), setAspectRatio: jest.fn(), toggleRecording: jest.fn(), + togglePictureInPicture: jest.fn(), }; const controller: PlayerController = { capabilities, diff --git a/libs/ui/playback/src/lib/player-controls/player-controls.model.ts b/libs/ui/playback/src/lib/player-controls/player-controls.model.ts index 7785612c5..25185df21 100644 --- a/libs/ui/playback/src/lib/player-controls/player-controls.model.ts +++ b/libs/ui/playback/src/lib/player-controls/player-controls.model.ts @@ -17,6 +17,7 @@ export interface PlayerControlsCapabilities { playbackSpeed: boolean; aspectRatio: boolean; recording: boolean; + pictureInPicture: boolean; fullscreen: boolean; seriesNavigation: boolean; } @@ -61,6 +62,8 @@ export interface PlayerControlsState { aspectRatio: string; aspectPresets: ReadonlyArray>; recording: PlayerRecordingState; + pictureInPictureActive: boolean; + canPictureInPicture: boolean; canPreviousEpisode: boolean; canNextEpisode: boolean; } @@ -76,6 +79,7 @@ export interface PlayerControlsCommands { setPlaybackSpeed(speed: number): void; setAspectRatio(value: string): void; toggleRecording(): void; + togglePictureInPicture(): void; } /** diff --git a/libs/ui/playback/src/lib/player-controls/web-video-controls.adapter.ts b/libs/ui/playback/src/lib/player-controls/web-video-controls.adapter.ts index e4ce5836e..dc8937592 100644 --- a/libs/ui/playback/src/lib/player-controls/web-video-controls.adapter.ts +++ b/libs/ui/playback/src/lib/player-controls/web-video-controls.adapter.ts @@ -115,6 +115,7 @@ export class WebVideoControlsAdapter implements PlayerController { subtitles: hasSubtitles, aspectRatio: false, recording: false, + pictureInPicture: false, seriesNavigation: !isLive && this.seriesNavigation() !== null, }; }); @@ -151,6 +152,8 @@ export class WebVideoControlsAdapter implements PlayerController { aspectRatio: 'no', aspectPresets: DEFAULT_ASPECT_PRESETS, recording: { active: false, elapsedSeconds: 0, message: null }, + pictureInPictureActive: false, + canPictureInPicture: false, canPreviousEpisode: seriesNavCapable && seriesNav?.canPrevious === true, canNextEpisode: seriesNavCapable && seriesNav?.canNext === true, @@ -170,6 +173,7 @@ export class WebVideoControlsAdapter implements PlayerController { setPlaybackSpeed: (speed) => this.applySpeed(speed), setAspectRatio: () => undefined, toggleRecording: () => undefined, + togglePictureInPicture: () => undefined, }; /** Binds to a `