> ## Documentation Index
> Fetch the complete documentation index at: https://api-docs.rhombus.community/llms.txt
> Use this file to discover all available pages before exploring further.

# Superposición de detección en tiempo real por LAN

> Analiza los cuadros delimitadores de IA incrustados en el stream WebSocket H.264 por LAN y renderízalos sobre el video en vivo usando el React SDK de Rhombus o tu propio cliente.

<Note>
  Esta página fue traducida automáticamente. Si encuentra errores o tiene sugerencias, [contáctenos](mailto:support@rhombus.com).
</Note>

## Descripción general

El stream de video WebSocket del agente LAN incrusta los resultados de detección de IA directamente en el encabezado de encapsulación H.264. Cuando el pipeline de inferencia en la cámara produce una nueva detección, esta se inserta como un campo TLV en el siguiente frame de video saliente del mismo WebSocket — sin un canal de detección separado.

Esta guía cubre:

* El formato de encapsulación TLV y el esquema JSON dentro del campo `AI_DETECTIONS`
* La conexión al WebSocket H.264 por LAN en vivo y la lectura tanto de los frames binarios como del mensaje de texto de inicialización
* El dibujo de cuadros de detección sobre [`RhombusRealtimePlayer`](/es/implementations/react-sdk) usando un WebSocket paralelo solo de detección
* Una referencia de parser desde cero para consumidores que no usan React

<Note>
  Si solo necesitas un reproductor, incrusta [`RhombusRealtimePlayer`](/es/implementations/react-sdk) — maneja la autenticación, la decodificación con WebCodecs y la negociación de resolución. Esta guía es para agregar una capa de superposición de detección encima, o para clientes que no usan el React SDK.
</Note>

## Conexión al stream en tiempo real por LAN

### Obtener la URL del WebSocket

Llama a `POST /api/camera/getMediaUris` y lee:

* `lanLiveH264Uris` (array de strings) — URLs de LAN, cuando el cliente y la cámara comparten una red
* `wanLiveH264Uri` (string) — URL de WAN, enrutada a través de Rhombus

Para la variante de menor resolución, cambia `/ws` por `/wsl` en la ruta.

### Autenticar

Ambos modos usan un token de sesión federada generado en tu backend mediante `POST /api/org/generateFederatedSessionToken`. Nunca pongas tu API key en el código del navegador.

| Modo | Método de autenticación                                                                             |
| ---- | --------------------------------------------------------------------------------------------------- |
| WAN  | Agrega `?x-auth-scheme=federated-token&x-auth-ft=<TOKEN>` a la URL antes de abrir el WebSocket.     |
| LAN  | Establece una cookie `RFT=<TOKEN>` con alcance al dominio de la cámara antes de abrir el WebSocket. |

