import { useRouter } from "next/navigation"; import type { RefObject } from "react"; import { useEffect, useRef } from "react"; import type { AbLoop, LoopMode, Neighbors } from "../types.ts"; import { cleanupExpiredProgressStorage, releaseMediaElement } from "../utils"; interface UseVideoPlayerProps { enabled?: boolean; awemeId: string; videoRef: RefObject; volume: number; rate: number; loopMode: LoopMode; abLoop?: AbLoop; neighbors: Neighbors; progressRestored: boolean; setIsPlaying: (playing: boolean) => void; setProgress: (progress: number) => void; setProgressRestored: (restored: boolean) => void; } export function useVideoPlayer({ enabled = true, awemeId, videoRef, volume, rate, loopMode, abLoop, neighbors, progressRestored, setIsPlaying, setProgress, setProgressRestored, }: UseVideoPlayerProps): void { const router = useRouter(); const restoredAwemeIdRef = useRef(null); const lastProgressRef = useRef(0); const abLoopRef = useRef(abLoop); abLoopRef.current = abLoop; // 挂载时清理过期的播放进度缓存 useEffect(() => { if (!enabled) return; cleanupExpiredProgressStorage(); }, [enabled]); // 恢复播放进度 useEffect(() => { if (!enabled) return; if (progressRestored && restoredAwemeIdRef.current === awemeId) return; const v = videoRef.current; if (!v) return; const onLoadedMetadata = (): void => { if (restoredAwemeIdRef.current === awemeId) return; restoredAwemeIdRef.current = awemeId; try { const key = `aweme_progress_${awemeId}`; const saved = localStorage.getItem(key); if (!saved) { setProgressRestored(true); return; } const parsed = JSON.parse(saved) as { time?: unknown; timestamp?: unknown; }; const time = typeof parsed.time === "number" ? parsed.time : 0; const timestamp = typeof parsed.timestamp === "number" ? parsed.timestamp : 0; const now = Date.now(); const fiveMinutes = 5 * 60 * 1000; if ( now - timestamp < fiveMinutes && time > 1 && time < v.duration - 1 ) { v.currentTime = time; } else if (now - timestamp >= fiveMinutes) { localStorage.removeItem(key); } } catch (e) { console.error("恢复播放进度失败", e); } setProgressRestored(true); }; if (v.readyState >= 1) { onLoadedMetadata(); return; } v.addEventListener("loadedmetadata", onLoadedMetadata, { once: true }); return () => v.removeEventListener("loadedmetadata", onLoadedMetadata); }, [awemeId, enabled, progressRestored, videoRef, setProgressRestored]); // 保存播放进度并在卸载/切页时主动释放媒体解码缓冲区 useEffect(() => { if (!enabled) return; const v = videoRef.current; if (!v) return; const saveProgress = (): void => { if (!v.duration || Number.isNaN(v.duration) || v.currentTime < 1) return; try { const key = `aweme_progress_${awemeId}`; const value = JSON.stringify({ time: v.currentTime, timestamp: Date.now(), }); localStorage.setItem(key, value); } catch (e) { console.error("保存播放进度失败", e); } }; const interval = window.setInterval(saveProgress, 2000); const onBeforeUnload = (): void => saveProgress(); window.addEventListener("beforeunload", onBeforeUnload); return () => { window.clearInterval(interval); window.removeEventListener("beforeunload", onBeforeUnload); saveProgress(); releaseMediaElement(v); }; }, [awemeId, enabled, videoRef]); // 监听播放状态和进度(含 AB 循环区间控制) useEffect(() => { if (!enabled) return; const v = videoRef.current; if (!v) return; const syncVideoProgress = (): void => { if (!v.duration || Number.isNaN(v.duration)) return; const currentAb = abLoopRef.current; const hasAb = Boolean( currentAb && (currentAb.a !== null || currentAb.b !== null), ); if (hasAb && currentAb) { const startTime = Math.max(0, currentAb.a ?? 0); const endTime = currentAb.b !== null ? Math.min(v.duration, currentAb.b) : v.duration; if ( endTime > startTime && (v.currentTime >= endTime || (currentAb.b === null && v.currentTime >= v.duration - 0.03)) ) { v.currentTime = startTime; if (v.paused) { void v.play().catch(() => {}); } } } const nextProgress = Math.max(0, Math.min(1, v.currentTime / v.duration)); if (Math.abs(nextProgress - lastProgressRef.current) >= 0.001) { lastProgressRef.current = nextProgress; setProgress(nextProgress); } }; const onPlay = (): void => setIsPlaying(true); const onPause = (): void => setIsPlaying(false); const onEnded = (): void => { const currentAb = abLoopRef.current; if (currentAb && (currentAb.a !== null || currentAb.b !== null)) { const startTime = Math.max(0, currentAb.a ?? 0); v.currentTime = startTime; void v.play().catch(() => {}); return; } if (loopMode === "sequential" && neighbors?.next) { router.push(`/aweme/${neighbors.next.aweme_id}`); } }; v.addEventListener("timeupdate", syncVideoProgress); v.addEventListener("loadedmetadata", syncVideoProgress); v.addEventListener("play", onPlay); v.addEventListener("pause", onPause); v.addEventListener("ended", onEnded); return () => { v.removeEventListener("timeupdate", syncVideoProgress); v.removeEventListener("loadedmetadata", syncVideoProgress); v.removeEventListener("play", onPlay); v.removeEventListener("pause", onPause); v.removeEventListener("ended", onEnded); }; }, [ enabled, loopMode, neighbors?.next, router, videoRef, setIsPlaying, setProgress, ]); // 当启用 AB 循环时,使用 rAF 精确检测终点 B 并无缝跳回起点 A useEffect(() => { if (!enabled) return; if (!abLoop || (abLoop.a === null && abLoop.b === null)) return; let rafId: number | null = null; const checkAbLoop = (): void => { const v = videoRef.current; if (v && !v.paused && Number.isFinite(v.duration) && v.duration > 0) { const startTime = Math.max(0, abLoop.a ?? 0); const endTime = abLoop.b !== null ? Math.min(v.duration, abLoop.b) : v.duration; if ( endTime > startTime && (v.currentTime >= endTime || (abLoop.b === null && v.currentTime >= v.duration - 0.03)) ) { v.currentTime = startTime; const nextProgress = Math.max(0, Math.min(1, startTime / v.duration)); lastProgressRef.current = nextProgress; setProgress(nextProgress); } } rafId = window.requestAnimationFrame(checkAbLoop); }; rafId = window.requestAnimationFrame(checkAbLoop); return () => { if (rafId !== null) { window.cancelAnimationFrame(rafId); } }; }, [abLoop, enabled, setProgress, videoRef]); // 自动播放检测 useEffect(() => { if (!enabled) return; const v = videoRef.current; if (!v) return; const checkAutoplay = async (): Promise => { try { await v.play(); setIsPlaying(true); } catch { setIsPlaying(false); } }; if (v.readyState >= 1) { void checkAutoplay(); return; } const onLoaded = (): void => { void checkAutoplay(); }; v.addEventListener("loadedmetadata", onLoaded, { once: true }); return () => v.removeEventListener("loadedmetadata", onLoaded); }, [awemeId, enabled, videoRef, setIsPlaying]); // 更新音量和倍速 useEffect(() => { if (!enabled) return; const v = videoRef.current; if (v) v.volume = volume; }, [enabled, volume, videoRef]); useEffect(() => { if (!enabled) return; const v = videoRef.current; if (v) v.playbackRate = rate; }, [enabled, rate, videoRef]); }