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
Connecting
Camera ownership
You have two options for the local stream:- SDK-owned camera (simplest). Pass
publishCamera = truetoConnectOptionsand 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 resultingRealtimeMediaStreamtoconnect(...). Preview and publish share the same LiveKitRoom. Dispose the stream when you’re done.
RealTimeClient.createLocalVideoStream(context, model, ...) for building a preview before you’ve decided which API key to use.
Rendering with LiveKit
Realtime media tracks are LiveKit tracks. Use a LiveKit renderer such asTextureViewRenderer or SurfaceViewRenderer and initialize it with the Room’s EglBase context:
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 whenfacing == FacingMode.FRONT.
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 viainitialImage (base64-encoded):
Tuning the LiveKit publisher
Override codec, bitrate, or framerate viaRealtimeConfiguration:
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.
prompt: String(required) - Style descriptionenhance: Boolean(optional) - Auto-enhance the prompt (default:true)timeoutMs: Long(optional) - Ack timeout in milliseconds (default:15_000)
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 asuspend function with a Deferred-returning sibling.
imageBase64: String?(required) - Base64-encoded image, ornullto clearprompt: String?(optional) - Text prompt to send with the imageenhance: Boolean?(optional) - Whether to enhance the prompttimeout: Long(optional) - Ack timeout in milliseconds (default:30_000)
Connection State
Monitor connection state changes using a KotlinFlow:
DISCONNECTED- Not connected (initial state, afterdisconnect(), or after reconnect failure)CONNECTING- Initial connection in progressCONNECTED- Connected and ready to send promptsGENERATING- 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 theerrors SharedFlow:
INVALID_API_KEY- API key is invalid or missingWEBRTC_TIMEOUT_ERROR- Connection timed outWEBRTC_ICE_ERROR- ICE negotiation failedWEBRTC_WEBSOCKET_ERROR- WebSocket connection errorWEBRTC_SERVER_ERROR- Server-side errorWEBRTC_SIGNALING_ERROR- Signaling protocol error
Publisher Stats
Thestats SharedFlow emits outbound (publisher) video metrics roughly once per second:
PublishStatsEvent fields:
bytesSent,deltaBytes- Cumulative and per-sample bytes sentframesEncoded,deltaFrames- Cumulative and per-sample frames encodedframeWidth,frameHeight- Current encoded frame sizeencoderImplementation- Codec implementation reported by libwebrtcqualityLimitationReason-"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 sharedGOOD | 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 readGOOD 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.
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 amodel and costs a short session:
Session Identifiers
Once a session is established the SDK surfaces the server-side session id and a subscribe token: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.
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
Let the model registry size the camera
Let the model registry size the camera
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.Use suspend `setPrompt` from a coroutine
Use suspend `setPrompt` from a coroutine
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.Enable prompt enrichment
Enable prompt enrichment
Keep
enhance = true (default) so Decart’s AI expands simple prompts. Disable it only when you need exact prompt control.Re-attach the renderer on auto-reconnect
Re-attach the renderer on auto-reconnect
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.Dispose caller-owned streams
Dispose caller-owned streams
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.Test on real devices
Test on real devices
Always test camera features on real Android devices. The emulator does not support camera capture for realtime sessions.
Request permissions properly
Request permissions properly
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 forfacing: FacingMode-FRONT(default) orBACKmirror: MirrorMode-AUTO(default),OFF, orONconfiguration: RealtimeConfiguration(optional) - Codec / bitrate / framerate overridesincludeMicrophone: Boolean- Ignored; audio is not yet supported
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 fromRealtimeModelsinitialPrompt: InitialPrompt?- Starting prompt (text + enhance)initialImage: String?- Base64-encoded reference imageresolution: Resolution?-P720orP1080. Omit for the server’s 720p default; passP1080to request 1080p from supported models.realtimeConfiguration: RealtimeConfiguration- Codec / bitrate / framerate tuningpublishCamera: Boolean- Open the camera and publish (defaulttrue)publishMicrophone: Boolean- Ignored in 0.7; audio is not supportedfacing: FacingMode-FRONT(default) orBACK. Only used when the SDK opens its own camera.mirror: MirrorMode-AUTO(default),OFF, orONonRemoteStream: ((RealtimeMediaStream) -> Unit)?- Callback when a (re)connected remote stream is available
realtime.setPrompt(prompt, enhance, timeoutMs)
Suspend. Update the prompt and wait for the server ack.
Parameters:
prompt: String- Style descriptionenhance: Boolean- Auto-enhance the prompt (default:true)timeoutMs: Long- Ack timeout in ms (default:15_000)
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, ornullto clearprompt: String?- Optional text promptenhance: Boolean?- Whether to enhance the prompttimeout: Long- Ack timeout in ms (default:30_000)
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 viaaddRenderer(...)/removeRenderer(...).audioTrack: AudioTrack?- Deprecated; alwaysnullin 0.7.id: String- Stream identifier (stream-localorstream-remote).room: Room?- LiveKit room that owns the tracks. Useroom.lkObjects.eglBase.eglBaseContextwhen initializing renderers.dispose()- Tear down tracks and the room. Safe to call multiple times.
Observable State
| Property | Type | Description |
|---|---|---|
connectionState | StateFlow<ConnectionState> | Current connection state |
connectionChange | StateFlow<ConnectionState> | JS-aligned alias for connectionState |
errors | SharedFlow<DecartError> | Error events |
generationTicks | SharedFlow<GenerationTickMessage> | Per-second tick during generation |
generationEnded | SharedFlow<GenerationEndedMessage> | Generation lifecycle end events |
queuePositionUpdates | SharedFlow<QueuePositionMessage> | Queue position updates while waiting for a server slot |
localStreamUpdates | SharedFlow<RealtimeMediaStream> | Local LiveKit stream updates |
remoteStreamUpdates | SharedFlow<RealtimeMediaStream> | Remote LiveKit stream updates (including auto-reconnect rebinds) |
sessionStarted | StateFlow<SessionStarted?> | (sessionId, subscribeToken) once the LiveKit room info arrives |
subscribeToken | String? | Token to hand to viewer / subscribe clients |
sessionId | String? | Current session id |
diagnostics | SharedFlow<DiagnosticEvent> | Connection diagnostic events |
stats | SharedFlow<PublishStatsEvent> | Publisher outbound video stats |
Next Steps
Queue API
Generate and transform videos with batch processing and Flow-based progress
SDK Overview
Installation, setup, and Android SDK fundamentals