█████╗ ███████╗ ██████╗██╗██╗ ██╗███╗ ██╗███████╗ ██
██╔══██╗██╔════╝██╔════╝██║██║ ██║████╗ ██║██╔════╝ █████
███████║███████╗██║ ██║██║ ██║██╔██╗ ██║█████╗ ████████
██╔══██║╚════██║██║ ██║██║ ██║██║╚██╗██║██╔══╝ ████████
██║ ██║███████║╚██████╗██║███████╗██║██║ ╚████║███████╗ █████
╚═╝ ╚═╝╚══════╝ ╚═════╝╚═╝╚══════╝╚═╝╚═╝ ╚═══╝╚══════╝ ██
ASCILINE is a high-performance, cross-platform real-time ASCII video rendering engine. It maps pixels to text-based representations and streams the result over a low-overhead binary protocol, turning the browser canvas into a typographic display surface.
- Pure typographic manipulation: the visual stream is raw HTML/Canvas text, not a standard media file. That means real-time CSS filters (glows, shadows, animations) can be applied directly to what would otherwise be a video.
- Zero GPU, ultra-low bandwidth (ASCII modes): standard codecs (H.264/VP9) need dedicated hardware decoders, which chokes microcontrollers and weak devices. ASCILINE does the heavy lifting server-side and streams lightweight text frames — fewer columns means proportionally less bandwidth. This makes fluid playback possible on constrained networks and zero-GPU devices (smart appliances, retro terminals, basic microcontrollers).
- Works everywhere: no
<video>tag, no browser-side codec decoding, no autoplay restrictions. To the browser, it's just text on a canvas.
Roadmap idea, not implemented yet: because ASCII output is already a compact, structured text representation, it could in principle serve as a lightweight input for downstream text/LLM processing instead of feeding raw pixel streams to a vision model. Nothing in the current codebase does this — flagging it here as a direction, not a shipped feature.
- Cross-platform: Windows, macOS, Linux.
- Real-time ASCII and pixel streaming: low-latency video-to-text conversion; pixel mode replaces characters with colored blocks, approaching 360p quality.
- HTML5 Canvas rendering, tuned for 24–30 FPS playback. Higher-FPS sources are automatically decimated for stability.
- Master clock sync: the audio track is the absolute time reference, keeping A/V synchronized.
- Low-overhead binary protocol: frames are streamed as raw
Uint8Arraystraight to the canvas. - Multiple color modes: from black & white up to 16M-color ultra-fidelity.
- Flexible video management: JSON playlists (per-video mode & volume), folder-based auto-queuing, single-file mode, infinite loop — all via CLI flags.
- Backend (Python/FastAPI): decodes video via OpenCV, maps pixels to ASCII via NumPy, streams binary frames.
- Frontend (vanilla JS): receives binary frames over WebSocket, manages a jitter buffer, renders to a canvas grid.
- Communication: a custom
INIThandshake negotiates resolution/FPS, followed by the binary frame stream.
The original protocol re-sends the full grid every frame. An opt-in adaptive codec picks the smallest of several encodings per frame and tags it with a 1-byte header, without changing the rendered output:
| tag | encoding | best for |
|---|---|---|
0 RAW |
framebuffer as-is (legacy) | incompressible frames |
1 ZLIB |
zlib(framebuffer) |
general motion |
2 DELTA |
only the cells that changed since the last frame | static / low-motion |
3 RLE_FULL |
run-length encoded framebuffer | large flat-color regions |
Clients opt in with /ws?codec=adaptive; omit it and you get the original protocol byte-for-byte, so existing clients are unaffected. A keyframe is forced periodically so dropped packets / late joiners resync.
codec.js (the shared decoder used by both the live player and the test suite) understands all four tags. Not every encoder produces all four, though: the Python side (codec.py, used by the live server and by static_player/compiler.py) can emit RLE_FULL when it wins the size comparison. The browser-side JS encoder (static_player/studio/encoder.js, used by the client-only Studio compiler) intentionally only emits RAW/ZLIB/DELTA — it doesn't implement RLE run-building, to keep the in-browser encoder simple. RAW/ZLIB/DELTA already cover most cases reasonably well, so this is a deliberate simplicity/size trade-off, not a bug — decoders stay permissive, encoders stay conservative.
Measured wire savings (mode 5, 200×80 grid):
| content | vs. legacy |
|---|---|
| static screen / slideshow | 0.3% (≈375×) |
| high-motion / full-frame change | 63% (never worse than legacy) |
An optional --quality {lossless,high,balanced,low} enables lossy temporal delta: a color cell is only re-sent once it drifts past a tolerance from what the viewer already sees (the character plane stays exact), cutting the hard cases a further ~15–30% at imperceptible quality. Default is lossless (bit-exact).
Monitor bandwidth in real time: pass --debug when launching the server to see live RAW vs WIRE byte comparisons and the compression ratio in your terminal.
Verified two independent ways, both bit-exact: Python-encoded vectors decoded by
codec.jsin Node (experiments/gen_vectors.py→experiments/check_vectors.js), and a liveadaptive-vs-legacyWebSocket diff (experiments/test_e2e.js). Generate test clips withexperiments/make_test_clips.sh.
LAN / network streaming: use --host to expose the server on your network.
python stream_server.py video.mp4 --host 0.0.0.0ASCILINE can compile a video into a self-contained .ascf (ASCII Compressed Format) file and play it back with a static HTML page — no Python backend at runtime, hostable anywhere (GitHub Pages, Vercel, Netlify).
Trade-off: compiled
.ascffiles are naturally larger than standard.mp4. In exchange you get true DOM-level interaction, pixel-perfect text selection, and no dependency on the browser's video codecs.
There are two ways to produce a .ascf file:
python static_player/compiler.py your_video.mp4 --cols 250 --pixel --quantize 2--quantize 0-3: drops color bits to reduce file size (0 = lossless, 3 = aggressive).--tolerance: color drift tolerance before a pixel update is sent, to skip invisible changes.--hard: max zlib compression (level 9) — slower to compile, smaller output.
This is what powers the live demo at asciline.dev: the static clips there are compiled with this Python path.
static_player/studio/ is a standalone page (index.html + encoder.js, using pako from a CDN for deflate) that compiles a video to .ascf entirely client-side — drop a video in, get a .ascf out, nothing ever leaves the browser and no Python is required. It's the more experimental of the two paths (see the codec note above: it only emits RAW/ZLIB/DELTA, not RLE_FULL) and is best for quick, short clips rather than production output — for anything longer or size-sensitive, use the Python compiler.
Place the generated .ascf next to static_player/index.html and open it through any local web server.
Best practice: compile short clips (under 5–10 minutes).
.ascfstores raw render instructions for the canvas, so full-length movies can produce file sizes that exceed your browser's memory limits.
git clone https://github.com/YusufB5/ASCILINE.git
cd ASCILINEpip install fastapi uvicorn opencv-python numpy websocketsRunning headless (server / no display, e.g. a VPS or container)? opencv-python-headless is a lighter drop-in replacement for opencv-python and avoids pulling in GUI dependencies you won't use.
Optional — play from YouTube (and other yt-dlp sites):
pip install yt-dlpOnly needed if you pass a URL instead of a local file. Local playback works without it. URL playback also uses FFmpeg (see below) to normalize downloads.
Package manager (recommended):
- Windows:
winget install ffmpeg - macOS:
brew install ffmpeg - Linux:
sudo apt install ffmpeg
Manual (Windows): if you hit a FileNotFoundError or don't want to touch system variables, download the FFmpeg ZIP, extract ffmpeg.exe and ffprobe.exe from bin/, and drop both into the project folder next to stream_server.py.
Single video:
python stream_server.py video.mp4 --cols 240YouTube / URL (requires yt-dlp):
python stream_server.py "https://youtu.be/VIDEO_ID" --cols 240
python stream_server.py "https://www.youtube.com/playlist?list=..." --cols 220 --loopGarbage collection for cached downloads: ASCILINE includes an LRU cache limiter for on-demand YouTube downloads so disk usage doesn't grow unbounded.
python stream_server.py --cache-limit 5000 # cap the video cache at 5 GB (default 10240 MB)How caching works:
- ASCII rendering only needs a small grid, so yt-dlp fetches at ≤480p to save bandwidth.
- Downloads are cached by video ID in
videos/— replays are instant. - Playlist/channel URLs and
playlist.jsonexpand into a queue and fetch on demand; the server starts immediately instead of waiting for bulk downloads. - Every downloaded video is normalized to H.264/AAC constant frame rate, so A/V sync holds regardless of the source codec.
Folder mode — drop videos into videos/ and run:
python stream_server.py --folder videos --cols 200
python stream_server.py --folder videos --cols 230 --loop
python stream_server.py --folder videos --mode 5 --pixel --cols 320 --vol 2Videos play in filesystem order (as they appear in the folder, not alphabetically). Add/remove files to control the queue.
JSON playlist — per-video overrides:
python stream_server.py --playlist playlist.json --cols 220
python stream_server.py --playlist playlist.json --cols 220 --loopOpen http://localhost:8000 in your browser.
Hover previews are built once per video on first hover, in a single ffmpeg pass, kept in memory — nothing written to disk. Disable with --no-thumbnails. To use a prebuilt sprite instead, point the /scrub route at it.
python stream_server.py --webcam --cols 240
# Different camera device and target FPS
python stream_server.py --webcam --webcam-device 1 --webcam-fps 60
# Disable the automatic horizontal mirror
python stream_server.py --webcam --no-mirrorBypass the web interface and render inside an ANSI-capable terminal (zero flicker, true color):
python ascii_video_player2.py video.mp4 --cols 100 --quality 0
# Webcam directly in the terminal
python ascii_video_player2.py --webcam --cols 100Don't resize the terminal window during playback — dynamic text wrapping will corrupt the layout.
Edit style.css to change accent colors and typography via CSS variables:
:root {
--accent-color: #00ff41; /* Classic Matrix Green */
--bg-color: #050505;
}Click FX on the player controls (or press F) to open the filter overlay.
- Contrast — adjust the difference between light and dark areas
- Brightness — control the overall lightness of the output
- Gamma — recover detail from dark/washed-out sources
- Sharpen — Unsharp Mask, levels 0–10
- Invert — instantly invert all brightness values
- Palettes — swap character sets live:
Default: full detailed ASCII rampFlat/Anime: shortened, minimalist ramp (good for animation)Block: chunky, dense characters for a retro-terminal look
python stream_server.py --mode 5 --cols 240 --rows 1001: Black & White (DOM mode)2: 512 colors3: 32K colors4: 262K colors5: 16M colors (ultra)
Specify only --cols; ASCILINE derives --rows from the source aspect ratio.
- ASCII mode:
--cols 200–240(recommended starting point for the best balance of detail and 30 FPS performance; can be increased if your hardware allows). - Pixel mode:
--cols 600–900(recommended starting point for near-HD quality; performance depends heavily on CPU). - If
--colsisn't set, defaults are450in pixel mode and200in ASCII mode. - Hardware limits & A/V sync: pushing
--colsbeyond what your machine can encode/send in time causes the video to fall behind the audio (desync). If you see this, lower--cols.
python stream_server.py video.mp4 --mode 5 --cols 240
# Terminal shows: [AUTO] 1920x1080 → grid 240x67--vol (0–5). At 0, FFmpeg's audio path never runs — saves CPU and bandwidth.
--vol |
Multiplier | |
|---|---|---|
0 |
— | Muted (no processing) |
1 |
1.0× | Normal (default) |
3 |
1.5× | Loud |
5 |
2.0× | Double volume |
python stream_server.py video.mp4 --pixel --cols 560 --vol 0 # silent
python stream_server.py video.mp4 --cols 220 --vol 3 # loudEach entry can override the global --mode, --pixel, --vol, and --cols:
[
{ "video": "intro.mp4", "mode": 1, "vol": 1 },
{ "video": "main.mp4", "mode": 5, "pixel": true, "vol": 3, "cols": 520 },
{ "video": "https://youtu.be/VIDEO_ID", "mode": 3, "vol": 2, "cols": 240 }
]Paths are resolved automatically — the project root and videos/ are both checked, so a filename alone is usually enough.
Live, browser-based showcase across multiple rendering modes: asciline.dev
If this project is useful to you, crypto donations are welcome:
- Solana (SOL / USDC):
H1wSQAhjgsu7AxenF4e5ZBYiBjkhDLVzkKaZuVPcrE14 - Ethereum (ETH / USDT):
0x85B2f970045c0F7c282089Ab6CF897C20230e086 - Bitcoin (BTC):
bc1qvtcl55v54gkzwnp2zxn70usea3gf5ncncqa0fv
ASCILINE is distributed under a Custom License (Based on MIT) which includes an anti-advertisement clause. See LICENSE for the full text.
Join the Codequerors Discord Server to discuss creative coding, share ideas, or contribute to ASCILINE.


