Four theaters, one channel — the stream becomes TV
The 24/7 stream had a quiet lie baked into it: it called itself “Warbirds,” but it only ever showed the air game. Naval, land, and combined arms — three whole theaters — were dark to anyone tuning in. Now the channel runs like TV, giving each theater an equal timeslot on a fixed clock, with the guide printed right on the tin.
One channel, three-quarters dark
The 24/7 stream is one browser: a robot director joins a server as a spectator, chases the hottest fight, and ffmpeg shovels the framebuffer to YouTube all day. Simple and sturdy. But the theaters — air, naval, land, and the combined-arms Brick Gulf — run as separate servers on separate domains. One browser watches one server. So the channel could only ever be pointed at one theater, and it was pointed at air.
Put a number on it and it stings: 1 of 4 theaters on screen, air holding 100% of the airtime, the other three holding 0. We'd built a whole naval war and a whole tank war and a map where planes, ships, and armor fight at once — and a visitor who found the stream would never know they existed.
Make it a TV guide, not a shuffle
The obvious move is “switch servers every so often.” But a random shuffle is a bad channel: a viewer who likes the tank brawls has no idea when they're on, and the description can't tell them. Real TV solves this with a schedule — a thing that's on at a known time.
So the rotation is anchored to the wall clock, not to when the box booted. Slot length is fixed (30 minutes by default) and the lineup cycles air → naval → land → combined, measured from UTC midnight. The upshot is that the times are the same every day:
:00 ✈ AIR · :30 ⚓ NAVAL · 1:00 ⚔ LAND · 1:30 🎯 COMBINED ARMS — then it repeats, every two hours, around the clock.
AIR is always at the top of the hour. NAVAL is always at half past. That's the whole trick: because the schedule is a pure function of the clock, the listing we paste into the YouTube description is true by construction — nobody has to keep a guide in sync with reality, because the guide is the reality. Each theater gets exactly six hours a day. Equal time, to the minute.
The mistake I nearly shipped
My first sketch switched channels the blunt way: at each boundary, kill the browser and the encoder, and start them again pointed at the next server. It would have worked. It also would have been a slow-motion disaster.
This container has scar tissue — weeks of it — from exactly one thing: restarting the push to YouTube. Reconnecting the encoder is when the ingest turned a clean 16:9 feed into square, black-barred renditions; it's the fragile moment the whole design bends over backwards to avoid. And my blunt rotation would have forced that fragile moment 48 times a day, every day, forever. I'd have spent a hard-won lesson forty times over to save myself a little plumbing.
The fix is to change the channel without anyone at the encoder noticing. ffmpeg doesn't watch a URL — it grabs a framebuffer. So the switch just tells the same, already-running browser tab to navigate to the next theater, over the devtools port it's already listening on. The push to YouTube never so much as stutters. Chrome and ffmpeg keep running for months; a channel change touches neither. The encoder still only ever restarts on a real crash — exactly as it did before.
And because it's just a tab navigation, we could put a proper station
break in the gap. Between programmes the channel now holds a full minute
of color bars — the old test-card look — with
“UP NEXT ⚓ NAVAL,” a one-line pitch, and a M:SS
countdown ticking down to the cut. It's a single self-contained page
loaded off local disk into the same tab, so it costs the encoder nothing,
and it turns the one awkward moment (a theater warming up behind a load
screen) into the most TV thing on the whole channel — a break long
enough to read the guide before the next fight starts.
And it's not silent. The game's soundtrack is synthesized — no audio files, just a little WebAudio step-sequencer that arranges a groove and cycles through a dozen themes — so the ident simply runs that same engine under the bars. The break plays a real Warbirds track (the card even captions it, “♪ NOW PLAYING — Scramble,” just like the in-game HUD), routed through the same virtual sound card ffmpeg already records. The one snag was plumbing: the stream container is built in isolation from the game client and the ident loads off local disk, where the browser won't touch ES modules — so the soundtrack ships as a vendored classic-script copy, kept honest by a checker that fails the build if it drifts from the original.
Where the schedule actually lives
There's one rule I held onto: the schedule math exists in exactly one place. A guide in the description that disagrees with what's on screen is worse than no guide at all, and the surest way to get that drift is to compute the schedule twice. So a single small script owns it — it tells the launcher which theater is on air right now, it prints the guide into the logs, it feeds the guide to the auto-live supervisor for the description, and it's the thing that flips the channel at each boundary. One clock, one source of truth, four jobs.
It also had to be as unkillable as everything else on an unattended box. The switcher sits in its own supervised loop, apart from the part that renders and encodes. If the devtools port hiccups or the browser is still warming up, it shrugs and tries again on the next tick; it can never drag the stream down with it. And it never fights the crash-recovery: when the browser does relaunch after a crash, it rejoins the theater that's on air now, and the switcher — reading the same clock — agrees, so nothing flickers.
The theater that was never on camera
Building the rotation turned up a bug that had been hiding in plain sight for as long as the stream existed: point the director at the land server and it doesn't work. The camera would twitch around the steppe and never settle on the tanks — the very thing worth watching.
The cause was one word. When the director builds its list of things worth pointing at, it adds the driven hulls — warships at sea, tanks ashore — but the check that let hulls onto the list read “naval or combined,” and land had been left out. So on the land server the tanks were never candidates; the director scored an empty list, gave up, and the lull-fallback grabbed the nearest hull for a quarter-second before the next tick threw it away again. A flicker, not a broadcast. Nobody had caught it because nobody had ever watched the land server — the stream only ever showed air.
The fix is to let hulls onto the list in every hull theater, land included — the same rule the rest of the code already used everywhere else. With it in, the land director behaves like the naval one it was cribbed from: it locks onto a tank inside a second of one rolling into a fight, rides the brawl while the guns are talking, and pulls back for the occasional high orbit. Which is the whole point — a rotation that shows you three-quarters of the game is only worth building if the director actually works when it gets there.
Why it matters
A stream that only shows a quarter of the game undersells the game by three-quarters. Someone who'd have stayed for the torpedo runs or the tank duels tuned in, saw dogfights, and never learned the rest was there. Now the channel shows all of it, on a rhythm you can plan around: if you want armor on the Brick Steppe, it's on every couple of hours, at a time you can read off the description before you click. Four theaters, one channel, equal time — and the encoder never even notices the show changed.