WebRTC基礎使用

平平丶淡淡發表於2024-03-20

一、什麼是WebRTC

WebRTC(Web Real-Time Communication)是一個由Google、Mozilla、Opera等公司發起的開源專案,它支援網頁瀏覽器進行實時音影片對話。它允許網路應用或者站點,在不借助中間媒介的情況下,建立瀏覽器之間點對點(Peer-to-Peer)的連線,實現影片流和音訊流或者其他任意資料的傳輸。對於開發者而言,WebRTC提供了一套W3C Javascript API,包括音影片的採集、編解碼、網路傳輸、顯示等功能,使得開發者能夠快速構建出音影片應用。WebRTC標準在較高層面上涵蓋了兩種不同的技術:媒體捕獲和點對點連線。

二、媒體捕獲

媒體捕獲裝置包括攝像頭和麥克風,還包括螢幕捕獲裝置。可以利用navigator 這個瀏覽器內建物件身上的API來獲取媒體流。

  • navigator.mediaDevices.getUserMedia():獲取攝像頭和麥克風媒體流
  • navigator.mediaDevices.getDisplayMedia():獲取螢幕錄製的媒體流

API的具體使用可以參考 MDN。從技術手冊可以知道,getUserMedia()方法需要傳入一個constraints約束物件作為引數。該引數用來指定請求的媒體型別和相對應的引數,而且必須指定至少一個型別。媒體型別具體可以配置哪些屬性,可以使用navigator.mediaDevices.getSupportedConstraints()來獲取。

image-20240320171940086

2.1 獲取媒體流

// 指定約束物件,獲取音訊流和影片流,並設定影片流的解析度為 1280 x 720
const constraints = {
  audio: true,
  video: {
    with: 1280,
    height: 720,
  },
};
// 獲取媒體流
const stream = navigator.mediaDevices.getUserMedia(constraints);
console.log(stream);

執行上面的程式碼,瀏覽器會詢問是否開啟攝像頭和麥克風,設定為允許就能獲取媒體流了。

image-20240320172630472

開啟控制檯,發現getUserMedia()方法返回的是一個Promise。

image-20240320172754557

2.2 展示畫面

將獲取到的媒體流資料,繫結到一個video標籤的srcObject屬性上,就能透過video標籤看到影片畫面了。

這裡有兩個地方需要注意:

  • getUserMedia()方法返回的是Promise,所以需要在then方法中去繫結video標籤的srcObject屬性,或者使用async await的方式去獲取Promise的值
  • video標籤需要設定autoplay屬性,否則影片沒辦法自動播放
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>webrtc</title>
  <style>
    .play-content {
      width: 300px;
      height: 300px;
    }
  </style>
</head>

<body>
  <video class="play-content" autoplay></video>
  <script src="./webrtc01.js"></script>
</body>

</html>
// 指定約束物件,獲取音訊流和影片流,並設定影片流的解析度為 1280 x 720
const constraints = {
  audio: true,
  video: {
    with: 1280,
    height: 720,
  },
};

// 獲取媒體流
navigator.mediaDevices.getUserMedia(constraints).then(value => {
  // 將媒體流與video標籤繫結
  document.querySelector(".play-content").srcObject = value
})

執行上面的程式碼,就能看到影片畫面了

image-20240320173549039

2.3 捕獲螢幕

上面使用了getUserMedia()這個API來獲取媒體流。接下來使用getDisplayMedia()這個API來捕獲螢幕。使用方法和getUserMedia()一致

// 指定約束物件,獲取音訊流和影片流,並設定影片流的解析度為 1280 x 720
const constraints = {
  audio: true,
  video: {
    with: 1280,
    height: 720,
  },
};
// 獲取螢幕的媒體流,並與video標籤繫結
navigator.mediaDevices.getDisplayMedia(constraints).then((value) => {
  document.querySelector(".play-content").srcObject = value;
});

執行上面的程式碼,瀏覽器會讓你選擇一個要開啟的視窗。隨便選擇一個,就能獲取到媒體流了。

image-20240320173811082 image-20240320173910335

三、點對點連線

透過getUserMedia()getDisplayMedia()這兩個API已經可以獲取到媒體流了,那現在就需要考慮如何將媒體流傳送給遠端的瀏覽器。WebRTC要建立點對點的連線,首先有兩個關鍵的問題要解決:

  • 媒體協商:

    媒體協商主要關注通訊雙方支援的編解碼器及其引數。這包括音訊引數(如取樣率、取樣大小、通道數)和影片引數(如解析度、幀率等)。媒體協商的目的是確保雙方能夠交換和理解彼此的音影片流。這通常透過Session Description Protocol(SDP)進行。

  • 網路協商:

    網路協商則主要關注在網際網路上建立可通訊的鏈路。由於大多數裝置沒有獨立的公網IP,因此需要進行網路地址轉換(NAT)。WebRTC使用STUN(Session Traversal Utilities for NAT)和TURN(Traversal Using Relays around NAT)協議來幫助完成這一操作。STUN協議用於發現公共IP地址和UDP埠號,而TURN協議則用於在直接通訊不可行時提供中繼服務。網路協商的目的是找到一條可靠的網路路徑,使得通訊雙方能夠建立連線並進行實時通訊。

也就是說,要確保WebRTC成功通訊,需要透過媒體協商來確定雙方都可用的編解碼方式,還需要透過網路協商確定兩端可建立正確通訊鏈路的Ip地址。下面是WebRTC建立通訊的圖解,針對裡面的過程進行逐一講解

1c28bc75134bd14146e8767e86fbd322

3.1 RTCPeerConnection

具體介紹檢視MDN

RTCPeerConnection是WebRTC(Web實時通訊)API的一部分,它用於在瀏覽器之間建立點對點(peer-to-peer)連線,以支援實時通訊,如音訊、影片和資料交換。RTCPeerConnection提供了建立和管理這些連線所需的底層機制。

根據上面的圖解,可以知道WebRTC建立通訊的流程大致有以下過程:

  1. 客戶端1建立RTCPeerConnection物件,並將本地媒體流透過addTrack方法新增到物件身上
  2. 透過createOffer方法建立本地SDP描述,並透過RTCPeerConnection物件的setLocalDescription方法設定本地描述
  3. 將SDP描述資訊透過信令伺服器轉發給客戶端2
  4. 客戶端2收到發來的SDP描述,建立RTCPeerConnection物件,並透過setRemoteDescription方法設定遠端描述
  5. 客戶端2透過createAnswer方法建立本地的SDP描述,並透過 setLocalDescription方法設定本地描述
  6. 客戶端2將Answer SDP透過信令伺服器轉發給客戶端1
  7. 客戶端1收到發來的Answer SDP,透過setRemoteDescription方法設定遠端描述

下面根據以上流程,建立一個WebRtcClient類。該類具有繫結本地媒體流、建立offer、建立answer、接收answer等方法。

在獲取本地流時,我獲取的是螢幕捕獲的流,這樣方便透過兩個不一樣的視窗看到效果。

class WebRtcClient {
  // 初始化 RTCPeerConnection 物件
  constructor() {
    this.pc = new RTCPeerConnection();
    this.offer = "";
    this.answer = "";
  }

  // 獲取本地媒體流,並繫結到本地 video標籤
  async init() {
    const localVideoBox = document.querySelector(".local");
    const remoteVideoBox = document.querySelector(".remote");
    // 設定約束
    const constraints = {
      audio: true,
      video: true,
    };
    // 獲取影片流 (切換選擇使用攝像頭還是螢幕)
    // const stream = await navigator.mediaDevices.getUserMedia(constraints);
    const stream = await navigator.mediaDevices.getDisplayMedia(constraints);
    localVideoBox.srcObject = stream;

    // 將本地媒體流軌道新增到  RTCPeerConnection物件 中
    stream.getTracks().forEach((track) => {
      this.pc.addTrack(track, stream);
    });

    // 監聽遠端流變化
    this.pc.ontrack = (e) => {
      console.log("遠端流變化了", e);
      remoteVideoBox.srcObject = e.streams[0];
    };
  }

  // 建立offer
  async createOffer() {
    // 存在新的候選,需要更新SDP資訊
    this.pc.onicecandidate = async (e) => {
      if (e.candidate) {
        this.offer = this.pc.localDescription;
      }
    };
    // 建立offer, 並設定為本地SDP描述
    const offer = await this.pc.createOffer();
    await this.pc.setLocalDescription(offer);
  }

  // 建立answer
  async createAnswer(offer) {
    // 存在新的候選,需要更新SDP資訊
    this.pc.onicecandidate = async (e) => {
      if (e.candidate) {
        this.answer = this.pc.localDescription;
      }
    };
    // 收到對端傳送的offerSDP,設定為遠端SDP
    await this.pc.setRemoteDescription(offer);
    // 建立answer,設定為本地SDP
    const answer = await this.pc.createAnswer();
    await this.pc.setLocalDescription(answer);
  }

  // 新增answerSDP
  async addAnswer(answer) {
    await this.pc.setRemoteDescription(answer);
  }
}

const client = new WebRtcClient();
client.init();
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <style>
    .local,
    .remote {
      width: 300px;
      height: 300px;
    }
  </style>
</head>

<body>
  <video class="local" autoplay></video>
  <video class="remote" autoplay></video>
  <script src="./webrtcClient.js"></script>
</body>

</html>

下面一步一步操作,實現通訊

  1. 開啟兩個瀏覽器頁面,分別共享不同的畫面

    image-20240315180149695

  2. 左邊瀏覽器中呼叫client物件的createOffer方法

    image-20240315180403818

  3. 複製左邊client物件的offerSDP,呼叫右邊client物件的createAnswer方法,傳入offerSDP

    直接右鍵複製,實際應該使用信令伺服器進行轉發

    image-20240315180641383

    呼叫右邊client物件的createAnswer方法,傳入offerSDP

    image-20240315180758176

  4. 複製右邊client物件的answerSDP,呼叫左邊client物件的addAnswer方法,並傳入answerSDP

    image-20240315181100723

    image-20240315181132629

    然後,兩個瀏覽器頁面就可以進行視訊通話了

    image-20240315192801153

上面的SDP交換過程,是手動進行的,主要是為了瞭解整個通訊流程。後面可以建立一個信令伺服器,透過WebSocket實現SDP的交換。