diff --git a/package.json b/package.json index e5be487..0ac8e8e 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "@types/crypto-js": "^4.2.2", "@upstash/redis": "^1.25.0", "@vidstack/react": "^1.12.13", + "anime4k-webgpu": "^1.0.0", "artplayer": "^5.2.5", "bs58": "^6.0.0", "clsx": "^2.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7aa9521..f308a49 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -35,6 +35,9 @@ importers: '@vidstack/react': specifier: ^1.12.13 version: 1.12.13(@types/react@18.3.23)(react@18.3.1) + anime4k-webgpu: + specifier: ^1.0.0 + version: 1.0.0(@webgpu/types@0.1.66) artplayer: specifier: ^5.2.5 version: 5.2.5 @@ -1797,6 +1800,9 @@ packages: '@webassemblyjs/wast-printer@1.14.1': resolution: {integrity: sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==} + '@webgpu/types@0.1.66': + resolution: {integrity: sha512-YA2hLrwLpDsRueNDXIMqN9NTzD6bCDkuXbOSe0heS+f8YE8usA6Gbv1prj81pzVHrbaAma7zObnIC+I6/sXJgA==} + '@xtuc/ieee754@1.2.0': resolution: {integrity: sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==} @@ -1878,6 +1884,11 @@ packages: resolution: {integrity: sha512-S2Hw0TtNkMJhIabBwIojKL9YHO5T0n5eNqWJ7Lrlel/zDbftQpxpapi8tZs3X1HWa+u+QeydGmzzNU0m09+Rcg==} engines: {node: '>=0.4.2'} + anime4k-webgpu@1.0.0: + resolution: {integrity: sha512-syZZyDRHYukFnDLxdES4AuMHzKwxlu5Lo52yhEEc1hwKQmvza8DGC62/VDPrrWOu4KArU5jh/0yFJQBwetGuxg==} + peerDependencies: + '@webgpu/types': ^0.1.38 + ansi-escapes@4.3.2: resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==} engines: {node: '>=8'} @@ -7285,6 +7296,8 @@ snapshots: '@webassemblyjs/ast': 1.14.1 '@xtuc/long': 4.2.2 + '@webgpu/types@0.1.66': {} + '@xtuc/ieee754@1.2.0': {} '@xtuc/long@4.2.2': {} @@ -7357,6 +7370,10 @@ snapshots: amdefine@1.0.1: {} + anime4k-webgpu@1.0.0(@webgpu/types@0.1.66): + dependencies: + '@webgpu/types': 0.1.66 + ansi-escapes@4.3.2: dependencies: type-fest: 0.21.3 @@ -8220,8 +8237,8 @@ snapshots: '@typescript-eslint/parser': 5.62.0(eslint@8.57.1)(typescript@4.9.5) eslint: 8.57.1 eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@4.9.5))(eslint@8.57.1))(eslint@8.57.1) - eslint-plugin-import: 2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@4.9.5))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@4.9.5))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.31.0)(eslint@8.57.1) + eslint-plugin-import: 2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@4.9.5))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1) eslint-plugin-jsx-a11y: 6.10.2(eslint@8.57.1) eslint-plugin-react: 7.37.5(eslint@8.57.1) eslint-plugin-react-hooks: 5.0.0-canary-7118f5dd7-20230705(eslint@8.57.1) @@ -8244,7 +8261,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@4.9.5))(eslint@8.57.1))(eslint@8.57.1): + eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.31.0)(eslint@8.57.1): dependencies: '@nolyfill/is-core-module': 1.0.39 debug: 4.4.1(supports-color@9.4.0) @@ -8255,22 +8272,22 @@ snapshots: tinyglobby: 0.2.14 unrs-resolver: 1.9.0 optionalDependencies: - eslint-plugin-import: 2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@4.9.5))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@4.9.5))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1) + eslint-plugin-import: 2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@4.9.5))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1) transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@4.9.5))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@4.9.5))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1): + eslint-module-utils@2.12.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@4.9.5))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1): dependencies: debug: 3.2.7 optionalDependencies: '@typescript-eslint/parser': 5.62.0(eslint@8.57.1)(typescript@4.9.5) eslint: 8.57.1 eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@4.9.5))(eslint@8.57.1))(eslint@8.57.1) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.31.0)(eslint@8.57.1) transitivePeerDependencies: - supports-color - eslint-plugin-import@2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@4.9.5))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@4.9.5))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1): + eslint-plugin-import@2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@4.9.5))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 @@ -8281,7 +8298,7 @@ snapshots: doctrine: 2.1.0 eslint: 8.57.1 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@4.9.5))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@4.9.5))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1) + eslint-module-utils: 2.12.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@4.9.5))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 diff --git a/src/app/play/page.tsx b/src/app/play/page.tsx index d0a007d..cbca40a 100644 --- a/src/app/play/page.tsx +++ b/src/app/play/page.tsx @@ -7,6 +7,7 @@ import Hls from 'hls.js'; import { Heart } from 'lucide-react'; import { useRouter, useSearchParams } from 'next/navigation'; import { Suspense, useEffect, useRef, useState } from 'react'; +import { Anime4K } from 'anime4k-webgpu'; import { deleteFavorite, @@ -96,6 +97,58 @@ function PlayPageClient() { blockAdEnabledRef.current = blockAdEnabled; }, [blockAdEnabled]); + // Anime4K超分相关状态 + const [webGPUSupported, setWebGPUSupported] = useState(false); + const [anime4kEnabled, setAnime4kEnabled] = useState(() => { + if (typeof window !== 'undefined') { + const v = localStorage.getItem('enable_anime4k'); + if (v !== null) return v === 'true'; + } + return false; + }); + const [anime4kMode, setAnime4kMode] = useState(() => { + if (typeof window !== 'undefined') { + const v = localStorage.getItem('anime4k_mode'); + if (v !== null) return v; + } + return 'fast'; + }); + const anime4kRef = useRef(null); + const anime4kEnabledRef = useRef(anime4kEnabled); + const anime4kModeRef = useRef(anime4kMode); + useEffect(() => { + anime4kEnabledRef.current = anime4kEnabled; + anime4kModeRef.current = anime4kMode; + }, [anime4kEnabled, anime4kMode]); + + // 检测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(); + }, []); + // 视频基本信息 const [videoTitle, setVideoTitle] = useState(searchParams.get('title') || ''); const [videoYear, setVideoYear] = useState(searchParams.get('year') || ''); @@ -495,6 +548,106 @@ function PlayPageClient() { } }; + // 初始化Anime4K超分 + const initAnime4K = async () => { + if (!artPlayerRef.current?.video) return; + + try { + if (anime4kRef.current) { + await anime4kRef.current.destroy(); + anime4kRef.current = null; + } + + const video = artPlayerRef.current.video as HTMLVideoElement; + const canvas = document.createElement('canvas'); + const container = artPlayerRef.current.template.$video.parentElement; + + // 设置canvas样式 + canvas.style.position = 'absolute'; + canvas.style.top = '0'; + canvas.style.left = '0'; + canvas.style.width = '100%'; + canvas.style.height = '100%'; + canvas.style.objectFit = 'contain'; + + // 隐藏原始video元素 + video.style.display = 'none'; + + // 插入canvas到容器 + container.insertBefore(canvas, video); + + // 初始化Anime4K + const anime4k = new Anime4K({ + video: video, + canvas: canvas, + mode: anime4kModeRef.current as any, + sharpness: 1.0, + }); + + await anime4k.init(); + anime4kRef.current = anime4k; + + console.log('Anime4K超分已启用,模式:', anime4kModeRef.current); + if (artPlayerRef.current) { + artPlayerRef.current.notice.show = `超分已启用 (${anime4kModeRef.current})`; + } + } catch (err) { + console.error('初始化Anime4K失败:', err); + if (artPlayerRef.current) { + artPlayerRef.current.notice.show = '超分启用失败:' + (err instanceof Error ? err.message : '未知错误'); + } + } + }; + + // 清理Anime4K + const cleanupAnime4K = async () => { + if (anime4kRef.current) { + try { + await anime4kRef.current.destroy(); + anime4kRef.current = null; + + // 恢复原始video显示 + if (artPlayerRef.current?.video) { + artPlayerRef.current.video.style.display = 'block'; + } + + 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); + } + }; + function filterAdsFromM3U8(type: string, m3u8Content: string): string { if (!m3u8Content) return ''; @@ -1430,6 +1583,44 @@ function PlayPageClient() { return newVal ? '当前开启' : '当前关闭'; }, }, + ...(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: '快速模式', + value: 'fast', + default: anime4kModeRef.current === 'fast', + }, + { + html: '平衡模式', + value: 'balanced', + default: anime4kModeRef.current === 'balanced', + }, + { + html: '高质量模式', + value: 'high_quality', + default: anime4kModeRef.current === 'high_quality', + }, + ], + onSelect: async function (item: any) { + await changeAnime4KMode(item.value); + return item.html; + }, + } + ] : []), { name: '跳过片头片尾', html: '跳过片头片尾', @@ -1514,13 +1705,18 @@ function PlayPageClient() { }); // 监听播放器事件 - artPlayerRef.current.on('ready', () => { + artPlayerRef.current.on('ready', async () => { setError(null); // 播放器就绪后,如果正在播放则请求 Wake Lock if (artPlayerRef.current && !artPlayerRef.current.paused) { requestWakeLock(); } + + // 如果启用了Anime4K,初始化超分 + if (anime4kEnabledRef.current) { + await initAnime4K(); + } }); // 监听播放状态变化,控制 Wake Lock @@ -1689,6 +1885,9 @@ function PlayPageClient() { // 释放 Wake Lock releaseWakeLock(); + // 清理Anime4K + cleanupAnime4K(); + // 销毁播放器实例 cleanupPlayer(); };