一套会话,两个源

相机与麦克风各有一个管理器,各自建一个采集会话,并把会话与一条串行队列注入到每个源对象里。
挂输入的动作有固定顺序:开会话配置、把已有输入全部摘掉、按设备建输入、先问能不能加、再加进去、提交配置。摘光的做法是为了重配:每次打开都重新搭一遍。
输出那一侧有个复用判断:如果这个源还没有输出,就建一个视频或音频数据输出;已经有了就复用——复用意味着换源不必重建输出,委托与队列也不用重挂。然后把委托与那条串行队列挂上去,数据从委托进来。
委托是独立的类,只做一件事:把回调转给 C++ 的成员函数。这一层是为了让核心代码不沾委托协议。两个源建好后各自注册进视频与音频的设备槽——那张表与另外两个平台是同一张,上层按槽位取用,不必感知底下是哪套会话。
格式与方向
相机的可选格式来自设备自己报的那张表。这一侧同样只收一种平面格式(两个范围变体),其余不要;挑档走的是与另外两端同一套打分,默认目标按设备方位给——前置与后置各一套,分辨率与帧率一起给,其它方位取候选表里的第一项。
会话的预设分辨率是另一套固定档,设置之前先问一句能不能设,问不过就保持原样;上层要的分辨率在候选表里挑最近的一档。两套各自独立,可能出现实际输出与登记的尺寸不一致。
输出那边把像素格式定死成那一种平面格式,不留给系统挑。方向上前置镜像、后置不镜像,方向本身是硬设的——多设备各不相同方向的处理这一版没有做完。
回调、队列与丢帧
委托挂在一条串行队列上,等于同一时刻只有一帧在通道里,前一段没处理完,后一段就在队列里等着。迟到帧丢不丢由系统默认策略决定,这一侧没有显式设置。
丢帧不上报:回调里只对空样本直接返回,没有计数、也没有错误上报——这一层丢了多少、为什么丢,下游无从知道,只能自己数。这一点与 Android 那一侧不同,那边超时至少记一条日志。
采集拿到的时间戳其实被算过一次,但真正填进帧的是本地时钟那一刻的值。也就是说这一侧帧上的时间来自本地时钟,不是采集给的呈现时间——到编码那一篇会看到另一侧是怎么处理时间戳的。
设备失效之后

打开是「拆光重挂」:每次打开都先摘掉全部输入再重建,输出能复用就复用。
关流不停止会话,只把委托解绑。这样做是为了换相机的时候能秒切——会话还在跑,换的只是输入。常驻也有代价:设备一直被这套会话占着,与其它要用相机的应用怎么相处,这一版没有实测过,当下是按「换相机要快」取的舍。
进后台靠应用生命周期的通知:收到要失去活动状态的通知就置一个全局标志,回到活动再清掉。这是全局层面的进出后台,不是采集层的会话重配。
这里没有监听会话中断与设备拔插。也就是说相机被别的进程抢走、或者被系统中断时,这一层不会自己重配或重开。
三端采集对比
三条采集路在三个平台上的分布不一样,但收口处一样。
Windows 那边:屏幕与窗口共用一套抓取实现,主路是系统那套图形捕获、回退是 GDI;摄像头单独走一套媒体基础;声音两套,麦克风事件驱动、声卡回环轮询。
Android 那边:相机走 NDK 那套相机接口,输出面自己建;声音反过来落在 Java 侧的录音类上,同步读。
Apple 这一侧没有屏幕采集那一支——三条路里只有 Windows 有屏幕;相机与麦克风全落在同一套采集会话上,只是输入设备与输出类型不同。
三端一致的地方是收口:交出来的帧只有三种形态——纹理帧、CPU 平面帧、PCM 数据块;都汇到设备源那一个口子,再经原始帧源交给上层;设备失效都只上报、不自愈。
采到的帧:直接装系统给的像素缓冲
这一侧交出去的帧里装的直接就是系统给的像素缓冲,格式标成平面那一档。它的生命周期由那个样本管着,这一层不用手动放。
渲染那一侧拿到它之后按像素缓冲处理,直接映射成纹理——所以从采集到渲染这一段没有额外的拷贝。
音频那条同理,只是输出换成音频数据输出;音频描述从设备报的第一个采样描述转过来,采样率、声道数都从那一份描述取。
这里有一处与相机不同:相机的档是遍历设备格式筛出来的,音频只取设备报的第一个采样描述——音频设备的采样率与通道数一般就那么一档,不必筛。
关流的顺序是:先停任务线程,再解绑委托(不停止会话),真正清理时才摘输入。
引擎 avox 在 GitHub 开源:github.com/xxxzhou/avox,这套采集会话的搭建与委托桥都在里面。
音视频开发 播放器 AVCapture 采集 iOS macOS