Reference

Technical guide

How the three pieces fit together, every setting that matters, and what to do when a publisher will not connect. Covers SenderoGo Mobile, SenderoGo Receiver for macOS and Windows, and the sdgo Linux CLI.

Overview

SenderoGo is three programs that speak SRT to each other. Nothing passes through a SenderoGo server, and none of the pieces requires an account.

PieceRuns onJob
SenderoGo MobileiOS 15+, Android 8+Captures, publishes over SRT or RTMP, and keeps a full-quality copy on the phone
SenderoGo ReceivermacOS 13+, Windows 10 1809+Listens on one port, previews and records every publisher, forwards on
sdgoDebian 13+/Ubuntu 24.04+, amd64 or arm64The same receive hub with no GUI — for a headless box or a server

The shape is always the same: one UDP port on the receiver, many publishers, demultiplexed by streamid. Each publisher gets its own preview tile and its own recording file. The receiver is the listener; phones and encoders are callers.

The three-minute version

  1. Install the receiver and open it. It shows a LAN address, and a tailnet address if you run Tailscale.
  2. Set a passphrase and press Start Listening.
  3. Open the publisher setup sheet on the receiver and scan its QR code with the phone. That fills in address, passphrase, and capture settings in one step.
  4. Go live on the phone. A tile appears, and recording starts by itself if auto-record is on.

SenderoGo Mobile

The publisher. It captures from the phone's cameras and microphone, sends a live SRT or RTMP stream wherever you point it, and — the part that matters on a bad network — writes a full-quality copy locally at the same time. If the uplink degrades or drops entirely, the local file is unaffected.

Where it can stream

Any SRT or RTMP endpoint: SenderoGo Receiver, sdgo, MediaMTX, NGINX-RTMP, OBS, YouTube, Twitch, or your own server. SenderoGo adds nothing proprietary to either protocol.

Encryption is a property of the endpoint, not of the app. SenderoGo Receiver and sdgo refuse unencrypted publishers outright. A third-party endpoint may happily accept a stream with no passphrase — check before assuming yours is encrypted.

Importing a profile

Rather than typing an address and a 10–79 character passphrase on a phone keyboard, take them from the receiver: its publisher setup sheet renders the same payload as a QR code or as JSON you can hand to the phone as a file. Both apps accept it — iOS through its stream-import handler, Android through its profile importer.

The payload is a small JSON object describing the publisher's capture settings. It has no effect on the receiver.

{
  "type": "stream",
  "name": "Studio Mac — Manual",
  "protocol": "srt",
  "url": "srt://192.168.1.42:9000",
  "streamId": "cam1",
  "passphrase": "a-long-passphrase",
  "resolution": "1080p",
  "fps": 30,
  "latency": 120,
  "bitrate": 4000,
  "adaptiveBitrate": true
}
FieldValuesMeaning
type"stream"Required discriminator — both importers reject a payload without it
nametextProfile name as it appears in the phone's list
protocol"srt"Transport
urlsrt://host:portBare — passphrase and stream id ride as their own fields, not as query parameters
streamIdtext, optionalNames this publisher on the hub. Omit it and each phone keeps whatever it already has, which is usually what you want: the hub refuses two publishers sharing an id
passphrase10–79 chars, optionalSRT encryption passphrase. The length range is a protocol rule, not ours
resolution720p, 1080p, 4kCapture resolution
fpsintegerCapture frame rate
latencymillisecondsSRT buffer. Higher rides out worse networks at the cost of delay — 120 ms suits a good LAN, 200–500 ms suits cellular
bitratekbpsVideo target. 4000 is 4 Mbps
adaptiveBitratebooleanLet the app lower the bitrate when the link degrades instead of dropping frames

A profile exported from the receiver defaults to 1080p, 4 Mbps, 120 ms, 30 fps, adaptive bitrate on, with the stream id left blank.

Uploads

