Recipe: connect an ESP32-CAM and IoT data
Stream MJPEG frames from a two-dollar camera over one WebSocket, send sensor readings on the same connection, ingest MQTT from your fleet, and turn any of it into a workflow trigger.
Why IoT ingest
An ESP32-CAM costs a couple of dollars and has no H.264 encoder, so the usual RTMP path needs a transcode hop. StreamHub's WebSocket ingest takes the camera's raw MJPEG frames directly — and lets the same device send sensor data on the same socket. Add MQTT-in for the rest of your fleet, and every reading becomes an event you can automate on.
Architecture in words
The device opens one WebSocket to /ingest/ws, authenticated with a wsk_ key, and sends complete JPEG frames as binary messages. Viewers pull those frames as MJPEG with no relay and no transcode. On the same socket the device can send text data frames, and a separate MQTT-in subscriber pulls messages from your broker; both converge on a single iot.data event that fans out to webhooks and Streamflow.
1. Provision a WebSocket camera
export BASE="https://your-server.example.com/api/v1"
export TOKEN="sk_...."
curl -s -X POST $BASE/apps/iot/ws-ingest -H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-d '{"room":"cam1","identity":"porton-norte"}'
The response returns an id (wsi_…), a streamKey (wsk_…), and the wsUrl, mjpegUrl and playerUrl.
2. Point the device at the socket
The ESP32 connects and streams JPEG frames:
wss://your-server.example.com/ingest/ws?app=iot&room=cam1&key=wsk_...
Each binary message is one complete JPEG (FF D8 … FF D9). On connect the server sends a ready frame with the limits it will enforce (maxFps 15, maxFrameBytes, and the data-frame caps).
3. Send sensor data on the same connection
The device sends a text frame to publish a reading:
{ "type": "data", "topic": "door", "data": { "open": true, "battery": 87 } }
That becomes an iot.data event { topic, deviceId, data, source: "ws" }. Data-frame limits (from features.ws_ingest): max_data_bytes 16384, max_data_per_sec 5 — an oversize data frame is dropped (only an oversize JPEG closes the socket).
4. Ingest MQTT from the rest of the fleet
Add a subscribe list to the app's mqtt config so broker messages become the same event:
mqtt:
enabled: true
url: "mqtt://broker.example.com:1883"
subscribe:
- topic: devices/#
Each inbound message dispatches iot.data { topic, data, source: "mqtt" } (JSON when valid, else the raw string), capped at 64 subscriptions and 64 KB per message.
5. Watch the camera and trigger a workflow
View the live MJPEG with no token when public playback is on:
https://your-server.example.com/live/iot/cam1/mjpeg
https://your-server.example.com/live/iot/cam1/frame.jpg
Then, in Streamflow, a trigger.mqtt box firing on topic door (matched from either the WS data frame or MQTT-in) can capture a snapshot and notify you the instant the gate opens.
What you should see
The camera's frames appear at the MJPEG URL within seconds of the device connecting; each sensor reading and each broker message shows up as an iot.data event in your webhook log; and a Streamflow workflow keyed to the topic runs on cue.
Troubleshooting
- Socket closes immediately — check the key, app and room match the provisioned camera; a mismatch closes with
4401, a disabled feature or exceeded quota with4403. - Frames rejected — a JPEG over
max_frame_kb(256 KB default) closes with4413; lower the camera's resolution/quality. - No audio, no recording — WS ingest v1 is video and data only: no audio (the camera has no mic), no MP4/HLS recording, no adaptive bitrate. For full media, use the ffmpeg-relay-to-RTMP path instead.
- Data-out? — receiving data back on the device over the socket is not implemented yet; this wave is data-in only.
Closing
Cheap cameras and sensors, streamed and automated on your own server — that is the IoT story without a device cloud. If you want firmware, a fleet provisioning flow, or the workflows wired end to end, our team builds those around the open-source ingest as a consulting engagement.