WebRTC-Android 探索 - 建立音視頻通話程序的基本姿式

若要在 Android 上實現一個 WebRTC 通話應用,須要經過 採集 - 渲染本地預覽畫面 - 建立鏈接 - 信令交換相關信息 - 渲染遠端畫面 這五步的工做。WebRTC 中爲開發者作了一系列的封裝,減輕了開發者開發一個通話應用的壓力。本篇文章將經過介紹這五步的實現簡單介紹一下基本的使用姿式。

準備工做

咱們先要添加 WebRTC 依賴,在這篇文章中咱們直接引用 WebRTC 官方編好的包便可,即直接在 build.gradle 中添加:java

dependencies {
    implementation 'org.webrtc:google-webrtc:1.0.+'
}複製代碼

關於信令交換方式及信令服務器,不論是官方仍是開源社區會有一大堆的開源項目,能夠選擇各類例如 WebSocket、XMPP 等方式進行信令通信以交換相關信息建立鏈接。具體在此係列文章不進行敘述,可在文章末尾連接下載一整系列的代碼(來自公司裏我很敬佩的一位前輩)。git


0、建立 PeerConnectionFactory

PeerConnectionFactory 是建立鏈接以及建立在鏈接中傳輸採集的音視頻流數據的很是重要的一個類,且能在此處定義所使用的編解碼器,本篇文章對編解碼器不作深刻描述,直接使用默認的 DefaultVideoEncoderFactory 和 DefaultVideoDecoderFactory。建立 PeerConnectionFactory 方式以下:github

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.一、視頻採集

一個視頻通話須要進行視頻採集和音頻採集。作過 Android 相機應用開發的朋友都知道,Android 提供了一系列的上層相機操做接口,可以讓咱們很方便地去進行相機採集的操做,Android 在 API 21 時廢棄了 Camera1 的接口推薦使用新的 Camera2,可是因爲兼容問題幾乎大多數開發者仍須要使用 Camera1,且要作使用 Camera1 仍是 Camera2 的選擇和適配。WebRTC 視頻採集須要建立一個 VideoCapturer,WebRTC 提供了 CameraEnumerator 接口,分別有 Camera1Enumerator 和 Camera2Enumerator 兩個實現,可以快速建立所須要的 VideoCapturer,經過 Camera2Enumerator.isSupported 判斷是否支持 Camera2 來選擇建立哪一個 CameraEnumerator,選擇好便可快速建立 VideoCapturer 了:web

mVideoCapturer = cameraEnumerator.createCapturer(deviceName, null);複製代碼

其中 deviceName 可經過 cameraEnumerator.getDeviceNames 獲取,進而選擇前置仍是後置。而後咱們建立一個 VideoSource 來拿到 VideoCapturer 採集的數據,而且建立在 WebRTC 的鏈接中能傳輸的 VideoTrack 數據:bash

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);複製代碼


二、渲染本地視頻

不管是本地仍是遠端的視頻渲染,都是經過 WebRTC 提供的 SurfaceViewRenderer (繼承於 SurfaceView) 進行渲染的。session

視頻的數據須要 VideoTrack 綁定一個 VideoSink 的實現而後將數據渲染到 SurfaceViewRenderer 中,具體實現以下:ide

mVideoTrack.addSink(new VideoSink() {
    @Override
    public void onFrame(VideoFrame videoFrame) {
        mLocalSurfaceView.onFrame(videoFrame);
    }
});複製代碼


三、建立鏈接

建立鏈接即爲建立 PeerConnection,PeerConnection 是 WebRTC 很是重要的一個東西,是多人音視頻通話鏈接的關鍵。咱們在最開始建立了 PeerConnectionFactory,經過此工廠類便可很是簡單地建立一個 PeerConnection。gradle

PeerConnection.RTCConfiguration configuration = new PeerConnection.RTCConfiguration(new ArrayList<>());// 參數爲 iceServer 列表
PeerConnection connection = mPeerConnectionFactory.createPeerConnection(configuration, mPeerConnectionObserver);複製代碼

其中 PeerConnectionObserver 是用來監聽這個鏈接中的事件的監聽者,能夠用來監聽一些如數據的到達、流的增長或刪除等事件,其接口以下:ui

/** 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);複製代碼


四、交換相關信息

當須要通話時,那麼就須要交換相關信息了,信令服務器的做用就體現出來了。咱們先看一張圖:



意思就是說,兩端經過信令交換一些相關信息,對於本身來講,先要建立一個 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 中進行遠端流渲染的處理了。


五、渲染遠端畫面

前文提到遠端畫面的信息會在 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 端的實現。

相關文章
相關標籤/搜索