浏览代码

更新视频播放器组件,新增VideoJSPlayer组件以替代VideoProcessor,优化封面和视频播放逻辑,提升用户体验和代码整洁性。同时,更新依赖项并调整环境变量配置。

wuyi 2 月之前
父节点
当前提交
3752669e9b
共有 10 个文件被更改,包括 1815 次插入2031 次删除
  1. 6 6
      .env
  2. 1 1
      dev-dist/sw.js
  3. 264 1
      package-lock.json
  4. 3 0
      package.json
  5. 1197 0
      src/components/VideoJSPlayer.vue
  6. 0 1512
      src/components/VideoProcessor.vue
  7. 3 3
      src/views/Home.vue
  8. 1 1
      src/views/Purchased.vue
  9. 39 206
      src/views/VideoPlayer.vue
  10. 301 301
      yarn.lock

+ 6 - 6
.env

@@ -1,7 +1,7 @@
-VITE_API_URL=http://localhost:3010/api
+VITE_API_URL=http://192.168.6.244:3010/api
 
-VITE_VIDEO_API_URL=https://qa3-api.69mediatest.com/api
-VITE_VIDEO_PLAT_ID=30017
-VITE_VIDEO_CHANNEL_ID=30017001
-VITE_VIDEO_USER_ID=30005476
-VITE_VIDEO_TOKEN=ITM1cDO3gTN3EzImlTNllDOjJDNjJmNykTO0EmYyMWY2EWM5ETOxYDZlhzI2cDN1ADMwMzIzQmM5YGZwYjNlFTMwgzY1I2MkRWN5QGOmJTMjJGN4MzI2MjO0AjO2EDI1ITL5ATL1IDMyMCN
+VITE_VIDEO_API_URL=https://api.xmqspco.com/api
+VITE_VIDEO_PLAT_ID=60005
+VITE_VIDEO_CHANNEL_ID=60005001
+VITE_VIDEO_USER_ID=605500911
+VITE_VIDEO_TOKEN=ykDO5UTMwYzNxMCZiRGN0M2YzEjYiF2YjVWMlV2M2M2N0QTO1UGOhhjZ4MSMxkDMwUTNwYzIxMTNllTYkdDMklDOjlzNyUGMkNTY0czM1kDN0QTY4M2I3IjO1EjOzEDIxETLwETL1IDMyMCN

+ 1 - 1
dev-dist/sw.js

@@ -79,7 +79,7 @@ define(['./workbox-f2cb1a81'], (function (workbox) { 'use strict';
    */
   workbox.precacheAndRoute([{
     "url": "index.html",
-    "revision": "0.i86r50mttco"
+    "revision": "0.vad8o11l318"
   }], {});
   workbox.cleanupOutdatedCaches();
   workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), {

+ 264 - 1
package-lock.json

@@ -10,12 +10,15 @@
       "dependencies": {
         "@primevue/forms": "^4.3.9",
         "@types/axios": "^0.14.4",
+        "@videojs/http-streaming": "^3.17.2",
         "@vueuse/core": "^13.9.0",
         "axios": "^1.12.2",
         "hls.js": "^1.6.13",
         "pinia": "^2.1.0",
         "primeicons": "^7.0.0",
         "primevue": "^4.3.9",
+        "vconsole": "^3.15.1",
+        "video.js": "^8.23.4",
         "vue": "^3.5.18",
         "vue-router": "^4.5.1",
         "zod": "^4.1.9"
@@ -23,6 +26,7 @@
       "devDependencies": {
         "@tsconfig/node20": "^20.1.6",
         "@types/node": "^24.5.1",
+        "@types/video.js": "^7.3.58",
         "@vitejs/plugin-vue": "^6.0.1",
         "@vue/tsconfig": "^0.7.0",
         "autoprefixer": "^10.4.19",
@@ -1459,7 +1463,6 @@
       "version": "7.28.4",
       "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz",
       "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==",
-      "dev": true,
       "engines": {
         "node": ">=6.9.0"
       }
@@ -3034,12 +3037,67 @@
       "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
       "dev": true
     },
+    "node_modules/@types/video.js": {
+      "version": "7.3.58",
+      "resolved": "https://registry.npmjs.org/@types/video.js/-/video.js-7.3.58.tgz",
+      "integrity": "sha512-1CQjuSrgbv1/dhmcfQ83eVyYbvGyqhTvb2Opxr0QCV+iJ4J6/J+XWQ3Om59WiwCd1MN3rDUHasx5XRrpUtewYQ==",
+      "dev": true,
+      "license": "MIT"
+    },
     "node_modules/@types/web-bluetooth": {
       "version": "0.0.21",
       "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz",
       "integrity": "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==",
       "license": "MIT"
     },
+    "node_modules/@videojs/http-streaming": {
+      "version": "3.17.2",
+      "resolved": "https://registry.npmjs.org/@videojs/http-streaming/-/http-streaming-3.17.2.tgz",
+      "integrity": "sha512-VBQ3W4wnKnVKb/limLdtSD2rAd5cmHN70xoMf4OmuDd0t2kfJX04G+sfw6u2j8oOm2BXYM9E1f4acHruqKnM1g==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@babel/runtime": "^7.12.5",
+        "@videojs/vhs-utils": "^4.1.1",
+        "aes-decrypter": "^4.0.2",
+        "global": "^4.4.0",
+        "m3u8-parser": "^7.2.0",
+        "mpd-parser": "^1.3.1",
+        "mux.js": "7.1.0",
+        "video.js": "^7 || ^8"
+      },
+      "engines": {
+        "node": ">=8",
+        "npm": ">=5"
+      },
+      "peerDependencies": {
+        "video.js": "^8.19.0"
+      }
+    },
+    "node_modules/@videojs/vhs-utils": {
+      "version": "4.1.1",
+      "resolved": "https://registry.npmjs.org/@videojs/vhs-utils/-/vhs-utils-4.1.1.tgz",
+      "integrity": "sha512-5iLX6sR2ownbv4Mtejw6Ax+naosGvoT9kY+gcuHzANyUZZ+4NpeNdKMUhb6ag0acYej1Y7cmr/F2+4PrggMiVA==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.12.5",
+        "global": "^4.4.0"
+      },
+      "engines": {
+        "node": ">=8",
+        "npm": ">=5"
+      }
+    },
+    "node_modules/@videojs/xhr": {
+      "version": "2.7.0",
+      "resolved": "https://registry.npmjs.org/@videojs/xhr/-/xhr-2.7.0.tgz",
+      "integrity": "sha512-giab+EVRanChIupZK7gXjHy90y3nncA2phIOyG3Ne5fvpiMJzvqYwiTOnEVW2S4CoYcuKJkomat7bMXA/UoUZQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.5.5",
+        "global": "~4.4.0",
+        "is-function": "^1.0.1"
+      }
+    },
     "node_modules/@vitejs/plugin-vue": {
       "version": "6.0.1",
       "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.1.tgz",
@@ -3285,6 +3343,15 @@
         "vue": "^3.5.0"
       }
     },
+    "node_modules/@xmldom/xmldom": {
+      "version": "0.8.11",
+      "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.11.tgz",
+      "integrity": "sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=10.0.0"
+      }
+    },
     "node_modules/acorn": {
       "version": "8.15.0",
       "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
@@ -3297,6 +3364,18 @@
         "node": ">=0.4.0"
       }
     },
+    "node_modules/aes-decrypter": {
+      "version": "4.0.2",
+      "resolved": "https://registry.npmjs.org/aes-decrypter/-/aes-decrypter-4.0.2.tgz",
+      "integrity": "sha512-lc+/9s6iJvuaRe5qDlMTpCFjnwpkeOXp8qP3oiZ5jsj1MRg+SBVUmmICrhxHvc8OELSmc+fEyyxAuppY6hrWzw==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@babel/runtime": "^7.12.5",
+        "@videojs/vhs-utils": "^4.1.1",
+        "global": "^4.4.0",
+        "pkcs7": "^1.0.4"
+      }
+    },
     "node_modules/ajv": {
       "version": "8.17.1",
       "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
@@ -3826,6 +3905,29 @@
       "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
       "dev": true
     },
+    "node_modules/copy-text-to-clipboard": {
+      "version": "3.2.2",
+      "resolved": "https://registry.npmjs.org/copy-text-to-clipboard/-/copy-text-to-clipboard-3.2.2.tgz",
+      "integrity": "sha512-T6SqyLd1iLuqPA90J5N4cTalrtovCySh58iiZDGJ6FGznbclKh4UI+FGacQSgFzwKG77W7XT5gwbVEbd9cIH1A==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/core-js": {
+      "version": "3.46.0",
+      "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.46.0.tgz",
+      "integrity": "sha512-vDMm9B0xnqqZ8uSBpZ8sNtRtOdmfShrvT6h2TuQGLs0Is+cR0DYbj/KWP6ALVNbWPpqA/qPLoOuppJN07humpA==",
+      "hasInstallScript": true,
+      "license": "MIT",
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/core-js"
+      }
+    },
     "node_modules/core-js-compat": {
       "version": "3.46.0",
       "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.46.0.tgz",
@@ -4032,6 +4134,11 @@
       "dev": true,
       "license": "MIT"
     },
+    "node_modules/dom-walk": {
+      "version": "0.1.2",
+      "resolved": "https://registry.npmjs.org/dom-walk/-/dom-walk-0.1.2.tgz",
+      "integrity": "sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w=="
+    },
     "node_modules/dunder-proto": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
@@ -4678,6 +4785,16 @@
         "node": ">=10.13.0"
       }
     },
+    "node_modules/global": {
+      "version": "4.4.0",
+      "resolved": "https://registry.npmjs.org/global/-/global-4.4.0.tgz",
+      "integrity": "sha512-wv/LAoHdRE3BeTGz53FAamhGlPLhlssK45usmGFThIi4XqnBmjKQ16u+RNbP7WvigRZDxUsM0J3gcQ5yicaL0w==",
+      "license": "MIT",
+      "dependencies": {
+        "min-document": "^2.19.0",
+        "process": "^0.11.10"
+      }
+    },
     "node_modules/globalthis": {
       "version": "1.0.4",
       "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz",
@@ -5019,6 +5136,12 @@
         "node": ">=8"
       }
     },
+    "node_modules/is-function": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/is-function/-/is-function-1.0.2.tgz",
+      "integrity": "sha512-lw7DUp0aWXYg+CBCN+JKkcE0Q2RayZnSvnZBlwgxHBQhqt5pZNVy4Ri7H9GmmXkdu7LUthszM+Tor1u/2iBcpQ==",
+      "license": "MIT"
+    },
     "node_modules/is-generator-function": {
       "version": "1.1.2",
       "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz",
@@ -5446,6 +5569,17 @@
       "dev": true,
       "license": "ISC"
     },
+    "node_modules/m3u8-parser": {
+      "version": "7.2.0",
+      "resolved": "https://registry.npmjs.org/m3u8-parser/-/m3u8-parser-7.2.0.tgz",
+      "integrity": "sha512-CRatFqpjVtMiMaKXxNvuI3I++vUumIXVVT/JpCpdU/FynV/ceVw1qpPyyBNindL+JlPMSesx+WX1QJaZEJSaMQ==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@babel/runtime": "^7.12.5",
+        "@videojs/vhs-utils": "^4.1.1",
+        "global": "^4.4.0"
+      }
+    },
     "node_modules/magic-string": {
       "version": "0.30.19",
       "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.19.tgz",
@@ -5522,6 +5656,14 @@
         "node": ">= 0.6"
       }
     },
+    "node_modules/min-document": {
+      "version": "2.19.0",
+      "resolved": "https://registry.npmjs.org/min-document/-/min-document-2.19.0.tgz",
+      "integrity": "sha512-9Wy1B3m3f66bPPmU5hdA4DR4PB2OfDU/+GS3yAB7IQozE3tqXaVv2zOjgla7MEGSRv95+ILmOuvhLkOK6wJtCQ==",
+      "dependencies": {
+        "dom-walk": "^0.1.0"
+      }
+    },
     "node_modules/minimatch": {
       "version": "9.0.5",
       "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
@@ -5548,6 +5690,21 @@
         "node": ">=16 || 14 >=14.17"
       }
     },
+    "node_modules/mpd-parser": {
+      "version": "1.3.1",
+      "resolved": "https://registry.npmjs.org/mpd-parser/-/mpd-parser-1.3.1.tgz",
+      "integrity": "sha512-1FuyEWI5k2HcmhS1HkKnUAQV7yFPfXPht2DnRRGtoiiAAW+ESTbtEXIDpRkwdU+XyrQuwrIym7UkoPKsZ0SyFw==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@babel/runtime": "^7.12.5",
+        "@videojs/vhs-utils": "^4.0.0",
+        "@xmldom/xmldom": "^0.8.3",
+        "global": "^4.4.0"
+      },
+      "bin": {
+        "mpd-to-m3u8-json": "bin/parse.js"
+      }
+    },
     "node_modules/ms": {
       "version": "2.1.3",
       "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
@@ -5561,6 +5718,28 @@
       "dev": true,
       "license": "MIT"
     },
+    "node_modules/mutation-observer": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/mutation-observer/-/mutation-observer-1.0.3.tgz",
+      "integrity": "sha512-M/O/4rF2h776hV7qGMZUH3utZLO/jK7p8rnNgGkjKUw8zCGjRQPxB8z6+5l8+VjRUQ3dNYu4vjqXYLr+U8ZVNA=="
+    },
+    "node_modules/mux.js": {
+      "version": "7.1.0",
+      "resolved": "https://registry.npmjs.org/mux.js/-/mux.js-7.1.0.tgz",
+      "integrity": "sha512-NTxawK/BBELJrYsZThEulyUMDVlLizKdxyAsMuzoCD1eFj97BVaA8D/CvKsKu6FOLYkFojN5CbM9h++ZTZtknA==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@babel/runtime": "^7.11.2",
+        "global": "^4.4.0"
+      },
+      "bin": {
+        "muxjs-transmux": "bin/transmux.js"
+      },
+      "engines": {
+        "node": ">=8",
+        "npm": ">=5"
+      }
+    },
     "node_modules/mz": {
       "version": "2.7.0",
       "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz",
@@ -5823,6 +6002,18 @@
         "node": ">= 6"
       }
     },
+    "node_modules/pkcs7": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/pkcs7/-/pkcs7-1.0.4.tgz",
+      "integrity": "sha512-afRERtHn54AlwaF2/+LFszyAANTCggGilmcmILUzEjvs3XgFZT+xE6+QWQcAGmu4xajy+Xtj7acLOPdx5/eXWQ==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@babel/runtime": "^7.5.5"
+      },
+      "bin": {
+        "pkcs7": "bin/cli.js"
+      }
+    },
     "node_modules/possible-typed-array-names": {
       "version": "1.1.0",
       "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz",
@@ -6021,6 +6212,15 @@
         "node": ">=12.11.0"
       }
     },
