Skip to content

第 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 启动后会等待:

  1. 所有启用 stream 完成 packet_sink.open
  2. 需要 config 的 stream 队列至少有首包;
  3. 验证首包 pts == AV_NOPTS_VALUE
  4. 拷贝为 codecpar->extradata
  5. avformat_write_header

代码见 sc_recorder_process_header()。VP8/VP9 和 raw PCM 不期待独立 config,其他支持格式按 codec 决定。

四、两条流如何从同一时间原点开始

video/audio 首包到达顺序不确定。recorder 在双流启用时等两者都拿到首个媒体包,再取较小 PTS 为 pts_origin。每个包写出前减去 origin:

text
record_pts = source_pts - min(first_video_pts, first_audio_pts)

这样保留两条流真实起始偏移,又让文件时间轴接近 0。只有一条流时直接用它的首 PTS。

对应逻辑见 recorder.c

五、视频 duration 为何要看下一包

scrcpy 的协议发送 PTS,不直接发送 duration。可变帧率视频的当前帧持续时间只能在下一帧到来后计算:

text
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=falseaudio_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

compressed packet → FFmpeg decoder → YUV420P AVFrame → V4L2 single-frame buffer → rawvideo packet → /dev/videoN

初始化与写线程见 v4l2_sink.c

十、V4L2 也选择“最新帧”

V4L2 sink 与 screen 一样使用单帧 buffer。producer 若在前一帧未消费时 push,新帧覆盖旧帧且不重复 signal,见 sc_v4l2_sink_push()

虚拟摄像头属于实时输出,不应该因消费者慢而无限积压。若需要稳定 frame pacing,可在前面加独立 video_regulator

十一、两个输出的根本差异

维度RecorderV4L2
接入层packetframe
是否解码
是否允许丢中间数据是,保最新帧
时间策略保留 PTS、计算 durationregulator 调度后实时写
输出格式MP4/MKV/M4A/Opus/FLAC/WAV 等raw video device
主要目标完整、可回放当前、低延迟

十二、本章小结

recorder 与 V4L2 展示了扩展点必须服从数据语义:压缩包适合无损封装,解码帧适合像素消费者;完整性输出需要队列,实时输出需要覆盖。下一章把视角从数据转到线程,系统性梳理 scrcpy 如何中断阻塞调用、汇聚错误并保证没有 use-after-free。

基于 scrcpy 4.1 源码的独立学习笔记