天天看點

WebRTC源碼解讀一

此文章為個人學習和應用WebRTC的學習了解,有不對的地方希望大家提出來共同學習和進步,謝謝。

PeerConnectionFactory/PeerConnection:整個WebRTC中最核心的類,有了這個類才能獲得音視訊相關的其他操作。

PeerConnectionFactory類中包含了各種音視訊資料的初始化。

PeerConnectionFactory.initializeAndroidGlobals  中初始化了是否初始化音視訊,是否硬體加速,是否支援硬體渲染等内容。

PeerConnectionFactory簡化的類圖如下: 

WebRTC源碼解讀一

擷取媒體流

第一步:擷取視訊源videoSource

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

其中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)));           
  • 1
  • 2
  • 3
  • 4
  • 5

第二步:擷取音頻源audioSource

音頻源的擷取簡單許多:

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

第三步:獲得封裝VideoTrack/AudioTrack

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

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

第四步:擷取媒體流localMS

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

MediaStream localMS=factory.createLocalMediaStream("ARDAMS");
localMS.addTrack(videoTrack);
localMS.addTrack(audeoTrack);           
  • 1
  • 2
  • 3

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

建立連接配接通道

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

第一步:建立PeerConnection的對象。

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

PeerConnectionClient:PeerConnection的實作,有了這個類才能進行音視訊相關資料通訊;

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"));           
  • 1
  • 2
  • 3

監聽器建議同時實作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伺服器穿透的結構如下: 

WebRTC源碼解讀一

我們可以使用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"));           
  • 1

然後這個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);//也可以先添加,連接配接通道打通後一樣會觸發監聽響應。           
  • 1

在連接配接通道正常的情況下,對方的PeerConnection.Observer監聽就會調用

onAddStream()

響應函數并提供接收到的媒體流。

播放媒體流

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

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

然後建立一個VideoRenderer對象,并将其賦給videoTrack:

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

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);//調整界面           
  • 1
  • 2
  • 3

信令伺服器

信令伺服器主要是在用戶端打通連接配接通道前傳遞信令的,在用戶端開啟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 。

如果我們要實作用戶端的信令互動,隻需要修改這兩個檔案即可(實際上基本不用改)。

主要的API 有VideoCapturerAndroid, VideoRenderer, MediaStream, PeerConnection 和 PeerConnectionFactory

類圖

WebRTC源碼解讀一

PeerConnectionFactory中 initializeAndroidGlobals()傳回布爾值,true表示一切OK,false表示有失敗。

如果一切ok,可以使用PeerConnectionFactory 的構造方法建立工廠: 

PeerConnectionFactory peerConnectionFactory = new PeerConnectionFactory(); 

有了peerConnectionFactory執行個體,就可以從使用者裝置擷取視訊和音頻,最終将其渲染到螢幕上。

VideoCapturerAndroid & CameraEnumerationAndroid

VideoCapturerAndroid是VideoCapturer接口的實作,封裝了一系列Camera API,為通路攝像頭裝置的流資訊提供了友善。要建立VideoCapturerAndroid的執行個體,首先需要通過CameraEnumerationAndroid類擷取攝像頭裝置基本資訊,如數量、名稱。如下:

// Returns the number of camera devices
CameraEnumerationAndroid.getDeviceCount();

// Returns the name of the camera with camera index. Returns null if the
// camera can not be used.
CameraEnumerationAndroid.getDeviceName();

// Returns the front face device name
CameraEnumerationAndroid.getNameOfFrontFacingDevice();
// Returns the back facing device name
CameraEnumerationAndroid.getNameOfBackFacingDevice();
// Creates a VideoCapturerAndroid instance for the device name
VideoCapturerAndroid.create(name);           
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

有了包含攝像流資訊的VideoCapturerAndroid執行個體,就可以建立從本地裝置擷取到的包含視訊流資訊的MediaStream,進而發送給另一端。但做這些之前,我們首先研究下如何将自己的視訊顯示到應用上面。

VideoSource & VideoTrack

從VideoCapturer執行個體中擷取一些有用資訊,或者要達到最終目标————為連接配接端擷取合适的媒體流,或者僅僅是将它渲染給使用者,我們需要了解VideoSource 和 VideoTrack類。

VideoSource允許方法開啟、停止裝置捕獲視訊。這在為了延長電池壽命而禁止視訊捕獲的情況下比較有用。

VideoTrack 是簡單的添加VideoSource到MediaStream 對象的一個封裝。

我們通過代碼看看它們是如何一起工作的。capturer是VideoCapturer的執行個體,videoConstraints是MediaConstraints的執行個體。

// First we create a VideoSource
VideoSource videoSource = 
    peerConnectionFactory.createVideoSource(capturer, videoConstraints);

// Once we have that, we can create our VideoTrack
// Note that VIDEO_TRACK_ID can be any string that uniquely
// identifies that video track in your application
VideoTrack localVideoTrack = 
    peerConnectionFactory.createVideoTrack(VIDEO_TRACK_ID, videoSource);           
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

AudioSource & AudioTrack

AudioSource和AudioTrack與VideoSource和VideoTrack相似,隻是不需要AudioCapturer 來擷取麥克風,audioConstraints是 MediaConstraints的一個執行個體。

// First we create an AudioSource
AudioSource audioSource =
    peerConnectionFactory.createAudioSource(audioConstraints);

// Once we have that, we can create our AudioTrack
// Note that AUDIO_TRACK_ID can be any string that uniquely
// identifies that audio track in your application
AudioTrack localAudioTrack =
    peerConnectionFactory.createAudioTrack(AUDIO_TRACK_ID, audioSource);           
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

VideoRenderer

