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
主要是获取设备列表、检查朝向、创建 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
Camera2
相机方向
通常前置摄像头输出的图像方向是逆时针旋转 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 有几点注意事项:
// 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.
有哪些坑
//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();
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.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);}
内存抖动优化
运行 AppRTC-Android 程序,我们会发现内存抖动非常严重:
这块我们可以利用 Allocation Tracker 进行分析和优化
https://blog.piasy.com/2017/07/24/WebRTC-Android-Camera-Capture/