Streaming sends a live copy; uploading sends the file. When the event is over — or the moment each recording finishes — SenderoGo Mobile can push the full-quality recording to an HTTPS endpoint you run, so the footage reaches your newsroom, bucket, or asset system without anyone plugging in a cable.

There is no SenderoGo upload service. You supply the URL; the app speaks ordinary HTTPS to it. The receiver plays no part in this — uploads go straight from the phone to your endpoint.

Two request shapes

ModeRequestUse it for
Multipart (default)POST of multipart/form-data, the file under a file fieldAny handler you write yourself — PHP, Node, Python, Go — which receives it with no special work
Raw bodyPOST or PUT with the file bytes as the body, application/octet-stream by defaultObject stores and SaaS APIs: S3, GCS, Azure, R2, B2, Dropbox — usually via a presigned URL

Multipart is always POST; the method choice applies to raw uploads only. Either way the body is streamed from disk, so a multi-gigabyte recording never has to fit in memory.

Authentication and headers

  • An optional token is sent as Authorization: Bearer <token>. It is stored in the iOS Keychain or Android's encrypted preferences, never in plain settings.
  • Anything else your service wants goes in custom headers — an API key under its own name, a Dropbox Dropbox-API-Arg, a signature header.
  • A Content-Type you set as a custom header becomes the body's media type rather than being sent as a second, conflicting header.
  • {filename} is substituted with each file's name — percent-encoded in the URL, raw in header values. https://example.com/ingest/{filename} gives every recording its own path.
  • HTTPS is mandatory. An http:// endpoint is rejected before anything is sent.

What gets uploaded, and when

  • Automatically, if you turn auto-upload on: each recording is queued the moment it finishes.
  • Manually, from the app's files list — including files that aren't SenderoGo recordings, since you can pick anything on the device.

Transfers run in the background: on iOS through a background URLSession that survives leaving the screen, backgrounding the app, or the process being suspended; on Android through a scheduled work job that waits for a network and survives the app being closed. An upload history lists what went where, with a retry button on anything that failed.

Retries are deliberate about what is worth retrying: network errors and server-side 5xx, 408, and 429 responses are retried with exponential backoff, up to four attempts. A 4xx from your server — bad token, wrong path, rejected file — is reported as a failure straight away rather than hammered.

Provisioning by QR or file

Typing an endpoint and a token on a phone is the same problem as typing a passphrase, with the same answer. Both apps accept an upload-settings payload by QR scan or file import — hand the same JSON to a whole crew and every phone is configured identically.

{
  "type": "upload",
  "url": "https://example.com/ingest/{filename}",
  "token": "your-api-token",
  "bodyMode": "raw",
  "method": "PUT",
  "autoUpload": true,
  "headers": { "X-Api-Key": "…" }
}
FieldRequiredMeaning
typeyesMust be "upload". A "stream" payload is refused here, and vice versa
urlyesHTTPS endpoint that accepts the upload. May contain {filename}
tokennoSent as Authorization: Bearer. For any other format, use a custom header instead
bodyModeno"multipart" (default) or "raw"
methodno"POST" (default) or "PUT" — raw mode only
autoUploadnotrue uploads new recordings as they finish
headersnoObject of extra request headers. Values may contain {filename}

Receiving the upload

Anything that accepts an HTTPS file upload works. The smallest useful endpoint is a handler that reads the file field of a multipart POST and writes it to disk; the smallest useful raw setup is a presigned URL from your object store, with bodyMode: "raw" and method: "PUT". Since presigned URLs expire, they suit a per-shoot handout rather than a permanent profile.

SenderoGo Receiver (macOS and Windows)

One app, the same behaviour on both platforms: a multi-publisher receive hub with previews, recording, and forwarding. It listens; it never dials out to publish.

Listening and authentication

Pick a UDP port — 9000 by default — and a credential mode. Both gates below are enforced during the SRT handshake, before any media is accepted.

ModeBehaviour
Universal passphraseOne shared passphrase, 10–79 characters. Any stream id may publish with it
StreamID–passphrase pairsAn allowlist: every stream id has its own passphrase, and an unlisted id is refused outright. Entries are individually revocable