El ejemplo completo de backend para generar tokens (Express, FastAPI, Next.js) está en la [guía del React SDK](/es/implementations/react-sdk#configuración-del-backend) — reutilízalo.

## Qué envía el servidor

Inmediatamente después de la actualización del WebSocket y antes de cualquier frame binario, el servidor envía un único mensaje de **texto** que describe el stream:

```json theme={null}
{"action":"init","width":1920,"height":1080,"codec":"h264","framerate":15}
```

Lee las dimensiones si tu renderizador necesita la resolución de origen. Los cuadros delimitadores son independientes de la resolución (unidades permyriad), por lo que la mayoría de las superposiciones no lo necesitan.

Después del mensaje de inicialización, cada mensaje posterior es un frame **binario** que contiene el encabezado de encapsulación codificado en TLV seguido de los datos NAL H.264 sin procesar.

## Encabezado de encapsulación (formato TLV)

Cada mensaje binario contiene una secuencia de TLVs. Cada TLV usa el mismo formato de cable:

```text theme={null}
[1 byte type] [3 bytes length, big-endian] [N bytes value]
```

### Tipos de TLV

|   Type | Name             | Value             | Notas                                                                                                                                          |
| -----: | ---------------- | ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| `0x00` | `SPS_PPS_IFRAME` | H.264 NAL data    | Keyframe (SPS/PPS/I-frame). Siempre es el último TLV del mensaje.                                                                              |
| `0x01` | `NON_IFRAME`     | H.264 NAL data    | Frame delta (P/B). Siempre es el último TLV del mensaje.                                                                                       |
| `0x02` | `TIMESTAMP`      | 8-byte uint64 BE  | Hora del reloj de pared del servidor en **milisegundos**.                                                                                      |
| `0x03` | `PTS_US`         | 8-byte uint64 BE  | PTS de terceros en **microsegundos**. Opcional; se usa para el reordenamiento de B-frames.                                                     |
| `0x04` | `AI_DETECTIONS`  | UTF-8 JSON string | Nuevas detecciones de IA. Presente solo cuando hay un nuevo resultado de inferencia disponible. No termina en null — usa el campo de longitud. |

### Disposición de cable

```text theme={null}
┌──────────────────────────────────────────────────┐
│ TIMESTAMP (0x02): 4 + 8 = 12 bytes               │  always present
├──────────────────────────────────────────────────┤
│ PTS_US (0x03): 4 + 8 = 12 bytes                  │  optional
├──────────────────────────────────────────────────┤
│ AI_DETECTIONS (0x04): 4 + N bytes                │  only when a new
│                                                  │  detection is available
├──────────────────────────────────────────────────┤
│ frame-data (0x00 or 0x01): 4 + N bytes           │  always last;
│                                                  │  value is raw H.264
└──────────────────────────────────────────────────┘
```

El TLV de datos de frame (`0x00` o `0x01`) es **siempre la última entrada** — el codificador del agente LAN inserta explícitamente los TLVs de metadatos antes de la entrada de frame. Un parser seguro deja de recorrer los TLVs en cuanto encuentra un tipo de datos de frame.

## Análisis del encabezado de encapsulación

Recorre los campos TLV hasta que llegues al tipo `0x00` o `0x01` (la entrada de datos de frame):

```typescript theme={null}
type ParsedFrame = {
  timestampMs: number | null;
  ptsUs: number | null;
  detectionJson: string | null;
  isKeyframe: boolean;
  h264Data: Uint8Array;
};

export function parseEncapHeader(buffer: ArrayBuffer): ParsedFrame {
  const view = new DataView(buffer);
  const bytes = new Uint8Array(buffer);
  let offset = 0;
  let timestampMs: number | null = null;
  let ptsUs: number | null = null;
  let detectionJson: string | null = null;

  while (offset + 4 <= buffer.byteLength) {
    const type = view.getUint8(offset);
    const len =
      (view.getUint8(offset + 1) << 16) |
      (view.getUint8(offset + 2) << 8) |
      view.getUint8(offset + 3);
    const valueStart = offset + 4;

    if (type === 0x00 || type === 0x01) {
      return {
        timestampMs,
        ptsUs,
        detectionJson,
        isKeyframe: type === 0x00,
        h264Data: bytes.subarray(valueStart, valueStart + len),
      };
    }

    if (type === 0x02) {
      const hi = view.getUint32(valueStart);
      const lo = view.getUint32(valueStart + 4);
      timestampMs = hi * 0x100000000 + lo;
    } else if (type === 0x03) {
      const hi = view.getUint32(valueStart);
      const lo = view.getUint32(valueStart + 4);
      ptsUs = hi * 0x100000000 + lo;
    } else if (type === 0x04) {
      detectionJson = new TextDecoder().decode(
        bytes.subarray(valueStart, valueStart + len)
      );
    }
    // Unknown types are skipped silently.

    offset = valueStart + len;
  }

  throw new Error("Encapsulation header missing frame-data TLV");
}
```

El React SDK de Rhombus usa un parser equivalente en [`parseRhombusH264Binary.ts`](https://github.com/RhombusSystems/rhombus-react-sdk/blob/main/src/stream/parseRhombusH264Binary.ts) — la referencia canónica del lado del cliente.

## Esquema JSON de detección

`AI_DETECTIONS` transporta un array JSON de objetos de detección. Todas las detecciones de una misma inferencia comparten el mismo `ts`.

### Campos obligatorios

| Campo  | Tipo    | Unidades            | Descripción                                                                                                |
| ------ | ------- | ------------------- | ---------------------------------------------------------------------------------------------------------- |
| `t`    | int     | enum                | Tipo de detección. `0` Human, `1` Vehicle, `2` Face, `3` License Plate (LPR), `4` Pose, `5` CLIP Embedding |
| `c`    | int     | permyriad (0–10000) | Confianza. Divide entre 100 para obtener el porcentaje.                                                    |
| `id`   | int     | —                   | Id de objeto del rastreador. Estable entre frames para el mismo objeto rastreado.                          |
| `b`    | int\[4] | permyriad           | Cuadro delimitador `[left, top, right, bottom]`                                                            |
| `ts`   | int     | ms epoch            | Timestamp del frame que analizó el pipeline de IA. Úsalo para una alineación precisa por frame.            |
| `uuid` | string  | RUUID               | UUID del evento padre                                                                                      |
| `rs`   | float   | segundos            | Timestamp en segundos relativos dentro del evento                                                          |

### Campos opcionales

| Campo             | Tipo    | Notas                                                                                                                                                                                                       |
| ----------------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `clr`             | object  | Histograma de color. Las claves son nombres de color (p. ej. `"red"`, `"blue"`); los valores son permyriad.                                                                                                 |
| `tight_crop_xxyy` | int\[4] | Bbox ajustado dentro de la ventana de recorte de la detección: `[x_min, x_max, y_min, y_max]` (permyriad). Útil cuando el consumidor quiere un cuadro más ajustado que la ventana de detección con relleno. |
| `ec`              | int     | Confianza del embedding (permyriad), presente cuando se calcula un embedding.                                                                                                                               |
| `et`              | string  | Identificador del tipo de embedding.                                                                                                                                                                        |
| `e`               | string  | Vector de embedding (codificado como string; la longitud depende del tipo).                                                                                                                                 |
| `il`              | string  | Referencia de localizador de imagen para el recorte de la detección.                                                                                                                                        |

### Ejemplo

```json theme={null}
[
  {
    "t": 0,
    "c": 8500,
    "id": 3,
    "b": [1200, 3400, 4500, 8900],
    "ts": 1715030400000,
    "uuid": "AAAAAAAAAAAAAAAAAAAAAA",
    "rs": 2.5,
    "clr": {"red": 4000, "blue": 6000},
    "tight_crop_xxyy": [1500, 4200, 3700, 8500]
  }
]
```

<Note>
  **Análisis compatible hacia adelante.** Las futuras versiones de firmware agregarán texto de LPR (`lp_chars`, `lp_confidence`), esqueletos de pose (`pose_permyriad_points` — de 38 articulaciones, **no** el conjunto COCO de 17 articulaciones) y embeddings de reidentificación. Trata todos los campos no reconocidos como opcionales e ignora las claves desconocidas, para que tu cliente siga funcionando cuando esos campos lleguen.
</Note>

## Dibujo de cuadros delimitadores en un canvas

Las coordenadas de los cuadros delimitadores son permyriad (0–10000) e independientes de la resolución. Conviértelas a píxeles usando las dimensiones del canvas:

```javascript theme={null}
const TYPE_COLORS = {
  0: "#00ff00", // Human    — green
  1: "#0088ff", // Vehicle  — blue
  2: "#ff00ff", // Face     — magenta
  3: "#ffff00", // LPR      — yellow
  4: "#00ffff", // Pose     — cyan
  5: "#ff8800", // CLIP     — orange
};

const TYPE_LABELS = ["Human", "Vehicle", "Face", "LPR", "Pose", "CLIP"];

export function drawDetections(ctx, canvasWidth, canvasHeight, detections) {
  ctx.clearRect(0, 0, canvasWidth, canvasHeight);

  for (const det of detections) {
    const [left, top, right, bottom] = det.b;
    const x = (left / 10000) * canvasWidth;
    const y = (top / 10000) * canvasHeight;
    const w = ((right - left) / 10000) * canvasWidth;
    const h = ((bottom - top) / 10000) * canvasHeight;

    ctx.strokeStyle = TYPE_COLORS[det.t] ?? "#ffffff";
    ctx.lineWidth = 2;
    ctx.strokeRect(x, y, w, h);

    const conf = Math.round(det.c / 100);
    const label = `${TYPE_LABELS[det.t] ?? "Unknown"} ${conf}% #${det.id}`;
    ctx.fillStyle = ctx.strokeStyle;
    ctx.font = "12px monospace";
    ctx.fillText(label, x, Math.max(10, y - 4));
  }
}
```

Para un canvas de 1280×720 y `b: [1200, 3400, 4500, 8900]`, esto produce `(x=153.6, y=244.8, w=422.4, h=396.0)`.

## Comportamiento de temporización

* **Las detecciones no están presentes en cada frame.** El pipeline de IA analiza un subconjunto de frames (normalmente 2–10 fps). La mayoría de los frames no llevan un TLV `AI_DETECTIONS`.
* **`det.ts` puede preceder al `TIMESTAMP` del frame portador** hasta \~250 ms porque el pipeline de inferencia y el codificador se ejecutan de forma independiente — la nueva detección viaja en el frame que salga del codificador a continuación. **Alinea las superposiciones según `det.ts`, no según el `TIMESTAMP` del frame envolvente**, especialmente para VOD o reproducción con búfer.
* **Persiste entre actualizaciones.** Para mantener los cuadros visibles entre actualizaciones de detección, conserva el conjunto más reciente y sigue redibujándolo hasta que llegue un conjunto más nuevo o transcurra un TTL. Un TTL de 2 segundos es un valor predeterminado seguro.

## Extender `RhombusRealtimePlayer` con renderizado de detecciones

El `RhombusRealtimePlayer` del React SDK no expone actualmente las detecciones de IA a la aplicación anfitriona. Hasta que lo haga, el patrón más sencillo es abrir un **segundo WebSocket** a la misma URL únicamente para leer AI\_DETECTIONS, y dibujar el resultado en un `<canvas>` superpuesto sobre el reproductor.

<Warning>
  Un WebSocket paralelo duplica el egreso de esa cámara. Úsalo solo en la página que necesita detecciones y ciérralo al desmontar.
</Warning>

```tsx theme={null}
import { RhombusRealtimePlayer } from "@rhombussystems/react";
import { useEffect, useRef, useState } from "react";
import { parseEncapHeader } from "./parseEncapHeader"; // from earlier in this guide
import { drawDetections } from "./drawDetections";     // from earlier in this guide

