Get on the same wavelength. A Chrome extension that lets you and your friends listen to YouTube Music together, in sync. Someone hits play, everyone hears it. Someone skips, everyone skips. Someone seeks, everyone seeks.
┌──────────────────┐ ┌───────────────┐ ┌──────────────────┐
│ You (Chrome) │◄──────►│ relay server │◄──────►│ Friend (Chrome) │
│ extension + YTM │ wss │ (server/) │ wss │ extension + YTM │
└──────────────────┘ └───────────────┘ └──────────────────┘
Two parts:
| Folder | What it is |
|---|---|
chrome-extension/ |
The extension you load into Chrome (Manifest V3). |
server/ |
A ~200-line WebSocket relay that keeps rooms in sync. Everyone in a room connects to the same server. |
test/ |
An end-to-end test that drives two real browsers on the real YouTube Music site. |
Vocabulary: a wavelength is a room; its 6-letter frequency is the code friends tune in with.
- Download
wavelength-extension.zipfrom the Releases page and unzip it (or clone this repo — it's thechrome-extension/folder). Chrome 111 or newer. - Open Chrome →
chrome://extensions - Turn on Developer mode (top right).
- Click Load unpacked → pick the
chrome-extension/folder. - Pin Wavelength to the toolbar (puzzle-piece icon → 📌) so it's one click away.
The relay is what lets browsers talk to each other. Nobody's music goes through it — only tiny "paused at 1:23" / "now playing X" messages.
cd server
npm install
npm start # → ws://localhost:8787That's enough to try it out with two Chrome profiles on one computer. Friends on other computers
can't reach localhost, so for real use deploy it (next section) or expose it with a tunnel
(npx localtunnel --port 8787 gives you a temporary https://… URL — use it as wss://…).
Any host that runs Node and gives you a public HTTPS URL works. Two easy ones:
Render — https://render.com → New → Web Service → connect MowkE/wavelength (or a fork) → Root directory server, Build npm install, Start npm start, Instance
type Free. You'll get https://your-app.onrender.com → your server URL is
wss://your-app.onrender.com. (Free instances sleep after ~15 min idle; the first
connection takes ~30 s to wake it. Everything reconnects automatically.)
Railway — https://railway.app → New Project → Deploy from repo → set Root Directory to
server. Railway assigns a https://…up.railway.app domain → use it as wss://….
Fly.io, Koyeb, a $5 VPS with node server.js behind Caddy — all fine too. The server reads the
PORT env var, which every host sets for you.
Rule of thumb:
ws://only works forlocalhost. Anything remote must bewss://(TLS), which the hosts above give you automatically.
Click the extension → expand Server → paste wss://your-app.onrender.com → Save.
Every friend does the same with the same URL (it's saved to your Chrome profile).
- One person types a name and clicks Start a wavelength → they get a 6-letter frequency like
K7XQ2P. - Share the frequency, or click Copy invite link and send that.
The link looks like
https://music.youtube.com/?wave=K7XQ2P— a friend with the extension just opens it and they're in. - Friends who got a frequency: click the extension → name → paste it → Tune in.
- Open https://music.youtube.com and play something. Everyone's player follows.
Anyone on the wavelength can play, pause, seek, or put on a different song — it mirrors to everyone. A small pill in the corner of YouTube Music tells you what's happening ("Priya put on Blinding Lights", "Marcus paused", "3 on your wavelength").
The ★ next to a name marks the leader (first person on the wavelength). Their player is the reference clock: every 5 s it nudges anyone who has drifted more than 2 s, and when a song ends their next song is the one everyone gets. If the leader leaves, the next-oldest member takes over automatically.
- Ads. On free YouTube Music, each person's pre-roll ads are different lengths. The extension knows when an ad is playing: it never broadcasts during one, holds incoming changes until it's over, then jumps to where your friends are. Premium = totally seamless.
- Browse freely. Wandering around Home / Explore / playlists while music plays is fine — the extension tracks what's actually playing, not the page URL.
- One YouTube Music tab. The tab that first said hello is the synced one; extra YTM tabs you open to browse are left alone. Close the synced tab and the next one takes over.
- Autoplay blocked? If Chrome refuses to start audio (fresh profile, never clicked the site), the corner pill turns red: "Click here to get back on the wavelength." One click fixes it.
- Reconnects. Laptop lid closed, Wi-Fi hiccup, server restarted — the extension reconnects with backoff and rejoins the same room. Your seat is held for 45 s, so a quick blip keeps your name, ★ leader status and place in the list (friends just see you dimmed as "reconnecting…"). Empty wavelengths live 10 minutes so everyone can come back.
- Privacy. The relay sees frequencies, display names, video IDs and timestamps. It stores nothing on disk. Only people with the code can join.
- Nothing to sign in to. No accounts, no Google login for the extension itself; it just drives the YouTube Music page you're already using.
background.js(service worker) owns the WebSocket to the relay so it survives page loads, keeps itself alive with a 20 s ping + a 30 s alarm, and relays between the server and the synced YouTube Music tab.content.jsruns onmusic.youtube.com. It watches the<video>element and askspage.jswhich song is playing. When you do something it sends a snapshot{videoId, position, playing, title, …}; when a friend's snapshot arrives it applies only the difference (seek if off by > 0.75 s, play/pause if different, switch songs if it's another track). Snapshots instead of commands means everything is idempotent, so echoes and races converge instead of ping-ponging.page.jsis a few lines injected into the page's own JS world ("world": "MAIN", Chrome 111+). It reads YouTube's player API for the real current song — correct while you browse Home or a playlist (URL has no?v=) and even during ads — and performs song switches through YouTube Music's in-app navigation, so following a friend to a new song doesn't reload the page. If the in-app switch doesn't take within 4 s it falls back to a normal navigation.server/server.jskeepsrooms → members + last state, forwards snapshots, elects a leader, and hands newcomers the current state (with timestamps, so they can compute where the song is now).
cd server && npm test # protocol test, ~1 s
cd test && npm install && npx playwright install chromium
npm test # two headless browsers on real YouTube Music, 2–8 min
HEADFUL=1 npm test # same, but you can watchThe e2e test starts a wavelength in one browser, tunes in from another, and checks follow-navigation, pause, seek, play (both directions), track change, auto-advance at end of song, second-tab behaviour, leaving, and rejoining by invite link.