From cf0703f7f5c3f0b9d862d58e938c6a722380a248 Mon Sep 17 00:00:00 2001 From: Tronax Date: Fri, 15 May 2026 21:11:59 +0200 Subject: [PATCH] Some fixes --- README.md | 58 +++++++++++++ background.js | 124 ++++++++++++++++++++++++++ icons/icon.svg | 34 ++++++++ icons/icon128.png | Bin 0 -> 5642 bytes icons/icon16.png | Bin 0 -> 664 bytes icons/icon32.png | Bin 0 -> 1333 bytes icons/icon48.png | Bin 0 -> 2079 bytes manifest.json | 26 ++++++ offscreen.html | 8 ++ offscreen.js | 95 ++++++++++++++++++++ popup.css | 217 ++++++++++++++++++++++++++++++++++++++++++++++ popup.html | 90 +++++++++++++++++++ popup.js | 146 +++++++++++++++++++++++++++++++ 13 files changed, 798 insertions(+) create mode 100644 README.md create mode 100644 background.js create mode 100644 icons/icon.svg create mode 100644 icons/icon128.png create mode 100644 icons/icon16.png create mode 100644 icons/icon32.png create mode 100644 icons/icon48.png create mode 100644 offscreen.html create mode 100644 offscreen.js create mode 100644 popup.css diff --git a/README.md b/README.md new file mode 100644 index 0000000..6fe73ff --- /dev/null +++ b/README.md @@ -0,0 +1,58 @@ +# Volume Booster — Chrome Extension + +Ein Volume Booster für Chrome: macht jeden Tab lauter (bis 600 %) oder leiser, +mit stummschalten und Schnellwahl-Presets. + +## Installieren + +1. ZIP entpacken (falls noch nicht geschehen). +2. In Chrome `chrome://extensions` öffnen. +3. Oben rechts **Entwicklermodus** einschalten. +4. **Entpackte Erweiterung laden** klicken und den Ordner `volume-booster` auswählen. +5. Das Lautsprecher-Icon erscheint in der Toolbar — anpinnen, fertig. + +## Benutzen + +- Auf das Icon klicken, um das Panel zu öffnen. +- **Slider** ziehen oder die **+ / –** Knöpfe nutzen (10 %-Schritte). +- **Mute**-Knopf (mittig) schaltet stumm und beim erneuten Klick wieder zurück. +- **Presets**: 100 % / 200 % / 400 % / 600 % für einen Klick. +- Pfeiltasten ↑ / ↓ funktionieren ebenfalls, wenn das Panel offen ist. + +## Wie es funktioniert + +- Bis **100 %** passiert nichts Besonderes — die Seite spielt normal. +- Über **100 %** holt der Service Worker per `chrome.tabCapture.getMediaStreamId` + eine Stream-ID des Tabs und reicht sie an ein **Offscreen-Dokument** weiter. + Dort wird der Stream durch einen Web-Audio `GainNode` geschickt und + verstärkt wieder ausgegeben. +- Der Umweg über das Offscreen-Dokument ist in Manifest V3 nötig: Der + Service Worker hat kein DOM und kann selbst kein Audio verarbeiten. +- Pro Tab wird ein eigener Verstärkungswert gespeichert (für die Session). +- Schließt du den Tab oder lädst ihn neu, wird der Stream sauber freigegeben. + +## Hinweise + +- Bei sehr hohen Werten kann der Ton übersteuern/verzerren — das ist eine + physikalische Grenze der Quelle, dann einfach etwas zurückdrehen. +- Funktioniert nicht auf internen Seiten wie `chrome://`, + dem Web Store oder anderen Extension-Seiten — dort gibt Chrome keinen + Audio-Stream frei. +- Beim ersten Verstärken eines Tabs muss kurz Audio laufen, damit Chrome + den Stream herausgibt. +- Verstärkung gilt jeweils für den Tab, in dem du sie einstellst. +- Nach einem Update der Extension immer einmal auf `chrome://extensions` + neu laden. + +## Dateien + +| Datei | Zweck | +|------------------|----------------------------------------------------------| +| `manifest.json` | Extension-Manifest (Manifest V3) | +| `background.js` | Service Worker — holt Stream-ID, steuert Offscreen-Doc | +| `offscreen.html` | Unsichtbares Dokument, Träger des Audio-Codes | +| `offscreen.js` | AudioContext + GainNode — das eigentliche Verstärken | +| `popup.html` | Aufbau des Interfaces | +| `popup.css` | Styling (dunkles Studio-Theme, Amber-Akzent) | +| `popup.js` | Logik, State, Kommunikation mit dem Worker | +| `icons/` | Icon in 16 / 32 / 48 / 128 px + SVG-Quelle | diff --git a/background.js b/background.js new file mode 100644 index 0000000..5b6b3af --- /dev/null +++ b/background.js @@ -0,0 +1,124 @@ +// ============================================================ +// 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 + +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, + }); + 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) { + 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, + }); + 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, + }); + 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, + }); + 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(() => {}); +}); diff --git a/icons/icon.svg b/icons/icon.svg new file mode 100644 index 0000000..4183a92 --- /dev/null +++ b/icons/icon.svg @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/icons/icon128.png b/icons/icon128.png new file mode 100644 index 0000000000000000000000000000000000000000..74e6e79bd535248c889d84614bc826300bdea7c3 GIT binary patch literal 5642 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 literal 0 HcmV?d00001 diff --git a/icons/icon48.png b/icons/icon48.png new file mode 100644 index 0000000000000000000000000000000000000000..72b7db6cf519b3839c44de97c5fbcb7b657c3aa7 GIT binary patch literal 2079 zcmV+)2;ldLP)SKK~!jg?V4$9RM!>9fA_sN8(xNix)@^{8c%~+;({@htzqd0(kQAF zKn0{KD%sj5YAUr&TB@j`l%keKqEQ>Vq^+u^K-Gv^N~L~ilU6825Nfj}I5rR##|tKS zH?zI>?(K*1%)A+oX9i*iKICs5d3^7=_x{hh=bU@r8^g!Avk-+SEnc>Mo6y*dROJea zQL>4T9!3&MDs%6cDroDFitD@ z>dCkM(>%Ur7TmXbb-*2SzY&Q>7E5VQ9?t^c_PE1_%k^UOv48K#)~{zaELgE&n%m-3 zC={wyN=`n*Jg_a>rKDV0Rb5lv-FfA;%>FS3R8>?R4TVCrpgtOg1wcwElwwiE?4WtA z^U8mneQ5^NFI&4a9EogI^3Hoc6H-c{3vKzV*>f7ZJFebHv}+C>A*KDKv@PP^zxyN> z4Hqe?|H^4t(tyPcYqpxEITNM0TSX~_WyV%5S+c%ni~&O9OQvbw1%A#gu~^hlntCq5 z+emznQk9|Zp2QswN+~4r-o)QY0~FG)?xM<*3sTByiO(qm6vmz4UpqShK ziBFCyq$cdg&|zs!5r&?7*16SFY5eg+H168KgG(z4p5J*!X%7AqQYp&)E)G63mjh1+ zd1k{W^XSKy)`jG!+ZVE+vZ%my7;C^pRJx_X68`p``7EmTfs&LJ8#(pgz1qjlKO6@o z@fd;^9=R7o69ui6HH2nI=k+3w&MzLF%k!J6(S;l(FF{J;hfhj(*JCSq>fz7u>i6$w zepPX@efNPg42?uV**vjk8k#7~`(q5aRnG^@eH?zSjt3S6MhVN5=hdho4*lk~>mY4P zJc?gG9;Dc-0g8c#VAA7vz0#l+pMOWS9rkR{Qz(#(8*MB1LeNZ2hN- z@EIyaIL-SKrDWQiFC6+Gfhii>Rs^W<>pcFsQuh6`4}il*yZHLrsUU6E&-HNRd}Iul zY%uaEJiB>5>+UN9X*o!a{!Sxor;AL)e>#5jN)zkmuAtm)vUQ1<7ykuP!KI!MU43CH zrr0d1%u6eYR27o(%TlKJTp|??36XXd!ZOYK2_rJ+3r8&)k9LBzC@YfGOefjb+7SXJ znG+CMxg@O~=(o$$FeHZCazdx-0UF)rRn z?NV*I3uKtfy=GP}c@4;hy@%V7_C2X&mJ)~?vt*7BpCPk*KVhH)Lcq=~pJqmhfRM$Z zW|Krd2Ftxv_yt65PPG^Q4k~3p7Hxuw`dgBR8?^+(evn=C48pKq>hBn?s!I zh_ZicDP={BR-jac0l3&b!rs4+q>X)KT@?>EOoP}kZQT(LHWhY-NFW(PlGHBTt{l4* z<-wPFIoTcqCAl^rbL!Vtm9lF~5R^?MYVwW0^kUli*KwAH^c}dJ7|KBRfX$|t2dJ;o zX>PZ3Z>o$RT()iT%|Cb2(iKA~$?AnO=;;e_v3rDk-vBraNIY+Tc*JZtekFe?Y z2X;4sl(ck)0B{?ELp#=Ts;!%ces}~;)3S`ra+MHDm=nbRmeA6ko)d0epvf2>-AWam z3Ug-f&1WsHTD9>Sq8=Q{>Ln%o!&5i$h|@OZpfN*zj!O-u3lb)P-b z-+TT3LN8Ji2_+>;K&e;f!q1oT&gFhS|C3e_0u2$TDqw10dS~mo=E}sl^b{AyQ^h6z zo40zUmUx!oALYrm6nngK0mj-GS5y?fjqpZu_g+U-gx zCK(zD)6*XYM>}YgmIj*MIeUChn$NRs=YqOrzZ)JN+#ZdDvlZdVg#!N+LDW=4MvJ8o z8iDC$mp!^!fBN+40jK(yFZlpI=;?fIR`oqzO*0x|(THpES*IgnDy-3B=yrPu1U}K^ z*3H$KR_Kc@j(7z1Kt6RBH zDEqro*_G0=1GbWr5?Z>ZDWQoGT{F6XzqY)6;?>Dm=VMGH{tsHp?NB5LWsU#<002ov JPDHLkV1lLW^)~>>>>>> 2056a0d (First Commit, Code by Claude 4.7 Opus) } diff --git a/offscreen.html b/offscreen.html new file mode 100644 index 0000000..5094571 --- /dev/null +++ b/offscreen.html @@ -0,0 +1,8 @@ + + +Volume Booster — Offscreen + + + + + diff --git a/offscreen.js b/offscreen.js new file mode 100644 index 0000000..4c3c706 --- /dev/null +++ b/offscreen.js @@ -0,0 +1,95 @@ +// ============================================================ +// 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 +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 +}); diff --git a/popup.css b/popup.css new file mode 100644 index 0000000..5ff046e --- /dev/null +++ b/popup.css @@ -0,0 +1,217 @@ +/* ============================================================ + Volume Booster — Popup Styles + Aesthetik: dunkles "Audio-Studio"-Panel, warmer Amber-Akzent, + prägnante Typografie, taktile Buttons. + ============================================================ */ + +@import url('https://fonts.googleapis.com/css2?family=Sora:wght@400;600;700&family=DM+Mono:wght@400;500&display=swap'); + +:root { + --bg: #15171c; + --panel: #1d2027; + --panel-2: #23272f; + --line: #2f343d; + --text: #e8e6e1; + --text-dim: #8b8f99; + --accent: #ff9d2f; + --accent-dk: #e07d12; + --accent-gl: rgba(255, 157, 47, 0.18); + --danger: #ff5d5d; + --radius: 14px; +} + +* { margin: 0; padding: 0; box-sizing: border-box; } + +body { + width: 320px; + font-family: 'Sora', sans-serif; + background: + radial-gradient(120% 80% at 50% -10%, #232730 0%, var(--bg) 60%); + color: var(--text); + -webkit-font-smoothing: antialiased; + user-select: none; +} + +.app { + padding: 18px 18px 16px; + display: flex; + flex-direction: column; + gap: 16px; +} + +/* ---------- Kopf ---------- */ +.head { + display: flex; + align-items: center; + justify-content: space-between; +} + +.brand { display: flex; align-items: center; gap: 11px; } + +.brand-ico { + width: 34px; height: 34px; + color: var(--accent); + padding: 7px; + background: var(--accent-gl); + border-radius: 10px; + flex-shrink: 0; +} + +.brand-text { display: flex; flex-direction: column; line-height: 1.25; } +.brand-name { font-weight: 700; font-size: 14px; letter-spacing: .2px; } +.brand-site { + font-size: 11px; + color: var(--text-dim); + max-width: 150px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.badge { + font-family: 'DM Mono', monospace; + font-size: 12px; + font-weight: 500; + padding: 5px 9px; + border-radius: 8px; + background: var(--panel-2); + border: 1px solid var(--line); + color: var(--text-dim); + transition: all .2s ease; +} +.badge.boosting { + color: var(--accent); + border-color: var(--accent-dk); + background: var(--accent-gl); +} + +/* ---------- Großer Wert ---------- */ +.readout { + display: flex; + align-items: baseline; + justify-content: center; + gap: 4px; + padding: 6px 0 2px; +} +.readout-value { + font-family: 'DM Mono', monospace; + font-size: 54px; + font-weight: 500; + letter-spacing: -1px; + line-height: 1; + background: linear-gradient(180deg, #fff, #cfcdc7); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + transition: color .15s; +} +.readout.boosting .readout-value { + background: linear-gradient(180deg, #ffd9a0, var(--accent)); + -webkit-background-clip: text; +} +.readout-unit { + font-size: 20px; + color: var(--text-dim); + font-weight: 600; +} + +/* ---------- Slider ---------- */ +.slider-wrap { padding: 0 2px; } + +input[type="range"] { + -webkit-appearance: none; + width: 100%; + height: 8px; + border-radius: 6px; + background: var(--panel-2); + border: 1px solid var(--line); + outline: none; + cursor: pointer; +} +input[type="range"]::-webkit-slider-thumb { + -webkit-appearance: none; + width: 22px; height: 22px; + border-radius: 50%; + background: radial-gradient(circle at 35% 30%, #ffce8f, var(--accent) 70%); + border: 2px solid #1a1c22; + box-shadow: 0 0 0 1px var(--accent-dk), 0 4px 10px rgba(0,0,0,.5); + transition: transform .12s ease; +} +input[type="range"]::-webkit-slider-thumb:active { transform: scale(1.18); } + +.ticks { + display: flex; + justify-content: space-between; + margin-top: 7px; + font-family: 'DM Mono', monospace; + font-size: 10px; + color: var(--text-dim); +} + +/* ---------- Steuerknöpfe ---------- */ +.controls { + display: grid; + grid-template-columns: 1fr 1fr 1fr; + gap: 10px; +} +.ctrl { + height: 50px; + border-radius: var(--radius); + border: 1px solid var(--line); + background: linear-gradient(180deg, var(--panel-2), var(--panel)); + color: var(--text); + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + transition: transform .1s ease, border-color .15s, background .15s; +} +.ctrl svg { width: 22px; height: 22px; } +.ctrl:hover { border-color: var(--accent-dk); } +.ctrl:active { transform: translateY(2px) scale(.97); } + +.ctrl-mute { color: var(--text-dim); } +.ctrl-mute.active { + color: var(--danger); + border-color: var(--danger); + background: rgba(255, 93, 93, .12); +} + +/* ---------- Presets ---------- */ +.presets { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 8px; +} +.preset { + font-family: 'DM Mono', monospace; + font-size: 11px; + font-weight: 500; + padding: 8px 4px; + border-radius: 10px; + border: 1px solid var(--line); + background: var(--panel); + color: var(--text-dim); + cursor: pointer; + display: flex; + flex-direction: column; + align-items: center; + gap: 3px; + transition: all .15s ease; +} +.preset svg { width: 14px; height: 14px; } +.preset:hover { color: var(--text); border-color: #3c424d; } +.preset.active { + color: var(--accent); + border-color: var(--accent-dk); + background: var(--accent-gl); +} + +/* ---------- Hinweis ---------- */ +.note { + font-size: 10.5px; + color: var(--text-dim); + text-align: center; + line-height: 1.5; + padding-top: 2px; +} +.note.error { color: var(--danger); } diff --git a/popup.html b/popup.html index 3b5998d..9799242 100644 --- a/popup.html +++ b/popup.html @@ -1,6 +1,7 @@ +<<<<<<< HEAD