The synthetic publisher is a transport test, not a video encoder. It sends an animated
telex.cells test pattern through the same ticket, WebSocket, relay, timing,
and backpressure path that the eventual FFmpeg/Chafa projector will use.
Nothing it sends becomes IRC traffic or retained chat history.
Create a bot or agent automation and an app key, then grant it only
stage.publish in the target channel. No native automation operations are
needed for Stage publication:
{
"channel": "#movie-night",
"expected_revision": 1,
"account_name": "StageBot",
"grant": {
"permissions": ["stage.publish"],
"history_window_seconds": null,
"invocation_context_events": 0
}
}The account does not receive channel messages through this transport. Its publisher authority exists only while that exact grant remains active.
Use Node.js 22 or newer from the repository root. Supply the app key through
the hidden prompt or IRC_CLIENT_PASSWORD; never place it on the command
line.
node examples/stage/synthetic-publisher.js \
--url https://irc.example.net:8098 \
--auth-id 'StageBot/019f0000-0000-7000-8000-000000000000' \
--channel '#movie-night' \
--columns 120 \
--rows 34 \
--fps 20 \
--seconds 20Use the generated authentication_id returned with the app key. Its suffix is
the server-generated app-key UUID, not the human key label. The process closes
the Stage when the pattern ends or the process exits.
Starting another publisher while the channel Stage is occupied is rejected.
The server drops a publisher whose grant is removed while it is live.
The reference projector is deliberately outside the daemon. FFmpeg decodes and resizes the source into a constant-rate RGB stream; a small Chafa helper turns each frame into curated glyphs and xterm256 foreground/background cells; the Node publisher packs and sends complete Stage frames. When the source has an audio stream, a second FFmpeg path publishes synchronized AAC-LC in fragmented MP4 on the same transient Stage connection.
On Ubuntu 26.04, install the build/runtime dependencies:
sudo apt install ffmpeg chafa libchafa-dev libglib2.0-dev pkg-configBuild the helper:
examples/stage/build-projector.shThen publish a local clip:
node examples/stage/video-publisher.js \
--url https://irc.example.net:8098 \
--auth-id 'StageBot/019f0000-0000-7000-8000-000000000000' \
--channel '#movie-night' \
--input ./clip.mp4 \
--columns 160 \
--rows auto \
--fps 24 \
--profile video \
--color-mode foregroundVisual output remains constant-rate. Audio defaults to AAC-LC at 48 kHz,
stereo, and 128 kbit/s in approximately 500 ms fragments. It adds about
16 KiB/s per listening viewer beside the much larger cell-video stream.
The publisher normalizes source audio to -16 LUFS with a -1.5 dB true-peak
ceiling so unusually quiet or loud clips enter the Stage at a predictable
listening level. Use --raw-audio to preserve source levels or --silent to
suppress source audio.
With --rows auto, the publisher probes the source display aspect and derives
the cell rows for the reference 8x16 cell shape. A 5:4 source becomes 160x64,
the 704x570 comparison clip becomes 160x65, and a 16:9 source becomes 160x45.
This avoids transmitting black padding cells. An explicit --rows remains
available for a deliberate fixed canvas.
--sample-scale controls how many source pixels Chafa sees per cell-width; its
default is four. Foreground color mode keeps the presentation visibly
character-based by drawing colored glyphs over black. --color-mode cell
retains Chafa's foreground and background colors for denser ANSI-art-like
output.
To inspect one projected frame without a server, generate a self-contained Canvas preview:
node examples/stage/frame-preview.js \
--input ./clip.mp4 \
--output /tmp/stage-preview.html \
--time 8 \
--columns 160 \
--rows 65 \
--profile video \
--color-mode foregroundSee docs/stage.md for the protocol boundary,
selected baseline, measured raw rates, and cross-device acceptance record.
stage-publisher.js is the normal entry point for file playback. The lower
level video publisher and frame preview remain available for integrations and
experiments.
First verify the local toolchain:
examples/stage/telex-stage doctorThe guided publisher has three 24 fps visual presets. Rows remain automatic so the source display aspect is preserved:
| Preset | Columns | Approximate raw video rate | Intended use |
|---|---|---|---|
compact |
120 | 2.4–3.3 Mbit/s | small screens or constrained links |
balanced |
160 | 4.1–5.9 Mbit/s | accepted default |
detail |
200 | 6.5–9.2 Mbit/s | deliberate high-detail playback |
Rates span 16:9 through 5:4 sources and exclude the small protocol, WebSocket, and TLS overhead. Relay egress is the selected rate per viewer. All preset choices can be overridden explicitly; they are publisher conveniences, not new protocol profiles.
Project one real frame through the same FFmpeg/Chafa path before broadcasting:
node examples/stage/stage-publisher.js preview \
--input ./clip.mp4 \
--preset balanced \
--time 8 \
--output /tmp/telex-stage-preview.htmlOpen the resulting HTML file locally. It is self-contained and never contacts the server.
After reviewing the preview, publish with the same preset:
node examples/stage/stage-publisher.js publish \
--url https://irc.example.net:8098 \
--auth-id 'StageBot/019f0000-0000-7000-8000-000000000000' \
--channel '#movie-night' \
--input ./clip.mp4 \
--preset balancedThe command checks dependencies and media readability, resolves the exact cell
geometry, reports the estimated raw cell-video rate and audio behavior, and
only then asks for the app key. Use --help after any subcommand for its
focused options. The password remains confined to IRC_CLIENT_PASSWORD or the
hidden prompt.
Create a small deterministic test archive without FFmpeg, Chafa, media, or a server:
examples/stage/telex-stage sample \
--output ./telex-stage-sample.tstageRelease source kits already include that canonical sample at
examples/stage/sample.tstage. It is silent, redistributable under the
repository license, and can be passed directly to inspect, play, or
station.
Convert once into a checksummed .tstage artifact:
examples/stage/telex-stage encode \
--input ./clip.mp4 \
--output ./clip.tstage \
--title "Opening clip" \
--preset balancedThe artifact contains the exact compressed Stage packet timeline, not the source file or a credential. Verify it independently:
examples/stage/telex-stage inspect --input ./clip.tstagePublish it without FFmpeg, Chafa, or the source media:
examples/stage/telex-stage play \
--url https://irc.example.net:8098 \
--auth-id 'StageBot/019f0000-0000-7000-8000-000000000000' \
--channel '#movie-night' \
--input ./clip.tstagestation takes a bounded JSON playlist of .tstage files, verifies every
item before requesting the app key, and plays them sequentially. An optional
atomic state file restarts an interrupted item from its beginning. See
playlist.example.json and the complete
Stage publishing guide for provisioning,
packages, the canonical sample, containers, archive/XDCC exchange semantics,
station operation, release checks, and troubleshooting.