An embeddable radio. Publish one JSON file with your tracks and a start instant; point an iframe at it. Everyone tuned in hears the same minute — no server, no stream infrastructure, just arithmetic against the clock.
one file · one iframe · that's a station
<iframe src="https://zerg-fm.pages.dev/embed?station=https://your.site/station.json"
width="420" height="240" frameborder="0" allow="autoplay"></iframe>
<script src="https://zerg-fm.pages.dev/element.js" defer></script> <zerg-player station="https://your.site/station.json"></zerg-player>
Same player, nicer markup. <zerg-player> takes the embed options below as
attributes (manifest/ids for playlists, theme,
accent, height…) and re-renders live if you change them.
<iframe src="https://zerg-fm.pages.dev/embed?manifest=…/manifest.json&ids=ID1,ID2"
width="420" height="300" frameborder="0" allow="autoplay"></iframe>
| param | does |
|---|---|
| station | URL of a station.json — synced radio mode. Play/pause only; pausing and resuming rejoins the live position. |
| manifest | URL of a track manifest — playlist mode with seek, shuffle, volume, per-track stats. |
| ids | playlist mode: comma-separated track_ids to include, in order. |
| theme | zerg (default) or sleeve (paper + ink). |
| accent | hex override for the accent, e.g. %23FF2E9A. |
| api | counter endpoint base; defaults to the source origin's /api. off disables counters. |
| compact | 1 hides the seek strip in playlist mode. |
A station is a playlist with a start instant. The player computes (now − anchor) mod total and lands on that track at that offset, so every listener is in the same place without a server in the loop.
{
"name": "YOUR STATION",
"anchor_utc": "2026-07-15T00:00:00Z", // fixed forever; changing it re-phases the station
"total_s": 4790.74,
"tracks": [
{ "track_id": "NN-001-01", "title": "all 4", "artist": "NEON NOVENA",
"file": "/audio/NN-001-01-all-4.mp3", "duration_s": 147.0,
"accent": "#FF2E9A", "cover": "/art/neon-novena.jpg" }
]
}
Host it anywhere with Access-Control-Allow-Origin on the JSON and the audio
(the audio header also keeps the visualizer alive — a tainted source reads as silence).
Relative file/cover URLs resolve against the JSON's own URL.