@reactor-models/sana-streaming
v2.0.1
Published
Strongly-typed SDK for the SanaStreaming model on Reactor
Maintainers
Readme
@reactor-models/sana-streaming
Typed JavaScript + React SDK for the SanaStreaming model on Reactor. Version v2.0.1.
Get started
Scaffold a starter app for SanaStreaming with create-reactor-app:
npx create-reactor-app my-app --model=sana-streamingpnpm dlx create-reactor-app my-app --model=sana-streamingInstall
npm install @reactor-models/sana-streamingpnpm add @reactor-models/sana-streamingThe package exports a plain-JavaScript client and a set of React bindings. Import whichever you need from @reactor-models/sana-streaming:
import { SanaStreamingModel } from "@reactor-models/sana-streaming";import { SanaStreamingProvider, useSanaStreaming } from "@reactor-models/sana-streaming";React 18 or later is required when using the provider and hooks. The token-loading examples below use React 19's use(); on React 18, fetch the JWT in a useEffect and pass it to the provider once it resolves.
Authenticate
Reactor uses short-lived JWTs for session auth. You hold your API key on your server, mint a token on demand, and the client never sees the raw key. Tokens are valid for 6 hours — if one leaks, it expires on its own.
Mint a JWT with POST https://api.reactor.inc/tokens and the Reactor-API-Key header; the response JSON is { "jwt": "..." }.
JavaScript (Next.js route handler)
// app/api/reactor/token/route.ts
import { NextResponse } from "next/server";
export async function POST() {
const res = await fetch("https://api.reactor.inc/tokens", {
method: "POST",
headers: { "Reactor-API-Key": process.env.REACTOR_API_KEY! },
});
const { jwt } = await res.json();
return NextResponse.json({ jwt });
}React (provider)
Call the /api/reactor/token route above from a client component and pass the result to the provider:
"use client";
import { use } from "react";
import { SanaStreamingProvider } from "@reactor-models/sana-streaming";
import { ReactorView } from "@reactor-team/js-sdk";
async function getToken() {
const r = await fetch("/api/reactor/token", { method: "POST" });
const { jwt } = await r.json();
return jwt;
}
const tokenPromise = getToken();
export default function App() {
const token = use(tokenPromise);
return (
<SanaStreamingProvider jwtToken={token} connectOptions={{ autoConnect: true }}>
<ReactorView className="w-full aspect-video" />
</SanaStreamingProvider>
);
}Connect
JavaScript
import { SanaStreamingModel } from "@reactor-models/sana-streaming";
const sanaStreaming = new SanaStreamingModel();
await sanaStreaming.connect(jwt);React
The provider takes the JWT as a prop; fetch it from the same /api/reactor/token route the Authenticate example mints:
"use client";
import { use } from "react";
import { SanaStreamingProvider, useSanaStreaming } from "@reactor-models/sana-streaming";
async function getToken() {
const r = await fetch("/api/reactor/token", { method: "POST" });
const { jwt } = await r.json();
return jwt;
}
const tokenPromise = getToken();
function Controller() {
const { status } = useSanaStreaming();
return <span>Status: {status}</span>;
}
export default function App() {
const token = use(tokenPromise);
return (
<SanaStreamingProvider jwtToken={token}>
<Controller />
</SanaStreamingProvider>
);
}Events
Client-to-model commands. The typed surface is SanaStreamingModel (one method per event) in plain JS, and useSanaStreaming() in React — every field name below matches the parameter name the method accepts. Every awaited call resolves once the model's handler has completed; a command whose handler returns a message resolves with that reply (see each event's "Returns" line).
pause
Pause editing after the current chunk; frames stop until resume. Valid only while generating. Emits generation_paused + state, or command_error if not generating or already paused.
Returns: generation_paused — { type: "generation_paused", chunk_index: 0 } (or undefined when the send fails).
No parameters.
JavaScript
import { SanaStreamingModel } from "@reactor-models/sana-streaming";
const sanaStreaming = new SanaStreamingModel();
await sanaStreaming.connect(jwt);
const reply = await sanaStreaming.pause();
if (reply) {
console.log("generation_paused", reply.chunk_index);
}React
"use client";
import { useSanaStreaming } from "@reactor-models/sana-streaming";
function Example() {
const { pause } = useSanaStreaming();
return <button onClick={() => pause()}>pause</button>;
}reset
Abort the current run, clear the prompt, and return to the waiting state. Valid at any time. Emits generation_reset + state; re-arm with set_prompt / start.
Returns: generation_reset — { type: "generation_reset", reason: "" } (or undefined when the send fails).
No parameters.
JavaScript
import { SanaStreamingModel } from "@reactor-models/sana-streaming";
const sanaStreaming = new SanaStreamingModel();
await sanaStreaming.connect(jwt);
const reply = await sanaStreaming.reset();
if (reply) {
console.log("generation_reset", reply.reason);
}React
"use client";
import { useSanaStreaming } from "@reactor-models/sana-streaming";
function Example() {
const { reset } = useSanaStreaming();
return <button onClick={() => reset()}>reset</button>;
}start
Begin streaming on main_video, transforming the live camera track (webcam). A prompt is OPTIONAL — with none set the output stays close to the camera input. Emits generation_started + state, or command_error. No effect while already generating.
Returns: nothing — the awaited call resolves undefined once the model's handler has run.
No parameters.
JavaScript
import { SanaStreamingModel } from "@reactor-models/sana-streaming";
const sanaStreaming = new SanaStreamingModel();
await sanaStreaming.connect(jwt);
await sanaStreaming.start();React
"use client";
import { useSanaStreaming } from "@reactor-models/sana-streaming";
function Example() {
const { start } = useSanaStreaming();
return <button onClick={() => start()}>start</button>;
}resume
Resume editing after a pause, continuing from where it stopped. Valid only while paused. Emits generation_resumed + state, or command_error if not paused.
Returns: generation_resumed — { type: "generation_resumed", chunk_index: 0 } (or undefined when the send fails).
No parameters.
JavaScript
import { SanaStreamingModel } from "@reactor-models/sana-streaming";
const sanaStreaming = new SanaStreamingModel();
await sanaStreaming.connect(jwt);
const reply = await sanaStreaming.resume();
if (reply) {
console.log("generation_resumed", reply.chunk_index);
}React
"use client";
import { useSanaStreaming } from "@reactor-models/sana-streaming";
function Example() {
const { resume } = useSanaStreaming();
return <button onClick={() => resume()}>resume</button>;
}setSeed
Set the random seed that controls the edit's randomness, so the same source, prompt, and seed reproduce the same result. Valid at any time, but read once when generation begins — change it then reset to re-seed an in-progress run. Emits state.
Returns: nothing — the awaited call resolves undefined once the model's handler has run.
| Parameter | Type | Description |
|---|---|---|
| seed | number | Non-negative noise seed. (min 0, default 0) |
JavaScript
import { SanaStreamingModel } from "@reactor-models/sana-streaming";
const sanaStreaming = new SanaStreamingModel();
await sanaStreaming.connect(jwt);
await sanaStreaming.setSeed({ seed: 0 });React
"use client";
import { useSanaStreaming } from "@reactor-models/sana-streaming";
function Example() {
const { setSeed } = useSanaStreaming();
return <button onClick={() => setSeed({ seed: 0 })}>setSeed</button>;
}setPrompt
Set the natural-language instruction that steers the edit. Optional and valid at any time — with no prompt the output stays close to the source video; set or change it (including while streaming) to steer the edit live, visible about one chunk later on a not-yet-shown chunk. Emits prompt_accepted + state, or command_error (the previous prompt is kept) if the prompt cannot be processed.
Returns: prompt_accepted — { type: "prompt_accepted", prompt: "A sunset over the ocean" } (or undefined when the send fails).
| Parameter | Type | Description |
|---|---|---|
| prompt | string | Natural-language edit instruction. (default "") |
JavaScript
import { SanaStreamingModel } from "@reactor-models/sana-streaming";
const sanaStreaming = new SanaStreamingModel();
await sanaStreaming.connect(jwt);
const reply = await sanaStreaming.setPrompt({ prompt: "A sunset over the ocean" });
if (reply) {
console.log("prompt_accepted", reply.prompt);
}React
"use client";
import { useSanaStreaming } from "@reactor-models/sana-streaming";
function Example() {
const { setPrompt } = useSanaStreaming();
return <button onClick={() => setPrompt({ prompt: "A sunset over the ocean" })}>setPrompt</button>;
}setAnchorInterval
Periodically re-ground the edit on the source video every chunks chunks (0 = never). Over a long edit the output can slowly drift away from the source; re-grounding pulls it back, at the cost of a brief visible refresh on the following chunk. Valid at any time, including while streaming (it takes effect at the next chunk boundary). Each re-ground emits anchored.
Returns: nothing — the awaited call resolves undefined once the model's handler has run.
| Parameter | Type | Description |
|---|---|---|
| chunks | number | Chunks between anchors; 0 disables anchoring. (min 0, default 20) |
JavaScript
import { SanaStreamingModel } from "@reactor-models/sana-streaming";
const sanaStreaming = new SanaStreamingModel();
await sanaStreaming.connect(jwt);
await sanaStreaming.setAnchorInterval({ chunks: 20 });React
"use client";
import { useSanaStreaming } from "@reactor-models/sana-streaming";
function Example() {
const { setAnchorInterval } = useSanaStreaming();
return <button onClick={() => setAnchorInterval({ chunks: 20 })}>setAnchorInterval</button>;
}Messages
Model-to-client messages. Register a typed listener with on… on SanaStreamingModel, or a useSanaStreaming… hook in React, to receive only the messages you care about.
state
Emitted as a full snapshot of the session's observable state.
Sent on connect, after every accepted state-changing command, and at each chunk boundary, so a client can render its UI from one message instead of accumulating individual events.
Listener: onState · React hook: useSanaStreamingState
| Field | Type | Description |
|---|---|---|
| seed | number | Current value of the noise seed. |
| paused | boolean | True while generation is paused via pause. |
| running | boolean | True while the chunk loop is actively producing frames. |
| started | boolean | True once start has been accepted; reset to false by reset. |
| has_prompt | boolean | True once an edit prompt has been set. |
| current_chunk | number | Zero-based index of the last completed chunk; 0 before the first. |
| current_prompt | string \| null | The prompt currently steering the edit, or null. |
| anchor_interval | number | Re-ground generation every this many chunks (0 = never). Set via set_anchor_interval. |
JavaScript
import { SanaStreamingModel } from "@reactor-models/sana-streaming";
const sanaStreaming = new SanaStreamingModel();
sanaStreaming.onState((msg) => {
console.log(
"state",
msg.seed,
msg.paused,
msg.running,
msg.started,
msg.has_prompt,
msg.current_chunk,
msg.current_prompt,
msg.anchor_interval,
);
});
await sanaStreaming.connect(jwt);React
import { useSanaStreamingState } from "@reactor-models/sana-streaming";
// Inside a React component wrapped by <SanaStreamingProvider>:
useSanaStreamingState((msg) => {
console.log(
"state",
msg.seed,
msg.paused,
msg.running,
msg.started,
msg.has_prompt,
msg.current_chunk,
msg.current_prompt,
msg.anchor_interval,
);
});anchored
Emitted each time the edit re-grounds on the source video to pull back
drift. Fires every anchor_interval chunks; the following chunk may show a
brief visible refresh.
Listener: onAnchored · React hook: useSanaStreamingAnchored
| Field | Type | Description |
|---|---|---|
| chunk_index | number | Zero-based index of the first chunk generated after this re-ground. |
JavaScript
import { SanaStreamingModel } from "@reactor-models/sana-streaming";
const sanaStreaming = new SanaStreamingModel();
sanaStreaming.onAnchored((msg) => {
console.log("anchored", msg.chunk_index);
});
await sanaStreaming.connect(jwt);React
import { useSanaStreamingAnchored } from "@reactor-models/sana-streaming";
// Inside a React component wrapped by <SanaStreamingProvider>:
useSanaStreamingAnchored((msg) => {
console.log("anchored", msg.chunk_index);
});command_error
Emitted when a command is rejected; the session's prior state is unchanged.
Listener: onCommandError · React hook: useSanaStreamingCommandError
| Field | Type | Description |
|---|---|---|
| reason | string | Why the command was rejected. |
| command | string | Name of the command that was rejected. |
JavaScript
import { SanaStreamingModel } from "@reactor-models/sana-streaming";
const sanaStreaming = new SanaStreamingModel();
sanaStreaming.onCommandError((msg) => {
console.log("command_error", msg.reason, msg.command);
});
await sanaStreaming.connect(jwt);React
import { useSanaStreamingCommandError } from "@reactor-models/sana-streaming";
// Inside a React component wrapped by <SanaStreamingProvider>:
useSanaStreamingCommandError((msg) => {
console.log("command_error", msg.reason, msg.command);
});chunk_complete
Emitted once per completed chunk of main_video.
Listener: onChunkComplete · React hook: useSanaStreamingChunkComplete
| Field | Type | Description |
|---|---|---|
| chunk_index | number | Zero-based index of the chunk that just completed. |
| active_prompt | string | The prompt that drove this chunk; empty means the output stayed close to the source. |
| frames_emitted | number | Edited pixel frames emitted by this chunk. |
JavaScript
import { SanaStreamingModel } from "@reactor-models/sana-streaming";
const sanaStreaming = new SanaStreamingModel();
sanaStreaming.onChunkComplete((msg) => {
console.log(
"chunk_complete",
msg.chunk_index,
msg.active_prompt,
msg.frames_emitted,
);
});
await sanaStreaming.connect(jwt);React
import { useSanaStreamingChunkComplete } from "@reactor-models/sana-streaming";
// Inside a React component wrapped by <SanaStreamingProvider>:
useSanaStreamingChunkComplete((msg) => {
console.log(
"chunk_complete",
msg.chunk_index,
msg.active_prompt,
msg.frames_emitted,
);
});prompt_accepted
Emitted when set_prompt accepts a new edit prompt.
Listener: onPromptAccepted · React hook: useSanaStreamingPromptAccepted
| Field | Type | Description |
|---|---|---|
| prompt | string | The prompt text that was accepted. |
JavaScript
import { SanaStreamingModel } from "@reactor-models/sana-streaming";
const sanaStreaming = new SanaStreamingModel();
sanaStreaming.onPromptAccepted((msg) => {
console.log("prompt_accepted", msg.prompt);
});
await sanaStreaming.connect(jwt);React
import { useSanaStreamingPromptAccepted } from "@reactor-models/sana-streaming";
// Inside a React component wrapped by <SanaStreamingProvider>:
useSanaStreamingPromptAccepted((msg) => {
console.log("prompt_accepted", msg.prompt);
});generation_reset
Emitted when reset aborts the run and clears the prompt and source video.
Listener: onGenerationReset · React hook: useSanaStreamingGenerationReset
| Field | Type | Description |
|---|---|---|
| reason | string | Short human-readable reason the reset was issued. |
JavaScript
import { SanaStreamingModel } from "@reactor-models/sana-streaming";
const sanaStreaming = new SanaStreamingModel();
sanaStreaming.onGenerationReset((msg) => {
console.log("generation_reset", msg.reason);
});
await sanaStreaming.connect(jwt);React
import { useSanaStreamingGenerationReset } from "@reactor-models/sana-streaming";
// Inside a React component wrapped by <SanaStreamingProvider>:
useSanaStreamingGenerationReset((msg) => {
console.log("generation_reset", msg.reason);
});generation_paused
Emitted when pause succeeds and frames stop until resume.
Listener: onGenerationPaused · React hook: useSanaStreamingGenerationPaused
| Field | Type | Description |
|---|---|---|
| chunk_index | number | Index of the last completed chunk before pausing. |
JavaScript
import { SanaStreamingModel } from "@reactor-models/sana-streaming";
const sanaStreaming = new SanaStreamingModel();
sanaStreaming.onGenerationPaused((msg) => {
console.log("generation_paused", msg.chunk_index);
});
await sanaStreaming.connect(jwt);React
import { useSanaStreamingGenerationPaused } from "@reactor-models/sana-streaming";
// Inside a React component wrapped by <SanaStreamingProvider>:
useSanaStreamingGenerationPaused((msg) => {
console.log("generation_paused", msg.chunk_index);
});generation_resumed
Emitted when resume succeeds and frames continue from where they stopped.
Listener: onGenerationResumed · React hook: useSanaStreamingGenerationResumed
| Field | Type | Description |
|---|---|---|
| chunk_index | number | Index of the last completed chunk before resuming. |
JavaScript
import { SanaStreamingModel } from "@reactor-models/sana-streaming";
const sanaStreaming = new SanaStreamingModel();
sanaStreaming.onGenerationResumed((msg) => {
console.log("generation_resumed", msg.chunk_index);
});
await sanaStreaming.connect(jwt);React
import { useSanaStreamingGenerationResumed } from "@reactor-models/sana-streaming";
// Inside a React component wrapped by <SanaStreamingProvider>:
useSanaStreamingGenerationResumed((msg) => {
console.log("generation_resumed", msg.chunk_index);
});generation_started
Emitted once when start succeeds and frames begin streaming.
Listener: onGenerationStarted · React hook: useSanaStreamingGenerationStarted
| Field | Type | Description |
|---|---|---|
| prompt | string | The prompt active at the start of generation (may be empty). |
| chunk_num | number | Total chunks the run will produce (0 — not known up front). |
| frame_num | number | Approximate total edited pixel frames (0 — not known up front). |
JavaScript
import { SanaStreamingModel } from "@reactor-models/sana-streaming";
const sanaStreaming = new SanaStreamingModel();
sanaStreaming.onGenerationStarted((msg) => {
console.log(
"generation_started",
msg.prompt,
msg.chunk_num,
msg.frame_num,
);
});
await sanaStreaming.connect(jwt);React
import { useSanaStreamingGenerationStarted } from "@reactor-models/sana-streaming";
// Inside a React component wrapped by <SanaStreamingProvider>:
useSanaStreamingGenerationStarted((msg) => {
console.log(
"generation_started",
msg.prompt,
msg.chunk_num,
msg.frame_num,
);
});generation_complete
Emitted when the run ends (via reset or disconnect).
Listener: onGenerationComplete · React hook: useSanaStreamingGenerationComplete
| Field | Type | Description |
|---|---|---|
| total_chunks | number | Total chunks produced by the run. |
JavaScript
import { SanaStreamingModel } from "@reactor-models/sana-streaming";
const sanaStreaming = new SanaStreamingModel();
sanaStreaming.onGenerationComplete((msg) => {
console.log("generation_complete", msg.total_chunks);
});
await sanaStreaming.connect(jwt);React
import { useSanaStreamingGenerationComplete } from "@reactor-models/sana-streaming";
// Inside a React component wrapped by <SanaStreamingProvider>:
useSanaStreamingGenerationComplete((msg) => {
console.log("generation_complete", msg.total_chunks);
});Tracks
Named media channels between your app and the SanaStreaming model. Use the typed helpers below — SanaStreamingModel.publish<Track> / on<Track> in plain JS, and useSanaStreamingTrack or the per-track <SanaStreaming<Track>View> components in React — so track names are checked at compile time.
camera
A video channel you push media into (for example, the user's webcam).
JavaScript
import { SanaStreamingModel } from "@reactor-models/sana-streaming";
const sanaStreaming = new SanaStreamingModel();
await sanaStreaming.connect(jwt);
const media = await navigator.mediaDevices.getUserMedia({ video: true });
const videoTrack = media.getVideoTracks()[0];
await sanaStreaming.publishCamera(videoTrack);
// later, to stop sending:
await sanaStreaming.unpublishCamera();React
"use client";
import { SanaStreamingCameraView } from "@reactor-models/sana-streaming";
// Inside a component wrapped by <SanaStreamingProvider>:
export function Example() {
return <SanaStreamingCameraView />;
}main_video
A video channel you subscribe to — the model publishes this for your app to render.
JavaScript
import { SanaStreamingModel } from "@reactor-models/sana-streaming";
const sanaStreaming = new SanaStreamingModel();
sanaStreaming.onMainVideo((track, stream) => {
// attach to a <video> element, pipe to a canvas, etc.
videoEl.srcObject = stream;
});
await sanaStreaming.connect(jwt);React
"use client";
import { SanaStreamingMainVideoView } from "@reactor-models/sana-streaming";
// Inside a component wrapped by <SanaStreamingProvider>:
export function Example() {
return <SanaStreamingMainVideoView className="w-full aspect-video" />;
}