Skip to main content

Documentation Index

Fetch the complete documentation index at: https://crsdk.app/llms.txt

Use this file to discover all available pages before exploring further.

React apps usually need three pieces at once:
  • a local route that starts or adopts the camera server
  • a lifecycle layer that handles polling + SSE + reconnect
  • component state that renders camera/session data cleanly
The real TypeScript example app in this repo does not hide all of that behind one giant library call. It keeps the moving pieces explicit in:
  • example_app/src/app/api/server/route.ts
  • example_app/src/lib/event-stream.ts
  • example_app/src/lib/camera-manager.ts
  • example_app/src/app/page.tsx
This recipe shows the same pattern as a reusable hook.

When to use

  • Next.js / React apps that want to embed the server lifecycle
  • Apps that should auto-discover and auto-connect cameras
  • Apps that want typed action helpers without owning raw SSE parsing everywhere

When NOT to use

  • Scripts or CLIs — call the SDK client directly
  • Non-React apps — use the discovery/SSE recipes directly

Hook pattern

// use-camera-session.ts

import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { CameraManager, type ManagedCamera } from "./camera-manager";

type ServerState = "starting" | "running" | "error";

export interface CameraSessionState {
  serverState: ServerState;
  serverError: string | null;
  cameras: ManagedCamera[];
  selectedCameraId: string | null;
}

export interface CameraSessionActions {
  selectCamera: (cameraId: string) => void;
  refreshCameras: () => void;
  disconnectCamera: () => Promise<void>;
}

export function useCameraSession(): CameraSessionState & CameraSessionActions {
  const [serverState, setServerState] = useState<ServerState>("starting");
  const [serverError, setServerError] = useState<string | null>(null);
  const [cameras, setCameras] = useState<ManagedCamera[]>([]);
  const [selectedCameraId, setSelectedCameraId] = useState<string | null>(null);

  const managerRef = useRef<CameraManager | null>(null);
  const selectedCameraIdRef = useRef<string | null>(null);
  selectedCameraIdRef.current = selectedCameraId;

  const syncCameras = useCallback(() => {
    const manager = managerRef.current;
    if (!manager) return;
    const next = manager.getCameras();
    setCameras(next);
  }, []);

  useEffect(() => {
    let cancelled = false;

    (async () => {
      try {
        const response = await fetch("/api/server", { method: "POST" });
        const payload = await response.json();

        if (cancelled) return;
        if (payload.status !== "started" && payload.status !== "already_running") {
          setServerState("error");
          setServerError(payload.message ?? "Failed to start server");
          return;
        }

        const manager = new CameraManager({
          baseUrl: `http://localhost:${payload.port}`,
          autoConnect: true,
          connectionMode: "remote-transfer",
          pollInterval: 5000,
          autoReconnect: true,
        });

        managerRef.current = manager;

        manager.on("camera-found", ({ camera }) => {
          syncCameras();
          if (!selectedCameraIdRef.current) {
            setSelectedCameraId(camera.id);
          }
        });

        manager.on("camera-lost", () => {
          syncCameras();
        });

        manager.on("camera-ready", ({ cameraId }) => {
          syncCameras();
          if (!selectedCameraIdRef.current) {
            setSelectedCameraId(cameraId);
          }
        });

        manager.on("camera-disconnected", () => {
          syncCameras();
        });

        manager.on("connection-failed", () => {
          syncCameras();
        });

        manager.on("error", ({ message }) => {
          setServerError(message);
        });

        await manager.start();
        if (cancelled) return;

        syncCameras();
        setServerState("running");
      } catch (error) {
        if (cancelled) return;
        setServerState("error");
        setServerError(error instanceof Error ? error.message : String(error));
      }
    })();

    return () => {
      cancelled = true;
      managerRef.current?.close();
      managerRef.current = null;
    };
  }, [syncCameras]);

  const disconnectCamera = useCallback(async () => {
    const manager = managerRef.current;
    const cameraId = selectedCameraIdRef.current;
    if (!manager || !cameraId) return;
    await manager.disconnect(cameraId);
    syncCameras();
  }, [syncCameras]);

  return useMemo(
    () => ({
      serverState,
      serverError,
      cameras,
      selectedCameraId,
      selectCamera: setSelectedCameraId,
      refreshCameras: syncCameras,
      disconnectCamera,
    }),
    [serverState, serverError, cameras, selectedCameraId, syncCameras, disconnectCamera]
  );
}

Usage

import { useCameraSession } from "./use-camera-session";

export function CameraSidebar() {
  const {
    serverState,
    serverError,
    cameras,
    selectedCameraId,
    selectCamera,
    refreshCameras,
  } = useCameraSession();

  if (serverState === "starting") return <div>Starting camera server…</div>;
  if (serverState === "error") return <div>Error: {serverError}</div>;

  return (
    <div>
      <button onClick={refreshCameras}>Refresh</button>
      {cameras.map((camera) => (
        <button
          key={camera.info.id}
          onClick={() => selectCamera(camera.info.id)}
          style={{
            display: "block",
            fontWeight: selectedCameraId === camera.info.id ? "bold" : "normal",
          }}
        >
          {camera.info.model}{camera.state}
        </button>
      ))}
    </div>
  );
}

Why this matches the real app

  • The browser layer starts the server through POST /api/server, not by spawning a binary directly in client code.
  • CameraManager owns polling, SSE, connect timing, reconnect, and post-connect setup.
  • React components stay focused on state + rendering, not transport details.
That is the same boundary the repo’s actual Next.js example uses.