guides

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 /v1 provisions the same input and hands back the same SRT credential in ONE call on the public API, authenticated with an sk_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 /v1 for 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 /v1 carries.

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.
  • deviceLabel is 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.
  • autoSeat seats 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:

  1. Before you dial, POST …/sources/{sourceId}/camera/prepare with {deviceId, publishNonce} (scope sources: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 SRT srtStreamId of the form publish:<path>:<nonce>:<key> and an RTMP URL with user=<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.
  2. Once the push is up, POST …/sources/{sourceId}/camera/attach with {deviceId, publishNonce, device, capabilities}. Attach is the step that writes the device identity and camera.capabilities onto the source — prepare writes nothing — so until it lands the studio shows no controls. It returns a sessionToken (a secret handed back once) and the public publishSessionId.

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

  1. Build a client — the transport map, native OAuth + PKCE, and the studio-control WebSocket.
  2. Stream to a live input — the /v1 path when the feed should be a plain input rather than a participant, driven by an API key instead of a user login.
  3. Drive the studio canvas — control layout, sources, presets, and scenes over the studio WebSocket while you publish.
  4. Monitor the program output — watch the composited program feed over WHEP.
  5. SRT: the streamid contract — the streamid grammar, the 512-byte cap, and how publishes fail.
  6. Stream paths — the path grammar shared by every media transport.
  7. Authentication & session tokens — the user-login (OIDC + PKCE) and media-token models in full.