第 12 章:录制与 V4L2——同一份视频,两种输出层级
录像要保留压缩包,虚拟摄像头要消费像素帧。scrcpy 没有为了统一 API 把两者硬塞在同一层,而是分别挂到 packet graph 和 frame graph。
一、为什么录制不经过 decoder
设备已经用硬件编码器生成 H.264/H.265/AV1 等压缩包。录制只需把包封装进 MP4/MKV,无需解码再编码:
- CPU 和电量开销更低;
- 无额外有损压缩;
- 保留设备编码器原始质量;
- 即使
--no-playback,仍可录制; - 本地缺少视频渲染能力也不影响封装。
因此 recorder 是 packet_sink,在 scrcpy.c 直接挂到 video/audio demuxer。
二、Recorder 为什么有自己的线程和双队列
视频、音频来自两个 demuxer 线程,FFmpeg muxer 却必须由单一顺序写入。recorder 为两条流各维护 vecdeque<AVPacket*>:producer av_packet_ref 后入队,recorder 线程按可用性取出并调用 av_interleaved_write_frame。
线程被设为 low priority,避免磁盘封装抢占播放与控制,见 run_recorder()。
这里队列没有像屏幕那样主动丢帧,因为录像的正确性要求完整包序列。磁盘长期跟不上时,内存会增长;这是“完整记录”相对“实时显示”的不同服务目标。
三、必须等齐 stream 和 config 才能写容器头
FFmpeg 的输出 header 需要提前知道 stream codec parameters 与 extradata。recorder 启动后会等待:
- 所有启用 stream 完成
packet_sink.open; - 需要 config 的 stream 队列至少有首包;
- 验证首包
pts == AV_NOPTS_VALUE; - 拷贝为
codecpar->extradata; - 调
avformat_write_header。
代码见 sc_recorder_process_header()。VP8/VP9 和 raw PCM 不期待独立 config,其他支持格式按 codec 决定。
四、两条流如何从同一时间原点开始
video/audio 首包到达顺序不确定。recorder 在双流启用时等两者都拿到首个媒体包,再取较小 PTS 为 pts_origin。每个包写出前减去 origin:
record_pts = source_pts - min(first_video_pts, first_audio_pts)这样保留两条流真实起始偏移,又让文件时间轴接近 0。只有一条流时直接用它的首 PTS。
对应逻辑见 recorder.c。
五、视频 duration 为何要看下一包
scrcpy 的协议发送 PTS,不直接发送 duration。可变帧率视频的当前帧持续时间只能在下一帧到来后计算:
current.duration = next.pts - current.pts所以 recorder 暂存 video_pkt_previous,收到新视频包后才写前一包。结束时最后一帧没有 successor,赋一个任意 100 ms duration;即使最后一包写失败,文件通常仍可用。
六、单调 PTS 修复
某些编码器或时间转换可能产生相等/倒退 PTS。容器 muxer 要求单调递增时,recorder 将当前 PTS 修正为 last_pts + 1,并同步 DTS,见 sc_recorder_write_stream()。
这不是重排媒体内容,而是对极小时间戳异常做最小合法化,避免整次录制因一个坏 timestamp 失败。
七、旋转用 metadata,不动像素
--record-orientation 不把每帧旋转后再编码,而是在 video stream 写 AV_PKT_DATA_DISPLAYMATRIX side data。播放器按矩阵显示,压缩数据保持原样。实现见 sc_recorder_set_orientation()。
代价是少数播放器可能忽略旋转 metadata;收益是零重编码成本。
八、音频禁用如何改变 recorder 状态
如果设备用 codec id 0 表示 audio disabled,demuxer 调用 audio sink 的 disable。recorder 在 mutex 下把 audio=false、audio_init=true,唤醒仍在等待 header 的线程,见 sc_recorder_audio_packet_sink_disable()。
没有这个状态转换,header 线程会永远等待一条永远不会出现的 audio config。
九、V4L2 为什么必须接 frame 层
Linux 虚拟摄像头通常期望 raw video。v4l2_sink 因此接收 AVFrame,用 FFmpeg rawvideo encoder/Video4Linux2 muxer 写 /dev/videoN:
初始化与写线程见 v4l2_sink.c。
十、V4L2 也选择“最新帧”
V4L2 sink 与 screen 一样使用单帧 buffer。producer 若在前一帧未消费时 push,新帧覆盖旧帧且不重复 signal,见 sc_v4l2_sink_push()。
虚拟摄像头属于实时输出,不应该因消费者慢而无限积压。若需要稳定 frame pacing,可在前面加独立 video_regulator。
十一、两个输出的根本差异
| 维度 | Recorder | V4L2 |
|---|---|---|
| 接入层 | packet | frame |
| 是否解码 | 否 | 是 |
| 是否允许丢中间数据 | 否 | 是,保最新帧 |
| 时间策略 | 保留 PTS、计算 duration | regulator 调度后实时写 |
| 输出格式 | MP4/MKV/M4A/Opus/FLAC/WAV 等 | raw video device |
| 主要目标 | 完整、可回放 | 当前、低延迟 |
十二、本章小结
recorder 与 V4L2 展示了扩展点必须服从数据语义:压缩包适合无损封装,解码帧适合像素消费者;完整性输出需要队列,实时输出需要覆盖。下一章把视角从数据转到线程,系统性梳理 scrcpy 如何中断阻塞调用、汇聚错误并保证没有 use-after-free。