Switching modes is not destructive — leaving pairs mode parks the allowlist rather than deleting it.

Who may publish, by address

A second, independent gate. A publisher has to pass both.

ScopeAccepts
This networkPrivate ranges only — 10/8, 172.16/12, 192.168/16, link-local, loopback. The venue Wi-Fi default
This network + TailscaleThe above plus the tailnet range (100.64.0.0/10)
Tailscale onlyThe tailnet alone. On a hostile venue network, nothing on the LAN reaches the hub
Any addressNo address filter at all. Only for a deliberate port-forward

Two further limits guard the port: a cap on concurrent streams, 16 by default, and per-source-IP throttling of repeated handshake attempts, which blunts both floods and passphrase guessing.

Feeds

Each publisher gets a tile. Per feed you can turn preview off — it keeps recording, since decoding is the expensive part — unmute its audio, pause it, or slice the recording, which finalizes the current file and opens the next without dropping the connection. A paused feed's reconnection attempts are refused until you resume it.

Recording

  • Two containers: fragmented MP4, the default, and MPEG-TS. Files are named for the stream id and the time the recording started.
  • Streams are written as they arrive and finalized when recording stops. In MP4 the index is written at the end of the file, which is what players and editors expect from this kind of capture.
  • If the app is interrupted mid-recording, the finished portion is recovered on the next launch rather than lost.
  • Auto-record starts a recording the moment a publisher connects. With it off, feeds preview until you arm them.

Forwarding

A forward destination is a URL plus one or more triggers. Any feed whose stream id matches a trigger is re-streamed there while it records — a stream copy, no transcode, so it costs almost no CPU. SRT and RTMP destinations are both supported, and the transport is taken from the URL scheme. A destination can be switched off without being deleted.

Running out of disk

Recording onto a volume that fills up is the classic way to lose an event. The receiver treats free space as a first-class constraint, with the same thresholds on every platform.

WhenWhat happens
Under 1 GiB free as a recording would startThe recording is refused and the reason named. The stream itself keeps running and previewing
Under 120 s of headroom at the current arrival rateOne warning, recording continues, and stale temporary files older than an hour are reclaimed
Under 200 MiB free, or under 30 s of headroomClean stop: the file is finalized and saved. The stream stays connected

The consequence worth remembering: a full disk costs you the recording, never the live stream.

Where things live

WindowsmacOS
Application%LOCALAPPDATA%\Programs\SenderoGo Receiver — per-user, no administrator prompt/Applications
Settings%APPDATA%\SenderoGo Receiver\settings.jsonStandard app preferences
PassphrasesEncrypted with Windows DPAPI, readable only by your user accountThe macOS Keychain
RecordingsThe folder you choose in SettingsThe folder you choose in Settings

Updates

The app asks once, on its second launch, whether it may check for updates; you can also check on demand from Settings. Checks go to the public releases repository on GitHub and nowhere else, and every update is signature-verified before it is installed.

Linux CLI (sdgo)

The same hub, headless. One binary, subcommands, and a single config file that the background service also reads.

curl -fsSL https://senderogo.com/install.sh | sudo bash

That adds the signed apt repository and installs sdgo; updates then arrive through normal apt upgrade. Each build bundles its own LGPL FFmpeg and libsrt, so no distribution media libraries are needed.

Commands

CommandWhat it does
sdgo setupSet the saved defaults — format, directory, and the whole hub policy: port, auth mode, passphrases, access, limits. The background service runs a bare command, so this config is all it reads
sdgo listenRun the hub. With flags it runs in the foreground; listen start|stop|status runs the same thing in the background, supervised by systemd
sdgo authWho may publish: the credential mode and the source-address scope
sdgo forwardManage forward destinations and their triggers
sdgo statusSaved defaults, current hub policy, and the boot service's state
sdgo logsThe recording event log. -f follows it live, -n sets how much history
sdgo serviceInstall or remove the boot service. To start or stop the hub right now, use listen start|stop

