volume-booster/offscreen.js
2026-05-15 21:11:59 +02:00

95 lines
2.9 KiB
JavaScript

// ============================================================
// Volume Booster — Offscreen Document
// Läuft in einem unsichtbaren DOM-Kontext. Nur hier sind
// getUserMedia + AudioContext verfügbar. Hält pro Tab einen
// GainNode und gibt den verstärkten Stream wieder aus.
// ============================================================
// Map<tabId, { context, gain, source, stream }>
const active = new Map();
function teardown(tabId) {
const entry = active.get(tabId);
if (!entry) return;
try {
entry.source.disconnect();
entry.gain.disconnect();
entry.stream.getTracks().forEach((t) => t.stop());
entry.context.close();
} catch (e) {
/* trotzdem aufräumen */
}
active.delete(tabId);
}
async function applyBoost(tabId, streamId, value) {
// Falls (z. B. nach Fehler) noch ein alter Eintrag existiert: erst sauber weg.
if (active.has(tabId)) {
teardown(tabId);
}
// Stream aus der ID erzeugen
const stream = await navigator.mediaDevices.getUserMedia({
audio: {
mandatory: {
chromeMediaSource: "tab",
chromeMediaSourceId: streamId,
},
},
video: false,
});
const context = new AudioContext();
const source = context.createMediaStreamSource(stream);
const gain = context.createGain();
gain.gain.value = value;
source.connect(gain);
gain.connect(context.destination); // Audio bleibt für den Nutzer hörbar
// Wenn der Stream endet (Tab neu geladen / geschlossen)
const track = stream.getAudioTracks()[0];
if (track) {
track.addEventListener("ended", () => teardown(tabId));
}
active.set(tabId, { context, gain, source, stream });
return { ok: true, boosting: true, value };
}
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
if (msg.target !== "offscreen") return false;
(async () => {
try {
if (msg.type === "APPLY") {
const res = await applyBoost(msg.tabId, msg.streamId, msg.value);
sendResponse(res);
} else if (msg.type === "GAIN") {
// Nur die Lautstärke eines bereits aktiven Tabs ändern
const entry = active.get(msg.tabId);
if (entry) {
entry.gain.gain.value = msg.value;
sendResponse({ ok: true, boosting: true, value: msg.value });
} else {
// Kein aktiver Stream (mehr) — Popup soll neu anwenden
sendResponse({ ok: false, error: "no active stream", boosting: false });
}
} else if (msg.type === "STOP") {
teardown(msg.tabId);
sendResponse({ ok: true, boosting: false });
} else if (msg.type === "STATE") {
const entry = active.get(msg.tabId);
sendResponse({
ok: true,
boosting: !!entry,
value: entry ? entry.gain.gain.value : 1,
});
}
} catch (err) {
sendResponse({ ok: false, error: String((err && err.message) || err) });
}
})();
return true; // async
});