chrome-extensions-samples 实战:使用 chrome.tabCapture 与 Offscreen Document 实现后台标签页录制
chrome-extensions-samples 实战使用 chrome.tabCapture 与 Offscreen Document 实现后台标签页录制【免费下载链接】chrome-extensions-samplesChrome Extensions Samples项目地址: https://gitcode.com/gh_mirrors/ch/chrome-extensions-samples本指南以 chrome-extensions-samples 仓库中的 sample.tabcapture-recorder 示例为核心系统讲解如何借助 Manifest V3 的 service worker、offscreen document 与chrome.tabCaptureAPI在浏览器后台对当前活动标签页进行录制点击扩展图标开始录制再次点击或 30 秒后自动停止并将录制结果以 WebM 视频形式保存。读完本文你将掌握tabCapture.getMediaStreamId()、offscreen.createDocument()、MediaRecorder的完整调用链以及service worker 与离屏文档如何低开销同步录制状态这一关键设计。示例概览与整体工作流sample.tabcapture-recorder的核心理念是录制不发生在 service worker 中也不发生在可见页面中而是交给一个不可见的离屏文档offscreen document执行。整体流程如下用户点击扩展的 action 图标触发chrome.action.onClicked事件service worker 检查是否已有离屏文档若无则通过chrome.offscreen.createDocument()创建offscreen.htmlservice worker 调用chrome.tabCapture.getMediaStreamId({ targetTabId })获取当前标签页的媒体流 IDservice worker 通过chrome.runtime.sendMessage()将流 ID 发送给离屏文档离屏文档用getUserMedia({ chromeMediaSource: tab })消费该流并用MediaRecorder录制为video/webm再次点击 action或录制满 30 秒后离屏文档自行停止结束录制生成 Blob 并打开新窗口展示下载。关于30 秒后自动停止README 描述的是示例的设计语义见 README仓库中的offscreen.js同时实现了再次点击停止的路径两种方式都会触发recorder.onstop保存录制结果。极简清单manifest.json 的权限与配置示例的 manifest.json 非常精简是理解该方案权限边界的最佳入口{ name: Tab Capture - Recorder, description: Records the current tab in an offscreen document., version: 1, manifest_version: 3, minimum_chrome_version: 116, action: { default_icon: icons/not-recording.png }, background: { service_worker: service-worker.js }, permissions: [tabCapture, offscreen] }要点说明manifest_version: 3本示例仅支持 MV3service worker 作为唯一的后台宿主minimum_chrome_version: 116chrome.tabCapture与chrome.offscreen组合的稳定可用版本底线低于 116 的 Chrome 不建议直接运行本示例permissions: [tabCapture, offscreen]两个权限缺一不可。tabCapture用于获取标签页媒体流offscreen用于创建离屏文档。示例没有申请tabs权限因为chrome.action.onClicked的tab参数已携带tab.idaction.default_icon默认图标为icons/not-recording.png录制开始后 service worker 会动态切换为icons/recording.png作为是否在录制的可视化反馈。service worker 侧创建离屏文档并分发流 IDservice-worker.js 是整条调用链的起点其关键实现如下chrome.action.onClicked.addListener(async (tab) { const existingContexts await chrome.runtime.getContexts({}); let recording false; const offscreenDocument existingContexts.find( (c) c.contextType OFFSCREEN_DOCUMENT ); // If an offscreen document is not already open, create one. if (!offscreenDocument) { await chrome.offscreen.createDocument({ url: offscreen.html, reasons: [USER_MEDIA], justification: Recording from chrome.tabCapture API }); } else { recording offscreenDocument.documentUrl.endsWith(#recording); } if (recording) { chrome.runtime.sendMessage({ type: stop-recording, target: offscreen }); chrome.action.setIcon({ path: icons/not-recording.png }); return; } // Get a MediaStream for the active tab. const streamId await chrome.tabCapture.getMediaStreamId({ targetTabId: tab.id }); // Send the stream ID to the offscreen document to start recording. chrome.runtime.sendMessage({ type: start-recording, target: offscreen, data: streamId }); chrome.action.setIcon({ path: /icons/recording.png }); });这段代码蕴含三个重要的 MV3 设计决策1. 用chrome.runtime.getContexts()探测离屏文档是否已存在。service worker 可能随时被终止因此不能把离屏文档是否已创建的状态存放在内存变量中getContexts()以OFFSCREEN_DOCUMENT为contextType进行过滤是官方推荐的状态探活方式。2. 用 URL hash 作为低带宽状态通道。离屏文档在开始录制后会把window.location.hash设为#recording见后文service worker 通过offscreenDocument.documentUrl.endsWith(#recording)即可判断当前是否正在录制从而决定是发停止消息还是发开始消息。这种设计比把状态写入chrome.storage更简单也避免了 storage 读写可能带来的状态不同步问题。3. 流 ID 由 service worker 获取、由离屏文档消费。getMediaStreamId({ targetTabId: tab.id })在 service worker 中调用通过runtime.sendMessage把streamId传给离屏文档。作为对照仓库中 api-samples/tabCapture 的基础示例演示了另一种消费方式service worker 同样调用tabCapture.getMediaStreamId()但把targetTabId与consumerTabId通过tabs.sendMessage()传给一个可见的 receiver 页面见 service-worker.js。两者对比可知捕获获取流 ID必须在扩展上下文中完成而消费真正用流既可以在可见页面也可以在离屏文档中完成——本示例选择离屏文档是为了实现点击图标即可后台录制无需打开任何可见窗口的用户体验。离屏文档侧消费流、播放与 MediaRecorder 录制offscreen.js 运行在 offscreen.html一个仅引入脚本的极简 HTML 页面中负责实际录制。其消息分发逻辑如下chrome.runtime.onMessage.addListener(async (message) { if (message.target offscreen) { switch (message.type) { case start-recording: startRecording(message.data); break; case stop-recording: stopRecording(); break; default: throw new Error(Unrecognized message:, message.type); } } });startRecording把 streamId 变成可录制的 MediaStreamasync function startRecording(streamId) { if (recorder?.state recording) { throw new Error(Called startRecording while recording is in progress.); } const media await navigator.mediaDevices.getUserMedia({ audio: { mandatory: { chromeMediaSource: tab, chromeMediaSourceId: streamId } }, video: { mandatory: { chromeMediaSource: tab, chromeMediaSourceId: streamId } } }); // Continue to play the captured audio to the user. const output new AudioContext(); const source output.createMediaStreamSource(media); source.connect(output.destination); // Start recording. recorder new MediaRecorder(media, { mimeType: video/webm }); recorder.ondataavailable (event) data.push(event.data); recorder.onstop () { const blob new Blob(data, { type: video/webm }); window.open(URL.createObjectURL(blob), _blank); // Clear state ready for next recording recorder undefined; data []; }; recorder.start(); // Record the current state in the URL... window.location.hash recording; }关键实现细节chromeMediaSource: tabchromeMediaSourceId: streamId这是离屏文档消费tabCapture流的固定写法。getUserMedia的mandatory约束告诉浏览器我要用这个已授权标签页的流其中chromeMediaSourceId必须与 service worker 传来的streamId完全一致同时请求 audio 与 video示例对音频和视频都声明了chromeMediaSource: tab因此录制结果是含音轨的标签页视频AudioContext回放把捕获的音频源连接到output.destination保证录制期间用户仍能听到标签页的声音避免录了但用户听不到的困惑MediaRecorder(media, { mimeType: video/webm })以 WebM 容器格式录制ondataavailable将每个数据块推入data数组onstop时合并为Blob并用URL.createObjectURL生成可下载的 URL通过window.open新窗口展示window.location.hash recording把正在录制这一状态写进文档 URL。之所以不依赖 service worker 内存变量是因为录制期间 service worker 可能已被浏览器终止而离屏文档只要还在运行其 URL 就是可靠的实时状态源——service worker 重启后通过getContexts()读回documentUrl即可还原判断。stopRecording停止录制与清理async function stopRecording() { recorder.stop(); // Stopping the tracks makes sure the recording icon in the tab is removed. recorder.stream.getTracks().forEach((t) t.stop()); // Update current state in URL window.location.hash ; }recorder.stop()触发onstop从而生成 Blob 并打开下载窗口随后停止所有轨道getTracks().forEach(t t.stop())这会移除标签页上的录制指示图标并释放摄像头/标签页捕获资源最后清空 URL hash与 service worker 侧的endsWith(#recording)判断保持同步。源码注释还特别提醒见 offscreen.js在真实生产扩展中应将录制数据写入 IndexedDB 等持久化存储然后关闭离屏文档避免长期驻留不必要的文档。本示例刻意保留离屏文档是为了保证Object URL在window.open后仍然有效同时让示例保持简单易懂。完整调用链与状态同步机制小结将以上两部分串联可以得到完整的时序点击 action │ ▼ service worker: chrome.runtime.getContexts() 检查离屏文档 │不存在 │存在且 hash#recording ▼ ▼ offscreen.createDocument(...) 发 stop-recording 消息 │ │ ▼ ▼ tabCapture.getMediaStreamId() 离屏文档 stopRecording() │ │ ▼ ▼ sendMessage(start-recording, 生成 Blob、打开下载窗口、 streamId) 停止轨道、清空 hash │ ▼ 离屏文档 getUserMedia 消费流 → MediaRecorder 录制 → hash#recording这一设计中最重要的经验是双通道通信控制通道chrome.runtime.sendMessage()service worker 向离屏文档下达start-recording/stop-recording指令状态通道离屏文档 URL 的#recordinghashservice worker 通过getContexts()读取从而在自身被终止重启后仍能准确判断录制状态。这种消息传递 URL 状态的组合避免了在 service worker 中维护易失内存状态也避免了为简单状态引入 storage 同步的复杂度是 MV3 离屏文档场景下值得复用的模式。运行与验证步骤按照 README 的说明本示例的运行步骤如下克隆当前仓库chrome-extensions-samples到本地在 Chrome 中打开chrome://extensions开启开发者模式点击加载已解压的扩展程序选择functional-samples/sample.tabcapture-recorder目录在扩展菜单中把 Tab Capture - Recorder 固定到工具栏点击扩展的 action 图标开始录制当前标签页图标会切换为icons/recording.png再次点击该图标停止录制浏览器会新开一个窗口展示录制的 WebM 视频供保存使用。验证要点录制开始时工具栏图标应从icons/not-recording.png变为icons/recording.png录制结束后恢复对应 service-worker.js 与 service-worker.js 的chrome.action.setIcon调用录制期间标签页会出现系统级的录制指示标识停止后消失对应offscreen.js中停止所有轨道的行为依赖minimum_chrome_version: 116请确认本地 Chrome 版本不低于 116否则chrome.offscreen与tabCapture的组合行为不受保证。延伸阅读仓库内的相关示例api-samples/tabCapturechrome.tabCapture的基础用法展示了在可见 receiver 页面中消费流、并通过tabs.sendMessage()传递targetTabId/consumerTabId的另一种架构functional-samples/cookbook.offscreen-dom离屏文档 DOM 操作的最小可运行示例可用于对照理解chrome.offscreen的通用机制functional-samples/cookbook.offscreen-user-media在离屏文档中获取用户媒体摄像头/麦克风的示例其offscreen目录下的脚本同样使用了getUserMedia约束模式可作为本示例权限与约束写法的补充参考。以上示例共同构成了MV3 下如何使用离屏文档承载多媒体能力的完整参考矩阵本示例解决的是录制标签页tabCapturegetUserMediaMediaRecorder而 cookbook 系列则分别解决了离屏文档中使用 DOM与离屏文档中使用用户媒体等相邻场景可交叉阅读。【免费下载链接】chrome-extensions-samplesChrome Extensions Samples项目地址: https://gitcode.com/gh_mirrors/ch/chrome-extensions-samples创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考