v0.1.31-alpha

The manual.

Everything an operator needs, in one place: storage and retention, the main site, receiving on a PC or a Raspberry Pi, network audio, running cloud-free on one site, remote control, automation, public simulcast, and what to check when something looks wrong.

New here? The install guide gets you broadcasting in about twenty minutes; come back here for everything past that. This page is written by hand rather than generated from the repository, so if it and a doc under docs/ ever disagree, the repository is the one that's current — and an issue saying so is welcome.

Before you start

Storage and retention

Every install needs an S3-compatible bucket and a key that can read and write it. Pick a storage provider from the dropdown in any settings dialog — Cloudflare R2, AWS S3, Backblaze B2, Wasabi, or Custom — and only the fields that provider actually needs appear.

The decoder's event list also needs s3:ListBucket on that key. Cloudflare's “Object Read & Write” token includes it; an object-scoped or read-only token often doesn't, and the dock says so rather than showing an empty list.

Set a retention rule before your first broadcast

Nothing in this project deletes anything on its own. Automatic expiry is a bucket lifecycle rule you set up once, in your provider's console. Without one, every service you ever broadcast stays in the bucket and the bill grows without limit — roughly 2.7 GB per hour at 6 Mbps.

PrefixWhat it holds
events/the media — all of the volume
rooms/the per-room event index — tiny, but if it outlives the media the event list fills with recordings that can't be played

Both prefixes need the same age. Seven days is the design default, and the rule is also your DVR depth — a satellite can timeslip back only as far as retention allows.

On Cloudflare R2: your bucket → Settings → Object lifecycle rules → Add rule → prefix events/, delete objects 7 days after creation; then the same for rooms/. (rooms/{room}/live.json is rewritten on every heartbeat, so it stays fresh while a room is in use, and ageing out between services is harmless — the next Go Live recreates it.) On AWS S3, Garage, Backblaze B2 or Wasabi: the equivalent lifecycle configuration, an Expiration rule per prefix.

Testing before you rely on it

In Settings → Storage, Test connection checks the bucket exactly as typed, before you press Apply — at the main site it writes a tiny file and removes it again, which is what proves the key can really send an event. Test upload speed sends a measured burst so you know what the venue's uplink has spare before a service rather than during one.

A second bucket

One bucket is one place for an event to go missing — a provider outage, a locked account. Under Settings → Storage → Second bucket, Also write every event to a second bucket adds an independent copy: two uploads from the same queue, each with its own confirmed position, so a stall at one can't hold the other up or leave it a step behind. The encoder dock says whether the copy is keeping up and, when it isn't, why.

At a campus, Also read from a second bucket when the first cannot be reached makes it read LAN first, then the primary, then the second — staying on the primary until it genuinely can't serve an object, because the second provider's egress is a cost. The failover ends when the copy being read stops advancing, not on a timer. Expect it to double storage and origin writes, and give the second bucket its own retention rule.

Multisite Cloud: pairing instead of typing keys

For a site with a Multisite Cloud account — Stage Audio Works' hosted storage service, which is not part of this repository — pick Multisite Cloud as the storage provider and pair the machine from the dock's Cloud section with a short code, the way a television signs in. It then records to, or plays from, the account's bucket with no endpoint or keys typed; credentials are short-lived and refreshed before they expire. The Pi player's page has the same section, and so does the relay. Typed keys work exactly as before, and a machine that has them is never moved onto paired storage behind your back. A machine that is both encoder and decoder pairs once per role.

Playing from a paired bucket is verified on a Mac, with an encoder and a decoder; on a Pi it is not yet proven. A health heartbeat to a monitoring collector is optional and off by default; it sends the status the dock already shows — no pictures, no sound, no credentials, no IP addresses.

Deleting by hand, without waiting for the rule

The encoder's Manage storage… button lists the room's events and lets you delete one — or everything older than a chosen number of days — with a confirmation and a verification pass afterwards. This needs a key that can s3:ListBucket and s3:DeleteObject; the dialog says so plainly if it can't.

  • Built for a bucket holding months of events: events appear as soon as their manifests are read, and each one's size fills in beside it afterwards, six at a time.
  • A size that couldn't be measured is reported as unknown, never as 0 B — a zero against an event holding gigabytes is worse than no figure at all in a window whose whole purpose is deleting things.
  • The event currently on air is never offered for deletion. Closing the window stops the work rather than leaving it running unwatched.

