前端可以知道的录制浏览器标签页,没有黑魔法

1,232 阅读7分钟

record3.gif 最近有个录制浏览器标签页的需求,调研发现Screenity浏览器插件就可以实现这个功能,于是就调研了具体的实现方法。

由于我们的功能适配是基于mainfest V2的,所以我们先来看V2如何实现页面录制,注意这里我们只进行浏览器页面的录制,并不涉及桌面,窗口的录制。该部分的录制可以通过navigator.mediaDevices.getDisplayMedia或者chrome.desktopCapture.chooseDesktopMedia来实现。

image.png

manifest V2 实现

主要流程

对于插件来说,我们既可以调用html5提供的录制api也可以调用chrome扩展提供的api。

在插件中不管使用哪种方式获取stream,都必须满足一些条件,不然获取到的stream为null。所以在内容脚本中直接发送消息到后台获取 stream 是无效的。默认只能录制当前聚焦的页面。

经过测试发现有三种方式可以获取到 stream

  • 点击 popup 和插件进行交互(点击后发送消息)
  • 设置右键菜单和插件进行交互
  • 设置快捷键和插件进行交互
// 获取streamId
chrome.tabCapture.getMediaStreamId({
  targetTabId: id,
});

chrome.tabCapture.capture({
  audio: true,
  video: true,
  audioConstraints: {
    mandatory: {
      chromeMediaSource: "tab",
      chromeMediaSourceId: streamId, // 也可以设置当前tabId, 如果未指定,默认捕获当前活跃标签页
    },
  },
  videoConstraints: {
    mandatory: {
      chromeMediaSource: "tab",
      chromeMediaSourceId: streamId, // 也可以设置当前tabId, 如果未指定,默认捕获当前活跃标签页
    },
  },
});

// 或者
navigator.mediaDevices.getUserMedia({
    audio: {
      mandatory: {
        chromeMediaSource: "tab",
        chromeMediaSourceId: streamId,
      },
    },
    video: {
      mandatory: {
        chromeMediaSource: "tab",
        chromeMediaSourceId: streamId,
        maxWidth: width,
        maxHeight: height,
        maxFrameRate: fps,
      },
    },
});

获取 stream 后,通过MediaRecorder创建录制对象,监听dataavailable事件就可以拿到对应的录制数据,然后我们就可以通过可写流将其写入本地文件。这里我们可以调用showSaveFilePicker选择本地目录边录边写入文件,防止因内存过大造成卡死。

//  选择要保存的文件
const fileHandle = await window.showSaveFilePicker({
    suggestedName: `recording-${Date.now()}.webm`,
    types: [{
      description: 'record tab',
      accept: { 'video/webm': ['.webm'] }
    }]
});
const writer = await fileHandle.createWritable();
recorder.ondataavailable = async (e) => {
  try {
    await writer.write(e.data);
  } catch (error) {
    console.error('写入失败:', error);
    stopRecording(tabId);
  }
};

监听录制停止,然后通过处理录制文件(.webm),将webm 转换为mp4, 因为录制写入的文件没有时间进度且不支持拖拽进度条。

// 方式一: 手动记录录制时间,通过fix-webm-duration进行修复
recorder.onstop = async () => {
  const recordingDuration = Date.now() - startTime;
  // 修复时长元数据
  fixWebmDuration(
    new Blob([arrayBuffer], { type: "video/webm" }),
    recordingDuration,
    async (fixedWebm) => {
      // 然后在进行下载
    },
    { logger: false }
  );
};

// 方式二:通过ffmpeg进行处理(转换时间根据文件大小决定)
ffmpeg -i input.webm -c copy video.mp4

参数配置

具体请参考这里

interface MediaStreamConstraints {
  video?: boolean | MediaTrackConstraints; // 视频配置
  audio?: boolean | MediaTrackConstraints; // 音频配置
  preferCurrentTab?: boolean; // 是否优先捕获当前标签(Chrome 特有,用于屏幕共享)
}

video: {
  // 基础参数
  width: { ideal: 1280 },       // 理想宽度
  height: { min: 720 },         // 最小高度
  frameRate: { max: 30 },       // 最大帧率 默认值 30
  aspectRatio: 1.7777777777777777,     // 宽高比(16:9)

  // 设备选择
  facingMode: "environment",   // 后置摄像头("user" 为前置)
  deviceId: "摄像头设备ID",     // 指定摄像头设备 enumerateDevices()获取

  // 高级功能
  noiseSuppression: false,       // 降噪
  zoom: { exact: 2 },           // 缩放倍数
  focusMode: "continuous",      // 对焦模式
  resizeMode: "crop-and-scale", // 缩放策略

  // 浏览器兼容性扩展(部分浏览器支持)
  latency: 0,                   // 延迟配置(实验性)
}

