Skip to main content
The Realtime API transforms live camera video with minimal latency. Signaling is a Decart-owned WebSocket; media flows through a LiveKit room that the SDK joins for you. Perfect for camera effects, video restyling apps, AR experiences, and interactive live streaming.

Quick Start

Client-Side Authentication

For production Android apps, use ephemeral keys instead of embedding your permanent API key in the APK. Ephemeral keys are short-lived tokens safe to include in client applications.
Learn more about client tokens and why they’re important for security.

Fetching an Ephemeral Key

Your app should fetch an ephemeral key from your backend server before connecting:

Connecting with an Ephemeral Key

Never hardcode your permanent API key in Android apps. APKs can be decompiled, exposing embedded secrets. Always use ephemeral keys from your backend.

Connecting

Camera ownership

You have two options for the local stream:
  • SDK-owned camera (simplest). Pass publishCamera = true to ConnectOptions and the SDK opens the camera, joins the LiveKit room, and publishes for you.
  • Caller-owned preview. Call realtime.createLocalVideoStream(model, facing, mirror) ahead of time to drive a preview UI before connecting, then pass the resulting RealtimeMediaStream to connect(...). Preview and publish share the same LiveKit Room. Dispose the stream when you’re done.
There’s also a static RealTimeClient.createLocalVideoStream(context, model, ...) for building a preview before you’ve decided which API key to use.
Camera capture requires a real Android device. The emulator does not support camera-driven realtime sessions.

Rendering with LiveKit

Realtime media tracks are LiveKit tracks. Use a LiveKit renderer such as TextureViewRenderer or SurfaceViewRenderer and initialize it with the Room’s EglBase context:
You can react to remote-stream changes (initial frame, auto-reconnect rebinds) via realtime.remoteStreamUpdates:

Front-camera mirroring

mirror pre-flips the captured frames before they’re published, so server-baked pixels (watermarks, overlays) remain readable. Render both local previews and remote streams as-is — do not also set renderer-level mirroring or you’ll double-flip.
MirrorMode values:
  • OFF — never mirror.
  • ON — always mirror.
  • AUTO (default) — mirror only when facing == FacingMode.FRONT.
The same mirror argument is available on createLocalVideoStream(...).

Output resolution

Opt into 1080p output from supported models; otherwise the server defaults to 720p:

Reference images

Send a reference image at connect time via initialImage (base64-encoded):
Set initialImage and/or initialPrompt so the first frame is already transformed — otherwise viewers briefly see the raw camera feed.

Tuning the LiveKit publisher

Override codec, bitrate, or framerate via RealtimeConfiguration:
Defaults: codec VP8, max bitrate 2,000,000 bps, max framerate 30 fps, simulcast on.

Realtime audio

The Android LiveKit publisher is video-only in 0.7. publishMicrophone, includeMicrophone, and RealtimeMediaStream.audioTrack are retained for source compatibility but are deprecated and ignored — SDK-created streams always expose audioTrack = null. Use the JavaScript or Swift realtime SDKs if you need audio.

Managing Prompts

setPrompt is a suspend function that waits for the server ack and throws on ack failure, timeout, or websocket disconnect. Always call it from a coroutine.
Parameters:
  • prompt: String (required) - Style description
  • enhance: Boolean (optional) - Auto-enhance the prompt (default: true)
  • timeoutMs: Long (optional) - Ack timeout in milliseconds (default: 15_000)
If you’d rather fire-and-forget and observe the ack later, use setPromptAsync(...) which returns Deferred<Unit>:
Prompt enhancement uses Decart’s AI to expand simple prompts for better results. Disable it if you want full control over the exact prompt.

Reference Images

Send a reference image (and optionally a prompt) for image-guided models. Also a suspend function with a Deferred-returning sibling.
Parameters:
  • imageBase64: String? (required) - Base64-encoded image, or null to clear
  • prompt: String? (optional) - Text prompt to send with the image
  • enhance: Boolean? (optional) - Whether to enhance the prompt
  • timeout: Long (optional) - Ack timeout in milliseconds (default: 30_000)

Connection State

Monitor connection state changes using a Kotlin Flow:
Connection States:
  • DISCONNECTED - Not connected (initial state, after disconnect(), or after reconnect failure)
  • CONNECTING - Initial connection in progress
  • CONNECTED - Connected and ready to send prompts
  • GENERATING - Actively generating transformed video (sticky until disconnected)
  • RECONNECTING - Connection lost unexpectedly; the SDK is automatically retrying
