Fire TV track · Amazon Developer Hackathon

Skip to the goal.Not the score.

A fan saves last night's replay for lunch, and scrubbing for a goal shows the score. Flipbook reads the trickplay file the player already downloaded — one small JPEG every 10 s — marks where the score box changes and holds, and lands 30 s before the last frame with the old score. It never decodes the video.

The development match at 2:20, a wide shot of the pitch PICTURE · 2:20 · 320×180 thumbnail

READY>> next change · << previous

2:20
86:10

The trickplay file · 517 thumbnailsengine view

The picture shows the present. Every thumbnail of the future is masked — hover the seek bar.A web replay of the app's own status line and seek bar, driven by the recorded scan of our development match: read order, markers and change distances are recorded; the replay's speed is not. The pictures are trickplay thumbnails at the playhead, not video.
npm run trace no backendno event feedno video decode
0

bytes of video decoded, by construction: the scan only opens trickplay JPEGs

src · scan.ts
2/2

jumps landed before the play on the development match — our labels, not ground truth

bench · goalJumpsBeforePlay
0 vs 237

false markers: Flipbook vs a plain frame differ at its best threshold for the same two goals

bench · ablationBaseline
4,085ms

pass 1 on the Vega virtual device: Amazon's own 428-frame BIF, 203 decoded, detector v1 build

VVD log · pass1 done

The trickplay file we built from the CC0 match

It reads the one box it hides from you.

Here is the whole development match as one contact sheet: 517 thumbnails, one every 10 s, every score box masked. We built the BIF from the CC0 broadcast with make-bif; the app on the virtual device fetched the same 6,575,564 bytes. The amber rule replays the scan's recorded read order — 66 sprocket samples first, then 11 bisect reads only where the box differed, then pass 2 through everything else.

2pass
517decoded
77pass-1 decodes
2markers
517 trickplay thumbnails of the development match in 22 rows of 24, each with its score box masked in black

Rows run four minutes each, 0:00 to 86:00. Frames 0–2 (the opening) and 515–516 (the credits) carry no score graphic; pass 2 counts them as skipped. In the app, blind mode shows none of this — only how far the scan has read. Switch the hero to Highlights to see the two changes circled here.

What the detector sees

Three frames before, three after. It has to hold.

These are the actual pixels inside the calibrated box around the first change, unmasked. Flipbook never reads the digits: it shrinks the box to a 64×5 luma grid, takes the median of three present frames on each side, and fires only if the top-3 cell distance clears τ = 20. The marker sits on the anchor — the last frame that still shows the old score — and goes 30 s before it.

beforeScore box at 16:30: Belmont 0, Winchester 0
beforeScore box at 16:40: Belmont 0, Winchester 0
anchorScore box at 16:50, the anchor: Belmont 0, Winchester 0
changeScore box at 17:00, the change: Belmont 0, Winchester 1
holdsScore box at 17:10: Belmont 0, Winchester 1
holdsScore box at 17:20: Belmont 0, Winchester 1

Vega virtual device · Apple M-series · Vega SDK 0.24

Running on the Vega virtual device, not in a browser.

These are captures from the Vega Virtual Device playing Amazon's own webinar trickplay file — the title the sample app itself streams, our "witness". Beside them, the device's log from the run with the detector's first build (5be6a4a), verbatim apart from the date, with elided lines marked.