+    "node_modules/process": {
+      "version": "0.11.10",
+      "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
+      "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6.0"
+      }
+    },
     "node_modules/proxy-from-env": {
       "version": "1.1.0",
       "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
@@ -7313,6 +7513,69 @@
       "dev": true,
       "license": "MIT"
     },
+    "node_modules/vconsole": {
+      "version": "3.15.1",
+      "resolved": "https://registry.npmjs.org/vconsole/-/vconsole-3.15.1.tgz",
+      "integrity": "sha512-KH8XLdrq9T5YHJO/ixrjivHfmF2PC2CdVoK6RWZB4yftMykYIaXY1mxZYAic70vADM54kpMQF+dYmvl5NRNy1g==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.17.2",
+        "copy-text-to-clipboard": "^3.0.1",
+        "core-js": "^3.11.0",
+        "mutation-observer": "^1.0.3"
+      }
+    },
+    "node_modules/video.js": {
+      "version": "8.23.4",
+      "resolved": "https://registry.npmjs.org/video.js/-/video.js-8.23.4.tgz",
+      "integrity": "sha512-qI0VTlYmKzEqRsz1Nppdfcaww4RSxZAq77z2oNSl3cNg2h6do5C8Ffl0KqWQ1OpD8desWXsCrde7tKJ9gGTEyQ==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@babel/runtime": "^7.12.5",
+        "@videojs/http-streaming": "^3.17.2",
+        "@videojs/vhs-utils": "^4.1.1",
+        "@videojs/xhr": "2.7.0",
+        "aes-decrypter": "^4.0.2",
+        "global": "4.4.0",
+        "m3u8-parser": "^7.2.0",
+        "mpd-parser": "^1.3.1",
+        "mux.js": "^7.0.1",
+        "videojs-contrib-quality-levels": "4.1.0",
+        "videojs-font": "4.2.0",
+        "videojs-vtt.js": "0.15.5"
+      }
+    },
+    "node_modules/videojs-contrib-quality-levels": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/videojs-contrib-quality-levels/-/videojs-contrib-quality-levels-4.1.0.tgz",
+      "integrity": "sha512-TfrXJJg1Bv4t6TOCMEVMwF/CoS8iENYsWNKip8zfhB5kTcegiFYezEA0eHAJPU64ZC8NQbxQgOwAsYU8VXbOWA==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "global": "^4.4.0"
+      },
+      "engines": {
+        "node": ">=16",
+        "npm": ">=8"
+      },
+      "peerDependencies": {
+        "video.js": "^8"
+      }
+    },
+    "node_modules/videojs-font": {
+      "version": "4.2.0",
+      "resolved": "https://registry.npmjs.org/videojs-font/-/videojs-font-4.2.0.tgz",
+      "integrity": "sha512-YPq+wiKoGy2/M7ccjmlvwi58z2xsykkkfNMyIg4xb7EZQQNwB71hcSsB3o75CqQV7/y5lXkXhI/rsGAS7jfEmQ==",
+      "license": "Apache-2.0"
+    },
+    "node_modules/videojs-vtt.js": {
+      "version": "0.15.5",
+      "resolved": "https://registry.npmjs.org/videojs-vtt.js/-/videojs-vtt.js-0.15.5.tgz",
+      "integrity": "sha512-yZbBxvA7QMYn15Lr/ZfhhLPrNpI/RmCSCqgIff57GC2gIrV5YfyzLfLyZMj0NnZSAz8syB4N0nHXpZg9MyrMOQ==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "global": "^4.3.1"
+      }
+    },
     "node_modules/vite": {
       "version": "7.1.5",
       "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.5.tgz",

+ 3 - 0
package.json

@@ -11,6 +11,7 @@
   "dependencies": {
     "@primevue/forms": "^4.3.9",
     "@types/axios": "^0.14.4",
+    "@videojs/http-streaming": "^3.17.2",
     "@vueuse/core": "^13.9.0",
     "axios": "^1.12.2",
     "hls.js": "^1.6.13",
@@ -18,6 +19,7 @@
     "primeicons": "^7.0.0",
     "primevue": "^4.3.9",
     "vconsole": "^3.15.1",
+    "video.js": "^8.23.4",
     "vue": "^3.5.18",
     "vue-router": "^4.5.1",
     "zod": "^4.1.9"
@@ -25,6 +27,7 @@
   "devDependencies": {
     "@tsconfig/node20": "^20.1.6",
     "@types/node": "^24.5.1",
+    "@types/video.js": "^7.3.58",
     "@vitejs/plugin-vue": "^6.0.1",
     "@vue/tsconfig": "^0.7.0",
     "autoprefixer": "^10.4.19",

+ 1197 - 0
src/components/VideoJSPlayer.vue

@@ -0,0 +1,1197 @@
+<template>
+  <div class="video-js-container">
+    <!-- 封面图片 -->
+    <img
+      v-if="processedCoverUrl && !isVideoMode && !forceShowPlayer"
+      :src="processedCoverUrl"
+      :alt="alt"
+      :class="coverClass"
+      @error="handleCoverError"
+      @load="handleCoverLoad"
+      style="display: block; width: 100%; height: 100%"
+    />
+
+    <!-- Video.js 播放器容器 -->
+    <div
+      v-if="isVideoMode || forceShowPlayer"
+      ref="videoContainer"
+      class="video-js-wrapper"
+      :class="[videoClass, { 'loading-video': loading }]"
+    ></div>
+
+    <!-- 加载状态 -->
+    <div v-if="loading" class="loading-overlay">
+      <div class="spinner"></div>
+      <span class="loading-text">加载中...</span>
+    </div>
+
+    <!-- 错误状态 -->
+    <div v-if="error && !hideError" class="error-overlay">
+      <div class="error-content">
+        <div class="error-icon">⚠️</div>
+        <div class="error-text">{{ error }}</div>
+        <button v-if="showRetryButton" @click="retry" class="retry-btn">
+          重试
+        </button>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, computed, watch, onMounted, onUnmounted, nextTick } from "vue";
+import videojs from "video.js";
+import "video.js/dist/video-js.css";
+
+// Props
+interface Props {
+  coverUrl?: string;
+  m3u8Url?: string;
+  alt?: string;
+  coverClass?: string;
+  videoClass?: string;
+  autoPlay?: boolean;
+  hideError?: boolean;
+  enableRetry?: boolean;
+}
+
+const props = withDefaults(defineProps<Props>(), {
+  coverUrl: "",
+  m3u8Url: "",
+  alt: "Video Cover",
+  coverClass: "w-full h-full object-cover",
+  videoClass: "w-full h-full object-contain",
+  autoPlay: false,
+  hideError: false,
+  enableRetry: true,
+});
+
+// Emits
+const emit = defineEmits<{
+  coverLoaded: [url: string];
+  videoLoaded: [url: string];
+  error: [error: string];
+  retry: [];
+  play: [];
+  timeupdate: [];
+  seeking: [];
+  canplay: [];
+}>();
+
+// 响应式数据
+const processedCoverUrl = ref<string>("");
+const processedVideoUrl = ref<string>("");
+const loading = ref(false);
+const error = ref("");
+const retryCount = ref(0);
+const maxRetries = 3;
+const videoContainer = ref<HTMLDivElement>();
+const player = ref<any>(null);
+
+// 计算属性
+const isVideoMode = computed(
+  () => !!props.m3u8Url && props.m3u8Url.trim() !== ""
+);
+const showRetryButton = computed(
+  () => props.enableRetry && retryCount.value < maxRetries
+);
+
+// 强制显示播放器的状态
+const forceShowPlayer = ref(false);
+
+// 核心解密函数(仅用于封面)
+const loader = async (url: string): Promise<Blob | string> => {
+  const response = await fetch(url, { mode: "cors" });
+  if (!response.ok) {
+    throw new Error(`HTTP error! status: ${response.status}`);
+  }
+
+  const reader = response.body!.getReader();
+  const key = new Uint8Array(8);
+  const buffer: Uint8Array[] = [];
+  let len = 0;
+  let offset = 0;
+
+  try {
+    for (;;) {
+      const read = await reader.read();
+      if (read.done) break;
+      if (!read.value) continue;
+
+      if (len < 8) {
+        let i = 0;
+        while (i < read.value.length) {
+          key[len++] = read.value[i++];
+          if (len > 7) break;
+        }
+        if (len < 8) continue;
+        read.value = read.value.slice(i);
+      }
+
+      // 复制数据以避免修改原始数据
+      const decryptedValue = new Uint8Array(read.value.length);
+      for (let i = 0; i < read.value.length; ++i) {
+        decryptedValue[i] = read.value[i] ^ key[offset++ % 8];
+      }
+
+      buffer.push(decryptedValue);
+    }
+
+    // 合并所有缓冲区
+    const totalLength = buffer.reduce((sum, arr) => sum + arr.length, 0);
+    const result = new Uint8Array(totalLength);
+    let position = 0;
+    for (const arr of buffer) {
+      result.set(arr, position);
+      position += arr.length;
+    }
+
+    const isPoster = url.includes("cover");
+    const type = isPoster ? "application/octet-stream" : "text/plain";
+    const blob = new Blob([result], { type: type });
+
+    return isPoster ? blob : await blob.text();
+  } finally {
+    reader.releaseLock();
+  }
+};
+
+// 处理封面 URL
+const processCover = async (url: string): Promise<void> => {
+  if (!url) return;
+
+  try {
+    loading.value = true;
+    error.value = "";
+    console.log("处理封面URL:", url);
+
+    // 检查是否需要解密(仅对封面进行解密)
+    if (url.includes("cover")) {
+      try {
+        const decryptedData = await loader(url);
+
+        if (decryptedData instanceof Blob) {
+          // 直接使用解密后的 Blob 创建 URL
+          const blobUrl = URL.createObjectURL(decryptedData);
+          processedCoverUrl.value = blobUrl;
+          console.log("封面解密成功,创建Blob URL:", blobUrl);
+        } else {
+          processedCoverUrl.value = decryptedData;
+          console.log("封面解密成功,使用解密后的URL");
+        }
+      } catch (decryptErr) {
+        console.error("封面解密失败,使用原始URL:", decryptErr);
+        processedCoverUrl.value = url;
+      }
+    } else {
+      console.log("使用原始封面URL");
+      processedCoverUrl.value = url;
+    }
+
+    // 预加载图片以确保它可以正确显示
+    const img = new Image();
+    img.onload = () => {
+      console.log("封面图片加载成功,URL:", processedCoverUrl.value);
+
+      // 如果不是视频模式,确保封面图片立即显示
+      if (!isVideoMode.value) {
+        nextTick(() => {
+          const imgElement = document.querySelector(
+            ".video-js-container img"
+          ) as HTMLImageElement;
+          if (imgElement) {
+            imgElement.style.display = "block";
+            imgElement.style.opacity = "1";
+          }
+        });
+      }
+
+      emit("coverLoaded", processedCoverUrl.value);
+    };
+    img.onerror = () => {
+      console.error("封面图片加载失败");
+      // 如果加载失败,尝试使用原始URL
+      if (processedCoverUrl.value !== url) {
+        processedCoverUrl.value = url;
+        emit("coverLoaded", url);
+      }
+    };
+    img.src = processedCoverUrl.value;
+  } catch (err) {
+    console.error("处理封面失败:", err);
+    // 封面处理失败时使用原始 URL
+    processedCoverUrl.value = url;
+    emit("coverLoaded", processedCoverUrl.value);
+  } finally {
+    loading.value = false;
+  }
+};
+
+// 处理视频 URL - 直接使用
+const processVideo = async (url: string): Promise<void> => {
+  if (!url) return;
+
+  processedVideoUrl.value = url;
+  await nextTick();
+  await initVideoJSPlayer();
+  emit("videoLoaded", processedVideoUrl.value);
+};
+
+// 初始化 Video.js 播放器
+const initVideoJSPlayer = async (): Promise<void> => {
+  return new Promise((resolve, reject) => {
+    if (!videoContainer.value || !processedVideoUrl.value) {
+      console.log("初始化失败:缺少容器或视频URL");
+      reject(new Error("缺少容器或视频URL"));
+      return;
+    }
+
+    // 确保容器是干净的
+    if (videoContainer.value) {
+      videoContainer.value.innerHTML = "";
+    }
+
+    console.log("开始初始化 Video.js 播放器");
+    console.log("视频URL:", processedVideoUrl.value);
+
+    // 先测试 URL 是否可访问
+    fetch(processedVideoUrl.value, { method: "HEAD" })
+      .then((response) => {
+        console.log("URL 可访问性测试:", response.status, response.ok);
+      })
+      .catch((error) => {
+        console.error("URL 访问测试失败:", error);
+      });
+
+    // 创建 video 元素
+    const videoElement = document.createElement("video");
+    videoElement.className = "video-js vjs-big-play-centered";
+    videoElement.controls = true;
+    videoElement.preload = "auto";
+
+    // 确保设置封面
+    if (processedCoverUrl.value) {
+      videoElement.poster = processedCoverUrl.value;
+      console.log("设置视频封面:", processedCoverUrl.value);
+    } else if (props.coverUrl) {
+      videoElement.poster = props.coverUrl;
+      console.log("使用原始封面URL:", props.coverUrl);
+    }
+
+    // 添加到容器
+    videoContainer.value.appendChild(videoElement);
+
+    const options = {
+      controls: true,
+      responsive: true,
+      fluid: true,
+      preload: "auto",
+      techOrder: ["html5"],
+      bigPlayButton: true, // 启用大型播放按钮
+      userActions: {
+        // 禁用默认的点击暂停行为,但允许点击进度条
+        click: false,
+        doubleClick: false,
+        hotkeys: true,
+      },
+      controlBar: {
+        // 自定义控制栏
+        children: [
+          "playToggle", // 播放/暂停按钮
+          "replayButton", // 回退按钮
+          "forwardButton", // 快进按钮
+          "currentTimeDisplay", // 当前时间
+          "timeDivider", // 时间分隔符
+          "durationDisplay", // 总时长
+          "progressControl", // 进度条
+          "volumePanel", // 音量控制
+          "fullscreenToggle", // 全屏按钮
+        ],
+        // 确保全屏按钮在右侧
+        fullscreenToggle: {
+          index: 9,
+        },
+      },
+      // 使用默认的 HTML5 配置,Video.js 内置 HLS 支持
+      sources: [
+        {
+          src: processedVideoUrl.value,
+          type: "application/x-mpegURL",
+        },
+      ],
+      poster: processedCoverUrl.value || props.coverUrl,
+    };
+
+    try {
+      // 注册快进快退按钮组件
+      const registerSeekButtons = () => {
+        try {
+          // 注册回退按钮
+          const ReplayButton = videojs.getComponent("Button");
+          class ReplayButtonComponent extends ReplayButton {
+            constructor(player: any, options: any) {
+              super(player, options);
+              (this as any).controlText("回退10秒");
+            }
+
+            handleClick() {
+              const time = (this as any).player().currentTime();
+              if (typeof time === "number") {
+                (this as any).player().currentTime(Math.max(0, time - 10));
+              }
+            }
+
+            buildCSSClass() {
+              return `vjs-replay-button ${super.buildCSSClass()}`;
+            }
+          }
+          videojs.registerComponent("ReplayButton", ReplayButtonComponent);
+
+          // 注册快进按钮
+          const ForwardButton = videojs.getComponent("Button");
+          class ForwardButtonComponent extends ForwardButton {
+            constructor(player: any, options: any) {
+              super(player, options);
+              (this as any).controlText("快进10秒");
+            }
+
+            handleClick() {
+              const time = (this as any).player().currentTime();
+              const duration = (this as any).player().duration();
+              if (typeof time === "number" && typeof duration === "number") {
+                (this as any)
+                  .player()
+                  .currentTime(Math.min(duration, time + 10));
+              }
+            }
+
+            buildCSSClass() {
+              return `vjs-forward-button ${super.buildCSSClass()}`;
+            }
+          }
+          videojs.registerComponent("ForwardButton", ForwardButtonComponent);
+        } catch (err) {
+          console.error("注册快进快退按钮失败:", err);
+        }
+      };
+
+      registerSeekButtons();
+      player.value = videojs(videoElement, options);
+      console.log("Video.js 播放器创建成功");
+
+      // 自定义点击行为
+      player.value.ready(() => {
+        console.log("Video.js 播放器准备就绪");
+        error.value = "";
+        loading.value = false;
+        emit("canplay");
+
+        // 禁用默认的点击暂停行为
+        player.value.tech_.off("tap");
+
+        // 添加自定义点击处理
+        const playerEl = player.value.el();
+        let clickTimeout: number | null = null;
+        let clickCount = 0;
+
+        playerEl.addEventListener("click", (event: MouseEvent) => {
+          // 忽略控制栏和进度条上的点击
+          const target = event.target as HTMLElement;
+          if (
+            target.closest(".vjs-control-bar") ||
+            target.closest(".vjs-big-play-button") ||
+            target.closest(".vjs-progress-control") ||
+            target.closest(".vjs-progress-holder") ||
+            target.closest(".vjs-play-progress") ||
+            target.closest(".vjs-load-progress") ||
+            target.classList.contains("vjs-progress-control") ||
+            target.classList.contains("vjs-progress-holder") ||
+            target.classList.contains("vjs-play-progress") ||
+            target.classList.contains("vjs-load-progress")
+          ) {
+            return;
+          }
+
+          clickCount++;
+          console.log("点击计数:", clickCount);
+
+          if (clickCount === 1) {
+            clickTimeout = window.setTimeout(() => {
+              // 单击 - 只显示控制栏
+              console.log("单击 - 显示控制栏");
+              player.value.userActive(true);
+              clickCount = 0;
+              clickTimeout = null;
+            }, 300);
+          } else if (clickCount === 2) {
+            // 双击 - 暂停/播放
+            console.log("双击 - 切换播放/暂停状态");
+            if (clickTimeout) {
+              clearTimeout(clickTimeout);
+              clickTimeout = null;
+            }
+
+            if (player.value.paused()) {
+              player.value.play();
+            } else {
+              player.value.pause();
+            }
+
+            clickCount = 0;
+          }
+        });
+
+        resolve();
+      });
+
+      player.value.on("loadstart", () => {
+        console.log("开始加载视频");
+        loading.value = true;
+      });
+
+      player.value.on("loadeddata", () => {
+        console.log("视频数据加载完成");
+        loading.value = false;
+      });
+
+      player.value.on("canplay", () => {
+        console.log("视频可以播放");
+        loading.value = false;
+        error.value = "";
+        emit("canplay");
+      });
+
+      player.value.on("play", () => {
+        console.log("开始播放");
+        emit("play");
+      });
+
+      player.value.on("timeupdate", () => {
+        emit("timeupdate");
+      });
+
+      player.value.on("seeking", () => {
+        emit("seeking");
+      });
+
+      player.value.on("error", (event: any) => {
+        console.error("Video.js 播放错误:", event);
+        error.value = "视频播放失败";
+        loading.value = false;
+        emit("error", error.value);
+        reject(event);
+      });
+    } catch (err) {
+      console.error("Video.js 初始化失败:", err);
+      error.value = "播放器初始化失败";
+      loading.value = false;
+      emit("error", error.value);
+      reject(err);
+    }
+  });
+};
+
+// 销毁播放器
+const destroyPlayer = (): void => {
+  if (player.value) {
+    player.value.dispose();
+    player.value = null;
+  }
+};
+
+// 重试功能
+const retry = (): void => {
+  if (retryCount.value >= maxRetries) return;
+  retryCount.value++;
+  error.value = "";
+  emit("retry");
+  destroyPlayer();
+  if (props.coverUrl) processCover(props.coverUrl);
+  if (props.m3u8Url) processVideo(props.m3u8Url);
+};
+
+// 停止视频播放
+const stopVideo = (): void => {
+  if (player.value) {
+    player.value.pause();
+    player.value.currentTime(0);
+  }
+  destroyPlayer();
+  forceShowPlayer.value = false;
+};
+
+// 事件处理
+const handleCoverError = (event: Event): void => {
+  const img = event.target as HTMLImageElement;
+  if (img.src.startsWith("blob:")) {
+    processedCoverUrl.value = props.coverUrl;
+  }
+};
+
+const handleCoverLoad = (): void => {
+  console.log("封面图片加载完成,显示封面");
+  // 确保图片可见
+  if (!isVideoMode.value && !forceShowPlayer.value) {
+    const img = document.querySelector(
+      ".video-js-container img"
+    ) as HTMLImageElement;
+    if (img) {
+      img.style.display = "block";
+      img.style.opacity = "1";
+    }
+  }
+};
+
+// 监听 props 变化
+watch(
+  () => props.coverUrl,
+  (newUrl, oldUrl) => {
+    if (newUrl && newUrl !== oldUrl) {
+      if (processedCoverUrl.value?.startsWith("blob:")) {
+        URL.revokeObjectURL(processedCoverUrl.value);
+        processedCoverUrl.value = "";
+      }
+      processCover(newUrl);
+    } else if (newUrl) {
+      processCover(newUrl);
+    }
+  },
+  { immediate: true }
+);
+
+watch(
+  () => props.m3u8Url,
+  (newUrl, oldUrl) => {
+    if (oldUrl && newUrl !== oldUrl) {
+      console.log("视频URL变化,停止当前播放并加载新视频", { newUrl, oldUrl });
+      stopVideo();
+      if (newUrl) {
+        processVideo(newUrl);
+      }
+    } else if (newUrl) {
+      processVideo(newUrl);
+    }
+  },
+  { immediate: true }
+);
+
+// 组件卸载时清理资源
+onUnmounted(() => {
+  destroyPlayer();
+
+  // 清理 Blob URL
+  if (processedCoverUrl.value?.startsWith("blob:")) {
+    URL.revokeObjectURL(processedCoverUrl.value);
+  }
+});
+
+// 手动播放方法
+const playVideo = (): void => {
+  console.log("手动播放被调用");
+
+  // 强制显示播放器
+  forceShowPlayer.value = true;
+
+  // 先确保销毁现有播放器
+  destroyPlayer();
+
+  // 等待 DOM 更新后再初始化播放器
+  nextTick(async () => {
+    try {
+      // 重新初始化播放器
+      await initVideoJSPlayer();
+
+      // 确保播放器准备就绪后再播放
+      if (player.value) {
+        player.value.ready(() => {
+          console.log("播放器准备就绪,开始播放");
+
+          // 检查播放器状态
+          console.log("播放器技术:", player.value.techName_);
+          console.log("播放器源:", player.value.currentSource());
+
+          // 禁用默认的点击暂停行为
+          if (player.value.tech_) {
+            player.value.tech_.off("tap");
+
+            // 添加自定义点击处理
+            const playerEl = player.value.el();
+            let clickTimeout: number | null = null;
+            let clickCount = 0;
+
+            playerEl.addEventListener("click", (event: MouseEvent) => {
+              // 忽略控制栏和进度条上的点击
+              const target = event.target as HTMLElement;
+              if (
+                target.closest(".vjs-control-bar") ||
+                target.closest(".vjs-big-play-button") ||
+                target.closest(".vjs-progress-control") ||
+                target.closest(".vjs-progress-holder") ||
+                target.closest(".vjs-play-progress") ||
+                target.closest(".vjs-load-progress") ||
+                target.classList.contains("vjs-progress-control") ||
+                target.classList.contains("vjs-progress-holder") ||
+                target.classList.contains("vjs-play-progress") ||
+                target.classList.contains("vjs-load-progress")
+              ) {
+                return;
+              }
+
+              clickCount++;
+              console.log("点击计数:", clickCount);
+
+              if (clickCount === 1) {
+                clickTimeout = window.setTimeout(() => {
+                  // 单击 - 只显示控制栏
+                  console.log("单击 - 显示控制栏");
+                  player.value.userActive(true);
+                  clickCount = 0;
+                  clickTimeout = null;
+                }, 300);
+              } else if (clickCount === 2) {
+                // 双击 - 暂停/播放
+                console.log("双击 - 切换播放/暂停状态");
+                if (clickTimeout) {
+                  clearTimeout(clickTimeout);
+                  clickTimeout = null;
+                }
+
+                if (player.value.paused()) {
+                  player.value.play();
+                } else {
+                  player.value.pause();
+                }
+
+                clickCount = 0;
+              }
+            });
+          }
+
+          // 检查浏览器 HLS 支持
+          const testVideo = document.createElement("video");
+          console.log("浏览器 HLS 支持:", {
+            "application/x-mpegURL": testVideo.canPlayType(
+              "application/x-mpegURL"
+            ),
+            "application/vnd.apple.mpegurl": testVideo.canPlayType(
+              "application/vnd.apple.mpegurl"
+            ),
+          });
+
+          // 延迟一点时间再播放
+          setTimeout(() => {
+            try {
+              if (player.value) {
+                // 确保源已经设置
+                player.value.src(player.value.currentSource());
+
+                console.log("尝试播放...");
+
+                const playPromise = player.value.play();
+
+                if (playPromise !== undefined) {
+                  playPromise
+                    .then(() => {
+                      console.log("播放成功启动");
+                    })
+                    .catch((err: any) => {
+                      console.error("播放失败:", err);
+
+                      // 尝试使用原生 video 元素播放
+                      console.log("尝试使用原生 video 元素播放");
+                      const video = document.createElement("video");
+                      video.src = processedVideoUrl.value;
+                      video.controls = true;
+                      video.style.width = "100%";
+                      video.style.height = "100%";
+
+                      if (videoContainer.value) {
+                        // 清空容器并添加原生视频
+                        destroyPlayer();
+                        videoContainer.value.innerHTML = "";
+                        videoContainer.value.appendChild(video);
+
+                        // 尝试播放
+                        video
+                          .play()
+                          .catch((e) => console.error("原生播放也失败:", e));
+                      }
+                    });
+                }
+              }
+            } catch (e) {
+              console.error("播放时发生异常:", e);
+            }
+          }, 500);
+        });
+      }
+    } catch (err) {
+      console.error("初始化播放器失败:", err);
+    }
+  });
+};
+
+// 暂停播放方法
+const pauseVideo = (): void => {
+  player.value?.pause();
+};
+
+// 暴露方法给父组件
+defineExpose({
+  retry,
+  stopVideo,
+  playVideo,
+  pauseVideo,
+  processedCoverUrl: computed(() => processedCoverUrl.value),
+  processedVideoUrl: computed(() => processedVideoUrl.value),
+  loading: computed(() => loading.value),
+  error: computed(() => error.value),
+});
+</script>
+
+<style scoped>
+.video-js-container {
+  position: relative;
+  width: 100%;
+  height: 100%;
+  background-color: #000;
+  overflow: hidden;
+}
+
+.video-js-container img {
+  display: block;
+  width: 100%;
+  height: 100%;
+  object-fit: cover;
+  opacity: 1;
+}
+
+.video-js-wrapper {
+  width: 100%;
+  height: 100%;
+}
+
+/* 加载中时的视频容器样式 */
+.loading-video :deep(.vjs-big-play-button) {
+  display: none !important;
+}
+
+/* Video.js 自定义样式 */
+:deep(.video-js) {
+  width: 100%;
+  height: 100%;
+  background-color: #000;
+}
+
+/* 确保封面图片始终显示 */
+:deep(.video-js .vjs-poster) {
+  background-size: cover;
+  opacity: 1 !important;
+  display: block !important;
+  background-position: center center;
+  background-repeat: no-repeat;
+  z-index: 1;
+}
+
+/* 确保封面图片在加载中状态下也显示 */
+:deep(.video-js.vjs-has-started .vjs-poster) {
+  display: none;
+}
+
+:deep(.video-js.vjs-paused .vjs-poster) {
+  display: block !important;
+}
+
+/* 自定义中央播放按钮样式 */
+:deep(.video-js .vjs-big-play-button) {
+  font-size: 3em;
+  line-height: 1.5em;
+  height: 1.5em;
+  width: 1.5em;
+  border: none;
+  border-radius: 50%;
+  background-color: rgba(59, 130, 246, 0.8);
+  box-shadow: 0 0 20px rgba(59, 130, 246, 0.4);
+  position: absolute;
+  top: 50%;
+  left: 50%;
+  margin-top: -0.75em;
+  margin-left: -0.75em;
+  padding: 0;
+  cursor: pointer;
+  transition: all 0.3s ease;
+  z-index: 100;
+}
+
+:deep(.video-js:hover .vjs-big-play-button) {
+  background-color: rgba(59, 130, 246, 1);
+  transform: scale(1.1);
+}
+
+:deep(.video-js .vjs-control-bar) {
+  background: linear-gradient(
+    to top,
+    rgba(0, 0, 0, 0.95) 0%,
+    rgba(0, 0, 0, 0.7) 60%,
+    rgba(0, 0, 0, 0) 100%
+  );
+  height: 4em; /* 增加控制栏高度 */
+  padding: 0 1.2em; /* 增加左右内边距 */
+  opacity: 1; /* 始终显示控制栏 */
+  transform: translateY(0);
+  transition: opacity 0.3s ease;
+}
+
+/* 确保控制栏在用户交互时始终可见 */
+:deep(.video-js.vjs-user-active .vjs-control-bar) {
+  opacity: 1;
+  visibility: visible;
+  pointer-events: auto;
+  animation: fadeIn 0.3s ease;
+}
+
+@keyframes fadeIn {
+  from {
+    opacity: 0;
+    transform: translateY(5px);
+  }
+  to {
+    opacity: 1;
+    transform: translateY(0);
+  }
+}
+
+/* 控制栏按钮样式 */
+:deep(.video-js .vjs-control) {
+  width: 3.2em;
+  opacity: 0.8;
+  transition: opacity 0.2s ease, transform 0.2s ease;
+}
+
+:deep(.video-js .vjs-control:hover) {
+  opacity: 1;
+  transform: scale(1.1);
+}
+
+/* 全屏按钮样式 */
+:deep(.video-js .vjs-fullscreen-control) {
+  position: absolute;
+  right: 0.5em;
+}
+
+/* 播放按钮样式 */
+:deep(.video-js .vjs-play-control) {
+  font-size: 1.3em;
+}
+
+/* 进度条样式 */
+:deep(.video-js .vjs-progress-control) {
+  position: absolute;
+  bottom: 4em;
+  left: 0;
+  right: 0;
+  height: 1em;
+  background: rgba(0, 0, 0, 0.3);
+  transition: height 0.2s ease;
+  opacity: 1 !important;
+  visibility: visible !important;
+  display: flex !important;
+  align-items: center;
+  z-index: 10;
+  pointer-events: auto !important; /* 确保进度条可点击 */
+}
+
+:deep(.video-js.vjs-user-active .vjs-progress-control),
+:deep(.video-js:hover .vjs-progress-control) {
+  height: 1.2em;
+}
+
+/* 音量控制样式 */
+:deep(.video-js .vjs-volume-panel) {
+  position: absolute;
+  right: 3.5em;
+}
+
+:deep(.video-js .vjs-progress-holder) {
+  height: 0.8em;
+  background: rgba(255, 255, 255, 0.2);
+  border-radius: 1em;
+  margin: 0 0.5em;
+  transition: height 0.2s ease;
+  display: flex !important;
+  align-items: center;
+  pointer-events: auto !important; /* 确保进度条可点击 */
+  cursor: pointer !important; /* 显示点击光标 */
+}
+
+:deep(.video-js.vjs-user-active .vjs-progress-holder),
+:deep(.video-js:hover .vjs-progress-holder) {
+  height: 1em;
+}
+
+/* 确保进度条上的所有元素都可以点击 */
+:deep(.video-js .vjs-progress-control .vjs-mouse-display),
+:deep(.video-js .vjs-progress-control .vjs-play-progress),
+:deep(.video-js .vjs-progress-control .vjs-load-progress),
+:deep(.video-js .vjs-progress-control .vjs-load-progress div) {
+  pointer-events: auto !important;
+}
+
+:deep(.video-js .vjs-play-progress) {
+  background: linear-gradient(to right, #3b82f6, #60a5fa);
+  border-radius: 1em;
+  box-shadow: 0 0 8px rgba(59, 130, 246, 0.5);
+}
+
+:deep(.video-js .vjs-play-progress:before) {
+  font-size: 0.9em;
+  top: -0.3em;
+}
+
+:deep(.video-js .vjs-load-progress) {
+  background: rgba(255, 255, 255, 0.3);
+  border-radius: 1em;
+}
+
+/* 时间显示样式 */
+:deep(.video-js .vjs-time-control) {
+  padding-left: 0.5em;
+  padding-right: 0.5em;
+  min-width: 2.2em;
+  font-family: "Arial", sans-serif;
+  font-weight: 500;
+  font-size: 0.9em;
+  opacity: 0.9;
+}
+
+:deep(.video-js .vjs-current-time) {
+  padding-right: 0.1em;
+}
+
+:deep(.video-js .vjs-duration) {
+  padding-left: 0.1em;
+}
+
+:deep(.video-js .vjs-time-divider) {
+  padding: 0;
+  min-width: 1em;
+  color: rgba(255, 255, 255, 0.8);
+}
+
+/* 快进快退按钮 */
+:deep(.video-js .vjs-replay-button) {
+  font-size: 1.3em;
+  position: relative;
+}
+
+:deep(.video-js .vjs-replay-button::before) {
+  content: "⟲";
+  text-shadow: 0 0 8px rgba(255, 255, 255, 0.3);
+}
+
+:deep(.video-js .vjs-forward-button) {
+  font-size: 1.3em;
+  position: relative;
+}
+
+:deep(.video-js .vjs-forward-button::before) {
+  content: "⟳";
+  text-shadow: 0 0 8px rgba(255, 255, 255, 0.3);
+}
+
+/* 美化播放器整体外观 */
+:deep(.video-js) {
+  border-radius: 8px;
+  overflow: hidden;
+  box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
+}
+
+.loading-overlay {
+  position: absolute;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  background: rgba(0, 0, 0, 0.6);
+  backdrop-filter: blur(4px);
+  color: white;
+  z-index: 10;
+  border-radius: 8px;
+}
+
+.spinner {
+  width: 40px;
+  height: 40px;
+  border: 3px solid rgba(59, 130, 246, 0.2);
+  border-top: 3px solid rgba(59, 130, 246, 1);
+  border-radius: 50%;
+  animation: spin 1s cubic-bezier(0.4, 0, 0.2, 1) infinite;
+  margin-bottom: 12px;
+  box-shadow: 0 0 15px rgba(59, 130, 246, 0.3);
+}
+
+@keyframes spin {
+  0% {
+    transform: rotate(0deg);
+  }
+  100% {
+    transform: rotate(360deg);
+  }
+}
+
+.loading-text {
+  font-size: 16px;
+  color: white;
+  font-weight: 500;
+  text-shadow: 0 0 10px rgba(255, 255, 255, 0.5);
+  animation: pulse 1.5s ease-in-out infinite;
+}
+
+@keyframes pulse {
+  0%,
+  100% {
+    opacity: 0.8;
+  }
+  50% {
+    opacity: 1;
+  }
+}
+
+.error-overlay {
+  position: absolute;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  background: rgba(0, 0, 0, 0.75);
+  backdrop-filter: blur(4px);
+  z-index: 10;
+  border-radius: 8px;
+}
+
+.error-content {
+  text-align: center;
+  color: white;
+  padding: 24px;
+  background: rgba(255, 255, 255, 0.1);
+  border-radius: 12px;
+  backdrop-filter: blur(8px);
+  box-shadow: 0 10px 25px rgba(0, 0, 0, 0.2);
+  max-width: 300px;
+  width: 80%;
+}
+
+.error-icon {
+  font-size: 40px;
+  margin-bottom: 16px;
+  animation: shake 0.5s cubic-bezier(0.36, 0.07, 0.19, 0.97) both;
+}
+
+@keyframes shake {
+  10%,
+  90% {
+    transform: translate3d(-1px, 0, 0);
+  }
+  20%,
+  80% {
+    transform: translate3d(2px, 0, 0);
+  }
+  30%,
+  50%,
+  70% {
+    transform: translate3d(-3px, 0, 0);
+  }
+  40%,
+  60% {
+    transform: translate3d(3px, 0, 0);
+  }
+}
+
+.error-text {
+  font-size: 16px;
+  margin-bottom: 20px;
+  color: #ff6b6b;
+  line-height: 1.5;
+}
+
+.retry-btn {
+  padding: 10px 20px;
+  background: linear-gradient(to right, #3b82f6, #60a5fa);
+  color: white;
+  border: none;
+  border-radius: 8px;
+  font-size: 15px;
+  font-weight: 600;
+  cursor: pointer;
+  transition: all 0.3s ease;
+  box-shadow: 0 4px 12px rgba(59, 130, 246, 0.4);
+}
+
+.retry-btn:hover {
+  background: linear-gradient(to right, #2563eb, #3b82f6);
+  transform: translateY(-2px);
+  box-shadow: 0 6px 16px rgba(59, 130, 246, 0.5);
+}
+
+/* 移动端优化 */
+@media screen and (max-width: 768px) {
+  :deep(.video-js .vjs-big-play-button) {
+    font-size: 2.5em;
+    height: 1.5em;
+    width: 1.5em;
+    margin-top: -0.75em;
+    margin-left: -0.75em;
+  }
+
+  :deep(.video-js .vjs-control-bar) {
+    height: 3.5em;
+    padding: 0 0.8em;
+    opacity: 1;
+  }
+
+  :deep(.video-js .vjs-control) {
+    width: 2.8em;
+  }
+
+  :deep(.video-js .vjs-progress-control) {
+    bottom: 3.5em;
+    height: 0.8em;
+    opacity: 1 !important;
+    visibility: visible !important;
+  }
+
+  :deep(.video-js .vjs-progress-holder) {
+    height: 0.8em;
+  }
+
+  :deep(.video-js .vjs-time-control) {
+    min-width: 1.8em;
+    font-size: 0.8em;
+  }
+
+  .error-content {
+    padding: 18px;
+    max-width: 260px;
+  }
+
+  .error-icon {
+    font-size: 32px;
+  }
+
+  .error-text {
+    font-size: 14px;
+  }
+
+  .retry-btn {
+    padding: 8px 16px;
+    font-size: 14px;
+  }
+}
+</style>

+ 0 - 1512
src/components/VideoProcessor.vue

@@ -1,1512 +0,0 @@
-<template>
-  <div class="video-processor" @click="toggleControls">
-    <!-- 封面图片 -->
-    <img
-      v-if="processedCoverUrl && !isVideoMode"
-      :src="processedCoverUrl"
-      :alt="alt"
-      :class="coverClass"
-      @error="handleCoverError"
-      @load="handleCoverLoad"
-    />
-
-    <!-- 视频播放器 -->
-    <video
-      v-if="isVideoMode"
-      ref="videoElement"
-      :class="videoClass"
-      :poster="processedCoverUrl"
-      preload="metadata"
-      playsinline
-      webkit-playsinline
-      x5-playsinline
-      x5-video-player-type="h5"
-      x5-video-player-fullscreen="true"
-      x5-video-orientation="landscape"
-      @loadstart="onVideoLoadStart"
-      @loadeddata="onVideoLoadedData"
-      @error="onVideoError"
-      @canplay="onVideoCanPlay"
-      @play="onVideoPlayEvent"
-      @pause="onVideoPauseEvent"
-      @timeupdate="onVideoTimeUpdate"
-      @seeking="onVideoSeeking"
-      @durationchange="onDurationChange"
-      @ended="onVideoEnded"
-    >
-      您的浏览器不支持视频播放
-    </video>
-
-    <!-- 自定义播放控制条 -->
-    <div
-      v-if="isVideoMode && !error"
-      class="custom-controls"
-      :class="{ show: showControls }"
-      @click.stop
-    >
-      <!-- 播放/暂停按钮(中央) -->
-      <div v-if="!isPlaying" class="play-button-center" @click="togglePlay">
-        <svg class="w-16 h-16" fill="white" viewBox="0 0 24 24">
-          <path d="M8 5v14l11-7z" />
-        </svg>
-      </div>
-
-      <!-- 底部控制栏 -->
-      <div class="controls-bottom">
-        <!-- 进度条 -->
-        <div class="progress-container" @click="seekVideo">
-          <div class="progress-bar">
-            <div
-              class="progress-played"
-              :style="{ width: progress + '%' }"
-            ></div>
-            <div
-              class="progress-buffered"
-              :style="{ width: buffered + '%' }"
-            ></div>
-          </div>
-        </div>
-
-        <!-- 控制按钮区域 -->
-        <div class="controls-buttons">
-          <!-- 左侧:播放/暂停 + 时间 -->
-          <div class="controls-left">
-            <button class="control-btn" @click="togglePlay">
-              <svg
-                v-if="!isPlaying"
-                class="w-6 h-6"
-                fill="white"
-                viewBox="0 0 24 24"
-              >
-                <path d="M8 5v14l11-7z" />
-              </svg>
-              <svg v-else class="w-6 h-6" fill="white" viewBox="0 0 24 24">
-                <path d="M6 4h4v16H6V4zm8 0h4v16h-4V4z" />
-              </svg>
-            </button>
-            <span class="time-display"
-              >{{ formatTime(currentTime) }} / {{ formatTime(duration) }}</span
-            >
-          </div>
-
-          <!-- 右侧:音量 + 全屏 -->
-          <div class="controls-right">
-            <!-- 音量控制 -->
-            <div class="volume-control">
-              <button class="control-btn" @click="toggleMute">
-                <svg
-                  v-if="!isMuted && volume > 0.5"
-                  class="w-6 h-6"
-                  fill="white"
-                  viewBox="0 0 24 24"
-                >
-                  <path
-                    d="M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02zM14 3.23v2.06c2.89.86 5 3.54 5 6.71s-2.11 5.85-5 6.71v2.06c4.01-.91 7-4.49 7-8.77s-2.99-7.86-7-8.77z"
-                  />
-                </svg>
-                <svg
-                  v-else-if="!isMuted && volume > 0"
-                  class="w-6 h-6"
-                  fill="white"
-                  viewBox="0 0 24 24"
-                >
-                  <path d="M7 9v6h4l5 5V4l-5 5H7z" />
-                </svg>
-                <svg v-else class="w-6 h-6" fill="white" viewBox="0 0 24 24">
-                  <path
-                    d="M16.5 12c0-1.77-1.02-3.29-2.5-4.03v2.21l2.45 2.45c.03-.2.05-.41.05-.63zm2.5 0c0 .94-.2 1.82-.54 2.64l1.51 1.51C20.63 14.91 21 13.5 21 12c0-4.28-2.99-7.86-7-8.77v2.06c2.89.86 5 3.54 5 6.71zM4.27 3L3 4.27 7.73 9H3v6h4l5 5v-6.73l4.25 4.25c-.67.52-1.42.93-2.25 1.18v2.06c1.38-.31 2.63-.95 3.69-1.81L19.73 21 21 19.73l-9-9L4.27 3zM12 4L9.91 6.09 12 8.18V4z"
-                  />
-                </svg>
-              </button>
-              <input
-                v-if="showVolumeSlider"
-                type="range"
-                class="volume-slider"
-                min="0"
-                max="100"
-                :value="volume * 100"
-                @input="changeVolume"
-              />
-            </div>
-
-            <!-- 全屏按钮 -->
-            <button
-              class="control-btn fullscreen-btn"
-              @click="toggleFullscreen"
-            >
-              <svg
-                v-if="!isFullscreen"
-                class="w-6 h-6"
-                fill="white"
-                viewBox="0 0 24 24"
-              >
-                <path
-                  d="M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z"
-                />
-              </svg>
-              <svg v-else class="w-6 h-6" fill="white" viewBox="0 0 24 24">
-                <path
-                  d="M5 16h3v3h2v-5H5v2zm3-8H5v2h5V5H8v3zm6 11h2v-3h3v-2h-5v5zm2-11V5h-2v5h5V8h-3z"
-                />
-              </svg>
-            </button>
-          </div>
-        </div>
-      </div>
-    </div>
-
-    <!-- 加载状态 -->
-    <div v-if="loading" class="loading-overlay">
-      <div class="spinner"></div>
-      <span class="loading-text">加载中...</span>
-    </div>
-
-    <!-- 错误状态 -->
-    <div v-if="error && !hideError" class="error-overlay">
-      <div class="error-content">
-        <div class="error-icon">⚠️</div>
-        <div class="error-text">{{ error }}</div>
-        <button v-if="showRetryButton" @click="retry" class="retry-btn">
-          重试
-        </button>
-      </div>
-    </div>
-  </div>
-</template>
-
-<script setup lang="ts">
-import { ref, computed, watch, onMounted, onUnmounted, nextTick } from "vue";
-import Hls from "hls.js";
-
-// Props
-interface Props {
-  coverUrl?: string;
-  m3u8Url?: string;
-  alt?: string;
-  coverClass?: string;
-  videoClass?: string;
-  autoPlay?: boolean;
-  hideError?: boolean;
-  enableRetry?: boolean;
-}
-
-const props = withDefaults(defineProps<Props>(), {
-  coverUrl: "",
-  m3u8Url: "",
-  alt: "Video Cover",
-  coverClass: "w-full h-full object-cover",
-  videoClass: "w-full h-full object-contain",
-  autoPlay: false,
-  hideError: false,
-  enableRetry: true,
-});
-
-// Emits
-const emit = defineEmits<{
-  coverLoaded: [url: string];
-  videoLoaded: [url: string];
-  error: [error: string];
-  retry: [];
-  play: [];
-  timeupdate: [];
-  seeking: [];
-  canplay: [];
-}>();
-
-// 响应式数据
-const processedCoverUrl = ref<string>("");
-const processedVideoUrl = ref<string>("");
-const loading = ref(false);
-const error = ref("");
-const retryCount = ref(0);
-const maxRetries = 3;
-const videoElement = ref<HTMLVideoElement>();
-const hlsInstance = ref<Hls | null>(null);
-
-// 播放器控制状态
-const isPlaying = ref(false);
-const currentTime = ref(0);
-const duration = ref(0);
-const progress = ref(0);
-const buffered = ref(0);
-const volume = ref(1);
-const isMuted = ref(false);
-const isFullscreen = ref(false);
-const showControls = ref(true);
-const showVolumeSlider = ref(true); // 桌面端显示音量滑块,移动端通过CSS隐藏
-let controlsTimer: ReturnType<typeof setTimeout> | null = null;
-let errorDebounceTimer: ReturnType<typeof setTimeout> | null = null; // 错误防抖定时器
-const isVideoLoading = ref(false); // 视频是否正在加载
-
-// 计算属性
-const isVideoMode = computed(() => !!props.m3u8Url);
-const showRetryButton = computed(
-  () => props.enableRetry && retryCount.value < maxRetries
-);
-
-// 核心解密函数(基于原始 JS)
-const loader = async (url: string): Promise<Blob | string> => {
-  const response = await fetch(url, { mode: "cors" });
-  if (!response.ok) {
-    throw new Error(`HTTP error! status: ${response.status}`);
-  }
-
-  const reader = response.body!.getReader();
-  const key = new Uint8Array(8);
-  const buffer: Uint8Array[] = [];
-  let len = 0;
-  let offset = 0;
-
-  try {
-    for (;;) {
-      const read = await reader.read();
-      if (read.done) break;
-      if (!read.value) continue;
-
-      if (len < 8) {
-        let i = 0;
-        while (i < read.value.length) {
-          key[len++] = read.value[i++];
-          if (len > 7) break;
-        }
-        if (len < 8) continue;
-        read.value = read.value.slice(i);
-      }
-
-      // 复制数据以避免修改原始数据
-      const decryptedValue = new Uint8Array(read.value.length);
-      for (let i = 0; i < read.value.length; ++i) {
-        decryptedValue[i] = read.value[i] ^ key[offset++ % 8];
-      }
-
-      buffer.push(decryptedValue);
-    }
-
-    // 合并所有缓冲区
-    const totalLength = buffer.reduce((sum, arr) => sum + arr.length, 0);
-    const result = new Uint8Array(totalLength);
-    let position = 0;
-    for (const arr of buffer) {
-      result.set(arr, position);
-      position += arr.length;
-    }
-
-    const isPoster = url.includes("cover");
-    const type = isPoster ? "application/octet-stream" : "text/plain";
-    const blob = new Blob([result], { type: type });
-
-    return isPoster ? blob : await blob.text();
-  } finally {
-    reader.releaseLock();
-  }
-};
-
-// 处理封面 URL
-const processCover = async (url: string): Promise<void> => {
-  if (!url) return;
-
-  try {
-    loading.value = true;
-    error.value = "";
-
-    // 检查是否需要解密
-    if (url.includes("cover") || url.includes("play")) {
-      const decryptedData = await loader(url);
-
-      if (decryptedData instanceof Blob) {
-        // 直接使用解密后的 Blob 创建 URL
-        processedCoverUrl.value = URL.createObjectURL(decryptedData);
-      } else {
-        processedCoverUrl.value = decryptedData;
-      }
-    } else {
-      processedCoverUrl.value = url;
-    }
-
-    emit("coverLoaded", processedCoverUrl.value);
-  } catch (err) {
-    // 封面处理失败时使用原始 URL
-    processedCoverUrl.value = url;
-    emit("coverLoaded", processedCoverUrl.value);
-  } finally {
-    loading.value = false;
-  }
-};
-
-// 判断是否为标准的 HLS 流地址
-const isStandardHlsUrl = (url: string): boolean => {
-  // 简单判断:只要包含 .m3u8 就是 HLS 流
-  return url.includes(".m3u8");
-};
-
-// 处理视频 URL
-const processVideo = async (url: string): Promise<void> => {
-  if (!url) return;
-
-  try {
-    loading.value = true;
-    error.value = "";
-
-    // 检查是否为标准的 HLS 流地址(不需要解密的)
-    if (
-      isStandardHlsUrl(url) &&
-      !url.includes("play") &&
-      !url.includes("cover")
-    ) {
-      processedVideoUrl.value = url;
-      await nextTick();
-      await initVideoPlayer();
-      emit("videoLoaded", processedVideoUrl.value);
-      return;
-    }
-
-    let processedUrl = url;
-    if (processedUrl.includes("cover")) {
-      processedUrl = processedUrl.replace("cover", "play");
-    }
-
-    // 检查是否需要解密
-    if (processedUrl.includes("play") || processedUrl.includes("cover")) {
-      const decryptedData = await loader(processedUrl);
-
-      if (typeof decryptedData === "string") {
-        const playlist = processM3u8Content(decryptedData, processedUrl);
-
-        // 根据不同浏览器选择合适的 MIME 类型和 URL 格式
-        let mimeType = "application/x-mpegURL";
-
-        if (isQuarkBrowser() && !isIOSQuarkBrowser()) {
-          // Android 夸克使用 Apple 格式
-          mimeType = "application/vnd.apple.mpegurl";
-          console.log("夸克浏览器使用 Apple HLS 格式");
-        }
-
-        if (isQQBrowser()) {
-          // QQ 浏览器(X5内核)必须使用 Apple 格式
-          mimeType = "application/x-mpegURL";
-          console.log("QQ浏览器使用 Apple HLS 格式(X5内核要求)");
-        }
-
-        // iOS 夸克浏览器:使用 Data URL(iOS 不支持 Blob URL + HLS.js)
-        if (isIOSQuarkBrowser()) {
-          try {
-            const base64 = btoa(unescape(encodeURIComponent(playlist)));
-            processedVideoUrl.value = `data:${mimeType};base64,${base64}`;
-            console.log("iOS 夸克浏览器使用 Data URL");
-          } catch (e) {
-            console.error("Data URL 生成失败:", e);
-            error.value =
-              "抱歉,iOS 夸克浏览器暂不支持播放此视频,建议使用 Safari 打开";
-            emit("error", error.value);
-            loading.value = false;
-            return;
-          }
-        }
-        // else if (isQQBrowser()) {
-        //   // QQ浏览器:使用正确的MIME类型
-        //   console.log("检测到QQ浏览器,使用HLS.js兼容模式");
-
-        //   try {
-        //     // 使用正确的HLS MIME类型
-        //     const blob = new Blob([playlist], { type: "application/vnd.apple.mpegurl" });
-        //     processedVideoUrl.value = URL.createObjectURL(blob);
-        //     console.log("QQ浏览器使用Blob URL,MIME类型:", "application/vnd.apple.mpegurl");
-        //   } catch (e) {
-        //     console.error("QQ浏览器Blob URL生成失败,尝试Data URL:", e);
-
-        //     // Blob失败则尝试Data URL
-        //     try {
-        //       const base64 = btoa(unescape(encodeURIComponent(playlist)));
-        //       processedVideoUrl.value = `data:application/vnd.apple.mpegurl;base64,${base64}`;
-        //       console.log("QQ浏览器降级使用Data URL");
-        //     } catch (e2) {
-        //       console.error("QQ浏览器Data URL生成失败:", e2);
-        //       error.value = "QQ浏览器视频处理失败,建议使用Chrome浏览器";
-        //       emit("error", error.value);
-        //       loading.value = false;
-        //       return;
-        //     }
-        //   }
-        // }
-
-        // 其他所有浏览器(Chrome、Firefox、Edge、UC等)使用 Blob URL
-        if (!isIOSQuarkBrowser()) {
-          const blob = new Blob([playlist], { type: mimeType });
-          processedVideoUrl.value = URL.createObjectURL(blob);
-          console.log("其他浏览器使用 Blob URL");
-        }
-      } else {
-        processedVideoUrl.value = url;
-      }
-    } else {
-      processedVideoUrl.value = url;
-    }
-
-    await nextTick();
-    await initVideoPlayer();
-    emit("videoLoaded", processedVideoUrl.value);
-  } catch (err) {
-    error.value = err instanceof Error ? err.message : "视频处理失败";
-    emit("error", error.value);
-  } finally {
-    loading.value = false;
-  }
-};
-
-// 处理 M3U8 内容
-const processM3u8Content = (m3u8Text: string, baseUrl: string): string => {
-  const playlist: string[] = [];
-  const m3u8 = m3u8Text.match(/#[^#]+/g);
-
-  if (!m3u8) {
-    throw new Error("无效的 M3U8 文件");
-  }
-
-  let processedUrl = baseUrl;
-  if (processedUrl.includes("/play")) {
-    processedUrl = processedUrl.replace("/play", "");
-  }
-
-  for (let i = 0; i < m3u8.length; ++i) {
-    const line = m3u8[i].trim();
-    if (line.startsWith("#EXTINF")) {
-      const pattern = line.match(/#EXTINF:(\d+(?:\.\d+)?),\s*([^\n]+)/);
-      if (!pattern) continue;
-
-      const segment = /^(?!http:\/\/)/.test(pattern[2])
-        ? line.replace(pattern[2], `${processedUrl}/${pattern[2]}`)
-        : line;
-      playlist.push(segment);
-    } else {
-      const processedLine = line.startsWith("#EXT-X-KEY")
-        ? line.replace(/URI="([^"]+)"/, `URI="${processedUrl}/$1"`)
-        : line;
-      playlist.push(processedLine);
-    }
-  }
-
-  return playlist.join("\n");
-};
-
-// 检测是否为 Safari 浏览器
-const isSafari = (): boolean => {
-  return /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
-};
-
-// 检测是否为 iOS Safari
-const isIOSSafari = (): boolean => {
-  return (
-    /iPad|iPhone|iPod/.test(navigator.userAgent) && !(window as any).MSStream
-  );
-};
-
-// 检测是否为夸克浏览器
-const isQuarkBrowser = (): boolean => {
-  const ua = navigator.userAgent.toLowerCase();
-  return ua.includes("quark") || ua.includes("quarks");
-};
-
-// 检测是否为 iOS 夸克浏览器
-const isIOSQuarkBrowser = (): boolean => {
-  return isQuarkBrowser() && /iPad|iPhone|iPod/.test(navigator.userAgent);
-};
-
-// 检测是否为 QQ 浏览器
-const isQQBrowser = (): boolean => {
-  const ua = navigator.userAgent.toLowerCase();
-  return ua.includes("mqqbrowser") || ua.includes("qq/");
-};
-
-// 检测是否为 UC 浏览器
-const isUCBrowser = (): boolean => {
-  const ua = navigator.userAgent.toLowerCase();
-  return (
-    ua.includes("ucbrowser") || ua.includes("ubrowser") || ua.includes("ucweb")
-  );
-};
-
-// 初始化视频播放器
-const initVideoPlayer = async (): Promise<void> => {
-  if (!videoElement.value || !processedVideoUrl.value) {
-    return;
-  }
-
-  const video = videoElement.value;
-  destroyHls();
-
-  // 设置加载状态
-  isVideoLoading.value = true;
-  error.value = "";
-
-  // 清除错误防抖定时器
-  if (errorDebounceTimer) {
-    clearTimeout(errorDebounceTimer);
-    errorDebounceTimer = null;
-  }
-
-  // 清除之前的src,避免缓存问题
-  if (video.src) {
-    video.removeAttribute("src");
-    video.load();
-  }
-
-  // iOS 夸克浏览器特殊处理(最优先)
-  // iOS 不支持 HLS.js,且夸克的自定义播放器对 Blob/Data URL 支持有限
-  // 直接使用原生播放器,让 iOS 处理
-  if (isIOSQuarkBrowser()) {
-    console.log("检测到 iOS 夸克浏览器,使用原生播放器");
-    video.src = processedVideoUrl.value;
-    video.load(); // 强制重新加载
-    return;
-  }
-
-  // Android 夸克浏览器特殊处理
-  if (isQuarkBrowser()) {
-    console.log("检测到 Android 夸克浏览器");
-
-    // Android 夸克支持 HLS.js,直接使用
-    if (Hls.isSupported()) {
-      console.log("使用 HLS.js 播放");
-      initHlsPlayer();
-    } else {
-      // 降级到原生播放
-      console.log("HLS.js 不支持,使用原生播放器");
-      video.src = processedVideoUrl.value;
-      video.load(); // 强制重新加载
-    }
-    return;
-  }
-
-  // QQ 浏览器不支持提示
-  //   if (isQQBrowser()) {
-  //     console.log("检测到 QQ 浏览器,不支持加密视频播放");
-  //     error.value =
-  //       "QQ浏览器暂不支持播放此类视频\n请使用Chrome、UC或其他浏览器访问";
-  //     // 不提供重试选项,直接返回
-  //     return;
-  //   }
-
-  // UC 浏览器特殊处理 - UC通常支持标准的HLS播放
-  if (isUCBrowser()) {
-    console.log("检测到 UC 浏览器");
-
-    // UC浏览器优先使用HLS.js,这样可以使用自定义控制条
-    if (Hls.isSupported()) {
-      console.log("UC浏览器使用HLS.js播放器");
-      initHlsPlayer();
-    } else {
-      // 如果HLS.js不支持,使用原生播放器
-      console.log("UC浏览器使用原生播放器");
-      video.src = processedVideoUrl.value;
-      video.load();
-    }
-    return;
-  }
-
-  // Safari 特殊处理(排除夸克浏览器)
-  if ((isSafari() || isIOSSafari()) && !isQuarkBrowser()) {
-    // Safari 对 blob URL 的 HLS 支持有限,特别是加密流
-    if (processedVideoUrl.value.startsWith("blob:")) {
-      // 对于 Safari,加密的 HLS 流需要使用 HLS.js
-      if (Hls.isSupported()) {
-        initHlsPlayer();
-        return;
-      } else {
-        error.value = "Safari不支持此加密视频格式";
-        return;
-      }
-    }
-
-    // Safari 原生支持 HLS(非加密流)
-    if (video.canPlayType("application/vnd.apple.mpegurl")) {
-      video.src = processedVideoUrl.value;
-      video.load();
-
-      // Safari 错误处理 - 使用一次性事件监听器
-      const handleError = () => {
-        console.log("Safari原生播放失败,尝试HLS.js");
-        error.value = "";
-        if (Hls.isSupported()) {
-          initHlsPlayer();
-        } else {
-          error.value = "Safari 视频播放失败";
-        }
-      };
-      video.addEventListener("error", handleError, { once: true });
-
-      return;
-    }
-  }
-
-  // 其他浏览器使用 HLS.js(性能更好)
-  if (Hls.isSupported()) {
-    console.log("使用 HLS.js 播放");
-    initHlsPlayer();
-  } else {
-    // 降级到原生播放器
-    console.log("HLS.js 不支持,使用原生播放器");
-    video.src = processedVideoUrl.value;
-    video.load();
-  }
-};
-
-// 初始化 HLS.js 播放器
-const initHlsPlayer = (): void => {
-  if (!videoElement.value || !processedVideoUrl.value) {
-    return;
-  }
-
-  const video = videoElement.value;
-
-  const hlsConfig = {
-    debug: false,
-    enableWorker: true,
-    lowLatencyMode: true,
-    maxBufferLength: 30,
-    maxMaxBufferLength: 60,
-    backBufferLength: 10,
-    // 添加更宽松的配置以提高兼容性
-    manifestLoadingTimeOut: 10000,
-    manifestLoadingMaxRetry: 3,
-    levelLoadingTimeOut: 10000,
-    levelLoadingMaxRetry: 3,
-  };
-
-  try {
-    hlsInstance.value = new Hls(hlsConfig);
-
-    hlsInstance.value.loadSource(processedVideoUrl.value);
-    hlsInstance.value.attachMedia(video);
-
-    hlsInstance.value.on(Hls.Events.MANIFEST_PARSED, () => {
-      console.log("HLS manifest 解析成功");
-      error.value = "";
-    });
-
-    hlsInstance.value.on(Hls.Events.ERROR, (event, data) => {
-      console.error("HLS 错误:", data);
-
-      if (data.fatal) {
-        switch (data.type) {
-          case Hls.ErrorTypes.NETWORK_ERROR:
-            console.log("网络错误,尝试恢复");
-            if (data.details === "manifestParsingError") {
-              error.value = "视频清单解析失败";
-              hlsInstance.value?.destroy();
-              hlsInstance.value = null;
-            } else {
-              // 尝试重新加载
-              hlsInstance.value?.startLoad();
-            }
-            break;
-          case Hls.ErrorTypes.MEDIA_ERROR:
-            console.log("媒体错误,尝试恢复");
-            hlsInstance.value?.recoverMediaError();
-            break;
-          default:
-            console.log("其他致命错误,销毁 HLS");
-            error.value = "视频加载失败";
-            hlsInstance.value?.destroy();
-            hlsInstance.value = null;
-
-            // 尝试降级到原生播放器
-            setTimeout(() => {
-              if (videoElement.value && processedVideoUrl.value) {
-                console.log("降级到原生播放器");
-                error.value = "";
-                videoElement.value.src = processedVideoUrl.value;
-                videoElement.value.load();
-              }
-            }, 500);
-            break;
-        }
-      }
-    });
-  } catch (err) {
-    console.error("初始化 HLS.js 失败:", err);
-    error.value = "播放器初始化失败";
-    // 降级到原生播放器
-    if (videoElement.value && processedVideoUrl.value) {
-      videoElement.value.src = processedVideoUrl.value;
-      videoElement.value.load();
-    }
-  }
-};
-
-// 销毁 HLS 实例
-const destroyHls = (): void => {
-  if (hlsInstance.value) {
-    hlsInstance.value.destroy();
-    hlsInstance.value = null;
-  }
-};
-
-// 重试功能
-const retry = (): void => {
-  if (retryCount.value >= maxRetries) return;
-
-  retryCount.value++;
-  error.value = "";
-  emit("retry");
-
-  // 清理现有资源
-  destroyHls();
-  if (processedVideoUrl.value && processedVideoUrl.value.startsWith("blob:")) {
-    URL.revokeObjectURL(processedVideoUrl.value);
-  }
-  if (processedVideoUrl.value && processedVideoUrl.value.startsWith("data:")) {
-    processedVideoUrl.value = "";
-  }
-
-  // 重新处理
-  if (props.coverUrl) {
-    processCover(props.coverUrl);
-  }
-  if (props.m3u8Url) {
-    processVideo(props.m3u8Url);
-  }
-};
-
-// 播放器控制方法
-const togglePlay = (): void => {
-  if (!videoElement.value) return;
-
-  if (isPlaying.value) {
-    videoElement.value.pause();
-  } else {
-    videoElement.value.play();
-  }
-};
-
-const toggleMute = (): void => {
-  if (!videoElement.value) return;
-
-  isMuted.value = !isMuted.value;
-  videoElement.value.muted = isMuted.value;
-};
-
-const changeVolume = (event: Event): void => {
-  if (!videoElement.value) return;
-
-  const target = event.target as HTMLInputElement;
-  const newVolume = parseInt(target.value) / 100;
-  volume.value = newVolume;
-  videoElement.value.volume = newVolume;
-
-  if (newVolume > 0 && isMuted.value) {
-    isMuted.value = false;
-    videoElement.value.muted = false;
-  }
-};
-
-const toggleFullscreen = async (): Promise<void> => {
-  if (!videoElement.value) return;
-
-  try {
-    if (!document.fullscreenElement) {
-      // 进入全屏
-      const container = videoElement.value.parentElement;
-      if (container) {
-        // 尝试不同的全屏API
-        if (container.requestFullscreen) {
-          await container.requestFullscreen();
-        } else if ((container as any).webkitRequestFullscreen) {
-          await (container as any).webkitRequestFullscreen();
-        } else if ((container as any).mozRequestFullScreen) {
-          await (container as any).mozRequestFullScreen();
-        } else if ((container as any).msRequestFullscreen) {
-          await (container as any).msRequestFullscreen();
-        }
-        // iOS特殊处理
-        else if ((videoElement.value as any).webkitEnterFullscreen) {
-          (videoElement.value as any).webkitEnterFullscreen();
-        }
-      }
-    } else {
-      // 退出全屏
-      if (document.exitFullscreen) {
-        await document.exitFullscreen();
-      } else if ((document as any).webkitExitFullscreen) {
-        await (document as any).webkitExitFullscreen();
-      } else if ((document as any).mozCancelFullScreen) {
-        await (document as any).mozCancelFullScreen();
-      } else if ((document as any).msExitFullscreen) {
-        await (document as any).msExitFullscreen();
-      }
-    }
-  } catch (err) {
-    console.error("全屏切换失败:", err);
-  }
-};
-
-const seekVideo = (event: MouseEvent): void => {
-  if (!videoElement.value || !duration.value) return;
-
-  const progressBar = event.currentTarget as HTMLElement;
-  const rect = progressBar.getBoundingClientRect();
-  const pos = (event.clientX - rect.left) / rect.width;
-  const seekTime = pos * duration.value;
-
-  videoElement.value.currentTime = seekTime;
-};
-
-const formatTime = (seconds: number): string => {
-  if (isNaN(seconds)) return "00:00";
-
-  const mins = Math.floor(seconds / 60);
-  const secs = Math.floor(seconds % 60);
-  return `${mins.toString().padStart(2, "0")}:${secs
-    .toString()
-    .padStart(2, "0")}`;
-};
-
-const toggleControls = (): void => {
-  showControls.value = true;
-
-  if (controlsTimer) {
-    clearTimeout(controlsTimer);
-  }
-
-  // 如果正在播放,3秒后自动隐藏控制条
-  if (isPlaying.value) {
-    controlsTimer = setTimeout(() => {
-      showControls.value = false;
-    }, 3000);
-  }
-};
-
-// 事件处理
-const handleCoverError = (event: Event): void => {
-  const img = event.target as HTMLImageElement;
-  if (img.src.startsWith("blob:")) {
-    processedCoverUrl.value = props.coverUrl;
-  }
-};
-
-const handleCoverLoad = (): void => {};
-
-const onVideoLoadStart = (): void => {
-  console.log("视频开始加载");
-  isVideoLoading.value = true;
-  // 清除之前的错误
-  error.value = "";
-};
-
-const onVideoLoadedData = (): void => {
-  console.log("视频数据已加载");
-};
-
-const onVideoCanPlay = (): void => {
-  console.log("视频可以播放");
-
-  // 清除加载状态和错误
-  isVideoLoading.value = false;
-  error.value = "";
-
-  // 清除错误防抖定时器
-  if (errorDebounceTimer) {
-    clearTimeout(errorDebounceTimer);
-    errorDebounceTimer = null;
-  }
-
-  // 发出canplay事件
-  emit("canplay");
-
-  if (props.autoPlay && videoElement.value) {
-    videoElement.value.play().catch(() => {});
-  }
-};
-
-const onVideoPlayEvent = (): void => {
-  isPlaying.value = true;
-  emit("play");
-  toggleControls(); // 开始自动隐藏倒计时
-};
-
-const onVideoPauseEvent = (): void => {
-  isPlaying.value = false;
-  showControls.value = true;
-  if (controlsTimer) {
-    clearTimeout(controlsTimer);
-  }
-};
-
-const onDurationChange = (): void => {
-  if (videoElement.value) {
-    duration.value = videoElement.value.duration;
-  }
-};
-
-const onVideoEnded = (): void => {
-  isPlaying.value = false;
-  showControls.value = true;
-};
-
-const onVideoTimeUpdate = (): void => {
-  if (videoElement.value) {
-    currentTime.value = videoElement.value.currentTime;
-
-    // 更新进度
-    if (duration.value > 0) {
-      progress.value = (currentTime.value / duration.value) * 100;
-    }
-
-    // 更新缓冲进度
-    if (videoElement.value.buffered.length > 0) {
-      const bufferedEnd = videoElement.value.buffered.end(
-        videoElement.value.buffered.length - 1
-      );
-      buffered.value = (bufferedEnd / duration.value) * 100;
-    }
-  }
-
-  emit("timeupdate");
-};
-
-const onVideoSeeking = (): void => {
-  emit("seeking");
-};
-
-const onVideoError = (event: Event): void => {
-  const video = event.target as HTMLVideoElement;
-  const errorCode = video.error?.code;
-  const errorMessage = video.error?.message;
-
-  console.log(
-    "视频错误事件:",
-    errorCode,
-    errorMessage,
-    "加载中:",
-    isVideoLoading.value
-  );
-
-  // 清除之前的错误防抖定时器
-  if (errorDebounceTimer) {
-    clearTimeout(errorDebounceTimer);
-    errorDebounceTimer = null;
-  }
-
-  // 如果视频正在加载中,延迟显示错误(给视频更多时间初始化)
-  if (isVideoLoading.value) {
-    console.log("视频正在初始化,延迟错误显示");
-
-    // 对于夸克浏览器,给更长的初始化时间(从2s提升至3s)
-    const delayTime = isQuarkBrowser() ? 3000 : 1000;
-
-    errorDebounceTimer = setTimeout(() => {
-      // 再次检查视频状态
-      if (video.readyState >= 2) {
-        // 视频已经准备好,忽略错误
-        console.log("视频已准备好,忽略错误");
-        return;
-      }
-
-      // 显示错误
-      showVideoError(errorCode, errorMessage);
-    }, delayTime);
-
-    return;
-  }
-
-  // 如果不是加载中,立即显示错误
-  showVideoError(errorCode, errorMessage);
-};
-
-// 显示视频错误的辅助函数
-const showVideoError = (errorCode?: number, errorMessage?: string): void => {
-  let errorText = "视频播放失败";
-
-  if (errorCode) {
-    switch (errorCode) {
-      case 1: // MEDIA_ERR_ABORTED
-        errorText = "视频播放被中止";
-        break;
-      case 2: // MEDIA_ERR_NETWORK
-        errorText = "网络错误,无法加载视频";
-        break;
-      case 3: // MEDIA_ERR_DECODE
-        errorText = isIOSQuarkBrowser()
-          ? "抱歉,iOS 夸克浏览器暂不支持此视频,建议使用 Safari 打开"
-          : "视频解码失败";
-        break;
-      case 4: // MEDIA_ERR_SRC_NOT_SUPPORTED
-        errorText = isIOSQuarkBrowser()
-          ? "抱歉,iOS 夸克浏览器暂不支持此视频,建议使用 Safari 打开"
-          : "视频格式不支持";
-        break;
-      default:
-        errorText = `视频播放失败 (错误代码: ${errorCode})`;
-    }
-  }
-
-  console.error("显示视频错误:", errorCode, errorMessage);
-  error.value = errorText;
-  emit("error", error.value);
-
-  // Safari 原生播放失败时尝试 HLS.js(排除夸克和iOS设备)
-  if (
-    isSafari() &&
-    !isQuarkBrowser() &&
-    !isIOSSafari() &&
-    !hlsInstance.value &&
-    Hls.isSupported()
-  ) {
-    setTimeout(() => {
-      error.value = "";
-      initHlsPlayer();
-    }, 1000);
-  }
-};
-
-// 监听 props 变化
-watch(
-  () => props.coverUrl,
-  (newUrl, oldUrl) => {
-    // 只有当URL真正变化时才处理
-    if (newUrl && newUrl !== oldUrl) {
-      console.log("封面URL变化,重新处理封面", { newUrl, oldUrl });
-      // 清理旧的封面URL(如果是blob)
-      if (processedCoverUrl.value?.startsWith("blob:")) {
-        URL.revokeObjectURL(processedCoverUrl.value);
-        processedCoverUrl.value = "";
-      }
-      // 处理新封面
-      processCover(newUrl);
-    } else if (newUrl) {
-      // 初始加载
-      processCover(newUrl);
-    }
-  },
-  { immediate: true }
-);
-
-watch(
-  () => props.m3u8Url,
-  (newUrl, oldUrl) => {
-    // 如果URL发生变化,先停止当前播放
-    if (oldUrl && newUrl !== oldUrl) {
-      console.log("视频URL变化,停止当前播放并加载新视频", { newUrl, oldUrl });
-      // 停止旧视频
-      stopVideo();
-      // 重置状态
-      isPlaying.value = false;
-      currentTime.value = 0;
-      duration.value = 0;
-      progress.value = 0;
-      buffered.value = 0;
-      // 处理新视频
-      if (newUrl) {
-        processVideo(newUrl);
-      }
-    } else if (newUrl) {
-      // 初始加载
-      processVideo(newUrl);
-    }
-  },
-  { immediate: true }
-);
-
-// 全屏状态监听
-const handleFullscreenChange = (): void => {
-  isFullscreen.value = !!(
-    document.fullscreenElement ||
-    (document as any).webkitFullscreenElement ||
-    (document as any).mozFullScreenElement ||
-    (document as any).msFullscreenElement
-  );
-};
-
-// 组件挂载时添加全屏监听
-onMounted(() => {
-  document.addEventListener("fullscreenchange", handleFullscreenChange);
-  document.addEventListener("webkitfullscreenchange", handleFullscreenChange);
-  document.addEventListener("mozfullscreenchange", handleFullscreenChange);
-  document.addEventListener("MSFullscreenChange", handleFullscreenChange);
-});
-
-// 组件卸载时清理资源
-onUnmounted(() => {
-  destroyHls();
-
-  // 清理全屏监听
-  document.removeEventListener("fullscreenchange", handleFullscreenChange);
-  document.removeEventListener(
-    "webkitfullscreenchange",
-    handleFullscreenChange
-  );
-  document.removeEventListener("mozfullscreenchange", handleFullscreenChange);
-  document.removeEventListener("MSFullscreenChange", handleFullscreenChange);
-
-  // 清理控制条定时器
-  if (controlsTimer) {
-    clearTimeout(controlsTimer);
-  }
-
-  // 清理错误防抖定时器
-  if (errorDebounceTimer) {
-    clearTimeout(errorDebounceTimer);
-  }
-
-  // 清理 Blob URL
-  if (processedCoverUrl.value?.startsWith("blob:")) {
-    URL.revokeObjectURL(processedCoverUrl.value);
-  }
-  if (processedVideoUrl.value?.startsWith("blob:")) {
-    URL.revokeObjectURL(processedVideoUrl.value);
-  }
-});
-
-// 停止HLS加载
-const stopHlsLoading = (): void => {
-  if (hlsInstance.value) {
-    hlsInstance.value.stopLoad();
-  }
-};
-
-// 停止视频播放
-const stopVideo = (): void => {
-  if (videoElement.value) {
-    videoElement.value.pause();
-    videoElement.value.currentTime = 0;
-
-    // 清除视频源
-    videoElement.value.removeAttribute("src");
-    videoElement.value.load();
-  }
-
-  // 停止HLS加载
-  stopHlsLoading();
-
-  // 清理HLS实例
-  destroyHls();
-
-  // 清理Blob URL
-  if (processedVideoUrl.value?.startsWith("blob:")) {
-    URL.revokeObjectURL(processedVideoUrl.value);
-    processedVideoUrl.value = "";
-  }
-};
-
-// 暴露方法给父组件
-defineExpose({
-  retry,
-  stopHlsLoading,
-  stopVideo,
-  processedCoverUrl: computed(() => processedCoverUrl.value),
-  processedVideoUrl: computed(() => processedVideoUrl.value),
-  loading: computed(() => loading.value),
-  error: computed(() => error.value),
-});
-</script>
-
-<style scoped>
-.video-processor {
-  position: relative;
-  width: 100%;
-  height: 100%;
-}
-
-/* 自定义控制条 */
-.custom-controls {
-  position: absolute;
-  top: 0;
-  left: 0;
-  right: 0;
-  bottom: 0;
-  pointer-events: none;
-  opacity: 0;
-  transition: opacity 0.3s ease;
-  z-index: 5;
-}
-
-.custom-controls.show {
-  opacity: 1;
-}
-
-.custom-controls > * {
-  pointer-events: auto;
-}
-
-/* 中央播放按钮 */
-.play-button-center {
-  position: absolute;
-  top: 50%;
-  left: 50%;
-  transform: translate(-50%, -50%);
-  width: 80px;
-  height: 80px;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  background: rgba(0, 0, 0, 0.6);
-  border-radius: 50%;
-  cursor: pointer;
-  transition: all 0.3s ease;
-  backdrop-filter: blur(10px);
-}
-
-.play-button-center:hover {
-  background: rgba(0, 0, 0, 0.8);
-  transform: translate(-50%, -50%) scale(1.1);
-}
-
-/* 底部控制栏 */
-.controls-bottom {
-  position: absolute;
-  left: 0;
-  right: 0;
-  bottom: 0;
-  background: linear-gradient(
-    to top,
-    rgba(0, 0, 0, 0.8) 0%,
-    rgba(0, 0, 0, 0.4) 70%,
-    transparent 100%
-  );
-  padding: 10px 15px;
-  display: flex;
-  flex-direction: column;
-  gap: 8px;
-}
-
-/* 进度条容器 */
-.progress-container {
-  width: 100%;
-  padding: 8px 0;
-  cursor: pointer;
-}
-
-.progress-bar {
-  position: relative;
-  width: 100%;
-  height: 4px;
-  background: rgba(255, 255, 255, 0.3);
-  border-radius: 2px;
-  overflow: hidden;
-}
-
-.progress-buffered {
-  position: absolute;
-  top: 0;
-  left: 0;
-  height: 100%;
-  background: rgba(255, 255, 255, 0.5);
-  border-radius: 2px;
-  transition: width 0.2s ease;
-}
-
-.progress-played {
-  position: absolute;
-  top: 0;
-  left: 0;
-  height: 100%;
-  background: #3b82f6;
-  border-radius: 2px;
-  transition: width 0.1s ease;
-}
-
-/* 控制按钮区域 */
-.controls-buttons {
-  display: flex;
-  justify-content: space-between;
-  align-items: center;
-}
-
-.controls-left,
-.controls-right {
-  display: flex;
-  align-items: center;
-  gap: 12px;
-}
-
-.control-btn {
-  background: none;
-  border: none;
-  padding: 8px;
-  cursor: pointer;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  transition: all 0.2s ease;
-  border-radius: 4px;
-}
-
-.control-btn:hover {
-  background: rgba(255, 255, 255, 0.2);
-}
-
-.control-btn:active {
-  transform: scale(0.95);
-}
-
-/* 时间显示 */
-.time-display {
-  color: white;
-  font-size: 14px;
-  font-weight: 500;
-  white-space: nowrap;
-  user-select: none;
-}
-
-/* 音量控制 */
-.volume-control {
-  display: flex;
-  align-items: center;
-  gap: 8px;
-}
-
-.volume-slider {
-  width: 60px;
-  height: 4px;
-  appearance: none;
-  background: rgba(255, 255, 255, 0.3);
-  border-radius: 2px;
-  outline: none;
-  cursor: pointer;
-}
-
-.volume-slider::-webkit-slider-thumb {
-  appearance: none;
-  width: 12px;
-  height: 12px;
-  background: white;
-  border-radius: 50%;
-  cursor: pointer;
-}
-
-.volume-slider::-moz-range-thumb {
-  width: 12px;
-  height: 12px;
-  background: white;
-  border-radius: 50%;
-  border: none;
-  cursor: pointer;
-}
-
-/* 移动端优化 */
-@media screen and (max-width: 768px) {
-  .controls-bottom {
-    padding: 8px 12px;
-  }
-
-  .play-button-center {
-    width: 60px;
-    height: 60px;
-  }
-
-  .time-display {
-    font-size: 12px;
-  }
-
-  .control-btn {
-    padding: 6px;
-  }
-
-  .volume-slider {
-    display: none; /* 移动端隐藏音量滑块 */
-  }
-}
-
-/* iOS全屏样式优化 */
-@media screen and (max-width: 768px) {
-  .video-processor:fullscreen .custom-controls,
-  .video-processor:-webkit-full-screen .custom-controls,
-  .video-processor:-moz-full-screen .custom-controls {
-    background: rgba(0, 0, 0, 0.3);
-  }
-
-  .video-processor:fullscreen .controls-bottom,
-  .video-processor:-webkit-full-screen .controls-bottom,
-  .video-processor:-moz-full-screen .controls-bottom {
-    background: linear-gradient(
-      to top,
-      rgba(0, 0, 0, 0.9) 0%,
-      rgba(0, 0, 0, 0.6) 70%,
-      transparent 100%
-    );
-  }
-}
-
-.loading-overlay {
-  position: absolute;
-  top: 0;
-  left: 0;
-  right: 0;
-  bottom: 0;
-  display: flex;
-  flex-direction: column;
-  align-items: center;
-  justify-content: center;
-  background: rgba(0, 0, 0, 0.7);
-  color: white;
-  z-index: 10;
-}
-
-.spinner {
-  width: 24px;
-  height: 24px;
-  border: 3px solid rgba(255, 255, 255, 0.3);
-  border-top: 3px solid white;
-  border-radius: 50%;
-  animation: spin 1s linear infinite;
-  margin-bottom: 8px;
-}
-
-@keyframes spin {
-  0% {
-    transform: rotate(0deg);
-  }
-  100% {
-    transform: rotate(360deg);
-  }
-}
-
-.loading-text {
-  font-size: 14px;
-  color: white;
-}
-
-.error-overlay {
-  position: absolute;
-  top: 0;
-  left: 0;
-  right: 0;
-  bottom: 0;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  background: rgba(0, 0, 0, 0.8);
-  z-index: 10;
-}
-
-.error-content {
-  text-align: center;
-  color: white;
-  padding: 20px;
-}
-
-.error-icon {
-  font-size: 32px;
-  margin-bottom: 12px;
-}
-
-.error-text {
-  font-size: 14px;
-  margin-bottom: 16px;
-  color: #ff6b6b;
-}
-
-.retry-btn {
-  padding: 8px 16px;
-  background: #10b981;
-  color: white;
-  border: none;
-  border-radius: 6px;
-  font-size: 14px;
-  cursor: pointer;
-  transition: background-color 0.2s;
-}
-
-.retry-btn:hover {
-  background: #059669;
-}
-</style>

+ 3 - 3
src/views/Home.vue

@@ -51,7 +51,7 @@
       >
         <!-- 横屏封面 -->
         <div class="aspect-[16/9] relative">
-          <VideoProcessor
+          <VideoJSPlayer
             :cover-url="video.cover"
             :alt="video.name"
             cover-class="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300"
@@ -368,7 +368,7 @@ import { ref, onMounted, onBeforeUnmount, watch, nextTick } from "vue";
 import { useRouter, useRoute } from "vue-router";
 import { searchVideoByTags, searchVideoByKeyword } from "@/services/api";
 import { videoMenus as fixedVideoMenus } from "@/data/videoMenus";
-import VideoProcessor from "@/components/VideoProcessor.vue";
+import VideoJSPlayer from "@/components/VideoJSPlayer.vue";
 import { useUserStore } from "@/store/user";
 import { VipLevel } from "@/types/vip";
 
@@ -442,7 +442,7 @@ const formatNumber = (num: string | number): string => {
   return n.toString();
 };
 
-// 图片加载错误处理已移至 VideoProcessor 组件
+// 图片加载错误处理已移至 VideoJSPlayer 组件
 
 // 跳转到指定页面
 const jumpToPageHandler = () => {

+ 1 - 1
src/views/Purchased.vue

@@ -44,7 +44,7 @@ const generateMacAddress = (): string => {
 
 const device = generateMacAddress();
 
-// 解密封面图片的函数(从VideoProcessor组件复制)
+// 解密封面图片的函数(从VideoJSPlayer组件复制)
 const decryptCover = async (url: string): Promise<string> => {
   if (!url.includes("cover")) {
     return url;

+ 39 - 206
src/views/VideoPlayer.vue

@@ -29,14 +29,6 @@
         v-if="!isSinglePurchased && !isVip"
         class="flex items-center gap-1 sm:gap-1.5"
       >
-        <!-- 游客和免费用户显示试看时间信息 -->
-        <div
-          v-if="isGuestOrFree"
-          class="text-2xs sm:text-xs text-white/60 px-1 sm:px-2 py-0.5 sm:py-1"
-        >
-          试看{{ trialDuration }}秒
-        </div>
-
         <!-- 只有非游客用户才显示购买会员按钮 -->
         <button
           v-if="!isGuest"
@@ -57,7 +49,9 @@
     <!-- 视频播放器区域 -->
     <div class="relative rounded-2xl overflow-hidden bg-black">
       <div class="aspect-video video-container">
-        <VideoProcessor
+        <!-- 只有付费用户才能看到播放器 -->
+        <VideoJSPlayer
+          v-if="isSinglePurchased || isVip"
           ref="videoProcessorRef"
           :cover-url="videoInfo.cover"
           :m3u8-url="videoInfo.m3u8"
@@ -73,61 +67,32 @@
           @retry="onVideoProcessorRetry"
           @play="onVideoPlay"
           @timeupdate="onVideoTimeUpdate"
-          @seeking="onVideoSeeking"
           @canplay="onVideoCanPlay"
         />
 
-        <!-- iOS兼容的中央播放按钮(当有封面但没有开始播放时显示) -->
-        <div
-          v-if="videoInfo.cover && !isVideoMode"
-          class="absolute inset-0 flex items-center justify-center z-10 cursor-pointer"
-          @click="startPlayVideo"
-        >
-          <div
-            class="w-20 h-20 bg-black/60 rounded-full flex items-center justify-center"
-          >
-            <svg class="w-12 h-12" fill="white" viewBox="0 0 24 24">
-              <path d="M8 5v14l11-7z" />
-            </svg>
-          </div>
-        </div>
-
-        <!-- 快进加载动画 -->
-        <div
-          v-if="isVideoSeeking"
-          class="absolute inset-0 bg-black/40 flex items-center justify-center z-20"
-        >
-          <div
-            class="w-16 h-16 rounded-full bg-black/60 flex items-center justify-center"
-          >
-            <div
-              class="w-10 h-10 border-4 border-white/20 border-t-brand rounded-full animate-spin"
-            ></div>
-          </div>
-        </div>
-
-        <!-- 试看提示 -->
-        <div
-          v-if="isTrialMode"
-          class="absolute top-4 right-4 text-white px-3 py-1.5 text-sm font-medium"
-        >
-          试看中
-        </div>
+        <!-- 非付费用户只显示封面,使用VideoJSPlayer组件确保封面解密 -->
+        <VideoJSPlayer
+          v-else
+          :cover-url="videoInfo.cover"
+          :alt="videoInfo.name"
+          cover-class="w-full h-full object-cover"
+          @cover-loaded="onCoverLoaded"
+        />
 
-        <!-- 试看结束遮罩 -->
+        <!-- 非付费用户提示 -->
         <div
-          v-if="showTrialEndModal && !isSinglePurchased && !isVip"
+          v-if="!isSinglePurchased && !isVip"
           class="absolute inset-0 bg-black/80 flex items-center justify-center z-50"
         >
           <div
             class="bg-white/10 backdrop-blur-sm rounded-2xl p-6 max-w-sm mx-4 text-center"
           >
             <div class="text-white text-lg font-semibold mb-4">
-              试看时间已结束
+              需要购买才能观看
             </div>
             <div class="text-white/70 text-sm mb-6">
-              <span v-if="!isGuest">购买会员或单独购买本片继续观看</span>
-              <span v-else>单独购买本片继续观看</span>
+              <span v-if="!isGuest">购买会员或单独购买本片观看</span>
+              <span v-else>单独购买本片观看</span>
             </div>
             <div class="space-y-3">
               <!-- 只有非游客用户才显示购买会员按钮 -->
@@ -144,12 +109,6 @@
               >
                 单独购买本片
               </button>
-              <button
-                @click="showTrialEndModal = false"
-                class="w-full text-white/60 hover:text-white/80 py-2 text-sm transition"
-              >
-                取消
-              </button>
             </div>
           </div>
         </div>
@@ -557,7 +516,7 @@
           >
             <!-- 横屏封面 -->
             <div class="aspect-[16/9] relative">
-              <VideoProcessor
+              <VideoJSPlayer
                 :cover-url="video.cover"
                 :alt="video.name"
                 cover-class="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300"
@@ -620,7 +579,7 @@ import {
   userQueryOrder,
   checkSinglePurchase,
 } from "@/services/api";
-import VideoProcessor from "@/components/VideoProcessor.vue";
+import VideoJSPlayer from "@/components/VideoJSPlayer.vue";
 import LoginDialog from "@/components/LoginDialog.vue";
 import { useUserStore } from "@/store/user";
 import { usePriceStore } from "@/store/price";
@@ -634,7 +593,7 @@ const router = useRouter();
 const userStore = useUserStore();
 const priceStore = usePriceStore();
 
-// 视频播放器引用(现在由 VideoProcessor 组件管理)
+// 视频播放器引用(现在由 VideoJSPlayer 组件管理)
 const videoPlayer = ref<HTMLVideoElement>();
 const videoProcessorRef = ref<any>();
 
@@ -655,12 +614,9 @@ const videoInfo = ref<any>({
 const relatedVideos = ref<any[]>([]);
 
 // 状态管理
-const showTrialEndModal = ref(false);
 const showMembershipPurchaseModal = ref(false);
 const showSinglePurchaseModal = ref(false);
 const showLoginDialog = ref(false);
-const isTrialMode = ref(false);
-const isVideoSeeking = ref(false); // 视频快进状态
 
 // 单片购买状态
 const isSinglePurchased = ref(false);
@@ -686,13 +642,12 @@ const errorMessage = ref("");
 const showSinglePaymentWaitingDialog = ref(false);
 const singleCurrentOrderNo = ref("");
 
-// 检测是否为夸克浏览器
+// 浏览器检测函数(保留用于UI特殊处理)
 const isQuarkBrowser = () => {
   const ua = navigator.userAgent.toLowerCase();
   return ua.includes("quark");
 };
 
-// 检测是否为iOS夸克浏览器
 const isIOSQuarkBrowser = () => {
   const ua = navigator.userAgent.toLowerCase();
   return isQuarkBrowser() && /iPad|iPhone|iPod/.test(navigator.userAgent);
@@ -769,25 +724,14 @@ const generateMacAddress = (): string => {
 
 const device = generateMacAddress();
 
-// Safari兼容性处理:打开支付页面
+// 支付页面打开处理(简化,Video.js 已处理浏览器兼容性)
 const openPaymentPage = (url: string) => {
-  // 检测是否为Safari浏览器
-  const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
-
-  if (isSafari) {
-    // Safari浏览器
-    setTimeout(() => window.open(url, "_blank"));
-    // window.location.href = url;
-  } else {
-    // 其他浏览器:正常使用window.open
-    window.open(url, "_blank");
-  }
+  // 使用统一的支付页面打开方式
+  setTimeout(() => window.open(url, "_blank"));
 };
 
 // 计算属性
 const currentVipLevel = computed(() => userStore.getVipLevel());
-const isGuestOrFree = computed(() => !canWatchFullVideo(currentVipLevel.value));
-const trialDuration = computed(() => getTrialDuration(priceStore));
 
 // 参考Account.vue的用户类型判断
 const isGuest = computed(() => {
@@ -798,7 +742,7 @@ const isVip = computed(() => {
   return level && level !== VipLevel.GUEST && level !== VipLevel.FREE;
 });
 
-// VideoProcessor 事件处理
+// VideoJSPlayer 事件处理
 const onCoverLoaded = (url: string) => {
   // 封面加载完成
 };
@@ -808,17 +752,16 @@ const onVideoLoaded = (url: string) => {
 };
 
 const onVideoProcessorError = (error: string) => {
-  console.error("VideoProcessor 错误:", error);
+  console.error("VideoJSPlayer 错误:", error);
 };
 
 const onVideoProcessorRetry = () => {
-  // VideoProcessor 重试
+  // VideoJSPlayer 重试
 };
 
 // 处理视频可以播放事件
 const onVideoCanPlay = () => {
-  // 视频可以播放时,关闭加载动画
-  isVideoSeeking.value = false;
+  // 视频可以播放时的处理
 };
 
 // 处理登录成功
@@ -835,49 +778,11 @@ const onLoginSuccess = async () => {
 const onVideoPlay = () => {
   // 标记为视频模式
   isVideoMode.value = true;
-
-  // 如果是guest或free用户且还没有开始试看,且未购买当前视频,则开始试看
-  if (isGuestOrFree.value && !isTrialMode.value && !isSinglePurchased.value) {
-    startTrial();
-  }
 };
 
 // 监听视频播放进度
 const onVideoTimeUpdate = () => {
-  if (isGuestOrFree.value && isTrialMode.value) {
-    const video = document.querySelector("video");
-    if (video && video.currentTime > trialDuration.value) {
-      // 超过试看时间,停止播放并显示购买弹窗
-      video.pause();
-      // 将视频时间重置到试看结束时间
-      video.currentTime = trialDuration.value;
-      // 停止HLS加载
-      if (videoProcessorRef.value) {
-        videoProcessorRef.value.stopHlsLoading();
-      }
-      showTrialEndModal.value = true;
-    }
-  }
-};
-
-// 监听视频时间变化(包括拖拽进度条)
-const onVideoSeeking = () => {
-  // 设置快进状态
-  isVideoSeeking.value = true;
-
-  // 创建一个定时器,在视频开始播放后一段时间关闭加载动画
-  setTimeout(() => {
-    isVideoSeeking.value = false;
-  }, 1500);
-
-  // 试看限制逻辑
-  if (isGuestOrFree.value && isTrialMode.value) {
-    const video = document.querySelector("video");
-    if (video && video.currentTime > trialDuration.value) {
-      // 如果拖拽到超过试看时间,强制回到试看结束时间
-      video.currentTime = trialDuration.value;
-    }
-  }
+  // 移除试看逻辑,付费用户正常播放
 };
 
 // 格式化时长
@@ -921,22 +826,6 @@ const goBack = () => {
 // 视频模式状态
 const isVideoMode = ref(false);
 
-// 开始播放当前视频
-const startPlayVideo = () => {
-  // 标记为视频模式
-  isVideoMode.value = true;
-
-  // 如果视频元素存在,尝试播放
-  if (videoProcessorRef.value) {
-    const videoEl = videoProcessorRef.value.$el.querySelector("video");
-    if (videoEl) {
-      videoEl.play().catch((err: Error) => {
-        console.error("自动播放失败:", err);
-      });
-    }
-  }
-};
-
 // 播放推荐视频
 const playVideo = (video: any) => {
   const vipLevel = userStore.getVipLevel();
@@ -948,7 +837,6 @@ const playVideo = (video: any) => {
 
   // 强制重置视频模式状态
   isVideoMode.value = false;
-  isTrialMode.value = false;
 
   // 确保使用replace而不是push,避免浏览器历史堆积
   if (vipLevel === VipLevel.GUEST || vipLevel === VipLevel.FREE) {
@@ -984,12 +872,6 @@ const playVideo = (video: any) => {
   });
 };
 
-// 开始试看
-const startTrial = () => {
-  isTrialMode.value = true;
-  isVideoMode.value = true;
-};
-
 // 处理顶部会员购买按钮点击
 const handleMembershipClick = () => {
   // 检查用户是否已登录
@@ -1067,14 +949,10 @@ const purchaseMembership = () => {
   // 检查用户是否已登录
   if (!userStore.token) {
     // 未登录,显示登录弹窗
-    showTrialEndModal.value = false;
     showLoginDialog.value = true;
     return;
   }
 
-  // 关闭试看结束弹窗
-  showTrialEndModal.value = false;
-
   // iOS夸克浏览器特殊处理:滚动到播放器下方
   if (isIOSQuarkBrowser()) {
     setTimeout(() => {
@@ -1123,9 +1001,8 @@ const handleMembershipPurchase = async () => {
       // 关闭购买弹窗
       showMembershipPurchaseModal.value = false;
 
-      // Safari兼容性处理:使用多种方式打开支付页面
-      // openPaymentPage(response.code_url);
-      setTimeout(() => window.open(response.code_url, "_blank"));
+      // 打开支付页面
+      openPaymentPage(response.code_url);
 
       // 显示支付等待弹窗
       showPaymentWaitingDialog.value = true;
@@ -1204,7 +1081,6 @@ const purchaseVideo = async () => {
   // 检查用户是否已登录
   if (!userStore.token) {
     // 未登录,显示登录弹窗
-    showTrialEndModal.value = false;
     showSinglePurchaseModal.value = false;
     showLoginDialog.value = true;
     return;
@@ -1250,12 +1126,10 @@ const purchaseVideo = async () => {
       singleCurrentOrderNo.value = response.out_trade_no;
 
       // 关闭弹窗
-      showTrialEndModal.value = false;
       showSinglePurchaseModal.value = false;
 
-      // Safari兼容性处理:使用多种方式打开支付页面
-      // openPaymentPage(response.code_url);
-      setTimeout(() => window.open(response.code_url, "_blank"));
+      // 打开支付页面
+      openPaymentPage(response.code_url);
 
       // 显示支付等待弹窗
       showSinglePaymentWaitingDialog.value = true;
@@ -1293,16 +1167,16 @@ const loadVideoInfo = async () => {
   // 获取视频ID(无论什么用户类型都使用相同的获取方式)
   const videoId = route.params.id || route.query.id;
 
-  // 如果是guest或free用户,从query参数获取视频信息
+  // 如果是guest或free用户,从query参数获取视频信息(只显示封面和名称)
   if (vipLevel === VipLevel.GUEST || vipLevel === VipLevel.FREE) {
-    const { cover, m3u8, name, duration, view, like } = route.query;
+    const { cover, name, duration, view, like } = route.query;
 
-    if (cover && m3u8) {
+    if (cover) {
       videoInfo.value = {
         id: videoId || "unknown",
-        name: name || "试看视频",
+        name: name || "视频",
         cover: cover as string,
-        m3u8: m3u8 as string,
+        m3u8: "", // 不提供 m3u8 地址
         duration: parseInt(duration as string) || 0,
         view: parseInt(view as string) || 0,
         like: parseInt(like as string) || 0,
@@ -1311,7 +1185,7 @@ const loadVideoInfo = async () => {
       };
 
       // 设置页面标题
-      document.title = `${videoInfo.value.name} - 试看`;
+      document.title = `${videoInfo.value.name} - 视频`;
 
       return;
     }
@@ -1441,7 +1315,7 @@ watch(
     const queryChanged = JSON.stringify(newQuery) !== JSON.stringify(oldQuery);
 
     if (idChanged || queryChanged) {
-      console.log("路由变化,重新加载视频", { idChanged, queryChanged });
+      // console.log("路由变化,重新加载视频", { idChanged, queryChanged });
 
       // 停止当前视频播放
       if (videoProcessorRef.value) {
@@ -1451,7 +1325,6 @@ watch(
       // 重置购买状态和视频模式
       isSinglePurchased.value = false;
       isVideoMode.value = false;
-      isTrialMode.value = false;
 
       // 重置视频信息和处理封面
       await loadVideoInfo();
@@ -1523,44 +1396,4 @@ onUnmounted(() => {
 .glow-animation {
   animation: glow 2s ease-in-out infinite;
 }
-
-/* 移动端全屏样式 */
-@media screen and (max-width: 768px) {
-  video {
-    /* 强制横屏全屏 */
-    object-fit: contain;
-  }
-
-  /* 全屏时的样式 */
-  video:fullscreen {
-    width: 100vw;
-    height: 100vh;
-    object-fit: contain;
-    background: black;
-  }
-
-  /* WebKit全屏样式 */
-  video:-webkit-full-screen {
-    width: 100vw;
-    height: 100vh;
-    object-fit: contain;
-    background: black;
-  }
-
-  /* Mozilla全屏样式 */
-  video:-moz-full-screen {
-    width: 100vw;
-    height: 100vh;
-    object-fit: contain;
-    background: black;
-  }
-
-  /* MS全屏样式 */
-  video:-ms-fullscreen {
-    width: 100vw;
-    height: 100vh;
-    object-fit: contain;
-    background: black;
-  }
-}
 </style>

文件差异内容过多而无法显示
+ 301 - 301
yarn.lock


部分文件因为文件数量过多而无法显示