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

# RTSP

> Capture segments from a live RTSP camera or encoder into a Mixpeek bucket.

<Note>
  One connection is one camera or encoder. Mixpeek cuts the stream into fixed-length segments, and each completed segment becomes one bucket object.
</Note>

## Overview

Each sync run opens the stream, captures `segment_seconds * segments_per_run` seconds, and closes it.

<Warning>
  **Capture is windowed, not gapless.** Whatever the camera shows between runs is not captured. A sync polling every 300 seconds with a 30 second window keeps 10% of the wall clock.

  An RTSP connection samples the stream. It does not archive it.
</Warning>

Coverage is `segment_seconds * segments_per_run` divided by `polling_interval_seconds`. Raise either of the first two to widen it, or shorten the poll interval. A longer poll interval narrows coverage, because it is the divisor.

| Poll interval | `segment_seconds` | `segments_per_run` | Captured | Run holds the stream |
| ------------- | ----------------- | ------------------ | -------- | -------------------- |
| 300s          | 30                | 1                  | 10%      | 39s                  |
| 300s          | 30                | 5                  | 50%      | 159s                 |
| 300s          | 30                | 9                  | 90%      | 279s                 |

<Warning>
  **Keep the run shorter than the interval.** A run holds the stream open for the whole capture plus about 9 seconds of setup. Runs overlap when that total exceeds `polling_interval_seconds`.

  This caps coverage below 100%. On a 300 second interval the ceiling is 97%; on a 60 second interval it is 84%.
</Warning>

## Prerequisites

* A camera or encoder reachable over `rtsp://` or `rtsps://`.
* Network access from Mixpeek to that host and port.
* The stream's username and password, if it is protected.

## Configuration

### Connection-level fields

| Field                      | Required | Description                                                                           |
| -------------------------- | -------- | ------------------------------------------------------------------------------------- |
| `url`                      | Yes      | `rtsp://` or `rtsps://` endpoint, for example `rtsp://camera.example.com:554/stream1` |
| `credentials`              | No       | `{"type": "basic", "username": ..., "password": ...}` for a protected stream          |
| `transport`                | No       | `tcp` (default) or `udp`                                                              |
| `segment_seconds`          | No       | 5 to 300, defaults to `30`                                                            |
| `segments_per_run`         | No       | 1 to 20, defaults to `1`                                                              |
| `connect_timeout_seconds`  | No       | 5 to 120, defaults to `15`                                                            |
| `scene_change_threshold`   | No       | 0 to 64. Omit to publish every segment                                                |
| `gate_retriever_id`        | No       | Saved retriever used as the gate. Mutually exclusive with `gate_inference_name`       |
| `gate_retriever_min_score` | No       | Score the retriever's top result must reach. Unset keeps on any result                |
| `gate_inference_name`      | No       | Real-time inference plugin that judges each segment                                   |
| `gate_on_error`            | No       | `publish` (default) or `drop`                                                         |
| `gate_timeout_seconds`     | No       | 1 to 300, defaults to `30`                                                            |

### Sync-level fields

| Field                      | Required | Description       |
| -------------------------- | -------- | ----------------- |
| `source_path`              | Yes      | The stream path   |
| `polling_interval_seconds` | No       | Defaults to `300` |

## Credentials go in `credentials`, never in the URL

Most camera vendors document an RTSP URL as `rtsp://user:pass@host:554/stream`. Mixpeek rejects that form at validation time.

```json theme={null}
{
  "url": "rtsp://admin:hunter2@camera.example.com:554/stream1"
}
```

```
url must not embed credentials. Move the username and password into the
`credentials` field so the endpoint stays safe to log, then use the bare
host form, for example rtsp://camera.example.com:554/stream1.
```

A URL carrying `user:pass@` travels through every log line, error message, and connection listing that prints the endpoint. Splitting the fields keeps the endpoint safe to display.

Write it this way instead:

```json theme={null}
{
  "url": "rtsp://camera.example.com:554/stream1",
  "credentials": {
    "type": "basic",
    "username": "admin",
    "password": "hunter2"
  }
}
```

<Warning>
  The scheme check rejects anything other than `rtsp://` and `rtsps://`. Many public camera feeds published as "streams" are HLS, which needs a different connector.
</Warning>

## `segment_seconds` is a target, not a guarantee

Segment cuts land on keyframes. A camera sending a keyframe every 3 seconds cannot produce a cut at an arbitrary second.

Measured against such a camera, a request for 10 second segments produced segments of 11.8, 9.0, and 12.0 seconds.

<Tip>
  Put the camera's keyframe interval in your budget before you size a downstream limit on segment length or file size.
</Tip>

## Choosing `segment_seconds`

Start at the default of 30.

