# Camera Component

KinesteX Motion Recognition: Real-Time Engagement.

- **Interactive Tracking**: Advanced motion recognition for immersive fitness experiences
- **Real-Time Feedback**: Instantly track reps, spot mistakes, and calculate calories burned
- **Boost Motivation**: Keep users engaged with detailed exercise feedback
- **Custom Integration**: Adapt camera placement to fit your app's design

**Important — what to pass for `currentExercise` and `exercises`:** we **recommend exercise IDs** — they're stable, human-readable, and you already have them when listing exercises from the Content API. Set `exerciseFetchType: "exercise_id"` to use them. The Camera Component also accepts **model IDs** (the default, kept for backward compatibility — but they require an extra round-trip through the Content API to look up) and **exercise titles** (case-sensitive — convenient for prototyping, but title matching depends on locale-normalization and can mismatch similar exercises, so prefer IDs in production). See the **Fetching Exercises** section below.

**Before showing the camera UI**, wait for both `model_warmedup` and `models_loaded` events to fire. See the **Preloading & Events** section below.

**Quick Start**

_Swift (iOS)_
```swift
// Model IDs come from the Content API or admin dashboard.
// See "Camera: Model IDs" subsection.
@State var currentExercise = "3"

kinestex.createCameraView(
    exercises: ["3"], // preload every model ID you may switch to
    currentExercise: $currentExercise,
    user: nil,
    isLoading: $isLoading,
    onMessageReceived: { message in
        switch message {
        case .reps(let value):
            reps = value["value"] as? Int ?? 0
        case .mistake(let value):
            mistake = value["value"] as? String ?? "--"
        default:
            break
        }
    }
)
```

_Kotlin (Android)_
```kotlin
// Model IDs come from the Content API or admin dashboard.
// See "Camera: Model IDs" subsection.
val cameraView = KinesteXSDK.createCameraComponent(
    context = this,
    currentExercise = "3",
    exercises = listOf("3"),
    user = userDetails,
    isLoading = viewModel.isLoading,
    onMessageReceived = { message ->
        when (message) {
            is WebViewMessage.Reps ->
                (message.data["value"] as? Int)?.let { reps = it }
            is WebViewMessage.Mistake ->
                (message.data["value"] as? String)?.let { mistake = it }
            else -> {}
        }
    },
    permissionHandler = this
)
```

_React Native_
```jsx
// Model IDs come from the Content API or admin dashboard.
// See "Camera: Model IDs" subsection.
// postData seeds the SDK with INITIAL values. To change them at runtime,
// call methods on kinestexSDKRef (e.g. changeExercise) — see "Camera: Controls".
const postData: IPostData = {
  key: 'YOUR_API_KEY',
  userId: 'YOUR_USER_ID',
  company: 'YOUR_COMPANY_NAME',
  currentExercise: '3',
  exercises: ['3'], // preload every model ID you may switch to
  style: { style: 'dark' },
};

<KinestexSDK
  ref={kinestexSDKRef}
  data={postData}
  integrationOption={IntegrationOption.CAMERA}
  handleMessage={(type, data) => {
    if (type === 'successful_repeat') setReps(data.value);
    if (type === 'mistake') setMistake(data.value);
  }}
/>
```

_Flutter_
```dart
// Model IDs come from the Content API or admin dashboard.
// See "Camera: Model IDs" subsection.
KinesteXAIFramework.createCameraComponent(
  isShowKinestex: showKinesteX,
  exercises: ["3"],
  currentExercise: "3",
  isLoading: ValueNotifier<bool>(false),
  onMessageReceived: (message) {
    if (message is Reps) {
      setState(() => reps = message.data['value']);
    }
    if (message is Mistake) {
      setState(() => mistake = message.data['value']);
    }
  },
)
```

_HTML / JavaScript_
```html
// Model IDs come from the Content API or admin dashboard.
// See "Camera: Model IDs" subsection.
const postData = {
  // ... your initial fields (key, userId, company, etc.)
  currentExercise: "3",
  exercises: ["3"],
};

const srcURL = "https://ai.kinestex.com/camera";
webView.src = srcURL;
webView.onload = () => sendMessage(postData);

window.addEventListener("message", (event) => {
  if (event.origin !== "https://ai.kinestex.com") return;
  const msg = JSON.parse(event.data);
  if (msg.type === 'successful_repeat') console.log('Rep:', msg.value);
  if (msg.type === 'mistake') console.log('Mistake:', msg.data?.value);
});
```

