WebRTC Streaming¶
The inference-sdk includes a WebRTC client for low-latency, real-time video inference. Instead of sending images one HTTP request at a time, you open a streaming session: video frames flow to the server over a WebRTC connection, and processed frames plus prediction data flow back continuously.
Experimental
The WebRTC SDK is experimental and under active development. The API may change in future releases. Please report issues at github.com/roboflow/inference/issues.
WebRTC streaming requires extra dependencies:
pip install "inference-sdk[webrtc]"
Quickstart: stream a model¶
The fastest way to get live predictions is to pass a model_id — no Workflow required. The SDK builds a minimal single-model Workflow under the hood, and your on_frame handler receives each video frame together with the raw predictions dict for that frame:
import cv2
import supervision as sv
from inference_sdk import InferenceHTTPClient
from inference_sdk.webrtc import WebcamSource
# Replace ROBOFLOW_API_KEY with your Roboflow API Key
client = InferenceHTTPClient(
api_url="http://localhost:9001",
api_key="ROBOFLOW_API_KEY",
)
session = client.webrtc.stream(
source=WebcamSource(),
model_id="rfdetr-nano",
)
box_annotator = sv.BoxAnnotator()
@session.on_frame
def show(frame, data):
# data is the raw predictions dict, exactly as returned by the server
# (None when predictions are unavailable for this frame)
if data is None:
return
detections = sv.Detections.from_inference(data)
annotated = box_annotator.annotate(frame.copy(), detections)
cv2.imshow("Preview", annotated)
if cv2.waitKey(1) & 0xFF == ord("q"):
session.close()
session.run() # blocks until the stream ends or session.close() is called
model_id works for any task type with a generic Workflow model block. The model's task type is resolved automatically via a Roboflow API lookup, and the matching model block is selected for you. Supported task types:
object-detectioninstance-segmentationsemantic-segmentationclassificationmulti-label-classificationkeypoint-detection
data is the serialized predictions dict passed through verbatim — its shape follows the task type. For detection-family models it is inference-response-shaped, so you can convert it with the matching supervision helper — sv.Detections.from_inference(data) for object detection and instance segmentation, sv.KeyPoints.from_inference(data) for keypoint models. Classification predictions carry top/confidence keys, and semantic-segmentation predictions carry run-length-encoded masks (rle_mask) that you decode yourself.
When predictions are unavailable for a frame (e.g. the paired prediction message never arrived for a live stream frame), data is None — check for it in your handler before use.
VLMs are not supported in model_id mode (each VLM family has its own dedicated Workflow block, so there is no generic block to wrap them with) — stream them with a full workflow instead.
Skipping the task-type lookup: pass task_type explicitly to avoid the network call — useful for air-gapped or self-hosted deployments:
session = client.webrtc.stream(
source=WebcamSource(),
model_id="my-project/3",
task_type="object-detection",
)
In model_id mode, on_frame handlers can take either (frame, data) or (frame, data, metadata) — the third argument is the VideoMetadata for the frame.
Streaming a Workflow¶
For multi-step pipelines, pass a workflow instead of a model_id. Reference a Workflow saved in your Roboflow workspace by ID, or provide a full specification dict:
import cv2
from inference_sdk import InferenceHTTPClient
from inference_sdk.webrtc import WebcamSource, StreamConfig
client = InferenceHTTPClient(
api_url="http://localhost:9001",
api_key="ROBOFLOW_API_KEY",
)
session = client.webrtc.stream(
source=WebcamSource(),
workflow="my-workflow-id",
workspace="my-workspace-name",
config=StreamConfig(
stream_output=["output_image"], # workflow output streamed back as video
data_output=["predictions"], # workflow outputs delivered via data channel
),
)
@session.on_frame
def show(frame, metadata):
cv2.imshow("Preview", frame)
if cv2.waitKey(1) & 0xFF == ord("q"):
session.close()
@session.on_data("predictions")
def handle_predictions(predictions, metadata):
print(f"Frame {metadata.frame_id}: {predictions}")
session.run()
import cv2
from inference_sdk import InferenceHTTPClient
from inference_sdk.webrtc import WebcamSource, StreamConfig
client = InferenceHTTPClient(
api_url="http://localhost:9001",
api_key="ROBOFLOW_API_KEY",
)
workflow_spec = {
"version": "1.0",
"inputs": [{"type": "InferenceImage", "name": "image"}],
"steps": [
{
"type": "roboflow_core/roboflow_object_detection_model@v2",
"name": "model",
"images": "$inputs.image",
"model_id": "rfdetr-nano",
}
],
"outputs": [
{
"type": "JsonField",
"name": "predictions",
"selector": "$steps.model.predictions",
},
{"type": "JsonField", "name": "image", "selector": "$inputs.image"},
],
}
session = client.webrtc.stream(
source=WebcamSource(),
workflow=workflow_spec,
config=StreamConfig(
stream_output=["image"],
data_output=["predictions"],
),
)
@session.on_frame
def show(frame, metadata):
cv2.imshow("Preview", frame)
if cv2.waitKey(1) & 0xFF == ord("q"):
session.close()
session.run()
Notes:
workflowandmodel_idare mutually exclusive — pass exactly one.workspaceis required whenworkflowis an ID string; it is not needed for a specification dict.image_input(default"image") names the Workflow image input the video frames are bound to.- In workflow mode,
on_framehandlers receive(frame, metadata)— prediction data arrives separately throughon_datahandlers, routed by thedata_outputnames inStreamConfig.
Video sources¶
The first argument to stream() selects where video comes from:
from inference_sdk.webrtc import (
WebcamSource,
RTSPSource,
LocalStreamSource,
MJPEGSource,
VideoFileSource,
ManualSource,
)
WebcamSource¶
Captures frames from a local camera device and sends them to the server:
source = WebcamSource() # default camera
source = WebcamSource(device_id=1, resolution=(1920, 1080))
The camera's FPS is auto-detected and reported to the server.
RTSPSource¶
The server connects to the RTSP camera and streams processed video back to you — use this when the camera is reachable from the server:
source = RTSPSource("rtsp://user:pass@camera.local/stream")
LocalStreamSource¶
Captures an RTSP/RTMP stream locally (on the client machine) and sends frames to the server — use this when the camera is only reachable from your machine, not from the server:
source = LocalStreamSource("rtsp://192.168.1.10/stream") # also rtsps://, rtmp://, rtmps://
MJPEGSource¶
Like RTSPSource, but for MJPEG streams captured by the server:
source = MJPEGSource("http://camera.local/mjpeg")
VideoFileSource¶
Uploads a video file to the server over the data channel; the server processes it and streams results back. More efficient than frame-by-frame streaming for pre-recorded video:
source = VideoFileSource("video.mp4")
# Track upload progress and process at original FPS (live-preview pacing)
source = VideoFileSource(
"video.mp4",
on_upload_progress=lambda uploaded, total: print(f"{uploaded}/{total} chunks"),
realtime_processing=True, # default False = process as fast as possible
)
By default frames come back through the data channel (guaranteed order and quality). Pass use_datachannel_frames=False to receive them via a hardware-accelerated WebRTC video track instead (lower bandwidth).
ManualSource¶
Send frames programmatically — useful when frames come from a custom pipeline:
import threading
import time
import cv2
from inference_sdk.webrtc import ManualSource, StreamConfig
source = ManualSource()
session = client.webrtc.stream(
source=source,
model_id="rfdetr-nano",
config=StreamConfig(declared_fps=30),
)
@session.on_frame
def handle(frame, data):
print(data)
# run() establishes the connection and dispatches handlers; start it in a
# background thread so this thread can feed frames.
threading.Thread(target=session.run, daemon=True).start()
cap = cv2.VideoCapture("video.mp4")
while True:
ret, frame = cap.read()
if not ret:
break
try:
source.send(frame) # BGR numpy array
except RuntimeError:
pass # session still connecting — frame skipped
time.sleep(1 / 30) # pace sends to the declared FPS
session.close()
send() raises RuntimeError until the connection is established, and queued frames are dropped oldest-first if you send faster than the stream is consumed. ManualSource has no FPS auto-detection, so declare the frame rate via StreamConfig(declared_fps=...).
Consuming results¶
The session lifecycle¶
stream() returns a WebRTCSession. The connection starts lazily on first use (run(), video(), or wait()) and must be closed to release resources. Three equivalent patterns:
# 1. run() — auto-closes when it exits (recommended with handlers)
session.run()
# 2. Context manager — auto-closes on exit (recommended with the video() iterator)
with client.webrtc.stream(source=source, model_id="rfdetr-nano") as session:
for frame, data in session.video():
...
# 3. Manual — you must call close() yourself
session = client.webrtc.stream(source=source, model_id="rfdetr-nano")
for frame, data in session.video():
...
session.close()
session.close() is idempotent and safe to call from inside a handler — it ends run() and the video() iterator. session.wait(timeout=None) blocks until the stream ends without consuming frames yourself.
Receiving frames: on_frame and video()¶
@session.on_frame registers a handler invoked for every processed video frame when using run(). session.video() is the iterator equivalent — same data, pull-based:
# model_id mode: (frame, data) — data is the raw predictions dict
# (None when predictions are unavailable for the frame)
for frame, data in session.video():
...
# workflow mode: (frame, metadata)
for frame, metadata in session.video():
...
Frames are BGR numpy arrays. If your handler falls behind in realtime mode, the oldest frames are dropped so the stream stays live.
Receiving data: on_data¶
Workflow outputs listed in StreamConfig.data_output arrive over the data channel. Register handlers per output name, or one global handler for the whole payload:
@session.on_data("predictions") # a single output field
def handle_predictions(predictions, metadata):
print(f"Frame {metadata.frame_id}: {predictions}")
@session.on_data # global: full output dict
def handle_all(data, metadata):
print(data)
Handlers may accept (value, metadata) or just (value) — the signature is auto-detected.
Handling errors: on_error¶
The server reports per-frame errors (workflow execution failures, output serialization failures) alongside each data channel message. on_error handlers fire only for frames with a non-empty error list:
@session.on_error
def on_err(errors, metadata):
print(f"Frame {metadata.frame_id} failed: {errors}")
session.close() # e.g. bail out on first error
These are server-side per-frame failures; connection and setup errors surface as exceptions from run() instead. Errors are also attached to metadata.errors on every frame, so on_frame / on_data handlers can inspect them directly.
Frame metadata¶
VideoMetadata accompanies each frame and data message:
| Attribute | Description |
|---|---|
frame_id |
Unique identifier of the frame in the stream |
received_at |
When the server received the frame |
pts / time_base |
Presentation timestamp of the video stream |
declared_fps / measured_fps |
Declared vs. measured stream FPS |
errors |
Per-frame errors reported by the server (empty when the frame processed cleanly) |
StreamConfig¶
StreamConfig controls output routing, processing behavior, and network settings:
from inference_sdk.webrtc import StreamConfig
config = StreamConfig(
stream_output=["output_image"],
data_output=["predictions"],
realtime_processing=True,
)
session = client.webrtc.stream(source=source, workflow="...", workspace="...", config=config)
| Field | Default | Description |
|---|---|---|
stream_output |
[] |
Workflow output names streamed back as video |
data_output |
[] |
Workflow output names delivered via the data channel |
realtime_processing |
True |
Drop frames to keep up in real time; set False to queue and process every frame |
declared_fps |
None |
FPS declaration for sources without auto-detection (e.g. ManualSource) |
turn_server |
None |
TURN server config: {"urls": "turn:...", "username": "...", "credential": "..."} |
workflow_parameters |
{} |
Parameters passed to the Workflow execution |
requested_plan |
None |
Compute plan for Roboflow serverless endpoints (e.g. "webrtc-gpu-small") |
requested_region |
None |
Processing region for serverless endpoints (e.g. "us", "eu") |
processing_timeout |
None |
Server-side session time limit in seconds (serverless endpoints) |
In model_id mode, empty stream_output / data_output are filled automatically (["image"] and ["predictions"]); any other settings you provide are preserved.
TURN servers: when connecting to Roboflow-hosted endpoints, TURN configuration is fetched automatically. For self-hosted servers behind restrictive NATs or firewalls, provide turn_server explicitly; direct connection is attempted when it is not set.