Every command takes -h.

listen

FlagDefaultMeaning
--port, -p9000UDP port
--dir/srv/sdgoOutput directory — one auto-named file per stream
--format, -fmp4Container: ts or mp4
--authuniversaluniversal or pairs
--passphrasefrom configShared passphrase under universal. A value here is visible in ps — prefer being prompted
--stream-passphraseOne publisher, as <streamid> to be prompted, or <streamid>=<passphrase>. Repeatable; needs --auth pairs
--accesslocal+tailscalelocal, local+tailscale, tailscale, or any
--max-streams16Concurrent publisher cap; 0 removes it
--rate-limitonThrottle rapid handshake attempts per source IP

auth

sdgo auth list                    # both gates: mode, who may publish, scope
sdgo auth mode pairs              # switch credential mode (non-destructive)
sdgo auth add cam1                # pairs: allow a publisher, prompts for its passphrase
sdgo auth rm  cam1                # pairs: revoke it
sdgo auth passphrase              # universal: set or replace the shared passphrase
sdgo auth access local+tailscale  # source-address scope
sdgo auth forget                  # permanently delete passphrases parked by a mode switch

Passphrases are prompted for with terminal echo off, so they never reach the screen, your shell history, or the process table. They are stored in the config file at mode 0600 and never printed back.

forward

sdgo forward add studio --url rtmp://a.rtmp.youtube.com/live2 --key XXXX --match studio
sdgo forward add studio --match cam1     # add another trigger
sdgo forward rm  studio --match cam1     # detach just that trigger
sdgo forward set studio --key NEW-KEY
sdgo forward off studio                  # keep it saved, stop using it

Triggers are an OR-set, matched case-insensitively against the stream id; the transport is inferred from the URL scheme. Changes apply to the next feed automatically — restart the hub to apply them to one already in progress.

Configuration

~/.config/senderogo/config.json, mode 0600, because it holds passphrases. The systemd service runs a bare listen, so whatever setup writes there is exactly what the service uses.

Remote over Tailscale

Everything above assumes the phone and the receiver share a network. When they don't — a reporter on cellular, a receiver in an office — the answer is not to forward a port. Tailscale puts both devices on one private network of their own, and SenderoGo treats that network as a first-class case.

Why not a port-forward

Forwarding UDP 9000 puts your receiver on the open internet, where anyone scanning that port finds it. It also forces the access scope to Any address, which is the one setting that turns off address filtering altogether. Your passphrase becomes the only thing standing between a stranger and your hub. Tailscale avoids the trade entirely: the port stays closed to the internet, and only your own devices can reach it.

Setting it up

  1. Install Tailscale on the receiver machine and on the phone, and sign both into the same tailnet.
  2. On the receiver, set Who can publish to This network + Tailscale. On sdgo: sdgo auth access local+tailscale.
  3. The receiver now shows a second address beside the LAN one — its tailnet address, in the 100.x.y.z range. Export a publisher profile using that address, and scan it with the phone.
  4. Keep Tailscale connected on the phone while you stream. Both platforms allow only one VPN at a time, so another VPN will displace it.
The tailnet range is 100.64.0.0/10, and it is what the Tailscale scopes match on. An address in that range is reachable only by devices in your tailnet — it is not routable from the public internet.

Which scope to choose

SituationScope
Phones on the venue Wi-Fi, plus a remote cameraThis network + Tailscale
A venue network you don't trust, everything over the tailnetTailscale only — nothing on the local network reaches the hub, even from the same room
Everything local, Tailscale installed for other reasonsThis network

