Skip to content

Handles for addressing scenes, inputs, sources, scene items and filters - #31

Merged
Agash merged 11 commits into
masterfrom
feat/entity-handles
Aug 31, 2026
Merged

Handles for addressing scenes, inputs, sources, scene items and filters#31
Agash merged 11 commits into
masterfrom
feat/entity-handles

Conversation

@Agash

@Agash Agash commented Aug 26, 2026

Copy link
Copy Markdown
Owner

Draft. Verified against a live OBS 32.2.2 on both transports.

Addresses #19.

What it does

await client.Scene("Intro").SetCurrentProgramAsync(ct);          // a string is a name
await client.Scene(sceneGuid).SetNameAsync("Outro", ct);         // a Guid is a uuid
await client.Input("Mic").Filter("EQ").SetEnabledAsync(true, ct);

SceneOperations intro = await client.Scene("Intro").ResolveAsync(ct);
SceneItemOperations logo = await client.Scene("Intro").ItemAsync("Logo", cancellationToken: ct);

client.Scenes.CurrentProgramSceneChanged += (_, e) => Act(e.EventData.Scene);   // already a uuid

Why a handle is the accurate type

From Request::AcquireSource upstream: a uuid wins outright, a name is read only when no uuid was sent, canvasUuid is consulted only on the name path, and neither field present is MissingRequestField. So today new SetCurrentProgramSceneRequestData() compiles and fails at runtime, and sending both silently ignores the name.

A handle is that choice made once. It holds identity only, never cached state.

Generated, not transcribed

The protocol never says "this request takes a scene". It says the request has an optional sceneName beside an optional sceneUuid, repeated across 68 requests. Reading that shape back out gives 66 requests across five operation types: Input 30, SceneItem 13, Scene 11, Filter 7, Source 5. DuplicateSceneItem's second scene falls out of the rule as a destinationScene parameter without anyone having thought about it, which is the test that it is a rule and not a list.

The same reading of events and responses gives 47 handles that cost no round trip, including CreateScene and CreateInput, which already answer with a uuid.

Two decisions worth reviewing

These are not overloads of the existing methods. That was the first attempt and it broke 48 call sites: M(XRequestData) and M(XHandle) are ambiguous the moment a caller writes M(new(...)), and that idiom is all over the surface and the README.

Guid, not a wrapper struct. OBS writes RFC 4122 uuids, so Guid's "D" format matches exactly. A raw Guid does not distinguish a scene from an input, but OBS does not either: obs_get_source_by_uuid takes any source uuid, and AcquireScene catches the mismatch with InvalidResourceType. Handles carry the uuid as the wire string and never parse it, so a value OBS sends that is not a Guid cannot break a response.

Numbers OBS does not bound

The request sweep caught GetOutputList failing outright: an idle virtual camera reported outputHeight as 2586032160. It comes from obs_output_get_width/_height, which are uint32_t and pass through unclamped, so an int could not hold it — and one bad field fails the whole response, not just itself. Intermittent, and only for whoever has a virtual camera or a capture plugin installed.

That prompted a sweep of the whole numeric table against the obs-websocket and obs-studio sources. The rule that came out of it: a field is safe as an int only where obs-websocket validates a range; where it copies a value out of libobs or a settings blob, the C type decides. Reclassified:

Field Upstream
outputWidth/outputHeight (stub) uint32_t, unclamped
render*Frames, output*Frames uint32_t, monotonic — passes int32 at ~414 days at 60fps
webSocketSession*Messages uint64_t
sceneItemId int64_t, validated >= 0 with no upper bound
transitionDuration int64_t out of private settings, not revalidated on read
alignment, boundsAlignment uint32_t, validated to 0 .. uint32_t max

sceneIndex becomes nullable for the same class of reason: GetCanvasSceneList enumerates through a callback with no index to report and sends null.

Everything else stayed int on evidence, not assumption — the resolutions are validated 8..4096, the indices 0..8192, and the monitor fields are Qt int.

sceneItemId widening to long is the one API-visible cost, and it is why this is in the handle PR rather than a follow-up: SceneItemHandle is built around that type, and changing it now costs a compile error at ~20 call sites instead of a break after release.

Verified against OBS 32.2.2, both transports

The rename check is the one that matters:

resolved to fb07b481-..., both read 1 item(s); after a rename the uuid still resolves (True) and the name does not (True)

Also live: a scene item resolving by source name and navigating back to its scene, and a failed lookup naming what does exist. That last one is free, because resolving already fetched the list.

