← built

projector-remote

A phone remote and media backend running on my projector, written in Go.

kind
side project
year
2026

§01 context

We carried a projector to a friend's place for movie night and nobody had the remote. No IR codes existed for it anywhere, so we plugged in a laptop and drove the menus over ADB, one input keyevent at a time. We never watched the movie, but I couldn't stop thinking about it.

It's now a Go server that boots with the projector, serves a React PWA, and drives the input hardware directly. I reverse engineered NetMirror's API so I could pick something on my phone and play it on the projector through my own Media3 ExoPlayer app. Disabled the tracking too. Then I built an entire anime aggregator API into the same Go server. Tailscale lets me reach it privately from my other devices.

§02 shape

How it works

phone / laptop (PWA)
        │  HTTP + WS over LAN / Tailscale
        ▼
Go server, running ON the projector
 ├─ serves the embedded React build (go:embed)
 ├─ /ws        JSON message protocol for the remote
 ├─ /api/catalog  NetMirror client + anime aggregator API
 ├─ deviceio   local command execution
 └─ playback   sessions, resume, next episode
        │                     │  WS /player (localhost)
        ▼                     ▼
 kernel input (uinput)    player app (ExoPlayer)
 + input text / am          HLS / MP4 + subtitles

§03 hard parts

The hard parts

  1. 01

    Surviving a reboot

    ADB over TCP turned itself off on every boot because of a vendor init script. Fixing it meant understanding the boot chain and dynamic partitions, then patching one file inside a mounted, read-only partition without bricking the thing.

  2. 02

    Finding a backdoor

    While going through the boot scripts I found an OEM backdoor that was still active. I disabled it before putting the projector back on my home network. It's in the write-up, without anything that identifies the device.

  3. 03

    Making input fast

    Running input keyevent for every button press is slow. So the server writes straight to uinput and the pointer node, and only uses input and am for typing text and opening apps.

  4. 04

    Reverse engineering NetMirror

    I went through the app and captured its HTTPS traffic with mitmproxy to figure out the catalog, search, episode, and playback calls. Now I can pick a title and control the audio, subtitles, and quality from my phone while it plays on the projector. I disabled the tracking calls since they weren't needed for any of that.

  5. 05

    Building the anime aggregator API

    AniList handles the metadata, and Go adapters find streams from different providers. The server does the title matching, episode lists, and sub/dub selection. It checks providers concurrently, takes the first playable source, and leaves a failed provider out when trying again. Watch progress uses stable release and episode IDs, so changing the source doesn't lose my place.

stack

  • Go
  • WebSockets
  • React PWA
  • Android
  • Media3 ExoPlayer
  • Tailscale

next →

WhatsApp service

A WhatsApp Cloud API service that a multi-tenant CRM can plug into.