- npm
- pnpm
skill/SKILL.md documents the same patterns for coding agents building on top.
Installation and setup
You will need:- Node.js 18+ (and React 18+ if you use the provider and hooks)
- a Reactor API key (starts with
rk_)
@reactor-team/js-sdk as a dependency:
- npm
- pnpm
How X2 works
The mental model that the rest of this page builds on:- You stream video in; the model streams edited video back. The client publishes a
sourcevideo track, and the model publishesmain_video. Everything else is control surface. - There is no start command. Generation begins on its own once a non-empty prompt is set and source
frames are arriving. To stop and clear, call
reset. - The model is the source of truth. It broadcasts a full
state_updatesnapshot on connect and after every observable change. Reduce that snapshot into your UI state instead of tracking your own copy of what you sent. - Everything lands on block boundaries. X2 generates one block at a time; prompt swaps apply from the next block, and the model samples the pointer once per block.
Authentication
Yourrk_… API key must never reach the browser. Keep it server-side and mint a short-lived JWT the
client connects with; see Authentication for the token route pattern. Hand the
provider a getJwt resolver rather than a static token. The SDK calls it before every Reactor API
request (connect, ICE refresh, uploads, clip manifests), so a fresh token is always in reach.
- Expose it as GET, not POST. Browsers do not cache POST responses; a GET route whose handler still
POSTs to the Reactor
/tokensendpoint lets the browser’s HTTP cache serve repeat calls without a network round trip. - Send
Cache-Control: private. JWTs are per-user;privatekeeps CDNs and shared proxies from storing them. - Derive
max-agefrom the response’sexpires_at, minus a small skew, instead of hardcoding a number. The cache window then always tracks the lifetime the server granted.
getJwt function is fine: the provider stabilizes it through a ref, so a parent re-render
does not tear the session down. Do not wrap it in useCallback.
Streaming a source
The model edits whatever arrives on thesource track; the app never uploads source media, it
streams it. Three source shapes cover most apps, and all three end in the same call, publishing a
MediaStreamTrack:
- For a webcam, call
getUserMedia({ video: true })and publish the video track. - For a clip, play a video element (muted, looped) and publish the video track from
captureStream(). - For a still image, draw it to a canvas and publish
canvas.captureStream(24). The capturer only emits when the canvas repaints, so keep repainting the same frame on an interval at the stream rate. From the model’s side this is a video of a motionless scene, which is the drag-to-animate setup.
keep_backlog: false (the default) reads the newest frames and keeps latency bounded, right for a
live webcam. keep_backlog: true consumes every frame in order for smoother motion at the cost of
growing delay, right for clips and drag-to-animate.
Starting a run
Generation starts at the first moment a non-empty prompt exists while frames arrive. Set the reference image before the prompt if the first block should already carry it:main_video track to a video element. Use <X2MainVideoView />
in React, or onMainVideo on the typed client.
Reading the event stream
Reducestate_update into your UI state and treat it as authoritative:
reference_image_acceptedis the one place to read the decoded image’s width and height; the snapshot only says whether a reference is set.generation_stoppedfires on any stop. Check thereasonfield:reference_image_changedis the automatic restart after a reference swap, andresetmeans the run is over.command_errorreports a rejected command, covered below.
generation_stopped and lift the blank when the next state_update reports
generating: true.
Steering with the drag pointer
The pointer steers the edited subject while a drag is held. Wire it to pointer events on the output pane:- On pointer-down, send
{ x, y, active: true }and capture the pointer. - On pointer-move, keep sending positions, throttled to about 30 Hz with a trailing send so the last position of a fast gesture lands. The model samples the pointer once per block, so faster sends buy nothing.
- On pointer-up or cancel, send
{ active: false }. Also send it if the overlay unmounts or the session leaves ready mid-drag, so the model does not keep steering toward a stale point.
object-fit: contain), map from the pane’s box to the visible content using the aspect ratio
from generation_started, and clamp to 0..1.
The model echoes the pointer back in every state_update as pointer_x, pointer_y, and
pointer_active. These reflect the model’s view of the pointer rather than your local gesture, so
surfacing them in a debug readout shows you the payload each set_pointer call delivered.
For drag-to-animate, combine the pieces. Stream a still image as the source, write a prompt that
binds motion to the drag, such as the character in the video follows the drag trajectory, and set
keep_backlog: true for smooth motion.
Surfacing command_error
A rejected command has no effect, and the rejection arrives as acommand_error message rather than
a thrown error. Surface it as a transient banner and move on; the session stays healthy:
set_reference_image with a file the model cannot decode as an
image.
Resetting and disconnecting
reset stops generation and clears the prompt, reference image, and pointer server-side. Mirror it
client-side by clearing prompt drafts and reference previews in the same motion (keying those
components on a reset counter is a clean way to do it). On a full disconnect, drop all local session
state so a reconnect starts clean; the model’s first state_update re-seeds the UI.