Files
teslayoutube/README.md
Erhan Keseli 74f49c7712 Initial commit: ASCILINE YouTube Streamer
ASCII-art YouTube streaming for the Tesla in-car browser.

- FastAPI server on a Mac mini, no Docker.
- yt-dlp resolver: ID/URL/search.
- ffmpeg with -re -fps_mode cfr for source-paced video; trivial drain
  consumer.  Separate ffmpeg for AAC/ADTS audio.
- Vendored ASCILINE renderer (MIT) for the binary wire protocol; pure
  fillText color path, on-demand selection flush.
- HMAC PIN-gated cookie; Secure flag scheme-aware so /audio works on
  plain http during local dev.
- LOW preset (120x50 24fps) verified clean on M4: FPS 24/24, JIT ~42ms.
2026-06-13 18:05:19 +02:00

5.7 KiB
Raw Permalink Blame History

ASCILINE YouTube Streamer

Plays any YouTube video (live or VOD) as a real-time ASCII art stream in a web browser. Designed for the Tesla in-car browser, works in any Chromium browser. Runs directly on a Mac mini — no Docker.

Requirements

  • macOS (Apple Silicon recommended)
  • Python 3.11+
  • ffmpeg (Homebrew)

Setup

brew install ffmpeg
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt

Or run ./run.sh once and it does the same thing idempotently, prompts for a PIN, and starts the server.

Running

source .venv/bin/activate
python server.py --pin 1234

The server listens on http://0.0.0.0:8000 by default. Open it, enter the PIN, then type a YouTube URL, video ID, or search query and press Play.

Flag Default Description
--pin required 48 digit PIN (or ASCIILINE_PIN env var)
--port 8000 TCP port to listen on
--bind 0.0.0.0 Bind address
--max-source-height 720 Maximum source video resolution

Quality presets

Preset Grid FPS Notes
Low (default) 120×50 24 Tesla / target
Med 160×68 30 Desktop
High 200×84 30 Desktop

LOW is the only preset that's been verified clean on the M4 reference (FPS 24/24, JIT ~42 ms). MED/HIGH are paint-bound on the per-cell fillText loop on the M4 — they work, but with visible jitter. The Tesla in-car browser runs LOW.

Preset changes take effect on the next Play (server restarts ffmpeg with the new dimensions).

Architecture

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

Wire format and rendering are vendored from ASCILINE. See vendor/asciline/PROTOCOL-NOTES.md.

Pipeline pacing

ffmpeg is invoked with -re -fps_mode cfr -r <target_fps> so frames arrive evenly paced at the target rate. The consumer is then a trivial drain — read from the queue, encode, send. Earlier iterations tried consumer-side pacing (relative sleep, absolute targets, skip-ahead, re-anchor) and all failed the same way: a bursty producer cannot be smoothed by any consumer-side scheme. Source-side pacing fixed it in one flag.

Cloudflare Tunnel

The service is designed to run behind a Cloudflare Tunnel for internet access from the Tesla:

brew install cloudflare/cloudflare/cloudflared
cloudflared tunnel login
cloudflared tunnel create asciline
cloudflared tunnel route dns asciline yourdomain.com
cloudflared tunnel run asciline

The PIN brute-force lockout uses CF-Connecting-IP, so each car/device is locked out independently — one device's 5 bad attempts does not lock out the others.

The auth cookie is marked Secure only when the auth request itself came in over https. Over plain http://localhost (local dev) the cookie is set without Secure so the browser will actually send it on the /audio GET; otherwise audio would silently 403 while video kept working.

Tesla browser notes

  • Audio is AAC/ADTS via <audio>; no extra codecs needed.
  • Touch targets are ≥ 48 px; PIN keypad has large tap targets.
  • Cookie persists 30 days; you should not re-enter the PIN often.
  • Preset: stick to LOW.

Known limitations

  • A/V drift on long VODs. Video and audio run as two independent ffmpeg processes with no shared clock. They start together but drift is inevitable over hours. Negligible on live streams; audible after 2040 min on long VODs. Timestamp-level sync is out of scope.
  • Signed stream URL expiry (~6 h). YouTube googlevideo.com URLs carry time-limited signatures. Live sessions left running past ~6 h will stop when the manifest URL expires. Press Play again to re-resolve.
  • In-memory PIN lockout resets on restart (acceptable for a personal service).
  • No seeking, no pause/resume. Stop and re-Play restarts from the beginning (VOD) or live head (live).
  • Single active session. A new Play immediately kills the previous.
  • MED/HIGH paint-bound on M4. The per-cell fillText color render caps at ~150300 K calls/s on Chromium; MED at 30 fps × 10880 cells is near the wall, HIGH is past it. The known fix if MED/HIGH smoothness ever matters is an ImageData/putImageData pixel renderer (the pixelMode path already does this for upstream pixel-mode streams) — NOT a glyph atlas (we tried; LRU thrashes on mode-3 colors and the result regressed to single-digit FPS).
  • Tesla in-car untested in this code state; numbers above are M4 proxy.

Test matrix

Test Input Expected
VOD dQw4w9WgXcQ ASCII video, audio, no LIVE badge
Live https://www.youtube.com/watch?v=X4VbdwhkE10 ASCII + audio + LIVE badge
Search lofi hip hop First search hit, plays
Invalid not-a-real-id-!! Error shown in status bar

Process cleanup

The server registers atexit + SIGTERM/SIGINT handlers. Both ffmpeg processes get SIGTERM in parallel; SIGKILL after 3 s if needed. No orphans survive a clean shutdown.

# Verify after Ctrl-C or kill:
ps aux | grep ffmpeg
# Expected: (nothing)