Browse Source

重构视频播放器组件,优化视频处理逻辑,新增视频详情接口,移除冗余代码,提升用户体验

wuyi 3 months ago
parent
commit
310c1fb597
6 changed files with 560 additions and 1950 deletions
  1. 0 131
      VIDEO_PLAYER_README.md
  2. 0 1473
      api_example.md
  3. 505 0
      src/components/VideoProcessor.vue
  4. 9 31
      src/services/api.ts
  5. 7 12
      src/views/Home.vue
  6. 39 303
      src/views/VideoPlayer.vue

+ 0 - 131
VIDEO_PLAYER_README.md

@@ -1,131 +0,0 @@
-# 视频播放功能使用说明
-
-## 功能概述
-
-本项目已成功实现了完整的视频播放功能,包括:
-
-1. **视频列表展示** - 在首页显示视频缩略图和基本信息
-2. **视频播放页面** - 点击视频后进入专门的播放页面
-3. **视频播放器** - 支持HTML5原生视频播放控制
-4. **相关推荐** - 播放页面显示相关视频推荐
-5. **播放统计** - 记录视频播放行为(播放、暂停、结束等)
-
-## 主要功能
-
-### 1. 视频列表页面 (Home.vue)
-- 显示视频缩略图网格布局
-- 支持搜索功能
-- 支持标签分类筛选
-- 支持排序(最新、热门、点赞)
-- 支持分页浏览
-- 点击视频卡片跳转到播放页面
-
-### 2. 视频播放页面 (VideoPlayer.vue)
-- 全屏视频播放器
-- 视频信息展示(标题、时长、观看数、点赞数等)
-- 标签信息展示
-- 相关视频推荐
-- 收藏功能(UI已实现)
-- 分享功能(UI已实现)
-- 返回按钮
-
-### 3. 视频播放器功能
-- HTML5原生视频播放控制
-- 支持播放/暂停
-- 支持进度条拖拽
-- 支持音量控制
-- 支持全屏播放
-- 支持移动端播放(playsinline)
-- 自动重试机制
-- 加载状态提示
-- 错误处理
-
-### 4. 播放统计功能
-- 记录播放开始
-- 记录暂停
-- 记录播放结束
-- 记录播放进度(每30秒)
-- 模拟API调用(实际项目中需要后端支持)
-
-## 技术实现
-
-### 路由配置
-```typescript
-{
-  path: "/video/:id",
-  name: "VideoPlayer", 
-  component: VideoPlayer,
-  props: true,
-}
-```
-
-### API接口
-- `searchVideoByKeyword` - 关键字搜索视频
-- `searchVideoByTags` - 标签搜索视频
-- `getVideoMenu` - 获取视频分类菜单
-- `recordVideoPlay` - 记录播放统计(模拟)
-
-### 数据传递
-通过路由参数和查询参数传递视频信息:
-- `params.id` - 视频ID
-- `query.name` - 视频标题
-- `query.cover` - 封面图片URL
-- `query.m3u8` - 视频播放地址
-- `query.duration` - 视频时长
-- `query.view` - 观看数
-- `query.like` - 点赞数
-- `query.time` - 发布时间
-- `query.taginfo` - 标签信息(JSON字符串)
-
-## 使用方法
-
-1. **启动开发服务器**
-   ```bash
-   npm run dev
-   ```
-
-2. **浏览视频**
-   - 在首页浏览视频列表
-   - 使用搜索功能查找特定视频
-   - 使用标签筛选视频
-   - 使用排序功能调整显示顺序
-
-3. **播放视频**
-   - 点击任意视频卡片
-   - 自动跳转到播放页面
-   - 使用播放器控制播放
-   - 查看视频信息和相关推荐
-
-4. **播放控制**
-   - 点击播放/暂停按钮
-   - 拖拽进度条跳转
-   - 调整音量
-   - 全屏播放
-   - 返回上一页
-
-## 注意事项
-
-1. **视频源** - 确保视频M3U8地址有效且可访问
-2. **跨域问题** - 视频服务器需要支持CORS
-3. **移动端适配** - 已添加playsinline属性支持移动端播放
-4. **错误处理** - 包含加载失败重试机制
-5. **性能优化** - 相关视频推荐限制数量避免过度加载
-
-## 扩展功能
-
-可以进一步扩展的功能:
-- 视频评论系统
-- 用户收藏管理
-- 播放历史记录
-- 视频下载功能
-- 画质选择
-- 字幕支持
-- 播放速度调节
-- 键盘快捷键支持
-
-## 浏览器兼容性
-
-- Chrome/Edge: 完全支持
-- Firefox: 完全支持  
-- Safari: 完全支持
-- 移动端浏览器: 支持(iOS Safari, Android Chrome)

+ 0 - 1473
api_example.md

