Files
NewTube/src/components/video-player/video-player.component.ts
T

138 lines
4.3 KiB
TypeScript

import { ChangeDetectionStrategy, Component, ElementRef, ViewChild, input, OnDestroy, AfterViewInit } from '@angular/core';
import { HistoryService } from '../../services/history.service';
@Component({
selector: 'app-video-player',
templateUrl: './video-player.component.html',
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class VideoPlayerComponent implements AfterViewInit, OnDestroy {
videoSource = input.required<string>();
// Optional resume position (in seconds)
startPositionSeconds = input<number | null>(null);
// Watch history id to update progress
watchHistoryId = input<string | null>(null);
@ViewChild('player', { static: false }) playerRef?: ElementRef<HTMLVideoElement>;
get el(): HTMLVideoElement | null {
return this.playerRef?.nativeElement ?? null;
}
private progressTimer: any = null;
private lastSent = 0;
constructor(private history: HistoryService) {}
ngAfterViewInit(): void {
const v = this.el; if (!v) return;
// Seek to saved position when metadata is ready
v.addEventListener('loadedmetadata', () => {
const pos = this.startPositionSeconds();
if (typeof pos === 'number' && pos > 0 && isFinite(pos)) {
try { v.currentTime = Math.min(Math.max(pos, 0), v.duration || pos); } catch {}
}
});
// Start periodic progress updates when playing
v.addEventListener('play', () => {
// Ensure we seek on first play if metadata event already fired
const pos = this.startPositionSeconds();
if (typeof pos === 'number' && pos > 0 && isFinite(pos)) {
try {
if (!isNaN(v.currentTime) && v.currentTime < Math.max(0, pos - 1)) v.currentTime = pos;
} catch {}
}
this.startProgressLoop();
});
v.addEventListener('pause', () => this.sendProgressOnce());
v.addEventListener('ended', () => { this.sendProgressOnce(true); this.stopProgressLoop(); });
// As a safeguard, send progress on visibility change/unload
document.addEventListener('visibilitychange', this.onVisibilityChange);
window.addEventListener('beforeunload', this.onBeforeUnload);
}
ngOnDestroy(): void {
this.stopProgressLoop();
document.removeEventListener('visibilitychange', this.onVisibilityChange);
window.removeEventListener('beforeunload', this.onBeforeUnload);
}
private onVisibilityChange = () => {
if (document.hidden) this.sendProgressOnce();
};
private onBeforeUnload = () => {
this.sendProgressOnce();
};
private startProgressLoop(): void {
this.stopProgressLoop();
this.progressTimer = setInterval(() => this.sendProgressDebounced(), 4000);
}
private stopProgressLoop(): void {
if (this.progressTimer) {
clearInterval(this.progressTimer);
this.progressTimer = null;
}
}
private currentPos(): number {
const v = this.el; if (!v) return 0;
try { return Math.max(0, Math.floor(v.currentTime || 0)); } catch { return 0; }
}
private sendProgressDebounced(): void {
const now = Date.now();
if (now - this.lastSent < 3500) return;
this.lastSent = now;
this.sendProgress(false);
}
private sendProgressOnce(final: boolean = false): void {
this.sendProgress(final);
}
private sendProgress(final: boolean): void {
const id = this.watchHistoryId();
if (!id) return;
const pos = this.currentPos();
this.history.updateWatchProgress(id, pos, pos).subscribe({ next: () => {}, error: () => {} });
if (final) this.lastSent = Date.now();
}
togglePlayPause(): void {
const v = this.el; if (!v) return;
if (v.paused) v.play(); else v.pause();
}
seekBy(seconds: number): void {
const v = this.el; if (!v) return;
try { v.currentTime = Math.max(0, v.currentTime + seconds); } catch {}
}
toggleMute(): void {
const v = this.el; if (!v) return;
v.muted = !v.muted;
}
setPlaybackRate(delta: number): void {
const v = this.el; if (!v) return;
const next = Math.min(2.0, Math.max(0.25, (v.playbackRate || 1) + delta));
v.playbackRate = next;
}
async toggleFullscreen(): Promise<void> {
const v = this.el; if (!v) return;
const anyDoc: any = document;
if (!anyDoc.fullscreenElement) {
await v.requestFullscreen?.();
} else {
await anyDoc.exitFullscreen?.();
}
}
}