diff --git a/src/app/play/page.tsx b/src/app/play/page.tsx index 17b689e..4e5224f 100644 --- a/src/app/play/page.tsx +++ b/src/app/play/page.tsx @@ -1271,6 +1271,14 @@ function PlayPageClient() { Map >(new Map()); + // 当前源的视频信息(用于标题旁边显示) + const [currentSourceVideoInfo, setCurrentSourceVideoInfo] = useState<{ + quality: string; + loadSpeed: string; + pingTime: number; + bitrate: string; + } | null>(null); + // 折叠状态(仅在 lg 及以上屏幕有效) const [isEpisodeSelectorCollapsed, setIsEpisodeSelectorCollapsed] = useState(false); @@ -1357,6 +1365,27 @@ function PlayPageClient() { return 'unknown'; }; + // 获取当前源的视频信息(分辨率和码率) + const fetchCurrentSourceVideoInfo = async () => { + if (!detail || !detail.episodes || detail.episodes.length === 0) { + return; + } + + // 获取当前集数的播放地址 + const episodeUrl = detail.episodes[currentEpisodeIndex]; + if (!episodeUrl) { + return; + } + + try { + const info = await getVideoResolutionFromM3u8(episodeUrl, 4000); + setCurrentSourceVideoInfo(info); + } catch (error) { + console.error('获取视频信息失败:', error); + setCurrentSourceVideoInfo(null); + } + }; + // 播放源优选函数 const preferBestSource = async ( sources: SearchResult[] @@ -3480,6 +3509,13 @@ function PlayPageClient() { } }, [searchParams, currentSource, currentId, availableSources, currentEpisodeIndex]); + // 监听 detail 和 currentEpisodeIndex 变化,自动获取视频信息 + useEffect(() => { + if (detail && detail.episodes && detail.episodes.length > 0) { + fetchCurrentSourceVideoInfo(); + } + }, [detail, currentEpisodeIndex]); + // 监听 detail 和 currentEpisodeIndex 变化,动态更新字幕 useEffect(() => { if (!artPlayerRef.current || !detail) return; @@ -8193,10 +8229,23 @@ function PlayPageClient() { {doubanYear || detail?.year || videoYear} )} {detail?.source_name && ( - + {detail.source_name} + {/* 视频信息悬浮提示 */} + {currentSourceVideoInfo && ( +
+
+
分辨率: {currentSourceVideoInfo.quality}
+
码率: {currentSourceVideoInfo.bitrate}
+
+
+
+ )}
)} {detail?.type_name && {detail.type_name}} diff --git a/src/components/EpisodeSelector.tsx b/src/components/EpisodeSelector.tsx index 73ef15a..799bebc 100644 --- a/src/components/EpisodeSelector.tsx +++ b/src/components/EpisodeSelector.tsx @@ -104,6 +104,10 @@ const EpisodeSelector: React.FC = ({ ); // 标记初始测速是否已完成 const [initialTestingCompleted, setInitialTestingCompleted] = useState(false); + // 标记是否正在进行全部重测 + const [isRetestingAll, setIsRetestingAll] = useState(false); + // 标记是否正在进行初始测速 + const [isInitialTesting, setIsInitialTesting] = useState(false); // 使用 ref 来避免闭包问题 const attemptedSourcesRef = useRef>(new Set()); @@ -248,6 +252,36 @@ const EpisodeSelector: React.FC = ({ } }, [speedTestTimeout]); + // 重测所有源的函数 + const retestAllSources = useCallback(async () => { + if (!availableSources || availableSources.length === 0) return; + + setIsRetestingAll(true); + + // 清空之前的测速结果 + setVideoInfoMap(new Map()); + setAttemptedSources(new Set()); + attemptedSourcesRef.current = new Set(); + videoInfoMapRef.current = new Map(); + + // 筛选需要测速的源(排除 openlist/emby/xiaoya) + const sourcesToTest = availableSources.filter((source) => { + if (source.source === 'openlist' || source.source === 'emby' || source.source.startsWith('emby_') || source.source === 'xiaoya') { + return false; + } + return true; + }); + + // 分批测速,每批最多5个 + const batchSize = 5; + for (let i = 0; i < sourcesToTest.length; i += batchSize) { + const batch = sourcesToTest.slice(i, i + batchSize); + await Promise.all(batch.map(source => getVideoInfo(source))); + } + + setIsRetestingAll(false); + }, [availableSources, getVideoInfo]); + // 当有预计算结果时,先合并到videoInfoMap中 useEffect(() => { if (precomputedVideoInfo && precomputedVideoInfo.size > 0) { @@ -301,6 +335,9 @@ const EpisodeSelector: React.FC = ({ if (pendingSources.length === 0) return; + // 标记开始初始测速 + setIsInitialTesting(true); + const batchSize = Math.ceil(pendingSources.length / 2); for (let start = 0; start < pendingSources.length; start += batchSize) { @@ -309,6 +346,7 @@ const EpisodeSelector: React.FC = ({ } // 初始测速完成后,标记为已完成 + setIsInitialTesting(false); if (!initialTestingCompleted) { setInitialTestingCompleted(true); } @@ -727,7 +765,24 @@ const EpisodeSelector: React.FC = ({ {/* 换源 Tab 内容 */} {activeTab === 'sources' && ( -
+
+ {/* 全部重测按钮 - 右上角 */} + {!sourceSearchLoading && !sourceSearchError && availableSources.length > 0 && ( +
+ +
+ )} + {sourceSearchLoading && (