> ## 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.

# Realtime API

> Transform video streams in realtime with WebRTC on iOS and macOS

The Realtime API enables you to transform live video streams with minimal latency using WebRTC. Perfect for building iOS camera effects, video conferencing filters, AR applications, and interactive live streaming.

## Quick Start

```swift theme={null}
import DecartSDK
import LiveKit

let model = Models.realtime(.lucy-restyle-2)

// Create client
let config = DecartConfiguration(apiKey: "your-api-key-here")
let client = DecartClient(decartConfiguration: config)

// Create realtime manager
let manager = try client.createRealtimeManager(
    options: RealtimeConfiguration(
        model: model,
        initialPrompt: DecartPrompt(text: "Anime style", enrich: true)
    )
)

// Build a LiveKit camera track sized to the model
let captureOptions = CameraCaptureOptions(
    position: .front,
    dimensions: Dimensions(width: Int32(model.height), height: Int32(model.width)),
    fps: model.fps
)
let mirror = MirroringVideoProcessor(mode: .auto)
let videoTrack = LocalVideoTrack.createCameraTrack(name: "video0", options: captureOptions, processor: mirror)

// Connect and get transformed stream
let localStream = RealtimeMediaStream(videoTrack: videoTrack, id: .localStream)
let remoteStream = try await manager.connect(localStream: localStream)

// Change style on the fly (suspends until the server acks)
try await manager.setPrompt(DecartPrompt(text: "Cyberpunk city", enrich: true))

// Disconnect when done
try? await videoTrack.stop()
await manager.disconnect()
```

## Client-Side Authentication

For iOS and macOS applications, use ephemeral keys instead of embedding your permanent API key in the app bundle. Ephemeral keys are short-lived tokens safe to include in client applications.

<Info>
  Learn more about [client tokens](/getting-started/client-tokens) and why they're important for security.
</Info>

### Fetching an Ephemeral Key

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

```swift theme={null}
import Foundation

struct EphemeralKeyResponse: Codable {
    let apiKey: String
    let expiresAt: String
}

func fetchEphemeralKey() async throws -> String {
    // Replace with your backend URL
    let url = URL(string: "https://your-backend.com/api/realtime-token")!

    var request = URLRequest(url: url)
    request.httpMethod = "POST"
    // Add any auth headers your backend requires
    // request.setValue("Bearer \(userToken)", forHTTPHeaderField: "Authorization")

    let (data, response) = try await URLSession.shared.data(for: request)

    guard let httpResponse = response as? HTTPURLResponse,
          httpResponse.statusCode == 200 else {
        throw DecartError.invalidAPIKey
    }

    let keyResponse = try JSONDecoder().decode(EphemeralKeyResponse.self, from: data)
    return keyResponse.apiKey
}
```

### Connecting with an Ephemeral Key

```swift theme={null}
import DecartSDK

func connectToRealtime() async throws -> DecartRealtimeManager {
    // 1. Fetch ephemeral key from your backend
    let ephemeralKey = try await fetchEphemeralKey()

    // 2. Create client with ephemeral key
    let config = DecartConfiguration(apiKey: ephemeralKey)
    let client = DecartClient(decartConfiguration: config)

    // 3. Set up manager and camera, then connect
    let model = Models.realtime(.lucy-restyle-2)

    let manager = try client.createRealtimeManager(
        options: RealtimeConfiguration(
            model: model,
            initialPrompt: DecartPrompt(text: "Anime style", enrich: true)
        )
    )

    let captureOptions = CameraCaptureOptions(
        position: .front,
        dimensions: Dimensions(width: Int32(model.height), height: Int32(model.width)),
        fps: model.fps
    )
    let mirror = MirroringVideoProcessor(mode: .auto)
    let videoTrack = LocalVideoTrack.createCameraTrack(name: "video0", options: captureOptions, processor: mirror)

    let localStream = RealtimeMediaStream(videoTrack: videoTrack, id: .localStream)
    _ = try await manager.connect(localStream: localStream)

    return manager
}
```

<Warning>
  Never hardcode your permanent API key in iOS apps. App bundles can be decompiled, exposing embedded secrets.
