From ee02afec563c28a290993515d7f623bce999a5b3 Mon Sep 17 00:00:00 2001 From: Quentin Fuxa Date: Thu, 4 Sep 2025 23:58:48 +0200 Subject: [PATCH] workaround to get the list of microphones in the extension --- chrome-extension/README.md | 6 +- chrome-extension/background.js | 9 + chrome-extension/example_tab_capture.js | 315 ------------------ chrome-extension/icons/icon128.png | Bin 0 -> 5972 bytes chrome-extension/icons/icon16.png | Bin 0 -> 376 bytes chrome-extension/icons/icon32.png | Bin 0 -> 823 bytes chrome-extension/icons/icon48.png | Bin 0 -> 1395 bytes .../{web => }/live_transcription.js | 71 +++- chrome-extension/manifest.json | 52 ++- chrome-extension/popup.html | 7 +- chrome-extension/service-worker.js | 249 -------------- chrome-extension/welcome.html | 12 + 12 files changed, 122 insertions(+), 599 deletions(-) create mode 100644 chrome-extension/background.js delete mode 100644 chrome-extension/example_tab_capture.js create mode 100644 chrome-extension/icons/icon128.png create mode 100644 chrome-extension/icons/icon16.png create mode 100644 chrome-extension/icons/icon32.png create mode 100644 chrome-extension/icons/icon48.png rename chrome-extension/{web => }/live_transcription.js (93%) delete mode 100644 chrome-extension/service-worker.js create mode 100644 chrome-extension/welcome.html diff --git a/chrome-extension/README.md b/chrome-extension/README.md index ab353c3..3c4298a 100644 --- a/chrome-extension/README.md +++ b/chrome-extension/README.md @@ -9,5 +9,9 @@ Capture the audio of your current tab, transcribe or translate it using Whisperl ## Devs: -- Impossible to capture audio from tabs if extension is a pannel, unfortunately: https://issues.chromium.org/issues/40926394 +- Impossible to capture audio from tabs if extension is a pannel, unfortunately: +- https://issues.chromium.org/issues/40926394 +- https://groups.google.com/a/chromium.org/g/chromium-extensions/c/DET2SXCFnDg +- https://issues.chromium.org/issues/40916430 + - To capture microphone in an extension, there are tricks: https://github.com/justinmann/sidepanel-audio-issue , https://medium.com/@lynchee.owo/how-to-enable-microphone-access-in-chrome-extensions-by-code-924295170080 (comments) diff --git a/chrome-extension/background.js b/chrome-extension/background.js new file mode 100644 index 0000000..b62268a --- /dev/null +++ b/chrome-extension/background.js @@ -0,0 +1,9 @@ +chrome.runtime.onInstalled.addListener((details) => { + if (details.reason.search(/install/g) === -1) { + return + } + chrome.tabs.create({ + url: chrome.runtime.getURL("welcome.html"), + active: true + }) +}) \ No newline at end of file diff --git a/chrome-extension/example_tab_capture.js b/chrome-extension/example_tab_capture.js deleted file mode 100644 index 8095371..0000000 --- a/chrome-extension/example_tab_capture.js +++ /dev/null @@ -1,315 +0,0 @@ -const extend = function() { //helper function to merge objects - let target = arguments[0], - sources = [].slice.call(arguments, 1); - for (let i = 0; i < sources.length; ++i) { - let src = sources[i]; - for (key in src) { - let val = src[key]; - target[key] = typeof val === "object" - ? extend(typeof target[key] === "object" ? target[key] : {}, val) - : val; - } - } - return target; -}; - -const WORKER_FILE = { - wav: "WavWorker.js", - mp3: "Mp3Worker.js" -}; - -// default configs -const CONFIGS = { - workerDir: "/workers/", // worker scripts dir (end with /) - numChannels: 2, // number of channels - encoding: "wav", // encoding (can be changed at runtime) - - // runtime options - options: { - timeLimit: 1200, // recording time limit (sec) - encodeAfterRecord: true, // process encoding after recording - progressInterval: 1000, // encoding progress report interval (millisec) - bufferSize: undefined, // buffer size (use browser default) - - // encoding-specific options - wav: { - mimeType: "audio/wav" - }, - mp3: { - mimeType: "audio/mpeg", - bitRate: 192 // (CBR only): bit rate = [64 .. 320] - } - } -}; - -class Recorder { - - constructor(source, configs) { //creates audio context from the source and connects it to the worker - extend(this, CONFIGS, configs || {}); - this.context = source.context; - if (this.context.createScriptProcessor == null) - this.context.createScriptProcessor = this.context.createJavaScriptNode; - this.input = this.context.createGain(); - source.connect(this.input); - this.buffer = []; - this.initWorker(); - } - - isRecording() { - return this.processor != null; - } - - setEncoding(encoding) { - if(!this.isRecording() && this.encoding !== encoding) { - this.encoding = encoding; - this.initWorker(); - } - } - - setOptions(options) { - if (!this.isRecording()) { - extend(this.options, options); - this.worker.postMessage({ command: "options", options: this.options}); - } - } - - startRecording() { - if(!this.isRecording()) { - let numChannels = this.numChannels; - let buffer = this.buffer; - let worker = this.worker; - this.processor = this.context.createScriptProcessor( - this.options.bufferSize, - this.numChannels, this.numChannels); - this.input.connect(this.processor); - this.processor.connect(this.context.destination); - this.processor.onaudioprocess = function(event) { - for (var ch = 0; ch < numChannels; ++ch) - buffer[ch] = event.inputBuffer.getChannelData(ch); - worker.postMessage({ command: "record", buffer: buffer }); - }; - this.worker.postMessage({ - command: "start", - bufferSize: this.processor.bufferSize - }); - this.startTime = Date.now(); - } - } - - cancelRecording() { - if(this.isRecording()) { - this.input.disconnect(); - this.processor.disconnect(); - delete this.processor; - this.worker.postMessage({ command: "cancel" }); - } - } - - finishRecording() { - if (this.isRecording()) { - this.input.disconnect(); - this.processor.disconnect(); - delete this.processor; - this.worker.postMessage({ command: "finish" }); - } - } - - cancelEncoding() { - if (this.options.encodeAfterRecord) - if (!this.isRecording()) { - this.onEncodingCanceled(this); - this.initWorker(); - } - } - - initWorker() { - if (this.worker != null) - this.worker.terminate(); - this.onEncoderLoading(this, this.encoding); - this.worker = new Worker(this.workerDir + WORKER_FILE[this.encoding]); - let _this = this; - this.worker.onmessage = function(event) { - let data = event.data; - switch (data.command) { - case "loaded": - _this.onEncoderLoaded(_this, _this.encoding); - break; - case "timeout": - _this.onTimeout(_this); - break; - case "progress": - _this.onEncodingProgress(_this, data.progress); - break; - case "complete": - _this.onComplete(_this, data.blob); - } - } - this.worker.postMessage({ - command: "init", - config: { - sampleRate: this.context.sampleRate, - numChannels: this.numChannels - }, - options: this.options - }); - } - - onEncoderLoading(recorder, encoding) {} - onEncoderLoaded(recorder, encoding) {} - onTimeout(recorder) {} - onEncodingProgress(recorder, progress) {} - onEncodingCanceled(recorder) {} - onComplete(recorder, blob) {} - -} - -const audioCapture = (timeLimit, muteTab, format, quality, limitRemoved) => { - chrome.tabCapture.capture({audio: true}, (stream) => { // sets up stream for capture - let startTabId; //tab when the capture is started - let timeout; - let completeTabID; //tab when the capture is stopped - let audioURL = null; //resulting object when encoding is completed - chrome.tabs.query({active:true, currentWindow: true}, (tabs) => startTabId = tabs[0].id) //saves start tab - const liveStream = stream; - const audioCtx = new AudioContext(); - const source = audioCtx.createMediaStreamSource(stream); - let mediaRecorder = new Recorder(source); //initiates the recorder based on the current stream - mediaRecorder.setEncoding(format); //sets encoding based on options - if(limitRemoved) { //removes time limit - mediaRecorder.setOptions({timeLimit: 10800}); - } else { - mediaRecorder.setOptions({timeLimit: timeLimit/1000}); - } - if(format === "mp3") { - mediaRecorder.setOptions({mp3: {bitRate: quality}}); - } - mediaRecorder.startRecording(); - - function onStopCommand(command) { //keypress - if (command === "stop") { - stopCapture(); - } - } - function onStopClick(request) { //click on popup - if(request === "stopCapture") { - stopCapture(); - } else if (request === "cancelCapture") { - cancelCapture(); - } else if (request.cancelEncodeID) { - if(request.cancelEncodeID === startTabId && mediaRecorder) { - mediaRecorder.cancelEncoding(); - } - } - } - chrome.commands.onCommand.addListener(onStopCommand); - chrome.runtime.onMessage.addListener(onStopClick); - mediaRecorder.onComplete = (recorder, blob) => { - audioURL = window.URL.createObjectURL(blob); - if(completeTabID) { - chrome.tabs.sendMessage(completeTabID, {type: "encodingComplete", audioURL}); - } - mediaRecorder = null; - } - mediaRecorder.onEncodingProgress = (recorder, progress) => { - if(completeTabID) { - chrome.tabs.sendMessage(completeTabID, {type: "encodingProgress", progress: progress}); - } - } - - const stopCapture = function() { - let endTabId; - //check to make sure the current tab is the tab being captured - chrome.tabs.query({active: true, currentWindow: true}, (tabs) => { - endTabId = tabs[0].id; - if(mediaRecorder && startTabId === endTabId){ - mediaRecorder.finishRecording(); - chrome.tabs.create({url: "complete.html"}, (tab) => { - completeTabID = tab.id; - let completeCallback = () => { - chrome.tabs.sendMessage(tab.id, {type: "createTab", format: format, audioURL, startID: startTabId}); - } - setTimeout(completeCallback, 500); - }); - closeStream(endTabId); - } - }) - } - - const cancelCapture = function() { - let endTabId; - chrome.tabs.query({active: true, currentWindow: true}, (tabs) => { - endTabId = tabs[0].id; - if(mediaRecorder && startTabId === endTabId){ - mediaRecorder.cancelRecording(); - closeStream(endTabId); - } - }) - } - -//removes the audio context and closes recorder to save memory - const closeStream = function(endTabId) { - chrome.commands.onCommand.removeListener(onStopCommand); - chrome.runtime.onMessage.removeListener(onStopClick); - mediaRecorder.onTimeout = () => {}; - audioCtx.close(); - liveStream.getAudioTracks()[0].stop(); - sessionStorage.removeItem(endTabId); - chrome.runtime.sendMessage({captureStopped: endTabId}); - } - - mediaRecorder.onTimeout = stopCapture; - - if(!muteTab) { - let audio = new Audio(); - audio.srcObject = liveStream; - audio.play(); - } - }); -} - - - -//sends reponses to and from the popup menu -chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { - if (request.currentTab && sessionStorage.getItem(request.currentTab)) { - sendResponse(sessionStorage.getItem(request.currentTab)); - } else if (request.currentTab){ - sendResponse(false); - } else if (request === "startCapture") { - startCapture(); - } -}); - -const startCapture = function() { - chrome.tabs.query({active: true, currentWindow: true}, (tabs) => { - // CODE TO BLOCK CAPTURE ON YOUTUBE, DO NOT REMOVE - // if(tabs[0].url.toLowerCase().includes("youtube")) { - // chrome.tabs.create({url: "error.html"}); - // } else { - if(!sessionStorage.getItem(tabs[0].id)) { - sessionStorage.setItem(tabs[0].id, Date.now()); - chrome.storage.sync.get({ - maxTime: 1200000, - muteTab: false, - format: "mp3", - quality: 192, - limitRemoved: false - }, (options) => { - let time = options.maxTime; - if(time > 1200000) { - time = 1200000 - } - audioCapture(time, options.muteTab, options.format, options.quality, options.limitRemoved); - }); - chrome.runtime.sendMessage({captureStarted: tabs[0].id, startTime: Date.now()}); - } - // } - }); -}; - - -chrome.commands.onCommand.addListener((command) => { - if (command === "start") { - startCapture(); - } -}); \ No newline at end of file diff --git a/chrome-extension/icons/icon128.png b/chrome-extension/icons/icon128.png new file mode 100644 index 0000000000000000000000000000000000000000..35ef57b03ccb5964f1b8c685c3632582b5213899 GIT binary patch literal 5972 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%0Dxs$ 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 { + if (details.reason.search(/install/g) === -1) { + return + } + chrome.tabs.create({ + url: chrome.runtime.getURL("welcome.html"), + active: true + }) +}) + function getWaveStroke() { const styles = getComputedStyle(document.documentElement); const v = styles.getPropertyValue("--wave-stroke").trim(); @@ -602,18 +614,47 @@ recordButton.addEventListener("click", toggleRecording); if (microphoneSelect) { microphoneSelect.addEventListener("change", handleMicrophoneChange); } -// document.addEventListener('DOMContentLoaded', async () => { -// try { -// await enumerateMicrophones(); -// } catch (error) { -// console.log("Could not enumerate microphones on load:", error); -// } -// }); -// navigator.mediaDevices.addEventListener('devicechange', async () => { -// console.log('Device change detected, re-enumerating microphones'); -// try { -// await enumerateMicrophones(); -// } catch (error) { -// console.log("Error re-enumerating microphones:", error); -// } -// }); +document.addEventListener('DOMContentLoaded', async () => { + try { + await enumerateMicrophones(); + } catch (error) { + console.log("Could not enumerate microphones on load:", error); + } +}); +navigator.mediaDevices.addEventListener('devicechange', async () => { + console.log('Device change detected, re-enumerating microphones'); + try { + await enumerateMicrophones(); + } catch (error) { + console.log("Error re-enumerating microphones:", error); + } +}); + + +async function run() { + const micPermission = await navigator.permissions.query({ + name: "microphone", + }); + + document.getElementById( + "audioPermission" + ).innerText = `MICROPHONE: ${micPermission.state}`; + + if (micPermission.state !== "granted") { + chrome.tabs.create({ url: "welcome.html" }); + } + + const intervalId = setInterval(async () => { + const micPermission = await navigator.permissions.query({ + name: "microphone", + }); + if (micPermission.state === "granted") { + document.getElementById( + "audioPermission" + ).innerText = `MICROPHONE: ${micPermission.state}`; + clearInterval(intervalId); + } + }, 100); +} + +void run(); diff --git a/chrome-extension/manifest.json b/chrome-extension/manifest.json index a925ee5..2d8e3ab 100644 --- a/chrome-extension/manifest.json +++ b/chrome-extension/manifest.json @@ -1,17 +1,37 @@ { - "manifest_version": 3, - "name": "WhisperLiveKit Tab Capture", - "version": "1.0", - "description": "Capture and transcribe audio from browser tabs using WhisperLiveKit.", - "action": { - "default_title": "WhisperLiveKit Tab Capture", - "default_popup": "popup.html" - }, - "permissions": ["scripting", "tabCapture", "offscreen", "activeTab", "storage"], - "web_accessible_resources": [ - { - "resources": ["requestPermissions.html", "requestPermissions.js"], - "matches": [""] - } - ] -} + "manifest_version": 3, + "name": "WhisperLiveKit Tab Capture", + "version": "1.0", + "description": "Capture and transcribe audio from browser tabs using WhisperLiveKit.", + "background": { + "service_worker": "background.js" + }, + "icons": { + "16": "icons/icon16.png", + "32": "icons/icon32.png", + "48": "icons/icon48.png", + "128": "icons/icon128.png" + }, + "action": { + "default_title": "WhisperLiveKit Tab Capture", + "default_popup": "popup.html" + }, + "permissions": [ + "scripting", + "tabCapture", + "offscreen", + "activeTab", + "storage" + ], + "web_accessible_resources": [ + { + "resources": [ + "requestPermissions.html", + "requestPermissions.js" + ], + "matches": [ + "" + ] + } + ] +} \ No newline at end of file diff --git a/chrome-extension/popup.html b/chrome-extension/popup.html index 1677c5d..ae9f0b3 100644 --- a/chrome-extension/popup.html +++ b/chrome-extension/popup.html @@ -9,6 +9,7 @@ +
@@ -67,7 +68,7 @@
- + \ No newline at end of file diff --git a/chrome-extension/service-worker.js b/chrome-extension/service-worker.js deleted file mode 100644 index 8d69af7..0000000 --- a/chrome-extension/service-worker.js +++ /dev/null @@ -1,249 +0,0 @@ -console.log("Service worker loaded"); - -let isRecording = false; -let currentStreamId = null; - -chrome.runtime.onInstalled.addListener((details) => { - console.log("Extension installed/updated"); -}); - -chrome.action.onClicked.addListener((tab) => { - // Get the current tab ID - const tabId = tab.id; - - // Inject the content script into the current tab - chrome.scripting.executeScript({ - target: { tabId: tabId }, - files: ['style_popup.js'] - }); - - console.log(`Content script injected into tab ${tabId}`); -}); - - -// Handle messages from popup -chrome.runtime.onMessage.addListener(async (message, sender, sendResponse) => { - console.log("Service worker received message:", message); - - try { - switch (message.type) { - case 'start-capture': - const startResult = await startTabCapture(message.tabId, message.websocketUrl); - sendResponse(startResult); - break; - - case 'stop-capture': - const stopResult = await stopTabCapture(); - sendResponse(stopResult); - break; - - case 'get-recording-state': - sendResponse({ isRecording: isRecording }); - break; - - default: - sendResponse({ success: false, error: 'Unknown message type' }); - } - } catch (error) { - console.error('Error handling message:', error); - sendResponse({ success: false, error: error.message }); - } - - return true; // Keep message channel open for async response -}); - -async function startTabCapture(tabId, websocketUrl) { - console.log('Service worker: Starting tab capture process...'); - console.log('Service worker: tabId:', tabId, 'websocketUrl:', websocketUrl); - - try { - if (isRecording) { - console.log('Service worker: Already recording, aborting'); - return { success: false, error: 'Already recording' }; - } - - // Check if offscreen document exists - console.log('Service worker: Checking for existing offscreen document...'); - const existingContexts = await chrome.runtime.getContexts({}); - console.log('Service worker: Found contexts:', existingContexts.length); - - const offscreenDocument = existingContexts.find( - (c) => c.contextType === 'OFFSCREEN_DOCUMENT' - ); - - console.log('Service worker: Offscreen document exists:', !!offscreenDocument); - - // Create offscreen document if it doesn't exist - if (!offscreenDocument) { - console.log('Service worker: Creating offscreen document...'); - try { - await chrome.offscreen.createDocument({ - url: 'offscreen.html', - reasons: ['USER_MEDIA'], - justification: 'Capturing and processing tab audio for transcription' - }); - console.log('Service worker: Offscreen document created successfully'); - - // Wait for offscreen document to initialize - console.log('Service worker: Waiting for offscreen document to initialize...'); - await new Promise(resolve => setTimeout(resolve, 500)); - console.log('Service worker: Offscreen document initialization delay complete'); - - } catch (offscreenError) { - console.error('Service worker: Failed to create offscreen document:', offscreenError); - return { success: false, error: 'Failed to create offscreen document: ' + offscreenError.message }; - } - } - - // Get media stream ID for the tab - console.log('Service worker: Getting media stream ID for tab:', tabId); - try { - currentStreamId = await chrome.tabCapture.getMediaStreamId({ - targetTabId: tabId - }); - console.log('Service worker: Media stream ID:', currentStreamId); - } catch (tabCaptureError) { - console.error('Service worker: Failed to get media stream ID:', tabCaptureError); - return { success: false, error: 'Failed to get media stream ID: ' + tabCaptureError.message }; - } - - if (!currentStreamId) { - console.log('Service worker: No media stream ID returned'); - return { success: false, error: 'Failed to get media stream ID - no stream returned' }; - } - - // Send message to offscreen document to start capture with retry logic - console.log('Service worker: Sending start message to offscreen document...'); - - let response; - let retryCount = 0; - const maxRetries = 5; - - while (!response && retryCount < maxRetries) { - try { - console.log(`Service worker: Attempt ${retryCount + 1}/${maxRetries} to communicate with offscreen document`); - - // Send message to offscreen document without target property - response = await chrome.runtime.sendMessage({ - type: 'start-recording', - target: 'offscreen', - data: { - streamId: currentStreamId, - websocketUrl: websocketUrl - } - }); - - if (!response) { - console.warn(`Service worker: No response from offscreen document, waiting before retry...`); - await new Promise(resolve => setTimeout(resolve, 200)); - retryCount++; - } else { - console.log(`Service worker: Successfully communicated with offscreen document on attempt ${retryCount + 1}`); - } - } catch (sendError) { - console.error(`Service worker: Error sending message to offscreen document (attempt ${retryCount + 1}):`, sendError); - response = { success: false, error: 'Failed to communicate with offscreen document: ' + sendError.message }; - break; - } - } - - console.log('Service worker: Final offscreen document response:', response); - - if (response && response.success) { - isRecording = true; - console.log('Service worker: Recording started successfully'); - - // Notify popup of state change - try { - chrome.runtime.sendMessage({ - type: 'recording-state', - isRecording: true - }); - } catch (e) { - console.warn('Service worker: Could not notify popup of state change:', e); - } - - return { success: true }; - } else { - console.log('Service worker: Offscreen document returned failure'); - return { success: false, error: response?.error || 'Failed to start recording in offscreen document' }; - } - - } catch (error) { - console.error('Service worker: Exception in startTabCapture:', error); - return { success: false, error: 'Exception: ' + error.message }; - } -} - -async function stopTabCapture() { - try { - if (!isRecording) { - return { success: false, error: 'Not currently recording' }; - } - - // Send message to offscreen document to stop capture - const response = await chrome.runtime.sendMessage({ - type: 'stop-recording', - target: 'offscreen' - }); - - isRecording = false; - currentStreamId = null; - - // Notify popup of state change - try { - chrome.runtime.sendMessage({ - type: 'recording-state', - isRecording: false - }); - } catch (e) { - // Popup might be closed, ignore error - } - - return { success: true }; - - } catch (error) { - console.error('Error stopping tab capture:', error); - isRecording = false; - currentStreamId = null; - return { success: false, error: error.message }; - } -} - -// Handle messages from offscreen document -chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { - if (message.target === 'service-worker') { - switch (message.type) { - case 'recording-stopped': - isRecording = false; - currentStreamId = null; - - // Notify popup - try { - chrome.runtime.sendMessage({ - type: 'recording-state', - isRecording: false - }); - } catch (e) { - // Popup might be closed, ignore error - } - break; - - case 'recording-error': - isRecording = false; - currentStreamId = null; - - // Notify popup - try { - chrome.runtime.sendMessage({ - type: 'status-update', - status: 'error', - message: message.error || 'Recording error occurred' - }); - } catch (e) { - // Popup might be closed, ignore error - } - break; - } - } -}); diff --git a/chrome-extension/welcome.html b/chrome-extension/welcome.html new file mode 100644 index 0000000..b95d737 --- /dev/null +++ b/chrome-extension/welcome.html @@ -0,0 +1,12 @@ + + + + Welcome + + + + This page exists to workaround an issue with Chrome that blocks permission + requests from chrome extensions + + +