偷偷摘套内射激情视频,久久精品99国产国产精,中文字幕无线乱码人妻,中文在线中文a,性爽19p

使用WebRTC實現(xiàn)P2P視頻流

網(wǎng)絡(luò) 網(wǎng)絡(luò)管理
網(wǎng)絡(luò)實時通信(WebRTC)是一個開源標(biāo)準(zhǔn),允許網(wǎng)絡(luò)應(yīng)用程序和網(wǎng)站之間的實時通信,而無需插件或額外的軟件安裝。它也可以作為iOS和安卓應(yīng)用程序的庫,提供與標(biāo)準(zhǔn)相同的功能。

前言

網(wǎng)絡(luò)實時通信(WebRTC)是一個開源標(biāo)準(zhǔn),允許網(wǎng)絡(luò)應(yīng)用程序和網(wǎng)站之間的實時通信,而無需插件或額外的軟件安裝。它也可以作為iOS和安卓應(yīng)用程序的庫,提供與標(biāo)準(zhǔn)相同的功能。

WebRTC適用于任何操作系統(tǒng),可用于所有現(xiàn)代瀏覽器,包括谷歌Chrome、Mozilla火狐和Safari。使用WebRTC的一些主要項目包括谷歌會議和Hangouts、WhatsApp、亞馬遜Chime、臉書Messenger、Snapchat和Discord。

在本文中,我們將介紹WebRTC的主要用例之一:從一個系統(tǒng)到另一個系統(tǒng)的點對點(P2P)音頻和視頻流。此功能類似于Twitch等實時流媒體服務(wù),但規(guī)模更小、更簡單。

要了解的核心WebRTC概念

在本節(jié)中,我將回顧您應(yīng)該了解的五個基本概念,以了解使用WebRTC的Web應(yīng)用程序的工作原理。這些概念包括點對點通信、Signal服務(wù)器和ICE協(xié)議。

點對點通信

在本指南中,我們將使用WebRTC的RTCPeerConnection對象,該對象主要涉及連接兩個應(yīng)用程序并允許它們使用點對點協(xié)議進(jìn)行通信。

在去中心化網(wǎng)絡(luò)中,對等通信是網(wǎng)絡(luò)中計算機(jī)系統(tǒng)(對等點)之間的直接鏈接,沒有中介(例如服務(wù)器)。雖然WebRTC不允許對等點在所有場景下直接相互通信,但它使用的ICE協(xié)議和Signal服務(wù)器允許類似的行為。您將在下面找到更多關(guān)于它們的信息。

Signal 服務(wù)器

對于WebRTC應(yīng)用程序中的每一對要開始通信,它們必須執(zhí)行“握手”,這是通過offer或answer完成的。一個對等點生成offer并與另一個對等點共享,另一個對等點生成answer并與第一個對等點共享。

為了使握手成功,每個對等點都必須有一種方法來共享他們的offer或answer。這就是Signal 服務(wù)器的用武之地。

Signal 服務(wù)器的主要目標(biāo)是啟動對等點之間的通信。對等點使用信號服務(wù)器與另一個對等點共享其offer或answer,另一個可以使用Signal 服務(wù)器與第一個對等點共享其offer或answer。

ICE協(xié)議

在特定情況下,比如當(dāng)所有涉及的設(shè)備都不在同一個本地網(wǎng)絡(luò)中時,WebRTC應(yīng)用程序可能很難相互建立對等連接。這是因為除非對等點在同一個本地網(wǎng)絡(luò)中,否則它們之間的直接socket連接并不總是可能的。

當(dāng)您想使用跨不同網(wǎng)絡(luò)的對等連接時,您需要使用交互式連通建立方式(ICE)協(xié)議。ICE協(xié)議用于在Internet上的對等點之間建立連接。ICE服務(wù)器使用該協(xié)議在對等點之間建立連接和中繼信息。

ICE協(xié)議包括用于NAT的會話遍歷實用程序(STUN)協(xié)議、圍繞NAT使用中繼的遍歷(TURN)協(xié)議或兩者的混合。

在本教程中,我們不會涵蓋ICE協(xié)議的實際方面,因為構(gòu)建服務(wù)器、讓它工作和測試它所涉及的復(fù)雜性。然而,了解WebRTC應(yīng)用程序的限制以及ICE協(xié)議在哪里可以解決這些限制是有幫助的。

WebRTC P2P視頻流入門

現(xiàn)在我們已經(jīng)完成了所有這些,是時候開始復(fù)雜的工作了。在下一節(jié)中,我們將研究視頻流項目。當(dāng)我們開始時,您可以在這里看到該項目的現(xiàn)場演示。

