WebRTC 的 Android 2 Android 實現

公司讓做一個小項目作爲入職測試,簡單的說就是實現WebRTC的android端互連。boss給提供了一個開源項目作爲參考,這個項目是WebRTC的android客戶端,可以實現android端連接PC端——在我動手之前我以爲自己要建一棟房子,然而最後只是掛了個窗簾,不過至少知道了房子的構造O(∩_∩)O~

環境準備

參考項目1:https://github.com/pchab/ProjectRTC

ProjectRTC是一個WebRTC的PC端項目,實現了WebRTC的服務器功能和PC客戶端功能,我們拿來當服務器用的,運行需要安裝Node.JS。使用方法如下:

  • 安裝Node.JS
  • ProjectRTC根目錄下,命令行:npm install
  • 命令行:node app.js (雖然ProjectRTC說明中讓輸入:npm start,然是打開之後就關不掉了,會自啓動的,而且不帶任何控制檯提示)

參考項目2:https://github.com/pchab/AndroidRTC

AndroidRTC是ProjectRTC的android客戶端,下載後直接AndroidStudio打開。AndroidRTC中包含兩個moudle,app是主界面,webrtc-client是工具類moudle 。

添加WebRTC庫

需要去官網下載源碼,然後到Linux下編譯,最後放到Android項目中並添加依賴和權限(很麻煩的樣子,所幸參考項目2已經幫我們配置好了,偷下懶^_^)

背景知識

WebRTC被譽爲是web長期開源開發的一個新啓元,是近年來web開發的最重要創新。WebRTC允許Web開發者在其web應用中添加視頻聊天或者點對點數據傳輸,不需要複雜的代碼或者昂貴的配置。目前支持Chrome、Firefox和Opera,後續會支持更多的瀏覽器,它有能力達到數十億的設備。

然而,WebRTC一直被誤解爲僅適合於瀏覽器。事實上,WebRTC最重要的一個特徵是允許本地和web應用間的互操作,自然也可以在Android應用中植入WebRTC 。

WebRTC Android端的大體實現過程如下:(在不考慮播放本地視頻的情況下)

  • 連接服務器,並通過服務器打通兩個客戶端的網絡通道。
  • 從攝像頭和麥克風獲取媒體流 。
  • 將本地媒體流通過網絡通道傳送給對方的客戶端 。
  • 渲染播放接收到的媒體流 。

關鍵技術

核心類PeerConnectionFactory

首先需要初始化PeerConnectionFactory,這是WebRTC的核心工具類,初始化方法如下:

PeerConnectionFactory.initializeAndroidGlobals(
    context,//上下文,可自定義監聽
    initializeAudio,//是否初始化音頻,布爾值
    initializeVideo,//是否初始化視頻,布爾值
    videoCodecHwAcceleration,//是否支持硬件加速,布爾值
    renderEGLContext);//是否支持硬件渲染,布爾值

然後就可以獲得對象:PeerConnectionFactory factory= new PeerConnectionFactory();

獲取媒體流

第一步:獲取視頻源videoSource

String frontCameraName = VideoCapturerAndroid.getNameOfFrontFacingDevice();
VideoCapturer videoCapturer = VideoCapturerAndroid.create(frontCameraName);
VideoSource videoSource = factory.createVideoSource(videoCapturer,videoConstraints);

其中videoConstraints是對視頻流的一些限制,按如下方法創建。

MediaConstraints videoConstraints = new MediaConstraints();
videoConstraints.mandatory.add(new MediaConstraints.KeyValuePair("maxHeight", Integer.toString(pcParams.videoHeight)));
videoConstraints.mandatory.add(new MediaConstraints.KeyValuePair("maxWidth", Integer.toString(pcParams.videoWidth)));
videoConstraints.mandatory.add(new MediaConstraints.KeyValuePair("maxFrameRate", Integer.toString(pcParams.videoFps)));
videoConstraints.mandatory.add(new MediaConstraints.KeyValuePair("minFrameRate", Integer.toString(pcParams.videoFps)));

第二步:獲取音頻源audioSource

音頻源的獲取簡單許多:

AudioSource audioSource = factory.createAudioSource(new MediaConstraints());

第三步:獲得封裝VideoTrack/AudioTrack

VideoTrack/AudioTrack 是 VideoSource/AudioSource 的封裝,方便他們的播放和傳輸:

VideoTrack videoTrack = factory.createVideoTrack("ARDAMSv0", videoSource);
AudioTrack audioTrack = factory.createAudioTrack("ARDAMSa0", audioSource);

第四步:獲取媒體流localMS

其實 VideoTrack/AudioTrack 已經可以播放了,不過我們先不考慮本地播放。那麼如果要把他們發送到對方客戶端,我們需要把他們添加到媒體流中:

MediaStream localMS=factory.createLocalMediaStream("ARDAMS");
localMS.addTrack(videoTrack);
localMS.addTrack(audeoTrack);

然後,如果有建立好的連接通道,我們就可以把 localMS 發送出去了。

建立連接通道