Want the storage itself self-hosted, not only self-configured? The bucket only needs to speak the S3 API, so a self-hosted object store works exactly like the cloud ones above. Garage is one such project — a single dependency-free binary that runs on modest hardware, S3-compatible, with the lifecycle expiration rule described above. It ships no web interface of its own, but Garage Web UI adds one as a second container, so a volunteer still gets a page to click through. It's a separate project we haven't run this pipeline against ourselves; nothing here depends on it, the same as any other provider on this page.

MinIO used to be the usual answer here, and an existing MinIO deployment still works — the endpoint doesn't care. But its community edition is end-of-life (repository archived February 2026, binaries and images stopped October 2025, no backported security fixes). RustFS is the closest drop-in successor — the same storage model, Apache-2.0, with the built-in console MinIO had — but its lifecycle management is still marked under testing, so it isn't yet a like-for-like replacement for the rule above.

Broadcasting

The main site

  1. Open the dock and go live

    Open the Multisite Encoder dock (View → Docks) → Settings… Enter your bucket details, choose a video encoder, name the tracks, and give this machine a Site name — it is what appears on the cues this box drops. Press Apply to commit them — opening the settings mid-service and closing them changes nothing.

    Press Go live. Watch the status readout: how much of the service has been sent, how much is waiting, and the Internet line (green / amber / red). That line is live even before you go on air — the dock checks the bucket every few seconds — so a broken connection is visible before it costs you a service.

Ending the broadcast

End waits only a few seconds for the upload queue, so the event finishes on air promptly. Whatever is still waiting uploads in the background afterwards — the recording simply gets longer. The dock shows Last event still uploading while it runs; it is safe to quit OBS, since it carries on next time, and your next broadcast always goes first. Nothing is deleted unless you press Stop uploading it. If the connection is down when you press End, campuses are told the event is over as soon as it returns, rather than calling it interrupted after ten minutes.

The queue is flushed to disk as it is written, so a power cut costs nothing, and a segment damaged on disk is skipped and logged rather than sent to every campus broken. After a crash, a resume carries on as one recording with one timeline — unless the encoder's settings changed in between (resolution, codec, audio tracks), in which case a new event starts and the log says why.

Production audio

Set up in OBS itself, not in the dock. In Settings → Output → Recording, enable the audio tracks you intend to send. In Advanced Audio Properties (right-click the mixer), assign each source to its tracks — a mix on track 1, mics on their own tracks, a click on another. Name them under Settings… → Track labels so satellites see “Click” rather than “Track 3”. Every enabled track travels in the same fragment, locked to the picture.

Sending stereo only? Do nothing — track 1 is the default at both ends.

Splitting the picture into tiles

A room sending two or four cameras can composite them into one canvas and say how at Settings → Media → Pictures in this feed — one picture, two side by side, two stacked, or a 2×2 grid. Each satellite gets every region back as its own, already-cropped source, for the cost of downloading one feed.

Set this before you go live. The layout is written down when the service starts, so changing it part way through does nothing until the next one — and it can't be inferred from the picture, since a wide frame is a plausible single camera as well as a pair. Left alone, it stays on one picture.

Managing storage

See Storage & retention above — the Manage storage… button lives on this dock.

Receiving

Receiving in OBS

  1. Add the source

    Open the Multisite Decoder dock and enter the same bucket details under Settings… — stored per machine, so every source you add afterwards is already configured. Add a Multisite Source (Decoder) to a scene.

  2. Follow, buffer, play

    Press Follow live, let the buffer fill, then Play when ready. The status line counts the buffer up against the gate (Filling buffer — 23 s of 60 s), so a slow link reads as slow rather than as stalled. Use Lock during the service so nothing gets clicked by accident.

    The decoder holds playback until a whole minute of the event is buffered (set in Settings… → Start after this much is ready). The buffer fills first, then the picture starts — so it doesn't chase the live edge and stall after one piece on a slow or uneven connection.

Reading the status box

The Internet line tells you whether the box can reach the bucket, separately from whether anything is on air. If it flips to red (“no connection”) mid-service, the Could broadcast for figure is how long the picture will keep going from what's already downloaded — enough notice to act, rather than a surprise when the picture freezes.

Cues

