DISPATCH · SYSTEM DESIGN · 7 MIN READ
A Baby Monitor for Cruise Cabins
Here's a deceptively simple ask: a camera watches a sleeping baby in a cruise cabin, and the parents, plus a grandparent two decks up at dinner, want to glance at the live stream from their phones. The camera should also notice when the baby cries and nudge everyone watching. All of this on the ship's WLAN, with no reliable internet.
It sounds like a weekend project. It isn't. Every assumption you'd make building this for a house with home broadband breaks the moment you're at sea. This is the architecture I'd reach for, and why.
The shape of the problem
One cabin, one camera, a handful of authorized viewers. That "handful" is the trap. It's more than one viewer, so peer-to-peer doesn't cut it, and the camera's uplink is the weakest link in the whole chain. A cheap IP camera asked to encode and send three independent streams will fall over. So the fan-out has to happen somewhere that isn't the camera.
The architecture
Why WebRTC, not MQTT
People reach for MQTT here because they've seen it in IoT diagrams. But MQTT moves messages, not video. The two belong on different planes:
- Video to a phone: WebRTC, over SRTP/DTLS. Sub-second latency, adaptive bitrate, and on a LAN there's no NAT traversal pain.
- Camera ingest: RTSP/ONVIF, what the camera natively speaks.
- Video to the cabin TV: HLS or MPEG-TS multicast, which the onboard IPTV set-top boxes already tune to. A few seconds of latency is fine for a TV.
- Events (a cry detected, a viewer joins): MQTT, which is exactly right for tiny pub/sub payloads.
- Assigning a camera, logging in, listing cabins: plain REST over HTTPS.
So the service isn't "a WebRTC thing" or "an MQTT thing": it's a control plane (REST) and a media plane (WebRTC) with a thin realtime spine (MQTT + WebSockets) tying them together.
The SFU earns its keep
The camera sends a single stream up. A lightweight Selective Forwarding Unit (go2rtc or MediaMTX, single Go binary) pulls that RTSP feed once and forwards encoded frames to each viewer. No transcoding on the app path, so it stays cheap.
Per cabin the fan-out is tiny. The number that sizes your hardware is the aggregate: a couple hundred cabins actively monitoring, three viewers each, is roughly 200 streams in and 600 out. That's comfortable for one mid-size server, but I'd still place SFU nodes per deck, not to add CPU, but to keep video traffic on the local access points instead of hauling it across the WLAN backbone.
Simulcast handles the rest: the camera publishes two or three quality layers, and the SFU hands the grandparent on a weak signal a 360p layer while the parent near the cabin gets 720p, from the same source.
Concretely, one cabin looks like this:
Cabin cam (1 RTSP, 720p H.264)
│ 1 stream up
▼
Media server (go2rtc) ── WebRTC ──► Parent A phone
── WebRTC ──► Parent B phone
── WebRTC ──► Guardian phone
└─ audio tap ──► baby-cry ML ──► MQTT alert ──► all N subscribersOnly the right people see the baby
Authorization is multi-grantee by design. The parent is the owner, granted access on check-in from the ship's PMS booking. They can invite a guardian from the app. The grant lives in a small table:
- Login issues a short-lived JWT tied to the cabin and booking.
- The app asks the API for cameras on that cabin; the server checks the grant table.
- If allowed, it issues a stream token scoped to that one camera, short expiry.
- The SFU validates that token before forwarding a single frame.
Revoking a guardian is one deleted row, and the next token request simply fails. And because a new grant publishes to cabin/{id}/cameras over MQTT, the moment a camera is assigned, the menu item appears live in the app. Tap it, fetch a token, and the stream starts. No polling.
Hearing the cry
An audio analytics service taps the camera's audio track, runs a small classifier (YAMNet, or a compact CNN over a mel-spectrogram) and publishes cabin/{id}/alert when it recognizes an infant cry. Every authorized viewer is subscribed, so all their phones buzz at once. Better still, run the model on the camera itself if the hardware allows, and then the audio never leaves the device.
The hard part nobody mentions
No internet means no APNs and no FCM. Cloud push notifications simply do not exist at sea.
This is the constraint that quietly kills naive designs. The cry alert can't ride Apple or Google's push infrastructure, because both need the public internet. On a ship it has to travel over a persistent MQTT-over-WebSocket connection the app holds while it's on the WLAN. That works beautifully while the app is foreground, and runs straight into mobile OS background limits otherwise. There's no clever workaround; it's a product decision about how the alert is allowed to reach a backgrounded phone, and it deserves to be made early, not discovered late.
Keeping the data plane lean
The database barely breaks a sweat: guests, cabins, cameras, camera_grants, alerts, and an audit log. SQLite is honestly enough for a single ship server. I'd still pick PostgreSQL for concurrent writers, durability, and a warm replica to a backup node, because cameras in cabins make the audit log non-negotiable, and privacy and consent rules deserve real rigor.
What sea duty teaches you
- Put the fan-out where the bandwidth is, never on the device with the weakest uplink.
- Match each protocol to its job; resist the urge to make one protocol do everything.
- Keep video on the local network segment, since locality beats raw horsepower on a WLAN.
- The offline constraint isn't a footnote. It reshapes auth, alerts, and storage all at once.
If you're designing streaming or AI systems for constrained, disconnected, or regulated environments, I'd be glad to compare notes. Reach out via email or LinkedIn.