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.
| Piece | Runs on | Job |
|---|---|---|
| SenderoGo Mobile | iOS 15+, Android 8+ | Captures, publishes over SRT or RTMP, and keeps a full-quality copy on the phone |
| SenderoGo Receiver | macOS 13+, Windows 10 1809+ | Listens on one port, previews and records every publisher, forwards on |
sdgo | Debian 13+/Ubuntu 24.04+, amd64 or arm64 | The 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
- Install the receiver and open it. It shows a LAN address, and a tailnet address if you run Tailscale.
- Set a passphrase and press Start Listening.
- 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.
- 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.
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
}
| Field | Values | Meaning |
|---|---|---|
type | "stream" | Required discriminator — both importers reject a payload without it |
name | text | Profile name as it appears in the phone's list |
protocol | "srt" | Transport |
url | srt://host:port | Bare — passphrase and stream id ride as their own fields, not as query parameters |
streamId | text, optional | Names 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 |
passphrase | 10–79 chars, optional | SRT encryption passphrase. The length range is a protocol rule, not ours |
resolution | 720p, 1080p, 4k | Capture resolution |
fps | integer | Capture frame rate |
latency | milliseconds | SRT buffer. Higher rides out worse networks at the cost of delay — 120 ms suits a good LAN, 200–500 ms suits cellular |
bitrate | kbps | Video target. 4000 is 4 Mbps |
adaptiveBitrate | boolean | Let 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
| Mode | Request | Use it for |
|---|---|---|
| Multipart (default) | POST of multipart/form-data, the file under a file field | Any handler you write yourself — PHP, Node, Python, Go — which receives it with no special work |
| Raw body | POST or PUT with the file bytes as the body, application/octet-stream by default | Object 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-Typeyou 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": "…" }
}
| Field | Required | Meaning |
|---|---|---|
type | yes | Must be "upload". A "stream" payload is refused here, and vice versa |
url | yes | HTTPS endpoint that accepts the upload. May contain {filename} |
token | no | Sent as Authorization: Bearer. For any other format, use a custom header instead |
bodyMode | no | "multipart" (default) or "raw" |
method | no | "POST" (default) or "PUT" — raw mode only |
autoUpload | no | true uploads new recordings as they finish |
headers | no | Object 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.
| Mode | Behaviour |
|---|---|
| Universal passphrase | One shared passphrase, 10–79 characters. Any stream id may publish with it |
| StreamID–passphrase pairs | An 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.
| Scope | Accepts |
|---|---|
| This network | Private ranges only — 10/8, 172.16/12, 192.168/16, link-local, loopback. The venue Wi-Fi default |
| This network + Tailscale | The above plus the tailnet range (100.64.0.0/10) |
| Tailscale only | The tailnet alone. On a hostile venue network, nothing on the LAN reaches the hub |
| Any address | No 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.
| When | What happens |
|---|---|
| Under 1 GiB free as a recording would start | The recording is refused and the reason named. The stream itself keeps running and previewing |
| Under 120 s of headroom at the current arrival rate | One warning, recording continues, and stale temporary files older than an hour are reclaimed |
| Under 200 MiB free, or under 30 s of headroom | Clean 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
| Windows | macOS | |
|---|---|---|
| Application | %LOCALAPPDATA%\Programs\SenderoGo Receiver — per-user, no administrator prompt | /Applications |
| Settings | %APPDATA%\SenderoGo Receiver\settings.json | Standard app preferences |
| Passphrases | Encrypted with Windows DPAPI, readable only by your user account | The macOS Keychain |
| Recordings | The folder you choose in Settings | The 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
| Command | What it does |
|---|---|
sdgo setup | Set 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 listen | Run the hub. With flags it runs in the foreground; listen start|stop|status runs the same thing in the background, supervised by systemd |
sdgo auth | Who may publish: the credential mode and the source-address scope |
sdgo forward | Manage forward destinations and their triggers |
sdgo status | Saved defaults, current hub policy, and the boot service's state |
sdgo logs | The recording event log. -f follows it live, -n sets how much history |
sdgo service | Install or remove the boot service. To start or stop the hub right now, use listen start|stop |
Every command takes -h.
listen
| Flag | Default | Meaning |
|---|---|---|
--port, -p | 9000 | UDP port |
--dir | /srv/sdgo | Output directory — one auto-named file per stream |
--format, -f | mp4 | Container: ts or mp4 |
--auth | universal | universal or pairs |
--passphrase | from config | Shared passphrase under universal. A value here is visible in ps — prefer being prompted |
--stream-passphrase | — | One publisher, as <streamid> to be prompted, or <streamid>=<passphrase>. Repeatable; needs --auth pairs |
--access | local+tailscale | local, local+tailscale, tailscale, or any |
--max-streams | 16 | Concurrent publisher cap; 0 removes it |
--rate-limit | on | Throttle 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
- Install Tailscale on the receiver machine and on the phone, and sign both into the same tailnet.
- On the receiver, set Who can publish to This network + Tailscale. On
sdgo:sdgo auth access local+tailscale. - The receiver now shows a second address beside the LAN one — its tailnet address, in the
100.x.y.zrange. Export a publisher profile using that address, and scan it with the phone. - Keep Tailscale connected on the phone while you stream. Both platforms allow only one VPN at a time, so another VPN will displace it.
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
| Situation | Scope |
|---|---|
| Phones on the venue Wi-Fi, plus a remote camera | This network + Tailscale |
| A venue network you don't trust, everything over the tailnet | Tailscale only — nothing on the local network reaches the hub, even from the same room |
| Everything local, Tailscale installed for other reasons | This 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 statuson 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.
| Code | Reason | Fix |
|---|---|---|
| 2001 | No stream id | Set one on the publisher |
| 2002 | That stream id is already live | Use a different id — or wait a few seconds, see below |
| 2003 | That feed is paused on the receiver | Resume it |
| 2004 | Concurrent-stream cap reached | End a stream, or raise the cap |
| 2005 | Source address outside the access scope | Widen the scope, or publish from an allowed network |
| 2006 | Too many handshake attempts from this address | Stop retrying in a tight loop; the throttle clears on its own |
| 2007 | Stream id not in the allowlist | Add 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.