**Capture cost does not depend on segment length.** Setup costs about 9.4 seconds per run whatever the configuration, and the marginal cost per segment is zero. Capture uses stream copy, so cutting more segments does not re-encode anything.

Measured against a live camera, four ways of capturing the same 24 seconds:

| Configuration | Wall clock | Overhead |
| ------------- | ---------- | -------- |
| 1 x 24s       | 33.5s      | 9.5s     |
| 2 x 12s       | 32.6s      | 8.6s     |
| 3 x 8s        | 34.0s      | 10.0s    |
| 4 x 6s        | 32.9s      | 8.9s     |

**The cost that scales is downstream.** Each segment is one object, one storage write, and one pipeline invocation. Halving segment length doubles the invocation count for the same footage. Size the segment against what your pipeline charges per invocation, not against capture.

**Shorten a segment to get a result sooner.** Latency to a first result is roughly `segment_seconds` plus 9.4 seconds plus your pipeline's time. A 30 second segment means you learn about a moment about 40 seconds after it happens.

**A static scene argues for longer segments.** Deduplication granularity is the segment, so a segment holding 5 seconds of activity still counts as changed.

<Tip>
  Ask the camera for its keyframe interval before picking a number below 30.

  ```bash theme={null}
  ffprobe -v error -select_streams v:0 -show_entries frame=key_frame \
          -read_intervals '%+20' -of csv=p=0 rtsp://your-camera/stream
  ```

  A camera sending a keyframe every 10 seconds cannot produce 5 second segments.
</Tip>

## Dropping segments that do not matter

An overnight camera captures the same empty room every run. Two filters drop those before they cost extraction, and they run in that order.

**`scene_change_threshold` needs no model.** It is a Hamming distance over a 64-bit perceptual hash of each segment's first frame. A segment is dropped when its distance from the previous kept segment is at or below the number.

| Value  | Drops                                         |
| ------ | --------------------------------------------- |
| `0`    | Only pixel-identical scenes                   |
| `5`    | The same frame allowing for compression noise |
| Higher | Collapses more aggressively                   |

It costs a few milliseconds of CPU per segment, so it runs first and anything it drops never reaches the gate.

**A gate judges each segment**, and there are two ways to write one. They are mutually exclusive.

| Field                 | The gate is                                                                                                                                                    | Change it by          |
| --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------- |
| `gate_retriever_id`   | A saved retriever. The segment becomes its query content, so the question is whether the segment resembles anything in the collections that retriever searches | Editing the retriever |
| `gate_inference_name` | A real-time inference plugin. Takes a segment, returns `keep` plus any fields you want on the object                                                           | Deploying the plugin  |

Reach for the retriever first. The policy is then an object you can read, edit, and version per camera, and changing what a camera keeps does not need a deploy. A plugin is the right answer when the decision is not a similarity question.

`gate_retriever_min_score` keeps the segment when the retriever's top result scores at or above it. Leave it unset to keep on any result, which is right when the retriever's own stages already filter. It is a second filter on top of the retriever, not a replacement for one.

Omit all of these and every captured segment is published.

<Warning>
  `gate_on_error` decides what happens when the gate itself fails, times out, or returns something unreadable. It defaults to `publish`, so a broken gate costs extraction rather than footage.

  Choose `drop` only when storing an unjudged segment is worse than losing it. A dropped segment of a live stream is gone, and the moment it covered cannot be recaptured.
</Warning>

Keep `gate_timeout_seconds` well below `segment_seconds * segments_per_run`, or the gate becomes the bottleneck and runs start overlapping.

## Setup

