Output class and
the tracks it receives on a MediaInput class. The runtime handles
the encoding and the transport at both ends; you deal in NumPy arrays.
The examples on this page are a video-to-video model in the shape of SANA-Streaming: the client
publishes its camera, the model restyles it according to a prompt, and the restyled video comes
back.
A track is a name shared by both sides. The client publishes or subscribes by the name you gave the field.
Output tracks
SubclassOutput with one field per track. The field name is the track name a client subscribes
to, and the annotation says what it carries:
Output. If a step has no audio for an audio track,
send an empty array rather than leaving the field out.
A video frame is an RGB array of shape (height, width, 3) with dtype uint8. A step that
produces several frames returns them as one (frames, height, width, 3) array; the runtime
splits the batch and paces it. Audio is int16 of shape (1, samples) at 48 kHz, mono; the
Audio section below covers rates and sync.
Several tracks
A model can send more than one stream, and each client picks the ones it wants. Take a two-player world model where each player gets their own point of view. TheOutput declares one track per
player, and every step fills both:
player_one, player two’s to player_two, and both take ambience.
Tracks returned in one Output are kept in step with each other, so the two views and the sound
line up.
Frame rate
generate() hands frames over one chunk at a time, and a chunk starts playing the moment it
lands. While it plays, the runtime is already asking for the next one. Generation runs one chunk
ahead of what the client is playing, and the chunks play in the order they were made:
Each chunk starts playing when it lands and the next one is generated meanwhile. By default a chunk plays for as long as it took to make.
fps on the class:
Output waits while frames already handed over
are still playing. That is what stops a fast model from running ahead of the client, and it is why
you never need to sleep in generate().
Two controls sit on self.output, and both reach every connected client:
flush() when the scene changes, so nothing from the old scene plays after the cut. A
recording keeps running across the cut.
Frame metadata
A frame can carry a small tag. Wrap the array inTrackPayload and pass metadata: a mapping
travels as JSON, bytes travel as they are. On a batch, pass one value for every frame or a list
with one value per frame.
Input tracks
SubclassMediaInput with one field per track you expect the client to publish, and annotate it
on your model. The runtime finds it by its type, so the attribute name is up to you; media is
the convention:
self.media.webcam. Each track holds the last 128 frames and drops the oldest when
full, so a model that falls behind does not grow memory without bound.
Input is the old name for MediaInput. It still imports, with a deprecation warning.
Several tracks
A client can publish more than one stream too. A robot policy driven from three cameras declares one track per camera and reads them all inprocess_input():
Reading frames
Read tracks inprocess_input() with try_read(n). It returns the n frames you asked for, or
None if fewer than that have arrived, and it never waits. Too few frames is a reason to skip the
step, so the model runs only with a full input:
InputFrame, whose data holds the array: (height, width, 3) uint8
for video, (1, samples) int16 for audio. A frame also carries the bytes the client tagged it
with and, when the sender supplied one, a presentation timestamp.
Reads take a mode. ReadMode.LATEST, the default, returns the newest frames and drops the
backlog, which is what video wants: if the model fell behind, it should catch up to now rather
than restyle stale frames. ReadMode.FIFO returns the oldest in order and leaves the rest queued,
which is what audio wants, since dropped samples are audible gaps.
await read(n) waits until n frames are available instead of returning None. Inside
process_input() a wait holds the step and no command runs until a frame arrives, so prefer
try_read() there and keep read() for a hand-written run().
When the last client leaves or the session ends, every input buffer is reset, so the next client
starts from empty tracks.
Inbound media reference
Every field on a frame, both read calls with their arguments, and the rest of the buffer surface.
Audio
Audio tracks work like video tracks. Declare them on theOutput, return samples alongside the
frames, and the runtime encodes and transports them:
int16, shaped (1, samples) for mono, at 48 kHz. The runtime encodes them as Opus,
whose native rate is 48 kHz, so audio already at that rate needs no resampling. When a step has
no audio, send an empty (1, 0) array rather than leaving the field out.
Keeping audio and video together
When oneOutput carries both tracks, the runtime keeps them aligned: if the video is a batch,
the audio is spread across those frames. So each Output should carry the audio that belongs to
its video. At 30 fps one frame is 48000 / 30 = 1600 samples, so a batch of 3 frames carries
about 4,800 samples. A frame’s worth of video with a second’s worth of audio drifts, however good
the transport.
Other sample rates
Live audio always goes out at 48 kHz, and the runtime never resamples for you. If your model produces audio at another rate, resample to 48 kHz before it goes on theOutput, or playback
runs at the wrong pitch.
If you would rather emit at the native rate, subclass Audio and declare it:
Two producers
A model whose audio and video come from separate producers has to pair them before emitting. That is a loop which blocks on input, so it keeps a hand-writtenrun() (see
Controlling the loop). Buffer
whichever stream runs ahead, and emit only when both have material for the same span:
Reading audio from the client
Inbound audio arrives as(1, samples) int16 at 48 kHz and is read like video, but in FIFO
order so no samples are dropped:
A video-to-video model
Everything above, in one class. The client publisheswebcam and subscribes to main_video.
Each step takes the four newest camera frames and the current prompt, restyles them, and sends
them back tagged with the step they came from.
sana_streaming.py
generate() never runs. Once
frames flow, the model keeps up with the camera by design: LATEST reads always start from the
newest frames, so a slow step costs a few dropped camera frames rather than growing lag.
Next
Session Recording
Record sessions and let clients capture clips.
The Step Loop
How process_input() and process_output() fit around generate().