电视 / 电台新闻监听平台:端到端流程技术调研

电视 / 电台新闻监听平台:端到端流程技术调研

主题 :从多路公网直播源,到可播放、可转写、可翻译、可检索、可研判的新闻情报,整条流水线该怎么拆、怎么选、怎么验。

定位 :通用流程调研为主线,最后用一个小节对照我们自己的项目(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. 结论速览

如果你只想拿走一页纸,看这里。

整条流水线的骨架是十段,可以粗暴地记成四句话:

复制代码
收得进来  →  播得流畅  →  听得清楚  →  说得明白  →  找得回来  →  判得有理
(接入)      (播放)      (转写)      (翻译)      (归档检索)   (研判告警)

八条最值得先记住的判断

  1. 中间必须有一层流媒体服务。浏览器只吃 HLS / DASH / WebRTC,上游却常是 RTSP、RTMP、带防盗链的 m3u8;把「对接上游」和「服务浏览器」这两件事分开,是整个系统的第一个解耦点。
  2. 播放与转写必须共用同一条时间轴。这是全篇第一原则。任何"另开一路去拉音频"的做法,都会在几分钟内积累出十几秒的漂移,字幕和画面再也对不上。
  3. 浏览器的解码器是稀缺资源,不是内存也不是带宽。一面墙能同时播几路,由 GPU 硬解通道数决定,通常是个位到十几路的量级。墙的设计要围绕这个约束做(焦点独占高清、非焦点降级)。
  4. HTTP 200 不等于在播。验收必须看到解码帧在增长、音频 PTS 在推进,而不是接口返回成功。
  5. 文字要晚一点到,但不能早到。让播放缓冲(3~5 秒)去吸收 ASR 的 1~2 秒延迟;反之则永远对不齐。
  6. 分析层必须是旁路。ASR、翻译、研判任何一个挂掉,播放和配置链路都不能受影响。
  7. 浏览器永远不能拿到上游地址。上游 URL、Header、Cookie、密钥必须在接入层被彻底吸收掉,这是安全底线也是合规底线。
  8. 能力要用曲线证明,不要用副本数证明。「四路没问题,所以八路也没问题」是这类系统最常见的错误结论。

1. 需求定义:什么叫「新闻监听平台」

同一个词,在不同组织里指三件很不相同的事。先把形态定清楚,后面的技术选型才有意义。

1.1 三种典型形态

形态 核心问题 关键指标 典型用户
播出监测型 我们的信号还在播吗?有没有黑屏、静帧、音量归零? 可用性、告警时效、误报率 播出机房、运维
内容情报型 外面在说什么?谁在说?口径怎么变? 覆盖率、检索能力、语种数、响应速度 情报/研究/宣传
合规审查型 有没有出现不该出现的内容? 召回率、可追溯性、留证完整度 风控、法务、监管

真实系统通常是混合体,但优先级必须排出一个,因为它直接决定架构重心:

  • 播出监测型 → 重心在接入层稳定性指标采集,转写只是辅助。
  • 内容情报型 → 重心在转写+翻译+检索+研判,播放只是给人核对用的。
  • 合规审查型 → 重心在存储与不可篡改性,音视频要能被"定格保存"。

1.2 能力清单

把需求翻译成可交付的能力,大致是这九项:

  1. 接入:把 N 路公网电视/电台直播源稳定收进来,支持回退与自愈。
  2. 播放:在浏览器里同时呈现多路画面,焦点路高清流畅,非焦点路可识别即可。
  3. 转写:把每路音频实时转成原始语种文字。
  4. 翻译:把非中文内容译成中文(或目标语),且不阻塞原文。
  5. 对齐:让文字、声音、画面落在同一条时间轴上。
  6. 归档:把文字按信源、时间、语种、片段存起来,可检索、可导出。
  7. 研判:对文本做结构化分析(分类、实体、事件、情绪、口径)。
  8. 告警:把达到阈值的事件推给人,并附带可核查的证据。
  9. 运维:看得见每路健康度、算得清容量、扛得住重连和抖动。

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 两条贯穿全程的约束线

在进入细节前,先立两条不可协商的线,后面所有取舍都从这里推导:

  1. 一源一条时间轴。一个频道在同一时刻,只应该有一个"现在"。画面、声音、文字、缓存、告警,全部挂在这条轴上。任何"为了省事另拉一路"的设计,都是给未来埋漂移。
  2. 增强可缺席,主干不可停。把系统显式分为"主干"(配置、接入、播放)和"增强"(转写、翻译、研判、告警)。增强层全部挂掉时,主干应当仍然是一个可用的直播墙。

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.levelwall_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/16127.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 换源纪律

这是流程问题,不是技术问题,但值得写进规范:

  1. 实测(不是"看起来像");
  2. 同国家/同类型优先替换(别为了可用性牺牲信源结构);
  3. 每次改配置先备份、留版本号、留回滚路径;
  4. 不要为了测试通过而改安全配置(比如把没验证的域名加进白名单)。

4. 阶段二:接入与转封装

4.1 为什么必须有中间层

新手最容易犯的错,是把上游 m3u8 直接塞给浏览器播放器。它会在三天内以四种方式崩掉:

  1. 协议不通:RTSP / RTMP / SRT / Icecast 浏览器根本不认。
  2. 跨域与防盗链 :上游 Access-Control-Allow-Origin 不为你而设;很多源校验 Referer / UA / Cookie,浏览器发不了自定义 Header。
  3. 凭证暴露:要让浏览器直接拉,就得把带 token 的 URL 送到前端------安全上不可接受。
  4. 上游格式千奇百怪 :同一份 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     周期性采样片段(可选)

这样带来三个好处:

  1. 资源按需:focus 只在有人聚焦时存在,没有人看就回收。项目实测中,"释放最后一个聚焦会话即停止上游拉流"是显著降低成本的一招。
  2. 时钟一致 :focus 的浏览器播放与分析消费的是同一条发布流,天然同源。
  3. 隔离:分析抽音频不再干扰播放路径。

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 不只是反向代理,它还承担四件事,每一件都踩过坑:

  1. Cookie 注入:把内部会话令牌映射成媒体服务器的 cookie,浏览器不需要知道任何密钥。
  2. 关闭缓冲 :LL-HLS 必须 proxy_buffering off,否则 nginx 会把 part 攒起来再发,低延迟白做。
  3. 临时目录 :这是个经典事故------nginx 以部署用户身份运行,client_body_temp_path / proxy_temp_path 还指向 /var/lib/nginx/...,于是所有超过内存缓冲的请求体全部 Permission denied。表现为客户端 ERR_CONTENT_LENGTH_MISMATCH<video>MEDIA_ERR_NETWORK → 每块瓦片每秒重下整段视频。务必把临时路径显式指到有写权限的目录下。
  4. 路径重写 :把对外友好的 /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=SUBTITLESCLOSED-CAPTIONS / WebVTT)。它的质量高于 ASR,且零延迟、零成本

