WebRTC學習(六)端對端傳輸


一:媒體能力協商

(一)RTCPeerConnection回顧

WebRTC學習(一)WebRTC了解

RTCPeerConnection類是整個WebRTC的一個核心類,它是上層的一個統一的接口,但是在底層做了非常多的復雜邏輯,包括了整個媒體的協商,流和軌道的處理,接收與發送,統計數據,都是由這一個類處理的。

所以對上層來說,你可能簡單的調用了這個類或者里面的幾個簡單的API,但是實際在底層做了大量的工作,所以這個類是整個WebRTC的一個核心。

RTCPeerConnection基本格式:

注意:configuration配置參數,是可選的

(二)RTCPeerConnection方法分類

1.媒體協商

第一類,就是媒體協商相關的,比較簡單,包括四個方法。

通過這幾個方法之后,就可以拿到整個雙方之間的媒體信息,然后雙方會進行交換信息,去協商雙方用的編碼器,音頻格式,協商一致后,他才真正地進行數據傳輸與編解碼。

2.Stream/Track

第二大類,就是流與軌道,在整個WebRTC當中,每一路它是一路流,這個流里面有很多軌,有音頻軌和視頻軌,多路音頻軌多路視頻軌,在此前MediaStream中已經做過介紹,那在這里是同樣的理念,在傳輸中我們傳輸的就是Stream和軌,在軌道中就包含了音頻數據和視頻數據。

3.傳輸的相關方法

第三類,是與傳輸相關的,就是通過RTP協議去傳輸,通過RTCP反饋這個鏈路的質量好壞。再通過數據的統計分析,查看鏈路的質量,是不是已經發生擁塞,還是說鏈路就是不太好的,都可以通過這個傳輸相關的方法來獲取到去計算。

4.統計相關方法

最后一類的就是統計相關的,包括編解碼器,音頻格式,視頻格式,整個傳輸相關的數據都可以通過這個統計相關的方法匯報。

以上就是RTCPeerConnection方法的分類。在后面,我們也會學習到如何通過這些方法來獲取到這些數據,來為我們真正的產品的質量提供幫助。

(三)媒體協商過程(其中SDP信息和Candidate數據的處理,詳細參考二:端對端連接)

對於這兩個端來說,一個假設是A,第二個是B。

1.對於A,首先是要創建一個offer,這個offer就是SDP的一種描述格式去描述的,實際就形成了一個SDP。SDP是包含了一些媒體信息和編解碼信息,包括這個傳輸的相關的信息。

2.創建完成SDP之后,A通過雲端的信令Channel將SDP傳給B,但是傳之前的那他還要調一個方法setLocalDescription,觸發一個非常重要的動作,就是setLocalDescription會觸發一個非常重要動作就是去收集candidate,就是收集候選者。最后通過信令服務器發送SDP信息,在收到candidate后(由TURN/STUN服務器響應),也通過信令服務器轉發給對端!!!

補充:收集候選者

p2p學習過程中了解到,當我們去創建這個連接之前,首先要拿到所有的那種候選者,去收集候選者,是像這個stun或者是turn發送一個請求,在這個請求過程中,會拿到本地主機的IP地址,還有通過NAT之后反射的這個地址以及TURN服務的中繼地址。

3.B端收到這個offer之后,他首先調用setRemoteDescription,將這個offer所形成的SDP的這個數據放到自己遠端的描述信息的槽里。

4.之后B端要回一個answer,通過這個PeerConnection連接,調用方法去創建answer,也就是說創建一個包含B端的媒體信息(和A端offer中的信息一樣)。

5.在傳遞answer到雲端信令服務之前,B端也會去調用setLocalDescription方法,去觸發收集候選者,我的這個網絡有多少個候選者都要收集起來形成一個列表,調用這個函數之后它將請求獲取候選者,之后通過這個信令服務就將answer轉給了A,同時處理候選者列表。

6.A收到這個answer之后它又把它存到這個setRemoteDescription,存到它的遠程槽里,這樣在每一端實際都有兩個SDP

7.那么第一個SDP是我自己的這個媒體信息,第二個SDP是描述對方的媒體信息。那個拿着這兩個媒體信息之后,他在內部就進行一個協商,協商雙方所支持的音視頻、編解碼格式,取出這個交集之后,整個協商過程就算建立完成。

8.協商完成之后才能進行后面的操作,進行真正的編解碼。傳輸數據到對方進行解碼,去渲染音頻,渲染視頻。這就是完整的一個協商過程。

總之,每一端是有四個步驟:

調用方創建offer,設置LocalDescription,然后是接收answer,設置RemoteDescription;

被調用方,他就是先接收offer,然后設置setRemoteDescription,然后是創建answer上設置setLocalDescription。

(四)協商狀態變化

那么下面呢,我們再看看協商的狀態的變化:stable、have-local-offer、have-remote-offer、have-local-PRanswer、have-remote-PRanswer

1.當我們一開始創建這個RTCPeerConnection的時候,處於stable穩定狀態,那么這個時候實際connection就可以使用了,但用的時候它是不能進行編解碼的,為什么呢

因為他沒有進行數據協商,雖然我這個connection類是可以用,但是並沒有進行數據協商,所以他沒法兒進行數據的傳輸與編解碼。 

2.對於調用者來說,首先創建了connection之后,需要創建這個offer,創建offer之后通過調用那個setLocalDescription將這個offer傳參進去后,狀態變化,變成什么呢?

變成have-local-offer,當調用者設完這個之后,如果對方沒有給我回他的answer的時候,那實際我的狀態就一直處於have-local-offer狀態,無論我再接受多少次這個setLocalDescription方法仍在處理這個狀態,所以這個狀態是不會變的。 

3.那什么時候調用者才會進行編解碼呢?只有在遠端的answer回來的時候,如前面所講的遠端的answer創建好,然后通過消息傳給這個調用者的時候,那它會調用這個setRemoteDescription,那么將answer設進去之后,他又回到了stable狀態,這個時候RTCpeerConnection又可以使用了,並且是已經協商過的了,這時候調用端可以進行編解碼,進行傳輸,這是對於調用者來說。

4.那么對於這個被調用者來說,同樣,那當他收到這個offer之后呢,它要調用setRemote offer,這個時候呢,他從那個stable狀態就變成了have-remote-offer,那同樣的,當他自己創建了一個answer之后,並且調用了setLocalDescription這個方法將answer設置進去之后,他又從這個remote-offer變成了stable狀態,那這個時候他也可以工作了

以上狀態變化過程如下:

 對於調用者來說,首先在創建offer之后呢,會調用setLocalDescription將這個offer設置進去,調用者的狀態,就變成了have-local-offer,那當他收到對端的這個answer之后呢,它會調用setRemoteDescription將這個offer設置進去,這樣就完成了一個協商,就從這個have-local-offer變為了stable狀態,那他就可以繼續下面的工作了。

而對於被調用者,他首先是從信令服務器收到一個offer,那他首先調用setRemoteDescription獲取這個offer,那它就變成了have-remote-offer狀態,這個時候再調用自己的這個create answer方法, 創建完自己的這個answer之后調用setLocalDescription(answer)那就從這個have-remote-offer變為了stable狀態,這樣的被調用者他也就完成了自己的協商工作,可以繼續下面的操作。 

但是還是兩種情況,會有一種中間的這個狀態叫做PRanswer,就是提前應答,這個狀態是什么時候會產生呢,就是在雙方通訊的時候其中被調用者還沒有准備好數據的時候,那可以先創建一個臨時的這個answer

那這個臨時的answer有一個特點,就是它沒有媒體數據也就是說沒有音頻流和視頻流,並且將這個發送的方向設置成send  only,

什么意思呢, 對於B來說,他回的這個answer是一個什么樣的answer呢 ?

就是說,我的媒體流還沒有准備好,所以就沒有媒體流,但是我呢,只能發送,不能接受,當他發給對方A的時候,A收到這樣一個send  only,他就知道,對方還不能進入數據,所以這時候他們的通訊雖然是做了的協商,但是他們之間還不能進行通訊。

因為第一個是對方沒有媒體流,第二個是對方不接受我的數據。

處於這樣一個狀態有什么好處呢?

那就是可以提前建立這個鏈路的連接,也就是說包括ICE,包括這個DLS這些跟鏈路相關的這個協商其實都已經創建好了,對剛才我們已經介紹了,就是對於B來說,他已經提前准備好了一個answer,但這個answer里有沒有媒體數據,但是呢實際是有網絡數據的,我收集的各種各種候選者實際都已經有了

那么就可以提前交給這個A,那AB之間,實際就是鏈路層已經協商好了,包括這個DLS還要進行這個握手,因為是安全加密,加密所以要進行握手,握手的時間其實還是蠻長的,那在B准備好這個自己的流之前,將所有的鏈路都准備好,

那一旦這個B向那個用戶申請說想開啟音頻和視頻,當用戶授權說可以,這個時候呢,他們拿到數據之后,只要將數據傳進去,就可以進行這個通訊了。

以上狀態變化過程如下:

B沒有准備好之前,他可以使用一個PRanswer,就是提前預定好的一個answer給這個A發過去,發過去之后,它就變成了這個have-remote-offer這個狀態,這是一個中間狀態在這個狀態下,雙方的這個鏈路是可以協商好的,只是沒有這個媒體數據。
當B設置好他自己的媒體流之后,就是一切都准備好之后,然后再給他回一個最終的answer當調用者收到它這個最終的answer之后呢,他又變成了stable狀態,那雙方就可以就真正協商好了。
這時候呢,實際是減少了底層的這個網絡流的這個握手,以及一些其他的邏輯處理工作,這樣就節省了時間。

對於對端A也是類似的,所以在他回這個真正的answer之前,他是處於這have
-local-PRanswer的,當真正的這個最終的Answer,准備好之后,再重新設一下setLocalAnswer,他又變成了stable狀態。

這就是一個整個協商完整的一個狀態變化,只有在整個協商完成之后,才能進行我們后邊的真正的音視頻數據的傳輸以及編解碼,這就是協商狀態的變化。

(五)媒體協商方法

createOffer,創建一個本地的這個媒體信息,音頻編解碼視頻編解碼等等。

對於這個對端呢,就是收到offer之后呢,它會創建一個createAnswer,這是第二個方法,也就是說我本地的一個信息最終要傳給這個調用者。

那第三個就是setLocalDescription,我把我自己本地的這個SDP的描述信息設置好之后我就可以觸發去采集這個收集候選者了 。

那第四個就是setRemoteDescription,當收到到對端的這個描述信息之后,將它設的setRemoteDescription這個槽兒里去,在內部做真正的協商,就是媒體協商的方法。

createOffer:

PeerConnection類中有一個方法就是createOffer,它有一個可選的option,有幾個選項,每個選項都有其特殊的意義。返回的是一個promise,創建成功之后有一個邏輯處理,失敗會做另外一個邏輯處理。

createAnswer:

那這個createAnswer這個格式其實跟他是類似的,就變成了createAnswer,還有一個option,這個option其實就是作用不大,主要是createOffer是這個option有很多作用。

setLocalDescription:

setLocalDescription的格式就是將createOffer或者createAnswer的結果,包括參數設置到這里就設置好了,

setRemoteDescription:

同樣道理,setRemoteDescription。那這個格式也比較簡單,也是剛那個對端的這個sessionDescription設置進來就好了,這是四個協商相關的方法。

(六)Track方法

要在RTCPeerConnection里就有兩個重要的這個Track的方法,一個是添加,一個是移除。

這個比較簡單,添加的格式就是:

第一個是要添加的這個Track的是音頻的Track還是視頻的Track。

那么第二個是stream,那么這個stream從哪兒來呢?實際就是我們之前介紹的getUserMedia那里我們會拿到一個流,這個流里面可能有音頻Track有視頻Track,那就要遍歷一下,讓他們一個個都加入到這個PeerConnection里去,這樣PeerConnection就可以控制這每一路軌了,從軌中獲取到數據進行發送,這是添加。

還有其他一些參數。

removeTrack這個比較簡單,就是將Addtrack里頭這個send放進去,然后他就可以這個移除掉。

(七)重要事件

就是PeerConnection還有幾個比較重要的事件,現在呢,我們首先介紹兩個:

 

那么第一個是這個協商事件,當進行媒體協商的時候,就會觸發這個事件,onnegotiationneeded就是需要協商,只要協商的時候會觸發這個事件。

那么第二個是onicecandidate,就是當我們收到一個ICE的候選者的時候,也會從底層觸發這個事件,告訴我們現在有一個候選者來了,那么我們要拿到這個候選者,將它添加到我們的這個ICE里去。

二:端到端連接的基本流程

端到端連接的一個基本流程,下圖非常清楚的表達了A與B這兩個端首先進行媒體協商、最終進行鏈路的連接、最后進行媒體數據的傳輸,下面就來進行分析:

(一)流程圖成員

首先在這張圖里面有4個實體:

第一個是A,也就是端到端連接的A端。

然后是B,是端到端連接的B端。

然后是信令服務器Signal。

最后是stun/turn,這個stun和turn服務用的是同一台服務器,既具有stun功能又具有turn功能。

(二)通信流程

1.首先A是發起端也就是呼叫端,呼叫端要與信令服務器建立連接,被呼叫端B端也要與信令服務器建立連接,這樣他們就可以經過信令服務器對信令消息進行中轉。

2.接下來A如果想要發起呼叫,首先它要創建一個PeerConnect,對端的連接對象,創建一個這樣的實例,之后通過getUserMedia拿到本地的音視頻流,將這個流添加到連接里去,這是第一步。

在進行媒體協商之前,我們需要先將流(本地采集的數據)添加到peerConnection連接中去。這樣在媒體協商之前,我們才知道有哪些媒體數據。
如果先做媒體協商的話,知道這是連接中沒有數據媒體流,就不會設置相關底層的接收器、發送器,即使后面設置了媒體流,傳遞給了peerConnection,他也不會進行媒體傳輸,所以我們要先添加流

3.接下來第二步它就可以調用PeerConnect的CreateOffer的方法去創建一個Offer的SDP,創建好SDP之后再調用setLocalDescription,把它設置到LocalDescription這個槽里去,那調用完這個方法之后實際在底層會發送一個bind請求給stun和turn服務,那這個時候它就開始收集所有與對方連接的候選者了。(還沒收集完成,因為stun和turn服務還沒有進行響應)

4.那與此同時調用完setLocalDescription之后,那之前CreateOffer方法拿到這個SDP那也要發送給信令服務器,那通過信令服務器的中轉,最終轉給B,這個時候B就拿到了offer,也就是說A這端的媒體相關的描述信息。

5.我們再來看B端 ,B端收到這個SDP之后呢,首先要創建一個PeerConnetion,創建一個連接對象,創建好這個對象之后它會調用setRemoteDescription將這個收到的SDP設置進去,那設置完成之后它要給一個應答,它要調用Create Answer,這時候它就產生了本機相關的媒體的信息也就是Answer SDP,創建好之后它也要調用setLocalDescription,講這個本地的Answer SDP設置進去,這樣對B來說它的協商就OK了。也就是說它有遠端的SDP同時它自己這端的SDP也獲取到了,這時候在底層就會進行協商。

6.對於B端,在setLocalDescription的時候它也要向stun和turn服務發送一個bind請求,也就是收集它能夠與A進行通信的所有的候選者,在調用完setLocalDescription之后,它將它這個Answer SDP發送給信令服務器 ,通過信令服務器又轉給了A,那A這個時候就拿到了B 這一端的媒體描述信息,然后它再設置setRemoteDescription,那這個時候A也可以進行媒體協商了,這個時候A和B進行媒體協商的動作就算完成了。這是媒體協商這一部分。

7.那接下來stun和turn服務將這個信息回給A,這個時候就會觸發A端的這個onIceCandidate事件,因為我們上面是有一個請求(3中出現的),所有這個時候我們就能收到很多不同的onIceCandidate,A收到這個候選者之后它將這個候選者發送給這個信令服務器,通過信令服務器轉給對端,也就是讓對端知道我都有哪些通路(讓B端知道本機A有哪些通路),那對端B收到這個Candidate之后要調用AddIceCandidate這個方法將它添加到對端的這個連接通路的候選者列表中去。

8.那同樣的道理,當B收到這個Candidate之后,它也發給信令,通過信令轉發給A,那這個時候A也拿到B的所有的候選者,並將它添加到這個候選者列表中去,也就是AddIceCandidate,那這個時候雙方就拿到了所有的對方的可以互通的候選者,這個時候它底層就會做連接檢測,看看哪些,首先他會做一個個的Candidate pair也就是候選者對,然后進行排序,排序完了之后進行連接檢測等等等一系列的連接檢測。

9.在我們之前都做過這個方面的介紹,當它找到一個最優的線路之后呢,A與B就進行通訊了,那首先是A將數據流發送給B,那B在收到這個數據流之后,因為它們前面已經做了綁定了,就知道是誰來的數據給我了,給我之后就與它的這個Connection進行對連,收到這個數據之后它是不能顯示的,B雖然收到數據但是還是顯示不出來,那它要將這個數據進行onAddStream,要添加進來,添加進行之后才能把這個視頻數據和音頻數據向上拋,那才能走到上一層進行視頻的渲染和音頻的渲染。

那以上就是一個基本的端對端連接的基本流程。那經過這樣一個分析大家就會清楚,整個A要與B進行通訊,要走幾步:

第一大塊就是整個媒體的協商,看A端有什么媒體能力看B端有什么媒體能力,他們直接所有的媒體取一個交集,取大家都能夠識別的支持的能力,包括音頻編解碼視頻編解碼,這個采樣率是多少,幀率是多少,以及網絡的一些信息;
第二大部分就是通過ICE對整個可連通的鏈路進行這個鏈路地址的收集,它收集完了之后進行排序和連接檢測,找出雙方可以連接的最優的這條線路;
那么最后拿到線路之后就可以進行媒體數據的傳輸了,當從一端傳輸到另一端之后呢,另一端會收到一個事件,就是onAddStream,當收到這個事件之后就可以將這個媒體流添加到自己的video標簽和audio標簽中進行音頻的播放和視頻的渲染,這個就是整個端對端連接的基本流程。

也就是分成這個三大塊,熟悉了這個基本的流程之后,我們再編寫這個程序的時候,就非常的簡單了。

我們只要按照這個步驟一步步的 操作那么就能實現兩個端之間的通訊。

三:實戰音視頻通信

為了盡量的簡化,並沒有使用真實的跨網絡(從一台電腦的網絡到另一台電腦的網絡進行真實的音視頻的傳輸),而是在我們一個頁面里面其中一個video取展示我們本地采集的音頻和視頻,

那之后創建兩個PeerConnection,然后將這個媒體流加入到其中一個PeerConnection之后讓他們進行連接,連接之后進行本機底層的網絡傳輸,傳到另一端的PeerConnection,

當另一端PeerConnection收到這個音視頻數據之后取回調這個事件,也就是onAddStream,當另一端收到這個onAddStream之后,將這個收到的數據轉給視頻標簽,視頻就被渲染出來了,

雖然沒有經過真實的網絡,但是他們整體的流程和真實網絡的流程是一摸一樣的,雖然不用信令傳輸了,但是我們還是要走這樣一個邏輯。

那么在后面我們就會將真實的網絡加入進去,那么大家就會看到實際整個流程並沒有變,只是把這個信令通過信令服務器進行中轉,網絡連接也不是在自己本地中轉 ,而是通過真實的網絡進行傳輸。

(一)代碼實現

'use strict'

var http = require("http");
var https = require("https");
var fs = require("fs");

var express = require("express");
var serveIndex = require("serve-index");

var socketIo = require("socket.io");    //引入socket.io

var log4js = require('log4js');            //開啟日志
var logger = log4js.getLogger();
logger.level = 'info';

var app = express();                    //實例化express
app.use(serveIndex("./"));        //設置首路徑,url會直接去訪問該目錄下的文件
app.use(express.static("./"));    //可以訪問目錄下的所有文件

//https server
var options = {
    key : fs.readFileSync("./ca/learn.webrtc.com-key.pem"),            //同步讀取文件key
    cert: fs.readFileSync("./ca/learn.webrtc.com.pem"),                //同步讀取文件證書
};

var https_server = https.createServer(options,app);
//綁定socket.io與https服務端
var io = socketIo.listen(https_server);    //io是一個節點(站點),內部有多個房間
https_server.listen(443,"0.0.0.0");
//---------實現了兩個服務,socket.io與https server;都是綁定在443,復用端口

//-----處理事件
io.sockets.on("connection",(socket)=>{    //處理客戶端到達的socket
    //監聽客戶端加入、離開房間消息
    socket.on("join",(room)=>{
        socket.join(room);                //客戶端加入房間
        //io.sockets指io下面的所有客戶端
        //如果是第一個客戶端加入房間(原本房間不存在),則會創建一個新的房間
        var myRoom = io.sockets.adapter.rooms[room];    //從socket.io中獲取房間
        var users = Object.keys(myRoom.sockets).length;    //獲取所有用戶數量

        logger.info("the number of user in room is:"+users);

        //開始回復消息,包含兩個數據房間和socket.id信息
        //socket.emit("joined",room,socket.id);    //給本人
        //socket.to(room).emit("joined",room,socket.id);    //給房間內其他所有人發消息
        //io.in(room).emit("joined",room,socket.id);        //給房間中所有人(包括自己)發送消息
        socket.broadcast.emit("joined",room,socket.id);    //給節點內其他所有人發消息
    });
    
    socket.on("leave",(room)=>{
        var myRoom = io.sockets.adapter.rooms[room];    //從socket.io中獲取房間
        var users = Object.keys(myRoom.sockets).length;    //獲取所有用戶數量

        logger.info("the number of user in room is:"+(users-1));

        socket.leave(room);    //離開房間
        //開始回復消息,包含兩個數據房間和socket.id信息
        socket.broadcast.emit("leaved",room,socket.id);    //給節點內其他所有人發消息
    });

    socket.on("message",(room,msg)=>{
        var myRoom = io.sockets.adapter.rooms[room];    //從socket.io中獲取房間
        logger.info("send data is:"+msg);

        socket.broadcast.emit("message",room,socket.id,msg);    //給節點內其他所有人發消息
    });
});
服務器實現
<html>
    <head>
        <title>    WebRTC PeerConnection </title>
    </head>
    <body>
        <h1>Index.html</h1>
        <div>
            <video autoplay playsinline id="localvideo"></video>
            <video autoplay playsinline id="remotevideo"></video>
        </div>

        <div>
            <button id="start">Start</button> <!--采集音視頻數據-->
            <button id="call">Call</button> <!--創建雙方的peerconnection,開始通信-->
            <button id="hangup">HangUp</button> <!--掛斷-->
        </div>
    </body>
    <script type="text/javascript" src="https://webrtc.github.io/adapter/adapter-latest.js"></script>
    <script type="text/javascript" src="./js/main.js"></script>
    
</html>
index.html

main.js主要javascript文件:

'use strict'

var localVideo = document.querySelector("video#localvideo");
var remoteVideo = document.querySelector("video#remotevideo");

var btnStart = document.querySelector("button#start");
var btnCall = document.querySelector("button#call");
var btnHangup = document.querySelector("button#hangup");

var localStream;                    //設置全局流,用來addStream發送給對端時使用
var pc1;                            //處理pc1與pc2時候,只需要站在一個角度就可以了,因為對端也是一樣的
var pc2;

function handleError(err){
    console.err(err.name+":"+err.message);
}

function getMediaStream(stream){
    localVideo.srcObject = stream;    //顯示到網頁視頻控件
    localStream = stream;            //保存到全局流中
}

//采集本機音視頻數據
function start(){
    if(!navigator.mediaDevices ||
        !navigator.mediaDevices.getUserMedia){
        console.error("the getUserMedia is not support!");
        return;
    }else{
        var constraints = {
            audio:false,
            video:true
        };
        navigator.mediaDevices.getUserMedia(constraints)
                                .then(getMediaStream)    //獲取數據流
                                .catch(handleError);
    }
}

function getRemoteStream(e){            //會有多個流
    remoteVideo.srcObject = e.streams[0];        //只取其中一個就可以了,就將遠端的音視頻流傳給了remoteVideo
}

function getAnswer(desc){
    pc2.setLocalDescription(desc);    //7.遠端設置本地描述信息
    //發送描述信息SDP到signal信令服務端,與pc1進行交換
    //8.pc1設置遠端描述信息
    pc1.setRemoteDescription(desc);    //-----這里開始獲取了所有對端的SDP信息,雙端信息協商完成!!!!----

}

function getOffer(desc){            //獲取了描述信息,開始設置到peerConnection中去
    //4.設置本地的描述信息,添加到peerconnection
    pc1.setLocalDescription(desc);

    //發送描述信息SDP到signal信令服務端,與pc2進行交換
    //5.對端接收設置SDP信息
    pc2.setRemoteDescription(desc);

    //6.創建Answer信息
    pc2.createAnswer()
            .then(getAnswer)        //7.遠端設置本地描述信息
            .catch(handleError);
}

function call(){
    //1.創建peerConnect,pc1與pc2同時連接到signal服務器(這里是一起到本機)
    /*
    在這個Connection里面實際上是有一個可選參數的,
    這個可選參數就涉及到網絡傳輸的一些配置 
    我們整個ICE的一個配置,但是由於是我們在本機內進行傳輸,所以在這里我們就不設置參數了,因為它也是可選的 
    所以它這里就會使用本機host類型的candidate 
    */
    pc1 = new RTCPeerConnection();            //調用方
    pc2 = new RTCPeerConnection();            //被調用方

    //當收到candidate后,會觸發事件,獲取候選者列表,之后調用send candidate發送給signal服務器,從而發送給對端。雙方獲取之后進行連通性檢測
    pc1.onicecandidate = (e) => {    
        pc2.addIceCandidate(e.candidate);    //開始添加給對端
    };
    pc2.onicecandidate = (e) => {    
        pc1.addIceCandidate(e.candidate);    //開始添加給對端
    };

    //pc2是相對特殊的,因為是被調用者,用於接受數據
    pc2.ontrack = getRemoteStream;            //被調用方,接收數據,有數據經過的時候調用ontrack事件

    //下面要先添加媒體流,然后才進行媒體協商
    //2.添加媒體流
    localStream.getTracks().forEach((track)=>{    //獲取所有的軌
        pc1.addTrack(track,localStream);        //將本地產生的音視頻流添加到pc1的peerConnection
    });

    //3.創建offer
    var offerOptions = {
        offerToRecieveAudio:0,    //不處理音頻
        offerToRecieveVideo:1
    };

    pc1.createOffer(offerOptions)
            .then(getOffer)        //4.設置本地的描述信息,添加到peerconnection
            .catch(handleError);
}

function hangup(){
    pc1.close();
    pc2.close();
    pc1 = null;
    pc2 = null;
}

btnStart.onclick = start;
btnCall.onclick = call;
btnHangup.onclick = hangup;

(二)測試結果

 


免責聲明!

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



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