RambosPlayer 加了个拉流录制,只改了 150 行代码

项目github:https://github.com/johnjiamzhong-project/RambosPlayer
起因
最近在用 RambosPlayer 拉 AlertGateway 推出的RTMP流,想录下来。翻了一遍功能列表——有推流录制,但那是配合文件播放用的,拉流模式下没这个功能。
那就加一个吧。
先看看手头有什么
打开代码一看,其实基础设施都有了:
DemuxThread的读包循环里,本来就有分叉逻辑——每读一个包,除了推给播放队列,还会同步拷贝给所有注册的录制器
LocalRecorder已经实现了 -c copy 直通写 FLV,PTS 归零、seek 抑制、关键帧对齐全都有
PlayerController暴露了addLocalRecorder()接口,一行就能注册
所以本质上,拉流录制就是在拉流播放的同时,往 DemuxThread 的分叉列表里塞一个 LocalRecorder。
暂停/恢复怎么搞
网络流是实时的,"暂停"不可能真的暂停网络。那就只能停止写入,丢弃数据。
关键问题是恢复的时候 PTS 怎么续接。暂停期间网络流的 PTS 跑了 30 秒,恢复后第一帧的源 PTS 已经跳了很远。如果直接写,输出文件会出现 30 秒的空洞。
解法很朴素:
pause() → 设 paused_ 标志,write 直接返回
resume() → videoAccumPts_ = videoLastOut_ // 记住写到哪了
videoSegBase_ = AV_NOPTS_VALUE // 下一帧重新采集基准
恢复后第一帧算出来:
outputPTS = newSrcPTS - newSegBase + videoAccumPts_
≈ 0 + videoLastOut_
= 暂停前最后一帧
时间轴无缝接上。
保存策略的纠结
最初设计是点"录制"就弹保存对话框。后来发现不对——看直播的时候想录就录,哪有功夫选路径?
改成临时文件方案:
点"录制" → 直接写
%TEMP%/rambos_record_XXXXXX.flv,零打断点"停止" → 先写完文件,再弹框让你选保存位置和格式
选 FLV → 移动临时文件,瞬间完成
选 MP4 → 用 avformat 做一次容器转换(-c copy,不重编码),100MB 的文件大概 1 秒
取消 → 删临时文件,当无事发生

MP4 容器转换
为什么不直接录 MP4?因为 MP4 的 moov atom 在文件尾部,录到一半断电,文件就废了。FLV 没这个问题,随时断都能播放。
所以策略是录制用 FLV 保平安,保存时可选转 MP4 求兼容。
转换代码很短,核心就是一个循环:
打开源 FLV → 创建 MP4 输出 → 逐包读写(只换容器不换数据)→ 写 trailer
速度极快,本质就是复制数据。
UI 怎么加的
在拉流控制栏末尾塞了三个按钮,全部初始隐藏:
录制→ 只在拉流连接成功后显示
暂停/恢复→ 录制中显示,点击切换文案
停止录制→ 录制中显示,点击弹保存框
状态机很简单:
未连接 → 全部隐藏
已连接 → [录制]
录制中 → [暂停] [停止录制]
暂停中 → [恢复] [停止录制]
停止 → [录制]
断开 → 全部隐藏
断开拉流时如果正在录制,自动触发停止并弹框保存,不会丢数据。
状态栏的蓝色条子
录制期间底部会显示一个蓝色状态栏(style.qss 里 QStatusBar 的背景色是 #007acc),提示"录制中..."。
录完之后光清文字没用,底色还在。得 statusBar()->hide() 整个隐藏掉。开始录制时再 statusBar()->show()。
就这一个小细节,多改了两行。
代码审查发现的问题
写完跑了一遍,发现 resume() 里只重置了 segBase,没更新 accumPts_。这会导致恢复后 PTS 回跳到暂停前的位置,输出文件时间轴倒退。
修复就一行:resume() 开头加 videoAccumPts_ = videoLastOut_。
最终改动清单
| 文件 | 改了什么 |
|---|---|
localrecorder.h |
+3 方法声明, +1 成员变量 |
localrecorder.cpp |
+20 行(pause/resume/init 重置) |
mainwindow.ui |
+3 按钮控件 |
mainwindow.h |
+4 成员变量, +4 方法声明 |
mainwindow.cpp |
+120 行(录制控制 + remux) |
150 行新增代码,零修改已有逻辑,零新增线程。
感受
这个功能之所以好加,是因为项目早期的架构做对了一件事——DemuxThread 的分叉机制。
所有录制、推流、转发,都是在读包循环里挂一个写入器。新功能不需要改 DemuxThread 一行代码,只需要注册一个新的消费者。
当初设计这个架构的时候没想到要加拉流录制,但它天然支持。
好的架构不是预知未来,是让未来的事情不需要推翻重来。
相关项目:RambosPlayer
相关项目:AlertGateway
相关项目:MqttMonitor
技术栈:C++17 / Qt 5.14.2 / FFmpeg 4.x

浙公网安备 33010602011771号