A JavaScript SDK for Decart's models.
npm install @decartai/sdk
# or
pnpm add @decartai/sdk
# or
yarn add @decartai/sdkFor complete documentation, guides, and examples, visit: https://docs.platform.decart.ai/sdks/javascript
Realtime connections are LiveKit-backed in the SDK. Existing client usage stays the same: provide a
camera MediaStream, choose a realtime model, and handle the transformed remote stream.
import { createDecartClient, models } from "@decartai/sdk";
const model = models.realtime("lucy-restyle-2");
// Get user's camera stream
const stream = await navigator.mediaDevices.getUserMedia({
audio: true,
video: {
frameRate: model.fps,
width: model.width,
height: model.height,
}
});
// Create a client
const client = createDecartClient({
apiKey: "your-api-key-here"
});
// Connect and transform the video stream
const realtimeClient = await client.realtime.connect(stream, {
model,
onRemoteStream: (transformedStream) => {
videoElement.srcObject = transformedStream;
},
initialState: {
prompt: {
text: "Anime",
enhance: true
}
}
});
// Change the style on the fly
realtimeClient.setPrompt("Cyberpunk city");
// Disconnect when done
realtimeClient.disconnect();Pre-flip the input stream:
const realtimeClient = await client.realtime.connect(stream, {
model,
mirror: "auto", // or true to always mirror
// ...
});Options:
false(default) — never mirror."auto"— mirror when the input track reportsfacingMode: "user"(mobile front cameras).true— always mirror (e.g. desktop webcams).
A connected realtime session exposes an SDK subscribeToken once it reaches a
connected state. Share that SDK token with viewers — client.realtime.subscribe
uses it to request receive-only LiveKit credentials from Decart, then connects to
the LiveKit room for the styled output stream. No viewer camera is required.
Producer — capture the token from the active session:
const realtimeClient = await client.realtime.connect(stream, {
model,
onRemoteStream: (transformedStream) => {
videoElement.srcObject = transformedStream;
},
});
realtimeClient.on("connectionChange", (state) => {
if ((state === "connected" || state === "generating") && realtimeClient.subscribeToken) {
const subscribeToken = realtimeClient.subscribeToken;
// Pass `subscribeToken` to the viewer snippet below.
}
});Viewer — attach to the producer's stream with the token:
import { createDecartClient, type RealTimeSubscribeClient } from "@decartai/sdk";
const client = createDecartClient({ apiKey: "your-api-key-here" });
const subscriber: RealTimeSubscribeClient = await client.realtime.subscribe({
token: subscribeToken,
onRemoteStream: (stream) => {
videoElement.srcObject = stream;
},
});
subscriber.on("connectionChange", (state) => {
console.log(`Viewer state: ${state}`);
});
// Disconnect when done
subscriber.disconnect();There are two layers: a preflight check before connecting, and an in-session quality
signal while connected. Both report on a shared "good" | "fair" | "poor" | "critical" scale —
the SDK reports, you decide what to do (gate the UI, warn the user, etc.).
Preflight (before connecting). A fast, network-only reachability check — it spins up a throwaway peer connection against public STUN, so there's no session and no cost:
const { quality, metrics, reasons } = await client.realtime.checkConnectivity();
// metrics: { transport: "udp" | "relay" | "failed", rttMs }
if (quality === "critical") showFallbackUI(reasons);In-session quality. While connected, the SDK derives a smoothed verdict from WebRTC stats (latency, packet loss, bandwidth headroom, frame rate) and tells you which dimension is the bottleneck:
const realtimeClient = await client.realtime.connect(stream, {
model,
onRemoteStream: (s) => { videoElement.srcObject = s; },
onConnectionQuality: ({ quality, limitingFactor, metrics }) => {
// limitingFactor: "bandwidth" | "latency" | "loss" | "stall" | "cpu" | "none"
// metrics: { rttMs, fps, packetLoss, upstreamJitterMs, availableUpstreamKbps, ... }
console.log(quality, limitingFactor);
},
});
// also available as an event and a getter:
realtimeClient.on("connectionQuality", (report) => { /* ... */ });
realtimeClient.getConnectionQuality(); // latest report, or null before the first sample
onConnectionQualityand theconnectionQualityevent are debounced — they only re-fire when the verdict (quality/limitingFactor) changes, so they're not a live metrics feed. For a continuous read of the raw numbers, updated every stats tick (~1/s), use thestatsevent or pollgetConnectionQuality()?.metrics:realtimeClient.on("stats", (s) => updateLatencyHud(s.glassToGlass?.medianMs, s.video?.framesPerSecond)); // or, on your own interval: setInterval(() => console.log(realtimeClient.getConnectionQuality()?.metrics.g2gMs), 1000);
Glass-to-glass latency (opt-in, diagnostic). Network RTT hides the dominant cost in
real-time video — model inference — so a session can read "good" while actually feeling laggy.
Set debugQuality: true to measure the real camera→display latency. The SDK attaches a capture
timestamp using LiveKit frame metadata; the server propagates it through inference and the SDK
matches it to output playout. This surfaces startup (ttffMs) and steady-state
(g2gMs) latency. When present, glass-to-glass drives the latency verdict instead of RTT.
Frame metadata is currently experimental in LiveKit and requires encoded-transform support. It does not alter visible pixels.
g2gDropRatioremainsnulluntil frame IDs are propagated through the server pipeline as well as timestamps.
const realtimeClient = await client.realtime.connect(stream, {
model,
debugQuality: true,
});
// g2g updates every stats tick — read it from the `stats` event (the
// `connectionQuality` verdict is debounced and would go stale between changes):
realtimeClient.on("stats", ({ glassToGlass }) => {
console.log(glassToGlass?.ttffMs, glassToGlass?.medianMs);
});For a measured verdict before connecting (instead of the network-only check), use the deep
probe: it briefly opens a real session with a synthetic source, measures glass-to-glass, then
tears it down. It requires a model and costs a short GPU session:
const probe = await client.realtime.checkConnectivity({ deep: true, model });
console.log(probe.quality, probe.metrics.g2gMs, probe.metrics.ttffMs);For video generation jobs, use the queue API to submit jobs and poll for results:
import { createDecartClient, models } from "@decartai/sdk";
const client = createDecartClient({
apiKey: "your-api-key-here"
});
// Submit and poll automatically
const result = await client.queue.submitAndPoll({
model: models.video("lucy-pro-v2v"),
prompt: "Make it look like a watercolor painting",
data: videoFile,
onStatusChange: (job) => {
console.log(`Status: ${job.status}`);
}
});
if (result.status === "completed") {
videoElement.src = URL.createObjectURL(result.data);
} else {
console.error("Job failed:", result.error);
}Or manage the polling manually:
// Submit the job
const job = await client.queue.submit({
model: models.video("lucy-pro-v2v"),
prompt: "Make it look like a watercolor painting",
data: videoFile
});
console.log(`Job ID: ${job.job_id}`);
// Poll for status
const status = await client.queue.status(job.job_id);
console.log(`Status: ${status.status}`);
// Get result when completed
if (status.status === "completed") {
const blob = await client.queue.result(job.job_id);
videoElement.src = URL.createObjectURL(blob);
}React Native realtime requires LiveKit's React Native packages
and an early registerGlobals() call. Plain react-native-webrtc is not
supported. SDK imports and createDecartClient(...) usage stay the same.
npm install @decartai/sdk livekit-client@2.20.1 \
@livekit/react-native@2.11.1 \
@livekit/react-native-webrtc@144.1.1Call LiveKit setup from a side-effect module that runs before your router or app entrypoint:
// livekit-bootstrap.ts
import { registerGlobals } from "@livekit/react-native";
registerGlobals();// index.ts
import "./livekit-bootstrap";
import "expo-router/entry"; // or import your root App componentWith the globals registered, realtime usage is the same as on the web. Use the
model's numeric FPS when configuring native camera capture, then pass that
MediaStream to the SDK:
import { createDecartClient, models, resolveFpsNumber } from "@decartai/sdk";
import { mediaDevices } from "@livekit/react-native-webrtc";
const client = createDecartClient({ apiKey: "your-api-key-here" });
const model = models.realtime("lucy-2.5");
const captureFps = resolveFpsNumber(model.fps);
const cameraStream = await mediaDevices.getUserMedia({
audio: false,
video: {
facingMode: "user",
frameRate: captureFps,
width: model.width,
height: model.height,
},
});
const realtimeClient = await client.realtime.connect(cameraStream, {
model,
preferredVideoCodec: "vp8",
onRemoteStream,
});For Expo, install the config plugins:
npm install @livekit/react-native-expo-plugin@1.0.2 \
@config-plugins/react-native-webrtcAdd them to app.json:
{
"expo": {
"plugins": [
"@livekit/react-native-expo-plugin",
"@config-plugins/react-native-webrtc"
]
}
}Run npx expo prebuild and rebuild the native app. LiveKit does not run in Expo
Go. Bare React Native apps must follow LiveKit's native setup instructions.
Outgoing mirror, debugQuality, and deep connectivity preflight are browser-only
and fail with UNSUPPORTED_PLATFORM_FEATURE on React Native. Mirror the local
preview with your native video view instead.
pnpm installpnpm build- Build the projectpnpm dev:example- Run Vite dev server for examplespnpm test- Run unit testspnpm test:e2e- Run end-to-end testspnpm typecheck- Type check with TypeScriptpnpm format- Format code with Biomepnpm lint- Lint code with Biome
- Version bump: Run
pnpm releaseto bump the version (this usesbumppto create a new version tag) and push it to GitHub - Automated publish: The GitHub Actions workflow will:
- Build the project
- Publish to npm
- Create a GitHub release with changelog
The package is published to npm as @decartai/sdk.
MIT