Telecine
2026 On airBroadcast protocol · Data pipelines with a legal gate · Two runtimes held to parity
A public broadcast network for old films. Four channels run around the clock with no streaming server and no account: every receiver reads the same schedule file and the same clock, and lands on the same frame. You don’t press play; you tune in.
A schedule is a file, and the file is the broadcast
Each channel is a Transmission: a fixed epoch and an ordered list of blocks — features, shorts and station breaks — that loop forever. Every client reads the same static file and the same wall clock, does the same arithmetic, and lands on the same frame:
// what is on, and how far into it are we
loop = Σ durationSec
into = ((t − epoch) mod loop + loop) mod loop
// walk the blocks; the one containing `into` is on air;
// the remainder is the seek offsetThat is the decision worth defending: a format, not a service. A schedule in a JSON file is diffable, versionable and forkable. There is no origin to fall over when everyone tunes in at programme start, because the documents are static and the video comes from the Internet Archive. And there is no operator who can move a programme for one audience and not another — the shared present tense is enforced by arithmetic, not policy.
One answer, proved across languages
The web set and the build resolve the schedule with one pure, dependency-free file, broadcast.js. The Apple receivers — iPhone, iPad and Apple TV — resolve it again in Swift. Two implementations of one protocol drift unless something holds them together, so a parity fixture does: the JavaScript generates 7,751 resolve cases, 7,592 programme beginnings and 19,726 standby cases, and the Swift test suite must reproduce every one.
/**
* Resolve a channel against a moment in time.
*/
export function resolve(channel, now) {
const t = typeof now === 'number' ? now : now.getTime();
const epoch = Date.parse(channel.epoch);
const loop = loopDuration(channel);
if (loop <= 0) throw new Error(`channel ${channel.id}: empty schedule`);
// seconds into the current loop (well-defined for t before the epoch too)
const into = (((t - epoch) / 1000) % loop + loop) % loop;
let cum = 0;
for (let i = 0; i < channel.blocks.length; i++) {
const b = channel.blocks[i];
if (into < cum + b.durationSec) {
const loopStart = t - into * 1000;
return {
block: b,
index: i,
offsetSec: into - cum,
blockStart: new Date(loopStart + cum * 1000),
blockEnd: new Date(loopStart + (cum + b.durationSec) * 1000),
loopSec: loop,
};
}
cum += b.durationSec;
}
/* unreachable: `into` is always < loop */
throw new Error('resolve: fell off the schedule');
}The widget below imports that exact file and today’s schedule, and resolves all four channels live in this page.
| Channel | On air | Offset into it |
|---|---|---|
| loading the network file… | ||
broadcast.js and network.json are vendored byte-identical from the Telecine source at commit 6bb478b (5 November 2026), the same files the set and the receivers run. Nothing on this page was rewritten to make the demo work.
A film airs only where it is cleared
“Public domain” is a claim about one country, and about the film, not the file. Half the silent prints in the first audit were rips of commercial restorations — a modern score, new intertitles, a restorer’s copyright line — and a US film that lapsed can still infringe the novel it was built on. So every film has an entry in a rights ledger, with its basis and sources, before it can be scheduled.
The ledger is a build gate, not a document. The bake fails on a film with no entry, holds back any film not cleared for the network’s zone, and fails if the print on air is not the exact file the ledger inspected. Inspection is measured: soundtrack loudness sampled at eight points to tell silence from a modern score, and contact sheets of the titles and credits. The ledger holds 118 entries today; it clears films for the United States, and it records honestly that outside it almost nothing clears.
A grid, with the wait measured
A channel’s loop is one week, 604,800 seconds. Every feature starts on a half-hour mark, the station break flexes to fill the gap, and the channels are staggered by quarter hours so the dial always has something about to begin. The bake measures the longest anyone can wait for a beginning anywhere on the dial: 75 minutes today, against a goal of 15. Missed, and printed.
Everyone sees the same frame, defended
“Everyone sees the same frame” is arithmetic on paper and a fight on a real device — drift, scrubbing fingers, background tabs, autoplay policy, a flaky connection. When a print stops answering, every receiver stands by on the same reel at the same frame, because the reel’s offset is the wall clock modulo its length; the block’s own countdown runs on, and the schedule resumes by itself.
| Case | Handling |
|---|---|
| Drift between sets | Re-resolve on a 500 ms tick; correct when |currentTime − offsetSec| > 2.5 s, at most once every 15 s — tight enough that two sets in one room agree, loose enough never to stutter. |
| Scrubbing | A seeking listener snaps back to LIVE past 4 s. No pause, no scrubbing, on purpose: a broadcast you can rewind is video-on-demand with extra steps. |
| Joining mid-film | Seek to offsetSec on loadedmetadata — you join what's on, never from the top. Byte-range support makes the mid-film seek possible, which is why the pipeline proves it per film rather than assuming it. |
| Cold start | The source loads and seeks behind the TUNE IN lens before power-on, so the set switches on into a moving picture. |
| Station breaks | The next reel pre-buffers behind the test card while the countdown runs. |
| Safari autoplay policy | The first play() happens inside the click gesture; if sound is refused anyway, the set falls back to muted and says so honestly on screen. |
| Signal loss | SIGNAL LOST · STANDING BY. A print the head-end reports down plays a standby reel for everyone at once; one that fails only on this set retries. |
| Backgrounded tab | visibilitychange re-syncs on return — the broadcast went on without you, as broadcasts do. |
| Block boundaries | Half-open intervals: the end instant belongs to the next block. Tested, because two clients straddling a boundary is the exact moment agreement matters most. |
| Before the epoch | The loop is congruent backwards — resolution is well-defined for any instant, so the broadcast has always been on. Tested. |
| Clock skew | Bounded by the viewers' system clocks, which NTP keeps within fractions of a second — see the honest edge in 03. |
The station counts itself, never its audience
No accounts, no analytics, no location. The schedule is anchored in UTC and identical for the planet; your browser turns it into your local hours. The station’s own numbers — features on air, hours, the longest wait, the ledger’s counts, the preflight’s age — come from one pure function, and a test fails if any sheet carries a visitor, session, device, country or referrer. The headers deny framing, and a content security policy runs in report-only mode while it is tuned.
A mirror is built for the day the Archive slows: an HLS ladder at 720p and 480p, cut to each block’s exact second so a receiver can switch to it and land on the same frame, with the source file’s SHA-256 and every removal written down. It waits on storage before it goes live.
On the air; the Apple receivers in development
The web set is live and has been on air since its epochs. The receivers for iPhone, iPad and Apple TV are built and tested but not yet submitted. The source is private while they are.