阅读 969

WebRTC-Android 探索 - 创建音视频通话程序的基本姿势

若要在 Android 上实现一个 WebRTC 通话应用,需要通过 采集 - 渲染本地预览画面 - 创建连接 - 信令交换相关信息 - 渲染远端画面 这五步的工作。WebRTC 中为开发者做了一系列的封装,减轻了开发者开发一个通话应用的压力。本篇文章将通过介绍这五步的实现简单介绍一下基本的使用姿势。

准备工作

我们先要添加 WebRTC 依赖,在这篇文章中我们直接引用 WebRTC 官方编好的包即可,即直接在 build.gradle 中添加:

dependencies {
    implementation 'org.webrtc:google-webrtc:1.0.+'
}复制代码

关于信令交换方式及信令服务器,不管是官方还是开源社区会有一大堆的开源项目,可以选择各种例如 WebSocket、XMPP 等方式进行信令通讯以交换相关信息创建连接。具体在此系列文章不进行叙述,可在文章末尾链接下载一整系列的代码(来自公司里我很敬佩的一位前辈)。


0、创建 PeerConnectionFactory

PeerConnectionFactory 是创建连接以及创建在连接中传输采集的音视频流数据的非常重要的一个类,且能在此处定义所使用的编解码器,本篇文章对编解码器不做深入描述,直接使用默认的 DefaultVideoEncoderFactory 和 DefaultVideoDecoderFactory。创建 PeerConnectionFactory 方式如下:

PeerConnectionFactory.initialize(PeerConnectionFactory.InitializationOptions.builder(context)
        .setEnableInternalTracer(true)
        .createInitializationOptions());
PeerConnectionFactory.Builder builder = PeerConnectionFactory.builder()
        .setVideoEncoderFactory(encoderFactory)
        .setVideoDecoderFactory(decoderFactory);
builder.setOptions(null);
mPeerConnectionFactory = builder.createPeerConnectionFactory();复制代码


1、采集

1.1、视频采集

一个视频通话需要进行视频采集和音频采集。做过 Android 相机应用开发的朋友都知道,Android 提供了一系列的上层相机操作接口,能够让我们很方便地去进行相机采集的操作,Android 在 API 21 时废弃了 Camera1 的接口推荐使用新的 Camera2,但是由于兼容问题几乎大多数开发者仍需要使用 Camera1,且要做使用 Camera1 还是 Camera2 的选择和适配。WebRTC 视频采集需要创建一个 VideoCapturer,WebRTC 提供了 CameraEnumerator 接口,分别有 Camera1Enumerator 和 Camera2Enumerator 两个实现,能够快速创建所需要的 VideoCapturer,通过 Camera2Enumerator.isSupported 判断是否支持 Camera2 来选择创建哪个 CameraEnumerator,选择好即可快速创建 VideoCapturer 了:

mVideoCapturer = cameraEnumerator.createCapturer(deviceName, null);复制代码

其中 deviceName 可通过 cameraEnumerator.getDeviceNames 获取,进而选择前置还是后置。然后我们创建一个 VideoSource 来拿到 VideoCapturer 采集的数据,并且创建在 WebRTC 的连接中能传输的 VideoTrack 数据:

VideoSource videoSource = mPeerConnectionFactory.createVideoSource(false);// 参数说明是否为屏幕录制采集

// 由于内部数据处理为 OpenGL 处理,则需要 EGL 环境相关的东西,本文不展开细讲
mSurfaceTextureHelper = SurfaceTextureHelper.create("CaptureThread", mRootEglBase.getEglBaseContext());
mVideoCapturer.initialize(mSurfaceTextureHelper, getApplicationContext(), videoSource.getCapturerObserver());

mVideoTrack = mPeerConnectionFactory.createVideoTrack(VIDEO_TRACK_ID, videoSource);
mVideoTrack.setEnabled(true); 
复制代码


1.2 音频采集

音频采集则没有视频采集那么麻烦,仅需要创建 AudioSource 则可直接得到音频采集数据。同样最后创建一个 AudioTrack 即可在 WebRTC 的连接中传输。

AudioSource audioSource = mPeerConnectionFactory.createAudioSource(new MediaConstraints());
mAudioTrack = mPeerConnectionFactory.createAudioTrack(AUDIO_TRACK_ID, audioSource);
mAudioTrack.setEnabled(true);复制代码


2、渲染本地视频

无论是本地还是远端的视频渲染,都是通过 WebRTC 提供的 SurfaceViewRenderer (继承于 SurfaceView) 进行渲染的。

视频的数据需要 VideoTrack 绑定一个 VideoSink 的实现然后将数据渲染到 SurfaceViewRenderer 中,具体实现如下:

mVideoTrack.addSink(new VideoSink() {
    @Override
    public void onFrame(VideoFrame videoFrame) {
        mLocalSurfaceView.onFrame(videoFrame);
    }
});复制代码