</Warning>

## Camera Capture

Realtime media uses LiveKit tracks. Build a `LocalVideoTrack` from LiveKit's camera-track factory, size it from the model registry, and attach a `MirroringVideoProcessor` to pre-flip the front camera.

### Setting Up Capture

```swift theme={null}
import DecartSDK
import LiveKit

let model = Models.realtime(.lucy-restyle-2)

let captureOptions = CameraCaptureOptions(
    position: .front,                                                                  // .front or .back
    dimensions: Dimensions(width: Int32(model.height), height: Int32(model.width)),    // portrait — swap for landscape
    fps: model.fps
)

let mirror = MirroringVideoProcessor(mode: .auto)
let videoTrack = LocalVideoTrack.createCameraTrack(
    name: "video0",
    options: captureOptions,
    processor: mirror
)
```

**`CameraCaptureOptions` parameters:**

* `position` — `.front` or `.back` (LiveKit's `AVCaptureDevice.Position`)
* `dimensions` — `Dimensions(width:height:)`. For portrait output, pass `model.height` as width and `model.width` as height (swap for landscape).
* `fps` — target framerate; use `model.fps`

`LocalVideoTrack.createCameraTrack(name:options:processor:)` starts capture immediately. There's no separate `startCapture()` call.

### Switching Cameras

LiveKit's `CameraCapturer` toggles between front and back. Keep the `MirroringVideoProcessor` in sync so `.auto` mirroring follows the active camera:

```swift theme={null}
guard let cameraCapturer = videoTrack.capturer as? CameraCapturer else { return }
try await cameraCapturer.switchCameraPosition()
mirror.cameraPosition = cameraCapturer.position
```

### Stopping Capture

```swift theme={null}
try? await videoTrack.stop()
```

<Tip>Use `model.fps`, `model.width`, and `model.height` to size the capture so the encoder doesn't have to rescale.</Tip>

<Warning>Camera capture requires a **real iOS device**. The simulator does not support camera access.</Warning>

### Front-camera mirroring

Pre-flipping the selfie input keeps server-baked pixels (watermarks, overlays) correctly oriented when you render the remote stream as-is.

`MirrorMode` values:

* `.off` (default) — never mirror.
* `.auto` — mirror only when the active camera is `.front`. Update `mirror.cameraPosition = cameraCapturer.position` on camera switch.
* `.on` — always mirror.

With mirroring enabled, render both the local preview and the remote stream with `RTCMLVideoViewWrapper(track:)` — no `mirror:` argument.

## Connecting

Create a `DecartRealtimeManager` and connect with your local media stream:

```swift theme={null}
let manager = try client.createRealtimeManager(
    options: RealtimeConfiguration(
        model: Models.realtime(.lucy-restyle-2),
        initialPrompt: DecartPrompt(
            text: "Lego World",
            enrich: true  // Let Decart enhance the prompt (recommended)
        ),
        connection: .init(
            connectionTimeout: 15  // seconds, default
        ),
        media: .init(
            video: .init(
                maxBitrate: 2_500_000,  // default
                preferredCodec: "VP8"   // default
            )
        )
    )
)

let localStream = RealtimeMediaStream(videoTrack: videoTrack, id: .localStream)
let remoteStream = try await manager.connect(localStream: localStream)
```

**`RealtimeConfiguration` parameters:**

* `model` (required) - Realtime model from `Models.realtime()`
* `resolution` (optional) - `.p720` or `.p1080`. Omit for the server's 720p default; pass `.p1080` to request a 1080p remote stream from supported models.
* `initialPrompt` (optional) - Initial transformation prompt
  * `text` - Prompt text
  * `enrich` - Whether to auto-enhance the prompt
  * `referenceImageData` - Optional reference image `Data`
* `connection` (optional) - Connection configuration
  * `iceServers` - STUN/TURN server URLs (default: Google STUN)
  * `connectionTimeout` - Connection timeout in seconds (default: 15)
  * `rtcConfiguration` - Custom `RTCConfiguration` for advanced WebRTC tuning
* `media` (optional) - Media configuration
  * `video.maxBitrate` - Max bitrate in bps (default: 2,500,000)
  * `video.minBitrate` - Min bitrate in bps (default: 300,000)
  * `video.maxFramerate` - Max framerate (default: 30)
  * `video.preferredCodec` - Preferred video codec (default: "VP8")

**Returns:** `RealtimeMediaStream` — the transformed remote stream containing an optional `videoTrack` you can render

For image-capable models, pass the reference image data on `DecartPrompt`:

```swift theme={null}
let imageData = try Data(contentsOf: characterImageURL)

let manager = try client.createRealtimeManager(
    options: RealtimeConfiguration(
        model: Models.realtime(.lucy_v2v_14b_rt),
        initialPrompt: DecartPrompt(
            text: "Substitute the character in the video with the person in the reference image.",
            referenceImageData: imageData,
            enrich: true
        )
    )
)
```

<Tip>
  Set `initialPrompt` (with `referenceImageData` for image-capable models) so the first frame is already transformed — otherwise viewers briefly see the raw camera feed.
</Tip>

## Managing Prompts

Change the transformation style dynamically without reconnecting:

```swift theme={null}
// Simple prompt with automatic enhancement
manager.setPrompt(DecartPrompt(text: "Anime style", enrich: true))

// Custom detailed prompt without enhancement
manager.setPrompt(
    DecartPrompt(
        text: "A detailed artistic style with vibrant colors and dramatic lighting",
        enrich: false
    )
)

// With reference image (for lucy-2.1 model)
// On iOS: let referenceData = UIImage(named: "reference")!.jpegData(compressionQuality: 0.8)!
// On macOS: let referenceData = NSImage(named: "reference")!.tiffRepresentation!
let referenceData = try Data(contentsOf: Bundle.main.url(forResource: "reference", withExtension: "jpg")!)
manager.setPrompt(
    DecartPrompt(
        text: "Match this character style",
        referenceImageData: referenceData,
        enrich: true
    )
)
```

**`DecartPrompt` parameters:**

* `text` (required) - Text description of desired style
* `referenceImageData` (optional) - Reference image data (used with `lucy-2.1`)
* `enrich` (optional) - Whether to enhance the prompt (default: `false`)

<Note>Prompt enhancement uses Decart's AI to expand simple prompts for better results. Only the `lucy-2.1` model supports reference images.</Note>

## Connection State

Monitor connection state, service status, generation ticks, and session ID using the `events` AsyncStream:

```swift theme={null}
// Observe state changes
Task {
    for await state in manager.events {
        switch state.connectionState {
        case .connecting:
            showLoadingIndicator()
        case .connected:
            hideLoadingIndicator()
        case .generating:
            showGeneratingIndicator()
        case .reconnecting:
            showReconnectingIndicator()
        case .disconnected:
            showReconnectButton()
        case .idle:
            break
        case .error:
            showError()
        }

        // Track generation progress
        if let tick = state.generationTick {
            showGenerationTime("\(tick)s")
        }

        // Track session ID
        if let sessionId = state.sessionId {
            print("Session: \(sessionId)")
        }

        // Track queue position
        if let position = state.queuePosition, let size = state.queueSize {
            showQueueStatus("Position \(position) of \(size)")
        }

        // Track service status
        switch state.serviceStatus {
        case .enteringQueue:
            showQueueMessage()
        case .ready:
            hideQueueMessage()
        case .unknown:
            break
        }
    }
}
```

**`DecartRealtimeState` properties:**

* `connectionState` — `.idle`, `.connecting`, `.connected`, `.generating`, `.reconnecting`, `.disconnected`, `.error`
* `serviceStatus` — `.unknown`, `.enteringQueue`, `.ready`
* `queuePosition` — Current position in queue (nil if not queued)
* `queueSize` — Total queue size (nil if not queued)
* `generationTick` — Seconds elapsed during generation (nil when not generating)
* `sessionId` — Current session identifier (nil before session established)

**`DecartRealtimeConnectionState` helpers:**

* `.isConnected` — `true` when connected or generating
* `.isInSession` — `true` when connected, connecting, generating, or reconnecting

## Auto-Reconnect

The SDK automatically reconnects when an unexpected disconnection occurs (e.g., network interruption). During auto-reconnect, the connection state transitions to `.reconnecting` while the SDK retries with exponential backoff (up to 5 attempts, max 10s delay).

When auto-reconnect succeeds, a new `RealtimeMediaStream` is emitted via `remoteStreamUpdates`. You must rebind your UI to the new stream's video track:

```swift theme={null}
Task {
    for await newRemoteStream in manager.remoteStreamUpdates {
        // Update your UI with the new video track
        self.remoteVideoTrack = newRemoteStream.videoTrack
    }
}
```

<Info>
  Auto-reconnect is **not** triggered on user-initiated `disconnect()`, permanent errors (401/403, invalid key, expired session), or after all retries are exhausted. If all retries fail, the state moves to `.error`.
</Info>

## 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. Never throws — degrades to `.critical` / `.failed` on any error:

```swift theme={null}
let report = await client.checkConnectivity()
// report.metrics: transport (.udp / .relay / .failed), rttMs
if report.quality == .critical {
    showFallbackUI(report.reasons)
}
```

### 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. The stream yields on debounced level changes; `getConnectionQuality()` returns the live snapshot whose metrics refresh on every poll:

```swift theme={null}
Task {
    for await report in manager.connectionQualityUpdates {
        // report.limitingFactor: .bandwidth | .latency | .loss | .stall | .cpu | .none
        // report.metrics: rttMs, fps, packetLoss, availableUpstreamKbps, ...
        updateBadge(report.quality)
    }
}

let latest = manager.getConnectionQuality() // ConnectionQualityReport? — nil before the first sample
```

In-session quality is on by default. Opt out by passing `observability: .init(connectionQualityEnabled: false)` in `RealtimeConfiguration`.

### 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 frame drops (`g2gDropRatio`). When present, glass-to-glass drives the latency verdict instead of RTT.

```swift theme={null}
let localStream = client.createLocalCameraStream(model: model, debugQuality: true)
let manager = try client.createRealtimeManager(
    options: .init(model: model, debugQuality: true)
)
let remoteStream = try await manager.connect(localStream: localStream)

Task {
    for await report in manager.connectionQualityUpdates {
        // report.metrics.ttffMs / g2gMs / g2gDropRatio
    }
}
```

<Warning>Diagnostic only. The marker is visible (bottom-left of the published and rendered video) and adds per-frame pixel work — don't enable it for production / end-user sessions. The `debugQuality` flag must match on both `createLocalCameraStream` and `RealtimeConfiguration`.</Warning>

### 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:

```swift theme={null}
let probe = await client.checkConnectivity(.init(deep: true, model: model))
// probe.metrics.g2gMs / ttffMs / g2gDropRatio
```

## Error Handling

Errors are thrown from async methods and can also arrive through the `events` stream when the connection state becomes `.error`:

```swift theme={null}
do {
    let remoteStream = try await manager.connect(localStream: localStream)
} catch let error as DecartError {
    switch error {
    case .invalidAPIKey:
        showError("Invalid API key. Please check your credentials.")
    case .webRTCError(let message):
        showError("Connection error: \(message)")
    case .websocketError(let message):
        showError("WebSocket error: \(message)")
    case .connectionTimeout:
        showError("Connection timed out. Please try again.")
    case .serverError(let message):
        showError("Server error: \(message)")
    default:
        showError(error.localizedDescription)
    }
    print("Error code: \(error.errorCode)")
}
```

**Error Cases:**

* `.invalidAPIKey` - API key is invalid or missing
* `.invalidBaseURL(String?)` - Base URL is malformed
* `.webRTCError(String)` - WebRTC connection failed
* `.websocketError(String)` - WebSocket connection error
* `.connectionTimeout` - Connection timed out
* `.serverError(String)` - Server returned an error
* `.processingError(String)` - Processing failed
* `.invalidInput(String)` - Invalid input parameters
* `.modelNotFound(String)` - Specified model doesn't exist
* `.networkError(Error)` - Network request failed
* `.queueError(String)` - Queue operation failed

## Cleanup

Always stop the camera track and disconnect the manager when done:

```swift theme={null}
try? await videoTrack.stop()
await manager.disconnect()
```

<Warning>Failing to disconnect can leave the LiveKit room open and waste resources.</Warning>

## Complete SwiftUI Example

Here's a full SwiftUI application using the SDK's built-in `RTCMLVideoViewWrapper`:

```swift theme={null}
import SwiftUI
import DecartSDK
import LiveKit

@main
struct RealtimeApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

struct ContentView: View {
    @StateObject private var viewModel = RealtimeViewModel()

    var body: some View {
        ZStack {
            // Remote video background
            RTCMLVideoViewWrapper(track: viewModel.remoteVideoTrack)
                .ignoresSafeArea()

            VStack(spacing: 16) {
                // Status bar
                HStack {
                    VStack(alignment: .leading, spacing: 4) {
                        Text("Decart Realtime")
                            .font(.headline)
                            .foregroundColor(.white)
                        Text(viewModel.statusText)
                            .font(.caption)
                            .foregroundColor(
                                viewModel.isConnected ? .green : .white
                            )
                    }
                    Spacer()
                }
                .padding()
                .background(Color.black.opacity(0.6))

                Spacer()

                // Local video preview
                if viewModel.isConnected {
                    HStack {
                        Spacer()
                        RTCMLVideoViewWrapper(track: viewModel.localVideoTrack)
                        .frame(width: 120, height: 160)
                        .clipShape(RoundedRectangle(cornerRadius: 12))
                        .overlay(
                            RoundedRectangle(cornerRadius: 12)
                                .stroke(Color.white, lineWidth: 2)
                        )
                        .padding()
                    }
                }

                // Controls
                VStack(spacing: 12) {
                    if let error = viewModel.lastError {
                        Text(error)
                            .foregroundColor(.red)
                            .font(.caption)
                            .padding(8)
                            .background(Color.black.opacity(0.8))
                            .clipShape(RoundedRectangle(cornerRadius: 8))
                    }

                    HStack(spacing: 12) {
                        TextField("Enter style prompt", text: $viewModel.promptText)
                            .textFieldStyle(.roundedBorder)

                        Button {
                            viewModel.updatePrompt()
                        } label: {
                            Image(systemName: "paperplane.fill")
                                .foregroundColor(.white)
                                .padding(12)
                                .background(
                                    viewModel.isConnected ? Color.blue : Color.gray
                                )
                                .clipShape(RoundedRectangle(cornerRadius: 8))
                        }
                        .disabled(!viewModel.isConnected)
                    }

                    HStack(spacing: 12) {
                        Button {
                            Task { try? await viewModel.switchCamera() }
                        } label: {
                            Image(systemName: "camera.rotate")
                                .foregroundColor(.white)
                                .padding(12)
                                .background(Color.gray.opacity(0.8))
                                .clipShape(Circle())
                        }
                        .disabled(!viewModel.isConnected)

                        Button {
                            Task { await viewModel.toggleConnection() }
                        } label: {
                            Text(viewModel.isConnected ? "Disconnect" : "Connect")
                                .fontWeight(.semibold)
                                .foregroundColor(.white)
                                .frame(maxWidth: .infinity)
                                .padding()
                                .background(
                                    viewModel.isConnected ? Color.red : Color.green
                                )
                                .clipShape(RoundedRectangle(cornerRadius: 12))
                        }
                    }
                }
                .padding()
                .background(Color.black.opacity(0.8))
                .clipShape(RoundedRectangle(cornerRadius: 16))
                .padding()
            }
        }
    }
}

@MainActor
class RealtimeViewModel: ObservableObject {
    @Published var statusText = "Disconnected"
    @Published var promptText = "Turn into a fantasy figure"
    @Published var lastError: String?
    @Published var isConnected = false
    @Published var localVideoTrack: VideoTrack?
    @Published var remoteVideoTrack: VideoTrack?

    private var manager: DecartRealtimeManager?
    private var localTrack: LocalVideoTrack?
    private var mirror: MirroringVideoProcessor?
    private var stateTask: Task<Void, Never>?
    private var reconnectTask: Task<Void, Never>?

    func toggleConnection() async {
        if isConnected {
            await disconnect()
        } else {
            await connect()
        }
    }

    func connect() async {
        statusText = "Connecting"
        lastError = nil

        do {
            let config = DecartConfiguration(
                apiKey: ProcessInfo.processInfo.environment["DECART_API_KEY"] ?? ""
            )
            let client = DecartClient(decartConfiguration: config)
            let model = Models.realtime(.lucy-restyle-2)

            // Create manager
            let manager = try client.createRealtimeManager(
                options: RealtimeConfiguration(
                    model: model,
                    initialPrompt: DecartPrompt(text: promptText, enrich: true)
                )
            )
            self.manager = manager

            // Build a LiveKit camera track sized to the model
            let captureOptions = CameraCaptureOptions(
                position: .front,
                dimensions: Dimensions(width: Int32(model.height), height: Int32(model.width)),
                fps: model.fps
            )
            let mirror = MirroringVideoProcessor(mode: .auto)
            let videoTrack = LocalVideoTrack.createCameraTrack(
                name: "video0",
                options: captureOptions,
                processor: mirror
            )
            self.mirror = mirror
            self.localTrack = videoTrack
            self.localVideoTrack = videoTrack

            // Connect
            let localStream = RealtimeMediaStream(
                videoTrack: videoTrack,
                id: .localStream
            )
            let remoteStream = try await manager.connect(localStream: localStream)
            self.remoteVideoTrack = remoteStream.videoTrack

            // Observe state changes
            stateTask = Task { [weak self] in
                for await state in manager.events {
                    guard let self else { return }
                    self.handleState(state)
                }
            }

            // Handle auto-reconnect track rebinding
            reconnectTask = Task { [weak self] in
                for await newStream in manager.remoteStreamUpdates {
                    guard let self else { return }
                    self.remoteVideoTrack = newStream.videoTrack
                }
            }
        } catch {
            lastError = error.localizedDescription
            statusText = "Disconnected"
        }
    }

    func disconnect() async {
        stateTask?.cancel()
        stateTask = nil
        reconnectTask?.cancel()
        reconnectTask = nil
        try? await localTrack?.stop()
        await manager?.disconnect()
        manager = nil
        localTrack = nil
        mirror = nil
        localVideoTrack = nil
        remoteVideoTrack = nil
        isConnected = false
        statusText = "Disconnected"
    }

    func updatePrompt() {
        guard let manager else { return }
        Task {
            do {
                try await manager.setPrompt(DecartPrompt(text: promptText, enrich: true))
            } catch {
                lastError = "Prompt rejected: \(error.localizedDescription)"
            }
        }
    }

    func switchCamera() async throws {
        guard let cameraCapturer = localTrack?.capturer as? CameraCapturer else { return }
        try await cameraCapturer.switchCameraPosition()
        mirror?.cameraPosition = cameraCapturer.position
    }

    private func handleState(_ state: DecartRealtimeState) {
        switch state.connectionState {
        case .connecting:
            statusText = "Connecting"
            isConnected = false
        case .connected:
            statusText = "Connected"
            isConnected = true
        case .generating:
            if let tick = state.generationTick {
                statusText = "Generating (\(String(format: "%.1f", tick))s)"
            } else {
                statusText = "Generating"
            }
            isConnected = true
        case .reconnecting:
            statusText = "Reconnecting..."
            isConnected = false
        case .disconnected:
            statusText = "Disconnected"
            isConnected = false
        case .error:
            statusText = "Error"
            isConnected = false
        case .idle:
            break
        }

        if state.serviceStatus == .enteringQueue,
           let position = state.queuePosition {
            statusText = "In queue (position \(position))"
        }
    }
}
```

## Best Practices

<AccordionGroup>
  <Accordion title="Use model properties for video constraints">
    Size `CameraCaptureOptions` from the model registry so the encoder doesn't have to rescale.

    ```swift theme={null}
    let model = Models.realtime(.lucy-restyle-2)
    let captureOptions = CameraCaptureOptions(
        position: .front,
        dimensions: Dimensions(width: Int32(model.height), height: Int32(model.width)),
        fps: model.fps
    )
    ```
  </Accordion>

  <Accordion title="Enable prompt enrichment">
    For best results, set `enrich: true` to let Decart's AI enhance your prompts. Only disable it if you need exact prompt control.
  </Accordion>

  <Accordion title="Handle auto-reconnect streams">
    Always observe `remoteStreamUpdates` to rebind your video track when the SDK auto-reconnects after a network interruption.
  </Accordion>

  <Accordion title="Observe state with AsyncStream">
    Use `for await state in manager.events` to track connection state, generation ticks, session ID, and queue position in a structured concurrency context.
  </Accordion>

  <Accordion title="Clean up properly">
    Always call `manager.disconnect()` and `capture.stopCapture()` when done to avoid memory leaks and unnecessary resource usage.
  </Accordion>

  <Accordion title="Test on real devices">
    Always test camera features on real iOS devices, as the simulator does not support WebRTC camera access.
  </Accordion>

  <Accordion title="Request permissions properly">
    Add camera and microphone usage descriptions to your Info.plist and handle permission denials gracefully in your UI.
  </Accordion>
</AccordionGroup>

## API Reference

### `DecartClient.createRealtimeManager(options:)`

Creates a realtime manager for a WebRTC session.

**Parameters:**

* `options: RealtimeConfiguration` - Configuration for the realtime session
  * `model: ModelDefinition` - Realtime model from `Models.realtime()`
  * `initialPrompt: DecartPrompt` - Initial transformation prompt (default: empty)
    * `text: String` - Prompt text
    * `enrich: Bool` - Whether to auto-enhance the prompt
    * `referenceImageData: Data?` - Optional reference image data
  * `connection: ConnectionConfig` - Connection settings (default: standard)
  * `media: MediaConfig` - Media settings (default: standard)

**Returns:** `DecartRealtimeManager`

**Throws:** `DecartError` if the signaling URL cannot be constructed

### `DecartRealtimeManager.connect(localStream:)`

Connects to the realtime transformation service.

**Parameters:**

* `localStream: RealtimeMediaStream` - Local media stream with camera video track

**Returns:** `RealtimeMediaStream` — the transformed remote stream

**Throws:** `DecartError` if connection fails or times out

### `DecartRealtimeManager.setPrompt(_:)`

Changes the transformation style.

**Parameters:**

* `prompt: DecartPrompt` - Prompt with text, optional reference image, and enrich flag

### `DecartRealtimeManager.disconnect()`

Closes the connection and cleans up WebRTC resources.

### `DecartRealtimeManager.events`

An `AsyncStream<DecartRealtimeState>` that emits state changes.

### `DecartRealtimeManager.remoteStreamUpdates`

An `AsyncStream<RealtimeMediaStream>` that emits new remote streams after auto-reconnect.

### `LocalVideoTrack.createCameraTrack(name:options:processor:)`

LiveKit factory that opens the camera and produces a `LocalVideoTrack`. Capture starts immediately — there is no separate `startCapture()` call.

**Parameters:**

* `name: String` - Track identifier (e.g. `"video0"`)
* `options: CameraCaptureOptions` - Position, dimensions, fps
* `processor: VideoProcessor?` - Optional `MirroringVideoProcessor` (or any LiveKit `VideoProcessor`)

### `CameraCapturer.switchCameraPosition()`

Toggles between front and back cameras. Access via `videoTrack.capturer as? CameraCapturer`. After switching, update `mirror.cameraPosition = cameraCapturer.position` to keep `MirrorMode.auto` in sync.

### `LocalVideoTrack.stop()`

Async. Stops capture and releases the track. Call before `manager.disconnect()` during cleanup.

## Next Steps

<CardGroup cols={2}>
  <Card title="SDK Overview" icon="swift" href="/sdks/swift">
    Learn about installation, setup, and Swift SDK fundamentals
  </Card>

  <Card title="GitHub" icon="github" href="https://github.com/DecartAI/decart-ios">
    Browse the SDK source code and contribute
  </Card>
</CardGroup>
