guides

Control the canvas over /v1

You are holding a phone. The show is on a laptop in another room, or on an encoder in a rack, and you want to move a tile — or put the wide shot back, or cut to the scene you saved before the show. This guide is that: read the canvas, change one thing, watch it land.

Eight endpoints, all under /v1/studios/{id}, all reaching the same control plane the studio's own canvas uses. A tile you move here and a tile the operator drags on the laptop are the same write to the same authority, which is why the two cannot end up disagreeing about where it is.

Which host. These endpoints are on dev today: https://api-dev.traxstreaming.live/v1 with a dev key. This page will say so when they reach production.

Scopes. canvas:read for the two reads, canvas:write for everything that changes something. Both are opt-in — a key minted without naming them has neither, and studios:write does not confer canvas:write. Renaming a studio and moving what viewers are looking at are different powers, and only one of them is visible on air the instant it lands.

The model in one paragraph

A studio has a canvas: a fixed 1920×1080 coordinate space with one placement per source — a rectangle, a stacking order, an opacity, a crop. Two layout modes decide who owns those rectangles. In auto the server's planner resolves them from the active preset and the current source set; you express intent and it computes pixels. In advanced you own them and the planner keeps its hands off. Either way the rectangles are stored in one place, so what you read is what the encoder is compositing.

Read the canvas

GET /v1/studios/{studioId}/canvas
Authorization: Bearer sk_live_...
{
  "layoutMode": "auto",
  "activePreset": "2up",
  "activeSceneId": "",
  "canvasWidth": 1920,          // the COORDINATE SPACE rectangles live in
  "canvasHeight": 1080,
  "aspectRatio": "16/9",
  "fitMode": "contain",
  "focusSourceId": "src-camera",
  "tileOrder": ["src-camera", "src-screen"],
  "traySourceIds": [],
  "output": { "width": 1920, "height": 1080, "fpsNum": 60, "fpsDen": 1 },
  "layoutRev": 42,
  "membershipBasis": "resolved",
  "placements": [
    {
      "sourceId": "src-camera",
      "name": "Main camera",
      "type": "webcam",
      "active": true,
      "onCanvas": true,
      "inTray": false,
      "rect": { "x": 0, "y": 270, "width": 960, "height": 540 },
      "zIndex": 0,
      "alpha": 1,
      "visible": true
    }
    // …one row per source in the studio
  ]
}

One call, not two. A tile is a source and a rectangle; if you fetched the layout and the source list separately you would have two answers taken at two moments, and the miniature you drew from them could show a tile that was deleted between the requests.

canvasWidth/canvasHeight is not the broadcast resolution. Rectangles are in canvas units — always a 1920×1080 space — while output is what the encoder actually produces. A 720p60 program still composites on a 1920×1080 canvas. Scale your miniature by the canvas; label the stream by output.

Drawing the miniature

placements comes back in paint order: placed tiles first, back to front (z ascending, source id breaking ties), then the sources that have never been placed. Walk it in order and draw each rect scaled by yourWidth / canvasWidth. Three fields decide what a row means:

Field Means
onCanvas The canvas is compositing this source right now. Draw it.
inTray Active and eligible, but it did not get a seat. "Up next", not on air.
no rect Not placed. There is no rectangle — do not draw a zero one.

onCanvas is resolved per layout mode, from the same state the encoder is driven from, so you never have to work it out yourself:

  • auto — the source holds a seat in the server planner's current resolution of activePreset. A source the preset displaced comes back onCanvas: false and with no rect, even though the underlying layout row still holds the rectangle it had under the previous preset.
  • advanced — the source has a stored, visible, non-degenerate rectangle.

Do not derive membership from traySourceIds. That list is the auto planner's overflow intent. Nothing maintains it in advanced mode, so a tile you placed by hand reads onCanvas: true while still appearing in it — that is correct, not a contradiction. It ships on the read because POST /canvas/preset takes it as a whole-set replacement and you need to be able to send back what you read; it is not a membership signal.

A source that is inactive is neither onCanvas nor inTray. It is an input that exists. Saying it is in the tray would imply it is one action from air when it is two.

How sure is that answer?

membershipBasis on the canvas tells you where onCanvas/inTray came from:

Value Means
resolved The normal, authoritative answer. Trust it.
stored The auto planner could not resolve this activePreset — the encoder ships the preset catalog independently, so a studio can hold one this server does not know. Membership fell back to persisted columns and may be stale. Render the canvas, but do not conclude a source is off air.
"" An older server. It makes no claim; do not read it as either value.

ownerSub, when present, is the person a device source belongs to (a browser camera, a shared screen, a phone leg). Group rows by it if you want to fold one person's several legs into one tile the way the studio does.

There are no thumbnail images here. The canvas read is geometry and identity; to see the actual picture, open a program monitor — Monitor the program output.

Polling cheaply

The read is conditional. Keep the ETag and send it back:

GET /v1/studios/{studioId}/canvas
If-None-Match: "a3f9…"

An unchanged canvas answers 304 with no body. The tag covers the whole snapshot, not just layoutRev, so a source renamed or deleted invalidates it too — a 304 always means your picture is still correct.

Move a tile

PATCH /v1/studios/{studioId}/canvas/placements/{sourceId}
Content-Type: application/json

{
  "rect": { "x": 100, "y": 50, "width": 800, "height": 450 },
  "baseRev": 42
}

Every field is optional and omitted means preserve. That is what makes this legal:

{ "zIndex": 3 }          // bring it forward. Rect, alpha, crop all untouched.
{ "visible": false }     // hide it, keeping its rectangle so it does not jump back
{ "cropZoom": 1.8, "cropPanX": 0.4, "cropPanY": 0.5 }   // punch in

The one exception: a source that has never been placed has nothing to preserve, so its first write must carry a rect. You will get a 400 that says so.

This flips the studio to advanced. Per-source geometry is the mode: the server writes it alongside the rectangle, atomically, because a rectangle sitting under auto is geometry the next re-plan is about to erase. If you wanted to nudge one tile inside an auto arrangement, you wanted a preset — or this, then a replan to hand it back.

Units, and what happens if you get them wrong:

Field Range Out of range
rect canvas units; must intersect the canvas; positive, no larger than it 400
zIndex -10000 … 10000, higher draws in front 400
alpha 0.0 … 1.0 400
cropZoom 1.0 (none) … 10.0 400
cropPanX / cropPanY 0.0 … 1.0, the visible-rect centre 400
fitMode contain or stretch 400

These are refused, never clamped. If you sent a pan as a percentage, a 400 is the only version of this exchange where you find out.

Editing next to a live operator

layoutRev is a revision that every canvas write bumps. Send back the one you read as baseRev and a write based on a revision the canvas has already passed comes back 409 instead of clobbering. Re-read, rebase, retry.

Omit baseRev and you get last-write-wins. A phone editing while someone else is at the desk should send it; a script making one considered change need not.

Retrying on a bad connection

A retried identical write is not a conflict. If your request lands and the response never arrives — which on a train is most of them — send the same request again with the same baseRev. A stale baseRev whose request describes the tile exactly as it already is comes back 200, because the write it asks for is already in effect.

That exception is narrow on purpose. A different write on the same stale baseRev is still a 409. So: retry until you get an answer, and treat a 409 as what it is — someone else moved something.

Every 503 carries Retry-After. Back off on it rather than hammering.

Pick a preset

Usually the better call from a phone. You name the arrangement; the server resolves the pixels — and keeps resolving them as guests join and leave, which six hand-placed rectangles will not.

POST /v1/studios/{studioId}/canvas/preset

{ "presetId": "2up", "focusSourceId": "src-camera" }

Discover the ids, do not hardcode them:

GET /v1/studios/{studioId}/canvas/capabilities
{
  "presets": [
    { "id": "solo", "displayName": "Solo", "slotCount": 1,
      "slots": [ { "index": 0, "x": 0, "y": 0, "w": 1, "h": 1 } ],
      "overflowToTray": true, "schematicSvg": "<svg …>" }
  ],
  "libraryVersion": "1",
  "layoutModes": ["auto", "advanced"],
  "fitModes": ["contain", "stretch"],
  "transitions": ["cut", "fade", "slide_left", "…"],
  "canvasWidth": 1920, "canvasHeight": 1080
}

The catalog belongs to the encoder, not to this API — ids come and go with encoder releases, and a client with a baked-in list quietly loses the ones it never heard of. Cache on libraryVersion and refetch when it changes. slots are normalized 0–1 fractions, so schematicSvg (or the slots themselves) draws as a picker thumbnail at any size.

An empty presets with an empty libraryVersion means the encoder could not be reached. That is a state to render — fall back to what you cached, or hide the picker — not an error to retry into. The vocabularies below it never degrade; they are the gateway's own.