_React (TypeScript)_
```tsx
// Model IDs come from the Content API or admin dashboard.
// See "Camera: Model IDs" subsection.
import { useRef } from 'react';
import {
  IntegrationOption,
  KinesteXSDK,
  type IPostData,
  type KinesteXSDKCamera,
} from 'kinestex-sdk-react-ts';

const ref = useRef<KinesteXSDKCamera>(null);

// postData seeds the SDK with INITIAL values. To change them at runtime,
// call methods on the ref (e.g. ref.current?.changeExercise) — see "Camera: Controls".
const postData: IPostData = {
  key: 'YOUR_API_KEY',
  userId: 'YOUR_USER_ID',
  company: 'YOUR_COMPANY_NAME',
  currentExercise: '3',
  exercises: ['3'],
  style: { style: 'dark' },
};

<KinesteXSDK
  ref={ref}
  data={postData}
  integrationOption={IntegrationOption.CAMERA}
  handleMessage={(type, data) => {
    if (type === 'successful_repeat') setReps(data.value as number);
    if (type === 'mistake') setMistake(data.value as string);
  }}
/>
```

## Model IDs

> **Recommendation:** prefer **exercise IDs** — they're easier to get (you already have them when listing exercises from the Content API) and don't require an extra round-trip just to look up a numeric model ID. Set `exerciseFetchType: "exercise_id"` and pass `exercise.id` directly. See the [Fetching Exercises](#camera-component-fetching-exercises) section.

Model IDs are still supported (it's the default when `exerciseFetchType` is omitted) and useful when you already have one handy — e.g. from a stored `WorkoutModel.sequence` or the admin dashboard. They are numeric values like `"3"` for Squats or `"394"` for Jumping Jack.

**Three ways to get a model ID:**

1. **Content API** — call `fetchExercises()` (Swift / Kotlin), `fetchContent(contentType: ContentType.exercise)` (Flutter), or `GET /api/v1/exercises` (REST). Each `ExerciseModel` has a `model_id` field. See the [Content API](/docs/content-api) section for full details. *(Note: this is the extra round-trip that exercise IDs let you skip.)*
2. **Admin dashboard** — open the exercise in [admin.kinestex.com](https://admin.kinestex.com); the model ID is shown at the top of the page header.
3. **Workout sequences** — when iterating `WorkoutModel.sequence`, each `ExerciseModel` entry exposes its own `model_id`.

**Fetch a Model ID**

_Swift (iOS)_
```swift
Task {
    let result = await kinestex.fetchExercises(limit: 10)
    if case .success(let response) = result,
       let exercise = response.exercises.first {
        // exercise.modelId is what the Camera Component expects
        currentExercise = exercise.modelId
    }
}
```

_Kotlin (Android)_
```kotlin
lifecycleScope.launch {
    val result = withContext(Dispatchers.IO) {
        KinesteXSDK.api.fetchAPIContentData(
            contentType = ContentType.EXERCISE,
            limit = 10
        )
    }
    if (result is APIContentResult.Exercises) {
        // exercise.modelId is what the Camera Component expects
        currentExercise = result.exercises.firstOrNull()?.modelId ?: ""
    }
}
```

_React Native_
```jsx
const res = await fetch(
  'https://admin.kinestex.com/api/v1/exercises?limit=10',
  { headers: { 'x-api-key': API_KEY, 'x-company-name': COMPANY_NAME } },
);
const { exercises } = await res.json();
// exercise.model_id is what the Camera Component expects.
// Pass it as initial value, or switch later via ref.current?.changeExercise(...).
const modelId = exercises[0].model_id;
```

_Flutter_
```dart
final result = await KinesteXAIFramework.apiService.fetchContent(
  contentType: ContentType.exercise,
  limit: 10,
);
// exercise.modelId is what the Camera Component expects
if (result is ExercisesResult) {
  final modelId = result.response.exercises.first.modelId;
}
```

_HTML / JavaScript_
```html
const res = await fetch(
  'https://admin.kinestex.com/api/v1/exercises?limit=10',
  { headers: { 'x-api-key': API_KEY, 'x-company-name': COMPANY_NAME } },
);
const { exercises } = await res.json();
// exercise.model_id is what the Camera Component expects
const modelId = exercises[0].model_id;
```

_React (TypeScript)_
```tsx
const res = await fetch(
  'https://admin.kinestex.com/api/v1/exercises?limit=10',
  { headers: { 'x-api-key': API_KEY, 'x-company-name': COMPANY_NAME } },
);
const { exercises } = await res.json() as { exercises: ExerciseModel[] };
// exercise.model_id is what the Camera Component expects
const modelId = exercises[0].model_id;
```

## Fetching Exercises (by ID or Title)

Besides model IDs, the Camera Component can fetch exercises by **exercise ID** (recommended) or **exercise title**. Set the `exerciseFetchType` parameter to choose the form:

| `exerciseFetchType` | Meaning | What goes in `exercises` / `currentExercise` |
|---|---|---|
| `"exercise_id"` ✅ **recommended** | Exercise IDs from the Content API — easiest to use, no extra round-trip | e.g. `"squats_v2"` |
| `"model_id"` *(default)* | Numeric model IDs — kept for backward compatibility; requires a Content API lookup to obtain | `"3"`, `"394"` |
| `"exercise_title"` | Exercise titles (case-sensitive) — handy for prototyping, but title matching depends on locale-normalization and can mismatch similar exercises | `"Squats"`, `"Jumping Jack"` |

Omitting `exerciseFetchType` keeps the default model-ID behavior — no migration needed for existing integrations.

**Where to pass it:**
- **React Native (SDK v1.3.1+):** directly in `postData`, alongside `exercises` and `currentExercise`.
- **Swift, Kotlin, Flutter, HTML/JS, React (TypeScript):** inside `customParams` / `customParameters` along with `exercises` and `currentExercise`.

**Keep one form per session:** use the same form for both `exercises` and `currentExercise`, and for any later switches.

**Fetch by Exercise Title**

_Swift (iOS)_
```swift
// exerciseFetchType goes inside customParams
kinestex.createCameraView(
    exercises: ["Squats", "Jumping Jack"],
    currentExercise: $currentExercise, // e.g. "Squats"
    customParams: [
        "exerciseFetchType": "exercise_title" // "model_id" (default) | "exercise_id" | "exercise_title"
    ]
)
```

_Kotlin (Android)_
```kotlin
// exerciseFetchType goes inside customParams
KinesteXSDK.createCameraComponent(
    context = this,
    currentExercise = "Squats",
    exercises = listOf("Squats", "Jumping Jack"),
    customParams = mutableMapOf(
        "exerciseFetchType" to "exercise_title" // "model_id" (default) | "exercise_id" | "exercise_title"
    ),
    permissionHandler = this
)
```

_React Native_
```jsx
// React Native v1.3.1+: exerciseFetchType is direct in postData
const postData: IPostData = {
  key: 'YOUR_API_KEY',
  userId: 'YOUR_USER_ID',
  company: 'YOUR_COMPANY_NAME',
  currentExercise: 'Squats',
  exercises: ['Squats', 'Jumping Jack'],
  exerciseFetchType: 'exercise_title', // "model_id" (default) | "exercise_id" | "exercise_title"
};
```

_Flutter_
```dart
// exerciseFetchType goes inside customParams
KinesteXAIFramework.createCameraComponent(
  isShowKinestex: showKinesteX,
  exercises: ["Squats", "Jumping Jack"],
  currentExercise: "Squats",
  customParams: {
    "exerciseFetchType": "exercise_title", // "model_id" (default) | "exercise_id" | "exercise_title"
  },
  isLoading: ValueNotifier<bool>(false),
  onMessageReceived: handleWebViewMessage,
)
```

_HTML / JavaScript_
```html
// exerciseFetchType goes inside customParams
const postData = {
  // ... your initial fields (key, userId, company, etc.)
  currentExercise: "Squats",
  exercises: ["Squats", "Jumping Jack"],
  customParams: {
    exerciseFetchType: "exercise_title", // "model_id" (default) | "exercise_id" | "exercise_title"
  },
};
```

_React (TypeScript)_
```tsx
// exerciseFetchType goes inside customParameters
const postData: IPostData = {
  key: 'YOUR_API_KEY',
  userId: 'YOUR_USER_ID',
  company: 'YOUR_COMPANY_NAME',
  currentExercise: 'Squats',
  exercises: ['Squats', 'Jumping Jack'],
  customParameters: {
    exerciseFetchType: 'exercise_title', // "model_id" (default) | "exercise_id" | "exercise_title"
  },
};
```

## Loading More Exercises at Runtime (React Native v1.3.1+)

**Availability:** React Native SDK `v1.3.1+` only. Other platforms must pass the full `exercises` list at initialization.

In React Native you can fetch and cache **additional** exercise models **after the session has started**, without re-mounting the camera. This is done by calling `sendAction` with the `"load_models"` action and an `extras` object containing the new identifiers.

**Three-step flow:**

1. Send the `load_models` action with the identifiers you want to add.
2. Wait for the `models_loaded` event — `data.modelIds` echoes the identifiers that resolved.
3. Switch the active exercise with `changeExercise(...)` (do **not** repeat `exerciseFetchType`).

If `exercises` is missing or empty, the SDK posts back `{ "type": "error_occurred", "message": "load_models: no exercises provided" }`. Per-identifier failures arrive as separate `error_occurred` messages; any identifiers that did resolve are still listed in `modelIds` and are switchable.

**Load More Models, Then Switch**

_React Native_
```jsx
// Step 1 — fetch additional models at runtime
sdkRef.current?.sendAction(
  "workout_activity_action",
  "load_models",
  {
    exercises: ["Jumping Jack", "Lunges"],
    exerciseFetchType: "exercise_title", // match the form you used initially
  }
);

// Step 2 — wait for models_loaded, then switch
const handleMessage = (type: string, data: { [key: string]: any }) => {
  if (type === "models_loaded" && data.modelIds?.includes("Jumping Jack")) {
    // Step 3 — switch the active exercise. Do NOT repeat exerciseFetchType here.
    sdkRef.current?.changeExercise("Jumping Jack");
  }
  if (type === "error_occurred") {
    console.warn("KinesteX error:", data.message);
  }
};
```

## Preloading & Events

Two events fire as the component initializes. Wait for **both** before revealing the camera UI to the user.

| Event | Meaning |
|---|---|
| `model_warmedup` | Pose-tracking (MediaPipe) model is ready |
| `models_loaded` | All exercise models in `exercises` have downloaded |

**Pattern:** mount the camera hidden (e.g. `opacity: 0`) with a loader on top. Reveal once both events have fired.

**Wait for Both Events**

_Swift (iOS)_
```swift
@State private var modelWarmedUp = false
@State private var modelsLoaded = false
var isReady: Bool { modelWarmedUp && modelsLoaded }

kinestex.createCameraView(
    exercises: ["3"],
    currentExercise: $currentExercise,
    user: nil,
    isLoading: $isLoading,
    onMessageReceived: { message in
        if case .custom_type(let value) = message,
           let type = value["type"] as? String {
            if type == "model_warmedup" { modelWarmedUp = true }
            if type == "models_loaded"  { modelsLoaded  = true }
        }
    }
)
.opacity(isReady ? 1 : 0)
```

_Kotlin (Android)_
```kotlin
var modelWarmedUp = false
var modelsLoaded = false

val cameraView = KinesteXSDK.createCameraComponent(
    context = this,
    currentExercise = "3",
    exercises = listOf("3"),
    user = null,
    isLoading = viewModel.isLoading,
    onMessageReceived = { message ->
        if (message is WebViewMessage.CustomType) {
            when (message.data["type"] as? String) {
                "model_warmedup" -> modelWarmedUp = true
                "models_loaded"  -> modelsLoaded  = true
            }
            cameraView.alpha = if (modelWarmedUp && modelsLoaded) 1f else 0f
        }
    },
    permissionHandler = this
)
```

_React Native_
```jsx
const [modelWarmedUp, setModelWarmedUp] = useState(false);
const [modelsLoaded, setModelsLoaded] = useState(false);
const isReady = modelWarmedUp && modelsLoaded;

<View style={{ opacity: isReady ? 1 : 0 }}>
  <KinestexSDK
    data={postData}
    integrationOption={IntegrationOption.CAMERA}
    handleMessage={(type) => {
      if (type === 'model_warmedup') setModelWarmedUp(true);
      if (type === 'models_loaded')  setModelsLoaded(true);
    }}
  />
</View>
```

_Flutter_
```dart
bool modelWarmedUp = false;
bool modelsLoaded  = false;

KinesteXAIFramework.createCameraComponent(
  isShowKinestex: showKinesteX,
  exercises: ["3"],
  currentExercise: "3",
  isLoading: ValueNotifier<bool>(false),
  onMessageReceived: (message) {
    // Model-readiness events arrive as CustomType; the name is in data['type']
    if (message is CustomType) {
      if (message.data['type'] == 'model_warmedup') {
        setState(() => modelWarmedUp = true);
      }
      if (message.data['type'] == 'models_loaded') {
        setState(() => modelsLoaded = true);
      }
    }
  },
)
```

_HTML / JavaScript_
```html
let modelWarmedUp = false, modelsLoaded = false;
const reveal = () => {
  if (modelWarmedUp && modelsLoaded) iframe.style.opacity = '1';
};

iframe.style.opacity = '0';
window.addEventListener("message", (event) => {
  if (event.origin !== "https://ai.kinestex.com") return;
  const msg = JSON.parse(event.data);
  if (msg.type === 'model_warmedup') { modelWarmedUp = true; reveal(); }
  if (msg.type === 'models_loaded')  { modelsLoaded  = true; reveal(); }
});
```

_React (TypeScript)_
```tsx
const [modelWarmedUp, setModelWarmedUp] = useState(false);
const [modelsLoaded, setModelsLoaded] = useState(false);
const isReady = modelWarmedUp && modelsLoaded;

<div style={{ opacity: isReady ? 1 : 0 }}>
  <KinesteXSDK
    data={postData}
    integrationOption={IntegrationOption.CAMERA}
    handleMessage={(type) => {
      if (type === 'model_warmedup') setModelWarmedUp(true);
      if (type === 'models_loaded')  setModelsLoaded(true);
    }}
  />
</div>
```

## Controls

**Switching exercises in real time.** Set `currentExercise` to any model ID from the `exercises` array — the camera component swaps tracking immediately.

**Control commands.** Send any of these strings as `currentExercise` to control the session:

| Command | Effect |
|---|---|
| `"Pause Exercise"` | Pauses motion tracking; rep counter freezes |
| `"Pause Audio"` | Mutes voice feedback |
| `"Resume Audio"` | Re-enables voice feedback |
| `"Workout Overview"` | Triggers a summary snapshot for the current session |
| `"Stop Camera"` | ⚠️ **Destructive** — releases camera + models, fires `stop_camera`. Not recoverable without re-mounting the component (see warning below) |

To resume tracking after a pause, set `currentExercise` back to a real model ID from `exercises`.

> ⚠️ **`"Stop Camera"` is destructive — avoid it for normal flows.** It tears down the camera, MediaPipe, and all loaded exercise models, then fires the `stop_camera` event. **You cannot recover from it without unmounting and re-creating the entire camera component.** Only send it when the user is permanently leaving the camera screen. For temporary pauses, use `"Pause Exercise"` instead.

**Switch Exercise and Send Control Commands**

_Swift (iOS)_
```swift
// Switch exercise (Swift uses two-way binding via @State)
currentExercise = "394"

// Pause / resume tracking
currentExercise = "Pause Exercise"
currentExercise = "3"
```

_Kotlin (Android)_
```kotlin
// Switch exercise
KinesteXSDK.updateCurrentExercise("394")

// Pause / resume tracking
KinesteXSDK.updateCurrentExercise("Pause Exercise")
KinesteXSDK.updateCurrentExercise("3")
```

_React Native_
```jsx
// Switch exercise
kinestexSDKRef.current?.changeExercise("394");

// Pause / resume tracking
kinestexSDKRef.current?.changeExercise("Pause Exercise");
kinestexSDKRef.current?.changeExercise("3");
```

_Flutter_
```dart
// All controls go through your updateExercise ValueNotifier
updateExercise.value = "394";
updateExercise.value = "Pause Exercise";
updateExercise.value = "3";
```

_HTML / JavaScript_
```html
// Switch exercise
webView.contentWindow.postMessage(
  { currentExercise: "394" },
  srcURL
);

// Pause / resume tracking
webView.contentWindow.postMessage({ currentExercise: "Pause Exercise" }, srcURL);
webView.contentWindow.postMessage({ currentExercise: "3" }, srcURL);
```

_React (TypeScript)_
```tsx
// Switch exercise
ref.current?.changeExercise("394");

// Pause / resume tracking
ref.current?.changeExercise("Pause Exercise");
ref.current?.changeExercise("3");
```

## Customization

Pass any of these fields in `customParams` (Swift / Kotlin / Flutter) or directly in `postData` (HTML/JS, React Native, React TS) at initialization.

| Field | Type | Description |
|---|---|---|
| `restSpeeches` | `string[]` | Audio phrases to preload (from `ExerciseModel.rest_speech`) |
| `videoURL` | `string` | Use a video file instead of the live camera |
| `landmarkColor` | `string` | Pose overlay color in hex with `#` (default `#14FF00`) |
| `showSilhouette` | `boolean` | Show "get into frame" silhouette (default `true`) |
| `includeRealtimeAccuracy` | `boolean` | (Beta) Stream live position-confidence alongside reps |
| `includePoseData` | `string[]` | Any of `"angles"`, `"poseLandmarks"`, `"worldLandmarks"`. **Performance impact** — only enable for custom calculations |

## Event Reference

All events the Camera Component emits to the host app:

| Event | Payload | When |
|---|---|---|
| `model_warmedup` | `{ message }` | Pose model is ready |
| `models_loaded` | `{ message }` | All exercise models in `exercises` finished downloading |
| `person_in_frame` | `{ message }` | User entered the silhouette frame |
| `successful_repeat` | `{ exercise, value, accuracy }` | A rep was counted (`value` = total reps so far) |
| `mistake` | `{ value }` | Form mistake detected |
| `correct_position_accuracy` | `{ accuracy }` | (Beta) Live position confidence — only when `includeRealtimeAccuracy: true` |
| `pose_landmarks` | `{ poseLandmarks }` | Per-frame screen-space landmarks — only when `includePoseData` includes `"poseLandmarks"` |
| `world_landmarks` | `{ worldLandmarks }` | Per-frame world-space landmarks — only when `includePoseData` includes `"worldLandmarks"` |
| `speech_fetch_complete` | `{ successCount, failureCount }` | All `restSpeeches` finished loading |
| `error_occurred` | `{ message }` or `{ data, error }` | Any error (model fetch fail, phrase fail, etc.) |
| `warning` | `{ data }` | Non-fatal config issue (e.g. no model IDs provided) |
| `stop_camera` | `{ message }` | Confirms the `"Stop Camera"` command finished |

## Pose Data

When `includePoseData` contains `"poseLandmarks"` or `"worldLandmarks"`, the camera emits per-frame events with raw skeleton data. **Only enable this if you're doing custom calculations — there is a performance cost.**

**Two coordinate spaces:**

- `poseLandmarks` — values 0–1, normalized to the camera frame.
- `worldLandmarks` — meters, relative to the hips (best Z accuracy).

Each landmark has `{ x, y, z, visibility }` (all 0–1).

**Available landmarks** (same names in both spaces): `nose`, `leftEyeInner`, `leftEye`, `leftEyeOuter`, `rightEyeInner`, `rightEye`, `rightEyeOuter`, `leftEar`, `rightEar`, `mouthLeft`, `mouthRight`, `leftShoulder`, `rightShoulder`, `leftElbow`, `rightElbow`, `leftWrist`, `rightWrist`, `leftPinky`, `rightPinky`, `leftIndex`, `rightIndex`, `leftThumb`, `rightThumb`, `leftHip`, `rightHip`, `leftKnee`, `rightKnee`, `leftAnkle`, `rightAnkle`, `leftHeel`, `rightHeel`, `leftFootIndex`, `rightFootIndex`.

**Available angles** (when `"angles"` is included — both 2D and 3D versions are emitted): `leftKneeAngle`, `rightKneeAngle`, `leftHipAngle`, `rightHipAngle`, `leftShoulderAngle`, `rightShoulderAngle`, `leftElbowAngle`, `rightElbowAngle`, `leftWristAngle`, `rightWristAngle`, `leftAnkleAngle`, `rightAnkleAngle`, `leftArmpitAngle`, `rightArmpitAngle`.

## Complete Example

Minimal working example with **Next** / **Previous** buttons that cycle between exercises and a live rep counter in the UI.

**Complete Implementation**

_Swift (iOS)_
```swift
import SwiftUI
import KinesteXAIKit

struct CameraScreen: View {
    let kinestex = KinesteXAIKit(
        apiKey: "YOUR_API_KEY",
        companyName: "YOUR_COMPANY_NAME",
        userId: "YOUR_USER_ID"
    )

    // 3 = Squats, 394 = Jumping Jack
    let exerciseIds = ["3", "394"]

    @State private var index = 0
    @State private var currentExercise = "3"
    @State private var reps = 0
    @State private var isLoading = false

    var body: some View {
        VStack(spacing: 16) {
            Text("Reps: \(reps)")
                .font(.title)
                .padding(.top)

            kinestex.createCameraView(
                exercises: exerciseIds,
                currentExercise: $currentExercise,
                user: nil,
                isLoading: $isLoading,
                onMessageReceived: { message in
                    if case .reps(let value) = message {
                        reps = value["value"] as? Int ?? 0
                    }
                }
            )

            HStack(spacing: 24) {
                Button("Previous") { switchTo(index - 1) }
                Button("Next")     { switchTo(index + 1) }
            }
            .padding(.bottom)
        }
    }

    private func switchTo(_ newIndex: Int) {
        index = (newIndex + exerciseIds.count) % exerciseIds.count
        currentExercise = exerciseIds[index]
        reps = 0
    }
}
```

_Kotlin (Android)_
```kotlin
import android.Manifest
import android.os.Bundle
import android.widget.Button
import android.widget.LinearLayout
import android.widget.TextView
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import com.kinestex.kinestexsdkkotlin.GenericWebView
import com.kinestex.kinestexsdkkotlin.KinesteXSDK
import com.kinestex.kinestexsdkkotlin.PermissionHandler
import com.kinestex.kinestexsdkkotlin.WebViewMessage
import kotlinx.coroutines.flow.MutableStateFlow

class CameraActivity : AppCompatActivity(), PermissionHandler {
    // 3 = Squats, 394 = Jumping Jack
    private val exerciseIds = listOf("3", "394")
    private var index = 0
    private val isLoading = MutableStateFlow(false)
    private lateinit var camera: GenericWebView
    private lateinit var tvReps: TextView

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_camera)

        tvReps = findViewById(R.id.tvReps)

        camera = KinesteXSDK.createCameraComponent(
            context = this,
            currentExercise = exerciseIds[0],
            exercises = exerciseIds,
            user = null,
            isLoading = isLoading,
            onMessageReceived = { msg ->
                if (msg is WebViewMessage.Reps) {
                    val v = msg.data["value"] as? Int ?: 0
                    runOnUiThread { tvReps.text = "Reps: $v" }
                }
            },
            permissionHandler = this
        ) as GenericWebView
        findViewById<LinearLayout>(R.id.cameraContainer).addView(camera)

        findViewById<Button>(R.id.btnPrev).setOnClickListener { switchTo(index - 1) }
        findViewById<Button>(R.id.btnNext).setOnClickListener { switchTo(index + 1) }
    }

    private fun switchTo(newIndex: Int) {
        index = (newIndex + exerciseIds.size) % exerciseIds.size
        KinesteXSDK.updateCurrentExercise(exerciseIds[index])
        runOnUiThread { tvReps.text = "Reps: 0" }
    }

    override fun requestCameraPermission() {
        registerForActivityResult(
            ActivityResultContracts.RequestPermission()
        ) { granted -> camera.handlePermissionResult(granted) }
            .launch(Manifest.permission.CAMERA)
    }
}
```

_React Native_
```jsx
import { useRef, useState } from 'react';
import { View, Text, Button } from 'react-native';
import KinestexSDK from 'kinestex-sdk-react-native';
import {
  IntegrationOption,
  KinesteXSDKCamera,
  IPostData,
} from 'kinestex-sdk-react-native/src/types';

// 3 = Squats, 394 = Jumping Jack
const exerciseIds = ['3', '394'];

export default function CameraScreen() {
  const ref = useRef<KinesteXSDKCamera>(null);
  const [index, setIndex] = useState(0);
  const [reps, setReps] = useState(0);

  const postData: IPostData = {
    key: 'YOUR_API_KEY',
    userId: 'YOUR_USER_ID',
    company: 'YOUR_COMPANY_NAME',
    currentExercise: exerciseIds[0],
    exercises: exerciseIds,
    style: { style: 'dark' },
  };

  const switchTo = (newIndex: number) => {
    const wrapped = (newIndex + exerciseIds.length) % exerciseIds.length;
    setIndex(wrapped);
    setReps(0);
    ref.current?.changeExercise(exerciseIds[wrapped]);
  };

  return (
    <View style={{ flex: 1 }}>
      <Text style={{ fontSize: 24, padding: 16 }}>Reps: {reps}</Text>
      <View style={{ flex: 1 }}>
        <KinestexSDK
          ref={ref}
          data={postData}
          integrationOption={IntegrationOption.CAMERA}
          handleMessage={(type, data) => {
            if (type === 'successful_repeat') setReps(data.value);
          }}
        />
      </View>
      <View style={{ flexDirection: 'row', justifyContent: 'space-around', padding: 16 }}>
        <Button title="Previous" onPress={() => switchTo(index - 1)} />
        <Button title="Next"     onPress={() => switchTo(index + 1)} />
      </View>
    </View>
  );
}
```

_Flutter_
```dart
import 'package:flutter/material.dart';
import 'package:kinestex_sdk_flutter/kinestex_sdk.dart';

class CameraScreen extends StatefulWidget {
  const CameraScreen({super.key});
  @override
  State<CameraScreen> createState() => _CameraScreenState();
}

class _CameraScreenState extends State<CameraScreen> {
  // 3 = Squats, 394 = Jumping Jack
  final exerciseIds = const ['3', '394'];

  int index = 0;
  int reps = 0;
  final showKinesteX = ValueNotifier<bool>(true);
  final updateExercise = ValueNotifier<String?>('3');

  void switchTo(int newIndex) {
    final wrapped = (newIndex + exerciseIds.length) % exerciseIds.length;
    setState(() {
      index = wrapped;
      reps = 0;
    });
    updateExercise.value = exerciseIds[wrapped];
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Column(
        children: [
          Padding(
            padding: const EdgeInsets.all(16),
            child: Text('Reps: $reps', style: const TextStyle(fontSize: 24)),
          ),
          Expanded(
            child: ValueListenableBuilder<String?>(
              valueListenable: updateExercise,
              builder: (context, value, _) {
                return KinesteXAIFramework.createCameraComponent(
                  isShowKinestex: showKinesteX,
                  exercises: exerciseIds,
                  currentExercise: value ?? exerciseIds[0],
                  updatedExercise: value,
                  isLoading: ValueNotifier<bool>(false),
                  onMessageReceived: (m) {
                    if (m is Reps) {
                      setState(() => reps = m.data['value'] ?? 0);
                    }
                  },
                );
              },
            ),
          ),
          Padding(
            padding: const EdgeInsets.all(16),
            child: Row(
              mainAxisAlignment: MainAxisAlignment.spaceAround,
              children: [
                ElevatedButton(
                  onPressed: () => switchTo(index - 1),
                  child: const Text('Previous'),
                ),
                ElevatedButton(
                  onPressed: () => switchTo(index + 1),
                  child: const Text('Next'),
                ),
              ],
            ),
          ),
        ],
      ),
    );
  }
}
```

_HTML / JavaScript_
```html
<!doctype html>
<html>
  <body style="margin:0;display:flex;flex-direction:column;height:100vh;">
    <h2 id="reps" style="padding:16px;margin:0;">Reps: 0</h2>
    <iframe
      id="camera"
      src="https://ai.kinestex.com/camera"
      style="flex:1;border:0;"
      allow="camera"
    ></iframe>
    <div style="display:flex;justify-content:space-around;padding:16px;">
      <button id="prev">Previous</button>
      <button id="next">Next</button>
    </div>

    <script>
      // 3 = Squats, 394 = Jumping Jack
      const exerciseIds = ["3", "394"];
      let index = 0;

      const camera = document.getElementById("camera");
      const repsEl = document.getElementById("reps");
      const srcURL = "https://ai.kinestex.com/camera";

      const postData = {
        key: "YOUR_API_KEY",
        userId: "YOUR_USER_ID",
        company: "YOUR_COMPANY_NAME",
        currentExercise: exerciseIds[0],
        exercises: exerciseIds,
      };

      camera.onload = () => camera.contentWindow.postMessage(postData, srcURL);

      window.addEventListener("message", (event) => {
        if (event.origin !== "https://ai.kinestex.com") return;
        const msg = JSON.parse(event.data);
        if (msg.type === "successful_repeat") {
          repsEl.textContent = "Reps: " + msg.value;
        }
      });

      function switchTo(newIndex) {
        index = (newIndex + exerciseIds.length) % exerciseIds.length;
        repsEl.textContent = "Reps: 0";
        camera.contentWindow.postMessage(
          { currentExercise: exerciseIds[index] },
          srcURL
        );
      }

      document.getElementById("prev").onclick = () => switchTo(index - 1);
      document.getElementById("next").onclick = () => switchTo(index + 1);
    </script>
  </body>
</html>
```

_React (TypeScript)_
```tsx
import { useRef, useState } from 'react';
import {
  IntegrationOption,
  KinesteXSDK,
  type IPostData,
  type KinesteXSDKCamera,
} from 'kinestex-sdk-react-ts';

// 3 = Squats, 394 = Jumping Jack
const exerciseIds = ['3', '394'];

export default function CameraScreen() {
  const ref = useRef<KinesteXSDKCamera>(null);
  const [index, setIndex] = useState(0);
  const [reps, setReps] = useState(0);

  const postData: IPostData = {
    key: 'YOUR_API_KEY',
    userId: 'YOUR_USER_ID',
    company: 'YOUR_COMPANY_NAME',
    currentExercise: exerciseIds[0],
    exercises: exerciseIds,
    style: { style: 'dark' },
  };

  const switchTo = (newIndex: number) => {
    const wrapped = (newIndex + exerciseIds.length) % exerciseIds.length;
    setIndex(wrapped);
    setReps(0);
    ref.current?.changeExercise(exerciseIds[wrapped]);
  };

  return (
    <div style={{ display: 'flex', flexDirection: 'column', height: '100vh' }}>
      <h2 style={{ padding: 16, margin: 0 }}>Reps: {reps}</h2>
      <div style={{ flex: 1 }}>
        <KinesteXSDK
          ref={ref}
          data={postData}
          integrationOption={IntegrationOption.CAMERA}
          handleMessage={(type, data) => {
            if (type === 'successful_repeat') setReps(data.value as number);
          }}
        />
      </div>
      <div style={{ display: 'flex', justifyContent: 'space-around', padding: 16 }}>
        <button onClick={() => switchTo(index - 1)}>Previous</button>
        <button onClick={() => switchTo(index + 1)}>Next</button>
      </div>
    </div>
  );
}
```

---
Source: https://www.kinestex.com/docs/integration/camera-component · Index: https://www.kinestex.com/llms.txt
