用js實現web端錄屏

喆星高照發表於2021-11-15

用js實現web端錄屏

隨著網際網路技術飛速發展,網頁錄屏技術已趨於成熟。例如可將錄屏技術運用到線上考試中,實現遠端監考、螢幕共享以及錄屏等;而在我們開發人員研發過程中,對於部分偶發事件,異常監控系統僅僅只能告知程式出錯,而不能清晰的告知錯誤的復現路徑,而錄屏技術或許能幫我們定位並復現問題。那麼本文將從有感錄屏和無感錄屏兩方面給讀者分享一下錄屏這項技術,希望可以幫助你對網頁錄屏有一個初步認識。

什麼是有感錄屏?

有感錄屏一般指通過獲得使用者的授權或者通知使用者接下來的操作將會被錄製成視訊,並且在錄製過程中,使用者有權關閉中斷錄屏。即無論在錄屏前還是錄屏的過程中,使用者都始終能夠決定錄屏能否進行。

基於 WebRTC 的有感錄屏

常見的有感錄屏方案主要是通過 WebRTC (https://developer.mozilla.org/zh-CN/docs/Web/API/WebRTC_API) 錄製。WebRTC 是一套基於音視軌的實時資料流傳播的技術方案。由瀏覽器提供的原生 API navigator.mediaDevices.getDisplayMedia 方法實現提示使用者選擇和授權捕獲展示的內容或視窗,進而將獲取 stream (錄製的螢幕音視流)。我們可以對 stream 進行轉化處理,轉成相對應的媒體資料,並將其資料儲存。後續需要回溯該次錄製內容時,則取出媒體資料進行播放。

具體的有感錄屏流程如下:

用js實現web端錄屏

 

實現初始化錄屏和資料儲存

使用 navigator.mediaDevices.getDisplayMedia 初始化錄屏,觸發彈窗獲取使用者授權,效果圖如下所示:

用js實現web端錄屏

 

實現 WebRTC 初始化錄屏核心程式碼如下:

const tracks = []; // 媒體資料
const options = {
  mimeType : "video/webm; codecs = vp8", // 媒體格式
};
let mediaRecorder;
// 初始化請求使用者授權監控
navigator.mediaDevices.getDisplayMedia(constraints).then((stream) => {
  // 對音視流進行操作
  startFunc(stream);
});
// 開始錄製方法
function start(stream) {
  // 建立 MediaRecorder 的例項物件,對指定的媒體流進行錄製
  mediaRecorder = new MediaRecorder(stream, options);
  // 當生成媒體流資料時觸發該事件,回撥傳參 event 指本次生成處理的媒體資料
  mediaRecorder.ondataavailable = event => {
     if(event?.data?.size > 0){
      tracks.push(event.data); // 儲存媒體資料
    }
  };
  mediaRecorder.start();
  console.log("************開始錄製************")
};
// 結束錄製方法
function stop() {
  mediaRecorder.stop();
  console.log("************錄製結束************")
}
// 定義constraints資料型別
interface constraints {
  audio: boolean | MediaTrackConstraints, // 指定是否請求音軌或者約束軌道屬性值的物件
  video: boolean | MediaTrackConstraints, // 指定是否請求視訊軌道或者約束軌道屬性值的物件
}

實現錄屏回溯

獲取該次錄屏的媒體資料,可以將其轉成 blob 物件,並且生成 blob物件的 url 字串,再賦值 video.src 中,便可以回放到錄製結果,回溯的視訊效果如下:

用js實現web端錄屏

 

錄屏回溯方法的核心程式碼如下所示:

// 回放錄製內容
function replay() {
  const video = document.getElementById("video");
  const blob = new Blob(tracks, {type : "video/webm"});
  video.src = window.URL.createObjectURL(blob);
  video.srcObject = null;
  video.controls = true;
  video.play();
}

實現實時直播功能

由於儲存的媒體資料是實時的,因此可以利用該資料實現直播功能。通過給 video.srcObject 賦值媒體流可以實現直播功能。

實現實時直播核心程式碼如下:

// 直播
function live() {
  const video = document.getElementById("video");
  video.srcObject = window.stream;
  video.controls = true;
  video.play();
}

瀏覽器相容性

用js實現web端錄屏

 

什麼是無感錄屏?

無感錄屏指在使用者無感知的情況,對使用者在頁面上的操作進行錄製。實現上與有感錄製區別在於,無感錄製通常是利用記錄頁面的 DOM 來進行錄製。常見的有 canvas 截圖繪製視訊和 rrweb 錄製等方案。

canvas 截圖繪製視訊

使用者在瀏覽頁面時,可以通過 canvas 繪製多個 DOM 快照截圖,再將多個截圖合併成一段錄屏視訊。但是考慮到假設視訊幀數為 30 幀,幀數代表著每秒所需的截圖數量,為了視訊的流暢和清晰,每張截圖為 400 KB ,那麼當視訊長度為 1 分鐘,則需要上傳 703.125 MB 的資源,這麼大的頻寬浪費無疑會造成效能,甚至影響使用者體驗,不推薦使用,也不在此詳細介紹本方案實現。

rrweb 錄製

rrweb (record and replay the web) 是一個對於 DOM 錄製的支援性非常好,利用現代瀏覽器所提供的強大 API 錄製並回放任意 web 介面中的使用者操作,能夠將頁面 DOM 結構通過相應演算法高效轉換 JSON 資料的開源庫。相比較於使用 canvas 繪製錄屏,rrweb 在保證錄製不掉幀的基礎上,讓網路傳輸資料更加快速和輕量化,極大地優化了網路效能。

rrweb 開源庫主要由 rrweb-snapshot、rrweb 和 rrweb-play 三部分組成,並且提供了動作篩選,資料加密、資料壓縮、資料切片、遮蔽元素等功能。

用js實現web端錄屏

 

rrweb-snapshot

rrweb-snapshot 提供 snapshot 和 rebuild 兩個 API,分別實現生成可序列化虛擬 DOM 快照的資料結構和將其資料結構重建為對應 DOM 節點的兩個功能。

snapshot 將 DOM 及其狀態轉化為可序列化的資料結構並新增唯一標識 id,使得一個 id 對映對應的一個 DOM 節點,方便後續以增量的方式來操作。

首先需要通過深拷貝 document 生成初始化 DOM 快照。

// 深拷貝 document 節點
const docEl = document.documentElement.cloneNode(true);
// 回放時再將深拷貝的節點掛在回去即可
document.replaceChild(docEl, document.documentElement);

由於獲取到的 DOM 物件並不是可序列化的,因此仍需要將其轉成特定的文字格式(如 JSON)進行傳輸,否則無法做到遠端錄製。在實現 DOM 快照可序列化的過程中,還需對資料進行特殊處理:

  1. 將相對路徑改成絕對路徑;
  2. 將頁面引用的樣式改成內聯樣式;
  3. 禁止指令碼執行,被錄製頁面中的所有 JavaScript 都不應該被執行。把 <script> 轉成 <noscrpit> ;
  4. 由於部分表單(如 <input type="text" /> )不會把值暴露在 html 中,故需讀取表單的 value 值。

雖然已經能夠獲取到全量的 DOM 物件,但是無法將增量快照中被互動的 DOM 節點和現已有的 DOM 節點關聯上,所以還需要給 DOM 新增一層對映關係(id => Node),後續 DOM 節點的更新都通過該 id 來記錄並對應到完整的 DOM 節點中。

如下是初始時獲取到的 DOM 節點:

<html>
  <body>
    <header>
    </header>
  </body>
</html>

通過遍歷整個 DOM 樹,以 Node 節點為單位,給每個遍歷到的 Node 都新增了唯一標識 id ,生成全量序列化的 DOM 物件快照 。以下是序列化後的資料結構示意:

{
  "type": "Document",
  "childNodes": [
    {
      "type": "Element",
      "tagName": "html",
      "attributes": {},
      "childNodes": [
        {
          "type": "Element",
          "tagName": "head",
          "attributes": {},
          "childNodes": [],
          "id": 3
        },
        {
          "type": "Element",
          "tagName": "body",
          "attributes": {},
          "childNodes": [
            {
              "type": "Text",
              "textContent": "\n    ",
              "id": 5
            },
            {
              "type": "Element",
              "tagName": "header",
              "attributes": {},
              "childNodes": [
                {
                  "type": "Text",
                  "textContent": "\n    ",
                  "id": 7
                }
              ],
              "id": 6
            }
          ],
          "id": 4
        }
      ],
      "id": 2
    }
  ],
  "id": 1
}
  • rebuild

將 snapshot 記錄的初始化快照的資料結構,繼而通過遞迴給每個節點新增屬性來重建 DOM ,生成可序列化的 DOM 節點快照。

rrweb

rrweb 提供 record 和 replay 兩個 API,分別實現記錄所有增量資料和將記錄的資料按照時間戳回放的兩個功能。

  • record

通過觸發檢視的變化和 DOM 結構的改變(如 DOM 節點的刪減和屬性值的變化)來劫持增量變化資料存入 JSON 物件中,每個增量資料對應一個時間戳,這些資料稱之為 Oplog(operations log)。

用js實現web端錄屏

 

檢視的變化可通過全域性事件監聽和事件代理方法收集增量資料,而這些事件大多是和使用者的操作行為相關,能夠觸發這類事件的動作如 DOM 節點或內容的變動、滑鼠移動或互動、頁面或元素滾動、鍵盤互動和視窗大小變動。

DOM 結構的改變可以通過瀏覽器提供的 MutationObserver (https://developer.mozilla.org/zh-CN/docs/Web/API/MutationObserver) 介面能監視,觸發引數回撥,獲取到本次 DOM 的變動的節點資訊,進而對資料進行篩選重組等處理。回撥引數的資料結構如下:

let MutationRecord1: MutationRecordObject[];
interface MutationRecordObject {
  /**
   * 如果是屬性變化,則返回 "attributes"* 如果是 characterData 節點變化,則返回 "characterData"* 如果是子節點樹 childList 變化,則返回 "childList"*/
  type: String,
  // 返回被新增的節點。如果沒有節點被新增,則該屬性將是一個空的 NodeList。
  addedNodes: NodeList,
  // 返回被移除的節點。如果沒有節點被移除,則該屬性將是一個空的 NodeList。
  removedNodes: NodeList,
  // 返回被修改的屬性的屬性名,或者 nullattributeName: String | null,
  // 返回被修改屬性的名稱空間,或者 nullattributeNamespace: String | null,
  // 返回被新增或移除的節點之前的兄弟節點,或者 nullpreviousSibling: Node | null,
  // 返回被新增或移除的節點之後的兄弟節點,或者 nullnextSibling: Node | null,
  /** 返回值取決於 MutationRecord.type。
   * 對於屬性 attributes 變化,返回變化之前的屬性值。
   * 對於 characterData 變化,返回變化之前的資料。
   * 對於子節點樹 childList 變化,返回 null*/
  oldValue: String | null,
}

record 收集的 Oplog 資料結構如下圖所示:

let Oplog: OplogObject[];
interface OplogObject {
  /** 返回值取決於收集的事件型別
   * DomContentLoaded: 0, Load: 1,
   * FullSnapshot: 2, IncrementalSnapshot: 3,
   * Meta: 4, Custom: 5, Plugin: 6
  */
  type: Number,
  data: {
    // 返回新增的節點資料
    adds: [],
    // 返回修改的節點屬性資料
    attributes: [],
    // 返回移除的節點屬性資料
    removes: [],
    /** 返回值取決於增量資料的增量型別
     * Mutation: 0, MouseMove: 1,
     * MouseInteraction: 2, Scroll: 3,
     * ViewportResize: 4, Input: 5,
     * TouchMove: 6, MediaInteraction: 7,
     * StyleSheetRule: 8, CanvasMutation: 9,
     * Font: 10, Log: 11,
     * Drag: 12, StyleDeclaration: 13
    **/
    source: Number,
    // 返回當前修改的值,無則不返回
    text: String | undefined,
  },
  // 當前時間戳
  timestamp: Number,
}
  • replay

基於初始化的快照資料和增量資料,將其按照對應的時間戳一一回放。由於一開始建立快照時已經禁止了指令碼執行,所以可以通過 iframe 作為容器來重建 DOM 全量快照 ,並且通過 sanbox 屬性禁止了指令碼執行、彈出窗和表單提交之類的操作。把 Oplog 放入操作佇列中,按照每個的時間戳先後進行排序,再使用定時器 requestAnimationFrame 回放 Oplog 快照。

rrweb-player

為 rrweb 提供一套 UI 控制元件,提供基於 GUI 的暫停、快進、拖拽至任意時間點播放等功能。

相關文章