/* eslint-disable @typescript-eslint/ban-ts-comment, @typescript-eslint/no-explicit-any, react-hooks/exhaustive-deps, no-console, @next/next/no-img-element */ 'use client'; import Artplayer from 'artplayer'; import artplayerPluginDanmuku from 'artplayer-plugin-danmuku'; import Hls from 'hls.js'; import { Heart, PlayCircleIcon } from 'lucide-react'; import { useRouter, useSearchParams } from 'next/navigation'; import { Suspense, useEffect, useRef, useState } from 'react'; import { deleteFavorite, deletePlayRecord, deleteSkipConfig, generateStorageKey, getAllPlayRecords, getSkipConfig, isFavorited, saveFavorite, savePlayRecord, saveSkipConfig, subscribeToDataUpdates, } from '@/lib/db.client'; import { SearchResult } from '@/lib/types'; import { getVideoResolutionFromM3u8, processImageUrl } from '@/lib/utils'; import { getShortDramaRecommend, ShortDramaRecommendResponse } from '@/lib/shortdrama.client'; import EpisodeSelector from '@/components/EpisodeSelector'; import PageLayout from '@/components/PageLayout'; // 扩展 HTMLVideoElement 类型以支持 hls 属性 declare global { interface HTMLVideoElement { hls?: any; } } // Wake Lock API 类型声明 interface WakeLockSentinel { released: boolean; release(): Promise; addEventListener(type: 'release', listener: () => void): void; removeEventListener(type: 'release', listener: () => void): void; } function PlayPageClient() { const router = useRouter(); const searchParams = useSearchParams(); // ----------------------------------------------------------------------------- // 状态变量(State) // ----------------------------------------------------------------------------- const [loading, setLoading] = useState(true); const [loadingStage, setLoadingStage] = useState< 'searching' | 'preferring' | 'fetching' | 'ready' >('searching'); const [loadingMessage, setLoadingMessage] = useState('正在搜索播放源...'); const [error, setError] = useState(null); const [detail, setDetail] = useState(null); // 收藏状态 const [favorited, setFavorited] = useState(false); // 推荐短剧状态 const [recommendedShortDramas, setRecommendedShortDramas] = useState(null); const [recommendLoading, setRecommendLoading] = useState(false); // 跳过片头片尾配置 const [skipConfig, setSkipConfig] = useState<{ enable: boolean; intro_time: number; outro_time: number; }>({ enable: false, intro_time: 0, outro_time: 0, }); const skipConfigRef = useRef(skipConfig); useEffect(() => { skipConfigRef.current = skipConfig; }, [ skipConfig, skipConfig.enable, skipConfig.intro_time, skipConfig.outro_time, ]); // 跳过检查的时间间隔控制 const lastSkipCheckRef = useRef(0); // 去广告开关(从 localStorage 继承,默认 true) const [blockAdEnabled, setBlockAdEnabled] = useState(() => { if (typeof window !== 'undefined') { const v = localStorage.getItem('enable_blockad'); if (v !== null) return v === 'true'; } return true; }); const blockAdEnabledRef = useRef(blockAdEnabled); useEffect(() => { blockAdEnabledRef.current = blockAdEnabled; }, [blockAdEnabled]); // 视频基本信息 const [videoTitle, setVideoTitle] = useState(searchParams.get('title') || ''); const [videoYear, setVideoYear] = useState(searchParams.get('year') || ''); const [videoCover, setVideoCover] = useState(''); const [videoDoubanId, setVideoDoubanId] = useState(0); // 当前源和ID const [currentSource, setCurrentSource] = useState( searchParams.get('source') || '' ); const [currentId, setCurrentId] = useState(searchParams.get('id') || ''); // 短剧ID(用于调用短剧全集地址API) const [shortdramaId,] = useState(searchParams.get('shortdrama_id') || ''); // 短剧分类和标签信息(从URL参数获取) const [vodClass] = useState(searchParams.get('vod_class') || ''); const [vodTag] = useState(searchParams.get('vod_tag') || ''); // 搜索所需信息 const [searchTitle] = useState(searchParams.get('stitle') || ''); const [searchType] = useState(searchParams.get('stype') || ''); // 是否需要优选 const [needPrefer, setNeedPrefer] = useState( searchParams.get('prefer') === 'true' ); const needPreferRef = useRef(needPrefer); useEffect(() => { needPreferRef.current = needPrefer; }, [needPrefer]); // 集数相关 const [currentEpisodeIndex, setCurrentEpisodeIndex] = useState(0); const currentSourceRef = useRef(currentSource); const currentIdRef = useRef(currentId); const videoTitleRef = useRef(videoTitle); const videoYearRef = useRef(videoYear); const detailRef = useRef(detail); const currentEpisodeIndexRef = useRef(currentEpisodeIndex); // 同步最新值到 refs useEffect(() => { currentSourceRef.current = currentSource; currentIdRef.current = currentId; detailRef.current = detail; currentEpisodeIndexRef.current = currentEpisodeIndex; videoTitleRef.current = videoTitle; videoYearRef.current = videoYear; }, [ currentSource, currentId, detail, currentEpisodeIndex, videoTitle, videoYear, ]); // 视频播放地址 const [videoUrl, setVideoUrl] = useState(''); // 总集数 const totalEpisodes = detail?.episodes?.length || 0; // 用于记录是否需要在播放器 ready 后跳转到指定进度 const resumeTimeRef = useRef(null); // 上次使用的音量,默认 0.7 const lastVolumeRef = useRef(0.7); // 上次使用的播放速率,默认 1.0 const lastPlaybackRateRef = useRef(1.0); // 换源相关状态 const [availableSources, setAvailableSources] = useState([]); const [sourceSearchLoading, setSourceSearchLoading] = useState(false); const [sourceSearchError, setSourceSearchError] = useState( null ); // 优选和测速开关 const [optimizationEnabled] = useState(() => { if (typeof window !== 'undefined') { const saved = localStorage.getItem('enableOptimization'); if (saved !== null) { try { return JSON.parse(saved); } catch { /* ignore */ } } } return true; }); // 保存优选时的测速结果,避免EpisodeSelector重复测速 const [precomputedVideoInfo, setPrecomputedVideoInfo] = useState< Map >(new Map()); // 折叠状态(仅在 lg 及以上屏幕有效) const [isEpisodeSelectorCollapsed, setIsEpisodeSelectorCollapsed] = useState(false); // 换源加载状态 const [isVideoLoading, setIsVideoLoading] = useState(true); const [videoLoadingStage, setVideoLoadingStage] = useState< 'initing' | 'sourceChanging' >('initing'); // 播放进度保存相关 const saveIntervalRef = useRef(null); const lastSaveTimeRef = useRef(0); const artPlayerRef = useRef(null); const artRef = useRef(null); const isComponentMountedRef = useRef(true); // 组件挂载状态 // Wake Lock 相关 const wakeLockRef = useRef(null); // ----------------------------------------------------------------------------- // 工具函数(Utils) // ----------------------------------------------------------------------------- // 获取推荐短剧 const fetchRecommendedShortDramas = async (forceShortdrama?: boolean) => { // 只在短剧播放页面显示推荐,或者强制调用时执行 if (!forceShortdrama && currentSource !== 'shortdrama') return; try { setRecommendLoading(true); const recommendData = await getShortDramaRecommend({ size: '5' }); setRecommendedShortDramas(recommendData); } catch (error) { console.error('获取推荐短剧失败:', error); } finally { setRecommendLoading(false); } }; // 播放源优选函数 const preferBestSource = async ( sources: SearchResult[] ): Promise => { if (sources.length === 1) return sources[0]; // 将播放源均分为两批,并发测速各批,避免一次性过多请求 const batchSize = Math.ceil(sources.length / 2); const allResults: Array<{ source: SearchResult; testResult: { quality: string; loadSpeed: string; pingTime: number }; } | null> = []; for (let start = 0; start < sources.length; start += batchSize) { const batchSources = sources.slice(start, start + batchSize); const batchResults = await Promise.all( batchSources.map(async (source) => { try { // 检查是否有第一集的播放地址 if (!source.episodes || source.episodes.length === 0) { console.warn(`播放源 ${source.source_name} 没有可用的播放地址`); return null; } const episodeUrl = source.episodes.length > 1 ? source.episodes[1] : source.episodes[0]; const testResult = await getVideoResolutionFromM3u8(episodeUrl); return { source, testResult, }; } catch (error) { return null; } }) ); allResults.push(...batchResults); } // 等待所有测速完成,包含成功和失败的结果 // 保存所有测速结果到 precomputedVideoInfo,供 EpisodeSelector 使用(包含错误结果) const newVideoInfoMap = new Map< string, { quality: string; loadSpeed: string; pingTime: number; hasError?: boolean; } >(); allResults.forEach((result, index) => { const source = sources[index]; const sourceKey = `${source.source}-${source.id}`; if (result) { // 成功的结果 newVideoInfoMap.set(sourceKey, result.testResult); } }); // 过滤出成功的结果用于优选计算 const successfulResults = allResults.filter(Boolean) as Array<{ source: SearchResult; testResult: { quality: string; loadSpeed: string; pingTime: number }; }>; setPrecomputedVideoInfo(newVideoInfoMap); if (successfulResults.length === 0) { console.warn('所有播放源测速都失败,使用第一个播放源'); return sources[0]; } // 找出所有有效速度的最大值,用于线性映射 const validSpeeds = successfulResults .map((result) => { const speedStr = result.testResult.loadSpeed; if (speedStr === '未知' || speedStr === '测量中...') return 0; const match = speedStr.match(/^([\d.]+)\s*(KB\/s|MB\/s)$/); if (!match) return 0; const value = parseFloat(match[1]); const unit = match[2]; return unit === 'MB/s' ? value * 1024 : value; // 统一转换为 KB/s }) .filter((speed) => speed > 0); const maxSpeed = validSpeeds.length > 0 ? Math.max(...validSpeeds) : 1024; // 默认1MB/s作为基准 // 找出所有有效延迟的最小值和最大值,用于线性映射 const validPings = successfulResults .map((result) => result.testResult.pingTime) .filter((ping) => ping > 0); const minPing = validPings.length > 0 ? Math.min(...validPings) : 50; const maxPing = validPings.length > 0 ? Math.max(...validPings) : 1000; // 计算每个结果的评分 const resultsWithScore = successfulResults.map((result) => ({ ...result, score: calculateSourceScore( result.testResult, maxSpeed, minPing, maxPing ), })); // 按综合评分排序,选择最佳播放源 resultsWithScore.sort((a, b) => b.score - a.score); console.log('播放源评分排序结果:'); resultsWithScore.forEach((result, index) => { console.log( `${index + 1}. ${result.source.source_name } - 评分: ${result.score.toFixed(2)} (${result.testResult.quality}, ${result.testResult.loadSpeed }, ${result.testResult.pingTime}ms)` ); }); return resultsWithScore[0].source; }; // 计算播放源综合评分 const calculateSourceScore = ( testResult: { quality: string; loadSpeed: string; pingTime: number; }, maxSpeed: number, minPing: number, maxPing: number ): number => { let score = 0; // 分辨率评分 (40% 权重) const qualityScore = (() => { switch (testResult.quality) { case '4K': return 100; case '2K': return 85; case '1080p': return 75; case '720p': return 60; case '480p': return 40; case 'SD': return 20; default: return 0; } })(); score += qualityScore * 0.4; // 下载速度评分 (40% 权重) - 基于最大速度线性映射 const speedScore = (() => { const speedStr = testResult.loadSpeed; if (speedStr === '未知' || speedStr === '测量中...') return 30; // 解析速度值 const match = speedStr.match(/^([\d.]+)\s*(KB\/s|MB\/s)$/); if (!match) return 30; const value = parseFloat(match[1]); const unit = match[2]; const speedKBps = unit === 'MB/s' ? value * 1024 : value; // 基于最大速度线性映射,最高100分 const speedRatio = speedKBps / maxSpeed; return Math.min(100, Math.max(0, speedRatio * 100)); })(); score += speedScore * 0.4; // 网络延迟评分 (20% 权重) - 基于延迟范围线性映射 const pingScore = (() => { const ping = testResult.pingTime; if (ping <= 0) return 0; // 无效延迟给默认分 // 如果所有延迟都相同,给满分 if (maxPing === minPing) return 100; // 线性映射:最低延迟=100分,最高延迟=0分 const pingRatio = (maxPing - ping) / (maxPing - minPing); return Math.min(100, Math.max(0, pingRatio * 100)); })(); score += pingScore * 0.2; return Math.round(score * 100) / 100; // 保留两位小数 }; // 更新视频地址 const updateVideoUrl = ( detailData: SearchResult | null, episodeIndex: number ) => { if ( !detailData || !detailData.episodes || episodeIndex >= detailData.episodes.length ) { setVideoUrl(''); return; } const newUrl = detailData?.episodes[episodeIndex] || ''; if (newUrl !== videoUrl) { setVideoUrl(newUrl); } }; // 检查是否需要代理访问的通用函数 const needsProxyUrl = (url: string): boolean => { return url.includes('quark.cn') || url.includes('drive.quark.cn') || url.includes('dl-c-zb-') || url.includes('dl-c-') || url.match(/https?:\/\/[^/]*\.drive\./) !== null; }; // 获取代理URL的通用函数 const getProxyUrl = (url: string): string => { const needsProxy = needsProxyUrl(url); if (needsProxy) { console.log('Using proxy for URL:', url); const proxyUrl = `/api/proxy/video?url=${encodeURIComponent(url)}`; console.log('Proxy URL:', proxyUrl); return proxyUrl; } return url; }; const ensureVideoSource = (video: HTMLVideoElement | null, url: string) => { if (!video || !url) return; const sources = Array.from(video.getElementsByTagName('source')); const existed = sources.some((s) => s.src === url); if (!existed) { // 移除旧的 source,保持唯一 sources.forEach((s) => s.remove()); const sourceEl = document.createElement('source'); sourceEl.src = url; video.appendChild(sourceEl); } // 始终允许远程播放(AirPlay / Cast) video.disableRemotePlayback = false; // 如果曾经有禁用属性,移除之 if (video.hasAttribute('disableRemotePlayback')) { video.removeAttribute('disableRemotePlayback'); } }; // Wake Lock 相关函数 const requestWakeLock = async () => { try { if ('wakeLock' in navigator) { wakeLockRef.current = await (navigator as any).wakeLock.request( 'screen' ); console.log('Wake Lock 已启用'); } } catch (err) { console.warn('Wake Lock 请求失败:', err); } }; const releaseWakeLock = async () => { try { if (wakeLockRef.current) { await wakeLockRef.current.release(); wakeLockRef.current = null; console.log('Wake Lock 已释放'); } } catch (err) { console.warn('Wake Lock 释放失败:', err); } }; // 清理播放器资源的统一函数 const cleanupPlayer = () => { if (artPlayerRef.current) { try { const player = artPlayerRef.current; // 先设置为null防止在清理过程中被访问 artPlayerRef.current = null; // 移除所有事件监听器 try { if (typeof player.off === 'function') { // 移除常见的事件监听器 const events = [ 'ready', 'play', 'pause', 'video:ended', 'video:volumechange', 'video:ratechange', 'video:canplay', 'video:timeupdate', 'error' ]; events.forEach(event => { try { player.off(event); } catch (eventErr) { console.warn(`移除事件监听器 ${event} 失败:`, eventErr); } }); } } catch (offErr) { console.warn('移除事件监听器时出错:', offErr); } // 销毁 HLS 实例 if (player.video && player.video.hls) { try { player.video.hls.destroy(); } catch (hlsErr) { console.warn('清理HLS实例时出错:', hlsErr); } } // 销毁 ArtPlayer 实例 if (typeof player.destroy === 'function') { player.destroy(); } console.log('播放器资源已清理'); } catch (err) { console.warn('清理播放器资源时出错:', err); artPlayerRef.current = null; } } // 重置所有相关状态 resumeTimeRef.current = null; lastVolumeRef.current = 0.7; lastPlaybackRateRef.current = 1.0; lastSaveTimeRef.current = 0; }; // 去广告相关函数 function filterAdsFromM3U8(m3u8Content: string): string { if (!m3u8Content) return ''; // 按行分割M3U8内容 const lines = m3u8Content.split('\n'); const filteredLines = []; for (let i = 0; i < lines.length; i++) { const line = lines[i]; // 只过滤#EXT-X-DISCONTINUITY标识 if (!line.includes('#EXT-X-DISCONTINUITY')) { filteredLines.push(line); } } return filteredLines.join('\n'); } // 跳过片头片尾配置相关函数 const handleSkipConfigChange = async (newConfig: { enable: boolean; intro_time: number; outro_time: number; }) => { if (!currentSourceRef.current || !currentIdRef.current) return; try { setSkipConfig(newConfig); if (!newConfig.enable && !newConfig.intro_time && !newConfig.outro_time) { await deleteSkipConfig(currentSourceRef.current, currentIdRef.current); artPlayerRef.current.setting.update({ name: '跳过片头片尾', html: '跳过片头片尾', switch: skipConfigRef.current.enable, onSwitch: function (item: any) { const newConfig = { ...skipConfigRef.current, enable: !item.switch, }; handleSkipConfigChange(newConfig); return !item.switch; }, }); artPlayerRef.current.setting.update({ name: '设置片头', html: '设置片头', icon: '', tooltip: skipConfigRef.current.intro_time === 0 ? '设置片头时间' : `${formatTime(skipConfigRef.current.intro_time)}`, onClick: function () { if (!isComponentMountedRef.current || !artPlayerRef.current) return; const currentTime = artPlayerRef.current?.currentTime || 0; if (currentTime > 0) { const newConfig = { ...skipConfigRef.current, intro_time: currentTime, }; handleSkipConfigChange(newConfig); return `${formatTime(currentTime)}`; } }, }); artPlayerRef.current.setting.update({ name: '设置片尾', html: '设置片尾', icon: '', tooltip: skipConfigRef.current.outro_time >= 0 ? '设置片尾时间' : `-${formatTime(-skipConfigRef.current.outro_time)}`, onClick: function () { if (!isComponentMountedRef.current || !artPlayerRef.current) return; const outroTime = -( artPlayerRef.current?.duration - artPlayerRef.current?.currentTime ) || 0; if (outroTime < 0) { const newConfig = { ...skipConfigRef.current, outro_time: outroTime, }; handleSkipConfigChange(newConfig); return `-${formatTime(-outroTime)}`; } }, }); } else { await saveSkipConfig( currentSourceRef.current, currentIdRef.current, newConfig ); } console.log('跳过片头片尾配置已保存:', newConfig); } catch (err) { console.error('保存跳过片头片尾配置失败:', err); } }; const formatTime = (seconds: number): string => { if (seconds === 0) return '00:00'; const hours = Math.floor(seconds / 3600); const minutes = Math.floor((seconds % 3600) / 60); const remainingSeconds = Math.round(seconds % 60); if (hours === 0) { // 不到一小时,格式为 00:00 return `${minutes.toString().padStart(2, '0')}:${remainingSeconds .toString() .padStart(2, '0')}`; } else { // 超过一小时,格式为 00:00:00 return `${hours.toString().padStart(2, '0')}:${minutes .toString() .padStart(2, '0')}:${remainingSeconds.toString().padStart(2, '0')}`; } }; class CustomHlsJsLoader extends Hls.DefaultConfig.loader { constructor(config: any) { super(config); const load = this.load.bind(this); this.load = function (context: any, config: any, callbacks: any) { // 拦截manifest和level请求 if ( (context as any).type === 'manifest' || (context as any).type === 'level' ) { const onSuccess = callbacks.onSuccess; callbacks.onSuccess = function ( response: any, stats: any, context: any ) { // 如果是m3u8文件,处理内容以移除广告分段 if (response.data && typeof response.data === 'string') { // 过滤掉广告段 - 实现更精确的广告过滤逻辑 response.data = filterAdsFromM3U8(response.data); } return onSuccess(response, stats, context, null); }; } // 执行原始load方法 load(context, config, callbacks); }; } } // 当集数索引变化时自动更新视频地址 useEffect(() => { updateVideoUrl(detail, currentEpisodeIndex); }, [detail, currentEpisodeIndex]); // 进入页面时直接获取全部源信息 useEffect(() => { const fetchSourceDetail = async ( source: string, id: string ): Promise => { try { const detailResponse = await fetch( `/api/detail?source=${source}&id=${id}` ); if (!detailResponse.ok) { throw new Error('获取视频详情失败'); } const detailData = (await detailResponse.json()) as SearchResult; setAvailableSources([detailData]); return [detailData]; } catch (err) { console.error('获取视频详情失败:', err); return []; } finally { setSourceSearchLoading(false); } }; const fetchSourcesData = async (query: string): Promise => { // 根据搜索词获取全部源信息 try { const response = await fetch( `/api/search?q=${encodeURIComponent(query.trim())}` ); if (!response.ok) { throw new Error('搜索失败'); } const data = await response.json(); // 处理搜索结果,根据规则过滤 const results = data.results.filter( (result: SearchResult) => result.title.replaceAll(' ', '').toLowerCase() === videoTitleRef.current.replaceAll(' ', '').toLowerCase() && (videoYearRef.current ? result.year.toLowerCase() === videoYearRef.current.toLowerCase() : true) && (searchType ? (searchType === 'tv' && result.episodes.length > 1) || (searchType === 'movie' && result.episodes.length === 1) : true) ); setAvailableSources(results); return results; } catch (err) { setSourceSearchError(err instanceof Error ? err.message : '搜索失败'); setAvailableSources([]); return []; } finally { setSourceSearchLoading(false); } }; const initAll = async () => { console.log('播放页面初始化参数:', { currentSource, currentId, videoTitle, searchTitle, shortdramaId, videoYear, allSearchParams: Array.from(searchParams.entries()) }); if (!currentSource && !currentId && !videoTitle && !searchTitle && !shortdramaId) { console.error('缺少必要参数,所有参数都为空'); setError('缺少必要参数'); setLoading(false); return; } // 如果是短剧,直接调用短剧全集地址API if (shortdramaId) { setLoadingStage('fetching'); setLoadingMessage('🎬 正在获取短剧播放源...'); try { const apiUrl = `/api/shortdrama/parse/all?id=${shortdramaId}`; const response = await fetch(apiUrl); if (!response.ok) { throw new Error(`获取短剧播放源失败: ${response.status}`); } const data = await response.json(); // 验证API响应格式 if (!data || !Array.isArray(data.results)) { throw new Error('API响应格式不正确或results为空'); } // 按index排序,确保从第0集开始 const sortedResults = data.results .filter((item: any) => item.status === 'success' && item.parsedUrl) .sort((a: any, b: any) => a.index - b.index); if (sortedResults.length === 0) { throw new Error('没有可用的播放地址'); } // 构造播放地址和集数标题数组,确保从index 0开始 const episodes = sortedResults.map((item: any) => item.parsedUrl); const episodes_titles = sortedResults.map((item: any) => item.label || `第${item.index + 1}集`); const detailData: SearchResult = { id: shortdramaId, source: 'shortdrama', source_name: '短剧', title: data.videoName || videoTitle || '短剧播放', year: videoYear || new Date().getFullYear().toString(), poster: data.cover || '', douban_id: 0, episodes: episodes, episodes_titles: episodes_titles, desc: data.description || '暂无剧情简介', // 添加描述信息字段 class: vodClass || '', // 使用从URL参数获取的分类字段 tag: vodTag || '', // 使用从URL参数获取的标签字段 }; setVideoTitle(detailData.title); setVideoYear(detailData.year); setVideoCover(detailData.poster); setDetail(detailData); setCurrentSource('shortdrama'); setCurrentId(shortdramaId); setLoadingStage('ready'); setLoadingMessage('✨ 短剧播放源准备就绪...'); // 获取推荐短剧 - 强制调用,因为是短剧页面 setTimeout(() => { fetchRecommendedShortDramas(true); }, 1500); setTimeout(() => { setLoading(false); }, 1000); return; } catch (error) { setError(`获取短剧播放源失败: ${error instanceof Error ? error.message : String(error)}`); setLoading(false); return; } } setLoading(true); setLoadingStage(currentSource && currentId ? 'fetching' : 'searching'); setLoadingMessage( currentSource && currentId ? '🎬 正在获取视频详情...' : '🔍 正在搜索播放源...' ); let sourcesInfo = await fetchSourcesData(searchTitle || videoTitle); if ( currentSource && currentId && !sourcesInfo.some( (source) => source.source === currentSource && source.id === currentId ) ) { sourcesInfo = await fetchSourceDetail(currentSource, currentId); } if (sourcesInfo.length === 0) { setError('未找到匹配结果'); setLoading(false); return; } let detailData: SearchResult = sourcesInfo[0]; // 指定源和id且无需优选 if (currentSource && currentId && !needPreferRef.current) { const target = sourcesInfo.find( (source) => source.source === currentSource && source.id === currentId ); if (target) { detailData = target; } else { setError('未找到匹配结果'); setLoading(false); return; } } // 未指定源和 id 或需要优选,且开启优选开关 if ( (!currentSource || !currentId || needPreferRef.current) && optimizationEnabled ) { setLoadingStage('preferring'); setLoadingMessage('⚡ 正在优选最佳播放源...'); detailData = await preferBestSource(sourcesInfo); } console.log(detailData.source, detailData.id); setNeedPrefer(false); setCurrentSource(detailData.source); setCurrentId(detailData.id); setVideoYear(detailData.year); setVideoTitle(detailData.title || videoTitleRef.current); setVideoCover(detailData.poster); setVideoDoubanId(detailData.douban_id || 0); setDetail(detailData); if (currentEpisodeIndex >= detailData.episodes.length) { setCurrentEpisodeIndex(0); } // 规范URL参数 const newUrl = new URL(window.location.href); newUrl.searchParams.set('source', detailData.source); newUrl.searchParams.set('id', detailData.id); newUrl.searchParams.set('year', detailData.year); newUrl.searchParams.set('title', detailData.title); newUrl.searchParams.delete('prefer'); window.history.replaceState({}, '', newUrl.toString()); setLoadingStage('ready'); setLoadingMessage('✨ 准备就绪,即将开始播放...'); // 短暂延迟让用户看到完成状态 setTimeout(() => { setLoading(false); }, 1000); }; initAll(); }, []); // 播放记录处理 useEffect(() => { // 仅在初次挂载时检查播放记录 const initFromHistory = async () => { if (!currentSource || !currentId) return; try { const allRecords = await getAllPlayRecords(); const key = generateStorageKey(currentSource, currentId); const record = allRecords[key]; if (record) { const targetIndex = record.index - 1; const targetTime = record.play_time; // 更新当前选集索引 if (targetIndex !== currentEpisodeIndex) { setCurrentEpisodeIndex(targetIndex); } // 保存待恢复的播放进度,待播放器就绪后跳转 resumeTimeRef.current = targetTime; } } catch (err) { console.error('读取播放记录失败:', err); } }; initFromHistory(); }, []); // 跳过片头片尾配置处理 useEffect(() => { // 仅在初次挂载时检查跳过片头片尾配置 const initSkipConfig = async () => { if (!currentSource || !currentId) return; try { const config = await getSkipConfig(currentSource, currentId); if (config) { setSkipConfig(config); } } catch (err) { console.error('读取跳过片头片尾配置失败:', err); } }; initSkipConfig(); }, []); // 处理换源 const handleSourceChange = async ( newSource: string, newId: string, newTitle: string ) => { try { // 显示换源加载状态 setVideoLoadingStage('sourceChanging'); setIsVideoLoading(true); // 记录当前播放进度(仅在同一集数切换时恢复) const currentPlayTime = artPlayerRef.current?.currentTime || 0; console.log('换源前当前播放时间:', currentPlayTime); // 清除前一个历史记录 if (currentSourceRef.current && currentIdRef.current) { try { await deletePlayRecord( currentSourceRef.current, currentIdRef.current ); console.log('已清除前一个播放记录'); } catch (err) { console.error('清除播放记录失败:', err); } } // 清除并设置下一个跳过片头片尾配置 if (currentSourceRef.current && currentIdRef.current) { try { await deleteSkipConfig( currentSourceRef.current, currentIdRef.current ); await saveSkipConfig(newSource, newId, skipConfigRef.current); } catch (err) { console.error('清除跳过片头片尾配置失败:', err); } } const newDetail = availableSources.find( (source) => source.source === newSource && source.id === newId ); if (!newDetail) { setError('未找到匹配结果'); return; } // 尝试跳转到当前正在播放的集数 let targetIndex = currentEpisodeIndex; // 如果当前集数超出新源的范围,则跳转到第一集 if (!newDetail.episodes || targetIndex >= newDetail.episodes.length) { targetIndex = 0; } // 如果仍然是同一集数且播放进度有效,则在播放器就绪后恢复到原始进度 if (targetIndex !== currentEpisodeIndex) { resumeTimeRef.current = 0; } else if ( (!resumeTimeRef.current || resumeTimeRef.current === 0) && currentPlayTime > 1 ) { resumeTimeRef.current = currentPlayTime; } // 更新URL参数(不刷新页面) const newUrl = new URL(window.location.href); newUrl.searchParams.set('source', newSource); newUrl.searchParams.set('id', newId); newUrl.searchParams.set('year', newDetail.year); window.history.replaceState({}, '', newUrl.toString()); setVideoTitle(newDetail.title || newTitle); setVideoYear(newDetail.year); setVideoCover(newDetail.poster); setVideoDoubanId(newDetail.douban_id || 0); setCurrentSource(newSource); setCurrentId(newId); setDetail(newDetail); setCurrentEpisodeIndex(targetIndex); } catch (err) { // 隐藏换源加载状态 setIsVideoLoading(false); setError(err instanceof Error ? err.message : '换源失败'); } }; useEffect(() => { document.addEventListener('keydown', handleKeyboardShortcuts); return () => { document.removeEventListener('keydown', handleKeyboardShortcuts); }; }, []); // --------------------------------------------------------------------------- // 集数切换 // --------------------------------------------------------------------------- // 处理集数切换 const handleEpisodeChange = (episodeNumber: number) => { if (episodeNumber >= 0 && episodeNumber < totalEpisodes) { // 在更换集数前保存当前播放进度 if (artPlayerRef.current && artPlayerRef.current.paused) { saveCurrentPlayProgress(); } setCurrentEpisodeIndex(episodeNumber); } }; const handlePreviousEpisode = () => { const d = detailRef.current; const idx = currentEpisodeIndexRef.current; if (d && d.episodes && idx > 0) { if (artPlayerRef.current && !artPlayerRef.current.paused) { saveCurrentPlayProgress(); } setCurrentEpisodeIndex(idx - 1); } }; const handleNextEpisode = () => { const d = detailRef.current; const idx = currentEpisodeIndexRef.current; if (d && d.episodes && idx < d.episodes.length - 1) { if (artPlayerRef.current && !artPlayerRef.current.paused) { saveCurrentPlayProgress(); } setCurrentEpisodeIndex(idx + 1); } }; // --------------------------------------------------------------------------- // 键盘快捷键 // --------------------------------------------------------------------------- // 处理全局快捷键 const handleKeyboardShortcuts = (e: KeyboardEvent) => { // 检查组件是否已卸载 if (!isComponentMountedRef.current) { return; } // 忽略输入框中的按键事件 if ( (e.target as HTMLElement).tagName === 'INPUT' || (e.target as HTMLElement).tagName === 'TEXTAREA' ) return; // Alt + 左箭头 = 上一集 if (e.altKey && e.key === 'ArrowLeft') { if (detailRef.current && currentEpisodeIndexRef.current > 0) { handlePreviousEpisode(); e.preventDefault(); } } // Alt + 右箭头 = 下一集 if (e.altKey && e.key === 'ArrowRight') { const d = detailRef.current; const idx = currentEpisodeIndexRef.current; if (d && idx < d.episodes.length - 1) { handleNextEpisode(); e.preventDefault(); } } // 左箭头 = 快退 if (!e.altKey && e.key === 'ArrowLeft') { if (isComponentMountedRef.current && artPlayerRef.current && artPlayerRef.current.currentTime > 5) { artPlayerRef.current.currentTime -= 10; e.preventDefault(); } } // 右箭头 = 快进 if (!e.altKey && e.key === 'ArrowRight') { if ( isComponentMountedRef.current && artPlayerRef.current && artPlayerRef.current.currentTime < artPlayerRef.current.duration - 5 ) { artPlayerRef.current.currentTime += 10; e.preventDefault(); } } // 上箭头 = 音量+ if (e.key === 'ArrowUp') { if (isComponentMountedRef.current && artPlayerRef.current && artPlayerRef.current.volume < 1) { artPlayerRef.current.volume = Math.round((artPlayerRef.current.volume + 0.1) * 10) / 10; if (artPlayerRef.current.notice) { artPlayerRef.current.notice.show = `音量: ${Math.round( artPlayerRef.current.volume * 100 )}`; } e.preventDefault(); } } // 下箭头 = 音量- if (e.key === 'ArrowDown') { if (isComponentMountedRef.current && artPlayerRef.current && artPlayerRef.current.volume > 0) { artPlayerRef.current.volume = Math.round((artPlayerRef.current.volume - 0.1) * 10) / 10; if (artPlayerRef.current.notice) { artPlayerRef.current.notice.show = `音量: ${Math.round( artPlayerRef.current.volume * 100 )}`; } e.preventDefault(); } } // 空格 = 播放/暂停 if (e.key === ' ') { if (isComponentMountedRef.current && artPlayerRef.current) { artPlayerRef.current.toggle(); e.preventDefault(); } } // f 键 = 切换全屏 if (e.key === 'f' || e.key === 'F') { if (isComponentMountedRef.current && artPlayerRef.current) { artPlayerRef.current.fullscreen = !artPlayerRef.current.fullscreen; e.preventDefault(); } } }; // --------------------------------------------------------------------------- // 播放记录相关 // --------------------------------------------------------------------------- // 保存播放进度 const saveCurrentPlayProgress = async () => { // 检查组件是否已卸载 if (!isComponentMountedRef.current) { return; } if ( !artPlayerRef.current || !currentSourceRef.current || !currentIdRef.current || !videoTitleRef.current || !detailRef.current?.source_name ) { return; } const player = artPlayerRef.current; const currentTime = player.currentTime || 0; const duration = player.duration || 0; // 如果播放时间太短(少于5秒)或者视频时长无效,不保存 if (currentTime < 1 || !duration) { return; } try { await savePlayRecord(currentSourceRef.current, currentIdRef.current, { title: videoTitleRef.current, source_name: detailRef.current?.source_name || '', year: detailRef.current?.year, cover: detailRef.current?.poster || '', index: currentEpisodeIndexRef.current + 1, // 转换为1基索引 total_episodes: detailRef.current?.episodes.length || 1, play_time: Math.floor(currentTime), total_time: Math.floor(duration), save_time: Date.now(), search_title: searchTitle, }); lastSaveTimeRef.current = Date.now(); console.log('播放进度已保存:', { title: videoTitleRef.current, episode: currentEpisodeIndexRef.current + 1, year: detailRef.current?.year, progress: `${Math.floor(currentTime)}/${Math.floor(duration)}`, }); } catch (err) { console.error('保存播放进度失败:', err); } }; useEffect(() => { // 页面即将卸载时保存播放进度和清理资源 const handleBeforeUnload = () => { saveCurrentPlayProgress(); releaseWakeLock(); cleanupPlayer(); }; // 页面可见性变化时保存播放进度和释放 Wake Lock const handleVisibilityChange = () => { if (document.visibilityState === 'hidden') { saveCurrentPlayProgress(); releaseWakeLock(); } else if (document.visibilityState === 'visible') { // 页面重新可见时,如果正在播放则重新请求 Wake Lock if (artPlayerRef.current && !artPlayerRef.current.paused) { requestWakeLock(); } } }; // 添加事件监听器 window.addEventListener('beforeunload', handleBeforeUnload); document.addEventListener('visibilitychange', handleVisibilityChange); return () => { // 清理事件监听器 window.removeEventListener('beforeunload', handleBeforeUnload); document.removeEventListener('visibilitychange', handleVisibilityChange); }; }, [currentEpisodeIndex, detail]); // 清理定时器 useEffect(() => { return () => { if (saveIntervalRef.current) { clearInterval(saveIntervalRef.current); } }; }, []); // --------------------------------------------------------------------------- // 收藏相关 // --------------------------------------------------------------------------- // 每当 source 或 id 变化时检查收藏状态 useEffect(() => { if (!currentSource || !currentId) return; (async () => { try { const fav = await isFavorited(currentSource, currentId); setFavorited(fav); } catch (err) { console.error('检查收藏状态失败:', err); } })(); }, [currentSource, currentId]); // 监听收藏数据更新事件 useEffect(() => { if (!currentSource || !currentId) return; const unsubscribe = subscribeToDataUpdates( 'favoritesUpdated', (favorites: Record) => { const key = generateStorageKey(currentSource, currentId); const isFav = !!favorites[key]; setFavorited(isFav); } ); return unsubscribe; }, [currentSource, currentId]); // 切换收藏 const handleToggleFavorite = async () => { if ( !videoTitleRef.current || !detailRef.current || !currentSourceRef.current || !currentIdRef.current ) return; try { if (favorited) { // 如果已收藏,删除收藏 await deleteFavorite(currentSourceRef.current, currentIdRef.current); setFavorited(false); } else { // 如果未收藏,添加收藏 await saveFavorite(currentSourceRef.current, currentIdRef.current, { title: videoTitleRef.current, source_name: detailRef.current?.source_name || '', year: detailRef.current?.year, cover: detailRef.current?.poster || '', total_episodes: detailRef.current?.episodes.length || 1, save_time: Date.now(), search_title: searchTitle, }); setFavorited(true); } } catch (err) { console.error('切换收藏失败:', err); } }; useEffect(() => { if ( !Artplayer || !Hls || !videoUrl || loading || currentEpisodeIndex === null || !artRef.current ) { return; } // 确保选集索引有效 if ( !detail || !detail.episodes || currentEpisodeIndex >= detail.episodes.length || currentEpisodeIndex < 0 ) { setError(`选集索引无效,当前共 ${totalEpisodes} 集`); return; } if (!videoUrl) { setError('视频地址无效'); return; } console.log(videoUrl); // 检测是否为WebKit浏览器 const isWebkit = typeof window !== 'undefined' && typeof (window as any).webkitConvertPointFromNodeToPage === 'function'; // 非WebKit浏览器且播放器已存在,使用switch方法切换 if (!isWebkit && artPlayerRef.current) { const finalVideoUrl = getProxyUrl(videoUrl); artPlayerRef.current.switch = finalVideoUrl; artPlayerRef.current.title = `${videoTitle} - 第${currentEpisodeIndex + 1 }集`; artPlayerRef.current.poster = videoCover; if (artPlayerRef.current?.video) { ensureVideoSource( artPlayerRef.current.video as HTMLVideoElement, finalVideoUrl ); } return; } // WebKit浏览器或首次创建:销毁之前的播放器实例并创建新的 if (artPlayerRef.current) { cleanupPlayer(); } try { // 创建新的播放器实例 Artplayer.PLAYBACK_RATE = [0.5, 0.75, 1, 1.25, 1.5, 2, 3]; Artplayer.USE_RAF = true; const finalVideoUrl = getProxyUrl(videoUrl); artPlayerRef.current = new Artplayer({ container: artRef.current, url: finalVideoUrl, poster: videoCover, volume: 0.7, isLive: false, muted: false, autoplay: true, pip: true, autoSize: false, autoMini: false, screenshot: false, setting: true, loop: false, flip: false, playbackRate: true, aspectRatio: false, fullscreen: true, fullscreenWeb: true, subtitleOffset: false, miniProgressBar: false, mutex: true, playsInline: true, autoPlayback: false, airplay: true, theme: '#3b82f6', lang: 'zh-cn', hotkey: false, fastForward: true, autoOrientation: true, lock: true, moreVideoAttr: { crossOrigin: 'anonymous', preload: 'metadata', }, plugins: [ artplayerPluginDanmuku({ danmuku: async () => { try { // 检查组件是否已卸载 if (!isComponentMountedRef.current || !artPlayerRef.current) { return []; } // 生成弹幕唯一ID(基于视频源和ID) const videoId = `${currentSource}-${currentId}`; const response = await fetch(`/api/danmu?videoId=${encodeURIComponent(videoId)}`); // 再次检查组件是否已卸载 if (!isComponentMountedRef.current || !artPlayerRef.current) { return []; } if (response.ok) { const data = await response.json(); return data; } return []; } catch (error) { console.error('获取弹幕失败:', error); return []; } }, speed: 8, // 弹幕持续时间,单位秒 opacity: 0.8, // 弹幕透明度 fontSize: 20, // 字体大小 color: '#FFFFFF', // 默认字体颜色 mode: 0, // 默认模式,0-滚动,1-顶部,2-底部 margin: [10, '20%'], // 弹幕上下边距 antiOverlap: true, // 防重叠 filter: (danmu: any) => danmu?.text && danmu.text.length <= 100, // 弹幕过滤 theme: 'dark', // 输入框主题 beforeEmit: async (danmu: any) => { try { // 验证弹幕内容 if (!danmu?.text || !danmu.text.trim()) { return false; } // 检查组件挂载状态和播放器是否仍然存在 const currentPlayer = artPlayerRef.current; if (!isComponentMountedRef.current || !currentPlayer) { return false; } // 发送弹幕到服务器 const videoId = `${currentSource}-${currentId}`; const currentTime = currentPlayer.currentTime || 0; const response = await fetch('/api/danmu', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ videoId, text: danmu.text.trim(), color: danmu.color || '#FFFFFF', mode: danmu.mode || 0, time: currentTime, }), }); // 再次检查组件挂载状态和播放器是否仍然存在 const playerAfterRequest = artPlayerRef.current; if (!isComponentMountedRef.current || !playerAfterRequest) { return false; } if (response.ok) { const result = await response.json(); if (result.success) { console.log('弹幕发送成功'); return true; } } else { const error = await response.json(); console.error('发送弹幕失败:', error.error); if (playerAfterRequest && typeof playerAfterRequest.notice?.show !== 'undefined') { playerAfterRequest.notice.show = `发送失败: ${error.error}`; } } return false; } catch (error) { console.error('发送弹幕出错:', error); const currentPlayer = artPlayerRef.current; if (isComponentMountedRef.current && currentPlayer && typeof currentPlayer.notice?.show !== 'undefined') { currentPlayer.notice.show = '发送弹幕失败'; } return false; } }, }), ], // HLS 和 MP4 支持配置 customType: { mp4: async function (video: HTMLVideoElement, url: string) { console.log('Loading MP4 video:', url); // 对于需要代理的视频文件,通过代理API来避免403错误 if (needsProxyUrl(url)) { console.log('Using proxy for MP4 video URL:', url); // 先测试URL的可达性 try { console.log('Testing video URL accessibility...'); const testResponse = await fetch(`/api/proxy/video/test?url=${encodeURIComponent(url)}`); const testData = await testResponse.json(); console.log('Video URL test results:', testData); } catch (testError) { console.warn('URL test failed, proceeding with proxy anyway:', testError); } const proxyUrl = getProxyUrl(url); // 设置视频元素的属性 video.crossOrigin = 'anonymous'; video.preload = 'metadata'; video.setAttribute('playsinline', 'true'); // 使用代理URL video.src = proxyUrl; // 添加更详细的错误处理 video.onerror = function (e) { console.error('Proxy video load error:', e); console.error('Video error details:', { error: video.error, networkState: video.networkState, readyState: video.readyState, currentSrc: video.currentSrc }); console.log('Trying direct URL as fallback...'); // 作为备用方案,尝试直接加载 video.crossOrigin = null; // 重置跨域设置 video.src = url; }; // 添加加载成功的日志 video.onloadstart = function () { console.log('Video started loading through proxy'); }; video.oncanplay = function () { console.log('Video can start playing through proxy'); }; } else { console.log('Loading video directly:', url); // 对于其他来源的MP4,直接加载 video.src = url; video.crossOrigin = 'anonymous'; video.preload = 'metadata'; } }, m3u8: function (video: HTMLVideoElement, url: string) { if (!Hls) { console.error('HLS.js 未加载'); return; } if (video.hls) { try { video.hls.destroy(); } catch (err) { console.warn('销毁旧HLS实例时出错:', err); } video.hls = null; } // 检查是否需要使用代理URL const finalUrl = getProxyUrl(url); const hls = new Hls({ debug: false, // 关闭日志 enableWorker: true, // WebWorker 解码,降低主线程压力 lowLatencyMode: true, // 开启低延迟 LL-HLS /* 缓冲/内存相关 */ maxBufferLength: 30, // 前向缓冲最大 30s,过大容易导致高延迟 backBufferLength: 30, // 仅保留 30s 已播放内容,避免内存占用 maxBufferSize: 60 * 1000 * 1000, // 约 60MB,超出后触发清理 /* 自定义loader */ loader: blockAdEnabledRef.current ? CustomHlsJsLoader : Hls.DefaultConfig.loader, }); hls.loadSource(finalUrl); hls.attachMedia(video); video.hls = hls; ensureVideoSource(video, finalUrl); hls.on(Hls.Events.ERROR, function (event: any, data: any) { console.error('HLS Error:', event, data); if (data.fatal) { switch (data.type) { case Hls.ErrorTypes.NETWORK_ERROR: console.log('网络错误,尝试恢复...'); hls.startLoad(); break; case Hls.ErrorTypes.MEDIA_ERROR: console.log('媒体错误,尝试恢复...'); hls.recoverMediaError(); break; default: console.log('无法恢复的错误'); try { hls.destroy(); } catch (destroyErr) { console.warn('销毁HLS时出错:', destroyErr); } break; } } }); }, }, icons: { loading: '', }, settings: [ { html: '去广告', icon: 'AD', tooltip: blockAdEnabled ? '已开启' : '已关闭', onClick() { const newVal = !blockAdEnabled; try { localStorage.setItem('enable_blockad', String(newVal)); if (artPlayerRef.current) { resumeTimeRef.current = artPlayerRef.current.currentTime; const player = artPlayerRef.current; artPlayerRef.current = null; // 先清空引用 if (player.video && player.video.hls) { try { player.video.hls.destroy(); } catch (err) { console.warn('清理HLS实例失败:', err); } } try { player.destroy(); } catch (err) { console.warn('销毁播放器失败:', err); } } setBlockAdEnabled(newVal); } catch (_) { // ignore } return newVal ? '当前开启' : '当前关闭'; }, }, { name: '跳过片头片尾', html: '跳过片头片尾', switch: skipConfigRef.current.enable, onSwitch: function (item) { const newConfig = { ...skipConfigRef.current, enable: !item.switch, }; handleSkipConfigChange(newConfig); return !item.switch; }, }, { html: '删除跳过配置', onClick: function () { handleSkipConfigChange({ enable: false, intro_time: 0, outro_time: 0, }); return ''; }, }, { name: '设置片头', html: '设置片头', icon: '', tooltip: skipConfigRef.current.intro_time === 0 ? '设置片头时间' : `${formatTime(skipConfigRef.current.intro_time)}`, onClick: function () { if (!isComponentMountedRef.current || !artPlayerRef.current) return; const currentTime = artPlayerRef.current?.currentTime || 0; if (currentTime > 0) { const newConfig = { ...skipConfigRef.current, intro_time: currentTime, }; handleSkipConfigChange(newConfig); return `${formatTime(currentTime)}`; } }, }, { name: '设置片尾', html: '设置片尾', icon: '', tooltip: skipConfigRef.current.outro_time >= 0 ? '设置片尾时间' : `-${formatTime(-skipConfigRef.current.outro_time)}`, onClick: function () { if (!isComponentMountedRef.current || !artPlayerRef.current) return; const outroTime = -( artPlayerRef.current?.duration - artPlayerRef.current?.currentTime ) || 0; if (outroTime < 0) { const newConfig = { ...skipConfigRef.current, outro_time: outroTime, }; handleSkipConfigChange(newConfig); return `-${formatTime(-outroTime)}`; } }, }, ], // 控制栏配置 controls: [ { position: 'left', index: 13, html: '', tooltip: '播放下一集', click: function () { handleNextEpisode(); }, }, ], }); // 监听播放器事件 artPlayerRef.current.on('ready', () => { setError(null); // 播放器就绪后,如果正在播放则请求 Wake Lock if (artPlayerRef.current && !artPlayerRef.current.paused) { requestWakeLock(); } }); // 监听播放状态变化,控制 Wake Lock artPlayerRef.current.on('play', () => { if (!isComponentMountedRef.current) return; requestWakeLock(); }); artPlayerRef.current.on('pause', () => { if (!isComponentMountedRef.current) return; releaseWakeLock(); saveCurrentPlayProgress(); }); artPlayerRef.current.on('video:ended', () => { if (!isComponentMountedRef.current) return; releaseWakeLock(); }); // 如果播放器初始化时已经在播放状态,则请求 Wake Lock if (artPlayerRef.current && !artPlayerRef.current.paused) { requestWakeLock(); } artPlayerRef.current.on('video:volumechange', () => { if (!isComponentMountedRef.current || !artPlayerRef.current) return; lastVolumeRef.current = artPlayerRef.current.volume; }); artPlayerRef.current.on('video:ratechange', () => { if (!isComponentMountedRef.current || !artPlayerRef.current) return; lastPlaybackRateRef.current = artPlayerRef.current.playbackRate; }); // 监听视频可播放事件,这时恢复播放进度更可靠 artPlayerRef.current.on('video:canplay', () => { if (!isComponentMountedRef.current || !artPlayerRef.current) return; // 若存在需要恢复的播放进度,则跳转 if (resumeTimeRef.current && resumeTimeRef.current > 0) { try { const duration = artPlayerRef.current.duration || 0; let target = resumeTimeRef.current; if (duration && target >= duration - 2) { target = Math.max(0, duration - 5); } artPlayerRef.current.currentTime = target; console.log('成功恢复播放进度到:', resumeTimeRef.current); } catch (err) { console.warn('恢复播放进度失败:', err); } } resumeTimeRef.current = null; setTimeout(() => { if (!isComponentMountedRef.current || !artPlayerRef.current) return; if ( Math.abs(artPlayerRef.current.volume - lastVolumeRef.current) > 0.01 ) { artPlayerRef.current.volume = lastVolumeRef.current; } if ( Math.abs( artPlayerRef.current.playbackRate - lastPlaybackRateRef.current ) > 0.01 && isWebkit ) { artPlayerRef.current.playbackRate = lastPlaybackRateRef.current; } if (artPlayerRef.current.notice) { artPlayerRef.current.notice.show = ''; } }, 0); // 隐藏换源加载状态 setIsVideoLoading(false); }); // 监听视频时间更新事件,实现跳过片头片尾 artPlayerRef.current.on('video:timeupdate', () => { if (!isComponentMountedRef.current || !artPlayerRef.current || !skipConfigRef.current.enable) return; const currentTime = artPlayerRef.current.currentTime || 0; const duration = artPlayerRef.current.duration || 0; const now = Date.now(); // 限制跳过检查频率为1.5秒一次 if (now - lastSkipCheckRef.current < 1500) return; lastSkipCheckRef.current = now; // 跳过片头 if ( skipConfigRef.current.intro_time > 0 && currentTime < skipConfigRef.current.intro_time ) { artPlayerRef.current.currentTime = skipConfigRef.current.intro_time; artPlayerRef.current.notice.show = `已跳过片头 (${formatTime( skipConfigRef.current.intro_time )})`; } // 跳过片尾 if ( skipConfigRef.current.outro_time < 0 && duration > 0 && currentTime > artPlayerRef.current.duration + skipConfigRef.current.outro_time ) { if ( currentEpisodeIndexRef.current < (detailRef.current?.episodes?.length || 1) - 1 ) { handleNextEpisode(); } else { artPlayerRef.current.pause(); } artPlayerRef.current.notice.show = `已跳过片尾 (${formatTime( skipConfigRef.current.outro_time )})`; } }); artPlayerRef.current.on('error', (err: any) => { console.error('播放器错误:', err); if (!isComponentMountedRef.current || !artPlayerRef.current) { return; } if (artPlayerRef.current.currentTime > 0) { return; } }); // 监听视频播放结束事件,自动播放下一集 artPlayerRef.current.on('video:ended', () => { if (!isComponentMountedRef.current) return; const d = detailRef.current; const idx = currentEpisodeIndexRef.current; if (d && d.episodes && idx < d.episodes.length - 1) { setTimeout(() => { if (isComponentMountedRef.current) { setCurrentEpisodeIndex(idx + 1); } }, 1000); } }); artPlayerRef.current.on('video:timeupdate', () => { if (!isComponentMountedRef.current || !artPlayerRef.current) return; const now = Date.now(); let interval = 5000; if (process.env.NEXT_PUBLIC_STORAGE_TYPE === 'upstash') { interval = 20000; } if (now - lastSaveTimeRef.current > interval) { saveCurrentPlayProgress(); lastSaveTimeRef.current = now; } }); artPlayerRef.current.on('pause', () => { if (!isComponentMountedRef.current) return; saveCurrentPlayProgress(); }); if (artPlayerRef.current?.video) { ensureVideoSource( artPlayerRef.current.video as HTMLVideoElement, videoUrl ); } } catch (err) { console.error('创建播放器失败:', err); setError('播放器初始化失败'); } }, [Artplayer, Hls, videoUrl, loading, blockAdEnabled]); // 组件挂载时的初始化和卸载时的清理 useEffect(() => { // 组件挂载时,确保先清理任何可能残留的资源 console.log('播放页面组件挂载,进行初始化清理'); cleanupPlayer(); isComponentMountedRef.current = true; return () => { // 标记组件已卸载 isComponentMountedRef.current = false; console.log('播放页面组件卸载,进行资源清理'); // 清理定时器 if (saveIntervalRef.current) { clearInterval(saveIntervalRef.current); saveIntervalRef.current = null; } // 释放 Wake Lock releaseWakeLock(); // 销毁播放器实例 cleanupPlayer(); }; }, []); if (loading) { return (
{/* 动画影院图标 */}
{loadingStage === 'searching' && '🔍'} {loadingStage === 'preferring' && '⚡'} {loadingStage === 'fetching' && '🎬'} {loadingStage === 'ready' && '✨'}
{/* 旋转光环 */}
{/* 浮动粒子效果 */}
{/* 进度指示器 */}
{/* 进度条 */}
{/* 加载消息 */}

