WebRTC 是一种点对点的实时通信技术,本文将基于这一技术实现一个实时的在线编程面试工具,让远程面试时双方不只能够音视频通话,面试官还能实时看到面试者的编程状况。javascript
效果就像这样: java
Agora SDK 是声网提供的一套实时通讯解决方案,其中也包含了对 WebRTC 的封装,咱们将基于它开发 WebRTC 相关的功能,以提供更接近生产级别的实时通讯体验。git
此工具完整源代码存放于此仓库中,能够配合文章阅读其源码。github
这个在线编程面试工具须要解决两部分的需求:web
经过了解 Agora SDK 提供的功能,发现有两种 SDK 能够用于实现咱们的需求:面试
音视频部分 Video SDK 已经提供了渲染相关的实现,能够将视频输出到指定的 DOM 节点中,基本开箱即用。而代码编辑器及其数据传输则须要必定的开发。编程
通过一些对比和挑选,最终选择使用 VScode 的编辑器部分 monaco-editor 做为内置的代码编辑器,再使用以前开源的 Web 录制和回放库 rrweb 记录 monaco-editor 中的操做,将数据经过 Signaling SDK 传输至面试官一侧,一样经过 rrweb 进行实时的回放,达到代码同步的效果。浏览器
注意,本工具只是一个概念验证性质的项目,仅供讨论。优化程度还不足以应用在生产中,设计自己也有很大的改进空间,例如依赖完整的 VScode 提供代码执行、debug 等功能,实现一个更接近于 live share 的方案。 网络
Agora SDK 的 API 自己比较清晰易懂,文档也足够完善,可是 API 大可能是异步,而且以回调的形式提供。数据结构
以视频功能为例,完成初始化、加入频道、建立流、发布、订阅等一系列准备动做以后可能已经嵌套了四五层回调。因此我先对用到的 API 进行了简单的封装,使其提供 Promise 风格的接口,能够在使用时经过 async/await 保持更清晰的代码结构以及更好的控制能力。
以初始化为例,SDK 的 API 使用方式是:
client.init(appId, function () {
console.log("AgoraRTC client initialized");
}, function (err) {
console.log("AgoraRTC client init failed", err);
});
复制代码
咱们能够用这种方式将其转化为 Promise:
const init = appId =>
new Promise((resolve, reject) => {
client.init(appId, () => resolve(), err => reject(err));
});
复制代码
将全部 API 这样封装后,咱们的基本流程代码也就更加简单清晰:
async function main() {
try {
await rtc.init(APP_ID);
const uid = await rtc.join(null, CHANNEL_ID, ACCOUNT);
const stream = rtc.createStream();
await rtc.initStream(stream);
await rtc.subscribe(...);
await rtc.publish(...);
} catch (error) {
console.error(error);
}
}
复制代码
以上对 SDK 的异步封装能够参考此文件。
音视频通话的功能主要参照 quick start guide 实现,步骤能够概括为:
?id=abc123
,面试双方打开的 query 一致就能保证加入到同一个 channel 中。在实际实现的过程当中,因为咱们对 SDK 进行了 Promise 的封装,因此第 4 和 第 5 步针对面试双方作了顺序上的调整:
这主要是为了不发布时对方还未订阅,致使最终没能创建链接的问题。
在设计思路中咱们已经提到将使用 monaco-editor 做为在线编辑器,并用 rrweb 记录编辑器中的操做。两个工具的 API 都很是易用,在面试者的页面中,经过十几行初始化代码就完成了集成:
import * as monaco from "monaco-editor/esm/vs/editor/editor.main.js";
import { record } from "rrweb";
self.MonacoEnvironment = {
getWorkerUrl: function(moduleId, label) {
// get worker urls
}
};
monaco.editor.create(document.body, {
value: ["function x() {", '\tconsole.log("Hello world!");', "}"].join("\n"),
language: "javascript"
});
record({
emit(event) {
parent.postMessage({ event }, parent.origin);
},
inlineStylesheet: false
});
复制代码
在实现时,咱们将编辑器以 iframe 的形式嵌套在面试者页面中,rrweb 录制到操做记录时会经过 parent.postMessage
的方式将数据传递给主页面,交由 Signaling SDK 传输。
但在实际使用 Signaling SDK 时,咱们遇到了两个比较典型的问题:
解决数据体积限制的一个思路是将数据切分为多个 chunk,并在每一个 chunk 中标识这是一个不完整的数据记录,须要拼接后再使用。
对应的实现以下:(此处使用了一个较为粗糙的方式进行标识,实际上还能够记录更多 meta 信息提升识别的准确性)
// 将操做数据转化为字符串
const eventStr = JSON.stringify(e.data.event);
export const CHUNK_START = "_0_";
export const CHUNK_SIZE = 8 * 1024 - CHUNK_START.length;
export const CHUNK_REG = new RegExp(`.{1,${CHUNK_SIZE}}`, "g");
const chunks = [];
if (eventStr.length > CHUNK_SIZE) {
for (const chunk of eventStr.match(CHUNK_REG)) {
chunks.push(CHUNK_START + chunk);
}
}
复制代码
在面试官页面接收到 Signaling SDK 传入的数据时,就能够根据数据的头部是否有 CHUNK_START
的特殊标识来判断当前是一个完整数据仍是一个须要拼接的数据:
let largeMessage = "";
on("messageInstantReceive", (messageAccount, uid, message) => {
const events = [];
if (message.startsWith(CHUNK_START)) {
largeMessage += message.slice(CHUNK_START.length, message.length);
} else {
if (largeMessage) {
// reset chunks
events.push(JSON.parse(largeMessage));
largeMessage = "";
}
events.push(JSON.parse(message));
}
});
复制代码
上文已经提到,因为 rrweb 的实现下传输的数据可能为较大的全量快照,也可能为较小的单次 Oplog,因此在网络传输速度的影响下,若是不加以控制,有可能会出现较晚发生的操做先传输完成的状况,致使回放异常。因此咱们须要自行实现传输数据保序。
Signaling SDK 提供的发送数据 API messageInstantSend 提供了第三个参数 callback,当发送成功时调用。但实际测试时 callback 触发并不保证接收端已经下载完成,因此咱们仍需自行实现包含下载在内的保序。如个人理解或测试有误,请指正。
一种较为简单的实现是在面试者这一侧增长一个消息队列,当 rrweb 录制到新的操做时先将数据放入队列中。
同时,在面试官一侧准备好接受数据时,先向对方发出一个 START 信号,面试者一侧收到信号后从消息队列中取出第一条数据发送。此后,面试官一侧每收到一条数据就回复一个 ACK 信号,面试者一侧收到此信号后才继续从队列中取出消息发送,就能够保证面试官一侧接收到的数据都是严格保序的。
对应的示例以下:
on("messageInstantReceive", async (messageAccount, uid, message) => {
if (message === START) {
// 发送第一条数据
await signal.messageInstantSend(interviewerAccount, eventQueue.dequeue());
}
if (message === ACK && eventQueue.length > 0) {
await signal.messageInstantSend(interviewerAccount, eventQueue.dequeue());
}
});
复制代码
更完整的实际实现能够参考此文件。
上述基于接收端 ACK 的保序方式也有比较明显的缺点:因为 Signaling SDK 自己基于 TCP 实现,这样的已读确认机制产生的额外通讯会形成较大的时延,致使面试官一侧观看回放时的实时性受到影响。
一些可行的优化思路包括:
相信在增长以上优化以后,咱们的在线编程面试工具会更具实用性。
随着 Web API 的不断进化以及愈来愈多成熟工具、服务的出现,开发者能够基于它们快速地开发出各类实用的工具、产品。以本文中的项目为例,因为使用了 Agora SDK、monaco-editor 和 rrweb 三个工具/服务,用很是少的代码量就完成了功能的可行性验证。
当 VScode remote/browser 相关的功能更为成熟时,编辑器部分的功能还会被进一步强化,可能就能够造成一个实际可用的产品。因此咱们有理由相信当浏览器提供的 API 更增强大、性能更好时,会诞生更多以浏览器为客户端的服务,而 WebRTC 提供的实时通讯会是颇有价值一环。