基於 WebRTC 實如今線編程面試工具 | 掘金技術徵文

WebRTC 是一種點對點的實時通信技術,本文將基於這一技術實現一個實時的在線編程面試工具,讓遠程面試時雙方不只能夠音視頻通話,面試官還能實時看到面試者的編程狀況。javascript

效果就像這樣: java

preview

Agora SDK 是聲網提供的一套實時通訊解決方案,其中也包含了對 WebRTC 的封裝,咱們將基於它開發 WebRTC 相關的功能,以提供更接近生產級別的實時通訊體驗。git

此工具完整源代碼存放於此倉庫中,能夠配合文章閱讀其源碼。github

需求

這個在線編程面試工具須要解決兩部分的需求:web

  1. 面試官和麪試者能夠實時音視頻通話進行交流。
  2. 面試者可以使用一個在線代碼編輯器進行答題,面試官可以實時的看到面試者編寫的代碼。編輯器最好能有高亮、代碼補全的功能方便麪試者發揮。

設計思路

經過了解 Agora SDK 提供的功能,發現有兩種 SDK 能夠用於實現咱們的需求:面試

  • Video SDK,提供可靠的實時音視頻通話服務,能夠用於面試官和麪試者溝通交流。
  • Signaling SDK,提供穩定的消息通道,能夠用於將面試者的編寫過程對應的數據傳遞給面試官。

音視頻部分 Video SDK 已經提供了渲染相關的實現,能夠將視頻輸出到指定的 DOM 節點中,基本開箱即用。而代碼編輯器及其數據傳輸則須要必定的開發。編程

通過一些對比和挑選,最終選擇使用 VScode 的編輯器部分 monaco-editor 做爲內置的代碼編輯器,再使用以前開源的 Web 錄製和回放庫 rrweb 記錄 monaco-editor 中的操做,將數據經過 Signaling SDK 傳輸至面試官一側,一樣經過 rrweb 進行實時的回放,達到代碼同步的效果。瀏覽器

注意,本工具只是一個概念驗證性質的項目,僅供討論。優化程度還不足以應用在生產中,設計自己也有很大的改進空間,例如依賴完整的 VScode 提供代碼執行、debug 等功能,實現一個更接近於 live share 的方案。 網絡

封裝 SDK

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 實現,步驟能夠概括爲:

  1. 基於 APP ID 初始化一個客戶端。
  2. 加入一個 channel,每一個 channel 有本身的惟一 id,channel 裏的用戶能夠訂閱到同 channel 裏其它用戶發佈的視頻音頻流。在咱們的工具中,使用 url query 存放一個 channel id,例如 ?id=abc123,面試雙方打開的 query 一致就能保證加入到同一個 channel 中。
  3. 建立並初始化本地的音視頻流(視頻內容爲使用者本人),並將視頻初始化到 DOM 中。在工具中咱們會同時看到本身和對方的視頻,此步驟中渲染的爲本身的視頻。
  4. 發佈本身的音視頻流。
  5. 訂閱對方發佈的音視頻流,接收到對方音視頻流後渲染到 DOM 中。

在實際實現的過程當中,因爲咱們對 SDK 進行了 Promise 的封裝,因此第 4 和 第 5 步針對面試雙方作了順序上的調整:

  1. 面試官訂閱對方的流。
  2. 面試者發佈本身的流,同時訂閱對方的流。
  3. 面試官訂閱成功以後,才發佈本身的流,此時對方已處於訂閱狀態,必定可以成功接收到這一發布信息。

這主要是爲了不發佈時對方還未訂閱,致使最終沒能創建鏈接的問題。

實時編程

在設計思路中咱們已經提到將使用 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 時,咱們遇到了兩個比較典型的問題:

  1. 傳輸數據有體積限制,每條消息可見字符大小不能超過 8 KB。
  2. 因爲 rrweb 的錄製是 log-structured 的數據結構,因此須要在每一個操做的數據大小不1、傳輸速度不一樣的狀況下嚴格保序。

數據切分

解決數據體積限制的一個思路是將數據切分爲多個 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 實現,這樣的已讀確認機制產生的額外通訊會形成較大的時延,致使面試官一側觀看回放時的實時性受到影響。

一些可行的優化思路包括:

  1. 再也不嚴格的限制數據接收的時序,而是在數據的 meta 區域中記錄編號索引,接收端再發現兩次接收的數據之間有「空隙」時選擇不當即回放,而是等待數據傳輸完成補全後從新排序再回放。這樣就無需傳遞 ACK 信號,減小一輪網絡往返的延遲。
  2. 發送端隊列裏有超過一條記錄時,嘗試將多條記錄在不超過體積限制的狀況下拼接成一條,經過一次數據傳輸批量發送到對端。這樣可以減小一些數據傳輸創建鏈接時的開銷,尤爲是小數據塊數量較多時優化效果會比較明顯。

相信在增長以上優化以後,咱們的在線編程面試工具會更具實用性。

總結

隨着 Web API 的不斷進化以及愈來愈多成熟工具、服務的出現,開發者能夠基於它們快速地開發出各類實用的工具、產品。以本文中的項目爲例,因爲使用了 Agora SDK、monaco-editor 和 rrweb 三個工具/服務,用很是少的代碼量就完成了功能的可行性驗證。

當 VScode remote/browser 相關的功能更爲成熟時,編輯器部分的功能還會被進一步強化,可能就能夠造成一個實際可用的產品。因此咱們有理由相信當瀏覽器提供的 API 更增強大、性能更好時,會誕生更多以瀏覽器爲客戶端的服務,而 WebRTC 提供的實時通訊會是頗有價值一環。

Agora SDK 使用體驗徵文大賽 | 掘金技術徵文,徵文活動正在進行中

相關文章
相關標籤/搜索