> ## Documentation Index
> Fetch the complete documentation index at: https://docs.platform.decart.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Streaming Best Practices

> Optimize your realtime video integration for quality, performance, and reliability

Build production-ready realtime video integrations. This guide covers camera setup, connection management, prompt strategy, error handling, and mobile-specific considerations.

## Camera setup

Always use the model's built-in constraints when requesting camera access. Each model defines the `fps`, `width`, and `height` it expects — passing these directly avoids scaling artifacts and wasted bandwidth.

```typescript theme={null}
import { models } from "@decartai/sdk";

const model = models.realtime("lucy-latest");

const stream = await navigator.mediaDevices.getUserMedia({
  video: {
    frameRate: model.fps,
    width: model.width,
    height: model.height,
  },
});
```

<Warning>
  Requesting a resolution that doesn't match the model's expectations forces the browser to scale the video. This adds latency and can reduce output quality.
</Warning>

### Handling constraint failures

Not every device supports every resolution. Use `ideal` constraints as a fallback when targeting a wide range of hardware:

```typescript theme={null}
const stream = await navigator.mediaDevices.getUserMedia({
  video: {
    frameRate: { ideal: model.fps },
    width: { ideal: model.width },
    height: { ideal: model.height },
  },
});
```

### Front vs back camera

On mobile devices, specify the `facingMode` to select the camera:

```typescript theme={null}
const stream = await navigator.mediaDevices.getUserMedia({
  video: {
    facingMode: "user", // front camera
    // facingMode: "environment" // back camera
    frameRate: model.fps,
    width: model.width,
    height: model.height,
  },
});
```

<Tip>
  When switching between front and back cameras, recapture the stream — each camera has different hardware capabilities that affect resolution and frame rate.
</Tip>

## Front-camera mirroring

For selfie streams, pre-flip the input with `mirror`. Server-baked pixels (watermarks, overlays) then come out in display orientation, and the output renders as-is.

```typescript theme={null}
const realtimeClient = await client.realtime.connect(stream, {
  model,
  mirror: "auto",
  onRemoteStream: (s) => { videoElement.srcObject = s; },
});
```