A cue — “Sermon start”, “Go to local” — belongs to the event, not to the main site. The Multisite Cues dock is on every machine whatever its role, shows one merged list, and lets any site add to it with any name; each cue carries the name of the site that set it, so one dropped at another campus is never mistaken for the main site's. A satellite can drop a cue with a key scoped to its own file, and nothing else. Cues are ordered by where they fall in the event, not by anybody's clock — and a machine whose clock is plainly wrong is told so.

Recordings and timeslipping

To play something other than the live event, use the Recordings list: pick a past one and press Load recording. Playback stays on it — if a new event starts mid-watch, the dock offers the switch rather than taking it, since being pulled out of a recording you're part-way through is worse than being told. The same goes for an event you were following once it ends: the next one starting doesn't take the picture away either. Back to live returns to following the room.

Each satellite runs its own clock: hold the picture for a local welcome, resume, then catch up to now — or sit a set number of minutes behind live all service. Jog and scrub move within what's buffered. Hotkeys for all of it are in Settings → Hotkeys; see Stream Deck & automation.

Extra tracks and picture regions

The Multisite Source carries the video plus one audio track (track 1 by default). To bring in another as well, add a Multisite Audio Track (Decoder) source for the same room and pick the track — it attaches to the decoder already running, so it costs no extra download. If the main site is sending tiles, add a Multisite Picture (Decoder) source per region instead of cropping by hand.

Deciding

OBS or the Pi appliance?

A satellite can receive one of two ways, and they suit different rooms. Neither has yet run a real service.

OBS on a PC

The decoder is a source in a scene, so everything OBS does applies to the relayed picture: lower thirds and campus graphics keyed over it, cutting away to a local camera or worship set and back (the decoder keeps downloading off-screen, so no re-buffering on return), simulcasting the campus feed locally while it plays in the room. DeckLink, AJA and NDI in and out; Dante or AES67 audio out through OBS's own monitoring device. Choose this where the campus produces around the relay, or needs existing SDI, NDI or Dante infrastructure.

The Pi appliance

A fixed-function box: no scene, no overlays, no local sources. It plays the relayed event and nothing else, on hardware that costs less than a monitor, boots into the event on power-up, and is driven from a phone with no desktop to leave in the wrong state. Choose this where a room needs the service on a screen and nothing more — an overflow room, a chapel, a plant meeting in a school hall.

More than one camera angle, in guaranteed sync

The protocol carries exactly one video stream per event. A room that needs several angles at a satellite — a wide shot and a stage-left ISO, four SDI sources for a video wall — should composite them into one canvas at the main site first (see tile layout above) rather than send several streams: every camera is then a region of the same decoded picture, so there's no possibility of the angles drifting apart the way independently-encoded streams could. The cost is bandwidth — a composited 4K canvas costs roughly what a single 4K stream does, whatever number of cameras are inside it.

Any machine running OBS can be the origin, not just the usual main site — a guest speaker publishing from a hotel, a conference venue for a week, a second campus hosting this week's combined service. Adding one costs a room name and a key that can write to it; nothing is licensed per location. Because segments are written to disk and resent until storage confirms them, an occasional origin on the worst connection in the chain survives a link that would kill a direct stream. Keep rooms separate — a guest publishes to guest-speaker, not to main-auditorium — so it's never mistaken for the main programme.

The Pi appliance

Installing it

On stock Raspberry Pi OS Lite (64-bit):

curl -fsSL --retry 5 \
  https://raw.githubusercontent.com/stageaudioworks/obs-multisite/main/scripts/player/install.sh \
  | sudo bash

That installs the dependencies, builds the player, installs it as a service that starts on power-up, and puts a screen up on the HDMI output showing the box's own address and a QR code of it — point a phone at the screen and the control page opens, nothing to type. Everything else is done from a phone or tablet on the same network: storage credentials, which room to follow, output resolution and frame rate, the sound device, the clock, and the transport controls during a service.

  • It owns the display. The player sets the KMS mode itself, so the output resolution and frame rate are exactly what was asked for, with no desktop to be left in the wrong state.
  • The cache belongs on a USB SSD. It writes roughly 3 GB an hour, which will wear an SD card out. The installer looks for a USB drive and uses it; if there's none, both it and the interface say so.

Running it by hand

To watch it directly instead of through the service:

sudo multisite-player --config /etc/multisite-player/config.json --verbose