@@ -1,1473 +0,0 @@
-
-   视频关键字 查询接口:
-   curl --location --request POST 'https://qa3-api.69mediatest.com/api/media/30017/keyword' \
-   --form 'plat_id="30017"' \
-   --form 'channel_id="30017001"' \
-   --form 'user_id="30005470"' \
-   --form 'app_type="4"' \
-   --form 'token="0YzN4gDM4UzNxMSZzAjNjdTN1IGNhNmY4ADZlVWO5UjY1YDOmVjNilTNwMiNwEDO5QTNwYzI3ETZ3I2MxMGZhJmZ0MzY2IDZyUjNlVGOlVzNwIjZmJzI0IjO5UjOzEDI3ETL5ATL1IDMyMCN"' \
-   --form 'device="213213"' \
-   --form 'device_type="3"' \
-   --form 'page_count="1"' \
-   --form 'page_size="20"' \
-   --form 'keyword="三上"' \ 搜索词
-   --form 'resource_type="long"' \ 视频类型 长/短 (long /short)
-   --form 'lang="zh_CN"'
-
-   {
-      "status": 0,
-      "msg": "success",
-      "extend": {
-          "date": "2025-09-23 17:22:05",
-          "microtime": 1758619325.057152,
-          "unique": "68d266bab78ff253958681",
-          "version": "2023-02-28 08:50",
-          "request_unique": "",
-          "runtime": "2305.38 ms"
-      },
-      "data": {
-          "list": [
-              {
-                  "time": "1635011584",
-                  "resource_type": "long",
-                  "view": "19089",
-                  "name": "SSNI-516 女友旅行不在期间和她闺蜜国名偶像三上悠亚一直从早到晚做爱",
-                  "limit_free_time_end": "",
-                  "hash": "CEVVCQ63ST8P",
-                  "resource_id": "9748",
-                  "preview": "502333500",
-                  "actor": "",
-                  "require": "0",
-                  "comments": "0",
-                  "type": "long",
-                  "tags": "Dv36,NTty,iHyT,nRMD,QRoJ",
-                  "duration": "12775",
-                  "limit_free_time_start": "",
-                  "update_at": "1751532578",
-                  "coupon": "0",
-                  "favorite": "0",
-                  "like": "1520",
-                  "sync": "publish",
-                  "disk_id": "1",
-                  "id": "9748",
-                  "serial_no": "",
-                  "cover": "https://sftp.69mediatest.com/resources/2110/CEVVCQ63ST8P/cover",
-                  "m3u8": "https://sftp.69mediatest.com/resources/2110/CEVVCQ63ST8P/play",
-                  "path": "https://sftp.69mediatest.com/resources/2110/CEVVCQ63ST8P",
-                  "r_desc": "免费",
-                  "taginfo": [
-                      {
-                          "hash": "Dv36",
-                          "name": "口交",
-                          "level": "4"
-                      },
-                      {
-                          "hash": "NTty",
-                          "name": "巨乳",
-                          "level": "6"
-                      },
-                      {
-                          "hash": "iHyT",
-                          "name": "三上悠亚",
-                          "level": "11"
-                      },
-                      {
-                          "hash": "nRMD",
-                          "name": "少女",
-                          "level": "5"
-                      },
-                      {
-                          "hash": "QRoJ",
-                          "name": "女优",
-                          "level": "5"
-                      }
-                  ]
-              },
-              {
-                  "time": "1635018062",
-                  "resource_type": "long",
-                  "view": "18866",
-                  "name": "SSNI-730 不嫌弃我们部员汗臭味的女经理三上悠亚合宿三天雨夜大汗淋漓的做爱",
-                  "limit_free_time_end": "",
-                  "hash": "BK7JJBCR6FH2",
-                  "resource_id": "9135",
-                  "preview": "353959996",
-                  "actor": "",
-                  "require": "0",
-                  "comments": "0",
-                  "type": "long",
-                  "tags": "Dv36,45VN,NTty,UQP-,iHyT,nRMD",
-                  "duration": "9002",
-                  "limit_free_time_start": "",
-                  "update_at": "1751532559",
-                  "coupon": "0",
-                  "favorite": "0",
-                  "like": "1926",
-                  "sync": "publish",
-                  "disk_id": "1",
-                  "id": "9135",
-                  "serial_no": "",
-                  "cover": "https://sftp.69mediatest.com/resources/2110/BK7JJBCR6FH2/cover",
-                  "m3u8": "https://sftp.69mediatest.com/resources/2110/BK7JJBCR6FH2/play",
-                  "path": "https://sftp.69mediatest.com/resources/2110/BK7JJBCR6FH2",
-                  "r_desc": "免费",
-                  "taginfo": [
-                      {
-                          "hash": "Dv36",
-                          "name": "口交",
-                          "level": "4"
-                      },
-                      {
-                          "hash": "45VN",
-                          "name": "乳交",
-                          "level": "4"
-                      },
-                      {
-                          "hash": "NTty",
-                          "name": "巨乳",
-                          "level": "6"
-                      },
-                      {
-                          "hash": "UQP-",
-                          "name": "校服",
-                          "level": "8"
-                      },
-                      {
-                          "hash": "iHyT",
-                          "name": "三上悠亚",
-                          "level": "11"
-                      },
-                      {
-                          "hash": "nRMD",
-                          "name": "少女",
-                          "level": "5"
-                      }
-                  ]
-              },
-              {
-                  "time": "1635006312",
-                  "resource_type": "long",
-                  "view": "16998",
-                  "name": "SSNI-916 和泳衣巨乳美女教练三上悠亚老师控制不住的浑身是汗亲密性交 三上悠亚",
-                  "limit_free_time_end": "",
-                  "hash": "EME2BMK3PKGU",
-                  "resource_id": "11411",
-                  "preview": "342753340",
-                  "actor": "",
-                  "require": "0",
-                  "comments": "0",
-                  "type": "long",
-                  "tags": "NP3_,45VN,49Xk,NTty,iHyT,nRMD,ImOX,swim",
-                  "duration": "8718",
-                  "limit_free_time_start": "",
-                  "update_at": "1751532628",
-                  "coupon": "0",
-                  "favorite": "0",
-                  "like": "1872",
-                  "sync": "publish",
-                  "disk_id": "1",
-                  "id": "11411",
-                  "serial_no": "",
-                  "cover": "https://sftp.69mediatest.com/resources/2110/EME2BMK3PKGU/cover",
-                  "m3u8": "https://sftp.69mediatest.com/resources/2110/EME2BMK3PKGU/play",
-                  "path": "https://sftp.69mediatest.com/resources/2110/EME2BMK3PKGU",
-                  "r_desc": "免费",
-                  "taginfo": [
-                      {
-                          "hash": "NP3_",
-                          "name": "教师",
-                          "level": "5"
-                      },
-                      {
-                          "hash": "45VN",
-                          "name": "乳交",
-                          "level": "4"
-                      },
-                      {
-                          "hash": "49Xk",
-                          "name": "校园",
-                          "level": "7"
-                      },
-                      {
-                          "hash": "NTty",
-                          "name": "巨乳",
-                          "level": "6"
-                      },
-                      {
-                          "hash": "iHyT",
-                          "name": "三上悠亚",
-                          "level": "11"
-                      },
-                      {
-                          "hash": "nRMD",
-                          "name": "少女",
-                          "level": "5"
-                      },
-                      {
-                          "hash": "ImOX",
-                          "name": "教练",
-                          "level": "5"
-                      },
-                      {
-                          "hash": "swim",
-                          "name": "泳衣",
-                          "level": "8"
-                      }
-                  ]
-              }
-          ],
-          "pageInfo": {
-              "pageSize": 3,
-              "pageTotal": 19,
-              "pageCurrent": 1,
-              "pageCount": 7
-          }
-      }
-  }
-
-  标签分类 查询接口
-  curl --location --request POST 'https://qa3-api.69mediatest.com/api/media/30017/search' \
---form 'plat_id="30017"' \
---form 'channel_id="30017001"' \
---form 'user_id="30005470"' \
---form 'app_type="4"' \
---form 'token="0YzN4gDM4UzNxMSZzAjNjdTN1IGNhNmY4ADZlVWO5UjY1YDOmVjNilTNwMiNwEDO5QTNwYzI3ETZ3I2MxMGZhJmZ0MzY2IDZyUjNlVGOlVzNwIjZmJzI0IjO5UjOzEDI3ETL5ATL1IDMyMCN"' \
---form 'device="213213"' \
---form 'device_type="3"' \
---form 'page_count="1"' \
---form 'page_size="3"' \
---form 'tag="0Bn5,kq2k,yT2C,Ride,fher,hTmi,home"' \ 标签 hash
---form 'resource_type="long"' \ 视频类型 : 长/短 (long /short)
---form 'sort="time"' 默认全部。可选('view', 'like', 'time')
-
-{
-   "status": 0,
-   "msg": "success",
-   "extend": {
-   "date": "2023-05-08 14:38:17",
-   "microtime": 1683527897.52491,
-   "unique": "645898d6a051c1574223763",
-   "version": "2023-02-28 08:50",
-   "request_unique": "",
-   "runtime": "2877.83 ms"
-   },
-   "data": {
-   "list": [
-   {
-   "resource_id": 33,
-   "like": 0,
-   "view": 0,
-   "favorite": 0,
-   "limit_free_time": 0,
-   "name": "教室里哦",
-   "require": 0,
-   "coupon": 1,
-   "preview": 15,
-   "duration": 20,
-   "tags": "0Bn5,kq2k,yT2C,Ride,fher,hTmi,home",
-   "hash": "0JNK8P2L4SHK",
-   "type": "long",
-   "path": "https://r.yongyinsoft.com/2305/0JNK8P2L4SHK/",
-   "cover": "https://r.yongyinsoft.com/2305/0JNK8P2L4SHK/cover",
-   "m3u8": "https://r.yongyinsoft.com/2305/0JNK8P2L4SHK/m3u8"
-   }
-   ],
-   "pageInfo": {
-   "pageCurrent": 1,
-   "pageCount": 1,
-   "pageSize": 2,
-   "pageTotal": 1
-   }
-   }
-  }
-
-  长/短视频类型 查询接口
-  curl --location --request POST 'https://qa3-api.69mediatest.com/api/media/30017/search' \
---form 'plat_id="30017"' \
---form 'channel_id="30017001"' \
---form 'user_id="30005470"' \
---form 'app_type="4"' \
---form 'token="0YzN4gDM4UzNxMSZzAjNjdTN1IGNhNmY4ADZlVWO5UjY1YDOmVjNilTNwMiNwEDO5QTNwYzI3ETZ3I2MxMGZhJmZ0MzY2IDZyUjNlVGOlVzNwIjZmJzI0IjO5UjOzEDI3ETL5ATL1IDMyMCN"' \
---form 'device="213213"' \
---form 'device_type="3"' \
---form 'page_count="1"' \
---form 'page_size="3"' \
---form 'resource_type="long"' \ 视频类型 : 长/短 (long /short)
---form 'sort="time"' 默认全部。可选('view', 'like', 'time')
-
-{
-   "status": 0,
-   "msg": "success",
-   "extend": {
-       "date": "2025-09-23 17:27:25",
-       "microtime": 1758619645.28696,
-       "unique": "68d267fc32a751456269581",
-       "version": "2023-02-28 08:50",
-       "request_unique": "",
-       "runtime": "1079.51 ms"
-   },
-   "data": {
-       "list": [
-           {
-               "resource_id": "121703",
-               "name": "1202-4",
-               "serial_no": "",
-               "like": "1473",
-               "view": "20788",
-               "comments": "0",
-               "favorite": "0",
-               "limit_free_time_start": "",
-               "limit_free_time_end": "",
-               "coupon": "0",
-               "require": "0",
-               "time": "1733126112",
-               "preview": "22",
-               "type": "short",
-               "resource_type": "short",
-               "actor": "",
-               "tags": "-Bht,0j0O,2wzR,0DLd",
-               "hash": "EAHPMF0JF6NR",
-               "id": "121703",
-               "duration": "38",
-               "sync": "publish",
-               "disk_id": "1",
-               "update_at": "1752490068",
-               "cover": "https://sftp.69mediatest.com/resources/2412/EAHPMF0JF6NR/cover",
-               "m3u8": "https://sftp.69mediatest.com/resources/2412/EAHPMF0JF6NR/play",
-               "path": "https://sftp.69mediatest.com/resources/2412/EAHPMF0JF6NR",
-               "r_desc": "免费",
-               "taginfo": [
-                   {
-                       "hash": "-Bht",
-                       "name": "猫爪影像",
-                       "level": "10"
-                   },
-                   {
-                       "hash": "0j0O",
-                       "name": "良家",
-                       "level": "5"
-                   },
-                   {
-                       "hash": "2wzR",
-                       "name": "福利姬",
-                       "level": "5"
-                   },
-                   {
-                       "hash": "0DLd",
-                       "name": "深田咏美",
-                       "level": "11"
-                   }
-               ]
-           },
-           {
-               "resource_id": "121702",
-               "name": "1202-3",
-               "serial_no": "",
-               "like": "556",
-               "view": "13275",
-               "comments": "0",
-               "favorite": "0",
-               "limit_free_time_start": "",
-               "limit_free_time_end": "",
-               "coupon": "0",
-               "require": "0",
-               "time": "1733126033",
-               "preview": "3",
-               "type": "short",
-               "resource_type": "short",
-               "actor": "",
-               "tags": "",
-               "hash": "N9D0563S3AI9",
-               "id": "121702",
-               "duration": "10",
-               "sync": "publish",
-               "disk_id": "1",
-               "update_at": "1752490068",
-               "cover": "https://sftp.69mediatest.com/resources/2412/N9D0563S3AI9/cover",
-               "m3u8": "https://sftp.69mediatest.com/resources/2412/N9D0563S3AI9/play",
-               "path": "https://sftp.69mediatest.com/resources/2412/N9D0563S3AI9",
-               "r_desc": "免费"
-           },
-           {
-               "resource_id": "121699",
-               "name": "1102-5",
-               "serial_no": "",
-               "like": "1064",
-               "view": "7428",
-               "comments": "0",
-               "favorite": "0",
-               "limit_free_time_start": "",
-               "limit_free_time_end": "",
-               "coupon": "0",
-               "require": "0",
-               "time": "1733122943",
-               "preview": "5",
-               "type": "short",
-               "resource_type": "short",
-               "actor": "",
-               "tags": "",
-               "hash": "O4LRV3NITQHN",
-               "id": "121699",
-               "duration": "14",
-               "sync": "publish",
-               "disk_id": "1",
-               "update_at": "1751532673",
-               "cover": "https://sftp.69mediatest.com/resources/2412/O4LRV3NITQHN/cover",
-               "m3u8": "https://sftp.69mediatest.com/resources/2412/O4LRV3NITQHN/play",
-               "path": "https://sftp.69mediatest.com/resources/2412/O4LRV3NITQHN",
-               "r_desc": "免费"
-           }
-       ],
-       "pageInfo": {
-           "pageSize": 3,
-           "pageTotal": 3002,
-           "pageCurrent": 1,
-           "pageCount": 1001
-       }
-   }
-}
-
-视频顶部标签 查询接口:
-curl --location --request POST 'https://qa3-api.69mediatest.com/api/media/30017/menu' \
---form 'plat_id="30017"' \
---form 'channel_id="30017001"' \
---form 'user_id="30005470"' \
---form 'app_type="4"' \
---form 'token="0YzN4gDM4UzNxMSZzAjNjdTN1IGNhNmY4ADZlVWO5UjY1YDOmVjNilTNwMiNwEDO5QTNwYzI3ETZ3I2MxMGZhJmZ0MzY2IDZyUjNlVGOlVzNwIjZmJzI0IjO5UjOzEDI3ETL5ATL1IDMyMCN"' \
---form 'device="213213"' \
---form 'device_type="3"' \
---form 'page_count="1"' \
---form 'page_size="3"' \
---form 'type="1"' 1.代表长视频的菜单 2. 获取短视频的菜单
-
-{
-   "status": 0,
-   "msg": "success",
-   "extend": {
-   "date": "2023-07-14 17:53:43",
-   "microtime": 1689328423.453458,
-   "unique": "64b11b27609db736481449",
-   "version": "2023-02-28 08:50",
-   "request_unique": "",
-   "runtime": "58.25 ms"
-   },
-   "data": [
-   {
-   "hash": "fdwr",
-   "name": "AV",
-   "sort": -35,
-   "ads": "", // 广告内容 ID
-   "ad_gap": 0 // 间隔数
-   },
-   {
-   "hash": "MZ4v",
-   "name": "123123124",
-   "sort": -34,
-   "ads": "40,42,43,46,52",
-   "ad_gap": 2
-   },
-   {
-   "hash": "nkfi",
-   "name": "\u63a8\u8350",
-   "sort": -33,
-   "ads": "40,42,43,44,46",
-   "ad_gap": 3
-   },
-   {
-   "hash": "R8bS",
-   "name": "\u4f18\u9009",
-   "sort": -27,
-   "ads": "42,44",
-   "ad_gap": 3
-   },
-   {
-   "hash": "nsse",
-   "name": "\u65b0\u4eba\u514d\u8d39",
-   "sort": -25,
-   "ads": "44,43",
-   "ad_gap": 0
-   },
-   {
-   "hash": "4Pwj",
-   "name": "\u6d4b\u8bd5",
-   "sort": -17,
-   "ads": "42,46",
-   "ad_gap": 0
-   },
-   {
-   "hash": "sdnf",
-   "name": "1\u5143\u79d2\u6740",
-   "sort": -15,
-   "ads": "",
-   "ad_gap": 0
-   },
-   {
-   "hash": "hkfs",
-   "name": "UP\u4e3b",
-   "sort": -2,
-   "ads": "43,44,52",
-   "ad_gap": 3
-   },
-   {
-   "hash": "fdsg",
-   "name": "\u7eaf\u6b32",
-   "sort": 5,
-   "ads": "",
-   "ad_gap": 3
-   },
-   {
-   "hash": "cjkf",
-   "name": "69\u5927\u795e",
-   "sort": 6,
-   "ads": "",
-   "ad_gap": 0
-   },
-   {
-   "hash": "bner",
-   "name": "\u4e13\u9898",
-   "sort": 7,
-   "ads": "",
-   "ad_gap": 0
-   },
-   {
-   "hash": "fdsf",
-   "name": "\u56fd\u4ea7",
-   "sort": 8,
-   "ads": "",
-   "ad_gap": 0
-   },
-   {
-   "hash": "fslj",
-   "name": "\u5973\u4f18",
-   "sort": 10,
-   "ads": "",
-   "ad_gap": 0
-   },
-   {
-   "hash": "jpu7",
-   "name": "\u52a8\u6f2b",
-   "sort": 74,
-   "ads": "",
-   "ad_gap": 0
-   },
-   {
-   "hash": "qDHw",
-   "name": "\u6b27\u7f8e",
-   "sort": 76,
-   "ads": "",
-   "ad_gap": 0
-   },
-   {
-   "hash": "HMOy",
-   "name": "\u7efc\u827a",
-   "sort": 77,
-   "ads": "",
-   "ad_gap": 0
-   },
-   {
-   "hash": "orLS",
-   "name": "\u7535\u5f71",
-   "sort": 93,
-   "ads": "",
-   "ad_gap": 0
-   }
-   ]
-}
-
-点播集 查询接口:
-curl --location --request POST 'https://qa3-api.69mediatest.com/api/media/menu/vods' \
---form 'plat_id="30017"' \
---form 'channel_id="30017001"' \
---form 'user_id="30005470"' \
---form 'app_type="4"' \
---form 'token="0YzN4gDM4UzNxMSZzAjNjdTN1IGNhNmY4ADZlVWO5UjY1YDOmVjNilTNwMiNwEDO5QTNwYzI3ETZ3I2MxMGZhJmZ0MzY2IDZyUjNlVGOlVzNwIjZmJzI0IjO5UjOzEDI3ETL5ATL1IDMyMCN"' \
---form 'device="213213"' \
---form 'device_type="3"' \
---form 'page_count="1"' \
---form 'page_size="3"' \
---form 'hash="0210E8M1G4MQ"' 根据首页菜单的 hash 获取点播集列表
-
-{
-   "status": 0,
-   "msg": "success",
-   "extend": {
-   "date": "2023-05-08 11:24:54",
-   "microtime": 1683516295.001233,
-   "unique": "64586b843c7a6554652695",
-   "version": "2023-02-28 08:50",
-   "request_unique": "",
-   "runtime": "2759.19 ms"
-   },
-   "data": [
-   {
-   "vod_id": 1, //点拨集 id
-   "vod_name": "id_1", //点播集名字
-   "template_id": 1,//模版 id : 1 預設双联横 2 預設双联竖 3 預設大橫图 4 預設大
-  一偶小 5預設竖中滑动 6 預設横中滑动 7 預設横小滑动 8 預設竖小滑动 9 預設三联竖 10 預設 4
-  小圓图 11 預設單圓图
-   "resource_type": "long",//视频类型
-   "preview_resource_ids": "3,4",
-   "is_showed_desc": 0,//是否展示 点播名称。 如果不展示,则整个就是一个列表。跟点播集更多一
-  样。
-   "asort": 0,
-   "movies": [
-   {
-   "id": 3,// 跟下面 resource_id 一样, 资源 id
-   "hash": "0210E8M1G4MQ",
-   "time": 1658902887,
-   "preview": 15,
-   "duration": 343,
-   "type": "short",
-   "tags": "0Bn5,DQFQ,Ride,TwMR,QTHc,sOsr,slim,home,5f9C",
-   "name": "Onlyfans #Littlesulaa 5",
-   "actor": "",
-   "serial_no": "AAAA",
-   "limit_free_time": 0,
-   "coupon": 1,
-   "resource_id": 3,
-   "path": "https://r.yongyinsoft.com/2207/0210E8M1G4MQ/",
-   "cover": "https://r.yongyinsoft.com/2207/0210E8M1G4MQ/cover",
-   "m3u8": "https://r.yongyinsoft.com/2207/0210E8M1G4MQ/m3u8"
-   },
-   {
-   "id": 4,
-   "hash": "02659MI0BV12",
-   "time": 1661351589,
-   "preview": 50200636,
-   "duration": 1278,
-   "type": "long",
-   "tags": "lqe2,vHzu,IJQH,gRpU",
-   "name": "内裤嗅探器的大奖 ",
-   "actor": "",
-   "serial_no": "AAAA",
-   "limit_free_time": 0,
-   "coupon": 1,
-   "resource_id": 4,
-   "path": "https://r.yongyinsoft.com/2208/02659MI0BV12/",
-   "cover": "https://r.yongyinsoft.com/2208/02659MI0BV12/cover",
-   "m3u8": "https://r.yongyinsoft.com/2208/02659MI0BV12/m3u8"
-   }
-   ]
-   },
-   {
-   "vod_id": 2,
-   "vod_name": "id_2",
-   "template_id": 2,
-   "resource_type": "long",
-   "preview_resource_ids": "5,6",
-   "is_showed_desc": 0,
-   "asort": 0,
-   "movies": [
-   {
-   "id": 5,
-   "hash": "026NIETAAB5I",
-   "time": 1660733122,
-   "preview": 90505276,
-   "duration": 2303,
-   "type": "long",
-   "tags": "DQFQ,cD2Z,Cazx",
-   "name": "周宁.凌薇.吴梦梦.情欲天堂的成名之路.麻豆传媒映画",
-   "actor": "",
-   "serial_no": "AAAA",
-   "limit_free_time": 0,
-   "coupon": 1,
-   "resource_id": 5,
-   "path": "https://r.yongyinsoft.com/2208/026NIETAAB5I/",
-   "cover": "https://r.yongyinsoft.com/2208/026NIETAAB5I/cover",
-   "m3u8": "https://r.yongyinsoft.com/2208/026NIETAAB5I/m3u8"
-   },
-   {
-   "id": 6,
-   "hash": "02KSMT186M28",
-   "time": 1658638020,
-   "preview": 15,
-   "duration": 72,
-   "type": "short",
-   "tags": "F2i7,danc,06Rb,bozz,slim,hotl,tGe1",
-   "name": "韩国直播裸舞精选 17",
-   "actor": "",
-   "serial_no": "AAAA",
-   "limit_free_time": 0,
-   "coupon": 1,
-   "resource_id": 6,
-   "path": "https://r.yongyinsoft.com/2207/02KSMT186M28/",
-   "cover": "https://r.yongyinsoft.com/2207/02KSMT186M28/cover",
-   "m3u8": "https://r.yongyinsoft.com/2207/02KSMT186M28/m3u8"
-   }
-   ]
-   }
-   ]
-  }
-  返回结果
-  {
-   "status": 0,
-   "msg": "success",
-   "extend": {
-   "date": "2023-05-03 20:34:28",
-   "microtime": 1683117268.428462,
-   "unique": "645254d44409e2028174429",
-   "version": "2023-02-28 08:50",
-   "request_unique": "",
-   "runtime": "153.81 ms"
-   },
-   "data": [
-   {
-   "vod_name": "mody 老师",// 点播集名称
-   "vod_id": "1", //点播集 id
-   "template_id": "1", //模版 id : 1 預設双联横 2 預設双联竖 3 預設大橫图 4 預設大
-  一偶小 5預設竖中滑动 6 預設横中滑动 7 預設横小滑动 8 預設竖小滑动 9 預設三联竖 10 預設 4
-  小圓图 11 預設單圓图
-   "resource_type": "long", //视频类型 //enum('long','short','live','movie')
-   "sort": 100, // 后断已经拍好
-   "movies": [ //点播集下面的预设影片
-   {
-   "play_num": 849, //播放量
-   "duration": "5:01", // 时常
-   "require": 2, // -1 会员 0 免费 >0. 代表多少个金币
-   "title": "HongKongDoll 玩偶姐姐 森林 第一集", //影片标题
-   "hash": "UJK549J1E71F", //影片 hash 唯一
-   "cover": "https://rfjs.tpptwpqd.cc/2211/UJK549J1E71F/cover", //预览图
-   "m3u8": "https://rfjs.tpptwpqd.cc/2110/UJK549J1E71F/play.m3u8" //播放地址
-   },
-   {
-   "play_num": 442,
-   "duration": "3:01",
-   "require": 2,
-   "title": "HongKongDoll 青蛇#cosplay#小青#白蛇传#许仙",
-   "hash": "MDSE00002350",
-   "cover": "https://rfjs.tpptwpqd.cc/2211/MDSE00002350/cover",
-   "m3u8": "https://rfjs.tpptwpqd.cc/2110/MDSE00002350/play.m3u8"
-   },
-   {
-   "play_num": 981,
-   "duration": "5:01",
-   "require": 0,
-   "title": "HongKongDoll 玩偶姐姐 森林 第一集",
-   "hash": "UJK549J1E71F",
-   "cover": "https://rfjs.tpptwpqd.cc/2211/UJK549J1E71F/cover",
-   "m3u8": "https://rfjs.tpptwpqd.cc/2110/UJK549J1E71F/play.m3u8"
-   },
-   {
-   "play_num": 235,
-   "duration": "3:01",
-   "require": 5,
-   "title": "HongKongDoll 青蛇#cosplay#小青#白蛇传#许仙",
-   "hash": "MDSE00002350",
-   "cover": "https://rfjs.tpptwpqd.cc/2211/MDSE00002350/cover",
-   "m3u8": "https://rfjs.tpptwpqd.cc/2110/MDSE00002350/play.m3u8"
-   },
-   {
-   "play_num": 214,
-   "duration": "5:01",
-   "require": 2,
-   "title": "HongKongDoll 玩偶姐姐 森林 第一集",
-   "hash": "UJK549J1E71F",
-   "cover": "https://rfjs.tpptwpqd.cc/2211/UJK549J1E71F/cover",
-   "m3u8": "https://rfjs.tpptwpqd.cc/2110/UJK549J1E71F/play.m3u8"
-   },
-   {
-   "play_num": 206,
-   "duration": "3:01",
-   "require": 5,
-   "title": "HongKongDoll 青蛇#cosplay#小青#白蛇传#许仙",
-   "hash": "MDSE00002350",
-   "cover": "https://rfjs.tpptwpqd.cc/2211/MDSE00002350/cover",
-   "m3u8": "https://rfjs.tpptwpqd.cc/2110/MDSE00002350/play.m3u8"
-   }
-   ]
-   },
-   {
-   "vod_name": "bill 老师",
-   "vod_id": "2",
-   "template_id": "2",
-   "resource_type": "long",
-   "sort": 100,
-   "movies": [
-   {
-   "play_num": 175,
-   "duration": "2:01",
-   "require": 4,
-   "title": "HongKongDoll 玩偶姐姐 森林 第一集",
-   "hash": "UJK549J1E71F",
-   "cover": "https://rfjs.tpptwpqd.cc/2211/UJK549J1E71F/cover",
-   "m3u8": "https://rfjs.tpptwpqd.cc/2110/UJK549J1E71F/play.m3u8"
-   },
-   {
-   "play_num": 770,
-   "duration": "4:01",
-   "require": 3,
-   "title": "HongKongDoll 青蛇#cosplay#小青#白蛇传#许仙",
-   "hash": "MDSE00002350",
-   "cover": "https://rfjs.tpptwpqd.cc/2211/MDSE00002350/cover",
-   "m3u8": "https://rfjs.tpptwpqd.cc/2110/MDSE00002350/play.m3u8"
-   },
-   {
-   "play_num": 757,
-   "duration": "5:01",
-   "require": 5,
-   "title": "HongKongDoll 玩偶姐姐 森林 第一集",
-   "hash": "UJK549J1E71F",
-   "cover": "https://rfjs.tpptwpqd.cc/2211/UJK549J1E71F/cover",
-   "m3u8": "https://rfjs.tpptwpqd.cc/2110/UJK549J1E71F/play.m3u8"
-   },
-   {
-   "play_num": 191,
-   "duration": "3:01",
-   "require": 1,
-   "title": "HongKongDoll 青蛇#cosplay#小青#白蛇传#许仙",
-   "hash": "MDSE00002350",
-   "cover": "https://rfjs.tpptwpqd.cc/2211/MDSE00002350/cover",
-   "m3u8": "https://rfjs.tpptwpqd.cc/2110/MDSE00002350/play.m3u8"
-   },
-   {
-   "play_num": 845,
-   "duration": "5:01",
-   "require": 2,
-   "title": "HongKongDoll 玩偶姐姐 森林 第一集",
-   "hash": "UJK549J1E71F",
-   "cover": "https://rfjs.tpptwpqd.cc/2211/UJK549J1E71F/cover",
-   "m3u8": "https://rfjs.tpptwpqd.cc/2110/UJK549J1E71F/play.m3u8"
-   },
-   {
-   "play_num": 431,
-   "duration": "3:01",
-   "require": 4,
-   "title": "HongKongDoll 青蛇#cosplay#小青#白蛇传#许仙",
-   "hash": "MDSE00002350",
-   "cover": "https://rfjs.tpptwpqd.cc/2211/MDSE00002350/cover",
-   "m3u8": "https://rfjs.tpptwpqd.cc/2110/MDSE00002350/play.m3u8"
-   }
-   ]
-   },
-   {
-   "vod_name": "mody 老师",
-   "vod_id": "3",
-   "template_id": "3",
-   "resource_type": "long",
-   "sort": 100,
-   "movies": [
-   {
-   "play_num": 540,
-   "duration": "5:01",
-   "require": 3,
-   "title": "HongKongDoll 玩偶姐姐 森林 第一集",
-   "hash": "UJK549J1E71F",
-   "cover": "https://rfjs.tpptwpqd.cc/2211/UJK549J1E71F/cover",
-   "m3u8": "https://rfjs.tpptwpqd.cc/2110/UJK549J1E71F/play.m3u8"
-   },
-   {
-   "play_num": 989,
-   "duration": "3:01",
-   "require": -1,
-   "title": "HongKongDoll 青蛇#cosplay#小青#白蛇传#许仙",
-   "hash": "MDSE00002350",
-   "cover": "https://rfjs.tpptwpqd.cc/2211/MDSE00002350/cover",
-   "m3u8": "https://rfjs.tpptwpqd.cc/2110/MDSE00002350/play.m3u8"
-   },
-   {
-   "play_num": 370,
-   "duration": "5:01",
-   "require": 4,
-   "title": "HongKongDoll 玩偶姐姐 森林 第一集",
-   "hash": "UJK549J1E71F",
-   "cover": "https://rfjs.tpptwpqd.cc/2211/UJK549J1E71F/cover",
-   "m3u8": "https://rfjs.tpptwpqd.cc/2110/UJK549J1E71F/play.m3u8"
-   },
-   {
-   "play_num": 703,
-   "duration": "3:01",
-   "require": -1,
-   "title": "HongKongDoll 青蛇#cosplay#小青#白蛇传#许仙",
-   "hash": "MDSE00002350",
-   "cover": "https://rfjs.tpptwpqd.cc/2211/MDSE00002350/cover",
-   "m3u8": "https://rfjs.tpptwpqd.cc/2110/MDSE00002350/play.m3u8"
-   },
-   {
-   "play_num": 186,
-   "duration": "5:01",
-   "require": 0,
-   "title": "HongKongDoll 玩偶姐姐 森林 第一集",
-   "hash": "UJK549J1E71F",
-   "cover": "https://rfjs.tpptwpqd.cc/2211/UJK549J1E71F/cover",
-   "m3u8": "https://rfjs.tpptwpqd.cc/2110/UJK549J1E71F/play.m3u8"
-   },
-   {
-   "play_num": 837,
-   "duration": "3:01",
-   "require": -1,
-   "title": "HongKongDoll 青蛇#cosplay#小青#白蛇传#许仙",
-   "hash": "MDSE00002350",
-   "cover": "https://rfjs.tpptwpqd.cc/2211/MDSE00002350/cover",
-   "m3u8": "https://rfjs.tpptwpqd.cc/2110/MDSE00002350/play.m3u8"
-   }
-   ]
-   },
-   {
-   "vod_name": "bill 老师",
-   "vod_id": "4",
-   "template_id": "4",
-   "resource_type": "long",
-   "sort": 100,
-   "movies": [
-   {
-   "play_num": 662,
-   "duration": "2:01",
-   "require": 4,
-   "title": "HongKongDoll 玩偶姐姐 森林 第一集",
-   "hash": "UJK549J1E71F",
-   "cover": "https://rfjs.tpptwpqd.cc/2211/UJK549J1E71F/cover",
-   "m3u8": "https://rfjs.tpptwpqd.cc/2110/UJK549J1E71F/play.m3u8"
-   },
-   {
-   "play_num": 385,
-   "duration": "4:01",
-   "require": 2,
-   "title": "HongKongDoll 青蛇#cosplay#小青#白蛇传#许仙",
-   "hash": "MDSE00002350",
-   "cover": "https://rfjs.tpptwpqd.cc/2211/MDSE00002350/cover",
-   "m3u8": "https://rfjs.tpptwpqd.cc/2110/MDSE00002350/play.m3u8"
-   },
-   {
-   "play_num": 320,
-   "duration": "5:01",
-   "require": 0,
-   "title": "HongKongDoll 玩偶姐姐 森林 第一集",
-   "hash": "UJK549J1E71F",
-   "cover": "https://rfjs.tpptwpqd.cc/2211/UJK549J1E71F/cover",
-   "m3u8": "https://rfjs.tpptwpqd.cc/2110/UJK549J1E71F/play.m3u8"
-   },
-   {
-   "play_num": 932,
-   "duration": "3:01",
-   "require": 2,
-   "title": "HongKongDoll 青蛇#cosplay#小青#白蛇传#许仙",
-   "hash": "MDSE00002350",
-   "cover": "https://rfjs.tpptwpqd.cc/2211/MDSE00002350/cover",
-   "m3u8": "https://rfjs.tpptwpqd.cc/2110/MDSE00002350/play.m3u8"
-   },
-   {
-   "play_num": 541,
-   "duration": "5:01",
-   "require": 2,
-   "title": "HongKongDoll 玩偶姐姐 森林 第一集",
-   "hash": "UJK549J1E71F",
-   "cover": "https://rfjs.tpptwpqd.cc/2211/UJK549J1E71F/cover",
-   "m3u8": "https://rfjs.tpptwpqd.cc/2110/UJK549J1E71F/play.m3u8"
-   },
-   {
-   "play_num": 311,
-   "duration": "3:01",
-   "require": 4,
-   "title": "HongKongDoll 青蛇#cosplay#小青#白蛇传#许仙",
-   "hash": "MDSE00002350",
-   "cover": "https://rfjs.tpptwpqd.cc/2211/MDSE00002350/cover",
-   "m3u8": "https://rfjs.tpptwpqd.cc/2110/MDSE00002350/play.m3u8"
-   }
-   ]
-   },
-   {
-   "vod_name": "mody 老师",
-   "vod_id": "5",
-   "template_id": "5",
-   "resource_type": "long",
-   "sort": 100,
-   "movies": [
-   {
-   "play_num": 272,
-   "duration": "5:01",
-   "require": 5,
-   "title": "HongKongDoll 玩偶姐姐 森林 第一集",
-   "hash": "UJK549J1E71F",
-   "cover": "https://rfjs.tpptwpqd.cc/2211/UJK549J1E71F/cover",
-   "m3u8": "https://rfjs.tpptwpqd.cc/2110/UJK549J1E71F/play.m3u8"
-   },
-   {
-   "play_num": 871,
-   "duration": "3:01",
-   "require": 4,
-   "title": "HongKongDoll 青蛇#cosplay#小青#白蛇传#许仙",
-   "hash": "MDSE00002350",
-   "cover": "https://rfjs.tpptwpqd.cc/2211/MDSE00002350/cover",
-   "m3u8": "https://rfjs.tpptwpqd.cc/2110/MDSE00002350/play.m3u8"
-   },
-   {
-   "play_num": 334,
-   "duration": "5:01",
-   "require": 4,
-   "title": "HongKongDoll 玩偶姐姐 森林 第一集",
-   "hash": "UJK549J1E71F",
-   "cover": "https://rfjs.tpptwpqd.cc/2211/UJK549J1E71F/cover",
-   "m3u8": "https://rfjs.tpptwpqd.cc/2110/UJK549J1E71F/play.m3u8"
-   },
-   {
-   "play_num": 305,
-   "duration": "3:01",
-   "require": 5,
-   "title": "HongKongDoll 青蛇#cosplay#小青#白蛇传#许仙",
-   "hash": "MDSE00002350",
-   "cover": "https://rfjs.tpptwpqd.cc/2211/MDSE00002350/cover",
-   "m3u8": "https://rfjs.tpptwpqd.cc/2110/MDSE00002350/play.m3u8"
-   },
-   {
-   "play_num": 334,
-   "duration": "5:01",
-   "require": 2,
-   "title": "HongKongDoll 玩偶姐姐 森林 第一集",
-   "hash": "UJK549J1E71F",
-   "cover": "https://rfjs.tpptwpqd.cc/2211/UJK549J1E71F/cover",
-   "m3u8": "https://rfjs.tpptwpqd.cc/2110/UJK549J1E71F/play.m3u8"
-   },
-   {
-   "play_num": 603,
-   "duration": "3:01",
-   "require": 4,
-   "title": "HongKongDoll 青蛇#cosplay#小青#白蛇传#许仙",
-   "hash": "MDSE00002350",
-   "cover": "https://rfjs.tpptwpqd.cc/2211/MDSE00002350/cover",
-   "m3u8": "https://rfjs.tpptwpqd.cc/2110/MDSE00002350/play.m3u8"
-   }
-   ]
-   },
-   {
-   "vod_name": "bill 老师",
-   "vod_id": "6",
-   "template_id": "6",
-   "resource_type": "long",
-   "sort": 100,
-   "movies": [
-   {
-   "play_num": 131,
-   "duration": "2:01",
-   "require": -1,
-   "title": "HongKongDoll 玩偶姐姐 森林 第一集",
-   "hash": "UJK549J1E71F",
-   "cover": "https://rfjs.tpptwpqd.cc/2211/UJK549J1E71F/cover",
-   "m3u8": "https://rfjs.tpptwpqd.cc/2110/UJK549J1E71F/play.m3u8"
-   },
-   {
-   "play_num": 348,
-   "duration": "4:01",
-   "require": 0,
-   "title": "HongKongDoll 青蛇#cosplay#小青#白蛇传#许仙",
-   "hash": "MDSE00002350",
-   "cover": "https://rfjs.tpptwpqd.cc/2211/MDSE00002350/cover",
-   "m3u8": "https://rfjs.tpptwpqd.cc/2110/MDSE00002350/play.m3u8"
-   },
-   {
-   "play_num": 670,
-   "duration": "5:01",
-   "require": -1,
-   "title": "HongKongDoll 玩偶姐姐 森林 第一集",
-   "hash": "UJK549J1E71F",
-   "cover": "https://rfjs.tpptwpqd.cc/2211/UJK549J1E71F/cover",
-   "m3u8": "https://rfjs.tpptwpqd.cc/2110/UJK549J1E71F/play.m3u8"
-   },
-   {
-   "play_num": 582,
-   "duration": "3:01",
-   "require": 3,
-   "title": "HongKongDoll 青蛇#cosplay#小青#白蛇传#许仙",
-   "hash": "MDSE00002350",
-   "cover": "https://rfjs.tpptwpqd.cc/2211/MDSE00002350/cover",
-   "m3u8": "https://rfjs.tpptwpqd.cc/2110/MDSE00002350/play.m3u8"
-   },
-   {
-   "play_num": 575,
-   "duration": "5:01",
-   "require": -1,
-   "title": "HongKongDoll 玩偶姐姐 森林 第一集",
-   "hash": "UJK549J1E71F",
-   "cover": "https://rfjs.tpptwpqd.cc/2211/UJK549J1E71F/cover",
-   "m3u8": "https://rfjs.tpptwpqd.cc/2110/UJK549J1E71F/play.m3u8"
-   },
-   {
-   "play_num": 856,
-   "duration": "3:01",
-   "require": 1,
-   "title": "HongKongDoll 青蛇#cosplay#小青#白蛇传#许仙",
-   "hash": "MDSE00002350",
-   "cover": "https://rfjs.tpptwpqd.cc/2211/MDSE00002350/cover",
-   "m3u8": "https://rfjs.tpptwpqd.cc/2110/MDSE00002350/play.m3u8"
-   }
-   ]
-   },
-   {
-   "vod_name": "mody 老师",
-   "vod_id": "7",
-   "template_id": "7",
-   "resource_type": "long",
-   "sort": 100,
-   "movies": [
-   {
-   "play_num": 201,
-   "duration": "5:01",
-   "require": 4,
-   "title": "HongKongDoll 玩偶姐姐 森林 第一集",
-   "hash": "UJK549J1E71F",
-   "cover": "https://rfjs.tpptwpqd.cc/2211/UJK549J1E71F/cover",
-   "m3u8": "https://rfjs.tpptwpqd.cc/2110/UJK549J1E71F/play.m3u8"
-   },
-   {
-   "play_num": 183,
-   "duration": "3:01",
-   "require": 3,
-   "title": "HongKongDoll 青蛇#cosplay#小青#白蛇传#许仙",
-   "hash": "MDSE00002350",
-   "cover": "https://rfjs.tpptwpqd.cc/2211/MDSE00002350/cover",
-   "m3u8": "https://rfjs.tpptwpqd.cc/2110/MDSE00002350/play.m3u8"
-   },
-   {
-   "play_num": 423,
-   "duration": "5:01",
-   "require": 4,
-   "title": "HongKongDoll 玩偶姐姐 森林 第一集",
-   "hash": "UJK549J1E71F",
-   "cover": "https://rfjs.tpptwpqd.cc/2211/UJK549J1E71F/cover",
-   "m3u8": "https://rfjs.tpptwpqd.cc/2110/UJK549J1E71F/play.m3u8"
-   },
-   {
-   "play_num": 869,
-   "duration": "3:01",
-   "require": 3,
-   "title": "HongKongDoll 青蛇#cosplay#小青#白蛇传#许仙",
-   "hash": "MDSE00002350",
-   "cover": "https://rfjs.tpptwpqd.cc/2211/MDSE00002350/cover",
-   "m3u8": "https://rfjs.tpptwpqd.cc/2110/MDSE00002350/play.m3u8"
-   },
-   {
-   "play_num": 125,
-   "duration": "5:01",
-   "require": 3,
-   "title": "HongKongDoll 玩偶姐姐 森林 第一集",
-   "hash": "UJK549J1E71F",
-   "cover": "https://rfjs.tpptwpqd.cc/2211/UJK549J1E71F/cover",
-   "m3u8": "https://rfjs.tpptwpqd.cc/2110/UJK549J1E71F/play.m3u8"
-   },
-   {
-   "play_num": 186,
-   "duration": "3:01",
-   "require": -1,
-   "title": "HongKongDoll 青蛇#cosplay#小青#白蛇传#许仙",
-   "hash": "MDSE00002350",
-   "cover": "https://rfjs.tpptwpqd.cc/2211/MDSE00002350/cover",
-   "m3u8": "https://rfjs.tpptwpqd.cc/2110/MDSE00002350/play.m3u8"
-   }
-   ]
-   },
-   {
-   "vod_name": "bill 老师",
-   "vod_id": "8",
-   "template_id": "8",
-   "resource_type": "long",
-   "sort": 100,
-   "movies": [
-   {
-   "play_num": 727,
-   "duration": "2:01",
-   "require": -1,
-   "title": "HongKongDoll 玩偶姐姐 森林 第一集",
-   "hash": "UJK549J1E71F",
-   "cover": "https://rfjs.tpptwpqd.cc/2211/UJK549J1E71F/cover",
-   "m3u8": "https://rfjs.tpptwpqd.cc/2110/UJK549J1E71F/play.m3u8"
-   },
-   {
-   "play_num": 805,
-   "duration": "4:01",
-   "require": -1,
-   "title": "HongKongDoll 青蛇#cosplay#小青#白蛇传#许仙",
-   "hash": "MDSE00002350",
-   "cover": "https://rfjs.tpptwpqd.cc/2211/MDSE00002350/cover",
-   "m3u8": "https://rfjs.tpptwpqd.cc/2110/MDSE00002350/play.m3u8"
-   },
-   {
-   "play_num": 244,
-   "duration": "5:01",
-   "require": 0,
-   "title": "HongKongDoll 玩偶姐姐 森林 第一集",
-   "hash": "UJK549J1E71F",
-   "cover": "https://rfjs.tpptwpqd.cc/2211/UJK549J1E71F/cover",
-   "m3u8": "https://rfjs.tpptwpqd.cc/2110/UJK549J1E71F/play.m3u8"
-   },
-   {
-   "play_num": 623,
-   "duration": "3:01",
-   "require": 3,
-   "title": "HongKongDoll 青蛇#cosplay#小青#白蛇传#许仙",
-   "hash": "MDSE00002350",
-   "cover": "https://rfjs.tpptwpqd.cc/2211/MDSE00002350/cover",
-   "m3u8": "https://rfjs.tpptwpqd.cc/2110/MDSE00002350/play.m3u8"
-   },
-   {
-   "play_num": 503,
-   "duration": "5:01",
-   "require": 4,
-   "title": "HongKongDoll 玩偶姐姐 森林 第一集",
-   "hash": "UJK549J1E71F",
-   "cover": "https://rfjs.tpptwpqd.cc/2211/UJK549J1E71F/cover",
-   "m3u8": "https://rfjs.tpptwpqd.cc/2110/UJK549J1E71F/play.m3u8"
-   },
-   {
-   "play_num": 662,
-   "duration": "3:01",
-   "require": 0,
-   "title": "HongKongDoll 青蛇#cosplay#小青#白蛇传#许仙",
-   "hash": "MDSE00002350",
-   "cover": "https://rfjs.tpptwpqd.cc/2211/MDSE00002350/cover",
-   "m3u8": "https://rfjs.tpptwpqd.cc/2110/MDSE00002350/play.m3u8"
-   }
-   ]
-   },
-   {
-   "vod_name": "mody 老师",
-   "vod_id": "9",
-   "template_id": "9",
-   "resource_type": "long",
-   "sort": 100,
-   "movies": [
-   {
-   "play_num": 980,
-   "duration": "5:01",
-   "require": 4,
-   "title": "HongKongDoll 玩偶姐姐 森林 第一集",
-   "hash": "UJK549J1E71F",
-   "cover": "https://rfjs.tpptwpqd.cc/2211/UJK549J1E71F/cover",
-   "m3u8": "https://rfjs.tpptwpqd.cc/2110/UJK549J1E71F/play.m3u8"
-   },
-   {
-   "play_num": 826,
-   "duration": "3:01",
-   "require": -1,
-   "title": "HongKongDoll 青蛇#cosplay#小青#白蛇传#许仙",
-   "hash": "MDSE00002350",
-   "cover": "https://rfjs.tpptwpqd.cc/2211/MDSE00002350/cover",
-   "m3u8": "https://rfjs.tpptwpqd.cc/2110/MDSE00002350/play.m3u8"
-   },
-   {
-   "play_num": 344,
-   "duration": "5:01",
-   "require": 1,
-   "title": "HongKongDoll 玩偶姐姐 森林 第一集",
-   "hash": "UJK549J1E71F",
-   "cover": "https://rfjs.tpptwpqd.cc/2211/UJK549J1E71F/cover",
-   "m3u8": "https://rfjs.tpptwpqd.cc/2110/UJK549J1E71F/play.m3u8"
-   },
-   {
-   "play_num": 826,
-   "duration": "3:01",
-   "require": 2,
-   "title": "HongKongDoll 青蛇#cosplay#小青#白蛇传#许仙",
-   "hash": "MDSE00002350",
-   "cover": "https://rfjs.tpptwpqd.cc/2211/MDSE00002350/cover",
-   "m3u8": "https://rfjs.tpptwpqd.cc/2110/MDSE00002350/play.m3u8"
-   },
-   {
-   "play_num": 923,
-   "duration": "5:01",
-   "require": 0,
-   "title": "HongKongDoll 玩偶姐姐 森林 第一集",
-   "hash": "UJK549J1E71F",
-   "cover": "https://rfjs.tpptwpqd.cc/2211/UJK549J1E71F/cover",
-   "m3u8": "https://rfjs.tpptwpqd.cc/2110/UJK549J1E71F/play.m3u8"
-   },
-   {
-   "play_num": 487,
-   "duration": "3:01",
-   "require": 2,
-   "title": "HongKongDoll 青蛇#cosplay#小青#白蛇传#许仙",
-   "hash": "MDSE00002350",
-   "cover": "https://rfjs.tpptwpqd.cc/2211/MDSE00002350/cover",
-   "m3u8": "https://rfjs.tpptwpqd.cc/2110/MDSE00002350/play.m3u8"
-   }
-   ]
-   },
-   {
-   "vod_name": "bill 老师",
-   "vod_id": "10",
-   "template_id": "10",
-   "resource_type": "long",
-   "sort": 100,
-   "movies": [
-   {
-   "play_num": 186,
-   "duration": "2:01",
-   "require": 1,
-   "title": "HongKongDoll 玩偶姐姐 森林 第一集",
-   "hash": "UJK549J1E71F",
-   "cover": "https://rfjs.tpptwpqd.cc/2211/UJK549J1E71F/cover",
-   "m3u8": "https://rfjs.tpptwpqd.cc/2110/UJK549J1E71F/play.m3u8"
-   },
-   {
-   "play_num": 113,
-   "duration": "4:01",
-   "require": 1,
-   "title": "HongKongDoll 青蛇#cosplay#小青#白蛇传#许仙",
-   "hash": "MDSE00002350",
-   "cover": "https://rfjs.tpptwpqd.cc/2211/MDSE00002350/cover",
-   "m3u8": "https://rfjs.tpptwpqd.cc/2110/MDSE00002350/play.m3u8"
-   },
-   {
-   "play_num": 639,
-   "duration": "5:01",
-   "require": -1,
-   "title": "HongKongDoll 玩偶姐姐 森林 第一集",
-   "hash": "UJK549J1E71F",
-   "cover": "https://rfjs.tpptwpqd.cc/2211/UJK549J1E71F/cover",
-   "m3u8": "https://rfjs.tpptwpqd.cc/2110/UJK549J1E71F/play.m3u8"
-   },
-   {
-   "play_num": 192,
-   "duration": "3:01",
-   "require": 1,
-   "title": "HongKongDoll 青蛇#cosplay#小青#白蛇传#许仙",
-   "hash": "MDSE00002350",
-   "cover": "https://rfjs.tpptwpqd.cc/2211/MDSE00002350/cover",
-   "m3u8": "https://rfjs.tpptwpqd.cc/2110/MDSE00002350/play.m3u8"
-   },
-   {
-   "play_num": 653,
-   "duration": "5:01",
-   "require": 1,
-   "title": "HongKongDoll 玩偶姐姐 森林 第一集",
-   "hash": "UJK549J1E71F",
-   "cover": "https://rfjs.tpptwpqd.cc/2211/UJK549J1E71F/cover",
-   "m3u8": "https://rfjs.tpptwpqd.cc/2110/UJK549J1E71F/play.m3u8"
-   },
-   {
-   "play_num": 387,
-   "duration": "3:01",
-   "require": 4,
-   "title": "HongKongDoll 青蛇#cosplay#小青#白蛇传#许仙",
-   "hash": "MDSE00002350",
-   "cover": "https://rfjs.tpptwpqd.cc/2211/MDSE00002350/cover",
-   "m3u8": "https://rfjs.tpptwpqd.cc/2110/MDSE00002350/play.m3u8"
-   }
-   ]
-   },
-   {
-   "vod_name": "mody 老师",
-   "vod_id": "11",
-   "template_id": "11",
-   "resource_type": "long",
-   "sort": 100,
-   "movies": [
-   {
-   "play_num": 142,
-   "duration": "5:01",
-   "require": 1,
-   "title": "HongKongDoll 玩偶姐姐 森林 第一集",
-   "hash": "UJK549J1E71F",
-   "cover": "https://rfjs.tpptwpqd.cc/2211/UJK549J1E71F/cover",
-   "m3u8": "https://rfjs.tpptwpqd.cc/2110/UJK549J1E71F/play.m3u8"
-   },
-   {
-   "play_num": 873,
-   "duration": "3:01",
-   "require": -1,
-   "title": "HongKongDoll 青蛇#cosplay#小青#白蛇传#许仙",
-   "hash": "MDSE00002350",
-   "cover": "https://rfjs.tpptwpqd.cc/2211/MDSE00002350/cover",
-   "m3u8": "https://rfjs.tpptwpqd.cc/2110/MDSE00002350/play.m3u8"
-   },
-   {
-   "play_num": 524,
-   "duration": "5:01",
-   "require": 2,
-   "title": "HongKongDoll 玩偶姐姐 森林 第一集",
-   "hash": "UJK549J1E71F",
-   "cover": "https://rfjs.tpptwpqd.cc/2211/UJK549J1E71F/cover",
-   "m3u8": "https://rfjs.tpptwpqd.cc/2110/UJK549J1E71F/play.m3u8"
-   },
-   {
-   "play_num": 783,
-   "duration": "3:01",
-   "require": 4,
-   "title": "HongKongDoll 青蛇#cosplay#小青#白蛇传#许仙",
-   "hash": "MDSE00002350",
-   "cover": "https://rfjs.tpptwpqd.cc/2211/MDSE00002350/cover",
-   "m3u8": "https://rfjs.tpptwpqd.cc/2110/MDSE00002350/play.m3u8"
-   },
-   {
-   "play_num": 345,
-   "duration": "5:01",
-   "require": 4,
-   "title": "HongKongDoll 玩偶姐姐 森林 第一集",
-   "hash": "UJK549J1E71F",
-   "cover": "https://rfjs.tpptwpqd.cc/2211/UJK549J1E71F/cover",
-   "m3u8": "https://rfjs.tpptwpqd.cc/2110/UJK549J1E71F/play.m3u8"
-   },
-   {
-   "play_num": 842,
-   "duration": "3:01",
-   "require": 3,
-   "title": "HongKongDoll 青蛇#cosplay#小青#白蛇传#许仙",
-   "hash": "MDSE00002350",
-   "cover": "https://rfjs.tpptwpqd.cc/2211/MDSE00002350/cover",
-   "m3u8": "https://rfjs.tpptwpqd.cc/2110/MDSE00002350/play.m3u8"
-   }
-   ]
-   },
-   {
-   "vod_name": "bill 老师",
-   "vod_id": "12",
-   "template_id": "2",
-   "resource_type": "long",
-   "sort": 100,
-   "movies": [
-   {
-   "play_num": 947,
-   "duration": "2:01",
-   "require": 1,
-   "title": "HongKongDoll 玩偶姐姐 森林 第一集",
-   "hash": "UJK549J1E71F",
-   "cover": "https://rfjs.tpptwpqd.cc/2211/UJK549J1E71F/cover",
-   "m3u8": "https://rfjs.tpptwpqd.cc/2110/UJK549J1E71F/play.m3u8"
-   },
-   {
-   "play_num": 263,
-   "duration": "4:01",
-   "require": 0,
-   "title": "HongKongDoll 青蛇#cosplay#小青#白蛇传#许仙",
-   "hash": "MDSE00002350",
-   "cover": "https://rfjs.tpptwpqd.cc/2211/MDSE00002350/cover",
-   "m3u8": "https://rfjs.tpptwpqd.cc/2110/MDSE00002350/play.m3u8"
-   },
-   {
-   "play_num": 627,
-   "duration": "5:01",
-   "require": 5,
-   "title": "HongKongDoll 玩偶姐姐 森林 第一集",
-   "hash": "UJK549J1E71F",
-   "cover": "https://rfjs.tpptwpqd.cc/2211/UJK549J1E71F/cover",
-   "m3u8": "https://rfjs.tpptwpqd.cc/2110/UJK549J1E71F/play.m3u8"
-   },
-   {
-   "play_num": 766,
-   "duration": "3:01",
-   "require": 3,
-   "title": "HongKongDoll 青蛇#cosplay#小青#白蛇传#许仙",
-   "hash": "MDSE00002350",
-   "cover": "https://rfjs.tpptwpqd.cc/2211/MDSE00002350/cover",
-   "m3u8": "https://rfjs.tpptwpqd.cc/2110/MDSE00002350/play.m3u8"
-   },
-   {
-   "play_num": 786,
-   "duration": "5:01",
-   "require": 4,
-   "title": "HongKongDoll 玩偶姐姐 森林 第一集",
-   "hash": "UJK549J1E71F",
-   "cover": "https://rfjs.tpptwpqd.cc/2211/UJK549J1E71F/cover",
-   "m3u8": "https://rfjs.tpptwpqd.cc/2110/UJK549J1E71F/play.m3u8"
-   },
-   {
-   "play_num": 408,
-   "duration": "3:01",
-   "require": 0,
-   "title": "HongKongDoll 青蛇#cosplay#小青#白蛇传#许仙",
-   "hash": "MDSE00002350",
-   "cover": "https://rfjs.tpptwpqd.cc/2211/MDSE00002350/cover",
-   "m3u8": "https://rfjs.tpptwpqd.cc/2110/MDSE00002350/play.m3u8"
-   }
-   ]
-   }
-   ]
-  }

