使用 WebRTC 进行视频通话第 4 步:查找联系人

这是我们使用 WebRTC 创建视频聊天的系列文章的第四篇。

以下是本系列文章的链接:

1.来自网络摄像头和麦克风的数据流(访问用户设备以及数据流,并在浏览器上显示视频(带音频))
2.通过 WebSocket 建立连接(在两个用户之间通过 WebSocket 建立 P2P 连接)
3.建立 WebRTC 连接( 建立 WebRTC 连接,真正开启视频聊天)
4.查找联系人(调整 WebSocket 服务器和客户端代码,使用户只有输入相同代码才会建立彼此联系)
5.与 WebRTC 共享您的屏幕(将 RTCPeerConnection 对象的视频轨道替换为显示媒体流的视频轨道,使用户之间共享屏幕)

通过上一篇文章,我们已经在两个对等点之间建立了 WebRTC 连接。这种连接只能在对等体之间通过信令机制交换消息后才能建立,这种机制不是规范的一部分,可以自由选择。我们使用了 WebSocket,但到目前为止,我们的信令非常简单,可以将用户的任何消息发送给所有其他连接的用户,这意味着 WebRTC 连接是在用户之间随机建立的。

在现实生活应用中,你可能并不想这么做。在本文中,我们将调整我们的 WebSocket 服务器和我们的客户端代码,让用户通过使用代码找到他们的联系人。只有输入相同代码的用户才会建立联系。因此,如果你还没有阅读上一篇文章,那你就不会对此篇文章理解得十分透彻。

在我们的示例中,同伴在聊天起始页输入代码后,才能真正进入聊天室

将代码添加到信令消息中

我们首先需要调整起始页面,以便用户可以输入其代码:

在输入代码之前,开始按钮被禁用。代码长度至少应为 9 个字符。

输入代码并单击按钮后,此代码视图将隐藏并显示聊天室。代码保存在一个变量中,与所有信令消息一起发送。

HTML 有两个部分:一个是起始页 的 id start ,一个是聊天室的 id视频

<!DOCTYPE html>
<html>

<head>
  <meta charset="UTF-8">
  <title>VideoChat</title>
  <script src="index.js"></script>
  <link rel="stylesheet" href="styles.css">
</head>

<body>
  <div id="chat-room" style="display: none;">
    <div id="videos">
      <video id="self-view" autoplay></video>
      <video id="remote-view" autoplay></video>
    </div>
  </div>
  <div id="start">
    <input id="code-input" placeholder="Enter your code" />
    <button id="start-button" disabled>Start Video Chat</button>
  </div>
</body>

</html>
html, body {
  height: 100%;
  margin: 0;
}

body {
  background-color:#01284a;
  padding: 8px;
}

#start {
  position: relative;
  top: 40%;
  width: 100%;
  text-align: center;
}

input {
  border-radius: 3px;
  margin-bottom: 20px;
  padding: 5px;
  font-size: 16px;
  text-align: center;
}

button {
  display: block;
  margin: auto;
  padding: 10px;
  font-size: 16px;
  border-radius: 3px;
  font-weight: bold;
  color: white; 
}

button:not(:disabled) {
  cursor: pointer;
  background-color: #000f1c;
}

button:disabled {
  cursor: not-allowed ;
  background-color: #7691a8;
}

video {
  width: 100%;
}

#videos {
  display: grid;
  grid-gap: 16px;
  width: 100%;
  grid-template-columns: 1fr 3fr;
}

#self-view {
  grid-column: 1;
  background-color: black;
}

#remote-view {
  grid-column: 2;
  background-color: black;
  height: 95vh;
}

我们对上一个例子的客户端代码进行改编:

(function () {
  "use strict";
  
  let code;

  document.addEventListener('input', async (event) => {
    if (event.target.id === 'code-input') {
      const { value } = event.target;
      if (value.length > 8) {
        document.getElementById('start-button').disabled = false;
        code = value;
      } else {
        document.getElementById('start-button').disabled = true;
        code = null;
      }
    }
  });
  
  document.addEventListener('click', async (event) => {
    if (event.target.id === 'start-button' && code) {
      startChat();
    }
  });
  
  [...]
  
  const sendMessage = (signaling, message) => {
    if (code) {
      signaling.send(JSON.stringify({
        ...message,
        code,
      }));
    }
  };
});

首先,我们创建一个变量code 来保存代码。侦听input 上的input 事件,代码长度至少为 9 个字符时才能启用按钮。同从前一样,点击按钮开始聊天,这意味着隐藏开始页面,显示聊天室并开始信号处理。我们还需要调整在此过程中发送的消息。我们创建了一个函数sendMessage ,该函数可在实现发送消息之前添加代码。

以下是完整的客户端代码:

