这里写自定义目录标题
- [多路直播 + AI 实时翻译:一套「会翻译的直播墙」的工程复盘](#多路直播 + AI 实时翻译:一套「会翻译的直播墙」的工程复盘)
-
- 目录
- [0. 先给结论(TL;DR)](#0. 先给结论(TL;DR))
- [1. 我们到底要做一个什么系统](#1. 我们到底要做一个什么系统)
- [2. 整体架构:一张图看懂](#2. 整体架构:一张图看懂)
- [3. 播放层选型:为什么是 hls.js](#3. 播放层选型:为什么是 hls.js)
-
- [3.1 结论](#3.1 结论)
- [3.2 为什么](#3.2 为什么)
- [3.3 同类方案横向对比](#3.3 同类方案横向对比)
- [4. 接入层选型:为什么是 MediaMTX](#4. 接入层选型:为什么是 MediaMTX)
-
- [4.1 结论与职责](#4.1 结论与职责)
- [4.2 同类方案横向对比](#4.2 同类方案横向对比)
- [5. 关键设计:一套「两档播放」模型](#5. 关键设计:一套「两档播放」模型)
- [6. 转录层选型:ASR 与翻译怎么选](#6. 转录层选型:ASR 与翻译怎么选)
-
- [6.1 两条转录车道](#6.1 两条转录车道)
- [6.2 ASR 选型对比](#6.2 ASR 选型对比)
- [6.3 翻译选型对比](#6.3 翻译选型对比)
- [6.4 端到端开源方案参考](#6.4 端到端开源方案参考)
- [6.5 存储归档](#6.5 存储归档)
- [7. 最难的部分:让字幕跟着画面走](#7. 最难的部分:让字幕跟着画面走)
-
- [7.1 时钟从哪来](#7.1 时钟从哪来)
- [7.2 一个真实的坑:原生 HLS 没有时钟](#7.2 一个真实的坑:原生 HLS 没有时钟)
- [7.3 诚实的边界](#7.3 诚实的边界)
- [8. AI 研判:让转录文本真正「有用」](#8. AI 研判:让转录文本真正「有用」)
- [9. 踩坑实录:9 个真实花时间的问题](#9. 踩坑实录:9 个真实花时间的问题)
-
- [坑 1 · nginx 写不了代理临时文件 → 播放器疯狂重下](#坑 1 · nginx 写不了代理临时文件 → 播放器疯狂重下)
- [坑 2 · 纯 HTTP 页面没有 `crypto.randomUUID` → 功能静默死亡](#坑 2 · 纯 HTTP 页面没有
crypto.randomUUID→ 功能静默死亡) - [坑 3 · `IntersectionObserver` 挂在被销毁的 `<video>` 上](#坑 3 ·
IntersectionObserver挂在被销毁的<video>上) - [坑 4 · `sample.mp4` 不支持 Range 请求](#坑 4 ·
sample.mp4不支持 Range 请求) - [坑 5 · 聚焦会话:页面刷新被 409 拒绝](#坑 5 · 聚焦会话:页面刷新被 409 拒绝)
- [坑 6 · 聚焦会话:每 50 秒重连一次](#坑 6 · 聚焦会话:每 50 秒重连一次)
- [坑 7 · 一个 ffmpeg 参数让采样全停](#坑 7 · 一个 ffmpeg 参数让采样全停)
- [坑 8 · 浏览器解码器是有预算的](#坑 8 · 浏览器解码器是有预算的)
- [坑 9 · 一个没解决的坑:上游信源本身损坏(如实记录)](#坑 9 · 一个没解决的坑:上游信源本身损坏(如实记录))
- [10. 经验总结:做「视频 + AI」系统的 8 条实践](#10. 经验总结:做「视频 + AI」系统的 8 条实践)
- [11. 结语](#11. 结语)
- 参考
多路直播 + AI 实时翻译:一套「会翻译的直播墙」的工程复盘
项目 :一个多信源电视/电台直播监控 + AI 转录翻译系统(下文简称「本系统」)
主题 :浏览器端 HLS 播放、多路直播接入、AI 实时转录与翻译、以及音画字幕同步
适合读者 :正在做「视频 + AI」方向、或准备自建直播/转录管线的工程师
阅读时长 :约 15 分钟。建议顺序阅读,第 2 章的架构图是全篇的地基。
核心亮点:一套「两档播放」模型 + 播放/转录共用同一时钟,让几十路直播墙稳定运行、字幕跟着画面走。
目录
- 先给结论(TL;DR)
- 我们到底要做一个什么系统
- 整体架构:一张图看懂
- [播放层选型:为什么是 hls.js](#播放层选型:为什么是 hls.js)
- [接入层选型:为什么是 MediaMTX](#接入层选型:为什么是 MediaMTX)
- 关键设计:一套「两档播放」模型
- [转录层选型:ASR 与翻译怎么选](#转录层选型:ASR 与翻译怎么选)
- 最难的部分:让字幕跟着画面走
- [AI 研判:让转录文本真正「有用」](#AI 研判:让转录文本真正「有用」)
- [踩坑实录:9 个真实花时间的问题](#踩坑实录:9 个真实花时间的问题)
- [经验总结:做「视频 + AI」系统的 8 条实践](#经验总结:做「视频 + AI」系统的 8 条实践)
- 结语
- 参考
0. 先给结论(TL;DR)
如果你只想知道结论,这一节足够;想理解「为什么」,请从第 1 章往下读。本文所有结论均来自真实项目的踩坑与复盘。
- 浏览器播放 :用 hls.js(MSE 播放 HLS),原生 HLS 仅作兜底。
- 流媒体服务器 :用 MediaMTX,负责 RTSP 收流 + 低延迟 HLS(LL-HLS)分发。
- 媒体处理 :用 ffmpeg 拉流、转码、采样、抽音频。
- 实时转录 :用实时 ASR (本项目取 Qwen-Audio Realtime),翻译用大模型。
- 同类可选项 :播放器(Video.js / Shaka / dash.js)、服务器(SRS / ZLMediaKit /
OvenMediaEngine)、ASR(Whisper / FunASR)、翻译(NLLB / OPUS-MT)。
全篇有两个反复出现的核心观点,先划重点:
- 播放和转录必须共用同一条时序 ------选中的频道,画面、声音、字幕走同一条链路、
同一个时钟,才谈得上「同步」。 - HTTP 200 不等于真的在播------验收必须看到解码帧增长 / 音频推进,而不是接口成功。
1. 我们到底要做一个什么系统
这是一个「直播监控指挥台」:把国内外多路电视与电台直播源接入进来,在一面大屏墙上同时监控,对每一路做实时语音转录 、翻译成中文 ,并把结果归档成可检索、可下载的字幕库,还能对转录文本发起 AI 研判。
一句话概括:让每一路直播都「听得懂、看得见、可检索」。
它同时踩在三个技术领域上,也决定了本文的三条主线:
① 浏览器端视频播放 → 一个页面稳定解码多路 HLS 直播
② 流媒体接入与转发 → 把五花八门的公网源(HLS/RTSP)收进来,统一转成浏览器能播的格式
③ AI 转录 + 翻译 → 把直播音频实时转成原文并翻译,还要和画面对齐
把它拆成一个「输入 → 处理 → 输出」的流水线,理解起来最自然:
多路公网直播源 → 统一接入与转发 → 浏览器播放 → AI 转录/翻译 → 归档与研判
(HLS/RTSP) (ffmpeg+MediaMTX) (hls.js) (ASR+LLM) (PostgreSQL+Agent)
后面每一章,都对应这条流水线上的一段。
2. 整体架构:一张图看懂
先看全貌。这张图是后面所有章节的地基:
┌────────────────────────── 公网直播源 ──────────────────────────┐
│ HLS(m3u8) / RTSP / 电台音频流 ...... │
└───────────────────────────┬─────────────────────────────────┘
│ ffmpeg 拉流(worker 进程)
▼
┌──────────────────────────► MediaMTX ◄──────────────────────────┐
│ RTSP 收流 + LL-HLS 分发 │
│ │ │
│ RTSP(给后端 ASR) │ LL-HLS(给浏览器) │
▼ ▼ │
后端连续 ASR nginx(同源代理) │
(WebSocket 长连接) │ │
│ ▼ │
▼ 浏览器 hls.js 播放器 │
原文 + 译文 ───────────────────────► 字幕栏 / 管理页 │
再记住两条贯穿全项目的架构门禁(这是设计底线,不是可选项):
- 浏览器永远拿不到上游地址 。前端只能访问平台 API、SSE、快照和 HLS,
永远不接触上游 URL / Header / Cookie / 密钥。所有源信息都收在服务端。 - AI 分析可以缺席,主链路不能停 。分析服务停掉或缺席时,配置、媒体接入、
播放这条主链路仍然正常工作------转录是「增强」,不是「依赖」。
💡 读到这里,你应该已经理解 :系统是分层的,播放和转录是两条并行消费
同一条媒体流的链路。这个「一源两消费」的结构,是后面所有设计取舍的根源。
3. 播放层选型:为什么是 hls.js
3.1 结论
浏览器播放器用 hls.js 1.5.17。核心代码非常典型:
ts
// 播放器模块:创建并挂载 hls.js
const hls = new Hls({
lowLatencyMode: true, // 开启低延迟 HLS(LL-HLS)
liveMaxLatencyDuration: 12, // 直播最大允许延迟
});
hls.loadSource("/hls/<channel>/focus/index.m3u8");
hls.attachMedia(video);
3.2 为什么
浏览器的 <video> 原生只对 HLS 有部分支持(Safari 好一些,Chromium 基本不支持)。
hls.js 是社区事实标准 :它把 HLS 的 m3u8 清单和 ts/fmp4 分片,用 MSE
(Media Source Extensions)喂给 <video>,让 Chrome / Edge / Firefox 都能播 HLS。
它有两个对本项目不可替代的能力:
lowLatencyMode:支持 LL-HLS(分片级别 200ms ~ 1s),这是「单击一个频道
能立刻看到实时画面」的前提;playingDate(PROGRAM-DATE-TIME) :能从 HLS 清单里拿到每一段的墙钟时间,
这是字幕能和画面对齐的关键------没有它,字幕只能靠「猜时间」(见第 7 章)。
3.3 同类方案横向对比
| 项目 | 协议 | 特点 | 适合谁 |
|---|---|---|---|
| hls.js(本项目) | HLS | 轻量、LL-HLS 支持好、事实标准 | Web 端播 HLS 的默认答案 |
| Video.js | HLS/DASH | 生态最全、插件多、皮肤丰富 | 需要成熟商业级 UI |
| Shaka Player(Google) | DASH/HLS | DRM 强、规范严谨 | 需要 Widevine/PlayReady |
| dash.js | DASH | DASH 参考实现 | MPEG-DASH |
| Plyr / Vidstack | 皮肤层 | 好看的控件层,常与 hls.js 组合 | 只想要控件 |
| Clappr | 插件化 | 可扩展播放器框架 | 需要自定义插件 |
| ExoPlayer / Media3 | 多协议 | 移动端标准 | Android App |
| AVPlayer | HLS 原生 | iOS/Safari 原生 | 苹果生态 |
选型结论 :Web 端播 HLS,hls.js 是默认答案 ;要花哨控件就套 Plyr/Vidstack,
要完整商业生态就 Video.js,要 DRM 就 Shaka。
4. 接入层选型:为什么是 MediaMTX
4.1 结论与职责
流媒体服务器用 MediaMTX 1.20(原名 rtsp-simple-server,Go 编写,单文件即可运行):
- 收流:worker 用 ffmpeg 把公网源(m3u8 / rtsp)拉下来,以 RTSP 推到 MediaMTX;
- 分发 :MediaMTX 同时对外提供 RTSP 和 LL-HLS 。前端播放器走 LL-HLS,
后端 ASR 走 RTSP,两路消费者互不干扰。
它比传统的 nginx-rtmp 更现代,开箱即用支持 LL-HLS,配置极简。一个实际调优细节:
yaml
# mediamtx.yml(流媒体服务器配置)
readTimeout: 30s # 默认 10s 太短:直播源偶发卡顿会让 MediaMTX 直接拆流
writeTimeout: 30s # 放宽到 30s,让上游扛过瞬断,避免频道闪断
4.2 同类方案横向对比
| 项目 | 语言 | 特点 |
|---|---|---|
| MediaMTX(本项目) | Go | 单文件、RTSP/LL-HLS/WebRTC、配置极简 |
| SRS | C++ | 国产老牌、RTMP/HLS/WebRTC 全能 |
| ZLMediaKit | C++ | 国产高性能、协议极全 |
| OvenMediaEngine | C++ | 主打亚秒级 LLHLS / WebRTC |
| LiveKit | Go | WebRTC 实时音视频、房间模型 |
| go2rtc | Go | 智能家居 RTSP 转 WebRTC/HLS |
| nginx-rtmp | C | 经典但已停止维护 |
| Ant Media Server | Java | 企业级、社区版开源 |
选型结论 :只要「收流 + 分发」,MediaMTX / go2rtc 最省心;要高并发多协议,
SRS / ZLMediaKit 更成熟;要做实时互动/连麦,选 LiveKit / OvenMediaEngine。
5. 关键设计:一套「两档播放」模型
这是本项目最值得分享的设计,直接决定了系统的稳定性。核心思路:把「展示」和「采集」解耦。
问题 :一个页面同屏几十路直播,浏览器扛不住------每个 HLS 流都要独立解码器
和网络连接,Chromium 对同时解码的 MediaSource 数量有硬上限。
解法:把「看得见的墙」和「真正在拉的流」解耦,做两档播放:
┌──────────────────────── 浏览器墙(N 路平铺)────────────────────────┐
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ 缩略图 1 │ │ 缩略图 2 │ │ 缩略图 3 │ │ ...... │ 非聚焦: │
│ │ 20s循环MP4│ │ 20s循环MP4│ │ 20s循环MP4│ │ │ 服务端采样 │
│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │
│ │
│ ┌────────────────────────────────────────────────┐ │
│ │ 聚焦(主屏)= 真·LL-HLS 直播 │ 单击→申请 │
│ └────────────────────────────────────────────────┘ focus 会话 │
└────────────────────────────────────────────────────────────────────┘
- 非聚焦的缩略图 :不播直播流 ,而是播一段 20 秒循环的采样 MP4(服务端每
5 分钟重新采一次)。N 路缩略图只占极少解码和带宽,画面却一直是「动」的。 - 聚焦(主屏)的那一路 :单击切到主屏时才申请一个 focus 会话 ,服务端
单独拉一路低延迟 RTSP → LL-HLS,浏览器切到/hls/<channel>/focus/。 - 同一时刻只有一路真正在拉公网直播源,其余全部走采样循环。
ts
// 非聚焦格子:走服务端生成的 20 秒 MP4 循环
video.src = `/api/v1/channels/${id}/sample.mp4`;
// 聚焦格子:申请 focus 会话后切到 LL-HLS
video.src = `/hls/${id}/focus/index.m3u8`;
这个设计把「几十路并发解码」的浏览器压力,降到了「一路高清 + 若干路低码率 MP4」。
💡 读到这里,你应该已经理解 :稳定性的关键不是「拼命并行拉流」,而是
把「展示」和「采集」解耦。这几乎是所有多路直播墙的通用思路。
6. 转录层选型:ASR 与翻译怎么选
6.1 两条转录车道
接入之后就是转录。本项目把转录分成两条车道,兼顾「实时」与「覆盖」:
-
聚焦快车道(连续 ASR) :某频道被聚焦时,服务端用一个 ffmpeg 持续读取该
频道的 focus 路径,按 200ms 一包 PCM 连续上传到 ASR 的单个 WebSocket
长连接 ;每约 4 秒提交一个识别窗口,结果按item_id归档。这样被选中的
这一路,画面、声音、字幕共用同一个时钟。 -
后台慢车道(全目录) :其余频道仍由调度器以短片轮询方式转录,保证
「全信源都有覆盖」,但不宣称逐秒无遗漏(这是诚实的边界,见第 7 章)。聚焦频道 ─► ffmpeg 连续读流 ─► 单 WebSocket ─► 4s 窗口提交 ─► 原文+译文
其他频道 ─► 短片轮询 ────────► 覆盖但不保证逐秒
6.2 ASR 选型对比
| 方案 | 类型 | 特点 | 代价 |
|---|---|---|---|
| Qwen-Audio Realtime(本项目) | 云服务 | 中文强、实时好、接入快 | 按量计费、受并发配额 |
| Whisper / faster-whisper | 开源 | 多语言、离线、社区标准 | 自扛 GPU、延迟与并发 |
| whisper.cpp | 开源 | 无 GPU 也能跑、边缘友好 | 精度/速度折中 |
| FunASR(达摩院) | 开源 | 中文场景强,含 VAD/标点 | 需自建服务 |
| Vosk | 开源 | 轻量、流式、小语种 | 精度一般 |
| wav2vec 2.0 | 开源 | 可针对特定语言微调 | 需训练 |
取舍 :自建(Whisper/FunASR)无按量成本、数据不出内网,但要自己扛 GPU 和并发;
云服务(Qwen-Audio Realtime)国内合规、接入快、实时性好,代价是按量计费和配额。
6.3 翻译选型对比
| 方案 | 特点 |
|---|---|
| 大模型(qwen3.8-max)(本项目) | 整段翻译,兼顾语境和术语,中文自然 |
| NLLB-200(Meta) | 200 语言,开源旗舰 |
| OPUS-MT / Marian | 按语言对分发的轻量模型,易自托管 |
| Argos Translate | 离线、纯 Python,安装即用 |
6.4 端到端开源方案参考
- WhisperLive / whisper-stream:把 Whisper 做成实时流式服务;
- RealtimeSTT + RealtimeTTS:实时识别 + 合成组合件;
- faster-whisper-server:OpenAI 兼容的转录 HTTP 服务。
它们大多只解决「音频 → 文字」。本项目的差异化在于多路直播 + 字幕与画面按时钟
对齐 + 归档检索------这正是最花功夫的部分,也是下一章的主题。
6.5 存储归档
转录结果写入 PostgreSQL 16 (Docker 容器,仅监听本机回环端口,不暴露公网),
按「频道 + 北京日期」融合成频道日报,支持按电视台/电台、频道、来源筛选,并可导出
带 UTF-8 BOM 的 TXT(兼容 Windows 记事本)。未配置数据库时,本地开发回退到
SQLite。
7. 最难的部分:让字幕跟着画面走
「能转录」不难,「让字幕和画面对上」才是真难点。 本章是全篇的技术核心。
7.1 时钟从哪来
对齐的核心是时间锚点:
- 播放侧 :从 hls.js 的
playingDate(HLS 清单的PROGRAM-DATE-TIME)拿到
当前播放的墙钟时间; - 转录侧 :每条字幕记录
audio_session_id、audio_sequence、媒体起止时间; - 渲染时 :只显示「当前 HLS 播放时间落入的那一个字幕窗口」的原文和译文,
过期或未来的记录一律不显示。
ts
// 电台/电视共用:按 HLS 播放时刻选择当前字幕窗口
const playingDate = hls.playingDate; // 墙钟时间
showOnly(recordsInWindow(playingDate, 4 /* 秒 */)); // 4 秒窗口内的那一条
7.2 一个真实的坑:原生 HLS 没有时钟
刚开始电台用原生 HLS 播放,能播但 playingDate 永远是空,字幕栏一直空白。
修复 :电台也优先用 hls.js 来拿 PROGRAM-DATE-TIME;只有真的不能用 hls.js
时才回退原生,且此时宁可字幕空白,也不猜时间。
7.3 诚实的边界
当前锚点仍是「接收端估算」:源 PTS/RTCP 与 HLS 播放时刻的绝对误差尚未独立标定 。
所以做到的是片段级(约 4 秒窗口)对齐 ,不是毫秒级------这一点必须在文档里如实写明,
不能靠「窗口连续」就宣称「绝对同步」。
8. AI 研判:让转录文本真正「有用」
转录只是「原料」,本项目的目标是让 AI 在文本上做用户触发的研判 (不是视频格子上
一直跑的实时分析按钮):
- 值班员对已存储的转录文本发起研判,由大模型给出结论;
- 界面右下角接入外部 Agent 平台 (
videos_analysis_agent),支持流式回答、
按频道隔离会话、一键生成「今日简报 / 关键要闻 / 重点关注」。
一个工程上的关键处理:模型会在答案里混入「思维链」和内部标记。桥接层直接丢弃
思维链增量 ,并在前端渲染前剔除 <!-- ... --> 类注释,所以用户只看到干净答案,
同时保留逐字流式体验。
Upstream SSE ─► 桥接层归一化(status/delta/final) ─► 丢弃思维链 ─► 前端流式渲染
9. 踩坑实录:9 个真实花时间的问题
这一章是本文最值钱的部分。 所有问题都在真实 Chromium(CDP 远程调试)驱动公开
页面时复现并修复的,而不是靠看日志猜。按「现象 → 根因 → 修复 → 教训」组织。
坑 1 · nginx 写不了代理临时文件 → 播放器疯狂重下
- 现象 :每个墙格每秒钟重下一整段
sample.mp4,两分钟内一个浏览器发出约
3560 次请求,客户端报ERR_CONTENT_LENGTH_MISMATCH,<video>抛MEDIA_ERR_NETWORK。 - 根因 :nginx 以部署用户运行,
/var/lib/nginx/proxy/没有写权限 ,所有超出
内存缓冲的响应体都失败(日志里 2 万多条Permission denied)。 - 修复 :把
client_body_temp_path、proxy_temp_path等全部改到$DATA_DIR/web/下。 - 教训:播放问题不一定是播放器的锅,反向代理的一个权限就能制造「雪崩式重下」。
坑 2 · 纯 HTTP 页面没有 crypto.randomUUID → 功能静默死亡
- 现象 :实时按钮从来没用过、配置发布失败、事件发不出去------全都没报错,只是悄悄不工作。
- 根因 :
crypto.randomUUID只在 Secure Context(HTTPS 或 localhost) 下存在。
公开端点走 HTTP,调用直接抛TypeError,把所有需要 id 的逻辑一起带崩。 - 修复 :自己封装
randomId()(有randomUUID就用,否则用getRandomValues)。 - 教训:HTTP 下的浏览器 API 会用「安全上下文」给你挖坑,光看后端日志永远找不到。
坑 3 · IntersectionObserver 挂在被销毁的 <video> 上
- 现象:墙重绘后所有格子变回静态快照。
- 根因 :重绘替换了
<video>元素,但旧观察者还盯着已从 DOM 移除的旧节点 ,
报告「不可见」并顺手把新格子stop()了。 - 修复 :每次
ensure()都先disconnect()再重新观察。 - 教训:任何「观察者/订阅者」模式,都要在元素重建时重新绑定。
坑 4 · sample.mp4 不支持 Range 请求
- 现象:Chromium 无法在片段内 seek,每循环一次就重下整个文件。
- 修复 :接口支持
206 Partial Content+Content-Range,并支持If-None-Match
返回304。 - 教训:媒体文件接口必须支持 Range,否则浏览器会退化成「每次重下」。
坑 5 · 聚焦会话:页面刷新被 409 拒绝
- 现象 :
sessionStorage里的旧会话让FocusManager.acquire判定冲突,墙卡在
采样循环且不报错。 - 修复 :同一用户 + 同一频道复用会话并刷新 TTL,换频道才移交槽位。
坑 6 · 聚焦会话:每 50 秒重连一次
- 现象:续期逻辑是「先释放再申请」,导致周期性重新缓冲。
- 修复 :同会话走「纯 TTL 刷新」,不再重启 LL-HLS。
坑 7 · 一个 ffmpeg 参数让采样全停
- 现象:采样画面冻结 6 分钟,worker 日志却一片安静。
- 根因 :
-rw_timeout在服务器上的 ffmpeg 4.4 对 RTSP 无效 ,直接把每次采样
搞崩,而且失败不报错。 - 修复 :去掉该参数,改用
asyncio.wait_for(duration + 30s)加超时,并在失败时打日志。 - 教训:参数「看起来有效」不代表「真的生效」,静默失败最可怕。
坑 8 · 浏览器解码器是有预算的
- 现象 :一次性初始化太多 HLS 实例,连主屏也卡在
HAVE_METADATA。 - 根因:Chromium 能同时解码的 MediaSource 数量有限。
- 修复 :只有进入视口的非聚焦预览 才分配解码器,最多 3 个;移出视口就释放,
为「主屏 + 缓存接续」预留容量。
坑 9 · 一个没解决的坑:上游信源本身损坏(如实记录)
- 现象 :某路电视直播源,浏览器截图严重马赛克/灰屏。服务端直接捕获 focus 输出也
损坏,用独立 ffmpeg 解同一上游同样报错(一次统计 77 条 H.264error while decoding MB),priority=90 的备用源也有 64 条错误。 - 结论 :问题定位在上游输入的解码阶段 ,尚未区分是源内容损坏、传输损坏,还是
与现有 ffmpeg 的兼容性。 - 我们的坚持 :不靠调高分辨率、不靠循环旧缓存、不靠忽略错误来假装修好 。
没有干净可用的替代源之前,如实说明这是上游问题。 - 教训 :不是所有 bug 都能修。能定位、能如实上报边界,本身就是工程能力。
10. 经验总结:做「视频 + AI」系统的 8 条实践
- 播放和转录共用一条时序 。选中的频道,画面、声音、字幕走同一条链路、同一个
时钟,才能谈「同步」;多条链路各转各的,字幕永远只能「大概齐」。 - 区分「能拉到流」和「真的在播」 。HTTP 200 ≠ 有画面。验收要看
readyState、
解码帧是否增长、音频是否推进。 - 把「展示」和「采集」解耦 。多路墙别硬并行拉流;用低码率采样回放 + 单路
高清聚焦,稳定性会高一个数量级。 - 给浏览器解码器做预算 。多路视频页面一定要有「只在视口内分配解码器」的策略,
否则主屏会被拖死。 - 低延迟 HLS 别贪短 。保留片段数过少会加剧驱逐失败;靠
lowLatencyMode+
合理的liveMaxLatencyDuration,而不是拼命缩短窗口。 - 字幕容忍「迟到」,但拒绝「错位」 。慢翻译可以归档、可以显示等待,但绝不能
迟到后覆盖下一段音频的字幕。 - 用真实浏览器做验收 。Chromium + CDP(
--remote-debugging-port)驱动真实点击、
抓Network事件、读getVideoPlaybackQuality(),比看服务端日志有效一个数量级。 - 诚实标注边界 。片段级对齐就写片段级,不宣称毫秒级;上游损坏就写上游损坏,
不用「循环旧视频」掩盖断流。
11. 结语
一个「多路流媒体 + AI 转录翻译」系统,真正的难点从来不在「能不能播」,而在
稳定性、同步性和诚实度:
- 播放层 :用 hls.js + MediaMTX + ffmpeg 这套开源组合,能把多路直播收进来、
以极低成本在浏览器里动起来; - 转录层 :用实时 ASR(如 Qwen-Audio Realtime)+ 大模型翻译 ,能快速拿到
高质量中文输出; - 而真正的工程价值 ,在于把两者粘起来、让字幕跟着画面走,并且如实告诉用户
哪里还没解决。
如果你正在做「视频 + AI」方向,希望这份复盘能帮你少踩几个坑。
参考
播放与流媒体
- hls.js:https://github.com/video-dev/hls.js
- MediaMTX:https://github.com/bluenviron/mediamtx
- ffmpeg:https://ffmpeg.org/
- Video.js:https://github.com/videojs/video.js
- Shaka Player:https://github.com/shaka-project/shaka-player
- SRS:https://github.com/ossrs/srs
- ZLMediaKit:https://github.com/ZLMediaKit/ZLMediaKit
语音识别与翻译
- Whisper:https://github.com/openai/whisper
- faster-whisper:https://github.com/SYSTRAN/faster-whisper
- FunASR:https://github.com/modelscope/FunASR
- NLLB-200:https://github.com/facebookresearch/fairseq/tree/nllb
- 阿里云 Qwen-Audio Realtime 文档:https://help.aliyun.com/zh/model-studio/qwen-audio-realtime-user-guidesl