16
0

视频播放页的技术方案分享

2026-05-04
2026-05-04
视频播放页的技术方案分享

Halo Fuwari 主题 — 视频放映室 技术方案

概述

首先非常感谢Fuwari主题项目的人员,这里是他们的github代码:https://github.com/jiewenhuang/halo-theme-fuwari
我的方案基于他们的主题方案架构,你在本网页的“追番”页面可以实际体验到,我在此分享我的技术方案,欢迎讨论!

为 Halo 2.x + Fuwari 主题新增「视频放映室」功能页面,支持两种视频来源:

  • Bilibili:通过官方 iframe 嵌入,支持多P选集
  • 资源站解析:通过自建 API + Playwright 无头浏览器截获真实播放地址,DPlayer 直播

用户在 主题设置 → 视频放映室 中配置视频列表,前台自动展示卡片列表和播放器。


架构总览

┌─────────────┐     ┌──────────────────────────────────────────┐
│  用户浏览器  │     │              你的服务器                    │
│             │     │                                          │
│  DPlayer    │────→│  Nginx (443)                             │
│  B站 iframe │     │    ├── /api/getVideo    → Node.js:3000   │
│             │     │    ├── /bili-api/       → api.bilibili.com│
│             │←────│    ├── /video-proxy/   → 视频CDN(可选)   │
│             │     │    └── /*              → Halo :8090      │
└─────────────┘     │                                          │
                    │  Node.js (3000)                          │
                    │    └── Playwright 无头浏览器               │
                    │         └── 访问资源站 → 截获 .mp4 直链    │
                    │                                          │
                    │  Halo (8090)                             │
                    │    └── 主题模板 page.html (slug: videos)  │
                    └──────────────────────────────────────────┘

文件清单

主题文件(theme-fuwari)

文件作用
settings.yaml新增 video 设置组,定义视频来源和元数据
templates/page.html通过 slug 判断渲染视频组件或普通页面
i18n/zh_CN.properties国际化
i18n/default.properties国际化(英文)
i18n/zh_TW.properties国际化(繁体)

解析服务(独立部署)

文件作用
server.jsExpress API 服务,Playwright 截获视频直链
index.jsCLI 调试工具(--debug 查看全量请求)

一、主题配置(settings.yaml)

spec.forms 末尾新增 video 设置组:

- group: video
  label: 视频放映室
  formSchema:
    - $formkit: text
      name: page_title
      label: 页面标题
      value: 视频放映室
    - $formkit: text
      name: api_base
      label: 解析接口地址
      value: "/api/getVideo"
      help: "本地解析服务的 API 路径(仅资源站类型使用)"
    - $formkit: array
      name: videos
      label: 视频列表
      value: []
      children:
        - $formkit: select
          name: type
          label: 视频来源
          value: bilibili
          options:
            - label: Bilibili
              value: bilibili
            - label: 资源站解析
              value: resolve
        - $formkit: text
          name: title
          label: 标题
          validation: "required"
        - $formkit: attachment
          name: cover
          label: 封面图
        - $formkit: text
          name: bvid
          label: BV号
          if: "$value.type === 'bilibili'"
          validation: "required"
          help: "如 BV1GJ411x7h7"
        - $formkit: text
          name: targetUrl
          label: 资源站播放页地址
          if: "$value.type === 'resolve'"
          validation: "required"
          help: "第三方影视网站播放页完整URL"
        - $formkit: text
          name: episodes
          label: 分集地址
          if: "$value.type === 'resolve'"
          help: "用 | 分隔,格式:集名,URL"

关键字段说明:

  • type:选择视频来源,控制显示哪些字段
  • bvid:B站视频 BV 号,仅 Bilibili 类型显示
  • targetUrl:资源站播放页 URL,仅资源站类型显示
  • episodes:分集配置,格式 第1集,https://xxx/1|第2集,https://xxx/2

二、前端模板(page.html)

路由机制

Halo 主题模板不会自动注册路由。通过在 page.html 中判断页面 slug:

<th:block th:if="${singlePage.spec.slug == 'videos'}">
  <!-- 视频放映室组件 -->
</th:block>

<th:block th:if="${singlePage.spec.slug != 'videos'}">
  <!-- 普通页面内容 -->
</th:block>

部署步骤: 后台新建页面,slug 设为 videos,发布即可。

页面结构

┌─────────────────────────────────────────┐
│  页面标题(来自 theme.config.video.page_title)  │
│  "X 个视频"                              │
├─────────────────────────────────────────┤
│  [列表视图]                              │
│  ┌──────┐  ┌──────┐  ┌──────┐           │
│  │ 封面  │  │ 封面  │  │ 封面  │           │
│  │ 标题  │  │ 标题  │  │ 标题  │           │
│  │ B站   │  │ 解析  │  │ B站   │           │
│  └──────┘  └──────┘  └──────┘           │
├─────────────────────────────────────────┤
│  [播放器视图] 点击卡片后切换              │
│  ← 返回列表                             │
│  视频标题                               │
│  ┌─────────────────────────────────┐     │
│  │                                 │     │
│  │   B站 iframe / DPlayer 播放器    │     │
│  │   (16:9 响应式)                 │     │
│  │                                 │     │
│  └─────────────────────────────────┘     │
│  [选集按钮列表](多P/多集时显示)          │
└─────────────────────────────────────────┘

Bilibili 类型流程

1. 页面加载 → fetch /bili-api/x/web-interface/view?bvid=XXX
2. API 返回标题、封面、时长、播放量、多P列表
3. 渲染卡片(封面 + 标题 + 统计信息)
4. 点击卡片 → 嵌入 iframe: //player.bilibili.com/player.html?bvid=XXX
5. 多P视频 → 自动渲染选集按钮

依赖: Nginx 反向代理 /bili-api/api.bilibili.com

location /bili-api/ {
    proxy_pass https://api.bilibili.com/;
    proxy_set_header Host api.bilibili.com;
    proxy_set_header Referer "https://www.bilibili.com";
    proxy_set_header User-Agent "Mozilla/5.0";
    proxy_ssl_server_name on;
}

资源站类型流程

1. 页面加载 → 使用 settings 中配置的封面和标题渲染卡片
2. 点击卡片 → fetch /api/getVideo?targetUrl=资源站URL
3. API 返回 { playUrl: "MP4直链" }
4. DPlayer 播放 MP4 直链
5. 多集视频 → 渲染选集按钮,切换时重新请求 API

URL 路由: ?idx=N 定位视频,history.pushState 支持前进后退。


三、解析服务(server.js)

技术栈

  • Node.js + Express
  • Playwright (Chromium)
  • CORS 中间件
npm install express cors playwright
npx playwright install chromium
npx playwright install-deps    # 服务器需安装系统依赖

API 端点

方法路径说明
GET/api/getVideo?targetUrl=<URL>解析视频直链
GET/api/health健康检查

返回格式:

{
  "code": 0,
  "msg": "success",
  "data": {
    "playUrl": "https://videooc.tc.qq.com/...mp4?..."
  }
}

核心抓取逻辑

async function grabVideoUrl(targetUrl) {
  let browser = null;
  try {
    browser = await chromium.launch({ headless: true });

    const context = await browser.newContext({
      userAgent: FAKE_USER_AGENT,  // 伪装 Chrome
    });

    const page = await context.newPage();

    // 1. 屏蔽无关资源加速加载
    await page.route('**/*', (route) => {
      const type = route.request().resourceType();
      if (['image', 'stylesheet', 'font'].includes(type)) {
        route.abort();  // 只保留 HTML 和 JS
      } else {
        route.continue();
      }
    });

    // 2. 拦截网络请求,截获视频直链
    const videoUrlPromise = new Promise((resolve) => {
      context.on('request', (request) => {
        const url = request.url();
        const type = request.resourceType();
        if (isTargetVideo(url, type)) {
          resolve(url);
        }
      });
    });

    // 3. 加载页面并触发播放
    await page.goto(targetUrl, { waitUntil: 'domcontentloaded' });
    await page.waitForLoadState('networkidle');
    await tryClickPlay(page);  // 自动点击播放按钮

    // 4. 等待截获结果(超时 15s)
    const result = await Promise.race([
      videoUrlPromise,
      timeout(15000),
    ]);

    return result;
  } finally {
    if (browser) await browser.close();  // 防内存泄漏
  }
}

