diff --git a/src/app/play/page.tsx b/src/app/play/page.tsx index 440d170..b103b79 100644 --- a/src/app/play/page.tsx +++ b/src/app/play/page.tsx @@ -567,6 +567,7 @@ function PlayPageClient() { // 当集数改变时,重置下集预缓存标记 useEffect(() => { nextEpisodePreCacheTriggeredRef.current = false; + nextEpisodeDanmakuPreloadTriggeredRef.current = false; // 清理之前的预缓存 HLS 实例 if (nextEpisodePreCacheHlsRef.current) { try { @@ -1180,6 +1181,7 @@ function PlayPageClient() { // 下集预缓存相关 const nextEpisodePreCacheTriggeredRef = useRef(false); const nextEpisodePreCacheHlsRef = useRef(null); + const nextEpisodeDanmakuPreloadTriggeredRef = useRef(false); const artPlayerRef = useRef(null); const artRef = useRef(null); @@ -3375,6 +3377,120 @@ function PlayPageClient() { } }; + // 预加载下一集弹幕(完全复制 loadDanmakuForCurrentEpisode 的逻辑) + const preloadNextEpisodeDanmaku = async () => { + try { + const title = videoTitleRef.current; + if (!title) { + return; + } + + const currentIdx = currentEpisodeIndexRef.current; + const nextEpisodeIndex = currentIdx + 1; + + // 1. 检查是否有下一集 + const episodes = detailRef.current?.episodes; + if (!episodes || nextEpisodeIndex >= episodes.length) { + return; + } + + // 2. 检查缓存是否已存在 + const cachedData = await getDanmakuFromCache(title, nextEpisodeIndex); + if (cachedData && cachedData.comments.length > 0) { + return; + } + + // 3. 检查是否有手动选择的剧集 ID + const manualEpisodeId = getManualDanmakuSelection(title, nextEpisodeIndex); + if (manualEpisodeId) { + try { + await getDanmakuById(manualEpisodeId, title, nextEpisodeIndex); + return; + } catch (error) { + // 继续执行后续逻辑 + } + } + + // 4. 尝试使用保存的动漫ID自动匹配剧集 + const savedAnimeId = getDanmakuAnimeId(title); + if (savedAnimeId) { + try { + const episodesResult = await getEpisodes(savedAnimeId); + + if (episodesResult.success && episodesResult.bangumi.episodes.length > 0) { + const nextVideoEpTitle = detailRef.current?.episodes_titles?.[nextEpisodeIndex]; + const episode = matchDanmakuEpisode(nextEpisodeIndex, episodesResult.bangumi.episodes, nextVideoEpTitle); + + if (episode) { + await getDanmakuById( + episode.episodeId, + title, + nextEpisodeIndex, + { + animeId: savedAnimeId, + animeTitle: episodesResult.bangumi.animeTitle, + episodeTitle: episode.episodeTitle, + } + ); + return; + } + } + } catch (error) { + // 继续执行后续逻辑 + } + } + + // 5. 执行自动搜索弹幕 + const savedKeyword = getDanmakuSearchKeyword(title); + const searchKeyword = savedKeyword || title; + + const searchResult = await searchAnime(searchKeyword); + if (!searchResult.success || searchResult.animes.length === 0) { + return; + } + + // 应用智能过滤 + const videoYear = detailRef.current?.year; + const filteredAnimes = filterDanmakuSources(searchResult.animes, title, videoYear); + + if (filteredAnimes.length === 0) { + return; + } + + // 检查是否有记忆的选择 + let selectedAnime = filteredAnimes[0]; + if (filteredAnimes.length > 1) { + const rememberedIndex = getDanmakuSourceIndex(title); + if (rememberedIndex !== null && rememberedIndex < filteredAnimes.length) { + selectedAnime = filteredAnimes[rememberedIndex]; + } + } + + // 获取剧集列表并匹配 + const episodesResult = await getEpisodes(selectedAnime.animeId); + if (episodesResult.success && episodesResult.bangumi.episodes.length > 0) { + const nextVideoEpTitle = detailRef.current?.episodes_titles?.[nextEpisodeIndex]; + const episode = matchDanmakuEpisode(nextEpisodeIndex, episodesResult.bangumi.episodes, nextVideoEpTitle); + + if (episode) { + await getDanmakuById( + episode.episodeId, + title, + nextEpisodeIndex, + { + animeId: selectedAnime.animeId, + animeTitle: selectedAnime.animeTitle, + episodeTitle: episode.episodeTitle, + searchKeyword: searchKeyword, + } + ); + } + } + } catch (error) { + // 静默处理失败 + } + }; + // 处理上传弹幕 const handleUploadDanmaku = async (comments: DanmakuComment[]) => { setDanmakuLoading(true); @@ -6054,6 +6170,26 @@ function PlayPageClient() { preloadNextEpisode(); } } + + // 下集弹幕预加载逻辑 + const nextEpisodeDanmakuPreloadEnabled = typeof window !== 'undefined' + ? localStorage.getItem('nextEpisodeDanmakuPreload') === 'true' + : false; + + if (nextEpisodeDanmakuPreloadEnabled) { + const currentTime = artPlayerRef.current?.currentTime || 0; + const duration = artPlayerRef.current?.duration || 0; + const progress = duration > 0 ? currentTime / duration : 0; + + // 检查是否已经到达90%播放进度 + if (duration > 0 && progress >= 0.9 && !nextEpisodeDanmakuPreloadTriggeredRef.current) { + // 标记已触发,防止重复执行 + nextEpisodeDanmakuPreloadTriggeredRef.current = true; + + // 异步执行弹幕预加载 + preloadNextEpisodeDanmaku(); + } + } }); if (artPlayerRef.current?.video) { diff --git a/src/components/UserMenu.tsx b/src/components/UserMenu.tsx index 1f7f507..6b24e1c 100644 --- a/src/components/UserMenu.tsx +++ b/src/components/UserMenu.tsx @@ -105,6 +105,7 @@ export const UserMenu: React.FC = () => { useState(false); const [bufferStrategy, setBufferStrategy] = useState('medium'); const [nextEpisodePreCache, setNextEpisodePreCache] = useState(true); + const [nextEpisodeDanmakuPreload, setNextEpisodeDanmakuPreload] = useState(true); const [isBufferStrategyDropdownOpen, setIsBufferStrategyDropdownOpen] = useState(false); const [searchTraditionalToSimplified, setSearchTraditionalToSimplified] = useState(false); @@ -370,6 +371,11 @@ export const UserMenu: React.FC = () => { setNextEpisodePreCache(savedNextEpisodePreCache === 'true'); } + const savedNextEpisodeDanmakuPreload = localStorage.getItem('nextEpisodeDanmakuPreload'); + if (savedNextEpisodeDanmakuPreload !== null) { + setNextEpisodeDanmakuPreload(savedNextEpisodeDanmakuPreload === 'true'); + } + // 加载首页模块配置 const savedHomeModules = localStorage.getItem('homeModules'); if (savedHomeModules !== null) { @@ -648,6 +654,13 @@ export const UserMenu: React.FC = () => { } }; + const handleNextEpisodeDanmakuPreloadToggle = (value: boolean) => { + setNextEpisodeDanmakuPreload(value); + if (typeof window !== 'undefined') { + localStorage.setItem('nextEpisodeDanmakuPreload', String(value)); + } + }; + const handleSearchTraditionalToSimplifiedToggle = (value: boolean) => { setSearchTraditionalToSimplified(value); if (typeof window !== 'undefined') { @@ -745,6 +758,7 @@ export const UserMenu: React.FC = () => { setDoubanImageProxyUrl(defaultDoubanImageProxyUrl); setBufferStrategy('medium'); setNextEpisodePreCache(true); + setNextEpisodeDanmakuPreload(true); setHomeModules(defaultHomeModules); setSearchTraditionalToSimplified(false); @@ -761,6 +775,7 @@ export const UserMenu: React.FC = () => { localStorage.setItem('doubanImageProxyUrl', defaultDoubanImageProxyUrl); localStorage.setItem('bufferStrategy', 'medium'); localStorage.setItem('nextEpisodePreCache', 'true'); + localStorage.setItem('nextEpisodeDanmakuPreload', 'true'); localStorage.setItem('homeModules', JSON.stringify(defaultHomeModules)); localStorage.setItem('searchTraditionalToSimplified', 'false'); window.dispatchEvent(new CustomEvent('homeModulesUpdated')); @@ -1593,6 +1608,30 @@ export const UserMenu: React.FC = () => { {isDanmakuSectionOpen && (
+ {/* 下集弹幕预加载 */} +
+
+

+ 下集弹幕预加载 +

+

+ 播放进度达到90%时,自动预加载下一集弹幕 +

+
+ +
+ {/* 清除弹幕缓存 */}