journalctl -u multisite-player -f is the whole diagnostic story; the last few hundred lines are also in the interface, under Log, for an operator with a phone and no SSH.

Display, idle screen and hold

idle_mode says what the box shows when there's nothing to play: the box's own details, black, or a holding slide.

A feed laid out as two pictures side by side can go out with the left half on one screen and the right half on the other, from one download and one decode.

Hold beats the idle screen. Pressing Hold picture is an operator asking for the frame in front of them to stay — so the held frame stays up whatever idle_mode is set to, and changing the idle screen mid-service can't replace a picture that was deliberately frozen. Stop and waiting for the main site are the deliberate acts the idle screen is for, and both still show it.

The web UI's preview is a copy, not a second output — watching it doesn't change or interrupt what's on the screen in the room. With a tile region selected the screen shows one region while the interface offers both views: what's going out and the whole feed, each with its own fallback frame so switching between them never blanks the picture.

When an event ends, an unattended box picks up whatever the room does next, which is what a satellite relaying a whole day should do. If this screen is deliberately holding a recording, set When the event ends to hold instead — the Follow whatever is live button on the front page still takes it back to the room on demand.

Audio over HDMI, and the cache drive

Up to eight channels of LPCM leave over HDMI, recovered at the campus with a de-embedder. If the device won't take every channel the feed carries, the player says so loudly rather than silently dropping the click — this is the one place packed multi-channel is the better mode, since eight channels in one stream map straight onto HDMI's eight, in order, with nothing to route. A box fed multi-track instead plays one chosen track — the first by default, with a picker in Settings.

To put the sound on the network instead of leaving it in the HDMI picture, see Network audio (AES67) below.

Remote access, so the box doesn't need a visit

What makes a wrong setting at a campus expensive is that fixing it means driving there. The installer can bring up two optional tools, and either can be turned on later from Settings → Remote access:

  • ZeroTier puts the box on a private network that follows it, reachable from the office wherever it's plugged in — pass ZT_NETWORK_ID=… to the installer or type it at its prompt.
  • cloudflared publishes the control page on a public hostname with no port-forward and no static address — pass CF_TUNNEL_TOKEN=….

The box's ZeroTier address is shown on its own screen, labelled REMOTE ACCESS IP and kept well apart from the in-room addresses — those are typed into a phone standing in the building, this one isn't. Neither tool is required to play a service; a box with neither says nothing about remote access and behaves exactly as before.

Uninstalling

The mirror of the installer:

sudo bash scripts/player/uninstall.sh

Or, on a box with no checkout:

curl -fsSL \
  https://raw.githubusercontent.com/stageaudioworks/obs-multisite/main/scripts/player/uninstall.sh \
  | sudo bash

It stops and disables the service and removes its unit, the program, the web page, the checkout and build tree, the settings and the downloaded event — including a cache on a USB SSD, not just under the default path. Every step looks before it acts, and the run ends by checking the same list again.

FlagEffect
--checkreports what's on the box, changes nothing
--dry-runprints every change, makes none
--keep-configremoves the software, leaves settings and the downloaded event
--keep-cachekeeps only the downloaded event
--stockthe player and the AES67 stack, plus the packages that existed only to build them — one command back to stock, leaving ZeroTier on and up
--purge-remote-accessalso removes ZeroTier and cloudflared (refused alongside --stock)
--purge-aes67also removes the RAVENNA module and aes67-daemon, and puts PulseAudio back

Remote access and AES67 are decisions, not defaults, so neither is touched unless asked — the tunnel may still be how the box is reached, and the sound may still be on the network. ZeroTier is not merely left alone when it is kept: the run checks it is enabled and running, starts it if something had stopped it, and prints the address and the networks the box is on, because a box that has just been cleared with nobody able to log into it is worse than one that was never cleared at all.

Network audio

Installing AES67

Out of the box the sound leaves on the HDMI socket with the picture. A campus that wants it on its own console instead — a separate feed, its own level control, working whether or not a screen is attached — needs it on the network, and on a church network that means AES67:

curl -fsSL \
  https://raw.githubusercontent.com/stageaudioworks/obs-multisite/main/scripts/player/merging-aes67.sh \
  | sudo bash

