OnAir turns "click a game" into a live HLS stream that stays up. It collects candidate streams from pluggable source adapters, probes and ranks them, plays the best one, and when that stream stalls, errors, expires or freezes, it fails over to the next candidate without the picture ever going black. The demo and every capture here run against its built-in fixture source: two local streams that can be broken on command.
Never black
Two video elements, one visible and one staging. A replacement stream loads out of sight and becomes visible on its first decoded frame. The current picture is torn down only once something better is on screen. A browser test tier runs a real decoder through the swap to prove it: the visible element never drops below the have-current-data ready state, and position only ever moves forward.
Three detectors, because each failure hides from the other two
- A stall state machine catches a starved buffer. Two states, watching and stalled; progress resets the timer.
- A fatal-error classifier tells an expired token, which means re-fetching the URL and staying, apart from a dead source, which means moving on. The same status code means different things before and after the first frame.
- An off-air check catches a stream that returns success on everything while its live edge has stopped moving. The player library sees nothing wrong with that. It fetches the playlist twice, 2.5 seconds apart, every twenty seconds. An early version read the master playlist, which has no media sequence, so every healthy stream looked frozen and failed over every twenty to sixty seconds; it now follows the master to its media playlist, and a sequence it cannot read counts as unknown, never frozen.
An escalation ladder, not a retry loop
Continuity
Sources run 45 to 100 seconds apart. When a stream carries wall-clock timestamps, the replacement seeks to the same instant in the broadcast instead of the live edge, so a failover does not skip or repeat play.
Click to play
The interface
The home screen is built from ESPN's public scoreboard for six leagues, the NFL, NBA, MLB, NHL, college football and college basketball: team logos, colors and records, live scores, the game clock, the network and the venue, refreshed every sixty seconds while games are on. A live game gets a full-width hero in its teams' colors, with rows of upcoming games under it. The player is full-bleed video with controls that hide themselves and a details drawer: the scoreboard, the current stream's source, quality and distance behind live, the ranked candidates, and how many failures it has recovered from.


Guide and live channels
A Guide screen, added on 28 September 2026, puts always-on channels beside the games. A source adapter can optionally list the channels it carries; a discovery pass on startup and every 30 minutes collects those listings and folds each source's spelling into one canonical channel, so the same channel from several sources is one row. The guide's programs come from ESPN's games, already in the database, and TVmaze's US schedule for today and tomorrow, shown from about an hour behind to a day ahead. A channel is a watch target exactly like a game: it goes through the same probe, ranking and failover, with its match confidence fixed at 1.0 because the link already names the channel. The fixture source has no channels, so none of this is captured here.
The last change on main fixed a starved extraction. Only four headless pages run at once, and sources that didn't carry the game held them for about 12 seconds each, so the sources that did got a page at about 21 seconds and missed the 30-second extraction timeout. An adapter now stops as soon as no listing link names the game, and adapters are dispatched by priority: 0.7 × their startup success rate (a neutral 0.5 with no history) plus 0.3 × their confidence weight.
Status
Works end to end against the fixture source; the embedded-player rung is covered by unit tests only. macOS only, installed locally with one command as an ad-hoc arm64 build, not signed or notarized for distribution. 738 tests across 39 suites plus seven real-decoder browser tests, and CI runs typecheck, lint, tests, build and the browser tier on macOS for every pull request.