Chrome MV3录屏扩展getUserMedia报NotAllowedError: Invalid state错误
Chrome Manifest V3 屏幕录制扩展 Invalid state 错误解决方案
预期实现目标
- 扩展popup页放置录制启动按钮
- 点击按钮后后台启动录制逻辑,不干扰用户当前浏览
- 在用户当前可见的活动标签页弹出桌面/窗口选择器
- 录制进程常驻后台,普通浏览页面刷新、关闭时录制不中断
原有实现代码
popup 按钮逻辑
document.querySelector('#startFromBackgroundPage') .addEventListener('click', function(event) { chrome.runtime.sendMessage( { event: 'open-bg-page' }, function(response) { console.log(response); }); });
Background 逻辑
chrome.runtime.onMessage.addListener(function(message, sender, sendResponse) { if (message.event === 'open-bg-page') { chrome.tabs.create({ url: 'index.html', selected: false, }); } if (message.event === 'record') { chrome.tabs.query({ currentWindow: true, active: true }, function (tabs) { chrome.desktopCapture.chooseDesktopMedia( ["screen", "window"], tabs[0], function(id, options) { chrome.runtime.sendMessage( {'mediaId': id, options: options}); }); }); } });
录制页逻辑(index.html 配套 app.js)
// 页面加载后发消息请求唤起录制选择器 setTimeout(() => { chrome.runtime.sendMessage( { event: 'record' }, function(response) { console.log(response); }); }, 500); chrome.runtime.onMessage.addListener(function(message, sender, sendResponse) { console.log('on message', message); if (message.mediaId) { setTimeout(() => { onAccessApproved(message.mediaId, message.options); }, 100); } }); // 根据返回的mediaId调用getUserMedia启动录制流 function onAccessApproved(id, options) { if (!id) { console.log('Access rejected.'); return; } var audioConstraint = { mandatory: { chromeMediaSource: 'desktop', chromeMediaSourceId: id } }; if (!options.canRequestAudioTrack) audioConstraint = false; navigator.getUserMedia({ audio: audioConstraint, video: { mandatory: { chromeMediaSource: 'desktop', chromeMediaSourceId: id, maxWidth:screen.width, maxHeight:screen.height} } }, gotStream, getUserMediaError); } function getUserMediaError(error) { document.querySelector('#message').innerHTML = error; console.log('navigator.getUserMedia() error: ', error); }
问题表现
运行代码抛出错误:NotAllowedError: Invalid state。
如果将background.js中chooseDesktopMedia传入的tabs[0]参数替换为sender.tab,功能可正常运行,但此时桌面选择器会在后台隐藏的录制页弹出,不符合交互要求。
错误原因
chrome.desktopCapture.chooseDesktopMedia有严格的调用上下文限制:必须由用户手势(如点击按钮)直接触发调用链路,异步延迟超过手势有效期(通常为1秒)、或者调用链路和用户手势的触发页不匹配,都会抛出Invalid state错误。原有实现中,点击popup按钮后先打开后台tab,等后台tab加载完成再发消息查活动tab、调用选择器,此时用户手势上下文已经失效,且查询到的活动tab没有参与调用链路,权限校验直接失败。- Manifest V3 已禁用常驻后台页,用隐藏普通tab承载录制逻辑不可靠,浏览器会在资源紧张时挂起后台非激活标签页,导致录制中断。
修复方案
- 替换隐藏普通tab方案,使用Chrome官方为MV3提供的
Offscreen API创建离屏文档承载录制逻辑,离屏文档不会被浏览器随意回收,可保证录制进程不中断。 - 调整调用链路:点击popup按钮时,同步获取当前活动标签页ID,直接在用户手势有效期内调用
chooseDesktopMedia,不要通过多层异步消息传递丢失手势上下文。 - 拿到mediaId后定向发送给离屏文档初始化录制流,避免全局消息广播导致的上下文错乱。
核心修正代码
manifest.json 新增权限配置
{ "permissions": ["offscreen", "desktopCapture", "tabs", "activeTab"], "manifest_version": 3 }
popup 按钮点击逻辑修正
document.querySelector('#startFromBackgroundPage') .addEventListener('click', async function(event) { // 点击时同步获取当前活动tab,保留用户手势上下文 const [activeTab] = await chrome.tabs.query({ currentWindow: true, active: true }); // 先创建离屏文档(已存在则跳过) const existingContexts = await chrome.runtime.getContexts({ contextTypes: ['OFFSCREEN_DOCUMENT'] }); if (existingContexts.length === 0) { await chrome.offscreen.createDocument({ url: 'index.html', reasons: ['USER_MEDIA'], justification: '后台屏幕录制' }); } // 直接在手势有效期内调用选择器,目标为当前活动tab chrome.desktopCapture.chooseDesktopMedia( ["screen", "window"], activeTab, (id, options) => { if (!id) return; // 定向将mediaId发给离屏文档启动录制 chrome.runtime.sendMessage({ target: 'offscreen', mediaId: id, options: options }); } ); });
离屏文档(原index.html)逻辑修正,移除主动发消息请求录制的逻辑,只负责接收mediaId启动流
chrome.runtime.onMessage.addListener(function(message, sender, sendResponse) { if (message.target === 'offscreen' && message.mediaId) { onAccessApproved(message.mediaId, message.options); } }); // 原有onAccessApproved、流处理逻辑保持不变
内容的提问来源于stack exchange,提问作者coure2011
相关产品推荐
相关产品推荐

