Contribute a camera from a phone (SRT)
A phone can join a TRaX studio as a camera source and publish its camera and mic over SRT. The phone is not compositing anything — the cloud encoder owns the live program, so the show keeps running even if the app backgrounds, the screen locks, or the signal drops for a moment. This is the ingest path the TRaX iOS app uses, and it is open to your own client.
The model in one sentence: a contributing phone is a participant whose device
source has transport = srt. Media rides SRT; identity and control ride the
studio WebSocket. You reuse everything the platform already does with
participants — the person grouping, the mixer strip, the canvas tile, mute and
kick — instead of inventing a new kind of source.
There are now two ways to do this, and this is the older one. Go live from a phone over
/v1provisions the same input and hands back the same SRT credential in ONE call on the public API, authenticated with ansk_live_key or an OIDC bearer like everything else in/v1. It is the same server-side path — the same source row, the same canvas tile, the same reconnection behaviour — so the two are interchangeable rather than parallel implementations.Prefer
/v1for a new client. Stay here when you also need the studio WebSocket for identity and control, or the walk-up QR guest flow, neither of which/v1carries.
What you talk to
Contribution is a first-party flow on the studio control plane (the studio
API), not the /v1 REST gateway. Three hosts are involved:
| Host | |
|---|---|
| Studio API (control plane) | https://studio-api.traxstreaming.live |
| Identity (OAuth login) | https://auth.traxstreaming.live |
| SRT ingest | srt://ingest.traxstreaming.live:8890 |
Always publish to the exact srtIngestUrl the provisioning response hands you
rather than hardcoding the ingest host.
Authenticate as the user
Every call below carries the user's access token: Authorization: Bearer <jwt>,
the OAuth 2.0 + PKCE token described in Build a client.
It is the same bearer you use for the studio WebSocket and for /v1. The one
exception is the walk-up QR guest path, which uses a scoped greenroom session
instead of an account (see Walk-up guests below).
1. Provision an SRT ingest
Ask the studio for a scoped ingest, identifying the device that is about to publish:
POST /api/v1/studios/{studioId}/contribute/ingest
Authorization: Bearer <jwt>
Content-Type: application/json
{ "deviceId": "9C1B...stable-per-install-id", "deviceLabel": "EJ's iPhone" }
deviceId(required) is a stable id your app mints once and keeps in the Keychain / Keystore. It is what makes a reconnection resolve to the same camera tile instead of a new one.deviceLabelis the human name shown in the studio ("EJ's iPhone").
You must be a member of the studio (any role). If contribution is not enabled
for the studio, the endpoint returns 404.
The 201 response is the descriptor you publish with:
{
"source": { "id": "mc-…", "name": "EJ's iPhone", "type": "mobile-contrib", "…": "…" },
"path": "inputs/mobile-contrib-mc-8f3c…",
"streamId": "publish:inputs/mobile-contrib-mc-8f3c…:<streamKey>:<streamKey>",
"srtIngestUrl": "srt://ingest.traxstreaming.live:8890?streamid=publish:inputs/mobile-contrib-mc-8f3c…:<streamKey>:<streamKey>",
"autoSeat": true,
"autoGoLive": true,
"deviceId": "9C1B…"
}
| Field | What it is |
|---|---|
srtIngestUrl |
The full SRT URL to publish to — streamid included. Use it verbatim. |
streamId |
The SRT streamid on its own, if your SRT stack takes host and streamid separately. |
path |
The stream path this contribution lives at (inputs/mobile-contrib-<id>). |
autoSeat |
Whether the source auto-seats on the canvas when it starts publishing (see below). |
autoGoLive |
Deprecated alias of autoSeat — same value, removed in a future release. Read autoSeat. |
source |
The participant device source the feed is bound to — it appears in the studio's Sources immediately, offline until SRT flows. |
ingestHost / ingestMetro |
Display only: the host this contribution publishes to and the metro it names (for example "publishing to us-east"). ingestMetro is auto while the host is the global entry point and the metro is not yet decided. Additive and optional; never dial from these. |
2. Publish over SRT
Open an SRT publisher (libsrt on iOS/Android, or ffmpeg from a workstation)
to srtIngestUrl. The credential is entirely inside the streamid:
publish:<path>:<streamKey>:<streamKey>
The whole capability is the scoped per-path stream key — user and password positions are both the key. There is no JWT in the streamid, which keeps it well under libsrt's 512-byte cap. Do not URL-encode the streamid; libsrt wants the literal colons. See SRT: the streamid contract for the field grammar and the failure modes.
ffmpeg sanity check from a laptop:
ffmpeg -re -i camera.mp4 -c:v libx264 -c:a aac -f mpegts \
"srt://ingest.traxstreaming.live:8890?streamid=publish:inputs/mobile-contrib-mc-8f3c…:<streamKey>:<streamKey>"
What happens on the studio side
- The feed auto-seats as a participant device source under the contributing person, inheriting their name, avatar, and color. No host click is needed.
- The cloud encoder is the authority. If the phone backgrounds or loses signal, the source goes offline and the program falls back to a slate; when the phone comes back it re-seats the same tile. The broadcast itself never depended on the phone.
autoSeatseats the source, it does not start the broadcast. Bringing a camera onto the canvas is separate from going live to your destinations. The studio's master go-live control stays authoritative — a contributing phone can never force the program on air.
Reconnection and the device registry
Each phone is a durable device, not a per-connection source. List a user's devices and their history:
GET /api/v1/contribute/devices
Authorization: Bearer <jwt>
→ 200 { "devices": [ { "deviceId": "9C1B…", "label": "EJ's iPhone",
"lastSeenAt": "…", "connectionCount": 12, "…": "…" } ] }
Because the source id is derived from the studio plus the durable device, the
same deviceId reconnecting into the same studio re-provisions the same
path and re-seats the same tile — layout and position survive drops and
app restarts.
3. Release the input when you're done
When the phone is finished contributing, hand its input back to the studio:
DELETE /v1/studios/{studioId}/contribute
Authorization: Bearer <token>
Content-Type: application/json
{ "deviceId": "9C1B…" }
→ 204 No Content
This is the teardown twin of the provision call. It stops the cloud encoder
from re-arming a pull against a path the phone is no longer publishing to. Call
it from applicationWillTerminate (or your platform's equivalent) so a clean
exit gives the session back instead of leaving it to time out.
Keep-source. Release deactivates the input; it does not delete it. The
source row, its media path, and its canvas placement survive, so a later
provision with the same deviceId re-seats the same tile exactly like a
reconnection does. Use DELETE /v1/studios/{studioId}/sources/{sourceId} when
you actually want the input removed from the studio.
Safe from a background teardown. The call answers 204 on everything that
is merely "nothing to do" — an unknown deviceId, one already released, one
that never published, or an empty body. A shutting-down app has no screen to
show an error on and no chance to retry, so a teardown never fails on request
framing it cannot fix. The only answers you must not ignore are 403 (the
deviceId belongs to another user), 401 (no auth), and 5xx (the release
genuinely did not happen).
Unlike provisioning, release is not gated by the deployment's contribute switch: that flag stops new sessions being created, but a session that already exists must always be releasable.
Walk-up guests (QR)
A host can let someone contribute without an account. The host mints a short-lived join code (admin role required):
POST /api/v1/studios/{studioId}/contribute/qr
Authorization: Bearer <jwt>
→ 201 {
"studioId": "…",
"token": "…",
"deepLink": "traxstreaming://contribute?studio=…&token=…",
"joinUrl": "https://studio-api.traxstreaming.live/m/contribute?studio=…&token=…",
"expiresAt": "2026-08-11T20:15:00Z"
}
Render the token (or the deep link) as a QR code. The token is the only way
in — there is no enumeration — and it expires in 15 minutes with a small use
cap; re-mint to rotate. A guest scans it, enters a name, and their app
provisions an ingest against the greenroom session:
POST /api/v1/greenroom/contribute/ingest
Content-Type: application/json
Cookie: <greenroom session established by the join token>
{ "deviceId": "…", "deviceLabel": "Guest — Sam" }
The response is the same SRT descriptor as the authenticated path, and the guest auto-seats as a participant device source. Guest ingests are rate-limited, and the guest is kickable and forgotten on leave.
Control the studio while you publish
Contribution is only the camera leg. The same app holds a WebSocket to the studio for tally ("you're live"), mute, flip-camera, and stop — bidirectional control that has nothing to do with the media plane. That surface, and how to authenticate it, is in Build a client → Controlling a studio.
Remote camera control
The studio can drive the phone's camera — exposure, ISO, shutter, EV, white
balance, zoom, focus, torch, lens — and the phone reports back what its hardware
actually did. It is a desired / actual pair on the source row: the studio
owns desired, the phone owns capabilities and actual, and the server owns
the control lease. Controls only appear in the studio when the phone has
reported it supports them.
Registration is the contribute call. Send POST /v1/studios/{id}/contribute
with an optional publishNonce (a fresh 128-bit value your app mints per push),
a device identity, and camera.capabilities. The returned srtStreamId then
carries the nonce, so the device that dials it is provably the one that later
controls the source. Publish exactly as before — the nonce rides inside the
credential you already dial.
A plain input attaches instead. If the phone pushes to an existing live input by its stream key — an input the operator created, not one the phone contributed — contribute never runs, so nothing registers by itself. Register in two steps, in this order:
- Before you dial,
POST …/sources/{sourceId}/camera/preparewith{deviceId, publishNonce}(scopesources:write; the caller must be allowed to reveal that input's stream key). It opens a pending publish session for the nonce and returns the nonce-bearing ingest: an SRTsrtStreamIdof the formpublish:<path>:<nonce>:<key>and an RTMP URL withuser=<nonce>. Dial that, not the plain stream id you already have — the media plane witnesses the nonce on admission, and a publish without it can never be bound. - Once the push is up,
POST …/sources/{sourceId}/camera/attachwith{deviceId, publishNonce, device, capabilities}. Attach is the step that writes the device identity andcamera.capabilitiesonto the source — prepare writes nothing — so until it lands the studio shows no controls. It returns asessionToken(a secret handed back once) and the publicpublishSessionId.
Only the device the media plane actually admitted can present the matching
nonce, so no other member's phone can claim the input. After attach, open the
studio WebSocket and send bind_device exactly as a contributed phone does;
bind resolves the same session and mints a fresh sessionToken, so always
report with the token from whichever of attach or bind ran last — each one
replaces the other.
Reading state. GET …/sources/{sourceId}/camera returns the source with its
device and camera blocks. Pass your own ?deviceId= and device.mine comes
back true for the phone that owns the source, so it can show "you are attached"
without comparing ids. The ingest secret and the session token are never in
a read — only the public publishSessionId is.
Writing. A studio or a third-party controller writes desired state with
PATCH …/sources/{sourceId}/camera (scope sources:write); the body is a sparse
patch of control groups, and a group set to null releases it back to the
phone's automatic mode. This is an ordinary authorized write — it carries no
publisher secret. Every field is validated against the phone's reported
capabilities first, so an out-of-range value is refused 400 rather than sent.
Reporting. The phone reports its read-back with POST …/sources/{sourceId}/camera/report,
presenting its sessionToken in the X-Trax-Publish-Session header; a
heartbeat on the same header keeps an idle phone reachable. The report body
carries deviceId, publishSessionId, reportSequence and reportId at the
top level and the hardware read-back under actual. reportSequence must
increase with every report from the same session — the server drops anything
at or below the last sequence it stored — and a retry of a lost report must
reuse the same reportId so it is recognised as a duplicate rather than
refused. A change in what the camera can do (a lens switch, a gimbal attached)
rides the same sequenced report as a top-level capabilities object; a
capabilities-only request without a sequence is accepted once and stale
forever after. The status codes to
handle: 400 camera_unsupported for a value outside capabilities, 403
publish_session_invalid for a wrong nonce or a token presented for a different
device, 409 source_attached when another device already holds the input,
and 409 publish_session_stale / report_stale when the session has closed
or a report arrives out of order.
Where it shows up in the studio. Once a phone has registered, the source
grows a Camera item in two places: the row menu on the Sources tab and the
right-click menu on its canvas tile. Either opens the camera panel over the side
panel, showing only the groups the phone reported; viewers see it read-only.
The item is driven by one thing — settings.camera.capabilities on the
source — so its absence always means the same thing: no capability report has
reached the server for that source. A phone that is publishing but never
contributed with camera.capabilities, or never called attach, is a normal
input with no camera controls, and nothing in the studio will hint otherwise.
Verify a registration with one read: GET …/sources/{sourceId}/camera
should return a camera.capabilities block and, with your ?deviceId=, a
device.mine of true. If capabilities is missing, the phone did not
register — check the contribute body or the prepare/attach pair, not the
studio. To exercise the panel before your app is ready, attach a source with
a hand-written capability report from any HTTP client; the controls appear the
moment the write lands and go away when you release the group or detach.
Where to go next
- Build a client — the transport map, native OAuth + PKCE, and the studio-control WebSocket.
- Stream to a live input — the
/v1path when the feed should be a plain input rather than a participant, driven by an API key instead of a user login. - Drive the studio canvas — control layout, sources, presets, and scenes over the studio WebSocket while you publish.
- Monitor the program output — watch the composited program feed over WHEP.
- SRT: the streamid contract — the streamid grammar, the 512-byte cap, and how publishes fail.
- Stream paths — the path grammar shared by every media transport.
- Authentication & session tokens — the user-login (OIDC + PKCE) and media-token models in full.