WebRTC是基於P2P的,但是在連接通道建立好之前,我們仍然需要服務器幫助傳遞信令,而且需要服務器幫助進行網絡穿透。大體需要如下幾個步驟。

第一步:創建PeerConnection的對象。

PeerConnection pc = factory.createPeerConnection(
    iceServers,//ICE服務器列表
    pcConstraints,//MediaConstraints
    context);//上下文,可做監聽

iceServers 我們下面再說。
pcConstraints是媒體限制,可以添加如下約束:

pcConstraints.mandatory.add(new MediaConstraints.KeyValuePair("OfferToReceiveAudio", "true"));
pcConstraints.mandatory.add(new MediaConstraints.KeyValuePair("OfferToReceiveVideo", "true"));
pcConstraints.optional.add(new MediaConstraints.KeyValuePair("DtlsSrtpKeyAgreement", "true"));

監聽器建議同時實現SdpObserver、PeerConnection.Observer兩個接口。

第二步:信令交換

建立連接通道時我們需要在WebRTC兩個客戶端之間進行一些信令交換,我們以A作爲發起端,B作爲響應端(A call B,假設服務器和A、B已經連接好,並且只提供轉發功能,PeerConnection對象爲pc ):

  • A向B發出一個“init”請求(我覺得這步沒有也行)。
  • B收到後“init”請求後,調用pc.createOffer()方法創建一個包含SDP描述符(包含媒體信息,如分辨率、編解碼能力等)的offer信令。
  • offer信令創建成功後會調用SdpObserver監聽中的onCreateSuccess()響應函數,在這裏B會通過pc.setLocalDescription將offer信令(SDP描述符)賦給自己的PC對象,同時將offer信令發送給A 。
  • A收到B的offer信令後,利用pc.setRemoteDescription()方法將B的SDP描述賦給A的PC對象。
  • A在onCreateSuccess()監聽響應函數中調用pc.setLocalDescription將answer信令(SDP描述符)賦給自己的PC對象,同時將answer信令發送給B 。
  • B收到A的answer信令後,利用pc.setRemoteDescription()方法將A的SDP描述賦給B的PC對象。

這樣,A、B之間就完成裏了信令交換。

第三步:通過ICE框架穿透NAT/防火牆

如果在局域網內,信令交換後就已經可以傳遞媒體流了,但如果雙方不在同一個局域網,就需要進行NAT/防火牆穿透(我是在局域網下測試的,沒有穿透,但還是把這方面內容介紹下)。

WebRTC使用ICE框架來保證穿透。ICE全名叫交互式連接建立(Interactive Connectivity Establishment),一種綜合性的NAT/FW穿越技術,它是一種框架,可以整合各種NAT/FW穿越技術如STUN、TURN(Traversal Using Relay NAT 中繼NAT實現的穿透)。ICE會先使用STUN,嘗試建立一個基於UDP的連接,如果失敗了,就會去TCP(先嚐試HTTP,然後嘗試HTTPS),如果依舊失敗ICE就會使用一箇中繼的TURN服務器。使用STUN服務器穿透的結構如下:
這裏寫圖片描述
我們可以使用Google的stun服務器:stun:stun.l.google.com:19302(Google嘛,翻牆你懂得,當然如果有精力可以自己搭建一個stun服務器),那麼我們怎麼把這個地址告訴WebRTC呢,還記得之前的iceServers嗎,就是在創建PeerConnection對象的時候需要的參數,iceServers裏面存放的就是進行穿透地址變換的服務器地址,添加方法如下(保險起見可以多添加幾個服務器地址,如果有的話):

iceServers.add(new PeerConnection.IceServer("stun:stun.l.google.com:19302"));

然後這個stun服務器地址也需要通過信令交換,同樣以A、B客戶端爲例過程如下:

  • A、B分別創建PC實例pc(配置了穿透服務器地址) 。
  • 當網絡候選可用時,PeerConnection.Observer監聽會調用onIceCandidate()響應函數並提供IceCandidate(裏面包含穿透所需的信息)的對象。在這裏,我們可以讓A、B將IceCandidate對象的內容發送給對方。
  • A、B收到對方發來的candidate信令後,利用pc.addIceCandidate()方法將穿透信息賦給各自的PeerConnection對象。

至此,連接通道完全打通,然後我們只需要將之前獲取的媒體流localMS賦給pc即可:

pc.addStream(localMS);//也可以先添加,連接通道打通後一樣會觸發監聽響應。

在連接通道正常的情況下,對方的PeerConnection.Observer監聽就會調用onAddStream()響應函數並提供接收到的媒體流。

播放媒體流

WebRTC提供了一種很方便的播放方式:VideoRendererGui,首先設置VideoRendererGui,具體方法如下:

GLSurfaceView videoView = (GLSurfaceView) findViewById(R.id.glview_call);
VideoRendererGui.setView(videoView, runnable);//surface準備好後會調用runnable裏的run()函數

然後創建一個VideoRenderer對象,並將其賦給videoTrack:

VideoRenderer renderer = VideoRendererGui.createGui(x, y, width, height);//設置界面
videoTrack.addRenderer(renderer);

