前言
视频聚合应用中,第三方视频源的 HLS 流常携带贴片广告。如何在客户端侧干净地移除广告片段,同时避免误杀正片,是这类去广告方案的核心挑战。本文分享 Yam TV 项目中 HLS 去广告模块的设计演进与技术细节。
问题背景
Yam TV 是一个多视频源聚合搜索与播放平台(NestJS + Vue 3 + Flutter),播放器使用 hls.js(Web)和系统原生播放器(Flutter)。视频源返回的 m3u8 播放列表中可能包含广告分片,表现为重复的 segment 模式或来自特定广告 CDN 的请求。
早期的去广告方案仅依赖启发式算法:分析 m3u8 的结构,根据 segment 的重复模式和时长占比来判断哪些块是广告。这个方案有三个核心痛点:
- 指纹过于粗糙 — 仅取文件名作为指纹,不同路径下的同名文件(如
index.ts)导致大量误匹配 - 无人工干预手段 — 识别到某个 CDN 是广告源后,无法配置规则让其永久被拦截,每次播放都要重新启发
- 误杀无法纠正 — 正片被启发式误判为广告后,用户无法”放行”
解决过程
阶段一:指纹优化
旧指纹只取 URI 的最后一段路径(文件名):
seg001.ts|seg002.ts|...
这导致两个不同目录下的同名 segment 被误判为同一块。优化后使用完整的 hostname + pathname:
cdn.example.com/video/ep1/seg001.ts|cnd.example.com/video/ep1/seg002.ts|...
阶段二:规则引擎设计
在启发式之外,新增一套可持久化、可管理、可同步的规则系统。每条规则包含类型、模式、动作和优先级:
| 字段 | 说明 |
|---|---|
| `type` | `domain` / `path` / `regex` / `exact` |
| `pattern` | 匹配模式字符串 |
| `action` | `block`(拦截)或 `allow`(放行) |
| `priority` | 优先级,越大越先匹配 |
| `disabled` | 是否禁用 |
阶段三:客户端架构
规则存储在服务端 KV 中,通过 REST API 暴露给客户端:
- Web 端:
useHlsAdFilterRulescomposable 在页面初始化时懒加载规则(带 ETag 304 增量更新),通过 hls.js 自定义 Loader 在每次 playlist 响应时调用filterHlsManifestText - Flutter 端:
HlsLocalProxy本地 HTTP 代理在启动时拉取规则,代理重写 m3u8 URL,在代理端执行过滤
阶段四:三优先级判定链
最终判定每一块 segment 是否为广告时,经过三级判断:
逐 block 循环:
├─ 阶段 1: Allow 规则 ← 最高优先级,命中则跳过
├─ 阶段 2: Block 规则 ← 命中则标记删除
└─ 阶段 3: 启发式算法 ← 兜底,受时长保护
时长保护仅作用于启发式:无重复指纹 + 无 block 规则命中 + 总时长 < 60s → 跳过启发式。规则不受时长保护影响,始终执行。
阶段五:管理端与数据管道
后台提供完整的规则管理页面(/admin/hls-ad-filter),支持:
- 单个添加/编辑/启用禁用/删除
- JSON 文件导入导出
- 远程 URL 订阅(服务端 fetch,防 SSRF)
- 按
type + pattern自动去重
最终方案
完整的数据流如下:
管理员配置规则
↓
KV Storage → `GET /api/hls-ad-filter/rules` (带 ETag)
↓
客户端缓存规则 ────────────┐
↓ ↓
Web (hls.js Loader) Flutter (本地代理)
↓ ↓
filterHlsManifestText filterHlsManifestText
↓ ↓
1. Allow 规则检查 1. Allow 规则检查
2. Block 规则检查 2. Block 规则检查
3. 启发式判定 3. learnedAdKeys 检查
4. 启发式判定
↓ ↓
输出过滤后的 m3u8 输出过滤后的 m3u8
Web 端独有特性
- 悬停视频预览也应用规则(
useVodCardHoverPreview) - Artplayer 设置面板中可切换去广告开关
- Console 日志输出每条判定结果,便于调试
Flutter 端独有特性
learnedAdKeys:启发式丢弃的 segment 自动持久化到本地(host|path,FIFO ~2800 条)- 本地 HTTP 代理保证系统播放器也能享受过滤
- 嵌套 m3u8 递归过过滤
规则匹配示例
配置一条域名规则 type=domain, pattern=doubleclick.net, action=block:
| segment URI | 归一化目标 | 匹配结果 |
|---|---|---|
| `https://doubleclick.net/ad/seg.ts` | `doubleclick.net/ad/seg.ts` | ✅ startsWith |
| `https://ads.doubleclick.net/ad/seg.ts` | `ads.doubleclick.net/ad/seg.ts` | ✅ includes `.doubleclick.net` |
| `https://cdn.example.com/video/seg.ts` | `cdn.example.com/video/seg.ts` | ❌ 不匹配 |
配置一条放行规则 type=path, pattern=/live/, action=allow:命中该路径的 block 跳过后续所有判定,确保直播流不被启发式误杀。
总结
这套去广告方案的核心思路是规则优先,启发式兜底:
- 规则引擎提供了确定性 — 管理员精确控制哪些 CDN/路径需要拦截或放行
- 启发式提供了自适应性 — 对未知的广告模式仍然有发现能力
- 时长保护降低了误伤 — 短视频不会被启发式误判
- 客户端缓存保证了性能 — 规则带 ETag 增量更新,播放时无额外请求
- 双端架构统一 — Web 用 hls.js Loader 拦截,Flutter 用本地代理,共享同一套过滤逻辑
这种”确定性的规则 + 概率性的启发式”的组合方案,在处理不确定的外部数据源时,比单一策略更加健壮和可维护。
讨论
还没有留言,来留下第一条评论吧!