使用 PeerJS 快速创建一个基于 WebRTC 的网页通话平台

自2020年起,视频通话就已经与很多人的生活密不可分。尽管我们在Zoom&Google Meet邀请链接的沉重压力下不堪重负,但这种风靡的趋势仍将许多基于音、视频app的互联网公司、技术带到了最
前沿。如果你读过我上一篇帖子,你就会知道我一直在与WebRTC相爱相杀。

WebRTC是由一组API终端以及众多协议构成的,它不需要用传统的服务器,就可以将数据(以音频、视频或其他任何形式)从一个peer/设备发送到另一个。问题是,WebRTC的使用和开发本身就很复杂。因此在处理信令服务器以及何时调用正确的端点时就可能会让人陷入困惑。

但是我得到个好消息:PeerJ作为一个WebRTC框架,提取了所有ice和信令逻辑,因此你可以专注于研发应用程序的功能。 PeerJS由客户端框架和服务器两部分组成,我们将同时使用这两个部分,但大部分工作将是处理客户端代码。

既然厌倦了视频,那就建立通话吧

这是一个中级教程,所以在开始之前,你应该熟悉以下内容:

  • Vanilla JavaScript
  • Node
  • Express
  • HTML

我将只专注于JavaScript方面,所以你可以直接复制HTMLCSS文件,不用太过在意它们。在开始之前,你需要安装node和软件包管理器,我用的是Yarn,但你可以使用npm或任何你习惯用的管理器。

如果通过阅读、按步骤学习编写代码能更好帮助你,那么我已经用 代码 编写了本 教程
希望能对你有所帮助。

创建

那就开始吧。首先,你需要运行 mkdir audio_app,然后运行 cd audio_app ,最后,你需要通过运行 yarn init 来创建一个新的app。根据提示,为你的项目添加名称、版本、简述等。接下来安装依赖项:

Peer将用于对等服务器,而PeerJS将用于访问PeerJS API和框架。安装完依赖项后,你的 package.json 应像这样:

{
  "name": "audio_app",
  "version": "1.0.0",
  "description": "An audio app using WebRTC",
  "main": "server.js",
  "scripts": {
    "start": "node server.js",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "Lola Odelola",
  "license": "MIT",
  "dependencies": {
    "express": "^4.17.1",
    "peer": "^0.5.3",
    "peerjs": "^1.3.1"
  }
}

要完成创建,你需要将我之前提到的HTMLCSS文件复制到你的项目文件夹中。

构建服务器

服务器文件看起来像是常规的Express服务器文件,只是Peer服务器不同。

你需要在文件 const {ExpressPeerServer} = require('peer') 顶部命令peer服务器,这将确保我们能够访问peer服务器。

然后,你需要创建peer服务器:

const peerServer = ExpressPeerServer(server, {
    proxied: true,
    debug: true,
    path: '/myapp',
    ssl: {}
});

我们使用之前创建的 ExpressPeerServer 对象来创建peer服务器,并向其传递一些选项。peer服务器将处理WebRTC所需的信令,因为peer服务器为我们抽象了逻辑,所以我们不必担心STUN / TURN服务器或其他协议。

最后,你需要通过调用 app.use(peerServer) 来告诉你的app使用 peerServer 。你完成的 server.js 应包括其他必要依赖项,就像你在server文件中包含的那样。将 index.html 文件提供给根路径,因此,完成时应如下所示:

const express = require("express");
const http = require('http');
const path = require('path');
const app = express();
const server = http.createServer(app);
const { ExpressPeerServer } = require('peer');
const port = process.env.PORT || "8000";

const peerServer = ExpressPeerServer(server, {
    proxied: true,
    debug: true,
    path: '/myapp',
    ssl: {}
});

app.use(peerServer);

app.use(express.static(path.join(__dirname)));

app.get("/", (request, response) => {
    response.sendFile(__dirname + "/index.html");
});

server.listen(port);
console.log('Listening on: ' + port);

你应该能够通过本地主机连接到你的app,在 server.js 中,因为我使用的是端口8000(在第7行中定义),但你可能用的是其他端口号,所以在终端中运行 node . 并在浏览器中访问 localhost:8000 ,你应该会看到一个类似于以下内容的页面:


The home page

好的部分

实际创建对等连接和调用逻辑是你一直在等待的部分。这是一个复杂的过程,因此得认真对待。首先,创建一个 script.js 文件,这个文件包含你的所有逻辑。

我们需要创建一个带有ID的peer对象。该ID将被用来连接两个peer,如果有一方没创建成功,那另一方将被分配给thpeer。

const peer = new Peer(''+Math.floor(Math.random()*2**18).toString(36).padStart(4,0), {
    host: location.hostname,
    debug: 1,
    path: '/myapp'
});

你还需要将peer附加到窗口,以便访问

window.peer = peer;

在终端的另一个选项卡中,通过运行以下命令来启动peer服务器:

peerjs --port 443 --key peerjs --path /myapp

创建peer之后,你需要获得浏览器的权限访问麦克风。我们将使用 navigator.MediaDevices 对象上的 getUserMedia 函 数,该函数是Media Devices Web界面的一部分。 getUserMedia 终端需要一个能指定所需权限的 constraints 对象。 getUserMedia 是一个 promise 对象,当成功解决该promise 对象时会返回一个 MediaStream 对象。在这种情况下,这将是我们推流中的音频。如果 promise 对象未能成功解决,就会显示错误。

function getLocalStream() {
    navigator.mediaDevices.getUserMedia({video: false, audio: true}).then( stream => {
        window.localStream = stream; // A
        window.localAudio.srcObject = stream; // B
        window.localAudio.autoplay = true; // C
    }).catch( err => {
        console.log("u got an error:" + err)
    });
}

A. window.localStream = stream :在这里,我们将 MediaStream 对象(在上一行中已分配给 stream )作为 localStream 附加到窗口。

B . window.localAudio.srcObject = stream :在我们的HTML中,有一个音频元素ID为 localAudio ,我们正将该元素的src属性设置为promise所返回的 MediaStream 属性。

C . window.localAudio.autoplay = true :我们正在将音频元素的autoplay属性设置为自动播放。

当你调用 getLocalStream 函数并刷新浏览器时,你会看到弹出的以下权限:

在允许权限之前先用耳机,这样以后再取消静音时,就不会收到任何回音。如果看不到权限,那就打开检查器,看看是否有错误。要确保你的javascript文件也正确链接到你的 index.html 文件。

这应该是所有文件放在一起看起来的样子:

/* global Peer */


/**
 * Gets the local audio stream of the current caller
 * @param callbacks - an object to set the success/error behaviour
 * @returns {void}
 */


function getLocalStream() {
    navigator.mediaDevices.getUserMedia({video: false, audio: true}).then( stream => {
        window.localStream = stream;
        window.localAudio.srcObject = stream;
        window.localAudio.autoplay = true;
    }).catch( err => {
        console.log("u got an error:" + err)
    });
}

getLocalStream();

好了,你已经获得权限了,现在你要保证每个用户都知道他们的peer ID,以便他们能建立连接。peerJS框架为我们提供大量事件监听器,我们可以在之前创建的peer上调用它们。因此,当peer打开时,就会显示其ID:

peer.on('open', function () {
    window.caststatus.textContent = `Your device ID is: ${peer.id}`;
});

这里,你正用ID caststatus 而不是 connecting... 替换HTML元素中的文本,你会看到 Your device ID is: <peer ID>


一张peer ID的截屏

而在这里,你还可以创建用于显示和隐藏各种内容的函数,稍后就会使用它们。你应该创建两个函数, showCallContentshowConnectedContent 。这些函数会显示呼叫按钮、挂断按钮和音频元素。

const audioContainer = document.querySelector('.call-container');
/**
 * Displays the call button and peer ID
 * @returns{void}
 */

function showCallContent() {
    window.caststatus.textContent = `Your device ID is: ${peer.id}`;
    callBtn.hidden = false;
    audioContainer.hidden = true;
}

/**
 * Displays the audio controls and correct copy
 * @returns{void}
 */

function showConnectedContent() {
    window.caststatus.textContent = `You're connected`;
    callBtn.hidden = true;
    audioContainer.hidden = false;
}

接下来,你要确保用户有能连接其peer的方法。为了连接两个peer,你需要其中一个peer ID。你可以使用let创建一个变量,然后将其分配到一个函数中,以便以后调用。

let code;
function getStreamCode() {
    code = window.prompt('Please enter the sharing code');
}

获取相关peer ID的便捷方法是使用窗口提示,当你要收集创建连接所需的peer ID时可以使用此提示。

使用peerJS框架,你需要将 localPeer 连接到 remotePeer 。PeerJS为我们提供了获取peer ID进行创建连接的 connect 功能。

function connectPeers() {
    peer.connect(code)
}

创建连接后,应使用PeerJS框架 on(‘connection') 设置远程peer的ID并打开连接。该监听器的函数接受一个 connection 对象,该对象是 DataConnection 对象的实例(是WebRTC DataChannel 的封装),因此,在此函数中,你需要将其分配给一个变量。同样,你需要在函数外部创建变量,以便之后可以对其进行分配。

let conn;
peer.on('connection', function(connection){
    conn = connection;
});

现在,你要给你的用户提供创建调用的功能。首先获取在HTML中定义的通话按钮:

const callBtn = document.querySelector(‘.call-btn’);`

当呼叫者单击“呼叫”时,你需要询问他们要呼叫的peer ID(我们将其存储在 getStreamCode 中的 code ),然后你要用该代码创建连接。

callBtn.addEventListener('click', function(){
    getStreamCode();
    connectPeers();
    const call = peer.call(code, window.localStream); // A

    call.on('stream', function(stream) { // B
        window.remoteAudio.srcObject = stream; // C
        window.remoteAudio.autoplay = true; // D
        window.peerStream = stream; //E
        showConnectedContent(); //F
    });
})

A. const call = peer.call(code, window.localStream) :这将使用我们先前所分配的 codewindow.localStream 创建通话。注意:localStream将会是用户的localStream。因此,对于呼叫者A,将是对方的信息流,对于呼叫者B,是他们自己的信息流。

B. call.on('stream', function(stream) { :peerJS给我们提供了一个 stream 事件,你可以在所创建的 call 上使 用。当一个调用开始流式传输时,你必须把来自呼叫的远程流分配给正确的HTML元素和窗口,这是你需要执行的操作。

Ç. 这个匿名函数需要一个 MediaStream 对象作为参数,然后你必须像之前一样将其设置为你窗口的HTML。所以,你将获取远程音频元素,并将src属性分配为传递给该函数的流。

D. 确保元素的自动播放属性也设置为true。

E. 确保将窗口 peerStream 设置为传递给函数的流。

F. 最后,要想显示正确的内容,你就要调用之前创建的 showConnectedContent 函数。

打开两个浏览器窗口并单击通话进行测试。你会看到以下内容:

两种浏览器并排运行,一种带有询问代码的提示

如果你提交其他对peer的ID,就会接通呼叫,但我们需要让其他浏览器有机会应答或拒绝该呼叫。

peerJS框架使 .on('call') 事件可供使用,具体如下:

peer.on('call', function(call) {
    const answerCall = confirm("Do you want to answer?") // A

    if(answerCall){ 
        call.answer(window.localStream) // B
        showConnectedContent(); // C
        call.on('stream', function(stream) { // D
            window.remoteAudio.srcObject = stream;
            window.remoteAudio.autoplay = true;
            window.peerStream = stream;
        });
    } else {
        console.log("call denied"); // E
    }
});


浏览器提示是否接听通话

A. const answerCall = confirm("Do you want to answer") :首先,让我们用确认提示帮助用户接听。屏幕上会显示一个窗口(如图所示),用户可以选择“确定”或“取消”,该窗口会映射到一个布尔值,并返回。

B.call.answer(window.localStream) :如果 answerCall 是正确的,要想在呼叫中调用peerJS的 answer 函数构建应答,你需将其传递给本地流。

C. showCallContent :你想要确保被呼叫者看到正确的HTML内容,这与你在呼叫按钮事件监听器中所做的类似。

D. 在 call.on('stream', function(){...} 块中的所有内容都与呼叫按钮的事件监听器中的完全相同。你需要在此处也添加它的原因是为了让接听电话的人也能更新浏览器。

E. 如果对方拒绝通话,我们将在控制台中记录一条消息。

我们快要完成了。你现在拥有的代码足够创建呼叫并接听电话了。刷新浏览器并进行测试。你需要确保两个浏览器均已打开控制台,否则你将不会收到接听电话的提示。单击呼叫、为其它浏览器提交peer ID并接听电话。最后的页面应如下所示:


两个浏览器都能连接通话

最后一件事是要保证呼叫者可以终止呼叫。执行此操作最合适的方法是使用 close 关闭连接,你可以在事件监听器中为挂断按钮执行此操作。

const hangUpBtn = document.querySelector('.hangup-btn');
hangUpBtn.addEventListener('click', function (){
    conn.close();
    showCallContent();
})

关闭连接后,要想显示正确的HTML内容,你只需调用 showCallContent 函数即可。在call事件中,要想确保更新远程浏览器,你可以在 peer.on('call', function(stream){...} 事件监听器中,也就是条件块中添加另一个事件监听器。

conn.on('close', function (){
    showCallContent();
})

如果发起呼叫的人先单击“挂断”,则两个浏览器都会被更新。

瞧,你已经拥有一部联网电话了。:telephone_receiver:

下一步

部署方式

部署此app最简单方法是Glitch,因为你不必费力为peer服务器配置端口。

使其成为PWA

在Samsung Internet,我们致力于渐进式Web app的开发,因此下一阶段会添加 manifest.jsonserviceworker.js 将其设置为PWA。

Gotchas

  • 如果你在网上进行了一些调查,你可能遇到过 navigator.getUserMedia 这种情况,并假设你可以使用其代替 navigator.MediaDevices.getUserMedia, 但你错了。前者是不建议使用的方法,它需要回调以及约束作为参数。后者使用了Promise,所以你不用使用回调。

  • 由于我们使用 confirm 提示来询问用户是否要接听电话,因此被调用的浏览器和标签页且处于“活动状态”非常重要,这也意味着不应将窗口最小化,而是将标签页显示在屏幕上并将鼠标放在选项卡中的某个位置。理想情况下,你将使用HTML创建自己的模式,而这些模式不会受到这些限制。

  • 我们目前对事物进行编码的方式意味着,当连接断开时, 只有 当发起呼叫者先按“挂断”时,两个浏览器才会更新。如果接听者先单击“挂断”,则另一个呼叫者只能单击“挂断”才能查看正确的HTML。

  • 在火狐浏览器上无法使用在 conn 变量上调用的 on('close') 事件,这仅意味着在火狐浏览器中,每个调用者都必须单独挂断。

作者 lola odelola

原文链接 https://medium.com/samsung-internet-dev/building-an-internet-connected-phone-with-peerjs-775bd6ffebec

推荐阅读
作者信息
AgoraTechnicalTeam
TA 暂未填写个人简介
文章
153
相关专栏
本专栏仅用于分享音视频相关的技术文章,与其他开发者和 Agora 研发团队交流、分享行业前沿技术、资讯。发帖前,请参考「社区发帖指南」,方便您更好的展示所发表的文章和内容。