在我們開始之前,我有一個GitHub存儲庫 https://github.com/GhoulKingR/webrtc-project ,您可以克隆它以關(guān)注本文。此存儲庫有一個start-tutorial文件夾,按照您將在下一節(jié)中采取的步驟進(jìn)行組織,以及每個步驟末尾的代碼副本。雖然不需要使用repo,但它很有幫助。

我們將在repo中處理的文件夾稱為start-tutorial。它包含三個文件夾:step-1、step-2和step-3。這三個文件夾對應(yīng)于下一節(jié)中的步驟。

運(yùn)行視頻流項目

現(xiàn)在,讓我們開始構(gòu)建項目。我把這個過程分為三個步驟。我們將創(chuàng)建一個項目,我們可以在每個步驟中運(yùn)行、測試和使用。

這些步驟包括:

  • 網(wǎng)頁內(nèi)的視頻流
  • 使用BroadcastChannel在瀏覽器選項卡和窗口之間的流
  • 使用 signal服務(wù)器在同一設(shè)備上的不同瀏覽器之間流。

網(wǎng)頁內(nèi)的視頻流

在這一步中,我們只需要一個index.html文件。如果您在repo中工作,您可以使用start-tutorial/step-1/index.html文件。

現(xiàn)在,讓我們將此代碼粘貼到其中:

<body>
  <video id="local" autoplay muted></video>
  <video id="remote" autoplay></video>

  <button onclick="start(this)">start video</button>
  <button id="stream" onclick="stream(this)" disabled>stream video</button>

  <script>
    // get video elements
    const local = document.querySelector("video#local");
    const remote = document.querySelector("video#remote");

    function start(e) {
      e.disabled = true;
      navigator.mediaDevices.getUserMedia({ audio: true, video: true })
        .then((stream) => {
          local.srcObject = stream;
          document.getElementById("stream").disabled = false;  // enable the stream button
        })
        .catch(() => e.disabled = false);
    }
    
function stream(e) {
      // disable the stream button
      e.disabled = true;
      
      const config = {};
      const localPeerConnection = new RTCPeerConnection(config);  // local peer
      const remotePeerConnection = new RTCPeerConnection(config);  // remote peer
      
      // if an icecandidate event is triggered in a peer add the ice candidate to the other peer
      localPeerConnection.addEventListener("icecandidate", e => remotePeerConnection.addIceCandidate(e.candidate));
      remotePeerConnection.addEventListener("icecandidate", e => localPeerConnection.addIceCandidate(e.candidate));

      // if the remote peer detects a track in the connection, it forwards it to the remote video element
      remotePeerConnection.addEventListener("track", e => remote.srcObject = e.streams[0]);

      // get camera and microphone source tracks and add it to the local peer
      local.srcObject.getTracks()
        .forEach(track => localPeerConnection.addTrack(track, local.srcObject));
      // Start the handshake process
      localPeerConnection.createOffer({ offerToReceiveAudio: true, offerToReceiveVideo: true })
        .then(async offer => {
          await localPeerConnection.setLocalDescription(offer);
          await remotePeerConnection.setRemoteDescription(offer);
          console.log("Created offer");
        })
        .then(() => remotePeerConnection.createAnswer())
        .then(async answer => {
          await remotePeerConnection.setLocalDescription(answer);
          await localPeerConnection.setRemoteDescription(answer);
          console.log("Created answer");
        });
    }
  </script>
</body>

它會給你一些看起來像這樣的展示:

圖片圖片

現(xiàn)在,讓我們來看看這是怎么回事。

要構(gòu)建項目,我們需要兩個視頻元素。我們將使用一個來捕獲用戶的相機(jī)和麥克風(fēng)。之后,我們將使用WebRTC的RTCPeerConnection對象將此元素的音頻和視頻流饋送到另一個視頻元素:

<video id="local" autoplay muted></video>
<video id="remote" autoplay></video>

RTCPeerConnection對象是在Web瀏覽器或設(shè)備之間建立直接點對點連接的主要對象。

然后我們需要兩個按鈕。一個是激活用戶的網(wǎng)絡(luò)攝像頭和麥克風(fēng),另一個是將第一個視頻元素的內(nèi)容流式傳輸?shù)降诙€:

<button onclick="start(this)">start video</button>
<button id="stream" onclick="stream(this)" disabled>stream video</button>

單擊時,"start video"按鈕運(yùn)行start功能。單擊時,"stream video"按鈕運(yùn)行stream功能。

我們首先看一下start函數(shù):

function start(e) {
  e.disabled = true;
  navigator.mediaDevices.getUserMedia({ audio: true, video: true })
   .then((stream) => {
      local.srcObject = stream;
      document.getElementById("stream").disabled = false;  // enable the stream button
    })
    .catch(() => e.disabled = false);
}