This installs an open stack built on Merging's ravenna-alsa-lkm kernel module (which registers an ordinary ALSA sound card) and the GPL aes67-daemon that talks to it and does RTP, SDP/SAP and PTP. It builds from source, so give it a few minutes. One run installs the build dependencies, builds the module for the running kernel, builds the daemon, installs both with a systemd unit, and arranges for the module to load at boot — and it's safe to run again, reusing an existing build tree.

  • --check reports what's already on the box and changes nothing — the first thing to run on a machine you didn't install yourself.
  • --rewrite-config is needed to touch an existing /etc/daemon.conf; otherwise it's left alone.
  • A kernel upgrade means rerunning this script — the module is built from source against the running kernel and deliberately isn't put through DKMS.

A PTP master has to exist on the network, or nothing flows. The daemon slaves to a clock; it doesn't hand one out. A Dante device, a console or an Anubis can be that master — with none of them, the WebUI never says locked and there's silence. This is the most likely reason for quiet after a clean install, and it's a network question rather than a fault. The daemon's own WebUI is at http://<pi>:8081 — 8081, not the project's 8080, since the player's own interface already uses 8080 on this box.

The installer creates one stream as part of the install — eight channels, multicast, 1 ms packets, the AES67 defaults — so a box that's just been set up is already sending. The source appears in Dante Controller, but the route from it to a receiver is still made by hand in that application; test against the real Dante hardware the site will use.

Switching it on, and reading it back

Two places in the player's own page (port 8080):

  • Settings → Network audio output. The switch, the multicast address, and the channel count. One button applies all three, because they're one decision: switching it on starts the daemon, sets it to come back after a power cut, and creates or corrects the stream. Switching it off stops the stream rather than deleting it, so the address and width are kept and turning it back on is one click. It sits behind Lock, like other settings that change what's on air.
  • This box → Network audio output. What's actually being sent, read back from the daemon rather than assumed: whether it's running, whether the clock is locked, to which grandmaster, and how tightly — the lock's own jitter figure, which is the number to compare against a receiving console's — the address and port on the wire, and the SDP it publishes, which is what a console engineer will ask for.

The switch moves the sound. There's one output device on this box, not two, and the daemon publishes whatever is written to the AES67 card — so turning this on means the player writes to that card instead of HDMI, and turning it off puts the sound back. The output device picker under Sound is greyed out while this is on, because letting the two disagree would produce a stream that looks healthy and carries nothing.

It can't lock the clock for you — if the page says the clock isn't locked, the fault is on the network, not on this box. And if the AES67 stack isn't installed, both places say so rather than offering a switch that would only fail.

Reading the meters

The meters under the Play tab are taken from the one place that decides everything: where the samples are handed to the sound card — not from the decoded feed. A meter of the feed says an event carries sound; it can't tell you whether the box is putting it anywhere. Every real fault here — a card that won't open, a mute somebody forgot, a stream that's off the air while the feed is perfectly healthy — is invisible on a feed meter and obvious on an output meter.

ReadingWhat it meansWhat to do
playingCard open, sound on, frames arriving with signalNothing
feed-silentFrames arriving, no signal in themThe event carries no sound — a muted mic upstream, or a track never in the feed
idleCard open and fed, but nothing is being deliveredNothing, if the box is stopped, held or between events
mutedSwitched off in SettingsSwitch it back on under Sound
card-closedThe card isn't openA fault — the reason is beside it in the Sound readout

Muted and card-closed both show a flat meter and are not the same thing — the first is a decision, the second is a fault. Muting writes silence to a card that stays open rather than closing it, because on a box whose sound leaves over the network, closing the card takes the stream off air and receivers won't get it back until they re-subscribe. The panel is drawn at the width of the card, not the feed — a stereo card fed a six-track feed shows two bars, because two is what's actually leaving the box.

Networking

Skip the cloud, on one network

A satellite that shares a network with the main site — or reaches it over a VPN the church already runs — doesn't have to go through the bucket at all. It can pull straight from the encoder instead, with cloud storage as an automatic fallback.

