← Back to the blog
Guides #multi-camera

Recipe: stream an art exhibition with multiple cameras

Put every gallery room online at once: one ingress per camera, public play pages and embeds per room, optional recording to your own S3, and date/time plus watermark overlays. A hands-on multi-camera build.

4 min read
Leer en español

Why a gallery streams

A vernissage happens once, in one city, for the people who can be in the room. Streaming it turns a single evening into a global opening and a permanent archive — collectors abroad watch live, the press embeds a feed, and the recording becomes part of the show's catalogue. For a museum or gallery, that is reach and provenance for the cost of a few IP cameras.

This recipe wires one live channel per room, all under a single app, with public players you can drop into any page.

Architecture in words

Each camera becomes one ingress and therefore one room. An IP camera is pulled over RTSP; a phone or an OBS machine pushes over RTMP. Every room gets a public /play page, an iframe /embed, and an HLS playlist for scale. Overlays (a corner watermark and a CCTV-style timestamp) render on the player, and recording — when you want it — writes each room to the app's own S3 bucket as a VOD.

1. Create the app

export BASE="https://your-server.example.com/api/v1"
export TOKEN="sk_...."     # the seeded global token

curl -s -X POST $BASE/apps -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"name":"gallery","displayName":"Gallery Live"}'

2. One ingress per room

Pull an RTSP camera (transcoding stays on by default, which is what HLS needs):

curl -s -X POST $BASE/apps/gallery/ingress -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"inputType":"url","room":"hall-a","url":"rtsp://camera.local/stream"}'

For a room where a curator streams from OBS or a phone, create an RTMP ingress instead:

curl -s -X POST $BASE/apps/gallery/ingress -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"inputType":"rtmp","room":"hall-b"}'

The RTMP response returns everything the encoder needs — rtmp_url (rtmp://your-server.example.com:1935/live/<key>), stream_key, plus a public player_url and embed_iframe. Repeat once per camera: hall-a, hall-b, sculpture-garden, and so on. Each ingress counts against the app's concurrent-stream quota.

3. Add the overlays

Install and configure the two player-overlay plugins. Every field has a default, so you only send what you want to change:

curl -X POST $BASE/apps/gallery/plugins/timestamp/install -H "Authorization: Bearer $TOKEN"
curl -X PATCH $BASE/apps/gallery/plugins/timestamp -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"enabled": true, "config": {"format": "datetime-24h", "position": "bottom-left", "color": "#ffffff", "showName": true}}'

curl -X POST $BASE/apps/gallery/plugins/watermark/install -H "Authorization: Bearer $TOKEN"
curl -X PATCH $BASE/apps/gallery/plugins/watermark -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"enabled": true, "config": {"text": "Museo Example", "position": "bottom-right", "opacity": 0.6}}'

Watermark defaults are text: "StreamHub", position: "bottom-right", opacity: 0.6; timestamp defaults are format: "datetime-24h", position: "bottom-right", color: "#00e5ff", showName: true.

4. Publish the public gallery

Every room is now watchable with no token:

https://your-server.example.com/play/gallery/hall-a
https://your-server.example.com/embed/gallery/hall-a
https://your-server.example.com/hls/gallery/hall-a/index.m3u8

Build a simple landing page with one iframe per room and you have a live multi-camera gallery:

<iframe src="https://your-server.example.com/embed/gallery/hall-a"
        width="640" height="360" frameborder="0" allowfullscreen></iframe>

5. Optional — record each room to your S3

Point the app at your bucket, then start a recording per room:

curl -X PUT $BASE/apps/gallery/s3 -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"provider":"aws","bucket":"gallery-archive","region":"us-east-1","key":"AKIA...","secret":"..."}'

curl -s -X POST $BASE/apps/gallery/recording/start -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"roomName":"gallery-hall-a"}'

For a long opening, set recording.split_minutes (0, 15, 30, 60, 90 or 120) so each part becomes its own VOD, and recording.snapshot_seconds for periodic thumbnails.

What you should see

Each camera appears in the app's live tab within a moment. The /play pages show the feed with the timestamp and watermark drawn on top, and once egress finishes a recording, the VOD turns ready with a playable URL in your bucket.

Troubleshooting

  • RTSP room never goes live — confirm the server can reach the camera URL (a private camera.local must resolve from the server, not just your laptop), and that the camera speaks H.264.
  • Overlays missing from the recording — that is expected. Watermark and timestamp are drawn client-side on the player only; they are not burned into the recorded MP4 or any outbound RTMP. Treat them as a presentation layer.

Closing

StreamHub is open source, so a gallery can run this on a single VM it controls and keep every recording in its own archive. If you would rather hand off the camera provisioning, the public embed page and the archival workflow, that is exactly the kind of turnkey install our team does as a consulting engagement.