Files
douyin-archive/app/aweme/[awemeId]/hooks/useVideoPlayer.ts
T

216 lines
5.9 KiB
TypeScript

import { useRouter } from "next/navigation";
import type { RefObject } from "react";
import { useEffect, useRef } from "react";
import type { LoopMode, Neighbors } from "../types.ts";
import { cleanupExpiredProgressStorage, releaseMediaElement } from "../utils";
interface UseVideoPlayerProps {
enabled?: boolean;
awemeId: string;
videoRef: RefObject<HTMLVideoElement | null>;
volume: number;
rate: number;
loopMode: LoopMode;
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,
neighbors,
progressRestored,
setIsPlaying,
setProgress,
setProgressRestored,
}: UseVideoPlayerProps): void {
const router = useRouter();
const restoredAwemeIdRef = useRef<string | null>(null);
const lastProgressRef = useRef(0);
// 挂载时清理过期的播放进度缓存
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]);
// 监听播放状态和进度
useEffect(() => {
if (!enabled) return;
const v = videoRef.current;
if (!v) return;
const onTime = (): void => {
if (!v.duration || Number.isNaN(v.duration)) return;
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 => {
if (loopMode === "sequential" && neighbors?.next) {
router.push(`/aweme/${neighbors.next.aweme_id}`);
}
};
v.addEventListener("timeupdate", onTime);
v.addEventListener("loadedmetadata", onTime);
v.addEventListener("play", onPlay);
v.addEventListener("pause", onPause);
v.addEventListener("ended", onEnded);
return () => {
v.removeEventListener("timeupdate", onTime);
v.removeEventListener("loadedmetadata", onTime);
v.removeEventListener("play", onPlay);
v.removeEventListener("pause", onPause);
v.removeEventListener("ended", onEnded);
};
}, [
enabled,
loopMode,
neighbors?.next,
router,
videoRef,
setIsPlaying,
setProgress,
]);
// 自动播放检测
useEffect(() => {
if (!enabled) return;
const v = videoRef.current;
if (!v) return;
const checkAutoplay = async (): Promise<void> => {
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]);
}