Self-hosted service running on a Mac mini that plays any YouTube video — live or VOD — as a real-time ASCII video stream in a web browser. Primary client: the in-car Tesla browser (Chromium, modest Canvas budget).
The user types a YouTube URL, an 11-character video ID, or a free-text search query and hits Play. Audio plays alongside the ASCII video. A 4–8 digit PIN gates access; the page is exposed to the internet through Cloudflare.
Browser Mac mini
───────── ────────────────────────────────────────
GET / ──▶ static/index.html (or pin.html if no cookie)
POST /api/auth ──▶ HMAC cookie issued (Secure only on https)
POST /api/play ──▶ resolver.py (yt-dlp) → ResolvedMedia
pipeline.py spawns ffmpeg ×2 (video + audio)
WS /ws/video ◀── INIT message + binary ASCILINE frames
GET /audio ◀── AAC/ADTS chunked stream
| File | Role |
|---|---|
server.py | FastAPI app, routes, auth middleware |
auth.py | HMAC cookie, brute-force lockout, PIN check |
session.py | Single-active-session state machine |
resolver.py | yt-dlp wrapper: ID/URL/search → media URLs + metadata |
pipeline.py | ffmpeg subprocesses (video rawvideo + audio AAC), encoder, async iterator |
config.py | Quality presets, server defaults |
static/ | Frontend (index.html, app.js, style.css, pin.html) |
vendor/asciline/ | Vendored ASCILINE encoder + protocol notes (MIT) |
ffmpeg is invoked with -re -fps_mode cfr -r <target_fps>. -re makes the input read at native realtime rate; -fps_mode cfr -r N resamples the output to a constant N fps. Frames arrive on the consumer side evenly paced — no consumer-side pacing is needed. The async frames() iterator is a trivial drain: read from the queue, encode, send.
Vendored unchanged from ASCILINE. Plain-text INIT message (INIT:fps:mode:cols:rows:pixel) followed by binary frames. Each frame begins with a 4-byte big-endian frame index; the body is mode-dependent (mode 1 = utf-8 text; modes 2/3 = [charCode, r, g, b] per cell). See vendor/asciline/PROTOCOL-NOTES.md.
| Preset | Grid | FPS | Color mode | Verified |
|---|---|---|---|---|
| LOW (default) | 120 × 50 | 24 | 2 (512 colors) | FPS 24/24 JIT ~42 ms (M4) |
| MED | 160 × 68 | 30 | 3 (32K colors) | Paint-bound on M4 |
| HIGH | 200 × 84 | 30 | 3 (32K colors) | Paint-bound on M4 |
LOW is the preset used in the Tesla. MED/HIGH render correctly but the per-cell fillText color path on Chromium caps at ~150–300 K calls/s, which is at the wall for MED and over it for HIGH.
--pin <4-8 digits> or ASCIILINE_PIN env.itsdangerous.TimestampSigner); secret persisted to .secret across restarts; 30-day expiry.CF-Connecting-IP used behind Cloudflare so each car/device is locked out independently.Secure only when the auth request itself came in over https. Over plain http (local dev) the cookie omits Secure so the browser actually sends it on the /audio GET; otherwise audio would silently 403 while video kept working.idle | resolving | playing | error.POST /api/play kills the previous session: SIGTERM to both ffmpegs in parallel, SIGKILL after 3 s.play() returns immediately.atexit + SIGINT/SIGTERM handlers; on shutdown both pipelines are torn down synchronously and no orphan ffmpeg survives.The renderer perf pass that closed the project exposed three things that contradicted the original guesses:
fillText is NOT the bottleneck at LOW. Diagnostic measurement on the M4 showed the color render loop at 4.9–7.3 ms per frame (≈140 fps of headroom). The earlier "13/24" figure was the server's irregular delivery, not paint cost. A glyph atlas was tried; it regressed performance because mode-3 (32K colors) thrashes any reasonably-sized LRU. The atlas was reverted. Do not re-add a glyph atlas. If MED/HIGH smoothness ever matters, the known fix is an ImageData/putImageData pixel renderer (the pixelMode path already uses this approach).Secure flag. Over plain http://localhost the browser refuses to send Secure cookies, so the /audio GET arrived without auth and got 403'd, while the WebSocket worked because of its different upgrade handshake. Fix: only set Secure when the auth request itself came in over https.-re -fps_mode cfr -r N on ffmpeg makes frames arrive evenly paced; the consumer becomes a trivial drain.All numbers are from the M4 reference (Mac mini Apple Silicon, server + browser on the same machine). The Tesla in-car browser (Ryzen MCU, Chromium) is the deployment target but is currently untested in this build.
brew install ffmpeg python3 -m venv .venv source .venv/bin/activate pip install -r requirements.txt python server.py --pin 1234
Or run ./run.sh idempotently — it does the same setup, prompts for a PIN, and starts the server.
Open http://<host>:8000/, enter the PIN, type a video ID / URL / search, hit Play.