视频匹配规则

function isTargetVideo(url, resourceType) {
  // 1. .mp4/.m3u8/.flv 后缀匹配
  if (/\.(mp4|m3u8|flv)(\?|$)/i.test(url)) return true;

  // 2. Playwright resourceType 为 media 且 URL 够长
  if (resourceType === 'media' && url.length > 60) return true;

  // 3. 已知视频 CDN 域名
  const cdnDomains = [
    'videooc.tc.qq.com', 'groupvideo.photo.qq.com',
    '365yg.com', 'hdslb.com', 'bilivideo.com',
  ];
  for (const domain of cdnDomains) {
    if (url.toLowerCase().includes(domain)) return true;
  }

  return false;
}

四、Nginx 完整配置

server {
    listen 443 ssl;
    server_name your-domain.com;

    # Halo 主站
    location / {
        proxy_pass http://127.0.0.1:8090;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }

    # 视频解析 API → Node.js
    location /api/getVideo {
        proxy_pass http://127.0.0.1:3000/api/getVideo;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_read_timeout 30s;
    }

    # 视频流代理(解决跨域,可选)
    location /video-proxy/ {
        resolver 8.8.8.8;
        proxy_pass $arg_url;
        proxy_ssl_server_name on;
        proxy_set_header Referer "https://www.example.com";
        proxy_hide_header Access-Control-Allow-Origin;
        add_header Access-Control-Allow-Origin * always;
        proxy_force_ranges on;
        proxy_buffering off;
    }

    # B站 API 代理
    location /bili-api/ {
        proxy_pass https://api.bilibili.com/;
        proxy_set_header Host api.bilibili.com;
        proxy_set_header Referer "https://www.bilibili.com";
        proxy_set_header User-Agent "Mozilla/5.0";
        proxy_ssl_server_name on;
    }

    # SSL 证书
    ssl_certificate /path/to/cert.pem;
    ssl_certificate_key /path/to/key.pem;
}

五、部署步骤

1. 部署解析服务

mkdir /opt/video-parser && cd /opt/video-parser
npm init -y
npm install express cors playwright
npx playwright install chromium
npx playwright install-deps chromium

# 上传 server.js

# 使用 PM2 守护进程
pm2 start server.js --name video-parser
pm2 save

2. 部署主题文件

将修改后的 settings.yamlpage.htmli18n/*.properties 上传到 Halo 主题目录:

/path/to/halo/themes/theme-fuwari/
├── settings.yaml
├── templates/page.html
└── i18n/
    ├── default.properties
    ├── zh_CN.properties
    └── zh_TW.properties

3. 配置 Nginx

将上方配置加入 Nginx 的 server {} 块,然后:

nginx -t && nginx -s reload

4. Halo 后台配置

  1. 重启 Halo
  2. 主题设置 → 视频放映室 添加视频
  3. 页面 → 新建页面,slug 设为 videos,发布
  4. 访问 /videos

六、注意事项

问题解决方案
B站封面图 403<img>referrerpolicy="no-referrer"
资源站视频跨域无法播放配置 Nginx /video-proxy/ 代理视频流
视频解析超时增大 server.js 超时时间,确保 Playwright 系统依赖完整
内存泄漏server.js finally 块确保 browser.close(),PM2 限制实例数
时间戳防盗链过期每次播放重新请求 API,拿到最新的直链
主题更新覆盖page.html 需与其他模板同步维护 shell 更新

七、依赖版本

  • Halo >= 2.24.0
  • Node.js >= 18
  • Playwright >= 1.40
  • Express >= 4.18
  • DPlayer >= 1.27 (CDN 加载)

评论