+ 505 - 0
src/components/VideoProcessor.vue

@@ -0,0 +1,505 @@
+<template>
+  <div class="video-processor">
+    <!-- 封面图片 -->
+    <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"
+      controls
+      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"
+    >
+      您的浏览器不支持视频播放
+    </video>
+
+    <!-- 加载状态 -->
+    <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: [];
+}>();
+
+// 响应式数据
+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 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;
+  }
+};
+
+// 处理视频 URL
+const processVideo = async (url: string): Promise<void> => {
+  if (!url) return;
+
+  try {
+    loading.value = true;
+    error.value = "";
+
+    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") {
+        // 处理 M3U8 内容
+        const playlist = processM3u8Content(decryptedData, processedUrl);
+
+        // 创建 Blob URL
+        const blob = new Blob([playlist], { type: "application/x-mpegURL" });
+        processedVideoUrl.value = URL.createObjectURL(blob);
+      } else {
+        processedVideoUrl.value = url;
+      }
+    } else {
+      processedVideoUrl.value = url;
+    }
+
+    // 初始化播放器
+    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");
+};
+
+// 初始化视频播放器
+const initVideoPlayer = async (): Promise<void> => {
+  if (!videoElement.value || !processedVideoUrl.value) return;
+
+  const video = videoElement.value;
+
+  // 销毁现有的 HLS 实例
+  destroyHls();
+
+  // 检查浏览器是否原生支持 HLS
+  if (video.canPlayType("application/vnd.apple.mpegurl")) {
+    video.src = processedVideoUrl.value;
+  } else if (Hls.isSupported()) {
+    hlsInstance.value = new Hls({
+      debug: false,
+      enableWorker: true,
+      lowLatencyMode: true,
+    });
+
+    hlsInstance.value.loadSource(processedVideoUrl.value);
+    hlsInstance.value.attachMedia(video);
+
+    hlsInstance.value.on(Hls.Events.ERROR, (event, data) => {
+      if (data.fatal) {
+        switch (data.type) {
+          case Hls.ErrorTypes.NETWORK_ERROR:
+            if (data.details === "manifestParsingError") {
+              error.value = "视频加载失败";
+              hlsInstance.value?.destroy();
+              hlsInstance.value = null;
+              video.src = processedVideoUrl.value;
+            } else {
+              hlsInstance.value?.startLoad();
+            }
+            break;
+          case Hls.ErrorTypes.MEDIA_ERROR:
+            hlsInstance.value?.recoverMediaError();
+            break;
+          default:
+            error.value = "视频加载失败";
+            hlsInstance.value?.destroy();
+            hlsInstance.value = null;
+            video.src = processedVideoUrl.value;
+            break;
+        }
+      }
+    });
+  } else {
+    video.src = processedVideoUrl.value;
+  }
+};
+
+// 销毁 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");
+
+  // 重新处理
+  if (props.coverUrl) {
+    processCover(props.coverUrl);
+  }
+  if (props.m3u8Url) {
+    processVideo(props.m3u8Url);
+  }
+};
+
+// 事件处理
+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 => {};
+
+const onVideoLoadedData = (): void => {};
+
+const onVideoCanPlay = (): void => {
+  if (props.autoPlay && videoElement.value) {
+    videoElement.value.play().catch(console.warn);
+  }
+};
+
+const onVideoError = (): void => {
+  error.value = "视频播放失败";
+  emit("error", error.value);
+};
+
+// 监听 props 变化
+watch(
+  () => props.coverUrl,
+  (newUrl) => {
+    if (newUrl) {
+      processCover(newUrl);
+    }
+  },
+  { immediate: true }
+);
+
+watch(
+  () => props.m3u8Url,
+  (newUrl) => {
+    if (newUrl) {
+      processVideo(newUrl);
+    }
+  },
+  { immediate: true }
+);
+
+// 组件卸载时清理资源
+onUnmounted(() => {
+  destroyHls();
+  if (processedCoverUrl.value && processedCoverUrl.value.startsWith("blob:")) {
+    URL.revokeObjectURL(processedCoverUrl.value);
+  }
+  if (processedVideoUrl.value && processedVideoUrl.value.startsWith("blob:")) {
+    URL.revokeObjectURL(processedVideoUrl.value);
+  }
+});
+
+// 暴露方法给父组件
+defineExpose({
+  retry,
+  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%;
+}
+
+.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>

+ 9 - 31
src/services/api.ts

@@ -175,6 +175,15 @@ export const searchVideoByTags = async (
   return videoRequest(`/media/${plat_id}/search`, formData);
 };
 
+// 视频详情接口
+export const getVideoDetail = async (
+  device: string,
+  resource_id: string
+): Promise<any> => {
+  const formData = createVideoFormData(device, 1, 20, { resource_id });
+  return videoRequest(`/media/${user_id}/movie/play`, formData);
+};
+
 // 视频顶部标签 查询接口
 export const getVideoMenu = async (
   device: string,
@@ -197,35 +206,4 @@ export const getVodList = async (
   return videoRequest(`/media/${plat_id}/menu/vods`, formData);
 };
 
-/**
- * ===================== 视频播放相关接口 =====================
- */
-
-// 记录视频播放统计(模拟接口,实际项目中需要后端支持)
-export const recordVideoPlay = async (
-  videoId: string,
-  action: "play" | "pause" | "ended" | "timeupdate",
-  currentTime?: number,
-  duration?: number
-): Promise<any> => {
-  // 这里只是模拟,实际项目中需要调用后端API
-  console.log("记录视频播放统计:", {
-    videoId,
-    action,
-    currentTime,
-    duration,
-  });
-
-  // 模拟API调用
-  return new Promise((resolve) => {
-    setTimeout(() => {
-      resolve({
-        status: 0,
-        msg: "success",
-        data: { recorded: true },
-      });
-    }, 100);
-  });
-};
-
 export default api;

+ 7 - 12
src/views/Home.vue

@@ -133,11 +133,10 @@
         class="group rounded-2xl overflow-hidden bg-white/5 border border-white/10 cursor-pointer hover:bg-white/10 transition"
       >
         <div class="aspect-[9/12] relative">
-          <img
-            :src="video.cover"
+          <VideoProcessor
+            :cover-url="'https://sftp.69mediatest.com/resources/2412/L4PBMJ1MKVE8/cover'"
             :alt="video.name"
-            class="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300"
-            @error="handleImageError"
+            cover-class="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300"
           />
           <div
             class="absolute bottom-2 right-2 bg-black/70 text-white text-xs px-2 py-1 rounded"
@@ -431,6 +430,7 @@ import {
   searchVideoByTags,
   searchVideoByKeyword,
 } from "@/services/api";
+import VideoProcessor from "@/components/VideoProcessor.vue";
 
 // 路由
 const router = useRouter();
@@ -486,12 +486,7 @@ const formatDuration = (duration: string | number): string => {
   return `${minutes}:${remainingSeconds.toString().padStart(2, "0")}`;
 };
 
-// 处理图片加载错误
-const handleImageError = (event: Event) => {
-  const img = event.target as HTMLImageElement;
-  img.src =
-    "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAwIiBoZWlnaHQ9IjIwMCIgdmlld0JveD0iMCAwIDIwMCAyMDAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxyZWN0IHdpZHRoPSIyMDAiIGhlaWdodD0iMjAwIiBmaWxsPSIjMzMzIi8+CjxwYXRoIGQ9Ik0xMDAgNzVMMTI1IDEwMEgxMDBWMTI1SDc1TDEwMCAxMDBaIiBmaWxsPSIjNjY2Ii8+Cjwvc3ZnPgo=";
-};
+// 图片加载错误处理已移至 VideoProcessor 组件
 
 // 跳转到指定页面
 const jumpToPageHandler = () => {
@@ -628,8 +623,8 @@ const playVideo = (video: any) => {
     params: { id: video.id },
     query: {
       name: video.name,
-      cover: video.cover,
-      m3u8: video.m3u8,
+      cover: "https://sftp.69mediatest.com/resources/2412/L4PBMJ1MKVE8/cover",
+      m3u8: "https://sftp.69mediatest.com/resources/2412/L4PBMJ1MKVE8/play",
       duration: video.duration,
       view: video.view,
       like: video.like,

+ 39 - 303
src/views/VideoPlayer.vue

@@ -62,50 +62,20 @@
     <!-- 视频播放器区域 -->
     <div class="relative rounded-2xl overflow-hidden bg-black">
       <div class="aspect-video video-container">
-        <video
-          ref="videoPlayer"
-          :poster="videoInfo.cover"
-          class="w-full h-full object-contain"
-          controls
-          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"
-        >
-          您的浏览器不支持视频播放
-        </video>
-
-        <!-- 视频错误提示 -->
-        <div
-          v-if="videoError"
-          class="absolute inset-0 flex items-center justify-center bg-black/80 text-white"
-        >
-          <div class="text-center p-6">
-            <div class="text-4xl mb-4">⚠️</div>
-            <h3 class="text-lg font-semibold mb-2">视频加载失败</h3>
-            <p class="text-sm text-white/70 mb-4">{{ videoError }}</p>
-            <button
-              v-if="showRetryButton"
-              @click="retryVideoLoad"
-              :disabled="!canRetry"
-              class="px-4 py-2 rounded-lg transition"
-              :class="
-                canRetry
-                  ? 'bg-emerald-500 text-white hover:bg-emerald-600'
-                  : 'bg-gray-500 text-gray-300 cursor-not-allowed'
-              "
-            >
-              重试
-            </button>
-          </div>
-        </div>
+        <VideoProcessor
+          :cover-url="videoInfo.cover"
+          :m3u8-url="videoInfo.m3u8"
+          :alt="videoInfo.name"
+          :auto-play="false"
+          :hide-error="false"
+          :enable-retry="true"
+          cover-class="w-full h-full object-cover"
+          video-class="w-full h-full object-contain"
+          @cover-loaded="onCoverLoaded"
+          @video-loaded="onVideoLoaded"
+          @error="onVideoProcessorError"
+          @retry="onVideoProcessorRetry"
+        />
       </div>
     </div>
 
@@ -202,11 +172,10 @@
             class="group rounded-xl overflow-hidden bg-white/5 border border-white/10 cursor-pointer hover:bg-white/10 transition"
           >
             <div class="aspect-[9/12] relative">
-              <img
-                :src="video.cover"
+              <VideoProcessor
+                :cover-url="'https://sftp.69mediatest.com/resources/2412/L4PBMJ1MKVE8/cover'"
                 :alt="video.name"
-                class="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300"
-                @error="handleImageError"
+                cover-class="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300"
               />
               <div
                 class="absolute bottom-1 right-1 bg-black/70 text-white text-xs px-1.5 py-0.5 rounded"
@@ -235,18 +204,15 @@
 import { ref, onMounted, onUnmounted, computed } from "vue";
 import { useRoute, useRouter } from "vue-router";
 import { searchVideoByTags } from "@/services/api";
-import Hls from "hls.js";
+import VideoProcessor from "@/components/VideoProcessor.vue";
 
 // 路由相关
 const route = useRoute();
 const router = useRouter();
 
-// 视频播放器引用
+// 视频播放器引用(现在由 VideoProcessor 组件管理)
 const videoPlayer = ref<HTMLVideoElement>();
 
-// HLS实例
-let hls: Hls | null = null;
-
 // 视频信息
 const videoInfo = ref<any>({
   id: "",
@@ -265,12 +231,6 @@ const relatedVideos = ref<any[]>([]);
 
 // 状态管理
 const showShareModal = ref(false);
-const videoError = ref<string>("");
-const retryCount = ref(0);
-const maxRetries = 3;
-const lastRetryTime = ref(0);
-const retryCooldown = 3000; // 3秒冷却时间
-const forceUpdate = ref(0); // 强制更新触发器
 
 // 生成设备标识
 const generateMacAddress = (): string => {
@@ -286,18 +246,22 @@ const generateMacAddress = (): string => {
 
 const device = generateMacAddress();
 
-// 计算属性
-const canRetry = computed(() => {
-  forceUpdate.value; // 依赖forceUpdate来触发重新计算
-  const now = Date.now();
-  const isCooldownActive = now - lastRetryTime.value < retryCooldown;
-  const hasRetriesLeft = retryCount.value < maxRetries;
-  return !isCooldownActive && hasRetriesLeft;
-});
+// VideoProcessor 事件处理
+const onCoverLoaded = (url: string) => {
+  // 封面加载完成
+};
 
-const showRetryButton = computed(() => {
-  return retryCount.value < maxRetries;
-});
+const onVideoLoaded = (url: string) => {
+  // 视频加载完成
+};
+
+const onVideoProcessorError = (error: string) => {
+  console.error("VideoProcessor 错误:", error);
+};
+
+const onVideoProcessorRetry = () => {
+  // VideoProcessor 重试
+};
 
 // 格式化时长
 const formatDuration = (duration: string | number): string => {
@@ -316,218 +280,9 @@ const formatNumber = (num: string | number): string => {
   return n.toString();
 };
 
-// 处理图片加载错误
-const handleImageError = (event: Event) => {
-  const img = event.target as HTMLImageElement;
-  img.src =
-    "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAwIiBoZWlnaHQ9IjIwMCIgdmlld0JveD0iMCAwIDIwMCAyMDAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxyZWN0IHdpZHRoPSIyMDAiIGhlaWdodD0iMjAwIiBmaWxsPSIjMzMzIi8+CjxwYXRoIGQ9Ik0xMDAgNzVMMTI1IDEwMEgxMDBWMTI1SDc1TDEwMCAxMDBaIiBmaWxsPSIjNjY2Ii8+Cjwvc3ZnPgo=";
-};
-
-// 视频播放器事件处理
-const onVideoLoadStart = () => {
-  console.log("视频开始加载...");
-};
-
-const onVideoLoadedData = () => {
-  console.log("视频数据加载完成");
-};
-
-const onVideoCanPlay = () => {
-  console.log("视频可以播放");
-};
-
-const onVideoError = (event: Event) => {
-  const video = event.target as HTMLVideoElement;
-  console.error("视频播放错误:", video.error);
-  console.error("错误详情:", {
-    code: video.error?.code,
-    message: video.error?.message,
-    networkState: video.networkState,
-    readyState: video.readyState,
-  });
-
-  // 设置统一的错误信息
-  videoError.value = "视频加载失败";
-};
-
-// 重试视频加载
-const retryVideoLoad = () => {
-  const now = Date.now();
-
-  // 检查是否在冷却时间内
-  if (now - lastRetryTime.value < retryCooldown) {
-    const remainingTime = Math.ceil(
-      (retryCooldown - (now - lastRetryTime.value)) / 1000
-    );
-    console.log(`重试冷却中,还需等待 ${remainingTime} 秒`);
-    return;
-  }
-
-  // 检查是否超过最大重试次数
-  if (retryCount.value >= maxRetries) {
-    console.log("已达到最大重试次数");
-    return;
-  }
-
-  // 更新重试状态
-  retryCount.value++;
-  lastRetryTime.value = now;
-
-  console.log(`第 ${retryCount.value} 次重试`);
-
-  videoError.value = "";
-  destroyHls();
-
-  // 延迟重新初始化
-  setTimeout(() => {
-    initHlsPlayer();
-  }, 500);
-};
-
-// 初始化HLS播放器
-const initHlsPlayer = () => {
-  if (!videoPlayer.value) return;
+// 图片加载错误处理已移至 VideoProcessor 组件
 
-  const video = videoPlayer.value;
-  const videoSrc = videoInfo.value.m3u8;
-
-  if (!videoSrc) {
-    console.error("没有视频源地址");
-    return;
-  }
-
-  // console.log("初始化HLS播放器,视频源:", videoSrc);
-  console.log("初始化HLS播放器");
-
-  // 检查浏览器是否原生支持HLS
-  if (video.canPlayType("application/vnd.apple.mpegurl")) {
-    // Safari原生支持HLS
-    console.log("使用原生HLS支持");
-    video.src = videoSrc;
-  } else if (Hls.isSupported()) {
-    // 使用HLS.js
-    console.log("使用HLS.js支持");
-
-    // 先检测URL是否为HLS格式
-    detectVideoFormat(videoSrc)
-      .then((format) => {
-        if (format === "hls") {
-          initHlsJsPlayer(videoSrc, video);
-        } else {
-          console.log("检测到非HLS格式,尝试直接播放");
-          video.src = videoSrc;
-        }
-      })
-      .catch((error) => {
-        console.error("格式检测失败,尝试直接播放:", error);
-        video.src = videoSrc;
-      });
-  } else {
-    console.error("浏览器不支持HLS播放");
-    video.src = videoSrc;
-  }
-};
-
-// 检测视频格式
-const detectVideoFormat = async (url: string): Promise<"hls" | "direct"> => {
-  try {
-    const response = await fetch(url, {
-      method: "HEAD",
-      mode: "cors",
-    });
-
-    const contentType = response.headers.get("content-type") || "";
-    console.log("Content-Type:", contentType);
-
-    // 检查是否为HLS格式
-    if (
-      contentType.includes("application/vnd.apple.mpegurl") ||
-      contentType.includes("application/x-mpegURL") ||
-      url.includes(".m3u8")
-    ) {
-      return "hls";
-    }
-
-    // 检查是否为直接视频文件
-    if (contentType.includes("video/")) {
-      return "direct";
-    }
-
-    // 如果无法确定,尝试获取前几个字节来判断
-    const textResponse = await fetch(url, {
-      method: "GET",
-      mode: "cors",
-      headers: { Range: "bytes=0-1023" },
-    });
-    const text = await textResponse.text();
-
-    if (text.startsWith("#EXTM3U")) {
-      return "hls";
-    }
-
-    return "direct";
-  } catch (error) {
-    console.error("格式检测失败:", error);
-    return "direct";
-  }
-};
-
-// 初始化HLS.js播放器
-const initHlsJsPlayer = (videoSrc: string, video: HTMLVideoElement) => {
-  hls = new Hls({
-    debug: false, // 关闭调试日志
-    enableWorker: true,
-    lowLatencyMode: true,
-  });
-
-  hls.loadSource(videoSrc);
-  hls.attachMedia(video);
-
-  hls.on(Hls.Events.MANIFEST_PARSED, () => {
-    console.log("HLS清单解析完成,可以播放");
-  });
-
-  hls.on(Hls.Events.ERROR, (event, data) => {
-    console.error("HLS错误:", data);
-    if (data.fatal) {
-      switch (data.type) {
-        case Hls.ErrorTypes.NETWORK_ERROR:
-          console.error("网络错误,尝试恢复...");
-          if (data.details === "manifestParsingError") {
-            videoError.value = "视频加载失败";
-            // 销毁HLS实例,尝试直接播放
-            hls?.destroy();
-            hls = null;
-            video.src = videoSrc;
-          } else {
-            hls?.startLoad();
-          }
-          break;
-        case Hls.ErrorTypes.MEDIA_ERROR:
-          console.error("媒体错误,尝试恢复...");
-          hls?.recoverMediaError();
-          break;
-        default:
-          console.error("无法恢复的错误,尝试直接播放");
-          videoError.value = "视频加载失败";
-          // 销毁HLS实例,尝试直接播放
-          hls?.destroy();
-          hls = null;
-          video.src = videoSrc;
-          break;
-      }
-    }
-  });
-};
-
-// 清理HLS实例
-const destroyHls = () => {
-  if (hls) {
-    console.log("清理HLS实例");
-    hls.destroy();
-    hls = null;
-  }
-};
+// 视频播放器事件处理已移至 VideoProcessor 组件
 
 // 返回上一页
 const goBack = () => {
@@ -580,8 +335,8 @@ const loadVideoInfo = () => {
     videoInfo.value = {
       id: videoId,
       name: videoData.name || `视频 ${videoId}`,
-      cover: videoData.cover || "",
-      m3u8: "https://test-streams.mux.dev/x36xhzz/x36xhzz.m3u8",
+      cover: "https://sftp.69mediatest.com/resources/2412/L4PBMJ1MKVE8/cover",
+      m3u8: "https://sftp.69mediatest.com/resources/2412/L4PBMJ1MKVE8/play",
       duration: videoData.duration || 0,
       view: videoData.view || 0,
       like: videoData.like || 0,
@@ -655,28 +410,9 @@ const loadRelatedVideos = async () => {
 onMounted(() => {
   loadVideoInfo();
   loadRelatedVideos();
-
-  // 延迟初始化HLS播放器,确保DOM已渲染
-  setTimeout(() => {
-    initHlsPlayer();
-  }, 100);
-
-  // 启动定时器更新按钮状态
-  const timer = setInterval(() => {
-    if (videoError.value && retryCount.value < maxRetries) {
-      forceUpdate.value++; // 触发计算属性重新计算
-    }
-  }, 1000);
-
-  // 组件卸载时清除定时器
-  onUnmounted(() => {
-    clearInterval(timer);
-  });
 });
 
-onUnmounted(() => {
-  destroyHls();
-});
+// 组件卸载时的清理工作已移至 VideoProcessor 组件
 </script>
 
 <style scoped>