Skip to content

0.4.1: finish the grouping, type the protocol's numbers, and chain client options - #20

Merged
Agash merged 12 commits into
masterfrom
fix/screenshot-decode-and-v04-docs
Aug 26, 2026
Merged

0.4.1: finish the grouping, type the protocol's numbers, and chain client options#20
Agash merged 12 commits into
masterfrom
fix/screenshot-decode-and-v04-docs

Conversation

@Agash

@Agash Agash commented Aug 26, 2026

Copy link
Copy Markdown
Owner

Follow-up to #14. Everything here was found by auditing the built assembly rather than the source, which turned up surface that #14 already claimed was done.

Grouping, finished

  • 60 event stream accessors were still flat extensions on the client, so half the surface was grouped and half was not. They move onto their category group, generated from the same category field in protocol.json that the requests use. {Category}RequestGroup becomes {Category}Group.
  • Both client helper files had been left as ~130 lines of banner comments with nothing under them when their methods moved into groups.
  • WaitForEventAsync had overloads split across two static classes, and the two CallBatchAsync overloads returned different types, so picking the lambda one silently gave untyped results.

The protocol's numbers

JSON has one numeric type, so every Number became double and callers read scene item ids, frame counts and byte counts as floating point. Which fields are integral is not recoverable from the definition: sceneItemId and inputVolumeMul are both Number with a >= 0 restriction. The classification is an explicit table, not a rule over field names, because guessing wrong on a volume field truncates it silently while an unlisted field only stays double. 32 field names become int or long, 10 stay double, and an unlisted field reports OBSWSGEN012.

Client options

AddObsWebSocketClient returns a builder, so WithAutoConnect, WithHealthCheck and WithReconnectPipeline chain off it. WithAutoConnect previously hung off IServiceCollection and could not target one of two named clients.

Fixes

  • GetSourceScreenshot returns a data URI, so the in-memory helper threw and returned null for every call. The file variant was unaffected, which is why it went unnoticed.
  • Six helpers decided "not found" by substring matching the exception message. ObsWebSocketRequestException.StatusCode exposes the reported status as the protocol enum and they match on that.
  • The RequestStatus enum shared a name with the RequestStatus record on every response, which is the pair a caller has to disambiguate to write a status filter. It is RequestStatusCode.
  • csharpier was pinned in dotnet-tools.json and never run; 69 of 99 files had drifted. Some of that drift was misleading rather than cosmetic. CI checks it now.
  • Three tests failed only under parallel load, each for a real reason: two drove a FakeTimeProvider and then waited on real Task.Delay, and one put a 1s wall clock limit on a test that only builds a container.

Verification

Validated live against OBS 32.2.2 on both transports, 29 checks each with value assertions. Writing an int over MessagePack is asserted explicitly, including 0, which also covers the falsy-field fix from #11.

Closes #14

Agash added 12 commits August 26, 2026 07:15
GetSourceScreenshot returns "data:image/png;base64,..." rather than bare
Base64, so Convert.FromBase64String threw and the in-memory helper
returned null for every call. The file variant was unaffected because it
never touched the payload.
The suite covered the generated requests but not the hand written
conveniences, which is how the screenshot decode bug survived.
Every example still showed the flat extension methods, which are gone.
The 60 stream accessors were the last flat extensions on the client, so
half the surface was grouped and half was not. The protocol documents
requests and events under one set of category headings, and the groups
now hold both.

Renames the group types from {Category}RequestGroup to {Category}Group,
since they no longer hold only requests. Call sites go from
client.SceneCreatedStream() to client.Scenes.SceneCreatedStream().
Both helper files were left as hollow banner comments when their methods
moved onto the category groups, and WaitForEventAsync had overloads split
across two static classes. The lambda CallBatchAsync overload also still
returned the untyped list while the builder overload returned
BatchResults, so which overload you picked silently decided whether you
got typed results.
The tool was pinned in dotnet-tools.json but nothing ever ran it, so 69
of 99 files had drifted. Some of that drift was misleading rather than
cosmetic: the group structs kept the indentation of the extension blocks
their methods were lifted out of. CI now checks it.
Six helpers decided "not found" by substring matching the exception
message, so rewording a message would have turned a null return into a
throw. ObsWebSocketRequestException now exposes the reported status as
the protocol enum, and the helpers match on that.

SetInputMutesAsync still built its batch out of raw BatchRequestItem and
returned nothing, leaving per input failures visible only in the log. It
uses the typed builder now and returns the results.

FindSceneItemIdAsync returns int? so the Int32 suffixed duplicate is no
longer needed, and the Try prefixed forwarder announced for removal in
v0.4 is gone.
The protocol has one numeric type because JSON has one, so every Number
became a double and callers read scene item ids, frame counts and byte
counts as floating point. Which fields are integral is not recoverable
from the definition: sceneItemId and inputVolumeMul are both Number with
a >= 0 restriction. The classification is therefore an explicit table,
not a rule over field names, because guessing wrong on a volume field
truncates it silently while an unlisted field only stays double.

32 field names become int or long, 10 stay double. A refresh that adds
an unlisted Number field reports OBSWSGEN012 rather than drifting in.

Verified against OBS 32.2.2 on both transports: MessagePack decodes the
retyped fields, which was the risk.
WithAutoConnect hung off IServiceCollection, so it read as applying to
nothing and could not target one of two clients. AddObsWebSocketClient
now returns a builder, the shape AddHttpClient uses, and WithAutoConnect,
WithHealthCheck and WithReconnectPipeline chain off it. A named client
gets its own connection service, its own options instance and a health
check name that does not collide with the other client's.

Also fixes a test that drove a FakeTimeProvider with a real Task.Delay,
which cannot finish inside its own timeout when the suite runs in
parallel. It waits on a signal now.
The protocol's RequestStatus enum shared a name with the RequestStatus
record carried on every response, which is the pair a caller has to
disambiguate to write a status filter. The enum is RequestStatusCode now.

SwitchSceneAsync took a switchToProgram flag and sat next to the named
shorthands, so the group appeared to offer five ways to switch a scene.
The flag versions are private, reached through SwitchProgramSceneAsync
and SwitchPreviewSceneAsync, and the missing preview-and-wait shorthand
now exists.

Two more connection tests drove real delays rather than the fake clock,
and a container resolution test carried a one second wall clock limit
with nothing in it that could hang. All three failed only under load.
The example built a typed batch and then dropped it to Raw, and
registered the health check separately from the client. The README's
status filter still named the enum by its old name, and neither the
numeric typing nor the per client chain were documented.
@Agash
Agash merged commit c5c2a76 into master Aug 26, 2026
7 checks passed
@Agash
Agash deleted the fix/screenshot-decode-and-v04-docs branch August 26, 2026 07:07
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.

0.4: modernize the API surface for current .NET conventions

1 participant