當(dāng)start函數(shù)運(yùn)行時,它首先使開始按鈕不可單擊。然后,它通過navigator.mediaDevices.getUserMedia方法請求用戶使用其網(wǎng)絡(luò)攝像頭和麥克風(fēng)的權(quán)限。

如果用戶授予權(quán)限,start函數(shù)通過其srcObject字段將視頻和音頻流發(fā)送到第一個視頻元素,并啟用stream按鈕。如果從用戶那里獲得權(quán)限出現(xiàn)問題或用戶拒絕權(quán)限,該函數(shù)會再次單擊start按鈕。

現(xiàn)在,讓我們看一下stream函數(shù):

function stream(e) {
  // disable the stream button
  e.disabled = true;
  
  const config = {};
  const localPeerConnection = new RTCPeerConnection(config);  // local peer
  const remotePeerConnection = new RTCPeerConnection(config);  // remote peer
  
  // if an icecandidate event is triggered in a peer add the ice candidate to the other peer
  localPeerConnection.addEventListener("icecandidate", e => remotePeerConnection.addIceCandidate(e.candidate));
  remotePeerConnection.addEventListener("icecandidate", e => localPeerConnection.addIceCandidate(e.candidate));

  // if the remote peer receives track from the connection, it feeds them to the remote video element
  remotePeerConnection.addEventListener("track", e => remote.srcObject = e.streams[0]);

  // get camera and microphone tracks then feed them to local peer
  local.srcObject.getTracks()
    .forEach(track => localPeerConnection.addTrack(track, local.srcObject));
  
  // Start the handshake process
  localPeerConnection.createOffer({ offerToReceiveAudio: true, offerToReceiveVideo: true })
    .then(async offer => {
      await localPeerConnection.setLocalDescription(offer);
      await remotePeerConnection.setRemoteDescription(offer);
      console.log("Created offer");
    })
    .then(() => remotePeerConnection.createAnswer())
    .then(async answer => {
      await remotePeerConnection.setLocalDescription(answer);
      await localPeerConnection.setRemoteDescription(answer);
      console.log("Created answer");
    });
}

我添加了注釋來概述stream函數(shù)中的過程,以幫助理解它。然而,握手過程(第21-32行)和ICE候選事件(第10行和第11行)是我們將更詳細(xì)討論的重要部分。

在握手過程中,每對都會根據(jù)對創(chuàng)建的offer和answer設(shè)置其本地和遠(yuǎn)程描述:

  • 生成offer的對將其本地描述設(shè)置為該offer,然后將offer的副本發(fā)送到第二對以設(shè)置為其遠(yuǎn)程描述
  • 同樣,生成answer的對將answer設(shè)置為其本地描述,并將副本發(fā)送到第一對以設(shè)置為其遠(yuǎn)程描述

完成這個過程后,同行立即開始相互交流。

ICE候選是對等方的地址(IP、端口和其他相關(guān)信息)。RTCPeerConnection對象使用ICE候選來查找和相互通信。RTCPeerConnection對象中的icecandidate事件在對象生成ICE候選時觸發(fā)。

我們設(shè)置的事件偵聽器的目標(biāo)是將ICE候選人從一個對等點傳遞到另一個對等點。

在瀏覽器選項卡和帶有BroadcastChannel的窗口之間

使用WebRTC設(shè)置點對點應(yīng)用程序的挑戰(zhàn)之一是讓它跨不同的應(yīng)用程序?qū)嵗蚓W(wǎng)站工作。在本節(jié)中,我們將使用廣播頻道API允許我們的項目在單個網(wǎng)頁之外但在瀏覽器上下文中工作。

創(chuàng)建必要的文件

我們將從創(chuàng)建兩個文件開始,streamer.html和index.html。在repo中,這些文件位于start-tutorial/step-2文件夾中。streamer.html頁面允許用戶從他們的相機(jī)創(chuàng)建實時流,而index.html頁面將使用戶能夠觀看這些實時流。

現(xiàn)在,讓我們將這些代碼塊粘貼到文件中。然后,我們將更深入地研究它們。

首先,在streamer.html文件中,粘貼以下代碼:

<body>
  <video id="local" autoplay muted></video>
  <button onclick="start(this)">start video</button>
  <button id="stream" onclick="stream(this)" disabled>stream video</button>
  <script>
    // get video elements
    const local = document.querySelector("video#local");
    let peerConnection;
    const channel = new BroadcastChannel("stream-video");
    channel.onmessage = e => {
      if (e.data.type === "icecandidate") {
        peerConnection?.addIceCandidate(e.data.candidate);
      } else if (e.data.type === "answer") {
        console.log("Received answer")
        peerConnection?.setRemoteDescription(e.data);
      }
    }
    // function to ask for camera and microphone permission
    // and stream to #local video element
    function start(e) {
      e.disabled = true;
      document.getElementById("stream").disabled = false;  // enable the stream button
      navigator.mediaDevices.getUserMedia({ audio: true, video: true })
        .then((stream) => local.srcObject = stream);
    }
    
     function stream(e) {
      e.disabled = true;

      const config = {};
      peerConnection = new RTCPeerConnection(config);  // local peer connection

      // add ice candidate event listener
      peerConnection.addEventListener("icecandidate", e => {
        let candidate = null;
        
        // prepare a candidate object that can be passed through browser channel
        if (e.candidate !== null) {
          candidate = {
            candidate: e.candidate.candidate,
            sdpMid: e.candidate.sdpMid,
            sdpMLineIndex: e.candidate.sdpMLineIndex,
          };
        }
        channel.postMessage({ type: "icecandidate", candidate });
      });

      // add media tracks to the peer connection
      local.srcObject.getTracks()
        .forEach(track => peerConnection.addTrack(track, local.srcObject));
        // Create offer and send through the browser channel
      peerConnection.createOffer({ offerToReceiveAudio: true, offerToReceiveVideo: true })
        .then(async offer => {
          await peerConnection.setLocalDescription(offer);
          console.log("Created offer, sending...");
          channel.postMessage({ type: "offer", sdp: offer.sdp });
        });
    }
  </script>
</body>

然后,在index.html文件中,粘貼以下代碼:

<body>
  <video id="remote" controls></video>
  
  <script>
    // get video elements
    const remote = document.querySelector("video#remote");
    let peerConnection;

    const channel = new BroadcastChannel("stream-video");
    channel.onmessage = e => {
      if (e.data.type === "icecandidate") {
        peerConnection?.addIceCandidate(e.data.candidate)
      } else if (e.data.type === "offer") {
        console.log("Received offer")
        handleOffer(e.data)
      }
    }
    function handleOffer(offer) {
      const config = {};
      peerConnection = new RTCPeerConnection(config);
      peerConnection.addEventListener("track", e => remote.srcObject = e.streams[0]);
      peerConnection.addEventListener("icecandidate", e => {
        let candidate = null;
        if (e.candidate !== null) {
          candidate = {
            candidate: e.candidate.candidate,
            sdpMid: e.candidate.sdpMid,
            sdpMLineIndex: e.candidate.sdpMLineIndex,
          }
        }
        channel.postMessage({ type: "icecandidate", candidate })
      });
      peerConnection.setRemoteDescription(offer)
        .then(() => peerConnection.createAnswer())
        .then(async answer => {
          await peerConnection.setLocalDescription(answer);
          console.log("Created answer, sending...")
          channel.postMessage({
            type: "answer",
            sdp: answer.sdp,
          });
        });
    }
  </script>
</body>

在您的瀏覽器中,頁面的外觀和功能將類似于以下動畫:

圖片圖片

streamer.html文件的詳細(xì)分解

現(xiàn)在,讓我們更詳細(xì)地探索這兩個頁面。我們將從streamer.html頁面開始。此頁面只需要一個視頻和兩個按鈕元素:

<video id="local" autoplay muted></video>
<button onclick="start(this)">start video</button>
<button id="stream" onclick="stream(this)" disabled>stream video</button>

"start video"按鈕的工作方式與上一步相同:它請求用戶允許使用他們的相機(jī)和麥克風(fēng),并將流提供給視頻元素。然后,"stream video"按鈕初始化對等連接并將視頻流提供給對等連接。

由于此步驟涉及兩個網(wǎng)頁,我們正在使用廣播頻道API。在我們的index.html和streamer.html文件中,我們必須在每個頁面上初始化一個具有相同名稱的BroadcastChannel對象,以允許它們進(jìn)行通信。

BroadcastChannel對象允許您在具有相同URL來源的瀏覽上下文(例如窗口或選項卡)之間傳遞基本信息。

當(dāng)你初始化一個BroadcastChannel對象時,你必須給它一個名字。你可以把這個名字想象成聊天室的名字。如果你用相同的名字初始化兩個BroadcastChannel對象,他們可以像在聊天室一樣互相交談。但是如果他們有不同的名字,他們就不能交流,因為他們不在同一個聊天室里。

我說“聊天室”是因為您可以擁有多個具有相同名稱的BroadcastChannel對象,并且它們都可以同時相互通信。

由于我們正在處理兩個頁面,每個頁面都有對等連接,我們必須使用BroadcastChannel對象在兩個頁面之間來回傳遞offer和answer。我們還必須將對等連接的ICE候選傳遞給另一個。所以,讓我們看看它是如何完成的。

這一切都從stream函數(shù)開始:

// streamer.html -> script element

function stream(e) {
  e.disabled = true;

  const config = {};
  peerConnection = new RTCPeerConnection(config);  // local peer connection

  // add ice candidate event listener
  peerConnection.addEventListener("icecandidate", e => {
    let candidate = null;
    
    // prepare a candidate object that can be passed through browser channel
    if (e.candidate !== null) {
      candidate = {
        candidate: e.candidate.candidate,
        sdpMid: e.candidate.sdpMid,
        sdpMLineIndex: e.candidate.sdpMLineIndex,
      };
    }
    channel.postMessage({ type: "icecandidate", candidate });
  });
  
   // add media tracks to the peer connection
  local.srcObject.getTracks()
    .forEach(track => peerConnection.addTrack(track, local.srcObject));
    
  // Create offer and send through the browser channel
  peerConnection.createOffer({ offerToReceiveAudio: true, offerToReceiveVideo: true })
    .then(async offer => {
      await peerConnection.setLocalDescription(offer);
      console.log("Created offer, sending...");
      channel.postMessage({ type: "offer", sdp: offer.sdp });
    });
}

函數(shù)中有兩個區(qū)域與BrowserChannel對象交互。第一個是ICE候選事件偵聽器:

peerConnection.addEventListener("icecandidate", e => {
  let candidate = null;
  
  // prepare a candidate object that can be passed through browser channel
  if (e.candidate !== null) {
    candidate = {
      candidate: e.candidate.candidate,
      sdpMid: e.candidate.sdpMid,
      sdpMLineIndex: e.candidate.sdpMLineIndex,
    };
  }
  channel.postMessage({ type: "icecandidate", candidate });
});

另一種是生成offer后:

peerConnection.createOffer({ offerToReceiveAudio: true, offerToReceiveVideo: true })
  .then(async offer => {
    await peerConnection.setLocalDescription(offer);
    console.log("Created offer, sending...");
    channel.postMessage({ type: "offer", sdp: offer.sdp });
  });

讓我們先看看ICE候選事件偵聽器。如果您將e.candidate對象直接傳遞給BroadcastChannel對象,您將在控制臺中收到DataCloneError: object can not be cloned錯誤消息。

發(fā)生此錯誤是因為BroadcastChannel對象無法直接處理e.candidate。您需要從e.candidate創(chuàng)建一個包含所需詳細(xì)信息的對象以發(fā)送到BroadcastChannel對象。我們必須做同樣的事情來發(fā)送offer。

您需要調(diào)用channel.postMessage方法向BroadcastChannel對象發(fā)送消息。調(diào)用此消息時,另一個網(wǎng)頁上的BroadcastChannel對象會觸發(fā)其onmessage事件偵聽器。從index.html頁面查看此代碼:

channel.onmessage = e => {
  if (e.data.type === "icecandidate") {
    peerConnection?.addIceCandidate(e.data.candidate)
  } else if (e.data.type === "offer") {
    console.log("Received offer")
    handleOffer(e.data)
  }
}

如您所見,我們有條件語句檢查進(jìn)入BroadcastChannel對象的消息類型。消息的內(nèi)容可以通過e.data讀取。e.data.type對應(yīng)于我們通過channel.postMessage發(fā)送的對象的類型字段:

// from the ICE candidate event listener
channel.postMessage({ type: "icecandidate", candidate });

// from generating an offer
channel.postMessage({ type: "offer", sdp: offer.sdp });

現(xiàn)在,讓我們看一下處理收到的offer的index.html文件。

index.html文件的詳細(xì)分解

index.html文件以handleOffer函數(shù)開頭:

function handleOffer(offer) {
  const config = {};
  peerConnection = new RTCPeerConnection(config);
  peerConnection.addEventListener("track", e => remote.srcObject = e.streams[0]);
  peerConnection.addEventListener("icecandidate", e => {
    let candidate = null;
    if (e.candidate !== null) {
      candidate = {
        candidate: e.candidate.candidate,
        sdpMid: e.candidate.sdpMid,
        sdpMLineIndex: e.candidate.sdpMLineIndex,
      }
    }
    channel.postMessage({ type: "icecandidate", candidate })
  });
  peerConnection.setRemoteDescription(offer)
    .then(() => peerConnection.createAnswer())
    .then(async answer => {
      await peerConnection.setLocalDescription(answer);
      console.log("Created answer, sending...")
      channel.postMessage({
        type: "answer",
        sdp: answer.sdp,
      });
    });
}

當(dāng)觸發(fā)時,此方法創(chuàng)建對等連接并將其生成的任何ICE候選發(fā)送給另一個對等。然后,繼續(xù)握手過程,將流媒體的offer設(shè)置為其遠(yuǎn)程描述,生成answer,將該answer設(shè)置為其本地描述,并使用BroadcastChannel對象將該answer發(fā)送給流媒體。

與index.html文件中的BroadcastChannel對象一樣,streamer.html文件中的BroadcastChannel對象需要一個onmessage事件偵聽器來接收ICE候選者并從index.html文件中回答:

channel.onmessage = e => {
  if (e.data.type === "icecandidate") {
    peerConnection?.addIceCandidate(e.data.candidate);
  } else if (e.data.type === "answer") {
    console.log("Received answer")
    peerConnection?.setRemoteDescription(e.data);
  }
}

如果您想知道為什么問號?在peerConnection之后,它告訴JavaScript運(yùn)行時在peerConnection未定義時不要拋出錯誤。這在某種程度上是一個簡寫:

if (peerConnection) {
  peerConnection.setRemoteDescription(e.data);
}

用我們的Signal服務(wù)器替換BroadcastChannel

BroadcastChannel僅限于瀏覽器上下文。在這一步中,我們將通過使用一個簡單的Signal服務(wù)器來克服這個限制,我們將使用Node. js構(gòu)建它。與前面的步驟一樣,我將首先給您粘貼的代碼,然后解釋其中發(fā)生了什么。

那么,讓我們開始吧。這一步需要四個文件:index.html、streamer.html、signalserverclass.js和server/index.js。

我們將從signalserverclass.js文件開始:

class SignalServer {
  constructor(channel) {    
    this.socket = new WebSocket("ws://localhost:80");
    this.socket.addEventListener("open", () => {
      this.postMessage({ type: "join-channel", channel });
    });
    this.socket.addEventListener("message", (e) => {
      const object = JSON.parse(e.data);
      if (object.type === "connection-established") console.log("connection established");
      else if (object.type === "joined-channel") console.log("Joined channel: " + object.channel);
      else this.onmessage({ data: object });
    });
  }
  
  onmessage(e) {}
  postMessage(data) {
    this.socket.send( JSON.stringify(data) );
  }
}

接下來,讓我們更新index.html和streamer.html文件。對這些文件的唯一更改是我們初始化BroadcastChannel對象和導(dǎo)入signalserverclass.js腳本的腳本標(biāo)記。

這是更新的index.html文件:

<body>
  <video id="remote" controls></video>
  
  <script src="signalserverclass.js"></script>          <!-- new change -->
  <script>
    const remote = document.querySelector("video#remote");
    let peerConnection;
    const channel = new SignalServer("stream-video");     // <- new change
    channel.onmessage = e => {
      if (e.data.type === "icecandidate") {
        peerConnection?.addIceCandidate(e.data.candidate);
      } else if (e.data.type === "offer") {
        console.log("Received offer");
        handleOffer(e.data);
      }
    }function handleOffer(offer) {
      const config = {};
      peerConnection = new RTCPeerConnection(config);
      peerConnection.addEventListener("track", e => remote.srcObject = e.streams[0]);
      peerConnection.addEventListener("icecandidate", e => {
        let candidate = null;
        if (e.candidate !== null) {
          candidate = {
            candidate: e.candidate.candidate,
            sdpMid: e.candidate.sdpMid,
            sdpMLineIndex: e.candidate.sdpMLineIndex,
          };
        }
        channel.postMessage({ type: "icecandidate", candidate });
      });
      peerConnection.setRemoteDescription(offer)
        .then(() => peerConnection.createAnswer())
        .then(async answer => {
          await peerConnection.setLocalDescription(answer);
          console.log("Created answer, sending...");
          channel.postMessage({
            type: "answer",
            sdp: answer.sdp,
          });
        });
    }
  </script>
</body>

這是更新后的streamer.html文件:

<body>
  <video id="local" autoplay muted></video>
  <button onclick="start(this)">start video</button>
  <button id="stream" onclick="stream(this)" disabled>stream video</button>
  <script src="signalserverclass.js"></script>         <!-- new change -->
  <script>
    const local = document.querySelector("video#local");
    let peerConnection;

    const channel = new SignalServer("stream-video");     // <- new change
    channel.onmessage = e => {
      if (e.data.type === "icecandidate") {
        peerConnection?.addIceCandidate(e.data.candidate);
      } else if (e.data.type === "answer") {
        console.log("Received answer");
        peerConnection?.setRemoteDescription(e.data);
      }
    }

    // function to ask for camera and microphone permission
    // and stream to #local video element
    function start(e) {
      e.disabled = true;
      document.getElementById("stream").disabled = false;  // enable the stream button
      navigator.mediaDevices.getUserMedia({ audio: true, video: true })
        .then((stream) => local.srcObject = stream);
    }

    function stream(e) {
      e.disabled = true;
      
      const config = {};
      peerConnection = new RTCPeerConnection(config);  // local peer connection
      peerConnection.addEventListener("icecandidate", e => {
        let candidate = null;
        if (e.candidate !== null) {
          candidate = {
            candidate: e.candidate.candidate,
            sdpMid: e.candidate.sdpMid,
            sdpMLineIndex: e.candidate.sdpMLineIndex,
          };
        }
        channel.postMessage({ type: "icecandidate", candidate });
      });
      local.srcObject.getTracks()
        .forEach(track => peerConnection.addTrack(track, local.srcObject));
        
      peerConnection.createOffer({ offerToReceiveAudio: true, offerToReceiveVideo: true })
        .then(async offer => {
          await peerConnection.setLocalDescription(offer);
          console.log("Created offer, sending...");
          channel.postMessage({ type: "offer", sdp: offer.sdp });
        });
    }
  </script>
</body>

最后,這是server/index.js文件的內(nèi)容:

const { WebSocketServer } = require("ws");

const channels = {};
const server = new WebSocketServer({ port: 80 });
server.on("connection", handleConnection);

function handleConnection(ws) {
  console.log('New connection');
  ws.send( JSON.stringify({ type: 'connection-established' }) );
  
  let id;
  let channel = "";
  ws.on("error", () => console.log('websocket error'));
  ws.on('message', message => {
    const object = JSON.parse(message);
    
    if (object.type === "join-channel") {
      channel = object.channel;
      if (channels[channel] === undefined) channels[channel] = [];
      id = channels[channel].length || 0;
      channels[channel].push(ws);
      ws.send(JSON.stringify({type: 'joined-channel', channel}));
    } else {
      // forward the message to other channel memebers
      channels[channel]?.filter((_, i) => i !== id).forEach((member) => {
        member.send(message.toString());
      });
    }
  });
  ws.on('close', () => {
    console.log('Client has disconnected!');
    if (channel !== "") {
      channels[channel] = channels[channel].filter((_, i) => i !== id);
    }
  });
}

圖片圖片

要讓服務(wù)器運(yùn)行,您需要在終端中打開server文件夾,將該文件夾初始化為Node項目,安裝ws包,然后運(yùn)行index.js文件。這些步驟可以使用以下命令完成:

# initialize the project directory
npm init --y

# install the `ws` package
npm install ws

# run the `index.js` file
node index.js

現(xiàn)在,讓我們看看文件。為了減少在將BroadcastChannel對象構(gòu)造函數(shù)與SignalServer構(gòu)造函數(shù)交換后編輯代碼的需要,我嘗試讓SignalServer類模仿您使用BroadcastChannel所做的調(diào)用和事情-至少對于我們的用例:

class SignalServer {
  constructor(channel) {    
    // what the constructor does
  }
  
  onmessage(e) {}
  postMessage(data) {
    // what postMessage does
  }
}

此類有一個在初始化時加入通道的構(gòu)造函數(shù)。它還有一個postMessage函數(shù)來允許發(fā)送消息和一個onmessage方法,當(dāng)從另一個SignalServer對象接收到消息時調(diào)用該方法。

SignalServer類的另一個目的是抽象我們的后端進(jìn)程。我們的信號服務(wù)器是一個WebSocket服務(wù)器,因為它允許我們在服務(wù)器和客戶端之間進(jìn)行基于事件的雙向通信,這使得它成為構(gòu)建信號服務(wù)器的首選。

SignalServer類從其構(gòu)造函數(shù)開始其操作:

constructor(channel) {    
  this.socket = new WebSocket("ws://localhost:80");
  this.socket.addEventListener("open", () => {
    this.postMessage({ type: "join-channel", channel });
  });
  this.socket.addEventListener("message", (e) => {
    const object = JSON.parse(e.data);
    if (object.type === "connection-established") console.log("connection established");
    else if (object.type === "joined-channel") console.log("Joined channel: " + object.channel);
    else this.onmessage({ data: object });
  });
}

它首先初始化與后端的連接。當(dāng)連接變?yōu)榛顒訝顟B(tài)時,它會向服務(wù)器發(fā)送一個我們用作join-channel請求的對象:

this.socket.addEventListener("open", () => {
  this.postMessage({ type: "join-channel", channel });
});

現(xiàn)在,讓我們看一下我們的WebSocket服務(wù)器:

const { WebSocketServer } = require("ws");

const channels = {};
const server = new WebSocketServer({ port: 80 });
server.on("connection", handleConnection);

function handleConnection(ws) {
  // I cut out the details because it's not in focus right now
}

這是一個非常標(biāo)準(zhǔn)的WebSocket服務(wù)器。我們有服務(wù)器初始化和事件偵聽器,用于新客戶端連接到服務(wù)器時。唯一的新功能是channels變量,我們使用它來存儲每個SignalServer對象加入的通道。

如果一個通道不存在并且一個對象想要加入該通道,我們希望服務(wù)器創(chuàng)建一個空數(shù)組,其中WebSocket連接作為第一個元素。然后,我們將該數(shù)組存儲為channels對象中帶有通道名稱的字段。

您可以在下面的message事件偵聽器中看到這一點。代碼看起來有點復(fù)雜,但上面的解釋是對代碼作用的一般概述:

// ... first rest of the code
ws.on('message', message => {
  const object = JSON.parse(message);
  
  if (object.type === "join-channel") {
    channel = object.channel;
    if (channels[channel] === undefined) channels[channel] = [];
    id = channels[channel].length || 0;
    channels[channel].push(ws);
    ws.send(JSON.stringify({type: 'joined-channel', channel}));
// ... other rest of the code

之后,事件偵聽器向SignalServer對象發(fā)送joined-channel消息,告訴它加入通道的請求成功。

至于事件偵聽器的其余部分,它將任何不是join-channel類型的消息發(fā)送到通道中的其他SignalServer對象:

// rest of the event listener
  } else {
    // forward the message to other channel memebers
    channels[channel]?.filter((_, i) => i !== id).forEach((member) => {
      member.send(message.toString());
    });
  }
});

在handleConnection函數(shù)中,id和channel變量分別存儲SignalServer objectWebSocket連接在通道中的位置和SignalServer objectWebSocket連接存儲在其中的通道名稱:

let id;
let channel = ""; 讓id;讓頻道="";

這些變量是在SignalServer對象加入通道時設(shè)置的。它們有助于將來自一個SignalServer對象的消息傳遞給通道中的其他對象,正如您在else塊中看到的那樣。當(dāng)SignalServer對象因任何原因斷開連接時,它們也有助于從通道中刪除它們:

ws.on('close', () => {
  console.log('Client has disconnected!');
  if (channel !== "") {
    channels[channel] = channels[channel].filter((_, i) => i !== id);
  }
});

最后,回到signalserverclass.js文件中的SignalServer類。讓我們看一下從WebSocket服務(wù)器接收消息的部分:

this.socket.addEventListener("message", (e) => {
  const object = JSON.parse(e.data);
  if (object.type === "connection-established") console.log("connection established");
  else if (object.type === "joined-channel") console.log("Joined channel: " + object.channel);
  else this.onmessage({ data: object });
});

如果您查看WebSocket服務(wù)器的handleConnection函數(shù),服務(wù)器直接發(fā)送到SignalServer對象的消息類型有兩種:joined-channel和connection-established。這兩種消息類型由此事件偵聽器直接處理。

結(jié)論

在本文中,我們介紹了如何使用WebRTC構(gòu)建P2P視頻流應(yīng)用程序——它的主要用例之一。

我們從在單個頁面中創(chuàng)建對等連接開始,以便簡單了解WebRTC應(yīng)用程序是如何工作的,而無需擔(dān)心信令。然后,我們談到了使用廣播頻道API進(jìn)行信令。最后,我們構(gòu)建了自己的singal服務(wù)器。

關(guān)注我,變得更強(qiáng)。

原文:https://blog.logrocket.com/webrtc-video-streaming/

作者:Oduah Chigozie

責(zé)任編輯:武曉燕 來源: 宇宙一碼平川
相關(guān)推薦

2012-12-10 09:46:21

P2P云存儲Symform

2010-03-10 10:51:30

2010-03-22 15:27:40

云計算

2010-07-13 14:41:14

2020-03-05 20:30:15

Syncthing文件同步工具開源

2022-07-19 16:59:04

流媒體傳輸IPC物聯(lián)網(wǎng)

2010-10-29 09:43:50

Wi-Fi DirecWi-Fi聯(lián)

2018-08-16 07:29:02

2015-04-27 11:49:23

2010-06-28 11:15:45

BitTorrent協(xié)

2012-09-25 13:47:43

C#網(wǎng)絡(luò)協(xié)議P2P

2010-07-07 10:31:45

2013-03-13 09:24:56

2013-12-12 13:46:40

大數(shù)據(jù)金融P2P大數(shù)據(jù)

2015-04-27 14:29:53

C#UDP實現(xiàn)P2P語音聊天工具

2021-09-02 19:45:21

P2P互聯(lián)網(wǎng)加速

2009-01-08 09:52:00

2009-05-18 09:11:00

IPTV融合寬帶

2009-07-22 15:52:01

2011-11-29 09:48:43

點贊
收藏

51CTO技術(shù)棧公眾號