Chrome extension that captures audio from a browser tab and feeds live EQ bands into Monitron dynamic screensavers on Monitron.
Captures tab audio as a raw log-spaced spectrum (bands[32] + rms / peak) and streams it via window.postMessage to any allowlisted page that speaks the audio bus protocol. Musical meaning (bass / mid / high / beat / BPM) is derived on the page.
Source of truth in this repo: src/shared/protocol.ts (mirrors monitron-web/lib/audioBus.ts).
| Idle → connect | Listening + live bus |
|---|---|
![]() |
![]() |
npm install
npm run build # or: npm run dev- Open
chrome://extensions - Enable Developer mode
- Load unpacked → this repo’s
dist/folder - Open the library: monitron-web-gamma.vercel.app (or local
http://localhost:3000) - Pick a screen marked reactive (audio-bus capable savers only)
- Focus a tab with audio → extension popup → connect
- Open https://monitron-web-gamma.vercel.app/ — open a reactive screen. The HUD should show the extension as online (
hello/ not offline). - Focus a tab that is playing audio → extension popup → connect.
- Turn reactive on — the screen should move with the spectrum from
audio-frame. - Turn reactive off → page stops using bands; bus in the popup can still move while capture is up.
The extension injects a content script only on allowlisted origins. That script bridges:
- extension → page:
window.postMessage(...) - page → extension:
window.postMessage(...)(same window)
Always filter by event.source === window and data.source.
Extension → page (on inject, then every ~2.5s until answered; also on demand):
{ source: "monitron-extension", type: "hello" }Page → extension (stop the hello spam / re-announce after navigation):
{ source: "monitron-page", type: "hello-request" }Minimal listener:
window.addEventListener("message", (event) => {
if (event.source !== window) return;
const msg = event.data;
if (!msg || typeof msg !== "object") return;
if (
msg.source === "monitron-extension" &&
msg.type === "hello"
) {
// Extension is present — show “EQ / reactive” UI, etc.
window.postMessage(
{
source: "monitron-page",
type: "hello-request",
},
"*",
);
}
});While capture is active and analysis is on, frames arrive at ~analyser rate:
{
source: "monitron-extension",
type: "audio-frame",
t: 12345.6, // ms since capture start
sampleRate: 48000,
bands: [/* 32 floats 0..1, log 20Hz→16kHz */],
rms: 0.0, // time-domain RMS 0..1
peak: 0.0, // time-domain peak 0..1
}if (
msg.source === "monitron-extension" &&
msg.type === "audio-frame"
) {
const { t, bands, rms, peak, sampleRate } = msg;
// derive EQ / onset on the page — drive viz / shaders / rain / …
}bands are normalized floats in 0..1. No persistence — live only (no localStorage / chrome.storage bus). The extension does not emit named bass / mid / high / beat.
Tell the extension whether the page wants analysis (saves CPU when viz is off):
{
source: "monitron-page",
type: "visualizer-toggle", // alias also accepted: "eq-toggle"
enabled: true
}When enabled: false, analysis stops and the page should treat bands as idle/zero until re-enabled (and a stream is still connected).
On an allowlisted page with the extension loaded:
postMessage(
{ source: "monitron-extension", type: "hello" },
"*",
);
postMessage(
{
source: "monitron-extension",
type: "audio-frame",
t: performance.now(),
sampleRate: 48000,
bands: Array.from({ length: 32 }, (_, i) => (i < 8 ? 0.9 : 0.2)),
rms: 0.4,
peak: 0.7,
},
"*",
);(Those fake posts only exercise your page listener — they do not go through the extension.)
| Field | Value |
|---|---|
| Extension source | monitron-extension |
| Page source | monitron-page |
| Hello | hello |
| Hello request | hello-request |
| Frame | audio-frame |
| Viz toggle | visualizer-toggle |
Two different roles:
| Role | Example | Needs host permission? |
|---|---|---|
| Audio source (tab you connect to) | YouTube, Spotify web, any https tab | No — tabCapture works without listing that site |
Receiver (page that listens to audio-frame) |
Matrix / your viz site | Yes — content script must be injected there |
host_permissions + content_scripts.matches come from src/shared/hosts.ts
(MONITORN_ORIGINS → match patterns). That file is the only place to add/remove
receiver sites (also drives tabs.query + popup feed link + “don’t capture viz”).
Current origins (see hosts.ts):
http://localhost:3000http://127.0.0.1:3000https://monitron-web-gamma.vercel.app— deploy / library
For a local fork, add your origin to MONITORN_ORIGINS (and MONITORN_APP_ORIGIN if it should be the popup feed), rebuild, reload.
We keep a tight receiver allowlist (no <all_urls>). If you want your origin shipped in the upstream extension, open an issue with the exact origin(s) and a short note on the project.
Audio sources (YouTube, etc.) stay unrestricted either way.
Popup (focused tab → connect)
→ tabCapture.getMediaStreamId
→ offscreen (AudioContext + AnalyserNode)
→ background → content script on allowlisted receiver tabs
→ window.postMessage(audio-frame)
Page visualizer-toggle
→ content script → background EQ_TOGGLE
→ start/stop analysis
UI: src/ui/TabAudioPanel.tsx (popup only). No side panel.
tabs— focused tab title / url in the popuptabCapture— capture chosen tab audiooffscreen— keep Analyser alive after the popup closes- host access — inject receiver bridge on allowlisted origins only
npm run buildLoad dist/ (or the zip under release/).
- Extension: StoneZol/monitron-plugin
- Screens / library: StoneZol/monitron-web
- Deploy: monitron-web-gamma.vercel.app
MIT © 2026 StoneZol