Main campus Encoder, on the LAN Your S3 bucket Cloud, fallback path Same network / VPN Plain HTTP, no bucket in the path Satellite Prefers LAN automatically still uploads, in case preferred LAN, per segment fallback only
  • On the Multisite Encoder dock, open Settings → Storage and turn on LAN delivery. The encoder starts serving segments directly over plain HTTP on the network.
  • On each satellite that can reach it — the OBS decoder or the Pi appliance, both support this identically — enter the encoder's LAN host, port and token in its own storage settings. It prefers that path automatically the moment it answers, and falls back to cloud per segment, not per session the instant it doesn't — so one segment that happens to have aged out of the LAN side's retention falls back for that segment alone, rather than dropping the whole connection to cloud.
  • Cloud upload stays on by default throughout, since every other satellite and the archival recording still depend on it. A checkbox appears once LAN delivery is on to turn it off entirely for a service — with a confirmation prompt, since that direction isn't easily undone mid-service. Turning it back on needs no confirmation, since that's always the safe direction.

With cloud switched off, nothing for that event reaches the bucket at all. Every satellite still gets the full service, provided it can reach the encoder directly — a satellite that can't, or one joining from elsewhere, needs cloud back on.

The public simulcast relay uses the same LAN-preferred, cloud-fallback logic for its live feed, so a fully cloud-off encoder can still be relayed to YouTube or Facebook if the relay sits on the same network. Browsing, downloading and rebroadcasting past events stays cloud-only regardless — those list the bucket, and an encoder's LAN side only ever holds the event currently in progress.

Control from a phone

Both docks — and the Pi appliance — have a Remote control group in their settings. It serves the campus player's own operator interface on the church network: one page, polled twice a second, in the plain language of a service, so a marker can be pressed from the back of the room and the queue watched from the foyer. The address to type into a phone is shown in that group, and is selectable. It's on by default on port 8080, and binds every interface — a page that only answered localhost couldn't be reached from the tablet it exists for.

There is no password and no TLS. The building's own network is the guard. If that isn't the trust you want, switch it off in the same group.

What the encoder's page offers

  • Go live and End the broadcast, with the same editable event name the dock has.
  • A row of cue buttons, one per cue name the event already has — a tap to drop that cue, or type a new name.
  • A live readout: confirmed pieces, what's waiting to send, retries, bytes sent, the measured upload rate, the Cloudflare edge serving the bucket, and the last error if there is one.
  • Settings — the same fields as the dock, with the secret key never shown; the field is left as dots to mean “unchanged”.
  • Log — the last few hundred [multisite] lines, for somebody not sitting at the machine.

What the decoder's / appliance's page offers

Play, hold, catch up, jog, stay behind live, the recordings list, and the readout that says how long this campus could keep playing through an outage — reaching the same controls the hotkeys use, so a page and a keypress can never disagree about what they did.

Lock, in the top bar, refuses anything that would change what's on air while it's on — deliberately not remembered across a restart, since a lock that survived one would leave a campus unable to broadcast with no obvious reason why. Which pages exist follows the machine's role: a main site serves the encoder page only, a satellite the decoder page only, a machine set to Both serves both and links them.

On Windows, the first start raises the usual firewall prompt — allow it for private networks, or the page won't answer from another device.

Stream Deck and automation

A volunteer running a service reaches for a physical button, not a window. Two ways to give them one:

Hotkeys, today

Play, stop, hold, resume, catch-up, jog and markers are registered in Settings → Hotkeys. Companion's OBS module can trigger a hotkey by id, so they work on a Stream Deck now — but a hotkey carries no parameters and shows no feedback, so the button can't light up while an event is live.

The obs-websocket API

Every control the pages offer is also an obs-websocket vendor request, so any obs-websocket client — Companion's Custom Vendor Request action, a script, another automation system — can call them, under the vendor obs-multisite:

RequestWhat it does
encoder/status, decoder/statusthe same document the page polls
encoder/go-livego live, with an optional event_name
encoder/end, encoder/markerend the broadcast; drop a marker (label)
encoder/settings, decoder/settingsread, and apply a partial document
decoder/play, stop, hold, continue, catch-upthe transport controls
decoder/jog (seconds), decoder/seek (ms), decoder/delay (seconds)navigate
decoder/marker (id), decoder/load-event (event_id), decoder/return-to-livemarkers and recordings
decoder/events, decoder/events/refreshthe recording list

Each request answers with the new status, so a client never has to guess what its own button did, and a refusal says why in an error field. Alongside the requests, the plugin emits events — encoder/state and decoder/state — whenever something an operator cares about changes, which is what lets a Companion module light a button or show “12 s behind”.

