Android WebRTC開發入門


在學習 WebRTC 的過程中,學習的一個基本步驟是先通過 JS 學習 WebRTC的整體流程,在熟悉了整體流程之后,再學習其它端如何使用 WebRTC 進行互聯互通。
申請權限
  • Camera 權限
  • Record Audio 權限
  • Intenet 權限
在Android中,申請權限分為靜態權限申請和動態權限申請,這對於做 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 的發展,對安全性要求越來越高。除了申請靜態權限之外,還需要動態申請權限。代碼如下:
void requestPermissions (String[] permissions, intrequestCode);
實際上,對於權限這塊的處理真正做細了要寫不少代碼,好在 Android 官方給我們又提供了一個非常好用的庫  EasyPermissions  , 有了這個庫我們可以少寫不少代碼。使用  EasyPermissions  非常簡單,在MainActivity中添加代碼如下:
... 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); } } @Override public void onRequestPermissionsResult ( int requestCode, String[] permissions, int [] grantResults) { super .onRequestPermissionsResult(requestCode, permissions, grantResults); EasyPermissions.onRequestPermissionsResult(requestCode, permissions, grantResults, this ); } ...
通過添加以上代碼,就將權限申請好了,是不是非常簡單?
第二步,看在 Android 下如何引入 WebRTC 庫。
第一個是 WebRTC 庫,第二個是  socket.io  庫,用它來與信令服務器互聯。
首先我們看一下如何引入 WebRTC 庫(我這里使用的是最新 Android Studio 3.3.2)。在 Module 級別的 build.gradle 文件中增加以下代碼:
... dependencies { ... implementation 'org.webrtc:google-webrtc:1.0.+' ... }
接下來要引入  socket.io  庫,用它來與 Nodejs 搭建的信令服務器進行對接。再加上前面用到的EasyPermissions庫,所以真正的代碼應寫成下面的樣子:
... 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程序的起源就是 PeerConnectionFactory 。這也是與使用 JS 開發 WebRTC 程序最大的不同點之一,因為在 JS 中不需要使用 PeerConnectionFactory 來創建 PeerConnection 對象。
而在 Android/iOS 開發中,我們使用的 WebRTC 中的大部分對象基本上都是通過 PeerConnectionFactory 創建出來的。 下面這張圖就清楚的表達了 PeerConnectionFactory 在 WebRTC 中的地位。
PeerConnectionFactory.png 824×618 33.8 KB
通過該圖我們可以知道,WebRTC中的核心對象 PeerConnection、LocalMediaStream、LocalVideoTrack、LocalAudioTrack都是通過 WebRTC 創建出來的。
PeerConnectionFactory的初始化與構造
在 WebRTC 中使用了大量的設計模式,對於 PeerConnectionFactory 也是如此。它本身就是工廠模式,而這個構造 PeerConnection 等核心對象的工廠又是通過 builder 模式構建出來的。
下面我們就來看看如何構造 PeerConectionFactory。在我們構造 PeerConnectionFactory 之前,首先要對其進行初始化,其代碼如下:
PeerConnectionFactory.initialize(...);
初始化之后,就可以通過 builder 模式來構造 PeerConnecitonFactory 對象了。
... PeerConnectionFactory.Builder builder = PeerConnectionFactory.builder() .setVideoEncoderFactory(encoderFactory) .setVideoDecoderFactory(decoderFactory); ... return builder.createPeerConnectionFactory();
通過上面的代碼,大家也就能夠理解為什么 WebRTC 要使用 buider 模式來構造 PeerConnectionFactory 了吧?主要是方便調整建造 PeerConnectionFactory的組件,如編碼器、解碼器等。
從另外一個角度我們也可以了解到,要 更換WebRTC引警的編解碼器該從哪里設置了哈!
音視頻數據源
有了PeerConnectionFactory對象,我們就可以創建數據源了。實際上,數據源是 WebRTC 對音視頻數據的一種抽象,表示數據可以從這里獲取。
使用過 JS WebRTC API的同學都非常清楚, 在 JS中 VideoTrack 和 AudioTrack 就是數據源。 而在 Android 開發中我們可以知道 Video/AudioTrack 就是 Video/AudioSouce的封裝,可以認為他們是等同的。
創建數據源的方式如下:
... 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); ...
數據源只是對數據的一種抽象,它是從哪里獲取的數據呢?對於音頻來說,在創建 AudioSource時,就開始從音頻設備捕獲數據了。對於視頻來說我們可以指定采集視頻數據的設備,然后 使用觀察者模式從指定設備中獲取數據。
接下來我們就來看一下如何指定視頻設備。
視頻采集
在 Android 系統下有兩種 Camera,一種稱為 Camera1, 是一種比較老的采集視頻數據的方式,別一種稱為 Camera2, 是一種新的采集視頻的方法。它們之間的最大區別是 Camera1使用同步方式調用API,Camera2使用 異步方式 ,所以Camera2更高效。
我們看一下 WebRTC 是如何指定具體的 Camera 的:
private VideoCapturer createVideoCapturer () { if (Camera2Enumerator.isSupported( this )) { return createCameraCapturer( new Camera2Enumerator( this )); } else { return createCameraCapturer( new Camera1Enumerator( true )); } } private VideoCapturer createCameraCapturer (CameraEnumerator enumerator) { final String[] deviceNames = enumerator.getDeviceNames(); // First, try to find front facing camera Log.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 Log.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 ; }
上面代碼的邏輯也比較簡單:
  • 首先看 Android 設備是否支持 Camera2.
  • 如果支持就使用 Camera2, 如果不支持就使用 Camera1.
  • 在獲到到具體的設備后,再看其是否有前置攝像頭,如果有就使用
  • 如果沒有有效的前置攝像頭,則選一個非前置攝像頭。
通過上面的方法就可以拿到使用的攝像頭了,然后 將攝像頭與視頻源連接起來 ,這樣從攝像頭獲取的數據就源源不斷的送到 VideoTrack 里了。
下面我們來看看 VideoCapture 是如何與 VideoSource 關聯到一起的:
... mSurfaceTextureHelper = SurfaceTextureHelper.create( "CaptureThread" , mRootEglBase.getEglBaseContext()); mVideoCapturer.initialize(mSurfaceTextureHelper, getApplicationContext(), videoSource.getCapturerObserver()); ... mVideoTrack.setEnabled(true); ...
上面的代碼中,在初始化 VideoCaptuer 的時候,可以過 觀察者模式 將 VideoCapture 與 VideoSource 聯接到了一起。因為 VideoTrack 是 VideoSouce 的一層封裝,所以此時我們開啟 VideoTrack 后就可以拿到視頻數據了。
當然,最后還要調用一下 VideoCaptuer 對象的 startCapture 方法真正的打開攝像頭,這樣 Camera 才會真正的開始工作哈,代碼如下:
@Override protected void onResume () { super .onResume(); mVideoCapturer.startCapture(VIDEO_RESOLUTION_WIDTH, VIDEO_RESOLUTION_HEIGHT, VIDEO_FPS); }
拿到了視頻數據后,我們如何將它展示出來呢?
渲染視頻
Android 下 WebRTC 使用OpenGL ES 進行視頻渲染,用於展示視頻的控件是 WebRTC 對 Android 系統控件 SurfaceView 的封裝
WebRTC 封裝后的 SurfaceView 類為 org.webrtc.SurfaceViewRenderer。在界面定義中應該定義兩個SurfaceViewRenderer,一個用於顯示本地視頻,另一個用於顯示遠端視頻。
其定義如下:
... < org.webrtc.SurfaceViewRenderer android:id= "@+id/LocalSurfaceView" android:layout_width= "wrap_content" android:layout_height= "wrap_content" android:layout_gravity= "center" /> < org.webrtc.SurfaceViewRenderer android: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 /* enabled */ ); ...
其含義是:
  • 使用 OpenGL ES 的上下文初始化 View。
  • 設置圖像的拉伸比例。
  • 設置圖像顯示時反轉,不然視頻顯示的內容與實際內容正好相反。
  • 是否打開便件進行拉伸。