(function () {
  "use strict";

  let code;

  const MESSAGE_TYPE = {
    SDP: 'SDP',
    CANDIDATE: 'CANDIDATE',
  }

  document.addEventListener('input', async (event) => {
    if (event.target.id === 'code-input') {
      const { value } = event.target;
      if (value.length > 8) {
        document.getElementById('start-button').disabled = false;
        code = value;
      } else {
        document.getElementById('start-button').disabled = true;
        code = null;
      }
    }
  });

  document.addEventListener('click', async (event) => {
    if (event.target.id === 'start-button' && code) {
      startChat();
    }
  });

  const startChat = async () => {
    try {
      const stream = await navigator.mediaDevices.getUserMedia({ audio: true, video: true });
      showChatRoom();

      const signaling = new WebSocket('ws://127.0.0.1:1337');
      const peerConnection = createPeerConnection(signaling);

      addMessageHandler(signaling, peerConnection);

      stream.getTracks().forEach(track => peerConnection.addTrack(track, stream));
      document.getElementById('self-view').srcObject = stream;

    } catch (err) {
      console.error(err);
    }
  };

  const createPeerConnection = (signaling) => {
    const peerConnection = new RTCPeerConnection({
      iceServers: [{ urls: 'stun:stun.l.test.com:193000' }],
    });
    
    peerConnection.onnegotiationneeded = async () => {
      await createAndSendOffer();
    };

    peerConnection.onicecandidate = (iceEvent) => {
      if (iceEvent && iceEvent.candidate) {
        sendMessage(signaling, {
          message_type: MESSAGE_TYPE.CANDIDATE,
          content: iceEvent.candidate,
        });
      }
    };

    peerConnection.ontrack = (event) => {
      const video = document.getElementById('remote-view');
      if (!video.srcObject) {
        video.srcObject = event.streams[0];
      }
    };
    
    return peerConnection;
  }

  const addMessageHandler = (signaling, peerConnection) => {
    signaling.onmessage = async (message) => {
      const data = JSON.parse(message.data);

      if (!data) {
        return;
      }

      const { message_type, content } = data;
      try {
        if (message_type === MESSAGE_TYPE.CANDIDATE && content) {
          await peerConnection.addIceCandidate(content);
        } else if (message_type === MESSAGE_TYPE.SDP) {
          if (content.type === 'offer') {
            await peerConnection.setRemoteDescription(content);
            const answer = await peerConnection.createAnswer();
            await peerConnection.setLocalDescription(answer);
            sendMessage(signaling, {
              message_type: MESSAGE_TYPE.SDP,
              content: answer,
            });
          } else if (content.type === 'answer') {
            await peerConnection.setRemoteDescription(content);
          } else {
            console.log('Unsupported SDP type.');
          }
        }
      } catch (err) {
        console.error(err);
      }
    }:
  };

  const sendMessage = (signaling, message) => {
    if (code) {
      signaling.send(JSON.stringify({
        ...message,
        code,
      }));
    }
  };

  const createAndSendOffer = async (signaling, peerConnection) => {
    const offer = await peerConnection.createOffer();
    await peerConnection.setLocalDescription(offer);

    sendMessage(signaling, {
      message_type: MESSAGE_TYPE.SDP,
      content: offer,
    });
  };

  const showChatRoom = () => {
    document.getElementById('start').style.display = 'none';
    document.getElementById('chat-room').style.display = 'block';
  };
})();

建立与代码的连接

我们现在需要调整信令机制以在具有相同代码的对等点之间交换消息。为此,我们创建了对象peersByCode。 在这个对象中,代码将是键,值的形式为连接数组,具体如下:

{
  123456789: [
   { id: 1234, connection: ... },
   { id: 5678, connection: ... },
  ],
  789012345: [
   { id: 4321, connection: ... },
   { id: 8765, connection: ... },
  ],
}
When a peer s

当一个对等点通过信令机制发送消息时,需发送代码。我们首先检查代码是否已经是peersByCode 对象中的一个键,如果没有,我们需进行添加。我们还需检查用户的连接对象是否已经在此代码的连接数组中,如果没有,需进行添加。最后,我们将消息发送给所有拥有此代码的用户,当然,正发送消息的用户除外。

const http = require('http');
const server = require('websocket').server;

const httpServer = http.createServer(() => { });
httpServer.listen(1337, () => {
  console.log('Server listening at port 1337');
});

const wsServer = new server({
  httpServer,
});

const peersByCode = {};

wsServer.on('request', request => {
  const connection = request.accept();
  const id = Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);

  connection.on('message', message => {
    const { code } = JSON.parse(message.utf8Data);
    if (!peersByCode[code]) {
      peersByCode[code] = [{ connection, id }];
    } else if (!peersByCode[code].find(peer => peer.id === id )) {
      peersByCode[code].push({ connection, id });
    }
    
    const peer = peersByCode[code].find(peer => peer.id !== id)
    if (peer) {
      peer.connection.send(message.utf8Data);
    }
  });
});

我们在两个给定对等点之间建立了连接,而不是在随机对等点之间建立连接。在现实生活中的应用程序中,用户将从第三方获取此代码。例如,假设你的目标是允许雇主和申请人通过 WebRTC 组织面试。雇主将在你的申请中创建一个会议,雇主和申请人将收到聊天室的链接。在这种情况下,代码可能会在邀请电子邮件中给出,或者直接包含在他们收到的链接中。

在下一篇文章中,我们将允许对等端共享他们的屏幕。

原文作者 Heloise Parein
原文链接 https://medium.com/gitconnected/find-your-contact-videochat-with-javascript-step-4-4d527576b8cf

推荐阅读
相关专栏
开源技术
106 文章
本专栏仅用于分享音视频相关的技术文章,与其他开发者和声网 研发团队交流、分享行业前沿技术、资讯。发帖前,请参考「社区发帖指南」,方便您更好的展示所发表的文章和内容。