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

161 lines
5.7 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 | 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](https://github.com/YusufB5/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:
```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 `<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.
```bash
# Verify after Ctrl-C or kill:
ps aux | grep ffmpeg
# Expected: (nothing)
```