audio: {
  // 基础参数
  sampleRate: 48000,            // 采样率(Hz)
  sampleSize: 16,               // 采样位数(bit)
  channelCount: 2,              // 声道数(1=单声道,2=立体声)

  // 设备选择
  deviceId: "麦克风设备ID",     // 指定麦克风设备 enumerateDevices()获取
  groupId: "设备组ID",          // 同一物理设备的组 ID

  // 高级功能
  echoCancellation: false,      // 回音消除
  autoGainControl: false,       // 自动增益
  noiseSuppression: false,      // 降噪
  latency: 0.01,                // 延迟(实验性)

  // 浏览器兼容性扩展(部分浏览器支持)
  suppressLocalAudioPlayback: true // 是否禁止本地播放(WebRTC 场景)
}

其中一些参数可以设置约束条件

  • 精确匹配:exact: value(若设备不支持,直接报错)
  • 理想值:ideal: value(尝试优先匹配,但不强制)
  • 范围限制:min, max

mainfest V2 和 manifest V3后台脚本的区别

主要区别

Manifest V2

  • 运行环境:独立的 HTML 页面(background.html),通过 background.scripts 或 background.page 定义。
  • 生命周期:持久化(长期运行),即使没有事件触发也不会被终止。
  • 全局对象:支持完整的 Web API,包括 windowdocumentXMLHttpRequest 等。

Manifest V3

  • 运行环境:基于 Service Worker 的脚本(service_worker 字段定义),无 HTML 页面。
  • 生命周期:按需启动,空闲时终止。脚本仅在处理事件时运行,最长存活时间约 5 分钟。
  • 全局对象:无法访问 DOM 相关 API(如 windowdocument),仅支持有限的 Web API(如 fetchCacheStorage)。

API 兼容性变化

  • Web Request API
    • V2:可通过 chrome.webRequest 拦截和修改网络请求。
    • V3:仅支持只读模式,修改请求需使用新的 Declarative Net Request API(静态规则声明)。
  • Background Page 的全局状态
    • V2:可通过全局变量(如 window.myData)保存状态。
    • V3:Service Worker 无法持久化全局状态,需使用 chrome.storage.local 存储数据。
  • 长连接通信(如 chrome.extension.connect
    • V2:支持长连接(Port)保持后台与内容脚本的通信。
    • V3:推荐短连接chrome.runtime.sendMessage),因 Service Worker 可能随时终止。

新增或改进的 API

  • chrome.scripting API
    替代 V2 的 chrome.tabs.executeScript,支持动态注入脚本和 CSS。
  • chrome.action API
    统一 V2 的 chrome.browserAction 和 chrome.pageAction,简化扩展图标管理。
  • chrome.alarms
    替代 setTimeout/setInterval,支持跨 Service Worker 生命周期的定时任务。

所以对于V3的实现和V2有很大不同,API不能直接在V3中使用,这就需要扩展页面进行过渡了。

manifest V3实现

由于后台脚本非常驻,所以我们已经不能再后台脚本中直接调用录制tab页的api了。我们需要开启一个扩展页面进行录制,等到结束录制后关闭临时开启的扩展页面。

function openTempRecordTab(recordPageTabId) {
  chrome.tabs
  .create({
    url: "recorder.html",
    pinned: true,
    index: 0,
  }, (tab) => {
    chrome.tabs.onUpdated.addListener(async function _(
      tabId,
      changeInfo,
      updatedTab
    ) {
      if (tabId === tab.id && changeInfo.status === "complete") {
        tempTabsMap.set(recordPageTabId, tabId) // tabId: tempPageTabId
        chrome.tabs.onUpdated.removeListener(_);
        chrome.tabs.sendMessage(tab.id, {
          type: "loaded",
          tabId: recordPageTabId, // 录制页面tabId
        })
      }
    });
  })
}

其他逻辑和v2一样,只是多了几步扩展页面和后台脚本之间的通信而已。

V2和V3完整源码

请访问github

往期年度总结

往期文章

专栏文章

🔥如果此文对你有帮助的话,欢迎💗关注、👍点赞、⭐收藏✍️评论,    支持一下博主~

公众号:全栈追逐者,不定期的更新内容,关注不错过哦!