电视 / 电台新闻监听平台:端到端流程技术调研
主题 :从多路公网直播源,到可播放、可转写、可翻译、可检索、可研判的新闻情报,整条流水线该怎么拆、怎么选、怎么验。
定位 :通用流程调研为主线,最后用一个小节对照我们自己的项目(Media-Int 多信源直播监控平台)。
读者 :准备自建或改造「多语种广播电视监测系统」的架构师、后端/前端工程师、以及要拍板的产品负责人。
阅读建议:第 2 章是必读的地基图;第 9 章(时间轴对齐)是全篇技术含量最高、也最容易被低估的部分。
目录
- [0. 结论速览](#0. 结论速览)
- [1. 需求定义:什么叫「新闻监听平台」](#1. 需求定义:什么叫「新闻监听平台」)
- [2. 端到端流程总览](#2. 端到端流程总览)
- [3. 阶段一:信源治理](#3. 阶段一:信源治理)
- [4. 阶段二:接入与转封装](#4. 阶段二:接入与转封装)
- [5. 阶段三:分发与浏览器播放](#5. 阶段三:分发与浏览器播放)
- [6. 阶段四:音频旁路与语音端点检测](#6. 阶段四:音频旁路与语音端点检测)
- [7. 阶段五:语音识别(ASR)](#7. 阶段五:语音识别(ASR))
- [8. 阶段六:翻译](#8. 阶段六:翻译)
- [9. 阶段七:时间轴对齐(全篇最难)](#9. 阶段七:时间轴对齐(全篇最难))
- [10. 阶段八:存储、检索与归档](#10. 阶段八:存储、检索与归档)
- [11. 阶段九:AI 研判与告警](#11. 阶段九:AI 研判与告警)
- [12. 阶段十:展示层与运维面](#12. 阶段十:展示层与运维面)
- [13. 跨阶段的六条架构不变量](#13. 跨阶段的六条架构不变量)
- [14. 容量与成本模型](#14. 容量与成本模型)
- [15. 验收方法学:怎么证明它真的在工作](#15. 验收方法学:怎么证明它真的在工作)
- [16. 失败模式与降级矩阵](#16. 失败模式与降级矩阵)
- [17. 常见坑清单](#17. 常见坑清单)
- [18. 小团队落地路线图](#18. 小团队落地路线图)
- [19. 与我们项目的对照](#19. 与我们项目的对照)
- [20. 参考与延伸阅读](#20. 参考与延伸阅读)
0. 结论速览
如果你只想拿走一页纸,看这里。
整条流水线的骨架是十段,可以粗暴地记成四句话:
收得进来 → 播得流畅 → 听得清楚 → 说得明白 → 找得回来 → 判得有理
(接入) (播放) (转写) (翻译) (归档检索) (研判告警)
八条最值得先记住的判断:
- 中间必须有一层流媒体服务。浏览器只吃 HLS / DASH / WebRTC,上游却常是 RTSP、RTMP、带防盗链的 m3u8;把「对接上游」和「服务浏览器」这两件事分开,是整个系统的第一个解耦点。
- 播放与转写必须共用同一条时间轴。这是全篇第一原则。任何"另开一路去拉音频"的做法,都会在几分钟内积累出十几秒的漂移,字幕和画面再也对不上。
- 浏览器的解码器是稀缺资源,不是内存也不是带宽。一面墙能同时播几路,由 GPU 硬解通道数决定,通常是个位到十几路的量级。墙的设计要围绕这个约束做(焦点独占高清、非焦点降级)。
- HTTP 200 不等于在播。验收必须看到解码帧在增长、音频 PTS 在推进,而不是接口返回成功。
- 文字要晚一点到,但不能早到。让播放缓冲(3~5 秒)去吸收 ASR 的 1~2 秒延迟;反之则永远对不齐。
- 分析层必须是旁路。ASR、翻译、研判任何一个挂掉,播放和配置链路都不能受影响。
- 浏览器永远不能拿到上游地址。上游 URL、Header、Cookie、密钥必须在接入层被彻底吸收掉,这是安全底线也是合规底线。
- 能力要用曲线证明,不要用副本数证明。「四路没问题,所以八路也没问题」是这类系统最常见的错误结论。
1. 需求定义:什么叫「新闻监听平台」
同一个词,在不同组织里指三件很不相同的事。先把形态定清楚,后面的技术选型才有意义。
1.1 三种典型形态
| 形态 | 核心问题 | 关键指标 | 典型用户 |
|---|---|---|---|
| 播出监测型 | 我们的信号还在播吗?有没有黑屏、静帧、音量归零? | 可用性、告警时效、误报率 | 播出机房、运维 |
| 内容情报型 | 外面在说什么?谁在说?口径怎么变? | 覆盖率、检索能力、语种数、响应速度 | 情报/研究/宣传 |
| 合规审查型 | 有没有出现不该出现的内容? | 召回率、可追溯性、留证完整度 | 风控、法务、监管 |
真实系统通常是混合体,但优先级必须排出一个,因为它直接决定架构重心:
- 播出监测型 → 重心在接入层稳定性 与指标采集,转写只是辅助。
- 内容情报型 → 重心在转写+翻译+检索+研判,播放只是给人核对用的。
- 合规审查型 → 重心在存储与不可篡改性,音视频要能被"定格保存"。
1.2 能力清单
把需求翻译成可交付的能力,大致是这九项:
- 接入:把 N 路公网电视/电台直播源稳定收进来,支持回退与自愈。
- 播放:在浏览器里同时呈现多路画面,焦点路高清流畅,非焦点路可识别即可。
- 转写:把每路音频实时转成原始语种文字。
- 翻译:把非中文内容译成中文(或目标语),且不阻塞原文。
- 对齐:让文字、声音、画面落在同一条时间轴上。
- 归档:把文字按信源、时间、语种、片段存起来,可检索、可导出。
- 研判:对文本做结构化分析(分类、实体、事件、情绪、口径)。
- 告警:把达到阈值的事件推给人,并附带可核查的证据。
- 运维:看得见每路健康度、算得清容量、扛得住重连和抖动。
1.3 非功能需求(最容易被漏掉,也最容易翻车)
- 流畅优于清晰:先保证不卡,再在能稳住的候选里挑最高画质。这条要在需求阶段就写死,否则实现期一定被"上 1080p"带偏。
- 可降级:任一增强能力缺席,主链路照常。
- 可回退:每次信源/配置变更都要有备份和一键回滚。
- 可审计:谁在什么时候查了什么、导出过什么。
- 成本可控 :转写是按量计费的,全量不间断转写 和抽样转写的成本可能差一个数量级。
- 合规边界:抓什么、存多久、谁能看,通常是先于技术的问题。
2. 端到端流程总览
2.1 主图
先看全貌。后面每一章都是这张图上的一段。
┌───────────────────────────── L0 信源层 ─────────────────────────────┐
│ 公网电视/电台:HLS(m3u8) · RTSP · RTMP · SRT · Icecast/MP3 · 卫星落地 │
│ 信源台账:id / 地区 / 媒体类型 / 适配器+优先级 / 字幕能力 / 监控等级 │
└───────────────────────────────┬─────────────────────────────────────┘
│ ① 可达性探测(不是 HEAD 200,是媒体进度推进)
│ ② 域名白名单 / 私网拦截 / 凭证注入
▼
┌───────────────────────────── L1 接入层 ─────────────────────────────┐
│ FFmpeg 拉流 + 转封装(-c copy 优先)→ 推入流媒体服务器 │
│ 流媒体服务器:MediaMTX / SRS / ZLMediaKit / OME │
│ 路径约定:/<channel>/grid(全目录低码率) │
│ /<channel>/focus(聚焦高清) │
│ /<channel>/analysis(分析专用轨道) │
│ 健康看护:单路单进程 + 监督重启 + 熔断 + 采样回放 │
└──────────┬───────────────────────────────────────┬─────────────────┘
│ LL-HLS(给浏览器) │ RTSP/本地(给服务端)
▼ ▼
┌─────────────────────── L2 分发/播放层 ────────┐ ┌──── L3 音频旁路 ────┐
│ nginx 同源代理(Cookie 注入 / 关缓冲 / 临时目录)│ │ 取"浏览器正在播的同一份" │
│ hls.js 播放器池:焦点高清 + 非焦点降级轮播 │ │ 音频 rendition │
│ 单一有声源 · 解码器守卫 · 断流自愈 │ │ → 16k 单声道 PCM │
└───────────┬───────────────────────────────────┘ └──────────┬──────────┘
│ │ 200ms 块 / VAD 断句
│ ▼
│ ┌────── L4 识别层 ──────┐
│ │ 流式 ASR(长连接) │
│ │ 或上游字幕轨道(优先) │
│ └──────────┬────────────┘
│ │ 原文(先出)
│ ┌──────────▼────────────┐
│ │ L5 翻译层(异步/批量) │
│ └──────────┬────────────┘
│ │
│ ┌────────────────────▼─────────────────┐
└────── playingDate ──────►│ L6 对齐层:把文字钉到媒体时间轴上 │
│ 窗口戳 = 清单 PDT,不是收端字节计数 │
└────────────────────┬─────────────────┘
│
┌────────────────────▼─────────────────┐
│ L7 存储层:归档 / 分区 / 全文检索 │
└────────────────────┬─────────────────┘
│
┌────────────────────▼─────────────────┐
│ L8 分析层:研判 / 事件 / 聚类 / 告警 │
└────────────────────┬─────────────────┘
│
┌────────────────────▼─────────────────┐
│ L9 展示层:监控墙 / 字幕轨 / 检索 / 日报│
└──────────────────────────────────────┘
2.2 一句话解释每段
| 阶段 | 输入 | 输出 | 一句话职责 |
|---|---|---|---|
| L0 信源 | 外部 URL | 台账条目 | 把"一个台"变成一个可管理对象 |
| L1 接入 | 上游流 | 内部统一流 | 把五花八门的源变成一种格式 |
| L2 播放 | 内部流 | 屏幕上的画面 | 在浏览器解码器预算内把画面铺开 |
| L3 音频旁路 | 同一条流 | PCM 块 | 把声音从媒体流里取出来 |
| L4 识别 | PCM | 原文文本 | 声音变字 |
| L5 翻译 | 原文 | 译文 | 外语变母语 |
| L6 对齐 | 文本 + 时钟 | 带时间戳的字幕 | 让文字知道自己在哪一秒 |
| L7 存储 | 字幕行 | 可检索档案 | 把流水变成资产 |
| L8 分析 | 档案文本 | 事件/告警 | 把资产变成结论 |
| L9 展示 | 全部 | 界面 | 把结论交到人手上 |
2.3 两条贯穿全程的约束线
在进入细节前,先立两条不可协商的线,后面所有取舍都从这里推导:
- 一源一条时间轴。一个频道在同一时刻,只应该有一个"现在"。画面、声音、文字、缓存、告警,全部挂在这条轴上。任何"为了省事另拉一路"的设计,都是给未来埋漂移。
- 增强可缺席,主干不可停。把系统显式分为"主干"(配置、接入、播放)和"增强"(转写、翻译、研判、告警)。增强层全部挂掉时,主干应当仍然是一个可用的直播墙。
3. 阶段一:信源治理
3.1 为什么这是第一阶段
绝大多数这类项目的工期超支,不是超在算法上,而是超在**"这个源今天又不行了"**。信源治理的目标是把这件事从"事故"降级为"日常"。
3.2 把信源当数据,而不是代码
第一件事:信源必须是数据,能热更新、能回滚、能审计。典型做法是一份声明式配置(YAML / JSON)或数据库表,字段通常包括:
yaml
- id: CN-01 # 稳定主键,全系统引用它
name: CCTV-13 新闻频道
media_type: television # television | radio(互斥,决定播放与字幕策略)
country: CN
region_group: 中国 / 中央
category: continuous # continuous | entertainment | sports ...
adapters: # 一主一备,优先级降序
- {type: direct_hls, priority: 100, config: {url: "http://.../cctv13hd.m3u8", allowed_ports: [82]}}
- {type: direct_hls, priority: 90, config: {url: "http://.../0013_1.m3u8", allowed_ports: [9901]}}
audio_url: null # 音视频分离的源,这里补音频播放列表
subtitles: {enabled: true, mode: auto} # 上游自带字幕轨时优先用字幕,而不是 ASR
monitoring: {level: 1, wall_visible: true, wall_order: 10}
display: {logo: /logos/cn-01.png, location: {lng: 116.40, lat: 39.90}}
设计要点:
- 适配器列表 + 优先级:一个台可能有三四个镜像地址,主用挂了自动降级。这让"信源不稳"从架构问题变成配置问题。
media_type是硬边界 :电视和电台在播放、缓存、字幕、转写上策略完全不同,从一开始就分开建模,比后期加if便宜得多。monitoring.level与wall_visible分离 :完整目录要保留(能自我修复),但墙面上不该出现永久黑屏的死台。"能被系统使用"和"该展示在指挥屏上"是两件事。- 配置进数据库是有代价的 :改 YAML 不等于改线上。所以要有显式的两步操作------
import(导入草稿)+publish(发布生效),并且发布要产出版本号,回滚才有抓手。
3.3 可达性探测:HTTP 200 是最坏的判据
一个 m3u8 返回 200,可能:
- 只有音频轨(视频源没有画面);
- 每个
STREAM-INF都带PROGRAM-ID=1,ffmpeg 会把 4 档码率当成 8 条流,选到错的; - master 里的备选地址全部 404;
- 能拉到数据但没有任何一帧是完整的(SPS/PPS 拿不到,报
dimensions not set)。
正确的探测是"拉到真实的媒体进度":给定 10~15 秒预算,检查是否解析出真实视频/音频流、分辨率是否解得出来、媒体序号是否在推进。把探测做成自动化脚本,产出一张"信源实测清单"(域名、码率档、实测结论、失败原因),是这类项目最值钱的工程资产之一。
3.4 安全边界
上游 URL 常常携带 Referer、UA、Cookie、甚至密钥。因此:
- 域名白名单 + 端口白名单 :只允许清单内的域名,非标端口要显式声明(
allowed_ports)。 - 解析后校验 IP :防止 DNS rebinding 指向
169.254.0.0/16、127.0.0.0/8、内网段------这是 SSRF 的经典入口。 - 凭证只存在于部署机的私密环境,不进仓库、不进日志、不进文档。
- 日志脱敏 :ffmpeg 的 stderr 会把上游 URL 原样回显,写进日志前必须先把
scheme://...整个干掉。
3.5 健康度与熔断
给每个源一个显式状态机:
unknown → ok → degraded → down → (冷却) → unknown
- 连续 N 次失败才判 down(避免一次抖动误杀);
- down 之后进入冷却窗口,不要疯狂重试(会把上游 IP 打黑);
- 重启要有熔断:例如 300 秒内最多 8 次重启,超了就冷却 5 分钟------否则一个坏源会拖垮整个进程池。
3.6 换源纪律
这是流程问题,不是技术问题,但值得写进规范:
- 先实测(不是"看起来像");
- 同国家/同类型优先替换(别为了可用性牺牲信源结构);
- 每次改配置先备份、留版本号、留回滚路径;
- 不要为了测试通过而改安全配置(比如把没验证的域名加进白名单)。
4. 阶段二:接入与转封装
4.1 为什么必须有中间层
新手最容易犯的错,是把上游 m3u8 直接塞给浏览器播放器。它会在三天内以四种方式崩掉:
- 协议不通:RTSP / RTMP / SRT / Icecast 浏览器根本不认。
- 跨域与防盗链 :上游
Access-Control-Allow-Origin不为你而设;很多源校验 Referer / UA / Cookie,浏览器发不了自定义 Header。 - 凭证暴露:要让浏览器直接拉,就得把带 token 的 URL 送到前端------安全上不可接受。
- 上游格式千奇百怪 :同一份 master 里混着 8 档码率、
PROGRAM-ID重复、音视频分在不同播放列表(audio_url)、时段性 404。
中间层的职责是把"多对多"变成"多对一、一对多":上游 N 种形态 → 中间层统一成一种(内部 RTSP + LL-HLS)→ 下游一致地消费。
4.2 转封装 vs 转码:优先"原样搬运"
| 策略 | 命令特征 | CPU | 画质 | 何时用 |
|---|---|---|---|---|
| 转封装(copy) | -c:v copy -c:a copy |
极低 | 无损 | 上游是 H.264/AAC,浏览器能吃 |
| 降码率转码 | -vf scale=480:-2 -r 15 -c:v libx264 -crf 30 |
中 | 有损 | 多路墙、非焦点缩略图 |
| 重编码统一 | 强制 -c:v libx264 -c:a aac -ar 48000 |
高 | 有损 | 上游编码古怪(HEVC/AC-3/单声道) |
| 音频规范化 | -vn -c:a aac -ar 48000 -ac 2 |
极低 | 无损 | 电台源(MP3 流等) |
工程经验:
- 焦点/主屏路尽量 copy。转码一次就多一次画质损失和 CPU 占用,而主屏是给人看的。
- 全目录的格子路降码率。480 宽、12~15 fps、CRF 30,码率大约 0.24~0.36 Mbps。这个档位只要求"能看出在播什么"。
- 电台不做假视频:给音频流生成一个 H.264 画布,会白白占用浏览器一个视频解码器。音频路径保持 audio-only,视觉标识(台标、声波、圆形频谱)交给前端用 Canvas/CSS 画。这是一个很划算的小决策。
- 音频统一到 AAC 48 kHz 立体声,前端只需处理一种音频格式。
4.3 流媒体服务器选型
| 方案 | 协议覆盖 | LL-HLS | 运维复杂度 | 适合场景 |
|---|---|---|---|---|
| MediaMTX | RTSP/RTMP/HLS/WebRTC/SRT | ✅(1s 分片 + 200ms part) | 低(单二进制 + YAML) | 中小规模、快速落地、RTSP 收流为主 |
| SRS | RTMP/HLS/WebRTC/SRT/GB28181 | ✅ | 中 | 国内直播生态、需要 GB28181 接入 |
| ZLMediaKit | 同上,插件丰富 | ✅ | 中 | 需要按需拉流、录制、截图一体 |
| OvenMediaEngine | 主打 LL-HLS / WebRTC | ✅(强) | 中高 | 低延迟优先、WebRTC 主路 |
| nginx-rtmp | RTMP/HLS | ❌ | 低 | 老架构,不建议新项目 |
选型时真正要问的问题不是"哪个功能多",而是:
- 我需不需要服务端内部再拉一路(给分析用)?→ 需要,则必须支持 RTSP 出流。
- 我对延迟的底线是多少?→ 指挥屏 3~8 秒可接受;交互式需要 < 1 秒,则考虑 WebRTC。
- 我能不能接受每路一个 ffmpeg 进程?→ 绝大多数方案都是这样,所以进程管理与重启熔断必须自己写。
4.4 路径(Path)设计
用路径分层表达"用途",是整个接入层最有效的抽象:
/<channel>/grid 全目录低码率,给墙上的格子
/<channel>/focus 聚焦路,按需拉起,浏览器焦点复用
/<channel>/analysis 分析专用轨道,服务端自己消费
/<channel>/sample 周期性采样片段(可选)
这样带来三个好处:
- 资源按需:focus 只在有人聚焦时存在,没有人看就回收。项目实测中,"释放最后一个聚焦会话即停止上游拉流"是显著降低成本的一招。
- 时钟一致 :focus 的浏览器播放与分析消费的是同一条发布流,天然同源。
- 隔离:分析抽音频不再干扰播放路径。
4.5 LL-HLS 的几个关键参数
- 分片 1 秒 + part 200~300 ms:这是 LL-HLS 的标准档位。清单里的 part target 大约 250 ms。
- 保留窗口:分片保留要足够长(例如 7 个),短了会出现"段过期 404",客户端就卡。
EXT-X-PROGRAM-DATE-TIME必须带上:它后面会变成对齐字幕的唯一权威时钟(见第 9 章)。很多服务器默认不带,要显式开启。
4.6 一个决定字幕命运的参数:-re
这一条值得我们单独写一节,因为它是一个隐形的、不报错的事故源。
问题 :拉流发布时不加 -re,ffmpeg 会以最快速度把上游已有的 HLS 窗口和缓冲区一次性灌完,而不是按 1× 实时速率读取。于是:
- 发布刚起的几秒里,读取速率可以到 2.5×;
- 等缓冲追平后,才回落到 ~0.9×;
- 任何"按读取字节数推算时间"的消费者,都会在这一瞬间把时间锚点算错,并永久保持这个偏移。
项目实测(部署机上直接读聚焦 RTSP):
| 路径状态 | 读到音频 | 墙钟耗时 | 速率 |
|---|---|---|---|
| 刚发布约 6 秒 | 45.46 s | 18.10 s | 2.51× |
| 预热 35 秒后 | 19.97 s | 21.87 s | 0.91× |
| 长期运行的 grid | 14.95 s | 18.22 s | 0.82× |
结论 :要么加 -re 让发布侧匀速,要么(更好)让消费者不要依赖"读取速率 == 实时"这个假设。第 9 章给出的方案是后者------用清单自己的时间戳,从根上消除这个假设。
4.7 同源代理:把 nginx 用对
前端 nginx 不只是反向代理,它还承担四件事,每一件都踩过坑:
- Cookie 注入:把内部会话令牌映射成媒体服务器的 cookie,浏览器不需要知道任何密钥。
- 关闭缓冲 :LL-HLS 必须
proxy_buffering off,否则 nginx 会把 part 攒起来再发,低延迟白做。 - 临时目录 :这是个经典事故------nginx 以部署用户身份运行,
client_body_temp_path/proxy_temp_path还指向/var/lib/nginx/...,于是所有超过内存缓冲的请求体全部Permission denied。表现为客户端ERR_CONTENT_LENGTH_MISMATCH→<video>抛MEDIA_ERR_NETWORK→ 每块瓦片每秒重下整段视频。务必把临时路径显式指到有写权限的目录下。 - 路径重写 :把对外友好的
/hls/<channel>/...映射到内部结构。
4.8 进程监督
每路一个 ffmpeg 进程,必然要有监督:
- 重启:崩溃即拉,带退避;
- 熔断:窗口内重启次数超限 → 冷却,不拖垮整机;
- 超时 :用
asyncio.wait_for(duration + N)之类的硬边界,超时杀进程。不要迷信-rw_timeout------它在某些 ffmpeg 版本上对 RTSP 根本无效,结果是"进程还活着但一帧没出",日志一片安静。 - 可见性 :每路进程的启动/退出/失败原因都要有日志,且失败必须显式记录("静默失败"是这类系统最难查的 bug)。
5. 阶段三:分发与浏览器播放
5.1 真正的瓶颈是解码器
大多数人以为一面墙的瓶颈是带宽。实测下来通常不是:是浏览器能同时维持的硬件解码器数量。
- 有独显时,Chrome/Edge 大约能撑到十几路 H.264 硬解;
- 无独显(服务器上跑无头浏览器、或普通办公机)时,退化为软解,路数会掉到个位数;
- 每个
<video>+ MediaSource 还各自带着内存和渲染开销,20 路以上的 DOM 视频节点在任何机器上都会抖。
因此墙的设计原则是:解码器预算 > 带宽预算 > CPU 预算。
5.2 焦点(Focus)模型:一个被反复验证过的设计
把墙上的资源分成两档,是这套系统里性价比最高的一条设计:
| 档位 | 数量 | 码率 | 播放方式 | 目的 |
|---|---|---|---|---|
| 焦点路(主屏) | 1 | 原始/高清,带声音 | LL-HLS,独占一路解码 | 给人看清、听清 |
| 非焦点路(缩略图) | N | 低码率 / 低帧率 / 静音循环 | 降级的轻量播放 | 让人知道"在播什么" |
关键细节:
- 焦点是"谁在最大屏",不是"谁被点过" 。产品上要保证:最大屏展示的频道自动就是聚焦频道,首次打开页面也自动聚焦,不需要用户再点一次"接入"。这一点如果做错,用户每次进系统都要多点一步,体验上很致命。
- 同一时刻只允许一路有声。多路同时出声是灾难,不只因为吵,还因为浏览器会因自动播放策略把音频全掐掉。
- 焦点切换要"搬 DOM"而不是"重建 DOM" 。
<video>元素一旦重建,MediaSource 图、缓冲区、解码器全部重建,表现为一次明显的黑屏抖动。把瓦片元素在容器之间移动,可以做到焦点交接零抖动。 - 非焦点的降级可以更激进 :允许 3 个解码 + N 个静态快照,甚至用周期性更新的短视频循环(如 20 秒 MP4,每 5 分钟更新)替代直播。代价是"不是真直播",收益是浏览器压力、上行带宽、上游连接数全部下降一个档次。但要把这个取舍显式告知使用者,不能让人误以为在看 live。
5.3 hls.js 关键参数
ts
const hls = new Hls({
lowLatencyMode: true, // 开启 LL-HLS
liveMaxLatencyDuration: 12, // 允许的最大直播延迟
maxBufferLength: 6, // 前向缓冲(秒)
backBufferLength: 10, // 保留少量回退缓冲
enableWorker: true, // 解析放到 Worker,避免卡主线程
});
hls.loadSource("/hls/<channel>/focus/index.m3u8");
hls.attachMedia(video);
其他必须自己处理的:
- live edge 恢复:一旦落后太多,主动 seek 回直播边缘,而不是无限追。
- 错误分类重试:网络错误 → 退避重连;解码错误 → 重建 MediaSource;致命错误 → 降级到快照。
- 播放器池化 :
ensure()必须每次重新绑定 IntersectionObserver 。这是一个真实的坑------重建瓦片后旧 observer 还挂在已脱离文档的<video>上,于是报告"不可见"并停止播放,结果每个格子都黑成快照。
5.4 让"墙的选型变化"不触发整体重绘
一个高频错误:把"当前选中频道"放进墙的渲染 key 里。用户点一下,整个墙全部重建,所有解码器重新初始化,看起来像卡死。
正确做法 :墙的渲染 key 只包含频道集合 ;选中状态是独立的、增量应用的一次样式/结构变更。
5.5 布局:让每个格子都恰好 16:9
指挥屏的布局需求比想象中苛刻:屏幕尺寸五花八门,但每个瓦片都必须保持 16:9,不能拉伸变形。
成熟做法是由 JS 测量容器后写入 CSS 变量,用 CSS Grid 消费:
--wall-main-w / --wall-main-h 主屏尺寸
--wall-side-w 右侧缩略图列宽
--wall-strip-tile-w / -h 底部缩略条尺寸
测量逻辑要解一个约束:主屏宽度取"容器能给的宽度"和"主屏+底条高度还放得下"这两个值里的较小者,再由它推导列宽和底条高度,使得底条最右端的缩略图正好对齐右列右边缘。这些格子填不满任意形状的容器,所以整体居中、两侧均分留白。窄屏(如 < 641px)则退化为单列纵向堆叠。
5.6 字幕叠加的"不闪"原则
主屏上的字幕如果实现得糙,会出现明显的闪烁:一句话结束、下一句未到时,字幕块被清空又被填上。
规则:
- 字幕块常驻挂载,只切换显隐与文本,不要反复创建/销毁节点;
- 同一 tick 内完成"显示 + 填字",不要先显示空框再等下一拍填字;
- 未匹配到当前播放时刻的句子时,保持上一句(hold),不要清空;
- hold 必须绑定频道:换台时立刻失效,否则新频道会短暂显示上一台的字幕;
- 无内容可 hold 时显示等待文案(如"正在接入实时字幕..."),而不是空白。
6. 阶段四:音频旁路与语音端点检测
6.1 目标:拿到「和画面同一秒」的 PCM
转写的前提是把音频取出来,变成单声道 16 kHz PCM。看似简单,但有三种取法,代价完全不同:
| 取法 | 描述 | 时间轴 | 代价 |
|---|---|---|---|
| A. 另开一路拉上游 | 单独起一个 ffmpeg 从上游 URL 拉音频 | ❌ 独立时钟,会漂移 | 带宽翻倍、源端压力 |
| B. 从流媒体服务器再拉一路 | 从 MediaMTX 的 RTSP 路径取 | ⚠️ 服务端时钟,与播放器不同源 | 服务端多一路消费者 |
| C. 消费浏览器正在播的那份音频 | 取 LL-HLS 清单里的 TYPE=AUDIO rendition |
✅ 按构造对齐 | 无额外上游连接 |
C 是最优解 ,理由在第 9 章展开:它让"文字时间"直接等于浏览器 playingDate,中间不存在任何需要"估算"的时钟换算。
6.2 长连接 vs 反复重连
一个直觉上合理、实测很糟的做法是:每转 4 秒音频,就重新连一次上游 RTSP。实测代价:
直接取 12 秒音频 → 实际耗时约 17~19 秒(含建连、探测、缓冲)
也就是说接入开销接近音频本身长度的 1.5 倍,永远追不上实时。
正确做法:一条长连接持续读,PCM 产生即按小块(如 200 ms)上传,按自然停顿或最长窗口(如 4 秒)提交一次转写。项目实测这样能做到提交到返回原文中位数约 190 ms。
6.3 端点检测(VAD)与窗口切分
- 切分依据:优先按自然停顿(说话人换气),兜底是最长窗口。
- 窗口太短:短语被切断,识别质量下降,且请求数暴涨(成本)。
- 窗口太长:延迟上升,字幕跟不上画面。
- 静音窗口要显式丢弃,不要发起空请求(浪费配额)。
- 连续的相邻窗口要能被拼接(前一句结尾与后一句开头衔接),这是判断"是否连续转写"的重要验收点。
6.4 不要忘记:上游可能有现成字幕
很多广播机构会发布内嵌字幕轨(HLS EXT-X-MEDIA TYPE=SUBTITLES 或 CLOSED-CAPTIONS / WebVTT)。它的质量高于 ASR,且零延迟、零成本。
所以正确的策略是分级:
- 源带字幕轨且正在产出内容 → 直接用字幕,跳过 ASR;
- 源没有字幕轨 → ASR;
- 两者都有 → 字幕优先,ASR 作为字幕停更时的兜底。
但要诚实报告 :真实世界里"配了 subtitles: auto"不等于"真的有字幕"。实测中可能所有源都没有可用的字幕轨道(例如 CLOSED-CAPTIONS 全部标着 NONE)。这种情况下要明确写出"当前 ASR 承接全部频道",而不是含糊地说"已支持字幕接入"。
7. 阶段五:语音识别(ASR)
7.1 形态选型
| 形态 | 优点 | 缺点 | 适用 |
|---|---|---|---|
| 流式长连接(WebSocket) | 延迟低、上下文连续、单连接多窗口 | 协议复杂(乱序/重复/取消)、连接管理难 | 实时监控的首选 |
| 分段 HTTP | 实现简单、易重试 | 每次请求独立,上下文断、延迟随路数线性上升 | 离线/批量转写 |
| 本地部署(Whisper / FunASR 等) | 无调用成本、数据不出域 | 需要 GPU、并发与模型管理自建 | 有 GPU 且量大 |
| 云 API | 免运维、多语种好 | 按量计费、有并发配额、需凭据管理 | 中小规模、快速上线 |
7.2 云 API 的工程细节(比 API 本身更重要)
如果走云服务,真正的工作量在协议封装上:
- 用
item_id关联请求与结果。提交和返回到达顺序不一定一致,必须靠 ID 绑定,不能用"第几个窗口"。 - 显式处理四类异常结果:乱序到达、重复推送、静音窗口返回空、超时无返回。
- 限制在途窗口数(例如最多 4 个未完成窗口),超了就不提交,而不是无限堆积。
- 清理远端上下文:多数服务会保留对话项,长跑必须显式删除已完成项,否则内存与费用都会涨。
- 凭据池 + 限流:多组凭据分担并发,失败自动切换;凭据只存在于部署机的环境变量里。
- 空闲回收:一段时间(如 15 秒)无数据无结果就关闭连接并由调度器重建,避免僵尸连接占着配额。
7.3 并发与容量:用 RTF 算,不要拍脑袋
RTF(Real-Time Factor)= 处理 1 秒音频所需的处理秒数。项目实测:
4 秒音频 ≈ 0.86 秒识别 → RTF ≈ 0.215 → 单槽可覆盖约 4.65 路实时
于是"25 路全量实时"大约需要 6 个并发槽 (1 个优先给焦点 + 5~6 个给背景)。这个估算是起点 ,必须通过 2/4/6/8 路压力测试和实际限流结果来校准------用曲线证明,不用副本数证明。
7.4 调度优先级
有焦点概念的系统里,调度器必须显式分档:
- 焦点路优先:可以占用"整块预算",保证片段完成后约 1 秒内拿到原文;
- 背景路轮转 :按目录顺序轮询,每个都要有机会,不能长期饥饿;
- 降频但不停 :娱乐类内容可以降低分析频率(如两分钟一次),但基础采集与转写不能停------否则"全量监听"就成了空话。
7.5 一个必须说清楚的边界
"背景轮询转写"不等于"逐秒无遗漏转写"。
如果实现对非焦点路是"拉一段 4 秒 → 转 → 再拉下一段",那么中间必然有间隙,产生的是抽样字幕 而非完整字幕。这可以作为阶段性方案,但对外描述必须诚实,并且在目标里写明升级路径:连续音频留存 + 有界队列 + 覆盖率统计。
覆盖率应该有量化指标:每小时每路应产出约 900 条 4 秒窗口(理想),实际产出多少、平均间隔多少,都是可测的。
8. 阶段六:翻译
8.1 一条铁律:原文先出,译文后补
ASR 返回原文 → 立即入库 + 立即推送前端 → 翻译是独立的后台任务
翻译永远不能阻塞原文。理由很实际:原文是"有和无"的问题,译文是"好和更好"的问题。而且翻译失败很常见(模型超时、限流),不能让它把整条字幕链路带崩。
工程实现:
- 翻译任务放独立线程池/队列,设定在途上限(如 8 个);
- 队列满时丢译文、保原文,而不是阻塞;
- 译文回来后按同一条时间范围回填到同一行记录,不新建行。
8.2 什么时候不该翻译
- 源本来就是中文 → 不翻译。否则你会得到两行一样的字,白白占掉主屏一行高度。
- 实现细节:如果原文里已经含中文,译文为空时用原文兜底(
hasChinese判定),保证"中文预览"和"原文预览"逐行对齐。 - 例外:中文频道里夹带的英文片段(如引用的外媒声)仍然要翻译,这才是真正需要译文的场景。
- 实现细节:如果原文里已经含中文,译文为空时用原文兜底(
- 纯音乐/音效段落 → 不产出行记录即可(连原文都不需要)。
8.3 提升质量的几个廉价手段
- 术语表:人名、机构名、地名预置对照("Pentagon"→"五角大楼"这类固定译法),比换更大的模型有效。
- 上下文窗口:把前 1~2 句一起给模型,代词和省略主语的句子会好很多。
- 批量提交:多条短句合并成一次请求,显著降低成本。
- 不重复翻译:同文本哈希做缓存,新闻里重复播报的套话命中率很高。
8.4 前端呈现的一句话规则
目标语在上,源语在下:
┌──────────────────────────────┐
│ 中方对当前局势表示严重关切 │ ← 中文(目标语)
│ China expresses serious ... │ ← 源语(原文)
└──────────────────────────────┘
源语本身是中文时只显示一行,因为重复会让主屏白白损失一行高度。
9. 阶段七:时间轴对齐(全篇最难)
这是全篇技术含量最高的一节,也是绝大多数同类系统"看起来能用、用起来不对"的根因。
9.1 系统里同时存在四个时钟
| 时钟 | 来源 | 谁在用 |
|---|---|---|
| 源端 PTS / 墙钟 | 上游编码器 | 上游自己 |
清单 PDT (EXT-X-PROGRAM-DATE-TIME) |
流媒体服务器的 LL-HLS 清单 | 浏览器 hls.js → playingDate |
| 收端采样计数 | 消费者按"读了 N 字节 ÷ 每秒字节数"推算 | 老式 ASR 实现 |
播放器 currentTime |
媒体内部相对时间 | 前端 UI |
对齐的本质是:让"文字的时间戳"和"播放器播放的时刻"落在同一个坐标系里。
9.2 失败案例:采样计数时钟
一个非常自然、也非常错误的实现:
python
# 只在第一个满窗时算一次锚点
anchor = datetime.now(timezone.utc) - timedelta(seconds=self.seconds)
# 之后用"已读字节数"推算
window_end = anchor + timedelta(seconds=total_bytes / PCM_BYTES_PER_SECOND)
这个公式隐含一个假设:读取速率恰好是 1× 实时。
而实际读取速率是:
- 发布者拉上游时不加
-re→ 新消费者接入瞬间速率可达 2.51×; - 缓冲追平后回落到 0.82×~0.91×。
于是:ASR 恰好在突发期建立了锚点 → 采样计数瞬间超前墙钟 → 然后永久保持这个超前量。
生产库里的实测偏移:
| 路径 | 文字相对媒体的中位偏移 | 最大偏移 | 结论 |
|---|---|---|---|
| 聚焦路径 | +11.1 s | +33.3 s | 文字超前画面,字幕对不上 |
| 全目录路径 | −7.9 s | --- | 健康(因为每个窗口都重新锚定) |
前端用 playingDate 去匹配 [audio_started_at, audio_ended_at),而播放缓冲只有 5 秒------远小于 10~30 秒的漂移,所以主屏字幕要么空白,要么是十几秒之后的内容。
9.3 正确方案:让消费者使用播放器正在用的那份时间戳
核心思想一句话:
不要"估算"时间,去"读"时间。
具体做法:
- ASR 不去另拉一路 RTSP ,而是消费浏览器正在播放的同一份 LL-HLS 音频 rendition :
- 取主清单 → 选
TYPE=AUDIO的 rendition(没有就退回第一个 variant); - 按 1 秒分片,取最近的 3 段 →
init.mp4+ 3 段拼接 → ffmpeg 解成 16 kHz 单声道 PCM; - 窗口的
started_at/ended_at直接取清单里分片的EXT-X-PROGRAM-DATE-TIME。
- 取主清单 → 选
- 前端用 hls.js 提供的
playingDate做匹配。因为它和清单 PDT 同源,文字时间 == 播放时间,按构造对齐。 - 文字会比画面晚 1~2 秒(分片必须先可用),这点延迟正好被 5 秒播放缓冲吸收。
为什么这是"架构级"的修复而不只是补丁:它把"对齐"从"依赖速率假设的推算"变成了"同一个字段的两次读数",速率假设消失了,漂移从根上不存在。
9.4 播放缓冲是特性,不是浪费
很多人想把延迟压到最低,于是缓冲设成 1~2 秒。这在"转写+翻译"的系统里是错误目标:
画面已经进入缓冲队列 5 秒后才真正显示
↓
ASR 在这 5 秒内完成转写(约 2~4 秒)
↓
文字在画面出现前就准备好了 → 观感是"字幕跟着声音走"
目标是"感知同步",不是"最低延迟"。 宁可整体延迟 5 秒,也不要画面和字幕各说各的。
9.5 匹配逻辑的边界情况
| 情况 | 正确处理 |
|---|---|
| 当前时刻无匹配记录(静音间隙) | 保持上一句(hold),不清空 |
| 有记录但译文未到 | 只显示原文一行,不要打印两行相同的字 |
| 用户刚换台 | 立刻清除 hold,新台不得继承旧台字幕 |
| 播的是缓存/采样视频 | 字幕必须绑定 sample ID + 片段起止时间,绝不能混用实时行或上一版缓存 |
| 播放器暂停 | 保持当前句,不要继续推进 |
| 历史浏览 | 历史行可滚动查阅,但只有当前匹配行高亮,历史行不得冒充"当前字幕" |
9.6 怎么度量同步误差
只测"窗口是否连续"是不够的------那只证明相对时序 正确,不证明绝对同步。
可行的度量方法:
- 找一段有明确视觉事件(整点报时、片头台标、比分变化)的素材;
- 记录该画面出现的
playingDate; - 找到与之对应的那条字幕行的
audio_started_at; - 差值即为端到端字幕偏差。多测几次取中位数与最大值。
没有这个数字之前,不要声称"字幕已同步"。
10. 阶段八:存储、检索与归档
10.1 数据模型
字幕是时序文本,表结构要能回答"某频道某天说了什么",也要能回答"某句话出现在哪一秒"。核心字段:
sql
channel_id -- 频道
source -- 文字来源:asr | subtitle(人工/上游字幕)
language -- 原始语种
text -- 原文(保留原语种,不统一翻译)
translation_zh -- 中文译文(异步回填,可空)
audio_started_at -- 窗口起点(来自清单 PDT!)
audio_ended_at -- 窗口终点
audio_session_id -- ASR 会话 ID(换台会开新会话)
audio_sequence -- 会话内序号
sample_id -- 若来自缓存视频,绑定具体片段
created_at -- 入库时刻(≠ 音频时刻,不要混用)
两个必须强调的点:
audio_started_at是音频时间,不是入库时间 。用created_at反推音频时刻,是 9.2 节漂移问题的另一种写法。sample_id是必要字段。同一频道会不断轮换新的采样片段,旧字幕绝不能再套到新片段上。
10.2 存储分层与分区
| 层 | 介质 | 保留期 | 用途 |
|---|---|---|---|
| 热 | PostgreSQL(按天分区) | 数天~数周 | 实时查询、DSP 界面 |
| 温 | 同库或廉价实例 | 数月 | 检索、日报 |
| 冷 | 对象存储(JSONL/Parquet + 音频切片) | 年级 | 取证、回溯 |
按天分区的收益:删除旧数据是 DROP PARTITION(秒级),而不是 DELETE WHERE(可能锁表几分钟)。保留策略必须在设计阶段就定下来,否则磁盘会在某天早上突然满。
10.3 检索:中文全文检索是个专门问题
PostgreSQL 的 tsvector 对中文默认不友好------没有空格,标准分词器会把整句当一个词。三个方案:
| 方案 | 说明 | 代价 |
|---|---|---|
| pg_jieba / zhparser | PG 扩展,中文分词 | 需要编译安装扩展 |
| 外置搜索引擎 | Elasticsearch / Meilisearch / OpenSearch | 多一个组件要运维 |
| 字形匹配(LIKE / pg_trgm) | 简单子串匹配 | 语义能力弱,别指望它做分词检索 |
小规模(千万行以内) :pg_trgm 的 LIKE '%关键词%' 加 GIN 索引,足够用。
大规模 / 需要相关性排序:上 Meilisearch(轻)或 Elasticsearch(重)。
10.4 导出与呈现
真实用户的第一个需求往往是"给我导一份"。要点:
- TXT 导出必须带 UTF-8 BOM,否则 Windows 记事本打开是乱码(这个细节会立刻被用户注意到);
- 按"频道 + 当天"聚合 成频道日报,预览和下载使用同一份合并内容(不要两套逻辑,否则导出和预览不一致);
- 输出的每一行都要保留时间标记与来源,这样才能支撑"事后把文字对齐回音视频"的目标;
- 三栏浏览(日期导航 / 原文 / 译文)比单栏列表可用性高一个档次,且左右两栏滚动要双向同步。
10.5 一个易被忽略的细节
冷启动要能立刻显示历史。
刚打开页面时,接口还没回来,屏幕全空------体验上是"坏了"。做法:
- 前端把最近 N 条(如 50 条)字幕镜像到
localStorage,冷启动即渲染,并在状态上标注"本地缓存 N 条 · 同步中"; - 视频同理:把最近一段采样缓存到 Cache API,首帧不用等网络。
这是纯粹的体验工程,但对"这系统是不是活的"这个感知影响极大。
11. 阶段九:AI 研判与告警
11.1 触发方式:为什么不应该默认全自动
对每一段转写自动调一次大模型,会带来三个问题:
- 成本:25 路 × 每 4 秒一次 ≈ 每天 54 万次调用;
- 噪声:绝大多数片段是常规播报,产出大量无意义结论;
- 误导:模型看到孤立的一句话,很容易做出过度解读。
更稳的设计是混合触发:
- 人工触发为主:操作员看到有意思的内容,点"对当前转录发起研判"------这是明确的需求信号,准确率最高;
- 规则/关键词自动触发为辅:命中预置词表、或某频道某时段出现异常密度时,自动取样分析;
- 定时摘要:按小时/按天对某频道做聚合摘要,成本可控。
11.2 三级事件模型
这是把"分析"变成"产品"的关键抽象:
observation(观察) → incident(事件) → alert(告警)
原始记录 有关联的一组观察 需要人介入的结论
(只存不推) (进看板) (推送 + 追踪)
原则:没有足够证据,只能停在 observation,不能升级为正式 alert。 这避免了"模型猜了一句就报警"的信任崩塌------一次误报会让人开始忽略所有告警。
11.3 Prompt 工程的几个要点
- 给出证据包 :喂给模型的必须是带时间戳的原文(+译文),并要求引用具体时间作为依据,方便人工核查;
- 要求结构化输出(JSON schema):分类、实体、事件、置信度、引用片段;这样能直接入库,而不是一堆自由文本;
- 要求"无结论时明确说无结论":允许模型说"信息不足",比逼它给出结论好;
- 本体/词典约束:预置领域实体与关系(人名、机构、地区、事件类型),让输出口径统一------否则同一个主体在不同片段里会有五种译名。
11.4 去重与聚类
同一件事,20 个台会播 20 遍。不处理的话,报告里全是重复。
- 近文本去重:SimHash / MinHash 相似度;
- 按时间窗聚类:比如 6 小时内主题相似的观察合并成一条事件;
- 信源权重:官方信源和转载源的权重不同。
11.5 告警通道
- 多通道(站内、邮件、IM 机器人、短信);
- 必须节流:同一事件在窗口内只报一次;
- 必须可追溯:告警点进去要能看到原始音视频片段与字幕证据。
12. 阶段十:展示层与运维面
12.1 展示层的三条设计取向
指挥屏和普通 Web 应用的设计逻辑不同:
- 信息密度优先于美观。大屏是"一直看"的场景,减少空白、减少无意义的装饰。
- 状态要靠颜色和形状立刻可读。哪些在播、哪些断了、哪路是焦点,扫一眼就要知道。
- 不要放"操作日志"式的元素。顶部导航堆一排"源/事件/状态"入口,会持续分散注意力;把入口收在侧边或按需展开。
12.2 一个具体的设计判断:产品名锚在哪
"系统名"应该锚在主要内容模块的左上角 ,而不是全局顶栏。原因:全局顶栏在大屏上会被忽略,而模块左上角正好是视线起点。同时保留一两个与当前页面强相关的动作按钮(如"对当前转录发起研判"),比一长条通用导航有效得多。
12.3 导航的组织方式:按地理,不按字母
电视/电台源的天然组织维度是 大洲 → 国家/地区 → 台。这比按字母排序、按 ID 排序都更符合"我知道我要看哪个国家"的心智模型。
同时要区分两类导航:
- 完整目录(永远显示全部源,用于选台和检索);
- 监控档位 (严控 ⊂ 重控 ⊂ 机控 的累积子集,是"墙"的过滤器)。
这两者必须解耦:完整目录不能因为切换了监控档位而丢源;点击档位外的源,应该回到"全部"视图。
12.4 运维面要看什么
| 指标类 | 具体项 |
|---|---|
| 接入健康 | 每路状态、最近失败原因、重启次数、熔断状态 |
| 播放健康 | 解码帧增长、掉帧数、卡顿次数、端到端延迟 |
| 转写健康 | 每小时窗口数、平均提交延迟、失败率、各槽位负载 |
| 存储健康 | 分区大小、增长速率、清理任务是否正常 |
| 成本 | API 调用量、翻译量、推理量按天/按频道统计 |
别把三类状态压成一个 status 字段。配置状态、媒体运行状态、分析状态是三件独立的事,混在一起会导致"配置是对的但流没起来"这种问题无法定位。
13. 跨阶段的六条架构不变量
这六条是我认为最值得写进 ADR(架构决策记录)的约束。它们的作用是:在每次做新功能时,可以机械地判断"这样设计合不合法"。
不变量 1:浏览器永不接触上游
浏览器只能看到:平台 API、SSE、快照、同源 HLS。上游 URL、Header、Cookie、密钥、内部 RTSP 地址一律不下发。
违反后果:凭证泄露、防盗链被封、SSRF 面扩大。
判定方法:审查任何返回给前端的数据结构,是否含上游地址或可用于推导上游的信息。
不变量 2:分析是旁路
停掉分析组件,配置发布、媒体接入、播放必须全部正常。
违反后果:一个 ASR 服务抖动导致整面墙黑屏。
判定方法:把分析进程 kill 掉,看主链路是否照常。
不变量 3:控制面不碰媒体面
平台 API 只负责配置事实与事件,不拉流、不解码、不推理、不代理大体积媒体。
违反后果:API 进程被流媒体 IO 拖垮;扩容时控制面与媒体面无法独立伸缩。
判定方法:在 API 代码里搜索 subprocess / ffmpeg / Popen,应该为空。
不变量 4:媒体面不写配置事实
拉流进程可以上报状态,但不能写配置。
违反后果:多处写配置 → 状态不一致 → 无法判断"线上到底是什么版本"。
不变量 5:一源一时钟
同一频道同一时刻只有一个"现在",见第 9 章。
不变量 6:证据先于结论
没有证据只能记为 observation;容量声明必须由曲线证明,而不是副本数。
14. 容量与成本模型
14.1 分项估算
带宽
入向 ≈ Σ(每路上游码率) × (1 + 冗余系数 ~1.3)
出向 ≈ Σ(客户端数 × 同时观看路数 × 码率)
出向通常是真正的瓶颈。25 路 360p 给 10 个客户端,出向约 10 × 6 × 0.3 Mbps ≈ 18 Mbps;如果给 25 路全高清,就是几百 Mbps。
CPU(转码)
先测 1 路的单核占用,再线性外推,并用 4/8/16 路实测校准(超线程和中低负载下的频率调度会让线性外推偏乐观)。参考量级:480 宽 / 12~15 fps / CRF 30 的 H.264,约 0.2~0.4 核/路。
CPU/配额(ASR)
所需并发槽 ≈ Σ(实时路数) ÷ (1 / RTF)
按 RTF ≈ 0.215 算,1 槽 ≈ 4.65 路。
存储(文本)
示例:25 路 × 24 小时
假设中文语音约 200 字/分钟 → 25 × 60 × 24 × 200 ≈ 720 万字/天
纯文本约 20 MB/天,加索引与译文约 60~100 MB/天
文本本身很便宜------贵的是它背后的 API 调用量和音视频留存。
14.2 采样与降级的成本杠杆
| 杠杆 | 效果 | 代价 |
|---|---|---|
| 非焦点路用低码率或采样短视频 | 出向带宽 ↓ 90% | 不是真直播 |
| 焦点路按需拉起 / 释放 | 入向连接数 ↓ | 首帧需要缓冲桥接 |
| 上游字幕轨优先于 ASR | API 成本 ↓ 可到 0 | 仅部分源可用 |
| 娱乐频道降频分析 | API 成本 ↓ 50%+ | 时效下降 |
| 文本分层保留(热/温/冷) | 存储成本 ↓ | 回溯变慢 |
14.3 必须先做压力测试的三个数
- 2/4/6/8 路并发转写的实际延迟曲线(找拐点,而不是找"能跑");
- 浏览器同时解码多少路开始掉帧(换机器结果不同,必须在目标机上测);
- 一路转码的真实 CPU 占用(别用"官方推荐配置")。
15. 验收方法学:怎么证明它真的在工作
15.1 第一原则:HTTP 200 ≠ 在播
这是整个行业最容易自欺的地方。一个流可能:返回 200 但没有关键帧、有画面但没声音、有声音但时间戳不推进、播放器 <video> 存在但 paused === true。
验收必须看到"物理量在变化":
| 层次 | 可观测的物理量 |
|---|---|
| 拉流侧 | 包计数增长、PTS 推进、媒体序号递增、解出分辨率 |
| 服务端 | 每路 ffmpeg 进程存活 + 持续输出 |
| 浏览器 | video.currentTime 推进、getVideoPlaybackQuality().totalVideoFrames 增长、droppedVideoFrames 不涨 |
15.2 端到端自动化验收
最有效的手段是用 CDP(Chrome DevTools Protocol)驱动一个无头 Chromium,真实地点开页面、点瓦片、读指标。它能抓到只有真实浏览器才会暴露的问题:
- 播放器是否真的在解码(而不是停在快照上);
- 点击焦点时是否只新开了一个会话、只切换了一路;
- 其余瓦片是否停止请求(
src === ""、paused === true、无新增网络请求); - 瓦片的 DOM 元素是否被复用(同一元素引用不变)------这条是"焦点切换不黑屏"的直接证据。
这类验收的价值在项目里被反复验证 :多个真实缺陷(跨域临时目录权限、crypto.randomUUID 在 HTTP 源下不存在、observer 挂在脱离文档的元素上、Range 请求不支持)都只有在真实浏览器里才暴露出来,靠看服务端日志是发现不了的。
15.3 三条非常具体的断言
以下三条可以直接抄进测试用例,它们的失败率非常高:
sameVideoElement === true:焦点交接前后,主屏的<video>必须是同一个 DOM 节点。- 非焦点瓦片每秒请求数 == 0:焦点切换后,被释放的瓦片不应再产生任何媒体请求。
- 每路采样视频只有 1 次请求:如果看到每秒一次重复下载,说明 Range 请求或缓存没做对。
15.4 长稳测试
跑 2 小时以上,观察:
- 内存是否单调增长(MediaSource 与 worker 泄漏的典型表现);
- 是否出现周期性重连(每 N 秒一次的重缓冲通常意味着续期逻辑写成了"释放+重建");
- 转写窗口是否连续(相邻窗口首尾衔接)。
15.5 报告要区分"通过"和"没测出来"
一个健康的验收报告长这样:
✅ 已验证:聚焦切换只开一个会话;非焦点停止请求;15.4 fps / 0 丢帧
⚠️ 未验证:绝对字幕误差未量化(缺少带已知视觉事件的素材)
❌ 已知缺陷:上游个别源有解码花屏,未解决
不要写"整体通过"。 把"已验证 / 未验证 / 已知缺陷"分开列,是这个领域专业度的直接体现。
16. 失败模式与降级矩阵
这是设计评审时最有用的表:逐个问"这块挂了会怎样,降级路径是什么"。
| 失败点 | 现象 | 影响面 | 降级策略 |
|---|---|---|---|
| 单路上游断流 | 该路黑屏 | 单路 | 切备用 adapter → 提升优先级 → 采样循环兜底 |
| 上游整体不可达 | 多路黑屏 | 多路 | 展示上次快照 + 明确标注"离线" |
| 流媒体服务器挂了 | 全部黑屏 | 全局 | 重启服务 + 客户端自动重连(务必有) |
| nginx 代理异常 | 播放中断 / 反复重下 | 全局 | 客户端错误分类重试 + 关缓冲配置校验 |
| 浏览器解码器耗尽 | 卡顿、掉帧 | 客户端 | 焦点独占 + 非焦点降级为快照 |
| ASR 服务不可用 | 无新字幕 | 字幕 | 播放继续;字幕保留历史 + 提示等待 |
| 翻译服务不可用 | 无译文 | 字幕 | 只显示原文(单行),不阻塞 |
| 研判模型不可用 | 无结论 | 分析 | 按钮报错提示,不影响主链路 |
| 字幕归档库不可用 | 无法落库 | 存储 | 内存/文件缓冲,恢复后补写 |
| 时钟对齐失效 | 字幕错位 | 字幕 | 保留 hold 策略,宁可显示旧句也不显示错句 |
| 凭据配额耗尽 | 转写排队 | 字幕 | 降级到只转聚焦 + 背景抽样 |
这张表的价值在于:它把"可用性"从口号变成了可执行的检查项。
17. 常见坑清单
按"踩到的频率 × 排查难度"排序,前十条是血泪。
| # | 坑 | 症状 | 根因 / 修法 |
|---|---|---|---|
| 1 | 采样计数当时钟 | 字幕超前画面十几秒 | 读取速率不是 1×(无 -re);改用清单 PDT 打戳 |
| 2 | nginx 临时目录无写权限 | 每秒重下整段视频、ERR_CONTENT_LENGTH_MISMATCH |
显式配置 *_temp_path 到可写目录 |
| 3 | HTTP 源下 crypto.randomUUID 不存在 |
所有需要 id 的功能静默失败 | 用 getRandomValues 兜底实现 |
| 4 | observer 挂在已脱离文档的 <video> 上 |
重建后所有瓦片黑成快照 | 每次 ensure() 重新绑定 observer |
| 5 | MP4 不支持 Range 请求 | 循环播放时反复整段重下 | 返回 206 + Content-Range + Accept-Ranges |
| 6 | 每 4 秒重连上游拉音频 | 永远追不上实时(12s 音频要 17~19s) | 改长连接持续读,PCM 小块持续上传 |
| 7 | 续期写成"释放+重建" | 每 50 秒一次周期性卡顿 | 同会话应为纯 TTL 续期,不重启流 |
| 8 | -rw_timeout 对 RTSP 无效 |
进程活着但零输出,日志安静 | 用硬超时 + kill,并显式记录失败 |
| 9 | master 清单 PROGRAM-ID 重复 |
dimensions not set 无法开播 |
直接指向具体码率的 media playlist |
| 10 | 把选中状态放进墙的重建 key | 点一下整面墙重建、疑似卡死 | 渲染 key 只含频道集合,选中态增量应用 |
| 11 | 中文源再翻译一遍 | 主屏两行同样的字 | hasChinese 判定,原文兜底 |
| 12 | 冷启动空白 | 打开页面等接口才出内容 | localStorage 镜像 + Cache API 预热 |
| 13 | 导出 TXT 无 BOM | Windows 记事本乱码 | 写 UTF-8 BOM |
| 14 | 焦点未被自动聚焦 | 用户每次进系统都要多点一步 | 最大屏频道即为聚焦频道 |
| 15 | 多路同时出声 | 声音叠在一起 / 被浏览器静音 | 全局单一有声源 |
| 16 | 配置只改 YAML | "改了怎么没生效" | 配置在库里,需 import + publish |
| 17 | 上游 URL 进日志 | 凭证泄露 | 日志脱敏 scheme://... → <redacted-url> |
| 18 | 保留策略缺失 | 某天磁盘满 | 按天分区 + DROP PARTITION |
| 19 | 用副本数代替容量验证 | 上线即雪崩 | 2/4/6/8 路曲线 |
| 20 | 把抽样转写说成全量转写 | 验收时说不过去 | 明示覆盖率指标 |
18. 小团队落地路线图
假设 2~4 人、一台服务器起步,建议分四期。每期都以"能演示、可验收"为终点。
第一期:能看(约 2~3 周)
- 信源台账(配置文件即可)+ 可达性探测脚本;
- 单路拉流 → 流媒体服务器 → 浏览器 LL-HLS 播放跑通;
- 一个最小监控墙(3~4 路,含焦点模型)。
- 验收 :浏览器里看到画面在动,
currentTime持续推进。
第二期:能听(约 3~5 周)
- 音频旁路(直接走"浏览器同款清单"路线,不要先做另拉一路的临时方案,否则要重写);
- ASR 长连接 + 窗口切分 + 落库;
- 字幕叠加 + hold 策略 + 后台轮询调度。
- 验收:同一窗口内,相邻转写首尾衔接;字幕与画面的偏差有量化数字。
第三期:能查(约 2~4 周)
- 翻译异步回填;
- 字幕归档库(分区)+ 检索页 + 日报导出;
- 前端冷启动缓存。
- 验收:能按日期+频道检索并导出,导出内容与预览一致。
第四期:能判(约 3~4 周)
- 人工触发的研判接口 + 结构化输出;
- 三级事件模型 + 告警节流;
- 运维指标面板。
- 验收:一次研判结果可追溯到具体时间戳的字幕证据。
之后(持续)
- 全量覆盖率提升(连续音频留存 + 有界队列);
- 上游字幕轨优先化的覆盖;
- 压力曲线与容量自动化测试;
- 冷热分层与成本治理。
如果只能给一条建议 :第二期的音频旁路一定要用"消费浏览器正在播放的那份清单"的方案。这是唯一一个事后返工代价极高的选择。
19. 与我们项目的对照
上面是通用流程;这一节把它映射到我们自己的 Media-Int 多信源直播监控平台(电视+电台,多语种实时转写/翻译/研判),方便对照阅读。
| 流程阶段 | 通用做法 | 本项目现状 |
|---|---|---|
| 信源治理 | 台账 + 适配器优先级 + 白名单 | config/channels.yaml(频道/地区/媒体类型/适配器优先级/字幕能力/监控档位)+ source-domain-allowlist.yaml;配置进库,需 import + publish 两步才生效 |
| 接入 | FFmpeg 拉流 + 流媒体服务器 | 每路一个 ffmpeg 进程推入 MediaMTX;grid(全目录低码率)与 focus(按需拉起)双路径;supervisor 带重启熔断与日志脱敏 |
| 分发 | nginx 同源 + LL-HLS | nginx 注入 MediaMTX 会话 cookie、关闭缓冲、临时目录重定向;LL-HLS 1s 分片 + 200ms part |
| 播放 | hls.js 池化 + 焦点模型 | 焦点路 LL-HLS 独占;非焦点路请求 20 秒采样 MP4 循环(每 5 分钟刷新);瓦片在容器间搬移以保住 DOM 与解码器 |
| 音频旁路 | 优先用播放器同一份时钟 | 已从"另拉 RTSP"迁移到 消费 LL-HLS 的音频 rendition + 清单 PDT 打戳 (focus_hls.py),正是第 9 章描述的方案 |
| ASR | 长连接 + 窗口 + 优先级调度 | 聚焦路长连接持续读,200ms 上传 / 4s 提交;VoiceSubtitleScheduler 给焦点整块预算、背景轮询不饥饿;上游字幕轨存在时优先并跳过 ASR |
| 翻译 | 异步不阻塞、中文不译 | 译文独立线程池、在途上限、队列满时保原文;中文源用原文兜底避免重复行 |
| 对齐 | 用播放清单 PDT 对齐 | 前端用 playingDate 匹配 [audio_started_at, audio_ended_at);未匹配时 hold 上一句并绑定 channelId,换台即失效 |
| 存储检索 | 分区 + 全文检索 + 导出 | PostgreSQL 归档(原文/译文/频道/来源/语种/媒体起止/会话序号);支持日期+类型+频道筛选、预览、UTF-8 BOM 的 TXT 下载、按频道日报合并 |
| 研判告警 | 人工触发 + 三级事件 | "对当前转录发起研判"为操作员显式动作;实时转写流程不自动调用研判模型 |
| 展示运维 | 指挥屏 + 指标 | 极简直播指挥台:最大屏自动聚焦、缩略图列+底条轮播、字幕双行(中文在上)、可滚动历史与当前高亮、字幕管理页 |
我们踩过、并且在这篇里给了通用写法的坑(都出现在第 17 章清单):
- nginx 临时目录权限 → 每秒重下整段视频;
- HTTP 源下
crypto.randomUUID不存在 → 实时功能静默失效; - IntersectionObserver 挂在脱离文档的元素上 → 全墙黑成快照;
- 采样 MP4 不支持 Range → 循环播放反复整段下载;
- 每 4 秒重连 RTSP 抽音频 → 12 秒音频要 17~19 秒取完;
- 焦点续期写成"释放+重建" → 每 50 秒周期性重缓冲;
-rw_timeout对 RTSP 无效 → 进程活着但零输出;- 采样计数当时钟 → 字幕超前画面 11~33 秒(第 9 章的核心案例)。
其中第 8 条是最值得记住的一条 :它不报错、不告警,只是安静地让字幕错位,而且只在"发布刚起"的那几秒触发------排查难度和修复价值都排在第一位。
20. 参考与延伸阅读
标准与协议
- RFC 8216 --- HTTP Live Streaming
- HLS Low-Latency(LL-HLS)扩展与
EXT-X-PART/EXT-X-PROGRAM-DATE-TIME语义 - RFC 7826 / RFC 2326 --- RTSP(MediaMTX 内部路径与上游拉流)
组件文档
- hls.js 配置与 API 文档(
lowLatencyMode、liveMaxLatencyDuration、playingDate) - MediaMTX 配置参考(LL-HLS 参数、路径与鉴权)
- FFmpeg 过滤器与协议文档(
-re、-c copy、scale、aresample) - Chrome DevTools Protocol(
Media/Page域,用于自动化验收)
云服务协议
- 阿里云 Qwen-Audio Realtime 服务端事件与使用指南(流式 ASR 的事件模型、
item_id关联、上下文清理)
方法论
- Google SRE 关于 SLI/SLO 与"可观测性优先于猜测"的实践
- 架构决策记录(ADR)与不变量(invariants)驱动的评审方式
结语
如果只能从这篇里带走三句话:
- 一源一条时间轴。 播放、转写、翻译、告警都挂在同一条轴上;任何"另开一路"的捷径,最终都要用一次大规模返工来偿还。
- 增强可缺席,主干不可停。 把系统显式分成主干和增强,是可用性设计里性价比最高的一件事。
- 用物理量验收,不要用状态码验收。 画面在动、声音在推进、文字在增长------只有这三件事同时成立,才能说系统在工作。
这套流程并不依赖某个特定的云厂商或开源组件;换掉 hls.js、MediaMTX、ASR 服务商,上面的十段流程与六条不变量依然成立。变的只是实现,不变的是"收得进来、播得流畅、听得清楚、说得明白、找得回来、判得有理"这条主线。
版本:v1.0 · 2026-09-16
说明:本文为通用流程调研,第 19 章的实测数字来自 Media-Int 项目的部署环境验证记录;其余章节为通用工程结论与选型建议。