通過把VideoRenderer.Callbacks的實作作為參數傳入VideoRenderer的構造方法,WebRTC允許實作自己的渲染。另外,它提供了一種非常好的預設方式VideoRendererGui。簡而言之,VideoRendererGui是一個GLSurfaceView ,使用它可以繪制自己的視訊流。我們通過代碼看一下它是如何工作的,以及如何添加renderer 到 VideoTrack。

// To create our VideoRenderer, we can use the 
// included VideoRendererGui for simplicity
// First we need to set the GLSurfaceView that it should render to
GLSurfaceView videoView = (GLSurfaceView) findViewById(R.id.glview_call);

// Then we set that view, and pass a Runnable
// to run once the surface is ready
VideoRendererGui.setView(videoView, runnable);

// Now that VideoRendererGui is ready, we can get our VideoRenderer
VideoRenderer renderer = VideoRendererGui.createGui(x, y, width, height);

// And finally, with our VideoRenderer ready, we
// can add our renderer to the VideoTrack.
localVideoTrack.addRenderer(renderer);           
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

也可以通過SurfaceViewRenderer建立VideoRenderer的執行個體并添加到VideoTrack。SurfaceViewRenderer是一個SurfaceView并實作了VideoRenderer.Callbacks接口。

SurfaceViewRenderer localRender = (SurfaceViewRenderer) findViewById(R.id.local_video_view);

VideoRenderer renderer = new VideoRenderer(localRender);

localVideoTrack.addRenderer(renderer);           
  • 1
  • 2
  • 3
  • 4
  • 5
  • 1
  • 2
  • 3
  • 4
  • 5

MediaConstraints

MediaConstraints是MediaStream中音頻和視訊軌道的各種限制。對于大多數需要MediaConstraints的方法,一個簡單的MediaConstraints執行個體就可以做到。

MediaConstraints audioConstraints = new MediaConstraints();           
  • 1
  • 1

MediaStream

現在可以在本地看見自己了,接下來就要想辦法讓對方看見自己。這需要建立MediaStream,然後将其添加到PeerConnection 傳送給對方。接下來我們就研究如何添加本地的VideoTrack 和AudioTrack來建立一個合适的MediaStream。

// We start out with an empty MediaStream object, 
// created with help from our PeerConnectionFactory
// Note that LOCAL_MEDIA_STREAM_ID can be any string
MediaStream mediaStream = peerConnectionFactory.createLocalMediaStream(LOCAL_MEDIA_STREAM_ID);

// Now we can add our tracks.
mediaStream.addTrack(localVideoTrack);
mediaStream.addTrack(localAudioTrack);           
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

我們現在有了包含視訊流和音頻流的MediaStream執行個體,而且在螢幕上顯示了我們的臉龐。現在就該把這些資訊傳送給對方了。

PeerConnection

現在我們有了自己的MediaStream,就可以開始連接配接遠端了。這可以通過PeerConnection實作。建立PeerConnection很簡單,隻需要PeerConnectionFactory的協助即可。

PeerConnection peerConnection = peerConnectionFactory.createPeerConnection( iceServers, constraints,  observer);           
  • 1
  • 1

參數的作用如下:

iceServers
    連接配接到外部裝置或者網絡時需要用到這個參數。在這裡添加STUN 和 TURN 伺服器就允許進行連接配接,即使在網絡條件很差的條件下。

constraints
    MediaConstraints的一個執行個體,應該包含offerToRecieveAudio 和 offerToRecieveVideo

observer
     PeerConnection.Observer的一個執行個體。           
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

PeerConnection 包含了addStream、addIceCandidate、createOffer、createAnswer、getLocalDescription、setRemoteDescription 和其他類似方法。我們快速浏覽一下這幾個重要的方法,看它們是如何工作的。 

addStream 

這個是用來将MediaStream 添加到PeerConnection中的,如同它的命名一樣。如果你想要對方看到你的視訊、聽到你的聲音,就需要用到這個方法。

addIceCandidate 

一旦内部IceFramework發現有candidates允許其他方連接配接你時,就會建立IceCandidates 。當通過PeerConnectionObserver.onIceCandidate傳遞資料到對方時,需要通過任何一個你選擇的信号通道擷取到對方的IceCandidates。使用addIceCandidate 添加它們到PeerConnection,以便PeerConnection可以通過已有資訊試圖連接配接對方。

createOffer/createAnswer 

這兩個方法用于原始通話的建立。如你所知,在WebRTC中,已經有了caller和callee的概念,一個是呼叫,一個是應答。createOffer是caller使用的,它需要一個sdpObserver,它允許擷取和傳輸會話描述協定Session Description Protocol (SDP)給對方,還需要一個MediaConstraint。一旦對方得到了這個請求,它将建立一個應答并将其傳輸給caller。SDP是用來給對方描述期望格式的資料(如video、formats、codecs、encryption、resolution、 size等)。一旦caller收到這個應答資訊,雙方就互相建立的通信需求達成了一緻,如視訊、音頻、解碼器等。

setLocalDescription/setRemoteDescription 

這個是用來設定createOffer和createAnswer産生的SDP資料的,包含從遠端擷取到的資料。它允許内部PeerConnection 配置連結以便一旦開始傳輸音頻和視訊就可以開始真正工作。

PeerConnection.Observer

這個接口提供了一種監測PeerConnection事件的方法,例如收到MediaStream時,或者發現iceCandidates 時,或者需要重建立立通訊時。這個接口必須被實作,以便你可以有效處理收到的事件,例如當對方變為可見時,向他們發送信号iceCandidates。

調用順序

發起呼叫
WebRTC源碼解讀一
接受呼叫
WebRTC源碼解讀一
關閉連接配接
WebRTC源碼解讀一

繼續閱讀