commit fbca7ab31dfe4dc857390f5d82ea20cf823efca2
parent de1dd7f2705c63e7bac5df609a434dd2f8e8df8f
Author: Yubin Jamora <yjamora@mozilla.com>
Date: Fri, 3 Oct 2025 18:21:05 +0000
Bug 1906256 - allow to use mic in chatbot if the perm is already granted r=Mardak,firefox-ai-ml-reviewers,emz
Differential Revision: https://phabricator.services.mozilla.com/D261049
Diffstat:
2 files changed, 159 insertions(+), 0 deletions(-)
diff --git a/browser/actors/WebRTCParent.sys.mjs b/browser/actors/WebRTCParent.sys.mjs
@@ -534,6 +534,18 @@ function prompt(aActor, aBrowser, aRequest) {
}
}
+ // If the request comes from a sidebar,
+ // the user already gave a persistent permission, skip showing a notification
+ // otherwise deny request.
+ if (isSidebar(aBrowser)) {
+ if (!aActor.checkRequestAllowed(aRequest, principal, aBrowser)) {
+ aActor.denyRequest(aRequest);
+ return;
+ }
+
+ return;
+ }
+
// If the user has already denied access once in this tab,
// deny again without even showing the notification icon.
for (const type of requestTypes) {
@@ -1639,3 +1651,15 @@ function onCameraPromptShown(doc, isHandlingUserInput) {
// matching the user selection.
webrtcPreview?.startPreview({ deviceId, mediaSource: "camera" });
}
+
+function isSidebar(browser) {
+ const sidebarBrowser =
+ browser.browsingContext?.topChromeWindow?.SidebarController?.browser;
+ if (!sidebarBrowser) {
+ return false;
+ }
+
+ const nestedBrowsers =
+ sidebarBrowser.contentDocument.querySelectorAll("browser");
+ return Array.from(nestedBrowsers).some(b => b === browser);
+}
diff --git a/browser/components/genai/tests/browser/browser_chat_sidebar.js b/browser/components/genai/tests/browser/browser_chat_sidebar.js
@@ -352,3 +352,138 @@ add_task(async function test_pip_actor_not_chat_sidebar() {
await SidebarController.hide();
});
});
+
+add_task(
+ async function test_chatbot_microphone_access_if_persistent_perm_already_granted_in_tab() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["media.navigator.streams.fake", true],
+ ["browser.ml.chat.provider", "https://example.org"],
+ ],
+ });
+
+ await BrowserTestUtils.withNewTab("https://example.org", async browser => {
+ const { principal, rawId } = await SpecialPowers.spawn(
+ browser,
+ [],
+ async () => {
+ const stream = await content.navigator.mediaDevices.getUserMedia({
+ audio: true,
+ });
+ const track = stream.getAudioTracks()[0];
+ const id = track.getSettings().deviceId || "default";
+ stream.getTracks().forEach(t => t.stop());
+ return {
+ principal: content.document.nodePrincipal,
+ rawId: id,
+ };
+ }
+ );
+
+ Assert.ok(rawId, "Got microphone rawId from the tab");
+
+ const key = "microphone";
+ SitePermissions.setForPrincipal(
+ principal,
+ key,
+ SitePermissions.ALLOW,
+ SitePermissions.SCOPE_PERSISTENT,
+ browser
+ );
+
+ await SidebarController.show("viewGenaiChatSidebar");
+
+ const { document } = SidebarController.browser.contentWindow;
+ const chatbotBrowserContainer =
+ document.getElementById("browser-container");
+ const chatbotBrowser = chatbotBrowserContainer.querySelector("browser");
+
+ await BrowserTestUtils.browserLoaded(chatbotBrowser, false, url => {
+ return new URL(url).origin === "https://example.org";
+ });
+
+ const chatbotPrincipal = await SpecialPowers.spawn(
+ chatbotBrowser,
+ [],
+ async () => content.document.nodePrincipal
+ );
+
+ let shown = false;
+ let onShown = () => {
+ shown = true;
+ };
+ PopupNotifications.panel.addEventListener("popupshown", onShown);
+
+ await SpecialPowers.spawn(chatbotBrowser, [rawId], async id => {
+ const { WebRTCChild } = SpecialPowers.ChromeUtils.importESModule(
+ "resource:///actors/WebRTCChild.sys.mjs"
+ );
+
+ const mic = [
+ {
+ type: "audioinput",
+ rawName: "fake mic",
+ rawId: id,
+ id,
+ QueryInterface: ChromeUtils.generateQI([Ci.nsIMediaDevice]),
+ mediaSource: "microphone",
+ },
+ ];
+
+ const req = {
+ type: "getUserMedia",
+ windowID: content.windowGlobalChild.outerWindowId,
+ isSecure: true,
+ isHandlingUserInput: true,
+
+ audioInputDevices: mic,
+ videoInputDevices: [],
+ audioOutputDevices: [],
+
+ deviceIndex: 0,
+ devices: mic,
+
+ getConstraints: () => ({ audio: true }),
+ getAudioInputOptions: () => ({ deviceId: id }),
+ getVideoInputOptions: () => ({}),
+ getAudioOutputOptions: () => ({}),
+ };
+
+ WebRTCChild.observe(req, "getUserMedia:request");
+ });
+
+ const sidebarMicPerm = SitePermissions.getForPrincipal(
+ chatbotPrincipal,
+ key,
+ chatbotBrowser
+ );
+
+ is(
+ sidebarMicPerm.state,
+ SitePermissions.ALLOW,
+ "Sidebar chatbot has granted mic allow"
+ );
+ is(
+ sidebarMicPerm.scope,
+ SitePermissions.SCOPE_PERSISTENT,
+ "sidebar chatbot mic access is persistent"
+ );
+ Assert.ok(
+ !shown,
+ "PopupNotification didn't fire because the mic access has been granted"
+ );
+
+ PopupNotifications.panel.removeEventListener("popupshown", onShown);
+ SitePermissions.removeFromPrincipal(principal, key, browser);
+ SitePermissions.removeFromPrincipal(
+ chatbotPrincipal,
+ key,
+ chatbotBrowser
+ );
+ });
+
+ await SidebarController.hide();
+ await SpecialPowers.popPrefEnv();
+ Services.fog.testResetFOG();
+ }
+);