日日操夜夜添-日日操影院-日日草夜夜操-日日干干-精品一区二区三区波多野结衣-精品一区二区三区高清免费不卡

公告:魔扣目錄網(wǎng)為廣大站長(zhǎng)提供免費(fèi)收錄網(wǎng)站服務(wù),提交前請(qǐng)做好本站友鏈:【 網(wǎng)站目錄:http://www.ylptlb.cn 】, 免友鏈快審服務(wù)(50元/站),

點(diǎn)擊這里在線(xiàn)咨詢(xún)客服
新站提交
  • 網(wǎng)站:51998
  • 待審:31
  • 小程序:12
  • 文章:1030137
  • 會(huì)員:747

 

WebRTC(Web Real-Time Communication)是為了讓開(kāi)發(fā)者在瀏覽器實(shí)現(xiàn)多媒體交換的技術(shù),于2011年被加入W3C規(guī)范。當(dāng)前的支持情況可以見(jiàn)下圖。

 

WebRTC的核心在于建立PeerConnection實(shí)現(xiàn)視頻流雙端鏈接,要想理解WebRTC的工作流程,有如下后端服務(wù)的概念需要理解:

  • 信令(Signal)服務(wù)器
  • TURN/STUN服務(wù)器
  • 房間服務(wù)器
  • ICE候選者

視頻流的傳輸不是純前端的工作(顯然),然而WebRTC的規(guī)范只規(guī)定了前端的部分,后端的信令傳輸不在WebRTC的范圍之內(nèi),可以隨開(kāi)發(fā)者需求自行開(kāi)發(fā)。


下圖展現(xiàn)了WebRTC的工作流程

 

信令服務(wù)器(圖中黃色部分)主要作用是連接建立前的中轉(zhuǎn)工作。需要自行用websocket實(shí)現(xiàn)。

STUNSession Traversal Utilities for NAT,NAT會(huì)話(huà)穿越應(yīng)用程序)允許位于NAT(或多重NAT)后的客戶(hù)端找出自己的公網(wǎng)地址,查出自己位于哪種類(lèi)型的NAT之后以及NAT為某一個(gè)本地端口所綁定的Inte.NET端端口。這些信息被用來(lái)在兩個(gè)同時(shí)處于NAT路由器之后的主機(jī)之間創(chuàng)建UDP通信。該協(xié)議由RFC 5389定義。

TURNTraversal Using Relay NAT,通過(guò)Relay方式穿越NAT),TURN應(yīng)用模型通過(guò)分配TURNServer的地址和端口作為客戶(hù)端對(duì)外的接受地址和端口,即私網(wǎng)用戶(hù)發(fā)出的報(bào)文都要經(jīng)過(guò)TURNServer進(jìn)行Relay轉(zhuǎn)發(fā)。解決了STUN應(yīng)用無(wú)法穿透對(duì)稱(chēng)NAT(SymmetricNAT)以及類(lèi)似的防火墻的缺陷。

當(dāng)STUN無(wú)法直接建立P2P時(shí),便可以用TURN進(jìn)行中轉(zhuǎn)。

房間服務(wù)器 和RTC的建立并無(wú)直接關(guān)系。但考慮到不可能你的服務(wù)只能同時(shí)支持一對(duì)電腦鏈接,我們必須設(shè)置“房間”。在本項(xiàng)目中,我們的“房間”號(hào)碼就是投屏碼。在投屏碼投屏的應(yīng)用邏輯中,被叫方(投屏屏幕)首先用投屏碼向房間服務(wù)器注冊(cè),客戶(hù)端(請(qǐng)求投屏方)輸入正確的投屏碼后加入“房間”。自此,RTC之后的信令交換都只在這個(gè)“房間”內(nèi)完成,使服務(wù)支持多對(duì)計(jì)算機(jī)互聯(lián)。在實(shí)際實(shí)現(xiàn)中,房間服務(wù)器和信令服務(wù)器可以由同一服務(wù)完成。

ICEInteractive Connectivity Establishment,互動(dòng)式連接建立)提供一種框架,使各種NAT穿透技術(shù)可以實(shí)現(xiàn)統(tǒng)一。該技術(shù)可以讓基于SIP的VoIP客戶(hù)端成功地穿透遠(yuǎn)程用戶(hù)與網(wǎng)絡(luò)之間可能存在的各類(lèi)防火墻。

具體建立流程描述如下:

1、在連接建立之前,雙方不知道彼此,因此都需要向信令服務(wù)器進(jìn)行注冊(cè)。隨后,發(fā)起方創(chuàng)建PeerConnection,調(diào)用WebRTC的createOffer方法將SDPSession Description Protocol,理解為自己的一個(gè)“描述”)傳輸給信令服務(wù)器,由信令服務(wù)器做中繼傳遞給被叫方。

2、被叫方收到Offer以后,調(diào)用createAnswer方法生成針對(duì)發(fā)起方Offer的響應(yīng)。并通過(guò)信令服務(wù)器發(fā)回呼叫方。此時(shí)雙方均保存有兩個(gè)Description(對(duì)于呼叫方是自己的offer和對(duì)面的answer,對(duì)于接收方是對(duì)面的offer和自己的answer)

3、交換完Offer后需要進(jìn)行ICE交換,ICE交換同樣也要利用信令服務(wù)器進(jìn)行交換。在設(shè)置完雙方Description之后,發(fā)起方會(huì)自動(dòng)向配置的STUN服務(wù)器請(qǐng)求自己的ip和端口,STUN服務(wù)器會(huì)返回可能可用的ICE-Candidate。發(fā)起方收到Candidate后需要將其通過(guò)信令發(fā)送到被叫方。被叫方設(shè)定成自己的ICE-Candidate。與此同時(shí),被叫方也需要向STUN服務(wù)器發(fā)起ICE請(qǐng)求流程,把自己的ICE候選者發(fā)送給發(fā)起方。雙方經(jīng)過(guò)多次“協(xié)商”后最終選定ICE的交集進(jìn)行連接。這也就體現(xiàn)了雙方的“互動(dòng)”。

4、交換完ICE候選者后,P2P的PeerConnection建立完成,就可以傳輸各種媒體信息了。在實(shí)際測(cè)試中ICE的交換并不一定在收到Answer后才觸發(fā),是可以提前觸發(fā)的。


呼叫端的流程

0、加個(gè)按鈕吧!

輸入“投屏碼”,和屏幕端加入同一個(gè)“房間”,以便于進(jìn)行信令交換!點(diǎn)擊按鈕后,運(yùn)行如下代碼:也就是說(shuō),以下所有的代碼,都是在你點(diǎn)擊這個(gè)按鈕后運(yùn)行的。

socket = io.connect("你的信令服務(wù)器地址");
socket.on("connect", function () {
  socket.emit("CONNECT_TO_TV", {
    username: "lgy",
    projCode: store.projCode.toUpperCase(),
  });
});

在這里,我們建立了socket鏈接,并告訴了服務(wù)器我們想加入的“房間”。connect事件在建立連接后自動(dòng)觸發(fā)。(我的考慮是點(diǎn)擊“鏈接”按鈕再建立鏈接,而不是一直長(zhǎng)連接著,這個(gè)鏈接專(zhuān)門(mén)用于RTC流程建立,也就是說(shuō)點(diǎn)擊“鏈接”前,下文的過(guò)程都不會(huì)進(jìn)行。只有點(diǎn)擊按鈕后,才會(huì)有以下的流程)

1、建立PeerConnection對(duì)象

const configuration = {
  iceServers: [
    {
      urls: "turn:你的turn服務(wù)器地址,端口一般是3478",
      username: "turn用戶(hù)名",
      credential: "turn密碼",
    },
    { urls: "stun:你的stun服務(wù)器地址,端口一般是3478" },
  ],
  iceCandidatePoolSize: 2,
};
const peerConnection = new RTCPeerConnection(configuration);

建立RTCPeerConnection是應(yīng)該傳入候選iceServer,其中turnServer由于協(xié)議規(guī)定,必須有username和credential字段,stunServer不需要身份驗(yàn)證。詳情可以參考MDN RTCPeerConnection文檔

2、捕獲視頻流

const transferStream = await navigator.mediaDevices.getUserMedia({
          video: {
            mandatory: {
              chromeMediaSource: "desktop",
              chromeMediaSourceId: Screensources[screenid].id,
              minWidth: 640,
              maxWidth: 1920,
              minHeight: 360,
              maxHeight: 1080,
            },
          },
        });

在這里,我們利用getUserMedia獲取到了視頻流MediaStream對(duì)象,若要同時(shí)獲取音頻,可以再增加audio選項(xiàng),詳情->getUserMedia

 何時(shí)獲取視頻流?
請(qǐng)注意,您不必現(xiàn)在就獲取視頻流,getUserMedia()會(huì)返回一個(gè)Promise,因此這里采用了await的寫(xiě)法,但是您最好提前聲明一個(gè)MediaStream對(duì)象,因?yàn)樵贠ffer生成之前媒體流必須添加到PeerConnection中,詳見(jiàn)
https://stackoverflow.com/questions/17391750/remote-videostream-not-working-with-webrtc

3、創(chuàng)建Offer

// 重要!在生成offer前確保已添加視頻流,不然可能連接建立完成后無(wú)法觸發(fā)對(duì)面的onaddstream監(jiān)聽(tīng)器。
peerConnection.addStream(transferStream);
 
const offer = await peerConnection.createOffer({
  offerToReceiveVideo: 1,
 // 已過(guò)時(shí),最好用RTCRtpTransceiver替代
});
await peerConnection.setLocalDescription(offer);
 // 設(shè)置自己的Description
// 發(fā)送websocket到信令服務(wù)器
socket.emit("RTC_Client_Offer_To_Server", {
  offer: offer,
});

 關(guān)于addStream和offerToReceiveVideo
根據(jù)最新的規(guī)范,addStream和offerToReceiveVideo兩處已經(jīng)過(guò)時(shí),根據(jù)官方建議(
https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/addstream_event),還是采用最新的addTrack和RTCRtpTransceiver來(lái)替換為好。詳見(jiàn)下文

這里有幾點(diǎn)需要注意:

  • 請(qǐng)?jiān)谶B接建立之前為peerConnection添加stream或tracks
  • 請(qǐng)?jiān)谡{(diào)用createOffer時(shí)參數(shù)務(wù)必傳入offerToReceiveVideo或設(shè)置RTCRtpTransceiver。您可以console.log()您的offer查看,若您的描述十分的短(只有一兩行)大概率是沒(méi)有設(shè)置該參數(shù),正常情況下offer應(yīng)該有幾十行。而且沒(méi)有設(shè)置該參數(shù)會(huì)導(dǎo)致無(wú)法觸發(fā)ICE的收集工作,因而無(wú)法觸發(fā)onicecandidate事件(將在后文提到)。

若采用track的寫(xiě)法,addStream應(yīng)該這樣寫(xiě):

transferStream.getTracks().forEach(track => {
    peerConnection.addTrack(track, transferStream);
});

你可能已經(jīng)注意到我們?cè)谶@里用了socket.emit這一個(gè)函數(shù)來(lái)發(fā)送Offer,這是Socket.IO的使用方法,在本項(xiàng)目中我使用了Node.js用做websocket鏈接,調(diào)用Socket.IO這個(gè)包。先不用管這是干嘛的,他就是向信令服務(wù)器發(fā)送了一個(gè)指令,要求傳送第二個(gè)參數(shù)(就是Offer)的內(nèi)容。

4、注冊(cè)事件監(jiān)聽(tīng)器

首先注冊(cè)ICE監(jiān)聽(tīng)器。當(dāng)Offer正常交換完成后,會(huì)自動(dòng)觸發(fā)ICE的收集,收集過(guò)后的結(jié)果會(huì)觸發(fā)onicecandidate監(jiān)聽(tīng)器。我們要做的很簡(jiǎn)單——拿到這個(gè)ICE收集結(jié)果,并通過(guò)類(lèi)似的方式通過(guò)信令服務(wù)器傳遞給接收方。

peerConnection.onicecandidate = function (event) {
       console.log(event);
       if (event.candidate) {
         socket.emit("RTC_Candidate_Exchange", {
           iceCandidate: event.candidate,
         });
       }
};
// 或者你也可以用監(jiān)聽(tīng)器的寫(xiě)法:
peerConnection.addEventListener('icecandidate', event => {
       console.log(event);
       if (event.candidate) {
         socket.emit("RTC_Candidate_Exchange", {
           iceCandidate: event.candidate,
         });
       }
})

websocket向信令服務(wù)器發(fā)送了RTC_Candidate_Exchange指令,并傳遞了從事件中獲取的ICE候選人信息。

接著注冊(cè)ICE接收器,當(dāng)收到對(duì)面的ICE候選人信息時(shí),我們要將它添加到自己的ICE候選人列表。

socket.on("RTC_Candidate_Exchange", async (message) => {
        if (message.iceCandidate) {
          try {
            await peerConnection.addIceCandidate(message.iceCandidate);
          } catch (e) {
            console.error("Error adding received ice candidate", e);
          }
        }
});

當(dāng)收到信令服務(wù)器主題為RTC_Candidate_Exchange的消息時(shí),取出消息中的ICE候選者,使用addIceCandidate加入到自己的列表。

也不能忘記注冊(cè)Answer接收器——當(dāng)對(duì)面收到了我們的Offer,把Answer發(fā)送過(guò)來(lái)時(shí),添加到RemoteDescription中

socket.on("RTC_TV_Answer_To_Client", async (msg) => {
        if (msg.answer) {
            const remoteDesc = new RTCSessionDescription(msg.answer);
            await peerConnection.setRemoteDescription(remoteDesc);
            console.log("RTC TV answer received", peerConnection);
        }
});

在這里,我們使用RTCSessionDescription包裹了Answer,并將它通過(guò)setRemoteDescription(注意最開(kāi)始的Offer是setLocalDescription,不要搞混)方法加入到了peerConnection中。

事實(shí)上到這里必要的工作已經(jīng)準(zhǔn)備完成,但是你肯定想知道你的鏈接建立的狀態(tài),因此我們?cè)僮?cè)一個(gè)狀態(tài)監(jiān)聽(tīng)器來(lái)反饋連接的狀態(tài):

peerConnection.onconnectionstatechange = function (event) {
    console.log(
      "RTC Connection State Change :",
      peerConnection.connectionState
    );
};

被叫端的流程

前面提到,我們需要讓服務(wù)器加入以其投屏碼命名的“房間”以便信令交互。所以我們可以讓頁(yè)面生成投屏碼后向服務(wù)器發(fā)起Socket注冊(cè)。

// 發(fā)送注冊(cè)請(qǐng)求,可以攜帶你想要的數(shù)據(jù)。
socket.on("connect", function () {
  socket.emit("TV_REGISTER");
});
// 注冊(cè)成功后服務(wù)器發(fā)起TV_REGISTER_SUCCESS事件并傳回生成的投屏碼
socket.on("TV_REGISTER_SUCCESS", function (config) {
  that.code = config.projCode || "獲取投屏碼失敗";
  console.log("Regist Successful, config:", config);
});

第一條“connect”是定義好的事件,將在socket建立成功后觸發(fā)。我在這里的思路是服務(wù)器生成投屏碼,再下發(fā)過(guò)去。當(dāng)然也可以TV生成然后去服務(wù)器“報(bào)備”。(默默說(shuō)一句其實(shí)我覺(jué)得客戶(hù)端生成好,要不然斷鏈以后服務(wù)端很可能返回另一個(gè)投屏碼,在網(wǎng)絡(luò)不好的環(huán)境下每次重連都是新的房間就沒(méi)辦法實(shí)現(xiàn)自動(dòng)恢復(fù)了,打算之后有空改一下,生成以后存在localStorage里)

被叫端和呼叫端差不多,甚至更為簡(jiǎn)單。大部分由注冊(cè)的監(jiān)聽(tīng)器來(lái)完成

首先我們需要?jiǎng)?chuàng)建RTCPeerConnection

peerConnection = new RTCPeerConnection(configuration);

我們需要收到呼叫端的Offer并創(chuàng)建Answer:

socket.on("RTC_Client_Offer_To_TV", async (data) => {
  console.log("RTC_Client_Offer_To_TV");
  if (data.offer) {
    peerConnection.setRemoteDescription(
      new RTCSessionDescription(data.offer)
    );
 // 是呼叫方的Offer,放Remote
    // 創(chuàng)建Answer,并保存為L(zhǎng)ocal
    const answer = await peerConnection.createAnswer();
    await peerConnection.setLocalDescription(answer);
    // 利用信令回復(fù)Answer
    socket.emit("RTC_TV_Answer_To_Server", { answer: answer });
  }
});

我們通過(guò)注冊(cè)一個(gè)socket事件,當(dāng)服務(wù)器返回RTC_Client_Offer_To_TV事件時(shí),提取出Offer并保存,生成Answer并發(fā)布RTC_TV_Answer_To_Server事件讓服務(wù)器轉(zhuǎn)發(fā)給發(fā)起端。

類(lèi)似于呼叫方,注冊(cè)ICE事件監(jiān)聽(tīng)器和RTC狀態(tài)變化監(jiān)聽(tīng)器(見(jiàn)呼叫方代碼,一模一樣)

此外,我們需要將視頻流提取出來(lái),并作為視頻源給到到頁(yè)面上的video元素中。

peerConnection.onaddstream = (event) => {
    player = document.getElementById('video');
    player.srcObject = event.stream;
}

// 若您在呼叫方使用Track而不是用Stram,則注冊(cè)這個(gè)
peerConnection.addEventListener('track', async (event) => {
    player = document.getElementById('video');
    player.srcObject = remoteStream;
    remoteStream.addTrack(event.track, remoteStream);
});

至此,所有的客戶(hù)端和投屏端配置已經(jīng)完成。接下來(lái)就要進(jìn)行后端服務(wù)器開(kāi)發(fā)了。我將會(huì)在以后的文章中寫(xiě)如何建立websocket信令服務(wù)和如何部署TURN/STUN服務(wù)器并解釋TURN服務(wù)器的動(dòng)態(tài)身份驗(yàn)證機(jī)制。感謝閱讀。

分享到:
標(biāo)簽:WEBRtc
用戶(hù)無(wú)頭像

網(wǎng)友整理

注冊(cè)時(shí)間:

網(wǎng)站:5 個(gè)   小程序:0 個(gè)  文章:12 篇

  • 51998

    網(wǎng)站

  • 12

    小程序

  • 1030137

    文章

  • 747

    會(huì)員

趕快注冊(cè)賬號(hào),推廣您的網(wǎng)站吧!
最新入駐小程序

數(shù)獨(dú)大挑戰(zhàn)2018-06-03

數(shù)獨(dú)一種數(shù)學(xué)游戲,玩家需要根據(jù)9

答題星2018-06-03

您可以通過(guò)答題星輕松地創(chuàng)建試卷

全階人生考試2018-06-03

各種考試題,題庫(kù),初中,高中,大學(xué)四六

運(yùn)動(dòng)步數(shù)有氧達(dá)人2018-06-03

記錄運(yùn)動(dòng)步數(shù),積累氧氣值。還可偷

每日養(yǎng)生app2018-06-03

每日養(yǎng)生,天天健康

體育訓(xùn)練成績(jī)?cè)u(píng)定2018-06-03

通用課目體育訓(xùn)練成績(jī)?cè)u(píng)定