Inspiration

Inox is my watch-party platform: friends join a room from their browsers and watch the same video in sync, with chat. But the best screen in the house was missing. Movie night happens on the living-room TV, not a laptop balanced on a knee. I wanted a family on the couch and friends on laptops to share the same moment, down to the frame, using nothing but a TV remote.

What it does

FireOS-Inox is a Fire TV app for Vega OS, built with React Native for Vega:

  • Sign in once. The session persists on the device. There's an app-drawn D-pad keyboard, because the Vega Virtual Device shows no system keyboard.
  • A lobby built for the couch. "Now Streaming" shows which rooms are active, what's playing and how many people are watching. It refreshes without stealing focus.
  • Frame-tight sync. The TV follows the room's play, pause, seek and media changes. Measured against the Inox web client: TV vs room p50 0.046 s, p95 0.059 s.
  • Couch Reactions. Press ▲ to send 😂 😮 🔥 👏 ❤️ 😢 or a quick reply ("Pause please!"). Reactions from anyone, on the TV or the web, float over the video with the sender's name. They travel as ordinary Inox chat, so the backend needed zero changes.
  • Sync you can see. A pill reads ● In sync or ↻ Catching up…, and the control bar shows the measured offset.
  • Permission-aware remote controls. Members without playback rights get a toast, and nothing is sent.
  • Resilient. It reconnects forever with jittered backoff, then resyncs from server state. After 30 s it shows "Can't reach Inox · Retry".

How I built it

  • packages/core is platform-free TypeScript: the Inox wire protocol with runtime guards, the REST client, a reconnecting room socket, the sync engine, and a room store (a reducer plus a session). fetch, WebSocket, timers and the clock are injected, so the same code runs under Node tests, against a mock and on the TV. It has zero runtime dependencies.
  • apps/vega is the React Native for Vega app: Boot, Sign-in, Lobby and Room screens, plus focus-driven overlays. A VegaPlayer adapter wraps Amazon's hls.js sample for HLS and uses the W3C media element directly for MP4.
  • tools/mock-inox is a mock backend with failure injection. Every faked behaviour cites the Inox source file it mirrors. Reviewers can run the app without the real backend.
  • Validation tooling: a scripted room participant, a headless browser host, a sync analyser, network-kill tests, a remote-only D-pad walk of every screen, and a player bench.
  • 129 unit and contract tests, plus device runs on the Vega Virtual Device for every feature.

The sync model

I don't store a "current time". Each client keeps an anchor: position p₀ at local time t₀. While playing, the expected position is p(t) = p₀ + (t − t₀).

Each SYNC_PLAYBACK gives me the device-to-server clock offset Δ, which matters because the VVD clock runs about 0.9 s behind the host. A PLAY event sent at server time tₛ is compensated by its one-way latency ℓ = t_now − (tₛ + Δ), applied only if 0 < ℓ < 5 s, which matches the web client's rule. When the player is more than 0.5 s from p(t), the TV corrects by seeking only. It seeks slightly ahead and holds until the buffer is ready, and the lead it uses adapts to the drift left over after each correction.

Challenges

The emulator fought back, and I logged 15 friction entries along the way. The worst ones:

  • The player raced ahead and wedged. Calling play() on a paused player could jump seconds ahead and then freeze. Seeking didn't fix it, and touching playbackRate blocked JS for seconds. The fix: never start with a plain play() (always seek-and-hold), never change the rate, and run a watchdog that rebuilds the media element when the playhead races or freezes.
  • A second stream never loaded. Loading a new URL into a used media element never became ready, so every media switch rebuilds the player.
  • The platform acted behind the app's back. w3cmedia's legacy PlayerSession paused the video itself when a member pressed ⏯, even though the app had refused the press. I had to disable it.
  • Small platform surprises. The OK key arrives as enter, not select. Vega's clearTimeout throws on stale handles, which crashed hls.js. The legacy AsyncStorage doesn't survive a restart. Apps lay out on a 960×540 canvas.
  • Video file limits. Files play over https only, MKV ignores every seek, and 5.1 AAC doesn't decode.

What I learned

  • Sync is a clock problem before it's a video problem. An anchor plus a measured offset beats chasing currentTime.
  • A strict, platform-free core with injected I/O made the hard logic testable on Node. The TV only had to prove the platform glue.
  • Don't trust the platform until you've checked it: every Vega assumption was verified on the device and recorded.
  • TV UX is its own discipline: focus, key-up timing, 10-foot type and "say what's happening" states.

What's next

Testing on real Fire TV hardware, playback-speed support once the player allows it, live channels, and an AI "What did I miss?" chat recap for late joiners.

Inox (the backend and web client) existed before the hackathon and was not modified. The TV app, the core package, the mock backend and all the tooling were built during the submission period. Demo content: Big Buck Bunny © Blender Foundation, CC-BY 3.0.

Share this project:

Updates

Submission history