The SDK automatically reconnects when an unexpected disconnection occurs (e.g., network interruption). During auto-reconnect, the state transitions to RECONNECTING while the SDK retries. If all retries fail, the state moves to DISCONNECTED and an error is emitted. A new RealtimeMediaStream is delivered via remoteStreamUpdates after each successful rebind — re-attach your renderer.

Error Handling

Errors are emitted via the errors SharedFlow:
Error Codes:
  • INVALID_API_KEY - API key is invalid or missing
  • WEBRTC_TIMEOUT_ERROR - Connection timed out
  • WEBRTC_ICE_ERROR - ICE negotiation failed
  • WEBRTC_WEBSOCKET_ERROR - WebSocket connection error
  • WEBRTC_SERVER_ERROR - Server-side error
  • WEBRTC_SIGNALING_ERROR - Signaling protocol error

Publisher Stats

The stats SharedFlow emits outbound (publisher) video metrics roughly once per second:
PublishStatsEvent fields:
  • bytesSent, deltaBytes - Cumulative and per-sample bytes sent
  • framesEncoded, deltaFrames - Cumulative and per-sample frames encoded
  • frameWidth, frameHeight - Current encoded frame size
  • encoderImplementation - Codec implementation reported by libwebrtc
  • qualityLimitationReason - "cpu", "bandwidth", etc. when the encoder is throttling

Generation Ticks

Track session duration for billing and usage display:

Diagnostics

Monitor detailed connection diagnostics for debugging:

Connection Quality

Two layers report network health on a shared GOOD | FAIR | POOR | CRITICAL scale: a preflight check before connecting, and an in-session signal while connected.

Preflight

A fast, network-only reachability check. No session, no cost:

In-session quality

While connected, the SDK derives a smoothed verdict from live connection stats (latency, packet loss, upstream bandwidth, frame rate) and tells you the limiting factor. Level is debounced; metrics refresh each stats sample (~few seconds):

Glass-to-glass latency (opt-in)

Network RTT alone doesn’t reflect the latency users actually feel — a session can read GOOD while still feeling laggy. Set debugQuality = true to measure the real camera→display latency: the SDK stamps a pixel marker into each outgoing frame and reads it back off the rendered output, surfacing startup (ttffMs), steady-state (g2gMs), and end-to-end drops (g2gDropRatio). When present, glass-to-glass drives the latency verdict instead of RTT.
Diagnostic only. The marker is visible (bottom-left of the published + rendered video) and adds per-frame pixel work — don’t enable it for production / end-user sessions. With a caller-provided stream, build it via createLocalVideoStream(..., debugQuality = true) so the same flag is set on both the stream and connect().

Deep preflight

For a measured verdict before connecting, use the deep probe — it briefly opens a real session with a synthetic source, measures glass-to-glass, then tears it down. Requires a model and costs a short session:

Session Identifiers

Once a session is established the SDK surfaces the server-side session id and a subscribe token:
Both come from the sessionStarted: StateFlow<SessionStarted?> — collect it if you need to react when they appear.

Cleanup

Always tear down the session and release native resources when you’re done:
disconnect() ends the current session but leaves the client reusable. release() cancels the SDK’s coroutine scope and shuts down the underlying LiveKit room ownership; call it when the client is no longer needed.
Forgetting to dispose() a caller-owned RealtimeMediaStream leaks the LiveKit Room (and its capturer + native PeerConnectionFactory). Tie disposal to your lifecycle (onCleared, DisposableEffect, etc.).

Complete Jetpack Compose Example

A full Jetpack Compose tab with a caller-owned preview and the LiveKit renderer, modeled on the sample app on GitHub:

Best Practices

Use realtime.createLocalVideoStream(model, ...) (or the static factory) so capture dimensions match the model exactly. Lucy 2.1 / VTON models capture at 1088x624; Lucy Restyle 2 at 1280x704.
setPrompt and setImage are suspend functions that wait for the server ack and throw on ack failure, timeout, or disconnect. Always call them from a coroutine (lifecycleScope, viewModelScope, rememberCoroutineScope()) and handle exceptions. Reach for setPromptAsync / setImageAsync only when you need a Deferred handle.
Keep enhance = true (default) so Decart’s AI expands simple prompts. Disable it only when you need exact prompt control.
remoteStreamUpdates emits a fresh RealtimeMediaStream after each reconnect. Remove the previous track from your renderer and add the new one — failing to do so leaves the renderer bound to a dead track.
If you call createLocalVideoStream yourself, dispose the returned RealtimeMediaStream from your lifecycle hook (onCleared, DisposableEffect.onDispose). The Room owns native resources that don’t get freed otherwise.
Always test camera features on real Android devices. The emulator does not support camera capture for realtime sessions.
Request CAMERA permission at runtime before attempting to connect. (Realtime audio is not currently supported, so RECORD_AUDIO isn’t required.) Handle permission denials gracefully in your UI.

