From 4437a4c46168264185124e1cbf0a4181e517d6bc Mon Sep 17 00:00:00 2001 From: mtvpls Date: Sun, 22 Mar 2026 17:07:28 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E8=A7=A3=E6=9E=90=E5=88=87?= =?UTF-8?q?=E9=9B=86=E7=9A=84=E5=8A=A0=E8=BD=BD=E7=8A=B6=E6=80=81=E9=97=AE?= =?UTF-8?q?=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 6 +++-- src/app/play/page.tsx | 60 ++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 61 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 2f2828e..be691e4 100644 --- a/README.md +++ b/README.md @@ -27,11 +27,13 @@ - ✨ **视频超分 (Anime4K)**:使用 WebGPU 技术实现实时视频画质增强(支持 1.5x/2x/3x/4x 超分) - 💬 **弹幕系统**:完整的弹幕搜索、匹配、加载功能,支持弹幕设置持久化、弹幕屏蔽 - 📝 **豆瓣评论抓取**:自动抓取并展示豆瓣电影短评,支持分页加载 +- 🧩 **视频源脚本**:支持通过脚本自定义视频源、搜索、详情与播放解析逻辑(实验性) - 🪒**自定义去广告**:你可以自定义你的去广告代码,实现更强力的去广告功能 +- 🚀 **更快更顺滑**:相较原版项目整体速度更快,交互体验更好 - 🎭 **观影室**:支持多人同步观影、实时聊天、语音通话等功能(实验性)。 -- 📥 **M3U8完整下载**:通过合并m3u8片段实现完整视频下载。 +- 📥 **M3U8完整下载**:支持浏览器内合并 m3u8 片段下载,也支持下载到本地文件夹并无感播放本地视频。 - 💾 **服务器离线下载**:支持在服务器端下载视频文件,支持断点续传,提前下载到家秒加载 。 -- 📚 **私人影库**:接入 OpenList或Emby,可打造专属私人影库,亦可观看网盘资源。 +- 📚 **私人影库**:接入 OpenList、Emby 或小雅,可打造专属私人影库,亦可观看网盘资源。 ## ✨ 功能特性 diff --git a/src/app/play/page.tsx b/src/app/play/page.tsx index 1862086..5390392 100644 --- a/src/app/play/page.tsx +++ b/src/app/play/page.tsx @@ -1367,13 +1367,15 @@ function PlayPageClient() { // 换源加载状态 const [isVideoLoading, setIsVideoLoading] = useState(true); const [videoLoadingStage, setVideoLoadingStage] = useState< - 'initing' | 'sourceChanging' + 'initing' | 'sourceChanging' | 'episodeChanging' >('initing'); const [videoError, setVideoError] = useState(null); // 直链播放时 CORS 失败的原始 URL,用于显示"使用代理播放"按钮 const [corsFailedUrl, setCorsFailedUrl] = useState(null); // 标记当前视频是否已经尝试过代理(防止 415→直连→失败→代理 的无限循环) const proxyAttemptedRef = useRef(false); + const videoUrlRequestSeqRef = useRef(0); + const lastVideoRequestKeyRef = useRef(null); // 直链代理域名记忆:检查某个域名是否需要代理 const isDirectplayDomainProxied = (url: string): boolean => { @@ -2124,11 +2126,39 @@ function PlayPageClient() { return; } + const requestKey = `${detailData.source}|${detailData.id}|${episodeIndex}`; + const isEpisodeSwitchRequest = lastVideoRequestKeyRef.current !== requestKey; + lastVideoRequestKeyRef.current = requestKey; + const requestSeq = ++videoUrlRequestSeqRef.current; + let newUrl = detailData?.episodes[episodeIndex] || ''; + const isXiaoyaLazyPlayUrl = newUrl.startsWith('/api/xiaoya/play'); + + if (isEpisodeSwitchRequest && isXiaoyaLazyPlayUrl) { + setVideoLoadingStage('episodeChanging'); + setIsVideoLoading(true); + setVideoError(null); + setCorsFailedUrl(null); + + if (artPlayerRef.current?.video) { + try { + const video = artPlayerRef.current.video as HTMLVideoElement; + video.pause(); + video.removeAttribute('src'); + video.load(); + } catch (error) { + console.warn('切集时清空旧视频源失败:', error); + } + } + + if (videoUrl) { + setVideoUrl(''); + } + } // 如果是小雅或 openlist 接口,先请求获取真实 URL const isSpecialLazyPlayUrl = - newUrl.startsWith('/api/xiaoya/play') || + isXiaoyaLazyPlayUrl || newUrl.startsWith('/api/openlist/play') || newUrl.startsWith('/api/source-script/play'); @@ -2145,6 +2175,9 @@ function PlayPageClient() { const response = await fetch(fetchUrl); const data = await response.json(); + if (requestSeq !== videoUrlRequestSeqRef.current) { + return; + } if (data.url) { newUrl = data.url; // 保存清晰度列表 @@ -2155,6 +2188,9 @@ function PlayPageClient() { } } } catch (error) { + if (requestSeq !== videoUrlRequestSeqRef.current) { + return; + } console.error('获取播放链接失败:', error); setVideoQualities([]); currentXiaoyaUrlRef.current = ''; // 获取失败,清空 @@ -2172,6 +2208,9 @@ function PlayPageClient() { currentId || undefined, episodeIndex ); + if (requestSeq !== videoUrlRequestSeqRef.current) { + return; + } if (fileSystemCheck.hasLocal && fileSystemCheck.dirHandle) { // 使用本地文件播放 @@ -2242,6 +2281,9 @@ function PlayPageClient() { // 如果没有 File System API 本地文件,检查服务器端本地下载 if (!fileSystemCheck.hasLocal) { const hasLocalFile = await checkLocalDownload(currentSource, currentId, episodeIndex); + if (requestSeq !== videoUrlRequestSeqRef.current) { + return; + } if (hasLocalFile) { // 使用本地代理接口,URL以.m3u8结尾以便Artplayer自动识别 @@ -2269,7 +2311,10 @@ function PlayPageClient() { } } - if (newUrl !== videoUrl) { + if (isEpisodeSwitchRequest || newUrl !== videoUrl) { + if (requestSeq !== videoUrlRequestSeqRef.current) { + return; + } setVideoUrl(newUrl); } }; @@ -3934,6 +3979,9 @@ function PlayPageClient() { if (artPlayerRef.current && artPlayerRef.current.paused) { saveCurrentPlayProgress(); } + setVideoLoadingStage('episodeChanging'); + setIsVideoLoading(true); + setVideoError(null); setCurrentEpisodeIndex(episodeNumber); } }; @@ -3945,6 +3993,9 @@ function PlayPageClient() { if (artPlayerRef.current && !artPlayerRef.current.paused) { saveCurrentPlayProgress(); } + setVideoLoadingStage('episodeChanging'); + setIsVideoLoading(true); + setVideoError(null); setCurrentEpisodeIndex(idx - 1); } }; @@ -3993,6 +4044,9 @@ function PlayPageClient() { const isFiltered = episodeTitle && isEpisodeFilteredByTitle(episodeTitle); if (!isFiltered) { + setVideoLoadingStage('episodeChanging'); + setIsVideoLoading(true); + setVideoError(null); setCurrentEpisodeIndex(nextIdx); return; }