mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-14 05:06:16 -08:00
refactor(embedded-mpv): isolate controls timing
This commit is contained in:
1 parent
fec7190dd4
commit
051c7f9ff4
5 files changed
+400
-278
No files matched your search
@@ -369,10 +369,17 @@ controls host. The renderer files live under
|
||||
`EmbeddedMpvSessionController`, and projects series navigation and recording.
|
||||
- `embedded-mpv-controls-recording.ts` — frame-copy shared-controls recording
|
||||
coordinator. Serializes toggles, correlates command settlement with fresh
|
||||
same-owner snapshots, owns acknowledgement/message timers, and cancels on
|
||||
ownership or engine changes.
|
||||
same-owner snapshots, and cancels pending state on ownership or engine
|
||||
changes.
|
||||
- `embedded-mpv-controls-recording-feedback.ts` — semantic raw/translated
|
||||
recording feedback values and late translation resolution.
|
||||
- `embedded-mpv-controls-recording-timers.ts` — frame-copy recording feedback
|
||||
signal plus acknowledgement and message-dismiss timer ownership. Keeps
|
||||
transient timing lifecycle out of the recording correlation state machine.
|
||||
- `embedded-mpv-legacy-interactions.ts` — native-view-only pointer listeners,
|
||||
click/double-click arbitration, popover closing, controls auto-hide, and
|
||||
volume-close timers. An engine handoff cancels its pending legacy
|
||||
interactions before frame-copy takes ownership.
|
||||
- `embedded-mpv-shortcuts.ts` — native-view-only `EmbeddedMpvShortcuts` class
|
||||
with `attach(handlers)` / `detach()`. Owns the legacy document keydown
|
||||
listener and routes through a callback interface; the component supplies
|
||||
@@ -394,8 +401,9 @@ controls host. The renderer files live under
|
||||
- `embedded-mpv-player.component.ts` — view shell and engine-specific controls
|
||||
host. It mounts shared controls only for frame-copy and the legacy dock only
|
||||
for native-view; holds view children, derived `computed` signals, DOM event
|
||||
listeners, and effects for session lifecycle, bounds sync, session fan-out,
|
||||
playback-ended emission, engine handoff, and native-only recording ticks.
|
||||
handling for fullscreen, and effects for session lifecycle, bounds sync,
|
||||
session fan-out, playback-ended emission, engine handoff, and native-only
|
||||
recording ticks.
|
||||
|
||||
### Bounds compositing strategy
|
||||
|
||||
@@ -433,11 +441,12 @@ the helper's render size.
|
||||
feedback, and recording elapsed timer stay authoritative. The shared
|
||||
recording adapter is inert outside frame-copy.
|
||||
|
||||
An engine handoff clears native controls-hide/click/volume timers and closes
|
||||
native menus when frame-copy takes ownership. Legacy feedback is cleared on
|
||||
each engine transition and its overlay is never rendered for frame-copy, so a
|
||||
late native command completion cannot paint above shared controls. The handoff
|
||||
also changes the shared recording owner, which cancels pending operations and
|
||||
An engine handoff asks `EmbeddedMpvLegacyInteractions` to clear native
|
||||
controls-hide/click/volume timers and close native menus when frame-copy takes
|
||||
ownership. Legacy feedback is cleared on each engine transition and its overlay
|
||||
is never rendered for frame-copy, so a late native command completion cannot
|
||||
paint above shared controls. The handoff also changes the shared recording
|
||||
owner, which cancels pending operations, acknowledgement/message timers, and
|
||||
feedback. This prevents both systems from acting on the same session.
|
||||
|
||||
Both engines keep the component's `fullscreenchange` listener because
|
||||
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
import { signal } from '@angular/core';
|
||||
import type { RecordingFeedback } from './embedded-mpv-controls-recording-feedback';
|
||||
|
||||
const RECORDING_ACK_TIMEOUT_MS = 5000;
|
||||
const RECORDING_MESSAGE_DISMISS_DELAY_MS = 5000;
|
||||
|
||||
export class EmbeddedMpvControlsRecordingTimers {
|
||||
private readonly feedbackState = signal<RecordingFeedback | null>(null);
|
||||
readonly feedback = this.feedbackState.asReadonly();
|
||||
|
||||
private acknowledgementTimer: number | null = null;
|
||||
private messageTimer: number | null = null;
|
||||
private destroyed = false;
|
||||
|
||||
setFeedback(feedback: RecordingFeedback | null, autoDismiss = false): void {
|
||||
if (this.destroyed) {
|
||||
return;
|
||||
}
|
||||
this.clearMessage();
|
||||
this.feedbackState.set(feedback);
|
||||
if (!feedback || !autoDismiss) {
|
||||
return;
|
||||
}
|
||||
const timerId = window.setTimeout(() => {
|
||||
if (!this.destroyed && this.feedbackState() === feedback) {
|
||||
this.feedbackState.set(null);
|
||||
}
|
||||
if (this.messageTimer === timerId) {
|
||||
this.messageTimer = null;
|
||||
}
|
||||
}, RECORDING_MESSAGE_DISMISS_DELAY_MS);
|
||||
this.messageTimer = timerId;
|
||||
}
|
||||
|
||||
scheduleAcknowledgement(callback: () => void): void {
|
||||
this.clearAcknowledgement();
|
||||
this.acknowledgementTimer = window.setTimeout(() => {
|
||||
this.acknowledgementTimer = null;
|
||||
callback();
|
||||
}, RECORDING_ACK_TIMEOUT_MS);
|
||||
}
|
||||
|
||||
clearAcknowledgement(): void {
|
||||
if (this.acknowledgementTimer === null) {
|
||||
return;
|
||||
}
|
||||
window.clearTimeout(this.acknowledgementTimer);
|
||||
this.acknowledgementTimer = null;
|
||||
}
|
||||
|
||||
destroy(): void {
|
||||
this.destroyed = true;
|
||||
this.clearAcknowledgement();
|
||||
this.clearMessage();
|
||||
this.feedbackState.set(null);
|
||||
}
|
||||
|
||||
private clearMessage(): void {
|
||||
if (this.messageTimer === null) {
|
||||
return;
|
||||
}
|
||||
window.clearTimeout(this.messageTimer);
|
||||
this.messageTimer = null;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
import { signal } from '@angular/core';
|
||||
import type {
|
||||
EmbeddedMpvSession,
|
||||
ResolvedPortalPlayback,
|
||||
@@ -8,11 +7,9 @@ import {
|
||||
RECORDING_TRANSLATION,
|
||||
type RecordingFeedback,
|
||||
} from './embedded-mpv-controls-recording-feedback';
|
||||
import { EmbeddedMpvControlsRecordingTimers } from './embedded-mpv-controls-recording-timers';
|
||||
import type { EmbeddedMpvSessionController } from './embedded-mpv-session-controller';
|
||||
|
||||
const RECORDING_ACK_TIMEOUT_MS = 5000;
|
||||
const RECORDING_MESSAGE_DISMISS_DELAY_MS = 5000;
|
||||
|
||||
const RECORDING_OPERATION = {
|
||||
START: 'start',
|
||||
STOP: 'stop',
|
||||
@@ -50,13 +47,11 @@ export interface RecordingToggleContext {
|
||||
}
|
||||
|
||||
export class EmbeddedMpvControlsRecording {
|
||||
private readonly feedbackState = signal<RecordingFeedback | null>(null);
|
||||
readonly feedback = this.feedbackState.asReadonly();
|
||||
private readonly timers = new EmbeddedMpvControlsRecordingTimers();
|
||||
readonly feedback = this.timers.feedback;
|
||||
|
||||
private pending: PendingRecordingOperation | null = null;
|
||||
private operationGeneration = 0;
|
||||
private acknowledgementTimer: number | null = null;
|
||||
private messageTimer: number | null = null;
|
||||
private ownerIdentity: string | null | undefined;
|
||||
private destroyed = false;
|
||||
|
||||
@@ -92,7 +87,9 @@ export class EmbeddedMpvControlsRecording {
|
||||
sawErrorClear: initialError === null,
|
||||
};
|
||||
this.setFeedback(null);
|
||||
this.scheduleAcknowledgementTimeout(generation);
|
||||
this.timers.scheduleAcknowledgement(() =>
|
||||
this.handleAcknowledgementTimeout(generation)
|
||||
);
|
||||
const command =
|
||||
kind === RECORDING_OPERATION.START
|
||||
? this.controller.startRecording(
|
||||
@@ -207,9 +204,9 @@ export class EmbeddedMpvControlsRecording {
|
||||
}
|
||||
|
||||
destroy(): void {
|
||||
this.setFeedback(null);
|
||||
this.destroyed = true;
|
||||
this.cancelPending();
|
||||
this.timers.destroy();
|
||||
}
|
||||
|
||||
private handleAcknowledgementTimeout(generation: number): void {
|
||||
@@ -217,7 +214,6 @@ export class EmbeddedMpvControlsRecording {
|
||||
if (!pending || pending.generation !== generation) {
|
||||
return;
|
||||
}
|
||||
this.acknowledgementTimer = null;
|
||||
this.reconcile(
|
||||
this.controller.session(),
|
||||
this.currentPlaybackIdentity()
|
||||
@@ -280,7 +276,9 @@ export class EmbeddedMpvControlsRecording {
|
||||
}
|
||||
if (outcome) {
|
||||
this.setFeedback(outcome.feedback);
|
||||
this.scheduleAcknowledgementTimeout(generation);
|
||||
this.timers.scheduleAcknowledgement(() =>
|
||||
this.handleAcknowledgementTimeout(generation)
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (reconciledPending.timeoutFeedback) {
|
||||
@@ -296,7 +294,7 @@ export class EmbeddedMpvControlsRecording {
|
||||
return;
|
||||
}
|
||||
pending.observedOutcome = outcome;
|
||||
this.clearAcknowledgementTimer();
|
||||
this.timers.clearAcknowledgement();
|
||||
if (pending.commandSettled) {
|
||||
this.completePending(outcome.feedback, outcome.autoDismiss);
|
||||
} else if (
|
||||
@@ -332,7 +330,7 @@ export class EmbeddedMpvControlsRecording {
|
||||
feedback: RecordingFeedback | null,
|
||||
autoDismiss = false
|
||||
): void {
|
||||
this.clearAcknowledgementTimer();
|
||||
this.timers.clearAcknowledgement();
|
||||
this.pending = null;
|
||||
this.setFeedback(feedback, autoDismiss);
|
||||
}
|
||||
@@ -340,51 +338,13 @@ export class EmbeddedMpvControlsRecording {
|
||||
private cancelPending(): void {
|
||||
this.operationGeneration += 1;
|
||||
this.pending = null;
|
||||
this.clearAcknowledgementTimer();
|
||||
this.timers.clearAcknowledgement();
|
||||
}
|
||||
|
||||
private setFeedback(
|
||||
feedback: RecordingFeedback | null,
|
||||
autoDismiss = false
|
||||
): void {
|
||||
if (this.destroyed) {
|
||||
return;
|
||||
}
|
||||
this.clearMessageTimer();
|
||||
this.feedbackState.set(feedback);
|
||||
if (!feedback || !autoDismiss) {
|
||||
return;
|
||||
}
|
||||
const timerId = window.setTimeout(() => {
|
||||
if (!this.destroyed && this.feedbackState() === feedback) {
|
||||
this.feedbackState.set(null);
|
||||
}
|
||||
if (this.messageTimer === timerId) {
|
||||
this.messageTimer = null;
|
||||
}
|
||||
}, RECORDING_MESSAGE_DISMISS_DELAY_MS);
|
||||
this.messageTimer = timerId;
|
||||
}
|
||||
|
||||
private clearAcknowledgementTimer(): void {
|
||||
if (this.acknowledgementTimer !== null) {
|
||||
window.clearTimeout(this.acknowledgementTimer);
|
||||
this.acknowledgementTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
private scheduleAcknowledgementTimeout(generation: number): void {
|
||||
this.clearAcknowledgementTimer();
|
||||
this.acknowledgementTimer = window.setTimeout(
|
||||
() => this.handleAcknowledgementTimeout(generation),
|
||||
RECORDING_ACK_TIMEOUT_MS
|
||||
);
|
||||
}
|
||||
|
||||
private clearMessageTimer(): void {
|
||||
if (this.messageTimer !== null) {
|
||||
window.clearTimeout(this.messageTimer);
|
||||
this.messageTimer = null;
|
||||
}
|
||||
this.timers.setFeedback(feedback, autoDismiss);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,249 @@
|
||||
import type { WritableSignal } from '@angular/core';
|
||||
import type { EmbeddedMpvMenuState } from './embedded-mpv-ui-state';
|
||||
|
||||
const HIDE_CONTROLS_DELAY_MS = 2500;
|
||||
const VOLUME_POPOVER_CLOSE_DELAY_MS = 220;
|
||||
const VIEWPORT_CLICK_PAUSE_DELAY_MS = 250;
|
||||
|
||||
export interface EmbeddedMpvLegacyInteractionsDeps {
|
||||
readonly isAvailable: () => boolean;
|
||||
readonly playerRoot: () => HTMLElement | null;
|
||||
readonly menus: EmbeddedMpvMenuState;
|
||||
readonly controlsVisible: WritableSignal<boolean>;
|
||||
readonly isLoading: () => boolean;
|
||||
readonly isErrored: () => boolean;
|
||||
readonly isStalled: () => boolean;
|
||||
readonly isPlaying: () => boolean;
|
||||
readonly statusLabel: () => string;
|
||||
readonly togglePaused: () => void | Promise<void>;
|
||||
readonly toggleFullscreen: () => void | Promise<void>;
|
||||
readonly triggerBoundsSync: () => void;
|
||||
}
|
||||
|
||||
export class EmbeddedMpvLegacyInteractions {
|
||||
private controlsHideTimer: number | null = null;
|
||||
private volumeCloseTimer: number | null = null;
|
||||
private viewportClickTimer: number | null = null;
|
||||
private attached = false;
|
||||
|
||||
private readonly onDocumentPointerDown = (event: PointerEvent) => {
|
||||
if (!this.deps.isAvailable()) {
|
||||
return;
|
||||
}
|
||||
const playerRoot = this.deps.playerRoot();
|
||||
if (!playerRoot || event.composedPath().includes(playerRoot)) {
|
||||
return;
|
||||
}
|
||||
this.closePopovers();
|
||||
};
|
||||
|
||||
private readonly onDocumentPointerMove = (event: PointerEvent) => {
|
||||
if (!this.deps.isAvailable()) {
|
||||
return;
|
||||
}
|
||||
const playerRoot = this.deps.playerRoot();
|
||||
if (
|
||||
playerRoot &&
|
||||
!event.composedPath().includes(playerRoot) &&
|
||||
this.isPointerInsidePlayer(event)
|
||||
) {
|
||||
this.revealControls();
|
||||
}
|
||||
};
|
||||
|
||||
constructor(private readonly deps: EmbeddedMpvLegacyInteractionsDeps) {}
|
||||
|
||||
attach(): void {
|
||||
if (this.attached || typeof document === 'undefined') {
|
||||
return;
|
||||
}
|
||||
document.addEventListener('pointerdown', this.onDocumentPointerDown);
|
||||
document.addEventListener('pointermove', this.onDocumentPointerMove, {
|
||||
passive: true,
|
||||
});
|
||||
this.attached = true;
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
if (this.attached && typeof document !== 'undefined') {
|
||||
document.removeEventListener(
|
||||
'pointerdown',
|
||||
this.onDocumentPointerDown
|
||||
);
|
||||
document.removeEventListener(
|
||||
'pointermove',
|
||||
this.onDocumentPointerMove
|
||||
);
|
||||
}
|
||||
this.attached = false;
|
||||
this.clearControlsHideTimer();
|
||||
this.clearVolumeCloseTimer();
|
||||
this.clearViewportClickTimer();
|
||||
}
|
||||
|
||||
isAvailable(): boolean {
|
||||
return this.deps.isAvailable();
|
||||
}
|
||||
|
||||
handleEngineTransition(isFrameCopyEngine: boolean): void {
|
||||
if (!isFrameCopyEngine) {
|
||||
return;
|
||||
}
|
||||
this.clearControlsHideTimer();
|
||||
this.clearVolumeCloseTimer();
|
||||
this.clearViewportClickTimer();
|
||||
this.deps.menus.closeAll();
|
||||
}
|
||||
|
||||
onPlayerInteraction(): void {
|
||||
if (!this.deps.isAvailable()) {
|
||||
return;
|
||||
}
|
||||
this.revealControls();
|
||||
}
|
||||
|
||||
onViewportClick(event: MouseEvent): void {
|
||||
if (!this.deps.isAvailable()) {
|
||||
return;
|
||||
}
|
||||
const target = event.target as HTMLElement | null;
|
||||
if (target?.closest('button, input, [role="slider"]')) {
|
||||
return;
|
||||
}
|
||||
if (this.deps.menus.anyOpen()) {
|
||||
this.deps.menus.closeAll();
|
||||
return;
|
||||
}
|
||||
if (
|
||||
this.deps.isLoading() ||
|
||||
this.deps.isErrored() ||
|
||||
this.deps.isStalled()
|
||||
) {
|
||||
return;
|
||||
}
|
||||
// Delay the single-click transport toggle so a double-click can
|
||||
// cancel it and claim the interaction for fullscreen instead.
|
||||
this.clearViewportClickTimer();
|
||||
this.viewportClickTimer = window.setTimeout(() => {
|
||||
this.viewportClickTimer = null;
|
||||
void this.deps.togglePaused();
|
||||
}, VIEWPORT_CLICK_PAUSE_DELAY_MS);
|
||||
}
|
||||
|
||||
onPlayerDblClick(event: MouseEvent): void {
|
||||
if (!this.deps.isAvailable()) {
|
||||
return;
|
||||
}
|
||||
const target = event.target as HTMLElement | null;
|
||||
if (target?.closest('button, input, [role="slider"]')) {
|
||||
return;
|
||||
}
|
||||
this.clearViewportClickTimer();
|
||||
void this.deps.toggleFullscreen();
|
||||
}
|
||||
|
||||
onVolumeHoverEnter(): void {
|
||||
if (!this.deps.isAvailable()) {
|
||||
return;
|
||||
}
|
||||
this.clearVolumeCloseTimer();
|
||||
this.deps.menus.open('volume');
|
||||
}
|
||||
|
||||
onVolumeHoverLeave(): void {
|
||||
if (!this.deps.isAvailable()) {
|
||||
return;
|
||||
}
|
||||
this.clearVolumeCloseTimer();
|
||||
this.volumeCloseTimer = window.setTimeout(() => {
|
||||
this.deps.menus.close('volume');
|
||||
this.volumeCloseTimer = null;
|
||||
}, VOLUME_POPOVER_CLOSE_DELAY_MS);
|
||||
}
|
||||
|
||||
closePopovers(): void {
|
||||
if (!this.deps.isAvailable() || !this.deps.menus.anyOpen()) {
|
||||
return;
|
||||
}
|
||||
this.deps.menus.closeAll();
|
||||
this.deps.triggerBoundsSync();
|
||||
this.scheduleControlsHide();
|
||||
}
|
||||
|
||||
revealControls(scheduleHide = true): void {
|
||||
if (!this.deps.isAvailable()) {
|
||||
this.clearControlsHideTimer();
|
||||
return;
|
||||
}
|
||||
this.deps.controlsVisible.set(true);
|
||||
if (scheduleHide) {
|
||||
this.clearControlsHideTimer();
|
||||
this.scheduleControlsHide();
|
||||
}
|
||||
}
|
||||
|
||||
scheduleControlsHide(): void {
|
||||
if (!this.deps.isAvailable()) {
|
||||
this.clearControlsHideTimer();
|
||||
return;
|
||||
}
|
||||
if (
|
||||
!this.deps.isPlaying() ||
|
||||
this.deps.menus.anyOpen() ||
|
||||
Boolean(this.deps.statusLabel())
|
||||
) {
|
||||
this.clearControlsHideTimer();
|
||||
return;
|
||||
}
|
||||
if (!this.deps.controlsVisible() || this.controlsHideTimer !== null) {
|
||||
return;
|
||||
}
|
||||
this.controlsHideTimer = window.setTimeout(() => {
|
||||
if (
|
||||
this.deps.isPlaying() &&
|
||||
!this.deps.menus.anyOpen() &&
|
||||
!this.deps.statusLabel()
|
||||
) {
|
||||
this.deps.controlsVisible.set(false);
|
||||
}
|
||||
}, HIDE_CONTROLS_DELAY_MS);
|
||||
}
|
||||
|
||||
private clearControlsHideTimer(): void {
|
||||
if (this.controlsHideTimer === null) {
|
||||
return;
|
||||
}
|
||||
window.clearTimeout(this.controlsHideTimer);
|
||||
this.controlsHideTimer = null;
|
||||
}
|
||||
|
||||
private clearVolumeCloseTimer(): void {
|
||||
if (this.volumeCloseTimer === null) {
|
||||
return;
|
||||
}
|
||||
window.clearTimeout(this.volumeCloseTimer);
|
||||
this.volumeCloseTimer = null;
|
||||
}
|
||||
|
||||
private clearViewportClickTimer(): void {
|
||||
if (this.viewportClickTimer === null) {
|
||||
return;
|
||||
}
|
||||
window.clearTimeout(this.viewportClickTimer);
|
||||
this.viewportClickTimer = null;
|
||||
}
|
||||
|
||||
private isPointerInsidePlayer(event: PointerEvent): boolean {
|
||||
const playerRoot = this.deps.playerRoot();
|
||||
if (!playerRoot) {
|
||||
return false;
|
||||
}
|
||||
const rect = playerRoot.getBoundingClientRect();
|
||||
return (
|
||||
event.clientX >= rect.left &&
|
||||
event.clientX <= rect.right &&
|
||||
event.clientY >= rect.top &&
|
||||
event.clientY <= rect.bottom
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
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 { EmbeddedMpvLegacyInteractions } from './embedded-mpv-legacy-interactions';
|
||||
import { EmbeddedMpvOverlayVisibilityService } from './embedded-mpv-overlay-visibility.service';
|
||||
import { EmbeddedMpvSessionController } from './embedded-mpv-session-controller';
|
||||
import { EmbeddedMpvShortcuts } from './embedded-mpv-shortcuts';
|
||||
@@ -49,10 +50,7 @@ import {
|
||||
volumeIcon,
|
||||
} from './embedded-mpv-format.utils';
|
||||
|
||||
const HIDE_CONTROLS_DELAY_MS = 2500;
|
||||
const RECORDING_MESSAGE_DISMISS_DELAY_MS = 5000;
|
||||
const VOLUME_POPOVER_CLOSE_DELAY_MS = 220;
|
||||
const VIEWPORT_CLICK_PAUSE_DELAY_MS = 250;
|
||||
|
||||
@Component({
|
||||
selector: 'app-embedded-mpv-player',
|
||||
@@ -302,49 +300,38 @@ export class EmbeddedMpvPlayerComponent implements OnDestroy {
|
||||
});
|
||||
|
||||
private mutedVolume = 0;
|
||||
private controlsHideTimer: number | null = null;
|
||||
private volumeCloseTimer: number | null = null;
|
||||
private recordingMessageTimer: number | null = null;
|
||||
private viewportClickTimer: number | null = null;
|
||||
private lastEndedSessionId: string | null = null;
|
||||
private readonly recordingTick = signal(Date.now());
|
||||
private readonly recordingMessage = signal<string | null>(null);
|
||||
private readonly legacyInteractions: EmbeddedMpvLegacyInteractions;
|
||||
|
||||
private readonly onDocumentPointerDown = (event: PointerEvent) => {
|
||||
if (this.isFrameCopyEngine()) {
|
||||
return;
|
||||
}
|
||||
const playerRoot = this.playerRoot()?.nativeElement;
|
||||
if (!playerRoot || event.composedPath().includes(playerRoot)) {
|
||||
return;
|
||||
}
|
||||
this.closePopovers();
|
||||
};
|
||||
private readonly onDocumentPointerMove = (event: PointerEvent) => {
|
||||
if (this.isFrameCopyEngine()) {
|
||||
return;
|
||||
}
|
||||
const playerRoot = this.playerRoot()?.nativeElement;
|
||||
if (
|
||||
playerRoot &&
|
||||
!event.composedPath().includes(playerRoot) &&
|
||||
this.isPointerInsidePlayer(event)
|
||||
) {
|
||||
this.revealControls();
|
||||
}
|
||||
};
|
||||
private readonly onFullscreenChange = () => {
|
||||
const playerRoot = this.playerRoot()?.nativeElement;
|
||||
this.isFullscreen.set(
|
||||
Boolean(playerRoot && document.fullscreenElement === playerRoot)
|
||||
);
|
||||
if (!this.isFrameCopyEngine()) {
|
||||
this.revealControls();
|
||||
}
|
||||
this.legacyInteractions.revealControls();
|
||||
this.controller.triggerBoundsSync();
|
||||
};
|
||||
|
||||
constructor() {
|
||||
this.legacyInteractions = new EmbeddedMpvLegacyInteractions({
|
||||
isAvailable: () => !this.isFrameCopyEngine(),
|
||||
playerRoot: () => this.playerRoot()?.nativeElement ?? null,
|
||||
menus: this.menus,
|
||||
controlsVisible: this.controlsVisible,
|
||||
isLoading: this.isLoading,
|
||||
isErrored: this.isErrored,
|
||||
isStalled: this.stalled,
|
||||
isPlaying: this.isPlaying,
|
||||
statusLabel: this.statusLabel,
|
||||
togglePaused: () => this.togglePaused(),
|
||||
toggleFullscreen: () => this.toggleFullscreen(),
|
||||
triggerBoundsSync: () => this.controller.triggerBoundsSync(),
|
||||
});
|
||||
this.legacyInteractions.attach();
|
||||
|
||||
this.sharedControls.configure({
|
||||
playback: this.playback,
|
||||
seriesNavigation: this.seriesNavigation,
|
||||
@@ -356,15 +343,6 @@ export class EmbeddedMpvPlayerComponent implements OnDestroy {
|
||||
'fullscreenchange',
|
||||
this.onFullscreenChange
|
||||
);
|
||||
document.addEventListener(
|
||||
'pointerdown',
|
||||
this.onDocumentPointerDown
|
||||
);
|
||||
document.addEventListener(
|
||||
'pointermove',
|
||||
this.onDocumentPointerMove,
|
||||
{ passive: true }
|
||||
);
|
||||
}
|
||||
|
||||
this.controller.setBoundsProvider((host) => {
|
||||
@@ -393,13 +371,9 @@ export class EmbeddedMpvPlayerComponent implements OnDestroy {
|
||||
|
||||
this.shortcuts.attach({
|
||||
isAvailable: () =>
|
||||
!this.isFrameCopyEngine() &&
|
||||
this.legacyInteractions.isAvailable() &&
|
||||
!this.overlayVisibility.overlayActive(),
|
||||
onEscape: () => {
|
||||
if (!this.isFrameCopyEngine()) {
|
||||
this.closePopovers();
|
||||
}
|
||||
},
|
||||
onEscape: () => this.legacyInteractions.closePopovers(),
|
||||
togglePaused: () => void this.togglePaused(),
|
||||
toggleFullscreen: () => void this.toggleFullscreen(),
|
||||
seekBy: (delta) => void this.seekBy(delta),
|
||||
@@ -457,7 +431,7 @@ export class EmbeddedMpvPlayerComponent implements OnDestroy {
|
||||
currentTime: session.positionSeconds,
|
||||
duration: session.durationSeconds ?? 0,
|
||||
});
|
||||
this.scheduleControlsHide();
|
||||
this.legacyInteractions.scheduleControlsHide();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -491,13 +465,9 @@ export class EmbeddedMpvPlayerComponent implements OnDestroy {
|
||||
untracked(() => {
|
||||
this.feedback.clear();
|
||||
this.setRecordingMessage(null);
|
||||
if (!isFrameCopyEngine) {
|
||||
return;
|
||||
}
|
||||
this.clearControlsHideTimer();
|
||||
this.clearVolumeCloseTimer();
|
||||
this.clearViewportClickTimer();
|
||||
this.menus.closeAll();
|
||||
this.legacyInteractions.handleEngineTransition(
|
||||
isFrameCopyEngine
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -510,86 +480,30 @@ export class EmbeddedMpvPlayerComponent implements OnDestroy {
|
||||
'fullscreenchange',
|
||||
this.onFullscreenChange
|
||||
);
|
||||
document.removeEventListener(
|
||||
'pointerdown',
|
||||
this.onDocumentPointerDown
|
||||
);
|
||||
document.removeEventListener(
|
||||
'pointermove',
|
||||
this.onDocumentPointerMove
|
||||
);
|
||||
}
|
||||
this.clearVolumeCloseTimer();
|
||||
this.legacyInteractions.dispose();
|
||||
this.clearRecordingMessageTimer();
|
||||
this.clearControlsHideTimer();
|
||||
this.clearViewportClickTimer();
|
||||
}
|
||||
|
||||
private clearVolumeCloseTimer(): void {
|
||||
if (this.volumeCloseTimer !== null) {
|
||||
window.clearTimeout(this.volumeCloseTimer);
|
||||
this.volumeCloseTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
private clearViewportClickTimer(): void {
|
||||
if (this.viewportClickTimer !== null) {
|
||||
window.clearTimeout(this.viewportClickTimer);
|
||||
this.viewportClickTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
onPlayerInteraction(): void {
|
||||
if (this.isFrameCopyEngine()) {
|
||||
return;
|
||||
}
|
||||
this.revealControls();
|
||||
this.legacyInteractions.onPlayerInteraction();
|
||||
}
|
||||
|
||||
onViewportClick(event: MouseEvent): void {
|
||||
if (this.isFrameCopyEngine()) {
|
||||
return;
|
||||
}
|
||||
const target = event.target as HTMLElement | null;
|
||||
if (target?.closest('button, input, [role="slider"]')) {
|
||||
return;
|
||||
}
|
||||
if (this.menus.anyOpen()) {
|
||||
this.menus.closeAll();
|
||||
return;
|
||||
}
|
||||
if (this.isLoading() || this.isErrored() || this.stalled()) {
|
||||
return;
|
||||
}
|
||||
// The pause is deferred so a double-click (fullscreen) can cancel it;
|
||||
// pause feedback is the transport icon in the dock — the native video
|
||||
// surface paints over any DOM overlay we could flash here.
|
||||
this.clearViewportClickTimer();
|
||||
this.viewportClickTimer = window.setTimeout(() => {
|
||||
this.viewportClickTimer = null;
|
||||
void this.togglePaused();
|
||||
}, VIEWPORT_CLICK_PAUSE_DELAY_MS);
|
||||
this.legacyInteractions.onViewportClick(event);
|
||||
}
|
||||
|
||||
onPlayerDblClick(event: MouseEvent): void {
|
||||
if (this.isFrameCopyEngine()) {
|
||||
return;
|
||||
}
|
||||
const target = event.target as HTMLElement | null;
|
||||
if (target?.closest('button, input, [role="slider"]')) {
|
||||
return;
|
||||
}
|
||||
this.clearViewportClickTimer();
|
||||
void this.toggleFullscreen();
|
||||
this.legacyInteractions.onPlayerDblClick(event);
|
||||
}
|
||||
|
||||
async togglePaused(): Promise<void> {
|
||||
this.revealControls();
|
||||
this.legacyInteractions.revealControls();
|
||||
await this.controller.togglePaused();
|
||||
}
|
||||
|
||||
async toggleFullscreen(): Promise<void> {
|
||||
this.revealControls();
|
||||
this.legacyInteractions.revealControls();
|
||||
const playerRoot = this.playerRoot()?.nativeElement;
|
||||
if (!playerRoot || !this.canFullscreen()) {
|
||||
return;
|
||||
@@ -608,7 +522,7 @@ export class EmbeddedMpvPlayerComponent implements OnDestroy {
|
||||
}
|
||||
|
||||
async seekBy(deltaSeconds: number): Promise<void> {
|
||||
this.revealControls();
|
||||
this.legacyInteractions.revealControls();
|
||||
const ok = await this.controller.seekBy(deltaSeconds);
|
||||
if (ok) {
|
||||
this.feedback.flash(
|
||||
@@ -619,7 +533,7 @@ export class EmbeddedMpvPlayerComponent implements OnDestroy {
|
||||
}
|
||||
|
||||
requestPreviousEpisode(): void {
|
||||
this.revealControls();
|
||||
this.legacyInteractions.revealControls();
|
||||
if (!this.canPreviousEpisode()) {
|
||||
return;
|
||||
}
|
||||
@@ -627,7 +541,7 @@ export class EmbeddedMpvPlayerComponent implements OnDestroy {
|
||||
}
|
||||
|
||||
requestNextEpisode(): void {
|
||||
this.revealControls();
|
||||
this.legacyInteractions.revealControls();
|
||||
if (!this.canNextEpisode()) {
|
||||
return;
|
||||
}
|
||||
@@ -635,7 +549,7 @@ export class EmbeddedMpvPlayerComponent implements OnDestroy {
|
||||
}
|
||||
|
||||
onTimelineInput(event: Event): void {
|
||||
this.revealControls();
|
||||
this.legacyInteractions.revealControls();
|
||||
this.scrubPosition.set(
|
||||
Number((event.target as HTMLInputElement).value)
|
||||
);
|
||||
@@ -650,7 +564,7 @@ export class EmbeddedMpvPlayerComponent implements OnDestroy {
|
||||
onVolumeInput(event: Event): void {
|
||||
const next = Number((event.target as HTMLInputElement).value);
|
||||
this.applyVolume(next);
|
||||
this.revealControls(false);
|
||||
this.legacyInteractions.revealControls(false);
|
||||
}
|
||||
|
||||
onVolumeWheel(event: WheelEvent): void {
|
||||
@@ -659,16 +573,11 @@ export class EmbeddedMpvPlayerComponent implements OnDestroy {
|
||||
}
|
||||
|
||||
onVolumeHoverEnter(): void {
|
||||
this.clearVolumeCloseTimer();
|
||||
this.menus.open('volume');
|
||||
this.legacyInteractions.onVolumeHoverEnter();
|
||||
}
|
||||
|
||||
onVolumeHoverLeave(): void {
|
||||
this.clearVolumeCloseTimer();
|
||||
this.volumeCloseTimer = window.setTimeout(() => {
|
||||
this.menus.close('volume');
|
||||
this.volumeCloseTimer = null;
|
||||
}, VOLUME_POPOVER_CLOSE_DELAY_MS);
|
||||
this.legacyInteractions.onVolumeHoverLeave();
|
||||
}
|
||||
|
||||
toggleMute(): void {
|
||||
@@ -688,52 +597,52 @@ export class EmbeddedMpvPlayerComponent implements OnDestroy {
|
||||
`${Math.round(restored * 100)}%`
|
||||
);
|
||||
}
|
||||
this.revealControls();
|
||||
this.legacyInteractions.revealControls();
|
||||
}
|
||||
|
||||
toggleAudioMenu(): void {
|
||||
this.menus.toggle('audio');
|
||||
this.revealControls();
|
||||
this.legacyInteractions.revealControls();
|
||||
}
|
||||
toggleSubtitleMenu(): void {
|
||||
this.menus.toggle('subtitle');
|
||||
this.revealControls();
|
||||
this.legacyInteractions.revealControls();
|
||||
}
|
||||
toggleSpeedMenu(): void {
|
||||
this.menus.toggle('speed');
|
||||
this.revealControls();
|
||||
this.legacyInteractions.revealControls();
|
||||
}
|
||||
toggleAspectMenu(): void {
|
||||
this.menus.toggle('aspect');
|
||||
this.revealControls();
|
||||
this.legacyInteractions.revealControls();
|
||||
}
|
||||
|
||||
async selectAudioTrack(trackId: number): Promise<void> {
|
||||
this.revealControls(false);
|
||||
this.legacyInteractions.revealControls(false);
|
||||
await this.controller.setAudioTrack(trackId);
|
||||
this.menus.close('audio');
|
||||
this.scheduleControlsHide();
|
||||
this.legacyInteractions.scheduleControlsHide();
|
||||
}
|
||||
|
||||
async selectSubtitleTrack(trackId: number): Promise<void> {
|
||||
this.revealControls(false);
|
||||
this.legacyInteractions.revealControls(false);
|
||||
await this.controller.setSubtitleTrack(trackId);
|
||||
this.menus.close('subtitle');
|
||||
this.scheduleControlsHide();
|
||||
this.legacyInteractions.scheduleControlsHide();
|
||||
}
|
||||
|
||||
async selectSpeed(speed: number): Promise<void> {
|
||||
this.revealControls(false);
|
||||
this.legacyInteractions.revealControls(false);
|
||||
await this.controller.setSpeed(speed);
|
||||
this.menus.close('speed');
|
||||
this.scheduleControlsHide();
|
||||
this.legacyInteractions.scheduleControlsHide();
|
||||
}
|
||||
|
||||
async selectAspect(aspect: string): Promise<void> {
|
||||
this.revealControls(false);
|
||||
this.legacyInteractions.revealControls(false);
|
||||
await this.controller.setAspect(aspect);
|
||||
this.menus.close('aspect');
|
||||
this.scheduleControlsHide();
|
||||
this.legacyInteractions.scheduleControlsHide();
|
||||
}
|
||||
|
||||
async toggleRecording(): Promise<void> {
|
||||
@@ -741,7 +650,7 @@ export class EmbeddedMpvPlayerComponent implements OnDestroy {
|
||||
return;
|
||||
}
|
||||
|
||||
this.revealControls(false);
|
||||
this.legacyInteractions.revealControls(false);
|
||||
if (this.isRecording()) {
|
||||
const recording = await this.controller.stopRecording();
|
||||
if (recording?.targetPath) {
|
||||
@@ -771,7 +680,7 @@ export class EmbeddedMpvPlayerComponent implements OnDestroy {
|
||||
);
|
||||
this.flashRecordingFailed();
|
||||
}
|
||||
this.scheduleControlsHide();
|
||||
this.legacyInteractions.scheduleControlsHide();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -876,7 +785,7 @@ export class EmbeddedMpvPlayerComponent implements OnDestroy {
|
||||
private adjustVolume(delta: number): void {
|
||||
const next = Math.max(0, Math.min(1, this.volume() + delta));
|
||||
this.applyVolume(next);
|
||||
this.revealControls();
|
||||
this.legacyInteractions.revealControls();
|
||||
this.feedback.flash(volumeIcon(next), `${Math.round(next * 100)}%`);
|
||||
}
|
||||
|
||||
@@ -885,76 +794,6 @@ export class EmbeddedMpvPlayerComponent implements OnDestroy {
|
||||
persistVolume(value);
|
||||
void this.controller.applyVolume(value);
|
||||
}
|
||||
|
||||
private closePopovers(): void {
|
||||
if (!this.menus.anyOpen()) {
|
||||
return;
|
||||
}
|
||||
this.menus.closeAll();
|
||||
this.controller.triggerBoundsSync();
|
||||
this.scheduleControlsHide();
|
||||
}
|
||||
|
||||
private revealControls(scheduleHide = true): void {
|
||||
if (this.isFrameCopyEngine()) {
|
||||
this.clearControlsHideTimer();
|
||||
return;
|
||||
}
|
||||
this.controlsVisible.set(true);
|
||||
if (scheduleHide) {
|
||||
this.clearControlsHideTimer();
|
||||
this.scheduleControlsHide();
|
||||
}
|
||||
}
|
||||
|
||||
private scheduleControlsHide(): void {
|
||||
if (this.isFrameCopyEngine()) {
|
||||
this.clearControlsHideTimer();
|
||||
return;
|
||||
}
|
||||
if (
|
||||
!this.isPlaying() ||
|
||||
this.menus.anyOpen() ||
|
||||
Boolean(this.statusLabel())
|
||||
) {
|
||||
this.clearControlsHideTimer();
|
||||
return;
|
||||
}
|
||||
if (!this.controlsVisible() || this.controlsHideTimer !== null) {
|
||||
return;
|
||||
}
|
||||
this.controlsHideTimer = window.setTimeout(() => {
|
||||
if (
|
||||
this.isPlaying() &&
|
||||
!this.menus.anyOpen() &&
|
||||
!this.statusLabel()
|
||||
) {
|
||||
this.controlsVisible.set(false);
|
||||
}
|
||||
}, HIDE_CONTROLS_DELAY_MS);
|
||||
}
|
||||
|
||||
private clearControlsHideTimer(): void {
|
||||
if (this.controlsHideTimer === null) {
|
||||
return;
|
||||
}
|
||||
window.clearTimeout(this.controlsHideTimer);
|
||||
this.controlsHideTimer = null;
|
||||
}
|
||||
|
||||
private isPointerInsidePlayer(event: PointerEvent): boolean {
|
||||
const playerRoot = this.playerRoot()?.nativeElement;
|
||||
if (!playerRoot) {
|
||||
return false;
|
||||
}
|
||||
const rect = playerRoot.getBoundingClientRect();
|
||||
return (
|
||||
event.clientX >= rect.left &&
|
||||
event.clientX <= rect.right &&
|
||||
event.clientY >= rect.top &&
|
||||
event.clientY <= rect.bottom
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export type EmbeddedMpvAudioTrackView = EmbeddedMpvAudioTrack;
|
||||
Reference in new issue
Block a user