原始帧源与编码数据源分在两条枝上

两条链共用一个基类 BaseSource,放的是两边都要的东西:轨道描述、能不能 seek、放到哪儿了。往下分成两支。一支是 AVSource,吐压缩码流,数据源那一篇讲的就是它。另一支是 RawSource,吐拿到就能用的数据。
RawSource 这个名字对应的是一句判据:手里有 PCM、YUV 或者显卡上的一张纹理,就可以当源,也可以把处理过的这类数据送去编码器。录制那条链的入口就在这一族上。
这一族再往下是采集。DeviceSource 自己就是一个源,内部组合一个音频源与一个视频源,把两路帧一起转发出去;音视两支再各自往下分,视频那条是 VideoSource,音频那条是 AudioSource。两者下面各有一个模板基类,管的是单台设备这件事:设备实现只填两个钩子——打开时做什么,关闭时做什么;设备名、设备 id、打开状态这几样收在基类里。
文件解码那个源 AMediaSource 也挂在 RawSource 下面。它和采集的区别只在上游:一个从文件解出帧,一个从设备取帧,交出去的帧是同一种。数据源那一篇里说反向那条路几乎是白捡的,指的就是这一族。
设备这一层多出来的动作都在设备管理器上:列出全部设备、刷新列表、按 id 找、按序号取;源自己那几个接口动作见数据源那一篇。设备的类别与 SDK 槽位是两组枚举:视频设备分摄像头、窗口、显示器三种,音频设备分麦克风与声卡回放两种;一个 SDK 槽对一个设备实现,现在有五个槽,默认取哪个由当前平台定。设备 id 由实现自己定:采集卡那支用的是型号名加拓扑号,同一型号的多张卡也能分开。
帧的三种形态与缓冲类型

源的出口只有三种形态。
CPU 视频帧是 YUVFrame:三个平面指针、三个行距、像素格式、时间戳。行距单独给出来,是因为帧里通常带着对齐用的填充,按宽度乘高度取数据会取错,得按行距一行一行走。
GPU 视频帧是 GpuFrame:一个不透明的缓冲指针,加一个上下文指针。这块纹理是哪一家的对象、怎么用,由上下文那一侧决定——取设备、取纹理都从它身上拿,也就是渲染分层那一篇里像素处理那个角色的接口。源这一层不关心它指向 D3D11 还是别的图形对象。
音频帧是 AvoxAFrame:一个时间戳加一块数据——指针、长度、是不是引用。音频没有平面与行距那些事,一块 PCM 就是全部。
三种形态对上层的出口是三个回调:CPU 视频帧一个,GPU 帧一个,音频帧一个。打开之后源先交一次轨道描述(宽高、帧率、采样率),再按这三种之一出帧。
视频帧再往上走会被包一层:VideoFrame 里挂一个 VideoBuffer,缓冲对象上记着类型。类型一共五个取值:cpu、vulkan、dx11、opengles、metal——GPU 帧再按纹理是哪一家的对象分四档,加上 CPU 帧正好是这五个,和解码器那组类型不是一回事。这一层的用处是让下游不必猜手上这块是内存还是纹理,渲染和编码各自按类型决定怎么对待它。软解切硬解、硬解切软解,换的就是这个类型,帧对象本身可以复用。
采集的帧落在内存或纹理:下游按类型分流
同一个设备源交出来的是 CPU 帧还是 GPU 帧,取决于系统那套接口给什么。摄像头与麦克风给内存里的帧,像素格式是 NV12、YUY2 这一档;屏幕与窗口给纹理,因为系统捕获交出来的本来就是一张。设备源自己不挑这个,上游给什么它就交什么。
分岔在下游。渲染那条链两种都收:纹理走导入,内存的自己上传。编码那条链要分开——软件编码器吃 CPU 帧,硬件编码器吃纹理。这一章走的是软编那条;硬编替掉编码器的那一侧,归各平台的硬编那一篇。
采集源还有一个共通的脾气:它是实时的。帧队列只留很少几帧,满了丢最旧的那帧,丢帧不报错。这一条对录制是个硬约束:下游只能跟着来源的节奏走,接不住就丢,积压在这里不解决问题,只会把延迟堆起来。
外部纹理入口:跨设备的句柄与输入层

设备源走的是「源交帧」这条口子。还有一条路不进源:宿主自己渲染好的画面要进引擎的 Vulkan 管线,滤镜、字幕、合成都在那边做;反过来也有,引擎合成完的结果交给宿主上屏。这条路在渲染层,动作成对:输入向 enableVkInput、setVkInputHandle、disableVkInput,输出向 enableVkOutput、getVkOutputHandle、disableVkOutput。
跨这一道交换的不是纹理对象,是各平台自己的共享句柄:Windows 上是 NT 句柄,Android 上是 AHardwareBuffer,Apple 上是 IOSurface 加它的 id。句柄连同宽高放进一个结构体递给引擎,跨设备、跨进程的共享都靠它。结构体里按平台留了三档,这一版落地的是 Windows 那条——句柄怎么导进来、内存怎么绑,硬解帧那两条去处那一篇拿 Windows 走了一遍。
输入向落在 VK 渲染对象的输入层上。开启时记下宽高、取出输入层;拿到句柄之后导入成一张共享图,再把输入层的互操作标志打开。之后输入层每帧做的事很短:把这张导入的图拷进管线自己的图,剩下的处理与呈现走渲染分层那一篇讲的那条路。
这条路绕开的是整条源链:没有解封装,没有解码,也没有帧队列,外来的一张图直接进管线。它没有做成一个数据源类型,原因也在这里:宿主画面不必伪装成包,也不必冒充一台设备。
采集卡的插件实现:注册进设备槽的设备源
采集源一般跟着平台走:同一个契约,三个平台各拿一套系统接口实现,都编在引擎里。契约上只有虚函数,所以设备源也能整块做成插件。
采集卡这条就是这么进来的:一个单独编出来的动态库,加载时往设备管理表里登记一个 SDK 槽,引擎侧编译期不认识它。设备枚举由插件自己做——用采集卡 SDK 的迭代器把所有卡读出来,每张卡建一个源对象放进列表。
它填的东西不多。打开时按期望的宽高帧率在制式表里找最接近的那个;出帧时把 SDK 给的缓冲拷成自己的 CPU 帧,格式固定是 8Bit YUV,宽高与行距都自带;信号制式中途变了(换了一路信号源),它自己重开输入,不往上抛错。驱动没装的时候设备列表是空的,插件本身照常加载,不拦启动。
线程也归它自己:采集卡的回调线程只负责把帧拷进队列,推帧在它自己那条线程上做——数据源那一篇里那条规律在这里同样成立,谁实现源,谁管自己的线程。
这一路正好当录制要用的那个例子:不占平台那几套设备源,交出来的又是 CPU 帧,直接接得上软编。
到这里,录制这条链的入口齐了:两条枝的分法、三种帧形态、一条不经过源的外部入口,外加一支能整块做成插件的设备源。下一步把它反过来用——播放这条链是包进帧出,录制要帧进包出,还要接上软件编码器。下面三篇就是这条链的三段:帧进包出、包变成文件、目标换成一行地址。
引擎 avox 在 GitHub 开源:github.com/xxxzhou/avox,这一族的类层次、三种帧形态与那条外部入口都在里面。
音视频开发 播放器 数据源 采集 帧源 C++