From 2b482a031f3a65c4babc1a029ea0d1900aba79f9 Mon Sep 17 00:00:00 2001 From: mtvpls Date: Wed, 25 Mar 2026 23:15:32 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E6=9C=AC=E6=9C=BA=E8=BD=AC?= =?UTF-8?q?=E7=A0=81=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/play/page.tsx | 141 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 140 insertions(+), 1 deletion(-) diff --git a/src/app/play/page.tsx b/src/app/play/page.tsx index 5390392..d0b8968 100644 --- a/src/app/play/page.tsx +++ b/src/app/play/page.tsx @@ -2,7 +2,7 @@ 'use client'; -import { AlertCircle, Cloud, Heart, Sparkles, X } from 'lucide-react'; +import { AlertCircle, Cloud, Heart, Loader2, Router, Sparkles, X } from 'lucide-react'; import { useRouter, useSearchParams } from 'next/navigation'; import { Suspense, useEffect, useRef, useState } from 'react'; @@ -86,6 +86,7 @@ interface WakeLockSentinel { } function PlayPageClient() { + const LOCAL_TRANSCODER_BASE_URL = 'http://localhost:19080'; const router = useRouter(); const searchParams = useSearchParams(); const enableComments = useEnableComments(); @@ -491,6 +492,7 @@ function PlayPageClient() { const [showDanmakuFilterSettings, setShowDanmakuFilterSettings] = useState(false); const [currentSearchKeyword, setCurrentSearchKeyword] = useState(''); // 当前搜索使用的关键词 const [toast, setToast] = useState(null); + const [isTranscoding, setIsTranscoding] = useState(false); useEffect(() => { danmakuSettingsRef.current = danmakuSettings; @@ -596,6 +598,19 @@ function PlayPageClient() { ); }; + const isM3u8LikeUrl = (url?: string) => { + if (!url) return false; + const normalizedUrl = url.toLowerCase(); + return normalizedUrl.includes('.m3u8') || normalizedUrl.includes('/m3u8/'); + }; + + const buildAbsoluteUrl = (url: string) => { + if (url.startsWith('http://') || url.startsWith('https://')) { + return url; + } + return `${window.location.origin}${url.startsWith('/') ? '' : '/'}${url}`; + }; + // 搜索所需信息 const [searchTitle] = useState(searchParams.get('stitle') || ''); const [searchType] = useState(searchParams.get('stype') || ''); @@ -1299,6 +1314,105 @@ function PlayPageClient() { // 视频源代理模式状态 const [sourceProxyMode, setSourceProxyMode] = useState(false); + const resolveCurrentExternalPlaybackUrl = async () => { + let urlToUse = videoUrl; + if (sourceProxyMode && detail?.episodes && currentEpisodeIndex < detail.episodes.length) { + urlToUse = detail.episodes[currentEpisodeIndex]; + } + + if (!urlToUse) { + return null; + } + + return buildAbsoluteUrl(urlToUse); + }; + + const handleCreateTranscodeSession = async () => { + if (isTranscoding) return; + + try { + setIsTranscoding(true); + const currentPlayTime = artPlayerRef.current?.currentTime || 0; + + const sourceUrl = await resolveCurrentExternalPlaybackUrl(); + if (!sourceUrl) { + throw new Error('当前没有可转码的播放链接'); + } + + const requestHeaders: Record = {}; + if (sourceUrl.startsWith(window.location.origin)) { + if (document.cookie) { + requestHeaders.Cookie = document.cookie; + } + requestHeaders.Referer = `${window.location.origin}/`; + } + + const response = await fetch(`${LOCAL_TRANSCODER_BASE_URL}/v1/sessions`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + url: sourceUrl, + headers: Object.keys(requestHeaders).length > 0 ? requestHeaders : undefined, + subtitle: { + mode: 'burn_embedded', + stream: 'auto', + }, + refresh: false, + }), + }); + + const data = await response.json().catch(() => null); + if (!response.ok) { + throw new Error(data?.error || data?.message || `转码请求失败 (${response.status})`); + } + + const playUrl = data?.playlist_url || data?.play_url; + if (!playUrl) { + throw new Error('转码器未返回播放地址'); + } + + await new Promise((resolve) => setTimeout(resolve, 3000)); + + currentXiaoyaUrlRef.current = ''; + proxyAttemptedRef.current = false; + resumeTimeRef.current = currentPlayTime > 0 ? currentPlayTime : null; + setVideoQualities([]); + setVideoError(null); + setCorsFailedUrl(null); + setIsVideoLoading(true); + setVideoLoadingStage('sourceChanging'); + setVideoUrl(playUrl); + setToast({ + message: '转码任务已创建,等待 3 秒后已切换到转码地址', + type: 'success', + onClose: () => setToast(null), + }); + } catch (error) { + console.error('创建转码任务失败:', error); + setToast({ + message: error instanceof Error ? error.message : '创建转码任务失败', + type: 'error', + onClose: () => setToast(null), + }); + } finally { + setIsTranscoding(false); + } + }; + + const showExternalTranscodeButton = Boolean( + detail && + videoUrl && + !videoUrl.startsWith('blob:') && + !isM3u8LikeUrl(videoUrl) && + ( + detail.source === 'openlist' || + detail.source === 'xiaoya' || + detail.source.startsWith('emby') + ) + ); + // 总集数 const totalEpisodes = detail?.episodes?.length || 0; const directEpisodeLabel = detail?.episodes_titles?.[currentEpisodeIndex] || '直链'; @@ -8289,6 +8403,31 @@ function PlayPageClient() { + {showExternalTranscodeButton && ( + + )} + {/* PotPlayer */}