所以正确的策略是分级:

  1. 源带字幕轨且正在产出内容 → 直接用字幕,跳过 ASR;
  2. 源没有字幕轨 → ASR;
  3. 两者都有 → 字幕优先,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 / 墙钟 上游编码器 上游自己
清单 PDTEXT-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 正确方案:让消费者使用播放器正在用的那份时间戳

核心思想一句话:

不要"估算"时间,去"读"时间。

具体做法:

  1. 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
  2. 前端用 hls.js 提供的 playingDate 做匹配。因为它和清单 PDT 同源,文字时间 == 播放时间,按构造对齐
  3. 文字会比画面晚 1~2 秒(分片必须先可用),这点延迟正好被 5 秒播放缓冲吸收。

为什么这是"架构级"的修复而不只是补丁:它把"对齐"从"依赖速率假设的推算"变成了"同一个字段的两次读数",速率假设消失了,漂移从根上不存在。

9.4 播放缓冲是特性,不是浪费

很多人想把延迟压到最低,于是缓冲设成 1~2 秒。这在"转写+翻译"的系统里是错误目标

复制代码
画面已经进入缓冲队列 5 秒后才真正显示
    ↓
ASR 在这 5 秒内完成转写(约 2~4 秒)
    ↓
文字在画面出现前就准备好了 → 观感是"字幕跟着声音走"