type Props = {
  cameraUuid: string;
  /** WebSocket URL for this camera (from getMediaUris + auth append) */
  detectionWsUrl: string;
  /** Optional: how long to keep boxes after the last update */
  detectionTtlMs?: number;
};

export function RhombusRealtimePlayerWithDetections({
  cameraUuid,
  detectionWsUrl,
  detectionTtlMs = 2000,
}: Props) {
  const canvasRef = useRef<HTMLCanvasElement>(null);
  const detectionsRef = useRef<any[]>([]);
  const lastTsRef = useRef(0);
  const [size, setSize] = useState({ width: 1920, height: 1080 });

  useEffect(() => {
    const ws = new WebSocket(detectionWsUrl);
    ws.binaryType = "arraybuffer";

    ws.onmessage = (event) => {
      // The server sends one text init message before any binary frames.
      if (typeof event.data === "string") {
        try {
          const init = JSON.parse(event.data);
          if (init.action === "init" && init.width && init.height) {
            setSize({ width: init.width, height: init.height });
          }
        } catch {
          // Ignore non-JSON text messages.
        }
        return;
      }

      try {
        const { detectionJson } = parseEncapHeader(event.data);
        if (!detectionJson) return;
        const dets = JSON.parse(detectionJson);
        detectionsRef.current = dets;
        lastTsRef.current = Date.now();
      } catch (err) {
        console.warn("Failed to parse encap header", err);
      }
    };

    return () => ws.close();
  }, [detectionWsUrl]);

  useEffect(() => {
    let raf = 0;

    const tick = () => {
      const canvas = canvasRef.current;
      if (canvas) {
        const ctx = canvas.getContext("2d");
        if (ctx) {
          const fresh = Date.now() - lastTsRef.current < detectionTtlMs;
          drawDetections(
            ctx,
            canvas.width,
            canvas.height,
            fresh ? detectionsRef.current : []
          );
        }
      }
      raf = requestAnimationFrame(tick);
    };

    raf = requestAnimationFrame(tick);
    return () => cancelAnimationFrame(raf);
  }, [detectionTtlMs]);

  return (
    <div style={{ position: "relative", width: size.width, height: size.height }}>
      <RhombusRealtimePlayer
        cameraUuid={cameraUuid}
        connectionMode="wan"
      />
      <canvas
        ref={canvasRef}
        width={size.width}
        height={size.height}
        style={{
          position: "absolute",
          inset: 0,
          width: "100%",
          height: "100%",
          pointerEvents: "none",
        }}
      />
    </div>
  );
}
```

Resuelve `detectionWsUrl` en tu backend de la misma forma que lo hace el SDK: llama a `getMediaUris`, elige el `wanLiveH264Uri` o la entrada de LAN adecuada, luego agrega `?x-auth-scheme=federated-token&x-auth-ft=<TOKEN>` (WAN) o establece la cookie `RFT` (LAN) antes de pasar la URL al navegador.

## Referencia de parser desde cero

Para clientes que no usan React (una página web pura, Node, Electron), el mismo parser impulsa una superposición mínima. Decodificar el H.264 en sí requiere WebCodecs (navegador) o ffmpeg/libav (Node) y queda fuera del alcance, pero leer las detecciones del WebSocket solo necesita el parser anterior:

```html theme={null}
<canvas id="overlay" width="1920" height="1080"
  style="position:absolute; inset:0; pointer-events:none;"></canvas>