Flipbook on the Vega Virtual Device: after pressing fast-forward, a toast reads '→ 3:49 (−30 s lead-in)' and the playhead moved 30 seconds before the next marker's anchor, the last frame with the old score
pressed. The toast reads → 3:49 (−30 s lead-in); the seek bar's thumbnail area stays covered.
VVD · loggingctl log -f · [flipbook] lines · device UTC
15:45:57.572236 [flipbook] scan start frames=428 rect=0.050,0.050,0.900,0.90015:45:57.838317 [flipbook] ref frame=344 area=12,163,0,24015:45:58.010412 [flipbook] marker anchor=19926ms change=69741ms d=105.4 pass=115:45:58.134384 [flipbook] marker anchor=229147ms change=239110ms d=26.2 pass=115:45:58.217397 [flipbook] marker anchor=259036ms change=268999ms d=20.8 pass=1··· 31 more pass-1 marker lines and 1 line “box saved for witness-amazon-tv-webinar” ···15:46:01.656692 [flipbook] pass1 done n=203/428 markers=34 ms=4085··· 2 pass-2 marker lines ···15:46:06.401225 [flipbook] pass2 done added=2 lost=0 absent=0 ms=474515:46:06.401320 [flipbook] thumb mode=overlay video bytes decoded=015:46:42.838350 [flipbook] seek from=50572ms currentTime=199.1 anchor=229147ms15:46:44.342362 [flipbook] after seek currentTime=199.115:47:07.621282 [flipbook] seek from=226280ms currentTime=229.0 anchor=259036ms15:47:09.122096 [flipbook] after seek currentTime=229.0
Scrubbing on the Vega Virtual Device: the score rectangle of the scrub thumbnail is covered by a dark mask
Scrubbing. The black block is the mask drawn over the scrub thumbnail (displayAboveThumb); on this title the calibrated box covers most of the frame.
Calibration on the Vega Virtual Device: an amber box drawn around the broadcaster graphic with the instruction 'Put the box on the score'
Calibration. Arrows move the box, SELECT resizes, PLAY confirms — once per broadcaster graphic.

How it works

Scan. Mark. Jump. On the device.

No backend, no feed, no model. Amazon's video sample extended with a TypeScript detector with no UI dependencies, and four Vega platform surfaces.

The file the player already has

A BIF is a contact sheet of the whole match — one JPEG roughly every 10 s, downloaded so the seek bar can preview. The sample's BIF service fetches it; one added call hands Flipbook the same bytes, and Flipbook's own parser reads them as a corpus. Amazon's tv-webinar-hd.bif has 428 frames at 240×180 and a timestamp-multiplier field of 0xFFFFD8F0, so that parser sanity-checks the header and falls back to a 10 s interval.

getBifFilereadBif0xFFFFD8F0

Only the box

jpeg-js decodes each thumbnail on the device; only the calibrated rectangle becomes a 64×5 luma grid. Cells that jitter — the pitch showing through — are masked out.

jpeg-js 0.4.464×5

Two passes

Pass 1 reads every 8th frame and bisects only where the box differs: on the development match both ticks were up after 77 of 517 decodes. Pass 2 still reads all 517 and can only add.

stride 8bisect

It has to hold

Medians of three present frames on each side: a one-frame flash cannot fire, and in synthetic tests neither does a replay wipe or a dropped graphic. The anchor walks back over any graphic-absent run to the last old-score frame.

τ = 203-frame median

Fast-forward is ours

useTVEventHandler takes the skip keys and sets currentTime to anchor − 30 s. On the VVD Flipbook's target was 199.1 s (anchor 229.1 s − 30 s), and 1.5 s later the player still reported 199.1 s — no second 30 s seek stacked on top.

useTVEventHandlerpartialDisablingConfiguration

Thumbnails stay masked

The KUI SeekBar's displayAboveThumb draws film-base black over the score box on every scrub thumbnail; before calibration, thumbnails stay off. Blind mode is the default: no ticks, no counts, only how far the scan has read.

displayAboveThumbblind by default

Four Vega surfaces

  • VideoPlayer (W3C media) — playback; the jump sets currentTime
  • KUI SeekBar — markers for ticks, displayAboveThumb for the mask
  • useTVEventHandler — skip keys and calibration; TVFocusGuideView — seek-bar focus
  • Vega Virtual Device + CLI — build, run, on-device logs and captures

Every number on this page is in the repo

Measured, not drawn.

The development match was labelled by the detector's author to tune thresholds, so these numbers show the pipeline works, not how well it generalises. The held-out result waits for a human's labels.

The change distance at every frame

evidence/bench/dev-winchester-320-trace.json · D, same computation as scripts/visibility.ts