tileOrder, traySourceIds and slotAssignments are whole-set replacements when you send them, and untouched when you do not. Send the complete list; a partial one silently drops what it omits.

Which means read before you write. GET /canvas always returns slotAssignments — never null, and an empty object means "no sticky seats are assigned", a real state rather than a field that failed to populate. Take that map, change the entries you mean, and send the whole thing back:

GET  /v1/studios/{id}/canvas          -> { "slotAssignments": { "2up": { "0": "src-a" } }, ... }
POST /v1/studios/{id}/canvas/preset   <- { "presetId": "2up",
                                           "slotAssignments": { "2up": { "0": "src-a", "1": "src-b" } } }

Sending {"2up": {"1": "src-b"}} there does not add a seat — it replaces the map and unseats src-a.

Preset selection is last-write-wins and takes no baseRev. Picking a preset is a discrete human action taken a few times a show — two people a second apart should land on the second one, not on a conflict.

Switch scenes

A scene is a snapshot of the canvas an operator captured: the rectangles, the mode, and which sources were up.

GET  /v1/studios/{studioId}/scenes
POST /v1/studios/{studioId}/scenes/active     { "sceneId": "scn-wide" }
{ "data": [
  { "id": "scn-wide", "name": "Wide", "isActive": true,
    "transitionType": "cut", "transitionDuration": 0, "hasComposition": true }
] }

hasComposition: false means the scene is a label with nothing saved behind it — recalling it changes nothing. Grey the row rather than offering a recall that silently does nothing.

Recalling a scene on a live studio changes what viewers see, immediately. There is no preview bus here to rehearse on. Scene rows carry an is-preview flag, but no second canvas is composited from it, so a "preview" on this API would name a rehearsal that does not happen. Program is the only destination a scene has.

The recall returns the resulting canvas, so you can render straight from the response.

Go back to auto

POST /v1/studios/{studioId}/canvas/replan     { "layoutMode": "auto" }

The way out of advanced mode: the row flips and the planner re-runs in one call, replacing hand-placed rectangles with its own. Sent without a mode against a studio that is already in auto, it is a pure re-resolve.

Sent without a mode against an advanced studio it does nothing, and says so by returning the unchanged canvas. That is deliberate: the planner does not own geometry in advanced mode, and quietly re-planning a studio whose operator took manual control would move tiles on air that nobody asked to move.

You will rarely need it. A guest joining, a camera going live, a preset being picked — all of those re-plan on their own. replan is for the case where your own edits have wandered and the honest fix is to hand the arrangement back.

What is not here: dragging

There is no REST call for a drag, and there should not be one. Every endpoint on this page persists to the database, re-asserts the geometry to a running encoder, and fans an event out to every open studio tab. That is the right cost for a committed move and completely the wrong cost thirty times a second.

If you are building a surface where someone drags a tile with a finger, do it the way the web studio does: move the tile locally at whatever frame rate you like, and commit the final rectangle once with a single PATCH …/canvas/placements/{sourceId}. If you need the intermediate frames to appear on the program in real time — a hardware controller, a live nudge — that is the studio WebSocket's ephemeral transform frame, described in Drive the studio canvas, which persists nothing and acknowledges nothing by design.

A whole session

Read, move one tile, put it back the way the studio would have it:

BASE=https://api-dev.traxstreaming.live/v1
STUDIO=…; KEY=sk_live_…

# 1. What does it look like?
curl -s -H "Authorization: Bearer $KEY" "$BASE/studios/$STUDIO/canvas"

# 2. Move the camera tile, guarded by the rev you just read.
curl -s -X PATCH -H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' \
  "$BASE/studios/$STUDIO/canvas/placements/src-camera" \
  -d '{"rect":{"x":0,"y":0,"width":1920,"height":1080},"baseRev":42}'

# 3. Changed your mind — hand it back to the planner.
curl -s -X POST -H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' \
  "$BASE/studios/$STUDIO/canvas/replan" -d '{"layoutMode":"auto"}'

Each of those returns the whole canvas, so you never have to re-read to find out what your change did.

Where to go next

  1. Monitor the program output — see the picture the canvas you are driving actually produces.
  2. Control the mixer from your app — the audio half of the same surface.
  3. Drive the studio canvas — the WebSocket path, for a first-party app that needs live drag frames and push updates.
  4. The /v1 reference — every endpoint, every scope.