mirror of
https://github.com/MoonTechLab/LunaTV.git
synced 2026-02-21 09:14:42 +08:00
feat: port artplayer
This commit is contained in:
@@ -29,9 +29,10 @@
|
||||
"@heroicons/react": "^2.2.0",
|
||||
"@upstash/redis": "^1.25.0",
|
||||
"@vidstack/react": "^1.12.13",
|
||||
"artplayer": "^5.2.3",
|
||||
"clsx": "^2.0.0",
|
||||
"framer-motion": "^12.18.1",
|
||||
"hls.js": "^1.6.5",
|
||||
"hls.js": "^1.6.6",
|
||||
"lucide-react": "^0.438.0",
|
||||
"media-icons": "^1.1.5",
|
||||
"next": "^14.2.23",
|
||||
@@ -46,8 +47,6 @@
|
||||
"swiper": "^11.2.8",
|
||||
"tailwind-merge": "^2.6.0",
|
||||
"vidstack": "^0.6.15",
|
||||
"xgplayer": "^3.0.22",
|
||||
"xgplayer-hls.js": "^3.0.22",
|
||||
"zod": "^3.24.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
178
pnpm-lock.yaml
generated
178
pnpm-lock.yaml
generated
@@ -35,6 +35,9 @@ importers:
|
||||
'@vidstack/react':
|
||||
specifier: ^1.12.13
|
||||
version: 1.12.13(@types/react@18.3.23)(react@18.3.1)
|
||||
artplayer:
|
||||
specifier: ^5.2.3
|
||||
version: 5.2.3
|
||||
clsx:
|
||||
specifier: ^2.0.0
|
||||
version: 2.1.1
|
||||
@@ -42,8 +45,8 @@ importers:
|
||||
specifier: ^12.18.1
|
||||
version: 12.18.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
hls.js:
|
||||
specifier: ^1.6.5
|
||||
version: 1.6.5
|
||||
specifier: ^1.6.6
|
||||
version: 1.6.6
|
||||
lucide-react:
|
||||
specifier: ^0.438.0
|
||||
version: 0.438.0(react@18.3.1)
|
||||
@@ -86,12 +89,6 @@ importers:
|
||||
vidstack:
|
||||
specifier: ^0.6.15
|
||||
version: 0.6.15
|
||||
xgplayer:
|
||||
specifier: ^3.0.22
|
||||
version: 3.0.22(core-js@3.43.0)
|
||||
xgplayer-hls.js:
|
||||
specifier: ^3.0.22
|
||||
version: 3.0.22(core-js@3.43.0)(xgplayer@3.0.22(core-js@3.43.0))
|
||||
zod:
|
||||
specifier: ^3.24.1
|
||||
version: 3.25.67
|
||||
@@ -2418,6 +2415,9 @@ packages:
|
||||
resolution: {integrity: sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
artplayer@5.2.3:
|
||||
resolution: {integrity: sha512-WaOZQrpZn/L+GgI2f0TEsoAL3Wb+v16Mu0JmWh7qKFYuvr11WNt3dWhWeIaCfoHy3NtkCWM9jTP+xwwsxdElZQ==}
|
||||
|
||||
as-table@1.0.55:
|
||||
resolution: {integrity: sha512-xvsWESUJn0JN421Xb9MQw6AsMHRCUknCe0Wjlxvjud80mU4E6hQf1A6NzQKcYNmYw62MfzEtXc+badstZP3JpQ==}
|
||||
|
||||
@@ -2817,9 +2817,6 @@ packages:
|
||||
core-js-compat@3.43.0:
|
||||
resolution: {integrity: sha512-2GML2ZsCc5LR7hZYz4AXmjQw8zuy2T//2QntwdnpuYI7jteT6GVYJL7F6C2C57R7gSYrcqVW3lAALefdbhBLDA==}
|
||||
|
||||
core-js@3.43.0:
|
||||
resolution: {integrity: sha512-N6wEbTTZSYOY2rYAn85CuvWWkCK6QweMn7/4Nr3w+gDBeBhk/x4EJeY6FPo4QzDoJZxVTv8U7CMvgWk6pOHHqA==}
|
||||
|
||||
cosmiconfig-typescript-loader@2.0.2:
|
||||
resolution: {integrity: sha512-KmE+bMjWMXJbkWCeY4FJX/npHuZPNr9XF9q9CIQ/bpFwi1qHfCmSiKarrCcRa0LO4fWjk93pVoeRtJAkTGcYNw==}
|
||||
engines: {node: '>=12', npm: '>=6'}
|
||||
@@ -2892,16 +2889,9 @@ packages:
|
||||
csstype@3.1.3:
|
||||
resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
|
||||
|
||||
d@1.0.2:
|
||||
resolution: {integrity: sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw==}
|
||||
engines: {node: '>=0.12'}
|
||||
|
||||
damerau-levenshtein@1.0.8:
|
||||
resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==}
|
||||
|
||||
danmu.js@1.1.13:
|
||||
resolution: {integrity: sha512-knFd0/cB2HA4FFWiA7eB2suc5vCvoHdqio33FyyCSfP7C+1A+zQcTvnvwfxaZhrxsGj4qaQI2I8XiTqedRaVmg==}
|
||||
|
||||
dargs@7.0.0:
|
||||
resolution: {integrity: sha512-2iy1EkLdlBzQGvbweYRFxmFath8+K7+AKB0TlhHWkNuH+TmovaMH/Wp7V7R4u7f4SnX3OgLsU9t1NI9ioDnUpg==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -2968,10 +2958,6 @@ packages:
|
||||
deep-is@0.1.4:
|
||||
resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
|
||||
|
||||
deepmerge@2.0.1:
|
||||
resolution: {integrity: sha512-VIPwiMJqJ13ZQfaCsIFnp5Me9tnjURiaIFxfz7EH0Ci0dTSQpZtSLrqOicXqEd/z2r+z+Klk9GzmnRsgpgbOsQ==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
deepmerge@4.3.1:
|
||||
resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@@ -2995,9 +2981,6 @@ packages:
|
||||
resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
|
||||
engines: {node: '>=0.4.0'}
|
||||
|
||||
delegate@3.2.0:
|
||||
resolution: {integrity: sha512-IofjkYBZaZivn0V8nnsMJGBr4jVLxHDheKSW88PyxS5QC4Vo9ZbZVvhzlSxY87fVq3STR6r+4cGepyHkcWOQSw==}
|
||||
|
||||
depd@1.1.2:
|
||||
resolution: {integrity: sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==}
|
||||
engines: {node: '>= 0.6'}
|
||||
@@ -3068,9 +3051,6 @@ packages:
|
||||
resolution: {integrity: sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
downloadjs@1.4.7:
|
||||
resolution: {integrity: sha512-LN1gO7+u9xjU5oEScGFKvXhYf7Y/empUIIEAGBs1LzUq/rg5duiDrkuH5A2lQGd5jfMOb9X9usDa2oVXwJ0U/Q==}
|
||||
|
||||
dunder-proto@1.0.1:
|
||||
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -3157,17 +3137,6 @@ packages:
|
||||
resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
es5-ext@0.10.64:
|
||||
resolution: {integrity: sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg==}
|
||||
engines: {node: '>=0.10'}
|
||||
|
||||
es6-iterator@2.0.3:
|
||||
resolution: {integrity: sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==}
|
||||
|
||||
es6-symbol@3.1.4:
|
||||
resolution: {integrity: sha512-U9bFFjX8tFiATgtkJ1zg25+KviIXpgRvRHS8sau3GfhVzThRQrOeksPeT0BWW2MNZs1OEWJ1DPXOQMn0KKRkvg==}
|
||||
engines: {node: '>=0.12'}
|
||||
|
||||
esbuild-android-64@0.14.47:
|
||||
resolution: {integrity: sha512-R13Bd9+tqLVFndncMHssZrPWe6/0Kpv2/dt4aA69soX4PRxlzsVpCvoJeFE8sOEoeVEiBkI0myjlkDodXlHa0g==}
|
||||
engines: {node: '>=12'}
|
||||
@@ -3557,10 +3526,6 @@ packages:
|
||||
deprecated: This version is no longer supported. Please see https://eslint.org/version-support for other options.
|
||||
hasBin: true
|
||||
|
||||
esniff@2.0.1:
|
||||
resolution: {integrity: sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==}
|
||||
engines: {node: '>=0.10'}
|
||||
|
||||
espree@9.6.1:
|
||||
resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==}
|
||||
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
||||
@@ -3600,12 +3565,6 @@ packages:
|
||||
resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
||||
event-emitter@0.3.5:
|
||||
resolution: {integrity: sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==}
|
||||
|
||||
eventemitter3@4.0.7:
|
||||
resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==}
|
||||
|
||||
events-intercept@2.0.0:
|
||||
resolution: {integrity: sha512-blk1va0zol9QOrdZt0rFXo5KMkNPVSp92Eju/Qz8THwKWKRKeE0T8Br/1aW6+Edkyq9xHYgYxn2QtOnUKPUp+Q==}
|
||||
|
||||
@@ -3636,9 +3595,6 @@ packages:
|
||||
exsolve@1.0.7:
|
||||
resolution: {integrity: sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw==}
|
||||
|
||||
ext@1.7.0:
|
||||
resolution: {integrity: sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==}
|
||||
|
||||
fast-deep-equal@3.1.3:
|
||||
resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
|
||||
|
||||
@@ -3907,8 +3863,8 @@ packages:
|
||||
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
hls.js@1.6.5:
|
||||
resolution: {integrity: sha512-KMn5n7JBK+olC342740hDPHnGWfE8FiHtGMOdJPfUjRdARTWj9OB+8c13fnsf9sk1VtpuU2fKSgUjHvg4rNbzQ==}
|
||||
hls.js@1.6.6:
|
||||
resolution: {integrity: sha512-S4uTCwTHOtImW+/jxMjzG7udbHy5z682YQRbm/4f7VXuVNEoGBRjPJnD3Fxrufomdhzdtv24KnxRhPMXSvL6Fw==}
|
||||
|
||||
hosted-git-info@2.8.9:
|
||||
resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==}
|
||||
@@ -4797,9 +4753,6 @@ packages:
|
||||
react: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc
|
||||
react-dom: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc
|
||||
|
||||
next-tick@1.1.0:
|
||||
resolution: {integrity: sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==}
|
||||
|
||||
next@14.2.30:
|
||||
resolution: {integrity: sha512-+COdu6HQrHHFQ1S/8BBsCag61jZacmvbuL2avHvQFbWa2Ox7bE+d8FyNgxRLjXQ5wtPyQwEmk85js/AuaG2Sbg==}
|
||||
engines: {node: '>=18.17.0'}
|
||||
@@ -4941,6 +4894,9 @@ packages:
|
||||
resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
option-validator@2.0.6:
|
||||
resolution: {integrity: sha512-tmZDan2LRIRQyhUGvkff68/O0R8UmF+Btmiiz0SmSw2ng3CfPZB9wJlIjHpe/MKUZqyIZkVIXCrwr1tIN+0Dzg==}
|
||||
|
||||
optionator@0.9.4:
|
||||
resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
@@ -6010,9 +5966,6 @@ packages:
|
||||
resolution: {integrity: sha512-tLq3bSNx+xSpwvAJnzrK0Ep5CLNWjvFTOp71URMaAEWBfRb9nnJiBoUe0tF8bI4ZFO3omgBR6NvnbzVUT3Ly4g==}
|
||||
engines: {node: '>=14.16'}
|
||||
|
||||
type@2.7.3:
|
||||
resolution: {integrity: sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ==}
|
||||
|
||||
typed-array-buffer@1.0.3:
|
||||
resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -6359,22 +6312,6 @@ packages:
|
||||
resolution: {integrity: sha512-sqMMuL1rc0FmMBOzCpd0yuy9trqF2yTTVe+E9ogwCSWQCdDEtQUwrZPT6AxqtsFGRNxycgncbP/xmOOSPw5ZUw==}
|
||||
engines: {node: '>= 6.0'}
|
||||
|
||||
xgplayer-hls.js@3.0.22:
|
||||
resolution: {integrity: sha512-oNhiw1QnWNYmZsKnq7D8PUfAVHqAwv4kCwEwARMquGcUEZwNOOvazzZopTYPgwXj965LKSzgD36nhpbfYef/ZQ==}
|
||||
peerDependencies:
|
||||
core-js: '>=3.12.1'
|
||||
xgplayer: '>=3.0.13'
|
||||
|
||||
xgplayer-subtitles@3.0.22:
|
||||
resolution: {integrity: sha512-2XjamtZnWS/r4QjesOC34JmuGD3QPbgeqkI4t5Gq19dN1CWNBP7nJ8pbGLuAeHswKjGg8LFRpnsic7xjc/XSyA==}
|
||||
peerDependencies:
|
||||
core-js: '>=3.12.1'
|
||||
|
||||
xgplayer@3.0.22:
|
||||
resolution: {integrity: sha512-uVKffa02NxWnWMVzgnrU0HGwZFH0ymPHsD3zGxtV6oPPplA6EBLyh9N5q3b++J7jRs2usvKR2+WslT+je1RuwA==}
|
||||
peerDependencies:
|
||||
core-js: '>=3.12.1'
|
||||
|
||||
xml-name-validator@3.0.0:
|
||||
resolution: {integrity: sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==}
|
||||
|
||||
@@ -9033,6 +8970,10 @@ snapshots:
|
||||
|
||||
arrify@1.0.1: {}
|
||||
|
||||
artplayer@5.2.3:
|
||||
dependencies:
|
||||
option-validator: 2.0.6
|
||||
|
||||
as-table@1.0.55:
|
||||
dependencies:
|
||||
printable-characters: 1.0.42
|
||||
@@ -9427,8 +9368,6 @@ snapshots:
|
||||
dependencies:
|
||||
browserslist: 4.25.0
|
||||
|
||||
core-js@3.43.0: {}
|
||||
|
||||
cosmiconfig-typescript-loader@2.0.2(@types/node@24.0.3)(cosmiconfig@7.1.0)(typescript@4.9.5):
|
||||
dependencies:
|
||||
'@types/node': 24.0.3
|
||||
@@ -9504,17 +9443,8 @@ snapshots:
|
||||
|
||||
csstype@3.1.3: {}
|
||||
|
||||
d@1.0.2:
|
||||
dependencies:
|
||||
es5-ext: 0.10.64
|
||||
type: 2.7.3
|
||||
|
||||
damerau-levenshtein@1.0.8: {}
|
||||
|
||||
danmu.js@1.1.13:
|
||||
dependencies:
|
||||
event-emitter: 0.3.5
|
||||
|
||||
dargs@7.0.0: {}
|
||||
|
||||
data-uri-to-buffer@2.0.2: {}
|
||||
@@ -9570,8 +9500,6 @@ snapshots:
|
||||
|
||||
deep-is@0.1.4: {}
|
||||
|
||||
deepmerge@2.0.1: {}
|
||||
|
||||
deepmerge@4.3.1: {}
|
||||
|
||||
define-data-property@1.1.4:
|
||||
@@ -9600,8 +9528,6 @@ snapshots:
|
||||
|
||||
delayed-stream@1.0.0: {}
|
||||
|
||||
delegate@3.2.0: {}
|
||||
|
||||
depd@1.1.2: {}
|
||||
|
||||
dequal@2.0.3: {}
|
||||
@@ -9663,8 +9589,6 @@ snapshots:
|
||||
dependencies:
|
||||
is-obj: 2.0.0
|
||||
|
||||
downloadjs@1.4.7: {}
|
||||
|
||||
dunder-proto@1.0.1:
|
||||
dependencies:
|
||||
call-bind-apply-helpers: 1.0.2
|
||||
@@ -9819,24 +9743,6 @@ snapshots:
|
||||
is-date-object: 1.1.0
|
||||
is-symbol: 1.1.1
|
||||
|
||||
es5-ext@0.10.64:
|
||||
dependencies:
|
||||
es6-iterator: 2.0.3
|
||||
es6-symbol: 3.1.4
|
||||
esniff: 2.0.1
|
||||
next-tick: 1.1.0
|
||||
|
||||
es6-iterator@2.0.3:
|
||||
dependencies:
|
||||
d: 1.0.2
|
||||
es5-ext: 0.10.64
|
||||
es6-symbol: 3.1.4
|
||||
|
||||
es6-symbol@3.1.4:
|
||||
dependencies:
|
||||
d: 1.0.2
|
||||
ext: 1.7.0
|
||||
|
||||
esbuild-android-64@0.14.47:
|
||||
optional: true
|
||||
|
||||
@@ -10247,13 +10153,6 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
esniff@2.0.1:
|
||||
dependencies:
|
||||
d: 1.0.2
|
||||
es5-ext: 0.10.64
|
||||
event-emitter: 0.3.5
|
||||
type: 2.7.3
|
||||
|
||||
espree@9.6.1:
|
||||
dependencies:
|
||||
acorn: 8.15.0
|
||||
@@ -10282,13 +10181,6 @@ snapshots:
|
||||
|
||||
etag@1.8.1: {}
|
||||
|
||||
event-emitter@0.3.5:
|
||||
dependencies:
|
||||
d: 1.0.2
|
||||
es5-ext: 0.10.64
|
||||
|
||||
eventemitter3@4.0.7: {}
|
||||
|
||||
events-intercept@2.0.0: {}
|
||||
|
||||
events@3.3.0: {}
|
||||
@@ -10327,10 +10219,6 @@ snapshots:
|
||||
|
||||
exsolve@1.0.7: {}
|
||||
|
||||
ext@1.7.0:
|
||||
dependencies:
|
||||
type: 2.7.3
|
||||
|
||||
fast-deep-equal@3.1.3: {}
|
||||
|
||||
fast-glob@3.3.3:
|
||||
@@ -10622,7 +10510,7 @@ snapshots:
|
||||
dependencies:
|
||||
function-bind: 1.1.2
|
||||
|
||||
hls.js@1.6.5: {}
|
||||
hls.js@1.6.6: {}
|
||||
|
||||
hosted-git-info@2.8.9: {}
|
||||
|
||||
@@ -11771,8 +11659,6 @@ snapshots:
|
||||
react: 18.3.1
|
||||
react-dom: 18.3.1(react@18.3.1)
|
||||
|
||||
next-tick@1.1.0: {}
|
||||
|
||||
next@14.2.30(@babel/core@7.27.4)(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
|
||||
dependencies:
|
||||
'@next/env': 14.2.30
|
||||
@@ -11913,6 +11799,10 @@ snapshots:
|
||||
dependencies:
|
||||
mimic-fn: 2.1.0
|
||||
|
||||
option-validator@2.0.6:
|
||||
dependencies:
|
||||
kind-of: 6.0.3
|
||||
|
||||
optionator@0.9.4:
|
||||
dependencies:
|
||||
deep-is: 0.1.4
|
||||
@@ -12977,8 +12867,6 @@ snapshots:
|
||||
|
||||
type-fest@3.13.1: {}
|
||||
|
||||
type@2.7.3: {}
|
||||
|
||||
typed-array-buffer@1.0.3:
|
||||
dependencies:
|
||||
call-bound: 1.0.4
|
||||
@@ -13490,28 +13378,6 @@ snapshots:
|
||||
dependencies:
|
||||
os-paths: 4.4.0
|
||||
|
||||
xgplayer-hls.js@3.0.22(core-js@3.43.0)(xgplayer@3.0.22(core-js@3.43.0)):
|
||||
dependencies:
|
||||
core-js: 3.43.0
|
||||
deepmerge: 2.0.1
|
||||
event-emitter: 0.3.5
|
||||
hls.js: 1.6.5
|
||||
xgplayer: 3.0.22(core-js@3.43.0)
|
||||
|
||||
xgplayer-subtitles@3.0.22(core-js@3.43.0):
|
||||
dependencies:
|
||||
core-js: 3.43.0
|
||||
eventemitter3: 4.0.7
|
||||
|
||||
xgplayer@3.0.22(core-js@3.43.0):
|
||||
dependencies:
|
||||
core-js: 3.43.0
|
||||
danmu.js: 1.1.13
|
||||
delegate: 3.2.0
|
||||
downloadjs: 1.4.7
|
||||
eventemitter3: 4.0.7
|
||||
xgplayer-subtitles: 3.0.22(core-js@3.43.0)
|
||||
|
||||
xml-name-validator@3.0.0: {}
|
||||
|
||||
xmlchars@2.2.0: {}
|
||||
|
||||
@@ -153,20 +153,13 @@ div[data-media-provider] video {
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
/* Vidstack Menu 自定义样式:背景黑色,文字白色 */
|
||||
.vds-menu-items {
|
||||
background-color: #000;
|
||||
color: #fff;
|
||||
.art-notice {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.vds-radio {
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
.vds-radio:hover {
|
||||
background-color: rgba(245, 245, 245, 0.1) !important;
|
||||
}
|
||||
|
||||
.vds-radio .vds-icon {
|
||||
color: #fff !important;
|
||||
.art-poster {
|
||||
background-size: contain !important; /* 使图片完整展示 */
|
||||
background-position: center center !important; /* 居中显示 */
|
||||
background-repeat: no-repeat !important; /* 防止重复 */
|
||||
background-color: #000 !important; /* 其余区域填充为黑色 */
|
||||
}
|
||||
|
||||
@@ -2,179 +2,680 @@
|
||||
|
||||
'use client';
|
||||
|
||||
import { Suspense, useEffect, useRef } from 'react';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import { Suspense, useEffect, useLayoutEffect, useRef, useState } from 'react';
|
||||
|
||||
import 'xgplayer/dist/index.min.css';
|
||||
import {
|
||||
generateStorageKey,
|
||||
getAllPlayRecords,
|
||||
savePlayRecord,
|
||||
} from '@/lib/db.client';
|
||||
import {
|
||||
type VideoDetail,
|
||||
fetchVideoDetail,
|
||||
} from '@/lib/fetchVideoDetail.client';
|
||||
|
||||
import EpisodeSelector from '@/components/EpisodeSelector';
|
||||
import PageLayout from '@/components/PageLayout';
|
||||
|
||||
function PlayPageClient() {
|
||||
const playerRef = useRef<any>(null);
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
// 状态管理
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [detail, setDetail] = useState<VideoDetail | null>(null);
|
||||
|
||||
// 视频基本信息
|
||||
const [videoTitle, setVideoTitle] = useState(searchParams.get('title') || '');
|
||||
const videoYear = searchParams.get('year') || '';
|
||||
const [videoCover, setVideoCover] = useState('');
|
||||
|
||||
// 当前源和ID
|
||||
const [currentSource, setCurrentSource] = useState(
|
||||
searchParams.get('source') || ''
|
||||
);
|
||||
const [currentId, setCurrentId] = useState(searchParams.get('id') || '');
|
||||
|
||||
// 集数相关
|
||||
const initialIndex = parseInt(searchParams.get('index') || '1') - 1; // 转换为0基数组索引
|
||||
const [currentEpisodeIndex, setCurrentEpisodeIndex] = useState(initialIndex);
|
||||
|
||||
// 视频播放地址
|
||||
const [videoUrl, setVideoUrl] = useState('');
|
||||
|
||||
// 总集数
|
||||
const totalEpisodes = detail?.episodes?.length || 0;
|
||||
|
||||
// 用于记录是否需要在播放器 ready 后跳转到指定进度
|
||||
const resumeTimeRef = useRef<number | null>(null);
|
||||
|
||||
const currentSourceRef = useRef(currentSource);
|
||||
const currentIdRef = useRef(currentId);
|
||||
const videoTitleRef = useRef(videoTitle);
|
||||
|
||||
// 同步最新值到 refs
|
||||
useEffect(() => {
|
||||
currentSourceRef.current = currentSource;
|
||||
currentIdRef.current = currentId;
|
||||
detailRef.current = detail;
|
||||
currentEpisodeIndexRef.current = currentEpisodeIndex;
|
||||
videoTitleRef.current = videoTitle;
|
||||
}, [currentSource, currentId, detail, currentEpisodeIndex, videoTitle]);
|
||||
|
||||
// 播放进度保存相关
|
||||
const saveIntervalRef = useRef<NodeJS.Timeout | null>(null);
|
||||
const videoEventListenersRef = useRef<{
|
||||
video: HTMLVideoElement;
|
||||
listeners: Array<{ event: string; handler: EventListener }>;
|
||||
} | null>(null);
|
||||
|
||||
// 动态导入的 Artplayer 与 Hls 实例
|
||||
const [{ Artplayer, Hls }, setPlayers] = useState<{
|
||||
Artplayer: any | null;
|
||||
Hls: any | null;
|
||||
}>({ Artplayer: null, Hls: null });
|
||||
const artPlayerRef = useRef<any>(null);
|
||||
const artRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
// 添加缺少的状态和 ref
|
||||
const detailRef = useRef<VideoDetail | null>(detail);
|
||||
const currentEpisodeIndexRef = useRef(currentEpisodeIndex);
|
||||
|
||||
// 同步状态到 ref
|
||||
useEffect(() => {
|
||||
detailRef.current = detail;
|
||||
currentEpisodeIndexRef.current = currentEpisodeIndex;
|
||||
}, [detail, currentEpisodeIndex]);
|
||||
|
||||
useEffect(() => {
|
||||
const initPlayer = async () => {
|
||||
(async () => {
|
||||
try {
|
||||
// 动态导入 xgplayer 和 HLS 插件
|
||||
const [{ default: Player }, { default: HLS }] = await Promise.all([
|
||||
import('xgplayer'),
|
||||
import('xgplayer-hls.js'),
|
||||
const [ArtplayerModule, HlsModule] = await Promise.all([
|
||||
import('artplayer'),
|
||||
import('hls.js'),
|
||||
]);
|
||||
setPlayers({
|
||||
Artplayer: ArtplayerModule.default,
|
||||
Hls: HlsModule.default,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Failed to load players:', err);
|
||||
setError('播放器加载失败');
|
||||
setLoading(false);
|
||||
}
|
||||
})();
|
||||
}, []);
|
||||
|
||||
console.log('Player:', Player);
|
||||
console.log('HLS Plugin:', HLS);
|
||||
// 更新视频地址
|
||||
const updateVideoUrl = (
|
||||
detailData: VideoDetail | null,
|
||||
episodeIndex: number
|
||||
) => {
|
||||
if (
|
||||
!detailData ||
|
||||
!detailData.episodes ||
|
||||
episodeIndex >= detailData.episodes.length
|
||||
) {
|
||||
setVideoUrl('');
|
||||
return;
|
||||
}
|
||||
const newUrl = detailData?.episodes[episodeIndex] || '';
|
||||
if (newUrl !== videoUrl) {
|
||||
setVideoUrl(newUrl);
|
||||
}
|
||||
};
|
||||
|
||||
// 初始化 xgplayer
|
||||
const player = new Player({
|
||||
id: 'player-container',
|
||||
// 示例 HLS 流地址,您可以根据需要替换
|
||||
url: 'https://vip.dytt-cinema.com/20250607/24158_8e0c4a20/index.m3u8',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
// 播放器配置
|
||||
autoplay: true,
|
||||
preload: 'auto',
|
||||
// 禁用响应式设计,保持原始大小
|
||||
fluid: false,
|
||||
// 保持视频原始尺寸和宽高比
|
||||
videoFillMode: 'contain',
|
||||
// 禁用自动调整尺寸
|
||||
autoResize: false,
|
||||
// 保持原始视频比例
|
||||
aspectRatio: 'auto',
|
||||
// 控制栏配置
|
||||
controls: true,
|
||||
// 海报图片
|
||||
poster: '',
|
||||
// 其他配置
|
||||
playsinline: true,
|
||||
lang: 'zh-cn',
|
||||
volume: 0.8,
|
||||
// 插件配置 - 注册 HLS 插件
|
||||
plugins: [HLS],
|
||||
// HLS 特定配置
|
||||
hls: {
|
||||
// 使用 hls.js 引擎
|
||||
enableWorker: true,
|
||||
// 预加载配置
|
||||
maxBufferLength: 30,
|
||||
maxBufferSize: 60 * 1000 * 1000,
|
||||
// 错误处理
|
||||
enableSoftwareAES: true,
|
||||
debug: process.env.NODE_ENV === 'development',
|
||||
// 自适应码率
|
||||
enableLowInitialPlaylist: false,
|
||||
// 跨域配置
|
||||
withCredentials: false,
|
||||
// 错误恢复
|
||||
maxMaxBufferLength: 600,
|
||||
backBufferLength: 90,
|
||||
// 性能优化
|
||||
liveSyncDurationCount: 3,
|
||||
liveMaxLatencyDurationCount: 10,
|
||||
},
|
||||
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');
|
||||
}
|
||||
};
|
||||
|
||||
// 当集数索引变化时自动更新视频地址
|
||||
useEffect(() => {
|
||||
updateVideoUrl(detail, currentEpisodeIndex);
|
||||
}, [detail, currentEpisodeIndex]);
|
||||
|
||||
// 确保初始状态与URL参数同步
|
||||
useEffect(() => {
|
||||
const urlSource = searchParams.get('source');
|
||||
const urlId = searchParams.get('id');
|
||||
|
||||
if (urlSource && urlSource !== currentSource) {
|
||||
setCurrentSource(urlSource);
|
||||
}
|
||||
if (urlId && urlId !== currentId) {
|
||||
setCurrentId(urlId);
|
||||
}
|
||||
}, [searchParams, currentSource, currentId]);
|
||||
|
||||
// 获取视频详情
|
||||
useEffect(() => {
|
||||
if (!currentSource || !currentId) {
|
||||
setError('缺少必要参数');
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const fetchDetail = async () => {
|
||||
try {
|
||||
const detailData = await fetchVideoDetail({
|
||||
source: currentSource,
|
||||
id: currentId,
|
||||
fallbackTitle: videoTitle.trim(),
|
||||
fallbackYear: videoYear,
|
||||
});
|
||||
|
||||
// 保存播放器实例
|
||||
playerRef.current = player;
|
||||
// 更新状态保存详情
|
||||
setVideoTitle(detailData.title || videoTitle);
|
||||
setVideoCover(detailData.poster);
|
||||
setDetail(detailData);
|
||||
|
||||
// 添加事件监听器
|
||||
player.on('ready', () => {
|
||||
console.log('播放器已准备就绪');
|
||||
});
|
||||
// 确保集数索引在有效范围内
|
||||
if (currentEpisodeIndex >= detailData.episodes.length) {
|
||||
console.log('currentEpisodeIndex', currentEpisodeIndex);
|
||||
setCurrentEpisodeIndex(0);
|
||||
}
|
||||
|
||||
player.on('play', () => {
|
||||
console.log('开始播放');
|
||||
});
|
||||
|
||||
player.on('pause', () => {
|
||||
console.log('暂停播放');
|
||||
});
|
||||
|
||||
player.on('error', (err: any) => {
|
||||
console.error('播放器错误:', err);
|
||||
});
|
||||
|
||||
// HLS 特定事件
|
||||
player.on('hls_manifest_parsed', () => {
|
||||
console.log('HLS 清单解析完成');
|
||||
});
|
||||
|
||||
player.on('hls_level_switched', (event: any) => {
|
||||
console.log('HLS 质量切换:', event);
|
||||
});
|
||||
|
||||
player.on('hls_media_attached', () => {
|
||||
console.log('HLS 媒体已附加');
|
||||
});
|
||||
|
||||
player.on('hls_manifest_loaded', () => {
|
||||
console.log('HLS 清单已加载');
|
||||
});
|
||||
|
||||
player.on('hls_level_loaded', () => {
|
||||
console.log('HLS 级别已加载');
|
||||
});
|
||||
|
||||
player.on('hls_frag_loaded', () => {
|
||||
console.log('HLS 片段已加载');
|
||||
});
|
||||
|
||||
player.on('hls_error', (err: any) => {
|
||||
console.error('HLS 错误:', err);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('初始化播放器失败:', error);
|
||||
// 清理URL参数(移除index参数)
|
||||
if (searchParams.has('index')) {
|
||||
const newUrl = new URL(window.location.href);
|
||||
newUrl.searchParams.delete('index');
|
||||
newUrl.searchParams.delete('position');
|
||||
window.history.replaceState({}, '', newUrl.toString());
|
||||
}
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : '获取视频详情失败');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 初始化播放器
|
||||
initPlayer();
|
||||
fetchDetail();
|
||||
}, [currentSource]);
|
||||
|
||||
// 清理函数
|
||||
return () => {
|
||||
if (playerRef.current) {
|
||||
try {
|
||||
playerRef.current.destroy();
|
||||
} catch (error) {
|
||||
console.error('销毁播放器失败:', error);
|
||||
// 播放记录处理
|
||||
useEffect(() => {
|
||||
// 仅在初次挂载时检查播放记录
|
||||
const initFromHistory = async () => {
|
||||
if (!currentSource || !currentId) return;
|
||||
|
||||
try {
|
||||
const allRecords = await getAllPlayRecords();
|
||||
const key = generateStorageKey(currentSource, currentId);
|
||||
const record = allRecords[key];
|
||||
|
||||
// URL 参数
|
||||
const urlIndexParam = searchParams.get('index');
|
||||
const urlPositionParam = searchParams.get('position');
|
||||
|
||||
// 当index参数存在时的处理逻辑
|
||||
if (urlIndexParam) {
|
||||
const urlIndex = parseInt(urlIndexParam, 10) - 1;
|
||||
let targetTime = 0; // 默认从0开始
|
||||
|
||||
// 只有index参数和position参数都存在时才生效position
|
||||
if (urlPositionParam) {
|
||||
targetTime = parseInt(urlPositionParam, 10);
|
||||
} else if (record && urlIndex === record.index - 1) {
|
||||
// 如果有同集播放记录则跳转到播放记录处
|
||||
targetTime = record.play_time;
|
||||
}
|
||||
// 否则从0开始(targetTime已经是0)
|
||||
|
||||
// 更新当前选集索引
|
||||
if (urlIndex !== currentEpisodeIndex) {
|
||||
setCurrentEpisodeIndex(urlIndex);
|
||||
}
|
||||
|
||||
// 保存待恢复的播放进度,待播放器就绪后跳转
|
||||
resumeTimeRef.current = targetTime;
|
||||
} else if (record) {
|
||||
// 没有index参数但有播放记录时,使用原有逻辑
|
||||
const targetIndex = record.index - 1;
|
||||
const targetTime = record.play_time;
|
||||
|
||||
// 更新当前选集索引
|
||||
if (targetIndex !== currentEpisodeIndex) {
|
||||
setCurrentEpisodeIndex(targetIndex);
|
||||
}
|
||||
|
||||
// 保存待恢复的播放进度,待播放器就绪后跳转
|
||||
resumeTimeRef.current = targetTime;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('读取播放记录失败:', err);
|
||||
}
|
||||
};
|
||||
|
||||
initFromHistory();
|
||||
}, []);
|
||||
|
||||
// 处理集数切换
|
||||
const handleEpisodeChange = (episodeNumber: number) => {
|
||||
if (episodeNumber >= 0 && episodeNumber < totalEpisodes) {
|
||||
// 在更换集数前保存当前播放进度
|
||||
if (
|
||||
artPlayerRef.current &&
|
||||
artPlayerRef.current.video &&
|
||||
!artPlayerRef.current.video.paused
|
||||
) {
|
||||
saveCurrentPlayProgress();
|
||||
}
|
||||
setCurrentEpisodeIndex(episodeNumber);
|
||||
}
|
||||
};
|
||||
|
||||
const handleNextEpisode = () => {
|
||||
const d = detailRef.current;
|
||||
const idx = currentEpisodeIndexRef.current;
|
||||
if (d && d.episodes && idx < d.episodes.length - 1) {
|
||||
if (
|
||||
artPlayerRef.current &&
|
||||
artPlayerRef.current.video &&
|
||||
!artPlayerRef.current.video.paused
|
||||
) {
|
||||
saveCurrentPlayProgress();
|
||||
}
|
||||
setCurrentEpisodeIndex(idx + 1);
|
||||
}
|
||||
};
|
||||
|
||||
// 保存播放进度
|
||||
const saveCurrentPlayProgress = async () => {
|
||||
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(currentSource, currentId, {
|
||||
title: videoTitleRef.current,
|
||||
source_name: detailRef.current?.source_name || '',
|
||||
year: videoYear || detailRef.current?.year || '',
|
||||
cover: videoCover,
|
||||
index: currentEpisodeIndex + 1, // 转换为1基索引
|
||||
total_episodes: totalEpisodes,
|
||||
play_time: Math.floor(currentTime),
|
||||
total_time: Math.floor(duration),
|
||||
save_time: Date.now(),
|
||||
});
|
||||
console.log('播放进度已保存:', {
|
||||
title: videoTitleRef.current,
|
||||
episode: currentEpisodeIndexRef.current + 1,
|
||||
progress: `${Math.floor(currentTime)}/${Math.floor(duration)}`,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('保存播放进度失败:', err);
|
||||
}
|
||||
};
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const container = document.getElementById(
|
||||
'artplayer-container'
|
||||
) as HTMLDivElement;
|
||||
|
||||
if (
|
||||
!Artplayer ||
|
||||
!Hls ||
|
||||
!videoUrl ||
|
||||
loading ||
|
||||
currentEpisodeIndex === null ||
|
||||
!container
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
artRef.current = container;
|
||||
|
||||
// 确保选集索引有效
|
||||
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) {
|
||||
artPlayerRef.current.switch = videoUrl;
|
||||
artPlayerRef.current.title = `${videoTitle} - 第${
|
||||
currentEpisodeIndex + 1
|
||||
}集`;
|
||||
artPlayerRef.current.poster = videoCover;
|
||||
if (artPlayerRef.current?.video) {
|
||||
console.log('attachVideoEventListeners');
|
||||
attachVideoEventListeners(
|
||||
artPlayerRef.current.video as HTMLVideoElement
|
||||
);
|
||||
ensureVideoSource(
|
||||
artPlayerRef.current.video as HTMLVideoElement,
|
||||
videoUrl
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// WebKit浏览器或首次创建:销毁之前的播放器实例并创建新的
|
||||
if (artPlayerRef.current) {
|
||||
if (artPlayerRef.current.video && artPlayerRef.current.video.hls) {
|
||||
artPlayerRef.current.video.hls.destroy();
|
||||
}
|
||||
// 销毁播放器实例
|
||||
artPlayerRef.current.destroy();
|
||||
artPlayerRef.current = null;
|
||||
}
|
||||
|
||||
try {
|
||||
// 创建新的播放器实例
|
||||
Artplayer.PLAYBACK_RATE = [0.5, 0.75, 1, 1.25, 1.5, 2, 3];
|
||||
artPlayerRef.current = new Artplayer({
|
||||
container: artRef.current,
|
||||
url: videoUrl,
|
||||
title: `${videoTitle} - 第${currentEpisodeIndex + 1}集`,
|
||||
poster: videoCover,
|
||||
volume: 0.7,
|
||||
isLive: false,
|
||||
muted: false,
|
||||
autoplay: true,
|
||||
pip: false,
|
||||
autoSize: false,
|
||||
autoMini: false,
|
||||
screenshot: false,
|
||||
setting: true,
|
||||
loop: false,
|
||||
flip: false,
|
||||
playbackRate: true,
|
||||
aspectRatio: false,
|
||||
fullscreen: true,
|
||||
fullscreenWeb: false,
|
||||
subtitleOffset: false,
|
||||
miniProgressBar: false,
|
||||
mutex: true,
|
||||
backdrop: true,
|
||||
playsInline: true,
|
||||
autoPlayback: false,
|
||||
airplay: true,
|
||||
theme: '#22c55e',
|
||||
lang: 'zh-cn',
|
||||
hotkey: false,
|
||||
moreVideoAttr: {
|
||||
crossOrigin: 'anonymous',
|
||||
},
|
||||
// HLS 支持配置
|
||||
customType: {
|
||||
m3u8: function (video: HTMLVideoElement, url: string) {
|
||||
if (!Hls) {
|
||||
console.error('HLS.js 未加载');
|
||||
return;
|
||||
}
|
||||
|
||||
if (video.hls) {
|
||||
video.hls.destroy();
|
||||
}
|
||||
const hls = new Hls({
|
||||
debug: false, // 关闭日志
|
||||
enableWorker: true, // WebWorker 解码,降低主线程压力
|
||||
lowLatencyMode: true, // 开启低延迟 LL-HLS
|
||||
|
||||
/* 缓冲/内存相关 */
|
||||
maxBufferLength: 30, // 前向缓冲最大 30s,过大容易导致高延迟
|
||||
backBufferLength: 30, // 仅保留 30s 已播放内容,避免内存占用
|
||||
maxBufferSize: 60 * 1000 * 1000, // 约 60MB,超出后触发清理
|
||||
});
|
||||
|
||||
hls.loadSource(url);
|
||||
hls.attachMedia(video);
|
||||
video.hls = hls;
|
||||
|
||||
ensureVideoSource(video, url);
|
||||
|
||||
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('无法恢复的错误');
|
||||
hls.destroy();
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
},
|
||||
icons: {
|
||||
loading:
|
||||
'<img src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI1MCIgaGVpZ2h0PSI1MCIgdmlld0JveD0iMCAwIDUwIDUwIj48cGF0aCBkPSJNMjUuMjUxIDYuNDYxYy0xMC4zMTggMC0xOC42ODMgOC4zNjUtMTguNjgzIDE4LjY4M2g0LjA2OGMwLTguMDcgNi41NDUtMTQuNjE1IDE0LjYxNS0xNC42MTVWNi40NjF6IiBmaWxsPSIjMDA5Njg4Ij48YW5pbWF0ZVRyYW5zZm9ybSBhdHRyaWJ1dGVOYW1lPSJ0cmFuc2Zvcm0iIGF0dHJpYnV0ZVR5cGU9IlhNTCIgZHVyPSIxcyIgZnJvbT0iMCAyNSAyNSIgcmVwZWF0Q291bnQ9ImluZGVmaW5pdGUiIHRvPSIzNjAgMjUgMjUiIHR5cGU9InJvdGF0ZSIvPjwvcGF0aD48L3N2Zz4=">',
|
||||
},
|
||||
// 控制栏配置
|
||||
controls: [
|
||||
{
|
||||
position: 'left',
|
||||
index: 13,
|
||||
html: '<i class="art-icon flex"><svg width="22" height="22" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M6 18l8.5-6L6 6v12zM16 6v12h2V6h-2z" fill="currentColor"/></svg></i>',
|
||||
tooltip: '播放下一集',
|
||||
click: function () {
|
||||
handleNextEpisode();
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
// 监听播放器事件
|
||||
artPlayerRef.current.on('ready', () => {
|
||||
setError(null);
|
||||
|
||||
// 若存在需要恢复的播放进度,则跳转
|
||||
if (resumeTimeRef.current && resumeTimeRef.current > 0) {
|
||||
try {
|
||||
artPlayerRef.current.video.currentTime = resumeTimeRef.current;
|
||||
} catch (err) {
|
||||
console.warn('恢复播放进度失败:', err);
|
||||
}
|
||||
resumeTimeRef.current = null;
|
||||
}
|
||||
});
|
||||
|
||||
artPlayerRef.current.on('error', (err: any) => {
|
||||
console.error('播放器错误:', err);
|
||||
setError('视频播放失败');
|
||||
});
|
||||
|
||||
// 监听视频播放结束事件,自动播放下一集
|
||||
artPlayerRef.current.on('video:ended', () => {
|
||||
const d = detailRef.current;
|
||||
const idx = currentEpisodeIndexRef.current;
|
||||
if (d && d.episodes && idx < d.episodes.length - 1) {
|
||||
setTimeout(() => {
|
||||
setCurrentEpisodeIndex(idx + 1);
|
||||
}, 1000);
|
||||
}
|
||||
});
|
||||
if (artPlayerRef.current?.video) {
|
||||
console.log('attachVideoEventListeners');
|
||||
attachVideoEventListeners(
|
||||
artPlayerRef.current.video as HTMLVideoElement
|
||||
);
|
||||
ensureVideoSource(
|
||||
artPlayerRef.current.video as HTMLVideoElement,
|
||||
videoUrl
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('创建播放器失败:', err);
|
||||
setError('播放器初始化失败');
|
||||
}
|
||||
}, [Artplayer, Hls, videoUrl]);
|
||||
|
||||
const attachVideoEventListeners = (video: HTMLVideoElement) => {
|
||||
if (!video) return;
|
||||
|
||||
// 移除旧监听器(如果存在)
|
||||
if (videoEventListenersRef.current) {
|
||||
const { video: oldVideo, listeners } = videoEventListenersRef.current;
|
||||
listeners.forEach(({ event, handler }) => {
|
||||
oldVideo.removeEventListener(event, handler);
|
||||
});
|
||||
videoEventListenersRef.current = null;
|
||||
}
|
||||
|
||||
// 暂停时立即保存
|
||||
const pauseHandler = () => {
|
||||
saveCurrentPlayProgress();
|
||||
};
|
||||
|
||||
// 阻止移动端长按弹出系统菜单
|
||||
const contextMenuHandler = (e: Event) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
};
|
||||
|
||||
// timeupdate 节流(5 秒)保存
|
||||
let lastSave = 0;
|
||||
const timeUpdateHandler = () => {
|
||||
const now = Date.now();
|
||||
if (now - lastSave > 5000) {
|
||||
saveCurrentPlayProgress();
|
||||
lastSave = now;
|
||||
}
|
||||
};
|
||||
|
||||
video.addEventListener('pause', pauseHandler);
|
||||
video.addEventListener('timeupdate', timeUpdateHandler);
|
||||
video.addEventListener('contextmenu', contextMenuHandler);
|
||||
|
||||
videoEventListenersRef.current = {
|
||||
video,
|
||||
listeners: [
|
||||
{ event: 'pause', handler: pauseHandler },
|
||||
{ event: 'timeupdate', handler: timeUpdateHandler },
|
||||
{ event: 'contextmenu', handler: contextMenuHandler },
|
||||
],
|
||||
};
|
||||
};
|
||||
|
||||
// 当组件卸载时清理定时器
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (saveIntervalRef.current) {
|
||||
clearInterval(saveIntervalRef.current);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<PageLayout activePath='/play'>
|
||||
<div className='flex items-center justify-center h-screen'>
|
||||
<div className='text-center'>
|
||||
<div className='animate-spin rounded-full h-32 w-32 border-b-2 border-green-500 mx-auto mb-4'></div>
|
||||
<p className='text-lg text-gray-600 dark:text-gray-300'>
|
||||
加载中...
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</PageLayout>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<PageLayout activePath='/play'>
|
||||
<div className='flex items-center justify-center h-screen'>
|
||||
<div className='text-center'>
|
||||
<div className='text-red-500 text-6xl mb-4'>⚠️</div>
|
||||
<p className='text-lg text-red-600 dark:text-red-400 mb-4'>
|
||||
{error}
|
||||
</p>
|
||||
<button
|
||||
onClick={() => window.location.reload()}
|
||||
className='px-4 py-2 bg-green-500 text-white rounded-md hover:bg-green-600 transition-colors'
|
||||
>
|
||||
重试
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</PageLayout>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<PageLayout activePath='/play'>
|
||||
<div className='flex flex-col gap-6 py-4 px-10 md:px-32'>
|
||||
<div className='flex flex-col gap-6 py-4 px-10 md:px-24'>
|
||||
{/* 第一行:影片标题 */}
|
||||
<div className='py-1'>
|
||||
<h1 className='text-xl font-semibold text-gray-900 dark:text-gray-100'>
|
||||
影片标题
|
||||
{videoTitle || '影片标题'}
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
{/* 第二行:播放器和选集 */}
|
||||
<div className='grid grid-cols-1 md:grid-cols-4 gap-4 md:h-[650px]'>
|
||||
{/* 播放器 */}
|
||||
<div className='md:col-span-3 h-[400px] md:h-full'>
|
||||
<div
|
||||
id='player-container'
|
||||
className='bg-black w-full h-full rounded-lg dark:border dark:border-white/10'
|
||||
id='artplayer-container'
|
||||
className='bg-black w-full h-full rounded-2xl overflow-hidden border border-white/10'
|
||||
></div>
|
||||
</div>
|
||||
|
||||
{/* 选集 */}
|
||||
<div className='md:col-span-1 h-full md:overflow-hidden'>
|
||||
<EpisodeSelector totalEpisodes={300} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 海报(移动端在上方) */}
|
||||
<div className='block md:hidden'>
|
||||
<div className='p-4'>
|
||||
<div className='bg-gray-300 dark:bg-gray-700 aspect-[3/4] max-w-xs mx-auto flex items-center justify-center'>
|
||||
<span className='text-gray-600 dark:text-gray-400'>封面图片</span>
|
||||
</div>
|
||||
<EpisodeSelector
|
||||
totalEpisodes={totalEpisodes}
|
||||
value={currentEpisodeIndex + 1}
|
||||
onChange={handleEpisodeChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -182,56 +683,56 @@ function PlayPageClient() {
|
||||
<div className='grid grid-cols-1 md:grid-cols-4 gap-4'>
|
||||
{/* 文字区 */}
|
||||
<div className='md:col-span-3'>
|
||||
<div className='p-6'>
|
||||
<h2 className='text-2xl font-bold mb-4'>影片详情</h2>
|
||||
<div className='space-y-4'>
|
||||
<div>
|
||||
<h3 className='font-semibold text-lg mb-2'>剧情简介</h3>
|
||||
<p className='text-gray-700 dark:text-gray-300 leading-relaxed'>
|
||||
这里是影片的详细剧情介绍。可以包含影片的背景、故事梗概、主要人物关系等信息。
|
||||
文字内容可以根据实际的影片数据动态显示。
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className='font-semibold text-lg mb-2'>基本信息</h3>
|
||||
<div className='grid grid-cols-2 gap-4 text-sm'>
|
||||
<div>
|
||||
<span className='font-medium'>导演:</span>
|
||||
<span className='text-gray-700 dark:text-gray-300'>
|
||||
待获取
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className='font-medium'>主演:</span>
|
||||
<span className='text-gray-700 dark:text-gray-300'>
|
||||
待获取
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className='font-medium'>类型:</span>
|
||||
<span className='text-gray-700 dark:text-gray-300'>
|
||||
待获取
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className='font-medium'>年份:</span>
|
||||
<span className='text-gray-700 dark:text-gray-300'>
|
||||
待获取
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='p-6 flex flex-col min-h-0'>
|
||||
{/* 标题 */}
|
||||
<h1 className='text-3xl font-bold mb-2 tracking-wide flex items-center flex-shrink-0 text-center md:text-left w-full'>
|
||||
{videoTitle || '影片标题'}
|
||||
</h1>
|
||||
|
||||
{/* 关键信息行 */}
|
||||
<div className='flex flex-wrap items-center gap-3 text-base mb-4 opacity-80 flex-shrink-0'>
|
||||
{detail?.class && (
|
||||
<span className='text-green-600 font-semibold'>
|
||||
{detail.class}
|
||||
</span>
|
||||
)}
|
||||
{(detail?.year || videoYear) && (
|
||||
<span>{detail?.year || videoYear}</span>
|
||||
)}
|
||||
{detail?.source_name && (
|
||||
<span className='border border-gray-500/60 px-2 py-[1px] rounded'>
|
||||
{detail.source_name}
|
||||
</span>
|
||||
)}
|
||||
{detail?.type_name && <span>{detail.type_name}</span>}
|
||||
</div>
|
||||
{/* 剧情简介 */}
|
||||
{detail?.desc && (
|
||||
<div
|
||||
className='mt-0 text-base leading-relaxed opacity-90 overflow-y-auto pr-2 flex-1 min-h-0 scrollbar-hide'
|
||||
style={{ whiteSpace: 'pre-line' }}
|
||||
>
|
||||
{detail.desc}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 封面展示(桌面端在右侧) */}
|
||||
<div className='hidden md:block md:col-span-1'>
|
||||
<div className='p-4'>
|
||||
<div className='bg-gray-300 dark:bg-gray-700 aspect-[3/4] flex items-center justify-center'>
|
||||
<span className='text-gray-600 dark:text-gray-400'>
|
||||
封面图片
|
||||
</span>
|
||||
{/* 封面展示 */}
|
||||
<div className='hidden md:block md:col-span-1 md:order-first'>
|
||||
<div className='pl-0 py-4 pr-6'>
|
||||
<div className='bg-gray-300 dark:bg-gray-700 aspect-[3/4] flex items-center justify-center rounded-2xl overflow-hidden'>
|
||||
{videoCover ? (
|
||||
<img
|
||||
src={videoCover}
|
||||
alt={videoTitle}
|
||||
className='w-full h-full object-cover'
|
||||
/>
|
||||
) : (
|
||||
<span className='text-gray-600 dark:text-gray-400'>
|
||||
封面图片
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -243,7 +744,7 @@ function PlayPageClient() {
|
||||
|
||||
export default function PlayPage() {
|
||||
return (
|
||||
<Suspense>
|
||||
<Suspense fallback={<div>Loading...</div>}>
|
||||
<PlayPageClient />
|
||||
</Suspense>
|
||||
);
|
||||
|
||||
@@ -84,7 +84,7 @@ const EpisodeSelector: React.FC<EpisodeSelectorProps> = ({
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`md:ml-6 px-6 py-3 h-full rounded-lg bg-black/10 dark:bg-white/5 flex flex-col ${className}`.trim()}
|
||||
className={`md:ml-6 px-6 py-3 h-full rounded-2xl bg-black/10 dark:bg-white/5 flex flex-col ${className}`.trim()}
|
||||
>
|
||||
{/* 分类标签 */}
|
||||
<div className='flex items-center gap-4 mb-4 border-b border-gray-300 dark:border-gray-700 -mx-6 px-6 flex-shrink-0'>
|
||||
@@ -141,7 +141,7 @@ const EpisodeSelector: React.FC<EpisodeSelectorProps> = ({
|
||||
</div>
|
||||
|
||||
{/* 集数网格 */}
|
||||
<div className='grid grid-cols-[repeat(auto-fill,minmax(48px,1fr))] gap-3 overflow-y-auto h-full'>
|
||||
<div className='grid grid-cols-[repeat(auto-fill,minmax(40px,1fr))] auto-rows-[40px] gap-x-3 gap-y-3 overflow-y-auto h-full'>
|
||||
{(() => {
|
||||
const len = currentEnd - currentStart + 1;
|
||||
const episodes = Array.from({ length: len }, (_, i) =>
|
||||
|
||||
Reference in New Issue
Block a user