feat(embedded-mpv): use shared controls for frame-copy

This commit is contained in:
4gray committed 2026-07-16 16:17:39 +02:00
1 parent 1ec47624ed
commit 7913b75017
5 files changed
+665 -27

No files matched your search

@@ -3,10 +3,14 @@
class="embedded-mpv-player"
[class.embedded-mpv-player--fullscreen]="isFullscreen()"
[class.embedded-mpv-player--controls-enabled]="
showControls() && isSupported()
!isFrameCopyEngine() && showControls() && isSupported()
"
[class.embedded-mpv-player--controls-visible]="
!isFrameCopyEngine() && controlsAreVisible()
"
[class.embedded-mpv-player--cursor-hidden]="
!isFrameCopyEngine() && hideCursor()
"
[class.embedded-mpv-player--controls-visible]="controlsAreVisible()"
[class.embedded-mpv-player--cursor-hidden]="hideCursor()"
(pointerenter)="onPlayerInteraction()"
(pointermove)="onPlayerInteraction()"
(click)="onPlayerInteraction()"
@@ -90,7 +94,18 @@
}
</div>
@if (showControls() && isSupported()) {
@if (isFrameCopyEngine() && isSupported()) {
<app-player-controls
[controller]="sharedControls"
[playerSurface]="playerSurface()"
[showControls]="showControls()"
[shortcutsEnabled]="sharedShortcutsEnabled()"
(previousEpisodeRequested)="requestPreviousEpisode()"
(nextEpisodeRequested)="requestNextEpisode()"
/>
}
@if (!isFrameCopyEngine() && showControls() && isSupported()) {
<div
class="embedded-mpv-player__controls"
[class.embedded-mpv-player__controls--visible]="
@@ -104,14 +119,18 @@
class="embedded-mpv-player__control-button"
[disabled]="isLoading()"
(click)="togglePaused()"
[attr.aria-label]="(isPaused()
[attr.aria-label]="
(isPaused()
? 'EMBEDDED_MPV.PLAYER.PLAY'
: 'EMBEDDED_MPV.PLAYER.PAUSE'
) | translate"
[matTooltip]="(isPaused()
) | translate
"
[matTooltip]="
(isPaused()
? 'EMBEDDED_MPV.PLAYER.PLAY'
: 'EMBEDDED_MPV.PLAYER.PAUSE'
) | translate"
) | translate
"
matTooltipPosition="above"
>
<mat-icon>{{
@@ -266,14 +285,18 @@
type="button"
class="embedded-mpv-player__control-button"
(click)="toggleMute()"
[attr.aria-label]="(volume() <= 0
[attr.aria-label]="
(volume() <= 0
? 'EMBEDDED_MPV.PLAYER.UNMUTE'
: 'EMBEDDED_MPV.PLAYER.MUTE'
) | translate"
[matTooltip]="(volume() <= 0
) | translate
"
[matTooltip]="
(volume() <= 0
? 'EMBEDDED_MPV.PLAYER.UNMUTE'
: 'EMBEDDED_MPV.PLAYER.MUTE'
) | translate"
) | translate
"
matTooltipPosition="above"
>
<mat-icon>{{ volumeIcon() }}</mat-icon>
@@ -482,7 +505,7 @@
: {
speed: speedLabel(
playbackSpeed()
)
),
}
"
matTooltipPosition="above"
@@ -557,7 +580,7 @@
: {
aspect: aspectLabel(
aspectOverride()
)
),
}
"
matTooltipPosition="above"
@@ -0,0 +1,305 @@
import { signal } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { TranslateModule } from '@ngx-translate/core';
import type {
EmbeddedMpvEngine,
EmbeddedMpvSession,
ResolvedPortalPlayback,
} from '@iptvnator/shared/interfaces';
import { PlayerControlsComponent } from '../player-controls/player-controls.component';
import { EmbeddedMpvOverlayVisibilityService } from './embedded-mpv-overlay-visibility.service';
import { EmbeddedMpvPlayerComponent } from './embedded-mpv-player.component';
import { EmbeddedMpvSessionController } from './embedded-mpv-session-controller';
const PLAYBACK: ResolvedPortalPlayback = {
streamUrl: 'https://example.test/movie.mp4',
title: 'Movie',
contentInfo: {
playlistId: 'playlist-1',
contentXtreamId: 42,
contentType: 'movie',
},
};
const READY_SESSION: EmbeddedMpvSession = {
id: 'session-1',
title: 'Movie',
streamUrl: PLAYBACK.streamUrl,
status: 'playing',
positionSeconds: 30,
durationSeconds: 120,
volume: 1,
audioTracks: [],
selectedAudioTrackId: null,
subtitleTracks: [],
selectedSubtitleTrackId: null,
playbackSpeed: 1,
aspectOverride: 'no',
recording: { active: false },
startedAt: '2026-07-16T10:00:00Z',
updatedAt: '2026-07-16T10:00:00Z',
};
const overlayActive = signal(false);
describe('EmbeddedMpvPlayerComponent shared controls host', () => {
const fixtures: ComponentFixture<EmbeddedMpvPlayerComponent>[] = [];
let fullscreenElement: Element | null;
let fullscreenElementDescriptor: PropertyDescriptor | undefined;
beforeEach(async () => {
localStorage.removeItem('volume');
overlayActive.set(false);
fullscreenElementDescriptor = Object.getOwnPropertyDescriptor(
document,
'fullscreenElement'
);
fullscreenElement = null;
Object.defineProperty(document, 'fullscreenElement', {
configurable: true,
get: () => fullscreenElement,
});
await TestBed.configureTestingModule({
imports: [EmbeddedMpvPlayerComponent, TranslateModule.forRoot()],
providers: [
{
provide: EmbeddedMpvOverlayVisibilityService,
useValue: { overlayActive },
},
],
}).compileComponents();
});
afterEach(() => {
for (const fixture of fixtures.splice(0)) {
fixture.destroy();
}
restoreProperty(
document,
'fullscreenElement',
fullscreenElementDescriptor
);
jest.useRealTimers();
jest.restoreAllMocks();
});
function restoreProperty(
target: object,
property: PropertyKey,
descriptor: PropertyDescriptor | undefined
): void {
if (descriptor) {
Object.defineProperty(target, property, descriptor);
return;
}
Reflect.deleteProperty(target, property);
}
function support(engine: EmbeddedMpvEngine) {
return {
supported: true,
platform: 'darwin',
engine,
capabilities: {
subtitles: true,
playbackSpeed: true,
aspectOverride: true,
screenshot: true,
recording: true,
},
} as const;
}
function render(
engine: EmbeddedMpvEngine = 'frame-copy',
showControls = true
) {
const fixture = TestBed.createComponent(EmbeddedMpvPlayerComponent);
fixtures.push(fixture);
fixture.componentRef.setInput('playback', PLAYBACK);
fixture.componentRef.setInput('showControls', showControls);
fixture.componentRef.setInput('seriesNavigation', {
canPrevious: true,
canNext: false,
autoplayEnabled: true,
});
const component = fixture.componentInstance;
const controller = fixture.debugElement.injector.get(
EmbeddedMpvSessionController
);
controller.support.set(support(engine));
controller.session.set({ ...READY_SESSION });
fixture.detectChanges();
fixture.detectChanges();
return { fixture, component, controller };
}
function sharedControls(
fixture: ComponentFixture<EmbeddedMpvPlayerComponent>
): PlayerControlsComponent | null {
return (
fixture.debugElement.query(By.directive(PlayerControlsComponent))
?.componentInstance ?? null
);
}
function root(
fixture: ComponentFixture<EmbeddedMpvPlayerComponent>
): HTMLElement {
return fixture.debugElement.query(By.css('.embedded-mpv-player'))
.nativeElement;
}
function canvas(
fixture: ComponentFixture<EmbeddedMpvPlayerComponent>
): HTMLCanvasElement {
return fixture.debugElement.query(By.css('[data-embedded-mpv-frame]'))
.nativeElement;
}
it('renders the frame-copy canvas with shared controls and no legacy dock', () => {
const { fixture } = render();
const playerRoot = root(fixture);
expect(canvas(fixture)).toBeTruthy();
expect(sharedControls(fixture)).not.toBeNull();
expect(
fixture.debugElement.query(By.css('.embedded-mpv-player__controls'))
).toBeNull();
expect(
playerRoot.classList.contains(
'embedded-mpv-player--controls-enabled'
)
).toBe(false);
expect(
playerRoot.classList.contains(
'embedded-mpv-player--controls-visible'
)
).toBe(false);
});
it('keeps native playback on the legacy dock without shared controls', () => {
const { fixture } = render('native');
expect(sharedControls(fixture)).toBeNull();
expect(
fixture.debugElement.query(By.css('.embedded-mpv-player__controls'))
).not.toBeNull();
expect(
root(fixture).classList.contains(
'embedded-mpv-player--controls-enabled'
)
).toBe(true);
});
it('preserves the host previous and next episode guards', () => {
const { fixture, component } = render();
const controls = sharedControls(fixture);
const previous = jest.fn();
const next = jest.fn();
component.previousEpisodeRequested.subscribe(previous);
component.nextEpisodeRequested.subscribe(next);
controls?.previousEpisodeRequested.emit();
controls?.nextEpisodeRequested.emit();
expect(previous).toHaveBeenCalledTimes(1);
expect(next).not.toHaveBeenCalled();
fixture.componentRef.setInput('seriesNavigation', {
canPrevious: false,
canNext: true,
autoplayEnabled: true,
});
fixture.detectChanges();
controls?.previousEpisodeRequested.emit();
controls?.nextEpisodeRequested.emit();
expect(previous).toHaveBeenCalledTimes(1);
expect(next).toHaveBeenCalledTimes(1);
});
it('syncs bounds on fullscreen changes without revealing legacy state', () => {
const { fixture, component, controller } = render();
const triggerBoundsSync = jest.spyOn(controller, 'triggerBoundsSync');
component.controlsVisible.set(false);
fullscreenElement = root(fixture);
document.dispatchEvent(new Event('fullscreenchange'));
expect(component.isFullscreen()).toBe(true);
expect(triggerBoundsSync).toHaveBeenCalledTimes(1);
expect(component.controlsVisible()).toBe(false);
});
it('never renders both control systems across engine transitions', () => {
const { fixture, controller } = render();
jest.useFakeTimers();
const togglePaused = jest
.spyOn(controller, 'togglePaused')
.mockResolvedValue(undefined);
expect(sharedControls(fixture)).not.toBeNull();
controller.support.set(support('native'));
fixture.detectChanges();
expect(sharedControls(fixture)).toBeNull();
expect(
fixture.debugElement.query(By.css('.embedded-mpv-player__controls'))
).not.toBeNull();
fixture.debugElement
.query(By.css('.embedded-mpv-player__viewport'))
.nativeElement.dispatchEvent(
new MouseEvent('click', { bubbles: true })
);
controller.support.set(support('frame-copy'));
fixture.detectChanges();
expect(sharedControls(fixture)).not.toBeNull();
expect(
fixture.debugElement.query(By.css('.embedded-mpv-player__controls'))
).toBeNull();
jest.advanceTimersByTime(1000);
expect(togglePaused).not.toHaveBeenCalled();
});
it('leaves legacy pointer and popover state untouched on frame-copy', () => {
const { fixture, component } = render();
component.controlsVisible.set(false);
component.menus.open('audio');
root(fixture).dispatchEvent(new MouseEvent('pointermove'));
root(fixture).dispatchEvent(
new FocusEvent('focusin', { bubbles: true })
);
document.dispatchEvent(
new MouseEvent('pointermove', { clientX: 0, clientY: 0 })
);
document.body.dispatchEvent(
new MouseEvent('pointerdown', { bubbles: true })
);
expect(component.controlsVisible()).toBe(false);
expect(component.menus.audioOpen()).toBe(true);
});
it('uses only the adapter recording interval for frame-copy', () => {
const { fixture, controller } = render();
const setInterval = jest.spyOn(window, 'setInterval');
controller.session.update((session) =>
session
? {
...session,
recording: {
active: true,
startedAt: new Date().toISOString(),
},
}
: session
);
fixture.detectChanges();
expect(setInterval).toHaveBeenCalledTimes(1);
});
});
@@ -0,0 +1,251 @@
import { signal } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { TranslateModule } from '@ngx-translate/core';
import type {
EmbeddedMpvSession,
ResolvedPortalPlayback,
} from '@iptvnator/shared/interfaces';
import { EmbeddedMpvOverlayVisibilityService } from './embedded-mpv-overlay-visibility.service';
import { EmbeddedMpvPlayerComponent } from './embedded-mpv-player.component';
import { EmbeddedMpvSessionController } from './embedded-mpv-session-controller';
const PLAYBACK: ResolvedPortalPlayback = {
streamUrl: 'https://example.test/movie.mp4',
title: 'Movie',
contentInfo: {
playlistId: 'playlist-1',
contentXtreamId: 42,
contentType: 'movie',
},
};
const READY_SESSION: EmbeddedMpvSession = {
id: 'session-1',
title: 'Movie',
streamUrl: PLAYBACK.streamUrl,
status: 'playing',
positionSeconds: 30,
durationSeconds: 120,
volume: 1,
audioTracks: [],
selectedAudioTrackId: null,
subtitleTracks: [],
selectedSubtitleTrackId: null,
playbackSpeed: 1,
aspectOverride: 'no',
recording: { active: false },
startedAt: '2026-07-16T10:00:00Z',
updatedAt: '2026-07-16T10:00:00Z',
};
const overlayActive = signal(false);
describe('EmbeddedMpvPlayerComponent shared controls interactions', () => {
const fixtures: ComponentFixture<EmbeddedMpvPlayerComponent>[] = [];
let requestFullscreen: jest.Mock;
let requestFullscreenDescriptor: PropertyDescriptor | undefined;
let exitFullscreenDescriptor: PropertyDescriptor | undefined;
beforeEach(async () => {
localStorage.removeItem('volume');
overlayActive.set(false);
requestFullscreenDescriptor = Object.getOwnPropertyDescriptor(
HTMLElement.prototype,
'requestFullscreen'
);
exitFullscreenDescriptor = Object.getOwnPropertyDescriptor(
document,
'exitFullscreen'
);
requestFullscreen = jest.fn().mockResolvedValue(undefined);
Object.defineProperty(HTMLElement.prototype, 'requestFullscreen', {
configurable: true,
writable: true,
value: requestFullscreen,
});
Object.defineProperty(document, 'exitFullscreen', {
configurable: true,
writable: true,
value: jest.fn().mockResolvedValue(undefined),
});
await TestBed.configureTestingModule({
imports: [EmbeddedMpvPlayerComponent, TranslateModule.forRoot()],
providers: [
{
provide: EmbeddedMpvOverlayVisibilityService,
useValue: { overlayActive },
},
],
}).compileComponents();
});
afterEach(() => {
for (const fixture of fixtures.splice(0)) {
fixture.destroy();
}
restoreProperty(
HTMLElement.prototype,
'requestFullscreen',
requestFullscreenDescriptor
);
restoreProperty(document, 'exitFullscreen', exitFullscreenDescriptor);
jest.useRealTimers();
jest.restoreAllMocks();
});
function restoreProperty(
target: object,
property: PropertyKey,
descriptor: PropertyDescriptor | undefined
): void {
if (descriptor) {
Object.defineProperty(target, property, descriptor);
return;
}
Reflect.deleteProperty(target, property);
}
function render(showControls = true) {
const fixture = TestBed.createComponent(EmbeddedMpvPlayerComponent);
fixtures.push(fixture);
fixture.componentRef.setInput('playback', PLAYBACK);
fixture.componentRef.setInput('showControls', showControls);
const controller = fixture.debugElement.injector.get(
EmbeddedMpvSessionController
);
controller.support.set({
supported: true,
platform: 'darwin',
engine: 'frame-copy',
capabilities: {
subtitles: true,
playbackSpeed: true,
aspectOverride: true,
screenshot: true,
recording: true,
},
});
controller.session.set({ ...READY_SESSION });
fixture.detectChanges();
fixture.detectChanges();
return { fixture, controller };
}
function root(
fixture: ComponentFixture<EmbeddedMpvPlayerComponent>
): HTMLElement {
return fixture.debugElement.query(By.css('.embedded-mpv-player'))
.nativeElement;
}
function canvas(
fixture: ComponentFixture<EmbeddedMpvPlayerComponent>
): HTMLCanvasElement {
return fixture.debugElement.query(By.css('[data-embedded-mpv-frame]'))
.nativeElement;
}
function pressKey(key: string): KeyboardEvent {
const event = new KeyboardEvent('keydown', {
key,
bubbles: true,
cancelable: true,
});
document.dispatchEvent(event);
return event;
}
it('toggles pause exactly once after the shared click grace period', () => {
const { fixture, controller } = render();
jest.useFakeTimers();
const togglePaused = jest
.spyOn(controller, 'togglePaused')
.mockResolvedValue(undefined);
canvas(fixture).dispatchEvent(
new MouseEvent('click', { bubbles: true })
);
jest.advanceTimersByTime(249);
expect(togglePaused).not.toHaveBeenCalled();
jest.advanceTimersByTime(1);
expect(togglePaused).toHaveBeenCalledTimes(1);
});
it('double-clicks fullscreen exactly once on the player root', () => {
const { fixture, controller } = render();
jest.useFakeTimers();
const togglePaused = jest
.spyOn(controller, 'togglePaused')
.mockResolvedValue(undefined);
canvas(fixture).dispatchEvent(
new MouseEvent('click', { bubbles: true })
);
canvas(fixture).dispatchEvent(
new MouseEvent('dblclick', { bubbles: true })
);
jest.advanceTimersByTime(1000);
expect(togglePaused).not.toHaveBeenCalled();
expect(requestFullscreen).toHaveBeenCalledTimes(1);
expect(requestFullscreen.mock.instances[0]).toBe(root(fixture));
});
it('routes a keyboard playback toggle through shared controls once', () => {
const { controller } = render();
const togglePaused = jest
.spyOn(controller, 'togglePaused')
.mockResolvedValue(undefined);
const event = pressKey('k');
expect(event.defaultPrevented).toBe(true);
expect(togglePaused).toHaveBeenCalledTimes(1);
});
it('detaches shared surface and keyboard behavior when controls are hidden', () => {
const { fixture, controller } = render(false);
jest.useFakeTimers();
const togglePaused = jest
.spyOn(controller, 'togglePaused')
.mockResolvedValue(undefined);
canvas(fixture).dispatchEvent(
new MouseEvent('click', { bubbles: true })
);
canvas(fixture).dispatchEvent(
new MouseEvent('dblclick', { bubbles: true })
);
pressKey('k');
jest.advanceTimersByTime(1000);
expect(togglePaused).not.toHaveBeenCalled();
expect(requestFullscreen).not.toHaveBeenCalled();
});
it('disables shared keyboard shortcuts while an app overlay is active', () => {
const { fixture, controller } = render();
const togglePaused = jest
.spyOn(controller, 'togglePaused')
.mockResolvedValue(undefined);
overlayActive.set(true);
fixture.detectChanges();
const event = pressKey('k');
expect(event.defaultPrevented).toBe(false);
expect(togglePaused).not.toHaveBeenCalled();
});
it('does not let the legacy Escape handler mutate frame-copy menus', () => {
const { fixture } = render();
const component = fixture.componentInstance;
component.menus.open('audio');
pressKey('Escape');
expect(component.menus.audioOpen()).toBe(true);
});
});
@@ -55,6 +55,7 @@ describe('EmbeddedMpvPlayerComponent series navigation', () => {
controller.support.set({
supported: true,
platform: 'darwin',
engine: 'native',
capabilities: {
subtitles: false,
playbackSpeed: false,
@@ -97,7 +98,10 @@ describe('EmbeddedMpvPlayerComponent series navigation', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [EmbeddedMpvPlayerHostComponent, TranslateModule.forRoot()],
imports: [
EmbeddedMpvPlayerHostComponent,
TranslateModule.forRoot(),
],
providers: [
{
provide: EmbeddedMpvOverlayVisibilityService,
@@ -233,9 +237,8 @@ describe('EmbeddedMpvPlayerComponent series navigation', () => {
describe('timeline scrubbing', () => {
const slider = () =>
fixture.debugElement.query(
By.css('.embedded-mpv-player__slider')
).nativeElement as HTMLInputElement;
fixture.debugElement.query(By.css('.embedded-mpv-player__slider'))
.nativeElement as HTMLInputElement;
const dispatch = (type: string, value: string) => {
slider().value = value;
@@ -23,7 +23,9 @@ import {
EmbeddedMpvAudioTrack,
ResolvedPortalPlayback,
} from '@iptvnator/shared/interfaces';
import { PlayerControlsComponent } from '../player-controls/player-controls.component';
import type { SeriesPlaybackNavigation } from '../portal-inline-player/series-playback-navigation';
import { EmbeddedMpvControlsAdapter } from './embedded-mpv-controls.adapter';
import { EmbeddedMpvOverlayVisibilityService } from './embedded-mpv-overlay-visibility.service';
import { EmbeddedMpvSessionController } from './embedded-mpv-session-controller';
import { EmbeddedMpvShortcuts } from './embedded-mpv-shortcuts';
@@ -61,9 +63,10 @@ const VIEWPORT_CLICK_PAUSE_DELAY_MS = 250;
MatIconModule,
MatProgressSpinnerModule,
MatTooltipModule,
PlayerControlsComponent,
TranslatePipe,
],
providers: [EmbeddedMpvSessionController],
providers: [EmbeddedMpvControlsAdapter, EmbeddedMpvSessionController],
changeDetection: ChangeDetectionStrategy.OnPush,
host: {
class: 'embedded-mpv-player-host',
@@ -102,13 +105,20 @@ export class EmbeddedMpvPlayerComponent implements OnDestroy {
),
{ initialValue: null }
);
private readonly controller = inject(EmbeddedMpvSessionController);
readonly controller = inject(EmbeddedMpvSessionController);
readonly sharedControls = inject(EmbeddedMpvControlsAdapter);
private readonly shortcuts = new EmbeddedMpvShortcuts();
readonly menus = new EmbeddedMpvMenuState();
readonly feedback = new EmbeddedMpvFeedback();
readonly viewport = viewChild<ElementRef<HTMLDivElement>>('viewport');
readonly playerRoot = viewChild<ElementRef<HTMLDivElement>>('playerRoot');
readonly playerSurface = computed(
() => this.playerRoot()?.nativeElement ?? null
);
readonly sharedShortcutsEnabled = computed(
() => !this.overlayVisibility.overlayActive()
);
readonly support = this.controller.support;
readonly session = this.controller.session;
@@ -188,9 +198,7 @@ export class EmbeddedMpvPlayerComponent implements OnDestroy {
);
}
if (!session || session.status === 'loading') {
return this.translate.instant(
'EMBEDDED_MPV.PLAYER.LOADING_STREAM'
);
return this.translate.instant('EMBEDDED_MPV.PLAYER.LOADING_STREAM');
}
return '';
});
@@ -303,6 +311,9 @@ export class EmbeddedMpvPlayerComponent implements OnDestroy {
private readonly recordingMessage = signal<string | null>(null);
private readonly onDocumentPointerDown = (event: PointerEvent) => {
if (this.isFrameCopyEngine()) {
return;
}
const playerRoot = this.playerRoot()?.nativeElement;
if (!playerRoot || event.composedPath().includes(playerRoot)) {
return;
@@ -310,6 +321,9 @@ export class EmbeddedMpvPlayerComponent implements OnDestroy {
this.closePopovers();
};
private readonly onDocumentPointerMove = (event: PointerEvent) => {
if (this.isFrameCopyEngine()) {
return;
}
const playerRoot = this.playerRoot()?.nativeElement;
if (
playerRoot &&
@@ -324,11 +338,19 @@ export class EmbeddedMpvPlayerComponent implements OnDestroy {
this.isFullscreen.set(
Boolean(playerRoot && document.fullscreenElement === playerRoot)
);
this.revealControls();
if (!this.isFrameCopyEngine()) {
this.revealControls();
}
this.controller.triggerBoundsSync();
};
constructor() {
this.sharedControls.configure({
playback: this.playback,
seriesNavigation: this.seriesNavigation,
recordingFolder: this.recordingFolder,
});
if (typeof document !== 'undefined') {
document.addEventListener(
'fullscreenchange',
@@ -370,8 +392,14 @@ export class EmbeddedMpvPlayerComponent implements OnDestroy {
});
this.shortcuts.attach({
isAvailable: () => !this.overlayVisibility.overlayActive(),
onEscape: () => this.closePopovers(),
isAvailable: () =>
!this.isFrameCopyEngine() &&
!this.overlayVisibility.overlayActive(),
onEscape: () => {
if (!this.isFrameCopyEngine()) {
this.closePopovers();
}
},
togglePaused: () => void this.togglePaused(),
toggleFullscreen: () => void this.toggleFullscreen(),
seekBy: (delta) => void this.seekBy(delta),
@@ -447,7 +475,7 @@ export class EmbeddedMpvPlayerComponent implements OnDestroy {
});
effect((onCleanup) => {
if (!this.isRecording()) {
if (this.isFrameCopyEngine() || !this.isRecording()) {
return;
}
this.recordingTick.set(Date.now());
@@ -457,6 +485,17 @@ export class EmbeddedMpvPlayerComponent implements OnDestroy {
);
onCleanup(() => window.clearInterval(intervalId));
});
effect(() => {
if (!this.isFrameCopyEngine()) {
return;
}
untracked(() => {
this.clearControlsHideTimer();
this.clearViewportClickTimer();
this.menus.closeAll();
});
});
}
ngOnDestroy(): void {
@@ -493,10 +532,16 @@ export class EmbeddedMpvPlayerComponent implements OnDestroy {
}
onPlayerInteraction(): void {
if (this.isFrameCopyEngine()) {
return;
}
this.revealControls();
}
onViewportClick(event: MouseEvent): void {
if (this.isFrameCopyEngine()) {
return;
}
const target = event.target as HTMLElement | null;
if (target?.closest('button, input, [role="slider"]')) {
return;
@@ -519,6 +564,9 @@ export class EmbeddedMpvPlayerComponent implements OnDestroy {
}
onPlayerDblClick(event: MouseEvent): void {
if (this.isFrameCopyEngine()) {
return;
}
const target = event.target as HTMLElement | null;
if (target?.closest('button, input, [role="slider"]')) {
return;
@@ -845,6 +893,10 @@ export class EmbeddedMpvPlayerComponent implements OnDestroy {
}
private revealControls(scheduleHide = true): void {
if (this.isFrameCopyEngine()) {
this.clearControlsHideTimer();
return;
}
this.controlsVisible.set(true);
if (scheduleHide) {
this.clearControlsHideTimer();
@@ -853,6 +905,10 @@ export class EmbeddedMpvPlayerComponent implements OnDestroy {
}
private scheduleControlsHide(): void {
if (this.isFrameCopyEngine()) {
this.clearControlsHideTimer();
return;
}
if (
!this.isPlaying() ||
this.menus.anyOpen() ||