增加弹幕预加载
This commit is contained in:
@@ -567,6 +567,7 @@ function PlayPageClient() {
|
|||||||
// 当集数改变时,重置下集预缓存标记
|
// 当集数改变时,重置下集预缓存标记
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
nextEpisodePreCacheTriggeredRef.current = false;
|
nextEpisodePreCacheTriggeredRef.current = false;
|
||||||
|
nextEpisodeDanmakuPreloadTriggeredRef.current = false;
|
||||||
// 清理之前的预缓存 HLS 实例
|
// 清理之前的预缓存 HLS 实例
|
||||||
if (nextEpisodePreCacheHlsRef.current) {
|
if (nextEpisodePreCacheHlsRef.current) {
|
||||||
try {
|
try {
|
||||||
@@ -1180,6 +1181,7 @@ function PlayPageClient() {
|
|||||||
// 下集预缓存相关
|
// 下集预缓存相关
|
||||||
const nextEpisodePreCacheTriggeredRef = useRef<boolean>(false);
|
const nextEpisodePreCacheTriggeredRef = useRef<boolean>(false);
|
||||||
const nextEpisodePreCacheHlsRef = useRef<any>(null);
|
const nextEpisodePreCacheHlsRef = useRef<any>(null);
|
||||||
|
const nextEpisodeDanmakuPreloadTriggeredRef = useRef<boolean>(false);
|
||||||
|
|
||||||
const artPlayerRef = useRef<any>(null);
|
const artPlayerRef = useRef<any>(null);
|
||||||
const artRef = useRef<HTMLDivElement | null>(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[]) => {
|
const handleUploadDanmaku = async (comments: DanmakuComment[]) => {
|
||||||
setDanmakuLoading(true);
|
setDanmakuLoading(true);
|
||||||
@@ -6054,6 +6170,26 @@ function PlayPageClient() {
|
|||||||
preloadNextEpisode();
|
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) {
|
if (artPlayerRef.current?.video) {
|
||||||
|
|||||||
@@ -105,6 +105,7 @@ export const UserMenu: React.FC = () => {
|
|||||||
useState(false);
|
useState(false);
|
||||||
const [bufferStrategy, setBufferStrategy] = useState('medium');
|
const [bufferStrategy, setBufferStrategy] = useState('medium');
|
||||||
const [nextEpisodePreCache, setNextEpisodePreCache] = useState(true);
|
const [nextEpisodePreCache, setNextEpisodePreCache] = useState(true);
|
||||||
|
const [nextEpisodeDanmakuPreload, setNextEpisodeDanmakuPreload] = useState(true);
|
||||||
const [isBufferStrategyDropdownOpen, setIsBufferStrategyDropdownOpen] = useState(false);
|
const [isBufferStrategyDropdownOpen, setIsBufferStrategyDropdownOpen] = useState(false);
|
||||||
const [searchTraditionalToSimplified, setSearchTraditionalToSimplified] = useState(false);
|
const [searchTraditionalToSimplified, setSearchTraditionalToSimplified] = useState(false);
|
||||||
|
|
||||||
@@ -370,6 +371,11 @@ export const UserMenu: React.FC = () => {
|
|||||||
setNextEpisodePreCache(savedNextEpisodePreCache === 'true');
|
setNextEpisodePreCache(savedNextEpisodePreCache === 'true');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const savedNextEpisodeDanmakuPreload = localStorage.getItem('nextEpisodeDanmakuPreload');
|
||||||
|
if (savedNextEpisodeDanmakuPreload !== null) {
|
||||||
|
setNextEpisodeDanmakuPreload(savedNextEpisodeDanmakuPreload === 'true');
|
||||||
|
}
|
||||||
|
|
||||||
// 加载首页模块配置
|
// 加载首页模块配置
|
||||||
const savedHomeModules = localStorage.getItem('homeModules');
|
const savedHomeModules = localStorage.getItem('homeModules');
|
||||||
if (savedHomeModules !== null) {
|
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) => {
|
const handleSearchTraditionalToSimplifiedToggle = (value: boolean) => {
|
||||||
setSearchTraditionalToSimplified(value);
|
setSearchTraditionalToSimplified(value);
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== 'undefined') {
|
||||||
@@ -745,6 +758,7 @@ export const UserMenu: React.FC = () => {
|
|||||||
setDoubanImageProxyUrl(defaultDoubanImageProxyUrl);
|
setDoubanImageProxyUrl(defaultDoubanImageProxyUrl);
|
||||||
setBufferStrategy('medium');
|
setBufferStrategy('medium');
|
||||||
setNextEpisodePreCache(true);
|
setNextEpisodePreCache(true);
|
||||||
|
setNextEpisodeDanmakuPreload(true);
|
||||||
setHomeModules(defaultHomeModules);
|
setHomeModules(defaultHomeModules);
|
||||||
setSearchTraditionalToSimplified(false);
|
setSearchTraditionalToSimplified(false);
|
||||||
|
|
||||||
@@ -761,6 +775,7 @@ export const UserMenu: React.FC = () => {
|
|||||||
localStorage.setItem('doubanImageProxyUrl', defaultDoubanImageProxyUrl);
|
localStorage.setItem('doubanImageProxyUrl', defaultDoubanImageProxyUrl);
|
||||||
localStorage.setItem('bufferStrategy', 'medium');
|
localStorage.setItem('bufferStrategy', 'medium');
|
||||||
localStorage.setItem('nextEpisodePreCache', 'true');
|
localStorage.setItem('nextEpisodePreCache', 'true');
|
||||||
|
localStorage.setItem('nextEpisodeDanmakuPreload', 'true');
|
||||||
localStorage.setItem('homeModules', JSON.stringify(defaultHomeModules));
|
localStorage.setItem('homeModules', JSON.stringify(defaultHomeModules));
|
||||||
localStorage.setItem('searchTraditionalToSimplified', 'false');
|
localStorage.setItem('searchTraditionalToSimplified', 'false');
|
||||||
window.dispatchEvent(new CustomEvent('homeModulesUpdated'));
|
window.dispatchEvent(new CustomEvent('homeModulesUpdated'));
|
||||||
@@ -1593,6 +1608,30 @@ export const UserMenu: React.FC = () => {
|
|||||||
</button>
|
</button>
|
||||||
{isDanmakuSectionOpen && (
|
{isDanmakuSectionOpen && (
|
||||||
<div className='p-3 md:p-4 space-y-4 md:space-y-6'>
|
<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 className='space-y-3'>
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
Reference in New Issue
Block a user