Live streaming¶
asciinema server provides real-time terminal session broadcasting with minimal latency, enabling live streaming of terminal sessions to multiple viewers.
The streaming system supports several application-level protocols with protocol negotiation. For simpler clients/use-cases it provides basic protocol auto-detection. Streaming is subject to configurable limits including per-user stream count restrictions and bandwidth rate limiting.
Live terminal streaming is supported by asciinema CLI since version 3.0.
Architecture¶
The live streaming architecture follows a producer-consumer model with three key parties:
┌──────────────┐
│ producer │
│ (CLI) │
└──────────────┘
│
WS
│
▼
┌──────────────┐
│ relay │
│ (server) │
└──────────────┘
▲ ▲ ▲
/ │ \
WS WS WS
/ │ \
/ │ \
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ consumer │ │ consumer │ │ consumer │
│ (player 1) │ │ (player 2) │ │ (player 3) │
└──────────────┘ └──────────────┘ └──────────────┘
The producer, typically asciinema CLI, captures terminal events and streams
them to the producer endpoint (/ws/S/<producer-token>
) on the server.
The server acts as a relay, receiving events from producers and distributing them to consumers while maintaining the complete stream state.
Consumers, such as asciinema player, connect to the consumer endpoint
(/ws/s/<public-token>
) on the server to receive real-time terminal events and
display the live session.
Both producer and consumer connections utilize WebSocket transport, implementing application-level protocols (WebSocket sub-protocols) for event transmission.
The server maintains comprehensive state for each active stream by running the whole stream through asciinema's own virtual terminal emulator. This enables new consumers to connect mid-stream and immediately receive the current terminal view, ensuring a seamless viewing experience regardless of when viewers join the session.
Live stream lifecycle¶
Before the producer can start sending stream events to the producer WebSocket endpoint, streams must be created and configured through the HTTP API. This two-phase approach separates stream management from real-time data transmission.
The flow is as follows:
- create or update a stream with the
HTTP API, setting
{ "live": true }
- extract
ws_producer_url
from the JSON response - open WebSocket connection to
ws_producer_url
- start sending terminal session events as WebSocket messages, encoded for the selected sub-protocol (see Protocols section below)
Marking a stream "live" is required for the producer endpoint to accept a WebSocket connection.
The creation/update of a live stream ({ "live": true }
) in the API may
fail if given user's live stream limit has already been reached. See
Limits below.
When the WebSocket connection is closed gracefully, the server automatically
marks the stream "dead" ({ "live": false }
).
When the connection is closed abruptly (e.g. a network issue), the server keeps the stream "live" for 60 sec, giving the producer grace time to reconnect before marking the stream "dead".
After stream ends, if stream recording is enabled, a new recording is automatically created.
Example: creating a new stream and sending events
import requests
import websocket
import base64
import json
# Step 1: Create/update stream with HTTP API
token = "your-cli-token"
auth = base64.b64encode(f":{token}".encode()).decode()
response = requests.post(
"https://asciinema.org/api/v1/streams",
headers={"Authorization": f"Basic {auth}"},
json={"live": True, "title": "It's alive!"}
)
# Step 2: Extract ws_producer_url
stream_data = response.json()
ws_url = stream_data["ws_producer_url"]
# Step 3: Open WebSocket connection
ws = websocket.WebSocket()
ws.connect(ws_url, subprotocols=["v3.asciicast"])
# Step 4: Send terminal events (asciicast v3 format)
# Header message
header = {"version": 3, "term": {"cols": 80, "rows": 24}}
ws.send(json.dumps(header))
# Output event (1.0 second interval, output type, data)
output_event = [1.0, "o", "Hello, World!\n"]
ws.send(json.dumps(output_event))
import WebSocket from 'ws';
// Step 1: Create/update stream with HTTP API
const token = 'your-cli-token';
const auth = Buffer.from(`:${token}`).toString('base64');
const response = await fetch('https://asciinema.org/api/v1/streams', {
method: 'POST',
headers: {
'Authorization': `Basic ${auth}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ live: true, title: "It's alive!" })
});
// Step 2: Extract ws_producer_url
const streamData = await response.json();
const wsUrl = streamData.ws_producer_url;
// Step 3: Open WebSocket connection
const ws = new WebSocket(wsUrl, ['v3.asciicast']);
// Step 4: Send terminal events (asciicast v3 format)
ws.on('open', () => {
// Header message
const header = {"version": 3, "term": {"cols": 80, "rows": 24}};
ws.send(JSON.stringify(header));
// Output event (1.0 second interval, output type, data)
const outputEvent = [1.0, "o", "Hello, World!\n"];
ws.send(JSON.stringify(outputEvent));
});
Protocols¶
There are several WebSocket-based protocols for different client capabilities and use cases:
Protocol | Transport | Sec-WebSocket-Protocol name | Summary |
---|---|---|---|
ALiS v1 | binary WebSocket | v1.alis |
Full-featured, lean, precise timing |
asciicast v2 | text WebSocket | v2.asciicast |
Full-featured, heavy, precise timing, easy to implement |
asciicast v3 | text WebSocket | v3.asciicast |
Full-featured, heavy, precise timing, easy to implement |
raw | binary WebSocket | raw |
Output-only, the leanest, no resize and other events, no accurate timing, easy to implement |
Note
We call them "protocols" because they're negotiated using standard
Sec-WebSocket-Protocol
header, but in practice they're just serialization
formats as they're uni-directional.
The producer endpoint supports all of the above. The consumer endpoint supports
ALiS v1
only given its primary client is asciinema player, which has full
support for this protocol.
Upon connection a protocol is negotiated using Sec-WebSocket-Protocol header.
If no Sec-WebSocket-Protocol
header is provided by the client, the producer
endpoint tries to detect the protocol from the first received message, falling
back to raw
if its heuristics fail.
Info
Consumers always receive ALiS v1 encoded stream regardless of the protocol the producer uses. The server handles protocol translation transparently.
ALiS v1¶
ALiS (Asciinema Live Stream) is the primary binary protocol supporting all session event types, while being lightweight on the wire. It's supported by asciinema CLI, server and player.
Conceptually it's based on asciicast v3, but it's leaner and optimized for low latency live streaming.
Data encoding¶
ALiS uses LEB128 (Little Endian Base 128) encoding for all integers to minimize bandwidth. Currently only unsigned integers are in use by the protocol, so in practice it uses Unsigned LEB128 only.
All text data uses UTF-8 encoding with length prefix:
String := [Length: LEB128][Data: UTF-8 bytes]
Stream structure¶
The very first binary message sent right after opening a connection must be the 5 byte magic string:
[0x41, 0x4C, 0x69, 0x53, 0x01]
- "ALiS\x01"
in ASCII.
The messages following it represent an event stream. Each binary WebSocket message encodes a separate event:
Event := [EventType: uint8][EventData]
Where:
- EventType: one of event types listed below
- EventData: event type specific payload
Event types¶
Init (0x01) - Initialize or reset stream state
[0x01][LastId: LEB128][Time: LEB128][Cols: LEB128][Rows: LEB128][Theme][InitData: String]
Where:
- LastId: event sequence number for synchronization
- RelTime: microseconds since the stream start (typically 0, may be positive when joining a stream as a consumer later)
- Cols/Rows: terminal size
- Theme: color theme (see Theme below)
- InitData: optional pre-existing terminal content, to bring the consumer up to speed with terminal state
This event type is conceptually similar to the header in asciicast file format.
Example: Init with 80x24 terminal, no theme, "Hello!" initial data
\x01 \x00 \x00 \x50 \x18 \x00 \x06 Hello!
^init ^id ^time ^80 ^24 ^theme ^len ^data
Output (0x6F) - Terminal output
[0x6F][Id: LEB128][RelTime: LEB128][Data: String]
Where:
- Id: event sequence number
- RelTime: microseconds since last event
- Data: terminal output text
Example: Output "ls -la\n" after 125ms
\x6F \x01 \xE8\x07 \x07 ls -la\n
^out ^id1 ^125000μs ^len ^data
Input (0x69) - User (keyboard) input
[0x69][Id: LEB128][RelTime: LEB128][Data: String]
Where:
- Id: event sequence number
- RelTime: microseconds since last event
- Data: user input text
Example: Input "cd /tmp" after 50ms
\x69 \x02 \x50\xC3\x00 \x07 cd /tmp
^inp ^id2 ^50000μs ^len ^data
Resize (0x72) - Terminal window resize
[0x72][Id: LEB128][RelTime: LEB128][Cols: LEB128][Rows: LEB128]
Where:
- Id: event sequence number
- RelTime: microseconds since last event
- Cols/Rows: new terminal size
Example: Resize to 100x30 after 10ms
\x72 \x03 \x10\x27 \x64 \x1E
^res ^id3 ^10000μs ^100 ^30
Marker (0x6D) - Session annotations / bookmarking
[0x6D][Id: LEB128][RelTime: LEB128][Label: String]
Where:
- Id: event sequence number
- RelTime: microseconds since last event
- Label: human-readable marker description
Example: Marker with no label after 1s
\x6D \x04 \x40\x42\x0F\x00 \x00
^mrk ^id4 ^1000000μs ^len
Exit (0x78) - Process termination
[0x78][Id: LEB128][RelTime: LEB128][Status: LEB128]
Where:
- Id: event sequence number
- RelTime: microseconds since last event
- Status: exit status code (non-negative)
This event is typically sent as the last stream event, once the process that's being streamed (e.g. shell) exits.
Example: Exit with status 0 after 500ms
\x78 \x05 \xA0\x86\x01\x00 \x00
^exit ^id5 ^500000μs ^status
EOT (0x04) - End of Transmission
[0x04][RelTime: LEB128]
Where:
- RelTime: microseconds since last event
This event may be used to signal the stream end without closing the connection. Once EOT is received, the connection state goes back to post-magic-string pre-init state, expecting Init event to restart the stream from scratch.
It's sent from the consumer endpoint whenever a producer ends the stream. This allows asciinema player (consumer) to keep the connection open and be ready for instant stream restart. This approach eliminates the need for an additional channel between the server and the consumer for checking stream liveness (such as another WebSocket or polling).
Example: EOT after 1s
\x04 \x40\x42\x0F\x00
^eot ^1000000μs
Theme¶
Terminal theme is encoded as follows:
Theme := [Format: uint8][ColorData?]
Where Format determines the presence and structure of ColorData:
0x00
(no theme): no ColorData follows, only the format byte0x08
(8-color palette): ColorData with 8-color palette0x10
(16-color palette): ColorData with 16-color palette
When ColorData is present (format 0x08
or 0x10
):
ColorData := [Fg: RGB][Bg: RGB][Palette: N×RGB]
RGB := [R: uint8][G: uint8][B: uint8]
Where N is 8 or 16 depending on format identifier.
Theme examples
No theme (format 0x00):
\x00
^no theme
8-color palette (format 0x08):
\x08 \xD0\xD0\xD0 \x1C\x1C\x1C \x00\x00\x00\xFF\x00\x00 ... (6 more colors)
^8col ^fg(gray) ^bg(dark) ^palette ...
16-color palette (format 0x10):
\x10 \xD0\xD0\xD0 \x1C\x1C\x1C \x00\x00\x00\xFF\x00\x00 ... (14 more colors)
^16col ^fg(gray) ^bg(dark) ^palette ...
asciicast v2¶
This is just asciicast v2 file format sent via WebSocket, where each line is delivered as a separate text message.
It's simple, so creating a client is rather easy. However, it's much more bandwidth heavy than ALiS, and it doesn't provide a mechanism to sync the server with the client's recent state upon re-connections (like ALiS does).
Example: asciicast v2 streaming with asciinema 2.x and websocat
This is an example of streaming with asciinema CLI 2.x, which doesn't natively support streaming. Thankfully, it writes recordings in asciicast v2 format!
The example also uses curl
, jq
and websocat
.
# obtain install ID
INSTALL_ID=$(cat ~/.config/asciinema/install-id)
# create stream and capture JSON response
response=$(curl -s -X POST \
-u ":${INSTALL_ID}" \
-H "Content-Type: application/json" \
-d '{"live": true}' \
https://asciinema.org/api/v1/streams)
# extract WebSocket producer URL
url=$(echo "$response" | jq -r '.ws_producer_url')
# create named pipe, for connecting the CLI with websocat
mkfifo live.fifo
# run websocat to stream from the pipe to the producer URL
websocat --text $url < live.fifo
# in another terminal start asciinema recording session
asciinema rec live.fifo
asciicast v3¶
This is just asciicast v3 file format sent via WebSocket, where each line is delivered as a separate text message.
Same pros/cons as mentioned for v2 apply here.
Example: asciicast v3 streaming with asciinema 3.x and websocat
This is an example of alternative way of streaming with asciinema CLI 3.x.
CLI 3.0+ supports streaming natively using ALiS protocol (asciinema stream
/
asciinema session
), which should be always preferred.
Here is just an example of feeding asciicast v3 stream into the producer
endpoint. The example also uses curl
, jq
and websocat
.
# obtain install ID
INSTALL_ID=$(cat ~/.config/asciinema/install-id)
# create stream and capture JSON response
response=$(curl -s -X POST \
-u ":${INSTALL_ID}" \
-H "Content-Type: application/json" \
-d '{"live": true}' \
https://asciinema.org/api/v1/streams)
# extract WebSocket producer URL
url=$(echo "$response" | jq -r '.ws_producer_url')
# create named pipe, for connecting the CLI with websocat
mkfifo live.fifo
# run websocat to stream from the pipe to the producer URL
websocat --text $url < live.fifo
# in another terminal start asciinema recording session
asciinema rec -f asciicast-v3 live.fifo
raw¶
This protocol is just raw binary messages representing direct terminal output, without any additional encoding/metadata.
The whole binary stream is treated as session output to be fed into a terminal. That includes both printable characters and control sequences.
There's no support for input, resize, marker and other event types. Terminal theme reproduction is also not possible when using this protocol.
Given the messages don't include timing information, server-side time of arrival is used for timing each output event. That means, the original fine-grained timing of the session is lost, and playback smoothness is highly susceptible to network conditions.
The terminal size is assumed to be 80x24. However, the first received message is inspected for size hints.
If the escape sequence \e[8;Y;Xt
is found then Y
is assumed to be terminal
height in rows, and X
is assumed to be terminal width in columns. This
sequence is injected by termrec for example.
Another size hint that the server looks for is
script start
message, which typically includes COLUMNS=".." LINES=".."
.
Example: raw streaming with script and websocat
This is an example of streaming with
script. script
writes the captured terminal output as raw, binary data. The example also uses
curl
, jq
and websocat
.
# obtain install ID
INSTALL_ID=$(cat ~/.config/asciinema/install-id)
# create stream and capture JSON response
response=$(curl -s -X POST \
-u ":${INSTALL_ID}" \
-H "Content-Type: application/json" \
-d '{"live": true}' \
https://asciinema.org/api/v1/streams)
# extract WebSocket producer URL
url=$(echo "$response" | jq -r '.ws_producer_url')
# create named pipe, for connecting the CLI with websocat
mkfifo live.fifo
# run websocat to stream from the pipe to the producer URL
websocat --binary $url < live.fifo
# in another terminal start recording with script
script -f -O live.fifo
Recording¶
asciinema server can automatically record every live stream, converting them into regular recordings that can be replayed later. This allows streams to serve dual purposes: real-time broadcasting and permanent archival.
Recording behavior is controlled by the STREAM_RECORDING
environment
variable.
When recording is enabled, the complete terminal session is captured and stored as a regular recording.
Recent recordings made from a stream are listed on a given stream page. All stream recordings are accessible from user profile page (visibility subject to related settings in user account).
For detailed configuration options, see the Configuration documentation.
Stream recording on asciinema.org
Currently, stream recording is disabled on asciinema.org.
Limits¶
The streaming system enforces two types of limits to manage server resources and prevent abuse:
Per-user live stream count¶
By default, users can create unlimited number of streams. This can be
restricted system-wide through use DEFAULT_STREAM_LIMIT
config option (see
the Configuration documentation),
and individually per-user in the admin panel.
Live streams limit on asciinema.org
Currently, per-user live stream limit on asciinema.org is 1.
Bandwidth limiting¶
Each producer connection is subject to bandwidth rate limiting using token bucket algorithm to prevent individual connections from overwhelming the server while maintaining fair resource allocation across all active streams.
The algorithm is configured such that:
- 1 token == 1 byte of each received WebSocket message
- bucket size (capacity per connection) is 60 000 000 (60 MB) - this is maximum and initial value
- every 100 milliseconds the bucket is refilled with 10 000 tokens
This gives headroom for short bursts, and allows sustained bandwidth of 100 KB/s.
When the token bucket is exhausted, the connection is closed with error code 4004 ("Bandwidth Exceeded").