淺析Web錄屏技術方案與實現


前言

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

什么是有感錄屏?

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

基於 WebRTC 的有感錄屏

常見的有感錄屏方案主要是通過 WebRTC 錄制。WebRTC 是一套基於音視軌的實時數據流傳播的技術方案。由瀏覽器提供的原生 API navigator.mediaDevices.getDisplayMedia 方法實現提示用戶選擇和授權捕獲展示的內容或窗口,進而將獲取 stream (錄制的屏幕音視流)。我們可以對 stream 進行轉化處理,轉成相對應的媒體數據,並將其數據存儲。后續需要回溯該次錄制內容時,則取出媒體數據進行播放。

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

實現初始化錄屏和數據存儲

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

實現 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 中,便可以回放到錄制結果,回溯的視頻效果如下:

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

// 回放錄制內容
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();
}

 

瀏覽器兼容性

什么是無感錄屏?

無感錄屏指在用戶無感知的情況,對用戶在頁面上的操作進行錄制。實現上與有感錄制區別在於,無感錄制通常是利用記錄頁面的 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 三部分組成,並且提供了動作篩選,數據加密、數據壓縮、數據切片、屏蔽元素等功能。

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)。

視圖的變化可通過全局事件監聽和事件代理方法收集增量數據,而這些事件大多是和用戶的操作行為相關,能夠觸發這類事件的動作如 DOM 節點或內容的變動、鼠標移動或交互、頁面或元素滾動、鍵盤交互和窗口大小變動。

DOM 結構的改變可以通過瀏覽器提供的 MutationObserver 接口能監視,觸發參數回調,獲取到本次 DOM 的變動的節點信息,進而對數據進行篩選重組等處理。回調參數的數據結構如下:

let MutationRecord1: MutationRecordObject[];
interface MutationRecordObject {
  /**
   * 如果是屬性變化,則返回 "attributes";
   * 如果是 characterData 節點變化,則返回 "characterData";
   * 如果是子節點樹 childList 變化,則返回 "childList"。
  */
  type: String,
  // 返回被添加的節點。如果沒有節點被添加,則該屬性將是一個空的 NodeList。
  addedNodes: NodeList,
  // 返回被移除的節點。如果沒有節點被移除,則該屬性將是一個空的 NodeList。
  removedNodes: NodeList,
  // 返回被修改的屬性的屬性名,或者 null。
  attributeName: String | null,
  // 返回被修改屬性的命名空間,或者 null。
  attributeNamespace: String | null,
  // 返回被添加或移除的節點之前的兄弟節點,或者 null。
  previousSibling: Node | null,
  // 返回被添加或移除的節點之后的兄弟節點,或者 null。
  nextSibling: 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 的暫停、快進、拖拽至任意時間點播放等功能。

總結

文章從有感和無感兩個角度來淺析錄屏方案的實現。頁面錄屏的應用場景場景比較豐富,有感錄制常見用於網頁線上考試、直播和語音視頻通話等實時交互場景,而無感錄制則更多應用在重要操作記錄、bug 重現場景和產品運營分析用戶習慣等場景,二者各有千秋。基於用戶數據的安全和敏感,目前政采雲傾向采用有感錄制進行試點試用,避免引起安全糾紛。在錄屏技術方案不斷地完善和趨向成熟的同時,我們也應尊重用戶的數據安全和隱私,選擇更合適自身場景的方案使用。

參考

rrweb

如何用 JS 實現頁面錄制與回放

 

轉自https://www.zoo.team/article/webrtc-screen


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM