HTTP-MPEG-TS 低延迟推流开发实战:踩坑记录与 FLV 对比
一、背景
近期在开发一款多媒体播放器。除了本地播放,还内置了推流服务器,能将正在播放的视频实时推到局域网内的浏览器和平板上观看。推流方案经历了两个阶段:早期基于 HTTP-FLV(使用 flv.js),后期增加了 HTTP-MPEG-TS(使用 mpegts.js)。本文记录实现 MPEG-TS 推流过程中遇到的关键问题和解决方案,最后对两种协议做客观对比。
二、两种协议的简介
HTTP-FLV:将 FLV 格式数据通过 HTTP 分块传输推送给浏览器,由 flv.js 通过 MSE 喂给 video。成熟稳定、延迟极低(200-500ms),但 FLV 容器只支持 H.264 视频和 AAC 音频,编码兼容性受限。需要注意的是,flv.js 已进入维护停滞状态。
HTTP-MPEG-TS:使用广播电视领域广泛使用的传输流格式,通过 mpegts.js(与 flv.js 同作者,是后者官方继任者)在浏览器端解码。天然支持 H.264/H.265/AAC/MP3/AC-3 等多种编码;TS 流内部会周期性重发 PAT/PMT 表,新客户端可任意时刻加入。mpegts.js 社区相对较小,seek 时需要重建 SourceBuffer。
三、开发中解决的关键问题
问题 1:AAC 编码器 seek 后时间戳污染
现象:快进后服务端日志刷出大量音频写入错误,浏览器端声音停止。
根因:FFmpeg 原生 AAC 编码器内部有一个约 2048 个采样点的超前缓冲。seek 后调用冲刷缓冲区的 API 无法清除这个缓冲,旧缓存帧的时间戳从 0 重新计数。服务端的时间戳重映射逻辑收到这些旧帧后,计算出的新时间戳变成负数,导致写入时报错。
解决方案:仿照 H.264 编码器的处理方式——收到 seek 信号后,销毁整个编码器上下文,重新创建并打开。AAC 编码器重建后输出的编码参数与之前一致(使用相同参数配置),不会影响 TS 流的编码信息表。
教训:不是所有编码器的冲刷接口都可靠。某些编码器冲刷后会进入报废状态再也收不了帧,有些冲刷后残留内部缓冲。遇到这类编码器,必须在 seek 时重建上下文。
问题 2:多次 seek 后浏览器延迟逐步升高
现象:多次快进后,平板浏览器的延迟越来越高,从几百毫秒逐渐累积到数秒。日志显示同一 IP 在多次重连后,服务端维护的连接列表中的 socket 数量从 1 增长到 3。
根因有三层:
seek 后 H.264 解码预滚会产生目标点前的画面组重叠数据,旧浏览器连接继续接收这些数据并写入缓冲。
每次新连接流地址时旧 socket 未被主动替换,导致同一个浏览器持有多个连接,服务端向所有旧连接广播数据,造成带宽和缓冲双重浪费。
最近一个画面组的 TS 缓存数据在 seek 后仍包含历史位置的数据,晚连客户端先消费这些旧数据,形成固定延迟。
解决方案:
seek 信号到达流服务器时,主动立即关闭所有现有 TS 连接,让浏览器重连并重建缓冲。
增加同源连接替换机制:新连接流地址时,查找并替换同一 IP 的旧 socket,彻底消除同 IP 多连接残留。
晚连客户端不再发送历史画面组缓存数据,只接收实时广播数据,靠 TS 流周期性自带的编码信息表自然起播。
广播数据时检测每个客户端的套接字写缓冲大小(通过 SO_SNDBUF 或非阻塞写返回值),超过阈值的慢客户端主动断开,防止拖累整条链路。
问题 3:快进后重连风暴触发容器迭代器断言
现象:按右方向键快进后,浏览器连续请求流地址,随后程序崩溃,断言错误提示迭代器无效。
根因涉及两个 bug 叠加:
替换同源连接时使用迭代器遍历客户端列表,遍历过程中调用了断开连接的方法。断开信号可能同步进入连接移除函数修改同一个列表,导致当前迭代器失效。
之前的修复在 HTML 播放页添加了多个事件监听实现快速重连,但销毁播放器过程本身也会触发这些事件,造成 300ms 间隔的连续重连风暴。
解决方案:
替换旧客户端时,先循环收集所有待移除的 socket 指针,再统一从列表中移除,最后集中断开连接,彻底消除遍历时修改列表。
撤销多余的事件重连,只保留播放器内核的错误回调。
重连时清理并重建 MediaSource 对象,避免 SourceBuffer 状态残留。
教训:桌面框架的信号-槽机制是同步的,断开连接可能同步触发断开信号进而修改容器。遍历任何容器时都严禁增删元素。
问题 4:seek 后先推送目标前旧画面
现象:快进 10 秒后,浏览器端先播放 seek 之前的画面(约 5 秒旧内容),然后才切到新位置。
根因:解复用线程的 seek 为了保证 H.264 参考帧链完整,会从目标点前的关键帧开始推送视频包。解码器忠实地解码了所有这些包,但预滚区的解码帧本应丢弃等待新位置帧,它们却直接进入了重编码器,导致服务端先推送 5 秒旧画面。
解决方案:
解码器新增最小输出时间戳门控:解码过程中如果帧的时间戳小于门控值就丢弃该帧,只完成解码预滚不输出。
流管线暴露设置 seek 目标的接口,供 UI 层在 seek 前调用。
播放器窗口中进度条拖动和左右方向键触发 seek 前,先通知所有流管线设置目标时间。
HTML 播放页增加缓冲清理函数:重连前销毁播放器、清空视频元素的源地址并重新加载,确保缓冲全部清空。
问题 5:开局闪白
现象:推流页面打开时,浏览器先闪烁一下白色,然后才显示视频画面。
根因:渲染器初始化图像缓冲区时分配了未初始化的内存,绘制事件在第一个有效帧到达前将其绘制到屏幕,内容为随机的杂色。
解决方案:初始化后立即将图像缓冲区填充为纯黑色。
问题 6:快进提示误用“流结束”
现象:快进后浏览器显示“流结束,重连...”,让用户误以为直播已经结束。
根因:HTML 播放页的视频结束事件监听和定时器检测都使用“流结束”文案。但在这个直播场景中,结束事件只会在 seek 断连时触发——直播流不会自己结束。
解决方案:将结束事件处理文案改为“快进中,重连...”,与真正网络断开的“断开中,重连...”和画面卡住的“画面卡住,重连...”自然区分。
四、HTTP-MPEG-TS vs HTTP-FLV 对比
对比维度 HTTP-FLV (flv.js) HTTP-MPEG-TS (mpegts.js)
主要开发者 Bilibili 开源 flv.js 作者的新作品(官方继任者)
维护状态 已停止维护 积极维护
编码兼容性 仅支持 H.264 + AAC 支持 H.264/H.265 + AAC/MP3/AC-3 等
浏览器端库 flv.js 成熟稳定,社区庞大 mpegts.js 较新,社区资源较少
端到端延迟 200-500ms 200-500ms(无本质差异)
晚连客户端起播 需服务端额外维护编码配置头和最近 GOP 缓存,实现复杂 复用器周期性自动写入 PAT/PMT(约 0.5s),新客户端只需接入实时数据即可
Seek 行为 可复用现有缓冲,无缝切换 推荐断开重建缓冲(短暂黑屏 300-800ms),因为时间戳跳变可能引发解码异常
服务端实现复杂度 低,FLV 是连续 tag 序列 中等,需构建 PAT/PMT/音视频 PES 包
五、选择建议
适合选 HTTP-MPEG-TS 的场景:
需要支持 HEVC/H.265 编码(如 4K 视频推流)
希望晚连客户端逻辑简单
接受 seek 时短暂黑屏重建缓冲
目标浏览器为 Chrome/Edge/Firefox 最新版本
适合选 HTTP-FLV 的场景:
需要兼容更广泛的浏览器(包括低版本)
希望 seek 无缝切换不断开
FLV 社区更大,遇到问题更容易找到现成方案
编码限定 H.264
当前项目:两种方案共存。HTTP-FLV 作为成熟稳定的默认选择,HTTP-MPEG-TS 作为技术前瞻选项,用户可在推流配置界面选择目标协议。
六、技术总结
这次开发中踩过的坑可以归纳为几个方面:
FFmpeg 编码器状态管理:部分编码器的缓冲冲刷接口不可靠,必须重建上下文。这是 FFmpeg 编码器实现的历史遗留问题。
多线程数据流同步:seek 操作需要在整个管线中同步传播(解复用→解码→重编码→流服务)。使用空指针哨兵模式统一处理所有下游线程的状态重置。
时间戳续接算法:重编码场景下,编码器从 0 开始计数时间戳,但 TS 流要求时间戳单调递增。通过“当前值减去基数再加累积偏移量”的重映射算法,实现 seek 前后流时间的自然衔接。
浏览器 MSE 行为:视频结束事件、停滞事件、缓冲排空事件的实际触发时机与直播场景不完全匹配,需要根据实际行为反复调整重连策略和提示文案。
跨平台线程安全:桌面框架的容器在遍历时不能修改,网络套接字的断开操作可能同步触发信号回调。这类问题在桌面应用中容易忽略但后果严重,直接导致崩溃。

浙公网安备 33010602011771号