跳转到主要内容

crayonxiaoxin

Yam TV HLS 去广告方案:从启发式到规则引擎的演进

前言

视频聚合应用中,第三方视频源的 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 端useHlsAdFilterRules composable 在页面初始化时懒加载规则(带 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 用本地代理,共享同一套过滤逻辑

这种”确定性的规则 + 概率性的启发式”的组合方案,在处理不确定的外部数据源时,比单一策略更加健壮和可维护。

讨论

还没有留言,来留下第一条评论吧!

留下足迹