@reactor-models/vidu-s2-editing
v0.3.0
Published
Strongly-typed SDK for the ViduS2Editing model on Reactor
Maintainers
Readme
@reactor-models/vidu-s2-editing
Typed JavaScript + React SDK for the ViduS2Editing model on Reactor. Version v0.3.0.
Get started
Scaffold a starter app for ViduS2Editing with create-reactor-app:
npx create-reactor-app my-app --model=vidu-s2-editingpnpm dlx create-reactor-app my-app --model=vidu-s2-editingInstall
npm install @reactor-models/vidu-s2-editingpnpm add @reactor-models/vidu-s2-editingThe package exports a plain-JavaScript client and a set of React bindings. Import whichever you need from @reactor-models/vidu-s2-editing:
import { ViduS2EditingModel } from "@reactor-models/vidu-s2-editing";import { ViduS2EditingProvider, useViduS2Editing } from "@reactor-models/vidu-s2-editing";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 { ViduS2EditingProvider } from "@reactor-models/vidu-s2-editing";
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 (
<ViduS2EditingProvider jwtToken={token} connectOptions={{ autoConnect: true }}>
<ReactorView className="w-full aspect-video" />
</ViduS2EditingProvider>
);
}Connect
JavaScript
import { ViduS2EditingModel } from "@reactor-models/vidu-s2-editing";
const viduS2Editing = new ViduS2EditingModel();
await viduS2Editing.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 { ViduS2EditingProvider, useViduS2Editing } from "@reactor-models/vidu-s2-editing";
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 } = useViduS2Editing();
return <span>Status: {status}</span>;
}
export default function App() {
const token = use(tokenPromise);
return (
<ViduS2EditingProvider jwtToken={token}>
<Controller />
</ViduS2EditingProvider>
);
}Events
Client-to-model commands. The typed surface is ViduS2EditingModel (one method per event) in plain JS, and useViduS2Editing() 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).
endEdit
End the edit. Valid from starting through live. Replies edit_ended once everything is released, about 5 s later, after session_state broadcasts ending then ended; start_edit works again. Emits command_error with NOT_LIVE when no edit is active.
Returns: edit_ended — { type: "edit_ended", end_reason: "", duration_seconds: 0 } (or undefined when the send fails).
No parameters.
JavaScript
import { ViduS2EditingModel } from "@reactor-models/vidu-s2-editing";
const viduS2Editing = new ViduS2EditingModel();
await viduS2Editing.connect(jwt);
const reply = await viduS2Editing.endEdit();
if (reply) {
console.log("edit_ended", reply.end_reason, reply.duration_seconds);
}React
"use client";
import { useViduS2Editing } from "@reactor-models/vidu-s2-editing";
function Example() {
const { endEdit } = useViduS2Editing();
return <button onClick={() => endEdit()}>endEdit</button>;
}getState
Return the authoritative snapshot. Valid any time. Replies session_state; never fails.
Returns: session_state — { type: "session_state", phase: "", end_reason: null, last_error: null, editing_type: null, control_ready: false, reference_set: false, edit_started_at: null, video_receiving: false, edit_max_seconds: null, camera_forwarding: false, last_frame_age_ms: null, edit_elapsed_seconds: null } (or undefined when the send fails).
No parameters.
JavaScript
import { ViduS2EditingModel } from "@reactor-models/vidu-s2-editing";
const viduS2Editing = new ViduS2EditingModel();
await viduS2Editing.connect(jwt);
const reply = await viduS2Editing.getState();
if (reply) {
console.log(
"session_state",
reply.phase,
reply.end_reason,
reply.last_error,
reply.editing_type,
reply.control_ready,
reply.reference_set,
reply.edit_started_at,
reply.video_receiving,
reply.edit_max_seconds,
reply.camera_forwarding,
reply.last_frame_age_ms,
reply.edit_elapsed_seconds,
);
}React
"use client";
import { useViduS2Editing } from "@reactor-models/vidu-s2-editing";
function Example() {
const { getState } = useViduS2Editing();
return <button onClick={() => getState()}>getState</button>;
}startEdit
Start editing your camera track with one reference image and a scenario. Valid in idle, ended, and failed. Returns nothing; session_state broadcasts walk starting, warming_up, then live, about 4 s in all, and edited video appears on main_video a few seconds after camera_forwarding turns true. Emits command_error with BUSY while an edit is active, and INVALID_INPUT when the reference is missing, given twice, not an image, over 10 MB, or not an http(s) URL. A failure while starting moves the phase to failed: UPSTREAM_REJECTED when the service cannot use the image, another UPSTREAM_* code, WARMUP_TIMEOUT, MEDIA_JOIN_FAILED, or a passed-through service code.
Returns: nothing — the awaited call resolves undefined once the model's handler has run.
| Parameter | Type | Description |
|---|---|---|
| editing_type | "style_transfer" \| "virtual_tryon" \| "subject_replacement" \| "background_replacement" | What the image supplies: style_transfer (the look of the whole scene, the default), virtual_tryon (a garment you wear), subject_replacement (a character that replaces you), or background_replacement (the scene behind you). Change it later with switch_reference. (default null) |
| reference_image | FileRef \| null | The reference image, as an upload: PNG, JPG, or WEBP, under 10 MB. Give this or reference_image_url, not both. (default null) |
| reference_image_url | string \| null | A public http(s) URL of the reference image, instead of reference_image. The editor fetches it when the edit starts. (maxLength 2048, default null) |
JavaScript
import { ViduS2EditingModel } from "@reactor-models/vidu-s2-editing";
const viduS2Editing = new ViduS2EditingModel();
await viduS2Editing.connect(jwt);
const fileRef = await viduS2Editing.uploadFile(blob);
await viduS2Editing.startEdit({ reference_image: fileRef, editing_type: "style_transfer", reference_image_url: null });React
"use client";
import { useViduS2Editing } from "@reactor-models/vidu-s2-editing";
function Example() {
const { startEdit, uploadFile } = useViduS2Editing();
async function handlePick(file: File) {
const ref = await uploadFile(file);
await startEdit({ reference_image: ref, editing_type: "style_transfer", reference_image_url: null });
}
return <input type="file" onChange={(e) => handlePick(e.target.files![0])} />;
}switchReference
Change the reference image, the scenario, or both, without restarting the edit; what you omit stays as it is. Valid while live. Replies reference_switched once the switch is sent, and the picture changes within a few seconds. There is no later confirmation: if the editor cannot apply the switch, a command_error for switch_reference follows about 4 s later with a passed-through SWITCH_PROMPT_*, RENDER_*, or LIVE_NOT_ACTIVE code, the previous reference stays in effect, and the edit keeps running. Each call switches exactly once; do not resend on silence. Emits command_error with NOT_LIVE outside live, and INVALID_INPUT when nothing to change is given or the new reference fails the same checks as in start_edit.
Returns: reference_switched — { type: "reference_switched", editing_type: "", reference_changed: true } (or undefined when the send fails).
| Parameter | Type | Description |
|---|---|---|
| editing_type | "style_transfer" \| "virtual_tryon" \| "subject_replacement" \| "background_replacement" | A new scenario, with the same values as in start_edit. Omit to keep the current one; session_state.editing_type shows the one in effect. (default null) |
| reference_image | FileRef \| null | A new reference image, as an upload: PNG, JPG, or WEBP, under 10 MB. Give this or reference_image_url, not both; omit both to keep the current image. (default null) |
| reference_image_url | string \| null | A public http(s) URL of a new reference image, instead of reference_image. (maxLength 2048, default null) |
JavaScript
import { ViduS2EditingModel } from "@reactor-models/vidu-s2-editing";
const viduS2Editing = new ViduS2EditingModel();
await viduS2Editing.connect(jwt);
const fileRef = await viduS2Editing.uploadFile(blob);
const reply = await viduS2Editing.switchReference({ reference_image: fileRef, editing_type: "style_transfer", reference_image_url: null });
if (reply) {
console.log("reference_switched", reply.editing_type, reply.reference_changed);
}React
"use client";
import { useViduS2Editing } from "@reactor-models/vidu-s2-editing";
function Example() {
const { switchReference, uploadFile } = useViduS2Editing();
async function handlePick(file: File) {
const ref = await uploadFile(file);
await switchReference({ reference_image: ref, editing_type: "style_transfer", reference_image_url: null });
}
return <input type="file" onChange={(e) => handlePick(e.target.files![0])} />;
}Messages
Model-to-client messages. Register a typed listener with on… on ViduS2EditingModel, or a useViduS2Editing… hook in React, to receive only the messages you care about.
edit_ended
Emitted as the reply to end_edit once the edit is released.
Listener: onEditEnded · React hook: useViduS2EditingEditEnded
| Field | Type | Description |
|---|---|---|
| end_reason | string | Same values as session_state.end_reason. |
| duration_seconds | number | Seconds the edit was live, 0 if it never was. |
JavaScript
import { ViduS2EditingModel } from "@reactor-models/vidu-s2-editing";
const viduS2Editing = new ViduS2EditingModel();
viduS2Editing.onEditEnded((msg) => {
console.log("edit_ended", msg.end_reason, msg.duration_seconds);
});
await viduS2Editing.connect(jwt);React
import { useViduS2EditingEditEnded } from "@reactor-models/vidu-s2-editing";
// Inside a React component wrapped by <ViduS2EditingProvider>:
useViduS2EditingEditEnded((msg) => {
console.log("edit_ended", msg.end_reason, msg.duration_seconds);
});command_error
Emitted when a command is rejected or an edit fails; the command had no effect.
Broadcast, and stored as session_state.last_error. origin says whose
fault it is; code is stable and safe to branch on.
Listener: onCommandError · React hook: useViduS2EditingCommandError
| Field | Type | Description |
|---|---|---|
| code | string | Stable code, e.g. NOT_LIVE, UPSTREAM_CAPACITY. |
| origin | string | request (the input was invalid), state (not allowed in this phase), upstream (the generation service refused or failed), or platform (a failure on our side). |
| reason | string | A readable sentence. |
| command | string | The command that failed, or session when nothing the client sent caused it. |
| trace_id | string \| null | Correlation id to quote when reporting the failure. (default null) |
| retryable | boolean | Whether the same command may succeed if sent again later. (default false) |
| upstream_code | string \| null | Error code from the generation service, when it gave one. (default null) |
| upstream_status | number \| null | HTTP status from the generation service, when origin is upstream. (default null) |
JavaScript
import { ViduS2EditingModel } from "@reactor-models/vidu-s2-editing";
const viduS2Editing = new ViduS2EditingModel();
viduS2Editing.onCommandError((msg) => {
console.log(
"command_error",
msg.code,
msg.origin,
msg.reason,
msg.command,
msg.trace_id,
msg.retryable,
msg.upstream_code,
msg.upstream_status,
);
});
await viduS2Editing.connect(jwt);React
import { useViduS2EditingCommandError } from "@reactor-models/vidu-s2-editing";
// Inside a React component wrapped by <ViduS2EditingProvider>:
useViduS2EditingCommandError((msg) => {
console.log(
"command_error",
msg.code,
msg.origin,
msg.reason,
msg.command,
msg.trace_id,
msg.retryable,
msg.upstream_code,
msg.upstream_status,
);
});session_state
Emitted on connect, whenever anything in it changes, and as the reply to get_state: the authoritative snapshot.
Broadcast to every client. Mirror it; derive nothing from other messages.
Listener: onSessionState · React hook: useViduS2EditingSessionState
| Field | Type | Description |
|---|---|---|
| phase | string | Where the session is: idle, starting, warming_up, live, ending, ended, or failed. start_edit works from idle, ended, and failed. |
| end_reason | string \| null | Why the last edit ended: ended_by_client, max_duration, content_policy, upstream_quota, upstream_interrupted, media_lost, or bridge_failed. Null while an edit runs. (default null) |
| last_error | Record<string, unknown> \| null | The most recent command_error payload, or null. Cleared when an edit starts. (default null) |
| editing_type | string \| null | The scenario in effect: style_transfer, virtual_tryon, subject_replacement, or background_replacement. Null before the first start_edit. (default null) |
| control_ready | boolean | switch_reference is accepted. (default false) |
| reference_set | boolean | Whether a reference image is in effect for the current edit. (default false) |
| edit_started_at | number \| null | Unix time, in seconds, at which the edit became live. (default null) |
| video_receiving | boolean | Edited video is arriving on main_video. (default false) |
| edit_max_seconds | number \| null | Longest the edit may run before it ends on its own. (default null) |
| camera_forwarding | boolean | Your camera track is reaching the editor. Nothing is edited until it is. (default false) |
| last_frame_age_ms | number \| null | Milliseconds since the last edited frame. Grows when video stalls. (default null) |
| edit_elapsed_seconds | number \| null | Seconds since the edit became live. (default null) |
JavaScript
import { ViduS2EditingModel } from "@reactor-models/vidu-s2-editing";
const viduS2Editing = new ViduS2EditingModel();
viduS2Editing.onSessionState((msg) => {
console.log(
"session_state",
msg.phase,
msg.end_reason,
msg.last_error,
msg.editing_type,
msg.control_ready,
msg.reference_set,
msg.edit_started_at,
msg.video_receiving,
msg.edit_max_seconds,
msg.camera_forwarding,
msg.last_frame_age_ms,
msg.edit_elapsed_seconds,
);
});
await viduS2Editing.connect(jwt);React
import { useViduS2EditingSessionState } from "@reactor-models/vidu-s2-editing";
// Inside a React component wrapped by <ViduS2EditingProvider>:
useViduS2EditingSessionState((msg) => {
console.log(
"session_state",
msg.phase,
msg.end_reason,
msg.last_error,
msg.editing_type,
msg.control_ready,
msg.reference_set,
msg.edit_started_at,
msg.video_receiving,
msg.edit_max_seconds,
msg.camera_forwarding,
msg.last_frame_age_ms,
msg.edit_elapsed_seconds,
);
});reference_switched
Emitted as the reply to switch_reference once the switch is sent to the editor.
The picture changes within a few seconds. A switch the editor cannot apply
arrives afterwards, about 4 s later, as a command_error for
switch_reference, and the previous reference and scenario stay in effect.
Listener: onReferenceSwitched · React hook: useViduS2EditingReferenceSwitched
| Field | Type | Description |
|---|---|---|
| editing_type | string | The scenario now requested. |
| reference_changed | boolean | Whether a new reference image was sent. |
JavaScript
import { ViduS2EditingModel } from "@reactor-models/vidu-s2-editing";
const viduS2Editing = new ViduS2EditingModel();
viduS2Editing.onReferenceSwitched((msg) => {
console.log("reference_switched", msg.editing_type, msg.reference_changed);
});
await viduS2Editing.connect(jwt);React
import { useViduS2EditingReferenceSwitched } from "@reactor-models/vidu-s2-editing";
// Inside a React component wrapped by <ViduS2EditingProvider>:
useViduS2EditingReferenceSwitched((msg) => {
console.log("reference_switched", msg.editing_type, msg.reference_changed);
});Tracks
Named media channels between your app and the ViduS2Editing model. Use the typed helpers below — ViduS2EditingModel.publish<Track> / on<Track> in plain JS, and useViduS2EditingTrack or the per-track <ViduS2Editing<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 { ViduS2EditingModel } from "@reactor-models/vidu-s2-editing";
const viduS2Editing = new ViduS2EditingModel();
await viduS2Editing.connect(jwt);
const media = await navigator.mediaDevices.getUserMedia({ video: true });
const videoTrack = media.getVideoTracks()[0];
await viduS2Editing.publishCamera(videoTrack);
// later, to stop sending:
await viduS2Editing.unpublishCamera();React
"use client";
import { ViduS2EditingCameraView } from "@reactor-models/vidu-s2-editing";
// Inside a component wrapped by <ViduS2EditingProvider>:
export function Example() {
return <ViduS2EditingCameraView />;
}main_video
A video channel you subscribe to — the model publishes this for your app to render.
JavaScript
import { ViduS2EditingModel } from "@reactor-models/vidu-s2-editing";
const viduS2Editing = new ViduS2EditingModel();
viduS2Editing.onMainVideo((track, stream) => {
// attach to a <video> element, pipe to a canvas, etc.
videoEl.srcObject = stream;
});
await viduS2Editing.connect(jwt);React
"use client";
import { ViduS2EditingMainVideoView } from "@reactor-models/vidu-s2-editing";
// Inside a component wrapped by <ViduS2EditingProvider>:
export function Example() {
return <ViduS2EditingMainVideoView className="w-full aspect-video" />;
}