增加弹幕预加载

This commit is contained in:
mtvpls
2026-01-13 14:45:22 +08:00
parent 89be4aab87
commit 93688bb8da
2 changed files with 175 additions and 0 deletions

View File

@@ -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<boolean>(false);
const nextEpisodePreCacheHlsRef = useRef<any>(null);
const nextEpisodeDanmakuPreloadTriggeredRef = useRef<boolean>(false);
const artPlayerRef = useRef<any>(null);
const artRef = useRef<HTMLDivElement | null>(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) {

View File

@@ -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 = () => {
</button>
{isDanmakuSectionOpen && (
<div className='p-3 md:p-4 space-y-4 md:space-y-6'>
{/* 下集弹幕预加载 */}
<div className='flex items-center justify-between'>
<div>
<h4 className='text-sm font-medium text-gray-700 dark:text-gray-300'>
</h4>
<p className='text-xs text-gray-500 dark:text-gray-400 mt-1'>
90%
</p>
</div>
<label className='flex items-center cursor-pointer'>
<div className='relative'>
<input
type='checkbox'
className='sr-only peer'
checked={nextEpisodeDanmakuPreload}
onChange={(e) => handleNextEpisodeDanmakuPreloadToggle(e.target.checked)}
/>
<div className='w-11 h-6 bg-gray-300 rounded-full peer-checked:bg-green-500 transition-colors dark:bg-gray-600'></div>
<div className='absolute top-0.5 left-0.5 w-5 h-5 bg-white rounded-full transition-transform peer-checked:translate-x-5'></div>
</div>
</label>
</div>
{/* 清除弹幕缓存 */}
<div className='space-y-3'>
<div>