如何运用 Web Components 实现 WebRTC 客户端?

前言

随着 Web 技术的日益成熟,WebRTC 技术也逐渐走入人们的视野。WebRTC 技术是浏览器本身提供的一种实现互联网实时通信的技术,可以用于视频会议、音频通话、实时数据传输等场景。

为了更好地使用 WebRTC 技术,我们可以借助 Web Components 技术来实现一个 WebRTC 客户端,以达到更加灵活和可复用的效果。本篇文章将详细介绍如何利用 Web Components 技术实现 WebRTC 客户端。

WebRTC 概述

WebRTC 技术是一种能够在浏览器上实现点对点通信的技术,它包含了三个部分:

  • 媒体捕获:摄像头、麦克风等设备获取媒体流;
  • 媒体传输:将媒体流传输至远程设备;
  • 媒体处理:对传输过来的媒体流进行处理和展示。

以上三个部分都可以通过 WebRTC 技术来实现。不过本文主要介绍如何实现媒体捕获和传输两个部分。

Web Components 概述

Web Components 技术是用来创建可复用、可组合的组件的技术,相当于是一种浏览器原生支持的组件化编程的实现方案。Web Components 技术包含以下四个部分:

  • Custom Elements:允许开发者创建定制的 HTML 元素;
  • Shadow DOM:允许开发者创建隔离的 DOM 子树;
  • HTML Templates:允许开发者定义 HTML 模板;
  • HTML Imports:允许开发者引入 HTML 片段到当前文档中。

本文将会利用其中的 Custom Elements 和 Shadow DOM 来创建一个 WebRTC 客户端的组件。

实现过程

实现思路

根据 WebRTC 向远程设备传输媒体流的流程,大致可以分为以下几个步骤:

  1. 获取本地设备的媒体流(如摄像头、麦克风等);
  2. 将媒体流传输给远程设备;
  3. 接收远程设备传输过来的媒体流;
  4. 展示接收到的媒体流。

因此,我们需要实现以下四个功能:

  • 获取媒体流;
  • 传输媒体流;
  • 接收媒体流;
  • 展示媒体流。

创建 Custom Elements

首先我们需要创建一个用来展示视频的组件,可以通过 Custom Elements 来实现。

<video-player></video-player>

在 JavaScript 中定义上述的自定义元素:

class VideoPlayer extends HTMLElement {
  // ...
}

customElements.define('video-player', VideoPlayer);

在上述 class 中我们可以定义一些需要用到的属性和方法:

class VideoPlayer extends HTMLElement {
  constructor() {
    // 当我们自定义元素被创建时会执行此方法。
    // 在此方法中可以对自定义元素进行初始化工作。
    super(); // 调用父类构造函数。

    // 注册 Shadow DOM。
    const shadowRoot = this.attachShadow({ mode: 'open' });

    // 创建 video 元素。
    const video = document.createElement('video');
    video.autoplay = true;
    video.playsinline = true;
    video.style.width = '100%';
    video.style.height = '100%';

    // 将 video 元素添加到 Shadow DOM 中。
    shadowRoot.appendChild(video);

    this.video = video;
  }

  // ...
}

到目前为止,我们已经创建了一个用来展示视频的组件,接下来我们需要实现媒体流的相关功能。

获取媒体流

首先我们需要获取本地设备的媒体流:

class VideoPlayer extends HTMLElement {
  async getMediaStream() {
    const constraints = {
      audio: true,
      video: true,
    };

    let mediaStream;
    try {
      mediaStream = await navigator.mediaDevices.getUserMedia(constraints);
    } catch (err) {
      console.error(err);
    }
    
    return mediaStream;
  }
  
  async startCapturing() {
    this.mediaStream = await this.getMediaStream();
    this.video.srcObject = this.mediaStream;
  }
  
  async stopCapturing() {
    this.mediaStream.getTracks().forEach(track => track.stop());
    this.mediaStream = undefined;
    this.video.srcObject = null;
  }

  // ...
}

getMediaStream 方法中,我们使用 navigator.mediaDevices.getUserMedia 获取媒体流,这里设定了 audiovideo 两个约束条件,代表需要获取音频和视频两个媒体流。

startCapturing 方法中,我们获取本地媒体流,并将它绑定到 video 元素上之后展示出来。

stopCapturing 方法中,我们停止捕获媒体流并清空 video 元素的 srcObject 属性。

传输媒体流

接下来我们需要将媒体流传输给远程设备。这里我们可以用 WebSocket 技术来实现。

class VideoPlayer extends HTMLElement {
  constructor() {
    super();
    
    // ...
    
    this.socket = new WebSocket('ws://localhost:8080');
    this.socket.onmessage = event => {
      const data = JSON.parse(event.data);

      if (data.type === 'offer') {
        this.handleOffer(data.offer);
      } else if (data.type === 'answer') {
        this.handleAnswer(data.answer);
      } else if (data.type === 'candidate') {
        this.handleCandidate(data.candidate);
      }
    };
  }

  async sendOffer() {
    this.peerConnection = new RTCPeerConnection();
    this.mediaStream.getTracks().forEach(track => {
      this.peerConnection.addTrack(track);
    });

    const offer = await this.peerConnection.createOffer();
    await this.peerConnection.setLocalDescription(offer);

    this.socket.send(JSON.stringify({
      type: 'offer',
      offer: offer.toJSON(),
    }));
  }

  async sendAnswer() {
    const answer = await this.peerConnection.createAnswer();
    await this.peerConnection.setLocalDescription(answer);

    this.socket.send(JSON.stringify({
      type: 'answer',
      answer: answer.toJSON(),
    }));
  }

  handleOffer(offerJson) {
    const offer = new RTCSessionDescription(offerJson);
    this.peerConnection = new RTCPeerConnection();

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

    this.peerConnection.setRemoteDescription(offer);
    this.sendAnswer();
  }

  handleAnswer(answerJson) {
    const answer = new RTCSessionDescription(answerJson);
    this.peerConnection.setRemoteDescription(answer);
  }

  handleCandidate(candidate) {
    const rtcCandidate = new RTCIceCandidate(candidate);
    this.peerConnection.addIceCandidate(rtcCandidate);
  }
  
  // ...
}

在上述代码中,我们定义了以下一些方法:

  • sendOffer:创建 Peer Connection 并向远程设备发送 offer;
  • sendAnswer:创建 answer 并发送给远程设备;
  • handleOffer:将收到的 offer 设置为远程描述并发送 answer;
  • handleAnswer:将远程设备的 answer 设置为本地描述;
  • handleCandidate:将收到的 candidate 添加到 Peer Connection 中。

此处仅展示了通过 WebSocket 技术传输媒体流的部分,具体接入和后台开发根据实际情况进行修改。

展示媒体流

最终,我们需要实现展示收到的媒体流的功能。

class VideoPlayer extends HTMLElement {
  // ...
  
  async startReceiving() {
    this.peerConnection = new RTCPeerConnection();
    const stream = new MediaStream();

    this.peerConnection.ontrack = event => {
      stream.addTrack(event.track);
    };

    await this.peerConnection.setRemoteDescription(this.remoteDescription);
    await this.peerConnection.setLocalDescription(await this.peerConnection.createAnswer());

    this.socket.send(JSON.stringify({
      type: 'answer',
      answer: this.peerConnection.localDescription.toJSON(),
    }));

    this.video.srcObject = stream;
  }

  // ...
}

对于收到的媒体流,我们首先需要创建一个新的 MediaStream 对象,之后将事件中的媒体轨添加到该对象,并将该对象绑定到 video 元素上即可完成展示的功能。

整合代码

将上述代码整合在一起,我们就可以实现一个完整的 WebRTC 客户端了。

class VideoPlayer extends HTMLElement {
  constructor() {
    super();

    // 注册 Shadow DOM。
    const shadowRoot = this.attachShadow({ mode: 'open' });

    // 创建 video 元素。
    const video = document.createElement('video');
    video.autoplay = true;
    video.playsinline = true;
    video.style.width = '100%';
    video.style.height = '100%';

    // 将 video 元素添加到 Shadow DOM 中。
    shadowRoot.appendChild(video);

    this.video = video;
    this.socket = new WebSocket('ws://localhost:8080');
    this.socket.onmessage = event => {
      const data = JSON.parse(event.data);

      if (data.type === 'offer') {
        this.handleOffer(data.offer);
      } else if (data.type === 'answer') {
        this.handleAnswer(data.answer);
      } else if (data.type === 'candidate') {
        this.handleCandidate(data.candidate);
      }
    };
  }

  async getMediaStream() {...}

  async startCapturing() {...}

  async stopCapturing() {...}

  async sendOffer() {...}

  async sendAnswer() {...}

  handleOffer(offerJson) {...}

  handleAnswer(answerJson) {...}

  handleCandidate(candidate) {...}

  async startReceiving() {...}
}

customElements.define('video-player', VideoPlayer);

使用示例

在 HTML 中使用我们所实现的 video-player 组件:

<!doctype html>
<html>
<head>
  <meta charset="utf-8">
  <title>WebRTC Client</title>
</head>
<body>
  <video-player></video-player>
  
  <button id="start-btn">Start</button>
  <button id="stop-btn">Stop</button>

  <script>
    const videoPlayer = document.querySelector('video-player');
    
    async function start() {
      await videoPlayer.startCapturing();
      await videoPlayer.sendOffer();
    }
  
    async function stop() {
      await videoPlayer.stopCapturing();
      await videoPlayer.sendCandidates();
    }

    document.getElementById('start-btn').addEventListener('click', start);
    document.getElementById('stop-btn').addEventListener('click', stop);
  </script>
</body>
</html>

在 JavaScript 中,首先获取到 video-player 元素,调用其 startCapturing 方法可以启动媒体流捕获,sendOffer 方法可以通过 WebSocket 技术将本地媒体流的描述传输给远程设备。对于停止媒体捕获,为了保证程序正常退出,我们需要在停止前发送所有的 candidates,具体实现可以自行添加。

总结

本文介绍了如何利用 Web Components 技术来实现 WebRTC 客户端的技术要点和实现过程。Web Components 技术能够帮助我们实现一些可复用、可组合的组件,将不同的功能分离出来,达到更好的可维护性和重用性。而 WebRTC 技术则可以实现浏览器上的实时通信功能,具有很广泛的应用场景。最终通过实现一个完整的 WebRTC 客户端来演示这些技术的应用。

来源:JavaScript中文网 ,转载请注明来源 本文地址:https://www.javascriptcn.com/post/65b7c84dadd4f0e0ff058546