WebRTC允許我們實現自己的渲染,我們只需通過VideoRendererGui獲取VideoRenderer.Callbacks的對象,渲染後把其作爲參數傳入到VideoRenderer的構造方法即可。

此外利用VideoRenderer.Callbacks,我們可以動態調整播放界面,如下:

VideoRenderer.Callbacks cbRenderer = VideoRendererGui.create(x, y, width, height, scalingType, mirror);//設置界面
videoTrack.addRenderer(new VideoRenderer(cbRenderer ));
VideoRendererGui.update(cbRenderer ,x, y, width, height, scalingType);//調整界面

信令服務器

信令服務器主要是在客戶端打通連接通道前傳遞信令的,在客戶端開啓P2P通道後,這個服務器關了也不會影響媒體流傳輸。

我是用ProjectRTC作爲服務器,這個項目裏還包括PC客戶端的實現,不過我們不用管它們,ProjectRTC項目根目錄下的app.js是入口文件,裏面設置必要參數,如網口等。我們需要關注的文件是app文件夾下的:socketHandler.js 和 streams.js 文件。

socketHandler.js 是服務器用來和客戶端交互的接口,裏面的實現網口的監聽,每有新的連接接入,都在這裏進行存儲。通過分析這個文件可以發現,所有連接的socket都存放在sockets對象中,標誌是socket.id,socket的收發函數也是在這裏定。

streams.js是一個存儲的工具類,裏面有兩個成員:id和name,這個文件用來存放已經準備好打通連接通道的客戶端的信息,name是客戶端的名字,id是連接對應客戶端的socket的id 。

如果我們要實現客戶端的信令交互,只需要修改這兩個文件即可(實際上基本不用改)。

開發歷程

說是開發,其實就是學習下而已O(∩_∩)O~

首先拿到題目後,我去查閱相關技術,因爲之前並不瞭解WebRTC,我還以爲是個瀏覽器的插件什麼的。看了半天發現只能瞭解概要,還是先看看demo吧。

將boss提供的參考項目下載,安裝,運行——沒效果,或者說我看不懂它的效果/(ㄒoㄒ)/~~ 。沒辦法,開始看代碼吧,由於沒接觸過node.js,所以先看AndroidRTC,幸運的AndroidRTC的代碼很少,一個Activity,一個RTC核心類,一個參數類。

很快將代碼過了一遍,發現android端的demo異常簡陋,打開就是連接服務器,然後獲取本地視頻,獲取本地視頻的相關內容很快就理清了,關鍵是P2P通道的打通很麻煩,由於本人對網絡方面知識的匱乏(其實你哪裏都匱乏好吧。。),看的雲裏霧裏,只好又回去看別人的技術博客,然後對着代碼一點一點梳理。

這期間我總是試着給Android端加個登錄界面,但是每每構思到一段就不行了,爲什麼?因爲那時我還沒有搞清楚WebRTC是怎麼進行信令交互的,浪費不少時間。

期間測試demo時又發現正常了,是我翻牆時發現的,我發現PC客戶端只有在我翻牆時纔有用,然後打開手機客戶端發現可以正常連接通話,不過我沒必要去看PC的客戶端是怎麼回事,這時我已經理清了Android的結構,AndroidRTC的demo是這樣的:

在你打開服務器的情況下,打開Android端app,app會首先連接服務器(根據寫好的地址),然後服務器發送一個標誌給app(這個標誌其實是服務器端連接app的socket的id),之後app端會判斷:是否有一個callerId,有的話直接開始打通連接通道,沒有的話等待,並且獲取播放自己的媒體流。

之後我去熟悉了下node.js,本想着自己寫個服務器,後來發現node.js雖然簡單,但也不是說上來就能寫的(倒是上來就能改),所以最後決定看看ProjectRTC的服務器實現。由於這時我已經很瞭解整個信令的交互過程,所以很快就找到了進行信令轉發的代碼。然後發現,這個服務器很合適啊,只需要改一點點就可以了。。唉,早看就好了。但是如果沒有前面的鋪墊估計也不好看。

至此已經對對整個項目瞭然於胸了,但是也沒有時間了/(ㄒoㄒ)/~~,所以最後稍微修改下服務器,讓第一個接入的客戶端等待,對於第二個接入的客戶端,將第一個客戶端的id發給它。android端,只需修改幾句代碼,將接收到的對方id賦值給callerId即可。

運行效果

這裏寫圖片描述

這裏寫圖片描述

修改後的項目源碼:http://download.csdn.net/detail/youmingyu/9711362

總結

這次項目感覺不該用這麼長時間,主要是自己思路不清晰就胡亂嘗試,不但行不通而且浪費時間,然後覺得寫博客確實很有幫助,之前還思路混亂,但是在寫的過程中就越發清晰了。
還有一些不足之處以後再彌補:

  • 對添加WebRTC庫的編譯配置過程不清楚,AndroidRTC的demo配好了。
  • 不清楚VideoTrack和AudioTrack怎麼開始播放的,沒看到類似play的代碼。
  • 時間所限,沒有看WebRTC的開發文檔,裏面應該說的很詳細。

參考

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章