WebRTC 在 Android 端实现一对一通信
WebRTC 在 Android 端实现一对一通信申请权限申请静态权限申请动态权限 引入 WebRTC 库构造 PeerConnectionFactory创建音视频源视频采集视频渲染创建 PeerConnection建立信令系统参考
WebRTC 在 Android 端实现一对一通信
在 Android 端,我们将按以下几个步骤实现 WebRTC 一对一通信:
申请权限引入 WebRTC 库构造 PeerConnectionFactory创建音视频源视频采集视频渲染创建 PeerConnection建立信令系统申请权限
至少需要申请三种权限:
CAMERA 权限:用于采集视频数据RECORD_AUDIO 权限:用于采集音频数据INTERNET 权限:用于通过网卡传输媒体数据在Android中,申请权限分为静态权限申请和动态权限申请。
申请静态权限
在 Android 项目中的 AndroidManifest.xml 中增加以下代码:
<uses-feature android:name="android.hardware.camera" /><uses-feature android:glEsVersion="0x00020000" android:required="true" /><uses-permission android:name="android.permission.CAMERA" /><uses-permission android:name="android.permission.RECORD_AUDIO" /><uses-permission android:name="android.permission.INTENET" />
申请动态权限
随着 Android 的发展,对安全性要求越来越高。除了申请静态权限之外,还需要动态申请权限。
API:
void requestPermissions(String[] permissions, intrequestCode);
实际上,对于权限这块的处理真正做细了要写不少代码,好在 Android 官方给我们又提供了一个非常好用的库 EasyPermissions,有了这个库我们可以少写不少代码。使用 EasyPermissions 非常简单,在MainActivity 文件的 onCreate() 方法中调用 requestPermissions() 方法即可,当然还要实现 onRequestPermissionsResult() 回调方法。
protected void onCreate (Bundle savedInstanceState){String[] perms = {Manifest.permission.CAMERA,Manifest.permission.RECORD_AUDIO};if (!EasyPermissions.hasPermissions(this, perms)) {EasyPermissions.requestPermissions(this,"Need permissions for camera & microphone",0,perms);}}@Overridepublic void onRequestPermissionsResult(int requestCode,String[] permissions,int[] grantResults){super.onRequestPermissionsResultrequestCode,permissions,grantResults);EasyPermissions.onRequestPermissionsResult(requestCode,permissions,grantResults,this);}
引入 WebRTC 库
在 Module 级别的 build.gradle 文件中增加以下代码:
dependencies {implementation 'io.socket:socket.io-client:1.0.0'implementation 'org.webrtc:google-webrtc:1.0.+'implementation 'pub.devrel:easypermissions:1.1.3'}
第一个是 WebRTC 库,第二个是 socket.io 库,用它来与信令服务器互联,第三个是前面用到的 EasyPermissions 库。
构造 PeerConnectionFactory
在 WebRTC 中使用了大量的设计模式,对于 PeerConnectionFactory 也是如此。它本身就是工厂模式,而这个构造 PeerConnection 等核心对象的工厂又是通过构建者模式构建出来的。
在我们构造 PeerConnectionFactory 之前,首先要对其进行初始化,然后可以通过构建者模式来构造 PeerConnecitonFactory 对象了。
PeerConnectionFactory.initialize(...);PeerConnectionFactory.Builder builder =PeerConnectionFactory.builder().setOptions(options).setAudioDeviceModule(adm).setVideoEncoderFactory(encoderFactory).setVideoDecoderFactory(decoderFactory);return builder.createPeerConnectionFactory();
通过上面的代码,大家也就能够理解为什么 WebRTC 要使用 buider 模式来构造 PeerConnectionFactory 了吧?主要是方便调整建造 PeerConnectionFactory 的组件,如编码器、解码器等。
从另外一个角度我们也可以了解到,要更换WebRTC引擎的编解码器该从哪里设置了。
创建音视频源
有了PeerConnectionFactory对象,我们利用它就可以创建数据源了。
实际上,数据源是 WebRTC 对音视频数据的一种抽象,主要是让上层逻辑和底层的音视频设备之间解耦。数据源可以从不同的音视频设备中获取数据,并将数据输出给上层的 Track。
创建数据源的方式如下:
VideoSource videoSource = mPeerConnectionFactory.createVideoSource(false);mVideoTrack = mPeerConnectionFactory.createVideoTrack(VIDEO_TRACK_ID, videoSource);AudioSource audioSource = mPeerConnectionFactory.createAudioSource(new MediaConstraints());mAudioTrack = mPeerConnectionFactory.createAudioTrack(AUDIO_TRACK_ID, audioSource);
先创建 VideoSource 和 AudioSource,再绑定到对应的 VideoTrack 和 AudioTrack 上,相当于为 VideoSource/AudioSource 指定了输出。
对于音频来说,在创建 AudioSource 时,就开始从默认的音频设备捕获数据了;对于视频来说,我们还需要指定采集视频数据的设备,然后使用观察者模式从指定设备中获取数据。
视频采集
在 Android 系统下有两种 Camera,一种称为 Camera1,是一种比较老的采集视频数据的方式,另一种称为 Camera2,是一种新的采集视频的方法。它们之间的最大区别是 Camera1使用同步方式调用API,Camera2使用异步方式,所以Camera2更高效。
我们看一下 WebRTC 是如何选择控制摄像头的系统的:
private VideoCapturer createVideoCapturer(){if (Camera2Enumerator.isSupported(this)) {return createCameraCapturer(new Camera2Enumerator(this));} else {return createCameraCapturer(new Camera1Enumerator(true));}}
逻辑很简单,看 Android 设备是否支持 Camera2,如果支持就使用 Camera2,否则使用 Camera1。
一般情况下,移动端有两个摄像头,默认使用前置摄像头。通过 CameraEnumerator 类,我们可以获取 Android 系统上所有的摄像头,还能通过 isFrontFacing() 方法检测出该摄像头是前置的还是后置的。
private VideoCapturer createCameraCapturer(CameraEnumerator enumerator){final String[] deviceNames = enumerator.getDeviceNames();// First, try to find front facing cameraLogging.d(TAG, "Looking for front facing cameras.");for (String deviceName : deviceNames){if (enumerator.isFrontFacing(deviceName)){Logging.d(TAG, "Creating front facing camera capturer.");VideoCapturer videoCapturer = enumerator.createCapturer(deviceName, null);if (videoCapturer != null){return videoCapturer;}}} // Front facing camera not found, try something elseLogging.d(TAG, "Looking for other cameras.");for (String deviceName : deviceNames){if (!enumerator.isFrontFacing(deviceName)){Logging.d(TAG, "Creating other camera capturer.");VideoCapturer videoCapturer = enumerator.createCapturer(deviceName, null);if (videoCapturer != null){return videoCapturer;}}}return null;}
在获到到具体的设备后,再看其是否有前置摄像头,如果有就使用第一个前置摄像头作为默认摄像头,否则使用第一个后置摄像头作为默认摄像头。
目前 VideoSource 与 VideoTrack 已经关联在一起了,且 VideoCapturer 也创建好了。接下来只需要将 VideoCapturer 与 VideoSource 再次关联在一起,VideoTrack 就可以源源不断地从设备上获取数据了。
mSurfaceTextureHelper = SurfaceTextureHelper.create("CaptureThread",mRootEglBase.getEglBaseContext());mVideoCapturer.initialize(mSurfaceTextureHelper,getApplicationContext(),videoSource.getCapturerObserver());mVideoTrack.setEnabled(true);
VideoCapturer 与 VideoSource 通过 VideoCapturer 的 initialize() 函数关联在一起。在 Android 系统中,必须为 Camera 设置一个 Surface 才能开启摄像头,并从中获取数据。CapturerObserver 是 VideoCaptuer 的观察者,videoSource 可以通过它从 VideoCaptuer 中获取数据。
当然,最后还要调用一下 VideoCaptuer 对象的 startCapture 方法真正的打开摄像头,这样 Camera 才会真正的开始工作:
@Overrideprotected void onResume(){super.onResume();mVideoCapturer.startCapture(VIDEO_RESOLUTION_WIDTH,VIDEO_RESOLUTION_HEIGHT,VIDEO_FPS);}
采集的分辨率要符合16:9/9:16/4:3/3:4,帧率通常设置为15帧。
视频渲染
Android 系统下,WebRTC 使用 OpenGL ES 进行视频渲染。基本步骤为:
先将视频从主内存复制到 GPU 上。通过 OpenGL ES 管道渲染到 GPU 的内存中。输出给显卡并最终显示在手机屏幕上。用于展示视频的控件是 WebRTC 对 Android 系统控件 SurfaceView 的封装。WebRTC 封装后的 SurfaceView 类为 org.webrtc.SurfaceViewRenderer。
在界面定义中应该定义两个SurfaceViewRenderer,一个用于显示本地视频,另一个用于显示远端视频。
<org.webrtc.SurfaceViewRendererandroid:id="@+id/LocalSurfaceView"ndroid:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_gravity="center" /><org.webrtc.SurfaceViewRendererandroid:id="@+id/RemoteSurfaceView"android:layout_width="120dp"android:layout_height="160dp"android:layout_gravity="top|end"android:layout_margin="16dp"/>
通过上面的代码我们就将显示视频的 View 定义好了。光定义好这两个View 还不够,还要对它做进一步的设置:
mLocalSurfaceView.init(mRootEglBase.getEglBaseContext(), null);mLocalSurfaceView.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FILL);mLocalSurfaceView.setMirror(true);mLocalSurfaceView.setEnableHardwareScaler(false);
其含义是:
使用 OpenGL ES 的上下文初始化 View。设置图像的拉伸比例,SCALE_ASPECT_FILL 表示将视频按比例填充到 View 中。设置图像显示时按纵轴反转,不然视频显示的内容与实际内容正好相反。是否打开便件进行拉伸,设置为不打开。通过上面的设置,我们的 View 就设置好了,对于远端的 View 与本地 View 的设置是一样的。
接下来将从摄像头采集的数据设置到该 View 里就可以显示了:
mVideoTrack.addSink(mLocalSurfaceView);
上面代码的含义是将 mLocalSurfaceView 设置为 VideoTrack 的输出。
WebRTC 通过 Capturer 采集到视频数据后,会交给 VideoSource,VideoSource 作为 VideoTrack 的源又会将数据转发给 VideoTrack。而将 View 设置为 VideoTrack 的输出后,最终视频就会在 View 中展示。
对于远端来说与本地视频的渲染显示是类似的,只不过数据源是从网络获取的。
创建 PeerConnection
要想从远端获取数据,我们就必须创建 PeerConnection 对象。该对象的用处就是与远端建立联接,并最终为双方通讯提供网络通道。
我们来看下如何创建 PeerConnecion 对象。
PeerConnection.RTCConfiguration rtcConfig = new PeerConnection.RTCConfiguration(iceServers);PeerConnection connection = mPeerConnectionFactory.createPeerConnection(rtcConfig, mPeerConnectionObserver);connection.addTrack(mVideoTrack, mediaStreamLabels);connection.addTrack(mAudioTrack, mediaStreamLabels);
PeerConnection 对象的创建还是要使用我们之前讲过的 PeerConnectionFactory 来创建。WebRTC 在建立连接时使用 ICE 架构,一些参数需要在创建 PeerConnection 时设置进去。另外,当 PeerConnection 对象创建好后,我们应该将本地的音视频轨添加进去,这样 WebRTC 才能帮我们生成包含相应媒体信息的 SDP,以便于后面做媒体能力协商使用。通过上面的方式,我们就将 PeerConnection 对象创建好了。
与 JS 中的 PeerConnection 对象一样,当其创建好之后,可以监听一些我们感兴趣有事件了,如收到 Candidate 事件时,我们要与对方进行交换。PeerConnection 事件的监听与 JS 还是有一点差别的。在 JS 中,监听 PeerConnection的相关事件非常直接,直接实现peerconnection.onXXX就好了。而 Android 中的方式与 JS 略有区别,它是通过观察者模式来监听事件的。
mPeerConnectionObserver = new PeerConnection.Observer() {// 与 onicecandidate 方法对应@Overridepublic void onIceCandidate(IceCandidate iceCandidate) {...}// 与 ontrack 方法对应@Overridepublic void onAddTrack(RtpReceiver rtpReceiver, MediaStream[] mediaStreams) {...}}
建立信令系统
为了与 JS 端互通,Android 端必须使用与 JS 端一样的信令系统。这套系统是由信令、信令状态机构成的,与信令服务器的互联仍由 socket.io 库实现。
双方都创建好 PeerConnecton 对象后,就会进行媒体协商、交换 Candidate 等,都完成后,数据在底层就开始传输了。