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 SimpleSdpObserver() { @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 端的實現。