[Android] WebRTC 在 Android 中的使用

由于实习接触到了 WebRTC,并且慢慢开始了音视频领域的学习,这半个月来一直在进行 Android 与 WebRTC 的一些学习,在此我通过应用层上进行一些总结。由于各个版本的 API 有略微不同,所以在此只是通过一些关键 API 串起来对 WebRTC 在 Android 上的使用进行介绍,具体某版本的 API 可见其提供对应的 DEMO 进行学习。

一、WebRTC简介

WebRTC 是一个免费的、开源的程序,它能够用一些简单的 API 为浏览器以及 Android/iOS 设备提供实时音视频交流(Real Time Communications)的功能。由于能够通过 native 和浏览器跨平台实时进行音视频交流, WebRTC 已经被非常多人使用。

二、准备工作

我们首先要有一个信令服务器,推荐使用 Docker:

docker pull piasy/apprtc-server

具体使用: https://blog.piasy.com/2017/06/17/out-of-the-box-webrtc-dev-env/ 

感谢前辈的贡献,前人种树后人乘凉 🙂

三、获取相关库

我们可以自己编译 WebRTC for Android 的代码,我们可以通过 Depot Tools 获取到最新的源码来编译:

fetch --nohooks webrtc_android
gclient sync

然后通过 GN 和 ninja 编译。(具体参考:https://webrtc.org/native-code/android/

当然最简单的方式是通过Gradle(目前通过此方式添加的版本为1.0.24139)获取:

implementation 'org.webrtc:google-webrtc:1.0.+'

本文通过解析 WebRTC 官方提供的 DEMO 代码去对 WebRTC 在 Android 中的使用作介绍,完整代码可以通过 https://webrtc.googlesource.com/src/+/master/examples/androidapp 查看。

四、Android 中 WebRTC API 的使用

首先使用 WebRTC 时会用到一些权限,在 Android 6.0 之后要对部分如 Camera 等“危险权限”进行申请(此部分自行搜索),AndroidManifest文件如下:

  <uses-feature android:name="android.hardware.camera" />
  <uses-feature android:name="android.hardware.camera.autofocus" />
  <uses-feature android:glEsVersion="0x00020000" android:required="true" />

  <uses-permission android:name="android.permission.CAMERA" />
  <uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
  <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
  <uses-permission android:name="android.permission.RECORD_AUDIO" />
  <uses-permission android:name="android.permission.BLUETOOTH" />
  <uses-permission android:name="android.permission.INTERNET" />
  <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
  <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
  <uses-permission android:name="android.permission.CAPTURE_VIDEO_OUTPUT" />

核心类 PeerConncetionFactory

PeerConncetionFactory 是WebRTC的核心工具类,通过 PeerConncetionFactory 进行初始化后可用其创建 PeerConnectionFactory,其初始化方法如下:

PeerConnectionFactory.initialize(
          PeerConnectionFactory.InitializationOptions.builder(appContext)
              .setFieldTrials(fieldTrials)
              .setEnableInternalTracer(true)
              .createInitializationOptions());

其中 fieldTrials 是对 WebRTC 的一些配置项,比如配置是否允许 FlexFEC 和 WebRTC 的 AGC 算法这些东西。然后就可以通过以下代码创建 PeerConnectionFactory:

factory = PeerConnectionFactory.builder()
                  .setOptions(options)
                  .setAudioDeviceModule(adm)
                  .setVideoEncoderFactory(encoderFactory)
                  .setVideoDecoderFactory(decoderFactory)
                  .createPeerConnectionFactory();

VideoCapturer

VideoCapturer 封装了 Android 的 Camera 的使用方法,我们可以通过如下方式来获取到相机来使用相机:

private @Nullable VideoCapturer createCameraCapturer(CameraEnumerator enumerator) {
    final String[] deviceNames = enumerator.getDeviceNames();

    // First, try to find front facing camera
    Logging.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 else
    Logging.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;
  }

获取到 VideoCapturer 之后可以通过以下代码开始获取摄像头图像啦:

videoCapturer.startCapture(videoWidth, videoHeight, videoFps);

获取视频 VideoSource/VideoTrack

VideoSource 即是视频源,其通过 PeerConnectionFactory 创建,然后 VideoTrack 可以对其进行包装,包装后即可将其给 MediaStream 了。代码如下:

  private VideoTrack createVideoTrack(VideoCapturer capturer) {
    surfaceTextureHelper =
        SurfaceTextureHelper.create("CaptureThread", rootEglBase.getEglBaseContext());
    videoSource = factory.createVideoSource(capturer.isScreencast());
    capturer.initialize(surfaceTextureHelper, appContext, videoSource.getCapturerObserver());
    capturer.startCapture(videoWidth, videoHeight, videoFps);

    localVideoTrack = factory.createVideoTrack(VIDEO_TRACK_ID, videoSource);
    localVideoTrack.addSink(localRender);
    return localVideoTrack;
  }

获取音频 AudioSource/AudioTrack

AudioSource/AudioTrack 与 VideoSource/VideoTrack 相似,即为 AudioSource 生成音频源后给 AudioTrack 包装,包装后传递给 MediaStream。代码如下:

private AudioTrack createAudioTrack() {
    audioSource = factory.createAudioSource(audioConstraints);
    localAudioTrack = factory.createAudioTrack(AUDIO_TRACK_ID, audioSource);
    return localAudioTrack;
  }

在音频中可通过 MediaConstraints 去做一些约束限制,createAudioSource 的参数即为 MediaConstraints,我们可以通过以下对音频进行处理:

audioConstraints.mandatory.add(
          new MediaConstraints.KeyValuePair(AUDIO_ECHO_CANCELLATION_CONSTRAINT, "false"));
      audioConstraints.mandatory.add(
          new MediaConstraints.KeyValuePair(AUDIO_AUTO_GAIN_CONTROL_CONSTRAINT, "false"));
      audioConstraints.mandatory.add(
          new MediaConstraints.KeyValuePair(AUDIO_HIGH_PASS_FILTER_CONSTRAINT, "false"));
      audioConstraints.mandatory.add(
          new MediaConstraints.KeyValuePair(AUDIO_NOISE_SUPPRESSION_CONSTRAINT, "false"));

从上到下代表着:回声消除、自动增益、高音处理、噪音处理。

VideoSink

VideoSink 用来渲染播放 VideoTrack 的内容。在指定的 VideoTrack 用 addSink() 方法即可添加对应的 VideoSink。

MediaStream

MediaStream 通过 PeerConncetionFactory 创建,在官方的 DEMO 中直接通过 PeerConnection 的 addTrack() 方法添加的媒体流到 PeerConnection 中,但我们还需要了解一下 MediaStream 的用法:

MediaStream mediaStream = factory.createLocalMediaStream("localstream");
mediaStream.addTrack(videoTrack);
mediaStream.addTrack(audioTrack);

MediaStream 可以通过 PeerConnection 在网络中传递,所以这是一个很重要的部分。

PeerConnection

PeerConnection 用于信令交换和数据传输,通过 PeerConnectionFactory 的 createPeerConnection 进行创建,在 WebRTC 官方的 DEMO 中采用 createPeerConnection(RTCConfiguration rtcConfig, Observer observer) 这个方法进行创建, rtcConfig 代表的是 ICEServer 相关的配置, observer 是对连接状态的监听。

而大多数时候我们会用 createPeerConnection(List<IceServer> iceServers, MediaConstraints constraints, Observer observer) 方法去创建,其中 constraints 应该包含 offerToRecieveAudio 和 offerToRecieveVideo。

// Create SDP constraints.
    sdpMediaConstraints = new MediaConstraints();
    sdpMediaConstraints.mandatory.add(
        new MediaConstraints.KeyValuePair("OfferToReceiveAudio", "true"));
    sdpMediaConstraints.mandatory.add(new MediaConstraints.KeyValuePair(
        "OfferToReceiveVideo", Boolean.toString(isVideoCallEnabled())));

最后我们通过 PeerConnection 的 addStream() 方法将 MediaStream 加进去即可进行对 MediaStream 的传递了。

 

五、信令交换

首先我们来看一下 SdpObserver 有哪些回调:

package org.webrtc;

public interface SdpObserver {
    @CalledByNative
    void onCreateSuccess(SessionDescription var1);

    @CalledByNative
    void onSetSuccess();

    @CalledByNative
    void onCreateFailure(String var1);

    @CalledByNative
    void onSetFailure(String var1);
}

很明显,无非就是创建和设置 SDP 信息的成功与失败。我们A端通过 peerConnection.createOffer(sdpObserver, sdpMediaConstraints); 创建一个 offer 信令,创建成功后即调用上述的 onCreateSuccess() 回调,在此回调里通过 peerConnection.setLocalDescription(sdpObserver, sdp); 为自己设置 offer 信令,同时通过信令服务器发送到B端。

B端收到信令后,创建一个 SDP 解析A端发过来的 offer,并在 onCreateSuccess() 回调中通过 peerConnection.setLocalDescription(sdpObserver, sdp); 为自己设置 answer 信令,并通过信令服务器将 answer 发送到A端。

A端收到信令后,通过 PeerConnection 的 setRemoteDescription() 方法设置B端的 answer 之后信令交换就完成了。

补充: ICE 框架穿透防火墙

如果在非局域网环境下需要内网穿透。需要上面 PeerConnection 提到的 iceServers,它就是用来穿透防火墙和 NAT 的服务器地址,在 PeerConnection 实例化后我们可以通过 PeerConnection.Observer 的 onIceCandidate() 回调将 IceCandidate 对象发给对方。对方收到这个对象后通过 PeerConnection 的 addIceCandidate() 设置。

参考资料:https://blog.csdn.net/rd_w_csdn/article/details/56280641

六、播放

远端流推过来之后会通过 PeerConnection.Observer 的 onAddStream() 回调,里边包含了远端的 MediaStream,通过拿到 MediaStream 的 Track 并通过上面说的 addSink() 对其增加 VideoSink。

示例代码:

@Override
    public void onAddStream(final MediaStream stream) {
      remoteVideoTrack = mediaStream.videoTracks.get(0);
      remoteVideoTrack.addSink(remoteSinks.get(0));
    }

总结:

WebRTC 能够以更少的延迟去实现一些视频会议的功能,同时又有对 Camera 的高度封装,用起来更加便捷,同时其跨平台的特性更是让更多团队投身于 WebRTC 的开发中。我只是由于工作刚开始接触 WebRTC 不到一个月,很多更深层次的问题还等着我去探索,文中可能有不妥之处希望各位指正,我的邮箱:me@york1996.com。

发表评论

电子邮件地址不会被公开。 必填项已用*标注