API Reference

DecartClient(context, config)

Top-level entry point. Exposes realtime: RealTimeClient and queue: QueueClient. Use DecartClientConfig(apiKey, baseUrl?, httpBaseUrl?, logLevel?). Call client.release() to tear down both sub-clients.

realtime.createLocalVideoStream(model, facing, mirror, configuration?, includeMicrophone?)

Build a caller-owned preview RealtimeMediaStream sized to model.width × model.height. Pass it to connect(..., localStream = ...) so preview and publish share a LiveKit Room. Caller must dispose() the stream. Parameters:
  • model: RealtimeModel - Model to size the capture for
  • facing: FacingMode - FRONT (default) or BACK
  • mirror: MirrorMode - AUTO (default), OFF, or ON
  • configuration: RealtimeConfiguration (optional) - Codec / bitrate / framerate overrides
  • includeMicrophone: Boolean - Ignored; audio is not yet supported
Overload: createLocalVideoStream(width, height, facing, ...) takes explicit dimensions instead of a model. Static RealTimeClient.createLocalVideoStream(context, model, ...) lets you build a preview before instantiating a client.

realtime.connect(options, localStream?)

Suspend. Join the model’s LiveKit room and (by default) publish the local camera. Returns the remote RealtimeMediaStream and also emits it via onRemoteStream / remoteStreamUpdates. ConnectOptions parameters:
  • model: RealtimeModel (required) - Realtime model from RealtimeModels
  • initialPrompt: InitialPrompt? - Starting prompt (text + enhance)
  • initialImage: String? - Base64-encoded reference image
  • resolution: Resolution? - P720 or P1080. Omit for the server’s 720p default; pass P1080 to request 1080p from supported models.
  • realtimeConfiguration: RealtimeConfiguration - Codec / bitrate / framerate tuning
  • publishCamera: Boolean - Open the camera and publish (default true)
  • publishMicrophone: Boolean - Ignored in 0.7; audio is not supported
  • facing: FacingMode - FRONT (default) or BACK. Only used when the SDK opens its own camera.
  • mirror: MirrorMode - AUTO (default), OFF, or ON
  • onRemoteStream: ((RealtimeMediaStream) -> Unit)? - Callback when a (re)connected remote stream is available
Throws on signaling, ICE, or LiveKit room failures.

realtime.setPrompt(prompt, enhance, timeoutMs)

Suspend. Update the prompt and wait for the server ack. Parameters:
  • prompt: String - Style description
  • enhance: Boolean - Auto-enhance the prompt (default: true)
  • timeoutMs: Long - Ack timeout in ms (default: 15_000)
Throws IllegalStateException if not connected, or a DecartError on ack failure, timeout, or websocket disconnect.

realtime.setPromptAsync(prompt, enhance, timeoutMs)

Same parameters as setPrompt but returns Deferred<Unit>. Call await() to observe ack failure / timeout / disconnect.

realtime.setImage(imageBase64, prompt, enhance, timeout)

Suspend. Send (or clear, with null) a reference image. Parameters:
  • imageBase64: String? - Base64-encoded image, or null to clear
  • prompt: String? - Optional text prompt
  • enhance: Boolean? - Whether to enhance the prompt
  • timeout: Long - Ack timeout in ms (default: 30_000)
Throws IllegalStateException if not connected.

realtime.setImageAsync(imageBase64, prompt, enhance, timeout)

Same parameters as setImage but returns Deferred<Unit>.

realtime.disconnect() / realtime.release()

disconnect() ends the current session. release() cancels the SDK’s coroutine scope; call it when the client is no longer needed (and prefer client.release() to also clean up the queue client).

realtime.isConnected()

Returns: Boolean - Whether the current connection state is CONNECTED or GENERATING.

RealtimeMediaStream

  • videoTrack: VideoTrack? - LiveKit video track; pass to your renderer via addRenderer(...) / removeRenderer(...).
  • audioTrack: AudioTrack? - Deprecated; always null in 0.7.
  • id: String - Stream identifier (stream-local or stream-remote).
  • room: Room? - LiveKit room that owns the tracks. Use room.lkObjects.eglBase.eglBaseContext when initializing renderers.
  • dispose() - Tear down tracks and the room. Safe to call multiple times.

Observable State

Next Steps

Queue API

Generate and transform videos with batch processing and Flow-based progress

SDK Overview

Installation, setup, and Android SDK fundamentals