The settings-mode checks now build their own browser source and gain filter and remove them again, so a fresh OBS install runs the same suite. Verified by clearing OBS to a single input first: 0 failures, both transports, and nothing left behind afterwards.

340 tests, 0 warnings, full forced regeneration clean, zero unreadable payloads across the request sweep.

Not done

  • SetCurrentProgramAsync is what the naming rule produces from SetCurrentProgramScene. Mechanically right, reads slightly oddly.
  • No CanvasOperations: the canvases category holds only GetCanvasList, so there is nothing to scope. CanvasHandle still exists, because it scopes scene and source names.

Agash added 11 commits August 26, 2026 19:59
66 of the 147 requests address something by a name-or-uuid pair, and the
protocol never says so: it repeats an optional {X}Name beside an
optional {X}Uuid. Reading that shape back out gives five operation
types, so the set follows a protocol refresh instead of being
transcribed. DuplicateSceneItem's second scene falls out of the rule
without anyone having thought about it.

Not overloads of the existing methods: M(XRequestData) and M(XHandle)
are ambiguous the moment a caller writes M(new(...)), and that idiom is
all over the surface and the README.
…ween handles

client.SceneItem(logo).SetSceneItemEnabledAsync(false) says scene item
twice, once in the thing addressed and once in the verb. The operations
types name the request without it, and the protocol name stays in the
documentation and on the category group. The set is named at once so a
collision falls back to full names rather than one request shadowing
another; none collide today.

Navigation covers what the protocol does not describe: a scene contains
items, an input carries filters, and both are sources.
Found by the request sweep: GetOutputList became unreadable because an
idle virtual camera reported outputHeight as 2586032160. The field is
uint32_t in libobs and obs-websocket passes it through unclamped, so an
int could not hold it, and one bad field fails the whole response rather
than just itself.

Swept the rest of the numeric table against the obs-websocket and
obs-studio sources. A field is safe as an int only where obs-websocket
validates a range; where it copies out of libobs or a settings blob the C
type decides. That reclassifies:

  outputWidth/Height (stub)  uint32_t, unclamped
  render/output frames       uint32_t, monotonic
  webSocketSession messages  uint64_t
  sceneItemId                int64_t, validated >= 0 with no upper bound
  transitionDuration         int64_t out of private settings
  alignment/boundsAlignment  uint32_t, validated to the full range

sceneIndex becomes nullable for the same reason: GetCanvasSceneList has
no index to report and sends null.

Removes FindSceneItemIdInt32Async, which named a return type it no longer
has.
The handle API had no entry in the README at all, so nothing pointed at
it. Adds a table up front for choosing between handles, the category
groups and CallAsync, then a section on handles themselves.

Trims what was explaining implementation rather than behaviour: the
options-validation note, the non-null response note, and the parallel
batch section, which spent ninety lines on an upstream labelling bug.
The interactive commands all addressed things by name through the
category groups, so the handle API had no demonstration. Moves the ones
that gain from a handle: mute, list-filters, toggle-filter, the blend
mode in add-browser-source, and watch, which now acts on the uuid the
event already carries. Adds resolve, for the round trip and what a miss
reports.

set-text and get-input-settings stay on the group deliberately, because
the typed-settings helpers are hand-written and so have no handle form.
That contrast is the point; converting everything would hide the rule.

The settings-mode checks now create their browser source and gain filter
and remove them again. Discovering an existing input made the result
depend on the machine: a fresh OBS reported the modes as failing when all
that was missing was a source to try them on.
The stubs were written from what one OBS instance happened to return, so
fields that instance did not exercise were never typed. Diffing every
stub against the C++ that builds the JSON turns up eight:

  SceneItemStub   inputKind, sourceType, sceneItemBlendMode,
                  sceneItemBlendMethod
  InputStub       inputKindCaps
  OutputStub      outputFlags
  TransformStub   cropToBounds

They were landing in ExtensionData, so nothing failed, but none of them
were reachable as typed members.

OutputStub also carried an outputSettings member that GetOutputList never
sends; settings come from GetOutputSettings, per output.

inputKind and isGroup are nullable because OBS sends null for the case
that does not apply, and sceneItemBlendMethod because OBS 32.2.2 does not
send it at all.
CI checks formatting and the last three commits were not formatted.
@Agash
Agash marked this pull request as ready for review August 31, 2026 16:14
@Agash
Agash merged commit 2bb3d3f into master Aug 31, 2026
7 checks passed
@Agash
Agash deleted the feat/entity-handles branch August 31, 2026 16:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant