guides

Monitor the program output (WHEP)

A native or desktop client can play the studio's program output — the exact composited feed the cloud encoder is sending to your destinations — over WebRTC WHEP. This is the app analogue of the program-preview tile in the web studio: a low-latency confidence monitor of what is actually going out.

WHIP / WHEP documents the WebRTC play protocol, but a player still needs a read token and the URL to play. This guide is the one endpoint that hands you both, scoped to your studio's program path.

The same credential is on /v1 now. POST /v1/studios/{id}/monitor mints this token through the public gateway, authenticated with an sk_live_ key or an OIDC bearer, and adds an SRT alternative for clients that cannot speak WHEP. See Go live from a phone over /v1. Everything below about WHEP, the token and the expiry applies there unchanged.

What you talk to

The monitor descriptor comes from the studio control plane (the studio API), the same host you already use for login and contribution — not the /v1 REST gateway. Playback itself is a direct WebRTC dial to the media host in the descriptor.

Development Production
Studio API (control plane) https://studio-api-dev.traxstreaming.live https://studio-api.traxstreaming.live
Identity (OAuth login) https://auth-dev.traxstreaming.live https://auth.traxstreaming.live
WHEP playback the whepUrl in the descriptor the whepUrl in the descriptor

Always dial the exact whepUrl the descriptor hands you rather than reconstructing the media host yourself.

Authenticate as the user

The mint call carries the user's access token: Authorization: Bearer <jwt>, the OAuth 2.0 + PKCE token described in Build a client. Any studio member may monitor — owner or collaborator, in any role (view is enough). The mint verifies your membership before issuing the read capability.

1. Fetch the monitor descriptor

Ask the studio for a program-monitor capability. No request body is required:

POST /api/v1/studios/{studioId}/monitor
Authorization: Bearer <jwt>
curl -sS -X POST \
  https://studio-api-dev.traxstreaming.live/api/v1/studios/$STUDIO_ID/monitor \
  -H "Authorization: Bearer $USER_JWT"

The 200 response is the descriptor you play with:

{
  "whepUrl": "https://<media-host>:8889/s/<studioId>/pp/whep",
  "streamName": "s/<studioId>/pp",
  "token": "<JWT — aud trax-mediamtx-read>",
  "expiresAt": "2026-08-11T20:15:00Z"
}
Field What it is
whepUrl The WHEP play URL to POST your SDP offer to. Use it verbatim.
streamName The program path this monitor reads (s/<studioId>/pp).
token The read token — a short-lived JWT (aud: trax-mediamtx-read) scoped to exactly that one path. This is the Bearer for the WHEP POST, not your user token.
expiresAt RFC 3339 expiry of the read token.

403 means you are not a member of the studio; 503 means the monitor signer or media endpoint is momentarily unavailable — retry.

2. Play it over WHEP

This is the standard WHEP play flow from WHIP / WHEP, with one rule: the Bearer on the signaling POST is the descriptor's read token, not the user token.

  1. Create a recv-only RTCPeerConnection.
  2. Create an SDP offer, gather ICE (non-trickle — wait for iceGatheringState === "complete").
  3. POST the offer to whepUrl with Content-Type: application/sdp and Authorization: Bearer <token>.
  4. Set the returned SDP answer as the remote description and render the track.
// descriptor = the JSON from POST /monitor
const pc = new RTCPeerConnection();
pc.addTransceiver('video', { direction: 'recvonly' });
pc.addTransceiver('audio', { direction: 'recvonly' });
pc.ontrack = (e) => { videoEl.srcObject = e.streams[0]; };

await pc.setLocalDescription(await pc.createOffer());
// non-trickle: let ICE finish before signaling
await new Promise((resolve) => {
  if (pc.iceGatheringState === 'complete') return resolve();
  pc.addEventListener('icegatheringstatechange', () => {
    if (pc.iceGatheringState === 'complete') resolve();
  });
});

const res = await fetch(descriptor.whepUrl, {
  method: 'POST',
  headers: {
    'Content-Type': 'application/sdp',
    Authorization: `Bearer ${descriptor.token}`, // the READ token
  },
  body: pc.localDescription.sdp,
});
await pc.setRemoteDescription({ type: 'answer', sdp: await res.text() });

Audio arrives as Opus (feeds contributed as AAC are transcoded on the way out) — see WHIP / WHEP for the codec and failure-mode tables.

Token semantics — re-provision per open

  • Short-lived. The read token lasts about 5 minutes (hard-capped at 15). Do not cache or share it — it travels in the WHEP request.
  • mediamtx authorizes a WHEP read only at connect time. An established monitor keeps playing after the token expires; a fresh token is only needed to (re)connect. So the rule is simple: call POST /monitor again every time you open (or reopen) the player, and don't bother refreshing a token for a connection that is already up.
  • Single-path scope. The token authorizes exactly s/<studioId>/pp and nothing else — it cannot read any other source or studio.

The program only flows when the studio is live

The endpoint always mints the capability, but media flows only while the studio is live and pushing its program-preview feed. A monitor opened while the studio is off-air connects successfully but shows no video yet — it should start rendering frames when the studio goes live.

Keep the WHEP peer connection up across go-live (or reconnect on a short retry) so the monitor lights up the moment the program starts. Treat "connected, no frames" as a normal pre-live state, not an error.

Where to go next

  1. WHIP / WHEP — the WebRTC publish/play protocol, token audiences, and failure modes.
  2. Drive the studio canvas — control the layout your monitor is showing over the studio WebSocket.
  3. Build a client — the transport map and native OAuth
    • PKCE login that issues the user token.
  4. Contribute a camera from a phone (SRT) — the publish side of the same studio.

Knowing when to connect (and when to stop)

Do not poll stream-status to find out whether the program is worth watching.

GET /v1/studios/{id}/events is a Server-Sent Events stream that tells you when the studio goes on air, when it goes off, and when any source's publishing flips — one connection instead of a request every few seconds, which matters on the same cellular uplink your device may be publishing over.

Open it once, wait for event: streamStatus with "live": true, then mint your monitor token and connect WHEP. When "live" goes false, tear the peer connection down.

The full event vocabulary, the Last-Event-ID reconnect contract and the limits are in Go live from a phone.