录制链的装配:解码源、离屏处理与编码器

录制器只装三件东西:一个解码源、一段离屏处理、一个会编码的封装器。
解码源用的是 AMediaSource,播放那侧的选型、候选链与中途换道都在它里面(就是解码那套策略),录制这条链不另写一份。处理那段用的是播放时那个协调者类 WindowRender,区别是录制时把它设成离屏:没有窗口,也没有跟着屏幕刷新转的那条线程,一帧什么时候被处理由喂它的人决定。第三件是会编码的封装器 RawMuxer,它下面挂两路编码流,一路视频一路音频(封装这一层分几个层次,封装那一篇细讲)。
帧从回调进来,不是一个取帧接口。解码出来的 CPU 帧、硬解出来的 GPU 帧、音频帧各走一个回调;还有第四个入口,给「不解码、把源包直接转发进封装」那种用法。四个入口都由录制器自己实现——它本身就是帧源那一篇讲的那个原始帧源的观察者。
帧的处理路径:离屏处理与 CPU 回读

两个视频回调进来的是两种东西:CPU 帧是内存里的 YUV 平面,GPU 帧是硬解出来的纹理。不管哪一种,都先交给那段离屏处理,处理完再从它身上取回一张 CPU 帧。
硬解那条多一步:纹理先由平台像素层转成 RGBA,再映射进 Vulkan 管线,和播放时的进法是同一条路,共享纹理那篇展开过这一段。处理段里做的事就是播放时那些:缩放、色彩、水印,以及可选的画质增强滤镜。
回读是这一步的代价:图在 GPU 上出完,不呈现,而是拉回内存进编码队列——这是渲染分层那两条出图路之外的另一条去向。Vulkan 那条腿把图拷进一块缓冲、映射出来再拷走,各平台的原生腿用各自的办法。这一段同时也决定了后面对编码器交什么格式——编码器不会自己挑,它按上游给的描述读平面。
帧队列的反压:队列满时阻塞解码侧
编码不在解码线程上做。处理完的帧深拷贝进一条帧队列,视频音频各一条,各留很少几帧;编码线程从队列里取。
深拷贝不是多余动作:处理段的产物放在共享缓冲里,下一帧一进就把上一帧盖掉,不拷走就没法安心丢进队列。
队列满了不丢帧,而是阻塞入队:解码线程停在入队上,压力顺着解码回调一直顶回 IO 层。这样内存有界——编码慢,上游就得跟着慢。代价是整条链的节拍由最慢的一头定:画质增强那种逐帧推理的滤镜挂在编码这一侧,它一慢,队列一满,整条线就被它带着走。
关闭时要把堵在入队上的生产者叫醒,否则收尾会卡在那儿等队列。
编码这一侧的循环只有一条线程,视频与音频两条队列它一起照看:每轮先取一帧视频,再取一帧音频,取不到就歇一下。什么时候算完也在这里判——源已经结束、两条队列都空了,才收尾关掉编码器。seek 同样落在这条线程上,免得和 IO 那条线程抢同一批状态。
软件编码器的接入:注册、选型与打开时的配置
编码器走注册表,和源、解码器是同一个套路。构建期把 ffmpeg 里带编码器标记的那些枚举出来,按编码 id 挂进表,描述里带上 ffmpeg 自己的编码器名。选哪个走候选链:排法与退出路径跟解码那侧是同一套。
偏好名由平台宏给,软硬各一个默认名,构建时还能从外面注入。
打开编码器时配了这几样。时间基定成毫秒,和链上其它地方一致,帧的时间戳不用来回换。GOP 按秒折算成帧数,不按帧数写死。B 帧关掉。线程数交给 ffmpeg 自己按机器定。码率与预设按编码器名分发:软编那两档走 CRF 加一个快预设,带厂商后缀的那几档走各自的预设与质量档——同一份配置代码,按名字进不同的分支。
分辨率中途变了是重建编码器,不是缩放:编码器按新尺寸重新打开,缩放那件事归处理段管。
编码器开不起来才换道
接编码器的时候只接两样:描述,和出包的回调。开不开得起来要到第一帧才看得出来——建得起来但配置失败,是首帧喂进去那一刻才暴露的。
所以这一侧也有一条换道,但比解码那侧窄得多:首帧打不开就换同编码的下一条候选,把这一帧重编。敢在首帧换,是因为那时一个包都还没出过——封装头没写、编码描述没落,换一条实现不用回头收拾。出过包之后就不再换了,换一次就意味着封装头得重来。
早先这里是把编帧的返回值直接丢掉的。代价是硬编开不起来时整片没有视频,日志里一行都没有,只能靠肉眼发现文件里没有画面。
编码器拿到的东西:像素格式与时间戳
编码器要的像素格式不是它自己协商出来的。处理段出图的时候就按目标定了:走软编出的是 YUV420P,走硬编出的是 NV12,编码器打开时按描述里那个格式读平面。视频这一侧没有转换层——格式错配不会被纠正,编码器会按它以为的布局去读。所以格式必须由出图那一侧对齐,这条线上游说了算。
音频那侧相反:编码器只认固定几档采样格式与声道布局,链上就挂了一层重采样,帧进来先按目标格式重采样,再交给编码器。
时间戳这一侧很省事:帧的 pts 原样带进编码器(链上统一用毫秒),编码器出的包再换一次基。从包到文件还要再换一次到流自己的时间基,那一步归下一篇。
收尾:自然结束、开流期终止与解码道死
这条链什么时候算完、什么时候算失败,分三种。录制中读到源结束(对端断流、文件读完),按完成收尾:编码线程把两条队列排空,再关封装器。开流阶段一帧都没写就被源终止,按失败——那多半是压根没拿到数据。解码那条车道整个死掉,一律按失败:这时产物里没有任何帧,按完成报上去,上层拿到的就是一张坏图。
失败只报错误回调,不发完成回调,免得上层把失败当成功。
到这里,帧已经变成包了。包怎么变成文件——开流、归零、交错写——归封装那一篇。
引擎 avox 在 GitHub 开源:github.com/xxxzhou/avox,这条录制链的装配、队列与编码器配置都在里面。
音视频开发 播放器 录制 转码 FFmpeg 编码器 C++