WebRTC源码分析(一):安卓相机采集实现分析
WebRTC 的代码量不小,一次性看明白不太现实,在本系列中,我将试图搞清楚三个问题:
- 客户端之间如何建立连接?
- 客户端之间如何实现数据传输?
- 音视频数据的采集、预览、编码、传输、解码、渲染完整流程。
本文是第一篇,我将从最熟悉的采集入手,分析一下 WebRTC-Android 相机采集的实现。
WebRTC-Android 的相机采集主要涉及到以下几个类:Enumerator,Capturer,Session,SurfaceTextureHelper。
其中 Enumerator 创建 Capturer,Capturer 创建 Session,实现对相机的操作,SurfaceTextureHelper 实现用 SurfaceTexture 接收数据。
Enumerator
CameraEnumerator 接口如下:
public interface CameraEnumerator { public String[] getDeviceNames(); public boolean isFrontFacing(String deviceName); public boolean isBackFacing(String deviceName); public List<CaptureFormat> getSupportedFormats(String deviceName); public CameraVideoCapturer createCapturer( String deviceName, CameraVideoCapturer.CameraEventsHandler eventsHandler); }
主要是获取设备列表、检查朝向、创建 Capturer。
Capturer
WebRTC 视频采集的接口定义为 VideoCapturer,其中定义了初始化、启停、销毁等操作,以及接收启停事件、数据的回调。
相机采集的实现是 CameraCapturer,针对不同的相机 API 又分为 Camera1Capturer 和 Camera2Capturer。相机采集大部分逻辑都封装在 CameraCapturer 中,只有创建 CameraSession 的代码在两个子类中有不同的实现。
下面分别看看 VideoCapturer 几个重要的 API 实现逻辑。
initialize
initialize 比较简单,只是保存一下传入的相关对象。
startCapture
startCapture 则会先检查当前是否正在创建 session,或者已有 session 正在运行,这里保证了不会同时存在多个 session 在运行。而众多状态成员的访问都通过 stateLock 进行保护,避免多线程安全问题。
如果需要创建 session,则在相机操作线程创建 session,同时在主线程检测相机操作的超时。所有相机的操作都切换到了单独的相机线程,以避免造成主线程阻塞,而检查超时自然不能在相机线程,否则相机线程被阻塞住之后超时回调也不会执行。
我们发现 capturer 中并没有实际相机操作的代码,开启相机、预览的代码都封装在了 CameraSession 中,那这样 capturer 的逻辑就得到了简化,切换摄像头、失败重试都只需要创建 session 即可,capturer 可以专注于状态维护和错误处理的逻辑。
CameraCapturer 状态维护和错误处理的逻辑还是非常全面的:相机开启状态、相机运行状态、切换摄像头状态、错误重试、相机开启超时,全部都考虑到了。另外相机切换、开关相机、错误事件,统统都有回调通知。这里就充分体现出了 demo 和产品的差别,开启相机预览的 demo 十行代码就能搞定,而要全面考虑各种异常情况,就需要费一番苦心了。
不过这里仍有一点小瑕疵,错误回调的参数是字符串,虽然可以很方便的打入日志,但不利于代码判断错误类型。最好是参数使用错误码,然后准备一个错误码到错误信息的转换函数。
stopCapture
stopCapture 时会先判断是否正在创建 session,如果正在创建,那就需要等待其创建完毕。通过检查后,如果当前有 session 正在运行,就在相机线程关闭 session。
changeCaptureFormat
改变采集格式需要重启采集,即先 stopCapture,再 startCapture。这俩操作都是异步的,会不会有问题?这就涉及到 Handler 的一点知识了,向 Handler 提交的消息、任务,都会被加入到同一个队列中,提交到队列中的任务会保证按序执行,即先提交一定会先执行,所以这里我们不必担心关闭相机和开启相机顺序错乱。
switchCamera
switchCamera 也会先停止老的 session,再创建新的 session,只不过还需要检查相机个数、实现切换状态通知逻辑。
这块代码应该有个小问题:startCapture 会把 openAttemptsRemaining 设置为 MAX_OPEN_CAMERA_ATTEMPTS,但切换摄像头时只会将其设置为 1,这个不对称应该没什么道理,所以我认为应该保持一致。
Session
前面我们已经知道,和相机 API 实际打交道的代码都在 CameraSession 中,这里我们就一探其究竟。
开启相机、开启预览、设置事件回调的代码都在创建 session 的工厂方法 Camera1Session.create 和 Camera2Session.create 中。停止相机和预览则定义了一个 stop 接口。
具体的相机 API 使用就比较简单了。
Camera1
- 创建 Camera 对象:Camera.open;
- 设置预览 SurfaceTexture,用来接收帧数据(位于显存中):camera.setPreviewTexture;
- 选择合适的相机预览参数(尺寸、帧率、对焦):Parameters 和 camera.setParameters;
- 如果需要获取内存数据回调,则需要设置 buffer 和 listener:camera.addCallbackBuffer 和 camera.setPreviewCallbackWithBuffer;
- 如果需要相机服务为我们调整数据方向,则可以设置旋转角度:camera.setDisplayOrientation;
- 开启预览:camera.startPreview;
- 停止预览:camera.stopPreview 和 camera.release;
Camera2
- 创建 CameraManager 对象,相机操作始于“相机管家”:context.getSystemService(Context.CAMERA_SERVICE);
- 创建 CameraDevice 对象:cameraManager.openCamera;
- 和 Camera1 不同,Camera2 的操作都是异步的,调用 openCamera 时我们会传入一个回调,在其中接收相机操作状态的事件;
- 创建成功:CameraDevice.StateCallback#onOpened;
- 创建相机对象后,开启预览 session,设置数据回调:camera.createCaptureSession,同样,这个操作也会传入一个回调;
- session 开启成功:CameraCaptureSession.StateCallback#onConfigured;
- 开启 session 后,设置数据格式(尺寸、帧率、对焦),发出数据请求:CaptureRequest.Builder 和 session.setRepeatingRequest;
- 停止预览:cameraCaptureSession.stop 和 cameraDevice.close;
相机方向
通常前置摄像头输出的图像方向是逆时针旋转 270° 的,后置摄像头是 90°,但存在一些意外情况,例如 Nexus 5X 前后置都是 270°。
在 Camera1 里我们可以通过 camera.setDisplayOrientation 接口来控制相机的输出图像角度,但实际上无论是获取内存数据,还是获取显存数据(SurfaceTexture),这个调用都不会改变数据,它只是影响了相机输出数据时携带的变换矩阵的方向。Camera2 里没有相应的接口,但相机服务会自动为我们合理调整变换矩阵方向,所以相当于我们正确地调用了类似的接口。
如果利用 camera.setPreviewDisplay 或者 camera.setPreviewTexture 实现预览,那 camera.setDisplayOrientation 确实会让预览出来的图像方向发生变化,因为相机服务在渲染到 SurfaceView/TextureView 时会应用变换矩阵,使得预览画面是旋转之后的画面。
除了方向还有一个镜像的问题,Camera1 在前置摄像头时会自动为我们翻转一下画面(当然也只是修改了变换矩阵),例如前置摄像头输出的图像方向是逆时针旋转 270° 时,那就会把图像上下翻转,如果我们再设置一个旋转 90°,把图像旋正,那就相当于是左右翻转,也就达到了镜像的效果,即:前置摄像头我们用左手摸左边的脸,预览里也是显示在屏幕左边(但预览在和我们四目相对,所以实际是“他”的右边,是有点绕…)。
至于怎么设置 camera.setPreviewDisplay 的参数,使得直接预览可以方向正确,可以使用以下代码:
privatestaticintgetRotationDegree(intcameraId){intorientation=0;WindowManagerwm=(WindowManager)applicationContext.getSystemService(Context.WINDOW_SERVICE);switch(wm.getDefaultDisplay().getRotation()){caseSurface.ROTATION_90:orientation=90;break;caseSurface.ROTATION_180:orientation=180;break;caseSurface.ROTATION_270:orientation=270;break;caseSurface.ROTATION_0:default:orientation=0;break;}if(cameraInfo.facing==Camera.CameraInfo.CAMERA_FACING_FRONT){return(720-(cameraInfo.orientation+orientation))%360;}else{return(360-orientation+cameraInfo.orientation)%360;}}
SurfaceTextureHelper
SurfaceTextureHelper 负责创建 SurfaceTexture,接收 SurfaceTexture 数据,相机线程的管理。
创建 SurfaceTexture 有几点注意事项:
- 创建 OpenGL texture 时所在的线程需要准备好 GL 上下文,WebRTC 中将这部分逻辑封装在 EglBase 类中;
- 创建 SurfaceTexture 所在的线程,将是其数据回调 onFrameAvailable 发生的线程;不过 API 21 引入了一个新的重载版本,支持指定回调所在线程的 Handler;
// The onFrameAvailable() callback will be executed on the SurfaceTexture ctor thread. // See: http://grepcode.com/file/repository.grepcode.com/java/ext/com.google.android/// android/5.1.1_r1/android/graphics/SurfaceTexture.java#195.// Therefore, in order to control the callback thread on API lvl < 21, // the SurfaceTextureHelper is constructed on the |handler| thread.
有哪些坑
- 低版本(5.0 以前)的系统上,Camera1 停止预览时,不要手贱地调用下列接口设置 null 值:setPreviewDisplay/setPreviewCallback/setPreviewTexture(文档中确实也说过不要调用…),否则可能导致系统服务全线崩溃,最终导致手机重启:
- Camera1 停止预览可能存在死锁(没有解决):
//Note: stopPreview or other driver code might deadlock. Deadlock in// android.hardware.Camera._stopPreview(Native Method) has been observed on// Nexus 5 (hammerhead), OS version LMY48I.camera.stopPreview();
- Camera2 相关的代码在 4.4.2 之前的系统上遇到 VerifyError:
try{returncameraManager.getCameraIdList();// On Android OS pre 4.4.2, a class will not load because of VerifyError if it contains a// catch statement with an Exception from a newer API, even if the code is never executed.// https://code.google.com/p/android/issues/detail?id=209129}catch(/* CameraAccessException */AndroidExceptione){Logging.e(TAG,"Camera access exception: "+e);returnnewString[]{};}
- 利用 SurfaceTexture 接收帧数据,有些机型可能获取到的数据是黑屏(MX5 遇到过):需要设置 SurfaceTexture 的 buffer size,surfaceTexture.setDefaultBufferSize
- 利用 SurfaceTexture 接收帧数据,通过 SurfaceTexture.getTimestamp 接口获取时间戳,这个时间戳是相对时间,而且前面会有几帧值为 0:相对时间的问题可以在首帧记录下和物理时间的差值,然后计算后续每帧的物理时间戳,但头几帧时间戳为 0,所以我们记下差值就得等到非零时,而头几帧则可以直接使用物理时间作为时间戳;
- surfaceTexture.updateTexImage 和 eglSwapBuffers 会发生死锁,我们需要自行加锁:
// SurfaceTexture.updateTexImage apparently can compete and deadlock with eglSwapBuffers,// as observed on Nexus 5. Therefore, synchronize it with the EGL functions.// See https://bugs.chromium.org/p/webrtc/issues/detail?id=5702 for more info.synchronized(EglBase.lock){surfaceTexture.updateTexImage();}synchronized(EglBase.lock){EGL14.eglSwapBuffers(eglDisplay,eglSurface);}
- 有些机型上,用 TextureView 实现预览,onSurfaceTextureAvailable 回调不会被调用,导致无法开启预览,这个问题有可能可以通过开启硬件加速得以解决(参考 StackOverflow 这个问题,我还顶过),但有可能这个办法也不管用,那么恭喜你,得再费一番脑细胞了。我就遇到过这种情况,在一款 OPPO 4.3 的手机上,折腾半天发现延迟一会儿重设一次 LayoutParams 就能触发,所以就先这么搞了;
内存抖动优化
运行 AppRTC-Android 程序,我们会发现内存抖动非常严重:
这块我们可以利用 Allocation Tracker 进行分析和优化
https://blog.piasy.com/2017/07/24/WebRTC-Android-Camera-Capture/
WebRTC源码分析(一):安卓相机采集实现分析的更多相关文章
- WebRTC 源码分析(三):安卓视频硬编码
数据怎么送进编码器? 怎么从编码器取数据? 如何做流控? 在开始之前,我们先了解一下 MediaCodec 的基本知识. MediaCodec 基础 Developer 官网 上的描述已经很清楚了,下 ...
- WebRTC源码分析四:视频模块结构
转自:http://blog.csdn.net/neustar1/article/details/19492113 本文在上篇的基础上介绍WebRTC视频部分的模块结构,以进一步了解其实现框架,只有了 ...
- 1.4、WebRTC源码
文章导读:本篇文章给读者展示WebRTC的源码目录结构,为读者构建全方位的知识体系,如果你有兴趣下载webrtc的源码来编译运行,本节内容可以作为你了解源码的简要说明书,webrtc源码非常庞大的,讲 ...
- webRTC源码下载 Windows Mac(iOS) Linux(Android)全
webRTC源码下载地址:https://pan.baidu.com/s/18CjClvAuz3B9oF33ngbJIw 提取码:wl1e Windows版:visual studio 2017工 ...
- 编译最新版webrtc源码和编译好的整个项目10多个G【分享】
编译最新版webrtc源码和编译好的整个项目10多个G[分享] 参考https://webrtc.org/native-code/development/编译最新版webrtc源码: Git clon ...
- WebRtc 源码下载
项目需要用到WebRtc,记录下基本下载的步骤: 1.下载depot_tools,利用depot_tools 下载WebRtc源码 git clone https://chromium.googles ...
- WebRTC源码开发(一)MacOS下源码下载、编译及Demo运行
工作需要测试网络传输算法,逐学习WebRTC源码 工作环境 Mac OS 10.14 Xcode 10.2.1 源码下载 从google(需要[你懂的]) 首先[你懂的] 打开终端,输入curl ww ...
- HashMap源码深度剖析,手把手带你分析每一行代码,包会!!!
HashMap源码深度剖析,手把手带你分析每一行代码! 在前面的两篇文章哈希表的原理和200行代码带你写自己的HashMap(如果你阅读这篇文章感觉有点困难,可以先阅读这两篇文章)当中我们仔细谈到了哈 ...
- WEBRTC源码片段分析(1)音频缓冲拷贝
源码位置webrtc/webrtc/modules/audio_device/ios/audio_device_ios.cc函数OSStatus AudioDeviceIPhone::RecordPr ...
随机推荐
- Linux vi/vim替换命令的使用说明[转]
vi/vim 中可以使用 :s 命令来替换字符串.:s/vivian/sky/ 替换当前行第一个 vivian 为 sky:s/vivian/sky/g 替换当前行所有 vivian 为 sky:n, ...
- WPF 项目升级错误
今天将一个 WPF 项目从 .NET 4.0 升级至 .NET 4.6.1 时,出现一个错误: 错误 未知的生成错误"程序集"PresentationFramewor ...
- 【Linux运维-集群技术进阶】Nginx+Keepalived+Tomcat搭建高可用/负载均衡/动静分离的Webserver集群
额.博客名字有点长.. . 前言 最终到这篇文章了,心情是有点激动的. 由于这篇文章会集中曾经博客讲到的全部Nginx功能点.包含主要的负载均衡,还有动静分离技术再加上这篇文章的重点.通过Keepal ...
- 【R】R语言常用函数
R语言常用函数 基本 一.数据管理vector:向量 numeric:数值型向量 logical:逻辑型向量character:字符型向量 list:列表 data.frame:数据框c:连接为向量或 ...
- 获取IOS屏幕尺寸大小
转自:http://www.open-open.com/lib/view/open1395752090322.html 1.app尺寸,去掉状态栏 CGRect r = [ UIScreen main ...
- Angular的重和利
1.第一重:TypeScript,TypeScript语言的特性还是比较丰富的,而且一直在发展,再就是跨语言集成问题,要想Nice对第三方lib做集成,需要自己写d.ts,针对有些第三方库,这件事情有 ...
- [Windows Azure] Configuring and Deploying the Windows Azure Email Service application - 2 of 5
Configuring and Deploying the Windows Azure Email Service application - 2 of 5 This is the second tu ...
- 【Java】Java复习笔记-第二部分
类和对象 类:主观抽象,是对象的模板,可以实例化对象 习惯上类的定义格式: package xxx; import xxx; public class Xxxx { 属性 ······; 构造器 ·· ...
- 【Linux技术】linux库文件编写·入门
一.为什么要使用库文件 我们在实际编程中肯定会遇到这种情况:有几个项目里有一些函数模块的功能相同,实现代码也相同,也是我们所说的重复代码.比如,很多项目里都有一个用户验证的功能. 代码段如下: //U ...
- 安装C/C++交叉编译环境
转:http://blog.csdn.net/nokiaguy/article/details/8509739 X86架构的CPU采用的是复杂指令集(Complex Instruction Set C ...