目标是"感知同步",不是"最低延迟"。 宁可整体延迟 5 秒,也不要画面和字幕各说各的。

9.5 匹配逻辑的边界情况

情况 正确处理
当前时刻无匹配记录(静音间隙) 保持上一句(hold),不清空
有记录但译文未到 只显示原文一行,不要打印两行相同的字
用户刚换台 立刻清除 hold,新台不得继承旧台字幕
播的是缓存/采样视频 字幕必须绑定 sample ID + 片段起止时间,绝不能混用实时行或上一版缓存
播放器暂停 保持当前句,不要继续推进
历史浏览 历史行可滚动查阅,但只有当前匹配行高亮,历史行不得冒充"当前字幕"

9.6 怎么度量同步误差

只测"窗口是否连续"是不够的------那只证明相对时序 正确,不证明绝对同步

可行的度量方法:

  1. 找一段有明确视觉事件(整点报时、片头台标、比分变化)的素材;
  2. 记录该画面出现的 playingDate
  3. 找到与之对应的那条字幕行的 audio_started_at
  4. 差值即为端到端字幕偏差。多测几次取中位数与最大值。

没有这个数字之前,不要声称"字幕已同步"。


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        -- 入库时刻(≠ 音频时刻,不要混用)

两个必须强调的点:

  1. audio_started_at 是音频时间,不是入库时间 。用 created_at 反推音频时刻,是 9.2 节漂移问题的另一种写法。
  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_trgmLIKE '%关键词%' 加 GIN 索引,足够用。

大规模 / 需要相关性排序:上 Meilisearch(轻)或 Elasticsearch(重)。

10.4 导出与呈现

真实用户的第一个需求往往是"给我导一份"。要点:

  • TXT 导出必须带 UTF-8 BOM,否则 Windows 记事本打开是乱码(这个细节会立刻被用户注意到);
  • 按"频道 + 当天"聚合 成频道日报,预览和下载使用同一份合并内容(不要两套逻辑,否则导出和预览不一致);
  • 输出的每一行都要保留时间标记与来源,这样才能支撑"事后把文字对齐回音视频"的目标;
  • 三栏浏览(日期导航 / 原文 / 译文)比单栏列表可用性高一个档次,且左右两栏滚动要双向同步

10.5 一个易被忽略的细节

冷启动要能立刻显示历史

刚打开页面时,接口还没回来,屏幕全空------体验上是"坏了"。做法:

  • 前端把最近 N 条(如 50 条)字幕镜像到 localStorage,冷启动即渲染,并在状态上标注"本地缓存 N 条 · 同步中";
  • 视频同理:把最近一段采样缓存到 Cache API,首帧不用等网络。

这是纯粹的体验工程,但对"这系统是不是活的"这个感知影响极大


11. 阶段九:AI 研判与告警

11.1 触发方式:为什么不应该默认全自动

对每一段转写自动调一次大模型,会带来三个问题:

  1. 成本:25 路 × 每 4 秒一次 ≈ 每天 54 万次调用;
  2. 噪声:绝大多数片段是常规播报,产出大量无意义结论;
  3. 误导:模型看到孤立的一句话,很容易做出过度解读。

更稳的设计是混合触发

  • 人工触发为主:操作员看到有意思的内容,点"对当前转录发起研判"------这是明确的需求信号,准确率最高;
  • 规则/关键词自动触发为辅:命中预置词表、或某频道某时段出现异常密度时,自动取样分析;
  • 定时摘要:按小时/按天对某频道做聚合摘要,成本可控。

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 应用的设计逻辑不同:

  1. 信息密度优先于美观。大屏是"一直看"的场景,减少空白、减少无意义的装饰。
  2. 状态要靠颜色和形状立刻可读。哪些在播、哪些断了、哪路是焦点,扫一眼就要知道。
  3. 不要放"操作日志"式的元素。顶部导航堆一排"源/事件/状态"入口,会持续分散注意力;把入口收在侧边或按需展开。

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 必须先做压力测试的三个数

  1. 2/4/6/8 路并发转写的实际延迟曲线(找拐点,而不是找"能跑");
  2. 浏览器同时解码多少路开始掉帧(换机器结果不同,必须在目标机上测);
  3. 一路转码的真实 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 三条非常具体的断言

