Skip to content

Project / 02

TV Guide

A phone-sized YouTube remote for an Apple TV, built for one person who found the TV's own interface unusable.

Home

  • 30:47

    Sample News Network

    Evening briefing — Tuesday

  • 60:20

    Sample Debates

    Panel: should cities build more housing?

  • LIVE

    Sample News Network

    Live: budget hearing

  • 16:02

    Sample Science

    How tide gauges actually work

  • 35:40

    Sample Debates

    Interview: the case against speed limits

  • 12:25

    Sample Science

    Why bridges hum in the wind

A working replica of the interface, running on sample data — no sign-in, no television, nothing here talks to YouTube.

Snapshot

The fast read

Role
Design and build
Year
2026
Status
Deployed
Type
Personal
Team
Solo
Stack
Next.js, React, TypeScript, Tailwind CSS, YouTube Data API v3, YouTube Lounge API, Google OAuth, Vercel
Tags
Software · Web · Accessibility

Section

Why it exists

The YouTube app on a television is hard to use if you are not comfortable with a directional remote. Searching means pecking at an on-screen keyboard, the home screen is a wall of recommendations, and finding the channel you watch every night takes a dozen clicks.

TV Guide replaces the remote. It is a web app you open on a phone — installable to the home screen, where it behaves like a native app — showing the channels you actually subscribe to as a scrollable list of large thumbnails. Tap a video and it starts on the TV. A now-playing bar handles play and pause, skipping, seeking and chapter jumps.

It was built for one specific person: a parent who watches long-form news and debate on an Apple TV. That single-user brief drove every decision — 48-pixel minimum touch targets, 18-pixel minimum type, high-contrast dark mode, no nested modals, no horizontal scrolling, every screen usable one-handed.

Section

How it works

Neither half of the app can run in the browser. The Data API key would be exposed in the bundle, and YouTube's Lounge endpoints send no CORS headers, so browser requests to them are blocked outright. Everything therefore goes through the app's own server-side route handlers running as Vercel functions: the phone only ever talks to /api on its own origin, and the secrets never leave the server.

Casting rides the same undocumented protocol YouTube's own mobile app uses. The television shows a pairing code; the server exchanges it for a durable screen ID, that for a lounge token, and commands — set playlist, add video, play, pause, next, seek — are posted into a bind session on Google's BrowserChannel wire protocol.

Three pages sit behind a bottom tab bar: Home, which merges recent uploads across every subscription; Your Channels, a large dropdown of subscribed channels; and Queued, a session-only mirror of what has been lined up. The feed is the signed-in user's real subscription list, not a recommendation algorithm.

Section

Engineering decisions

The offset counter is the whole ballgame. Every message posted into a bind session carries a running count of messages already sent. Get it wrong and the server does not return an error — it returns 200 OK and silently drops the message, so the television simply does not respond. The session counters are threaded through every round trip and mirrored into local storage, because serverless functions keep no memory between invocations.

Failures escalate in tiers rather than dumping the user back to a pairing screen: retry on a fresh bind session, then re-mint the token from the stored screen ID, and only if that also fails clear storage and ask for a new TV code. The earlier build made you re-pair constantly.

The status reader is deliberately GET-only. It never posts, so it can never touch the offset counter the commands depend on — the part that reads state structurally cannot break the part that sends commands.

Reading status returns three results, not two: now playing, stopped, and no update. No update means nothing was caught this probe, which is the normal result during uninterrupted playback, and the interface has to retain its last known state. Treating it as nothing playing is the flicker bug that took longest to kill.

Subscriptions rather than recommendations, partly by necessity: no public API exposes the recommendation feed any more, and the only route to it is scraping an internal API with logged-in cookies, which violates the terms of service and risks the account. Recent uploads from channels you chose to follow turned out to suit the use case better anyway.

Security and quota shaped the rest. The refresh token lives only in an AES-256-GCM encrypted, http-only cookie — never in local storage, never in a public environment variable. Authenticated API calls are forced past Next's shared response cache so private data cannot enter it. Uploads are fetched through a channel's uploads playlist at one quota unit rather than search at a hundred, against a daily allowance of ten thousand.

Section

Reverse-engineering the back channel

The hardest-won knowledge in the app came from tracing YouTube's Lounge back channel against a real Apple TV rather than assuming how it behaves.

It is not a stream: the server sends one short batch and closes the connection in about half a second, even mid-playback, so the reader makes bounded quick reconnects inside an eight-second budget. Between events it sends only heartbeats. And steady playback emits no events at all — a twenty-second trace during untouched playback caught fifteen reconnects and zero playback events. The television speaks only on transitions: play, pause, seek, load, stop.

Because position only arrives at those transitions, the playhead is extrapolated locally: the television supplies an anchor, the client ticks a timer between anchors and re-anchors on the next event or user command. That is what makes the scrubber and the ten-second skips feel immediate.

Section

Where it stands

Deployed on Vercel and in use — the live app is linked below, though it needs a Google sign-in and a paired television to do anything, which is what the demo at the top of this page stands in for. Pairing is one-time: enter the code from the television once and the session persists across reloads.

All Lounge logic is confined to two directories, because the protocol is unofficial and can break without warning — it stays fixable in one place and never leaks into the interface components.

Links

Elsewhere

Next project