Image Streams
Image streams expose a continuous, low-latency video-like feed from a device's camera. They are intended for live preview, alignment and focus assistance — not for archival capture (use measurements for that). A stream is configured once via POST .../start, then consumed either as a Server-Sent-Events feed for real-time delivery or polled frame-by-frame via the snapshot endpoint.
Endpoints Overview
| Method | Endpoint | Description |
|---|---|---|
| POST | /v1/devices/{deviceId}/image-streams/{sourceKey}/start |
Start a stream for a specific image source |
| POST | /v1/devices/{deviceId}/image-streams/{sourceKey}/configure |
Change exposure / scale / encoding while running |
| GET | /v1/devices/{deviceId}/image-streams/{sourceKey}/stream |
Subscribe to live frames via SSE (text/event-stream) |
| GET | /v1/devices/{deviceId}/image-streams/{sourceKey}/snapshot |
Pull a single frame (binary image) |
| GET | /v1/devices/{deviceId}/image-streams |
List all active streams on a connected device |
| DELETE | /v1/devices/{deviceId}/image-streams/{sourceKey} |
Stop and tear down the stream |
Source Keys
A device may expose more than one camera or imaging path. Each is addressed by a sourceKey. For Aesthetix devices the available keys are:
sourceKey |
Camera |
|---|---|
spec |
Specular path |
aspec |
Aspecular path |
Other device classes may expose different keys; consult the device's documentation.
Lifecycle
┌──────────────────────────────┐
│ POST .../{sourceKey}/start │ ← configure exposure, scale, encoding
└──────────────┬───────────────┘
│
┌─────────┴─────────┐
│ │
▼ ▼
GET .../stream GET .../snapshot
(SSE feed) (single frame)
│ │
│ POST .../configure (optional, while running)
│ │
└─────────┬─────────┘
▼
┌──────────────────────────────┐
│ DELETE .../{sourceKey} │
└──────────────────────────────┘
A stream remains active until explicitly stopped via DELETE or until the device is disconnected. Reconnecting to the device does not automatically restore previously running streams.
Starting a Stream
POST /v1/devices/{deviceId}/image-streams/{sourceKey}/start
Content-Type: application/json
{
"exposureTimeMilliseconds": 50,
"scaleFactor": 0.5,
"mimeType": "image/jpeg",
"quality": 0.85
}
curl -X POST http://localhost:42042/v1/devices/12f1d7dd07ac42a088c8f961b39d68ff/image-streams/aspec/start \
-H "Content-Type: application/json" \
-d '{ "exposureTimeMilliseconds": 50, "scaleFactor": 0.5, "mimeType": "image/jpeg", "quality": 0.85 }'
Request Body
All fields are optional — omit them to use the server defaults.
| Field | Type | Default | Description |
|---|---|---|---|
exposureTimeMilliseconds |
double | device-dependent | Sensor exposure per frame. Typical range 1–1000 ms. Higher values yield brighter frames but lower frame rate. |
scaleFactor |
double | 1.0 |
Downscaling factor applied server-side before encoding. Lower values reduce bandwidth and CPU at the cost of resolution. Typical range 0.1–1.0. |
mimeType |
string | "image/png" |
Per-frame encoding. One of image/png or image/jpeg. PNG is lossless and larger, JPEG smaller and lossy. |
quality |
double | 0.85 |
JPEG quality (0.1–1.0). Ignored when mimeType is image/png. |
Response
{
"deviceId": "12f1d7dd07ac42a088c8f961b39d68ff",
"sourceKey": "aspec",
"exposureTimeMilliseconds": 50,
"scaleFactor": 0.5,
"mimeType": "image/jpeg",
"quality": 0.85,
"startedAt": "2025-11-12T08:20:15.4567890+00:00",
"status": "active"
}
Consuming the Stream (SSE)
GET /v1/devices/{deviceId}/image-streams/{sourceKey}/stream
Accept: text/event-stream
The response is a Server-Sent-Events stream (RFC 8895). The hub sets Content-Type: text/event-stream, Cache-Control: no-cache, Connection: keep-alive. Each frame arrives as one SSE event:
data: {"mimeType":"image/jpeg","image":"<base64-encoded image bytes>","timestamp":"2025-11-12T08:20:15.5123456+00:00"}
Event Payload
| Field | Type | Description |
|---|---|---|
mimeType |
string | MIME type of the encoded frame (matches the stream's current mimeType). |
image |
string | Base64-encoded image bytes. Decode and feed directly to your image viewer. |
timestamp |
string | ISO-8601 capture time of the frame (with +00:00 UTC offset). Useful for measuring end-to-end latency. |
Error Events
If the device errors out mid-stream, an SSE event with event: error is sent before the connection closes:
event: error
data: {"errorCode":"<code>","message":"<message>"}
Treat this as a terminal event — re-subscribe by issuing GET .../stream again, or stop and recreate the stream if the underlying error was caused by a configuration mismatch.
Following the Stream from the Shell
curl -N http://localhost:42042/v1/devices/12f1d7dd07ac42a088c8f961b39d68ff/image-streams/aspec/stream
-N disables curl's output buffering so each event prints as it arrives. Useful for smoke-testing the stream without writing a client.
Consuming the Stream from a Browser
EventSource is the standard Web API for SSE and the easiest way to consume the feed. The hub's bundled demo app (/app/) uses exactly this pattern:
const url = `http://localhost:42042/v1/devices/${deviceId}/image-streams/${sourceKey}/stream`;
const source = new EventSource(url);
source.onmessage = (event) => {
const { mimeType, image, timestamp } = JSON.parse(event.data);
document.getElementById('preview').src = `data:${mimeType};base64,${image}`;
};
source.addEventListener('error', () => {
source.close(); // terminal — re-subscribe by creating a new EventSource
});
The data: URL form works directly in <img> tags, <canvas>.drawImage() and createImageBitmap() without an explicit Base64 decode step.
Snapshot — Single Frame
GET /v1/devices/{deviceId}/image-streams/{sourceKey}/snapshot
curl -o frame.jpg \
http://localhost:42042/v1/devices/12f1d7dd07ac42a088c8f961b39d68ff/image-streams/aspec/snapshot
Returns one frame as a raw binary image with the Content-Type set to the stream's current mimeType (image/png, image/jpeg, or image/bmp). Use this when you need a single frame on demand and do not want the overhead of opening an SSE connection.
Snapshots and SSE both read from the same underlying camera. Pulling a snapshot while an SSE feed is active competes for the next frame and is rarely useful. The Elements Hub demo app disables the snapshot button while SSE is running for this reason.
Reconfiguring a Running Stream
POST /v1/devices/{deviceId}/image-streams/{sourceKey}/configure
Content-Type: application/json
{
"exposureTimeMilliseconds": 100,
"scaleFactor": 0.75
}
The same field set as start; only the fields you include are changed. Already-subscribed SSE clients keep their connection — subsequent frames simply use the new configuration. Use this for live UI sliders that adjust exposure or scaling without forcing the user to disconnect and reconnect.
Listing Active Streams
GET /v1/devices/{deviceId}/image-streams
Returns an array of ImageStreamResponse records (same shape as the start response), one per sourceKey that currently has an active stream on the device. An empty array means no stream is currently running.
Stopping a Stream
DELETE /v1/devices/{deviceId}/image-streams/{sourceKey}
curl -X DELETE http://localhost:42042/v1/devices/12f1d7dd07ac42a088c8f961b39d68ff/image-streams/aspec
Stops the stream and releases the camera-side resources. Any SSE consumers see the connection close immediately after the next pending frame. DELETE is idempotent at the API level — calling it a second time returns 404 (E37), which most clients can ignore safely on shutdown paths.
Best Practices
- Start before subscribe: always issue
POST .../startbeforeGET .../stream. Subscribing without a started stream returns404(E37). - Set
scaleFactorfor the consumer: a 0.5 factor cuts bandwidth and CPU to roughly a quarter without a noticeable difference for live-preview use cases. Full-resolution streaming is rarely the right default. - Prefer JPEG for live preview: at quality 0.85 the file size is typically 5–10× smaller than PNG, and the artefacts are invisible at preview resolutions.
- Close
EventSourceon tab hide: browsers throttle background tabs, which can starve a hot SSE connection. Listen forvisibilitychangeand close/reopen the stream as needed. - Reconfigure rather than restart:
POST .../configureis faster thanstop+startbecause the camera does not reinitialise. - Stop on disconnect: explicit
DELETEon shutdown frees the camera immediately.POST /v1/lifecycle/shutdownwill also stop all streams, but explicit cleanup surfaces errors earlier.
Error Cases
| Condition | HTTP | Error Code |
|---|---|---|
| Connected device not found | 404 | E5 |
Stream not started (subscribe/snapshot/configure/stop without a prior start) |
404 | E37 |
Unsupported sourceKey for the device |
404 | device-class–specific |
See Error Handling for the response format.