视频播放页的技术方案分享
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.js | Express API 服务,Playwright 截获视频直链 |
index.js | CLI 调试工具(--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.yaml、page.html、i18n/*.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 后台配置
- 重启 Halo
主题设置 → 视频放映室添加视频页面 → 新建页面,slug 设为videos,发布- 访问
/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 加载)