Skip to main content
This page documents the complete SANA-Streaming wire surface: every command you send with reactor.sendCommand(), the messages the model emits back, and an end-to-end example. For the conceptual model and a quick start, see the overview. SANA-Streaming has no typed model SDK yet, so there are no named command methods or React hooks for it. Drive it from the base Reactor class (JavaScript) or Reactor client (Python) instead: send commands by name with sendCommand(), and read the messages coming back as plain JSON in the shapes this page documents.

Tracks

DirectionNameTypeFormatRate
InboundcameraVideoWebRTC video trackCamera capture rate
Outboundmain_videoVideo(N, H, W, 3) uint8 RGB24-frame chunks, one every ~1-1.5s
Output resolution is 1280 × 704. In live mode you publish your camera to the camera track; in file mode the source arrives by upload (set_video), not as a track. Commands that change generation take effect on chunk boundaries. Chunks hold 24 frames through the middle of a run; the first and last chunks of a run can be shorter.
Browsers shrink and grow the camera’s resolution mid-stream to cope with bandwidth, and a resolution change mid-chunk crashes the live session. Set track.contentHint = "detail" on the camera track before you publish it; the browser then holds resolution steady and adapts the frame rate instead, which the model handles fine.

Session lifecycle

SANA-Streaming session states: IDLE, RUNNING, PAUSED, with set commands staging in IDLE, start moving to RUNNING, set_prompt applying mid-stream, pause and resume between RUNNING and PAUSED, and reset returning to IDLE
Once the connection reaches ready, the session begins in IDLE. Stage a source there (a published camera in live mode, set_video in file mode) and a prompt; start transitions to RUNNING; pause moves to PAUSED; resume returns to RUNNING; reset clears the source, prompt, and progress and returns to IDLE from any state. While RUNNING, a new set_prompt lands at the next chunk boundary without stopping the stream. See Sessions for the separate connection-level lifecycle (disconnected → connecting → waiting → ready) the session passes through before reaching the states above. In file mode a run also ends on its own: every source frame gets transformed, then generation_complete reports the total chunk count and the session returns to IDLE with the clip, prompt, and seed still staged. Send start again to replay the clip from the top; reset is only needed to swap in a different clip or to clear the staging. On completion the main_video track freezes on the last transformed frame rather than going dark, so blank or overlay the stage yourself.

Commands

Send commands to the model using reactor.sendCommand(). Below are all available commands:
CommandDescription
set_modeChoose the input source: live camera or uploaded file
set_videoSet an uploaded clip as the source (file mode only)
set_promptSet or change the edit prompt, before or during generation
set_seedSet the noise seed
startBegin streaming edited video on main_video
pausePause generation after the current chunk
resumeResume generation from a pause
resetClear the source, prompt, and progress; return to idle

set_mode

Select where the source video comes from: "live" (a published camera track) or "file" (a clip set with set_video). Repeating the command is safe, even with the same mode, so always send set_mode then start as a pair; the flow works no matter what was set before.Parameters:
ParameterTypeRequiredDescription
modestringYes"live" or "file"
Example:
await reactor.sendCommand("set_mode", { mode: "live" });

Messages

The model emits the following messages. Subscribe with reactor.on("message", ...) in JavaScript, useReactorMessage in React, or @reactor.on_message in Python. Every message is delivered as JSON { "type": "<name>", "data": { … } }.
MessageWhenPayload
prompt_acceptedA set_prompt was accepted{ prompt: string }
video_acceptedA set_video upload probe succeeded{ width: int, height: int, num_frames: int, num_latent_frames: int }
generation_startedstart succeeded, frames begin{ prompt: string, chunk_num: int, frame_num: int }
chunk_completeOnce per completed chunk of main_video{ chunk_index: int, frames_emitted: int, active_prompt: string }
generation_completeA file-mode source played through to its end{ total_chunks: int }
generation_resetIn response to reset{ reason: string }
command_errorA command was rejected (bad precondition){ command: string, reason: string }
stateOn connect, after every command, after each chunkFull session snapshot (see below)

state payload

state is the single source of truth for the session’s observable state. Subscribe once and gate your UI on it (start buttons, mode toggles, transport controls) rather than tracking individual acknowledgements yourself.
FieldTypeMeaning
runningboolFrames are actively streaming
startedboolTrue once start is accepted; false after reset or when a file-mode run completes
pausedboolTrue while generation is paused
current_chunkintChunks completed since start
current_promptstring | nullThe edit prompt now driving generation, or null if none set
has_promptboolWhether an edit prompt has been set
has_videoboolWhether a file-mode source clip has been accepted
num_source_framesintFrame count of the accepted source clip (file mode)
seedintCurrent seed value
One timing quirk: right after a restart, state still reports the previous run’s final current_chunk until the new run’s first chunk_complete lands (indices restart at 0). Don’t drive progress UI from current_chunk in that window.

Complete example

Live mode in the browser, file mode from Python:
import { Reactor } from "@reactor-team/js-sdk";

const video = document.querySelector("video")!;
const reactor = new Reactor({ modelName: "sana-streaming" });

// Render the edited stream as frames arrive.
reactor.on("trackReceived", (name, track, stream) => {
  if (name !== "main_video") return;
  video.srcObject = stream;
  void video.play();
});

reactor.on("message", (msg) => {
  if (msg.type === "command_error") console.error(msg.data.command, msg.data.reason);
});

reactor.on("statusChanged", async (status) => {
  if (status !== "ready") return;

  // Publish the camera as the live source. Pin the resolution first (see Tracks above).
  const cam = await navigator.mediaDevices.getUserMedia({ video: true });
  const track = cam.getVideoTracks()[0];
  track.contentHint = "detail";
  await reactor.publishTrack("camera", track);

  // Describe the edit, then start.
  await reactor.sendCommand("set_prompt", {
    prompt: "Van Gogh oil painting, swirling brushstrokes, vivid colors",
  });
  await reactor.sendCommand("set_mode", { mode: "live" });
  await reactor.sendCommand("start", {});
});

const jwt = await getToken(); // token minted on your server
await reactor.connect(jwt);