* `"auto"` mirrors when the input track reports `facingMode: "user"`.
* Pass `true` when your app already knows the camera is front-facing (desktop webcams often don't report `facingMode`).

The SDK only flips the stream it sends — not your local preview.

## Connection management

### Connect once, update prompts

Establishing a WebRTC connection takes a few hundred milliseconds. Don't reconnect just to change the style or prompt — use `set()` or `setPrompt()` instead:

```typescript theme={null}
// Good: update prompt without reconnecting
await realtimeClient.set({ prompt: "New style" });

// Avoid: disconnecting and reconnecting for every prompt change
realtimeClient.disconnect(); // unnecessary
await client.realtime.connect(stream, { model, ... }); // slow
```

### When to reconnect

Reconnect only when you need to:

* **Switch models** (e.g., from Lucy Restyle Live to Lucy 2.1) — each model runs on a different pipeline
* **Switch cameras** — the stream changes, so you need a new connection
* **Recover from a failed connection** after auto-reconnect gives up

```typescript theme={null}
async function switchModel(newModel: ModelDefinition) {
  realtimeClient.disconnect();
  await new Promise((r) => setTimeout(r, 100)); // allow cleanup
  realtimeClient = await client.realtime.connect(stream, {
    model: newModel,
    onRemoteStream: (s) => { videoElement.srcObject = s; },
  });
}
```

### Track connection state

Always listen to `connectionChange` events to update your UI:

```typescript theme={null}
realtimeClient.on("connectionChange", (state) => {
  switch (state) {
    case "connecting":
      showSpinner();
      break;
    case "connected":
    case "generating":
      hideSpinner();
      break;
    case "reconnecting":
      showReconnectingBanner();
      break;
    case "disconnected":
      showDisconnectedUI();
      break;
  }
});
```

**Connection states:**

| State          | Meaning                              | User-visible action        |
| -------------- | ------------------------------------ | -------------------------- |
| `connecting`   | Initial connection in progress       | Show loading spinner       |
| `connected`    | Ready to send prompts                | Show connected UI          |
| `generating`   | Actively producing transformed video | Show "live" indicator      |
| `reconnecting` | Auto-reconnecting after drop         | Show "reconnecting" banner |
| `disconnected` | Not connected                        | Show reconnect button      |

## Prompt strategy

### Use prompt enhancement

Enable `enhance: true` (the default) to let Decart expand short prompts into detailed descriptions. This dramatically improves output quality for simple inputs:

```typescript theme={null}
// Short prompt → enhanced automatically
realtimeClient.setPrompt("Anime style"); // enhanced behind the scenes

// Detailed prompt → disable enhancement for full control
realtimeClient.setPrompt(
  "A highly detailed 2D anime character with smooth cel-shaded lines, soft pastel highlights, large expressive eyes",
  { enhance: false }
);
```

<Tip>
  Start with enhancement enabled. Only disable it when you need exact control over the prompt text.
</Tip>

### Atomic updates with `set()`

Since `set()` replaces the entire state, always include every field you want to keep. When using Lucy 2.1 with both a prompt and reference image, include both in every call:

```typescript theme={null}
// Good: atomic update
await realtimeClient.set({
  prompt: "Transform into this character",
  image: characterPhoto,
  enhance: true,
});

// Avoid: separate calls can cause flickering
await realtimeClient.setPrompt("Transform into this character");
await realtimeClient.setImage(characterPhoto); // brief mismatch between prompt and image
```

### Debounce rapid prompt changes

If your UI lets users type prompts in a text field, debounce the input to avoid sending dozens of rapid updates:

```typescript theme={null}
let debounceTimer: ReturnType<typeof setTimeout>;

function onPromptInput(text: string) {
  clearTimeout(debounceTimer);
  debounceTimer = setTimeout(() => {
    realtimeClient.setPrompt(text, { enhance: true });
  }, 300); // wait 300ms after the user stops typing
}
```

## Error handling

### Listen for errors

Always register an error handler. Without one, connection failures go unnoticed:

```typescript theme={null}
import type { DecartSDKError } from "@decartai/sdk";

realtimeClient.on("error", (error: DecartSDKError) => {
  console.error(`[${error.code}] ${error.message}`);

  switch (error.code) {
    case "INVALID_API_KEY":
      redirectToLogin();
      break;
    case "WEB_RTC_ERROR":
      showNetworkError();
      break;
    case "MODEL_NOT_FOUND":
      showModelError();
      break;
    default:
      showGenericError(error.message);
  }
});
```

### Auto-reconnect

The SDK automatically reconnects when an unexpected disconnection occurs (e.g., network interruption). It retries with exponential backoff up to 5 times:

1. Connection drops → state moves to `"reconnecting"`
2. SDK retries with increasing delays
3. If reconnection succeeds → state moves back to `"generating"`
4. If all retries fail → state moves to `"disconnected"` and an `error` event fires

You don't need to implement reconnection logic yourself — but you should update your UI to reflect the `reconnecting` state.

### Wrap setup in try/catch

Camera access and connection can both fail. Catch errors early:

```typescript theme={null}
async function startRealtime() {
  try {
    const stream = await navigator.mediaDevices.getUserMedia({
      video: { frameRate: model.fps, width: model.width, height: model.height },
    });
    const realtimeClient = await client.realtime.connect(stream, {
      model,
      onRemoteStream: (s) => { videoElement.srcObject = s; },
    });
    return realtimeClient;
  } catch (error) {
    if (error instanceof DOMException && error.name === "NotAllowedError") {
      showCameraPermissionError();
    } else {
      showConnectionError(error);
    }
  }
}
```

## Cleanup

Always release resources when the session ends or the component unmounts:

```typescript theme={null}
function cleanup() {
  // 1. Disconnect from Decart
  realtimeClient.disconnect();

  // 2. Remove event listeners
  realtimeClient.off("connectionChange", onConnectionChange);
  realtimeClient.off("error", onError);

  // 3. Stop the camera
  stream.getTracks().forEach((track) => track.stop());
}

// Browser: cleanup on page unload
window.addEventListener("beforeunload", cleanup);

// React: cleanup on unmount
useEffect(() => {
  return () => cleanup();
}, []);
```

<Warning>
  Forgetting to call `disconnect()` leaves WebRTC connections open, consuming bandwidth and server resources. Always clean up.
</Warning>

## Client-side authentication

Never expose your permanent API key in client-side code. Use short-lived client tokens instead:

<Steps>
  <Step title="Create a backend endpoint">
    ```typescript theme={null}
    // Your server (Express, Next.js, etc.)
    const token = await client.tokens.create();
    res.json(token);
    ```
  </Step>

  <Step title="Fetch and use the token in the browser">
    ```typescript theme={null}
    const { apiKey } = await fetch("/api/realtime-token", { method: "POST" }).then((r) => r.json());
    const client = createDecartClient({ apiKey }); // short-lived token, not your permanent key
    ```
  </Step>
</Steps>

<Info>
  Client tokens only prevent **new** connections after expiry. Active WebRTC sessions continue working even after the token expires.
</Info>

## Firewall and network configuration

Realtime integrations need outbound access to Decart's API, signaling, diagnostics, and media endpoints.

### Domains to allow

| Domain               | Purpose                                 | Protocol           |
| -------------------- | --------------------------------------- | ------------------ |
| `api.decart.ai`      | REST API, authentication, session setup | HTTPS over TCP 443 |
| `api3.decart.ai`     | Realtime signaling                      | WSS over TCP 443   |
| `platform.decart.ai` | Diagnostics and telemetry               | HTTPS over TCP 443 |
| `turn.decart.ai`     | WebRTC media via TURN over TLS          | TCP 443            |
| `lk.decart.ai`       | Realtime media                          | WebRTC media       |

Staging environments use `turn.stage-decart.com` and `api.stage-decart.com` instead.

### Ports and protocols

| Port / protocol | Direction | Recommendation                                                                                                                                                         |
| --------------- | --------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| TCP 443         | Outbound  | Covers HTTPS, secure WebSocket (WSS) signaling, and TURN-over-TLS media.                                                                                               |
| UDP 3478        | Outbound  | Covers STUN/TURN as part of the WebRTC standard.                                                                                                                       |
| UDP 7882        | Outbound  | Covers video streaming using LiveKit / WebRTC protocols. Media-server IPs are dynamic, so allow outbound traffic by protocol and port instead of fixed destination IP. |

<Warning>
  UDP restrictions and proxy behavior are the most common causes of "it connects, but the video freezes" issues.
</Warning>

### UDP traffic and proxy configuration

* Allow outbound UDP traffic on port 7882.
* Allow TURN over UDP on port 3478.
* Do not force media through an HTTP or HTTPS proxy that cannot pass UDP if you want the lowest-latency path.
* For SNI or transparent proxies, allow the domains above and do not TLS-intercept `turn.decart.ai`; decrypting that traffic breaks the connection.

## Mobile considerations

### React Native / Expo

React Native's WebRTC implementation differs from the browser:

* **Force VP8 codec.** React Native works best with VP8. Use `customizeOffer` to reorder the SDP:

```typescript theme={null}
const realtimeClient = await client.realtime.connect(stream, {
  model,
  onRemoteStream: (s) => setRemoteStream(s),
  customizeOffer: (offer: RTCSessionDescriptionInit) => {
    forceCodecInSDP(offer, "VP8");
  },
});
```

* **Type casting.** React Native's `MediaStream` type differs from `globalThis.MediaStream`. Cast when needed:

```typescript theme={null}
const realtimeClient = await client.realtime.connect(
  stream as unknown as globalThis.MediaStream,
  { ... }
);
```

* **Handle app lifecycle.** Disconnect when backgrounded, reconnect when foregrounded:

```typescript theme={null}
const handleAppState = (nextState: AppStateStatus) => {
  if (nextState === "active") {
    initializeWebRTC({ model, facingMode });
  } else {
    cleanupWebRTC();
  }
};

AppState.addEventListener("change", handleAppState);
```

<Tip>
  See the [Realtime Mobile App](/examples/real-time-mobile-app) walkthrough for a complete Expo integration with model switching and style presets.
</Tip>

### Battery and bandwidth

Realtime WebRTC streaming consumes significant resources on mobile:

* **Disconnect when not visible.** Use app lifecycle events to pause streaming in the background.
* **Match camera constraints to model.** Don't request 4K when the model expects 720p — this wastes encoding power and bandwidth.
* **Use front camera when possible.** Front cameras typically have lower resolution, which reduces encoding overhead.

## Session tracking

Use the `generationTick` event to track session duration and display billing information to users:

```typescript theme={null}
realtimeClient.on("generationTick", ({ seconds }) => {
  document.getElementById("timer").textContent = `${seconds}s`;
});
```

## Quick reference

| Topic          | Recommendation                                                               |
| -------------- | ---------------------------------------------------------------------------- |
| Camera setup   | Use `model.fps`, `model.width`, `model.height`                               |
| Prompt changes | Use `set()` or `setPrompt()` — don't reconnect                               |
| Prompt + image | Use `set()` atomically — avoid separate calls                                |
| Enhancement    | Keep `enhance: true` unless you need exact control                           |
| Error handling | Register `error` and `connectionChange` listeners                            |
| Reconnection   | Handled by SDK automatically (5 retries, backoff)                            |
| Cleanup        | Always `disconnect()` + stop media tracks                                    |
| Auth           | Use client tokens in the browser, never your API key                         |
| Network        | Allow required domains, TCP 443, and outbound UDP 7882 / 3478 where possible |
| Mobile         | Force VP8, handle app lifecycle, match camera to model                       |

## Next steps

<CardGroup cols={2}>
  <Card title="Lucy 2.1 Guide" icon="sparkles" href="/models/realtime/lucy-2.1">
    Character transformation with reference images
  </Card>

  <Card title="Reference Image Guide" icon="image" href="/models/realtime/reference-images">
    Best practices for character and style reference images
  </Card>

  <Card title="JavaScript SDK" icon="js" href="/sdks/javascript-realtime">
    Full SDK reference for realtime features
  </Card>

  <Card title="Mobile App" icon="mobile" href="/examples/real-time-mobile-app">
    Complete Expo walkthrough
  </Card>
</CardGroup>
