> For the complete documentation index, see [llms.txt](https://docs.eyepop.ai/developer-documentation/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.eyepop.ai/developer-documentation/platform/sources-and-options/sources.md).

# Source Types

Files, streams, HTTP and RTSP URLs, WebRTC, dataset assets, and image groups

Every example below opens a session with a Pop, submits one source, and reads predictions. Only the source differs. Uploaded sources come first, then the ones the worker fetches, then live media. Which formats and codecs any of them may carry is [its own page](/developer-documentation/platform/sources-and-options/formats.md).

### The session used by every example

{% tabs %}
{% tab title="Python" %}

```python
from eyepop import EyePopSdk
from eyepop.worker.worker_types import InferenceComponent, Pop

pop = Pop(components=[InferenceComponent(ability="eyepop.person:latest")])

with EyePopSdk.sync_worker(pop=pop) as endpoint:
    ...  # submit a source here
```

{% endtab %}

{% tab title="Node" %}

```typescript
import { EyePop, PopComponentType } from '@eyepop.ai/eyepop'

const endpoint = await EyePop.workerEndpoint({
    pop: {
        components: [{ type: PopComponentType.INFERENCE, ability: 'eyepop.person:latest' }],
    },
}).connect()

try {
    // submit a source here
} finally {
    await endpoint.disconnect()
}
```

{% endtab %}
{% endtabs %}

### Local files

The SDK reads the file and uploads it. The content type is derived from the file extension.

{% tabs %}
{% tab title="Python" %}

```python
result = endpoint.upload("photo.jpg").predict()
print(result)
```

{% endtab %}

{% tab title="Node" %}

```typescript
const results = await endpoint.process({ source: { path: 'photo.jpg' } })
for await (const result of results) {
    console.log(result)
}
```

{% endtab %}
{% endtabs %}

A video file works the same way, and yields one prediction per frame — see [Video](#video) below.

### Binary streams

Anything you already hold in memory, or are producing as you go: a decoded frame, a download in flight, a `BytesIO`. There is no filename to infer from, so the content type is required.

{% tabs %}
{% tab title="Python" %}

```python
with open("photo.jpg", "rb") as file:
    result = endpoint.upload_stream(file, "image/jpeg").predict()
```

On the async endpoint the stream can also be an async iterable of `bytes`, which is how you feed frames you are generating rather than reading.
{% endtab %}

{% tab title="Node" %}

```typescript
import fs from 'node:fs'
import { Readable } from 'node:stream'

const stream = Readable.toWeb(fs.createReadStream('photo.jpg'))
const results = await endpoint.process({
    source: { stream, mimeType: 'image/jpeg' },
})
```

`stream` accepts a `ReadableStream`, a `Blob`, or a `BufferSource`.
{% endtab %}
{% endtabs %}

For a stream that arrives in real time rather than as fast as it can be read, set [`is_live`](/developer-documentation/platform/sources-and-options/options/live-uploads.md) so the worker drops frames to keep up instead of falling behind.

### Browser file inputs

In the browser, hand the `File` straight from an `<input type="file">` to the SDK — its name and type come with it. Node only; the Python SDK does not run in a browser.

{% tabs %}
{% tab title="Python" %}
Not applicable. The Python SDK does not run in a browser — use a [local file](#local-files) or a [binary stream](#binary-streams).
{% endtab %}

{% tab title="Node" %}

```typescript
const file = (document.getElementById('upload') as HTMLInputElement).files![0]
const results = await endpoint.process({ source: { file } })
```

{% endtab %}
{% endtabs %}

### HTTP and HTTPS URLs

The worker fetches the URL itself, so nothing uploads from your application. This is the cheapest way to run inference on media that already lives in object storage or behind a CDN.

{% tabs %}
{% tab title="Python" %}

```python
result = endpoint.load_from("https://example.com/photo.jpg").predict()
```

{% endtab %}

{% tab title="Node" %}

```typescript
const results = await endpoint.process({
    source: { url: 'https://example.com/photo.jpg' },
})
```

{% endtab %}
{% endtabs %}

The URL must be reachable from EyePop's network and needs no credentials — signed URLs are the usual way to serve private media. The worker reads the response's `Content-Type` to decide how to decode it.

### RTSP cameras

`rtsp://` and `rtsps://` URLs connect to an IP camera or NVR. The worker always treats them as **live**: frames are dropped rather than queued when inference cannot keep pace, so predictions stay on the present rather than falling further behind real time.

{% tabs %}
{% tab title="Python" %}

```python
job = endpoint.load_from("rtsp://user:password@camera.example.com/stream1")
while result := job.predict():
    print(result)
```

Stop watching with `job.cancel()`.
{% endtab %}

{% tab title="Node" %}

```typescript
const results = await endpoint.process({
    source: { url: 'rtsp://user:password@camera.example.com/stream1' },
})

for await (const result of results) {
    console.log(result)
}
```

Stop watching with `results.cancel()`.
{% endtab %}
{% endtabs %}

A live stream never ends on its own, so the loop runs until you cancel it or the camera disconnects. Cameras are where [`fps`](/developer-documentation/platform/sources-and-options/options/frame-rate.md) and [motion gating](/developer-documentation/platform/sources-and-options/options/motion-detection.md) earn their keep: a 30fps feed of a mostly empty loading dock does not need 30 inferences a second.

A camera on a private network has no route in from EyePop's cloud. [Private Cameras](/developer-documentation/deploying/cloud/private-cameras.md) covers closing that gap, and [On-Premise](/developer-documentation/deploying/on-premise.md) covers running the inference next to the camera instead.

### Relayed cameras

A camera on a private network is not reachable from EyePop, so `load_from` cannot fetch it. Read it in your own application instead and forward the stream — your application connects to the camera locally and pushes MPEG-TS out over HTTPS, so nothing needs an inbound route.

Predictions from a relayed camera carry `captured_at`, the time the camera captured the frame, exactly as they would if the worker had read the camera itself. See [Private Cameras](/developer-documentation/deploying/cloud/private-cameras.md#capture-time-on-a-relayed-camera) for what that requires of the camera and when it is unavailable.

The SDK reads the camera and builds that stream for you. Needs the `relay` extra and `eyepop` 3.21.1 or newer:

```bash
pip install 'eyepop[relay]'
```

`eyepop.relay.rtsp_relay_stream()` returns the camera's video as a stream you hand to `upload_stream()`. One call relays one RTSP session: detecting that a camera has dropped and deciding whether to try again are yours, because each attempt has to become a new upload. [Private Cameras](/developer-documentation/deploying/cloud/private-cameras.md#relaying-with-the-python-sdk) has the example and what a retry policy needs to get right.

Relaying is available in the Python SDK today. There is no Node equivalent yet.

### RTMP streams

`rtmp://` URLs are pulled the same way, for stream sources that publish rather than serve — encoders, streaming platforms, relays.

{% tabs %}
{% tab title="Python" %}

```python
job = endpoint.load_from("rtmp://media.example.com/live/stream")
while result := job.predict():
    print(result)
```

{% endtab %}

{% tab title="Node" %}

```typescript
const results = await endpoint.process({
    source: { url: 'rtmp://media.example.com/live/stream' },
})
```

{% endtab %}
{% endtabs %}

### Dataset assets

An asset already uploaded to an EyePop [dataset](/developer-documentation/train-your-own-model.md) is addressed by its UUID, and the worker fetches it from EyePop storage — no upload, no public URL. Useful for running a Pop over data you have already collected for training.

{% tabs %}
{% tab title="Python" %}

```python
result = endpoint.load_asset("f2b0c0a2-...").predict()
```

{% endtab %}

{% tab title="Node" %}

```typescript
const results = await endpoint.process({
    source: { assetUuid: 'f2b0c0a2-...' },
})
```

{% endtab %}
{% endtabs %}

### Live from a browser

A browser can publish its webcam or screen directly to the worker over WebRTC, with no file and no server in between. Pass a `MediaStream` from `getUserMedia()` or `getDisplayMedia()`. Node SDK only — this is a browser API.

{% tabs %}
{% tab title="Python" %}
Not applicable. WebRTC ingest is a browser capability; from Python, push frames as a [binary stream](#binary-streams) with [`is_live`](/developer-documentation/platform/sources-and-options/options/live-uploads.md), or point the worker at an [RTSP](#rtsp-cameras) or [RTMP](#rtmp-streams) URL.
{% endtab %}

{% tab title="Node" %}

```typescript
const stream = await navigator.mediaDevices.getUserMedia({ video: true })

const results = await endpoint.process({ source: { mediaStream: stream } })

for await (const result of results) {
    console.log(result)
}
```

`results.cancel()` stops publishing.
{% endtab %}
{% endtabs %}

### Video

A video — uploaded or fetched — yields one prediction per processed frame, so read until the stream ends rather than taking a single result.

{% tabs %}
{% tab title="Python" %}

```python
job = endpoint.load_from("https://example.com/clip.mp4")
while result := job.predict():
    print(result["seconds"], result.get("objects"))
```

`predict()` returns `None` when the video ends. `job.cancel()` stops it early.
{% endtab %}

{% tab title="Node" %}

```typescript
const results = await endpoint.process({
    source: { url: 'https://example.com/clip.mp4' },
})

for await (const result of results) {
    console.log(result.seconds, result.objects)
    if ((result.seconds ?? 0) >= 10) {
        results.cancel()
    }
}
```

{% endtab %}
{% endtabs %}

An **uploaded** video is buffered in full before processing starts, which is the safe default but means no prediction arrives until the upload finishes. [`videoMode`](/developer-documentation/platform/sources-and-options/options/video-mode.md) switches it to process while the bytes are still arriving.

Because buffering holds the whole file, an uploaded video is capped at **1 GiB** — a larger one is rejected with a `400`. Send it as `stream` instead, or point the worker at a URL and let it fetch. On an on-premise instance the cap is the `max-upload-buffer` setting in `eyepop-instance.yml`, in bytes.

### Image groups

A group is a **single** source made of several images, processed **together** as one inference unit — a multi-image VLM prompt, a before-and-after pair, a burst. It returns one prediction for the whole set. That is what separates it from submitting the images one by one, where each is independent.

{% tabs %}
{% tab title="Python" %}

```python
# local files
result = endpoint.upload_group(["a.jpg", "b.jpg", "c.jpg"]).predict()

# in-memory streams
with open("a.jpg", "rb") as a, open("b.jpg", "rb") as b:
    result = endpoint.upload_stream_group([a, b]).predict()

# remote URLs
result = endpoint.load_from_group([
    "https://example.com/a.jpg",
    "https://example.com/b.jpg",
]).predict()
```

{% endtab %}

{% tab title="Node" %}

```typescript
import fs from 'node:fs'
import { Readable } from 'node:stream'

// local files
const fromPaths = await endpoint.uploadGroup(['a.jpg', 'b.jpg', 'c.jpg'])

// in-memory streams, with optional parallel MIME types
const a = Readable.toWeb(fs.createReadStream('a.jpg'))
const b = Readable.toWeb(fs.createReadStream('b.jpg'))
const fromStreams = await endpoint.uploadStreamGroup([a, b], ['image/jpeg', 'image/jpeg'])

// remote URLs
const fromUrls = await endpoint.loadFromGroup([
    'https://example.com/a.jpg',
    'https://example.com/b.jpg',
])
```

{% endtab %}
{% endtabs %}

Image order is preserved end to end. A group holds **up to 16 images**, enforced server-side, and the Pop's ability must be multi-image capable — a single-image ability handed a group returns an error. Groups are still images, so the video and motion options do not apply to them.

### Batching independent images

To run many images as **independent** inferences, submit them all and then collect the results. The session queues them and processes them in order, so the submissions do not wait on each other.

{% tabs %}
{% tab title="Python" %}

```python
jobs = [endpoint.upload(p) for p in ["photo1.jpg", "photo2.jpg", "photo3.jpg"]]
for job in jobs:
    print(job.predict())
```

The async endpoint takes an `on_ready` callback instead, so results are handled as each finishes rather than in submission order.
{% endtab %}

{% tab title="Node" %}

```typescript
const pending = ['photo1.jpg', 'photo2.jpg', 'photo3.jpg'].map(
    (path) => endpoint.process({ source: { path } }),
)
for (const results of await Promise.all(pending)) {
    for await (const result of results) {
        console.log(result)
    }
}
```

Awaiting each `process()` inside the loop instead would submit the next image only after the previous one's results were fully drained.
{% endtab %}
{% endtabs %}

### Next steps

* [Supported Formats](/developer-documentation/platform/sources-and-options/formats.md) — the image formats, video codecs, and containers a Pop can decode
* [Source Options](/developer-documentation/platform/sources-and-options/options.md) — frame rate, region of interest, motion gating, and component parameters
* [Pop Defaults](/developer-documentation/platform/sources-and-options/pop-defaults.md) — set those options once on the Pop
