diff --git a/src/app/live/page.tsx b/src/app/live/page.tsx index b103075..a55bda6 100644 --- a/src/app/live/page.tsx +++ b/src/app/live/page.tsx @@ -241,6 +241,33 @@ function LivePageClient() { return cleanedPrograms; }; + // Anime4K超分相关状态 + const [webGPUSupported, setWebGPUSupported] = useState(false); + const [anime4kEnabled, setAnime4kEnabled] = useState(false); + const [anime4kMode, setAnime4kMode] = useState(() => { + if (typeof window !== 'undefined') { + const v = localStorage.getItem('anime4k_mode'); + if (v !== null) return v; + } + return 'ModeA'; + }); + const [anime4kScale, setAnime4kScale] = useState(() => { + if (typeof window !== 'undefined') { + const v = localStorage.getItem('anime4k_scale'); + if (v !== null) return parseFloat(v); + } + return 2.0; + }); + const anime4kRef = useRef(null); + const anime4kEnabledRef = useRef(anime4kEnabled); + const anime4kModeRef = useRef(anime4kMode); + const anime4kScaleRef = useRef(anime4kScale); + useEffect(() => { + anime4kEnabledRef.current = anime4kEnabled; + anime4kModeRef.current = anime4kMode; + anime4kScaleRef.current = anime4kScale; + }, [anime4kEnabled, anime4kMode, anime4kScale]); + // 播放器引用 const artPlayerRef = useRef(null); const artRef = useRef(null); @@ -594,11 +621,305 @@ function LivePageClient() { } }; + // 初始化Anime4K超分 + const initAnime4K = async () => { + if (!artPlayerRef.current?.video) return; + + let frameRequestId: number | null = null; + let outputCanvas: HTMLCanvasElement | null = null; + + try { + if (anime4kRef.current) { + anime4kRef.current.stop?.(); + anime4kRef.current = null; + } + + const video = artPlayerRef.current.video as HTMLVideoElement; + + // 等待视频元数据加载完成 + if (!video.videoWidth || !video.videoHeight) { + console.warn('视频尺寸未就绪,等待loadedmetadata事件'); + await new Promise((resolve) => { + const handler = () => { + video.removeEventListener('loadedmetadata', handler); + resolve(); + }; + video.addEventListener('loadedmetadata', handler); + if (video.videoWidth && video.videoHeight) { + video.removeEventListener('loadedmetadata', handler); + resolve(); + } + }); + } + + if (!video.videoWidth || !video.videoHeight) { + throw new Error('无法获取视频尺寸'); + } + + // 检测是否为Firefox + const isFirefox = navigator.userAgent.toLowerCase().includes('firefox'); + + // 创建输出canvas + outputCanvas = document.createElement('canvas'); + const container = artPlayerRef.current.template.$video.parentElement; + + const scale = anime4kScaleRef.current; + outputCanvas.width = Math.floor(video.videoWidth * scale); + outputCanvas.height = Math.floor(video.videoHeight * scale); + + if (!outputCanvas.width || !outputCanvas.height || + !isFinite(outputCanvas.width) || !isFinite(outputCanvas.height)) { + throw new Error(`outputCanvas尺寸无效: ${outputCanvas.width}x${outputCanvas.height}`); + } + + outputCanvas.style.position = 'absolute'; + outputCanvas.style.top = '0'; + outputCanvas.style.left = '0'; + outputCanvas.style.width = '100%'; + outputCanvas.style.height = '100%'; + outputCanvas.style.objectFit = 'contain'; + outputCanvas.style.cursor = 'pointer'; + outputCanvas.style.zIndex = '1'; + outputCanvas.style.backgroundColor = 'transparent'; + + // Firefox兼容性处理 + let sourceCanvas: HTMLCanvasElement | null = null; + let sourceCtx: CanvasRenderingContext2D | null = null; + + if (isFirefox) { + sourceCanvas = document.createElement('canvas'); + const canvasW = Math.floor(video.videoWidth); + const canvasH = Math.floor(video.videoHeight); + sourceCanvas.width = canvasW; + sourceCanvas.height = canvasH; + + if (!sourceCanvas.width || !sourceCanvas.height) { + throw new Error(`sourceCanvas尺寸无效: ${sourceCanvas.width}x${sourceCanvas.height}`); + } + + sourceCtx = sourceCanvas.getContext('2d', { + willReadFrequently: true, + alpha: false + }); + + if (!sourceCtx) { + throw new Error('无法创建2D上下文'); + } + + if (video.readyState >= video.HAVE_CURRENT_DATA) { + sourceCtx.drawImage(video, 0, 0, sourceCanvas.width, sourceCanvas.height); + } + } + + // 监听点击和双击事件 + const handleCanvasClick = () => { + if (artPlayerRef.current) { + artPlayerRef.current.toggle(); + } + }; + const handleCanvasDblClick = () => { + if (artPlayerRef.current) { + artPlayerRef.current.fullscreen = !artPlayerRef.current.fullscreen; + } + }; + outputCanvas.addEventListener('click', handleCanvasClick); + outputCanvas.addEventListener('dblclick', handleCanvasDblClick); + + // 隐藏原始video + video.style.opacity = '0'; + video.style.pointerEvents = 'none'; + video.style.position = 'absolute'; + video.style.zIndex = '-1'; + + container.insertBefore(outputCanvas, video); + + // Firefox视频帧捕获 + if (isFirefox && sourceCtx && sourceCanvas) { + const captureVideoFrame = () => { + if (sourceCtx && sourceCanvas && video.readyState >= video.HAVE_CURRENT_DATA) { + sourceCtx.drawImage(video, 0, 0, sourceCanvas.width, sourceCanvas.height); + } + frameRequestId = requestAnimationFrame(captureVideoFrame); + }; + captureVideoFrame(); + } + + // 动态导入anime4k-webgpu + const { render: anime4kRender, ModeA, ModeB, ModeC, ModeAA, ModeBB, ModeCA } = await import('anime4k-webgpu'); + + let ModeClass: any; + const modeName = anime4kModeRef.current; + + switch (modeName) { + case 'ModeA': ModeClass = ModeA; break; + case 'ModeB': ModeClass = ModeB; break; + case 'ModeC': ModeClass = ModeC; break; + case 'ModeAA': ModeClass = ModeAA; break; + case 'ModeBB': ModeClass = ModeBB; break; + case 'ModeCA': ModeClass = ModeCA; break; + default: ModeClass = ModeA; + } + + const renderConfig: any = { + video: isFirefox ? sourceCanvas : video, + canvas: outputCanvas, + pipelineBuilder: (device: GPUDevice, inputTexture: GPUTexture) => { + if (!outputCanvas) { + throw new Error('outputCanvas is null in pipelineBuilder'); + } + const mode = new ModeClass({ + device, + inputTexture, + nativeDimensions: { + width: Math.floor(video.videoWidth), + height: Math.floor(video.videoHeight), + }, + targetDimensions: { + width: Math.floor(outputCanvas.width), + height: Math.floor(outputCanvas.height), + }, + }); + return [mode]; + }, + }; + + const controller = await anime4kRender(renderConfig); + + anime4kRef.current = { + controller, + canvas: outputCanvas, + sourceCanvas: isFirefox ? sourceCanvas : null, + frameRequestId: isFirefox ? frameRequestId : null, + handleCanvasClick, + handleCanvasDblClick, + }; + + console.log('Anime4K超分已启用,模式:', anime4kModeRef.current, '倍数:', scale); + if (artPlayerRef.current) { + artPlayerRef.current.notice.show = `超分已启用 (${anime4kModeRef.current}, ${scale}x)`; + } + } catch (err) { + console.error('初始化Anime4K失败:', err); + if (artPlayerRef.current) { + artPlayerRef.current.notice.show = '超分启用失败:' + (err instanceof Error ? err.message : '未知错误'); + } + + if (frameRequestId) { + cancelAnimationFrame(frameRequestId); + } + + if (outputCanvas && outputCanvas.parentNode) { + outputCanvas.parentNode.removeChild(outputCanvas); + } + + if (artPlayerRef.current?.video) { + artPlayerRef.current.video.style.opacity = '1'; + artPlayerRef.current.video.style.pointerEvents = 'auto'; + artPlayerRef.current.video.style.position = ''; + artPlayerRef.current.video.style.zIndex = ''; + } + } + }; + + // 清理Anime4K + const cleanupAnime4K = async () => { + if (anime4kRef.current) { + try { + if (anime4kRef.current.frameRequestId) { + cancelAnimationFrame(anime4kRef.current.frameRequestId); + } + + anime4kRef.current.controller?.stop?.(); + + if (anime4kRef.current.canvas) { + if (anime4kRef.current.handleCanvasClick) { + anime4kRef.current.canvas.removeEventListener('click', anime4kRef.current.handleCanvasClick); + } + if (anime4kRef.current.handleCanvasDblClick) { + anime4kRef.current.canvas.removeEventListener('dblclick', anime4kRef.current.handleCanvasDblClick); + } + } + + if (anime4kRef.current.canvas && anime4kRef.current.canvas.parentNode) { + anime4kRef.current.canvas.parentNode.removeChild(anime4kRef.current.canvas); + } + + if (anime4kRef.current.sourceCanvas) { + const ctx = anime4kRef.current.sourceCanvas.getContext('2d'); + if (ctx) { + ctx.clearRect(0, 0, anime4kRef.current.sourceCanvas.width, anime4kRef.current.sourceCanvas.height); + } + } + + anime4kRef.current = null; + + if (artPlayerRef.current?.video) { + artPlayerRef.current.video.style.opacity = '1'; + artPlayerRef.current.video.style.pointerEvents = 'auto'; + artPlayerRef.current.video.style.position = ''; + artPlayerRef.current.video.style.zIndex = ''; + } + + console.log('Anime4K已清理'); + } catch (err) { + console.warn('清理Anime4K时出错:', err); + } + } + }; + + // 切换Anime4K状态 + const toggleAnime4K = async (enabled: boolean) => { + try { + if (enabled) { + await initAnime4K(); + } else { + await cleanupAnime4K(); + } + setAnime4kEnabled(enabled); + localStorage.setItem('enable_anime4k', String(enabled)); + } catch (err) { + console.error('切换超分状态失败:', err); + } + }; + + // 更改Anime4K模式 + const changeAnime4KMode = async (mode: string) => { + try { + setAnime4kMode(mode); + localStorage.setItem('anime4k_mode', mode); + + if (anime4kEnabledRef.current) { + await cleanupAnime4K(); + await initAnime4K(); + } + } catch (err) { + console.error('更改超分模式失败:', err); + } + }; + + // 更改Anime4K分辨率倍数 + const changeAnime4KScale = async (scale: number) => { + try { + setAnime4kScale(scale); + localStorage.setItem('anime4k_scale', scale.toString()); + + if (anime4kEnabledRef.current) { + await cleanupAnime4K(); + await initAnime4K(); + } + } catch (err) { + console.error('更改超分倍数失败:', err); + } + }; + // 清理播放器资源的统一函数 const cleanupPlayer = () => { // 重置不支持的类型状态 setUnsupportedType(null); + // 清理Anime4K + cleanupAnime4K(); + if (artPlayerRef.current) { try { // 先暂停播放 @@ -789,6 +1110,34 @@ function LivePageClient() { } }; + // 检测WebGPU支持 + useEffect(() => { + const checkWebGPUSupport = async () => { + if (typeof navigator === 'undefined' || !('gpu' in navigator)) { + setWebGPUSupported(false); + console.log('WebGPU不支持:浏览器不支持WebGPU API'); + return; + } + + try { + const adapter = await (navigator as any).gpu.requestAdapter(); + if (!adapter) { + setWebGPUSupported(false); + console.log('WebGPU不支持:无法获取GPU适配器'); + return; + } + + setWebGPUSupported(true); + console.log('WebGPU支持检测:✅ 支持'); + } catch (err) { + setWebGPUSupported(false); + console.log('WebGPU不支持:', err); + } + }; + + checkWebGPUSupport(); + }, []); + // 初始化 useEffect(() => { fetchLiveSources(); @@ -1006,7 +1355,7 @@ function LivePageClient() { autoSize: false, autoMini: false, screenshot: false, - setting: false, + setting: true, loop: false, flip: false, playbackRate: false, @@ -1035,6 +1384,91 @@ function LivePageClient() { loading: '', }, + settings: [ + ...(webGPUSupported ? [ + { + name: 'Anime4K超分', + html: 'Anime4K超分', + icon: '', + switch: anime4kEnabledRef.current, + onSwitch: async function (item: any) { + const newVal = !item.switch; + await toggleAnime4K(newVal); + return newVal; + }, + }, + { + name: '超分模式', + html: '超分模式', + selector: [ + { + html: 'ModeA (快速)', + value: 'ModeA', + default: anime4kModeRef.current === 'ModeA', + }, + { + html: 'ModeB (平衡)', + value: 'ModeB', + default: anime4kModeRef.current === 'ModeB', + }, + { + html: 'ModeC (质量)', + value: 'ModeC', + default: anime4kModeRef.current === 'ModeC', + }, + { + html: 'ModeAA (增强快速)', + value: 'ModeAA', + default: anime4kModeRef.current === 'ModeAA', + }, + { + html: 'ModeBB (增强平衡)', + value: 'ModeBB', + default: anime4kModeRef.current === 'ModeBB', + }, + { + html: 'ModeCA (最高质量)', + value: 'ModeCA', + default: anime4kModeRef.current === 'ModeCA', + }, + ], + onSelect: async function (item: any) { + await changeAnime4KMode(item.value); + return item.html; + }, + }, + { + name: '超分倍数', + html: '超分倍数', + selector: [ + { + html: '1.5x', + value: '1.5', + default: anime4kScaleRef.current === 1.5, + }, + { + html: '2.0x', + value: '2.0', + default: anime4kScaleRef.current === 2.0, + }, + { + html: '3.0x', + value: '3.0', + default: anime4kScaleRef.current === 3.0, + }, + { + html: '4.0x', + value: '4.0', + default: anime4kScaleRef.current === 4.0, + }, + ], + onSelect: async function (item: any) { + await changeAnime4KScale(parseFloat(item.value)); + return item.html; + }, + } + ] : []), + ], }); // 监听播放器事件