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(); // Optional resume position (in seconds) startPositionSeconds = input(null); // Watch history id to update progress watchHistoryId = input(null); @ViewChild('player', { static: false }) playerRef?: ElementRef; 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 { const v = this.el; if (!v) return; const anyDoc: any = document; if (!anyDoc.fullscreenElement) { await v.requestFullscreen?.(); } else { await anyDoc.exitFullscreen?.(); } } }