What to expect from the link

  • Raise the SRT latency. 120 ms suits a LAN. Over cellular or a long path, 200–500 ms is the difference between a clean stream and constant retransmission gaps. It is a buffer, not a delay you pay twice for.
  • Direct beats relayed. Tailscale connects two devices directly whenever it can and falls back to its DERP relays when a network blocks that — relayed paths add latency and are not built for sustained video. Run tailscale status on the receiver: it marks each peer direct or relay. If yours says relay and the stream struggles, that is the reason.
  • Uplink is the real limit. Cellular upload is usually the narrowest part of the path. 2–4 Mbps with adaptive bitrate on is a realistic target; the phone keeps its full-quality local copy regardless of what the link does.
  • The recording is unaffected by the network. A dropped tailnet connection costs you the live feed, never the copy on the phone.

If it doesn't connect

  • Check the scope first — a tailnet publisher hitting a This network receiver is refused with code 2005.
  • Confirm both devices are in the same tailnet and both show as online in the Tailscale admin console.
  • Publish to the tailnet address, not the LAN one. Both are shown; only one of them is reachable from outside the building.
  • Test the path independently: from the phone or another peer, ping the receiver's tailnet address. If that fails, the problem is Tailscale's, not SenderoGo's.

Protocols & URLs

With SenderoGo Mobile, import a profile and skip this section. For ffmpeg, OBS, hardware encoders, or anything else, these are the URL forms.

srt://<host>:<port>?streamid=<name>&passphrase=<pass>&pbkeylen=16&latency=200
rtmp://<host>/<app>/<stream-key>
  • Every publisher needs a streamid. The hub demultiplexes by it, names files after it, matches forward triggers against it, and in allowlist mode selects the passphrase with it. A publisher without one is refused.
  • Stream ids must be unique among live publishers on one hub.
  • Passphrases are 10–79 characters. That is an SRT rule: shorter is not "weak", it is invalid.
  • The receiver is always the listener; publishers are callers.
  • Streaming to a third-party service follows that service's own URL format — SenderoGo adds nothing to it.

An ffmpeg publisher, useful for testing a receiver without a phone:

ffmpeg -re -i input.mp4 -c copy -f mpegts \
  "srt://192.168.1.42:9000?streamid=cam1&passphrase=your-passphrase&pbkeylen=16&latency=200"

Connection refused: what the codes mean

When the hub turns a publisher away it returns a user-defined SRT reject code. Encoders that surface the number let you diagnose without touching the receiver.

CodeReasonFix
2001No stream idSet one on the publisher
2002That stream id is already liveUse a different id — or wait a few seconds, see below
2003That feed is paused on the receiverResume it
2004Concurrent-stream cap reachedEnd a stream, or raise the cap
2005Source address outside the access scopeWiden the scope, or publish from an allowed network
2006Too many handshake attempts from this addressStop retrying in a tight loop; the throttle clears on its own
2007Stream id not in the allowlistAdd it in pairs mode, or switch to a universal passphrase

A wrong passphrase has no code. Key exchange fails inside SRT itself, so the publisher sees a generic failure and the receiver reports that a handshake was approved but never completed. In practice: if a publisher gets that far and then dies, suspect the passphrase first.

A publisher that crashed reconnects into 2002. Its old socket counts as live until SRT's peer-idle timeout — about five seconds — declares it broken. The retry after that succeeds; nothing needs restarting.

Troubleshooting

Nothing connects at all

Check the firewall first. SRT is UDP, and a host firewall that drops inbound UDP does it silently — the publisher simply times out and the receiver's log stays empty, because nothing ever arrived. On Windows this is the single most common cause; allow the app on the network profile you are actually connected to.

The address shown is not the one that works

A machine with more than one active adapter — Wi-Fi plus Ethernet, or a VM bridge — has several plausible addresses, and the receiver shows one of them. If the handshake fails, try the other address before assuming anything else is wrong.

It works on the LAN but not remotely

Don't port-forward. Install Tailscale on both devices, set the access scope to include the tailnet, and publish to the tailnet address — see Remote over Tailscale above. Port-forwarding puts your SRT port on the open internet, where "any address" is the only scope that will accept it.

The stream is fine but the recording stopped

Free space. The receiver stops a recording cleanly rather than let a volume fill, and says so in the activity log. The live stream is deliberately left alone.