通過上面的設置,我們的 view 就設置好了,對於遠端的 Veiw 與本地 View 的設置是一樣的,
接下來將從攝像頭采集的數據設置到該view里就可以顯示了。設置非常的簡單,代碼如下:
... mVideoTrack.addSink(mLocalSurfaceView); ...
對於遠端來說與本地視頻的渲染顯示是類似的,只不過數據源是從網絡獲取的。
通過以上講解,大家應該對 WebRTC 如何采集數據、如何渲染數據有了基本的認識。下面我們再看來下 遠端的數據是如何來的。
創建 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 略有區別,它是 通過觀察者模式來監聽事件的 。大家這點一定要注意!
雙方都創建好 PeerConnecton 對象后,就會進行媒體協商,協商完成后,數據在底層就開始傳輸了。
信令驅動
雙方交互的過程中,其業務邏輯的核心是信令, 所有的模塊都是通過信令串聯起來的。
以 PeerConnection 對象的創建為例,該在什么時候創建 PeerConnection 對象呢?最好的時機當然是在用戶加入房間之后了 。
下面我們就來看一下,對於兩人通訊的情況,信令該如何設計。在我們這個例子中,可以將信令分成兩大類。第一類為客戶端命令;第二類為服務端命令;
客戶端命令有:
  • join: 用戶加入房間
  • leave: 用戶離開房間
  • message: 端到端命令(offer、answer、candidate)
服務端命令:
  • joined: 用戶已加入
  • leaved: 用戶已離開
  • other_joined:其它用戶已加入
  • bye: 其它用戶已離開
  • full: 房間已滿
通過以上幾條信令就可以實現一對一實時互動的要求
在本例子中我們仍然是通過socket.io與之前搭建的信令服備器互聯的。由於  socket.io  是跨平台的,所以無論是在 js 中,還是在 Android 中,我們都可以使用其客戶端與服務器相聯,非常的方便。
下面再來看一下,收到不同信令后,客戶端的狀態變化:
直播系統客戶端狀態機.png 715×558 27.9 KB
客戶端一開始的時候處於 Init/Leave 狀態。當發送 join 消息,並收到服務端的 joined 后,其狀態變為 joined。
此時,如果第二個用戶加入到房間,則客戶端的狀態變為了 joined_conn, 也就是說此時雙方可以進行實時互動了。
如果此時,該用戶離開,則其狀態就變成了 初始化狀態。其它 case 大家可以根據上面的圖自行理解。
IM和視頻聊天的,可以參考下這個 https://github.com/starrtc/starrtc-android-demo


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM