WebRTC建立P2P连接和发送/接收媒体(webrtc入门三)

欢迎来到正在进行的 WebRTC 入门系列的第 3 部分,这一次我们终于可以发送和接收媒体了。在这一部分中,我们将回顾 RTCPeerConnection API,我们如何使用它建立一个与另一个对等体的连接。

首先我们需要了解一些术语。

建立一个P2P连接需要采取的步骤,简单来说就是以下几点:

  1. 呼叫者创建他们的 RTCPeerConnection 并创建一个 offer。
  2. 然后呼叫者使用创建的要约并设置他们的本地描述。
  3. 呼叫者将创建的要约发送给被呼叫者。
  4. 被叫者收到呼叫者的提议,创建他们的RTCPeerConnection(创建RTCPeerConnection可以在这一步之前完成),然后设置远程描述。
  5. 被叫者然后根据他们的远程描述创建一个回答。
  6. 然后,被叫者将他们的本地描述设置为答案,并将答案发送给呼叫者。
  7. 呼叫者在收到答案时,设置他们的远程描述。
  8. 如果一切顺利,一个P2P连接就建立了。

这些步骤一开始似乎很难掌握,但用得越多就越容易记住。

接下来我们将讨论 SDP。SDP 代表“会话描述协议”。每个 SDP 消息都由键/值对组成,它包含诸如此类的详细信息。

  • 可访问的 IP/端口
  • 要使用多少音频/视频轨道。
  • 客户端支持哪些音频/视频编解码器
  • 安全性(证书指纹)

接下来我们将看看 STUN/TURN。STUN 代表“NAT 会话遍历实用程序”。它只是为了与 NATS 一起工作而创建的。简而言之,它的目的是回答“我的 IP 地址是什么?”这个问题。TURN 代表“Traversal Using Relays around NAT.”,当无法使用 STUN(防火墙规则/阻塞端口等)时使用它。简单来说,TURN 将充当对等点之间的中间人,对等点 A 将将他们的媒体发送到 TURN 服务器,TURN 服务器会将其中继到对等点 B。如果你计划使用 TURN 服务器,请注意这会占用带宽。如果你计划在生产环境中托管应用程序,我建议你托管自己的 STUN/TURN 服务器,一个好的开源解决方案是“coturn”。

最后是 ICE。ICE 是 “互动连接建立 “的意思。基本上,ICE 收集了所有可用的候选者,如 IP 地址、中继地址等。然后通过 SDP 发送给远程对等体。

如果你对这部分比较了解,那么让我们最后开始编码吧!

首先打开 public_index.html 并粘贴输入以下内容:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8"/>
    <title>Simple P2P example</title>
  </head>

  <body>
    <h1>Simple P2P Example</h1>
    <hr />
    <button onclick="start();">Start</button><br/>
    <b>Local Id: <span id="localId"/></b><br/>
    <input type="text" id="callId" placeholder="Enter remote peer id"/>
    <button id="callButton" onclick="call();" disabled>Call</button>
    <button id="hangupButton" onclick="hangup();" disabled>Hang Up</button>
    <hr />

    <h3>Local Video</h3>
    <video id="localVideo" width="640" height="480" autoplay muted></video>

    <h3>Remote Video</h3>
    <video id="remoteVideo" width="640" height="480" autoplay></video>

    <script src="./main.js"></script>
  </body>
</html>

这是一个非常简单的页面,显示了对等点的本地和远程视频,单击开始按钮后会生成一个随机 ID 并显示给本地对等点,需要将此 ID 传递给远程对等点,以便他们可以调用。

现在 HTML 文件已经完成,接下来我们创建 JavaScript 文件。打开 public/main.js 开始编码。

首先我们需要启动/声明一些变量:

const localVideo = document.getElementById('localVideo');
const remoteVideo = document.getElementById('remoteVideo');
const callButton = document.getElementById('callButton');
const hangupButton = document.getElementById('hangupButton');
const socket = new WebSocket('wss://localhost:8888');

let peerConnection;
let localMediaStream;
let remoteId;
const remoteMediaStream = new MediaStream();

接下来我们监听 WebSocket 的 onopen 事件:

socket.onopen = () => {
  console.log('socket::open');
};

当与 WebSocket 服务器的连接建立时会触发,现在我们只是在向控制台打印。

接下来,我们需要监听来自WebSocket服务器的远程消息,我们用 “onmessage “来做这件事,这是一个相当大的块,但我很快就会讨论它。

socket.onmessage = async ({ data }) => {
  try {
    const jsonMessage = JSON.parse(data);

    console.log('action', jsonMessage.action);
    switch (jsonMessage.action) {
      case 'start':
        console.log('start', jsonMessage.id);
        callButton.disabled = false;

       document.getElementById('localId').innerHTML = jsonMessage.id;
        break;
      case 'offer':
        remoteId = jsonMessage.data.remoteId;
        delete jsonMessage.data.remoteId;

        await initializePeerConnection(localMediaStream.getTracks());
        await peerConnection.setRemoteDescription(new RTCSessionDescription(jsonMessage.data.offer));

        const answer = await peerConnection.createAnswer();
        await peerConnection.setLocalDescription(answer);

        sendSocketMessage('answer', { remoteId, answer }); 
        break;
      case 'answer':
        await peerConnection.setRemoteDescription(new RTCSessionDescription(jsonMessage.data.answer));
        break;
      case 'iceCandidate':
        await peerConnection.addIceCandidate(jsonMessage.data.candidate);
        break;
      default: console.warn('unknown action', jsonMessage.action);
    }
  } catch (error) {
    console.error('failed to handle socket message', error);
  }
};

在这里,我们从WebSocket服务器获得一个消息,为了知道如何处理该消息,我们将其解析为json,并根据 “行动 “来处理它。如果动作是“开始”,我们所做的就是显示对等点的本地 ID,可以将其传递给远程对等点以发起调用。如果操作是“offer”,我们设置 remoteId 变量并删除它,因为它不再需要(如果你愿意,可以跳过这部分)。之后我们创建被调用者的 RTCPeerConnection 并将远程描述设置为调用者的提议,然后我们根据提议创建一个答案并设置被调用者的本地描述,最后需要将答案发送回调用者以便他们可以设置他们的 RTCPeerConnection远程描述。如果动作是“回答”,就像上面解释的那样,我们只需要设置 RTCPeerConnection 的回答。如果动作是“iceCandidate”,我们只需将 ice candidate 添加到 RTCPeerConnection。任何其他的动作都是出乎意料的,所以如果它发生了,我们只需记录到控制台。

接下来我们将添加最后两个套接字监听器:

socket.onerror = (error) => {
  console.error('socket::error', error);
};

socket.onclose = () => {
  console.log('socket::close');
  stop();
};

“onerror”事件在 WebSocket 由于错误而关闭时发生,“onclose”在 WebSocket 连接已无错误关闭时触发。在这里我们停止 P2P 会话。

接下来我们编写辅助函数来将消息发送到 WebSocket 服务器。

const sendSocketMessage = (action, data) => {
  const message = { action, data };
  socket.send(JSON.stringify(message));
};

这个函数基本上接受一个动作字符串和一个数据对象,然后将对象作为字符串发送到服务器。

接下来我们需要编写“启动”函数:

const start = async () => {
  try {
    localMediaStream = await getLocalMediaStream(); 

    sendSocketMessage('start');
  } catch (error) {
    console.error('failed to start stream', error);
  }
};

这个函数只是初始化本地媒体流并向服务器发送消息以启动会话。

接下来我们创建“call”函数:

const call = async () => {
  try {
    remoteId = document.getElementById('callId').value;

    if (!remoteId) {
      alert('Please enter a remote id');

      return;
    }

    console.log('call: ', remoteId);
    await initializePeerConnection(localMediaStream.getTracks());
    const offer = await peerConnection.createOffer();
    await peerConnection.setLocalDescription(offer);
    sendSocketMessage('offer', { offer, remoteId });
  } catch (error) {
    console.error('failed to initialize call', error);
  }
};

在这里,我们根据输入获取远程节点的 ID,如果没有输入,我们只向用户显示警告。一旦我们有了一个 id,我们就可以开始提供/回答过程。首先我们创建并初始化 RTCPeerConnection,接下来我们创建一个报价并将其设置为 RTCPeerConnection 的本地描述。最后,我们需要将它发送给远程对等方,以便我们能够得到答复。

接下来,我们创建处理挂断和关闭的函数。

const hangup = () => socket.close();

const stop = () => {
  if (!localVideo.srcObject) return;

  for (const track of localVideo.srcObject.getTracks()) {
    track.stop();
  }

  peerConnection.close();
  callButton.disabled = true;
  hangupButton.disabled = true;
  localVideo.srcObject = undefined;
  remoteVideo.srcObject = undefined;
};

挂断基本上只是关闭套接字,以便触发套接字 onclose 事件。

停止像前一部分释放用户媒体一样,它也关闭 RTCPeerConnection 并释放视频对象 src 对象。

接下来我们需要创建函数来初始化本地媒体。

const getLocalMediaStream = async () => {
  try {
    const mediaStream = await navigator.mediaDevices.getUserMedia({ audio: true, video: true });
    console.log('got local media stream');

    localVideo.srcObject = mediaStream;

    return mediaStream;
  } catch (error) {
    console.error('failed to get local media stream', error);
  }
};

这里我们获取用户的摄像头/麦克风设备并显示用户的本地媒体。

最后我们需要一个函数是初始化RTCPeerConnection。

const initializePeerConnection = async (mediaTracks) => {
  const config = { iceServers: [{ urls: [ 'stun:stun1.l.google.com:19302' ] } ] };
  peerConnection = new RTCPeerConnection(config);

  peerConnection.onicecandidate = ({ candidate }) => {
    if (!candidate) return;

    console.log('peerConnection::icecandidate', candidate);
    console.log('remote', remoteId);
    sendSocketMessage('iceCandidate', { remoteId, candidate });
  };

  peerConnection.oniceconnectionstatechange = () => {
console.log('peerConnection::iceconnectionstatechange newState=', peerConnection.iceConnectionState);
    if (peerConnection.iceConnectionState === 'disconnected') {
      alert('Connection has been closed stopping...');
      socket.close();
    }
  };

  peerConnection.ontrack = ({ track }) => {
    console.log('peerConnection::track', track);
    remoteMediaStream.addTrack(track);
    remoteVideo.srcObject = remoteMediaStream;
  };

  for (const track of mediaTracks) {
    peerConnection.addTrack(track);
  }
};

hangupButton.disabled = false;

这里我们创建 RTCPeerConnection 使用的配置,这需要一个 iceServers 数组,它们是 STUN_TURN。请注意,如果你打算将一个应用程序投入生产,你可能想托管你自己的 STUN_TURN 服务器。特别是TURN! 使用一个 “免费 “的TURN服务器是有风险的,我不建议这样做。

接下来我们创建 RTCPeerConnection 并设置它的事件侦听器:“onicecandidate”发生在本地对等连接创建 IceCandidate 对象时,这里我们检查是否存在实际候选者并将其发送到远程对等点。“oniceconnectionstatechange”在协商过程中连接状态发生变化时发生。如果状态是disconnected就意味着peer之间的连接已经关闭,所以这里我们也关闭了socket。”ontrack “在收到传入的音轨时发生,这里我们将音轨添加到远程媒体流中并显示。

最后我们将本地轨道添加到 RTCPeerConnection 中,并启用挂断按钮。

现在代码已经完成了,可以运行示例了,启动服务器!

npm start

将浏览器导航至 https://localhost:3000,应该会看到以下页面:

WebRTC建立P2P连接和发送/接收媒体(webrtc入门三)

点击开始,能够看到你的本地摄像头。请注意我使用的是假媒体设备。

WebRTC建立P2P连接和发送/接收媒体(webrtc入门三)

复制创建和显示的 ID,然后打开另一个浏览器选项卡/窗口。转到相同的 URL,单击开始并将远程对等方的 ID 粘贴到文本框中,然后一旦点击呼叫,你能够看到远程用户的媒体。如下图。

WebRTC建立P2P连接和发送/接收媒体(webrtc入门三)

这是一个简单的 P2P 示例。希望这能让你开始创建自己的应用程序。下篇文章将进入第 4 部分,分享如何获取了用户的屏幕并实现远程共享。

Github 地址:https://github.com/ethand91/webrtc-tutorial

相关阅读

使用 JavaScript 和 Nodejs 搭建 webrtc信令服务器(webrtc入门一)

WebRTC MediaDevices API 获取媒体设备的访问权限(webrtc入门二)

本文来自作者投稿,版权归原作者所有。如需转载,请注明出处:https://www.nxrte.com/jishu/webrtc/22455.html

(0)

相关推荐

发表回复

登录后才能评论