// ============================================================ // Volume Booster — Background Service Worker // In Manifest V3 kann der Service Worker selbst KEIN Audio // capturen (kein DOM, kein getUserMedia). Er holt nur eine // Stream-ID via chrome.tabCapture.getMediaStreamId und reicht // sie an ein Offscreen-Dokument weiter, das den GainNode hält. // ============================================================ let creating = null; // Promise-Lock, damit nicht zwei Offscreen-Docs entstehen // Badge im Toolbar-Icon aktualisieren (pro Tab). // Bei 100 % wird das Badge ausgeblendet, da das der Normalwert ist. function updateBadge(tabId, value) { const pct = Math.round(value * 100); const text = pct === 100 ? "" : String(pct); chrome.action.setBadgeText({ tabId, text }); let color; if (value > 1) color = "#ff9d2f"; // Boost: Amber else if (value === 0) color = "#ff5d5d"; // Mute: Rot else color = "#8b8f99"; // Dämpfung: Grau chrome.action.setBadgeBackgroundColor({ tabId, color }); try { chrome.action.setBadgeTextColor({ tabId, color: "#1a1c22" }); } catch { /* setBadgeTextColor nicht überall verfügbar */ } } async function ensureOffscreen() { const contexts = await chrome.runtime.getContexts({ contextTypes: ["OFFSCREEN_DOCUMENT"], }); if (contexts.length > 0) return; if (creating) { await creating; return; } creating = chrome.offscreen.createDocument({ url: "offscreen.html", reasons: ["USER_MEDIA"], justification: "Tab-Audio verstärken (Volume Booster)", }); await creating; creating = null; } // Nachrichten aus dem Popup chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => { // Nachrichten fürs Offscreen-Dokument hier nicht behandeln if (msg.target === "offscreen") return false; (async () => { try { if (msg.type === "SET_VOLUME") { const { tabId, value } = msg; // value: 0.0 – 6.0 await ensureOffscreen(); if (value === 1) { // Exakt 100 %: Boost beenden, kein Capture nötig const res = await chrome.runtime.sendMessage({ target: "offscreen", type: "STOP", tabId, }); updateBadge(tabId, 1); sendResponse(res || { ok: true, boosting: false }); return; } // ERST prüfen, ob dieser Tab schon einen aktiven Stream hat. // Wenn ja, NUR den Gain ändern — keine neue Stream-ID holen! // (Eine getMediaStreamId-ID ist einmalig und der Tab kann nicht // ein zweites Mal gecaptured werden, solange der erste Stream läuft.) const state = await chrome.runtime.sendMessage({ target: "offscreen", type: "STATE", tabId, }); if (state && state.boosting) { // Tab läuft schon: nur Lautstärke anpassen const gainRes = await chrome.runtime.sendMessage({ target: "offscreen", type: "GAIN", tabId, value, }); if (gainRes && gainRes.ok) { updateBadge(tabId, value); sendResponse(gainRes); return; } // Falls der Stream zwischendurch verloren ging: unten neu capturen } // Tab noch nicht aktiv: jetzt eine frische Stream-ID holen const streamId = await chrome.tabCapture.getMediaStreamId({ targetTabId: tabId, }); const applyRes = await chrome.runtime.sendMessage({ target: "offscreen", type: "APPLY", tabId, streamId, value, }); if (applyRes && applyRes.ok) updateBadge(tabId, value); sendResponse( applyRes || { ok: false, error: "Keine Antwort vom Offscreen-Dokument" } ); } else if (msg.type === "GET_STATE") { await ensureOffscreen(); const res = await chrome.runtime.sendMessage({ target: "offscreen", type: "STATE", tabId: msg.tabId, }); if (res && res.boosting) updateBadge(msg.tabId, res.value); sendResponse(res || { ok: true, boosting: false, value: 1 }); } else if (msg.type === "RESET") { await ensureOffscreen(); const res = await chrome.runtime.sendMessage({ target: "offscreen", type: "STOP", tabId: msg.tabId, }); updateBadge(msg.tabId, 1); sendResponse(res || { ok: true }); } } catch (err) { sendResponse({ ok: false, error: String((err && err.message) || err) }); } })(); return true; // async sendResponse }); // Aufräumen, wenn ein Tab geschlossen wird chrome.tabs.onRemoved.addListener((tabId) => { chrome.runtime .sendMessage({ target: "offscreen", type: "STOP", tabId }) .catch(() => {}); chrome.action.setBadgeText({ tabId, text: "" }); });