重构逐句视频播放器
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
2025-12-12 13:37:00 +08:00
parent b69e168558
commit 605c57f8bb
25 changed files with 1667 additions and 24 deletions

View File

@@ -0,0 +1,110 @@
"use client";
import { useCallback, useEffect, useRef } from "react";
import { SubtitleEntry } from "../types/subtitle";
export function useSubtitleSync(
subtitles: SubtitleEntry[],
currentTime: number,
isPlaying: boolean,
autoPause: boolean,
onSubtitleChange: (subtitle: SubtitleEntry | null) => void,
onAutoPauseTrigger?: (subtitle: SubtitleEntry) => void
) {
const lastSubtitleRef = useRef<SubtitleEntry | null>(null);
const rafIdRef = useRef<number>(0);
// 获取当前时间对应的字幕
const getCurrentSubtitle = useCallback((time: number): SubtitleEntry | null => {
return subtitles.find(subtitle => time >= subtitle.start && time <= subtitle.end) || null;
}, [subtitles]);
// 获取最近的字幕索引
const getNearestIndex = useCallback((time: number): number | null => {
if (subtitles.length === 0) return null;
// 如果时间早于第一个字幕开始时间
if (time < subtitles[0].start) return null;
// 如果时间晚于最后一个字幕结束时间
if (time > subtitles[subtitles.length - 1].end) return subtitles.length - 1;
// 二分查找找到当前时间对应的字幕
let left = 0;
let right = subtitles.length - 1;
while (left <= right) {
const mid = Math.floor((left + right) / 2);
const subtitle = subtitles[mid];
if (time >= subtitle.start && time <= subtitle.end) {
return mid;
} else if (time < subtitle.start) {
right = mid - 1;
} else {
left = mid + 1;
}
}
// 如果没有找到完全匹配的字幕,返回最近的字幕索引
return right >= 0 ? right : null;
}, [subtitles]);
// 检查是否需要自动暂停
const shouldAutoPause = useCallback((subtitle: SubtitleEntry, time: number): boolean => {
return autoPause &&
time >= subtitle.end - 0.2 && // 增加时间窗口,确保自动暂停更可靠
time < subtitle.end;
}, [autoPause]);
// 启动/停止同步循环
useEffect(() => {
const syncSubtitles = () => {
const currentSubtitle = getCurrentSubtitle(currentTime);
// 检查字幕是否发生变化
if (currentSubtitle !== lastSubtitleRef.current) {
const previousSubtitle = lastSubtitleRef.current;
lastSubtitleRef.current = currentSubtitle;
// 只有当有当前字幕时才调用onSubtitleChange
// 在字幕间隙时保持之前的字幕索引避免进度条跳到0
if (currentSubtitle) {
onSubtitleChange(currentSubtitle);
}
}
// 检查是否需要自动暂停
// 每次都检查,不只在字幕变化时检查
if (currentSubtitle && shouldAutoPause(currentSubtitle, currentTime)) {
onAutoPauseTrigger?.(currentSubtitle);
} else if (!currentSubtitle && lastSubtitleRef.current && shouldAutoPause(lastSubtitleRef.current, currentTime)) {
// 在字幕结束时,如果前一个字幕需要自动暂停,也要触发
onAutoPauseTrigger?.(lastSubtitleRef.current);
}
rafIdRef.current = requestAnimationFrame(syncSubtitles);
};
if (subtitles.length > 0) {
rafIdRef.current = requestAnimationFrame(syncSubtitles);
}
return () => {
if (rafIdRef.current) {
cancelAnimationFrame(rafIdRef.current);
}
};
}, [subtitles.length, currentTime, getCurrentSubtitle, onSubtitleChange, shouldAutoPause, onAutoPauseTrigger]);
// 重置最后字幕引用
useEffect(() => {
lastSubtitleRef.current = null;
}, [subtitles]);
return {
getCurrentSubtitle,
getNearestIndex,
shouldAutoPause,
};
}