Use Case
Camera Streaming
Stream live video from an OpenMV camera directly to Tendrl. View real-time feeds in your dashboard, send sensor data alongside video, and trigger workflows when something happens.
Only care about events, not the live feed? A camera can run the detector on-device and send you 200 bytes when something changes, with no video leaving the board at all. See Cameras as Sensors.
Why embedded camera streaming is hard
MJPEG streaming over HTTP requires managing multipart boundaries, chunked encoding, and long-lived connections on a device with kilobytes of RAM.
Running a camera capture loop and a network stack concurrently on a microcontroller demands cooperative multitasking, which is easy to get wrong and hard to debug.
You need a relay server, SSE or WebSocket endpoint, auth layer, and viewer client. Most teams end up with a fragile custom stack that only works on the local network.
Cameras rarely work in isolation: you want sensor readings, GPS, battery level, and alerts alongside the video feed. That means two parallel data paths to build.
How Tendrl solves it
The MicroPython SDK handles camera configuration, streaming protocol, and Wi-Fi reconnection. You write six lines of code.
Set up your device
Create an entity in Contact with the entity:Stream permission. Add your Wi-Fi credentials and entity API key to config.json on the device's filesystem.
{
"api_key": "YOUR_ENTITY_API_KEY",
"wifi_ssid": "YOUR_WIFI_SSID",
"wifi_pw": "YOUR_WIFI_PASSWORD"
} Start streaming with one call
The SDK auto-configures the camera (QVGA, JPEG, quality 70), connects to Wi-Fi, authenticates, and begins streaming. Default settings are optimized for stability: 15 FPS at 320x240 works reliably on most networks.
import asyncio
from tendrl import Client
async def main():
client = Client(mode="async", debug=True)
client.start()
await asyncio.sleep(5)
# Camera auto-configured: QVGA, JPEG, quality 70
client.start_streaming()
try:
await asyncio.sleep(3600) # Stream for 1 hour
except KeyboardInterrupt:
pass
finally:
await client.async_stop()
asyncio.run(main()) Stream and send sensor data at the same time
Streaming runs as a background task on the same async event loop as MQTT messaging. Publish temperature, humidity, battery level, or any other data while the video streams; messaging takes priority so your telemetry is never blocked.
import asyncio
from tendrl import Client
async def main():
client = Client(mode="async", debug=True)
client.start()
await asyncio.sleep(5)
# Start video stream as background task
client.start_streaming()
# Publish sensor data alongside the video
while True:
client.publish(
{"temperature": 23.5, "status": "streaming"},
tags=["sensors"]
)
await asyncio.sleep(10)
asyncio.run(main()) View the live feed in your dashboard
When a stream is active, a camera icon appears on the entity's detail page. Click it to open the live viewer. Tendrl relays frames via Server-Sent Events so any browser can view the feed: no plugins, no port forwarding, no local network required.
Trigger workflows from camera events
Use the same data flows and Strand workflows as any other entity. Tag messages with context so your workflows know when to fire.
Tunable for your network and use case
JPEG quality 45–90. Lower quality means smaller frames and more bandwidth headroom. Default is 70; raise it toward 80–90 when you have the bandwidth.
Target 10–25 FPS. Lower FPS for slow networks, higher for smoother video. Default 15 FPS is stable on most Wi-Fi.
QQVGA (160x120), QVGA (320x240), or VGA (640x480). Pick the right trade-off for your bandwidth.
Provide your own capture function for full control over camera settings, image processing, or non-standard camera modules.
If Wi-Fi drops, the client reconnects automatically and resumes streaming. Check debug output for reconnection status.
With debug=True, get FPS, frame sizes, send times, and bandwidth stats every 60 frames.
Advanced: custom camera settings
For full control, use Camera in stream mode. It handles the OpenMV csi (current firmware) vs sensor (legacy) split, and cam.jpeg already satisfies the capture-function contract.
import asyncio
from tendrl import Client
from tendrl.vision import Camera
cam = Camera(mode="stream", framesize="VGA", quality=60)
async def main():
client = Client(mode="async", debug=True)
client.start()
await asyncio.sleep(5)
client.start_streaming(capture_frame_func=cam.jpeg, target_fps=15)
try:
await asyncio.sleep(3600)
finally:
await client.async_stop()
asyncio.run(main()) The sensor holds one pixel format at a time. Hardware JPEG and the
detectors want different ones (cam.jpeg needs stream mode, detectors need
RGB565), so you can't run both off the sensor at once. You can still stream
detected frames: stay in detect mode and compress in software, which is what the
next section does.
Detect and stream
Stream what the model sees
The detectors return boxes and keypoints, and the draw helpers paint them into the frame's pixels, so they survive JPEG compression. Draw inside your capture function and the live feed carries the overlay with it. No server-side inference, no second video pipeline.
person_boxes
Where the people are. Uses OpenMV's built-in model: nothing to train or upload.
face_boxes
Face boxes plus six keypoints: both eyes, nose, mouth, both ears. The cheapest model; try it first on a board with no NPU.
palm_boxes
Hand boxes plus seven keypoints: wrist, knuckles, thumb. Draw as a skeleton with PALM_LINES.
hand_landmarks
All 21 joints per hand, labelled left or right. Pair with HAND_LINES for a full skeleton.
import asyncio
from tendrl import Client
from tendrl.vision import Camera, face_boxes, draw_boxes, draw_keypoints
# Detect mode = RGB565, which is what the detectors read.
cam = Camera(mode="detect", framesize="QVGA")
detect = face_boxes(min_conf=0.5)
def annotated_jpeg():
img = cam.snapshot()
dets = detect(img)
draw_boxes(img, dets, label="face")
for d in dets:
draw_keypoints(img, d[5])
# Drawn into the pixels, so the boxes survive compression.
return img.compress(quality=70).bytearray()
async def main():
client = Client(mode="async")
client.start()
await asyncio.sleep(5)
# Any callable returning JPEG bytes satisfies the contract.
client.start_streaming(capture_frame_func=annotated_jpeg, target_fps=10)
await asyncio.sleep(3600)
asyncio.run(main())
This compresses in software rather than using the sensor's JPEG encoder, and it
still keeps up: measured on an OpenMV AE3 at QVGA, with the face model running
every frame, one detect-draw-compress pass takes 43–50 ms (20 to 23 fps),
comfortably above the 15 fps default target. The same draw calls work before
send_clip_burst, which keeps the boxes in a recorded clip.
Pick the threshold before you write the code
Every detector takes a confidence or threshold you have to choose, and the right value depends on your scene, angle, and lighting. The Device tab's vision playground runs these same detectors on the board, live, so you can drag an ROI and watch the measured number move against the marker, then export the tuned watcher.
How the protocol works
The SDK captures JPEG frames from the camera and sends them over a long-lived HTTP POST using the multipart/x-mixed-replace protocol. Each frame is a separate multipart part. The connection stays open as long as the device is streaming.
Tendrl authenticates the stream using the entity's API key, validates the entity:Stream permission, and broadcasts frames to connected viewers. Invalid frames are silently skipped. One stream per entity at a time.
The dashboard opens a Server-Sent Events connection to GET /api/entities/:id/stream/view. Each SSE data event contains a base64-encoded JPEG frame rendered into an <img> tag in real time.
Streaming limits
Max frame size
Max frame rate
Max connection
Idle timeout: 5 minutes with no frames. One active stream per entity. Streaming data counts toward your account's monthly data usage.
Compatible devices
Current flagship. Camera + on-device vision with plenty of headroom.
Native hardware JPEG, fast capture, MicroPython built-in.
Plenty of headroom for streaming and on-device detection together.
No hardware JPEG on the sensor; the SDK compresses in software automatically, so streaming still works.
Any device that can capture JPEG and run the Tendrl SDK, via a custom capture function.
Stream your first camera feed in five minutes
Free tier includes 5 entities and 250 MB storage. No credit card required.
Tendrl