feat: 支持长按右键/D键二倍速播放与视频AB循环

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-11-04 02:00:05 +08:00
co-authored by Cursor
parent bcf1e6e349
commit 11d9d35002
8 changed files with 431 additions and 37 deletions
+77 -7
View File
@@ -1,7 +1,7 @@
import { useRouter } from "next/navigation";
import type { RefObject } from "react";
import { useEffect, useRef } from "react";
import type { LoopMode, Neighbors } from "../types.ts";
import type { AbLoop, LoopMode, Neighbors } from "../types.ts";
import { cleanupExpiredProgressStorage, releaseMediaElement } from "../utils";
interface UseVideoPlayerProps {
@@ -11,6 +11,7 @@ interface UseVideoPlayerProps {
volume: number;
rate: number;
loopMode: LoopMode;
abLoop?: AbLoop;
neighbors: Neighbors;
progressRestored: boolean;
setIsPlaying: (playing: boolean) => void;
@@ -25,6 +26,7 @@ export function useVideoPlayer({
volume,
rate,
loopMode,
abLoop,
neighbors,
progressRestored,
setIsPlaying,
@@ -34,6 +36,8 @@ export function useVideoPlayer({
const router = useRouter();
const restoredAwemeIdRef = useRef<string | null>(null);
const lastProgressRef = useRef(0);
const abLoopRef = useRef<AbLoop | undefined>(abLoop);
abLoopRef.current = abLoop;
// 挂载时清理过期的播放进度缓存
useEffect(() => {
@@ -128,37 +132,68 @@ export function useVideoPlayer({
};
}, [awemeId, enabled, videoRef]);
// 监听播放状态和进度
// 监听播放状态和进度(含 AB 循环区间控制)
useEffect(() => {
if (!enabled) return;
const v = videoRef.current;
if (!v) return;
const onTime = (): void => {
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", onTime);
v.addEventListener("loadedmetadata", onTime);
v.addEventListener("timeupdate", syncVideoProgress);
v.addEventListener("loadedmetadata", syncVideoProgress);
v.addEventListener("play", onPlay);
v.addEventListener("pause", onPause);
v.addEventListener("ended", onEnded);
return () => {
v.removeEventListener("timeupdate", onTime);
v.removeEventListener("loadedmetadata", onTime);
v.removeEventListener("timeupdate", syncVideoProgress);
v.removeEventListener("loadedmetadata", syncVideoProgress);
v.removeEventListener("play", onPlay);
v.removeEventListener("pause", onPause);
v.removeEventListener("ended", onEnded);
@@ -173,6 +208,41 @@ export function useVideoPlayer({
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;