以下三条可以直接抄进测试用例,它们的失败率非常高:

  1. sameVideoElement === true :焦点交接前后,主屏的 <video> 必须是同一个 DOM 节点。
  2. 非焦点瓦片每秒请求数 == 0:焦点切换后,被释放的瓦片不应再产生任何媒体请求。
  3. 每路采样视频只有 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 章清单):

  1. nginx 临时目录权限 → 每秒重下整段视频;
  2. HTTP 源下 crypto.randomUUID 不存在 → 实时功能静默失效;
  3. IntersectionObserver 挂在脱离文档的元素上 → 全墙黑成快照;
  4. 采样 MP4 不支持 Range → 循环播放反复整段下载;
  5. 每 4 秒重连 RTSP 抽音频 → 12 秒音频要 17~19 秒取完;
  6. 焦点续期写成"释放+重建" → 每 50 秒周期性重缓冲;
  7. -rw_timeout 对 RTSP 无效 → 进程活着但零输出;
  8. 采样计数当时钟 → 字幕超前画面 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 文档(lowLatencyModeliveMaxLatencyDurationplayingDate
  • MediaMTX 配置参考(LL-HLS 参数、路径与鉴权)
  • FFmpeg 过滤器与协议文档(-re-c copyscalearesample
  • Chrome DevTools Protocol(Media / Page 域,用于自动化验收)

云服务协议

  • 阿里云 Qwen-Audio Realtime 服务端事件与使用指南(流式 ASR 的事件模型、item_id 关联、上下文清理)

方法论

  • Google SRE 关于 SLI/SLO 与"可观测性优先于猜测"的实践
  • 架构决策记录(ADR)与不变量(invariants)驱动的评审方式

结语

如果只能从这篇里带走三句话:

  1. 一源一条时间轴。 播放、转写、翻译、告警都挂在同一条轴上;任何"另开一路"的捷径,最终都要用一次大规模返工来偿还。
  2. 增强可缺席,主干不可停。 把系统显式分成主干和增强,是可用性设计里性价比最高的一件事。
  3. 用物理量验收,不要用状态码验收。 画面在动、声音在推进、文字在增长------只有这三件事同时成立,才能说系统在工作。

这套流程并不依赖某个特定的云厂商或开源组件;换掉 hls.js、MediaMTX、ASR 服务商,上面的十段流程与六条不变量依然成立。变的只是实现,不变的是"收得进来、播得流畅、听得清楚、说得明白、找得回来、判得有理"这条主线。


版本:v1.0 · 2026-09-16

说明:本文为通用流程调研,第 19 章的实测数字来自 Media-Int 项目的部署环境验证记录;其余章节为通用工程结论与选型建议。

相关推荐
中视会议3 小时前
实时音视频 + AI Agent,打造新一代远程运维系统
实时音视频·智能巡检·实时远程运维·远程专家协作·asr语音识别
cc5725026533 小时前
2026 供应链数据分析师校招 JD 拆解|工具、新增能力、面试与备考路线
数据分析
m0_547486663 小时前
《Python数据分析与可视化项目教程》全套PPT课件2026
python·数据分析·数据可视化
甲维斯3 小时前
便宜又好用!Codex+Luna分析网站流量!
人工智能·数据分析
Data-Miner4 小时前
AI处理数据的脚本复用与逻辑复用有什么区别?哪种方式更能沉淀工作成果
数据分析·excel
Q26433650235 小时前
【有i源码】基于大数据的城市交通流量与出行特征可视化分析平台-基于Hadoop的城市交通拥堵关联规则与异常检测研究
大数据·hadoop·机器学习·数据挖掘·数据分析·spark·数据可视化
hz567896 小时前
音频视频sdk开发实践:从实时通话到视频互动的完整方案
音视频·实时音视频·信息与通信
IT毕设实战小研6 小时前
基于大数据的人工智能社交媒体情绪分析与可视化的设计与实现
大数据·信息可视化·数据挖掘·数据分析·课程设计
生信大杂烩6 小时前
Xenium H&E空间原位可视化——细胞轮廓、基因表达与转录本可视化
python·算法·数据分析