DONTHI.DEV
HAM--:--:--
← ALL DISPATCHES

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

Cruise cabin baby-monitor streaming architecture An IP camera feeds an edge media server (SFU) plus an audio cry-detection service. Those connect to core services (auth, REST API, signaling, and an MQTT broker) backed by a SQL database, and serve the mobile app, cabin IPTV, and an admin portal over the ship WLAN. IP camera (cabin) RTSP/ONVIF · H.264 · Opus Media server / SFU go2rtc · MediaMTX Audio analytics baby-cry ML (YAMNet) Auth · JWT REST API Signaling · WS MQTT broker SQL DB SQLite / PostgreSQL WebRTC video + WS events HLS / MPEG-TS multicast HTTPS REST + auth Mobile app iOS / Android · WebRTC Cabin IPTV / TV STB · multicast Admin portal assign cam ↔ cabin video / media path events / control path
ONE STREAM UP FROM THE CAMERA, FANNED OUT TO MANY VIEWERS BY AN EDGE SFU.

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:

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 subscribers

Only 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:

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

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.

← ALL DISPATCHES OPEN A CHANNEL →