<Steps>
  <Step title="Create the storage connection">
    <CodeGroup>
      ```python Python theme={null}
      from mixpeek import Mixpeek

      client = Mixpeek(api_key="your-mixpeek-api-key")

      connection = client.organizations.connections.create(
          name="Loading Dock Camera",
          provider_type="rtsp",
          provider_config={
              "url": "rtsp://camera.example.com:554/stream1",
              "credentials": {
                  "type": "basic",
                  "username": "admin",
                  "password": "your-camera-password",
              },
              "transport": "tcp",
              "segment_seconds": 30,
              "segments_per_run": 2,
          },
      )
      print(f"Created connection: {connection['connection_id']}")
      ```

      ```javascript JavaScript theme={null}
      import { Mixpeek } from 'mixpeek-sdk'

      const client = new Mixpeek({ apiKey: 'your-mixpeek-api-key' })

      const connection = await client.organizations.connections.create({
        name: 'Loading Dock Camera',
        provider_type: 'rtsp',
        provider_config: {
          url: 'rtsp://camera.example.com:554/stream1',
          credentials: {
            type: 'basic',
            username: 'admin',
            password: 'your-camera-password',
          },
          transport: 'tcp',
          segment_seconds: 30,
          segments_per_run: 2,
        },
      })
      console.log('Created connection:', connection.connection_id)
      ```

      ```bash cURL theme={null}
      curl -X POST https://api.mixpeek.com/v1/organizations/connections \
        -H "Authorization: Bearer YOUR_MIXPEEK_API_KEY" \
        -H "Content-Type: application/json" \
        -d '{
          "name": "Loading Dock Camera",
          "provider_type": "rtsp",
          "provider_config": {
            "url": "rtsp://camera.example.com:554/stream1",
            "credentials": {
              "type": "basic",
              "username": "admin",
              "password": "your-camera-password"
            },
            "transport": "tcp",
            "segment_seconds": 30,
            "segments_per_run": 2
          }
        }'
      ```
    </CodeGroup>
  </Step>

  <Step title="Test the connection">
    Confirm Mixpeek can open the stream before you create a sync.

    ```bash cURL theme={null}
    curl -X POST https://api.mixpeek.com/v1/organizations/connections/conn_your_connection_id/test \
      -H "Authorization: Bearer YOUR_MIXPEEK_API_KEY"
    ```

    A camera that does not start delivering within `connect_timeout_seconds` fails here rather than on the first sync run.
  </Step>

  <Step title="Create a bucket whose schema declares the segment property">
    ```bash cURL theme={null}
    curl -X POST https://api.mixpeek.com/v1/buckets \
      -H "Authorization: Bearer YOUR_MIXPEEK_API_KEY" \
      -H "X-Namespace: ns_your_namespace_id" \
      -H "Content-Type: application/json" \
      -d '{
        "bucket_name": "dock_camera",
        "bucket_schema": {
          "properties": {
            "segment": {"type": "video"},
            "captured_at": {"type": "datetime"},
            "segment_index": {"type": "integer"},
            "gate_applied": {"type": "boolean"},
            "motion_score": {"type": "number"}
          }
        }
      }'
    ```

    The response carries `bucket_id`. The name `segment` is yours to choose, and the next three steps all have to use the same one.
  </Step>

  <Step title="Create the sync, with a schema mapping">
    ```bash cURL theme={null}
    curl -X POST https://api.mixpeek.com/v1/buckets/bkt_your_bucket_id/syncs \
      -H "Authorization: Bearer YOUR_MIXPEEK_API_KEY" \
      -H "X-Namespace: ns_your_namespace_id" \
      -H "Content-Type: application/json" \
      -d '{
        "connection_id": "conn_your_connection_id",
        "source_path": "/stream1",
        "polling_interval_seconds": 60,
        "schema_mapping": {
          "segment": {
            "target_type": "blob",
            "blob_property": "segment",
            "blob_type": "video",
            "source": {"type": "file"}
          },
          "captured_at": {
            "target_type": "field",
            "source": {"type": "rtsp_field", "field": "captured_at"}
          },
          "segment_index": {
            "target_type": "field",
            "source": {"type": "rtsp_field", "field": "segment_index"}
          },
          "gate_applied": {
            "target_type": "field",
            "source": {"type": "rtsp_field", "field": "applied"}
          },
          "motion_score": {
            "target_type": "field",
            "source": {"type": "rtsp_field", "field": "motion_score"}
          }
        }
      }'
    ```

    <Warning>
      **Name the blob property, even though `schema_mapping` is optional.** Without it each segment lands on `content`, the default blob property, and not on the property your bucket schema declares.

      A bucket declaring a `segment` video property with `input_mappings` of `{"video": "segment"}` then passes collection validation and the batch matches nothing. You get a clean run over zero documents rather than an error.
    </Warning>
  </Step>
</Steps>

## Getting capture and gate values onto the object

A segment carries more than video. Capture records where it came from and when; a gate records what it decided. None of that reaches the object unless a `schema_mapping` names it, and the bucket schema declares a property to hold it.

Use `rtsp_field` as the source type and name the field without a prefix.

| `field`                                          | Comes from               |
| ------------------------------------------------ | ------------------------ |
| `source_url`                                     | Capture                  |
| `segment_index`                                  | Capture                  |
| `segment_duration_seconds`                       | Capture                  |
| `captured_at`                                    | Capture                  |
| `transport`                                      | Capture                  |
| `applied`                                        | The gate, when it failed |
| `error`                                          | The gate, when it failed |
| anything else the plugin returned besides `keep` | The gate                 |

```json theme={null}
"motion_score": {
  "target_type": "field",
  "source": {"type": "rtsp_field", "field": "motion_score"}
}
```

<Tip>
  **`gate_applied: false` is the useful one.** It records that the gate could not be reached and the segment was published unjudged.

  Without it an unjudged segment and a segment the gate passed look identical, so a run where the gate was down reads as a run where nothing moved.
</Tip>

<Warning>
  `target_type` takes `field` or `blob`. There is no `metadata` value: a sync created with one saves, then returns 400 on every read and disappears from `syncs/list`.