<script type="module">
  import { parseEncapHeader } from "./parseEncapHeader.js";
  import { drawDetections } from "./drawDetections.js";

  // Resolve detectionWsUrl from your backend after calling
  // /api/camera/getMediaUris and appending the federated token.
  const ws = new WebSocket(detectionWsUrl);
  ws.binaryType = "arraybuffer";

  const canvas = document.getElementById("overlay");
  const ctx = canvas.getContext("2d");
  let lastDetections = [];
  let lastTs = 0;
  const TTL_MS = 2000;

  ws.onmessage = (event) => {
    if (typeof event.data === "string") return; // skip init handshake
    const { detectionJson } = parseEncapHeader(event.data);
    if (!detectionJson) return;
    lastDetections = JSON.parse(detectionJson);
    lastTs = Date.now();
  };

  function tick() {
    const fresh = Date.now() - lastTs < TTL_MS;
    drawDetections(ctx, canvas.width, canvas.height, fresh ? lastDetections : []);
    requestAnimationFrame(tick);
  }
  requestAnimationFrame(tick);
</script>
```

## Streams HTTP vs WebSocket

La variante de stream HTTP `video/h264` elimina por completo el encabezado de encapsulación y entrega únicamente datos NAL H.264 sin procesar. **Las detecciones viajan solo por el transporte WebSocket.** Usa las URLs de WebSocket de `getMediaUris` (`lanLiveH264Uris` / `wanLiveH264Uri`) para cualquier flujo que necesite detecciones.

## Solución de problemas

**Los cuadros aparecen en la ubicación incorrecta**

Las coordenadas de bbox son permyriad (0–10000), no píxeles y no 0–1. Asegúrate de que el renderizador divida entre 10000 antes de multiplicar por las dimensiones del canvas.

**Los cuadros parecen retrasarse respecto al video**

Alinea según `det.ts`, no según el `TIMESTAMP` del frame portador. La detección viaja en el frame que salga del codificador a continuación, que puede ir hasta \~250 ms por detrás del frame analizado.

**Los cuadros desaparecen durante unos cientos de milisegundos y luego reaparecen**

El pipeline de IA produce resultados a 2–10 fps y las detecciones no viajan en cada frame de video. Conserva el conjunto de detección más reciente con un TTL (p. ej. 2 s) para que la superposición se mantenga estable entre actualizaciones.

**El receptor solo recibe frames binarios; nunca ve el mensaje de inicialización**

Confirma que tu manejador de WebSocket acepte frames de texto antes que los frames binarios. La inicialización es un único mensaje de texto que se envía una vez por conexión.

**La autenticación por cookie en LAN falla localmente**

La cookie `RFT` debe tener alcance al dominio de la cámara. Si tu app se sirve desde `localhost` y la cámara reside en un host de LAN diferente, el navegador no puede establecer la cookie — conéctate por WAN en su lugar, o usa un proxy a través de tu backend.

## Próximos pasos

<CardGroup cols={2}>
  <Card title="React SDK" icon="react" href="/es/implementations/react-sdk">
    Componentes `RhombusRealtimePlayer` y `RhombusBufferedPlayer` listos para usar.
  </Card>

  <Card title="Streaming de video" icon="video" href="/es/implementations/streaming-video">
    HLS, streams compartidos, miniaturas y captura de frames.
  </Card>
</CardGroup>
