This commit is contained in:
2026-02-24 21:18:30 +08:00
parent 559690dc56
commit d71c79c87a
26 changed files with 859 additions and 1316 deletions

View File

@@ -1,110 +1,85 @@
"use client";
import { useCallback, useEffect, useRef } from "react";
import { SubtitleEntry } from "../types/subtitle";
import { useEffect, useRef } from "react";
import { useSrtPlayerStore } from "../store";
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);
/**
* useSubtitleSync - 字幕同步 Hook
*
* 自动同步视频播放时间与字幕显示,支持自动暂停功能。
* 使用 Zustand store 获取状态,无需传入参数。
*/
export function useSubtitleSync() {
const lastSubtitleRef = useRef<number | 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]);
// 从 store 获取状态
const subtitleData = useSrtPlayerStore((state) => state.subtitle.data);
const currentTime = useSrtPlayerStore((state) => state.video.currentTime);
const isPlaying = useSrtPlayerStore((state) => state.video.isPlaying);
const autoPause = useSrtPlayerStore((state) => state.controls.autoPause);
// 获取最近的字幕索引
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];
// Store actions
const setCurrentSubtitle = useSrtPlayerStore((state) => state.setCurrentSubtitle);
const seek = useSrtPlayerStore((state) => state.seek);
const pause = useSrtPlayerStore((state) => state.pause);
// 获取当前时间对应的字幕索引
const getCurrentSubtitleIndex = (time: number): number | null => {
for (let i = 0; i < subtitleData.length; i++) {
const subtitle = subtitleData[i];
if (time >= subtitle.start && time <= subtitle.end) {
return mid;
} else if (time < subtitle.start) {
right = mid - 1;
} else {
left = mid + 1;
return i;
}
}
// 如果没有找到完全匹配的字幕,返回最近的字幕索引
return right >= 0 ? right : null;
}, [subtitles]);
return null;
};
// 检查是否需要自动暂停
const shouldAutoPause = useCallback((subtitle: SubtitleEntry, time: number): boolean => {
return autoPause &&
time >= subtitle.end - 0.2 && // 增加时间窗口,确保自动暂停更可靠
time < subtitle.end;
}, [autoPause]);
const shouldAutoPause = (subtitle: { start: number; end: number }, time: number): boolean => {
return autoPause && time >= subtitle.end - 0.2 && time < subtitle.end;
};
// 启动/停止同步循环
// 同步循环
useEffect(() => {
const syncSubtitles = () => {
const currentSubtitle = getCurrentSubtitle(currentTime);
const currentIndex = getCurrentSubtitleIndex(currentTime);
// 检查字幕是否发生变化
if (currentSubtitle !== lastSubtitleRef.current) {
const previousSubtitle = lastSubtitleRef.current;
lastSubtitleRef.current = currentSubtitle;
// 只有当有当前字幕时才调用onSubtitleChange
// 在字幕间隙时保持之前的字幕索引避免进度条跳到0
if (currentSubtitle) {
onSubtitleChange(currentSubtitle);
if (currentIndex !== lastSubtitleRef.current) {
lastSubtitleRef.current = currentIndex;
if (currentIndex !== null) {
const subtitle = subtitleData[currentIndex];
setCurrentSubtitle(subtitle.text, currentIndex);
} else {
setCurrentSubtitle('', null);
}
}
// 检查是否需要自动暂停
// 每次都检查,不只在字幕变化时检查
const currentSubtitle = currentIndex !== null ? subtitleData[currentIndex] : null;
if (currentSubtitle && shouldAutoPause(currentSubtitle, currentTime)) {
onAutoPauseTrigger?.(currentSubtitle);
} else if (!currentSubtitle && lastSubtitleRef.current && shouldAutoPause(lastSubtitleRef.current, currentTime)) {
// 在字幕结束时,如果前一个字幕需要自动暂停,也要触发
onAutoPauseTrigger?.(lastSubtitleRef.current);
seek(currentSubtitle.start);
pause();
}
rafIdRef.current = requestAnimationFrame(syncSubtitles);
};
if (subtitles.length > 0) {
if (subtitleData.length > 0 && isPlaying) {
rafIdRef.current = requestAnimationFrame(syncSubtitles);
}
return () => {
if (rafIdRef.current) {
cancelAnimationFrame(rafIdRef.current);
}
};
}, [subtitles.length, currentTime, getCurrentSubtitle, onSubtitleChange, shouldAutoPause, onAutoPauseTrigger]);
}, [subtitleData, currentTime, isPlaying, autoPause, setCurrentSubtitle, seek, pause]);
// 重置最后字幕引用
useEffect(() => {
lastSubtitleRef.current = null;
}, [subtitles]);
return {
getCurrentSubtitle,
getNearestIndex,
shouldAutoPause,
};
}
}, [subtitleData]);
}