</Warning>

## Searching what you captured

Segments in a bucket are not searchable yet. A collection processes them into documents, and a saved retriever searches those documents. That second half is why the first half exists.

```
camera -> connection -> sync -> bucket objects -> collection -> retriever
```

<Steps>
  <Step title="Create a collection over the bucket">
    ```bash cURL theme={null}
    curl -X POST https://api.mixpeek.com/v1/collections \
      -H "Authorization: Bearer YOUR_MIXPEEK_API_KEY" \
      -H "X-Namespace: ns_your_namespace_id" \
      -H "Content-Type: application/json" \
      -d '{
        "collection_name": "dock_camera_segments",
        "source": {
          "type": "bucket",
          "bucket_id": "bkt_your_bucket_id",
          "input_mappings": {"video": "segment"}
        },
        "features": ["video_search"]
      }'
    ```

    `input_mappings` points the extractor at the blob property your sync writes. It has to match the `blob_property` you set in `schema_mapping`, or the collection processes nothing.
  </Step>

  <Step title="Save a retriever over the collection">
    A retriever needs `retriever_name` and `stages`, and each stage is `{"stage_name": ..., "config": {"stage_id": ..., "parameters": {...}}}`.

    ```bash cURL theme={null}
    curl -X POST https://api.mixpeek.com/v1/retrievers \
      -H "Authorization: Bearer YOUR_MIXPEEK_API_KEY" \
      -H "X-Namespace: ns_your_namespace_id" \
      -H "Content-Type: application/json" \
      -d '{
        "retriever_name": "dock_camera_search",
        "collection_identifiers": ["dock_camera_segments"],
        "stages": [ ... ],
        "input_schema": {"query": {"type": "string", "required": true}}
      }'
    ```

    See [Retrievers](/docs/retrieval/retrievers) for a complete `feature_search` stage, including the `feature_uri` to search against. `input_schema` is a flat dict of field names, not JSON Schema.
  </Step>

  <Step title="Execute it">
    ```bash cURL theme={null}
    curl -X POST https://api.mixpeek.com/v1/retrievers/ret_your_retriever_id/execute \
      -H "Authorization: Bearer YOUR_MIXPEEK_API_KEY" \
      -H "X-Namespace: ns_your_namespace_id" \
      -H "Content-Type: application/json" \
      -d '{"inputs": {"query": "forklift near the loading bay"}}'
    ```

    The response carries `documents`, each one a segment the retriever scored against your query.
  </Step>

  <Step title="Record what the user did with the results">
    ```bash cURL theme={null}
    curl -X POST https://api.mixpeek.com/v1/retrievers/interactions \
      -H "Authorization: Bearer YOUR_MIXPEEK_API_KEY" \
      -H "X-Namespace: ns_your_namespace_id" \
      -H "Content-Type: application/json" \
      -d '{
        "feature_id": "doc_the_segment_they_opened",
        "interaction_type": ["click"],
        "position": 0,
        "retriever_id": "ret_your_retriever_id",
        "execution_id": "exec_from_the_response",
        "query_snapshot": {"query": "forklift near the loading bay"}
      }'
    ```

    Interactions are the signal a saved retriever learns from, which is the reason to save one rather than run an ad-hoc query. `retriever_id` and `query_snapshot` are optional and both are worth sending: without them an interaction cannot be tied back to what produced it.

    <Tip>
      A saved retriever can also run at the other end of this pipeline. Set `gate_retriever_id` on the connection and it decides which segments are worth keeping in the first place, before extraction is paid for.
    </Tip>
  </Step>
</Steps>

<Warning>
  **The `schema_mapping` on your sync decides whether any of this finds anything.** With no mapping each segment lands on `content`, the default blob property, and the fields your mapping would have written are never written at all.

  A collection whose `input_mappings` name `segment` then processes nothing, and a retriever over that collection returns zero documents. Every step reports success.
</Warning>

## Transport

`tcp` interleaves RTP over the RTSP control connection. It survives NAT and firewalls that drop the separate UDP ports, which is why it is the default.

Use `udp` when the camera or the network path does not handle interleaved TCP well.

## Related

<CardGroup cols={2}>
  <Card title="Sync configuration" icon="rotate" href="/docs/platform/syncs">
    Polling intervals, reconciliation, and scheduling.
  </Card>

  <Card title="Retrievers" icon="magnifying-glass" href="/docs/retrieval/retrievers">
    Stages, input schemas, and what execution returns.
  </Card>

  <Card title="Interactions" icon="hand-pointer" href="/docs/retrieval/interactions">
    The feedback signal a saved retriever learns from.
  </Card>

  <Card title="Mux" icon="video" href="/docs/integrations/object-storage/mux">
    Video assets from a hosted platform rather than a live camera.
  </Card>
</CardGroup>
