相机这一侧的设备枚举与格式白名单

相机的管理器在初始化时建一个系统相机管理器,拿到设备 id 列表,给每个 id 建一个源对象。往上接的是设备管理器那一层,列出设备、按序号取这些动作在帧源那一篇写过,这里只说 Android 这个槽里填的是谁。
建源对象的时候会读一次设备的能力信息,把可用的流配置遍历一遍。这一步里有两处筛选:跳过输入方向的配置项(只要输出),以及只留下一种格式——八位平面 YUV 那一档。别的格式(比如各种打包 RGB)一律丢掉。所以能进候选表的格式只有这一种,虽然它在不同设备上实际是 NV12 还是另一种平面排布,得看设备自己怎么报。
打开的时候才决定用哪一档:按上层要的分辨率与帧率,在候选表里挑一个最接近的。挑法是打分——先看宽高比差多少,再看长边差多少,再给「宁可大一点」一个惩罚项,最后看帧率差。上层不给的话有个默认:前置与后置各一套。
输出面:纹理直出,不走取帧接口
相机的帧可以落在内存里(用取帧接口一次拿一张图),也可以直接落进一个纹理。这里选的是纹理:开流之前建一条链路——起一个 EGL 上下文,建一个 SurfaceTexture 并给它一张外部纹理,再把它转成原生窗口,这个窗口就是要交给相机的输出面。
选纹理的理由在下面两处:编码那一侧要的是一个能收纹理的输入面,渲染那一侧本来就在处理外部纹理。走内存那条路,后面还要多一次上传。
会话的建法很直:用那个原生窗口建一个会话输出和一个相机输出目标,加上模板,然后用持续请求不停地出帧。改了模板或输出面就要重建会话。
一帧怎么走到上层:两个线程分工
相机出新帧时,系统是在 Java 侧回调的:自研的那个包装对象收到通知,把持有的原生指针回传给 C++。C++ 这个回调很短,只做两件事——置一个「有新帧」的标志,唤醒等待的线程。
真正取帧在源对象自己的任务线程里:等那个标志(带一个超时,免得白等),被唤醒后把这一帧更新到纹理上,再组装成帧对象交给上层。
这个分工带来两个行为:标志是单个布尔的,处理期间来的多次通知会合并成一次——也就是中间那些帧直接没了;队列本身由系统那层纹理对象提供,消费者每次只取最新的一张,生产者更快时旧的被丢掉。丢帧没有任何上报,只在等待超时那条路上记一条日志。
帧的形态:相机交出去的是纹理帧,格式标成八位平面那一档,而帧里的缓冲指针是空的——数据在那张外部纹理上,取的时候走上下文。
设备掉了之后
断开与出错走两个回调。断连就直接上报设备丢失;出错按系统给的错误码分档:被占用、被禁用、还是别的,各映射成一种错误类型往上报,附一句人能读的说明。会话关闭与激活也各有一个回调,用来维护那个打开标志。
这里没有自动重连:断了就只是上报,要重新拿到设备得由上层显式重开一次。权限被拒也不单独分一档,表现就是打开失败。
音频采集落在 Java 侧的录音类上

声音没有走纯 C 的那套录音接口,而是落到 Java 侧的录音类上。C++ 侧按名字把这个类和它的几个方法抓齐:构造、算最小缓冲、开始与停止、读、释放、取会话编号。
参数是几条默认:16 位有符号、采样率与声道数一套默认值。开流时按声道数选单声道或立体声的通道掩码,用静态方法算一次最小缓冲,再按它的两倍申请。如果算出来的值不合法,就按帧长回退算一个。
数据回 C++ 的方式是预分配一个字节数组并建全局引用,采集线程里阻塞读一次,把读到的字节拷进一个缓冲,组装成音频帧交上去。这里没有应用层队列,读是同步的,一帧交一次。
读出错(读回负数)就上报设备错误并退出循环。另外那个「是否已打开」的判据用的是会话编号是否大于零,这一条在默认音频会话编号可能为零的机型上会误判。
两条路都没有自动重连,恢复由上层重开
把这两条路放在一起看,有一个共同的取舍:相机与麦克风都没有自动重连。相机那条上报完设备丢失就停了,音频那条读失败就退出循环;要恢复,都得由上层重新走一遍打开。
这个取舍的代价是上层要多做一件事:订阅错误回调、在设备掉了之后决定是重开还是换设备。设备为什么掉、要不要重开、重开到哪一档,这些判断留给更清楚上下文的那一层。
对比 Windows 那一侧:屏幕采集的窗口不可见时是跳帧不是停采,摄像头掉线也只是上报。三端在这件事上的处理是一致的——采集这一层只负责把「掉了」这个事实报上去。
两条路汇到同一个口子
相机的纹理帧与麦克风的音频块,最后都进设备源那一个口子:设备源把两路合成一条,再经原始帧源交给上层观察者。后面讲录制的那条链就是从这儿接的,与 Windows 那侧是同一个模板——两端的差别只在拿哪块设备、以及输出面是自己建的还是宿主给的。
引擎 avox 在 GitHub 开源:github.com/xxxzhou/avox,相机那条链、音频那侧的 JNI 桥都在里面。
音视频开发 播放器 Camera2 AudioRecord JNI Android