3、创建连接

创建连接即为创建 PeerConnection,PeerConnection 是 WebRTC 非常重要的一个东西,是多人音视频通话连接的关键。我们在最开始创建了 PeerConnectionFactory,通过此工厂类即可非常简单地创建一个 PeerConnection。

PeerConnection.RTCConfiguration configuration = new PeerConnection.RTCConfiguration(new ArrayList<>());// 参数为 iceServer 列表
PeerConnection connection = mPeerConnectionFactory.createPeerConnection(configuration, mPeerConnectionObserver);复制代码

其中 PeerConnectionObserver 是用来监听这个连接中的事件的监听者,可以用来监听一些如数据的到达、流的增加或删除等事件,其接口如下:

/** Java 版本的 PeerConnectionObserver. */
public static interface Observer {
  /** 在 SignalingState 更改时触发。 */
  @CalledByNative("Observer") void onSignalingChange(SignalingState newState);

  /** 在 IceConnectionState 更改时触发。 */
  @CalledByNative("Observer") void onIceConnectionChange(IceConnectionState newState);

  /** 当 ICE 连接接收状态改变时触发。 */
  @CalledByNative("Observer") void onIceConnectionReceivingChange(boolean receiving);

  /** 当 IceGatheringState 改变时触发。 */
  @CalledByNative("Observer") void onIceGatheringChange(IceGatheringState newState);

  /** 当一个新的 IceCandidate 被发现时触发。 */
  @CalledByNative("Observer") void onIceCandidate(IceCandidate candidate);

  /** 当一些 IceCandidate被移除时触发。 */
  @CalledByNative("Observer") void onIceCandidatesRemoved(IceCandidate[] candidates);

  /** 当从远程的流发布时触发。 */
  @CalledByNative("Observer") void onAddStream(MediaStream stream);

  /** 当远程的流移除时触发。 */
  @CalledByNative("Observer") void onRemoveStream(MediaStream stream);

  /** 当远程打开 DataChannel 时触发。 */
  @CalledByNative("Observer") void onDataChannel(DataChannel dataChannel);

  /** 当需要重新协商时触发。 */
  @CalledByNative("Observer") void onRenegotiationNeeded();

  /**
   * 当远程端发出新的 Track 时触发, 这是 setRemoteDescription 回调的结果
   */
  @CalledByNative("Observer") void onAddTrack(RtpReceiver receiver, MediaStream[] mediaStreams);
}复制代码

具体哪些回调会在什么时候回调、需要做什么将在下文详细介绍。

通过 PeerConnectionFactory 创建好 PeerConnection 之后即可将之前创建的两个 Track 加入连接中了:

connection.addTrack(mVideoTrack);
connection.addTrack(mAudioTrack);复制代码


4、交换相关信息

当需要通话时,那么就需要交换相关信息了,信令服务器的作用就体现出来了。我们先看一张图:



意思就是说,两端通过信令交换一些相关信息,对于自己来说,先要创建一个 Offer,并且通过 setLocalDescription 设置为本地的信息,然后远端会将你的 Offer 通过 setRemoteDescription 设置到他那边,然后他那边创建一个 Answer 发送到你这边,你也要通过 setRemoteDescription 将他的 Answer 设置到自己这里,然后就会开始走 PeerConnectionObserver 内的事件了。

上面第三条线就是在我们上面讲的 PeerConnectionObserver 中的 onIceCandidate 里进行处理的。我们在 setLocalDescription 和 setRemoteDescription 后即会触发 onIceCandidate 回调生成一个 IceCandidate,IceCandidate 就是一个包装类,里边放了一些相关信息比如 sdp,通过信令服务器将这个 IceCandidate 发送到远端,远端通过 PeerConnection 的 addIceCandidate 方法将这个 IceCandidate 加到连接中,连接打通后会将远端的流通过 onAddTrack 回调传到本端进行处理。

4.1 创建 Offer

创建 Offer 时需要传入一些配置参数,可通过 MediaConstraints 传入。代码如下:

MediaConstraints mediaConstraints = new MediaConstraints();
mediaConstraints.mandatory.add(new MediaConstraints.KeyValuePair("OfferToReceiveAudio", "true")); // 允许音频
mediaConstraints.mandatory.add(new MediaConstraints.KeyValuePair("OfferToReceiveVideo", "true")); // 允许视频
mediaConstraints.optional.add(new MediaConstraints.KeyValuePair("DtlsSrtpKeyAgreement", "true")); // 加密
mPeerConnection.createOffer(new SdpObserver() {
    @Override
    public void onCreateSuccess(SessionDescription sessionDescription) {
        mPeerConnection.setLocalDescription(new SdpObserver(){...}, sessionDescription);// 设为 LocalDescription
        JSONObject message = new JSONObject();
        try {
            message.put("userId", RTCSignalClient.getInstance().getUserId());
            message.put("msgType", RTCSignalClient.MESSAGE_TYPE_OFFER);
            message.put("sdp", sessionDescription.description);
            sendMessage(message);// 信令发送
        } catch (JSONException e) {
            e.printStackTrace();
        }
    }
    // ...
}, mediaConstraints);复制代码

