From 74f49c7712cc514051d4450ecc30ff11f547f1b8 Mon Sep 17 00:00:00 2001 From: Erhan Keseli Date: Sat, 13 Jun 2026 18:05:19 +0200 Subject: [PATCH] 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. --- .gitignore | 21 + README.md | 160 ++++++++ auth.py | 162 ++++++++ config.py | 33 ++ docs/PROJECT-REPORT.html | 172 ++++++++ pipeline.py | 354 +++++++++++++++++ requirements.txt | 7 + resolver.py | 203 ++++++++++ run.sh | 55 +++ server.py | 352 +++++++++++++++++ session.py | 273 +++++++++++++ static/app.js | 527 +++++++++++++++++++++++++ static/index.html | 84 ++++ static/pin.html | 285 +++++++++++++ static/style.css | 412 +++++++++++++++++++ vendor/__init__.py | 0 vendor/asciline/LICENSE | 26 ++ vendor/asciline/__init__.py | 0 vendor/asciline/app.js | 354 +++++++++++++++++ vendor/asciline/ascii_video_player2.py | 334 ++++++++++++++++ vendor/asciline/stream_server.py | 313 +++++++++++++++ 21 files changed, 4127 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 auth.py create mode 100644 config.py create mode 100644 docs/PROJECT-REPORT.html create mode 100644 pipeline.py create mode 100644 requirements.txt create mode 100644 resolver.py create mode 100755 run.sh create mode 100644 server.py create mode 100644 session.py create mode 100644 static/app.js create mode 100644 static/index.html create mode 100644 static/pin.html create mode 100644 static/style.css create mode 100644 vendor/__init__.py create mode 100644 vendor/asciline/LICENSE create mode 100644 vendor/asciline/__init__.py create mode 100644 vendor/asciline/app.js create mode 100644 vendor/asciline/ascii_video_player2.py create mode 100644 vendor/asciline/stream_server.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..482cc00 --- /dev/null +++ b/.gitignore @@ -0,0 +1,21 @@ +# Python build artifacts +.venv/ +__pycache__/ +*.pyc +*.pyo +*.egg-info/ + +# Runtime / secrets +.secret +*.log + +# Process / IDE noise +.DS_Store +.claude/ + +# Internal-process docs — keep README.md only +ASCILINE-YOUTUBE-SPEC.md +PROGRESS.md +handover/ +docs/*.md +vendor/asciline/PROTOCOL-NOTES.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..d151cbc --- /dev/null +++ b/README.md @@ -0,0 +1,160 @@ +# 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 + +```bash +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 + +```bash +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 | 4–8 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](https://github.com/YusufB5/ASCILINE). See +`vendor/asciline/PROTOCOL-NOTES.md`. + +### Pipeline pacing + +ffmpeg is invoked with `-re -fps_mode cfr -r ` 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: + +```bash +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 `