// offscreen.js - Handles webkitSpeechRecognition in isolation let recognition = null; let isRecording = false; // Initialize recognition early if possible, or wait until start function initRecognition(language) { if (recognition) return recognition; const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition; if (!SpeechRecognition) { throw new Error('Speech recognition not supported'); } recognition = new SpeechRecognition(); recognition.continuous = true; recognition.interimResults = true; recognition.lang = language || 'ar-SA'; recognition.maxAlternatives = 1; recognition.onresult = (event) => { let interimText = ''; let finalText = ''; for (let i = event.resultIndex; i < event.results.length; i++) { const result = event.results[i]; if (result.isFinal) { finalText += result[0].transcript + ' '; } else { interimText += result[0].transcript; } } // Send results back to background script chrome.runtime.sendMessage({ type: 'OFFSCREEN_RECORDING_RESULT', payload: { interimText, finalText } }); }; recognition.onerror = (event) => { console.error('[Offscreen] Recognition error:', event.error); // Prevent infinite restart loop on fatal errors if (event.error !== 'no-speech') { isRecording = false; if (typeof stopMediaTracks === 'function') stopMediaTracks(); } chrome.runtime.sendMessage({ type: 'OFFSCREEN_RECORDING_ERROR', payload: { error: event.error } }); }; recognition.onend = () => { // If we still want to be recording, restart if (isRecording) { try { recognition.start(); } catch (e) { console.warn('[Offscreen] Restart failed:', e); } } else { chrome.runtime.sendMessage({ type: 'OFFSCREEN_RECORDING_END' }); } }; return recognition; } let localStream = null; function stopMediaTracks() { if (localStream) { localStream.getTracks().forEach(track => track.stop()); localStream = null; } } // Listen for messages from background script chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { if (message.type === 'START_RECORDING') { const lang = message.payload?.language || 'ar-SA'; // 1. Get an audio stream first. This is REQUIRED in offscreen documents // to actually activate the microphone and ensure permissions are active. navigator.mediaDevices.getUserMedia({ audio: true }) .then((stream) => { localStream = stream; try { if (!recognition || recognition.lang !== lang) { recognition = initRecognition(lang); } if (!isRecording) { recognition.start(); isRecording = true; sendResponse({ success: true }); } else { sendResponse({ success: true, warning: 'Already recording' }); } } catch (e) { console.error('[Offscreen] Failed to start:', e); sendResponse({ success: false, error: e.message }); } }) .catch((err) => { console.error('[Offscreen] getUserMedia failed:', err); // The user hasn't granted permission yet, or hardware error sendResponse({ success: false, error: 'not-allowed' }); chrome.runtime.sendMessage({ type: 'OFFSCREEN_RECORDING_ERROR', payload: { error: 'not-allowed' } }); }); return true; // Keep channel open for async sendResponse } if (message.type === 'STOP_RECORDING') { if (isRecording && recognition) { isRecording = false; try { recognition.stop(); } catch (e) { console.warn('[Offscreen] Stop failed:', e); } } stopMediaTracks(); sendResponse({ success: true }); return true; } });