Set the program resolution and frame rate
Two calls: read what a studio is set to composite and encode at, and change it.
Which host.
https://api.traxstreaming.live/v1, with a live key or a signed-in user's token. These endpoints are in production.
GET /v1/studios/{id}/output— the format in three parts.PATCH /v1/studios/{id}/output— set the resolution and/or frame rate.
Read the GET first, and read all of it. The response deliberately does not collapse into one number, because one number would be wrong for most studios.
Three answers, not one
{
"studioId": "…",
"selected": { "width": 2560, "height": 1440, "fpsNum": 60, "fpsDen": 1, "fps": 60 },
"effective": { "width": 1920, "height": 1080, "fpsNum": 60, "fpsDen": 1, "fps": 60 },
"caps": {
"maxWidth": 1920,
"maxHeight": 1080,
"cappedBy": "twitch",
"supported": [
{ "width": 1280, "height": 720, "available": true, "status": "available" },
{ "width": 1920, "height": 1080, "available": true, "status": "available" },
{ "width": 2560, "height": 1440, "available": true, "status": "available" },
{ "width": 3840, "height": 2160, "available": false, "status": "coming_soon" }
],
"supportedFrameRates": [ { "num": 60000, "den": 1001, "fps": 59.94 }, "…" ]
},
"codec": "h264",
"targetBitrateKbps": 6000,
"appliesToNextGoLive": true
}
selected is what the operator picked. effective is what the next go-live
will actually run. They differ here because Twitch ingests 1080p, and this
studio has a Twitch destination enabled — so a 1440p selection airs at 1080p.
caps.cappedBy names the platform responsible; it is an empty string when
nothing is capping, which is the field to check rather than comparing
maxWidth against the ladder top.
Show a user effective when answering "what am I streaming at". selected
is their intent and can legitimately be higher than what airs today.
The cap is a fact about the studio's destination set right now, not a rule. Disable Twitch and the same selection airs at 1440p with no further write.
Frame rate is a rational
fpsNum / fpsDen, not a decimal. 59.94 is exactly 60000/1001, and rounding
it to 59.94 or 60 produces drift the encoder cannot correct. The fps field is
the decimal rendering for display; there is no way to write it, and sending one
is a 400.
The accepted rates:
| num / den | reads as |
|---|---|
| 24000 / 1001 | 23.976 |
| 24 / 1 | 24 |
| 25 / 1 | 25 |
| 30000 / 1001 | 29.97 |
| 30 / 1 | 30 |
| 50 / 1 | 50 |
| 60000 / 1001 | 59.94 |
| 60 / 1 | 60 |
Read them from caps.supportedFrameRates rather than hardcoding this table.
Setting it
curl -X PATCH https://api.traxstreaming.live/v1/studios/$STUDIO/output \
-H "Authorization: Bearer $TRAX_API_KEY" \
-H "Content-Type: application/json" \
-d '{"width":1920,"height":1080,"fpsNum":60000,"fpsDen":1001}'
A PATCH: send only what you are changing. A resolution-only body leaves the frame rate exactly where it was, which is what lets a phone drop a studio to 720p without disturbing a 59.94 that someone chose deliberately.
The pairs are pairs. width and height move together and fpsNum and
fpsDen move together; sending half of either is a 400 naming the field. Half a
resolution is not a resolution, and a numerator with no denominator is not a
frame rate.
The response is the whole re-resolved view — the same shape GET returns — so you never have to model the cap chain yourself to know what your change did.
It rejects; it does not clamp
An off-ladder resolution is a 400 that names the field and lists what is allowed:
{
"error": {
"code": "invalid_request",
"message": "width: 1600x900 is not a supported program resolution; supported: 1280x720, 1920x1080, 2560x1440"
}
}
This is the point of the endpoint. Offer caps.supported in your picker and
you will never see this error; construct a resolution yourself and you will
find out immediately rather than at the next go-live.
Coming soon resolutions
Each rung in caps.supported carries an available flag and a status
string. A rung with "available": false ("status": "coming_soon") is part of
the ladder but not offered yet — 4K (3840x2160) reads as coming soon today.
Render it in your picker greyed out rather than hiding it, and do not let it be
selected.
Selecting a coming-soon resolution is rejected the same way an off-ladder one is — a 400 that names the reason:
{
"error": {
"code": "invalid_request",
"message": "width: 4K (3840x2160) is coming soon and not yet available"
}
}
While 4K is coming soon it is excluded from the destination soft-cap too, so
caps.maxWidth tops out at 2560x1440 even with only 4K-capable destinations
(e.g. YouTube) enabled. When 4K ships, its rung flips to "available": true
and the cap lifts with no change to your integration.
The one thing reported rather than enforced is the destination cap. Setting 1440p on a Twitch-only studio succeeds, and the response tells you it will air at 1080p. Refusing it would delete the operator's intent to punish a condition that lifts the moment they disable Twitch. (This is distinct from the coming-soon gate above, which IS enforced on write — a coming-soon resolution is a roadmap gap, not a per-destination condition.)
Codec and HDR
The output surface also describes the video codec and the color system
(colorimetry). Read them the same way you read the resolution: from caps,
never from assumptions.
Codec. codec on the response is what the next go-live encodes with.
Today that is h264 for every studio, because no egress path can carry
anything else to a destination — and caps.supportedCodecs says exactly that:
"caps": { "supportedCodecs": ["h264"] }
codec is writable in the PATCH body, validated against that same list. So
today:
curl -X PATCH .../v1/studios/$STUDIO/output \
-H "Authorization: Bearer $TRAX_API_KEY" \
-H "Content-Type: application/json" \
-d '{"codec":"h265"}'
{
"error": {
"code": "invalid_request",
"message": "codec: h265 egress is not yet enabled — no egress path can carry HEVC to a destination today; supported: h264"
}
}
That 400 is deliberate — it rejects rather than silently clamping to h264,
for the same reason the resolution does. When HEVC egress ships, "h265"
appears in caps.supportedCodecs and the identical PATCH starts succeeding,
with no change to the request or response shape. Poll the caps, not a
changelog. Writing "codec": "" clears the selection back to automatic (the
resolution decides).
The response also carries selectedCodec — your persisted choice, "" when
you never made one — separately from codec. They differ only if a codec you
selected stops being deliverable; the platform then falls back to the
automatic choice rather than sending dead video, and the split between the two
fields is how you see that happened.
Colorimetry. selected.colorimetry, effective.colorimetry and
caps.supportedColorimetry describe the color system:
"effective": {
"width": 1920, "height": 1080, "fpsNum": 60, "fpsDen": 1, "fps": 60,
"colorimetry": { "range": "sdr", "transfer": "bt709", "primaries": "bt709", "bitDepth": 8 }
},
"caps": {
"supportedColorimetry": [
{ "range": "sdr", "transfer": "bt709", "primaries": "bt709", "bitDepth": 8 }
]
}
The colorimetry fields are read-only, and there is exactly one supported
entry: SDR BT.709 at 8 bits. There is no colorimetry write path; when the
pipeline can run more than one color system, caps.supportedColorimetry grows.
As with codecs, the capability list is the announcement.
HDR intent. Separately from colorimetry, the PATCH body takes a boolean
hdr: whether the next session should run an HDR canvas. It is validated
against caps.supportsHdr: "hdr": true on a deployment where that is
false is a 400, while an explicit "hdr": false is always accepted and
stored. The response reports your stored choice as selectedHdr and the
decision the next session will actually make as hdr. Writing hdr does not
by itself turn on HDR where the deployment cannot run it.
Bitrate
The response carries targetBitrateKbps, the video bitrate the next session
will target, and a bitratePolicy object that explains it. There is one
program encode shared by every destination, not a separate encode per
platform, so the target is the one value that satisfies every enabled
destination at once.
bitratePolicy field |
What it is |
|---|---|
mode |
auto (the platform picks) or manual (you set a number) |
selectedKbps |
Your manual value. Present only in manual mode |
targetKbps |
What the next session will target. 0 when valid is false |
hard |
minKbps / maxKbps: the bounds the enabled destinations and the encoder actually allow |
constraints |
One entry per destination platform, with its hard limits and recommended range |
advisories |
Warnings that do not block the setting, each with a platform, code and message |
valid / error |
Whether the saved policy can be honoured, and if not, why |
appliesTo |
next_server_session |
To set a manual bitrate, send both fields:
curl -X PATCH https://api.traxstreaming.live/v1/studios/$STUDIO/output \
-H "Authorization: Bearer $TRAX_API_KEY" \
-H "Content-Type: application/json" \
-d '{"bitrateMode":"manual","bitrateKbps":8000}'
bitrateMode: "auto" clears the override and must be sent without
bitrateKbps. Once a studio is in manual mode, a body carrying only
bitrateKbps changes the number; the same body on a studio in auto mode is a
400. A value outside hard is a 400 that says so; it is never clamped.
targetBitrateKbps is a target, not a measurement of what is leaving the
encoder, and it does not apply to a browser publishing its own camera.
Advanced encoder settings
Four more next-session settings ride the same PATCH. Each is optional, and omitting one keeps its current value:
| Field | Accepted values |
|---|---|
keyframeIntervalSeconds |
1, 2 or 4 |
audioBitrateKbps |
128, 160, 192, 256 or 320 (a destination's limits can narrow this) |
rateControl |
cbr or vbr (the hardware can report a downgrade) |
encoderPreset |
quality, balanced or low_latency |
advancedMode: "auto" clears all four at once and cannot be combined with any
of them. To reset only some, name them in clearFields, e.g.
{"clearFields":["rateControl"]}. Naming a field in clearFields and setting
it in the same body, repeating a name, or naming an unknown field is a 400. The
response reports the result as advancedPolicy, with the same valid /
error / appliesTo shape as bitratePolicy.
For a studio that is live right now, currentAdmission reports what the
running session was started with: its codec, format, bitrate target and HDR
choice. It is absent when no session is known, and it never changes mid-session.
When it takes effect
appliesToNextGoLive is always true, and it is in the payload rather than
only in this page because it is easy to get wrong. The encoder does not
renegotiate resolution or frame rate mid-session: a studio that is live right
now keeps the format it started with until it goes offline and live again.
Patching a live studio is safe — it persists and does not disturb the session on air — but it will not change what viewers are watching until the next go-live. If your UI can be open during a broadcast, say so there.
Scopes
studios:read to read, studios:write to change. Both are in the default
scope set for a new key.
The format rides the studio scopes rather than having a pair of its own because
changing it does not move what is on air — it takes effect at the next go-live,
the same blast radius renaming a studio has. That is a different power from
canvas:write, which puts a tile in front of every viewer the instant it lands.
Related
- Control the canvas from your app — the canvas
read reports the same format as
output, in the same shape. The canvas coordinate space is a fixed 1920x1080 for every studio and is not the broadcast resolution; scale your miniature by the canvas and label the stream by the output.