0102030τ = 20max elsewhere 12.0 · 18:5017:00 · 32.049:30 · 31.80:0015:0030:0045:001:00:001:15:000102030τ = 20max elsewhere 12.017:0049:300:0030:001:00:00

Two spikes clear τ = 20: 32.0 at 17:00 and 31.8 at 49:30. Outside those two three-frame spikes, the noisiest frame reaches 12.0 (18:50) — a 2.64× margin at 320 px, 2.04× at 240×180 letterboxed.

A plain differ cannot do it

bench · ablationBaseline: consecutive frames, every cell, no hold

  1. Flipbook · τ 200 false · 2/2
  2. differ · τ 8496 false · 2/2
  3. differ · τ 12455 false · 2/2
  4. differ · τ 16389 false · 2/2
  5. differ · τ 20348 false · 2/2
  6. differ · τ 25283 false · 2/2
  7. differ · τ 30237 false · 2/2
  8. differ · τ 40149 false · 0/2
  9. differ · τ 5095 false · 0/2
  10. differ · τ 6055 false · 0/2
  11. differ · τ 8019 false · 0/2

To keep both goals the differ fires 237 false markers; raise its threshold to 40 and it misses both and still fires 149. Without the hold and the jitter mask, Flipbook itself would mark 350 times.

Where lands

bench · score.jumps, ablationAnchor (development labels)

Anchor on the last old-score frame28 s · 30 s
Naive: the first changed frame18 s · 20 s
  • Leads before the play: 28 s (16:20 → 16:48) and 30 s (48:50 → 49:20); 2/2 before the play, 95 % CI [0.158, 1].
  • Parameters frozen before any truth commit: PREREG.json, detector-params sha256 6f21e417…; the bench refuses matches A and B without a committed truth file.
  • Node (tsx) on Apple M-series, 20 runs — not device timings: pass 1 p50 210.8 ms, pass 2 p50 1,229.6 ms.

From the README's honest limits

Honest by design.

“Virtual device only. No Fire TV hardware was available; every timing is ‘Vega virtual device on Apple M-series’.”

README · Honest limits 1

“The held-out headline is pending. … Until then no such number is claimed.”

README · Honest limits 2

“The numbers above come from one development match with two labelled changes, labelled by the detector's author from 2 s stills. The 95 % confidence interval on 2/2 is [0.158, 1].”

README · Honest limits 3

Questions a judge would ask

FAQ

Does it really decode no video?

Yes, by construction: the scan code only ever opens the BIF's thumbnail JPEGs, decoded with jpeg-js. The bench and the device log print it — videoBytesDecoded: 0, video bytes decoded=0 — as a constant that records this, not as a measurement.

Did it run on a Fire TV?

On the Vega Virtual Device (Vega SDK 0.24.12112) on an Apple M-series Mac. No Fire TV hardware was available, so every timing says so. Some on-device captures are still owed — a calibrated scan of the development match among them — and the README lists them.

How good is it, really?

On the development match — two goals, labelled by the detector's author — it found 2 of 2 with 0 false markers, and both jumps landed before the play (95 % CI [0.158, 1]). That shows the pipeline works, not that it generalises.

The held-out number (goal jumps landing before the play on match B) will exist only after a human labels matches A and B. The parameters are frozen in PREREG.json first.

Why not read the digits?

Trickplay thumbnails are tiny — Amazon's are 240×180 — so Flipbook detects that the box changed and held, not what it says. No OCR, no ML, no LLM: arithmetic on a calibrated rectangle.

Doesn't blind mode still give something away?

A little, and the README says so: a jump tells you a change lies ahead, "No later change" tells you none does, and a goal the detector missed is skipped without any sign.

Is the footage OK to use?

The three matches are Belmont Media Center high-school broadcasts released CC0 1.0 on the Internet Archive. They show students and school marks; the marks are the schools', not ours, and CC0 does not cover trademarks. We mute the original commentary in everything we publish and use no close-ups. On this page the contact sheet and every seek-bar preview mask the score box; the stage's stills (the present, at the playhead) and the six crops show it on purpose.

Skip to the goal. Not the score.