Nothing changes for a church that hasn't turned obs-websocket on — the plugin logs one line and carries on. It ships with OBS 28+ and is enabled in Tools → WebSocket Server Settings.

The ready-made module

companion-module-obs-multisite is a Bitfocus Companion module carrying every action above, the feedbacks that light a button while an event is live or a campus is held, behind live or offline, the variables, and a preset bank for each half. It also drives the Pi appliance directly, over its own HTTP API, so a satellite needs no OBS at all.

Going further

Public simulcast

relay/ is a small self-hosted service that reads the same segments and pushes them out to YouTube, Facebook, any RTMP destination, or over SRT to anywhere that prefers it — relaying from the bucket rather than adding a second output to OBS, so the main site uploads once whether the event is going to two campuses or to two campuses and the internet.

docker run -d --name multisite-relay \
  -p 8080:8080 \
  -v multisite-relay-data:/data \
  -e RELAY_ROOM=main-auditorium \
  ghcr.io/stageaudioworks/multisite-relay:latest

Then open it in a browser, put in the bucket details, and add a destination. A $5/month VPS is the target rather than a stretch, since nothing is being re-encoded.

  • It has a login, and binds to localhost by default. Put HTTPS in front of it — relay/Caddyfile.example or relay/nginx.conf.example is a working config.
  • One chosen sound feed per destination, picked by the name the main site gave it, never a track number.
  • A delay you choose, three minutes by default — how much of the event the relay holds in hand, and so how long an outage at the main site can last before the public sees it.
  • It reconnects and resumes by itself, riding out a silence under 45 seconds without even dropping the connection, and tells apart a failure receiving from the main site and a failure sending to the destination.
  • RTMP or SRT, decided by the address you paste — rtmp:// or srt://. SRT can also listen, for a partner that pulls from you: leave the host out of the address, srt://:9000.
  • HEVC goes out either way, unchanged. Over SRT it travels in MPEG-TS, which has carried it for a decade; over RTMP it travels as Enhanced RTMP — the extended video tag YouTube documents alongside H.264, and recommends over RTMP(S) for HDR. Somewhere that has never implemented it will drop the stream, which is worth knowing before it surprises you.
  • It refuses rather than guesses — packed multi-channel audio is declined with a plain explanation. HEVC and AV1 both go out over RTMP as Enhanced RTMP, so what matters there is whether the destination takes them, and the page says so before you start. AV1 over SRT is still refused: MPEG-TS has no AV1 stream type to put it in.

Can one service go out in several languages?

Yes, and from the same upload. Interpreter feeds arrive as separate audio tracks — each one named — and the relay sends a different track to each destination: English to one stream, Spanish to another, a clean feed to a partner. Every destination reads the same files, so the main site uploads once however many languages there are, and nothing is re-encoded on the way.

The picture is the same on all of them, because there is no transcoder here — only the sound can differ, and there is no adaptive ladder. Each language needs its own stream key at the far end, each one costs the relay's uplink another copy of the bitrate, and there is no in-browser player yet: today's “web” means YouTube, Facebook, or an SRT receiver.

For a finished event, it can download one as an MP4 streamed straight from storage (no disk cost, every audio track included), or replay one to a destination as though it were happening now — one at a time, and it cannot schedule or loop itself. Two of the event’s cues can mark where the replay starts and stops, so a repeat can be an excerpt rather than the whole recording.

Full deployment notes, including bandwidth and disk sizing, are in relay/README.md.

Troubleshooting

  • Read the OBS log first. Help → Log Files → Show Log Files. Both plugins log everything they do under [multisite], including why they refused something. On a Pi, journalctl -u multisite-player -f is the same idea.
  • An empty recordings list usually means the token can't ListBucket. The dock says so rather than showing you nothing — see Storage & retention.
  • The plugin doesn't appear in View → Docks on macOS — the quarantine flag is still set: xattr -dr com.apple.quarantine …/obs-multisite.plugin, then restart OBS. See the install guide for the full command.
  • A satellite is a long way behind and not catching up — that's timeslipping working as designed. Press catch up to now.
  • AES67 is installed but silent — almost always a missing PTP master on the network, not a fault in the install. See Installing AES67.
  • A Windows remote-control page won't answer from another device — allow it through the firewall for private networks on first launch.

A good bug report is a real contribution: much of what works well here was fixed because somebody took the time to paste a log. Open an issue →