其中 SdpObserver 含有以下回调:

public interface SdpObserver {
  /** 创建 sdp 成功 */
  @CalledByNative void onCreateSuccess(SessionDescription sdp);

  /** 设置 {Local,Remote}Description 成功 */
  @CalledByNative void onSetSuccess();

  /** 创建 {Offer,Answer} 成功 */
  @CalledByNative void onCreateFailure(String error);

  /** 设置 {Local,Remote}Description 成功 */
  @CalledByNative void onSetFailure(String error);
}复制代码

我们在创建 sdp 成功时将其设为 LocalDescription 并通过信令发给远端即可。远端在接收到 sdp 时通过以下代码设置 RemoteDescription:

mPeerConnection.setRemoteDescription(new SdpObserver(){...}, new SessionDescription(SessionDescription.Type.OFFER, description));复制代码


4.2 创建 Answer

远端设置 RemoteDescription 完毕后,就要创建 Answer:

mPeerConnection.createAnswer(new SdpObserver() {
    @Override
    public void onCreateSuccess(SessionDescription sessionDescription) {
        mPeerConnection.setLocalDescription(new SimpleSdpObserver(), sessionDescription);// 设为 LocalDescription
        JSONObject message = new JSONObject();
        try {
            message.put("userId", RTCSignalClient.getInstance().getUserId());
            message.put("msgType", RTCSignalClient.MESSAGE_TYPE_ANSWER);
            message.put("sdp", sessionDescription.description);
            sendMessage(message);
        } catch (JSONException e) {
            e.printStackTrace();
        }
    }
    //...
},  new MediaConstraints());复制代码

Answer 也是作为远端的 LocalDescription,然后通过信令发送给本地,本地将其设为 RemoteDescription。


4.3 发送 IceCandidate

双方设置 {Local,Remote}Description 成功后,则开始了 IceCandidate 的发送和接收,此步需要把双方在 PeerConnectionObserver 的 onIceCandidate 回调中回调的 IceCandidate 通过信令发送到远端:

@Override
public void onIceCandidate(IceCandidate iceCandidate) {
    try {
        JSONObject message = new JSONObject();
        message.put("userId", RTCSignalClient.getInstance().getUserId());
        message.put("msgType", RTCSignalClient.MESSAGE_TYPE_CANDIDATE);
        message.put("label", iceCandidate.sdpMLineIndex);
        message.put("id", iceCandidate.sdpMid);
        message.put("candidate", iceCandidate.sdp);
        sendMessage(message);
    } catch (JSONException e) {
        e.printStackTrace();
    }
}复制代码

接收到双方给对方发送的 IceCandidate 之后通过以下方法添加到连接中:

IceCandidate remoteIceCandidate = new IceCandidate(message.getString("id"), message.getInt("label"), message.getString("candidate"));
mPeerConnection.addIceCandidate(remoteIceCandidate);复制代码

此时即可在 onAddTrack 中进行远端流渲染的处理了。


5、渲染远端画面

前文提到远端画面的信息会在 PeerConnectionObserver 中的 onAddTrack 回调中回调出来,此时我们在这个回调中像渲染本地画面一样渲染远端画面即可:

@Override
public void onAddTrack(RtpReceiver rtpReceiver, MediaStream[] mediaStreams) {
    MediaStreamTrack track = rtpReceiver.track();
    if (track instanceof VideoTrack) {
        Log.i(TAG, "onAddVideoTrack");
        VideoTrack remoteVideoTrack = (VideoTrack) track;
        remoteVideoTrack.setEnabled(true);
        remoteVideoTrack.addSink(new VideoSink() {
            @Override
            public void onFrame(VideoFrame videoFrame) {
                mRemoteSurfaceView.onFrame(videoFrame);
            }
        });
    }
}复制代码


至此,整个通话的流程就跑通了。当然不要忘了在 Activity 销毁时对 PeerConnection、VideoCapturer、SurfaceTextureHelper、PeerConnectionFactory、SurfaceViewRenderer 进行关闭/释放。


总结

WebRTC-Android 的使用相对来说还是比较简单的,在后续文章中我会继续深入分析每一个步骤内的细节及其实现。对于本文中的 Demo 可以直接通过看 WebRTC 官方的 AppRTCDemo,当然更加推荐大家看 Jhuster的开源项目(来自 卢俊 俊哥哥,公司里我很敬佩的前辈),包括了很齐全的服务端、Web 端及 Android 端的实现。


关注下面的标签,发现更多相似文章
评论