Skip to main content

MP4 转 RTSP/WebRTC 的流畅度调优记录

· 12 min read

本文记录 scripts/publish-mp4.cmd 的一次实际调优过程,目标是把本地 MP4 通过 FFmpeg 发布到 MediaMTX,再由浏览器通过 WebRTC 流畅播放。

数据链路​

MP4 文件
│
│ FFmpeg 解码、重新编码、按实时速度发布
▼
MediaMTX RTSP
│
│ WebRTC
▼
浏览器播放器

这条链路中的卡顿不一定来自同一个位置,可能来自:

  • MP4 本身帧率或时间戳不稳定
  • FFmpeg 编码速度跟不上
  • 编码瞬时码率波动过大
  • 关键帧间隔不适合实时传输
  • MediaMTX 或网络传输问题
  • 浏览器解码、页面渲染或本机 CPU 竞争

因此,调参之前应该先确定问题出现在哪一层。

素材信息​

使用 ffprobe 检查测试视频:

ffprobe -v error `
-show_entries format=duration,bit_rate:stream=index,codec_type,codec_name,width,height,r_frame_rate,avg_frame_rate,time_base,sample_rate,channels `
-of json `
.\scripts\test.mp4

测试素材的主要参数为:

项目数值
视频编码H.264 High Profile
分辨率720×404
帧率25 FPS
视频码率约 669 kbps
B 帧存在
音频编码AAC
采样率44.1 kHz
声道双声道

源视频包含 B 帧。浏览器通过 WebRTC 播放 H.264 时通常不适合直接传输带 B 帧的视频,因此不能简单使用:

-c:v copy

仍然需要将视频重新编码为不含 B 帧、浏览器兼容性更好的 H.264 Baseline 流。

调整前的参数​

原脚本的视频编码部分主要使用:

-c:v libx264
-preset ultrafast
-tune zerolatency
-profile:v baseline
-pix_fmt yuv420p
-crf 20
-g 30
-keyint_min 30
-sc_threshold 0
-bf 0

这里使用 CRF 控制画面质量。CRF 的特点是尽量保持视觉质量,但场景复杂度变化时,瞬时码率也会明显变化。

对于保存到磁盘的视频,码率波动通常不是问题;对于低延迟实时传输,过大的瞬时码率可能造成接收端缓冲不均匀,表现为画面偶尔停顿后追帧。

原来的关键帧间隔为 30 帧。对于 25 FPS 素材,相当于:

30 ÷ 25 = 1.2 秒

关键帧体积通常远大于普通预测帧。关键帧过于频繁,会增加瞬时带宽和编码数据量。

最终改动​

最终脚本的视频参数调整为:

-c:v libx264
-preset ultrafast
-tune zerolatency
-profile:v baseline
-pix_fmt yuv420p
-b:v 1500k
-maxrate 1500k
-bufsize 3000k
-g 50
-keyint_min 50
-sc_threshold 0
-bf 0

完整命令位于:

scripts/publish-mp4.cmd

1. 从 CRF 改为受约束的码率控制​

移除:

-crf 20

增加:

-b:v 1500k
-maxrate 1500k
-bufsize 3000k

参数含义:

  • -b:v 1500k:目标视频码率约为 1500 kbps。
  • -maxrate 1500k:限制编码器的瞬时输出码率。
  • -bufsize 3000k:设置码率控制缓冲区,大小约等于 2 秒目标码率。

这组参数的目的不是单纯降低码率,而是让数据输出更均匀,减少 WebRTC 接收端遇到突然码率峰值的概率。

1500 kbps 对当前 720×404、25 FPS 的素材有较大余量。更高分辨率可以参考以下起点:

分辨率帧率建议起始码率
640×36025–30 FPS600–1000 kbps
1280×72025–30 FPS1500–3000 kbps
1920×108025–30 FPS3000–6000 kbps

这些只是起点。人物动作复杂、画面纹理丰富时通常需要更高码率。

2. 关键帧间隔调整为 2 秒​

修改前:

-g 30 -keyint_min 30

修改后:

-g 50 -keyint_min 50

测试视频是 25 FPS,所以 50 帧对应 2 秒:

50 ÷ 25 = 2 秒

这样既能让新加入的 WebRTC 客户端较快等到关键帧,又不会过于频繁地产生较大的 I 帧。

如果素材是 30 FPS,可以考虑使用:

-g 60 -keyint_min 60

通用计算方式:

关键帧间隔 = 帧率 × 2

3. 禁止场景切换额外插入关键帧​

-sc_threshold 0

默认情况下,x264 可能在画面发生大幅变化时额外插入关键帧。这对普通视频压缩有帮助,但可能让实时流的码率出现不可预测的峰值。

设置为 0 后,关键帧节奏更加固定。

4. 禁用 B 帧​

-bf 0

B 帧需要参考前后帧,会增加帧重排和播放延迟。MediaMTX 官方也指出,浏览器 WebRTC 对包含 B 帧的 H.264 流兼容性不好。

实时 WebRTC 场景通常应禁用 B 帧。

5. 保留低延迟编码设置​

-preset ultrafast
-tune zerolatency
  • ultrafast:降低编码复杂度和 CPU 使用量,代价是相同画质下码率更高。
  • zerolatency:关闭或缩短编码器内部会增加延迟的缓冲机制。

当前项目还要同时运行本地 ASR 推理,因此优先减少视频编码对 CPU 的占用。

6. 保留 Baseline 与 yuv420p​

-profile:v baseline
-pix_fmt yuv420p

这两个参数用于提高浏览器和 WebRTC 的兼容性:

  • Baseline Profile 避免使用部分复杂编码特性。
  • yuv420p 是浏览器和硬件解码器支持最广泛的像素格式。

曾尝试但最终撤回的参数​

调试期间曾尝试:

-fflags +genpts
-fps_mode cfr

它们分别用于重新生成时间戳和强制输出恒定帧率。

但测试素材本身已经是标准的 25 FPS,并且时间戳正常。在约 15 秒的测试中,强制 CFR 导致 FFmpeg 报告:

dup=3 drop=0

这表示 FFmpeg 为了满足强制帧率复制了 3 帧。重复帧虽然不等于真正的编码卡顿,但肉眼可能看到轻微顿挫。

因此最终撤回了这两个参数。经验是:

不要在源视频时间戳正常时无条件重建时间戳或强制帧率。

如果某个素材确实是可变帧率、时间戳损坏或时间戳不连续,再针对该素材使用这些参数。

音频参数​

音频编码部分保持为:

-c:a libopus
-b:a 64k
-ar 48000
-ac 1
-application lowdelay

参数含义:

  • 转码为 WebRTC 兼容性良好的 Opus。
  • 码率为 64 kbps。
  • 采样率为 48 kHz,这是 Opus 和 WebRTC 的常用采样率。
  • 转换为单声道,足够用于语音识别并减少数据量。
  • 使用 Opus 低延迟模式。

ASR 后端会再次将音频转换为模型需要的 16 kHz 单声道 PCM。

如何判断编码器是否跟得上​

运行脚本时注意 FFmpeg 的状态行:

frame= 321 fps=25 ... speed=1.04x

重点关注:

字段含义
fpsFFmpeg 当前处理帧率
speed处理速度相对于实时速度的倍数
dup为满足帧率要求而复制的帧数
drop丢弃的帧数

对于 25 FPS 实时流,理想状态是:

fps ≈ 25
speed ≈ 1.0x
drop = 0

本次测试中 FFmpeg 稳定输出约 25 FPS,速度约为 1.04x,且没有丢帧,说明 CPU 编码能力足够,问题不是编码器处理不过来。

分层排查卡顿​

1. 直接播放原始 MP4​

ffplay .\scripts\test.mp4

如果这里也卡顿,问题来自素材本身。

2. 播放 RTSP​

先启动推流脚本,然后执行:

ffplay -rtsp_transport tcp rtsp://localhost:8554/mystream

如果原始 MP4 流畅,但 RTSP 卡顿,应检查 FFmpeg 日志、MediaMTX 和编码参数。

3. 播放 WebRTC​

打开:

http://localhost:8889/mystream/

如果 RTSP 流畅而 WebRTC 卡顿,问题更可能出现在浏览器解码、WebRTC 传输或页面渲染层。

4. 检查本机资源竞争​

当前项目会同时运行:

  • FFmpeg H.264 视频转码
  • FFmpeg 音频解码
  • sherpa-onnx CPU 语音识别
  • MediaMTX
  • 浏览器视频解码与页面渲染

即使 FFmpeg 没有报告丢帧,ASR 推理也可能抢占 CPU,造成浏览器渲染不稳定。可以临时停止后端 ASR,再观察视频是否改善。

最终命令​

脚本最终使用的核心命令如下:

ffmpeg -hide_banner -re -stream_loop -1 -i "输入文件.mp4" `
-map 0:v:0 -map 0:a:0? `
-c:v libx264 `
-preset ultrafast `
-tune zerolatency `
-profile:v baseline `
-pix_fmt yuv420p `
-b:v 1500k `
-maxrate 1500k `
-bufsize 3000k `
-g 50 `
-keyint_min 50 `
-sc_threshold 0 `
-bf 0 `
-c:a libopus `
-b:a 64k `
-ar 48000 `
-ac 1 `
-application lowdelay `
-f rtsp `
-rtsp_transport tcp `
"rtsp://localhost:8554/mystream"

其中:

  • -re 让 FFmpeg 按正常播放速度读取 MP4,而不是尽快发送完整文件。
  • -stream_loop -1 让视频无限循环。
  • -map 0:a:0? 中的 ? 表示音轨是可选的,没有音轨时仍可推送视频。

总结​

本次改善的核心不是让 FFmpeg“跑得更快”,而是让实时数据输出更加稳定:

  1. 从质量优先的 CRF 改为带上限和缓冲的码率控制。
  2. 将关键帧间隔调整为适合 25 FPS 实时流的 2 秒。
  3. 保持无 B 帧的 WebRTC 兼容编码。
  4. 不随意重建正常素材的时间戳或强制帧率。
  5. 通过 fps、speed、dup 和 drop 数据验证调优效果。