{loadingMessage}

); } if (error) { return (
{/* 错误图标 */}
😵
{/* 脉冲效果 */}
{/* 浮动错误粒子 */}
{/* 错误信息 */}

哎呀,出现了一些问题

{error}

请检查网络连接或尝试刷新页面

{/* 操作按钮 */}
); } return (
{/* 第一行:影片标题 */}

{videoTitle || '影片标题'} {totalEpisodes > 1 && ( {` > ${detail?.episodes_titles?.[currentEpisodeIndex] || `第 ${currentEpisodeIndex + 1} 集`}`} )}

{/* 第二行:播放器和选集 */}
{/* 折叠控制 - 仅在 lg 及以上屏幕显示 */}
{/* 播放器 */}
{/* 换源加载蒙层 */} {isVideoLoading && (
{/* 动画影院图标 */}
🎬
{/* 旋转光环 */}
{/* 浮动粒子效果 */}
{/* 换源消息 */}

{videoLoadingStage === 'sourceChanging' ? '🔄 切换播放源...' : '🔄 视频加载中...'}

)}
{/* 选集和换源 - 在移动端始终显示,在 lg 及以上可折叠 */}
{/* 详情展示 */}
{/* 文字区 */}
{/* 标题 */}

{videoTitle || '影片标题'}

{/* 关键信息行 */}
{detail?.class && ( {detail.class} )} {(detail?.year || videoYear) && ( {detail?.year || videoYear} )} {detail?.source_name && ( {detail.source_name} )} {detail?.type_name && {detail.type_name}}
{/* 标签展示 */} {detail?.tag && (
{detail.tag.split(',').filter(tag => tag.trim()).map((tag, index) => { // 根据不同标签设置不同颜色 const getTagColor = (tagName: string) => { const lowerTag = tagName.toLowerCase(); if (lowerTag.includes('甜宠') || lowerTag.includes('甜')) return 'bg-pink-100 text-pink-600 border-pink-200'; if (lowerTag.includes('霸总') || lowerTag.includes('总裁')) return 'bg-purple-100 text-purple-600 border-purple-200'; if (lowerTag.includes('现代') || lowerTag.includes('都市')) return 'bg-blue-100 text-blue-600 border-blue-200'; if (lowerTag.includes('古装') || lowerTag.includes('古代')) return 'bg-amber-100 text-amber-600 border-amber-200'; if (lowerTag.includes('穿越')) return 'bg-green-100 text-green-600 border-green-200'; if (lowerTag.includes('重生') || lowerTag.includes('逆袭')) return 'bg-indigo-100 text-indigo-600 border-indigo-200'; if (lowerTag.includes('复仇') || lowerTag.includes('虐渣')) return 'bg-red-100 text-red-600 border-red-200'; if (lowerTag.includes('家庭') || lowerTag.includes('伦理')) return 'bg-teal-100 text-teal-600 border-teal-200'; return 'bg-gray-100 text-gray-600 border-gray-200'; // 默认颜色 }; return ( {tag.trim()} ); })}
)} {/* 剧情简介 */} {detail?.desc && (
{detail.desc}
)}
{/* 封面展示 */}
{videoCover ? ( <> {videoTitle} {/* 豆瓣链接按钮 */} {videoDoubanId !== 0 && (
)} ) : ( 封面图片 )}
{/* 推荐短剧区域 */} {currentSource === 'shortdrama' && (

🎭 推荐短剧

为你精选更多精彩短剧

{recommendLoading ? (
) : recommendedShortDramas && recommendedShortDramas.items.length > 0 ? (
{recommendedShortDramas?.items.map((drama) => (
{ e.preventDefault(); e.stopPropagation(); const urlParams = new URLSearchParams(); urlParams.set('shortdrama_id', drama.vod_id.toString()); urlParams.set('title', drama.name); if (drama.vod_class) urlParams.set('vod_class', drama.vod_class); if (drama.vod_tag) urlParams.set('vod_tag', drama.vod_tag); const url = `/play?${urlParams.toString()}`; // 使用window.location.href来触发完整的页面导航,包括加载页面 window.location.href = url; }} style={{ WebkitUserSelect: 'none', userSelect: 'none', WebkitTouchCallout: 'none', WebkitTapHighlightColor: 'transparent', touchAction: 'manipulation', pointerEvents: 'auto', } as React.CSSProperties} onContextMenu={(e) => { e.preventDefault(); return false; }} > {/* 封面容器 */}
{drama.cover ? ( {drama.name} { const target = e.target as HTMLImageElement; target.src = 'https://via.placeholder.com/300x400?text=暂无封面'; }} style={{ WebkitUserSelect: 'none', userSelect: 'none', WebkitTouchCallout: 'none', } as React.CSSProperties} /> ) : (
暂无封面
)} {/* 评分标识 */} {drama.score && drama.score > 0 && (
{drama.score}
)} {/* 集数标识 */} {drama.total_episodes && (
{drama.total_episodes}集
)} {/* 播放按钮 */}
{ e.preventDefault(); return false; }} > { e.preventDefault(); return false; }} />
{/* 标题与来源 */}
{drama.name} {/* 自定义 tooltip */}
{drama.name}
短剧
))}
) : (
{!recommendedShortDramas ? '正在加载推荐内容...' : '暂无推荐内容'}
)}
)}
); } // FavoriteIcon 组件 const FavoriteIcon = ({ filled }: { filled: boolean }) => { if (filled) { return ( ); } return ( ); }; export default function PlayPage() { return ( Loading...}> ); }