fixed:修复短剧页面出现的各种问题

This commit is contained in:
djteang 2025-09-11 16:26:48 +08:00
parent ecaeaea49d
commit 2c6e99e241
4 changed files with 263 additions and 65 deletions

View File

@ -215,6 +215,7 @@ function PlayPageClient() {
const artPlayerRef = useRef<any>(null); const artPlayerRef = useRef<any>(null);
const artRef = useRef<HTMLDivElement | null>(null); const artRef = useRef<HTMLDivElement | null>(null);
const isComponentMountedRef = useRef<boolean>(true); // 组件挂载状态
// Wake Lock 相关 // Wake Lock 相关
const wakeLockRef = useRef<WakeLockSentinel | null>(null); const wakeLockRef = useRef<WakeLockSentinel | null>(null);
@ -455,6 +456,27 @@ function PlayPageClient() {
} }
}; };
// 检查是否需要代理访问的通用函数
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) => { const ensureVideoSource = (video: HTMLVideoElement | null, url: string) => {
if (!video || !url) return; if (!video || !url) return;
const sources = Array.from(video.getElementsByTagName('source')); const sources = Array.from(video.getElementsByTagName('source'));
@ -505,14 +527,44 @@ function PlayPageClient() {
const cleanupPlayer = () => { const cleanupPlayer = () => {
if (artPlayerRef.current) { if (artPlayerRef.current) {
try { 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 实例 // 销毁 HLS 实例
if (artPlayerRef.current.video && artPlayerRef.current.video.hls) { if (player.video && player.video.hls) {
artPlayerRef.current.video.hls.destroy(); try {
player.video.hls.destroy();
} catch (hlsErr) {
console.warn('清理HLS实例时出错:', hlsErr);
}
} }
// 销毁 ArtPlayer 实例 // 销毁 ArtPlayer 实例
artPlayerRef.current.destroy(); if (typeof player.destroy === 'function') {
artPlayerRef.current = null; player.destroy();
}
console.log('播放器资源已清理'); console.log('播放器资源已清理');
} catch (err) { } catch (err) {
@ -520,6 +572,12 @@ function PlayPageClient() {
artPlayerRef.current = null; artPlayerRef.current = null;
} }
} }
// 重置所有相关状态
resumeTimeRef.current = null;
lastVolumeRef.current = 0.7;
lastPlaybackRateRef.current = 1.0;
lastSaveTimeRef.current = 0;
}; };
// 去广告相关函数 // 去广告相关函数
@ -576,6 +634,7 @@ function PlayPageClient() {
? '设置片头时间' ? '设置片头时间'
: `${formatTime(skipConfigRef.current.intro_time)}`, : `${formatTime(skipConfigRef.current.intro_time)}`,
onClick: function () { onClick: function () {
if (!isComponentMountedRef.current || !artPlayerRef.current) return;
const currentTime = artPlayerRef.current?.currentTime || 0; const currentTime = artPlayerRef.current?.currentTime || 0;
if (currentTime > 0) { if (currentTime > 0) {
const newConfig = { const newConfig = {
@ -596,6 +655,7 @@ function PlayPageClient() {
? '设置片尾时间' ? '设置片尾时间'
: `-${formatTime(-skipConfigRef.current.outro_time)}`, : `-${formatTime(-skipConfigRef.current.outro_time)}`,
onClick: function () { onClick: function () {
if (!isComponentMountedRef.current || !artPlayerRef.current) return;
const outroTime = const outroTime =
-( -(
artPlayerRef.current?.duration - artPlayerRef.current?.duration -
@ -1101,6 +1161,11 @@ function PlayPageClient() {
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// 处理全局快捷键 // 处理全局快捷键
const handleKeyboardShortcuts = (e: KeyboardEvent) => { const handleKeyboardShortcuts = (e: KeyboardEvent) => {
// 检查组件是否已卸载
if (!isComponentMountedRef.current) {
return;
}
// 忽略输入框中的按键事件 // 忽略输入框中的按键事件
if ( if (
(e.target as HTMLElement).tagName === 'INPUT' || (e.target as HTMLElement).tagName === 'INPUT' ||
@ -1128,7 +1193,7 @@ function PlayPageClient() {
// 左箭头 = 快退 // 左箭头 = 快退
if (!e.altKey && e.key === 'ArrowLeft') { if (!e.altKey && e.key === 'ArrowLeft') {
if (artPlayerRef.current && artPlayerRef.current.currentTime > 5) { if (isComponentMountedRef.current && artPlayerRef.current && artPlayerRef.current.currentTime > 5) {
artPlayerRef.current.currentTime -= 10; artPlayerRef.current.currentTime -= 10;
e.preventDefault(); e.preventDefault();
} }
@ -1137,6 +1202,7 @@ function PlayPageClient() {
// 右箭头 = 快进 // 右箭头 = 快进
if (!e.altKey && e.key === 'ArrowRight') { if (!e.altKey && e.key === 'ArrowRight') {
if ( if (
isComponentMountedRef.current &&
artPlayerRef.current && artPlayerRef.current &&
artPlayerRef.current.currentTime < artPlayerRef.current.duration - 5 artPlayerRef.current.currentTime < artPlayerRef.current.duration - 5
) { ) {
@ -1147,31 +1213,35 @@ function PlayPageClient() {
// 上箭头 = 音量+ // 上箭头 = 音量+
if (e.key === 'ArrowUp') { if (e.key === 'ArrowUp') {
if (artPlayerRef.current && artPlayerRef.current.volume < 1) { if (isComponentMountedRef.current && artPlayerRef.current && artPlayerRef.current.volume < 1) {
artPlayerRef.current.volume = artPlayerRef.current.volume =
Math.round((artPlayerRef.current.volume + 0.1) * 10) / 10; Math.round((artPlayerRef.current.volume + 0.1) * 10) / 10;
artPlayerRef.current.notice.show = `音量: ${Math.round( if (artPlayerRef.current.notice) {
artPlayerRef.current.volume * 100 artPlayerRef.current.notice.show = `音量: ${Math.round(
)}`; artPlayerRef.current.volume * 100
)}`;
}
e.preventDefault(); e.preventDefault();
} }
} }
// 下箭头 = 音量- // 下箭头 = 音量-
if (e.key === 'ArrowDown') { if (e.key === 'ArrowDown') {
if (artPlayerRef.current && artPlayerRef.current.volume > 0) { if (isComponentMountedRef.current && artPlayerRef.current && artPlayerRef.current.volume > 0) {
artPlayerRef.current.volume = artPlayerRef.current.volume =
Math.round((artPlayerRef.current.volume - 0.1) * 10) / 10; Math.round((artPlayerRef.current.volume - 0.1) * 10) / 10;
artPlayerRef.current.notice.show = `音量: ${Math.round( if (artPlayerRef.current.notice) {
artPlayerRef.current.volume * 100 artPlayerRef.current.notice.show = `音量: ${Math.round(
)}`; artPlayerRef.current.volume * 100
)}`;
}
e.preventDefault(); e.preventDefault();
} }
} }
// 空格 = 播放/暂停 // 空格 = 播放/暂停
if (e.key === ' ') { if (e.key === ' ') {
if (artPlayerRef.current) { if (isComponentMountedRef.current && artPlayerRef.current) {
artPlayerRef.current.toggle(); artPlayerRef.current.toggle();
e.preventDefault(); e.preventDefault();
} }
@ -1179,7 +1249,7 @@ function PlayPageClient() {
// f 键 = 切换全屏 // f 键 = 切换全屏
if (e.key === 'f' || e.key === 'F') { if (e.key === 'f' || e.key === 'F') {
if (artPlayerRef.current) { if (isComponentMountedRef.current && artPlayerRef.current) {
artPlayerRef.current.fullscreen = !artPlayerRef.current.fullscreen; artPlayerRef.current.fullscreen = !artPlayerRef.current.fullscreen;
e.preventDefault(); e.preventDefault();
} }
@ -1191,6 +1261,11 @@ function PlayPageClient() {
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// 保存播放进度 // 保存播放进度
const saveCurrentPlayProgress = async () => { const saveCurrentPlayProgress = async () => {
// 检查组件是否已卸载
if (!isComponentMountedRef.current) {
return;
}
if ( if (
!artPlayerRef.current || !artPlayerRef.current ||
!currentSourceRef.current || !currentSourceRef.current ||
@ -1266,7 +1341,7 @@ function PlayPageClient() {
window.removeEventListener('beforeunload', handleBeforeUnload); window.removeEventListener('beforeunload', handleBeforeUnload);
document.removeEventListener('visibilitychange', handleVisibilityChange); document.removeEventListener('visibilitychange', handleVisibilityChange);
}; };
}, [currentEpisodeIndex, detail, artPlayerRef.current]); }, [currentEpisodeIndex, detail]);
// 清理定时器 // 清理定时器
useEffect(() => { useEffect(() => {
@ -1378,14 +1453,16 @@ function PlayPageClient() {
// 非WebKit浏览器且播放器已存在使用switch方法切换 // 非WebKit浏览器且播放器已存在使用switch方法切换
if (!isWebkit && artPlayerRef.current) { if (!isWebkit && artPlayerRef.current) {
artPlayerRef.current.switch = videoUrl; const finalVideoUrl = getProxyUrl(videoUrl);
artPlayerRef.current.switch = finalVideoUrl;
artPlayerRef.current.title = `${videoTitle} - 第${currentEpisodeIndex + 1 artPlayerRef.current.title = `${videoTitle} - 第${currentEpisodeIndex + 1
}`; }`;
artPlayerRef.current.poster = videoCover; artPlayerRef.current.poster = videoCover;
if (artPlayerRef.current?.video) { if (artPlayerRef.current?.video) {
ensureVideoSource( ensureVideoSource(
artPlayerRef.current.video as HTMLVideoElement, artPlayerRef.current.video as HTMLVideoElement,
videoUrl finalVideoUrl
); );
} }
return; return;
@ -1401,9 +1478,11 @@ function PlayPageClient() {
Artplayer.PLAYBACK_RATE = [0.5, 0.75, 1, 1.25, 1.5, 2, 3]; Artplayer.PLAYBACK_RATE = [0.5, 0.75, 1, 1.25, 1.5, 2, 3];
Artplayer.USE_RAF = true; Artplayer.USE_RAF = true;
const finalVideoUrl = getProxyUrl(videoUrl);
artPlayerRef.current = new Artplayer({ artPlayerRef.current = new Artplayer({
container: artRef.current, container: artRef.current,
url: videoUrl, url: finalVideoUrl,
poster: videoCover, poster: videoCover,
volume: 0.7, volume: 0.7,
isLive: false, isLive: false,
@ -1440,9 +1519,20 @@ function PlayPageClient() {
artplayerPluginDanmuku({ artplayerPluginDanmuku({
danmuku: async () => { danmuku: async () => {
try { try {
// 检查组件是否已卸载
if (!isComponentMountedRef.current || !artPlayerRef.current) {
return [];
}
// 生成弹幕唯一ID基于视频源和ID // 生成弹幕唯一ID基于视频源和ID
const videoId = `${currentSource}-${currentId}`; const videoId = `${currentSource}-${currentId}`;
const response = await fetch(`/api/danmu?videoId=${encodeURIComponent(videoId)}`); const response = await fetch(`/api/danmu?videoId=${encodeURIComponent(videoId)}`);
// 再次检查组件是否已卸载
if (!isComponentMountedRef.current || !artPlayerRef.current) {
return [];
}
if (response.ok) { if (response.ok) {
const data = await response.json(); const data = await response.json();
return data; return data;
@ -1460,17 +1550,25 @@ function PlayPageClient() {
mode: 0, // 默认模式0-滚动1-顶部2-底部 mode: 0, // 默认模式0-滚动1-顶部2-底部
margin: [10, '20%'], // 弹幕上下边距 margin: [10, '20%'], // 弹幕上下边距
antiOverlap: true, // 防重叠 antiOverlap: true, // 防重叠
filter: (danmu: any) => danmu.text.length <= 100, // 弹幕过滤 filter: (danmu: any) => danmu?.text && danmu.text.length <= 100, // 弹幕过滤
theme: 'dark', // 输入框主题 theme: 'dark', // 输入框主题
beforeEmit: async (danmu: any) => { beforeEmit: async (danmu: any) => {
try { try {
// 验证弹幕内容 // 验证弹幕内容
if (!danmu.text || !danmu.text.trim()) { if (!danmu?.text || !danmu.text.trim()) {
return false;
}
// 检查组件挂载状态和播放器是否仍然存在
const currentPlayer = artPlayerRef.current;
if (!isComponentMountedRef.current || !currentPlayer) {
return false; return false;
} }
// 发送弹幕到服务器 // 发送弹幕到服务器
const videoId = `${currentSource}-${currentId}`; const videoId = `${currentSource}-${currentId}`;
const currentTime = currentPlayer.currentTime || 0;
const response = await fetch('/api/danmu', { const response = await fetch('/api/danmu', {
method: 'POST', method: 'POST',
headers: { headers: {
@ -1481,10 +1579,16 @@ function PlayPageClient() {
text: danmu.text.trim(), text: danmu.text.trim(),
color: danmu.color || '#FFFFFF', color: danmu.color || '#FFFFFF',
mode: danmu.mode || 0, mode: danmu.mode || 0,
time: artPlayerRef.current?.currentTime || 0, time: currentTime,
}), }),
}); });
// 再次检查组件挂载状态和播放器是否仍然存在
const playerAfterRequest = artPlayerRef.current;
if (!isComponentMountedRef.current || !playerAfterRequest) {
return false;
}
if (response.ok) { if (response.ok) {
const result = await response.json(); const result = await response.json();
if (result.success) { if (result.success) {
@ -1494,15 +1598,16 @@ function PlayPageClient() {
} else { } else {
const error = await response.json(); const error = await response.json();
console.error('发送弹幕失败:', error.error); console.error('发送弹幕失败:', error.error);
if (artPlayerRef.current) { if (playerAfterRequest && typeof playerAfterRequest.notice?.show !== 'undefined') {
artPlayerRef.current.notice.show = `发送失败: ${error.error}`; playerAfterRequest.notice.show = `发送失败: ${error.error}`;
} }
} }
return false; return false;
} catch (error) { } catch (error) {
console.error('发送弹幕出错:', error); console.error('发送弹幕出错:', error);
if (artPlayerRef.current) { const currentPlayer = artPlayerRef.current;
artPlayerRef.current.notice.show = '发送弹幕失败'; if (isComponentMountedRef.current && currentPlayer && typeof currentPlayer.notice?.show !== 'undefined') {
currentPlayer.notice.show = '发送弹幕失败';
} }
return false; return false;
} }
@ -1515,14 +1620,8 @@ function PlayPageClient() {
console.log('Loading MP4 video:', url); console.log('Loading MP4 video:', url);
// 对于需要代理的视频文件通过代理API来避免403错误 // 对于需要代理的视频文件通过代理API来避免403错误
const needsProxy = url.includes('quark.cn') || if (needsProxyUrl(url)) {
url.includes('drive.quark.cn') || console.log('Using proxy for MP4 video URL:', url);
url.includes('dl-c-zb-') ||
url.includes('dl-c-') ||
url.match(/https?:\/\/[^/]*\.drive\./);
if (needsProxy) {
console.log('Using proxy for video URL:', url);
// 先测试URL的可达性 // 先测试URL的可达性
try { try {
@ -1534,7 +1633,7 @@ function PlayPageClient() {
console.warn('URL test failed, proceeding with proxy anyway:', testError); console.warn('URL test failed, proceeding with proxy anyway:', testError);
} }
const proxyUrl = `/api/proxy/video?url=${encodeURIComponent(url)}`; const proxyUrl = getProxyUrl(url);
// 设置视频元素的属性 // 设置视频元素的属性
video.crossOrigin = 'anonymous'; video.crossOrigin = 'anonymous';
@ -1584,8 +1683,17 @@ function PlayPageClient() {
} }
if (video.hls) { if (video.hls) {
video.hls.destroy(); try {
video.hls.destroy();
} catch (err) {
console.warn('销毁旧HLS实例时出错:', err);
}
video.hls = null;
} }
// 检查是否需要使用代理URL
const finalUrl = getProxyUrl(url);
const hls = new Hls({ const hls = new Hls({
debug: false, // 关闭日志 debug: false, // 关闭日志
enableWorker: true, // WebWorker 解码,降低主线程压力 enableWorker: true, // WebWorker 解码,降低主线程压力
@ -1602,11 +1710,11 @@ function PlayPageClient() {
: Hls.DefaultConfig.loader, : Hls.DefaultConfig.loader,
}); });
hls.loadSource(url); hls.loadSource(finalUrl);
hls.attachMedia(video); hls.attachMedia(video);
video.hls = hls; video.hls = hls;
ensureVideoSource(video, url); ensureVideoSource(video, finalUrl);
hls.on(Hls.Events.ERROR, function (event: any, data: any) { hls.on(Hls.Events.ERROR, function (event: any, data: any) {
console.error('HLS Error:', event, data); console.error('HLS Error:', event, data);
@ -1622,7 +1730,11 @@ function PlayPageClient() {
break; break;
default: default:
console.log('无法恢复的错误'); console.log('无法恢复的错误');
hls.destroy(); try {
hls.destroy();
} catch (destroyErr) {
console.warn('销毁HLS时出错:', destroyErr);
}
break; break;
} }
} }
@ -1644,14 +1756,22 @@ function PlayPageClient() {
localStorage.setItem('enable_blockad', String(newVal)); localStorage.setItem('enable_blockad', String(newVal));
if (artPlayerRef.current) { if (artPlayerRef.current) {
resumeTimeRef.current = artPlayerRef.current.currentTime; resumeTimeRef.current = artPlayerRef.current.currentTime;
if ( const player = artPlayerRef.current;
artPlayerRef.current.video && artPlayerRef.current = null; // 先清空引用
artPlayerRef.current.video.hls
) { if (player.video && player.video.hls) {
artPlayerRef.current.video.hls.destroy(); try {
player.video.hls.destroy();
} catch (err) {
console.warn('清理HLS实例失败:', err);
}
}
try {
player.destroy();
} catch (err) {
console.warn('销毁播放器失败:', err);
} }
artPlayerRef.current.destroy();
artPlayerRef.current = null;
} }
setBlockAdEnabled(newVal); setBlockAdEnabled(newVal);
} catch (_) { } catch (_) {
@ -1693,6 +1813,7 @@ function PlayPageClient() {
? '设置片头时间' ? '设置片头时间'
: `${formatTime(skipConfigRef.current.intro_time)}`, : `${formatTime(skipConfigRef.current.intro_time)}`,
onClick: function () { onClick: function () {
if (!isComponentMountedRef.current || !artPlayerRef.current) return;
const currentTime = artPlayerRef.current?.currentTime || 0; const currentTime = artPlayerRef.current?.currentTime || 0;
if (currentTime > 0) { if (currentTime > 0) {
const newConfig = { const newConfig = {
@ -1713,6 +1834,7 @@ function PlayPageClient() {
? '设置片尾时间' ? '设置片尾时间'
: `-${formatTime(-skipConfigRef.current.outro_time)}`, : `-${formatTime(-skipConfigRef.current.outro_time)}`,
onClick: function () { onClick: function () {
if (!isComponentMountedRef.current || !artPlayerRef.current) return;
const outroTime = const outroTime =
-( -(
artPlayerRef.current?.duration - artPlayerRef.current?.duration -
@ -1755,15 +1877,18 @@ function PlayPageClient() {
// 监听播放状态变化,控制 Wake Lock // 监听播放状态变化,控制 Wake Lock
artPlayerRef.current.on('play', () => { artPlayerRef.current.on('play', () => {
if (!isComponentMountedRef.current) return;
requestWakeLock(); requestWakeLock();
}); });
artPlayerRef.current.on('pause', () => { artPlayerRef.current.on('pause', () => {
if (!isComponentMountedRef.current) return;
releaseWakeLock(); releaseWakeLock();
saveCurrentPlayProgress(); saveCurrentPlayProgress();
}); });
artPlayerRef.current.on('video:ended', () => { artPlayerRef.current.on('video:ended', () => {
if (!isComponentMountedRef.current) return;
releaseWakeLock(); releaseWakeLock();
}); });
@ -1773,14 +1898,18 @@ function PlayPageClient() {
} }
artPlayerRef.current.on('video:volumechange', () => { artPlayerRef.current.on('video:volumechange', () => {
if (!isComponentMountedRef.current || !artPlayerRef.current) return;
lastVolumeRef.current = artPlayerRef.current.volume; lastVolumeRef.current = artPlayerRef.current.volume;
}); });
artPlayerRef.current.on('video:ratechange', () => { artPlayerRef.current.on('video:ratechange', () => {
if (!isComponentMountedRef.current || !artPlayerRef.current) return;
lastPlaybackRateRef.current = artPlayerRef.current.playbackRate; lastPlaybackRateRef.current = artPlayerRef.current.playbackRate;
}); });
// 监听视频可播放事件,这时恢复播放进度更可靠 // 监听视频可播放事件,这时恢复播放进度更可靠
artPlayerRef.current.on('video:canplay', () => { artPlayerRef.current.on('video:canplay', () => {
if (!isComponentMountedRef.current || !artPlayerRef.current) return;
// 若存在需要恢复的播放进度,则跳转 // 若存在需要恢复的播放进度,则跳转
if (resumeTimeRef.current && resumeTimeRef.current > 0) { if (resumeTimeRef.current && resumeTimeRef.current > 0) {
try { try {
@ -1798,6 +1927,8 @@ function PlayPageClient() {
resumeTimeRef.current = null; resumeTimeRef.current = null;
setTimeout(() => { setTimeout(() => {
if (!isComponentMountedRef.current || !artPlayerRef.current) return;
if ( if (
Math.abs(artPlayerRef.current.volume - lastVolumeRef.current) > 0.01 Math.abs(artPlayerRef.current.volume - lastVolumeRef.current) > 0.01
) { ) {
@ -1811,7 +1942,9 @@ function PlayPageClient() {
) { ) {
artPlayerRef.current.playbackRate = lastPlaybackRateRef.current; artPlayerRef.current.playbackRate = lastPlaybackRateRef.current;
} }
artPlayerRef.current.notice.show = ''; if (artPlayerRef.current.notice) {
artPlayerRef.current.notice.show = '';
}
}, 0); }, 0);
// 隐藏换源加载状态 // 隐藏换源加载状态
@ -1820,7 +1953,7 @@ function PlayPageClient() {
// 监听视频时间更新事件,实现跳过片头片尾 // 监听视频时间更新事件,实现跳过片头片尾
artPlayerRef.current.on('video:timeupdate', () => { artPlayerRef.current.on('video:timeupdate', () => {
if (!skipConfigRef.current.enable) return; if (!isComponentMountedRef.current || !artPlayerRef.current || !skipConfigRef.current.enable) return;
const currentTime = artPlayerRef.current.currentTime || 0; const currentTime = artPlayerRef.current.currentTime || 0;
const duration = artPlayerRef.current.duration || 0; const duration = artPlayerRef.current.duration || 0;
@ -1864,6 +1997,9 @@ function PlayPageClient() {
artPlayerRef.current.on('error', (err: any) => { artPlayerRef.current.on('error', (err: any) => {
console.error('播放器错误:', err); console.error('播放器错误:', err);
if (!isComponentMountedRef.current || !artPlayerRef.current) {
return;
}
if (artPlayerRef.current.currentTime > 0) { if (artPlayerRef.current.currentTime > 0) {
return; return;
} }
@ -1871,16 +2007,22 @@ function PlayPageClient() {
// 监听视频播放结束事件,自动播放下一集 // 监听视频播放结束事件,自动播放下一集
artPlayerRef.current.on('video:ended', () => { artPlayerRef.current.on('video:ended', () => {
if (!isComponentMountedRef.current) return;
const d = detailRef.current; const d = detailRef.current;
const idx = currentEpisodeIndexRef.current; const idx = currentEpisodeIndexRef.current;
if (d && d.episodes && idx < d.episodes.length - 1) { if (d && d.episodes && idx < d.episodes.length - 1) {
setTimeout(() => { setTimeout(() => {
setCurrentEpisodeIndex(idx + 1); if (isComponentMountedRef.current) {
setCurrentEpisodeIndex(idx + 1);
}
}, 1000); }, 1000);
} }
}); });
artPlayerRef.current.on('video:timeupdate', () => { artPlayerRef.current.on('video:timeupdate', () => {
if (!isComponentMountedRef.current || !artPlayerRef.current) return;
const now = Date.now(); const now = Date.now();
let interval = 5000; let interval = 5000;
if (process.env.NEXT_PUBLIC_STORAGE_TYPE === 'upstash') { if (process.env.NEXT_PUBLIC_STORAGE_TYPE === 'upstash') {
@ -1893,6 +2035,7 @@ function PlayPageClient() {
}); });
artPlayerRef.current.on('pause', () => { artPlayerRef.current.on('pause', () => {
if (!isComponentMountedRef.current) return;
saveCurrentPlayProgress(); saveCurrentPlayProgress();
}); });
@ -1908,12 +2051,23 @@ function PlayPageClient() {
} }
}, [Artplayer, Hls, videoUrl, loading, blockAdEnabled]); }, [Artplayer, Hls, videoUrl, loading, blockAdEnabled]);
// 组件卸载时清理定时器、Wake Lock 和播放器资源 // 组件挂载时的初始化和卸载时清理
useEffect(() => { useEffect(() => {
// 组件挂载时,确保先清理任何可能残留的资源
console.log('播放页面组件挂载,进行初始化清理');
cleanupPlayer();
isComponentMountedRef.current = true;
return () => { return () => {
// 标记组件已卸载
isComponentMountedRef.current = false;
console.log('播放页面组件卸载,进行资源清理');
// 清理定时器 // 清理定时器
if (saveIntervalRef.current) { if (saveIntervalRef.current) {
clearInterval(saveIntervalRef.current); clearInterval(saveIntervalRef.current);
saveIntervalRef.current = null;
} }
// 释放 Wake Lock // 释放 Wake Lock

View File

@ -130,10 +130,20 @@ const EpisodeSelector: React.FC<EpisodeSelectorProps> = ({
const info = await getVideoResolutionFromM3u8(episodeUrl); const info = await getVideoResolutionFromM3u8(episodeUrl);
setVideoInfoMap((prev) => new Map(prev).set(sourceKey, info)); setVideoInfoMap((prev) => new Map(prev).set(sourceKey, info));
} catch (error) { } catch (error) {
// 失败时保存错误状态 // 失败时保存错误状态,区分不同的错误类型
const errorMessage = error instanceof Error ? error.message : String(error);
const isNetworkRestricted = errorMessage.includes('Network access restricted') ||
errorMessage.includes('CORS') ||
errorMessage.includes('Forbidden');
// 只在开发环境下打印详细错误
if (process.env.NODE_ENV === 'development') {
console.warn(`Video info fetch failed for ${sourceKey}:`, errorMessage);
}
setVideoInfoMap((prev) => setVideoInfoMap((prev) =>
new Map(prev).set(sourceKey, { new Map(prev).set(sourceKey, {
quality: '错误', quality: isNetworkRestricted ? '受限' : '未知',
loadSpeed: '未知', loadSpeed: '未知',
pingTime: 0, pingTime: 0,
hasError: true, hasError: true,

View File

@ -131,9 +131,9 @@ const VideoCard = forwardRef<VideoCardHandle, VideoCardProps>(function VideoCard
? (actualEpisodes && actualEpisodes === 1 ? 'movie' : 'tv') ? (actualEpisodes && actualEpisodes === 1 ? 'movie' : 'tv')
: type; : type;
// 获取收藏状态(搜索结果页面不检查) // 获取收藏状态(搜索结果、豆瓣和短剧页面不检查)
useEffect(() => { useEffect(() => {
if (from === 'douban' || from === 'search' || !actualSource || !actualId) return; if (from === 'douban' || from === 'search' || from === 'shortdrama' || !actualSource || !actualId) return;
const fetchFavoriteStatus = async () => { const fetchFavoriteStatus = async () => {
try { try {
@ -164,7 +164,7 @@ const VideoCard = forwardRef<VideoCardHandle, VideoCardProps>(function VideoCard
async (e: React.MouseEvent) => { async (e: React.MouseEvent) => {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
if (from === 'douban' || !actualSource || !actualId) return; if (from === 'douban' || from === 'shortdrama' || !actualSource || !actualId) return;
try { try {
// 确定当前收藏状态 // 确定当前收藏状态
@ -375,7 +375,7 @@ const VideoCard = forwardRef<VideoCardHandle, VideoCardProps>(function VideoCard
showSourceName: true, showSourceName: true,
showProgress: false, showProgress: false,
showPlayButton: true, showPlayButton: true,
showHeart: true, showHeart: false, // 短剧不显示收藏功能
showCheckCircle: false, showCheckCircle: false,
showDoubanLink: false, showDoubanLink: false,
showRating: !!rate, showRating: !!rate,
@ -412,7 +412,7 @@ const VideoCard = forwardRef<VideoCardHandle, VideoCardProps>(function VideoCard
// 聚合源信息 - 直接在菜单中展示,不需要单独的操作项 // 聚合源信息 - 直接在菜单中展示,不需要单独的操作项
// 收藏/取消收藏操作 // 收藏/取消收藏操作
if (config.showHeart && from !== 'douban' && actualSource && actualId) { if (config.showHeart && from !== 'douban' && from !== 'shortdrama' && actualSource && actualId) {
const currentFavorited = from === 'search' ? searchFavorited : favorited; const currentFavorited = from === 'search' ? searchFavorited : favorited;
if (from === 'search') { if (from === 'search') {
@ -680,7 +680,7 @@ const VideoCard = forwardRef<VideoCardHandle, VideoCardProps>(function VideoCard
}} }}
/> />
)} )}
{config.showHeart && from !== 'search' && ( {config.showHeart && from !== 'search' && from !== 'shortdrama' && (
<Heart <Heart
onClick={handleToggleFavorite} onClick={handleToggleFavorite}
size={20} size={20}

View File

@ -72,18 +72,38 @@ export async function getVideoResolutionFromM3u8(m3u8Url: string): Promise<{
pingTime: number; // 网络延迟(毫秒) pingTime: number; // 网络延迟(毫秒)
}> { }> {
try { try {
// 直接使用m3u8 URL作为视频源避免CORS问题 // 检查是否需要使用代理
const needsProxy = m3u8Url.includes('quark.cn') ||
m3u8Url.includes('drive.quark.cn') ||
m3u8Url.includes('dl-c-zb-') ||
m3u8Url.includes('dl-c-') ||
m3u8Url.match(/https?:\/\/[^/]*\.drive\./) ||
// 添加更多可能需要代理的域名
m3u8Url.includes('ffzy-online') ||
m3u8Url.includes('bfikuncdn.com') ||
m3u8Url.includes('vip.') ||
!m3u8Url.includes('localhost');
const finalM3u8Url = needsProxy
? `/api/proxy/video?url=${encodeURIComponent(m3u8Url)}`
: m3u8Url;
if (needsProxy) {
console.log('Using proxy for M3U8 resolution detection:', m3u8Url);
}
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const video = document.createElement('video'); const video = document.createElement('video');
video.muted = true; video.muted = true;
video.preload = 'metadata'; video.preload = 'metadata';
// 测量网络延迟ping时间 - 使用m3u8 URL而不是ts文件 // 测量网络延迟ping时间
const pingStart = performance.now(); const pingStart = performance.now();
let pingTime = 0; let pingTime = 0;
// 测量ping时间使用m3u8 URL // 测量ping时间如果使用代理则测试代理URL的响应时间
fetch(m3u8Url, { method: 'HEAD', mode: 'no-cors' }) const pingUrl = needsProxy ? `/api/proxy/video/test?url=${encodeURIComponent(m3u8Url)}` : m3u8Url;
fetch(pingUrl, { method: 'HEAD', mode: needsProxy ? 'cors' : 'no-cors' })
.then(() => { .then(() => {
pingTime = performance.now() - pingStart; pingTime = performance.now() - pingStart;
}) })
@ -189,17 +209,31 @@ export async function getVideoResolutionFromM3u8(m3u8Url: string): Promise<{
} }
}); });
hls.loadSource(m3u8Url); hls.loadSource(finalM3u8Url);
hls.attachMedia(video); hls.attachMedia(video);
// 监听hls.js错误 // 监听hls.js错误
hls.on(Hls.Events.ERROR, (event: any, data: any) => { hls.on(Hls.Events.ERROR, (event: any, data: any) => {
console.error('HLS错误:', data); // 只在开发环境下打印详细错误,生产环境下简化错误信息
if (process.env.NODE_ENV === 'development') {
console.warn('Video resolution detection failed:', {
url: needsProxy ? 'via proxy' : m3u8Url,
error: data.details,
type: data.type
});
}
if (data.fatal) { if (data.fatal) {
clearTimeout(timeout); clearTimeout(timeout);
hls.destroy(); hls.destroy();
video.remove(); video.remove();
reject(new Error(`HLS播放失败: ${data.type}`));
// 对于CORS相关错误提供更友好的错误信息
if (data.details === 'manifestLoadError' || data.type === 'networkError') {
reject(new Error('Network access restricted'));
} else {
reject(new Error(`Video analysis failed: ${data.type}`));
}
} }
}); });