Moving from 3.x to 4.0
OBSClient 4.0 brings the client up to obs-websocket 5.7.6 (3.x implemented 5.5.2) and fixes a number of long-standing problems that came to light when every request and field was checked against the protocol definition. Most code that compiled against 3.x still compiles. This page lists what does not, what behaves differently, and what is new.
Note
4.0 is a release candidate. Install it with dotnet add package OBSClient --prerelease, and report what you find in an
issue. The public API is closed: what changes before 4.0.0 is fixes for what is
found, and a mistake in the API if somebody finds one. The changelog has the complete list.
Code that was compiled against 3.x has to be recompiled: a signature changed, even where the call looks the same.
What breaks
One parameter for a name or a UUID
3.x had a pair of overloads for everything that OBS Studio looks up by name or by UUID: GetInputMute(string inputName) and
GetInputMute(Guid inputUuid). 4.0 has one method that takes a SceneRef,
InputRef or SourceRef: a small value that holds a name or a
UUID. A string, a Guid, and the Scene, Input or SceneItem that OBS Studio reported convert to it implicitly, so calls read as
before:
await client.SetCurrentProgramScene("Gaming Scene");
await client.SetCurrentProgramScene(sceneUuid);
await client.SetCurrentProgramScene(scene); // identified by its UUID
What does break:
Named arguments.
inputName:andinputUuid:are nowinput:,sceneName:andsceneUuid:arescene:,sourceName:andsourceUuid:aresource:.// 3.x bool muted = await client.GetInputMute(inputName: "Microphone"); // 4.0 bool muted = await client.GetInputMute(input: "Microphone");A method group converted to a delegate. The implicit conversion from
stringtoInputRefapplies to a call, not to a method group.// 3.x Func<string, Task<bool>> mute = client.GetInputMute; // 4.0 Func<string, Task<bool>> mute = name => client.GetInputMute(name);
The same goes for the builders of RequestBatchMessage. The canvas of a request is a trailing optional
parameter: GetSceneItemList("Scene", canvasUuid: id).
Settings of CreateInput and SetCurrentSceneTransitionSettings
CreateInput (and AddCreateInputRequest) take a Dictionary<string, object>? for the settings of the new input instead of an Input?, and
SetCurrentSceneTransitionSettings (and its builder) takes a Dictionary<string, object> instead of a TransitionResponse?. Neither
of the old types is a settings object, so 3.x had no way to give a new input its settings, such as the URL of a browser source.
await client.CreateInput(
"Gaming Scene", "Overlay", "browser_source",
new Dictionary<string, object>
{
["url"] = "https://example.com",
["width"] = 800,
["height"] = 600
});
Passing null to CreateInput still works.
Events are raised off the receive thread
The events of OBS Studio and ConnectionClosed used to be raised by the thread that reads from OBS Studio. They are queued now, and raised
by a task of the thread pool: one at a time, in the order in which they happened. See the rules for handlers.
Most code does not notice, but:
- A handler can wait for a response (
client.GetSceneList().Result). In 3.x and 4.0.0-beta1 that made the request time out, because the handler held the thread that has to read the response. A handler that was written around that (withasync/awaitorTask.Run) keeps working. - An event is not guaranteed to be raised before the response of the request that caused it.
await client.SetCurrentProgramScene("Game")can return before theCurrentProgramSceneChangedfor it was raised. Code that read something that the handler sets, right after theawait, has to wait for the event instead. - Events queue up while a handler runs, in memory and without a limit; the client logs a warning when 1000 are waiting. Keep handlers short.
ConnectionClosedcomes after the events that arrived before the connection ended, and onceDispose()was called nothing that was waiting is raised any more (a handler that is running is not waited for). It is raised handler by handler, so a handler that throws no longer keeps the others from being called.PropertyChangedand the statistics events are not part of this: they are raised on the thread that causes the change, which can still be the thread that reads from OBS Studio.
SetSceneItemTransform takes an update
SetSceneItemTransform and its builder take a
SceneItemTransformUpdate instead of a SceneItemTransform. A SceneItemTransform converts to it
implicitly, so a call compiles unchanged. The update has a nullable member for each part of the transform, and sends only
what you set, which is what obs-websocket supports. See Calling requests.
A CancellationToken at the end of every request method
Every request method, ConnectAsync, SendRequestBatchAsync and ReidentifyAsync have a new last parameter,
CancellationToken cancellationToken = default: see cancelling a request. A call compiles unchanged,
but there are two things to know:
- Code that was compiled against 3.x or 4.0.0-beta1 has to be recompiled, because the signature of each of these methods changed. (Everything that was compiled against 3.x had to be recompiled anyway, see above.)
- A method group converted to a delegate no longer converts:
Func<Task<bool>> enabled = client.GetStudioModeEnabled;does not compile, because a method with an optional parameter does not fit a delegate type without it. Use a lambda:Func<Task<bool>> enabled = () => client.GetStudioModeEnabled();.
The classes are sealed, and the wire types are internal
4.0 freezes an API that can grow but never has to shrink: a type that is public cannot be made internal, and a class that others can derive from cannot be sealed, without breaking someone's code, whereas the other way round breaks nobody. So:
- The classes are sealed:
ObsClient, the classes of the responses and of the events, and the other classes of the API. Only the exceptions (as in the framework) andTransition, whichTransitionResponseextends, are not. Code that derives from a sealed class no longer compiles: hold the object in a class of your own instead.ObsClienthad nothing virtual to override butDispose(bool), which is gone:Dispose()does it all. - The types of the wire protocol are internal:
HelloMessage,IdentifyMessage,IdentifiedMessage,ReidentifyMessage,EventMessage,RequestBatchResponseMessage,Authentication,IMessageand the JSON converterNullableNumberToNumberConverterare what the client and OBS Studio exchange to connect, to deliver an event and to answer a batch, and the enumsOpCodeandEventTypeand the classKeyModifiersare what those messages are made of. What they carry reaches you through the methods and the events ofObsClient(the events are raised as events, with their own arguments, and a hotkey takes theKeyModifierflags). The types that build and read a batch stay public:RequestBatchMessage,RequestMessageandRequestResponseMessage(see Request batches).
Other signatures
| What | 3.x | 4.0 |
|---|---|---|
Scene |
Scene(int sceneIndex, string sceneName), SceneIndex is an int |
A sceneUuid is required, and SceneIndex is an int?: a scene of a canvas other than the main one has no index. |
CreateRecordChapter |
Task<string?> CreateRecordChapter(), which always threw an InvalidCastException |
Task CreateRecordChapter(string? chapterName = null). CreateRecordChapterResponse is gone. |
SceneTransitionResponse |
TransitionName and TransitionDuration could not be null |
string? and long?: a scene without a transition override has neither. |
SceneTransitionListResponse.CurrentSceneTransitionKind, RecordStateChangedEventArgs.OutputPath |
string |
string? |
SpecialInputsResponse (Desktop1 to Mic4) |
string |
string?: OBS Studio sends null for an input that is not configured. |
InputVolumeMeterResponse.InputKind, UnversionedInputKind |
string |
string?: OBS Studio never sends them. |
PropertyItem.ItemValue |
string |
JsonElement?: OBS Studio sends a string, a number or null. item.ItemValue!.Value.GetString() is what ItemValue was for a list of strings. |
SceneItemListReindexedEventArgs.SceneItems |
SceneItem[] |
BasicSceneItem[]: the SceneItemId and SceneItemIndex that OBS Studio sends for each item. |
TotalMessagesReceived, SessionMessagesReceived |
Counted WebSocket frames, so a large message counted several times | They count messages. |
AddOpenSourceProjectorRequest, AddOpenVideoMixProjectorRequest |
Threw an ArgumentException for an invalid combination of monitor and geometry |
Send what they are given and leave the check to obs-websocket, which answers an error. |
MonitorTypeResponse |
Existed, but no request returned it | Removed (GetInputAudioMonitorType returns the MonitorType). |
Crop, Coordinates, BalanceType, IconType |
Existed, but nothing used them: they were left over from the protocol of obs-websocket 4 | Removed. |
The constructors of RequestMessage that took a dynamic |
protected internal |
Removed. |
| The package dependency | Microsoft.Extensions.Logging 9.0.9 |
Microsoft.Extensions.Logging.Abstractions 8.0.0, which has the ILogger that the client uses. An application that makes its own logger with LoggerFactory references Microsoft.Extensions.Logging itself; one that gets its logger from dependency injection has it already. |
What behaves differently
These were bugs. Code that worked around them may not need the workaround any more.
SetInputSettingsno longer kills the connection. No response type was registered for it, so reading the response threw in the receive loop, which ended without cleaning up:ConnectionStatekept sayingConnectedwhile requests timed out. This was #17 and the symptoms of #15. A receive loop that ends for any reason now always cleans up, soConnectionStatereachesDisconnected,ConnectionClosedis raised andAutoReconnectworks.ConnectAsyncreturnsfalse, instead of hanging, when the connection is lost before OBS Studio accepted the handshake, gives up after 10 seconds when the handshake is never completed, and accepts port 65535.- Requests that are waiting when the connection is lost fail at once with an
ObsClientExceptioninstead of after their timeout. haltOnFailureof a batch takes effect. The client sent it under the wrong name, so a batch always ran on after a failure.- State changes are no longer dropped. 3.x passed on one event per 100 ms for each of
InputActiveStateChanged,InputShowStateChanged,SceneItemTransformChangedandInputVolumeMeters, whichever input or scene item it was about, so a scene switch reached you as one change. OnlyInputVolumeMetersis throttled now. - The data of a
CustomEventarrives. 3.x read a member that obs-websocket never sends. - A password with a character outside ASCII works: the client hashed it as ASCII, obs-websocket hashes UTF-8.
GetSceneSceneTransitionOverrideworks for a scene without an override, which is every scene by default.SetInputMuteby UUID,GetInputDeinterlaceFieldOrderandGetInputAudioMonitorTypework; each always failed.SetSceneItemTransformno longer fails for an item without bounds.- The
RecordFileChangedevent (obs-websocket 5.5.0) reaches you. - An event type that the client does not know is ignored instead of throwing.
- Multi-byte characters in scene names and settings are no longer damaged at the boundary of the receive buffer.
What is new
- Canvases (obs-websocket 5.7.0):
GetCanvasList, theCanvasclasses,CanvasCreated,CanvasRemovedandCanvasNameChanged, theEventSubscriptions.Canvasesflag, and an optionalcanvasUuidon every request that looks up a scene or a source by name. - The requests that had no method under their own name:
SetInputVolume,OpenSourceProjectorandOpenVideoMixProjector.SetInputVolumeMul,SetInputVolumeDband the projector helpers stay as shortcuts. - Batch builders that were missing, and batches that address inputs, scenes and sources by UUID.
SceneItemTransformUpdateandSceneItemTransform.ToUpdate().- Members that obs-websocket sends and 3.x did not expose, among them
InputCreatedEventArgs.InputKindCaps,Input.InputUuidandSceneItem.SourceUuid. - Every message the client sends is logged at the debug level, as what it receives already was.
- A
net10.0target next tonet8.0. - A test app, a WinUI 3 application to try every request and event by hand.
- A test suite with protocol conformance tests, and code that is generated from the protocol definition: see Staying in sync with obs-websocket.
Upgrading step by step
- Update the package:
dotnet add package OBSClient --prerelease(or--version 4.0.0-rc.1). - Build. The compiler points to what has to change: named arguments, method groups, the types in the table above.
- Read the list of behavior changes. If you kept a workaround for a bug that is listed there, try without it.
- Run your program against OBS Studio. The test app is a quick way to see what a response looks like.