From e957fb1e289d6558325675cf899b5ad481a056f8 Mon Sep 17 00:00:00 2001 From: Agash Date: Wed, 26 Aug 2026 07:15:32 +0200 Subject: [PATCH 01/12] fix(core): decode screenshots returned as a data URI 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. --- .../Groups/SourcesRequestGroup.cs | 15 ++++++- ObsWebSocket.Tests/ScreenshotDecodeTests.cs | 39 +++++++++++++++++++ 2 files changed, 52 insertions(+), 2 deletions(-) create mode 100644 ObsWebSocket.Tests/ScreenshotDecodeTests.cs diff --git a/ObsWebSocket.Core/Groups/SourcesRequestGroup.cs b/ObsWebSocket.Core/Groups/SourcesRequestGroup.cs index 60b4b37..5c3fd87 100644 --- a/ObsWebSocket.Core/Groups/SourcesRequestGroup.cs +++ b/ObsWebSocket.Core/Groups/SourcesRequestGroup.cs @@ -141,7 +141,7 @@ await client try { - return Convert.FromBase64String(response.ImageData); + return DecodeImageData(response.ImageData); } catch (FormatException formatEx) { @@ -150,7 +150,6 @@ await client "Failed to decode Base64 image data for screenshot of '{SourceName}'.", sourceName ); - // Wrap in ObsWebSocketException? Or just return null? Returning null seems reasonable for a helper. return null; } } @@ -252,4 +251,16 @@ await client.Sources.SaveSourceScreenshotAsync( ), cancellationToken).ConfigureAwait(false); } + + /// + /// Decodes the image OBS returns, which arrives as a data URI rather than bare Base64. + /// + /// The value of the response's image data field. + internal static byte[] DecodeImageData(string imageData) + { + ArgumentException.ThrowIfNullOrEmpty(imageData); + + int comma = imageData.IndexOf(',', StringComparison.Ordinal); + return Convert.FromBase64String(comma >= 0 ? imageData[(comma + 1)..] : imageData); + } } diff --git a/ObsWebSocket.Tests/ScreenshotDecodeTests.cs b/ObsWebSocket.Tests/ScreenshotDecodeTests.cs new file mode 100644 index 0000000..7511e1e --- /dev/null +++ b/ObsWebSocket.Tests/ScreenshotDecodeTests.cs @@ -0,0 +1,39 @@ +using ObsWebSocket.Core; + +namespace ObsWebSocket.Tests; + +/// +/// OBS returns screenshots as a data URI, not as bare Base64. Decoding the whole string +/// throws, so the in-memory screenshot helper silently returned nothing for every call. +/// +[TestClass] +public sealed class ScreenshotDecodeTests +{ + // The eight byte PNG signature, which is what a caller checks to know it got an image. + private static readonly byte[] PngSignature = + [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]; + + [TestMethod] + public void DecodeImageData_WithDataUriPrefix_ReturnsTheImageBytes() + { + string dataUri = "data:image/png;base64," + Convert.ToBase64String(PngSignature); + + byte[] decoded = SourcesRequestGroup.DecodeImageData(dataUri); + + CollectionAssert.AreEqual(PngSignature, decoded); + } + + [TestMethod] + public void DecodeImageData_WithBareBase64_StillDecodes() + { + byte[] decoded = SourcesRequestGroup.DecodeImageData( + Convert.ToBase64String(PngSignature) + ); + + CollectionAssert.AreEqual(PngSignature, decoded); + } + + [TestMethod] + public void DecodeImageData_WhenEmpty_Throws() => + Assert.ThrowsExactly(() => SourcesRequestGroup.DecodeImageData("")); +} From 91af632db314797f246b23de0e05c517b65a320a Mon Sep 17 00:00:00 2001 From: Agash Date: Wed, 26 Aug 2026 07:15:40 +0200 Subject: [PATCH 02/12] test(example): assert values for the helpers added in v0.4 The suite covered the generated requests but not the hand written conveniences, which is how the screenshot decode bug survived. --- ObsWebSocket.Example/Worker.cs | 143 +++++++++++++++++++++++++++++++++ 1 file changed, 143 insertions(+) diff --git a/ObsWebSocket.Example/Worker.cs b/ObsWebSocket.Example/Worker.cs index 6bf2770..1ad124c 100644 --- a/ObsWebSocket.Example/Worker.cs +++ b/ObsWebSocket.Example/Worker.cs @@ -1591,6 +1591,149 @@ in client.CurrentProgramSceneChangedStream(cancellationToken: streamCts.Token) ); }).ConfigureAwait(false)); + results.Add(await TrySettingsCheckAsync("SetInputVolumeMulAsync", async () => + { + GetInputVolumeResponseData? before = await client + .Inputs.GetInputVolumeAsync(new GetInputVolumeRequestData(inputName: inputName), cancellationToken) + .ConfigureAwait(false); + double original = before!.InputVolumeMul; + + await client.Inputs.SetInputVolumeMulAsync(inputName, 0.5, cancellationToken).ConfigureAwait(false); + GetInputVolumeResponseData? after = await client + .Inputs.GetInputVolumeAsync(new GetInputVolumeRequestData(inputName: inputName), cancellationToken) + .ConfigureAwait(false); + double mul = after!.InputVolumeMul; + + await client.Inputs.SetInputVolumeMulAsync(inputName, original, cancellationToken).ConfigureAwait(false); + return (Math.Abs(mul - 0.5) < 0.01, $"mul={mul:0.###}"); + }).ConfigureAwait(false)); + + results.Add(await TrySettingsCheckAsync("SwitchProgramSceneAsync", async () => + { + await client.Scenes.SwitchProgramSceneAsync(sceneName, cancellationToken: cancellationToken).ConfigureAwait(false); + GetSceneListResponseData? mid = await client + .Scenes.GetSceneListAsync(new GetSceneListRequestData(), cancellationToken) + .ConfigureAwait(false); + bool switched = string.Equals(mid?.CurrentProgramSceneName, sceneName, StringComparison.Ordinal); + + await client.Scenes.SwitchProgramSceneAsync(originalScene, cancellationToken: cancellationToken).ConfigureAwait(false); + GetSceneListResponseData? restored = await client + .Scenes.GetSceneListAsync(new GetSceneListRequestData(), cancellationToken) + .ConfigureAwait(false); + + return ( + switched && string.Equals(restored?.CurrentProgramSceneName, originalScene, StringComparison.Ordinal), + $"switched={switched}, restored to '{restored?.CurrentProgramSceneName}'" + ); + }).ConfigureAwait(false)); + + results.Add(await TrySettingsCheckAsync("FindSceneItemIdInt32Async", async () => + { + int? id = await client + .SceneItems.FindSceneItemIdInt32Async(sceneName, inputName, cancellationToken) + .ConfigureAwait(false); + int? miss = await client + .SceneItems.FindSceneItemIdInt32Async(sceneName, "__absent__", cancellationToken) + .ConfigureAwait(false); + + return (id is not null && miss is null, $"id={id}, miss={(miss is null ? "null" : "unexpected")}"); + }).ConfigureAwait(false)); + + results.Add(await TrySettingsCheckAsync("Screenshot helpers", async () => + { + byte[]? bytes = await client + .Sources.GetSourceScreenshotBytesAsync(sceneName, "png", cancellationToken: cancellationToken) + .ConfigureAwait(false); + + string path = Path.Combine(Path.GetTempPath(), $"obsws_{Guid.NewGuid():N}.png"); + try + { + await client + .Sources.SaveSourceScreenshotToFileAsync(sceneName, path, "png", cancellationToken: cancellationToken) + .ConfigureAwait(false); + + // A PNG starts with the eight byte signature, so this checks real image data + // rather than merely that the call returned. + byte[] written = await File.ReadAllBytesAsync(path, cancellationToken).ConfigureAwait(false); + bool pngOnDisk = + written.Length > 8 + && written[0] == 0x89 && written[1] == 0x50 && written[2] == 0x4E && written[3] == 0x47; + bool pngInMemory = + bytes is { Length: > 8 } + && bytes[0] == 0x89 && bytes[1] == 0x50 && bytes[2] == 0x4E && bytes[3] == 0x47; + + return (pngInMemory && pngOnDisk, $"{bytes?.Length ?? 0} bytes in memory, {written.Length} on disk"); + } + finally + { + if (File.Exists(path)) + { + File.Delete(path); + } + } + }).ConfigureAwait(false)); + + results.Add(await TrySettingsCheckAsync("Ensure profile and scene collection", async () => + { + // Asking for the one already active proves the check without disrupting OBS, + // since switching either of these reloads the whole configuration. + GetProfileListResponseData? profiles = await client + .Config.GetProfileListAsync(cancellationToken) + .ConfigureAwait(false); + GetSceneCollectionListResponseData? collections = await client + .Config.GetSceneCollectionListAsync(cancellationToken) + .ConfigureAwait(false); + + bool profileOk = await client + .Config.EnsureProfileActiveAsync(profiles!.CurrentProfileName!, cancellationToken) + .ConfigureAwait(false); + bool collectionOk = await client + .Config.EnsureSceneCollectionActiveAsync(collections!.CurrentSceneCollectionName!, cancellationToken) + .ConfigureAwait(false); + bool absent = await client + .Config.EnsureProfileActiveAsync("__no_such_profile__", cancellationToken) + .ConfigureAwait(false); + + return ( + profileOk && collectionOk && !absent, + $"profile={profiles.CurrentProfileName}, collection={collections.CurrentSceneCollectionName}, absent reported {absent}" + ); + }).ConfigureAwait(false)); + + results.Add(await TrySettingsCheckAsync("Media transport shorthands", async () => + { + await client.MediaInputs.PlayMediaAsync(inputName, cancellationToken).ConfigureAwait(false); + await client.MediaInputs.PauseMediaAsync(inputName, cancellationToken).ConfigureAwait(false); + await client.MediaInputs.RestartMediaAsync(inputName, cancellationToken).ConfigureAwait(false); + await client.MediaInputs.StopMediaAsync(inputName, cancellationToken).ConfigureAwait(false); + + GetMediaInputStatusResponseData? status = await client + .MediaInputs.GetMediaInputStatusAsync( + new GetMediaInputStatusRequestData(inputName: inputName), cancellationToken) + .ConfigureAwait(false); + + return (status is not null, $"state={status?.MediaState}"); + }).ConfigureAwait(false)); + + results.Add(await TrySettingsCheckAsync("Typed exception on a rejected request", async () => + { + try + { + _ = await client + .SceneItems.GetSceneItemListAsync( + new GetSceneItemListRequestData(sceneName: "__no_such_scene__"), cancellationToken) + .ConfigureAwait(false); + return (false, "no exception"); + } + catch (ObsWebSocketRequestException ex) + { + return ( + ex.Status?.Code == 600 && ex.RequestType == "GetSceneItemList", + $"{ex.RequestType} code {ex.Status?.Code}" + ); + } + }).ConfigureAwait(false)); + results.Add(await TrySettingsCheckAsync("Output state helpers", async () => { bool recording = await client.Record.IsRecordActiveAsync(cancellationToken).ConfigureAwait(false); From 621f430009e5e5801c234694f759d4111e2b2b55 Mon Sep 17 00:00:00 2001 From: Agash Date: Wed, 26 Aug 2026 07:15:40 +0200 Subject: [PATCH 03/12] docs: rewrite the README for the grouped surface Every example still showed the flat extension methods, which are gone. --- ObsWebSocket.Tests/ReadmeCompileCheck.cs | 31 ++ README.md | 362 +++++++++-------------- 2 files changed, 171 insertions(+), 222 deletions(-) diff --git a/ObsWebSocket.Tests/ReadmeCompileCheck.cs b/ObsWebSocket.Tests/ReadmeCompileCheck.cs index 22436b0..0a3c292 100644 --- a/ObsWebSocket.Tests/ReadmeCompileCheck.cs +++ b/ObsWebSocket.Tests/ReadmeCompileCheck.cs @@ -253,4 +253,35 @@ internal static void TelemetryAndKeyedRegistration(IServiceCollection services) _ = ObsWebSocketDiagnostics.MeterName; _ = ObsWebSocketResilience.ReconnectPipelineKey; } + + internal static async Task ScreenshotsAsync(ObsWebSocketClient client, CancellationToken ct) + { + byte[]? png = await client.Sources.GetSourceScreenshotBytesAsync("Intro", "png", cancellationToken: ct); + _ = png; + await client.Sources.SaveSourceScreenshotToFileAsync("Intro", "shot.png", cancellationToken: ct); + } + + internal static async Task ReplayBufferAsync2(ObsWebSocketClient client, CancellationToken ct) + { + var status = await client.Outputs.GetReplayBufferStatusAsync(ct); + if (status.OutputActive) + { + await client.Outputs.SaveReplayBufferAsync(ct); + } + } + + internal static async Task StudioModeAsync(ObsWebSocketClient client, CancellationToken ct) + { + try + { + await client.Ui.SetStudioModeEnabledAsync(new(true), ct); + } + catch (ObsWebSocketRequestException ex) + { + _ = $"{ex.RequestType} failed with {ex.Status?.Code}: {ex.Comment}"; + } + catch (ObsWebSocketTimeoutException) + { + } + } } diff --git a/README.md b/README.md index 200f58a..682ce08 100644 --- a/README.md +++ b/README.md @@ -18,29 +18,16 @@ Modern .NET client for OBS Studio WebSocket v5, with generated protocol types an dotnet add package ObsWebSocket.Core ``` -## Features - -- Strongly typed request/response DTOs generated from the obs-websocket protocol -- Strongly typed event args, observable as `IAsyncEnumerable` or as classic events -- Typed batch builder that pairs each request type with its own payload -- Async-first API with cancellation support -- DI helpers via `AddObsWebSocketClient()` -- JSON and MessagePack transports (configurable per environment) -- Reconnect, timeout, and event subscription options -- Typed settings helpers for inputs, filters, transitions, outputs, and stream service, working with built-in library types or your own AOT-safe source-generated types - > **OBS WebSocket v5 only** (OBS Studio 28+). Enable the server via *Tools → WebSocket Server Settings* in OBS. -## Quick Start +## Quick start `appsettings.json`: ```json { - "Obs": { - "ServerUri": "ws://localhost:4455", - "Password": "", - "Format": "Json" + "ConnectionStrings": { + "obs": "ws://localhost:4455?password=secret" } } ``` @@ -51,10 +38,11 @@ dotnet add package ObsWebSocket.Core using ObsWebSocket.Core; HostApplicationBuilder builder = Host.CreateApplicationBuilder(args); -builder.Services.Configure( - builder.Configuration.GetSection("Obs")); -builder.Services.AddObsWebSocketClient(); + +builder.AddObsWebSocketClient("obs"); // endpoint from ConnectionStrings:obs +builder.Services.WithAutoConnect(); // connect on start, disconnect on stop builder.Services.AddHostedService(); + await builder.Build().RunAsync(); ``` @@ -64,33 +52,44 @@ await builder.Build().RunAsync(); using ObsWebSocket.Core; using ObsWebSocket.Core.Events.Generated; -public sealed class Worker(ObsWebSocketClient client) : IHostedService +public sealed class Worker(ObsWebSocketClient client) : BackgroundService { - public async Task StartAsync(CancellationToken ct) + protected override async Task ExecuteAsync(CancellationToken ct) { - client.CurrentProgramSceneChanged += OnSceneChanged; - await client.ConnectAsync(ct); + var version = await client.General.GetVersionAsync(ct); + Console.WriteLine($"Connected to OBS {version.ObsVersion}"); - var version = await client.General.GetVersionAsync(cancellationToken: ct); - Console.WriteLine($"Connected to OBS {version?.ObsVersion}"); + await foreach (var e in client.CurrentProgramSceneChangedStream(cancellationToken: ct)) + { + Console.WriteLine($"Scene changed: {e.EventData.SceneName}"); + } } +} +``` - public async Task StopAsync(CancellationToken ct) - { - client.CurrentProgramSceneChanged -= OnSceneChanged; - if (client.IsConnected) await client.DisconnectAsync(); - } +## Everything is grouped by category - private static void OnSceneChanged(object? _, CurrentProgramSceneChangedEventArgs e) => - Console.WriteLine($"Scene changed: {e.EventData.SceneName}"); -} +The client mirrors the categories the OBS protocol defines, and the conveniences this library adds +sit in the same group as the requests they wrap, so there is one way to reach anything: + +```csharp +await client.Scenes.GetSceneListAsync(new(), ct); // generated request +await client.Scenes.SwitchProgramSceneAndWaitAsync("Intro", cancellationToken: ct); // convenience +await client.Inputs.SetInputVolumeDbAsync("Mic", -6, ct); +await client.SceneItems.SetSceneItemEnabledAsync("Intro", "Logo", false, ct); ``` -## Observing Events +The groups are `Canvases`, `Config`, `Filters`, `General`, `Inputs`, `MediaInputs`, `Outputs`, +`Record`, `SceneItems`, `Scenes`, `Sources`, `Stream`, `Transitions` and `Ui`. They come from the +protocol definition, so a refresh that recategorises a request moves it here too. + +`WaitForEventAsync` and `CallBatchAsync` stay directly on the client, since neither belongs to one +category. -Every OBS event is exposed as an async sequence. The stream subscribes for the lifetime of the -loop and unsubscribes when it ends, so there is no handler bookkeeping and cancellation is the -only thing that stops it: +## Observing events + +Every OBS event is exposed as an async sequence. The stream subscribes for the lifetime of the loop +and unsubscribes when it ends, so there is no handler bookkeeping: ```csharp await foreach (var e in client.CurrentProgramSceneChangedStream(cancellationToken: ct)) @@ -102,21 +101,19 @@ await foreach (var e in client.CurrentProgramSceneChangedStream(cancellationToke Streams buffer a bounded number of events and drop the oldest when a consumer falls behind, so a slow loop cannot stall the receive loop. Pass `capacity` to change that. -The classic events are unchanged and still work, including alongside a stream over the same event: +The classic events still work, including alongside a stream over the same event: ```csharp client.CurrentProgramSceneChanged += (_, e) => Console.WriteLine($"Program scene is now {e.EventData.SceneName}"); ``` -To wait for a single occurrence rather than a sequence, use `WaitForEventAsync`. It subscribes -before returning, so you can start the wait and then trigger the action without racing it: +To wait for a single occurrence, use `WaitForEventAsync`. It subscribes before returning, so you can +start the wait and then trigger the action without racing it: ```csharp -// Next occurrence, no timeout var changed = await client.WaitForEventAsync(ct); -// Next matching occurrence, giving up after 5 seconds var intro = await client.WaitForEventAsync( e => e.EventData.SceneName == "Intro", TimeSpan.FromSeconds(5), @@ -124,38 +121,37 @@ var intro = await client.WaitForEventAsync( ); ``` -## Common Use Cases +It throws `TimeoutException` when the wait elapses. + +## Common use cases ### Update a text source ```csharp -// One-liner helper await client.Inputs.SetInputTextAsync("NewsTicker", "Breaking: Live now!", ct); -// Or use a typed settings object to update multiple properties at once +// or several properties at once, with a typed settings object var settings = new TextGdiPlusInputSettings(Text: "Breaking: Live now!", WordWrap: true); await client.Inputs.SetInputSettingsAsync("NewsTicker", settings, cancellationToken: ct); ``` -`TextGdiPlusInputSettings` is a built-in library type. The same pattern applies to `TextFreetype2InputSettings`, `BrowserSourceSettings`, and the filter settings types, which live in `ObsWebSocket.Core.Protocol.Common.InputSettings` and `ObsWebSocket.Core.Protocol.Common.FilterSettings`. +`TextGdiPlusInputSettings` is a built-in library type, as are `TextFreetype2InputSettings`, +`BrowserSourceSettings` and the filter settings types, which live in +`ObsWebSocket.Core.Protocol.Common.InputSettings` and `.FilterSettings`. ### Check and save the replay buffer ```csharp -var status = await client.Outputs.GetReplayBufferStatusAsync(cancellationToken: ct); -if (status?.OutputActive == true) +var status = await client.Outputs.GetReplayBufferStatusAsync(ct); +if (status.OutputActive) { - await client.Outputs.SaveReplayBufferAsync(cancellationToken: ct); - Console.WriteLine("Replay saved."); + await client.Outputs.SaveReplayBufferAsync(ct); } ``` ### Create or update a browser source -Use a library type for common properties, or define your own type to target exactly what you need: - ```csharp -// Library type, covers the standard browser source properties var current = await client.Inputs.GetInputSettingsAsync("StreamOverlay", ct); Console.WriteLine($"Current URL: {current?.Url}"); @@ -166,14 +162,15 @@ await client.Inputs.SetInputSettingsAsync( ); ``` +Define your own type to target exactly what you need, and stay AOT-safe by passing its `JsonTypeInfo`: + ```csharp -// Consumer type, define only the properties you care about, fully AOT-safe [JsonSerializable(typeof(OverlaySettings))] internal partial class MyContext : JsonSerializerContext { } internal sealed record OverlaySettings( - [property: JsonPropertyName("url")] string? Url = null, - [property: JsonPropertyName("css")] string? Css = null + [property: JsonPropertyName("url")] string? Url = null, + [property: JsonPropertyName("css")] string? Css = null ); await client.Inputs.SetInputSettingsAsync( @@ -184,119 +181,19 @@ await client.Inputs.SetInputSettingsAsync( ); ``` -Both `Set` overloads take `overlay` before the cancellation token. It defaults to `true`, which merges your values onto the existing settings. Pass `overlay: false` to replace them outright. - -> Raw `JsonElement` access is also available. All settings helpers have counterparts in the generated types under `ObsWebSocket.Core.Protocol.Requests` if you need full control. - -## Requests and helpers - -Everything the client can do is reached through the category the OBS protocol puts it in, so a -generated request and a convenience that wraps several of them sit together and read the same way: - -```csharp -await client.Scenes.GetSceneListAsync(new(), ct); // generated request -await client.Scenes.SwitchProgramSceneAndWaitAsync("Intro", cancellationToken: ct); // convenience -``` - -The categories are OBS's own: `Canvases`, `Config`, `Filters`, `General`, `Inputs`, `MediaInputs`, -`Outputs`, `Record`, `SceneItems`, `Scenes`, `Sources`, `Stream`, `Transitions`, `Ui`. They follow -the protocol, so a refresh that recategorises a request moves it here too. The batch builder uses -the same grouping. - -`WaitForEventAsync` and `CallBatchAsync` stay directly on the client, since neither belongs to one -category. - -Every typed settings helper has two overloads: an implicit one for library-registered types, and an explicit one taking a `JsonTypeInfo` for consumer-provided types. Use the explicit overload to stay AOT-safe. - -**Settings read/write:** - -| Helper | Notes | -|---|---| -| `GetInputSettingsAsync` / `SetInputSettingsAsync` | Input settings; Set supports `overlay` | -| `GetInputDefaultSettingsAsync` | Default settings for a given input kind | -| `GetSourceFilterSettingsAsync` / `SetSourceFilterSettingsAsync` | Filter settings; Set supports `overlay` | -| `GetSourceFilterDefaultSettingsAsync` | Default settings for a given filter kind | -| `GetCurrentSceneTransitionSettingsAsync` / `SetCurrentSceneTransitionSettingsAsync` | Transition settings | -| `GetOutputSettingsAsync` / `SetOutputSettingsAsync` | Output settings | -| `GetStreamServiceSettingsAsync` / `SetStreamServiceSettingsAsync` | Stream service settings | - -Most of these take optional parameters ahead of the cancellation token, so pass it as `cancellationToken: ct`. - -**Scenes and scene items:** - -- `SwitchSceneAsync(scene, cancellationToken: ct)` switches the Program scene, or Preview with `switchToProgram: false`. Optional `transitionName` and `transitionDurationMs` apply to that switch only. -- `SwitchSceneAndWaitAsync(scene, cancellationToken: ct)` does the same and waits for the event confirming it. -- `SetSceneItemEnabledAsync(scene, sourceName, isEnabled, ct)` returns the resulting state. Leave `isEnabled` null to toggle. There is an overload taking a numeric `sceneItemId`. -- `FindSceneItemIdAsync(scene, sourceName, ct)` returns null instead of throwing when the item is not in the scene. -- `SourceExistsAsync(name, ct)` and `SceneExistsAsync(name, ct)` check for existence. - -**Inputs and filters:** - -- `SetInputTextAsync(name, text, ct)` is shorthand for updating text source content. -- `SetInputVolumeDbAsync(name, db, ct)` and `SetInputVolumeMulAsync(name, mul, ct)` each pick one unit. The underlying request accepts either and fails when given neither. -- `SetInputMutesAsync(inputMutes, ct)` sets many mute states in one batch, taking `IEnumerable<(string InputName, bool IsMuted)>`. -- `CreateInputAsync(kind, name, settings, ...)` creates an input with typed settings, optionally placing it in a scene. -- `CreateSourceFilterAsync(source, filterName, kind, settings, ct)` adds a typed filter. - -**Screenshots:** - -- `GetSourceScreenshotBytesAsync(source, ...)` returns the decoded image bytes rather than a base64 data URI. -- `GetSourceScreenshotOnCanvasBytesAsync(source, ...)` does the same at full canvas dimensions. -- `SaveSourceScreenshotToFileAsync(source, filePath, ...)` writes straight to disk. - -**Outputs:** - -- `SetRecordActiveAndWaitAsync(activate, timeout, ct)` and `SetStreamActiveAndWaitAsync(...)` start or stop the output and wait for OBS to confirm, returning the resulting `OutputState`. -- `IsRecordActiveAsync(ct)`, `IsStreamActiveAsync(ct)`, and `IsVirtualCamActiveAsync(ct)` read current state. -- `SetVirtualCamActiveAndWaitAsync(activate, timeout, ct)` does the same for the virtual camera. - -**Application state:** - -- `EnsureProfileActiveAsync(name, ct)` and `EnsureSceneCollectionActiveAsync(name, ct)` switch only if needed, returning whether the target is active afterwards rather than throwing when it does not exist. -- `TriggerHotkeyAsync(hotkeyName, ct)` fires a hotkey by name. -- `WaitForEventAsync(...)` awaits a single event. See [Observing Events](#observing-events). - -### Typed protocol enums - -Protocol enums that travel as strings have a real C# enum, so states can be matched rather than -compared against constants: - -```csharp -client.StreamStateChanged += (_, e) => -{ - string what = OutputStateExtensions.FromWireValue(e.EventData.OutputState) switch - { - OutputState.Started => "live", - OutputState.Starting or OutputState.Reconnecting => "coming up", - OutputState.Stopped or OutputState.Stopping => "going down", - null => $"unrecognised ({e.EventData.OutputState})", - _ => "in between", - }; - - Console.WriteLine($"Stream is {what}"); -}; -``` +Both `Set` overloads take `overlay` before the cancellation token. It defaults to `true`, merging +your values onto the existing settings; pass `overlay: false` to replace them. -Media transport works the same way, with shorthands for the common actions: +### Screenshots ```csharp -await client.MediaInputs.PlayMediaAsync("Stinger", ct); -await client.MediaInputs.TriggerMediaActionAsync("Stinger", MediaInputAction.Restart, ct); +byte[]? png = await client.Sources.GetSourceScreenshotBytesAsync("Intro", "png", cancellationToken: ct); +await client.Sources.SaveSourceScreenshotToFileAsync("Intro", "shot.png", cancellationToken: ct); ``` -The wire constants remain available as `const` strings on `ObsOutputState` and -`ObsMediaInputAction`, and `ToWireValue()` converts an enum back when you need the raw form. - -For direct low-level access, all generated request/response types are in: -- `ObsWebSocket.Core.Protocol.Requests` -- `ObsWebSocket.Core.Protocol.Responses` -- `ObsWebSocket.Core.Events.Generated` - -### Batch API +## Batches -Send several requests in one round trip, with OBS executing them back to back. Requests are -grouped by the protocol's own categories, and each one hands back a reference carrying its -response type: +Send several requests in one round trip. Each returns a reference carrying its response type: ```csharp ObsBatchBuilder batch = new(); @@ -316,9 +213,8 @@ Console.WriteLine(results.Get(version).ObsVersion); Console.WriteLine(results.Get(scenes).Scenes?.Count); ``` -`results.Get(reference)` returns the response record for that request, so neither its position nor -its type is restated. A request type may appear many times in one batch and each reference still -resolves to its own result. +`results.Get(reference)` restates neither the position nor the type, so a request type may appear +many times in one batch and each reference still resolves to its own result. `Sleep` is only valid inside a batch, and pairs with `SerialRealtime` to pace a sequence. @@ -335,34 +231,55 @@ if (!results.AllSucceeded()) } ``` -With `haltOnFailure: true` OBS stops at the first failure, so fewer results come back than -requests were sent. Reading a reference past that point throws, and `Count` reports how many ran. +With `haltOnFailure: true` OBS stops at the first failure, so fewer results come back than requests +were sent; reading a reference past that point throws, and `Count` reports how many ran. -`Add` takes anything the generated methods do not cover, including a raw `JsonElement` payload, -and an overload accepting a `JsonTypeInfo` keeps a custom payload AOT-safe: +`Add` covers anything the generated methods do not, including a raw `JsonElement`, and an overload +taking a `JsonTypeInfo` keeps a custom payload AOT-safe: ```csharp -batch.Add("GetStats").Add("SetInputSettings", myJsonElement); +batch.Add("GetStats"); +batch.Add("SetInputSettings", myJsonElement); ``` -The lower-level form still works, and remains the way to build a batch ahead of time: +> `RequestBatchExecutionType.Parallel` is best avoided when you care about the results. OBS collects +> them in completion order but labels them from the submission order, so every result carries +> another request's `responseData` and `requestStatus`. That happens before the response leaves OBS, +> so it cannot be corrected here; references refuse to resolve on such a batch and say why. See +> [#16](https://github.com/Agash/ObsWebSocket/issues/16). + +## Typed protocol enums + +Protocol enums that travel as strings have a real C# enum, so states can be matched rather than +compared against constants: ```csharp -List items = -[ - new("GetVersion", null), - new("SetCurrentProgramScene", new SetCurrentProgramSceneRequestData(sceneName: "Intro")), -]; +client.StreamStateChanged += (_, e) => +{ + string what = OutputStateExtensions.FromWireValue(e.EventData.OutputState) switch + { + OutputState.Started => "live", + OutputState.Starting or OutputState.Reconnecting => "coming up", + OutputState.Stopped or OutputState.Stopping => "going down", + null => $"unrecognised ({e.EventData.OutputState})", + _ => "in between", + }; -var raw = await client.CallBatchAsync(items, cancellationToken: ct); + Console.WriteLine($"Stream is {what}"); +}; ``` -Either way, an item's `RequestData` should be `null`, a generated `*RequestData` DTO, or a `JsonElement` built with `Utf8JsonWriter`. Anonymous types and reflection-based serialization are not AOT-safe here. +Media transport works the same way, with shorthands for the common actions: -## Host integration +```csharp +await client.MediaInputs.PlayMediaAsync("Stinger", ct); +await client.MediaInputs.TriggerMediaActionAsync("Stinger", MediaInputAction.Restart, ct); +``` -Connect with the host rather than writing a background service for it, and expose the connection -as a health check: +The wire constants remain available as `const` strings on `ObsOutputState` and `ObsMediaInputAction`, +and `ToWireValue()` converts an enum back. + +## Host integration ```csharp builder.AddObsWebSocketClient("obs"); // reads ConnectionStrings:obs @@ -370,23 +287,29 @@ builder.Services.WithAutoConnect(); // connects on start, disconnects builder.Services.AddHealthChecks().AddObsWebSocket(); ``` -```json +The password may travel in the connection string or be set on the options; either way it is kept off +`ServerUri`. A connection that cannot be established at startup is logged rather than thrown, because +OBS is often started after the application, and reconnect takes over from there. + +Options are read through `IOptionsMonitor`, so editing configuration takes effect without a restart. +Timeouts and reconnect settings apply to the next call that uses them; changing the endpoint, +password or transport reconnects, which `WithAutoConnect` performs. + +To configure in code instead: + +```csharp +builder.Services.AddObsWebSocketClient(o => { - "ConnectionStrings": { - "obs": "ws://localhost:4455?password=secret" - } -} + o.ServerUri = new Uri("ws://localhost:4455"); + o.Password = "secret"; + o.Format = SerializationFormat.MsgPack; +}); ``` -The password may travel in the connection string or be set on the options; either way it is kept -off `ServerUri`. A connection that cannot be established at startup is logged rather than thrown, -because OBS is often started after the application, and reconnect takes over from there. - -Options are read through `IOptionsMonitor`, so editing configuration takes effect without a -restart. Timeouts and reconnect settings apply to the next call that uses them; changing the -endpoint, password or transport reconnects, which `WithAutoConnect` performs. +Options are validated when the client is resolved, so a missing or malformed `ServerUri` fails at +startup with the offending option named, rather than on the first connection attempt. -## Multiple OBS instances +### Multiple OBS instances Register clients by name and resolve them with `[FromKeyedServices]`: @@ -410,7 +333,6 @@ try } catch (ObsWebSocketRequestException ex) { - // OBS rejected the request. ex.Status carries the protocol code and comment. Console.WriteLine($"{ex.RequestType} failed with {ex.Status?.Code}: {ex.Comment}"); } catch (ObsWebSocketTimeoutException) @@ -420,23 +342,23 @@ catch (ObsWebSocketTimeoutException) ``` `ObsWebSocketSerializationException` covers payloads that cannot be written or read, and all three -derive from `ObsWebSocketException` if you would rather catch the lot. +derive from `ObsWebSocketException`. -Options are validated when the client is resolved, so a missing or malformed `ServerUri` fails at -startup with the offending option named, rather than on the first connection attempt. +Requests return their response data non-nullable; a successful request that carries no payload +raises `ObsWebSocketException` rather than handing back null. ## Reconnect -Reconnect delays grow by `ReconnectBackoffMultiplier`, are capped at `MaxReconnectDelayMs`, and -carry jitter so that several clients recovering from one outage do not retry in lockstep. -Authentication failures are never retried, since they cannot succeed on a second attempt. +Reconnect delays grow by `ReconnectBackoffMultiplier`, are capped at `MaxReconnectDelayMs`, and carry +jitter so several clients recovering from one outage do not retry in lockstep. Authentication +failures are never retried, since they cannot succeed on a second attempt. To replace the policy outright rather than tune those options, register your own pipeline under `ObsWebSocketResilience.ReconnectPipelineKey` after adding the client. ## Telemetry -The client emits traces and metrics under the name `ObsWebSocket.Core`, inert until something +Traces and metrics are published under the name `ObsWebSocket.Core`, inert until something subscribes: ```csharp @@ -446,34 +368,30 @@ builder.Services.AddOpenTelemetry() ``` One activity per request, and one per batch rather than per item. Counters cover requests sent, -requests failed, events received and reconnect attempts, plus a request-duration histogram. +requests failed, events received and reconnect attempts, plus a request-duration histogram. The +instruments are created from `IMeterFactory`, so they belong to the container that built them. Timeouts and reconnect delays run on an injectable `TimeProvider`, so tests can drive them with `FakeTimeProvider` instead of waiting. -## Example App +## Serialization -`ObsWebSocket.Example` is a host-based sample with configuration and DI. +JSON and MessagePack are both supported, selected with `Format`. Everything in this document behaves +identically on either, and the validation suite exercises both. -- **Interactive mode**: command loop (`help`, `version`, `scene`, `watch`, `batch-example`, `get-all-settings-types`, etc.) -- **Transport validation mode**: exercises JSON and MsgPack across the whole surface, then enters the interactive loop -- **One-shot mode**: pass a command as a process argument for CI/automation, `ObsWebSocket.Example run-transport-tests` +## Example app -`run-transport-tests` creates its own scene and input, so it does not depend on a particular OBS -layout, and removes them afterwards. On each transport it covers the settings modes, event streams, -`WaitForEventAsync`, the typed batch builder including duplicate request types and partial failure, -typed protocol enums, and the scene, input, volume and output helpers. +`ObsWebSocket.Example` is a host-based sample with configuration and DI. -It reads the same `Obs` section as above, plus: +- **Interactive mode**: command loop (`help`, `version`, `scene`, `watch`, `media`, `status`, `batch-example`, and more) +- **Transport validation mode**: exercises the surface on JSON and MessagePack, then enters the interactive loop +- **One-shot mode**: `ObsWebSocket.Example run-transport-tests` -```json -{ - "ExampleValidation": { - "RunValidationOnStartup": false, - "ValidationIterations": 1 - } -} -``` +`run-transport-tests` creates its own scene and input, so it does not depend on a particular OBS +layout, and removes them afterwards. On each transport it asserts real values for the settings modes, +event streams and buffering, `WaitForEventAsync`, the typed batch builder including duplicate request +types, partial failure and truncation, typed protocol enums, screenshots, and the scene, input, +volume and output helpers. ## Native AOT From 846da82cdcf4d5a7452ed2ade804474caabaa3a6 Mon Sep 17 00:00:00 2001 From: Agash Date: Wed, 26 Aug 2026 07:21:55 +0200 Subject: [PATCH 04/12] feat(core)!: move event streams onto their category group 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(). --- .../Generation/Emitter.EventStreams.cs | 166 ++-- .../Generation/Emitter.cs | 4 +- .../ObsWebSocketClient.EventStreams.g.cs | 758 +++++++++--------- .../Client/ObsWebSocketClient.Extensions.g.cs | 56 +- .../{ConfigRequestGroup.cs => ConfigGroup.cs} | 2 +- ...FiltersRequestGroup.cs => FiltersGroup.cs} | 2 +- ...GeneralRequestGroup.cs => GeneralGroup.cs} | 2 +- .../{InputsRequestGroup.cs => InputsGroup.cs} | 2 +- ...utsRequestGroup.cs => MediaInputsGroup.cs} | 2 +- ...OutputsRequestGroup.cs => OutputsGroup.cs} | 2 +- .../{RecordRequestGroup.cs => RecordGroup.cs} | 2 +- ...temsRequestGroup.cs => SceneItemsGroup.cs} | 2 +- .../{ScenesRequestGroup.cs => ScenesGroup.cs} | 2 +- ...SourcesRequestGroup.cs => SourcesGroup.cs} | 2 +- .../{StreamRequestGroup.cs => StreamGroup.cs} | 2 +- ...onsRequestGroup.cs => TransitionsGroup.cs} | 2 +- ObsWebSocket.Example/Worker.cs | 6 +- ObsWebSocket.Tests/ReadmeCompileCheck.cs | 2 +- ObsWebSocket.Tests/ScreenshotDecodeTests.cs | 6 +- README.md | 14 +- 20 files changed, 522 insertions(+), 514 deletions(-) rename ObsWebSocket.Core/Groups/{ConfigRequestGroup.cs => ConfigGroup.cs} (99%) rename ObsWebSocket.Core/Groups/{FiltersRequestGroup.cs => FiltersGroup.cs} (99%) rename ObsWebSocket.Core/Groups/{GeneralRequestGroup.cs => GeneralGroup.cs} (97%) rename ObsWebSocket.Core/Groups/{InputsRequestGroup.cs => InputsGroup.cs} (99%) rename ObsWebSocket.Core/Groups/{MediaInputsRequestGroup.cs => MediaInputsGroup.cs} (98%) rename ObsWebSocket.Core/Groups/{OutputsRequestGroup.cs => OutputsGroup.cs} (99%) rename ObsWebSocket.Core/Groups/{RecordRequestGroup.cs => RecordGroup.cs} (98%) rename ObsWebSocket.Core/Groups/{SceneItemsRequestGroup.cs => SceneItemsGroup.cs} (99%) rename ObsWebSocket.Core/Groups/{ScenesRequestGroup.cs => ScenesGroup.cs} (99%) rename ObsWebSocket.Core/Groups/{SourcesRequestGroup.cs => SourcesGroup.cs} (99%) rename ObsWebSocket.Core/Groups/{StreamRequestGroup.cs => StreamGroup.cs} (98%) rename ObsWebSocket.Core/Groups/{TransitionsRequestGroup.cs => TransitionsGroup.cs} (99%) diff --git a/ObsWebSocket.Codegen.Tasks/Generation/Emitter.EventStreams.cs b/ObsWebSocket.Codegen.Tasks/Generation/Emitter.EventStreams.cs index cb57817..1e92b3f 100644 --- a/ObsWebSocket.Codegen.Tasks/Generation/Emitter.EventStreams.cs +++ b/ObsWebSocket.Codegen.Tasks/Generation/Emitter.EventStreams.cs @@ -12,7 +12,9 @@ internal static partial class Emitter { /// /// Generates one stream accessor per protocol event, each wrapping the corresponding - /// classic event so it can be consumed with await foreach. + /// classic event so it can be consumed with await foreach. The accessors go onto the + /// same category group as that category's requests, because the protocol documents requests + /// and events under one set of category headings. /// /// The source production context. /// The parsed protocol definition. @@ -26,6 +28,12 @@ ProtocolDefinition protocol return; } + HashSet requestCategories = new(StringComparer.OrdinalIgnoreCase); + foreach (RequestDefinition request in protocol.Requests ?? []) + { + _ = requestCategories.Add(request.Category ?? "general"); + } + StringBuilder builder = BuildSourceHeader("// Helper: per-event IAsyncEnumerable streams"); builder.AppendLine("using System;"); builder.AppendLine("using System.Collections.Generic;"); @@ -36,86 +44,112 @@ ProtocolDefinition protocol builder.AppendLine(); builder.AppendLine($"namespace {ExtensionsNamespace};"); builder.AppendLine(); - builder.AppendLine("/// "); - builder.AppendLine( - "/// Observes OBS events as async sequences. Each accessor subscribes for the lifetime" - ); - builder.AppendLine( - "/// of the enumeration and unsubscribes when it ends, so the caller never manages handlers." - ); - builder.AppendLine("/// "); - builder.AppendLine("public static class ObsWebSocketClientEventStreams"); - builder.AppendLine("{"); - foreach (OBSEvent? eventDef in protocol.Events) + foreach ( + IGrouping group in protocol + .Events.GroupBy(e => e.Category ?? "general", StringComparer.OrdinalIgnoreCase) + .OrderBy(g => g.Key, StringComparer.Ordinal) + ) { - try - { - string eventName = SanitizeIdentifier(eventDef.EventType); - if (string.IsNullOrEmpty(eventName)) - { - continue; - } + string groupName = ToGroupName(group.Key); - string eventArgsTypeName = $"{GeneratedEventArgsNamespace}.{eventName}EventArgs"; + builder.AppendLine("/// "); + builder.AppendLine( + $"/// Events in the {System.Security.SecurityElement.Escape(group.Key)} category, as async sequences." + ); + builder.AppendLine( + "/// Each accessor subscribes for the lifetime of the enumeration and unsubscribes" + ); + builder.AppendLine( + "/// when it ends, so the caller never manages handlers." + ); + builder.AppendLine("/// "); - builder.AppendLine(" /// "); + // A category with events but no requests has no group declared elsewhere, so this + // part has to carry the primary constructor. + if (requestCategories.Contains(group.Key)) + { + builder.AppendLine($"public readonly partial struct {groupName}Group"); + } + else + { + builder.AppendLine( + "/// The client these events are observed on." + ); builder.AppendLine( - $" /// Streams {eventName} events as they arrive." + $"public readonly partial struct {groupName}Group(ObsWebSocketClient client)" ); - if (!string.IsNullOrWhiteSpace(eventDef.Description)) + } + + builder.AppendLine("{"); + + foreach (OBSEvent eventDef in group) + { + try { + string eventName = SanitizeIdentifier(eventDef.EventType); + if (string.IsNullOrEmpty(eventName)) + { + continue; + } + + string eventArgsTypeName = $"{GeneratedEventArgsNamespace}.{eventName}EventArgs"; + + builder.AppendLine(" /// "); + builder.AppendLine($" /// Streams {eventName} events as they arrive."); + if (!string.IsNullOrWhiteSpace(eventDef.Description)) + { + builder.AppendLine( + $" /// {FlattenDescription(eventDef.Description)}" + ); + } + + builder.AppendLine(" /// "); builder.AppendLine( - $" /// {FlattenDescription(eventDef.Description)}" + " /// Events buffered before the oldest is dropped." ); - } + builder.AppendLine( + " /// Ends the enumeration and unsubscribes." + ); + if (!string.IsNullOrWhiteSpace(eventDef.EventSubscription)) + { + builder.AppendLine( + $" /// Requires the {System.Security.SecurityElement.Escape(eventDef.EventSubscription)} subscription." + ); + } - builder.AppendLine(" /// "); - builder.AppendLine(" /// The ObsWebSocketClient instance."); - builder.AppendLine( - " /// Events buffered before the oldest is dropped." - ); - builder.AppendLine( - " /// Ends the enumeration and unsubscribes." - ); - if (!string.IsNullOrWhiteSpace(eventDef.EventSubscription)) - { builder.AppendLine( - $" /// Requires the {System.Security.SecurityElement.Escape(eventDef.EventSubscription)} subscription." + $" public IAsyncEnumerable<{eventArgsTypeName}> {eventName}Stream(" + ); + builder.AppendLine(" int capacity = EventStream.DefaultCapacity,"); + builder.AppendLine(" CancellationToken cancellationToken = default)"); + builder.AppendLine(" {"); + builder.AppendLine(" ObsWebSocketClient source = client;"); + builder.AppendLine($" return EventStream.Create<{eventArgsTypeName}>("); + builder.AppendLine($" handler => source.{eventName} += handler,"); + builder.AppendLine($" handler => source.{eventName} -= handler,"); + builder.AppendLine(" capacity,"); + builder.AppendLine(" cancellationToken);"); + builder.AppendLine(" }"); + builder.AppendLine(); + } + catch (Exception ex) + { + context.ReportDiagnostic( + Diagnostic.Create( + Diagnostics.IdentifierGenerationError, + Location.None, + eventDef.EventType, + $"Generating event stream for {eventDef.EventType}", + ex.Message + ) ); } - - builder.AppendLine( - $" public static IAsyncEnumerable<{eventArgsTypeName}> {eventName}Stream(" - ); - builder.AppendLine(" this ObsWebSocketClient client,"); - builder.AppendLine(" int capacity = EventStream.DefaultCapacity,"); - builder.AppendLine(" CancellationToken cancellationToken = default)"); - builder.AppendLine(" {"); - builder.AppendLine(" ArgumentNullException.ThrowIfNull(client);"); - builder.AppendLine($" return EventStream.Create<{eventArgsTypeName}>("); - builder.AppendLine($" handler => client.{eventName} += handler,"); - builder.AppendLine($" handler => client.{eventName} -= handler,"); - builder.AppendLine(" capacity,"); - builder.AppendLine(" cancellationToken);"); - builder.AppendLine(" }"); - builder.AppendLine(); - } - catch (Exception ex) - { - context.ReportDiagnostic( - Diagnostic.Create( - Diagnostics.IdentifierGenerationError, - Location.None, - eventDef.EventType, - $"Generating event stream for {eventDef.EventType}", - ex.Message - ) - ); } - } - builder.AppendLine("}"); + builder.AppendLine("}"); + builder.AppendLine(); + } context.AddSource( "ObsWebSocketClient.EventStreams.g.cs", diff --git a/ObsWebSocket.Codegen.Tasks/Generation/Emitter.cs b/ObsWebSocket.Codegen.Tasks/Generation/Emitter.cs index dc4c3a9..c6e99dc 100644 --- a/ObsWebSocket.Codegen.Tasks/Generation/Emitter.cs +++ b/ObsWebSocket.Codegen.Tasks/Generation/Emitter.cs @@ -714,7 +714,7 @@ IGrouping group in protocol builder.AppendLine("/// "); builder.AppendLine("/// The client these requests are sent on."); builder.AppendLine( - $"public readonly partial struct {groupName}RequestGroup(ObsWebSocketClient client)" + $"public readonly partial struct {groupName}Group(ObsWebSocketClient client)" ); builder.AppendLine("{"); @@ -758,7 +758,7 @@ IGrouping group in protocol ); builder.AppendLine(" /// "); builder.AppendLine( - $" public {groupName}RequestGroup {groupName} => new(client);" + $" public {groupName}Group {groupName} => new(client);" ); builder.AppendLine(" }"); builder.AppendLine(); diff --git a/ObsWebSocket.Core/Generated/Client/ObsWebSocketClient.EventStreams.g.cs b/ObsWebSocket.Core/Generated/Client/ObsWebSocketClient.EventStreams.g.cs index a6636e6..ace3b09 100644 --- a/ObsWebSocket.Core/Generated/Client/ObsWebSocketClient.EventStreams.g.cs +++ b/ObsWebSocket.Core/Generated/Client/ObsWebSocketClient.EventStreams.g.cs @@ -12,28 +12,27 @@ namespace ObsWebSocket.Core; /// -/// Observes OBS events as async sequences. Each accessor subscribes for the lifetime -/// of the enumeration and unsubscribes when it ends, so the caller never manages handlers. +/// Events in the canvases category, as async sequences. +/// Each accessor subscribes for the lifetime of the enumeration and unsubscribes +/// when it ends, so the caller never manages handlers. /// -public static class ObsWebSocketClientEventStreams +public readonly partial struct CanvasesGroup { /// /// Streams CanvasCreated events as they arrive. /// A new canvas has been created. /// - /// The ObsWebSocketClient instance. /// Events buffered before the oldest is dropped. /// Ends the enumeration and unsubscribes. /// Requires the Canvases subscription. - public static IAsyncEnumerable CanvasCreatedStream( - this ObsWebSocketClient client, + public IAsyncEnumerable CanvasCreatedStream( int capacity = EventStream.DefaultCapacity, CancellationToken cancellationToken = default) { - ArgumentNullException.ThrowIfNull(client); + ObsWebSocketClient source = client; return EventStream.Create( - handler => client.CanvasCreated += handler, - handler => client.CanvasCreated -= handler, + handler => source.CanvasCreated += handler, + handler => source.CanvasCreated -= handler, capacity, cancellationToken); } @@ -42,19 +41,17 @@ public static class ObsWebSocketClientEventStreams /// Streams CanvasRemoved events as they arrive. /// A canvas has been removed. /// - /// The ObsWebSocketClient instance. /// Events buffered before the oldest is dropped. /// Ends the enumeration and unsubscribes. /// Requires the Canvases subscription. - public static IAsyncEnumerable CanvasRemovedStream( - this ObsWebSocketClient client, + public IAsyncEnumerable CanvasRemovedStream( int capacity = EventStream.DefaultCapacity, CancellationToken cancellationToken = default) { - ArgumentNullException.ThrowIfNull(client); + ObsWebSocketClient source = client; return EventStream.Create( - handler => client.CanvasRemoved += handler, - handler => client.CanvasRemoved -= handler, + handler => source.CanvasRemoved += handler, + handler => source.CanvasRemoved -= handler, capacity, cancellationToken); } @@ -63,40 +60,45 @@ public static class ObsWebSocketClientEventStreams /// Streams CanvasNameChanged events as they arrive. /// The name of a canvas has changed. /// - /// The ObsWebSocketClient instance. /// Events buffered before the oldest is dropped. /// Ends the enumeration and unsubscribes. /// Requires the Canvases subscription. - public static IAsyncEnumerable CanvasNameChangedStream( - this ObsWebSocketClient client, + public IAsyncEnumerable CanvasNameChangedStream( int capacity = EventStream.DefaultCapacity, CancellationToken cancellationToken = default) { - ArgumentNullException.ThrowIfNull(client); + ObsWebSocketClient source = client; return EventStream.Create( - handler => client.CanvasNameChanged += handler, - handler => client.CanvasNameChanged -= handler, + handler => source.CanvasNameChanged += handler, + handler => source.CanvasNameChanged -= handler, capacity, cancellationToken); } +} + +/// +/// Events in the config category, as async sequences. +/// Each accessor subscribes for the lifetime of the enumeration and unsubscribes +/// when it ends, so the caller never manages handlers. +/// +public readonly partial struct ConfigGroup +{ /// /// Streams CurrentSceneCollectionChanging events as they arrive. /// The current scene collection has begun changing. Note: We recommend using this event to trigger a pause of all polling requests, as performing any requests during a scene collection change is considered undefined behavior and can cause crashes! /// - /// The ObsWebSocketClient instance. /// Events buffered before the oldest is dropped. /// Ends the enumeration and unsubscribes. /// Requires the Config subscription. - public static IAsyncEnumerable CurrentSceneCollectionChangingStream( - this ObsWebSocketClient client, + public IAsyncEnumerable CurrentSceneCollectionChangingStream( int capacity = EventStream.DefaultCapacity, CancellationToken cancellationToken = default) { - ArgumentNullException.ThrowIfNull(client); + ObsWebSocketClient source = client; return EventStream.Create( - handler => client.CurrentSceneCollectionChanging += handler, - handler => client.CurrentSceneCollectionChanging -= handler, + handler => source.CurrentSceneCollectionChanging += handler, + handler => source.CurrentSceneCollectionChanging -= handler, capacity, cancellationToken); } @@ -105,19 +107,17 @@ public static class ObsWebSocketClientEventStreams /// Streams CurrentSceneCollectionChanged events as they arrive. /// The current scene collection has changed. Note: If polling has been paused during `CurrentSceneCollectionChanging`, this is the que to restart polling. /// - /// The ObsWebSocketClient instance. /// Events buffered before the oldest is dropped. /// Ends the enumeration and unsubscribes. /// Requires the Config subscription. - public static IAsyncEnumerable CurrentSceneCollectionChangedStream( - this ObsWebSocketClient client, + public IAsyncEnumerable CurrentSceneCollectionChangedStream( int capacity = EventStream.DefaultCapacity, CancellationToken cancellationToken = default) { - ArgumentNullException.ThrowIfNull(client); + ObsWebSocketClient source = client; return EventStream.Create( - handler => client.CurrentSceneCollectionChanged += handler, - handler => client.CurrentSceneCollectionChanged -= handler, + handler => source.CurrentSceneCollectionChanged += handler, + handler => source.CurrentSceneCollectionChanged -= handler, capacity, cancellationToken); } @@ -126,19 +126,17 @@ public static class ObsWebSocketClientEventStreams /// Streams SceneCollectionListChanged events as they arrive. /// The scene collection list has changed. /// - /// The ObsWebSocketClient instance. /// Events buffered before the oldest is dropped. /// Ends the enumeration and unsubscribes. /// Requires the Config subscription. - public static IAsyncEnumerable SceneCollectionListChangedStream( - this ObsWebSocketClient client, + public IAsyncEnumerable SceneCollectionListChangedStream( int capacity = EventStream.DefaultCapacity, CancellationToken cancellationToken = default) { - ArgumentNullException.ThrowIfNull(client); + ObsWebSocketClient source = client; return EventStream.Create( - handler => client.SceneCollectionListChanged += handler, - handler => client.SceneCollectionListChanged -= handler, + handler => source.SceneCollectionListChanged += handler, + handler => source.SceneCollectionListChanged -= handler, capacity, cancellationToken); } @@ -147,19 +145,17 @@ public static class ObsWebSocketClientEventStreams /// Streams CurrentProfileChanging events as they arrive. /// The current profile has begun changing. /// - /// The ObsWebSocketClient instance. /// Events buffered before the oldest is dropped. /// Ends the enumeration and unsubscribes. /// Requires the Config subscription. - public static IAsyncEnumerable CurrentProfileChangingStream( - this ObsWebSocketClient client, + public IAsyncEnumerable CurrentProfileChangingStream( int capacity = EventStream.DefaultCapacity, CancellationToken cancellationToken = default) { - ArgumentNullException.ThrowIfNull(client); + ObsWebSocketClient source = client; return EventStream.Create( - handler => client.CurrentProfileChanging += handler, - handler => client.CurrentProfileChanging -= handler, + handler => source.CurrentProfileChanging += handler, + handler => source.CurrentProfileChanging -= handler, capacity, cancellationToken); } @@ -168,19 +164,17 @@ public static class ObsWebSocketClientEventStreams /// Streams CurrentProfileChanged events as they arrive. /// The current profile has changed. /// - /// The ObsWebSocketClient instance. /// Events buffered before the oldest is dropped. /// Ends the enumeration and unsubscribes. /// Requires the Config subscription. - public static IAsyncEnumerable CurrentProfileChangedStream( - this ObsWebSocketClient client, + public IAsyncEnumerable CurrentProfileChangedStream( int capacity = EventStream.DefaultCapacity, CancellationToken cancellationToken = default) { - ArgumentNullException.ThrowIfNull(client); + ObsWebSocketClient source = client; return EventStream.Create( - handler => client.CurrentProfileChanged += handler, - handler => client.CurrentProfileChanged -= handler, + handler => source.CurrentProfileChanged += handler, + handler => source.CurrentProfileChanged -= handler, capacity, cancellationToken); } @@ -189,40 +183,45 @@ public static class ObsWebSocketClientEventStreams /// Streams ProfileListChanged events as they arrive. /// The profile list has changed. /// - /// The ObsWebSocketClient instance. /// Events buffered before the oldest is dropped. /// Ends the enumeration and unsubscribes. /// Requires the Config subscription. - public static IAsyncEnumerable ProfileListChangedStream( - this ObsWebSocketClient client, + public IAsyncEnumerable ProfileListChangedStream( int capacity = EventStream.DefaultCapacity, CancellationToken cancellationToken = default) { - ArgumentNullException.ThrowIfNull(client); + ObsWebSocketClient source = client; return EventStream.Create( - handler => client.ProfileListChanged += handler, - handler => client.ProfileListChanged -= handler, + handler => source.ProfileListChanged += handler, + handler => source.ProfileListChanged -= handler, capacity, cancellationToken); } +} + +/// +/// Events in the filters category, as async sequences. +/// Each accessor subscribes for the lifetime of the enumeration and unsubscribes +/// when it ends, so the caller never manages handlers. +/// +public readonly partial struct FiltersGroup +{ /// /// Streams SourceFilterListReindexed events as they arrive. /// A source's filter list has been reindexed. /// - /// The ObsWebSocketClient instance. /// Events buffered before the oldest is dropped. /// Ends the enumeration and unsubscribes. /// Requires the Filters subscription. - public static IAsyncEnumerable SourceFilterListReindexedStream( - this ObsWebSocketClient client, + public IAsyncEnumerable SourceFilterListReindexedStream( int capacity = EventStream.DefaultCapacity, CancellationToken cancellationToken = default) { - ArgumentNullException.ThrowIfNull(client); + ObsWebSocketClient source = client; return EventStream.Create( - handler => client.SourceFilterListReindexed += handler, - handler => client.SourceFilterListReindexed -= handler, + handler => source.SourceFilterListReindexed += handler, + handler => source.SourceFilterListReindexed -= handler, capacity, cancellationToken); } @@ -231,19 +230,17 @@ public static class ObsWebSocketClientEventStreams /// Streams SourceFilterCreated events as they arrive. /// A filter has been added to a source. /// - /// The ObsWebSocketClient instance. /// Events buffered before the oldest is dropped. /// Ends the enumeration and unsubscribes. /// Requires the Filters subscription. - public static IAsyncEnumerable SourceFilterCreatedStream( - this ObsWebSocketClient client, + public IAsyncEnumerable SourceFilterCreatedStream( int capacity = EventStream.DefaultCapacity, CancellationToken cancellationToken = default) { - ArgumentNullException.ThrowIfNull(client); + ObsWebSocketClient source = client; return EventStream.Create( - handler => client.SourceFilterCreated += handler, - handler => client.SourceFilterCreated -= handler, + handler => source.SourceFilterCreated += handler, + handler => source.SourceFilterCreated -= handler, capacity, cancellationToken); } @@ -252,19 +249,17 @@ public static class ObsWebSocketClientEventStreams /// Streams SourceFilterRemoved events as they arrive. /// A filter has been removed from a source. /// - /// The ObsWebSocketClient instance. /// Events buffered before the oldest is dropped. /// Ends the enumeration and unsubscribes. /// Requires the Filters subscription. - public static IAsyncEnumerable SourceFilterRemovedStream( - this ObsWebSocketClient client, + public IAsyncEnumerable SourceFilterRemovedStream( int capacity = EventStream.DefaultCapacity, CancellationToken cancellationToken = default) { - ArgumentNullException.ThrowIfNull(client); + ObsWebSocketClient source = client; return EventStream.Create( - handler => client.SourceFilterRemoved += handler, - handler => client.SourceFilterRemoved -= handler, + handler => source.SourceFilterRemoved += handler, + handler => source.SourceFilterRemoved -= handler, capacity, cancellationToken); } @@ -273,19 +268,17 @@ public static class ObsWebSocketClientEventStreams /// Streams SourceFilterNameChanged events as they arrive. /// The name of a source filter has changed. /// - /// The ObsWebSocketClient instance. /// Events buffered before the oldest is dropped. /// Ends the enumeration and unsubscribes. /// Requires the Filters subscription. - public static IAsyncEnumerable SourceFilterNameChangedStream( - this ObsWebSocketClient client, + public IAsyncEnumerable SourceFilterNameChangedStream( int capacity = EventStream.DefaultCapacity, CancellationToken cancellationToken = default) { - ArgumentNullException.ThrowIfNull(client); + ObsWebSocketClient source = client; return EventStream.Create( - handler => client.SourceFilterNameChanged += handler, - handler => client.SourceFilterNameChanged -= handler, + handler => source.SourceFilterNameChanged += handler, + handler => source.SourceFilterNameChanged -= handler, capacity, cancellationToken); } @@ -294,19 +287,17 @@ public static class ObsWebSocketClientEventStreams /// Streams SourceFilterSettingsChanged events as they arrive. /// An source filter's settings have changed (been updated). /// - /// The ObsWebSocketClient instance. /// Events buffered before the oldest is dropped. /// Ends the enumeration and unsubscribes. /// Requires the Filters subscription. - public static IAsyncEnumerable SourceFilterSettingsChangedStream( - this ObsWebSocketClient client, + public IAsyncEnumerable SourceFilterSettingsChangedStream( int capacity = EventStream.DefaultCapacity, CancellationToken cancellationToken = default) { - ArgumentNullException.ThrowIfNull(client); + ObsWebSocketClient source = client; return EventStream.Create( - handler => client.SourceFilterSettingsChanged += handler, - handler => client.SourceFilterSettingsChanged -= handler, + handler => source.SourceFilterSettingsChanged += handler, + handler => source.SourceFilterSettingsChanged -= handler, capacity, cancellationToken); } @@ -315,61 +306,111 @@ public static class ObsWebSocketClientEventStreams /// Streams SourceFilterEnableStateChanged events as they arrive. /// A source filter's enable state has changed. /// - /// The ObsWebSocketClient instance. /// Events buffered before the oldest is dropped. /// Ends the enumeration and unsubscribes. /// Requires the Filters subscription. - public static IAsyncEnumerable SourceFilterEnableStateChangedStream( - this ObsWebSocketClient client, + public IAsyncEnumerable SourceFilterEnableStateChangedStream( int capacity = EventStream.DefaultCapacity, CancellationToken cancellationToken = default) { - ArgumentNullException.ThrowIfNull(client); + ObsWebSocketClient source = client; return EventStream.Create( - handler => client.SourceFilterEnableStateChanged += handler, - handler => client.SourceFilterEnableStateChanged -= handler, + handler => source.SourceFilterEnableStateChanged += handler, + handler => source.SourceFilterEnableStateChanged -= handler, capacity, cancellationToken); } +} + +/// +/// Events in the general category, as async sequences. +/// Each accessor subscribes for the lifetime of the enumeration and unsubscribes +/// when it ends, so the caller never manages handlers. +/// +public readonly partial struct GeneralGroup +{ /// /// Streams ExitStarted events as they arrive. /// OBS has begun the shutdown process. /// - /// The ObsWebSocketClient instance. /// Events buffered before the oldest is dropped. /// Ends the enumeration and unsubscribes. /// Requires the General subscription. - public static IAsyncEnumerable ExitStartedStream( - this ObsWebSocketClient client, + public IAsyncEnumerable ExitStartedStream( int capacity = EventStream.DefaultCapacity, CancellationToken cancellationToken = default) { - ArgumentNullException.ThrowIfNull(client); + ObsWebSocketClient source = client; return EventStream.Create( - handler => client.ExitStarted += handler, - handler => client.ExitStarted -= handler, + handler => source.ExitStarted += handler, + handler => source.ExitStarted -= handler, + capacity, + cancellationToken); + } + + /// + /// Streams VendorEvent events as they arrive. + /// An event has been emitted from a vendor. A vendor is a unique name registered by a third-party plugin or script, which allows for custom requests and events to be added to obs-websocket. If a plugin or script implements vendor requests or events, documentation is expected to be provided with them. + /// + /// Events buffered before the oldest is dropped. + /// Ends the enumeration and unsubscribes. + /// Requires the Vendors subscription. + public IAsyncEnumerable VendorEventStream( + int capacity = EventStream.DefaultCapacity, + CancellationToken cancellationToken = default) + { + ObsWebSocketClient source = client; + return EventStream.Create( + handler => source.VendorEvent += handler, + handler => source.VendorEvent -= handler, + capacity, + cancellationToken); + } + + /// + /// Streams CustomEvent events as they arrive. + /// Custom event emitted by `BroadcastCustomEvent`. + /// + /// Events buffered before the oldest is dropped. + /// Ends the enumeration and unsubscribes. + /// Requires the General subscription. + public IAsyncEnumerable CustomEventStream( + int capacity = EventStream.DefaultCapacity, + CancellationToken cancellationToken = default) + { + ObsWebSocketClient source = client; + return EventStream.Create( + handler => source.CustomEvent += handler, + handler => source.CustomEvent -= handler, capacity, cancellationToken); } +} + +/// +/// Events in the inputs category, as async sequences. +/// Each accessor subscribes for the lifetime of the enumeration and unsubscribes +/// when it ends, so the caller never manages handlers. +/// +public readonly partial struct InputsGroup +{ /// /// Streams InputCreated events as they arrive. /// An input has been created. /// - /// The ObsWebSocketClient instance. /// Events buffered before the oldest is dropped. /// Ends the enumeration and unsubscribes. /// Requires the Inputs subscription. - public static IAsyncEnumerable InputCreatedStream( - this ObsWebSocketClient client, + public IAsyncEnumerable InputCreatedStream( int capacity = EventStream.DefaultCapacity, CancellationToken cancellationToken = default) { - ArgumentNullException.ThrowIfNull(client); + ObsWebSocketClient source = client; return EventStream.Create( - handler => client.InputCreated += handler, - handler => client.InputCreated -= handler, + handler => source.InputCreated += handler, + handler => source.InputCreated -= handler, capacity, cancellationToken); } @@ -378,19 +419,17 @@ public static class ObsWebSocketClientEventStreams /// Streams InputRemoved events as they arrive. /// An input has been removed. /// - /// The ObsWebSocketClient instance. /// Events buffered before the oldest is dropped. /// Ends the enumeration and unsubscribes. /// Requires the Inputs subscription. - public static IAsyncEnumerable InputRemovedStream( - this ObsWebSocketClient client, + public IAsyncEnumerable InputRemovedStream( int capacity = EventStream.DefaultCapacity, CancellationToken cancellationToken = default) { - ArgumentNullException.ThrowIfNull(client); + ObsWebSocketClient source = client; return EventStream.Create( - handler => client.InputRemoved += handler, - handler => client.InputRemoved -= handler, + handler => source.InputRemoved += handler, + handler => source.InputRemoved -= handler, capacity, cancellationToken); } @@ -399,19 +438,17 @@ public static class ObsWebSocketClientEventStreams /// Streams InputNameChanged events as they arrive. /// The name of an input has changed. /// - /// The ObsWebSocketClient instance. /// Events buffered before the oldest is dropped. /// Ends the enumeration and unsubscribes. /// Requires the Inputs subscription. - public static IAsyncEnumerable InputNameChangedStream( - this ObsWebSocketClient client, + public IAsyncEnumerable InputNameChangedStream( int capacity = EventStream.DefaultCapacity, CancellationToken cancellationToken = default) { - ArgumentNullException.ThrowIfNull(client); + ObsWebSocketClient source = client; return EventStream.Create( - handler => client.InputNameChanged += handler, - handler => client.InputNameChanged -= handler, + handler => source.InputNameChanged += handler, + handler => source.InputNameChanged -= handler, capacity, cancellationToken); } @@ -420,19 +457,17 @@ public static class ObsWebSocketClientEventStreams /// Streams InputSettingsChanged events as they arrive. /// An input's settings have changed (been updated). Note: On some inputs, changing values in the properties dialog will cause an immediate update. Pressing the "Cancel" button will revert the settings, resulting in another event being fired. /// - /// The ObsWebSocketClient instance. /// Events buffered before the oldest is dropped. /// Ends the enumeration and unsubscribes. /// Requires the Inputs subscription. - public static IAsyncEnumerable InputSettingsChangedStream( - this ObsWebSocketClient client, + public IAsyncEnumerable InputSettingsChangedStream( int capacity = EventStream.DefaultCapacity, CancellationToken cancellationToken = default) { - ArgumentNullException.ThrowIfNull(client); + ObsWebSocketClient source = client; return EventStream.Create( - handler => client.InputSettingsChanged += handler, - handler => client.InputSettingsChanged -= handler, + handler => source.InputSettingsChanged += handler, + handler => source.InputSettingsChanged -= handler, capacity, cancellationToken); } @@ -441,19 +476,17 @@ public static class ObsWebSocketClientEventStreams /// Streams InputActiveStateChanged events as they arrive. /// An input's active state has changed. When an input is active, it means it's being shown by the program feed. /// - /// The ObsWebSocketClient instance. /// Events buffered before the oldest is dropped. /// Ends the enumeration and unsubscribes. /// Requires the InputActiveStateChanged subscription. - public static IAsyncEnumerable InputActiveStateChangedStream( - this ObsWebSocketClient client, + public IAsyncEnumerable InputActiveStateChangedStream( int capacity = EventStream.DefaultCapacity, CancellationToken cancellationToken = default) { - ArgumentNullException.ThrowIfNull(client); + ObsWebSocketClient source = client; return EventStream.Create( - handler => client.InputActiveStateChanged += handler, - handler => client.InputActiveStateChanged -= handler, + handler => source.InputActiveStateChanged += handler, + handler => source.InputActiveStateChanged -= handler, capacity, cancellationToken); } @@ -462,19 +495,17 @@ public static class ObsWebSocketClientEventStreams /// Streams InputShowStateChanged events as they arrive. /// An input's show state has changed. When an input is showing, it means it's being shown by the preview or a dialog. /// - /// The ObsWebSocketClient instance. /// Events buffered before the oldest is dropped. /// Ends the enumeration and unsubscribes. /// Requires the InputShowStateChanged subscription. - public static IAsyncEnumerable InputShowStateChangedStream( - this ObsWebSocketClient client, + public IAsyncEnumerable InputShowStateChangedStream( int capacity = EventStream.DefaultCapacity, CancellationToken cancellationToken = default) { - ArgumentNullException.ThrowIfNull(client); + ObsWebSocketClient source = client; return EventStream.Create( - handler => client.InputShowStateChanged += handler, - handler => client.InputShowStateChanged -= handler, + handler => source.InputShowStateChanged += handler, + handler => source.InputShowStateChanged -= handler, capacity, cancellationToken); } @@ -483,19 +514,17 @@ public static class ObsWebSocketClientEventStreams /// Streams InputMuteStateChanged events as they arrive. /// An input's mute state has changed. /// - /// The ObsWebSocketClient instance. /// Events buffered before the oldest is dropped. /// Ends the enumeration and unsubscribes. /// Requires the Inputs subscription. - public static IAsyncEnumerable InputMuteStateChangedStream( - this ObsWebSocketClient client, + public IAsyncEnumerable InputMuteStateChangedStream( int capacity = EventStream.DefaultCapacity, CancellationToken cancellationToken = default) { - ArgumentNullException.ThrowIfNull(client); + ObsWebSocketClient source = client; return EventStream.Create( - handler => client.InputMuteStateChanged += handler, - handler => client.InputMuteStateChanged -= handler, + handler => source.InputMuteStateChanged += handler, + handler => source.InputMuteStateChanged -= handler, capacity, cancellationToken); } @@ -504,19 +533,17 @@ public static class ObsWebSocketClientEventStreams /// Streams InputVolumeChanged events as they arrive. /// An input's volume level has changed. /// - /// The ObsWebSocketClient instance. /// Events buffered before the oldest is dropped. /// Ends the enumeration and unsubscribes. /// Requires the Inputs subscription. - public static IAsyncEnumerable InputVolumeChangedStream( - this ObsWebSocketClient client, + public IAsyncEnumerable InputVolumeChangedStream( int capacity = EventStream.DefaultCapacity, CancellationToken cancellationToken = default) { - ArgumentNullException.ThrowIfNull(client); + ObsWebSocketClient source = client; return EventStream.Create( - handler => client.InputVolumeChanged += handler, - handler => client.InputVolumeChanged -= handler, + handler => source.InputVolumeChanged += handler, + handler => source.InputVolumeChanged -= handler, capacity, cancellationToken); } @@ -525,19 +552,17 @@ public static class ObsWebSocketClientEventStreams /// Streams InputAudioBalanceChanged events as they arrive. /// The audio balance value of an input has changed. /// - /// The ObsWebSocketClient instance. /// Events buffered before the oldest is dropped. /// Ends the enumeration and unsubscribes. /// Requires the Inputs subscription. - public static IAsyncEnumerable InputAudioBalanceChangedStream( - this ObsWebSocketClient client, + public IAsyncEnumerable InputAudioBalanceChangedStream( int capacity = EventStream.DefaultCapacity, CancellationToken cancellationToken = default) { - ArgumentNullException.ThrowIfNull(client); + ObsWebSocketClient source = client; return EventStream.Create( - handler => client.InputAudioBalanceChanged += handler, - handler => client.InputAudioBalanceChanged -= handler, + handler => source.InputAudioBalanceChanged += handler, + handler => source.InputAudioBalanceChanged -= handler, capacity, cancellationToken); } @@ -546,19 +571,17 @@ public static class ObsWebSocketClientEventStreams /// Streams InputAudioSyncOffsetChanged events as they arrive. /// The sync offset of an input has changed. /// - /// The ObsWebSocketClient instance. /// Events buffered before the oldest is dropped. /// Ends the enumeration and unsubscribes. /// Requires the Inputs subscription. - public static IAsyncEnumerable InputAudioSyncOffsetChangedStream( - this ObsWebSocketClient client, + public IAsyncEnumerable InputAudioSyncOffsetChangedStream( int capacity = EventStream.DefaultCapacity, CancellationToken cancellationToken = default) { - ArgumentNullException.ThrowIfNull(client); + ObsWebSocketClient source = client; return EventStream.Create( - handler => client.InputAudioSyncOffsetChanged += handler, - handler => client.InputAudioSyncOffsetChanged -= handler, + handler => source.InputAudioSyncOffsetChanged += handler, + handler => source.InputAudioSyncOffsetChanged -= handler, capacity, cancellationToken); } @@ -567,19 +590,17 @@ public static class ObsWebSocketClientEventStreams /// Streams InputAudioTracksChanged events as they arrive. /// The audio tracks of an input have changed. /// - /// The ObsWebSocketClient instance. /// Events buffered before the oldest is dropped. /// Ends the enumeration and unsubscribes. /// Requires the Inputs subscription. - public static IAsyncEnumerable InputAudioTracksChangedStream( - this ObsWebSocketClient client, + public IAsyncEnumerable InputAudioTracksChangedStream( int capacity = EventStream.DefaultCapacity, CancellationToken cancellationToken = default) { - ArgumentNullException.ThrowIfNull(client); + ObsWebSocketClient source = client; return EventStream.Create( - handler => client.InputAudioTracksChanged += handler, - handler => client.InputAudioTracksChanged -= handler, + handler => source.InputAudioTracksChanged += handler, + handler => source.InputAudioTracksChanged -= handler, capacity, cancellationToken); } @@ -588,19 +609,17 @@ public static class ObsWebSocketClientEventStreams /// Streams InputAudioMonitorTypeChanged events as they arrive. /// The monitor type of an input has changed. Available types are: - `OBS_MONITORING_TYPE_NONE` - `OBS_MONITORING_TYPE_MONITOR_ONLY` - `OBS_MONITORING_TYPE_MONITOR_AND_OUTPUT` /// - /// The ObsWebSocketClient instance. /// Events buffered before the oldest is dropped. /// Ends the enumeration and unsubscribes. /// Requires the Inputs subscription. - public static IAsyncEnumerable InputAudioMonitorTypeChangedStream( - this ObsWebSocketClient client, + public IAsyncEnumerable InputAudioMonitorTypeChangedStream( int capacity = EventStream.DefaultCapacity, CancellationToken cancellationToken = default) { - ArgumentNullException.ThrowIfNull(client); + ObsWebSocketClient source = client; return EventStream.Create( - handler => client.InputAudioMonitorTypeChanged += handler, - handler => client.InputAudioMonitorTypeChanged -= handler, + handler => source.InputAudioMonitorTypeChanged += handler, + handler => source.InputAudioMonitorTypeChanged -= handler, capacity, cancellationToken); } @@ -609,40 +628,45 @@ public static class ObsWebSocketClientEventStreams /// Streams InputVolumeMeters events as they arrive. /// A high-volume event providing volume levels of all active inputs every 50 milliseconds. /// - /// The ObsWebSocketClient instance. /// Events buffered before the oldest is dropped. /// Ends the enumeration and unsubscribes. /// Requires the InputVolumeMeters subscription. - public static IAsyncEnumerable InputVolumeMetersStream( - this ObsWebSocketClient client, + public IAsyncEnumerable InputVolumeMetersStream( int capacity = EventStream.DefaultCapacity, CancellationToken cancellationToken = default) { - ArgumentNullException.ThrowIfNull(client); + ObsWebSocketClient source = client; return EventStream.Create( - handler => client.InputVolumeMeters += handler, - handler => client.InputVolumeMeters -= handler, + handler => source.InputVolumeMeters += handler, + handler => source.InputVolumeMeters -= handler, capacity, cancellationToken); } +} + +/// +/// Events in the media inputs category, as async sequences. +/// Each accessor subscribes for the lifetime of the enumeration and unsubscribes +/// when it ends, so the caller never manages handlers. +/// +public readonly partial struct MediaInputsGroup +{ /// /// Streams MediaInputPlaybackStarted events as they arrive. /// A media input has started playing. /// - /// The ObsWebSocketClient instance. /// Events buffered before the oldest is dropped. /// Ends the enumeration and unsubscribes. /// Requires the MediaInputs subscription. - public static IAsyncEnumerable MediaInputPlaybackStartedStream( - this ObsWebSocketClient client, + public IAsyncEnumerable MediaInputPlaybackStartedStream( int capacity = EventStream.DefaultCapacity, CancellationToken cancellationToken = default) { - ArgumentNullException.ThrowIfNull(client); + ObsWebSocketClient source = client; return EventStream.Create( - handler => client.MediaInputPlaybackStarted += handler, - handler => client.MediaInputPlaybackStarted -= handler, + handler => source.MediaInputPlaybackStarted += handler, + handler => source.MediaInputPlaybackStarted -= handler, capacity, cancellationToken); } @@ -651,19 +675,17 @@ public static class ObsWebSocketClientEventStreams /// Streams MediaInputPlaybackEnded events as they arrive. /// A media input has finished playing. /// - /// The ObsWebSocketClient instance. /// Events buffered before the oldest is dropped. /// Ends the enumeration and unsubscribes. /// Requires the MediaInputs subscription. - public static IAsyncEnumerable MediaInputPlaybackEndedStream( - this ObsWebSocketClient client, + public IAsyncEnumerable MediaInputPlaybackEndedStream( int capacity = EventStream.DefaultCapacity, CancellationToken cancellationToken = default) { - ArgumentNullException.ThrowIfNull(client); + ObsWebSocketClient source = client; return EventStream.Create( - handler => client.MediaInputPlaybackEnded += handler, - handler => client.MediaInputPlaybackEnded -= handler, + handler => source.MediaInputPlaybackEnded += handler, + handler => source.MediaInputPlaybackEnded -= handler, capacity, cancellationToken); } @@ -672,40 +694,45 @@ public static class ObsWebSocketClientEventStreams /// Streams MediaInputActionTriggered events as they arrive. /// An action has been performed on an input. /// - /// The ObsWebSocketClient instance. /// Events buffered before the oldest is dropped. /// Ends the enumeration and unsubscribes. /// Requires the MediaInputs subscription. - public static IAsyncEnumerable MediaInputActionTriggeredStream( - this ObsWebSocketClient client, + public IAsyncEnumerable MediaInputActionTriggeredStream( int capacity = EventStream.DefaultCapacity, CancellationToken cancellationToken = default) { - ArgumentNullException.ThrowIfNull(client); + ObsWebSocketClient source = client; return EventStream.Create( - handler => client.MediaInputActionTriggered += handler, - handler => client.MediaInputActionTriggered -= handler, + handler => source.MediaInputActionTriggered += handler, + handler => source.MediaInputActionTriggered -= handler, capacity, cancellationToken); } +} + +/// +/// Events in the outputs category, as async sequences. +/// Each accessor subscribes for the lifetime of the enumeration and unsubscribes +/// when it ends, so the caller never manages handlers. +/// +public readonly partial struct OutputsGroup +{ /// /// Streams StreamStateChanged events as they arrive. /// The state of the stream output has changed. /// - /// The ObsWebSocketClient instance. /// Events buffered before the oldest is dropped. /// Ends the enumeration and unsubscribes. /// Requires the Outputs subscription. - public static IAsyncEnumerable StreamStateChangedStream( - this ObsWebSocketClient client, + public IAsyncEnumerable StreamStateChangedStream( int capacity = EventStream.DefaultCapacity, CancellationToken cancellationToken = default) { - ArgumentNullException.ThrowIfNull(client); + ObsWebSocketClient source = client; return EventStream.Create( - handler => client.StreamStateChanged += handler, - handler => client.StreamStateChanged -= handler, + handler => source.StreamStateChanged += handler, + handler => source.StreamStateChanged -= handler, capacity, cancellationToken); } @@ -714,19 +741,17 @@ public static class ObsWebSocketClientEventStreams /// Streams RecordStateChanged events as they arrive. /// The state of the record output has changed. /// - /// The ObsWebSocketClient instance. /// Events buffered before the oldest is dropped. /// Ends the enumeration and unsubscribes. /// Requires the Outputs subscription. - public static IAsyncEnumerable RecordStateChangedStream( - this ObsWebSocketClient client, + public IAsyncEnumerable RecordStateChangedStream( int capacity = EventStream.DefaultCapacity, CancellationToken cancellationToken = default) { - ArgumentNullException.ThrowIfNull(client); + ObsWebSocketClient source = client; return EventStream.Create( - handler => client.RecordStateChanged += handler, - handler => client.RecordStateChanged -= handler, + handler => source.RecordStateChanged += handler, + handler => source.RecordStateChanged -= handler, capacity, cancellationToken); } @@ -735,19 +760,17 @@ public static class ObsWebSocketClientEventStreams /// Streams RecordFileChanged events as they arrive. /// The record output has started writing to a new file. For example, when a file split happens. /// - /// The ObsWebSocketClient instance. /// Events buffered before the oldest is dropped. /// Ends the enumeration and unsubscribes. /// Requires the Outputs subscription. - public static IAsyncEnumerable RecordFileChangedStream( - this ObsWebSocketClient client, + public IAsyncEnumerable RecordFileChangedStream( int capacity = EventStream.DefaultCapacity, CancellationToken cancellationToken = default) { - ArgumentNullException.ThrowIfNull(client); + ObsWebSocketClient source = client; return EventStream.Create( - handler => client.RecordFileChanged += handler, - handler => client.RecordFileChanged -= handler, + handler => source.RecordFileChanged += handler, + handler => source.RecordFileChanged -= handler, capacity, cancellationToken); } @@ -756,19 +779,17 @@ public static class ObsWebSocketClientEventStreams /// Streams ReplayBufferStateChanged events as they arrive. /// The state of the replay buffer output has changed. /// - /// The ObsWebSocketClient instance. /// Events buffered before the oldest is dropped. /// Ends the enumeration and unsubscribes. /// Requires the Outputs subscription. - public static IAsyncEnumerable ReplayBufferStateChangedStream( - this ObsWebSocketClient client, + public IAsyncEnumerable ReplayBufferStateChangedStream( int capacity = EventStream.DefaultCapacity, CancellationToken cancellationToken = default) { - ArgumentNullException.ThrowIfNull(client); + ObsWebSocketClient source = client; return EventStream.Create( - handler => client.ReplayBufferStateChanged += handler, - handler => client.ReplayBufferStateChanged -= handler, + handler => source.ReplayBufferStateChanged += handler, + handler => source.ReplayBufferStateChanged -= handler, capacity, cancellationToken); } @@ -777,19 +798,17 @@ public static class ObsWebSocketClientEventStreams /// Streams VirtualcamStateChanged events as they arrive. /// The state of the virtualcam output has changed. /// - /// The ObsWebSocketClient instance. /// Events buffered before the oldest is dropped. /// Ends the enumeration and unsubscribes. /// Requires the Outputs subscription. - public static IAsyncEnumerable VirtualcamStateChangedStream( - this ObsWebSocketClient client, + public IAsyncEnumerable VirtualcamStateChangedStream( int capacity = EventStream.DefaultCapacity, CancellationToken cancellationToken = default) { - ArgumentNullException.ThrowIfNull(client); + ObsWebSocketClient source = client; return EventStream.Create( - handler => client.VirtualcamStateChanged += handler, - handler => client.VirtualcamStateChanged -= handler, + handler => source.VirtualcamStateChanged += handler, + handler => source.VirtualcamStateChanged -= handler, capacity, cancellationToken); } @@ -798,40 +817,45 @@ public static class ObsWebSocketClientEventStreams /// Streams ReplayBufferSaved events as they arrive. /// The replay buffer has been saved. /// - /// The ObsWebSocketClient instance. /// Events buffered before the oldest is dropped. /// Ends the enumeration and unsubscribes. /// Requires the Outputs subscription. - public static IAsyncEnumerable ReplayBufferSavedStream( - this ObsWebSocketClient client, + public IAsyncEnumerable ReplayBufferSavedStream( int capacity = EventStream.DefaultCapacity, CancellationToken cancellationToken = default) { - ArgumentNullException.ThrowIfNull(client); + ObsWebSocketClient source = client; return EventStream.Create( - handler => client.ReplayBufferSaved += handler, - handler => client.ReplayBufferSaved -= handler, + handler => source.ReplayBufferSaved += handler, + handler => source.ReplayBufferSaved -= handler, capacity, cancellationToken); } +} + +/// +/// Events in the scene items category, as async sequences. +/// Each accessor subscribes for the lifetime of the enumeration and unsubscribes +/// when it ends, so the caller never manages handlers. +/// +public readonly partial struct SceneItemsGroup +{ /// /// Streams SceneItemCreated events as they arrive. /// A scene item has been created. /// - /// The ObsWebSocketClient instance. /// Events buffered before the oldest is dropped. /// Ends the enumeration and unsubscribes. /// Requires the SceneItems subscription. - public static IAsyncEnumerable SceneItemCreatedStream( - this ObsWebSocketClient client, + public IAsyncEnumerable SceneItemCreatedStream( int capacity = EventStream.DefaultCapacity, CancellationToken cancellationToken = default) { - ArgumentNullException.ThrowIfNull(client); + ObsWebSocketClient source = client; return EventStream.Create( - handler => client.SceneItemCreated += handler, - handler => client.SceneItemCreated -= handler, + handler => source.SceneItemCreated += handler, + handler => source.SceneItemCreated -= handler, capacity, cancellationToken); } @@ -840,19 +864,17 @@ public static class ObsWebSocketClientEventStreams /// Streams SceneItemRemoved events as they arrive. /// A scene item has been removed. This event is not emitted when the scene the item is in is removed. /// - /// The ObsWebSocketClient instance. /// Events buffered before the oldest is dropped. /// Ends the enumeration and unsubscribes. /// Requires the SceneItems subscription. - public static IAsyncEnumerable SceneItemRemovedStream( - this ObsWebSocketClient client, + public IAsyncEnumerable SceneItemRemovedStream( int capacity = EventStream.DefaultCapacity, CancellationToken cancellationToken = default) { - ArgumentNullException.ThrowIfNull(client); + ObsWebSocketClient source = client; return EventStream.Create( - handler => client.SceneItemRemoved += handler, - handler => client.SceneItemRemoved -= handler, + handler => source.SceneItemRemoved += handler, + handler => source.SceneItemRemoved -= handler, capacity, cancellationToken); } @@ -861,19 +883,17 @@ public static class ObsWebSocketClientEventStreams /// Streams SceneItemListReindexed events as they arrive. /// A scene's item list has been reindexed. /// - /// The ObsWebSocketClient instance. /// Events buffered before the oldest is dropped. /// Ends the enumeration and unsubscribes. /// Requires the SceneItems subscription. - public static IAsyncEnumerable SceneItemListReindexedStream( - this ObsWebSocketClient client, + public IAsyncEnumerable SceneItemListReindexedStream( int capacity = EventStream.DefaultCapacity, CancellationToken cancellationToken = default) { - ArgumentNullException.ThrowIfNull(client); + ObsWebSocketClient source = client; return EventStream.Create( - handler => client.SceneItemListReindexed += handler, - handler => client.SceneItemListReindexed -= handler, + handler => source.SceneItemListReindexed += handler, + handler => source.SceneItemListReindexed -= handler, capacity, cancellationToken); } @@ -882,19 +902,17 @@ public static class ObsWebSocketClientEventStreams /// Streams SceneItemEnableStateChanged events as they arrive. /// A scene item's enable state has changed. /// - /// The ObsWebSocketClient instance. /// Events buffered before the oldest is dropped. /// Ends the enumeration and unsubscribes. /// Requires the SceneItems subscription. - public static IAsyncEnumerable SceneItemEnableStateChangedStream( - this ObsWebSocketClient client, + public IAsyncEnumerable SceneItemEnableStateChangedStream( int capacity = EventStream.DefaultCapacity, CancellationToken cancellationToken = default) { - ArgumentNullException.ThrowIfNull(client); + ObsWebSocketClient source = client; return EventStream.Create( - handler => client.SceneItemEnableStateChanged += handler, - handler => client.SceneItemEnableStateChanged -= handler, + handler => source.SceneItemEnableStateChanged += handler, + handler => source.SceneItemEnableStateChanged -= handler, capacity, cancellationToken); } @@ -903,19 +921,17 @@ public static class ObsWebSocketClientEventStreams /// Streams SceneItemLockStateChanged events as they arrive. /// A scene item's lock state has changed. /// - /// The ObsWebSocketClient instance. /// Events buffered before the oldest is dropped. /// Ends the enumeration and unsubscribes. /// Requires the SceneItems subscription. - public static IAsyncEnumerable SceneItemLockStateChangedStream( - this ObsWebSocketClient client, + public IAsyncEnumerable SceneItemLockStateChangedStream( int capacity = EventStream.DefaultCapacity, CancellationToken cancellationToken = default) { - ArgumentNullException.ThrowIfNull(client); + ObsWebSocketClient source = client; return EventStream.Create( - handler => client.SceneItemLockStateChanged += handler, - handler => client.SceneItemLockStateChanged -= handler, + handler => source.SceneItemLockStateChanged += handler, + handler => source.SceneItemLockStateChanged -= handler, capacity, cancellationToken); } @@ -924,19 +940,17 @@ public static class ObsWebSocketClientEventStreams /// Streams SceneItemSelected events as they arrive. /// A scene item has been selected in the Ui. /// - /// The ObsWebSocketClient instance. /// Events buffered before the oldest is dropped. /// Ends the enumeration and unsubscribes. /// Requires the SceneItems subscription. - public static IAsyncEnumerable SceneItemSelectedStream( - this ObsWebSocketClient client, + public IAsyncEnumerable SceneItemSelectedStream( int capacity = EventStream.DefaultCapacity, CancellationToken cancellationToken = default) { - ArgumentNullException.ThrowIfNull(client); + ObsWebSocketClient source = client; return EventStream.Create( - handler => client.SceneItemSelected += handler, - handler => client.SceneItemSelected -= handler, + handler => source.SceneItemSelected += handler, + handler => source.SceneItemSelected -= handler, capacity, cancellationToken); } @@ -945,40 +959,45 @@ public static class ObsWebSocketClientEventStreams /// Streams SceneItemTransformChanged events as they arrive. /// The transform/crop of a scene item has changed. /// - /// The ObsWebSocketClient instance. /// Events buffered before the oldest is dropped. /// Ends the enumeration and unsubscribes. /// Requires the SceneItemTransformChanged subscription. - public static IAsyncEnumerable SceneItemTransformChangedStream( - this ObsWebSocketClient client, + public IAsyncEnumerable SceneItemTransformChangedStream( int capacity = EventStream.DefaultCapacity, CancellationToken cancellationToken = default) { - ArgumentNullException.ThrowIfNull(client); + ObsWebSocketClient source = client; return EventStream.Create( - handler => client.SceneItemTransformChanged += handler, - handler => client.SceneItemTransformChanged -= handler, + handler => source.SceneItemTransformChanged += handler, + handler => source.SceneItemTransformChanged -= handler, capacity, cancellationToken); } +} + +/// +/// Events in the scenes category, as async sequences. +/// Each accessor subscribes for the lifetime of the enumeration and unsubscribes +/// when it ends, so the caller never manages handlers. +/// +public readonly partial struct ScenesGroup +{ /// /// Streams SceneCreated events as they arrive. /// A new scene has been created. /// - /// The ObsWebSocketClient instance. /// Events buffered before the oldest is dropped. /// Ends the enumeration and unsubscribes. /// Requires the Scenes subscription. - public static IAsyncEnumerable SceneCreatedStream( - this ObsWebSocketClient client, + public IAsyncEnumerable SceneCreatedStream( int capacity = EventStream.DefaultCapacity, CancellationToken cancellationToken = default) { - ArgumentNullException.ThrowIfNull(client); + ObsWebSocketClient source = client; return EventStream.Create( - handler => client.SceneCreated += handler, - handler => client.SceneCreated -= handler, + handler => source.SceneCreated += handler, + handler => source.SceneCreated -= handler, capacity, cancellationToken); } @@ -987,19 +1006,17 @@ public static class ObsWebSocketClientEventStreams /// Streams SceneRemoved events as they arrive. /// A scene has been removed. /// - /// The ObsWebSocketClient instance. /// Events buffered before the oldest is dropped. /// Ends the enumeration and unsubscribes. /// Requires the Scenes subscription. - public static IAsyncEnumerable SceneRemovedStream( - this ObsWebSocketClient client, + public IAsyncEnumerable SceneRemovedStream( int capacity = EventStream.DefaultCapacity, CancellationToken cancellationToken = default) { - ArgumentNullException.ThrowIfNull(client); + ObsWebSocketClient source = client; return EventStream.Create( - handler => client.SceneRemoved += handler, - handler => client.SceneRemoved -= handler, + handler => source.SceneRemoved += handler, + handler => source.SceneRemoved -= handler, capacity, cancellationToken); } @@ -1008,19 +1025,17 @@ public static class ObsWebSocketClientEventStreams /// Streams SceneNameChanged events as they arrive. /// The name of a scene has changed. /// - /// The ObsWebSocketClient instance. /// Events buffered before the oldest is dropped. /// Ends the enumeration and unsubscribes. /// Requires the Scenes subscription. - public static IAsyncEnumerable SceneNameChangedStream( - this ObsWebSocketClient client, + public IAsyncEnumerable SceneNameChangedStream( int capacity = EventStream.DefaultCapacity, CancellationToken cancellationToken = default) { - ArgumentNullException.ThrowIfNull(client); + ObsWebSocketClient source = client; return EventStream.Create( - handler => client.SceneNameChanged += handler, - handler => client.SceneNameChanged -= handler, + handler => source.SceneNameChanged += handler, + handler => source.SceneNameChanged -= handler, capacity, cancellationToken); } @@ -1029,19 +1044,17 @@ public static class ObsWebSocketClientEventStreams /// Streams CurrentProgramSceneChanged events as they arrive. /// The current program scene has changed. /// - /// The ObsWebSocketClient instance. /// Events buffered before the oldest is dropped. /// Ends the enumeration and unsubscribes. /// Requires the Scenes subscription. - public static IAsyncEnumerable CurrentProgramSceneChangedStream( - this ObsWebSocketClient client, + public IAsyncEnumerable CurrentProgramSceneChangedStream( int capacity = EventStream.DefaultCapacity, CancellationToken cancellationToken = default) { - ArgumentNullException.ThrowIfNull(client); + ObsWebSocketClient source = client; return EventStream.Create( - handler => client.CurrentProgramSceneChanged += handler, - handler => client.CurrentProgramSceneChanged -= handler, + handler => source.CurrentProgramSceneChanged += handler, + handler => source.CurrentProgramSceneChanged -= handler, capacity, cancellationToken); } @@ -1050,19 +1063,17 @@ public static class ObsWebSocketClientEventStreams /// Streams CurrentPreviewSceneChanged events as they arrive. /// The current preview scene has changed. /// - /// The ObsWebSocketClient instance. /// Events buffered before the oldest is dropped. /// Ends the enumeration and unsubscribes. /// Requires the Scenes subscription. - public static IAsyncEnumerable CurrentPreviewSceneChangedStream( - this ObsWebSocketClient client, + public IAsyncEnumerable CurrentPreviewSceneChangedStream( int capacity = EventStream.DefaultCapacity, CancellationToken cancellationToken = default) { - ArgumentNullException.ThrowIfNull(client); + ObsWebSocketClient source = client; return EventStream.Create( - handler => client.CurrentPreviewSceneChanged += handler, - handler => client.CurrentPreviewSceneChanged -= handler, + handler => source.CurrentPreviewSceneChanged += handler, + handler => source.CurrentPreviewSceneChanged -= handler, capacity, cancellationToken); } @@ -1071,40 +1082,45 @@ public static class ObsWebSocketClientEventStreams /// Streams SceneListChanged events as they arrive. /// The list of scenes has changed. TODO: Make OBS fire this event when scenes are reordered. /// - /// The ObsWebSocketClient instance. /// Events buffered before the oldest is dropped. /// Ends the enumeration and unsubscribes. /// Requires the Scenes subscription. - public static IAsyncEnumerable SceneListChangedStream( - this ObsWebSocketClient client, + public IAsyncEnumerable SceneListChangedStream( int capacity = EventStream.DefaultCapacity, CancellationToken cancellationToken = default) { - ArgumentNullException.ThrowIfNull(client); + ObsWebSocketClient source = client; return EventStream.Create( - handler => client.SceneListChanged += handler, - handler => client.SceneListChanged -= handler, + handler => source.SceneListChanged += handler, + handler => source.SceneListChanged -= handler, capacity, cancellationToken); } +} + +/// +/// Events in the transitions category, as async sequences. +/// Each accessor subscribes for the lifetime of the enumeration and unsubscribes +/// when it ends, so the caller never manages handlers. +/// +public readonly partial struct TransitionsGroup +{ /// /// Streams CurrentSceneTransitionChanged events as they arrive. /// The current scene transition has changed. /// - /// The ObsWebSocketClient instance. /// Events buffered before the oldest is dropped. /// Ends the enumeration and unsubscribes. /// Requires the Transitions subscription. - public static IAsyncEnumerable CurrentSceneTransitionChangedStream( - this ObsWebSocketClient client, + public IAsyncEnumerable CurrentSceneTransitionChangedStream( int capacity = EventStream.DefaultCapacity, CancellationToken cancellationToken = default) { - ArgumentNullException.ThrowIfNull(client); + ObsWebSocketClient source = client; return EventStream.Create( - handler => client.CurrentSceneTransitionChanged += handler, - handler => client.CurrentSceneTransitionChanged -= handler, + handler => source.CurrentSceneTransitionChanged += handler, + handler => source.CurrentSceneTransitionChanged -= handler, capacity, cancellationToken); } @@ -1113,19 +1129,17 @@ public static class ObsWebSocketClientEventStreams /// Streams CurrentSceneTransitionDurationChanged events as they arrive. /// The current scene transition duration has changed. /// - /// The ObsWebSocketClient instance. /// Events buffered before the oldest is dropped. /// Ends the enumeration and unsubscribes. /// Requires the Transitions subscription. - public static IAsyncEnumerable CurrentSceneTransitionDurationChangedStream( - this ObsWebSocketClient client, + public IAsyncEnumerable CurrentSceneTransitionDurationChangedStream( int capacity = EventStream.DefaultCapacity, CancellationToken cancellationToken = default) { - ArgumentNullException.ThrowIfNull(client); + ObsWebSocketClient source = client; return EventStream.Create( - handler => client.CurrentSceneTransitionDurationChanged += handler, - handler => client.CurrentSceneTransitionDurationChanged -= handler, + handler => source.CurrentSceneTransitionDurationChanged += handler, + handler => source.CurrentSceneTransitionDurationChanged -= handler, capacity, cancellationToken); } @@ -1134,19 +1148,17 @@ public static class ObsWebSocketClientEventStreams /// Streams SceneTransitionStarted events as they arrive. /// A scene transition has started. /// - /// The ObsWebSocketClient instance. /// Events buffered before the oldest is dropped. /// Ends the enumeration and unsubscribes. /// Requires the Transitions subscription. - public static IAsyncEnumerable SceneTransitionStartedStream( - this ObsWebSocketClient client, + public IAsyncEnumerable SceneTransitionStartedStream( int capacity = EventStream.DefaultCapacity, CancellationToken cancellationToken = default) { - ArgumentNullException.ThrowIfNull(client); + ObsWebSocketClient source = client; return EventStream.Create( - handler => client.SceneTransitionStarted += handler, - handler => client.SceneTransitionStarted -= handler, + handler => source.SceneTransitionStarted += handler, + handler => source.SceneTransitionStarted -= handler, capacity, cancellationToken); } @@ -1155,19 +1167,17 @@ public static class ObsWebSocketClientEventStreams /// Streams SceneTransitionEnded events as they arrive. /// A scene transition has completed fully. Note: Does not appear to trigger when the transition is interrupted by the user. /// - /// The ObsWebSocketClient instance. /// Events buffered before the oldest is dropped. /// Ends the enumeration and unsubscribes. /// Requires the Transitions subscription. - public static IAsyncEnumerable SceneTransitionEndedStream( - this ObsWebSocketClient client, + public IAsyncEnumerable SceneTransitionEndedStream( int capacity = EventStream.DefaultCapacity, CancellationToken cancellationToken = default) { - ArgumentNullException.ThrowIfNull(client); + ObsWebSocketClient source = client; return EventStream.Create( - handler => client.SceneTransitionEnded += handler, - handler => client.SceneTransitionEnded -= handler, + handler => source.SceneTransitionEnded += handler, + handler => source.SceneTransitionEnded -= handler, capacity, cancellationToken); } @@ -1176,40 +1186,45 @@ public static class ObsWebSocketClientEventStreams /// Streams SceneTransitionVideoEnded events as they arrive. /// A scene transition's video has completed fully. Useful for stinger transitions to tell when the video *actually* ends. `SceneTransitionEnded` only signifies the cut point, not the completion of transition playback. Note: Appears to be called by every transition, regardless of relevance. /// - /// The ObsWebSocketClient instance. /// Events buffered before the oldest is dropped. /// Ends the enumeration and unsubscribes. /// Requires the Transitions subscription. - public static IAsyncEnumerable SceneTransitionVideoEndedStream( - this ObsWebSocketClient client, + public IAsyncEnumerable SceneTransitionVideoEndedStream( int capacity = EventStream.DefaultCapacity, CancellationToken cancellationToken = default) { - ArgumentNullException.ThrowIfNull(client); + ObsWebSocketClient source = client; return EventStream.Create( - handler => client.SceneTransitionVideoEnded += handler, - handler => client.SceneTransitionVideoEnded -= handler, + handler => source.SceneTransitionVideoEnded += handler, + handler => source.SceneTransitionVideoEnded -= handler, capacity, cancellationToken); } +} + +/// +/// Events in the ui category, as async sequences. +/// Each accessor subscribes for the lifetime of the enumeration and unsubscribes +/// when it ends, so the caller never manages handlers. +/// +public readonly partial struct UiGroup +{ /// /// Streams StudioModeStateChanged events as they arrive. /// Studio mode has been enabled or disabled. /// - /// The ObsWebSocketClient instance. /// Events buffered before the oldest is dropped. /// Ends the enumeration and unsubscribes. /// Requires the Ui subscription. - public static IAsyncEnumerable StudioModeStateChangedStream( - this ObsWebSocketClient client, + public IAsyncEnumerable StudioModeStateChangedStream( int capacity = EventStream.DefaultCapacity, CancellationToken cancellationToken = default) { - ArgumentNullException.ThrowIfNull(client); + ObsWebSocketClient source = client; return EventStream.Create( - handler => client.StudioModeStateChanged += handler, - handler => client.StudioModeStateChanged -= handler, + handler => source.StudioModeStateChanged += handler, + handler => source.StudioModeStateChanged -= handler, capacity, cancellationToken); } @@ -1218,63 +1233,20 @@ public static class ObsWebSocketClientEventStreams /// Streams ScreenshotSaved events as they arrive. /// A screenshot has been saved. Note: Triggered for the screenshot feature available in `Settings -> Hotkeys -> Screenshot Output` ONLY. Applications using `Get/SaveSourceScreenshot` should implement a `CustomEvent` if this kind of inter-client communication is desired. /// - /// The ObsWebSocketClient instance. /// Events buffered before the oldest is dropped. /// Ends the enumeration and unsubscribes. /// Requires the Ui subscription. - public static IAsyncEnumerable ScreenshotSavedStream( - this ObsWebSocketClient client, + public IAsyncEnumerable ScreenshotSavedStream( int capacity = EventStream.DefaultCapacity, CancellationToken cancellationToken = default) { - ArgumentNullException.ThrowIfNull(client); + ObsWebSocketClient source = client; return EventStream.Create( - handler => client.ScreenshotSaved += handler, - handler => client.ScreenshotSaved -= handler, - capacity, - cancellationToken); - } - - /// - /// Streams VendorEvent events as they arrive. - /// An event has been emitted from a vendor. A vendor is a unique name registered by a third-party plugin or script, which allows for custom requests and events to be added to obs-websocket. If a plugin or script implements vendor requests or events, documentation is expected to be provided with them. - /// - /// The ObsWebSocketClient instance. - /// Events buffered before the oldest is dropped. - /// Ends the enumeration and unsubscribes. - /// Requires the Vendors subscription. - public static IAsyncEnumerable VendorEventStream( - this ObsWebSocketClient client, - int capacity = EventStream.DefaultCapacity, - CancellationToken cancellationToken = default) - { - ArgumentNullException.ThrowIfNull(client); - return EventStream.Create( - handler => client.VendorEvent += handler, - handler => client.VendorEvent -= handler, - capacity, - cancellationToken); - } - - /// - /// Streams CustomEvent events as they arrive. - /// Custom event emitted by `BroadcastCustomEvent`. - /// - /// The ObsWebSocketClient instance. - /// Events buffered before the oldest is dropped. - /// Ends the enumeration and unsubscribes. - /// Requires the General subscription. - public static IAsyncEnumerable CustomEventStream( - this ObsWebSocketClient client, - int capacity = EventStream.DefaultCapacity, - CancellationToken cancellationToken = default) - { - ArgumentNullException.ThrowIfNull(client); - return EventStream.Create( - handler => client.CustomEvent += handler, - handler => client.CustomEvent -= handler, + handler => source.ScreenshotSaved += handler, + handler => source.ScreenshotSaved -= handler, capacity, cancellationToken); } } + diff --git a/ObsWebSocket.Core/Generated/Client/ObsWebSocketClient.Extensions.g.cs b/ObsWebSocket.Core/Generated/Client/ObsWebSocketClient.Extensions.g.cs index fe45c15..d122759 100644 --- a/ObsWebSocket.Core/Generated/Client/ObsWebSocketClient.Extensions.g.cs +++ b/ObsWebSocket.Core/Generated/Client/ObsWebSocketClient.Extensions.g.cs @@ -17,7 +17,7 @@ namespace ObsWebSocket.Core; /// Requests in the canvases category. /// /// The client these requests are sent on. -public readonly partial struct CanvasesRequestGroup(ObsWebSocketClient client) +public readonly partial struct CanvasesGroup(ObsWebSocketClient client) { /// /// Gets an array of canvases in OBS. @@ -43,7 +43,7 @@ public readonly partial struct CanvasesRequestGroup(ObsWebSocketClient client) /// Requests in the config category. /// /// The client these requests are sent on. -public readonly partial struct ConfigRequestGroup(ObsWebSocketClient client) +public readonly partial struct ConfigGroup(ObsWebSocketClient client) { /// /// Gets the value of a "slot" from the selected persistent data realm. @@ -379,7 +379,7 @@ public async Task SetRecordDirectoryAsync(ObsWebSocket.Core.Protocol.Requests.Se /// Requests in the filters category. /// /// The client these requests are sent on. -public readonly partial struct FiltersRequestGroup(ObsWebSocketClient client) +public readonly partial struct FiltersGroup(ObsWebSocketClient client) { /// /// Gets an array of all available source filter kinds. @@ -578,7 +578,7 @@ public async Task SetSourceFilterEnabledAsync(ObsWebSocket.Core.Protocol.Request /// Requests in the general category. /// /// The client these requests are sent on. -public readonly partial struct GeneralRequestGroup(ObsWebSocketClient client) +public readonly partial struct GeneralGroup(ObsWebSocketClient client) { /// /// Gets data about the current plugin and RPC version. @@ -744,7 +744,7 @@ public async Task SleepAsync(ObsWebSocket.Core.Protocol.Requests.SleepRequestDat /// Requests in the inputs category. /// /// The client these requests are sent on. -public readonly partial struct InputsRequestGroup(ObsWebSocketClient client) +public readonly partial struct InputsGroup(ObsWebSocketClient client) { /// /// Gets an array of all inputs in OBS. @@ -1328,7 +1328,7 @@ public async Task PressInputPropertiesButtonAsync(ObsWebSocket.Core.Protocol.Req /// Requests in the media inputs category. /// /// The client these requests are sent on. -public readonly partial struct MediaInputsRequestGroup(ObsWebSocketClient client) +public readonly partial struct MediaInputsGroup(ObsWebSocketClient client) { /// /// Gets the status of a media input. @@ -1427,7 +1427,7 @@ public async Task TriggerMediaInputActionAsync(ObsWebSocket.Core.Protocol.Reques /// Requests in the outputs category. /// /// The client these requests are sent on. -public readonly partial struct OutputsRequestGroup(ObsWebSocketClient client) +public readonly partial struct OutputsGroup(ObsWebSocketClient client) { /// /// Gets the status of the virtualcam output. @@ -1747,7 +1747,7 @@ public async Task SetOutputSettingsAsync(ObsWebSocket.Core.Protocol.Requests.Set /// Requests in the record category. /// /// The client these requests are sent on. -public readonly partial struct RecordRequestGroup(ObsWebSocketClient client) +public readonly partial struct RecordGroup(ObsWebSocketClient client) { /// /// Gets the status of the record output. @@ -1920,7 +1920,7 @@ public async Task CreateRecordChapterAsync(ObsWebSocket.Core.Protocol.Requests.C /// Requests in the scene items category. /// /// The client these requests are sent on. -public readonly partial struct SceneItemsRequestGroup(ObsWebSocketClient client) +public readonly partial struct SceneItemsGroup(ObsWebSocketClient client) { /// /// Gets a list of all scene items in a scene. @@ -2295,7 +2295,7 @@ public async Task SetSceneItemBlendModeAsync(ObsWebSocket.Core.Protocol.Requests /// Requests in the scenes category. /// /// The client these requests are sent on. -public readonly partial struct ScenesRequestGroup(ObsWebSocketClient client) +public readonly partial struct ScenesGroup(ObsWebSocketClient client) { /// /// Gets an array of scenes in OBS. @@ -2523,7 +2523,7 @@ public async Task SetSceneSceneTransitionOverrideAsync(ObsWebSocket.Core.Protoco /// Requests in the sources category. /// /// The client these requests are sent on. -public readonly partial struct SourcesRequestGroup(ObsWebSocketClient client) +public readonly partial struct SourcesGroup(ObsWebSocketClient client) { /// /// Gets the active and show state of a source. @@ -2600,7 +2600,7 @@ public async Task SaveSourceScreenshotAsync(ObsWebSocket.Core.Protocol.Requests. /// Requests in the stream category. /// /// The client these requests are sent on. -public readonly partial struct StreamRequestGroup(ObsWebSocketClient client) +public readonly partial struct StreamGroup(ObsWebSocketClient client) { /// /// Gets the status of the stream output. @@ -2699,7 +2699,7 @@ public async Task SendStreamCaptionAsync(ObsWebSocket.Core.Protocol.Requests.Sen /// Requests in the transitions category. /// /// The client these requests are sent on. -public readonly partial struct TransitionsRequestGroup(ObsWebSocketClient client) +public readonly partial struct TransitionsGroup(ObsWebSocketClient client) { /// /// Gets an array of all available transition kinds. @@ -2881,7 +2881,7 @@ public async Task SetTBarPositionAsync(ObsWebSocket.Core.Protocol.Requests.SetTB /// Requests in the ui category. /// /// The client these requests are sent on. -public readonly partial struct UiRequestGroup(ObsWebSocketClient client) +public readonly partial struct UiGroup(ObsWebSocketClient client) { /// /// Gets whether studio is enabled. @@ -3055,7 +3055,7 @@ public static class ObsWebSocketClientExtensions /// /// Requests in the canvases category. /// - public CanvasesRequestGroup Canvases => new(client); + public CanvasesGroup Canvases => new(client); } extension(ObsWebSocketClient client) @@ -3063,7 +3063,7 @@ public static class ObsWebSocketClientExtensions /// /// Requests in the config category. /// - public ConfigRequestGroup Config => new(client); + public ConfigGroup Config => new(client); } extension(ObsWebSocketClient client) @@ -3071,7 +3071,7 @@ public static class ObsWebSocketClientExtensions /// /// Requests in the filters category. /// - public FiltersRequestGroup Filters => new(client); + public FiltersGroup Filters => new(client); } extension(ObsWebSocketClient client) @@ -3079,7 +3079,7 @@ public static class ObsWebSocketClientExtensions /// /// Requests in the general category. /// - public GeneralRequestGroup General => new(client); + public GeneralGroup General => new(client); } extension(ObsWebSocketClient client) @@ -3087,7 +3087,7 @@ public static class ObsWebSocketClientExtensions /// /// Requests in the inputs category. /// - public InputsRequestGroup Inputs => new(client); + public InputsGroup Inputs => new(client); } extension(ObsWebSocketClient client) @@ -3095,7 +3095,7 @@ public static class ObsWebSocketClientExtensions /// /// Requests in the media inputs category. /// - public MediaInputsRequestGroup MediaInputs => new(client); + public MediaInputsGroup MediaInputs => new(client); } extension(ObsWebSocketClient client) @@ -3103,7 +3103,7 @@ public static class ObsWebSocketClientExtensions /// /// Requests in the outputs category. /// - public OutputsRequestGroup Outputs => new(client); + public OutputsGroup Outputs => new(client); } extension(ObsWebSocketClient client) @@ -3111,7 +3111,7 @@ public static class ObsWebSocketClientExtensions /// /// Requests in the record category. /// - public RecordRequestGroup Record => new(client); + public RecordGroup Record => new(client); } extension(ObsWebSocketClient client) @@ -3119,7 +3119,7 @@ public static class ObsWebSocketClientExtensions /// /// Requests in the scene items category. /// - public SceneItemsRequestGroup SceneItems => new(client); + public SceneItemsGroup SceneItems => new(client); } extension(ObsWebSocketClient client) @@ -3127,7 +3127,7 @@ public static class ObsWebSocketClientExtensions /// /// Requests in the scenes category. /// - public ScenesRequestGroup Scenes => new(client); + public ScenesGroup Scenes => new(client); } extension(ObsWebSocketClient client) @@ -3135,7 +3135,7 @@ public static class ObsWebSocketClientExtensions /// /// Requests in the sources category. /// - public SourcesRequestGroup Sources => new(client); + public SourcesGroup Sources => new(client); } extension(ObsWebSocketClient client) @@ -3143,7 +3143,7 @@ public static class ObsWebSocketClientExtensions /// /// Requests in the stream category. /// - public StreamRequestGroup Stream => new(client); + public StreamGroup Stream => new(client); } extension(ObsWebSocketClient client) @@ -3151,7 +3151,7 @@ public static class ObsWebSocketClientExtensions /// /// Requests in the transitions category. /// - public TransitionsRequestGroup Transitions => new(client); + public TransitionsGroup Transitions => new(client); } extension(ObsWebSocketClient client) @@ -3159,7 +3159,7 @@ public static class ObsWebSocketClientExtensions /// /// Requests in the ui category. /// - public UiRequestGroup Ui => new(client); + public UiGroup Ui => new(client); } } diff --git a/ObsWebSocket.Core/Groups/ConfigRequestGroup.cs b/ObsWebSocket.Core/Groups/ConfigGroup.cs similarity index 99% rename from ObsWebSocket.Core/Groups/ConfigRequestGroup.cs rename to ObsWebSocket.Core/Groups/ConfigGroup.cs index d210228..87b79e2 100644 --- a/ObsWebSocket.Core/Groups/ConfigRequestGroup.cs +++ b/ObsWebSocket.Core/Groups/ConfigGroup.cs @@ -17,7 +17,7 @@ namespace ObsWebSocket.Core; /// /// Conveniences for the Config category, alongside its generated requests. /// -public readonly partial struct ConfigRequestGroup +public readonly partial struct ConfigGroup { /// /// Gets the current stream service settings as a strongly-typed object. The service type string is discarded. diff --git a/ObsWebSocket.Core/Groups/FiltersRequestGroup.cs b/ObsWebSocket.Core/Groups/FiltersGroup.cs similarity index 99% rename from ObsWebSocket.Core/Groups/FiltersRequestGroup.cs rename to ObsWebSocket.Core/Groups/FiltersGroup.cs index 7ee21ad..cf414ac 100644 --- a/ObsWebSocket.Core/Groups/FiltersRequestGroup.cs +++ b/ObsWebSocket.Core/Groups/FiltersGroup.cs @@ -17,7 +17,7 @@ namespace ObsWebSocket.Core; /// /// Conveniences for the Filters category, alongside its generated requests. /// -public readonly partial struct FiltersRequestGroup +public readonly partial struct FiltersGroup { /// /// Retrieves the settings for a specific filter on a source and deserializes them using an explicit . diff --git a/ObsWebSocket.Core/Groups/GeneralRequestGroup.cs b/ObsWebSocket.Core/Groups/GeneralGroup.cs similarity index 97% rename from ObsWebSocket.Core/Groups/GeneralRequestGroup.cs rename to ObsWebSocket.Core/Groups/GeneralGroup.cs index acec06f..17af9d4 100644 --- a/ObsWebSocket.Core/Groups/GeneralRequestGroup.cs +++ b/ObsWebSocket.Core/Groups/GeneralGroup.cs @@ -17,7 +17,7 @@ namespace ObsWebSocket.Core; /// /// Conveniences for the General category, alongside its generated requests. /// -public readonly partial struct GeneralRequestGroup +public readonly partial struct GeneralGroup { /// /// Triggers an OBS hotkey by its canonical name (e.g., "OBSWebSocket.StartStream"). diff --git a/ObsWebSocket.Core/Groups/InputsRequestGroup.cs b/ObsWebSocket.Core/Groups/InputsGroup.cs similarity index 99% rename from ObsWebSocket.Core/Groups/InputsRequestGroup.cs rename to ObsWebSocket.Core/Groups/InputsGroup.cs index 75e543e..08cb247 100644 --- a/ObsWebSocket.Core/Groups/InputsRequestGroup.cs +++ b/ObsWebSocket.Core/Groups/InputsGroup.cs @@ -17,7 +17,7 @@ namespace ObsWebSocket.Core; /// /// Conveniences for the Inputs category, alongside its generated requests. /// -public readonly partial struct InputsRequestGroup +public readonly partial struct InputsGroup { /// /// Sets the text content of a Text (GDI+, Freetype 2, Pango) source. diff --git a/ObsWebSocket.Core/Groups/MediaInputsRequestGroup.cs b/ObsWebSocket.Core/Groups/MediaInputsGroup.cs similarity index 98% rename from ObsWebSocket.Core/Groups/MediaInputsRequestGroup.cs rename to ObsWebSocket.Core/Groups/MediaInputsGroup.cs index 4d52298..1fd4801 100644 --- a/ObsWebSocket.Core/Groups/MediaInputsRequestGroup.cs +++ b/ObsWebSocket.Core/Groups/MediaInputsGroup.cs @@ -17,7 +17,7 @@ namespace ObsWebSocket.Core; /// /// Conveniences for the MediaInputs category, alongside its generated requests. /// -public readonly partial struct MediaInputsRequestGroup +public readonly partial struct MediaInputsGroup { /// /// Triggers a media action on an input using the typed enum diff --git a/ObsWebSocket.Core/Groups/OutputsRequestGroup.cs b/ObsWebSocket.Core/Groups/OutputsGroup.cs similarity index 99% rename from ObsWebSocket.Core/Groups/OutputsRequestGroup.cs rename to ObsWebSocket.Core/Groups/OutputsGroup.cs index a4f3ec5..8c21a28 100644 --- a/ObsWebSocket.Core/Groups/OutputsRequestGroup.cs +++ b/ObsWebSocket.Core/Groups/OutputsGroup.cs @@ -17,7 +17,7 @@ namespace ObsWebSocket.Core; /// /// Conveniences for the Outputs category, alongside its generated requests. /// -public readonly partial struct OutputsRequestGroup +public readonly partial struct OutputsGroup { /// /// Gets the settings for an output as a strongly-typed object. diff --git a/ObsWebSocket.Core/Groups/RecordRequestGroup.cs b/ObsWebSocket.Core/Groups/RecordGroup.cs similarity index 98% rename from ObsWebSocket.Core/Groups/RecordRequestGroup.cs rename to ObsWebSocket.Core/Groups/RecordGroup.cs index f4bada8..6993e3f 100644 --- a/ObsWebSocket.Core/Groups/RecordRequestGroup.cs +++ b/ObsWebSocket.Core/Groups/RecordGroup.cs @@ -17,7 +17,7 @@ namespace ObsWebSocket.Core; /// /// Conveniences for the Record category, alongside its generated requests. /// -public readonly partial struct RecordRequestGroup +public readonly partial struct RecordGroup { private static readonly TimeSpan s_defaultOutputTimeout = TimeSpan.FromSeconds(10); diff --git a/ObsWebSocket.Core/Groups/SceneItemsRequestGroup.cs b/ObsWebSocket.Core/Groups/SceneItemsGroup.cs similarity index 99% rename from ObsWebSocket.Core/Groups/SceneItemsRequestGroup.cs rename to ObsWebSocket.Core/Groups/SceneItemsGroup.cs index 51e0a28..442be69 100644 --- a/ObsWebSocket.Core/Groups/SceneItemsRequestGroup.cs +++ b/ObsWebSocket.Core/Groups/SceneItemsGroup.cs @@ -17,7 +17,7 @@ namespace ObsWebSocket.Core; /// /// Conveniences for the SceneItems category, alongside its generated requests. /// -public readonly partial struct SceneItemsRequestGroup +public readonly partial struct SceneItemsGroup { /// /// Sets or toggles the enabled (visibility) state of a scene item, identified by its numeric ID. diff --git a/ObsWebSocket.Core/Groups/ScenesRequestGroup.cs b/ObsWebSocket.Core/Groups/ScenesGroup.cs similarity index 99% rename from ObsWebSocket.Core/Groups/ScenesRequestGroup.cs rename to ObsWebSocket.Core/Groups/ScenesGroup.cs index 810f940..cf30c6b 100644 --- a/ObsWebSocket.Core/Groups/ScenesRequestGroup.cs +++ b/ObsWebSocket.Core/Groups/ScenesGroup.cs @@ -17,7 +17,7 @@ namespace ObsWebSocket.Core; /// /// Conveniences for the Scenes category, alongside its generated requests. /// -public readonly partial struct ScenesRequestGroup +public readonly partial struct ScenesGroup { /// /// Switches the active Program or Preview scene, optionally setting a specific transition and duration beforehand. diff --git a/ObsWebSocket.Core/Groups/SourcesRequestGroup.cs b/ObsWebSocket.Core/Groups/SourcesGroup.cs similarity index 99% rename from ObsWebSocket.Core/Groups/SourcesRequestGroup.cs rename to ObsWebSocket.Core/Groups/SourcesGroup.cs index 5c3fd87..efeb651 100644 --- a/ObsWebSocket.Core/Groups/SourcesRequestGroup.cs +++ b/ObsWebSocket.Core/Groups/SourcesGroup.cs @@ -17,7 +17,7 @@ namespace ObsWebSocket.Core; /// /// Conveniences for the Sources category, alongside its generated requests. /// -public readonly partial struct SourcesRequestGroup +public readonly partial struct SourcesGroup { /// /// Checks if an input or scene source with the given name exists in OBS. diff --git a/ObsWebSocket.Core/Groups/StreamRequestGroup.cs b/ObsWebSocket.Core/Groups/StreamGroup.cs similarity index 98% rename from ObsWebSocket.Core/Groups/StreamRequestGroup.cs rename to ObsWebSocket.Core/Groups/StreamGroup.cs index 435b82c..d28ee08 100644 --- a/ObsWebSocket.Core/Groups/StreamRequestGroup.cs +++ b/ObsWebSocket.Core/Groups/StreamGroup.cs @@ -17,7 +17,7 @@ namespace ObsWebSocket.Core; /// /// Conveniences for the Stream category, alongside its generated requests. /// -public readonly partial struct StreamRequestGroup +public readonly partial struct StreamGroup { private static readonly TimeSpan s_defaultOutputTimeout = TimeSpan.FromSeconds(10); diff --git a/ObsWebSocket.Core/Groups/TransitionsRequestGroup.cs b/ObsWebSocket.Core/Groups/TransitionsGroup.cs similarity index 99% rename from ObsWebSocket.Core/Groups/TransitionsRequestGroup.cs rename to ObsWebSocket.Core/Groups/TransitionsGroup.cs index 0338422..e426ff5 100644 --- a/ObsWebSocket.Core/Groups/TransitionsRequestGroup.cs +++ b/ObsWebSocket.Core/Groups/TransitionsGroup.cs @@ -17,7 +17,7 @@ namespace ObsWebSocket.Core; /// /// Conveniences for the Transitions category, alongside its generated requests. /// -public readonly partial struct TransitionsRequestGroup +public readonly partial struct TransitionsGroup { /// /// Gets the settings for the current scene transition as a strongly-typed object. diff --git a/ObsWebSocket.Example/Worker.cs b/ObsWebSocket.Example/Worker.cs index 1ad124c..61df870 100644 --- a/ObsWebSocket.Example/Worker.cs +++ b/ObsWebSocket.Example/Worker.cs @@ -534,7 +534,7 @@ await _obsClient.Filters.SetSourceFilterEnabledAsync( try { await foreach ( - CurrentProgramSceneChangedEventArgs sceneEvent in _obsClient.CurrentProgramSceneChangedStream( + CurrentProgramSceneChangedEventArgs sceneEvent in _obsClient.Scenes.CurrentProgramSceneChangedStream( cancellationToken: watchCts.Token ) ) @@ -1279,7 +1279,7 @@ state is not null try { await foreach (CurrentProgramSceneChangedEventArgs sceneEvent - in client.CurrentProgramSceneChangedStream(cancellationToken: streamCts.Token) + in client.Scenes.CurrentProgramSceneChangedStream(cancellationToken: streamCts.Token) .ConfigureAwait(false)) { observed.Add(sceneEvent.EventData.SceneName ?? string.Empty); @@ -1470,7 +1470,7 @@ in client.CurrentProgramSceneChangedStream(cancellationToken: streamCts.Token) cts.CancelAfter(TimeSpan.FromSeconds(10)); IAsyncEnumerator enumerator = client - .SceneItemEnableStateChangedStream(capacity: 2, cancellationToken: cts.Token) + .SceneItems.SceneItemEnableStateChangedStream(capacity: 2, cancellationToken: cts.Token) .GetAsyncEnumerator(cts.Token); try diff --git a/ObsWebSocket.Tests/ReadmeCompileCheck.cs b/ObsWebSocket.Tests/ReadmeCompileCheck.cs index 0a3c292..5946190 100644 --- a/ObsWebSocket.Tests/ReadmeCompileCheck.cs +++ b/ObsWebSocket.Tests/ReadmeCompileCheck.cs @@ -127,7 +127,7 @@ internal static async Task UtilitiesExtendedAsync(ObsWebSocketClient client, Can internal static async Task EventStreamsAsync(ObsWebSocketClient client, CancellationToken ct) { - await foreach (var e in client.CurrentProgramSceneChangedStream(cancellationToken: ct)) + await foreach (var e in client.Scenes.CurrentProgramSceneChangedStream(cancellationToken: ct)) { _ = e.EventData.SceneName; break; diff --git a/ObsWebSocket.Tests/ScreenshotDecodeTests.cs b/ObsWebSocket.Tests/ScreenshotDecodeTests.cs index 7511e1e..7e35e37 100644 --- a/ObsWebSocket.Tests/ScreenshotDecodeTests.cs +++ b/ObsWebSocket.Tests/ScreenshotDecodeTests.cs @@ -18,7 +18,7 @@ public void DecodeImageData_WithDataUriPrefix_ReturnsTheImageBytes() { string dataUri = "data:image/png;base64," + Convert.ToBase64String(PngSignature); - byte[] decoded = SourcesRequestGroup.DecodeImageData(dataUri); + byte[] decoded = SourcesGroup.DecodeImageData(dataUri); CollectionAssert.AreEqual(PngSignature, decoded); } @@ -26,7 +26,7 @@ public void DecodeImageData_WithDataUriPrefix_ReturnsTheImageBytes() [TestMethod] public void DecodeImageData_WithBareBase64_StillDecodes() { - byte[] decoded = SourcesRequestGroup.DecodeImageData( + byte[] decoded = SourcesGroup.DecodeImageData( Convert.ToBase64String(PngSignature) ); @@ -35,5 +35,5 @@ public void DecodeImageData_WithBareBase64_StillDecodes() [TestMethod] public void DecodeImageData_WhenEmpty_Throws() => - Assert.ThrowsExactly(() => SourcesRequestGroup.DecodeImageData("")); + Assert.ThrowsExactly(() => SourcesGroup.DecodeImageData("")); } diff --git a/README.md b/README.md index 682ce08..15c4516 100644 --- a/README.md +++ b/README.md @@ -59,7 +59,7 @@ public sealed class Worker(ObsWebSocketClient client) : BackgroundService var version = await client.General.GetVersionAsync(ct); Console.WriteLine($"Connected to OBS {version.ObsVersion}"); - await foreach (var e in client.CurrentProgramSceneChangedStream(cancellationToken: ct)) + await foreach (var e in client.Scenes.CurrentProgramSceneChangedStream(cancellationToken: ct)) { Console.WriteLine($"Scene changed: {e.EventData.SceneName}"); } @@ -69,12 +69,14 @@ public sealed class Worker(ObsWebSocketClient client) : BackgroundService ## Everything is grouped by category -The client mirrors the categories the OBS protocol defines, and the conveniences this library adds -sit in the same group as the requests they wrap, so there is one way to reach anything: +The client mirrors the categories the OBS protocol defines. Requests, event streams and the +conveniences this library adds all sit in the group their category owns, so there is one way to +reach anything: ```csharp await client.Scenes.GetSceneListAsync(new(), ct); // generated request await client.Scenes.SwitchProgramSceneAndWaitAsync("Intro", cancellationToken: ct); // convenience +await client.Scenes.CurrentProgramSceneChangedStream(cancellationToken: ct); // event stream await client.Inputs.SetInputVolumeDbAsync("Mic", -6, ct); await client.SceneItems.SetSceneItemEnabledAsync("Intro", "Logo", false, ct); ``` @@ -88,11 +90,11 @@ category. ## Observing events -Every OBS event is exposed as an async sequence. The stream subscribes for the lifetime of the loop -and unsubscribes when it ends, so there is no handler bookkeeping: +Every OBS event is exposed as an async sequence on its category group. The stream subscribes for the +lifetime of the loop and unsubscribes when it ends, so there is no handler bookkeeping: ```csharp -await foreach (var e in client.CurrentProgramSceneChangedStream(cancellationToken: ct)) +await foreach (var e in client.Scenes.CurrentProgramSceneChangedStream(cancellationToken: ct)) { Console.WriteLine($"Program scene is now {e.EventData.SceneName}"); } From 033606518b5c7da205b51642dbec4cc1a15df777 Mon Sep 17 00:00:00 2001 From: Agash Date: Wed, 26 Aug 2026 07:28:22 +0200 Subject: [PATCH 05/12] refactor(core): fold the client level helpers into one type 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. --- .../ObsWebSocketClient.Helper.Convenience.cs | 86 ++------- .../ObsWebSocketClient.Helper.cs | 177 +++--------------- ObsWebSocket.Example/Worker.cs | 21 +++ 3 files changed, 58 insertions(+), 226 deletions(-) diff --git a/ObsWebSocket.Core/ObsWebSocketClient.Helper.Convenience.cs b/ObsWebSocket.Core/ObsWebSocketClient.Helper.Convenience.cs index e9518d8..6b993f1 100644 --- a/ObsWebSocket.Core/ObsWebSocketClient.Helper.Convenience.cs +++ b/ObsWebSocket.Core/ObsWebSocketClient.Helper.Convenience.cs @@ -1,66 +1,17 @@ using ObsWebSocket.Core.Events; -using ObsWebSocket.Core.Events.Generated; -using ObsWebSocket.Core.Protocol.Generated; using ObsWebSocket.Core.Protocol; -using ObsWebSocket.Core.Protocol.Requests; -using ObsWebSocket.Core.Protocol.Responses; +using ObsWebSocket.Core.Protocol.Generated; namespace ObsWebSocket.Core; /// -/// Output control, existence checks, volume, media transport, and event-wait conveniences. +/// The client level conveniences: waiting for an event, and sending a batch. Neither belongs to +/// one protocol category, so both stay on the client rather than on a category group. /// -public static class ObsWebSocketClientConvenienceExtensions +public static partial class ObsWebSocketClientHelpers { - private static readonly TimeSpan s_defaultOutputTimeout = TimeSpan.FromSeconds(10); - extension(ObsWebSocketClient client) { - - // ──────────────────────────────────────────────────────────────────────── - // Output control - // ──────────────────────────────────────────────────────────────────────── - - - - - - - - - - // ──────────────────────────────────────────────────────────────────────── - // Existence checks - // ──────────────────────────────────────────────────────────────────────── - - - - // ──────────────────────────────────────────────────────────────────────── - // Volume - // ──────────────────────────────────────────────────────────────────────── - - - - - - // ──────────────────────────────────────────────────────────────────────── - // Media transport - // ──────────────────────────────────────────────────────────────────────── - - - - - - - - - - - - // ──────────────────────────────────────────────────────────────────────── - // WaitForEventAsync overloads - // ──────────────────────────────────────────────────────────────────────── - /// /// Waits for the next occurrence of a typed OBS event, with no timeout. The wait ends only /// when the event arrives or fires. @@ -85,7 +36,7 @@ public Task WaitForEventAsync( /// How long to wait before giving up. /// A token to cancel the wait. /// The event args. - /// Thrown if the timeout elapses first. + /// Thrown if the timeout elapses first. public Task WaitForEventAsync( TimeSpan timeout, CancellationToken cancellationToken = default @@ -107,10 +58,6 @@ public Task WaitForEventAsync( where TEventArgs : ObsEventArgs => client.WaitForEventAsync(predicate, Timeout.InfiniteTimeSpan, cancellationToken); - // ──────────────────────────────────────────────────────────────────────── - // Typed batch execution - // ──────────────────────────────────────────────────────────────────────── - /// /// Sends a batch built with , returning results addressable /// by the references the builder handed out. @@ -121,6 +68,7 @@ public Task WaitForEventAsync( /// Optional override for the request timeout. /// A token to cancel the operation. /// The results, addressable by reference or by position. + /// Thrown if the client is not connected. public async Task CallBatchAsync( ObsBatchBuilder batch, RequestBatchExecutionType? executionType = null, @@ -149,17 +97,17 @@ public async Task CallBatchAsync( } /// - /// Builds and sends a batch using the typed builder, so each request type is paired with - /// its own data record instead of a loose string and object. + /// Builds and sends a batch in one call, for the common case where the references are + /// captured in the same scope they are read in. /// /// Adds the requests to send. /// How OBS should schedule the requests. /// Whether OBS should stop at the first failing request. /// Optional override for the request timeout. /// A token to cancel the operation. - /// One result per request, in order. + /// The results, addressable by reference or by position. /// Thrown if the client is not connected. - public Task>> CallBatchAsync( + public Task CallBatchAsync( Action build, RequestBatchExecutionType? executionType = null, bool? haltOnFailure = null, @@ -172,24 +120,12 @@ public Task>> CallBatchAsync( ObsBatchBuilder builder = new(); build(builder); return client.CallBatchAsync( - builder.Build(), + builder, executionType, haltOnFailure, timeoutMs, cancellationToken ); } - - // Scene item ids are Number on the wire, so the generated surface uses double. - - - - - - - - - - } } diff --git a/ObsWebSocket.Core/ObsWebSocketClient.Helper.cs b/ObsWebSocket.Core/ObsWebSocketClient.Helper.cs index eb67fb2..df5762b 100644 --- a/ObsWebSocket.Core/ObsWebSocketClient.Helper.cs +++ b/ObsWebSocket.Core/ObsWebSocketClient.Helper.cs @@ -1,36 +1,37 @@ using System.Text.Json; using System.Text.Json.Serialization.Metadata; -using Microsoft.Extensions.Logging; -using ObsWebSocket.Core.Events; -using ObsWebSocket.Core.Events.Generated; -using ObsWebSocket.Core.Protocol; -using ObsWebSocket.Core.Protocol.Common.InputSettings; -using ObsWebSocket.Core.Protocol.Generated; // Assuming generated enums are here -using ObsWebSocket.Core.Protocol.Requests; -using ObsWebSocket.Core.Protocol.Responses; +using ObsWebSocket.Core.Serialization; -namespace ObsWebSocket.Core; // Or ObsWebSocket.Core.Extensions +namespace ObsWebSocket.Core; /// -/// Provides helpful extension methods for common OBS WebSocket tasks. +/// Client level extensions that do not belong to one protocol category, alongside the generated +/// WaitForEventAsync. Everything category scoped lives on the category group instead, so +/// client.Scenes, client.Inputs and the rest are where those methods are found. /// public static partial class ObsWebSocketClientHelpers { private static readonly JsonSerializerOptions s_helperJsonOptions = CreateHelperOptions(); - private static JsonSerializerOptions CreateHelperOptions() - { - JsonSerializerOptions options = new(ObsWebSocket.Core.Serialization.ObsWebSocketJsonContext.Default.Options) + private static JsonSerializerOptions CreateHelperOptions() => + new(ObsWebSocketJsonContext.Default.Options) { - TypeInfoResolver = System.Text.Json.Serialization.Metadata.JsonTypeInfoResolver.Combine( - ObsWebSocket.Core.Serialization.ObsWebSocketJsonContext.Default, - ObsWebSocket.Core.Serialization.ObsWebSocketSettingsJsonContext.Default + TypeInfoResolver = JsonTypeInfoResolver.Combine( + ObsWebSocketJsonContext.Default, + ObsWebSocketSettingsJsonContext.Default ), }; - return options; - } - internal static JsonTypeInfo GetRegisteredTypeInfo() where T : class + /// + /// Resolves the source generated metadata for a settings type the library knows about. + /// + /// The settings type to resolve. + /// + /// Thrown when the type is not registered in either generated context, since serializing it + /// would otherwise fall back to reflection and break under trimming. + /// + internal static JsonTypeInfo GetRegisteredTypeInfo() + where T : class { JsonTypeInfo? typeInfo; try @@ -39,139 +40,13 @@ internal static JsonTypeInfo GetRegisteredTypeInfo() where T : class } catch (Exception ex) when (ex is InvalidOperationException or NotSupportedException) { - throw new ObsWebSocketException( - $"Type '{typeof(T).Name}' is not registered in ObsWebSocketJsonContext. Pass an explicit JsonTypeInfo or use a library-registered settings type.", - ex - ); + throw new ObsWebSocketException(NotRegistered(), ex); } - return typeInfo ?? throw new ObsWebSocketException( - $"Type '{typeof(T).Name}' is not registered in ObsWebSocketJsonContext. Pass an explicit JsonTypeInfo or use a library-registered settings type." - ); - } - - - - - - - - - - // Helper #4 (SwitchSceneAndWaitAsync) - Deferred due to complexity without reflection - - - - - - - - - - - - - - - - - - - - // ──────────────────────────────────────────────────────────────────────── - // Generic input settings helpers - // ──────────────────────────────────────────────────────────────────────── - - - - - - - - - - - - - - - - - - // ------------------------------------------------------------------------- - // Transition Settings helpers - // ------------------------------------------------------------------------- - - - - - - - - - - // ------------------------------------------------------------------------- - // Output Settings helpers - // ------------------------------------------------------------------------- - - - - - - - - - - // ------------------------------------------------------------------------- - // Stream Service Settings helpers - // ------------------------------------------------------------------------- - - - - - - - - - - // ------------------------------------------------------------------------- - // Default Settings helpers (read-only) - // ------------------------------------------------------------------------- - - - - - - - - - - - - - - - - - - // Helper #14 (WaitForEventAsync) - Deferred due to complexity/reflection constraints. - - // ──────────────────────────────────────────────────────────────────────── - // Virtualcam helpers - // ──────────────────────────────────────────────────────────────────────── - - - - - - // ──────────────────────────────────────────────────────────────────────── - // Canvas-aware screenshot helpers - // ──────────────────────────────────────────────────────────────────────── - - - - + return typeInfo ?? throw new ObsWebSocketException(NotRegistered()); + } + private static string NotRegistered() => + $"Type '{typeof(T).Name}' is not registered in ObsWebSocketJsonContext. " + + "Pass an explicit JsonTypeInfo or use a library-registered settings type."; } - - diff --git a/ObsWebSocket.Example/Worker.cs b/ObsWebSocket.Example/Worker.cs index 61df870..7bef2cf 100644 --- a/ObsWebSocket.Example/Worker.cs +++ b/ObsWebSocket.Example/Worker.cs @@ -1715,6 +1715,27 @@ await client return (status is not null, $"state={status?.MediaState}"); }).ConfigureAwait(false)); + results.Add(await TrySettingsCheckAsync("Virtual camera toggle", async () => + { + bool before = await client.Outputs.IsVirtualCamActiveAsync(cancellationToken).ConfigureAwait(false); + + bool? turnedOn = await client + .Outputs.SetVirtualCamActiveAndWaitAsync(!before, cancellationToken: cancellationToken) + .ConfigureAwait(false); + bool observed = await client.Outputs.IsVirtualCamActiveAsync(cancellationToken).ConfigureAwait(false); + + // Put it back the way it was found. + _ = await client + .Outputs.SetVirtualCamActiveAndWaitAsync(before, cancellationToken: cancellationToken) + .ConfigureAwait(false); + bool restored = await client.Outputs.IsVirtualCamActiveAsync(cancellationToken).ConfigureAwait(false); + + return ( + turnedOn == !before && observed == !before && restored == before, + $"{before} -> {observed} -> {restored}" + ); + }).ConfigureAwait(false)); + results.Add(await TrySettingsCheckAsync("Typed exception on a rejected request", async () => { try From 3ccdad446e3da5499585541f5a0f55f35a73ed00 Mon Sep 17 00:00:00 2001 From: Agash Date: Wed, 26 Aug 2026 07:29:16 +0200 Subject: [PATCH 06/12] style: format the repository with csharpier 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. --- .github/workflows/build.yml | 5 + .../GenerateObsWebSocketSourcesTask.cs | 3 +- .../Generation/Emitter.DtoGeneration.cs | 2 +- .../Generation/Emitter.EventStreams.cs | 11 +- .../Generation/Emitter.Helpers.cs | 2 +- .../Generation/Emitter.Hierarchy.cs | 2 +- .../Generation/Emitter.JsonContext.cs | 28 +- .../Generation/Emitter.MsgPackResolver.cs | 27 +- .../Generation/Emitter.cs | 70 +- .../Generation/GenerationContext.cs | 33 +- .../Generation/ProtocolCodeGenerator.cs | 18 +- .../ObsWebSocket.Codegen.Tasks.csproj | 2 - .../ProtocolCodegenRunner.cs | 69 +- .../AuthenticationFailureException.cs | 8 +- ObsWebSocket.Core/BatchRef.cs | 3 +- ObsWebSocket.Core/BatchResultExtensions.cs | 3 +- .../ConnectionAttemptFailedException.cs | 8 +- ObsWebSocket.Core/Groups/ConfigGroup.cs | 40 +- ObsWebSocket.Core/Groups/FiltersGroup.cs | 72 +- ObsWebSocket.Core/Groups/GeneralGroup.cs | 7 +- ObsWebSocket.Core/Groups/InputsGroup.cs | 166 +- ObsWebSocket.Core/Groups/MediaInputsGroup.cs | 111 +- ObsWebSocket.Core/Groups/OutputsGroup.cs | 49 +- ObsWebSocket.Core/Groups/RecordGroup.cs | 103 +- ObsWebSocket.Core/Groups/SceneItemsGroup.cs | 106 +- ObsWebSocket.Core/Groups/ScenesGroup.cs | 141 +- ObsWebSocket.Core/Groups/SourcesGroup.cs | 76 +- ObsWebSocket.Core/Groups/StreamGroup.cs | 101 +- ObsWebSocket.Core/Groups/TransitionsGroup.cs | 35 +- ObsWebSocket.Core/ObsBatchBuilder.cs | 1 - ObsWebSocket.Core/ObsWebSocket.Core.csproj | 84 +- ObsWebSocket.Core/ObsWebSocketClient.cs | 116 +- ObsWebSocket.Core/ObsWebSocketClientLog.cs | 1029 ++++++-- .../ObsWebSocketClientOptionsValidator.cs | 4 +- ObsWebSocket.Core/ObsWebSocketHosting.cs | 6 +- ObsWebSocket.Core/ObsWebSocketMetrics.cs | 8 +- ...ObsWebSocketServiceCollectionExtensions.cs | 12 +- .../FilterSettings/CommonFilterSettings.cs | 7 +- .../InputSettings/CommonInputSettings.cs | 12 + .../CommonStreamServiceSettings.cs | 3 +- .../Protocol/Common/StubTypes.cs | 7 - ObsWebSocket.Core/Protocol/Messages.cs | 3 +- ObsWebSocket.Core/ReconnectDelays.cs | 4 +- .../Serialization/JsonMessageSerializer.cs | 111 +- .../MsgPackJsonElementResolver.cs | 17 +- .../Serialization/MsgPackMessageSerializer.cs | 21 +- .../MsgPackStubExtensionDataResolver.cs | 57 +- .../ObsWebSocketJsonContext.Settings.cs | 3 +- .../Serialization/SerializerLog.cs | 195 +- .../ObsWebSocket.Example.csproj | 3 +- ObsWebSocket.Example/Program.cs | 7 +- ObsWebSocket.Example/Worker.cs | 2133 +++++++++++------ .../BatchBuilderAndEnumTests.cs | 17 +- ObsWebSocket.Tests/BatchResultTests.cs | 25 +- ObsWebSocket.Tests/EventStreamTests.cs | 42 +- ObsWebSocket.Tests/FalsyRequestFieldTests.cs | 10 +- ObsWebSocket.Tests/HostingTests.cs | 8 +- .../JsonMessageSerializerTests.cs | 33 +- ObsWebSocket.Tests/ObsWebSocket.Tests.csproj | 11 +- .../ObsWebSocketClientConnectionTests.cs | 23 +- .../ObsWebSocketClientIntegrationTests.cs | 38 +- .../ObsWebSocketClientRequestTests.cs | 9 +- ObsWebSocket.Tests/ObsWebSocketClientTests.cs | 21 +- ObsWebSocket.Tests/ObsWebSocketDiTests.cs | 1 - ObsWebSocket.Tests/ReadmeCompileCheck.cs | 97 +- ObsWebSocket.Tests/ScreenshotDecodeTests.cs | 7 +- ObsWebSocket.Tests/SerializerBehaviorTests.cs | 115 +- ObsWebSocket.Tests/TestUtils.cs | 7 +- ObsWebSocket.Tests/TimeProviderTests.cs | 13 +- ObsWebSocket.Tests/TypedSettingsTests.cs | 1045 +++++--- 70 files changed, 4527 insertions(+), 2139 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 35212ea..2b17001 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -52,6 +52,11 @@ jobs: - name: Restore dependencies run: dotnet restore ObsWebSocket.sln + - name: Check formatting + run: | + dotnet tool restore + dotnet csharpier check . + - name: Build Solution run: dotnet build ObsWebSocket.sln --configuration Release --no-restore diff --git a/ObsWebSocket.Codegen.Tasks/GenerateObsWebSocketSourcesTask.cs b/ObsWebSocket.Codegen.Tasks/GenerateObsWebSocketSourcesTask.cs index 64861e1..54f15d0 100644 --- a/ObsWebSocket.Codegen.Tasks/GenerateObsWebSocketSourcesTask.cs +++ b/ObsWebSocket.Codegen.Tasks/GenerateObsWebSocketSourcesTask.cs @@ -14,7 +14,8 @@ public sealed class GenerateObsWebSocketSourcesTask : Microsoft.Build.Utilities. public override bool Execute() { - int exitCode = ProtocolCodegenRunner.GenerateAsync( + int exitCode = ProtocolCodegenRunner + .GenerateAsync( protocolPath: ProtocolPath, outputDirectory: OutputDirectory, downloadIfMissing: DownloadIfMissing, diff --git a/ObsWebSocket.Codegen.Tasks/Generation/Emitter.DtoGeneration.cs b/ObsWebSocket.Codegen.Tasks/Generation/Emitter.DtoGeneration.cs index 03adcbd..3ace87f 100644 --- a/ObsWebSocket.Codegen.Tasks/Generation/Emitter.DtoGeneration.cs +++ b/ObsWebSocket.Codegen.Tasks/Generation/Emitter.DtoGeneration.cs @@ -729,6 +729,7 @@ out bool isRootOfNested || f.ValueName.EndsWith("." + objectNode.Name) ); } + /// /// Reports whether a field's description says it can be null, which the protocol states in /// prose for fields it does not otherwise mark optional. @@ -736,5 +737,4 @@ out bool isRootOfNested private static bool DescriptionAllowsNull(string? description) => !string.IsNullOrEmpty(description) && description.IndexOf("null", StringComparison.OrdinalIgnoreCase) >= 0; - } diff --git a/ObsWebSocket.Codegen.Tasks/Generation/Emitter.EventStreams.cs b/ObsWebSocket.Codegen.Tasks/Generation/Emitter.EventStreams.cs index 1e92b3f..b48f3a6 100644 --- a/ObsWebSocket.Codegen.Tasks/Generation/Emitter.EventStreams.cs +++ b/ObsWebSocket.Codegen.Tasks/Generation/Emitter.EventStreams.cs @@ -60,9 +60,7 @@ IGrouping group in protocol builder.AppendLine( "/// Each accessor subscribes for the lifetime of the enumeration and unsubscribes" ); - builder.AppendLine( - "/// when it ends, so the caller never manages handlers." - ); + builder.AppendLine("/// when it ends, so the caller never manages handlers."); builder.AppendLine("/// "); // A category with events but no requests has no group declared elsewhere, so this @@ -93,10 +91,13 @@ IGrouping group in protocol continue; } - string eventArgsTypeName = $"{GeneratedEventArgsNamespace}.{eventName}EventArgs"; + string eventArgsTypeName = + $"{GeneratedEventArgsNamespace}.{eventName}EventArgs"; builder.AppendLine(" /// "); - builder.AppendLine($" /// Streams {eventName} events as they arrive."); + builder.AppendLine( + $" /// Streams {eventName} events as they arrive." + ); if (!string.IsNullOrWhiteSpace(eventDef.Description)) { builder.AppendLine( diff --git a/ObsWebSocket.Codegen.Tasks/Generation/Emitter.Helpers.cs b/ObsWebSocket.Codegen.Tasks/Generation/Emitter.Helpers.cs index d6f643b..6e7ccdc 100644 --- a/ObsWebSocket.Codegen.Tasks/Generation/Emitter.Helpers.cs +++ b/ObsWebSocket.Codegen.Tasks/Generation/Emitter.Helpers.cs @@ -238,7 +238,7 @@ string parentDtoName // Map specifically named 'Object' field to Stub record // Use the fully qualified name to avoid potential namespace conflicts return ($"{GeneratedCommonNamespace}.SceneItemTransformStub?", false); - // Add other specific 'Object' mappings here if needed in the future + // Add other specific 'Object' mappings here if needed in the future } // If not handled above, it falls through to the general 'Object'/'Any' handling below } diff --git a/ObsWebSocket.Codegen.Tasks/Generation/Emitter.Hierarchy.cs b/ObsWebSocket.Codegen.Tasks/Generation/Emitter.Hierarchy.cs index df97d58..36d75ca 100644 --- a/ObsWebSocket.Codegen.Tasks/Generation/Emitter.Hierarchy.cs +++ b/ObsWebSocket.Codegen.Tasks/Generation/Emitter.Hierarchy.cs @@ -88,7 +88,7 @@ SourceProductionContext context currentNode = newNode; } } - NextFieldPass1: + NextFieldPass1: ; } diff --git a/ObsWebSocket.Codegen.Tasks/Generation/Emitter.JsonContext.cs b/ObsWebSocket.Codegen.Tasks/Generation/Emitter.JsonContext.cs index f91863e..aea9103 100644 --- a/ObsWebSocket.Codegen.Tasks/Generation/Emitter.JsonContext.cs +++ b/ObsWebSocket.Codegen.Tasks/Generation/Emitter.JsonContext.cs @@ -19,7 +19,9 @@ ProtocolDefinition protocol { try { - StringBuilder builder = BuildSourceHeader("// Serialization Context: ObsWebSocketJsonContext"); + StringBuilder builder = BuildSourceHeader( + "// Serialization Context: ObsWebSocketJsonContext" + ); _ = builder.AppendLine("using System.Collections.Generic;"); _ = builder.AppendLine("using System.Text.Json;"); @@ -34,16 +36,26 @@ ProtocolDefinition protocol _ = builder.AppendLine(); _ = builder.AppendLine("[JsonSourceGenerationOptions("); _ = builder.AppendLine(" PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase,"); - _ = builder.AppendLine(" DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull)]"); + _ = builder.AppendLine( + " DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull)]" + ); // Fixed protocol wrapper and payload types. _ = builder.AppendLine("[JsonSerializable(typeof(OutgoingMessage))]"); _ = builder.AppendLine("[JsonSerializable(typeof(OutgoingMessage))]"); - _ = builder.AppendLine("[JsonSerializable(typeof(OutgoingMessage))]"); - _ = builder.AppendLine("[JsonSerializable(typeof(OutgoingMessage))]"); + _ = builder.AppendLine( + "[JsonSerializable(typeof(OutgoingMessage))]" + ); + _ = builder.AppendLine( + "[JsonSerializable(typeof(OutgoingMessage))]" + ); _ = builder.AppendLine("[JsonSerializable(typeof(IncomingMessage))]"); - _ = builder.AppendLine("[JsonSerializable(typeof(RequestResponsePayload))]"); - _ = builder.AppendLine("[JsonSerializable(typeof(RequestBatchResponsePayload))]"); + _ = builder.AppendLine( + "[JsonSerializable(typeof(RequestResponsePayload))]" + ); + _ = builder.AppendLine( + "[JsonSerializable(typeof(RequestBatchResponsePayload))]" + ); _ = builder.AppendLine("[JsonSerializable(typeof(EventPayloadBase))]"); _ = builder.AppendLine("[JsonSerializable(typeof(HelloPayload))]"); _ = builder.AppendLine("[JsonSerializable(typeof(IdentifiedPayload))]"); @@ -111,7 +123,9 @@ ProtocolDefinition protocol ); } - _ = builder.AppendLine("internal sealed partial class ObsWebSocketJsonContext : JsonSerializerContext"); + _ = builder.AppendLine( + "internal sealed partial class ObsWebSocketJsonContext : JsonSerializerContext" + ); _ = builder.AppendLine("{"); _ = builder.AppendLine("}"); diff --git a/ObsWebSocket.Codegen.Tasks/Generation/Emitter.MsgPackResolver.cs b/ObsWebSocket.Codegen.Tasks/Generation/Emitter.MsgPackResolver.cs index 2d1d188..b488f8e 100644 --- a/ObsWebSocket.Codegen.Tasks/Generation/Emitter.MsgPackResolver.cs +++ b/ObsWebSocket.Codegen.Tasks/Generation/Emitter.MsgPackResolver.cs @@ -105,12 +105,18 @@ void AddFixedType(string typeName) context.AddSource( "ObsWebSocketMsgPackResolver.RequestTypes.g.cs", - SourceText.From(BuildKnownTypeSource("IsRequestType", requestTypeNames), Encoding.UTF8) + SourceText.From( + BuildKnownTypeSource("IsRequestType", requestTypeNames), + Encoding.UTF8 + ) ); context.AddSource( "ObsWebSocketMsgPackResolver.ResponseTypes.g.cs", - SourceText.From(BuildKnownTypeSource("IsResponseType", responseTypeNames), Encoding.UTF8) + SourceText.From( + BuildKnownTypeSource("IsResponseType", responseTypeNames), + Encoding.UTF8 + ) ); context.AddSource( @@ -120,7 +126,10 @@ void AddFixedType(string typeName) context.AddSource( "ObsWebSocketMsgPackResolver.NestedTypes.g.cs", - SourceText.From(BuildKnownTypeSource("IsNestedType", nestedTypeNames), Encoding.UTF8) + SourceText.From( + BuildKnownTypeSource("IsNestedType", nestedTypeNames), + Encoding.UTF8 + ) ); } catch (Exception ex) @@ -139,7 +148,9 @@ void AddFixedType(string typeName) private static string BuildResolverRootSource() { - StringBuilder builder = BuildSourceHeader("// Serialization Resolver: ObsWebSocketMsgPackResolver"); + StringBuilder builder = BuildSourceHeader( + "// Serialization Resolver: ObsWebSocketMsgPackResolver" + ); builder.AppendLine("using System;"); builder.AppendLine("using MessagePack;"); builder.AppendLine("using MessagePack.Formatters;"); @@ -162,7 +173,9 @@ private static string BuildResolverRootSource() builder.AppendLine(" private ObsWebSocketMsgPackResolver() { }"); builder.AppendLine(); builder.AppendLine(" /// "); - builder.AppendLine(" /// Gets a formatter for when this resolver supports it."); + builder.AppendLine( + " /// Gets a formatter for when this resolver supports it." + ); builder.AppendLine(" /// "); builder.AppendLine( " public IMessagePackFormatter? GetFormatter() => ObsWebSocketMsgPackResolverCore.GetFormatter();" @@ -179,7 +192,9 @@ private static string BuildResolverRootSource() builder.AppendLine(" return null;"); builder.AppendLine(" }"); builder.AppendLine(); - builder.AppendLine(" return SourceGeneratedFormatterResolver.Instance.GetFormatter();"); + builder.AppendLine( + " return SourceGeneratedFormatterResolver.Instance.GetFormatter();" + ); builder.AppendLine(" }"); builder.AppendLine(); builder.AppendLine(" private static bool IsKnownType(Type type)"); diff --git a/ObsWebSocket.Codegen.Tasks/Generation/Emitter.cs b/ObsWebSocket.Codegen.Tasks/Generation/Emitter.cs index c6e99dc..3a61ae0 100644 --- a/ObsWebSocket.Codegen.Tasks/Generation/Emitter.cs +++ b/ObsWebSocket.Codegen.Tasks/Generation/Emitter.cs @@ -248,7 +248,11 @@ private static string FindCommonMemberPrefix(List identifiers) { string[] parts = identifier.Split('_'); int i = 0; - while (i < shared && i < parts.Length && string.Equals(parts[i], first[i], StringComparison.Ordinal)) + while ( + i < shared + && i < parts.Length + && string.Equals(parts[i], first[i], StringComparison.Ordinal) + ) { i++; } @@ -317,7 +321,9 @@ private static string SnakeToPascalCase(string upperSnake) => string prefix = FindCommonMemberPrefix([.. members.Select(m => m.Member)]); - StringBuilder builder = BuildSourceHeader("// Type: Typed Enum for a string-valued protocol enum"); + StringBuilder builder = BuildSourceHeader( + "// Type: Typed Enum for a string-valued protocol enum" + ); builder.AppendLine("using System;"); builder.AppendLine("using System.Text.Json.Serialization;"); builder.AppendLine(); @@ -328,7 +334,9 @@ private static string SnakeToPascalCase(string upperSnake) => $"Typed form of the {constantsClass} protocol enum. Use to obtain the string OBS expects.", 0 ); - builder.AppendLine("/// Generated from OBS WebSocket Protocol definition."); + builder.AppendLine( + "/// Generated from OBS WebSocket Protocol definition." + ); builder.AppendLine($"public enum {enumName}"); builder.AppendLine("{"); foreach ((string memberIdentifier, string wire) in members) @@ -340,7 +348,9 @@ private static string SnakeToPascalCase(string upperSnake) => } string memberName = SnakeToPascalCase(shortName); - builder.AppendLine($" /// Maps to {System.Security.SecurityElement.Escape(wire)}."); + builder.AppendLine( + $" /// Maps to {System.Security.SecurityElement.Escape(wire)}." + ); builder.AppendLine($" [JsonStringEnumMemberName(\"{wire}\")]"); builder.AppendLine($" {memberName},"); builder.AppendLine(); @@ -352,8 +362,12 @@ private static string SnakeToPascalCase(string upperSnake) => AppendXmlDocSummary(builder, $"Wire-value conversions for .", 0); builder.AppendLine($"public static class {enumName}Extensions"); builder.AppendLine("{"); - builder.AppendLine($" /// Returns the protocol string OBS expects for this value."); - builder.AppendLine($" public static string ToWireValue(this {enumName} value) => value switch"); + builder.AppendLine( + $" /// Returns the protocol string OBS expects for this value." + ); + builder.AppendLine( + $" public static string ToWireValue(this {enumName} value) => value switch" + ); builder.AppendLine(" {"); foreach ((string memberIdentifier, string wire) in members) { @@ -363,14 +377,22 @@ private static string SnakeToPascalCase(string upperSnake) => shortName = shortName.Substring(prefix.Length); } - builder.AppendLine($" {enumName}.{SnakeToPascalCase(shortName)} => {constantsClass}.{memberIdentifier},"); + builder.AppendLine( + $" {enumName}.{SnakeToPascalCase(shortName)} => {constantsClass}.{memberIdentifier}," + ); } - builder.AppendLine($" _ => throw new ArgumentOutOfRangeException(nameof(value), value, null),"); + builder.AppendLine( + $" _ => throw new ArgumentOutOfRangeException(nameof(value), value, null)," + ); builder.AppendLine(" };"); builder.AppendLine(); - builder.AppendLine($" /// Parses a protocol string into a , returning null when unrecognised."); - builder.AppendLine($" public static {enumName}? FromWireValue(string? value) => value switch"); + builder.AppendLine( + $" /// Parses a protocol string into a , returning null when unrecognised." + ); + builder.AppendLine( + $" public static {enumName}? FromWireValue(string? value) => value switch" + ); builder.AppendLine(" {"); foreach ((string memberIdentifier, string wire) in members) { @@ -380,7 +402,9 @@ private static string SnakeToPascalCase(string upperSnake) => shortName = shortName.Substring(prefix.Length); } - builder.AppendLine($" {constantsClass}.{memberIdentifier} => {enumName}.{SnakeToPascalCase(shortName)},"); + builder.AppendLine( + $" {constantsClass}.{memberIdentifier} => {enumName}.{SnakeToPascalCase(shortName)}," + ); } builder.AppendLine(" _ => null,"); @@ -712,7 +736,9 @@ IGrouping group in protocol $"/// Requests in the {System.Security.SecurityElement.Escape(group.Key)} category." ); builder.AppendLine("/// "); - builder.AppendLine("/// The client these requests are sent on."); + builder.AppendLine( + "/// The client these requests are sent on." + ); builder.AppendLine( $"public readonly partial struct {groupName}Group(ObsWebSocketClient client)" ); @@ -744,7 +770,9 @@ IGrouping group in protocol } builder.AppendLine("/// "); - builder.AppendLine("/// Exposes the request categories defined by the OBS WebSocket protocol."); + builder.AppendLine( + "/// Exposes the request categories defined by the OBS WebSocket protocol." + ); builder.AppendLine("/// "); builder.AppendLine("public static class ObsWebSocketClientExtensions"); builder.AppendLine("{"); @@ -757,9 +785,7 @@ IGrouping group in protocol $" /// Requests in the {System.Security.SecurityElement.Escape(category)} category." ); builder.AppendLine(" /// "); - builder.AppendLine( - $" public {groupName}Group {groupName} => new(client);" - ); + builder.AppendLine($" public {groupName}Group {groupName} => new(client);"); builder.AppendLine(" }"); builder.AppendLine(); } @@ -828,15 +854,11 @@ RequestDefinition reqDef { if (baseCallMethod == "CallAsyncValue") { - builder.Append( - $"Yields the response data." - ); + builder.Append($"Yields the response data."); } else // Assumed CallAsync (reference type) { - builder.Append( - $"Yields the response data." - ); + builder.Append($"Yields the response data."); } } else // No response data @@ -876,9 +898,7 @@ RequestDefinition reqDef ); } - builder.AppendLine( - $" public async {returnType} {methodName}({parameterList})" - ); + builder.AppendLine($" public async {returnType} {methodName}({parameterList})"); builder.AppendLine(" {"); // Method Body string callParams = hasRequestData ? requestParamName : "null"; diff --git a/ObsWebSocket.Codegen.Tasks/Generation/GenerationContext.cs b/ObsWebSocket.Codegen.Tasks/Generation/GenerationContext.cs index 5176b21..68e94d5 100644 --- a/ObsWebSocket.Codegen.Tasks/Generation/GenerationContext.cs +++ b/ObsWebSocket.Codegen.Tasks/Generation/GenerationContext.cs @@ -26,13 +26,19 @@ public void AddSource(string hintName, SourceText sourceText) private static string ResolveOutputPath(string hintName, string source) { string fileName = Path.GetFileName(hintName); - string namespaceLine = source - .Split('\n') - .Select(line => line.Trim()) - .FirstOrDefault(line => line.StartsWith("namespace ", StringComparison.Ordinal)) + string namespaceLine = + source + .Split('\n') + .Select(line => line.Trim()) + .FirstOrDefault(line => line.StartsWith("namespace ", StringComparison.Ordinal)) ?? string.Empty; - if (namespaceLine.Contains("ObsWebSocket.Core.Protocol.Common.NestedTypes", StringComparison.Ordinal)) + if ( + namespaceLine.Contains( + "ObsWebSocket.Core.Protocol.Common.NestedTypes", + StringComparison.Ordinal + ) + ) { return Path.Combine("Protocol", "Common", "NestedTypes", fileName); } @@ -42,7 +48,9 @@ private static string ResolveOutputPath(string hintName, string source) return Path.Combine("Protocol", "Requests", fileName); } - if (namespaceLine.Contains("ObsWebSocket.Core.Protocol.Responses", StringComparison.Ordinal)) + if ( + namespaceLine.Contains("ObsWebSocket.Core.Protocol.Responses", StringComparison.Ordinal) + ) { return Path.Combine("Protocol", "Responses", fileName); } @@ -52,15 +60,20 @@ private static string ResolveOutputPath(string hintName, string source) return Path.Combine("Protocol", "Events", fileName); } - if (namespaceLine.Contains("ObsWebSocket.Core.Protocol.Generated", StringComparison.Ordinal)) + if ( + namespaceLine.Contains("ObsWebSocket.Core.Protocol.Generated", StringComparison.Ordinal) + ) { return Path.Combine("Protocol", "Generated", fileName); } - return namespaceLine.Contains("ObsWebSocket.Core.Events.Generated", StringComparison.Ordinal) - ? Path.Combine("Events", "Generated", fileName) + return namespaceLine.Contains( + "ObsWebSocket.Core.Events.Generated", + StringComparison.Ordinal + ) + ? Path.Combine("Events", "Generated", fileName) : namespaceLine.Contains("ObsWebSocket.Core.Serialization", StringComparison.Ordinal) - ? Path.Combine("Serialization", fileName) + ? Path.Combine("Serialization", fileName) : Path.Combine("Client", fileName); } } diff --git a/ObsWebSocket.Codegen.Tasks/Generation/ProtocolCodeGenerator.cs b/ObsWebSocket.Codegen.Tasks/Generation/ProtocolCodeGenerator.cs index c2d09b2..65192a7 100644 --- a/ObsWebSocket.Codegen.Tasks/Generation/ProtocolCodeGenerator.cs +++ b/ObsWebSocket.Codegen.Tasks/Generation/ProtocolCodeGenerator.cs @@ -12,15 +12,27 @@ internal static class ProtocolCodeGenerator NumberHandling = JsonNumberHandling.AllowReadingFromString, }; - public static (IReadOnlyDictionary Sources, IReadOnlyList Diagnostics) Generate(string protocolJson) + public static ( + IReadOnlyDictionary Sources, + IReadOnlyList Diagnostics + ) Generate(string protocolJson) { ArgumentException.ThrowIfNullOrEmpty(protocolJson); GenerationContext context = new(); - ProtocolDefinition? protocol = JsonSerializer.Deserialize(protocolJson, s_jsonOptions); + ProtocolDefinition? protocol = JsonSerializer.Deserialize( + protocolJson, + s_jsonOptions + ); if (protocol is null) { - context.ReportDiagnostic(Diagnostic.Create(Diagnostics.ProtocolJsonParseError, Location.None, "Deserialization returned null.")); + context.ReportDiagnostic( + Diagnostic.Create( + Diagnostics.ProtocolJsonParseError, + Location.None, + "Deserialization returned null." + ) + ); return (context.Sources, context.Diagnostics); } diff --git a/ObsWebSocket.Codegen.Tasks/ObsWebSocket.Codegen.Tasks.csproj b/ObsWebSocket.Codegen.Tasks/ObsWebSocket.Codegen.Tasks.csproj index bd89f22..ee7162c 100644 --- a/ObsWebSocket.Codegen.Tasks/ObsWebSocket.Codegen.Tasks.csproj +++ b/ObsWebSocket.Codegen.Tasks/ObsWebSocket.Codegen.Tasks.csproj @@ -1,5 +1,4 @@  - net9.0 enable @@ -12,5 +11,4 @@ - diff --git a/ObsWebSocket.Codegen.Tasks/ProtocolCodegenRunner.cs b/ObsWebSocket.Codegen.Tasks/ProtocolCodegenRunner.cs index cb4bbeb..9965c0d 100644 --- a/ObsWebSocket.Codegen.Tasks/ProtocolCodegenRunner.cs +++ b/ObsWebSocket.Codegen.Tasks/ProtocolCodegenRunner.cs @@ -6,7 +6,8 @@ namespace ObsWebSocket.Codegen.Tasks; internal static class ProtocolCodegenRunner { - private const string ProtocolUrl = "https://raw.githubusercontent.com/obsproject/obs-websocket/master/docs/generated/protocol.json"; + private const string ProtocolUrl = + "https://raw.githubusercontent.com/obsproject/obs-websocket/master/docs/generated/protocol.json"; public static async Task GenerateAsync( string protocolPath, @@ -34,15 +35,24 @@ public static async Task GenerateAsync( return 2; } - await DownloadProtocolAsync(fullProtocolPath, cancellationToken).ConfigureAwait(false); + await DownloadProtocolAsync(fullProtocolPath, cancellationToken) + .ConfigureAwait(false); logInfo?.Invoke($"Downloaded protocol.json to '{fullProtocolPath}'."); } - string protocolJson = await File.ReadAllTextAsync(fullProtocolPath, cancellationToken).ConfigureAwait(false); - (IReadOnlyDictionary sources, IReadOnlyList diagnostics) = ProtocolCodeGenerator.Generate(protocolJson); - - Diagnostic[] errors = [.. diagnostics.Where(d => d.Severity == DiagnosticSeverity.Error)]; - Diagnostic[] warnings = [.. diagnostics.Where(d => d.Severity == DiagnosticSeverity.Warning)]; + string protocolJson = await File.ReadAllTextAsync(fullProtocolPath, cancellationToken) + .ConfigureAwait(false); + (IReadOnlyDictionary sources, IReadOnlyList diagnostics) = + ProtocolCodeGenerator.Generate(protocolJson); + + Diagnostic[] errors = + [ + .. diagnostics.Where(d => d.Severity == DiagnosticSeverity.Error), + ]; + Diagnostic[] warnings = + [ + .. diagnostics.Where(d => d.Severity == DiagnosticSeverity.Warning), + ]; Diagnostic[] infos = [.. diagnostics.Where(d => d.Severity == DiagnosticSeverity.Info)]; if (errors.Length > 0) { @@ -79,27 +89,50 @@ public static async Task GenerateAsync( } } - private static async Task DownloadProtocolAsync(string protocolPath, CancellationToken cancellationToken) + private static async Task DownloadProtocolAsync( + string protocolPath, + CancellationToken cancellationToken + ) { _ = Directory.CreateDirectory(Path.GetDirectoryName(protocolPath)!); using HttpClient http = new(); - using HttpResponseMessage response = await http.GetAsync(ProtocolUrl, cancellationToken).ConfigureAwait(false); + using HttpResponseMessage response = await http.GetAsync(ProtocolUrl, cancellationToken) + .ConfigureAwait(false); _ = response.EnsureSuccessStatusCode(); - string protocolJson = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false); - await File.WriteAllTextAsync(protocolPath, protocolJson, new UTF8Encoding(false), cancellationToken).ConfigureAwait(false); + string protocolJson = await response + .Content.ReadAsStringAsync(cancellationToken) + .ConfigureAwait(false); + await File.WriteAllTextAsync( + protocolPath, + protocolJson, + new UTF8Encoding(false), + cancellationToken + ) + .ConfigureAwait(false); } - private static void WriteSources(string outputDirectory, IReadOnlyDictionary sources) + private static void WriteSources( + string outputDirectory, + IReadOnlyDictionary sources + ) { _ = Directory.CreateDirectory(outputDirectory); - HashSet generatedRelativePaths = sources.Keys - .Select(NormalizeRelativePath) + HashSet generatedRelativePaths = sources + .Keys.Select(NormalizeRelativePath) .ToHashSet(StringComparer.OrdinalIgnoreCase); - foreach (string existingFile in Directory.GetFiles(outputDirectory, "*.g.cs", SearchOption.AllDirectories)) + foreach ( + string existingFile in Directory.GetFiles( + outputDirectory, + "*.g.cs", + SearchOption.AllDirectories + ) + ) { - string relativePath = NormalizeRelativePath(Path.GetRelativePath(outputDirectory, existingFile)); + string relativePath = NormalizeRelativePath( + Path.GetRelativePath(outputDirectory, existingFile) + ); if (!generatedRelativePaths.Contains(relativePath)) { File.Delete(existingFile); @@ -115,7 +148,7 @@ private static void WriteSources(string outputDirectory, IReadOnlyDictionary path - .Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar) + private static string NormalizeRelativePath(string path) => + path.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar) .TrimStart(Path.DirectorySeparatorChar); } diff --git a/ObsWebSocket.Core/AuthenticationFailureException.cs b/ObsWebSocket.Core/AuthenticationFailureException.cs index 2db843b..74d8383 100644 --- a/ObsWebSocket.Core/AuthenticationFailureException.cs +++ b/ObsWebSocket.Core/AuthenticationFailureException.cs @@ -31,5 +31,11 @@ public AuthenticationFailureException(string message) /// a specified error message and a reference to the inner exception that caused this one. /// public AuthenticationFailureException(string message, Exception? innerException) - : base(message, innerException ?? new InvalidOperationException("OBS authentication failed without a more specific cause.")) { } + : base( + message, + innerException + ?? new InvalidOperationException( + "OBS authentication failed without a more specific cause." + ) + ) { } } diff --git a/ObsWebSocket.Core/BatchRef.cs b/ObsWebSocket.Core/BatchRef.cs index cad2fb4..957c5b2 100644 --- a/ObsWebSocket.Core/BatchRef.cs +++ b/ObsWebSocket.Core/BatchRef.cs @@ -18,8 +18,7 @@ public readonly record struct BatchRef(int Index) { /// Drops the response type, leaving a plain reference. /// The reference to convert. - public static implicit operator BatchRef(BatchRef reference) => - new(reference.Index); + public static implicit operator BatchRef(BatchRef reference) => new(reference.Index); /// Drops the response type, leaving a plain reference. public BatchRef ToBatchRef() => new(Index); diff --git a/ObsWebSocket.Core/BatchResultExtensions.cs b/ObsWebSocket.Core/BatchResultExtensions.cs index 4712509..bb262bb 100644 --- a/ObsWebSocket.Core/BatchResultExtensions.cs +++ b/ObsWebSocket.Core/BatchResultExtensions.cs @@ -84,7 +84,8 @@ public static class BatchResultExtensions ); #endif } - catch (Exception ex) when (ex is JsonException or InvalidOperationException or NotSupportedException) + catch (Exception ex) + when (ex is JsonException or InvalidOperationException or NotSupportedException) { throw new ObsWebSocketSerializationException( $"Failed to read batch result for '{result.RequestType}' as {typeof(TResponse).Name}.", diff --git a/ObsWebSocket.Core/ConnectionAttemptFailedException.cs b/ObsWebSocket.Core/ConnectionAttemptFailedException.cs index 53b62f1..02061f0 100644 --- a/ObsWebSocket.Core/ConnectionAttemptFailedException.cs +++ b/ObsWebSocket.Core/ConnectionAttemptFailedException.cs @@ -26,5 +26,11 @@ public ConnectionAttemptFailedException(string message) /// with a specified error message and a reference to the inner exception that caused this one. /// public ConnectionAttemptFailedException(string message, Exception? innerException) - : base(message, innerException ?? new InvalidOperationException("OBS connection attempt failed without a more specific cause.")) { } + : base( + message, + innerException + ?? new InvalidOperationException( + "OBS connection attempt failed without a more specific cause." + ) + ) { } } diff --git a/ObsWebSocket.Core/Groups/ConfigGroup.cs b/ObsWebSocket.Core/Groups/ConfigGroup.cs index 87b79e2..220449d 100644 --- a/ObsWebSocket.Core/Groups/ConfigGroup.cs +++ b/ObsWebSocket.Core/Groups/ConfigGroup.cs @@ -1,11 +1,11 @@ -using Microsoft.Extensions.Logging; using System.Text.Json; using System.Text.Json.Serialization.Metadata; +using Microsoft.Extensions.Logging; using ObsWebSocket.Core.Events; using ObsWebSocket.Core.Events.Generated; +using ObsWebSocket.Core.Networking; using ObsWebSocket.Core.Protocol; using ObsWebSocket.Core.Protocol.Common; -using ObsWebSocket.Core.Networking; using ObsWebSocket.Core.Protocol.Common.FilterSettings; using ObsWebSocket.Core.Protocol.Common.InputSettings; using ObsWebSocket.Core.Protocol.Generated; @@ -28,7 +28,8 @@ public readonly partial struct ConfigGroup /// The deserialized stream service settings, or if no settings are present. /// Thrown if OBS returns an error or serialization fails. /// Thrown if the client is not connected. - public async Task GetStreamServiceSettingsAsync(JsonTypeInfo typeInfo, + public async Task GetStreamServiceSettingsAsync( + JsonTypeInfo typeInfo, CancellationToken cancellationToken = default ) where T : class @@ -40,7 +41,9 @@ public readonly partial struct ConfigGroup .Config.GetStreamServiceSettingsAsync(cancellationToken: cancellationToken) .ConfigureAwait(false); - return response?.StreamServiceSettings is not { } element ? null : JsonSerializer.Deserialize(element, typeInfo); + return response?.StreamServiceSettings is not { } element + ? null + : JsonSerializer.Deserialize(element, typeInfo); } /// @@ -51,8 +54,7 @@ public readonly partial struct ConfigGroup /// The deserialized stream service settings, or if no settings are present. /// Thrown if the type is not registered, OBS returns an error, or serialization fails. /// Thrown if the client is not connected. - public Task GetStreamServiceSettingsAsync(CancellationToken cancellationToken = default - ) + public Task GetStreamServiceSettingsAsync(CancellationToken cancellationToken = default) where T : class { JsonTypeInfo typeInfo = ObsWebSocketClientHelpers.GetRegisteredTypeInfo(); @@ -69,7 +71,8 @@ public readonly partial struct ConfigGroup /// A token to cancel the operation. /// Thrown if OBS returns an error or serialization fails. /// Thrown if the client is not connected. - public async Task SetStreamServiceSettingsAsync(string streamServiceType, + public async Task SetStreamServiceSettingsAsync( + string streamServiceType, T settings, JsonTypeInfo typeInfo, CancellationToken cancellationToken = default @@ -83,7 +86,8 @@ public async Task SetStreamServiceSettingsAsync(string streamServiceType, JsonElement settingsElement = JsonSerializer.SerializeToElement(settings, typeInfo); await client - .Config.SetStreamServiceSettingsAsync(new SetStreamServiceSettingsRequestData( + .Config.SetStreamServiceSettingsAsync( + new SetStreamServiceSettingsRequestData( streamServiceType: streamServiceType, streamServiceSettings: settingsElement ), @@ -101,14 +105,20 @@ await client /// A token to cancel the operation. /// Thrown if the type is not registered, OBS returns an error, or serialization fails. /// Thrown if the client is not connected. - public Task SetStreamServiceSettingsAsync(string streamServiceType, + public Task SetStreamServiceSettingsAsync( + string streamServiceType, T settings, CancellationToken cancellationToken = default ) where T : class { JsonTypeInfo typeInfo = ObsWebSocketClientHelpers.GetRegisteredTypeInfo(); - return client.Config.SetStreamServiceSettingsAsync(streamServiceType, settings, typeInfo, cancellationToken); + return client.Config.SetStreamServiceSettingsAsync( + streamServiceType, + settings, + typeInfo, + cancellationToken + ); } /// @@ -119,7 +129,8 @@ public Task SetStreamServiceSettingsAsync(string streamServiceType, /// True if the target scene collection is active after the call; false if the switch failed (e.g., not found). /// Thrown for unexpected OBS errors during the process. /// Thrown if the client is not connected. - public async Task EnsureSceneCollectionActiveAsync(string targetSceneCollectionName, + public async Task EnsureSceneCollectionActiveAsync( + string targetSceneCollectionName, CancellationToken cancellationToken = default ) { @@ -146,7 +157,9 @@ public async Task EnsureSceneCollectionActiveAsync(string targetSceneColle { await client .Config.SetCurrentSceneCollectionAsync( - new SetCurrentSceneCollectionRequestData(sceneCollectionName: targetSceneCollectionName), + new SetCurrentSceneCollectionRequestData( + sceneCollectionName: targetSceneCollectionName + ), cancellationToken: cancellationToken ) .ConfigureAwait(false); @@ -180,7 +193,8 @@ await client /// True if the target profile is active after the call; false if the switch failed (e.g., not found). /// Thrown for unexpected OBS errors during the process. /// Thrown if the client is not connected. - public async Task EnsureProfileActiveAsync(string targetProfileName, + public async Task EnsureProfileActiveAsync( + string targetProfileName, CancellationToken cancellationToken = default ) { diff --git a/ObsWebSocket.Core/Groups/FiltersGroup.cs b/ObsWebSocket.Core/Groups/FiltersGroup.cs index cf414ac..b6f29ba 100644 --- a/ObsWebSocket.Core/Groups/FiltersGroup.cs +++ b/ObsWebSocket.Core/Groups/FiltersGroup.cs @@ -1,11 +1,11 @@ -using Microsoft.Extensions.Logging; using System.Text.Json; using System.Text.Json.Serialization.Metadata; +using Microsoft.Extensions.Logging; using ObsWebSocket.Core.Events; using ObsWebSocket.Core.Events.Generated; +using ObsWebSocket.Core.Networking; using ObsWebSocket.Core.Protocol; using ObsWebSocket.Core.Protocol.Common; -using ObsWebSocket.Core.Networking; using ObsWebSocket.Core.Protocol.Common.FilterSettings; using ObsWebSocket.Core.Protocol.Common.InputSettings; using ObsWebSocket.Core.Protocol.Generated; @@ -31,7 +31,8 @@ public readonly partial struct FiltersGroup /// The deserialized settings, or null if the source/filter is not found or deserialization fails. /// Thrown for OBS errors other than 'ResourceNotFound'. /// Thrown if the client is not connected. - public async Task GetSourceFilterSettingsAsync(string sourceName, + public async Task GetSourceFilterSettingsAsync( + string sourceName, string filterName, JsonTypeInfo typeInfo, CancellationToken cancellationToken = default @@ -96,14 +97,20 @@ public readonly partial struct FiltersGroup /// The deserialized settings, or null if the source/filter is not found or deserialization fails. /// Thrown for OBS errors other than 'ResourceNotFound'. /// Thrown if the client is not connected. - public Task GetSourceFilterSettingsAsync(string sourceName, + public Task GetSourceFilterSettingsAsync( + string sourceName, string filterName, CancellationToken cancellationToken = default ) where T : class { JsonTypeInfo typeInfo = ObsWebSocketClientHelpers.GetRegisteredTypeInfo(); - return client.Filters.GetSourceFilterSettingsAsync(sourceName, filterName, typeInfo, cancellationToken); + return client.Filters.GetSourceFilterSettingsAsync( + sourceName, + filterName, + typeInfo, + cancellationToken + ); } /// @@ -119,7 +126,8 @@ public readonly partial struct FiltersGroup /// A token to cancel the operation. /// Thrown if OBS fails or serialization fails. /// Thrown if the client is not connected. - public async Task SetSourceFilterSettingsAsync(string sourceName, + public async Task SetSourceFilterSettingsAsync( + string sourceName, string filterName, T settings, JsonTypeInfo typeInfo, @@ -148,7 +156,8 @@ public async Task SetSourceFilterSettingsAsync(string sourceName, } await client - .Filters.SetSourceFilterSettingsAsync(new SetSourceFilterSettingsRequestData( + .Filters.SetSourceFilterSettingsAsync( + new SetSourceFilterSettingsRequestData( filterSettings: settingsElement, sourceName: sourceName, filterName: filterName, @@ -170,7 +179,8 @@ await client /// A token to cancel the operation. /// Thrown if the type is not registered, OBS returns an error, or serialization fails. /// Thrown if the client is not connected. - public Task SetSourceFilterSettingsAsync(string sourceName, + public Task SetSourceFilterSettingsAsync( + string sourceName, string filterName, T settings, bool overlay = true, @@ -179,7 +189,14 @@ public Task SetSourceFilterSettingsAsync(string sourceName, where T : class { JsonTypeInfo typeInfo = ObsWebSocketClientHelpers.GetRegisteredTypeInfo(); - return client.Filters.SetSourceFilterSettingsAsync(sourceName, filterName, settings, typeInfo, overlay, cancellationToken); + return client.Filters.SetSourceFilterSettingsAsync( + sourceName, + filterName, + settings, + typeInfo, + overlay, + cancellationToken + ); } /// @@ -195,7 +212,8 @@ public Task SetSourceFilterSettingsAsync(string sourceName, /// A token to cancel the operation. /// Thrown if OBS fails or serialization fails. /// Thrown if the client is not connected. - public async Task CreateSourceFilterAsync(string sourceName, + public async Task CreateSourceFilterAsync( + string sourceName, string filterName, string filterKind, T settings, @@ -225,7 +243,8 @@ public async Task CreateSourceFilterAsync(string sourceName, } await client - .Filters.CreateSourceFilterAsync(new CreateSourceFilterRequestData( + .Filters.CreateSourceFilterAsync( + new CreateSourceFilterRequestData( filterName: filterName, filterKind: filterKind, sourceName: sourceName, @@ -247,7 +266,8 @@ await client /// A token to cancel the operation. /// Thrown if the type is not registered, OBS returns an error, or serialization fails. /// Thrown if the client is not connected. - public Task CreateSourceFilterAsync(string sourceName, + public Task CreateSourceFilterAsync( + string sourceName, string filterName, string filterKind, T settings, @@ -256,7 +276,14 @@ public Task CreateSourceFilterAsync(string sourceName, where T : class { JsonTypeInfo typeInfo = ObsWebSocketClientHelpers.GetRegisteredTypeInfo(); - return client.Filters.CreateSourceFilterAsync(sourceName, filterName, filterKind, settings, typeInfo, cancellationToken); + return client.Filters.CreateSourceFilterAsync( + sourceName, + filterName, + filterKind, + settings, + typeInfo, + cancellationToken + ); } /// @@ -269,7 +296,8 @@ public Task CreateSourceFilterAsync(string sourceName, /// The deserialized default settings, or if no settings are present. /// Thrown if OBS returns an error or serialization fails. /// Thrown if the client is not connected. - public async Task GetSourceFilterDefaultSettingsAsync(string filterKind, + public async Task GetSourceFilterDefaultSettingsAsync( + string filterKind, JsonTypeInfo typeInfo, CancellationToken cancellationToken = default ) @@ -280,12 +308,15 @@ public Task CreateSourceFilterAsync(string sourceName, client.EnsureConnected(); GetSourceFilterDefaultSettingsResponseData? response = await client - .Filters.GetSourceFilterDefaultSettingsAsync(new GetSourceFilterDefaultSettingsRequestData(filterKind: filterKind), + .Filters.GetSourceFilterDefaultSettingsAsync( + new GetSourceFilterDefaultSettingsRequestData(filterKind: filterKind), cancellationToken: cancellationToken ) .ConfigureAwait(false); - return response?.DefaultFilterSettings is not { } element ? null : JsonSerializer.Deserialize(element, typeInfo); + return response?.DefaultFilterSettings is not { } element + ? null + : JsonSerializer.Deserialize(element, typeInfo); } /// @@ -297,12 +328,17 @@ public Task CreateSourceFilterAsync(string sourceName, /// The deserialized default settings, or if no settings are present. /// Thrown if the type is not registered, OBS returns an error, or serialization fails. /// Thrown if the client is not connected. - public Task GetSourceFilterDefaultSettingsAsync(string filterKind, + public Task GetSourceFilterDefaultSettingsAsync( + string filterKind, CancellationToken cancellationToken = default ) where T : class { JsonTypeInfo typeInfo = ObsWebSocketClientHelpers.GetRegisteredTypeInfo(); - return client.Filters.GetSourceFilterDefaultSettingsAsync(filterKind, typeInfo, cancellationToken); + return client.Filters.GetSourceFilterDefaultSettingsAsync( + filterKind, + typeInfo, + cancellationToken + ); } } diff --git a/ObsWebSocket.Core/Groups/GeneralGroup.cs b/ObsWebSocket.Core/Groups/GeneralGroup.cs index 17af9d4..02caebf 100644 --- a/ObsWebSocket.Core/Groups/GeneralGroup.cs +++ b/ObsWebSocket.Core/Groups/GeneralGroup.cs @@ -1,11 +1,11 @@ -using Microsoft.Extensions.Logging; using System.Text.Json; using System.Text.Json.Serialization.Metadata; +using Microsoft.Extensions.Logging; using ObsWebSocket.Core.Events; using ObsWebSocket.Core.Events.Generated; +using ObsWebSocket.Core.Networking; using ObsWebSocket.Core.Protocol; using ObsWebSocket.Core.Protocol.Common; -using ObsWebSocket.Core.Networking; using ObsWebSocket.Core.Protocol.Common.FilterSettings; using ObsWebSocket.Core.Protocol.Common.InputSettings; using ObsWebSocket.Core.Protocol.Generated; @@ -26,7 +26,8 @@ public readonly partial struct GeneralGroup /// A token to cancel the operation. /// Thrown if OBS fails to trigger the hotkey (e.g., hotkey not found). /// Thrown if the client is not connected. - public async Task TriggerHotkeyAsync(string hotkeyName, + public async Task TriggerHotkeyAsync( + string hotkeyName, CancellationToken cancellationToken = default ) { diff --git a/ObsWebSocket.Core/Groups/InputsGroup.cs b/ObsWebSocket.Core/Groups/InputsGroup.cs index 08cb247..d0650d3 100644 --- a/ObsWebSocket.Core/Groups/InputsGroup.cs +++ b/ObsWebSocket.Core/Groups/InputsGroup.cs @@ -1,11 +1,11 @@ -using Microsoft.Extensions.Logging; using System.Text.Json; using System.Text.Json.Serialization.Metadata; +using Microsoft.Extensions.Logging; using ObsWebSocket.Core.Events; using ObsWebSocket.Core.Events.Generated; +using ObsWebSocket.Core.Networking; using ObsWebSocket.Core.Protocol; using ObsWebSocket.Core.Protocol.Common; -using ObsWebSocket.Core.Networking; using ObsWebSocket.Core.Protocol.Common.FilterSettings; using ObsWebSocket.Core.Protocol.Common.InputSettings; using ObsWebSocket.Core.Protocol.Generated; @@ -27,7 +27,8 @@ public readonly partial struct InputsGroup /// A token to cancel the operation. /// Thrown if OBS fails to set the text (e.g., input not found, not a text source). /// Thrown if the client is not connected. - public async Task SetInputTextAsync(string inputName, + public async Task SetInputTextAsync( + string inputName, string text, CancellationToken cancellationToken = default ) @@ -37,7 +38,8 @@ public async Task SetInputTextAsync(string inputName, client.EnsureConnected(); await client - .Inputs.SetInputSettingsAsync(inputName: inputName, + .Inputs.SetInputSettingsAsync( + inputName: inputName, settings: new TextGdiPlusInputSettings(Text: text), overlay: true, cancellationToken: cancellationToken @@ -55,7 +57,8 @@ await client /// Thrown if the batch request itself fails (e.g., timeout). /// Thrown if the client is not connected. /// Thrown if inputMutes is null. - public async Task SetInputMutesAsync(IEnumerable<(string InputName, bool IsMuted)> inputMutes, + public async Task SetInputMutesAsync( + IEnumerable<(string InputName, bool IsMuted)> inputMutes, CancellationToken cancellationToken = default ) { @@ -118,7 +121,8 @@ public async Task SetInputMutesAsync(IEnumerable<(string InputName, bool IsMuted /// The deserialized settings, or null if the input is not found or deserialization fails. /// Thrown for unexpected OBS errors. /// Thrown if the client is not connected. - public async Task GetInputSettingsAsync(string inputName, + public async Task GetInputSettingsAsync( + string inputName, JsonTypeInfo typeInfo, CancellationToken cancellationToken = default ) @@ -132,7 +136,8 @@ public async Task SetInputMutesAsync(IEnumerable<(string InputName, bool IsMuted try { response = await client - .Inputs.GetInputSettingsAsync(new GetInputSettingsRequestData(inputName: inputName), + .Inputs.GetInputSettingsAsync( + new GetInputSettingsRequestData(inputName: inputName), cancellationToken: cancellationToken ) .ConfigureAwait(false); @@ -178,7 +183,8 @@ public async Task SetInputMutesAsync(IEnumerable<(string InputName, bool IsMuted /// The deserialized settings, or null if the input is not found or deserialization fails. /// Thrown if the type is not registered or OBS returns an error. /// Thrown if the client is not connected. - public Task GetInputSettingsAsync(string inputName, + public Task GetInputSettingsAsync( + string inputName, CancellationToken cancellationToken = default ) where T : class @@ -199,7 +205,8 @@ public async Task SetInputMutesAsync(IEnumerable<(string InputName, bool IsMuted /// A token to cancel the operation. /// Thrown if OBS fails or serialization fails. /// Thrown if the client is not connected. - public async Task SetInputSettingsAsync(string inputName, + public async Task SetInputSettingsAsync( + string inputName, T settings, JsonTypeInfo typeInfo, bool overlay = true, @@ -226,7 +233,8 @@ public async Task SetInputSettingsAsync(string inputName, } await client - .Inputs.SetInputSettingsAsync(new SetInputSettingsRequestData( + .Inputs.SetInputSettingsAsync( + new SetInputSettingsRequestData( inputSettings: settingsElement, inputName: inputName, overlay: overlay @@ -246,7 +254,8 @@ await client /// A token to cancel the operation. /// Thrown if the type is not registered, OBS returns an error, or serialization fails. /// Thrown if the client is not connected. - public Task SetInputSettingsAsync(string inputName, + public Task SetInputSettingsAsync( + string inputName, T settings, bool overlay = true, CancellationToken cancellationToken = default @@ -254,7 +263,13 @@ public Task SetInputSettingsAsync(string inputName, where T : class { JsonTypeInfo typeInfo = ObsWebSocketClientHelpers.GetRegisteredTypeInfo(); - return client.Inputs.SetInputSettingsAsync(inputName, settings, typeInfo, overlay, cancellationToken); + return client.Inputs.SetInputSettingsAsync( + inputName, + settings, + typeInfo, + overlay, + cancellationToken + ); } /// @@ -273,7 +288,8 @@ public Task SetInputSettingsAsync(string inputName, /// The response data containing the new scene item ID, or null on failure. /// Thrown if OBS fails or serialization fails. /// Thrown if the client is not connected. - public async Task CreateInputAsync(string inputKind, + public async Task CreateInputAsync( + string inputKind, string inputName, T settings, JsonTypeInfo typeInfo, @@ -304,7 +320,8 @@ public Task SetInputSettingsAsync(string inputName, } return await client - .Inputs.CreateInputAsync(new CreateInputRequestData( + .Inputs.CreateInputAsync( + new CreateInputRequestData( inputName: inputName, inputKind: inputKind, sceneName: sceneName, @@ -331,7 +348,8 @@ public Task SetInputSettingsAsync(string inputName, /// The response data containing the new scene item ID, or null on failure. /// Thrown if the type is not registered, OBS returns an error, or serialization fails. /// Thrown if the client is not connected. - public Task CreateInputAsync(string inputKind, + public Task CreateInputAsync( + string inputKind, string inputName, T settings, string? sceneName = null, @@ -342,7 +360,16 @@ public Task SetInputSettingsAsync(string inputName, where T : class { JsonTypeInfo typeInfo = ObsWebSocketClientHelpers.GetRegisteredTypeInfo(); - return client.Inputs.CreateInputAsync(inputKind, inputName, settings, typeInfo, sceneName, sceneUuid, sceneItemEnabled, cancellationToken); + return client.Inputs.CreateInputAsync( + inputKind, + inputName, + settings, + typeInfo, + sceneName, + sceneUuid, + sceneItemEnabled, + cancellationToken + ); } /// @@ -355,7 +382,8 @@ public Task SetInputSettingsAsync(string inputName, /// The deserialized default settings, or if no settings are present. /// Thrown if OBS returns an error or serialization fails. /// Thrown if the client is not connected. - public async Task GetInputDefaultSettingsAsync(string inputKind, + public async Task GetInputDefaultSettingsAsync( + string inputKind, JsonTypeInfo typeInfo, CancellationToken cancellationToken = default ) @@ -366,12 +394,15 @@ public Task SetInputSettingsAsync(string inputName, client.EnsureConnected(); GetInputDefaultSettingsResponseData? response = await client - .Inputs.GetInputDefaultSettingsAsync(new GetInputDefaultSettingsRequestData(inputKind: inputKind), + .Inputs.GetInputDefaultSettingsAsync( + new GetInputDefaultSettingsRequestData(inputKind: inputKind), cancellationToken: cancellationToken ) .ConfigureAwait(false); - return response?.DefaultInputSettings is not { } element ? null : JsonSerializer.Deserialize(element, typeInfo); + return response?.DefaultInputSettings is not { } element + ? null + : JsonSerializer.Deserialize(element, typeInfo); } /// @@ -383,7 +414,8 @@ public Task SetInputSettingsAsync(string inputName, /// The deserialized default settings, or if no settings are present. /// Thrown if the type is not registered, OBS returns an error, or serialization fails. /// Thrown if the client is not connected. - public Task GetInputDefaultSettingsAsync(string inputKind, + public Task GetInputDefaultSettingsAsync( + string inputKind, CancellationToken cancellationToken = default ) where T : class @@ -392,54 +424,54 @@ public Task SetInputSettingsAsync(string inputName, return client.Inputs.GetInputDefaultSettingsAsync(inputKind, typeInfo, cancellationToken); } - /// - /// Sets an input's volume in decibels. The underlying request accepts either decibels or - /// a multiplier and fails when given neither. - /// - /// The name of the input. - /// The desired volume in dB. OBS accepts -100 through 26. - /// A token to cancel the operation. - /// Thrown if OBS rejects the request. - /// Thrown if the client is not connected. - public async Task SetInputVolumeDbAsync( - string inputName, - double volumeDb, - CancellationToken cancellationToken = default - ) - { - ArgumentException.ThrowIfNullOrEmpty(inputName); - client.EnsureConnected(); + /// + /// Sets an input's volume in decibels. The underlying request accepts either decibels or + /// a multiplier and fails when given neither. + /// + /// The name of the input. + /// The desired volume in dB. OBS accepts -100 through 26. + /// A token to cancel the operation. + /// Thrown if OBS rejects the request. + /// Thrown if the client is not connected. + public async Task SetInputVolumeDbAsync( + string inputName, + double volumeDb, + CancellationToken cancellationToken = default + ) + { + ArgumentException.ThrowIfNullOrEmpty(inputName); + client.EnsureConnected(); - await client - .Inputs.SetInputVolumeAsync( - new SetInputVolumeRequestData { InputName = inputName, InputVolumeDb = volumeDb }, - cancellationToken - ) - .ConfigureAwait(false); - } + await client + .Inputs.SetInputVolumeAsync( + new SetInputVolumeRequestData { InputName = inputName, InputVolumeDb = volumeDb }, + cancellationToken + ) + .ConfigureAwait(false); + } - /// - /// Sets an input's volume as a linear multiplier, where 1.0 is unity gain. - /// - /// The name of the input. - /// The desired volume multiplier. OBS accepts 0 through 20. - /// A token to cancel the operation. - /// Thrown if OBS rejects the request. - /// Thrown if the client is not connected. - public async Task SetInputVolumeMulAsync( - string inputName, - double volumeMul, - CancellationToken cancellationToken = default - ) - { - ArgumentException.ThrowIfNullOrEmpty(inputName); - client.EnsureConnected(); + /// + /// Sets an input's volume as a linear multiplier, where 1.0 is unity gain. + /// + /// The name of the input. + /// The desired volume multiplier. OBS accepts 0 through 20. + /// A token to cancel the operation. + /// Thrown if OBS rejects the request. + /// Thrown if the client is not connected. + public async Task SetInputVolumeMulAsync( + string inputName, + double volumeMul, + CancellationToken cancellationToken = default + ) + { + ArgumentException.ThrowIfNullOrEmpty(inputName); + client.EnsureConnected(); - await client - .Inputs.SetInputVolumeAsync( - new SetInputVolumeRequestData { InputName = inputName, InputVolumeMul = volumeMul }, - cancellationToken - ) - .ConfigureAwait(false); - } + await client + .Inputs.SetInputVolumeAsync( + new SetInputVolumeRequestData { InputName = inputName, InputVolumeMul = volumeMul }, + cancellationToken + ) + .ConfigureAwait(false); + } } diff --git a/ObsWebSocket.Core/Groups/MediaInputsGroup.cs b/ObsWebSocket.Core/Groups/MediaInputsGroup.cs index 1fd4801..0c4f224 100644 --- a/ObsWebSocket.Core/Groups/MediaInputsGroup.cs +++ b/ObsWebSocket.Core/Groups/MediaInputsGroup.cs @@ -1,11 +1,11 @@ -using Microsoft.Extensions.Logging; using System.Text.Json; using System.Text.Json.Serialization.Metadata; +using Microsoft.Extensions.Logging; using ObsWebSocket.Core.Events; using ObsWebSocket.Core.Events.Generated; +using ObsWebSocket.Core.Networking; using ObsWebSocket.Core.Protocol; using ObsWebSocket.Core.Protocol.Common; -using ObsWebSocket.Core.Networking; using ObsWebSocket.Core.Protocol.Common.FilterSettings; using ObsWebSocket.Core.Protocol.Common.InputSettings; using ObsWebSocket.Core.Protocol.Generated; @@ -19,57 +19,68 @@ namespace ObsWebSocket.Core; /// public readonly partial struct MediaInputsGroup { - /// - /// Triggers a media action on an input using the typed enum - /// rather than a protocol string constant. - /// - /// The name of the media input. - /// The transport action to perform. - /// A token to cancel the operation. - /// Thrown if OBS rejects the request. - /// Thrown if the client is not connected. - public async Task TriggerMediaActionAsync( - string inputName, - MediaInputAction action, - CancellationToken cancellationToken = default - ) - { - ArgumentException.ThrowIfNullOrEmpty(inputName); - client.EnsureConnected(); + /// + /// Triggers a media action on an input using the typed enum + /// rather than a protocol string constant. + /// + /// The name of the media input. + /// The transport action to perform. + /// A token to cancel the operation. + /// Thrown if OBS rejects the request. + /// Thrown if the client is not connected. + public async Task TriggerMediaActionAsync( + string inputName, + MediaInputAction action, + CancellationToken cancellationToken = default + ) + { + ArgumentException.ThrowIfNullOrEmpty(inputName); + client.EnsureConnected(); - await client - .MediaInputs.TriggerMediaInputActionAsync( - new TriggerMediaInputActionRequestData - { - InputName = inputName, - MediaAction = action.ToWireValue(), - }, - cancellationToken - ) - .ConfigureAwait(false); - } + await client + .MediaInputs.TriggerMediaInputActionAsync( + new TriggerMediaInputActionRequestData + { + InputName = inputName, + MediaAction = action.ToWireValue(), + }, + cancellationToken + ) + .ConfigureAwait(false); + } - /// Plays a media input. - public Task PlayMediaAsync( - string inputName, - CancellationToken cancellationToken = default - ) => client.MediaInputs.TriggerMediaActionAsync(inputName, MediaInputAction.Play, cancellationToken); + /// Plays a media input. + public Task PlayMediaAsync(string inputName, CancellationToken cancellationToken = default) => + client.MediaInputs.TriggerMediaActionAsync( + inputName, + MediaInputAction.Play, + cancellationToken + ); - /// Pauses a media input. - public Task PauseMediaAsync( - string inputName, - CancellationToken cancellationToken = default - ) => client.MediaInputs.TriggerMediaActionAsync(inputName, MediaInputAction.Pause, cancellationToken); + /// Pauses a media input. + public Task PauseMediaAsync(string inputName, CancellationToken cancellationToken = default) => + client.MediaInputs.TriggerMediaActionAsync( + inputName, + MediaInputAction.Pause, + cancellationToken + ); - /// Stops a media input. - public Task StopMediaAsync( - string inputName, - CancellationToken cancellationToken = default - ) => client.MediaInputs.TriggerMediaActionAsync(inputName, MediaInputAction.Stop, cancellationToken); + /// Stops a media input. + public Task StopMediaAsync(string inputName, CancellationToken cancellationToken = default) => + client.MediaInputs.TriggerMediaActionAsync( + inputName, + MediaInputAction.Stop, + cancellationToken + ); - /// Restarts a media input from the beginning. - public Task RestartMediaAsync( - string inputName, - CancellationToken cancellationToken = default - ) => client.MediaInputs.TriggerMediaActionAsync(inputName, MediaInputAction.Restart, cancellationToken); + /// Restarts a media input from the beginning. + public Task RestartMediaAsync( + string inputName, + CancellationToken cancellationToken = default + ) => + client.MediaInputs.TriggerMediaActionAsync( + inputName, + MediaInputAction.Restart, + cancellationToken + ); } diff --git a/ObsWebSocket.Core/Groups/OutputsGroup.cs b/ObsWebSocket.Core/Groups/OutputsGroup.cs index 8c21a28..51ff2d5 100644 --- a/ObsWebSocket.Core/Groups/OutputsGroup.cs +++ b/ObsWebSocket.Core/Groups/OutputsGroup.cs @@ -1,11 +1,11 @@ -using Microsoft.Extensions.Logging; using System.Text.Json; using System.Text.Json.Serialization.Metadata; +using Microsoft.Extensions.Logging; using ObsWebSocket.Core.Events; using ObsWebSocket.Core.Events.Generated; +using ObsWebSocket.Core.Networking; using ObsWebSocket.Core.Protocol; using ObsWebSocket.Core.Protocol.Common; -using ObsWebSocket.Core.Networking; using ObsWebSocket.Core.Protocol.Common.FilterSettings; using ObsWebSocket.Core.Protocol.Common.InputSettings; using ObsWebSocket.Core.Protocol.Generated; @@ -29,7 +29,8 @@ public readonly partial struct OutputsGroup /// The deserialized output settings, or if no settings are present. /// Thrown if OBS returns an error or serialization fails. /// Thrown if the client is not connected. - public async Task GetOutputSettingsAsync(string outputName, + public async Task GetOutputSettingsAsync( + string outputName, JsonTypeInfo typeInfo, CancellationToken cancellationToken = default ) @@ -40,12 +41,15 @@ public readonly partial struct OutputsGroup client.EnsureConnected(); GetOutputSettingsResponseData? response = await client - .Outputs.GetOutputSettingsAsync(new GetOutputSettingsRequestData(outputName: outputName), + .Outputs.GetOutputSettingsAsync( + new GetOutputSettingsRequestData(outputName: outputName), cancellationToken: cancellationToken ) .ConfigureAwait(false); - return response?.OutputSettings is not { } element ? null : JsonSerializer.Deserialize(element, typeInfo); + return response?.OutputSettings is not { } element + ? null + : JsonSerializer.Deserialize(element, typeInfo); } /// @@ -57,7 +61,8 @@ public readonly partial struct OutputsGroup /// The deserialized output settings, or if no settings are present. /// Thrown if the type is not registered, OBS returns an error, or serialization fails. /// Thrown if the client is not connected. - public Task GetOutputSettingsAsync(string outputName, + public Task GetOutputSettingsAsync( + string outputName, CancellationToken cancellationToken = default ) where T : class @@ -76,7 +81,8 @@ public readonly partial struct OutputsGroup /// A token to cancel the operation. /// Thrown if OBS returns an error or serialization fails. /// Thrown if the client is not connected. - public async Task SetOutputSettingsAsync(string outputName, + public async Task SetOutputSettingsAsync( + string outputName, T settings, JsonTypeInfo typeInfo, CancellationToken cancellationToken = default @@ -90,7 +96,8 @@ public async Task SetOutputSettingsAsync(string outputName, JsonElement settingsElement = JsonSerializer.SerializeToElement(settings, typeInfo); await client - .Outputs.SetOutputSettingsAsync(new SetOutputSettingsRequestData( + .Outputs.SetOutputSettingsAsync( + new SetOutputSettingsRequestData( outputName: outputName, outputSettings: settingsElement ), @@ -108,14 +115,20 @@ await client /// A token to cancel the operation. /// Thrown if the type is not registered, OBS returns an error, or serialization fails. /// Thrown if the client is not connected. - public Task SetOutputSettingsAsync(string outputName, + public Task SetOutputSettingsAsync( + string outputName, T settings, CancellationToken cancellationToken = default ) where T : class { JsonTypeInfo typeInfo = ObsWebSocketClientHelpers.GetRegisteredTypeInfo(); - return client.Outputs.SetOutputSettingsAsync(outputName, settings, typeInfo, cancellationToken); + return client.Outputs.SetOutputSettingsAsync( + outputName, + settings, + typeInfo, + cancellationToken + ); } /// @@ -148,19 +161,23 @@ public async Task IsVirtualCamActiveAsync(CancellationToken cancellationTo /// or if the timeout elapsed before the event arrived. /// /// Thrown if the client is not connected. - public async Task SetVirtualCamActiveAndWaitAsync(bool activate, + public async Task SetVirtualCamActiveAndWaitAsync( + bool activate, TimeSpan? timeout = null, - CancellationToken cancellationToken = default) + CancellationToken cancellationToken = default + ) { client.EnsureConnected(); TimeSpan effectiveTimeout = timeout ?? TimeSpan.FromSeconds(10); // Set up the wait before issuing the command to avoid missing the event. - Task waitTask = client.WaitForEventAsync( - predicate: _ => true, - timeout: effectiveTimeout, - cancellationToken: cancellationToken); + Task waitTask = + client.WaitForEventAsync( + predicate: _ => true, + timeout: effectiveTimeout, + cancellationToken: cancellationToken + ); if (activate) { diff --git a/ObsWebSocket.Core/Groups/RecordGroup.cs b/ObsWebSocket.Core/Groups/RecordGroup.cs index 6993e3f..86566e8 100644 --- a/ObsWebSocket.Core/Groups/RecordGroup.cs +++ b/ObsWebSocket.Core/Groups/RecordGroup.cs @@ -1,11 +1,11 @@ -using Microsoft.Extensions.Logging; using System.Text.Json; using System.Text.Json.Serialization.Metadata; +using Microsoft.Extensions.Logging; using ObsWebSocket.Core.Events; using ObsWebSocket.Core.Events.Generated; +using ObsWebSocket.Core.Networking; using ObsWebSocket.Core.Protocol; using ObsWebSocket.Core.Protocol.Common; -using ObsWebSocket.Core.Networking; using ObsWebSocket.Core.Protocol.Common.FilterSettings; using ObsWebSocket.Core.Protocol.Common.InputSettings; using ObsWebSocket.Core.Protocol.Generated; @@ -21,64 +21,63 @@ public readonly partial struct RecordGroup { private static readonly TimeSpan s_defaultOutputTimeout = TimeSpan.FromSeconds(10); - /// - /// Starts or stops recording and waits for OBS to confirm the state change. - /// - /// to start recording; to stop it. - /// Maximum time to wait for the state-change event. Defaults to 10 seconds. - /// A token to cancel the operation. - /// - /// The state reported by the event, or if the timeout elapsed first. - /// - /// Thrown if the client is not connected. - public async Task SetRecordActiveAndWaitAsync( - bool activate, - TimeSpan? timeout = null, - CancellationToken cancellationToken = default - ) - { - client.EnsureConnected(); + /// + /// Starts or stops recording and waits for OBS to confirm the state change. + /// + /// to start recording; to stop it. + /// Maximum time to wait for the state-change event. Defaults to 10 seconds. + /// A token to cancel the operation. + /// + /// The state reported by the event, or if the timeout elapsed first. + /// + /// Thrown if the client is not connected. + public async Task SetRecordActiveAndWaitAsync( + bool activate, + TimeSpan? timeout = null, + CancellationToken cancellationToken = default + ) + { + client.EnsureConnected(); - // Set up the wait before issuing the command to avoid missing the event. - Task waitTask = client.WaitForEventAsync( + // Set up the wait before issuing the command to avoid missing the event. + Task waitTask = + client.WaitForEventAsync( predicate: _ => true, timeout: timeout ?? s_defaultOutputTimeout, cancellationToken: cancellationToken ); - if (activate) - { - await client.Record.StartRecordAsync(cancellationToken).ConfigureAwait(false); - } - else - { - _ = await client.Record.StopRecordAsync(cancellationToken).ConfigureAwait(false); - } - - try - { - RecordStateChangedEventArgs ev = await waitTask.ConfigureAwait(false); - return OutputStateExtensions.FromWireValue(ev.EventData.OutputState); - } - catch (TimeoutException) - { - return null; - } + if (activate) + { + await client.Record.StartRecordAsync(cancellationToken).ConfigureAwait(false); + } + else + { + _ = await client.Record.StopRecordAsync(cancellationToken).ConfigureAwait(false); } - /// - /// Returns whether recording is currently active. - /// - /// A token to cancel the operation. - /// Thrown if the client is not connected. - public async Task IsRecordActiveAsync( - CancellationToken cancellationToken = default - ) + try { - client.EnsureConnected(); - GetRecordStatusResponseData? status = await client - .Record.GetRecordStatusAsync(cancellationToken) - .ConfigureAwait(false); - return status?.OutputActive ?? false; + RecordStateChangedEventArgs ev = await waitTask.ConfigureAwait(false); + return OutputStateExtensions.FromWireValue(ev.EventData.OutputState); } + catch (TimeoutException) + { + return null; + } + } + + /// + /// Returns whether recording is currently active. + /// + /// A token to cancel the operation. + /// Thrown if the client is not connected. + public async Task IsRecordActiveAsync(CancellationToken cancellationToken = default) + { + client.EnsureConnected(); + GetRecordStatusResponseData? status = await client + .Record.GetRecordStatusAsync(cancellationToken) + .ConfigureAwait(false); + return status?.OutputActive ?? false; + } } diff --git a/ObsWebSocket.Core/Groups/SceneItemsGroup.cs b/ObsWebSocket.Core/Groups/SceneItemsGroup.cs index 442be69..ef044f7 100644 --- a/ObsWebSocket.Core/Groups/SceneItemsGroup.cs +++ b/ObsWebSocket.Core/Groups/SceneItemsGroup.cs @@ -1,11 +1,11 @@ -using Microsoft.Extensions.Logging; using System.Text.Json; using System.Text.Json.Serialization.Metadata; +using Microsoft.Extensions.Logging; using ObsWebSocket.Core.Events; using ObsWebSocket.Core.Events.Generated; +using ObsWebSocket.Core.Networking; using ObsWebSocket.Core.Protocol; using ObsWebSocket.Core.Protocol.Common; -using ObsWebSocket.Core.Networking; using ObsWebSocket.Core.Protocol.Common.FilterSettings; using ObsWebSocket.Core.Protocol.Common.InputSettings; using ObsWebSocket.Core.Protocol.Generated; @@ -29,7 +29,8 @@ public readonly partial struct SceneItemsGroup /// The final enabled state of the scene item after the operation. /// Thrown if OBS fails the operation (e.g., scene/item not found). /// Thrown if the client is not connected. - public async Task SetSceneItemEnabledAsync(string sceneName, + public async Task SetSceneItemEnabledAsync( + string sceneName, double sceneItemId, // Use double as sceneItemId is Number in protocol bool? isEnabled = null, // If null, toggles; otherwise sets to the specified state CancellationToken cancellationToken = default @@ -49,7 +50,10 @@ public async Task SetSceneItemEnabledAsync(string sceneName, GetSceneItemEnabledResponseData currentStateResponse = await client .SceneItems.GetSceneItemEnabledAsync( - new GetSceneItemEnabledRequestData(sceneItemId: sceneItemId, sceneName: sceneName), + new GetSceneItemEnabledRequestData( + sceneItemId: sceneItemId, + sceneName: sceneName + ), cancellationToken: cancellationToken ) .ConfigureAwait(false) @@ -60,7 +64,8 @@ await client } await client - .SceneItems.SetSceneItemEnabledAsync(new SetSceneItemEnabledRequestData( + .SceneItems.SetSceneItemEnabledAsync( + new SetSceneItemEnabledRequestData( sceneItemId: sceneItemId, sceneItemEnabled: targetState, sceneName: sceneName @@ -83,7 +88,8 @@ await client /// Thrown if OBS fails the operation (e.g., scene/item not found). /// Thrown if the client is not connected. /// Thrown if the source name is not found within the specified scene. - public async Task SetSceneItemEnabledAsync(string sceneName, + public async Task SetSceneItemEnabledAsync( + string sceneName, string sourceName, bool? isEnabled = null, // If null, toggles; otherwise sets to the specified state CancellationToken cancellationToken = default @@ -99,7 +105,8 @@ public async Task SetSceneItemEnabledAsync(string sceneName, return sceneItemId.HasValue ? await client - .SceneItems.SetSceneItemEnabledAsync(sceneName, + .SceneItems.SetSceneItemEnabledAsync( + sceneName, sceneItemId.Value, isEnabled, cancellationToken @@ -120,8 +127,11 @@ public async Task SetSceneItemEnabledAsync(string sceneName, /// A Task resulting in the nullable scene item ID (double?). Returns null if the item or scene is not found. /// Thrown for OBS errors other than 'ResourceNotFound'. /// Thrown if the client is not connected. - [Obsolete("Renamed to FindSceneItemIdAsync. Async methods cannot use the out-parameter Try pattern, so the Try prefix was misleading. This forwarder will be removed in a future release.")] - public Task TryGetSceneItemIdAsync(string sceneName, + [Obsolete( + "Renamed to FindSceneItemIdAsync. Async methods cannot use the out-parameter Try pattern, so the Try prefix was misleading. This forwarder will be removed in a future release." + )] + public Task TryGetSceneItemIdAsync( + string sceneName, string sourceName, CancellationToken cancellationToken = default ) => client.SceneItems.FindSceneItemIdAsync(sceneName, sourceName, cancellationToken); @@ -135,7 +145,8 @@ public async Task SetSceneItemEnabledAsync(string sceneName, /// A token to cancel the operation. /// The scene item id, or if the source is not in the scene. /// Thrown if the client is not connected. - public async Task FindSceneItemIdAsync(string sceneName, + public async Task FindSceneItemIdAsync( + string sceneName, string sourceName, CancellationToken cancellationToken = default ) @@ -172,42 +183,43 @@ public async Task SetSceneItemEnabledAsync(string sceneName, // Let other ObsWebSocketExceptions or different exception types propagate } - /// - /// Sets or toggles a scene item's enabled state using an integer item id. - /// - /// The name of the scene containing the item. - /// The numeric id of the scene item. - /// The desired state, or to toggle. - /// A token to cancel the operation. - /// The resulting enabled state. - public Task SetSceneItemEnabledAsync( - string sceneName, - int sceneItemId, - bool? isEnabled = null, - CancellationToken cancellationToken = default - ) => - client.SceneItems.SetSceneItemEnabledAsync(sceneName, - (double)sceneItemId, - isEnabled, - cancellationToken - ); + /// + /// Sets or toggles a scene item's enabled state using an integer item id. + /// + /// The name of the scene containing the item. + /// The numeric id of the scene item. + /// The desired state, or to toggle. + /// A token to cancel the operation. + /// The resulting enabled state. + public Task SetSceneItemEnabledAsync( + string sceneName, + int sceneItemId, + bool? isEnabled = null, + CancellationToken cancellationToken = default + ) => + client.SceneItems.SetSceneItemEnabledAsync( + sceneName, + (double)sceneItemId, + isEnabled, + cancellationToken + ); - /// - /// Returns the scene item id for a source within a scene as an , or - /// when the scene does not contain it. - /// - /// The name of the scene to search. - /// The name of the source to locate. - /// A token to cancel the operation. - public async Task FindSceneItemIdInt32Async( - string sceneName, - string sourceName, - CancellationToken cancellationToken = default - ) - { - double? id = await client - .SceneItems.FindSceneItemIdAsync(sceneName, sourceName, cancellationToken) - .ConfigureAwait(false); - return id is null ? null : checked((int)id.Value); - } + /// + /// Returns the scene item id for a source within a scene as an , or + /// when the scene does not contain it. + /// + /// The name of the scene to search. + /// The name of the source to locate. + /// A token to cancel the operation. + public async Task FindSceneItemIdInt32Async( + string sceneName, + string sourceName, + CancellationToken cancellationToken = default + ) + { + double? id = await client + .SceneItems.FindSceneItemIdAsync(sceneName, sourceName, cancellationToken) + .ConfigureAwait(false); + return id is null ? null : checked((int)id.Value); + } } diff --git a/ObsWebSocket.Core/Groups/ScenesGroup.cs b/ObsWebSocket.Core/Groups/ScenesGroup.cs index cf30c6b..6ead653 100644 --- a/ObsWebSocket.Core/Groups/ScenesGroup.cs +++ b/ObsWebSocket.Core/Groups/ScenesGroup.cs @@ -1,11 +1,11 @@ -using Microsoft.Extensions.Logging; using System.Text.Json; using System.Text.Json.Serialization.Metadata; +using Microsoft.Extensions.Logging; using ObsWebSocket.Core.Events; using ObsWebSocket.Core.Events.Generated; +using ObsWebSocket.Core.Networking; using ObsWebSocket.Core.Protocol; using ObsWebSocket.Core.Protocol.Common; -using ObsWebSocket.Core.Networking; using ObsWebSocket.Core.Protocol.Common.FilterSettings; using ObsWebSocket.Core.Protocol.Common.InputSettings; using ObsWebSocket.Core.Protocol.Generated; @@ -30,7 +30,8 @@ public readonly partial struct ScenesGroup /// A token to cancel the operation. /// Thrown if OBS fails to perform any step (e.g., scene/transition not found). /// Thrown if the client is not connected. - public async Task SwitchSceneAsync(string sceneName, + public async Task SwitchSceneAsync( + string sceneName, string? transitionName = null, int? transitionDurationMs = null, bool switchToProgram = true, @@ -106,7 +107,8 @@ await client /// Thrown if the expected event confirming the switch completion is not received within the timeout period. /// Thrown if the client is not connected, or if trying to switch Preview scene when Studio Mode is disabled. /// Thrown if the operation is canceled via the cancellationToken. - public async Task SwitchSceneAndWaitAsync(string sceneName, + public async Task SwitchSceneAndWaitAsync( + string sceneName, string? transitionName = null, int? transitionDurationMs = null, bool switchToProgram = true, @@ -210,75 +212,76 @@ await client // The finally block within WaitForEventAsync handles unsubscribing the temporary event handler. } - /// - /// Checks whether a scene with the given name exists. - /// - /// The scene name to look for. - /// A token to cancel the operation. - /// Thrown if the client is not connected. - public async Task SceneExistsAsync( - string sceneName, - CancellationToken cancellationToken = default - ) - { - ArgumentException.ThrowIfNullOrEmpty(sceneName); - client.EnsureConnected(); + /// + /// Checks whether a scene with the given name exists. + /// + /// The scene name to look for. + /// A token to cancel the operation. + /// Thrown if the client is not connected. + public async Task SceneExistsAsync( + string sceneName, + CancellationToken cancellationToken = default + ) + { + ArgumentException.ThrowIfNullOrEmpty(sceneName); + client.EnsureConnected(); - GetSceneListResponseData? scenes = await client - .Scenes.GetSceneListAsync(new GetSceneListRequestData(), cancellationToken) - .ConfigureAwait(false); + GetSceneListResponseData? scenes = await client + .Scenes.GetSceneListAsync(new GetSceneListRequestData(), cancellationToken) + .ConfigureAwait(false); - return scenes?.Scenes?.Any(s => + return scenes?.Scenes?.Any(s => string.Equals(s.SceneName, sceneName, StringComparison.Ordinal) - ) ?? false; - } + ) + ?? false; + } - /// Switches the Program scene. - /// The scene to switch to. - /// Optional transition to use for this switch only. - /// Optional transition duration for this switch only. - /// A token to cancel the operation. - public Task SwitchProgramSceneAsync( - string sceneName, - string? transitionName = null, - int? transitionDurationMs = null, - CancellationToken cancellationToken = default - ) => - client.Scenes.SwitchSceneAsync( - sceneName, - transitionName, - transitionDurationMs, - switchToProgram: true, - cancellationToken - ); + /// Switches the Program scene. + /// The scene to switch to. + /// Optional transition to use for this switch only. + /// Optional transition duration for this switch only. + /// A token to cancel the operation. + public Task SwitchProgramSceneAsync( + string sceneName, + string? transitionName = null, + int? transitionDurationMs = null, + CancellationToken cancellationToken = default + ) => + client.Scenes.SwitchSceneAsync( + sceneName, + transitionName, + transitionDurationMs, + switchToProgram: true, + cancellationToken + ); - /// Switches the Preview scene. Requires Studio Mode. - /// The scene to switch to. - /// A token to cancel the operation. - public Task SwitchPreviewSceneAsync( - string sceneName, - CancellationToken cancellationToken = default - ) => - client.Scenes.SwitchSceneAsync( - sceneName, - switchToProgram: false, - cancellationToken: cancellationToken - ); + /// Switches the Preview scene. Requires Studio Mode. + /// The scene to switch to. + /// A token to cancel the operation. + public Task SwitchPreviewSceneAsync( + string sceneName, + CancellationToken cancellationToken = default + ) => + client.Scenes.SwitchSceneAsync( + sceneName, + switchToProgram: false, + cancellationToken: cancellationToken + ); - /// Switches the Program scene and waits for OBS to confirm it. - /// The scene to switch to. - /// How long to wait for confirmation. - /// A token to cancel the operation. - /// Thrown if the confirmation does not arrive in time. - public Task SwitchProgramSceneAndWaitAsync( - string sceneName, - TimeSpan? timeout = null, - CancellationToken cancellationToken = default - ) => - client.Scenes.SwitchSceneAndWaitAsync( - sceneName, - switchToProgram: true, - timeout: timeout, - cancellationToken: cancellationToken - ); + /// Switches the Program scene and waits for OBS to confirm it. + /// The scene to switch to. + /// How long to wait for confirmation. + /// A token to cancel the operation. + /// Thrown if the confirmation does not arrive in time. + public Task SwitchProgramSceneAndWaitAsync( + string sceneName, + TimeSpan? timeout = null, + CancellationToken cancellationToken = default + ) => + client.Scenes.SwitchSceneAndWaitAsync( + sceneName, + switchToProgram: true, + timeout: timeout, + cancellationToken: cancellationToken + ); } diff --git a/ObsWebSocket.Core/Groups/SourcesGroup.cs b/ObsWebSocket.Core/Groups/SourcesGroup.cs index efeb651..8577792 100644 --- a/ObsWebSocket.Core/Groups/SourcesGroup.cs +++ b/ObsWebSocket.Core/Groups/SourcesGroup.cs @@ -1,11 +1,11 @@ -using Microsoft.Extensions.Logging; using System.Text.Json; using System.Text.Json.Serialization.Metadata; +using Microsoft.Extensions.Logging; using ObsWebSocket.Core.Events; using ObsWebSocket.Core.Events.Generated; +using ObsWebSocket.Core.Networking; using ObsWebSocket.Core.Protocol; using ObsWebSocket.Core.Protocol.Common; -using ObsWebSocket.Core.Networking; using ObsWebSocket.Core.Protocol.Common.FilterSettings; using ObsWebSocket.Core.Protocol.Common.InputSettings; using ObsWebSocket.Core.Protocol.Generated; @@ -27,7 +27,8 @@ public readonly partial struct SourcesGroup /// True if a source (input or scene) with the specified name exists, false otherwise. /// Thrown if an unexpected error occurs during API calls. /// Thrown if the client is not connected. - public async Task SourceExistsAsync(string sourceName, + public async Task SourceExistsAsync( + string sourceName, CancellationToken cancellationToken = default ) { @@ -46,7 +47,8 @@ public async Task SourceExistsAsync(string sourceName, if ( inputListResponse?.Inputs?.Any(i => string.Equals(i.InputName, sourceName, StringComparison.Ordinal) - ) ?? false + ) + ?? false ) { return true; @@ -59,7 +61,8 @@ await client .ConfigureAwait(false); return sceneListResponse?.Scenes?.Any(s => string.Equals(s.SceneName, sourceName, StringComparison.Ordinal) - ) ?? false; + ) + ?? false; } catch (ObsWebSocketException ex) { @@ -86,7 +89,8 @@ await client /// A byte array containing the image data, or null if the source was not found or an error occurred. /// Thrown for OBS errors other than 'ResourceNotFound' or Base64 decoding errors. /// Thrown if the client is not connected. - public async Task GetSourceScreenshotBytesAsync(string sourceName, + public async Task GetSourceScreenshotBytesAsync( + string sourceName, string imageFormat = "png", // Common default int? width = null, int? height = null, @@ -175,27 +179,32 @@ await client /// The decoded image bytes, or an empty array if OBS returned no data. /// Thrown if OBS rejects the request. /// Thrown if the client is not connected. - public async Task GetSourceScreenshotOnCanvasBytesAsync(string sourceName, + public async Task GetSourceScreenshotOnCanvasBytesAsync( + string sourceName, string imageFormat = "png", int? width = null, int? height = null, int compressionQuality = -1, string? sourceUuid = null, - CancellationToken cancellationToken = default) + CancellationToken cancellationToken = default + ) { ArgumentException.ThrowIfNullOrEmpty(sourceName); client.EnsureConnected(); - GetSourceScreenshotResponseData? response = await client.Sources.GetSourceScreenshotAsync( - new GetSourceScreenshotRequestData( - imageFormat: imageFormat, - sourceName: sourceName, - sourceUuid: sourceUuid, - imageWidth: width, - imageHeight: height, - imageCompressionQuality: compressionQuality - ), - cancellationToken).ConfigureAwait(false); + GetSourceScreenshotResponseData? response = await client + .Sources.GetSourceScreenshotAsync( + new GetSourceScreenshotRequestData( + imageFormat: imageFormat, + sourceName: sourceName, + sourceUuid: sourceUuid, + imageWidth: width, + imageHeight: height, + imageCompressionQuality: compressionQuality + ), + cancellationToken + ) + .ConfigureAwait(false); string? b64 = response?.ImageData; if (string.IsNullOrEmpty(b64)) @@ -226,30 +235,35 @@ public async Task GetSourceScreenshotOnCanvasBytesAsync(string sourceNam /// A token to cancel the operation. /// Thrown if OBS rejects the request. /// Thrown if the client is not connected. - public async Task SaveSourceScreenshotToFileAsync(string sourceName, + public async Task SaveSourceScreenshotToFileAsync( + string sourceName, string filePath, string imageFormat = "png", int? width = null, int? height = null, int compressionQuality = -1, string? sourceUuid = null, - CancellationToken cancellationToken = default) + CancellationToken cancellationToken = default + ) { ArgumentException.ThrowIfNullOrEmpty(sourceName); ArgumentException.ThrowIfNullOrEmpty(filePath); client.EnsureConnected(); - await client.Sources.SaveSourceScreenshotAsync( - new SaveSourceScreenshotRequestData( - imageFormat: imageFormat, - imageFilePath: filePath, - sourceName: sourceName, - sourceUuid: sourceUuid, - imageWidth: width, - imageHeight: height, - imageCompressionQuality: compressionQuality - ), - cancellationToken).ConfigureAwait(false); + await client + .Sources.SaveSourceScreenshotAsync( + new SaveSourceScreenshotRequestData( + imageFormat: imageFormat, + imageFilePath: filePath, + sourceName: sourceName, + sourceUuid: sourceUuid, + imageWidth: width, + imageHeight: height, + imageCompressionQuality: compressionQuality + ), + cancellationToken + ) + .ConfigureAwait(false); } /// diff --git a/ObsWebSocket.Core/Groups/StreamGroup.cs b/ObsWebSocket.Core/Groups/StreamGroup.cs index d28ee08..74ef2b7 100644 --- a/ObsWebSocket.Core/Groups/StreamGroup.cs +++ b/ObsWebSocket.Core/Groups/StreamGroup.cs @@ -1,11 +1,11 @@ -using Microsoft.Extensions.Logging; using System.Text.Json; using System.Text.Json.Serialization.Metadata; +using Microsoft.Extensions.Logging; using ObsWebSocket.Core.Events; using ObsWebSocket.Core.Events.Generated; +using ObsWebSocket.Core.Networking; using ObsWebSocket.Core.Protocol; using ObsWebSocket.Core.Protocol.Common; -using ObsWebSocket.Core.Networking; using ObsWebSocket.Core.Protocol.Common.FilterSettings; using ObsWebSocket.Core.Protocol.Common.InputSettings; using ObsWebSocket.Core.Protocol.Generated; @@ -21,63 +21,62 @@ public readonly partial struct StreamGroup { private static readonly TimeSpan s_defaultOutputTimeout = TimeSpan.FromSeconds(10); - /// - /// Starts or stops streaming and waits for OBS to confirm the state change. - /// - /// to start streaming; to stop it. - /// Maximum time to wait for the state-change event. Defaults to 10 seconds. - /// A token to cancel the operation. - /// - /// The state reported by the event, or if the timeout elapsed first. - /// - /// Thrown if the client is not connected. - public async Task SetStreamActiveAndWaitAsync( - bool activate, - TimeSpan? timeout = null, - CancellationToken cancellationToken = default - ) - { - client.EnsureConnected(); + /// + /// Starts or stops streaming and waits for OBS to confirm the state change. + /// + /// to start streaming; to stop it. + /// Maximum time to wait for the state-change event. Defaults to 10 seconds. + /// A token to cancel the operation. + /// + /// The state reported by the event, or if the timeout elapsed first. + /// + /// Thrown if the client is not connected. + public async Task SetStreamActiveAndWaitAsync( + bool activate, + TimeSpan? timeout = null, + CancellationToken cancellationToken = default + ) + { + client.EnsureConnected(); - Task waitTask = client.WaitForEventAsync( + Task waitTask = + client.WaitForEventAsync( predicate: _ => true, timeout: timeout ?? s_defaultOutputTimeout, cancellationToken: cancellationToken ); - if (activate) - { - await client.Stream.StartStreamAsync(cancellationToken).ConfigureAwait(false); - } - else - { - await client.Stream.StopStreamAsync(cancellationToken).ConfigureAwait(false); - } - - try - { - StreamStateChangedEventArgs ev = await waitTask.ConfigureAwait(false); - return OutputStateExtensions.FromWireValue(ev.EventData.OutputState); - } - catch (TimeoutException) - { - return null; - } + if (activate) + { + await client.Stream.StartStreamAsync(cancellationToken).ConfigureAwait(false); + } + else + { + await client.Stream.StopStreamAsync(cancellationToken).ConfigureAwait(false); } - /// - /// Returns whether streaming is currently active. - /// - /// A token to cancel the operation. - /// Thrown if the client is not connected. - public async Task IsStreamActiveAsync( - CancellationToken cancellationToken = default - ) + try { - client.EnsureConnected(); - GetStreamStatusResponseData? status = await client - .Stream.GetStreamStatusAsync(cancellationToken) - .ConfigureAwait(false); - return status?.OutputActive ?? false; + StreamStateChangedEventArgs ev = await waitTask.ConfigureAwait(false); + return OutputStateExtensions.FromWireValue(ev.EventData.OutputState); } + catch (TimeoutException) + { + return null; + } + } + + /// + /// Returns whether streaming is currently active. + /// + /// A token to cancel the operation. + /// Thrown if the client is not connected. + public async Task IsStreamActiveAsync(CancellationToken cancellationToken = default) + { + client.EnsureConnected(); + GetStreamStatusResponseData? status = await client + .Stream.GetStreamStatusAsync(cancellationToken) + .ConfigureAwait(false); + return status?.OutputActive ?? false; + } } diff --git a/ObsWebSocket.Core/Groups/TransitionsGroup.cs b/ObsWebSocket.Core/Groups/TransitionsGroup.cs index e426ff5..15bd666 100644 --- a/ObsWebSocket.Core/Groups/TransitionsGroup.cs +++ b/ObsWebSocket.Core/Groups/TransitionsGroup.cs @@ -1,11 +1,11 @@ -using Microsoft.Extensions.Logging; using System.Text.Json; using System.Text.Json.Serialization.Metadata; +using Microsoft.Extensions.Logging; using ObsWebSocket.Core.Events; using ObsWebSocket.Core.Events.Generated; +using ObsWebSocket.Core.Networking; using ObsWebSocket.Core.Protocol; using ObsWebSocket.Core.Protocol.Common; -using ObsWebSocket.Core.Networking; using ObsWebSocket.Core.Protocol.Common.FilterSettings; using ObsWebSocket.Core.Protocol.Common.InputSettings; using ObsWebSocket.Core.Protocol.Generated; @@ -28,7 +28,8 @@ public readonly partial struct TransitionsGroup /// The deserialized transition settings, or if no settings are present. /// Thrown if OBS returns an error or serialization fails. /// Thrown if the client is not connected. - public async Task GetCurrentSceneTransitionSettingsAsync(JsonTypeInfo typeInfo, + public async Task GetCurrentSceneTransitionSettingsAsync( + JsonTypeInfo typeInfo, CancellationToken cancellationToken = default ) where T : class @@ -40,7 +41,9 @@ public readonly partial struct TransitionsGroup .Transitions.GetCurrentSceneTransitionAsync(cancellationToken: cancellationToken) .ConfigureAwait(false); - return response?.TransitionSettings is not { } element ? null : JsonSerializer.Deserialize(element, typeInfo); + return response?.TransitionSettings is not { } element + ? null + : JsonSerializer.Deserialize(element, typeInfo); } /// @@ -51,12 +54,16 @@ public readonly partial struct TransitionsGroup /// The deserialized transition settings, or if no settings are present. /// Thrown if the type is not registered, OBS returns an error, or serialization fails. /// Thrown if the client is not connected. - public Task GetCurrentSceneTransitionSettingsAsync(CancellationToken cancellationToken = default + public Task GetCurrentSceneTransitionSettingsAsync( + CancellationToken cancellationToken = default ) where T : class { JsonTypeInfo typeInfo = ObsWebSocketClientHelpers.GetRegisteredTypeInfo(); - return client.Transitions.GetCurrentSceneTransitionSettingsAsync(typeInfo, cancellationToken); + return client.Transitions.GetCurrentSceneTransitionSettingsAsync( + typeInfo, + cancellationToken + ); } /// @@ -69,7 +76,8 @@ public readonly partial struct TransitionsGroup /// A token to cancel the operation. /// Thrown if OBS returns an error or serialization fails. /// Thrown if the client is not connected. - public async Task SetCurrentSceneTransitionSettingsAsync(T settings, + public async Task SetCurrentSceneTransitionSettingsAsync( + T settings, JsonTypeInfo typeInfo, bool? overlay = true, CancellationToken cancellationToken = default @@ -82,7 +90,8 @@ public async Task SetCurrentSceneTransitionSettingsAsync(T settings, JsonElement settingsElement = JsonSerializer.SerializeToElement(settings, typeInfo); await client - .Transitions.SetCurrentSceneTransitionSettingsAsync(new SetCurrentSceneTransitionSettingsRequestData( + .Transitions.SetCurrentSceneTransitionSettingsAsync( + new SetCurrentSceneTransitionSettingsRequestData( transitionSettings: settingsElement, overlay: overlay ), @@ -100,13 +109,19 @@ await client /// A token to cancel the operation. /// Thrown if the type is not registered, OBS returns an error, or serialization fails. /// Thrown if the client is not connected. - public Task SetCurrentSceneTransitionSettingsAsync(T settings, + public Task SetCurrentSceneTransitionSettingsAsync( + T settings, bool? overlay = true, CancellationToken cancellationToken = default ) where T : class { JsonTypeInfo typeInfo = ObsWebSocketClientHelpers.GetRegisteredTypeInfo(); - return client.Transitions.SetCurrentSceneTransitionSettingsAsync(settings, typeInfo, overlay, cancellationToken); + return client.Transitions.SetCurrentSceneTransitionSettingsAsync( + settings, + typeInfo, + overlay, + cancellationToken + ); } } diff --git a/ObsWebSocket.Core/ObsBatchBuilder.cs b/ObsWebSocket.Core/ObsBatchBuilder.cs index fd3713e..18fe763 100644 --- a/ObsWebSocket.Core/ObsBatchBuilder.cs +++ b/ObsWebSocket.Core/ObsBatchBuilder.cs @@ -95,5 +95,4 @@ internal int AddRequest(string requestType, object? requestData) /// Returns the accumulated items as the list takes. /// public List Build() => [.. _items]; - } diff --git a/ObsWebSocket.Core/ObsWebSocket.Core.csproj b/ObsWebSocket.Core/ObsWebSocket.Core.csproj index f3cb954..bbeb550 100644 --- a/ObsWebSocket.Core/ObsWebSocket.Core.csproj +++ b/ObsWebSocket.Core/ObsWebSocket.Core.csproj @@ -31,18 +31,35 @@ true - true + true - + - + - - + + @@ -61,23 +78,60 @@ $([System.IO.Path]::GetFullPath('$(MSBuildThisFileDirectory)Generated')) $([System.IO.Path]::GetFullPath('$(MSBuildThisFileDirectory)Generated\Serialization\ObsWebSocketJsonContext.g.cs')) - - + + - + - + true - - - + + + - - - + + + diff --git a/ObsWebSocket.Core/ObsWebSocketClient.cs b/ObsWebSocket.Core/ObsWebSocketClient.cs index 4bf4694..fe52b64 100644 --- a/ObsWebSocket.Core/ObsWebSocketClient.cs +++ b/ObsWebSocket.Core/ObsWebSocketClient.cs @@ -113,8 +113,12 @@ private readonly ConcurrentDictionary< TaskCreationOptions.RunContinuationsAsynchronously ); - private static readonly JsonSerializerOptions s_payloadJsonOptions = - ObsWebSocket.Core.Serialization.ObsWebSocketJsonContext.Default.Options; + private static readonly JsonSerializerOptions s_payloadJsonOptions = ObsWebSocket + .Core + .Serialization + .ObsWebSocketJsonContext + .Default + .Options; #endregion #region Properties @@ -208,7 +212,10 @@ public async Task ConnectAsync(CancellationToken cancellationToken = default) options.Value.HandshakeTimeoutMs * 2, DefaultRequestTimeoutMs ); // Be generous - linkedTimeoutCts.CancelAfterUsing(_timeProvider, TimeSpan.FromMilliseconds(overallTimeout)); + linkedTimeoutCts.CancelAfterUsing( + _timeProvider, + TimeSpan.FromMilliseconds(overallTimeout) + ); await currentInitialConnectionTcs .Task.WaitAsync(linkedTimeoutCts.Token) @@ -640,7 +647,11 @@ await SendMessageAsync( (baseTimeout * DefaultBatchTimeoutMultiplier) + (baseTimeout * requestPayloads.Count / 2) ); - _logger.LogWaitingForBatchResponseTimeoutMs(batchRequestId, requestPayloads.Count, effectiveTimeout); + _logger.LogWaitingForBatchResponseTimeoutMs( + batchRequestId, + requestPayloads.Count, + effectiveTimeout + ); object responseObj = await WaitForResponseAsync( tcs, @@ -825,7 +836,11 @@ CancellationToken externalCancellationToken TimeSpan backoff = await _reconnectDelays .GetDelayAsync(attempt - 2, loopToken) .ConfigureAwait(false); - _logger.LogReconnectingAttemptAfterMs(attempt, maxAttempts < 0 ? "Infinite" : maxAttempts.ToString(), (int)backoff.TotalMilliseconds); + _logger.LogReconnectingAttemptAfterMs( + attempt, + maxAttempts < 0 ? "Infinite" : maxAttempts.ToString(), + (int)backoff.TotalMilliseconds + ); _metrics.Reconnects.Add(1); await Task.Delay(backoff, _timeProvider, loopToken).ConfigureAwait(false); } @@ -894,7 +909,10 @@ await TryConnectAndIdentifyAsync(options, currentWebSocket, attempt, loopToken) } catch (ConnectionAttemptFailedException connEx) { - _logger.LogConnectAttemptFailedRetrying(connEx.InnerException ?? connEx, attempt); + _logger.LogConnectAttemptFailedRetrying( + connEx.InnerException ?? connEx, + attempt + ); attemptException = connEx; RaiseConnectionFailedEvent(options.ServerUri!, attempt, connEx); // Only fail initial TCS if retries are disabled or exhausted on the *first* attempt @@ -908,7 +926,10 @@ await TryConnectAndIdentifyAsync(options, currentWebSocket, attempt, loopToken) } catch (WebSocketException wsEx) { - _logger.LogWebsocketexceptionDuringConnectionReceiveAttemptRetrying(wsEx, attempt); + _logger.LogWebsocketexceptionDuringConnectionReceiveAttemptRetrying( + wsEx, + attempt + ); attemptException = wsEx; RaiseConnectionFailedEvent(options.ServerUri!, attempt, wsEx); if ( @@ -968,7 +989,9 @@ await TryConnectAndIdentifyAsync(options, currentWebSocket, attempt, loopToken) if (isConnectedThisAttempt && attemptException != null) { - _logger.LogConnectionLostDuringConnectedStateDueTo(attemptException.GetType().Name); + _logger.LogConnectionLostDuringConnectedStateDueTo( + attemptException.GetType().Name + ); } } } @@ -1028,7 +1051,10 @@ CancellationToken ct } catch (ArgumentException ex) { - _logger.LogAttemptedDuplicateSubprotocolAdd(ex, _serializer.ProtocolSubProtocol); + _logger.LogAttemptedDuplicateSubprotocolAdd( + ex, + _serializer.ProtocolSubProtocol + ); } } @@ -1056,7 +1082,10 @@ await ws.ConnectAsync(options.ServerUri!, linkedConnectCts.Token) ); } - _logger.LogAttemptWebsocketConnectionEstablishedProtocol(attempt, ws.SubProtocol ?? "(None)"); + _logger.LogAttemptWebsocketConnectionEstablishedProtocol( + attempt, + ws.SubProtocol ?? "(None)" + ); // --- Start Receive Loop *before* Handshake --- localReceiveCts = CancellationTokenSource.CreateLinkedTokenSource(ct); @@ -1100,11 +1129,16 @@ await ws.ConnectAsync(options.ServerUri!, linkedConnectCts.Token) ); } - EventSubscription requestedEventSubs = options.EventSubscriptions ?? EventSubscription.All; + EventSubscription requestedEventSubs = + options.EventSubscriptions ?? EventSubscription.All; await SendMessageAsync( WebSocketOpCode.Identify, - new IdentifyPayload(helloPayload.RpcVersion, authResponse, (uint)requestedEventSubs), + new IdentifyPayload( + helloPayload.RpcVersion, + authResponse, + (uint)requestedEventSubs + ), ct ) .ConfigureAwait(false); @@ -1123,7 +1157,10 @@ await SendMessageAsync( "Identified" ); - _logger.LogAttemptReceivedIdentifiedNegotiatedRpcVersion(attempt, identifiedPayload.NegotiatedRpcVersion); + _logger.LogAttemptReceivedIdentifiedNegotiatedRpcVersion( + attempt, + identifiedPayload.NegotiatedRpcVersion + ); NegotiatedRpcVersion = identifiedPayload.NegotiatedRpcVersion; CurrentEventSubscriptions = requestedEventSubs; // @@ -1206,7 +1243,9 @@ private async Task ReceiveLoopAsync(CancellationToken cancellationToken) ); } - _logger.LogReceiveLoopStartingForWebsocket(RuntimeHelpers.GetHashCode(currentWebSocket)); + _logger.LogReceiveLoopStartingForWebsocket( + RuntimeHelpers.GetHashCode(currentWebSocket) + ); while (!cancellationToken.IsCancellationRequested) { @@ -1279,7 +1318,9 @@ await bufferStream finally { ArrayPool.Shared.Return(buffer); - _logger.LogReceiveLoopFinishedForWebsocket(RuntimeHelpers.GetHashCode(currentWebSocket)); + _logger.LogReceiveLoopFinishedForWebsocket( + RuntimeHelpers.GetHashCode(currentWebSocket) + ); } } @@ -1331,7 +1372,10 @@ await ws.CloseOutputAsync( /// Cleans up current connection resources and fails pending tasks. private void CleanupConnectionOnly(Exception reasonException) { - _logger.LogCleanupconnectiononlyDueTo(reasonException.GetType().Name, reasonException.Message); + _logger.LogCleanupconnectiononlyDueTo( + reasonException.GetType().Name, + reasonException.Message + ); IsConnected = false; NegotiatedRpcVersion = null; @@ -1467,7 +1511,9 @@ private void ProcessIncomingMessage(object messageObject) (opCode, payloadData) = (msgpackMsg.Op, msgpackMsg.D); break; default: - _logger.LogUnexpectedIncomingMessageTypeEncountered(messageObject.GetType().FullName); + _logger.LogUnexpectedIncomingMessageTypeEncountered( + messageObject.GetType().FullName + ); return; } @@ -1515,7 +1561,10 @@ private void HandleRequestResponseMessage(object? payloadData) return; } - _logger.LogProcessingRequestresponseForRequestidStatus(response.RequestId, response.RequestStatus.Result); + _logger.LogProcessingRequestresponseForRequestidStatus( + response.RequestId, + response.RequestStatus.Result + ); if ( _pendingRequests.TryRemove( response.RequestId, @@ -1532,7 +1581,10 @@ out TaskCompletionSource? tcs } catch (Exception ex) { - _logger.LogExceptionDuringProcessingOfRequestresponsePayload(ex, payloadData?.ToString()); + _logger.LogExceptionDuringProcessingOfRequestresponsePayload( + ex, + payloadData?.ToString() + ); } } @@ -1555,7 +1607,10 @@ private void HandleRequestBatchResponseMessage(object? payloadData) return; } - _logger.LogProcessingRequestbatchresponseForRequestidResults(response.RequestId, response.Results?.Count ?? 0); + _logger.LogProcessingRequestbatchresponseForRequestidResults( + response.RequestId, + response.Results?.Count ?? 0 + ); if ( _pendingBatchRequests.TryRemove( response.RequestId, @@ -1572,7 +1627,10 @@ out TaskCompletionSource? tcs } catch (Exception ex) { - _logger.LogExceptionDuringProcessingOfRequestbatchresponsePayload(ex, payloadData?.ToString()); + _logger.LogExceptionDuringProcessingOfRequestbatchresponsePayload( + ex, + payloadData?.ToString() + ); } } @@ -1610,7 +1668,10 @@ private void HandleEventMessage(object? payloadData) } catch (Exception ex) { - _logger.LogExceptionOccurredWithinTheEventHandlerFor(ex, eventPayloadBase.EventType); + _logger.LogExceptionOccurredWithinTheEventHandlerFor( + ex, + eventPayloadBase.EventType + ); } } else @@ -1620,7 +1681,11 @@ private void HandleEventMessage(object? payloadData) } catch (Exception ex) { - _logger.LogCriticalExceptionDuringEventHandlingFor(ex, eventPayloadBase?.EventType ?? "Unknown Type", payloadData?.ToString()); + _logger.LogCriticalExceptionDuringEventHandlingFor( + ex, + eventPayloadBase?.EventType ?? "Unknown Type", + payloadData?.ToString() + ); } } @@ -1669,10 +1734,7 @@ Action invoker TPayload? payload = _serializer.DeserializePayload(rawData); if (payload is not null) { - _metrics.EventsReceived.Add( - 1, - new TagList { { "obsws.event_type", eventType } } - ); + _metrics.EventsReceived.Add(1, new TagList { { "obsws.event_type", eventType } }); invoker(argsFactory(payload)); } else diff --git a/ObsWebSocket.Core/ObsWebSocketClientLog.cs b/ObsWebSocket.Core/ObsWebSocketClientLog.cs index 3a52ccd..63d970d 100644 --- a/ObsWebSocket.Core/ObsWebSocketClientLog.cs +++ b/ObsWebSocket.Core/ObsWebSocketClientLog.cs @@ -9,202 +9,909 @@ namespace ObsWebSocket.Core; /// Source-generated log messages. internal static partial class ObsWebSocketClientLog { - [LoggerMessage(EventId = 1, Level = LogLevel.Information, Message = "Starting connection sequence for {Uri}...")] + [LoggerMessage( + EventId = 1, + Level = LogLevel.Information, + Message = "Starting connection sequence for {Uri}..." + )] public static partial void LogStartingConnectionSequenceFor(this ILogger logger, Uri? uri); - [LoggerMessage(EventId = 2, Level = LogLevel.Information, Message = "ConnectAsync initial connection confirmed successfully.")] - public static partial void LogConnectasyncInitialConnectionConfirmedSuccessfully(this ILogger logger); - [LoggerMessage(EventId = 3, Level = LogLevel.Error, Message = "ConnectAsync failed to establish initial connection.")] - public static partial void LogConnectasyncFailedToEstablishInitialConnection(this ILogger logger, Exception exception); - [LoggerMessage(EventId = 4, Level = LogLevel.Debug, Message = "Waiting for Identified after Reidentify (Timeout: {TimeoutMs}ms)...")] - public static partial void LogWaitingForIdentifiedAfterReidentifyTimeoutMs(this ILogger logger, int timeoutMs); - [LoggerMessage(EventId = 5, Level = LogLevel.Information, Message = "Re-identification successful. RPC Version: {RpcVersion}")] - public static partial void LogReIdentificationSuccessfulRpcVersion(this ILogger logger, int rpcVersion); + + [LoggerMessage( + EventId = 2, + Level = LogLevel.Information, + Message = "ConnectAsync initial connection confirmed successfully." + )] + public static partial void LogConnectasyncInitialConnectionConfirmedSuccessfully( + this ILogger logger + ); + + [LoggerMessage( + EventId = 3, + Level = LogLevel.Error, + Message = "ConnectAsync failed to establish initial connection." + )] + public static partial void LogConnectasyncFailedToEstablishInitialConnection( + this ILogger logger, + Exception exception + ); + + [LoggerMessage( + EventId = 4, + Level = LogLevel.Debug, + Message = "Waiting for Identified after Reidentify (Timeout: {TimeoutMs}ms)..." + )] + public static partial void LogWaitingForIdentifiedAfterReidentifyTimeoutMs( + this ILogger logger, + int timeoutMs + ); + + [LoggerMessage( + EventId = 5, + Level = LogLevel.Information, + Message = "Re-identification successful. RPC Version: {RpcVersion}" + )] + public static partial void LogReIdentificationSuccessfulRpcVersion( + this ILogger logger, + int rpcVersion + ); + [LoggerMessage(EventId = 6, Level = LogLevel.Error, Message = "ReidentifyAsync failed.")] public static partial void LogReidentifyasyncFailed(this ILogger logger, Exception exception); - [LoggerMessage(EventId = 7, Level = LogLevel.Warning, Message = "ReidentifyAsync canceled due to client shutdown.")] + + [LoggerMessage( + EventId = 7, + Level = LogLevel.Warning, + Message = "ReidentifyAsync canceled due to client shutdown." + )] public static partial void LogReidentifyasyncCanceledDueToClientShutdown(this ILogger logger); - [LoggerMessage(EventId = 8, Level = LogLevel.Debug, Message = "Waiting for Response {RequestId} ({RequestType}, Timeout: {TimeoutMs}ms)...")] - public static partial void LogWaitingForResponseTimeoutMs(this ILogger logger, string requestId, string requestType, int timeoutMs); - [LoggerMessage(EventId = 9, Level = LogLevel.Error, Message = "CallAsync failed for {RequestType} ({RequestId})")] - public static partial void LogCallasyncFailedFor(this ILogger logger, Exception exception, string requestType, string requestId); - [LoggerMessage(EventId = 10, Level = LogLevel.Warning, Message = "CallAsync for {RequestType} ({RequestId}) canceled.")] - public static partial void LogCallasyncForCanceled(this ILogger logger, string requestType, string requestId); - [LoggerMessage(EventId = 11, Level = LogLevel.Error, Message = "CallAsyncValue failed for {RequestType} ({RequestId})")] - public static partial void LogCallasyncvalueFailedFor(this ILogger logger, Exception exception, string requestType, string requestId); - [LoggerMessage(EventId = 12, Level = LogLevel.Warning, Message = "CallAsyncValue for {RequestType} ({RequestId}) canceled.")] - public static partial void LogCallasyncvalueForCanceled(this ILogger logger, string requestType, string requestId); + + [LoggerMessage( + EventId = 8, + Level = LogLevel.Debug, + Message = "Waiting for Response {RequestId} ({RequestType}, Timeout: {TimeoutMs}ms)..." + )] + public static partial void LogWaitingForResponseTimeoutMs( + this ILogger logger, + string requestId, + string requestType, + int timeoutMs + ); + + [LoggerMessage( + EventId = 9, + Level = LogLevel.Error, + Message = "CallAsync failed for {RequestType} ({RequestId})" + )] + public static partial void LogCallasyncFailedFor( + this ILogger logger, + Exception exception, + string requestType, + string requestId + ); + + [LoggerMessage( + EventId = 10, + Level = LogLevel.Warning, + Message = "CallAsync for {RequestType} ({RequestId}) canceled." + )] + public static partial void LogCallasyncForCanceled( + this ILogger logger, + string requestType, + string requestId + ); + + [LoggerMessage( + EventId = 11, + Level = LogLevel.Error, + Message = "CallAsyncValue failed for {RequestType} ({RequestId})" + )] + public static partial void LogCallasyncvalueFailedFor( + this ILogger logger, + Exception exception, + string requestType, + string requestId + ); + + [LoggerMessage( + EventId = 12, + Level = LogLevel.Warning, + Message = "CallAsyncValue for {RequestType} ({RequestId}) canceled." + )] + public static partial void LogCallasyncvalueForCanceled( + this ILogger logger, + string requestType, + string requestId + ); + [LoggerMessage(EventId = 13, Level = LogLevel.Warning, Message = "Empty batch request.")] public static partial void LogEmptyBatchRequest(this ILogger logger); - [LoggerMessage(EventId = 14, Level = LogLevel.Debug, Message = "Waiting for Batch Response {BatchRequestId} ({RequestCount}, Timeout: {TimeoutMs}ms)...")] - public static partial void LogWaitingForBatchResponseTimeoutMs(this ILogger logger, string batchRequestId, int requestCount, int timeoutMs); - [LoggerMessage(EventId = 15, Level = LogLevel.Debug, Message = "Received Batch Response {BatchRequestId} ({ResultCount} results).")] - public static partial void LogReceivedBatchResponseResults(this ILogger logger, string batchRequestId, int resultCount); - [LoggerMessage(EventId = 16, Level = LogLevel.Error, Message = "CallBatchAsync failed for batch {BatchRequestId}")] - public static partial void LogCallbatchasyncFailedForBatch(this ILogger logger, Exception exception, string batchRequestId); - [LoggerMessage(EventId = 17, Level = LogLevel.Warning, Message = "CallBatchAsync for {BatchRequestId} canceled.")] - public static partial void LogCallbatchasyncForCanceled(this ILogger logger, string batchRequestId); - [LoggerMessage(EventId = 18, Level = LogLevel.Debug, Message = "DisconnectAsync ignored, already {ConnectionState}.")] - public static partial void LogDisconnectasyncIgnoredAlready(this ILogger logger, ConnectionState connectionState); - [LoggerMessage(EventId = 19, Level = LogLevel.Information, Message = "DisconnectAsync initiating graceful shutdown...")] + + [LoggerMessage( + EventId = 14, + Level = LogLevel.Debug, + Message = "Waiting for Batch Response {BatchRequestId} ({RequestCount}, Timeout: {TimeoutMs}ms)..." + )] + public static partial void LogWaitingForBatchResponseTimeoutMs( + this ILogger logger, + string batchRequestId, + int requestCount, + int timeoutMs + ); + + [LoggerMessage( + EventId = 15, + Level = LogLevel.Debug, + Message = "Received Batch Response {BatchRequestId} ({ResultCount} results)." + )] + public static partial void LogReceivedBatchResponseResults( + this ILogger logger, + string batchRequestId, + int resultCount + ); + + [LoggerMessage( + EventId = 16, + Level = LogLevel.Error, + Message = "CallBatchAsync failed for batch {BatchRequestId}" + )] + public static partial void LogCallbatchasyncFailedForBatch( + this ILogger logger, + Exception exception, + string batchRequestId + ); + + [LoggerMessage( + EventId = 17, + Level = LogLevel.Warning, + Message = "CallBatchAsync for {BatchRequestId} canceled." + )] + public static partial void LogCallbatchasyncForCanceled( + this ILogger logger, + string batchRequestId + ); + + [LoggerMessage( + EventId = 18, + Level = LogLevel.Debug, + Message = "DisconnectAsync ignored, already {ConnectionState}." + )] + public static partial void LogDisconnectasyncIgnoredAlready( + this ILogger logger, + ConnectionState connectionState + ); + + [LoggerMessage( + EventId = 19, + Level = LogLevel.Information, + Message = "DisconnectAsync initiating graceful shutdown..." + )] public static partial void LogDisconnectasyncInitiatingGracefulShutdown(this ILogger logger); - [LoggerMessage(EventId = 20, Level = LogLevel.Debug, Message = "Waiting for connection loop task to complete during disconnect...")] + + [LoggerMessage( + EventId = 20, + Level = LogLevel.Debug, + Message = "Waiting for connection loop task to complete during disconnect..." + )] public static partial void LogWaitingForConnectionLoopTaskToComplete(this ILogger logger); - [LoggerMessage(EventId = 21, Level = LogLevel.Debug, Message = "Connection loop task completed or wait timed out/canceled during DisconnectAsync.")] + + [LoggerMessage( + EventId = 21, + Level = LogLevel.Debug, + Message = "Connection loop task completed or wait timed out/canceled during DisconnectAsync." + )] public static partial void LogConnectionLoopTaskCompletedOrWaitTimed(this ILogger logger); - [LoggerMessage(EventId = 22, Level = LogLevel.Warning, Message = "Wait for connection loop task timed out or canceled during DisconnectAsync.")] + + [LoggerMessage( + EventId = 22, + Level = LogLevel.Warning, + Message = "Wait for connection loop task timed out or canceled during DisconnectAsync." + )] public static partial void LogWaitForConnectionLoopTaskTimedOut(this ILogger logger); - [LoggerMessage(EventId = 23, Level = LogLevel.Error, Message = "Exception from connection loop task during DisconnectAsync.")] - public static partial void LogExceptionFromConnectionLoopTaskDuringDisconnectasync(this ILogger logger, Exception exception); + + [LoggerMessage( + EventId = 23, + Level = LogLevel.Error, + Message = "Exception from connection loop task during DisconnectAsync." + )] + public static partial void LogExceptionFromConnectionLoopTaskDuringDisconnectasync( + this ILogger logger, + Exception exception + ); + [LoggerMessage(EventId = 24, Level = LogLevel.Debug, Message = "DisposeAsync called.")] public static partial void LogDisposeasyncCalled(this ILogger logger); + [LoggerMessage(EventId = 25, Level = LogLevel.Debug, Message = "DisposeAsync completed.")] public static partial void LogDisposeasyncCompleted(this ILogger logger); - [LoggerMessage(EventId = 26, Level = LogLevel.Warning, Message = "Max reconnect attempts ({MaxAttempts}) reached or auto-reconnect disabled. Stopping.")] - public static partial void LogMaxReconnectAttemptsReachedOrAutoReconnect(this ILogger logger, int maxAttempts); - [LoggerMessage(EventId = 27, Level = LogLevel.Information, Message = "Reconnecting attempt {AttemptNumber}/{MaxAttempts} after {DelayMs}ms...")] - public static partial void LogReconnectingAttemptAfterMs(this ILogger logger, int attemptNumber, string maxAttempts, int delayMs); - [LoggerMessage(EventId = 28, Level = LogLevel.Information, Message = "Connected (Attempt {AttemptNumber}), but disconnect requested. Aborting.")] - public static partial void LogConnectedAttemptButDisconnectRequestedAborting(this ILogger logger, int attemptNumber); - [LoggerMessage(EventId = 29, Level = LogLevel.Debug, Message = "[Attempt:{AttemptNumber}] Handshake complete, receive loop is running.")] - public static partial void LogAttemptHandshakeCompleteReceiveLoopIsRunning(this ILogger logger, int attemptNumber); - [LoggerMessage(EventId = 30, Level = LogLevel.Information, Message = "Successfully connected and identified (Attempt {AttemptNumber}).")] - public static partial void LogSuccessfullyConnectedAndIdentifiedAttempt(this ILogger logger, int attemptNumber); - [LoggerMessage(EventId = 31, Level = LogLevel.Debug, Message = "Connection established. Waiting for receive loop completion or cancellation...")] - public static partial void LogConnectionEstablishedWaitingForReceiveLoopCompletion(this ILogger logger); - [LoggerMessage(EventId = 32, Level = LogLevel.Debug, Message = "Receive loop task completed while connected.")] + + [LoggerMessage( + EventId = 26, + Level = LogLevel.Warning, + Message = "Max reconnect attempts ({MaxAttempts}) reached or auto-reconnect disabled. Stopping." + )] + public static partial void LogMaxReconnectAttemptsReachedOrAutoReconnect( + this ILogger logger, + int maxAttempts + ); + + [LoggerMessage( + EventId = 27, + Level = LogLevel.Information, + Message = "Reconnecting attempt {AttemptNumber}/{MaxAttempts} after {DelayMs}ms..." + )] + public static partial void LogReconnectingAttemptAfterMs( + this ILogger logger, + int attemptNumber, + string maxAttempts, + int delayMs + ); + + [LoggerMessage( + EventId = 28, + Level = LogLevel.Information, + Message = "Connected (Attempt {AttemptNumber}), but disconnect requested. Aborting." + )] + public static partial void LogConnectedAttemptButDisconnectRequestedAborting( + this ILogger logger, + int attemptNumber + ); + + [LoggerMessage( + EventId = 29, + Level = LogLevel.Debug, + Message = "[Attempt:{AttemptNumber}] Handshake complete, receive loop is running." + )] + public static partial void LogAttemptHandshakeCompleteReceiveLoopIsRunning( + this ILogger logger, + int attemptNumber + ); + + [LoggerMessage( + EventId = 30, + Level = LogLevel.Information, + Message = "Successfully connected and identified (Attempt {AttemptNumber})." + )] + public static partial void LogSuccessfullyConnectedAndIdentifiedAttempt( + this ILogger logger, + int attemptNumber + ); + + [LoggerMessage( + EventId = 31, + Level = LogLevel.Debug, + Message = "Connection established. Waiting for receive loop completion or cancellation..." + )] + public static partial void LogConnectionEstablishedWaitingForReceiveLoopCompletion( + this ILogger logger + ); + + [LoggerMessage( + EventId = 32, + Level = LogLevel.Debug, + Message = "Receive loop task completed while connected." + )] public static partial void LogReceiveLoopTaskCompletedWhileConnected(this ILogger logger); - [LoggerMessage(EventId = 33, Level = LogLevel.Information, Message = "Connection loop canceled (Attempt {AttemptNumber}).")] - public static partial void LogConnectionLoopCanceledAttempt(this ILogger logger, int attemptNumber); - [LoggerMessage(EventId = 34, Level = LogLevel.Error, Message = "Authentication failed (Attempt {AttemptNumber}). Stopping.")] - public static partial void LogAuthenticationFailedAttemptStopping(this ILogger logger, Exception exception, int attemptNumber); - [LoggerMessage(EventId = 35, Level = LogLevel.Warning, Message = "Connect attempt {AttemptNumber} failed. Retrying...")] - public static partial void LogConnectAttemptFailedRetrying(this ILogger logger, Exception exception, int attemptNumber); - [LoggerMessage(EventId = 36, Level = LogLevel.Warning, Message = "WebSocketException during connection/receive (Attempt {AttemptNumber}). Retrying...")] - public static partial void LogWebsocketexceptionDuringConnectionReceiveAttemptRetrying(this ILogger logger, Exception exception, int attemptNumber); - [LoggerMessage(EventId = 37, Level = LogLevel.Error, Message = "Unexpected error in connection loop (Attempt {AttemptNumber}). Retrying...")] - public static partial void LogUnexpectedErrorInConnectionLoopAttemptRetrying(this ILogger logger, Exception exception, int attemptNumber); - [LoggerMessage(EventId = 38, Level = LogLevel.Warning, Message = "Connection lost during connected state due to: {ErrorType}")] - public static partial void LogConnectionLostDuringConnectedStateDueTo(this ILogger logger, string errorType); - [LoggerMessage(EventId = 39, Level = LogLevel.Critical, Message = "Catastrophic error in ConnectionLoopAsync.")] - public static partial void LogCatastrophicErrorInConnectionloopasync(this ILogger logger, Exception exception); - [LoggerMessage(EventId = 40, Level = LogLevel.Information, Message = "Exited connection loop. Finalizing state.")] + + [LoggerMessage( + EventId = 33, + Level = LogLevel.Information, + Message = "Connection loop canceled (Attempt {AttemptNumber})." + )] + public static partial void LogConnectionLoopCanceledAttempt( + this ILogger logger, + int attemptNumber + ); + + [LoggerMessage( + EventId = 34, + Level = LogLevel.Error, + Message = "Authentication failed (Attempt {AttemptNumber}). Stopping." + )] + public static partial void LogAuthenticationFailedAttemptStopping( + this ILogger logger, + Exception exception, + int attemptNumber + ); + + [LoggerMessage( + EventId = 35, + Level = LogLevel.Warning, + Message = "Connect attempt {AttemptNumber} failed. Retrying..." + )] + public static partial void LogConnectAttemptFailedRetrying( + this ILogger logger, + Exception exception, + int attemptNumber + ); + + [LoggerMessage( + EventId = 36, + Level = LogLevel.Warning, + Message = "WebSocketException during connection/receive (Attempt {AttemptNumber}). Retrying..." + )] + public static partial void LogWebsocketexceptionDuringConnectionReceiveAttemptRetrying( + this ILogger logger, + Exception exception, + int attemptNumber + ); + + [LoggerMessage( + EventId = 37, + Level = LogLevel.Error, + Message = "Unexpected error in connection loop (Attempt {AttemptNumber}). Retrying..." + )] + public static partial void LogUnexpectedErrorInConnectionLoopAttemptRetrying( + this ILogger logger, + Exception exception, + int attemptNumber + ); + + [LoggerMessage( + EventId = 38, + Level = LogLevel.Warning, + Message = "Connection lost during connected state due to: {ErrorType}" + )] + public static partial void LogConnectionLostDuringConnectedStateDueTo( + this ILogger logger, + string errorType + ); + + [LoggerMessage( + EventId = 39, + Level = LogLevel.Critical, + Message = "Catastrophic error in ConnectionLoopAsync." + )] + public static partial void LogCatastrophicErrorInConnectionloopasync( + this ILogger logger, + Exception exception + ); + + [LoggerMessage( + EventId = 40, + Level = LogLevel.Information, + Message = "Exited connection loop. Finalizing state." + )] public static partial void LogExitedConnectionLoopFinalizingState(this ILogger logger); - [LoggerMessage(EventId = 41, Level = LogLevel.Warning, Message = "Attempted duplicate subprotocol add: {SubProtocol}")] - public static partial void LogAttemptedDuplicateSubprotocolAdd(this ILogger logger, Exception exception, string subProtocol); - [LoggerMessage(EventId = 42, Level = LogLevel.Debug, Message = "[Attempt:{AttemptNumber}] Connecting...")] + + [LoggerMessage( + EventId = 41, + Level = LogLevel.Warning, + Message = "Attempted duplicate subprotocol add: {SubProtocol}" + )] + public static partial void LogAttemptedDuplicateSubprotocolAdd( + this ILogger logger, + Exception exception, + string subProtocol + ); + + [LoggerMessage( + EventId = 42, + Level = LogLevel.Debug, + Message = "[Attempt:{AttemptNumber}] Connecting..." + )] public static partial void LogAttemptConnecting(this ILogger logger, int attemptNumber); - [LoggerMessage(EventId = 43, Level = LogLevel.Information, Message = "[Attempt:{AttemptNumber}] WebSocket connection established. Protocol: {SubProtocol}")] - public static partial void LogAttemptWebsocketConnectionEstablishedProtocol(this ILogger logger, int attemptNumber, string subProtocol); - [LoggerMessage(EventId = 44, Level = LogLevel.Debug, Message = "[Attempt:{AttemptNumber}] Receive loop started for handshake.")] - public static partial void LogAttemptReceiveLoopStartedForHandshake(this ILogger logger, int attemptNumber); - [LoggerMessage(EventId = 45, Level = LogLevel.Debug, Message = "[Attempt:{AttemptNumber}] Waiting for Hello...")] + + [LoggerMessage( + EventId = 43, + Level = LogLevel.Information, + Message = "[Attempt:{AttemptNumber}] WebSocket connection established. Protocol: {SubProtocol}" + )] + public static partial void LogAttemptWebsocketConnectionEstablishedProtocol( + this ILogger logger, + int attemptNumber, + string subProtocol + ); + + [LoggerMessage( + EventId = 44, + Level = LogLevel.Debug, + Message = "[Attempt:{AttemptNumber}] Receive loop started for handshake." + )] + public static partial void LogAttemptReceiveLoopStartedForHandshake( + this ILogger logger, + int attemptNumber + ); + + [LoggerMessage( + EventId = 45, + Level = LogLevel.Debug, + Message = "[Attempt:{AttemptNumber}] Waiting for Hello..." + )] public static partial void LogAttemptWaitingForHello(this ILogger logger, int attemptNumber); - [LoggerMessage(EventId = 46, Level = LogLevel.Debug, Message = "[Attempt:{AttemptNumber}] Received Hello. RPC Version: {RpcVersion}")] - public static partial void LogAttemptReceivedHelloRpcVersion(this ILogger logger, int attemptNumber, int rpcVersion); - [LoggerMessage(EventId = 47, Level = LogLevel.Debug, Message = "[Attempt:{AttemptNumber}] Authentication required.")] - public static partial void LogAttemptAuthenticationRequired(this ILogger logger, int attemptNumber); - [LoggerMessage(EventId = 48, Level = LogLevel.Debug, Message = "[Attempt:{AttemptNumber}] Waiting for Identified...")] - public static partial void LogAttemptWaitingForIdentified(this ILogger logger, int attemptNumber); - [LoggerMessage(EventId = 49, Level = LogLevel.Debug, Message = "[Attempt:{AttemptNumber}] Received Identified. Negotiated RPC Version: {NegotiatedRpcVersion}")] - public static partial void LogAttemptReceivedIdentifiedNegotiatedRpcVersion(this ILogger logger, int attemptNumber, int negotiatedRpcVersion); - [LoggerMessage(EventId = 50, Level = LogLevel.Debug, Message = "Receive loop starting for WebSocket {HashCode}.")] - public static partial void LogReceiveLoopStartingForWebsocket(this ILogger logger, int hashCode); - [LoggerMessage(EventId = 51, Level = LogLevel.Warning, Message = "WebSocket state changed to {WebSocketState} during receive loop.")] - public static partial void LogWebsocketStateChangedToDuringReceiveLoop(this ILogger logger, WebSocketState webSocketState); + + [LoggerMessage( + EventId = 46, + Level = LogLevel.Debug, + Message = "[Attempt:{AttemptNumber}] Received Hello. RPC Version: {RpcVersion}" + )] + public static partial void LogAttemptReceivedHelloRpcVersion( + this ILogger logger, + int attemptNumber, + int rpcVersion + ); + + [LoggerMessage( + EventId = 47, + Level = LogLevel.Debug, + Message = "[Attempt:{AttemptNumber}] Authentication required." + )] + public static partial void LogAttemptAuthenticationRequired( + this ILogger logger, + int attemptNumber + ); + + [LoggerMessage( + EventId = 48, + Level = LogLevel.Debug, + Message = "[Attempt:{AttemptNumber}] Waiting for Identified..." + )] + public static partial void LogAttemptWaitingForIdentified( + this ILogger logger, + int attemptNumber + ); + + [LoggerMessage( + EventId = 49, + Level = LogLevel.Debug, + Message = "[Attempt:{AttemptNumber}] Received Identified. Negotiated RPC Version: {NegotiatedRpcVersion}" + )] + public static partial void LogAttemptReceivedIdentifiedNegotiatedRpcVersion( + this ILogger logger, + int attemptNumber, + int negotiatedRpcVersion + ); + + [LoggerMessage( + EventId = 50, + Level = LogLevel.Debug, + Message = "Receive loop starting for WebSocket {HashCode}." + )] + public static partial void LogReceiveLoopStartingForWebsocket( + this ILogger logger, + int hashCode + ); + + [LoggerMessage( + EventId = 51, + Level = LogLevel.Warning, + Message = "WebSocket state changed to {WebSocketState} during receive loop." + )] + public static partial void LogWebsocketStateChangedToDuringReceiveLoop( + this ILogger logger, + WebSocketState webSocketState + ); + [LoggerMessage(EventId = 52, Level = LogLevel.Trace, Message = "Received empty message.")] public static partial void LogReceivedEmptyMessage(this ILogger logger); - [LoggerMessage(EventId = 53, Level = LogLevel.Warning, Message = "Deserialization returned null (Length: {BufferLength}).")] - public static partial void LogDeserializationReturnedNullLength(this ILogger logger, long bufferLength); - [LoggerMessage(EventId = 54, Level = LogLevel.Information, Message = "Receive loop exiting: cancellation requested.")] + + [LoggerMessage( + EventId = 53, + Level = LogLevel.Warning, + Message = "Deserialization returned null (Length: {BufferLength})." + )] + public static partial void LogDeserializationReturnedNullLength( + this ILogger logger, + long bufferLength + ); + + [LoggerMessage( + EventId = 54, + Level = LogLevel.Information, + Message = "Receive loop exiting: cancellation requested." + )] public static partial void LogReceiveLoopExitingCancellationRequested(this ILogger logger); - [LoggerMessage(EventId = 55, Level = LogLevel.Information, Message = "Receive loop cancelled gracefully via token.")] + + [LoggerMessage( + EventId = 55, + Level = LogLevel.Information, + Message = "Receive loop cancelled gracefully via token." + )] public static partial void LogReceiveLoopCancelledGracefullyViaToken(this ILogger logger); - [LoggerMessage(EventId = 56, Level = LogLevel.Warning, Message = "WebSocketException in receive loop (Code: {WebSocketErrorCode}).")] - public static partial void LogWebsocketexceptionInReceiveLoopCode(this ILogger logger, Exception exception, WebSocketError webSocketErrorCode); - [LoggerMessage(EventId = 57, Level = LogLevel.Error, Message = "Unexpected exception in receive loop.")] - public static partial void LogUnexpectedExceptionInReceiveLoop(this ILogger logger, Exception exception); - [LoggerMessage(EventId = 58, Level = LogLevel.Debug, Message = "Receive loop finished for WebSocket {HashCode}.")] - public static partial void LogReceiveLoopFinishedForWebsocket(this ILogger logger, int hashCode); - [LoggerMessage(EventId = 59, Level = LogLevel.Information, Message = "Server acknowledged client closure. Status: {WebSocketCloseStatus}, Desc: {Description}")] - public static partial void LogServerAcknowledgedClientClosureStatusDesc(this ILogger logger, WebSocketCloseStatus? webSocketCloseStatus, string? description); - [LoggerMessage(EventId = 60, Level = LogLevel.Warning, Message = "Server initiated unexpected close. Status: {WebSocketCloseStatus}, Desc: {Description}")] - public static partial void LogServerInitiatedUnexpectedCloseStatusDesc(this ILogger logger, WebSocketCloseStatus? webSocketCloseStatus, string? description); - [LoggerMessage(EventId = 61, Level = LogLevel.Debug, Message = "Acknowledging server close frame...")] + + [LoggerMessage( + EventId = 56, + Level = LogLevel.Warning, + Message = "WebSocketException in receive loop (Code: {WebSocketErrorCode})." + )] + public static partial void LogWebsocketexceptionInReceiveLoopCode( + this ILogger logger, + Exception exception, + WebSocketError webSocketErrorCode + ); + + [LoggerMessage( + EventId = 57, + Level = LogLevel.Error, + Message = "Unexpected exception in receive loop." + )] + public static partial void LogUnexpectedExceptionInReceiveLoop( + this ILogger logger, + Exception exception + ); + + [LoggerMessage( + EventId = 58, + Level = LogLevel.Debug, + Message = "Receive loop finished for WebSocket {HashCode}." + )] + public static partial void LogReceiveLoopFinishedForWebsocket( + this ILogger logger, + int hashCode + ); + + [LoggerMessage( + EventId = 59, + Level = LogLevel.Information, + Message = "Server acknowledged client closure. Status: {WebSocketCloseStatus}, Desc: {Description}" + )] + public static partial void LogServerAcknowledgedClientClosureStatusDesc( + this ILogger logger, + WebSocketCloseStatus? webSocketCloseStatus, + string? description + ); + + [LoggerMessage( + EventId = 60, + Level = LogLevel.Warning, + Message = "Server initiated unexpected close. Status: {WebSocketCloseStatus}, Desc: {Description}" + )] + public static partial void LogServerInitiatedUnexpectedCloseStatusDesc( + this ILogger logger, + WebSocketCloseStatus? webSocketCloseStatus, + string? description + ); + + [LoggerMessage( + EventId = 61, + Level = LogLevel.Debug, + Message = "Acknowledging server close frame..." + )] public static partial void LogAcknowledgingServerCloseFrame(this ILogger logger); - [LoggerMessage(EventId = 62, Level = LogLevel.Debug, Message = "Server close frame acknowledged.")] + + [LoggerMessage( + EventId = 62, + Level = LogLevel.Debug, + Message = "Server close frame acknowledged." + )] public static partial void LogServerCloseFrameAcknowledged(this ILogger logger); - [LoggerMessage(EventId = 63, Level = LogLevel.Warning, Message = "Failed to acknowledge server close frame.")] - public static partial void LogFailedToAcknowledgeServerCloseFrame(this ILogger logger, Exception exception); - [LoggerMessage(EventId = 64, Level = LogLevel.Trace, Message = "CleanupConnectionOnly due to: {ExceptionType} - {ExceptionMessage}")] - public static partial void LogCleanupconnectiononlyDueTo(this ILogger logger, string exceptionType, string exceptionMessage); - [LoggerMessage(EventId = 65, Level = LogLevel.Warning, Message = "Exception cancelling receive CTS during cleanup.")] - public static partial void LogExceptionCancellingReceiveCtsDuringCleanup(this ILogger logger, Exception exception); - [LoggerMessage(EventId = 66, Level = LogLevel.Warning, Message = "Exception disposing receive CTS during cleanup.")] - public static partial void LogExceptionDisposingReceiveCtsDuringCleanup(this ILogger logger, Exception exception); - [LoggerMessage(EventId = 67, Level = LogLevel.Debug, Message = "Disposing WebSocket instance {HashCode}")] + + [LoggerMessage( + EventId = 63, + Level = LogLevel.Warning, + Message = "Failed to acknowledge server close frame." + )] + public static partial void LogFailedToAcknowledgeServerCloseFrame( + this ILogger logger, + Exception exception + ); + + [LoggerMessage( + EventId = 64, + Level = LogLevel.Trace, + Message = "CleanupConnectionOnly due to: {ExceptionType} - {ExceptionMessage}" + )] + public static partial void LogCleanupconnectiononlyDueTo( + this ILogger logger, + string exceptionType, + string exceptionMessage + ); + + [LoggerMessage( + EventId = 65, + Level = LogLevel.Warning, + Message = "Exception cancelling receive CTS during cleanup." + )] + public static partial void LogExceptionCancellingReceiveCtsDuringCleanup( + this ILogger logger, + Exception exception + ); + + [LoggerMessage( + EventId = 66, + Level = LogLevel.Warning, + Message = "Exception disposing receive CTS during cleanup." + )] + public static partial void LogExceptionDisposingReceiveCtsDuringCleanup( + this ILogger logger, + Exception exception + ); + + [LoggerMessage( + EventId = 67, + Level = LogLevel.Debug, + Message = "Disposing WebSocket instance {HashCode}" + )] public static partial void LogDisposingWebsocketInstance(this ILogger logger, int hashCode); - [LoggerMessage(EventId = 68, Level = LogLevel.Debug, Message = "Finalizing disconnection... Reason: {ReasonType}")] - public static partial void LogFinalizingDisconnectionReason(this ILogger logger, string reasonType); - [LoggerMessage(EventId = 69, Level = LogLevel.Information, Message = "Client definitively disconnected. Reason: {DisconnectionReason}")] - public static partial void LogClientDefinitivelyDisconnectedReason(this ILogger logger, string disconnectionReason); - [LoggerMessage(EventId = 70, Level = LogLevel.Error, Message = "Unexpected incoming message type encountered: {MessageType}")] - public static partial void LogUnexpectedIncomingMessageTypeEncountered(this ILogger logger, string? messageType); + + [LoggerMessage( + EventId = 68, + Level = LogLevel.Debug, + Message = "Finalizing disconnection... Reason: {ReasonType}" + )] + public static partial void LogFinalizingDisconnectionReason( + this ILogger logger, + string reasonType + ); + + [LoggerMessage( + EventId = 69, + Level = LogLevel.Information, + Message = "Client definitively disconnected. Reason: {DisconnectionReason}" + )] + public static partial void LogClientDefinitivelyDisconnectedReason( + this ILogger logger, + string disconnectionReason + ); + + [LoggerMessage( + EventId = 70, + Level = LogLevel.Error, + Message = "Unexpected incoming message type encountered: {MessageType}" + )] + public static partial void LogUnexpectedIncomingMessageTypeEncountered( + this ILogger logger, + string? messageType + ); + [LoggerMessage(EventId = 71, Level = LogLevel.Trace, Message = "Processing Hello message.")] public static partial void LogProcessingHelloMessage(this ILogger logger); - [LoggerMessage(EventId = 72, Level = LogLevel.Trace, Message = "Processing Identified message.")] + + [LoggerMessage( + EventId = 72, + Level = LogLevel.Trace, + Message = "Processing Identified message." + )] public static partial void LogProcessingIdentifiedMessage(this ILogger logger); - [LoggerMessage(EventId = 73, Level = LogLevel.Warning, Message = "Received message with unhandled OpCode: {OpCode}")] - public static partial void LogReceivedMessageWithUnhandledOpcode(this ILogger logger, WebSocketOpCode opCode); - [LoggerMessage(EventId = 74, Level = LogLevel.Warning, Message = "Received null payload for RequestResponse.")] + + [LoggerMessage( + EventId = 73, + Level = LogLevel.Warning, + Message = "Received message with unhandled OpCode: {OpCode}" + )] + public static partial void LogReceivedMessageWithUnhandledOpcode( + this ILogger logger, + WebSocketOpCode opCode + ); + + [LoggerMessage( + EventId = 74, + Level = LogLevel.Warning, + Message = "Received null payload for RequestResponse." + )] public static partial void LogReceivedNullPayloadForRequestresponse(this ILogger logger); - [LoggerMessage(EventId = 75, Level = LogLevel.Trace, Message = "Processing RequestResponse for RequestId: {RequestId}, Status: {StatusResult}")] - public static partial void LogProcessingRequestresponseForRequestidStatus(this ILogger logger, string requestId, bool statusResult); - [LoggerMessage(EventId = 76, Level = LogLevel.Warning, Message = "Received response for unknown or timed out RequestId: {RequestId}")] - public static partial void LogReceivedResponseForUnknownOrTimedOut(this ILogger logger, string requestId); - [LoggerMessage(EventId = 77, Level = LogLevel.Error, Message = "Exception during processing of RequestResponse payload: {Payload}")] - public static partial void LogExceptionDuringProcessingOfRequestresponsePayload(this ILogger logger, Exception exception, string? payload); - [LoggerMessage(EventId = 78, Level = LogLevel.Warning, Message = "Received null payload for RequestBatchResponse.")] + + [LoggerMessage( + EventId = 75, + Level = LogLevel.Trace, + Message = "Processing RequestResponse for RequestId: {RequestId}, Status: {StatusResult}" + )] + public static partial void LogProcessingRequestresponseForRequestidStatus( + this ILogger logger, + string requestId, + bool statusResult + ); + + [LoggerMessage( + EventId = 76, + Level = LogLevel.Warning, + Message = "Received response for unknown or timed out RequestId: {RequestId}" + )] + public static partial void LogReceivedResponseForUnknownOrTimedOut( + this ILogger logger, + string requestId + ); + + [LoggerMessage( + EventId = 77, + Level = LogLevel.Error, + Message = "Exception during processing of RequestResponse payload: {Payload}" + )] + public static partial void LogExceptionDuringProcessingOfRequestresponsePayload( + this ILogger logger, + Exception exception, + string? payload + ); + + [LoggerMessage( + EventId = 78, + Level = LogLevel.Warning, + Message = "Received null payload for RequestBatchResponse." + )] public static partial void LogReceivedNullPayloadForRequestbatchresponse(this ILogger logger); - [LoggerMessage(EventId = 79, Level = LogLevel.Trace, Message = "Processing RequestBatchResponse for RequestId: {RequestId} ({ResultCount} results)")] - public static partial void LogProcessingRequestbatchresponseForRequestidResults(this ILogger logger, string requestId, int resultCount); - [LoggerMessage(EventId = 80, Level = LogLevel.Warning, Message = "Received response for unknown or timed out BatchRequestId: {RequestId}")] - public static partial void LogReceivedResponseForUnknownOrTimedOut2(this ILogger logger, string requestId); - [LoggerMessage(EventId = 81, Level = LogLevel.Error, Message = "Exception during processing of RequestBatchResponse payload: {Payload}")] - public static partial void LogExceptionDuringProcessingOfRequestbatchresponsePayload(this ILogger logger, Exception exception, string? payload); - [LoggerMessage(EventId = 82, Level = LogLevel.Warning, Message = "Received null payload for Event.")] + + [LoggerMessage( + EventId = 79, + Level = LogLevel.Trace, + Message = "Processing RequestBatchResponse for RequestId: {RequestId} ({ResultCount} results)" + )] + public static partial void LogProcessingRequestbatchresponseForRequestidResults( + this ILogger logger, + string requestId, + int resultCount + ); + + [LoggerMessage( + EventId = 80, + Level = LogLevel.Warning, + Message = "Received response for unknown or timed out BatchRequestId: {RequestId}" + )] + public static partial void LogReceivedResponseForUnknownOrTimedOut2( + this ILogger logger, + string requestId + ); + + [LoggerMessage( + EventId = 81, + Level = LogLevel.Error, + Message = "Exception during processing of RequestBatchResponse payload: {Payload}" + )] + public static partial void LogExceptionDuringProcessingOfRequestbatchresponsePayload( + this ILogger logger, + Exception exception, + string? payload + ); + + [LoggerMessage( + EventId = 82, + Level = LogLevel.Warning, + Message = "Received null payload for Event." + )] public static partial void LogReceivedNullPayloadForEvent(this ILogger logger); - [LoggerMessage(EventId = 83, Level = LogLevel.Trace, Message = "Handling incoming event: {EventType}")] + + [LoggerMessage( + EventId = 83, + Level = LogLevel.Trace, + Message = "Handling incoming event: {EventType}" + )] public static partial void LogHandlingIncomingEvent(this ILogger logger, string eventType); - [LoggerMessage(EventId = 84, Level = LogLevel.Error, Message = "Exception occurred within the event handler for {EventType}.")] - public static partial void LogExceptionOccurredWithinTheEventHandlerFor(this ILogger logger, Exception exception, string eventType); - [LoggerMessage(EventId = 85, Level = LogLevel.Warning, Message = "Received event with unhandled type: {EventType}")] - public static partial void LogReceivedEventWithUnhandledType(this ILogger logger, string eventType); - [LoggerMessage(EventId = 86, Level = LogLevel.Error, Message = "Critical exception during event handling for '{EventType}': {Payload}")] - public static partial void LogCriticalExceptionDuringEventHandlingFor(this ILogger logger, Exception exception, string eventType, string? payload); - [LoggerMessage(EventId = 87, Level = LogLevel.Error, Message = "Exception while trying to handle event {EventType}.")] - public static partial void LogExceptionWhileTryingToHandleEvent(this ILogger logger, Exception exception, string eventType); - [LoggerMessage(EventId = 88, Level = LogLevel.Warning, Message = "{RequestDescription} canceled.")] + + [LoggerMessage( + EventId = 84, + Level = LogLevel.Error, + Message = "Exception occurred within the event handler for {EventType}." + )] + public static partial void LogExceptionOccurredWithinTheEventHandlerFor( + this ILogger logger, + Exception exception, + string eventType + ); + + [LoggerMessage( + EventId = 85, + Level = LogLevel.Warning, + Message = "Received event with unhandled type: {EventType}" + )] + public static partial void LogReceivedEventWithUnhandledType( + this ILogger logger, + string eventType + ); + + [LoggerMessage( + EventId = 86, + Level = LogLevel.Error, + Message = "Critical exception during event handling for '{EventType}': {Payload}" + )] + public static partial void LogCriticalExceptionDuringEventHandlingFor( + this ILogger logger, + Exception exception, + string eventType, + string? payload + ); + + [LoggerMessage( + EventId = 87, + Level = LogLevel.Error, + Message = "Exception while trying to handle event {EventType}." + )] + public static partial void LogExceptionWhileTryingToHandleEvent( + this ILogger logger, + Exception exception, + string eventType + ); + + [LoggerMessage( + EventId = 88, + Level = LogLevel.Warning, + Message = "{RequestDescription} canceled." + )] public static partial void LogCanceled(this ILogger logger, string requestDescription); + [LoggerMessage(EventId = 89, Level = LogLevel.Trace, Message = "Sending {OpCode} message...")] public static partial void LogSendingMessage(this ILogger logger, WebSocketOpCode opCode); - [LoggerMessage(EventId = 90, Level = LogLevel.Error, Message = "Serialization failed for {OpCode} message.")] - public static partial void LogSerializationFailedForMessage(this ILogger logger, Exception exception, WebSocketOpCode opCode); + + [LoggerMessage( + EventId = 90, + Level = LogLevel.Error, + Message = "Serialization failed for {OpCode} message." + )] + public static partial void LogSerializationFailedForMessage( + this ILogger logger, + Exception exception, + WebSocketOpCode opCode + ); + [LoggerMessage(EventId = 91, Level = LogLevel.Trace, Message = "{OpCode} message sent.")] public static partial void LogMessageSent(this ILogger logger, WebSocketOpCode opCode); - [LoggerMessage(EventId = 92, Level = LogLevel.Warning, Message = "Send operation for {OpCode} canceled.")] - public static partial void LogSendOperationForCanceled(this ILogger logger, WebSocketOpCode opCode); - [LoggerMessage(EventId = 93, Level = LogLevel.Error, Message = "Failed to send {OpCode} message via WebSocket.")] - public static partial void LogFailedToSendMessageViaWebsocket(this ILogger logger, Exception exception, WebSocketOpCode opCode); - [LoggerMessage(EventId = 94, Level = LogLevel.Debug, Message = "Failing {RequestCount} pending request(s) due to: {ExceptionType}")] - public static partial void LogFailingPendingRequestSDueTo(this ILogger logger, int requestCount, string exceptionType); - [LoggerMessage(EventId = 95, Level = LogLevel.Error, Message = "Exception in user-provided Connecting event handler.")] - public static partial void LogExceptionInUserProvidedConnectingEventHandler(this ILogger logger, Exception exception); - [LoggerMessage(EventId = 96, Level = LogLevel.Error, Message = "Exception in user-provided Connected event handler.")] - public static partial void LogExceptionInUserProvidedConnectedEventHandler(this ILogger logger, Exception exception); - [LoggerMessage(EventId = 97, Level = LogLevel.Error, Message = "Exception in user-provided Disconnected event handler.")] - public static partial void LogExceptionInUserProvidedDisconnectedEventHandler(this ILogger logger, Exception exception); - [LoggerMessage(EventId = 98, Level = LogLevel.Error, Message = "Exception in user-provided ConnectionFailed event handler.")] - public static partial void LogExceptionInUserProvidedConnectionfailedEventHandler(this ILogger logger, Exception exception); - [LoggerMessage(EventId = 99, Level = LogLevel.Error, Message = "Exception in user-provided AuthenticationFailure event handler.")] - public static partial void LogExceptionInUserProvidedAuthenticationfailureEventHandler(this ILogger logger, Exception exception); + + [LoggerMessage( + EventId = 92, + Level = LogLevel.Warning, + Message = "Send operation for {OpCode} canceled." + )] + public static partial void LogSendOperationForCanceled( + this ILogger logger, + WebSocketOpCode opCode + ); + + [LoggerMessage( + EventId = 93, + Level = LogLevel.Error, + Message = "Failed to send {OpCode} message via WebSocket." + )] + public static partial void LogFailedToSendMessageViaWebsocket( + this ILogger logger, + Exception exception, + WebSocketOpCode opCode + ); + + [LoggerMessage( + EventId = 94, + Level = LogLevel.Debug, + Message = "Failing {RequestCount} pending request(s) due to: {ExceptionType}" + )] + public static partial void LogFailingPendingRequestSDueTo( + this ILogger logger, + int requestCount, + string exceptionType + ); + + [LoggerMessage( + EventId = 95, + Level = LogLevel.Error, + Message = "Exception in user-provided Connecting event handler." + )] + public static partial void LogExceptionInUserProvidedConnectingEventHandler( + this ILogger logger, + Exception exception + ); + + [LoggerMessage( + EventId = 96, + Level = LogLevel.Error, + Message = "Exception in user-provided Connected event handler." + )] + public static partial void LogExceptionInUserProvidedConnectedEventHandler( + this ILogger logger, + Exception exception + ); + + [LoggerMessage( + EventId = 97, + Level = LogLevel.Error, + Message = "Exception in user-provided Disconnected event handler." + )] + public static partial void LogExceptionInUserProvidedDisconnectedEventHandler( + this ILogger logger, + Exception exception + ); + + [LoggerMessage( + EventId = 98, + Level = LogLevel.Error, + Message = "Exception in user-provided ConnectionFailed event handler." + )] + public static partial void LogExceptionInUserProvidedConnectionfailedEventHandler( + this ILogger logger, + Exception exception + ); + + [LoggerMessage( + EventId = 99, + Level = LogLevel.Error, + Message = "Exception in user-provided AuthenticationFailure event handler." + )] + public static partial void LogExceptionInUserProvidedAuthenticationfailureEventHandler( + this ILogger logger, + Exception exception + ); } diff --git a/ObsWebSocket.Core/ObsWebSocketClientOptionsValidator.cs b/ObsWebSocket.Core/ObsWebSocketClientOptionsValidator.cs index e564cf2..07cef4a 100644 --- a/ObsWebSocket.Core/ObsWebSocketClientOptionsValidator.cs +++ b/ObsWebSocket.Core/ObsWebSocketClientOptionsValidator.cs @@ -25,7 +25,9 @@ public ValidateOptionsResult Validate(string? name, ObsWebSocketClientOptions op } else if (options.ServerUri.Scheme is not ("ws" or "wss")) { - failures.Add($"ServerUri scheme must be ws or wss, but was '{options.ServerUri.Scheme}'."); + failures.Add( + $"ServerUri scheme must be ws or wss, but was '{options.ServerUri.Scheme}'." + ); } if (options.HandshakeTimeoutMs <= 0) diff --git a/ObsWebSocket.Core/ObsWebSocketHosting.cs b/ObsWebSocket.Core/ObsWebSocketHosting.cs index c6241d5..a1a3d2f 100644 --- a/ObsWebSocket.Core/ObsWebSocketHosting.cs +++ b/ObsWebSocket.Core/ObsWebSocketHosting.cs @@ -1,6 +1,6 @@ +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Diagnostics.HealthChecks; -using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -95,7 +95,9 @@ public async Task StopAsync(CancellationToken cancellationToken) { if (client.IsConnected) { - await client.DisconnectAsync(cancellationToken: cancellationToken).ConfigureAwait(false); + await client + .DisconnectAsync(cancellationToken: cancellationToken) + .ConfigureAwait(false); } } } diff --git a/ObsWebSocket.Core/ObsWebSocketMetrics.cs b/ObsWebSocket.Core/ObsWebSocketMetrics.cs index f429eb1..3b939d7 100644 --- a/ObsWebSocket.Core/ObsWebSocketMetrics.cs +++ b/ObsWebSocket.Core/ObsWebSocketMetrics.cs @@ -22,14 +22,18 @@ public ObsWebSocketMetrics(IMeterFactory meterFactory) ArgumentNullException.ThrowIfNull(meterFactory); _meter = meterFactory.Create(ObsWebSocketDiagnostics.MeterName); _ownsMeter = false; - (RequestsSent, RequestsFailed, RequestDuration, EventsReceived, Reconnects) = Create(_meter); + (RequestsSent, RequestsFailed, RequestDuration, EventsReceived, Reconnects) = Create( + _meter + ); } private ObsWebSocketMetrics() { _meter = new Meter(ObsWebSocketDiagnostics.MeterName); _ownsMeter = true; - (RequestsSent, RequestsFailed, RequestDuration, EventsReceived, Reconnects) = Create(_meter); + (RequestsSent, RequestsFailed, RequestDuration, EventsReceived, Reconnects) = Create( + _meter + ); } /// Instruments for a client built outside dependency injection. diff --git a/ObsWebSocket.Core/ObsWebSocketServiceCollectionExtensions.cs b/ObsWebSocket.Core/ObsWebSocketServiceCollectionExtensions.cs index 6fd3797..80a53bd 100644 --- a/ObsWebSocket.Core/ObsWebSocketServiceCollectionExtensions.cs +++ b/ObsWebSocket.Core/ObsWebSocketServiceCollectionExtensions.cs @@ -63,8 +63,10 @@ public static IServiceCollection AddObsWebSocketClient( // Resolve options through the monitor, so a configuration change is picked up rather // than the values captured when the container was built. - _ = services.AddSingleton>(sp => - new MonitorBackedOptions(sp.GetRequiredService>()) + _ = services.AddSingleton>( + sp => new MonitorBackedOptions( + sp.GetRequiredService>() + ) ); _ = services.AddMetrics(); services.TryAddSingleton(); @@ -143,11 +145,13 @@ public static IServiceCollection AddObsWebSocketClient( { ObsWebSocketClientOptions options = sp.GetRequiredService< IOptionsMonitor - >().Get((string)key!); + >() + .Get((string)key!); IWebSocketMessageSerializer serializer = options.Format switch { - SerializationFormat.MsgPack => sp.GetRequiredService(), + SerializationFormat.MsgPack => + sp.GetRequiredService(), SerializationFormat.Json or _ => sp.GetRequiredService(), }; diff --git a/ObsWebSocket.Core/Protocol/Common/FilterSettings/CommonFilterSettings.cs b/ObsWebSocket.Core/Protocol/Common/FilterSettings/CommonFilterSettings.cs index 90c4831..9fee89e 100644 --- a/ObsWebSocket.Core/Protocol/Common/FilterSettings/CommonFilterSettings.cs +++ b/ObsWebSocket.Core/Protocol/Common/FilterSettings/CommonFilterSettings.cs @@ -25,9 +25,7 @@ public sealed record CropPadFilterSettings( ); /// Settings for the 'gain_filter' filter. -public sealed record GainFilterSettings( - [property: JsonPropertyName("db")] double? Db = null -); +public sealed record GainFilterSettings([property: JsonPropertyName("db")] double? Db = null); /// Settings for the 'basic_eq_filter' (3-Band EQ) filter. public sealed record BasicEqFilterSettings( @@ -182,6 +180,7 @@ public static class DetectorValues { /// Root Mean Square level detection. public const string Rms = "RMS"; + /// Peak level detection. public const string Peak = "Peak"; } @@ -191,6 +190,7 @@ public static class PresetsValues { /// Expander — increases dynamic range below the threshold. public const string Expander = "expander"; + /// Gate — silences audio below the threshold. public const string Gate = "gate"; } @@ -212,6 +212,7 @@ public static class DetectorValues { /// Root Mean Square level detection. public const string Rms = "RMS"; + /// Peak level detection. public const string Peak = "Peak"; } diff --git a/ObsWebSocket.Core/Protocol/Common/InputSettings/CommonInputSettings.cs b/ObsWebSocket.Core/Protocol/Common/InputSettings/CommonInputSettings.cs index 0d7c2f2..1f590cb 100644 --- a/ObsWebSocket.Core/Protocol/Common/InputSettings/CommonInputSettings.cs +++ b/ObsWebSocket.Core/Protocol/Common/InputSettings/CommonInputSettings.cs @@ -47,8 +47,10 @@ public static class PlaybackBehaviorValues { /// Always play regardless of scene visibility. public const string AlwaysPlay = "always_play"; + /// Pause when invisible, unpause when visible. public const string PauseUnpause = "pause_unpause"; + /// Stop and restart from the beginning when made visible. public const string StopRestart = "stop_restart"; } @@ -58,8 +60,10 @@ public static class PlaybackModeValues { /// Loop through slides continuously. public const string Loop = "loop"; + /// Bounce back and forth through slides. public const string Bounce = "bounce"; + /// Manual advance only. public const string Manual = "manual"; } @@ -129,8 +133,10 @@ public static class AlignValues { /// Left-align text. public const string Left = "left"; + /// Center-align text. public const string Center = "center"; + /// Right-align text. public const string Right = "right"; } @@ -140,8 +146,10 @@ public static class ValignValues { /// Align text to the top. public const string Top = "top"; + /// Align text to the center. public const string Center = "center"; + /// Align text to the bottom. public const string Bottom = "bottom"; } @@ -179,8 +187,10 @@ public static class PlaybackBehaviorValues { /// Stop and restart from the beginning when made visible. public const string StopRestart = "stop_restart"; + /// Pause when invisible, unpause when visible. public const string PauseUnpause = "pause_unpause"; + /// Always play regardless of scene visibility. public const string AlwaysPlay = "always_play"; } @@ -231,8 +241,10 @@ public static class CaptureModeValues { /// Capture any fullscreen application. public const string AnyFullscreen = "any_fullscreen"; + /// Capture a specific window. public const string Window = "window"; + /// Toggle capture with a hotkey. public const string Hotkey = "hotkey"; } diff --git a/ObsWebSocket.Core/Protocol/Common/StreamServiceSettings/CommonStreamServiceSettings.cs b/ObsWebSocket.Core/Protocol/Common/StreamServiceSettings/CommonStreamServiceSettings.cs index 51d3a9e..cb50097 100644 --- a/ObsWebSocket.Core/Protocol/Common/StreamServiceSettings/CommonStreamServiceSettings.cs +++ b/ObsWebSocket.Core/Protocol/Common/StreamServiceSettings/CommonStreamServiceSettings.cs @@ -14,7 +14,8 @@ public sealed record RtmpCommonStreamServiceSettings( [property: JsonPropertyName("bwtest")] bool? BandwidthTest = null, [property: JsonPropertyName("stream_key_link")] string? StreamKeyLink = null, [property: JsonPropertyName("multitrack_video_name")] string? MultitrackVideoName = null, - [property: JsonPropertyName("multitrack_video_disclaimer")] string? MultitrackVideoDisclaimer = null + [property: JsonPropertyName("multitrack_video_disclaimer")] + string? MultitrackVideoDisclaimer = null ); /// diff --git a/ObsWebSocket.Core/Protocol/Common/StubTypes.cs b/ObsWebSocket.Core/Protocol/Common/StubTypes.cs index 6ff8adf..a5da120 100644 --- a/ObsWebSocket.Core/Protocol/Common/StubTypes.cs +++ b/ObsWebSocket.Core/Protocol/Common/StubTypes.cs @@ -482,10 +482,3 @@ public sealed class PropertyItemStub [JsonConstructor] public PropertyItemStub() { } } - - - - - - - diff --git a/ObsWebSocket.Core/Protocol/Messages.cs b/ObsWebSocket.Core/Protocol/Messages.cs index bcaeb6a..4465534 100644 --- a/ObsWebSocket.Core/Protocol/Messages.cs +++ b/ObsWebSocket.Core/Protocol/Messages.cs @@ -179,5 +179,6 @@ internal record RequestBatchPayload( [MessagePackObject(AllowPrivate = true, SuppressSourceGeneration = true)] internal record RequestBatchResponsePayload( [property: JsonPropertyName("requestId"), Key("requestId")] string RequestId, - [property: JsonPropertyName("results"), Key("results")] List> Results + [property: JsonPropertyName("results"), Key("results")] + List> Results ); diff --git a/ObsWebSocket.Core/ReconnectDelays.cs b/ObsWebSocket.Core/ReconnectDelays.cs index 1dd8bb3..c770d58 100644 --- a/ObsWebSocket.Core/ReconnectDelays.cs +++ b/ObsWebSocket.Core/ReconnectDelays.cs @@ -67,7 +67,5 @@ public async ValueTask GetDelayAsync( /// recovering from one outage do not retry in lockstep. /// private static TimeSpan ApplyJitter(TimeSpan delay) => - delay <= TimeSpan.Zero - ? delay - : delay * (0.75 + (Random.Shared.NextDouble() * 0.5)); + delay <= TimeSpan.Zero ? delay : delay * (0.75 + (Random.Shared.NextDouble() * 0.5)); } diff --git a/ObsWebSocket.Core/Serialization/JsonMessageSerializer.cs b/ObsWebSocket.Core/Serialization/JsonMessageSerializer.cs index 0d32d74..4385e5a 100644 --- a/ObsWebSocket.Core/Serialization/JsonMessageSerializer.cs +++ b/ObsWebSocket.Core/Serialization/JsonMessageSerializer.cs @@ -14,7 +14,9 @@ public class JsonMessageSerializer(ILogger logger) : IWebSocketMessageSerializer { private readonly ILogger _logger = logger; - private static readonly JsonSerializerOptions s_options = ObsWebSocketJsonContext.Default.Options; + private static readonly JsonSerializerOptions s_options = ObsWebSocketJsonContext + .Default + .Options; /// public string ProtocolSubProtocol => "obswebsocket.json"; @@ -59,9 +61,8 @@ public Task SerializeAsync( { // Deserialize into the generic IncomingMessage with JsonElement as the data type JsonTypeInfo> typeInfo = - (JsonTypeInfo>)s_options.GetTypeInfo( - typeof(IncomingMessage) - ); + (JsonTypeInfo>) + s_options.GetTypeInfo(typeof(IncomingMessage)); IncomingMessage? message = await JsonSerializer .DeserializeAsync(messageStream, typeInfo, cancellationToken) .ConfigureAwait(false); @@ -89,7 +90,10 @@ public Task SerializeAsync( messageStream.Position = 0; using StreamReader reader = new(messageStream, Encoding.UTF8, leaveOpen: true); string rawJson = await reader.ReadToEndAsync(cancellationToken).ConfigureAwait(false); - _logger.LogJsonDeserializationFailedRawJson(ex, rawJson.Length > 1024 ? rawJson[..1024] + "..." : rawJson); + _logger.LogJsonDeserializationFailedRawJson( + ex, + rawJson.Length > 1024 ? rawJson[..1024] + "..." : rawJson + ); return null; } catch (Exception ex) @@ -114,7 +118,10 @@ is not null and not JsonElement { ValueKind: JsonValueKind.Null or JsonValueKind.Undefined } ) { - _logger.LogJsonDeserializerExpectedJsonelementPayloadButReceived(rawPayloadData?.GetType().Name, typeof(TPayload).Name); + _logger.LogJsonDeserializerExpectedJsonelementPayloadButReceived( + rawPayloadData?.GetType().Name, + typeof(TPayload).Name + ); } return default; @@ -125,39 +132,40 @@ is not null if (typeof(TPayload) == typeof(EventPayloadBase)) { JsonTypeInfo> eventTypeInfo = - (JsonTypeInfo>)s_options.GetTypeInfo( - typeof(EventPayloadBase) - ); - EventPayloadBase? eventPayload = jsonElement.Deserialize(eventTypeInfo); + (JsonTypeInfo>) + s_options.GetTypeInfo(typeof(EventPayloadBase)); + EventPayloadBase? eventPayload = jsonElement.Deserialize( + eventTypeInfo + ); return eventPayload is null ? default : (TPayload) - (object) - new EventPayloadBase( - eventPayload.EventType, - eventPayload.EventIntent, - eventPayload.EventData - ); + (object) + new EventPayloadBase( + eventPayload.EventType, + eventPayload.EventIntent, + eventPayload.EventData + ); } if (typeof(TPayload) == typeof(RequestResponsePayload)) { JsonTypeInfo> responseTypeInfo = - (JsonTypeInfo>)s_options.GetTypeInfo( - typeof(RequestResponsePayload) - ); - RequestResponsePayload? responsePayload = - jsonElement.Deserialize(responseTypeInfo); + (JsonTypeInfo>) + s_options.GetTypeInfo(typeof(RequestResponsePayload)); + RequestResponsePayload? responsePayload = jsonElement.Deserialize( + responseTypeInfo + ); return responsePayload is null ? default : (TPayload) - (object) - new RequestResponsePayload( - responsePayload.RequestType, - responsePayload.RequestId, - responsePayload.RequestStatus, - responsePayload.ResponseData - ); + (object) + new RequestResponsePayload( + responsePayload.RequestType, + responsePayload.RequestId, + responsePayload.RequestStatus, + responsePayload.ResponseData + ); } if (typeof(TPayload) == typeof(RequestBatchResponsePayload)) @@ -165,9 +173,8 @@ is not null // Each result is deserialized on its own, because deserializing the batch in // one pass paired every responseData with the following request. JsonTypeInfo> itemTypeInfo = - (JsonTypeInfo>)s_options.GetTypeInfo( - typeof(RequestResponsePayload) - ); + (JsonTypeInfo>) + s_options.GetTypeInfo(typeof(RequestResponsePayload)); string batchRequestId = jsonElement.TryGetProperty("requestId", out JsonElement idElement) @@ -191,10 +198,12 @@ is not null continue; } - JsonElement data = - itemElement.TryGetProperty("responseData", out JsonElement dataElement) - ? dataElement.Clone() - : default; + JsonElement data = itemElement.TryGetProperty( + "responseData", + out JsonElement dataElement + ) + ? dataElement.Clone() + : default; mappedResults.Add( new RequestResponsePayload( @@ -208,20 +217,20 @@ is not null } return (TPayload) - (object)new RequestBatchResponsePayload( - batchRequestId, - mappedResults - ); + (object)new RequestBatchResponsePayload(batchRequestId, mappedResults); } - JsonTypeInfo typeInfo = (JsonTypeInfo)s_options.GetTypeInfo( - typeof(TPayload) - ); + JsonTypeInfo typeInfo = + (JsonTypeInfo)s_options.GetTypeInfo(typeof(TPayload)); return jsonElement.Deserialize(typeInfo); } catch (Exception ex) { - _logger.LogJsonFailedToDeserializePayloadToRaw(ex, typeof(TPayload).Name, jsonElement.GetRawText()); + _logger.LogJsonFailedToDeserializePayloadToRaw( + ex, + typeof(TPayload).Name, + jsonElement.GetRawText() + ); return default; } } @@ -241,7 +250,10 @@ is not null and not JsonElement { ValueKind: JsonValueKind.Null or JsonValueKind.Undefined } ) { - _logger.LogJsonDeserializerExpectedJsonelementPayloadButReceived2(rawPayloadData?.GetType().Name, typeof(TPayload).Name); + _logger.LogJsonDeserializerExpectedJsonelementPayloadButReceived2( + rawPayloadData?.GetType().Name, + typeof(TPayload).Name + ); } return default; @@ -251,14 +263,17 @@ is not null { // Deserialize will return default(TPayload) if JSON is null, which is valid for nullable structs, // but might be undesirable for non-nullable ones (though caught earlier if JSON is explicitly null). - JsonTypeInfo typeInfo = (JsonTypeInfo)s_options.GetTypeInfo( - typeof(TPayload) - ); + JsonTypeInfo typeInfo = + (JsonTypeInfo)s_options.GetTypeInfo(typeof(TPayload)); return jsonElement.Deserialize(typeInfo); } catch (Exception ex) { - _logger.LogJsonFailedToDeserializePayloadToValue(ex, typeof(TPayload).Name, jsonElement.GetRawText()); + _logger.LogJsonFailedToDeserializePayloadToValue( + ex, + typeof(TPayload).Name, + jsonElement.GetRawText() + ); return default; } } diff --git a/ObsWebSocket.Core/Serialization/MsgPackJsonElementResolver.cs b/ObsWebSocket.Core/Serialization/MsgPackJsonElementResolver.cs index 98015a5..b8547c7 100644 --- a/ObsWebSocket.Core/Serialization/MsgPackJsonElementResolver.cs +++ b/ObsWebSocket.Core/Serialization/MsgPackJsonElementResolver.cs @@ -11,11 +11,12 @@ internal sealed class MsgPackJsonElementResolver : IFormatterResolver private MsgPackJsonElementResolver() { } - public IMessagePackFormatter? GetFormatter() => typeof(T) == typeof(JsonElement) + public IMessagePackFormatter? GetFormatter() => + typeof(T) == typeof(JsonElement) ? (IMessagePackFormatter)(object)JsonElementFormatter.Instance - : typeof(T) == typeof(JsonElement?) + : typeof(T) == typeof(JsonElement?) ? (IMessagePackFormatter)(object)NullableJsonElementFormatter.Instance - : null; + : null; internal sealed class JsonElementFormatter : IMessagePackFormatter { @@ -80,7 +81,10 @@ public void Serialize( MessagePackSerializerOptions options ) { - if (!value.HasValue || value.Value.ValueKind is JsonValueKind.Null or JsonValueKind.Undefined) + if ( + !value.HasValue + || value.Value.ValueKind is JsonValueKind.Null or JsonValueKind.Undefined + ) { writer.WriteNil(); return; @@ -92,6 +96,9 @@ MessagePackSerializerOptions options public JsonElement? Deserialize( ref MessagePackReader reader, MessagePackSerializerOptions options - ) => reader.TryReadNil() ? null : JsonElementFormatter.Instance.Deserialize(ref reader, options); + ) => + reader.TryReadNil() + ? null + : JsonElementFormatter.Instance.Deserialize(ref reader, options); } } diff --git a/ObsWebSocket.Core/Serialization/MsgPackMessageSerializer.cs b/ObsWebSocket.Core/Serialization/MsgPackMessageSerializer.cs index ce49bfc..839e7f4 100644 --- a/ObsWebSocket.Core/Serialization/MsgPackMessageSerializer.cs +++ b/ObsWebSocket.Core/Serialization/MsgPackMessageSerializer.cs @@ -123,7 +123,11 @@ public Task SerializeAsync( } catch (Exception ex) { - _logger.LogMessagepackFailedToDeserializePayloadObjectTo(ex, typeof(TPayload).Name, rawPayloadData.GetType().Name); + _logger.LogMessagepackFailedToDeserializePayloadObjectTo( + ex, + typeof(TPayload).Name, + rawPayloadData.GetType().Name + ); return default; } } @@ -143,7 +147,11 @@ public Task SerializeAsync( } catch (Exception ex) { - _logger.LogMessagepackFailedToDeserializePayloadObjectTo2(ex, typeof(TPayload).Name, rawPayloadData.GetType().Name); + _logger.LogMessagepackFailedToDeserializePayloadObjectTo2( + ex, + typeof(TPayload).Name, + rawPayloadData.GetType().Name + ); return default; } } @@ -198,7 +206,11 @@ CancellationToken cancellationToken return [.. buffer.WrittenSpan]; } - byte[] payloadBytes = MessagePackSerializer.Serialize(message.D, s_msgPackOptions, cancellationToken); + byte[] payloadBytes = MessagePackSerializer.Serialize( + message.D, + s_msgPackOptions, + cancellationToken + ); writer.WriteRaw(payloadBytes); writer.Flush(); @@ -248,7 +260,8 @@ private static EventPayloadBase DeserializeEventPayloadBase(ReadOnlyMemo return new EventPayloadBase(eventType ?? string.Empty, eventIntent, eventData); } - private static IFormatterResolver[] CreateResolverChain() => [ + private static IFormatterResolver[] CreateResolverChain() => + [ MsgPackJsonElementResolver.Instance, MsgPackStubExtensionDataResolver.Instance, ObsWebSocketMsgPackResolver.Instance, diff --git a/ObsWebSocket.Core/Serialization/MsgPackStubExtensionDataResolver.cs b/ObsWebSocket.Core/Serialization/MsgPackStubExtensionDataResolver.cs index 797feda..d718fee 100644 --- a/ObsWebSocket.Core/Serialization/MsgPackStubExtensionDataResolver.cs +++ b/ObsWebSocket.Core/Serialization/MsgPackStubExtensionDataResolver.cs @@ -23,13 +23,14 @@ private MsgPackStubExtensionDataResolver() { } if (type == typeof(SceneItemTransformStub)) { - return (IMessagePackFormatter)(object) - new MsgPackJsonBridgeFormatter(); + return (IMessagePackFormatter) + (object)new MsgPackJsonBridgeFormatter(); } if (type == typeof(SceneItemStub)) { - return (IMessagePackFormatter)(object)new MsgPackJsonBridgeFormatter(); + return (IMessagePackFormatter) + (object)new MsgPackJsonBridgeFormatter(); } if (type == typeof(FilterStub)) @@ -44,40 +45,52 @@ private MsgPackStubExtensionDataResolver() { } if (type == typeof(TransitionStub)) { - return (IMessagePackFormatter)(object)new MsgPackJsonBridgeFormatter(); + return (IMessagePackFormatter) + (object)new MsgPackJsonBridgeFormatter(); } return type == typeof(List) - ? (IMessagePackFormatter)(object)new MsgPackJsonBridgeFormatter>() + ? (IMessagePackFormatter) + (object)new MsgPackJsonBridgeFormatter>() : type == typeof(List) - ? (IMessagePackFormatter)(object)new MsgPackJsonBridgeFormatter>() + ? (IMessagePackFormatter) + (object)new MsgPackJsonBridgeFormatter>() : type == typeof(List) - ? (IMessagePackFormatter)(object)new MsgPackJsonBridgeFormatter>() + ? (IMessagePackFormatter) + (object)new MsgPackJsonBridgeFormatter>() : type == typeof(List) - ? (IMessagePackFormatter)(object)new MsgPackJsonBridgeFormatter>() + ? (IMessagePackFormatter) + (object)new MsgPackJsonBridgeFormatter>() : type == typeof(List) - ? (IMessagePackFormatter)(object)new MsgPackJsonBridgeFormatter>() + ? (IMessagePackFormatter) + (object)new MsgPackJsonBridgeFormatter>() : type == typeof(List) - ? (IMessagePackFormatter)(object)new MsgPackJsonBridgeFormatter>() + ? (IMessagePackFormatter) + (object)new MsgPackJsonBridgeFormatter>() : type == typeof(List) - ? (IMessagePackFormatter)(object)new MsgPackJsonBridgeFormatter>() + ? (IMessagePackFormatter) + (object)new MsgPackJsonBridgeFormatter>() : type == typeof(List) - ? (IMessagePackFormatter)(object) - new MsgPackJsonBridgeFormatter>() + ? (IMessagePackFormatter) + (object)new MsgPackJsonBridgeFormatter>() : type == typeof(OutputStub) - ? (IMessagePackFormatter)(object)new MsgPackJsonBridgeFormatter() + ? (IMessagePackFormatter)(object)new MsgPackJsonBridgeFormatter() : type == typeof(MonitorStub) - ? (IMessagePackFormatter)(object)new MsgPackJsonBridgeFormatter() + ? (IMessagePackFormatter)(object)new MsgPackJsonBridgeFormatter() : type == typeof(PropertyItemStub) - ? (IMessagePackFormatter)(object) - new MsgPackJsonBridgeFormatter() + ? (IMessagePackFormatter) + (object)new MsgPackJsonBridgeFormatter() : null; } private sealed class MsgPackJsonBridgeFormatter : IMessagePackFormatter where T : class { - public void Serialize(ref MessagePackWriter writer, T? value, MessagePackSerializerOptions options) + public void Serialize( + ref MessagePackWriter writer, + T? value, + MessagePackSerializerOptions options + ) { if (value is null) { @@ -85,8 +98,8 @@ public void Serialize(ref MessagePackWriter writer, T? value, MessagePackSeriali return; } - JsonTypeInfo typeInfo = (JsonTypeInfo) - ObsWebSocketJsonContext.Default.Options.GetTypeInfo(typeof(T)); + JsonTypeInfo typeInfo = + (JsonTypeInfo)ObsWebSocketJsonContext.Default.Options.GetTypeInfo(typeof(T)); string json = JsonSerializer.Serialize(value, typeInfo); byte[] raw = MessagePackSerializer.ConvertFromJson(json); writer.WriteRaw(raw); @@ -104,8 +117,8 @@ public void Serialize(ref MessagePackWriter writer, T? value, MessagePackSeriali byte[] raw = ReadRawValue(ref reader); string json = MessagePackSerializer.ConvertToJson(raw); - JsonTypeInfo typeInfo = (JsonTypeInfo) - ObsWebSocketJsonContext.Default.Options.GetTypeInfo(typeof(T)); + JsonTypeInfo typeInfo = + (JsonTypeInfo)ObsWebSocketJsonContext.Default.Options.GetTypeInfo(typeof(T)); T? result = JsonSerializer.Deserialize(json, typeInfo); reader.Depth--; diff --git a/ObsWebSocket.Core/Serialization/ObsWebSocketJsonContext.Settings.cs b/ObsWebSocket.Core/Serialization/ObsWebSocketJsonContext.Settings.cs index 7deaf2b..81dd4fd 100644 --- a/ObsWebSocket.Core/Serialization/ObsWebSocketJsonContext.Settings.cs +++ b/ObsWebSocket.Core/Serialization/ObsWebSocketJsonContext.Settings.cs @@ -56,5 +56,6 @@ namespace ObsWebSocket.Core.Serialization; [JsonSerializable(typeof(RtmpCustomStreamServiceSettings))] [JsonSourceGenerationOptions( PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase, - DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull)] + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull +)] internal sealed partial class ObsWebSocketSettingsJsonContext : JsonSerializerContext { } diff --git a/ObsWebSocket.Core/Serialization/SerializerLog.cs b/ObsWebSocket.Core/Serialization/SerializerLog.cs index 85dddac..2534070 100644 --- a/ObsWebSocket.Core/Serialization/SerializerLog.cs +++ b/ObsWebSocket.Core/Serialization/SerializerLog.cs @@ -8,36 +8,171 @@ namespace ObsWebSocket.Core.Serialization; /// Source-generated log messages for the message serializers. internal static partial class SerializerLog { - [LoggerMessage(EventId = 1, Level = LogLevel.Error, Message = "JSON serialization failed for message with OpCode {OpCode}")] - public static partial void LogJsonSerializationFailedForMessageWithOpcode(this ILogger logger, Exception exception, WebSocketOpCode opCode); - [LoggerMessage(EventId = 2, Level = LogLevel.Warning, Message = "Attempted to deserialize an empty message stream.")] + [LoggerMessage( + EventId = 1, + Level = LogLevel.Error, + Message = "JSON serialization failed for message with OpCode {OpCode}" + )] + public static partial void LogJsonSerializationFailedForMessageWithOpcode( + this ILogger logger, + Exception exception, + WebSocketOpCode opCode + ); + + [LoggerMessage( + EventId = 2, + Level = LogLevel.Warning, + Message = "Attempted to deserialize an empty message stream." + )] public static partial void LogAttemptedToDeserializeAnEmptyMessageStream(this ILogger logger); - [LoggerMessage(EventId = 3, Level = LogLevel.Warning, Message = "JSON deserialization resulted in null.")] + + [LoggerMessage( + EventId = 3, + Level = LogLevel.Warning, + Message = "JSON deserialization resulted in null." + )] public static partial void LogJsonDeserializationResultedInNull(this ILogger logger); - [LoggerMessage(EventId = 4, Level = LogLevel.Trace, Message = "Deserialized JSON message: Op={Op}")] - public static partial void LogDeserializedJsonMessageOp(this ILogger logger, WebSocketOpCode op); - [LoggerMessage(EventId = 5, Level = LogLevel.Error, Message = "JSON deserialization failed. Raw JSON: {RawJson}")] - public static partial void LogJsonDeserializationFailedRawJson(this ILogger logger, Exception exception, string rawJson); - [LoggerMessage(EventId = 6, Level = LogLevel.Error, Message = "Failed to deserialize message from stream.")] - public static partial void LogFailedToDeserializeMessageFromStream(this ILogger logger, Exception exception); - [LoggerMessage(EventId = 7, Level = LogLevel.Warning, Message = "JSON Deserializer expected JsonElement payload but received {DataType} for {TargetType}.")] - public static partial void LogJsonDeserializerExpectedJsonelementPayloadButReceived(this ILogger logger, string? dataType, string targetType); - [LoggerMessage(EventId = 8, Level = LogLevel.Error, Message = "JSON failed to deserialize payload to {TargetType}. Raw JSON: {Json}")] - public static partial void LogJsonFailedToDeserializePayloadToRaw(this ILogger logger, Exception exception, string targetType, string json); - [LoggerMessage(EventId = 9, Level = LogLevel.Warning, Message = "JSON Deserializer expected JsonElement payload but received {DataType} for value type {TargetType}.")] - public static partial void LogJsonDeserializerExpectedJsonelementPayloadButReceived2(this ILogger logger, string? dataType, string targetType); - [LoggerMessage(EventId = 10, Level = LogLevel.Error, Message = "JSON failed to deserialize payload to value type {TargetType}. Raw JSON: {Json}")] - public static partial void LogJsonFailedToDeserializePayloadToValue(this ILogger logger, Exception exception, string targetType, string json); - [LoggerMessage(EventId = 11, Level = LogLevel.Error, Message = "MessagePack serialization failed for message with OpCode {OpCode}")] - public static partial void LogMessagepackSerializationFailedForMessageWithOpcode(this ILogger logger, Exception exception, WebSocketOpCode opCode); - [LoggerMessage(EventId = 12, Level = LogLevel.Error, Message = "Unexpected error during MessagePack serialization for OpCode {OpCode}")] - public static partial void LogUnexpectedErrorDuringMessagepackSerializationForOpcode(this ILogger logger, Exception exception, WebSocketOpCode opCode); - [LoggerMessage(EventId = 13, Level = LogLevel.Trace, Message = "Deserialized MessagePack message: Op={Op}")] - public static partial void LogDeserializedMessagepackMessageOp(this ILogger logger, WebSocketOpCode op); - [LoggerMessage(EventId = 14, Level = LogLevel.Error, Message = "MessagePack deserialization failed.")] - public static partial void LogMessagepackDeserializationFailed(this ILogger logger, Exception exception); - [LoggerMessage(EventId = 15, Level = LogLevel.Error, Message = "MessagePack failed to deserialize payload object to {TargetType}. Object Type: {ObjectType}")] - public static partial void LogMessagepackFailedToDeserializePayloadObjectTo(this ILogger logger, Exception exception, string targetType, string objectType); - [LoggerMessage(EventId = 16, Level = LogLevel.Error, Message = "MessagePack failed to deserialize payload object to value type {TargetType}. Object Type: {ObjectType}")] - public static partial void LogMessagepackFailedToDeserializePayloadObjectTo2(this ILogger logger, Exception exception, string targetType, string objectType); + + [LoggerMessage( + EventId = 4, + Level = LogLevel.Trace, + Message = "Deserialized JSON message: Op={Op}" + )] + public static partial void LogDeserializedJsonMessageOp( + this ILogger logger, + WebSocketOpCode op + ); + + [LoggerMessage( + EventId = 5, + Level = LogLevel.Error, + Message = "JSON deserialization failed. Raw JSON: {RawJson}" + )] + public static partial void LogJsonDeserializationFailedRawJson( + this ILogger logger, + Exception exception, + string rawJson + ); + + [LoggerMessage( + EventId = 6, + Level = LogLevel.Error, + Message = "Failed to deserialize message from stream." + )] + public static partial void LogFailedToDeserializeMessageFromStream( + this ILogger logger, + Exception exception + ); + + [LoggerMessage( + EventId = 7, + Level = LogLevel.Warning, + Message = "JSON Deserializer expected JsonElement payload but received {DataType} for {TargetType}." + )] + public static partial void LogJsonDeserializerExpectedJsonelementPayloadButReceived( + this ILogger logger, + string? dataType, + string targetType + ); + + [LoggerMessage( + EventId = 8, + Level = LogLevel.Error, + Message = "JSON failed to deserialize payload to {TargetType}. Raw JSON: {Json}" + )] + public static partial void LogJsonFailedToDeserializePayloadToRaw( + this ILogger logger, + Exception exception, + string targetType, + string json + ); + + [LoggerMessage( + EventId = 9, + Level = LogLevel.Warning, + Message = "JSON Deserializer expected JsonElement payload but received {DataType} for value type {TargetType}." + )] + public static partial void LogJsonDeserializerExpectedJsonelementPayloadButReceived2( + this ILogger logger, + string? dataType, + string targetType + ); + + [LoggerMessage( + EventId = 10, + Level = LogLevel.Error, + Message = "JSON failed to deserialize payload to value type {TargetType}. Raw JSON: {Json}" + )] + public static partial void LogJsonFailedToDeserializePayloadToValue( + this ILogger logger, + Exception exception, + string targetType, + string json + ); + + [LoggerMessage( + EventId = 11, + Level = LogLevel.Error, + Message = "MessagePack serialization failed for message with OpCode {OpCode}" + )] + public static partial void LogMessagepackSerializationFailedForMessageWithOpcode( + this ILogger logger, + Exception exception, + WebSocketOpCode opCode + ); + + [LoggerMessage( + EventId = 12, + Level = LogLevel.Error, + Message = "Unexpected error during MessagePack serialization for OpCode {OpCode}" + )] + public static partial void LogUnexpectedErrorDuringMessagepackSerializationForOpcode( + this ILogger logger, + Exception exception, + WebSocketOpCode opCode + ); + + [LoggerMessage( + EventId = 13, + Level = LogLevel.Trace, + Message = "Deserialized MessagePack message: Op={Op}" + )] + public static partial void LogDeserializedMessagepackMessageOp( + this ILogger logger, + WebSocketOpCode op + ); + + [LoggerMessage( + EventId = 14, + Level = LogLevel.Error, + Message = "MessagePack deserialization failed." + )] + public static partial void LogMessagepackDeserializationFailed( + this ILogger logger, + Exception exception + ); + + [LoggerMessage( + EventId = 15, + Level = LogLevel.Error, + Message = "MessagePack failed to deserialize payload object to {TargetType}. Object Type: {ObjectType}" + )] + public static partial void LogMessagepackFailedToDeserializePayloadObjectTo( + this ILogger logger, + Exception exception, + string targetType, + string objectType + ); + + [LoggerMessage( + EventId = 16, + Level = LogLevel.Error, + Message = "MessagePack failed to deserialize payload object to value type {TargetType}. Object Type: {ObjectType}" + )] + public static partial void LogMessagepackFailedToDeserializePayloadObjectTo2( + this ILogger logger, + Exception exception, + string targetType, + string objectType + ); } diff --git a/ObsWebSocket.Example/ObsWebSocket.Example.csproj b/ObsWebSocket.Example/ObsWebSocket.Example.csproj index 3fbfe8a..dde0801 100644 --- a/ObsWebSocket.Example/ObsWebSocket.Example.csproj +++ b/ObsWebSocket.Example/ObsWebSocket.Example.csproj @@ -7,7 +7,8 @@ true - 10.0.11 + 10.0.11 true true diff --git a/ObsWebSocket.Example/Program.cs b/ObsWebSocket.Example/Program.cs index 2e5c255..7dbd630 100644 --- a/ObsWebSocket.Example/Program.cs +++ b/ObsWebSocket.Example/Program.cs @@ -7,12 +7,7 @@ using Spectre.Console; HostApplicationBuilder builder = Host.CreateApplicationBuilder(args); -AnsiConsole.Write( - new Rule("[cyan]ObsWebSocket Example Tool[/]") - { - Justification = Justify.Left, - } -); +AnsiConsole.Write(new Rule("[cyan]ObsWebSocket Example Tool[/]") { Justification = Justify.Left }); // Reads appsettings.json, environment variables, command-line args builder diff --git a/ObsWebSocket.Example/Worker.cs b/ObsWebSocket.Example/Worker.cs index 7bef2cf..b010c6c 100644 --- a/ObsWebSocket.Example/Worker.cs +++ b/ObsWebSocket.Example/Worker.cs @@ -2,6 +2,7 @@ using System.Text.Json; using System.Text.Json.Serialization; using System.Text.Json.Serialization.Metadata; +using Microsoft.Extensions.Diagnostics.HealthChecks; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -16,7 +17,6 @@ using ObsWebSocket.Core.Protocol.Generated; using ObsWebSocket.Core.Protocol.Requests; using ObsWebSocket.Core.Protocol.Responses; -using Microsoft.Extensions.Diagnostics.HealthChecks; using ObsWebSocket.Core.Serialization; using Spectre.Console; @@ -89,10 +89,7 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) ) { string startupCommand = _startupCommandOptions.Command!; - _logger.LogInformation( - "Running startup command: {Command}", - startupCommand - ); + _logger.LogInformation("Running startup command: {Command}", startupCommand); _ = await ProcessCommandAsync( startupCommand, _startupCommandOptions.Arguments, @@ -112,10 +109,7 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) if (!string.IsNullOrWhiteSpace(_startupCommandOptions.Command)) { string startupCommand = _startupCommandOptions.Command!; - _logger.LogInformation( - "Running startup command: {Command}", - startupCommand - ); + _logger.LogInformation("Running startup command: {Command}", startupCommand); _ = await ProcessCommandAsync( startupCommand, _startupCommandOptions.Arguments, @@ -322,10 +316,11 @@ await _obsClient.Scenes.GetCurrentProgramSceneAsync( string inputNameToMute = string.Join(" ", args); _logger.LogInformation("Toggling mute for input: {InputName}", inputNameToMute); - ToggleInputMuteResponseData? muteState = await _obsClient.Inputs.ToggleInputMuteAsync( - new ToggleInputMuteRequestData(inputNameToMute), - cancellationToken: cancellationToken - ); + ToggleInputMuteResponseData? muteState = + await _obsClient.Inputs.ToggleInputMuteAsync( + new ToggleInputMuteRequestData(inputNameToMute), + cancellationToken: cancellationToken + ); if (muteState is null) { UiWarn($"Could not toggle mute state for {inputNameToMute}. Does it exist?"); @@ -357,9 +352,11 @@ await _obsClient.Scenes.GetCurrentProgramSceneAsync( ); // Now get the input settings using the *source name* (not the scene item ID) - GetInputSettingsResponseData? settings = await _obsClient.Inputs.GetInputSettingsAsync(new GetInputSettingsRequestData(inputForGetSettings), - cancellationToken: cancellationToken - ); + GetInputSettingsResponseData? settings = + await _obsClient.Inputs.GetInputSettingsAsync( + new GetInputSettingsRequestData(inputForGetSettings), + cancellationToken: cancellationToken + ); if (settings?.InputSettings is JsonElement inputSettingsElement) { @@ -385,9 +382,7 @@ await _obsClient.Scenes.GetCurrentProgramSceneAsync( case "set-text": if (args.Length < 3) { - UiWarn( - "Usage: set-text [scene name] [text source name] [new text...]" - ); + UiWarn("Usage: set-text [scene name] [text source name] [new text...]"); return false; } @@ -410,7 +405,11 @@ await _obsClient.Scenes.GetCurrentProgramSceneAsync( ); // Uses SetInputTextAsync helper which serializes TextGdiPlusInputSettings internally. - await _obsClient.Inputs.SetInputTextAsync(inputForSetText, newText, cancellationToken); + await _obsClient.Inputs.SetInputTextAsync( + inputForSetText, + newText, + cancellationToken + ); UiSuccess($"Successfully set text for '{inputForSetText}' to: '{newText}'"); } catch (SceneItemNotFoundException ex) @@ -445,7 +444,10 @@ await _obsClient.Filters.GetSourceFilterListAsync( ); if (filterList?.Filters is not null && filterList.Filters.Count > 0) { - Table table = new() { Title = new TableTitle($"Filters for '{sourceForFilters}'") }; + Table table = new() + { + Title = new TableTitle($"Filters for '{sourceForFilters}'"), + }; _ = table.AddColumn("Index"); _ = table.AddColumn("Name"); _ = table.AddColumn("Kind"); @@ -453,8 +455,10 @@ await _obsClient.Filters.GetSourceFilterListAsync( foreach (Core.Protocol.Common.FilterStub filterElement in filterList.Filters) { string filterIndex = filterElement.FilterIndex?.ToString() ?? "N/A"; - string filterName = Markup.Escape(filterElement.FilterName ?? "N/A") ?? "N/A"; - string filterKind = Markup.Escape(filterElement.FilterKind ?? "N/A") ?? "N/A"; + string filterName = + Markup.Escape(filterElement.FilterName ?? "N/A") ?? "N/A"; + string filterKind = + Markup.Escape(filterElement.FilterKind ?? "N/A") ?? "N/A"; _ = table.AddRow( filterIndex, filterName, @@ -523,12 +527,12 @@ await _obsClient.Filters.SetSourceFilterEnabledAsync( // Streams are the ergonomic way to observe events: subscribe for the lifetime // of the loop, no handler bookkeeping, and cancellation ends it cleanly. The // classic events on the client are untouched and still work alongside this. - int seconds = args.Length > 0 && int.TryParse(args[0], out int parsed) ? parsed : 15; + int seconds = + args.Length > 0 && int.TryParse(args[0], out int parsed) ? parsed : 15; UiInfo($"Watching scene changes for {seconds}s. Switch scenes in OBS."); - using CancellationTokenSource watchCts = CancellationTokenSource.CreateLinkedTokenSource( - cancellationToken - ); + using CancellationTokenSource watchCts = + CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); watchCts.CancelAfter(TimeSpan.FromSeconds(seconds)); try @@ -574,7 +578,9 @@ CurrentProgramSceneChangedEventArgs sceneEvent in _obsClient.Scenes.CurrentProgr ObsBatchBuilder exampleBatch = new(); _ = exampleBatch.General.GetVersion(); _ = exampleBatch.Scenes.GetCurrentProgramScene(); - _ = exampleBatch.Inputs.GetInputList(new GetInputListRequestData("text_gdiplus_v3")); + _ = exampleBatch.Inputs.GetInputList( + new GetInputListRequestData("text_gdiplus_v3") + ); _ = exampleBatch.General.Sleep(new SleepRequestData(sleepMillis: 100)); _ = exampleBatch.Inputs.SetInputSettings( new SetInputSettingsRequestData( @@ -587,14 +593,19 @@ CurrentProgramSceneChangedEventArgs sceneEvent in _obsClient.Scenes.CurrentProgr // Add remains for anything the generated methods do not cover. _ = exampleBatch.Add("GetStats"); - List> batchResults = (await _obsClient.CallBatchAsync( - exampleBatch, - executionType: RequestBatchExecutionType.SerialRealtime, - haltOnFailure: false, // Continue even if one fails - cancellationToken: cancellationToken - )).Raw.ToList(); + List> batchResults = ( + await _obsClient.CallBatchAsync( + exampleBatch, + executionType: RequestBatchExecutionType.SerialRealtime, + haltOnFailure: false, // Continue even if one fails + cancellationToken: cancellationToken + ) + ).Raw.ToList(); - Table batchTable = new() { Title = new TableTitle($"Batch Results ({batchResults.Count} items)") }; + Table batchTable = new() + { + Title = new TableTitle($"Batch Results ({batchResults.Count} items)"), + }; _ = batchTable.AddColumn("Request"); _ = batchTable.AddColumn("Status"); _ = batchTable.AddColumn("Code"); @@ -602,7 +613,9 @@ CurrentProgramSceneChangedEventArgs sceneEvent in _obsClient.Scenes.CurrentProgr foreach (RequestResponsePayload result in batchResults) { string shortId = result.RequestId[(result.RequestId.LastIndexOf('_') + 1)..]; - string status = result.RequestStatus.Result ? "[green]Success[/]" : "[red]Failed[/]"; + string status = result.RequestStatus.Result + ? "[green]Success[/]" + : "[red]Failed[/]"; string details = string.Empty; if (!result.RequestStatus.Result) { @@ -613,10 +626,9 @@ CurrentProgramSceneChangedEventArgs sceneEvent in _obsClient.Scenes.CurrentProgr string responseJson = "Could not serialize response data"; try { - responseJson = - result.ResponseData is JsonElement jsonElement - ? jsonElement.GetRawText() - : result.ResponseData.ToString() ?? string.Empty; + responseJson = result.ResponseData is JsonElement jsonElement + ? jsonElement.GetRawText() + : result.ResponseData.ToString() ?? string.Empty; } catch { /* Ignore serialization errors for logging */ @@ -653,10 +665,7 @@ result.ResponseData is JsonElement jsonElement "Current Intended Flags", $"{_currentSubscriptionFlags} ({(EventSubscription)_currentSubscriptionFlags})" ), - ( - "Note", - "Reflects last requested flags, not server-acknowledged state." - ), + ("Note", "Reflects last requested flags, not server-acknowledged state."), ] ); return false; @@ -664,8 +673,11 @@ result.ResponseData is JsonElement jsonElement case "media": { // Typed enum rather than a protocol string constant. - if (args.Length < 2 || MediaInputActionExtensions.FromWireValue(args[1]) is null - && !Enum.TryParse(args[1], ignoreCase: true, out MediaInputAction _)) + if ( + args.Length < 2 + || MediaInputActionExtensions.FromWireValue(args[1]) is null + && !Enum.TryParse(args[1], ignoreCase: true, out MediaInputAction _) + ) { UiWarn("Usage: media "); return false; @@ -679,13 +691,19 @@ result.ResponseData is JsonElement jsonElement try { - await _obsClient.MediaInputs.TriggerMediaActionAsync(args[0], action, cancellationToken); + await _obsClient.MediaInputs.TriggerMediaActionAsync( + args[0], + action, + cancellationToken + ); UiSuccess($"Sent {action} ({action.ToWireValue()}) to '{args[0]}'."); } catch (ObsWebSocketRequestException ex) { // Typed failure carries the protocol status, so no message matching. - UiWarn($"OBS rejected {ex.RequestType} with code {ex.Status?.Code}: {ex.Comment}"); + UiWarn( + $"OBS rejected {ex.RequestType} with code {ex.Status?.Code}: {ex.Comment}" + ); } return false; @@ -695,12 +713,8 @@ result.ResponseData is JsonElement jsonElement if (args.Length == 0 || !uint.TryParse(args[0], out uint newFlags)) { UiWarn("Usage: set-subs "); - UiInfo( - "Example: set-subs 65 (General | Scenes | Inputs, 1 | 4 | 8 = 13)" - ); - UiInfo( - "See ObsWebSocket.Core.Protocol.Generated.EventSubscription for flags." - ); + UiInfo("Example: set-subs 65 (General | Scenes | Inputs, 1 | 4 | 8 = 13)"); + UiInfo("See ObsWebSocket.Core.Protocol.Generated.EventSubscription for flags."); return false; } @@ -709,10 +723,7 @@ result.ResponseData is JsonElement jsonElement newFlags, (EventSubscription)newFlags ); - await _obsClient.ReidentifyAsync( - newFlags, - cancellationToken: cancellationToken - ); + await _obsClient.ReidentifyAsync(newFlags, cancellationToken: cancellationToken); _currentSubscriptionFlags = (EventSubscription)newFlags; UiSuccess( $"Re-identified successfully. Intended subscriptions set to: {_currentSubscriptionFlags}" @@ -737,10 +748,7 @@ await _obsClient.ReidentifyAsync( private async Task RunTransportValidationSuiteAsync(CancellationToken cancellationToken) { int iterations = Math.Max(1, _validationOptions.ValidationIterations); - Rule rule = new("[cyan]Transport Validation[/]") - { - Justification = Justify.Left - }; + Rule rule = new("[cyan]Transport Validation[/]") { Justification = Justify.Left }; AnsiConsole.Write(rule); for (int i = 0; i < iterations; i++) { @@ -804,9 +812,7 @@ version is null .ConfigureAwait(false); if (scenes?.Scenes is null || scenes.Scenes.Count == 0) { - throw new InvalidOperationException( - $"[{format}] GetSceneList returned no scenes." - ); + throw new InvalidOperationException($"[{format}] GetSceneList returned no scenes."); } _logger.LogInformation( @@ -821,9 +827,7 @@ version is null .ConfigureAwait(false); if (inputs?.Inputs is null || inputs.Inputs.Count == 0) { - throw new InvalidOperationException( - $"[{format}] GetInputList returned no inputs." - ); + throw new InvalidOperationException($"[{format}] GetInputList returned no inputs."); } _logger.LogInformation( @@ -899,13 +903,12 @@ int extensionEntryCount ) .RootElement.Clone(); - Task waitForCustomEvent = cycleClient.WaitForEventAsync< - CustomEventEventArgs - >( - predicate: _ => true, - timeout: TimeSpan.FromSeconds(2), - cancellationToken: cancellationToken - ); + Task waitForCustomEvent = + cycleClient.WaitForEventAsync( + predicate: _ => true, + timeout: TimeSpan.FromSeconds(2), + cancellationToken: cancellationToken + ); await cycleClient .General.BroadcastCustomEventAsync( @@ -933,7 +936,8 @@ out JsonElement actualCustomData ) && actualCustomData.GetProperty("testId").GetString() == testId && actualCustomData.GetProperty("nested").GetProperty("enabled").GetBoolean() - && actualCustomData.GetProperty("nested").GetProperty("levels").GetArrayLength() == 3 + && actualCustomData.GetProperty("nested").GetProperty("levels").GetArrayLength() + == 3 ) { customEventVerified = true; @@ -967,14 +971,19 @@ customEvent is not null ); } - _logger.LogInformation("[{Format}] Batch call results: {ResultCount}", format, batch.Count); + _logger.LogInformation( + "[{Format}] Batch call results: {ResultCount}", + format, + batch.Count + ); List<(string Label, bool Pass, string Detail)> settingsResults = await ValidateSettingsModesAsync(cycleClient, inputs, cancellationToken) .ConfigureAwait(false); List<(string Label, bool Pass, string Detail)> modernResults = - await ValidateModernApisAsync(cycleClient, healthChecks, cancellationToken).ConfigureAwait(false); + await ValidateModernApisAsync(cycleClient, healthChecks, cancellationToken) + .ConfigureAwait(false); Table summary = new() { Title = new TableTitle($"{format} Validation Summary") }; _ = summary.AddColumn("Check"); @@ -991,7 +1000,10 @@ await ValidateSettingsModesAsync(cycleClient, inputs, cancellationToken) ? $"[green]Pass[/] ({extensionBagCount} bag(s), {extensionEntryCount} entries)" : "[yellow]Unverified[/]" ); - _ = summary.AddRow("CustomEvent", customEventVerified ? "[green]Pass[/]" : "[yellow]Unverified[/]"); + _ = summary.AddRow( + "CustomEvent", + customEventVerified ? "[green]Pass[/]" : "[yellow]Unverified[/]" + ); _ = summary.AddRow("Batch", $"{batch.Count} result(s)"); foreach ((string label, bool pass, string detail) in settingsResults) { @@ -1017,7 +1029,8 @@ await ValidateSettingsModesAsync(cycleClient, inputs, cancellationToken) { if (cycleClient.IsConnected) { - await cycleClient.DisconnectAsync(cancellationToken: CancellationToken.None) + await cycleClient + .DisconnectAsync(cancellationToken: CancellationToken.None) .ConfigureAwait(false); } } @@ -1028,10 +1041,13 @@ await cycleClient.DisconnectAsync(cancellationToken: CancellationToken.None) /// All operations are read-then-write-back (overlay:true) so they are non-destructive. /// Requires at least one browser_source and one input with a gain_filter in OBS. /// - private static async Task> ValidateSettingsModesAsync( + private static async Task< + List<(string Label, bool Pass, string Detail)> + > ValidateSettingsModesAsync( ObsWebSocketClient client, GetInputListResponseData? inputs, - CancellationToken cancellationToken) + CancellationToken cancellationToken + ) { List<(string Label, bool Pass, string Detail)> results = []; if (inputs is null) @@ -1041,72 +1057,130 @@ await cycleClient.DisconnectAsync(cancellationToken: CancellationToken.None) } // ── InputSettings ───────────────────────────────────────────────────── - string? browserInputName = inputs.Inputs - ?.FirstOrDefault(i => string.Equals(i.InputKind, "browser_source", StringComparison.OrdinalIgnoreCase)) + string? browserInputName = inputs + .Inputs?.FirstOrDefault(i => + string.Equals(i.InputKind, "browser_source", StringComparison.OrdinalIgnoreCase) + ) ?.InputName; if (string.IsNullOrEmpty(browserInputName)) { - results.Add(("InputSettings [all modes]", false, "No browser_source in OBS — add one to test")); + results.Add( + ("InputSettings [all modes]", false, "No browser_source in OBS — add one to test") + ); } else { // Mode 1: raw JsonElement via protocol-level call - results.Add(await TrySettingsCheckAsync("InputSettings Mode1 (raw JsonElement)", async () => - { - GetInputSettingsResponseData? r = await client.Inputs.GetInputSettingsAsync(new GetInputSettingsRequestData(browserInputName), cancellationToken); - if (r?.InputSettings is not JsonElement el) - { - return (false, "null InputSettings in response"); - } + results.Add( + await TrySettingsCheckAsync( + "InputSettings Mode1 (raw JsonElement)", + async () => + { + GetInputSettingsResponseData? r = await client.Inputs.GetInputSettingsAsync( + new GetInputSettingsRequestData(browserInputName), + cancellationToken + ); + if (r?.InputSettings is not JsonElement el) + { + return (false, "null InputSettings in response"); + } - await client.Inputs.SetInputSettingsAsync(new SetInputSettingsRequestData(el, inputName: browserInputName, overlay: true), - cancellationToken); - string url = el.TryGetProperty("url", out JsonElement p) ? p.GetString() ?? "(no url)" : "(no url key)"; - return (true, $"'{browserInputName}' url={url}"); - })); + await client.Inputs.SetInputSettingsAsync( + new SetInputSettingsRequestData( + el, + inputName: browserInputName, + overlay: true + ), + cancellationToken + ); + string url = el.TryGetProperty("url", out JsonElement p) + ? p.GetString() ?? "(no url)" + : "(no url key)"; + return (true, $"'{browserInputName}' url={url}"); + } + ) + ); // Mode 2: library-registered type via implicit GetTypeInfo lookup - results.Add(await TrySettingsCheckAsync("InputSettings Mode2 (BrowserSourceSettings)", async () => - { - BrowserSourceSettings? s = await client.Inputs.GetInputSettingsAsync( - browserInputName, cancellationToken); - if (s is null) - { - return (false, "null result"); - } + results.Add( + await TrySettingsCheckAsync( + "InputSettings Mode2 (BrowserSourceSettings)", + async () => + { + BrowserSourceSettings? s = + await client.Inputs.GetInputSettingsAsync( + browserInputName, + cancellationToken + ); + if (s is null) + { + return (false, "null result"); + } - await client.Inputs.SetInputSettingsAsync(browserInputName, s, overlay: true, cancellationToken: cancellationToken); - return (true, $"'{browserInputName}' url={s.Url ?? "(null)"}"); - })); + await client.Inputs.SetInputSettingsAsync( + browserInputName, + s, + overlay: true, + cancellationToken: cancellationToken + ); + return (true, $"'{browserInputName}' url={s.Url ?? "(null)"}"); + } + ) + ); // Mode 3: consumer-defined type with explicit JsonTypeInfo - results.Add(await TrySettingsCheckAsync("InputSettings Mode3 (consumer JsonTypeInfo)", async () => - { - JsonTypeInfo typeInfo = WorkerSettingsJsonContext.Default.WorkerBrowserUrlSettings; - WorkerBrowserUrlSettings? s = await client.Inputs.GetInputSettingsAsync(browserInputName, typeInfo, cancellationToken); - if (s is null) - { - return (false, "null result"); - } + results.Add( + await TrySettingsCheckAsync( + "InputSettings Mode3 (consumer JsonTypeInfo)", + async () => + { + JsonTypeInfo typeInfo = WorkerSettingsJsonContext + .Default + .WorkerBrowserUrlSettings; + WorkerBrowserUrlSettings? s = await client.Inputs.GetInputSettingsAsync( + browserInputName, + typeInfo, + cancellationToken + ); + if (s is null) + { + return (false, "null result"); + } - await client.Inputs.SetInputSettingsAsync(browserInputName, s, typeInfo, overlay: true, cancellationToken: cancellationToken); - return (true, $"'{browserInputName}' url={s.Url ?? "(null)"}"); - })); + await client.Inputs.SetInputSettingsAsync( + browserInputName, + s, + typeInfo, + overlay: true, + cancellationToken: cancellationToken + ); + return (true, $"'{browserInputName}' url={s.Url ?? "(null)"}"); + } + ) + ); } // ── FilterSettings ──────────────────────────────────────────────────── // Find first gain_filter across the first 5 inputs. string? filterSourceName = null; string? gainFilterName = null; - foreach (Core.Protocol.Common.InputStub input in inputs.Inputs?.Where(i => !string.IsNullOrEmpty(i.InputName)).Take(5) ?? []) + foreach ( + Core.Protocol.Common.InputStub input in inputs + .Inputs?.Where(i => !string.IsNullOrEmpty(i.InputName)) + .Take(5) + ?? [] + ) { try { GetSourceFilterListResponseData? fl = await client.Filters.GetSourceFilterListAsync( - new GetSourceFilterListRequestData(sourceName: input.InputName!), cancellationToken); + new GetSourceFilterListRequestData(sourceName: input.InputName!), + cancellationToken + ); Core.Protocol.Common.FilterStub? gain = fl?.Filters?.FirstOrDefault(f => - string.Equals(f.FilterKind, "gain_filter", StringComparison.OrdinalIgnoreCase)); + string.Equals(f.FilterKind, "gain_filter", StringComparison.OrdinalIgnoreCase) + ); if (gain?.FilterName is not null) { filterSourceName = input.InputName; @@ -1114,60 +1188,126 @@ await client.Inputs.SetInputSettingsAsync(new SetInputSettingsRequestData(el, in break; } } - catch { /* skip inputs we can't query */ } + catch + { /* skip inputs we can't query */ + } } if (string.IsNullOrEmpty(filterSourceName) || string.IsNullOrEmpty(gainFilterName)) { - results.Add(("FilterSettings [all modes]", false, "No gain_filter found — add one to an input in OBS")); + results.Add( + ( + "FilterSettings [all modes]", + false, + "No gain_filter found — add one to an input in OBS" + ) + ); } else { // Mode 1: raw JsonElement via protocol-level call - results.Add(await TrySettingsCheckAsync("FilterSettings Mode1 (raw JsonElement)", async () => - { - GetSourceFilterResponseData? r = await client.Filters.GetSourceFilterAsync( - new GetSourceFilterRequestData { SourceName = filterSourceName, FilterName = gainFilterName }, - cancellationToken); - if (r?.FilterSettings is not JsonElement el) - { - return (false, "null FilterSettings in response"); - } + results.Add( + await TrySettingsCheckAsync( + "FilterSettings Mode1 (raw JsonElement)", + async () => + { + GetSourceFilterResponseData? r = await client.Filters.GetSourceFilterAsync( + new GetSourceFilterRequestData + { + SourceName = filterSourceName, + FilterName = gainFilterName, + }, + cancellationToken + ); + if (r?.FilterSettings is not JsonElement el) + { + return (false, "null FilterSettings in response"); + } - await client.Filters.SetSourceFilterSettingsAsync(new SetSourceFilterSettingsRequestData(gainFilterName, el, sourceName: filterSourceName, overlay: true), - cancellationToken); - string db = el.TryGetProperty("db", out JsonElement p) ? p.GetDouble().ToString("F1") : "(no db key)"; - return (true, $"'{filterSourceName}/{gainFilterName}' db={db}"); - })); + await client.Filters.SetSourceFilterSettingsAsync( + new SetSourceFilterSettingsRequestData( + gainFilterName, + el, + sourceName: filterSourceName, + overlay: true + ), + cancellationToken + ); + string db = el.TryGetProperty("db", out JsonElement p) + ? p.GetDouble().ToString("F1") + : "(no db key)"; + return (true, $"'{filterSourceName}/{gainFilterName}' db={db}"); + } + ) + ); // Mode 2: library-registered type via implicit GetTypeInfo lookup - results.Add(await TrySettingsCheckAsync("FilterSettings Mode2 (GainFilterSettings)", async () => - { - GainFilterSettings? s = await client.Filters.GetSourceFilterSettingsAsync( - filterSourceName, gainFilterName, cancellationToken); - if (s is null) - { - return (false, "null result"); - } + results.Add( + await TrySettingsCheckAsync( + "FilterSettings Mode2 (GainFilterSettings)", + async () => + { + GainFilterSettings? s = + await client.Filters.GetSourceFilterSettingsAsync( + filterSourceName, + gainFilterName, + cancellationToken + ); + if (s is null) + { + return (false, "null result"); + } - await client.Filters.SetSourceFilterSettingsAsync(filterSourceName, gainFilterName, s, overlay: true, cancellationToken: cancellationToken); - return (true, $"'{filterSourceName}/{gainFilterName}' db={s.Db?.ToString("F1") ?? "(null)"}"); - })); + await client.Filters.SetSourceFilterSettingsAsync( + filterSourceName, + gainFilterName, + s, + overlay: true, + cancellationToken: cancellationToken + ); + return ( + true, + $"'{filterSourceName}/{gainFilterName}' db={s.Db?.ToString("F1") ?? "(null)"}" + ); + } + ) + ); // Mode 3: consumer-defined type with explicit JsonTypeInfo - results.Add(await TrySettingsCheckAsync("FilterSettings Mode3 (consumer JsonTypeInfo)", async () => - { - JsonTypeInfo typeInfo = WorkerSettingsJsonContext.Default.WorkerGainDbSettings; - WorkerGainDbSettings? s = await client.Filters.GetSourceFilterSettingsAsync( - filterSourceName, gainFilterName, typeInfo, cancellationToken); - if (s is null) - { - return (false, "null result"); - } + results.Add( + await TrySettingsCheckAsync( + "FilterSettings Mode3 (consumer JsonTypeInfo)", + async () => + { + JsonTypeInfo typeInfo = WorkerSettingsJsonContext + .Default + .WorkerGainDbSettings; + WorkerGainDbSettings? s = await client.Filters.GetSourceFilterSettingsAsync( + filterSourceName, + gainFilterName, + typeInfo, + cancellationToken + ); + if (s is null) + { + return (false, "null result"); + } - await client.Filters.SetSourceFilterSettingsAsync(filterSourceName, gainFilterName, s, typeInfo, overlay: true, cancellationToken: cancellationToken); - return (true, $"'{filterSourceName}/{gainFilterName}' db={s.Db?.ToString("F1") ?? "(null)"}"); - })); + await client.Filters.SetSourceFilterSettingsAsync( + filterSourceName, + gainFilterName, + s, + typeInfo, + overlay: true, + cancellationToken: cancellationToken + ); + return ( + true, + $"'{filterSourceName}/{gainFilterName}' db={s.Db?.ToString("F1") ?? "(null)"}" + ); + } + ) + ); } return results; @@ -1178,10 +1318,13 @@ await client.Filters.SetSourceFilterSettingsAsync(new SetSourceFilterSettingsReq /// so the run does not depend on any particular OBS layout. Everything it makes is removed /// again, whether the checks pass or not. /// - private static async Task> ValidateModernApisAsync( + private static async Task< + List<(string Label, bool Pass, string Detail)> + > ValidateModernApisAsync( ObsWebSocketClient client, HealthCheckService healthChecks, - CancellationToken cancellationToken) + CancellationToken cancellationToken + ) { List<(string Label, bool Pass, string Detail)> results = []; @@ -1204,584 +1347,973 @@ await client .ConfigureAwait(false); sceneCreated = true; - results.Add(await TrySettingsCheckAsync("SceneExistsAsync", async () => - { - bool present = await client.Scenes.SceneExistsAsync(sceneName, cancellationToken).ConfigureAwait(false); - bool absent = await client.Scenes.SceneExistsAsync(sceneName + "__nope", cancellationToken).ConfigureAwait(false); - return (present && !absent, $"present={present}, absent={!absent}"); - }).ConfigureAwait(false)); + results.Add( + await TrySettingsCheckAsync( + "SceneExistsAsync", + async () => + { + bool present = await client + .Scenes.SceneExistsAsync(sceneName, cancellationToken) + .ConfigureAwait(false); + bool absent = await client + .Scenes.SceneExistsAsync(sceneName + "__nope", cancellationToken) + .ConfigureAwait(false); + return (present && !absent, $"present={present}, absent={!absent}"); + } + ) + .ConfigureAwait(false) + ); // A media source carries audio, so the volume and media transport helpers apply. _ = await client - .Inputs.CreateInputAsync("ffmpeg_source", + .Inputs.CreateInputAsync( + "ffmpeg_source", inputName, new MediaSourceSettings(IsLocalFile: true), sceneName: sceneName, - cancellationToken: cancellationToken) + cancellationToken: cancellationToken + ) .ConfigureAwait(false); inputCreated = true; - results.Add(await TrySettingsCheckAsync("FindSceneItemIdAsync", async () => - { - double? hit = await client.SceneItems.FindSceneItemIdAsync(sceneName, inputName, cancellationToken).ConfigureAwait(false); - double? miss = await client.SceneItems.FindSceneItemIdAsync(sceneName, "__not_here__", cancellationToken).ConfigureAwait(false); - return (hit is not null && miss is null, $"hit={hit}, miss={(miss is null ? "null" : "unexpected")}"); - }).ConfigureAwait(false)); - - results.Add(await TrySettingsCheckAsync("SetSceneItemEnabledAsync (toggle)", async () => - { - bool off = await client.SceneItems.SetSceneItemEnabledAsync(sceneName, inputName, false, cancellationToken).ConfigureAwait(false); - bool toggled = await client.SceneItems.SetSceneItemEnabledAsync(sceneName, inputName, null, cancellationToken).ConfigureAwait(false); - return (!off && toggled, $"set false -> {off}, toggled -> {toggled}"); - }).ConfigureAwait(false)); - - results.Add(await TrySettingsCheckAsync("SetInputVolumeDbAsync", async () => - { - await client.Inputs.SetInputVolumeDbAsync(inputName, -6, cancellationToken).ConfigureAwait(false); - GetInputVolumeResponseData? volume = await client - .Inputs.GetInputVolumeAsync(new GetInputVolumeRequestData(inputName: inputName), cancellationToken) - .ConfigureAwait(false); - double db = volume?.InputVolumeDb ?? double.NaN; - return (Math.Abs(db + 6) < 0.5, $"db={db:0.##}"); - }).ConfigureAwait(false)); + results.Add( + await TrySettingsCheckAsync( + "FindSceneItemIdAsync", + async () => + { + double? hit = await client + .SceneItems.FindSceneItemIdAsync( + sceneName, + inputName, + cancellationToken + ) + .ConfigureAwait(false); + double? miss = await client + .SceneItems.FindSceneItemIdAsync( + sceneName, + "__not_here__", + cancellationToken + ) + .ConfigureAwait(false); + return ( + hit is not null && miss is null, + $"hit={hit}, miss={(miss is null ? "null" : "unexpected")}" + ); + } + ) + .ConfigureAwait(false) + ); - results.Add(await TrySettingsCheckAsync("Media transport (typed enum)", async () => - { - await client - .MediaInputs.TriggerMediaActionAsync(inputName, MediaInputAction.Stop, cancellationToken) - .ConfigureAwait(false); + results.Add( + await TrySettingsCheckAsync( + "SetSceneItemEnabledAsync (toggle)", + async () => + { + bool off = await client + .SceneItems.SetSceneItemEnabledAsync( + sceneName, + inputName, + false, + cancellationToken + ) + .ConfigureAwait(false); + bool toggled = await client + .SceneItems.SetSceneItemEnabledAsync( + sceneName, + inputName, + null, + cancellationToken + ) + .ConfigureAwait(false); + return (!off && toggled, $"set false -> {off}, toggled -> {toggled}"); + } + ) + .ConfigureAwait(false) + ); - // Read the state back, so this proves the action landed rather than only that - // the request was accepted. - GetMediaInputStatusResponseData? status = await client - .MediaInputs.GetMediaInputStatusAsync( - new GetMediaInputStatusRequestData(inputName: inputName), - cancellationToken) - .ConfigureAwait(false); + results.Add( + await TrySettingsCheckAsync( + "SetInputVolumeDbAsync", + async () => + { + await client + .Inputs.SetInputVolumeDbAsync(inputName, -6, cancellationToken) + .ConfigureAwait(false); + GetInputVolumeResponseData? volume = await client + .Inputs.GetInputVolumeAsync( + new GetInputVolumeRequestData(inputName: inputName), + cancellationToken + ) + .ConfigureAwait(false); + double db = volume?.InputVolumeDb ?? double.NaN; + return (Math.Abs(db + 6) < 0.5, $"db={db:0.##}"); + } + ) + .ConfigureAwait(false) + ); - string? state = status?.MediaState; - bool stopped = - state is not null - && state.Contains("STOPPED", StringComparison.Ordinal) - || state is not null && state.Contains("NONE", StringComparison.Ordinal); + results.Add( + await TrySettingsCheckAsync( + "Media transport (typed enum)", + async () => + { + await client + .MediaInputs.TriggerMediaActionAsync( + inputName, + MediaInputAction.Stop, + cancellationToken + ) + .ConfigureAwait(false); + + // Read the state back, so this proves the action landed rather than only that + // the request was accepted. + GetMediaInputStatusResponseData? status = await client + .MediaInputs.GetMediaInputStatusAsync( + new GetMediaInputStatusRequestData(inputName: inputName), + cancellationToken + ) + .ConfigureAwait(false); + + string? state = status?.MediaState; + bool stopped = + state is not null + && state.Contains("STOPPED", StringComparison.Ordinal) + || state is not null + && state.Contains("NONE", StringComparison.Ordinal); + + return ( + stopped, + $"sent {MediaInputAction.Stop.ToWireValue()}, state={state}" + ); + } + ) + .ConfigureAwait(false) + ); - return (stopped, $"sent {MediaInputAction.Stop.ToWireValue()}, state={state}"); - }).ConfigureAwait(false)); + results.Add( + await TrySettingsCheckAsync( + "Event stream (await foreach)", + async () => + { + using CancellationTokenSource streamCts = + CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + streamCts.CancelAfter(TimeSpan.FromSeconds(10)); + + List observed = []; + Task consume = Task.Run( + async () => + { + try + { + await foreach ( + CurrentProgramSceneChangedEventArgs sceneEvent in client + .Scenes.CurrentProgramSceneChangedStream( + cancellationToken: streamCts.Token + ) + .ConfigureAwait(false) + ) + { + observed.Add( + sceneEvent.EventData.SceneName ?? string.Empty + ); + if (observed.Count >= 2) + { + await streamCts.CancelAsync().ConfigureAwait(false); + } + } + } + catch (OperationCanceledException) + { + // Expected once both switches are seen or the window elapses. + } + }, + CancellationToken.None + ); + + await Task.Delay(250, cancellationToken).ConfigureAwait(false); + await client + .Scenes.SwitchSceneAsync( + sceneName, + cancellationToken: cancellationToken + ) + .ConfigureAwait(false); + await Task.Delay(400, cancellationToken).ConfigureAwait(false); + if (!string.IsNullOrEmpty(originalScene)) + { + await client + .Scenes.SwitchSceneAsync( + originalScene, + cancellationToken: cancellationToken + ) + .ConfigureAwait(false); + } - results.Add(await TrySettingsCheckAsync("Event stream (await foreach)", async () => - { - using CancellationTokenSource streamCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); - streamCts.CancelAfter(TimeSpan.FromSeconds(10)); + await consume.ConfigureAwait(false); + return ( + observed.Count >= 2, + $"observed {observed.Count}: {string.Join(" -> ", observed)}" + ); + } + ) + .ConfigureAwait(false) + ); - List observed = []; - Task consume = Task.Run(async () => - { - try - { - await foreach (CurrentProgramSceneChangedEventArgs sceneEvent - in client.Scenes.CurrentProgramSceneChangedStream(cancellationToken: streamCts.Token) - .ConfigureAwait(false)) + results.Add( + await TrySettingsCheckAsync( + "WaitForEventAsync (timeout overload)", + async () => { - observed.Add(sceneEvent.EventData.SceneName ?? string.Empty); - if (observed.Count >= 2) + Task wait = + client.WaitForEventAsync( + TimeSpan.FromSeconds(5), + cancellationToken + ); + _ = await client + .SceneItems.SetSceneItemEnabledAsync( + sceneName, + inputName, + false, + cancellationToken + ) + .ConfigureAwait(false); + try + { + SceneItemEnableStateChangedEventArgs observed = + await wait.ConfigureAwait(false); + return (true, $"enabled={observed.EventData.SceneItemEnabled}"); + } + catch (TimeoutException) { - await streamCts.CancelAsync().ConfigureAwait(false); + return (false, "timed out"); } } - } - catch (OperationCanceledException) - { - // Expected once both switches are seen or the window elapses. - } - }, CancellationToken.None); - - await Task.Delay(250, cancellationToken).ConfigureAwait(false); - await client.Scenes.SwitchSceneAsync(sceneName, cancellationToken: cancellationToken).ConfigureAwait(false); - await Task.Delay(400, cancellationToken).ConfigureAwait(false); - if (!string.IsNullOrEmpty(originalScene)) - { - await client.Scenes.SwitchSceneAsync(originalScene, cancellationToken: cancellationToken).ConfigureAwait(false); - } - - await consume.ConfigureAwait(false); - return (observed.Count >= 2, $"observed {observed.Count}: {string.Join(" -> ", observed)}"); - }).ConfigureAwait(false)); - - results.Add(await TrySettingsCheckAsync("WaitForEventAsync (timeout overload)", async () => - { - Task wait = client - .WaitForEventAsync(TimeSpan.FromSeconds(5), cancellationToken); - _ = await client.SceneItems.SetSceneItemEnabledAsync(sceneName, inputName, false, cancellationToken).ConfigureAwait(false); - try - { - SceneItemEnableStateChangedEventArgs observed = await wait.ConfigureAwait(false); - return (true, $"enabled={observed.EventData.SceneItemEnabled}"); - } - catch (TimeoutException) - { - return (false, "timed out"); - } - }).ConfigureAwait(false)); - - results.Add(await TrySettingsCheckAsync("Typed batch builder", async () => - { - ObsBatchBuilder batch = new(); - BatchRef versionRef = batch.General.GetVersion(); - _ = batch.General.Sleep(new SleepRequestData(sleepMillis: 25)); - BatchRef scenesRef = batch.Scenes.GetSceneList( - new GetSceneListRequestData() - ); - BatchRef statsRef = batch.General.GetStats(); - - BatchResults typedBatch = await client - .CallBatchAsync( - batch, - executionType: RequestBatchExecutionType.SerialRealtime, - haltOnFailure: false, - cancellationToken: cancellationToken) - .ConfigureAwait(false); - - // Each result is read through the reference its request handed back, so neither - // the position nor the response type is restated here. - GetVersionResponseData version = typedBatch.Get(versionRef); - GetSceneListResponseData scenes = typedBatch.Get(scenesRef); - GetStatsResponseData stats = typedBatch.Get(statsRef); - - return ( - typedBatch.Count == 4 - && typedBatch.AllSucceeded() - && version.ObsVersion is not null - && scenes.Scenes is not null, - $"{typedBatch.Count} result(s), OBS {version.ObsVersion}, {scenes.Scenes?.Count} scene(s), {stats.ActiveFps:0} fps" - ); - }).ConfigureAwait(false)); - - results.Add(await TrySettingsCheckAsync("Batch order and duplicates", async () => - { - // Repeats one request type with different payloads and interleaves others, so a - // result can only be matched to its request by position. - GetSceneListResponseData? allScenes = await client - .Scenes.GetSceneListAsync(new GetSceneListRequestData(), cancellationToken) - .ConfigureAwait(false); - string otherScene = allScenes! - .Scenes!.Select(scene => scene.SceneName!) - .First(name => !string.Equals(name, sceneName, StringComparison.Ordinal)); - - // The same request type appears three times with two different payloads, so a - // result can only be matched to its request through the reference it returned. - ObsBatchBuilder mixedBatch = new(); - BatchRef firstRef = mixedBatch.SceneItems.GetSceneItemList( - new GetSceneItemListRequestData(sceneName: sceneName) - ); - BatchRef versionRef = mixedBatch.General.GetVersion(); - BatchRef secondRef = mixedBatch.SceneItems.GetSceneItemList( - new GetSceneItemListRequestData(sceneName: otherScene) - ); - BatchRef thirdRef = mixedBatch.SceneItems.GetSceneItemList( - new GetSceneItemListRequestData(sceneName: sceneName) - ); - _ = mixedBatch.General.GetStats(); - - BatchResults mixed = await client - .CallBatchAsync( - mixedBatch, - executionType: RequestBatchExecutionType.SerialRealtime, - haltOnFailure: false, - cancellationToken: cancellationToken) - .ConfigureAwait(false); - - if (mixed.Count != 5 || !mixed.AllSucceeded()) - { - return (false, $"expected 5 successes, got {mixed.Count} with {mixed.GetFailures().Count()} failure(s)"); - } - - GetSceneItemListResponseData first = mixed.Get(firstRef); - GetVersionResponseData version = mixed.Get(versionRef); - GetSceneItemListResponseData second = mixed.Get(secondRef); - GetSceneItemListResponseData third = mixed.Get(thirdRef); - - // The two lookups of the same scene must agree, and differ from the other scene. - int firstCount = first.SceneItems?.Count ?? -1; - int secondCount = second.SceneItems?.Count ?? -1; - int thirdCount = third.SceneItems?.Count ?? -1; - bool repeatsAgree = firstCount == thirdCount; - bool distinguishable = firstCount != secondCount || !string.Equals(sceneName, otherScene, StringComparison.Ordinal); - - return ( - repeatsAgree && distinguishable && version.ObsVersion is not null, - $"[{firstCount}, v{version.ObsVersion}, {secondCount}, {thirdCount}] repeats agree = {repeatsAgree}" - ); - }).ConfigureAwait(false)); - - results.Add(await TrySettingsCheckAsync("Batch partial failure", async () => - { - // haltOnFailure false, so the good requests either side of a bad one still run. - ObsBatchBuilder partialBatch = new(); - BatchRef goodRef = partialBatch.General.GetVersion(); - BatchRef badRef = partialBatch.SceneItems.GetSceneItemList( - new GetSceneItemListRequestData(sceneName: "__no_such_scene__") - ); - _ = partialBatch.General.GetStats(); - - BatchResults partial = await client - .CallBatchAsync( - partialBatch, - executionType: RequestBatchExecutionType.SerialRealtime, - haltOnFailure: false, - cancellationToken: cancellationToken) - .ConfigureAwait(false); - - RequestResponsePayload[] failures = [.. partial.GetFailures()]; - if (partial.Count != 3 || failures.Length != 1) - { - return (false, $"{partial.Count} result(s), {failures.Length} failure(s)"); - } - - // GetRequiredData surfaces the OBS status rather than a null payload. - string caught; - try - { - _ = partial.Get(badRef); - caught = "no exception"; - } - catch (ObsWebSocketRequestException ex) - { - caught = $"code {ex.Status?.Code}"; - } - - // TryGet reports the failure without throwing. - bool tryGetReportedFailure = !partial.TryGet(badRef, out _); - bool neighboursOk = - tryGetReportedFailure && partial.Get(goodRef).ObsVersion is not null; - - return ( - neighboursOk && caught.StartsWith("code ", StringComparison.Ordinal), - $"1 failed ({caught}), neighbours ran" - ); - }).ConfigureAwait(false)); - - results.Add(await TrySettingsCheckAsync("Event stream buffering", async () => - { - // A stream keeps the newest events when a consumer falls behind rather than - // stalling the receive loop, so a small capacity drops the oldest. - using CancellationTokenSource cts = CancellationTokenSource.CreateLinkedTokenSource( - cancellationToken - ); - cts.CancelAfter(TimeSpan.FromSeconds(10)); - - IAsyncEnumerator enumerator = client - .SceneItems.SceneItemEnableStateChangedStream(capacity: 2, cancellationToken: cts.Token) - .GetAsyncEnumerator(cts.Token); - - try - { - ValueTask pending = enumerator.MoveNextAsync(); - - // Toggle more times than the buffer holds. - for (int i = 0; i < 4; i++) - { - _ = await client - .SceneItems.SetSceneItemEnabledAsync(sceneName, inputName, i % 2 == 0, cancellationToken) - .ConfigureAwait(false); - } - - bool first = await pending.ConfigureAwait(false); - return (first, first ? "buffered and delivered under capacity pressure" : "no event"); - } - finally - { - await enumerator.DisposeAsync().ConfigureAwait(false); - } - }).ConfigureAwait(false)); - - results.Add(await TrySettingsCheckAsync("Single-request values (non-batch)", async () => - { - // The same response types that come back empty inside a batch, fetched singly. - GetSceneItemListResponseData? items = await client - .SceneItems.GetSceneItemListAsync(new GetSceneItemListRequestData(sceneName: sceneName), cancellationToken) - .ConfigureAwait(false); - GetStatsResponseData? st = await client.General.GetStatsAsync(cancellationToken).ConfigureAwait(false); - GetVersionResponseData? ver = await client.General.GetVersionAsync(cancellationToken).ConfigureAwait(false); - - int itemCount = items?.SceneItems?.Count ?? -1; - double fps = st?.ActiveFps ?? 0; - - return ( - itemCount >= 0 && fps > 0 && ver?.ObsVersion is not null, - $"items={itemCount}, {fps:0} fps, v={ver?.ObsVersion}" - ); - }).ConfigureAwait(false)); - - results.Add(await TrySettingsCheckAsync("Batch parallel execution", async () => - { - // OBS pairs each result with another request's response data under parallel - // execution, so reading by reference must refuse rather than return the wrong - // request's payload. - ObsBatchBuilder par = new(); - BatchRef v = par.General.GetVersion(); - _ = par.SceneItems.GetSceneItemList( - new GetSceneItemListRequestData(sceneName: sceneName) - ); - - BatchResults r = await client - .CallBatchAsync( - par, - executionType: RequestBatchExecutionType.Parallel, - haltOnFailure: false, - cancellationToken: cancellationToken) - .ConfigureAwait(false); - - string guarded; - try - { - _ = r.Get(v); - guarded = "returned data"; - } - catch (ObsWebSocketException ex) - { - guarded = ex.Message.Contains("Parallel", StringComparison.Ordinal) - ? "refused" - : "threw: " + ex.Message; - } - - return ( - r.Count == 2 && guarded == "refused" && !r.TryGet(v, out GetVersionResponseData? _), - $"{r.Count} raw result(s), reference {guarded}" - ); - }).ConfigureAwait(false)); - - results.Add(await TrySettingsCheckAsync("Batch halt on failure", async () => - { - ObsBatchBuilder halt = new(); - BatchRef first = halt.General.GetVersion(); - BatchRef bad = halt.SceneItems.GetSceneItemList( - new GetSceneItemListRequestData(sceneName: "__no_such_scene__") - ); - BatchRef never = halt.General.GetStats(); - - BatchResults r = await client - .CallBatchAsync( - halt, - executionType: RequestBatchExecutionType.SerialRealtime, - haltOnFailure: true, - cancellationToken: cancellationToken) - .ConfigureAwait(false); - - bool firstOk = r.Get(first).ObsVersion is not null; - bool badRejected = !r.TryGet(bad, out GetSceneItemListResponseData? _); - - // The third request never ran, so reading it explains itself rather than - // returning someone else's result. - string neverMsg; - try - { - _ = r.Get(never); - neverMsg = "returned a result"; - } - catch (ObsWebSocketException ex) - { - neverMsg = ex.Message.Contains("never ran", StringComparison.Ordinal) - ? "explained" - : "threw: " + ex.Message; - } - - return ( - firstOk && badRejected && neverMsg == "explained", - $"{r.Count} result(s), unrun request {neverMsg}" - ); - }).ConfigureAwait(false)); - - results.Add(await TrySettingsCheckAsync("SetInputVolumeMulAsync", async () => - { - GetInputVolumeResponseData? before = await client - .Inputs.GetInputVolumeAsync(new GetInputVolumeRequestData(inputName: inputName), cancellationToken) - .ConfigureAwait(false); - double original = before!.InputVolumeMul; - - await client.Inputs.SetInputVolumeMulAsync(inputName, 0.5, cancellationToken).ConfigureAwait(false); - GetInputVolumeResponseData? after = await client - .Inputs.GetInputVolumeAsync(new GetInputVolumeRequestData(inputName: inputName), cancellationToken) - .ConfigureAwait(false); - double mul = after!.InputVolumeMul; - - await client.Inputs.SetInputVolumeMulAsync(inputName, original, cancellationToken).ConfigureAwait(false); - return (Math.Abs(mul - 0.5) < 0.01, $"mul={mul:0.###}"); - }).ConfigureAwait(false)); - - results.Add(await TrySettingsCheckAsync("SwitchProgramSceneAsync", async () => - { - await client.Scenes.SwitchProgramSceneAsync(sceneName, cancellationToken: cancellationToken).ConfigureAwait(false); - GetSceneListResponseData? mid = await client - .Scenes.GetSceneListAsync(new GetSceneListRequestData(), cancellationToken) - .ConfigureAwait(false); - bool switched = string.Equals(mid?.CurrentProgramSceneName, sceneName, StringComparison.Ordinal); + ) + .ConfigureAwait(false) + ); - await client.Scenes.SwitchProgramSceneAsync(originalScene, cancellationToken: cancellationToken).ConfigureAwait(false); - GetSceneListResponseData? restored = await client - .Scenes.GetSceneListAsync(new GetSceneListRequestData(), cancellationToken) - .ConfigureAwait(false); + results.Add( + await TrySettingsCheckAsync( + "Typed batch builder", + async () => + { + ObsBatchBuilder batch = new(); + BatchRef versionRef = + batch.General.GetVersion(); + _ = batch.General.Sleep(new SleepRequestData(sleepMillis: 25)); + BatchRef scenesRef = + batch.Scenes.GetSceneList(new GetSceneListRequestData()); + BatchRef statsRef = batch.General.GetStats(); + + BatchResults typedBatch = await client + .CallBatchAsync( + batch, + executionType: RequestBatchExecutionType.SerialRealtime, + haltOnFailure: false, + cancellationToken: cancellationToken + ) + .ConfigureAwait(false); + + // Each result is read through the reference its request handed back, so neither + // the position nor the response type is restated here. + GetVersionResponseData version = typedBatch.Get(versionRef); + GetSceneListResponseData scenes = typedBatch.Get(scenesRef); + GetStatsResponseData stats = typedBatch.Get(statsRef); + + return ( + typedBatch.Count == 4 + && typedBatch.AllSucceeded() + && version.ObsVersion is not null + && scenes.Scenes is not null, + $"{typedBatch.Count} result(s), OBS {version.ObsVersion}, {scenes.Scenes?.Count} scene(s), {stats.ActiveFps:0} fps" + ); + } + ) + .ConfigureAwait(false) + ); - return ( - switched && string.Equals(restored?.CurrentProgramSceneName, originalScene, StringComparison.Ordinal), - $"switched={switched}, restored to '{restored?.CurrentProgramSceneName}'" - ); - }).ConfigureAwait(false)); + results.Add( + await TrySettingsCheckAsync( + "Batch order and duplicates", + async () => + { + // Repeats one request type with different payloads and interleaves others, so a + // result can only be matched to its request by position. + GetSceneListResponseData? allScenes = await client + .Scenes.GetSceneListAsync( + new GetSceneListRequestData(), + cancellationToken + ) + .ConfigureAwait(false); + string otherScene = allScenes! + .Scenes!.Select(scene => scene.SceneName!) + .First(name => + !string.Equals(name, sceneName, StringComparison.Ordinal) + ); + + // The same request type appears three times with two different payloads, so a + // result can only be matched to its request through the reference it returned. + ObsBatchBuilder mixedBatch = new(); + BatchRef firstRef = + mixedBatch.SceneItems.GetSceneItemList( + new GetSceneItemListRequestData(sceneName: sceneName) + ); + BatchRef versionRef = + mixedBatch.General.GetVersion(); + BatchRef secondRef = + mixedBatch.SceneItems.GetSceneItemList( + new GetSceneItemListRequestData(sceneName: otherScene) + ); + BatchRef thirdRef = + mixedBatch.SceneItems.GetSceneItemList( + new GetSceneItemListRequestData(sceneName: sceneName) + ); + _ = mixedBatch.General.GetStats(); + + BatchResults mixed = await client + .CallBatchAsync( + mixedBatch, + executionType: RequestBatchExecutionType.SerialRealtime, + haltOnFailure: false, + cancellationToken: cancellationToken + ) + .ConfigureAwait(false); + + if (mixed.Count != 5 || !mixed.AllSucceeded()) + { + return ( + false, + $"expected 5 successes, got {mixed.Count} with {mixed.GetFailures().Count()} failure(s)" + ); + } - results.Add(await TrySettingsCheckAsync("FindSceneItemIdInt32Async", async () => - { - int? id = await client - .SceneItems.FindSceneItemIdInt32Async(sceneName, inputName, cancellationToken) - .ConfigureAwait(false); - int? miss = await client - .SceneItems.FindSceneItemIdInt32Async(sceneName, "__absent__", cancellationToken) - .ConfigureAwait(false); + GetSceneItemListResponseData first = mixed.Get(firstRef); + GetVersionResponseData version = mixed.Get(versionRef); + GetSceneItemListResponseData second = mixed.Get(secondRef); + GetSceneItemListResponseData third = mixed.Get(thirdRef); + + // The two lookups of the same scene must agree, and differ from the other scene. + int firstCount = first.SceneItems?.Count ?? -1; + int secondCount = second.SceneItems?.Count ?? -1; + int thirdCount = third.SceneItems?.Count ?? -1; + bool repeatsAgree = firstCount == thirdCount; + bool distinguishable = + firstCount != secondCount + || !string.Equals(sceneName, otherScene, StringComparison.Ordinal); + + return ( + repeatsAgree && distinguishable && version.ObsVersion is not null, + $"[{firstCount}, v{version.ObsVersion}, {secondCount}, {thirdCount}] repeats agree = {repeatsAgree}" + ); + } + ) + .ConfigureAwait(false) + ); - return (id is not null && miss is null, $"id={id}, miss={(miss is null ? "null" : "unexpected")}"); - }).ConfigureAwait(false)); + results.Add( + await TrySettingsCheckAsync( + "Batch partial failure", + async () => + { + // haltOnFailure false, so the good requests either side of a bad one still run. + ObsBatchBuilder partialBatch = new(); + BatchRef goodRef = + partialBatch.General.GetVersion(); + BatchRef badRef = + partialBatch.SceneItems.GetSceneItemList( + new GetSceneItemListRequestData(sceneName: "__no_such_scene__") + ); + _ = partialBatch.General.GetStats(); + + BatchResults partial = await client + .CallBatchAsync( + partialBatch, + executionType: RequestBatchExecutionType.SerialRealtime, + haltOnFailure: false, + cancellationToken: cancellationToken + ) + .ConfigureAwait(false); + + RequestResponsePayload[] failures = [.. partial.GetFailures()]; + if (partial.Count != 3 || failures.Length != 1) + { + return ( + false, + $"{partial.Count} result(s), {failures.Length} failure(s)" + ); + } - results.Add(await TrySettingsCheckAsync("Screenshot helpers", async () => - { - byte[]? bytes = await client - .Sources.GetSourceScreenshotBytesAsync(sceneName, "png", cancellationToken: cancellationToken) - .ConfigureAwait(false); + // GetRequiredData surfaces the OBS status rather than a null payload. + string caught; + try + { + _ = partial.Get(badRef); + caught = "no exception"; + } + catch (ObsWebSocketRequestException ex) + { + caught = $"code {ex.Status?.Code}"; + } - string path = Path.Combine(Path.GetTempPath(), $"obsws_{Guid.NewGuid():N}.png"); - try - { - await client - .Sources.SaveSourceScreenshotToFileAsync(sceneName, path, "png", cancellationToken: cancellationToken) - .ConfigureAwait(false); + // TryGet reports the failure without throwing. + bool tryGetReportedFailure = !partial.TryGet(badRef, out _); + bool neighboursOk = + tryGetReportedFailure + && partial.Get(goodRef).ObsVersion is not null; + + return ( + neighboursOk + && caught.StartsWith("code ", StringComparison.Ordinal), + $"1 failed ({caught}), neighbours ran" + ); + } + ) + .ConfigureAwait(false) + ); - // A PNG starts with the eight byte signature, so this checks real image data - // rather than merely that the call returned. - byte[] written = await File.ReadAllBytesAsync(path, cancellationToken).ConfigureAwait(false); - bool pngOnDisk = - written.Length > 8 - && written[0] == 0x89 && written[1] == 0x50 && written[2] == 0x4E && written[3] == 0x47; - bool pngInMemory = - bytes is { Length: > 8 } - && bytes[0] == 0x89 && bytes[1] == 0x50 && bytes[2] == 0x4E && bytes[3] == 0x47; - - return (pngInMemory && pngOnDisk, $"{bytes?.Length ?? 0} bytes in memory, {written.Length} on disk"); - } - finally - { - if (File.Exists(path)) - { - File.Delete(path); - } - } - }).ConfigureAwait(false)); + results.Add( + await TrySettingsCheckAsync( + "Event stream buffering", + async () => + { + // A stream keeps the newest events when a consumer falls behind rather than + // stalling the receive loop, so a small capacity drops the oldest. + using CancellationTokenSource cts = + CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + cts.CancelAfter(TimeSpan.FromSeconds(10)); + + IAsyncEnumerator enumerator = + client + .SceneItems.SceneItemEnableStateChangedStream( + capacity: 2, + cancellationToken: cts.Token + ) + .GetAsyncEnumerator(cts.Token); + + try + { + ValueTask pending = enumerator.MoveNextAsync(); + + // Toggle more times than the buffer holds. + for (int i = 0; i < 4; i++) + { + _ = await client + .SceneItems.SetSceneItemEnabledAsync( + sceneName, + inputName, + i % 2 == 0, + cancellationToken + ) + .ConfigureAwait(false); + } + + bool first = await pending.ConfigureAwait(false); + return ( + first, + first + ? "buffered and delivered under capacity pressure" + : "no event" + ); + } + finally + { + await enumerator.DisposeAsync().ConfigureAwait(false); + } + } + ) + .ConfigureAwait(false) + ); - results.Add(await TrySettingsCheckAsync("Ensure profile and scene collection", async () => - { - // Asking for the one already active proves the check without disrupting OBS, - // since switching either of these reloads the whole configuration. - GetProfileListResponseData? profiles = await client - .Config.GetProfileListAsync(cancellationToken) - .ConfigureAwait(false); - GetSceneCollectionListResponseData? collections = await client - .Config.GetSceneCollectionListAsync(cancellationToken) - .ConfigureAwait(false); + results.Add( + await TrySettingsCheckAsync( + "Single-request values (non-batch)", + async () => + { + // The same response types that come back empty inside a batch, fetched singly. + GetSceneItemListResponseData? items = await client + .SceneItems.GetSceneItemListAsync( + new GetSceneItemListRequestData(sceneName: sceneName), + cancellationToken + ) + .ConfigureAwait(false); + GetStatsResponseData? st = await client + .General.GetStatsAsync(cancellationToken) + .ConfigureAwait(false); + GetVersionResponseData? ver = await client + .General.GetVersionAsync(cancellationToken) + .ConfigureAwait(false); + + int itemCount = items?.SceneItems?.Count ?? -1; + double fps = st?.ActiveFps ?? 0; + + return ( + itemCount >= 0 && fps > 0 && ver?.ObsVersion is not null, + $"items={itemCount}, {fps:0} fps, v={ver?.ObsVersion}" + ); + } + ) + .ConfigureAwait(false) + ); - bool profileOk = await client - .Config.EnsureProfileActiveAsync(profiles!.CurrentProfileName!, cancellationToken) - .ConfigureAwait(false); - bool collectionOk = await client - .Config.EnsureSceneCollectionActiveAsync(collections!.CurrentSceneCollectionName!, cancellationToken) - .ConfigureAwait(false); - bool absent = await client - .Config.EnsureProfileActiveAsync("__no_such_profile__", cancellationToken) - .ConfigureAwait(false); + results.Add( + await TrySettingsCheckAsync( + "Batch parallel execution", + async () => + { + // OBS pairs each result with another request's response data under parallel + // execution, so reading by reference must refuse rather than return the wrong + // request's payload. + ObsBatchBuilder par = new(); + BatchRef v = par.General.GetVersion(); + _ = par.SceneItems.GetSceneItemList( + new GetSceneItemListRequestData(sceneName: sceneName) + ); + + BatchResults r = await client + .CallBatchAsync( + par, + executionType: RequestBatchExecutionType.Parallel, + haltOnFailure: false, + cancellationToken: cancellationToken + ) + .ConfigureAwait(false); + + string guarded; + try + { + _ = r.Get(v); + guarded = "returned data"; + } + catch (ObsWebSocketException ex) + { + guarded = ex.Message.Contains("Parallel", StringComparison.Ordinal) + ? "refused" + : "threw: " + ex.Message; + } - return ( - profileOk && collectionOk && !absent, - $"profile={profiles.CurrentProfileName}, collection={collections.CurrentSceneCollectionName}, absent reported {absent}" - ); - }).ConfigureAwait(false)); + return ( + r.Count == 2 + && guarded == "refused" + && !r.TryGet(v, out GetVersionResponseData? _), + $"{r.Count} raw result(s), reference {guarded}" + ); + } + ) + .ConfigureAwait(false) + ); - results.Add(await TrySettingsCheckAsync("Media transport shorthands", async () => - { - await client.MediaInputs.PlayMediaAsync(inputName, cancellationToken).ConfigureAwait(false); - await client.MediaInputs.PauseMediaAsync(inputName, cancellationToken).ConfigureAwait(false); - await client.MediaInputs.RestartMediaAsync(inputName, cancellationToken).ConfigureAwait(false); - await client.MediaInputs.StopMediaAsync(inputName, cancellationToken).ConfigureAwait(false); - - GetMediaInputStatusResponseData? status = await client - .MediaInputs.GetMediaInputStatusAsync( - new GetMediaInputStatusRequestData(inputName: inputName), cancellationToken) - .ConfigureAwait(false); + results.Add( + await TrySettingsCheckAsync( + "Batch halt on failure", + async () => + { + ObsBatchBuilder halt = new(); + BatchRef first = halt.General.GetVersion(); + BatchRef bad = + halt.SceneItems.GetSceneItemList( + new GetSceneItemListRequestData(sceneName: "__no_such_scene__") + ); + BatchRef never = halt.General.GetStats(); + + BatchResults r = await client + .CallBatchAsync( + halt, + executionType: RequestBatchExecutionType.SerialRealtime, + haltOnFailure: true, + cancellationToken: cancellationToken + ) + .ConfigureAwait(false); + + bool firstOk = r.Get(first).ObsVersion is not null; + bool badRejected = !r.TryGet(bad, out GetSceneItemListResponseData? _); + + // The third request never ran, so reading it explains itself rather than + // returning someone else's result. + string neverMsg; + try + { + _ = r.Get(never); + neverMsg = "returned a result"; + } + catch (ObsWebSocketException ex) + { + neverMsg = ex.Message.Contains( + "never ran", + StringComparison.Ordinal + ) + ? "explained" + : "threw: " + ex.Message; + } - return (status is not null, $"state={status?.MediaState}"); - }).ConfigureAwait(false)); + return ( + firstOk && badRejected && neverMsg == "explained", + $"{r.Count} result(s), unrun request {neverMsg}" + ); + } + ) + .ConfigureAwait(false) + ); - results.Add(await TrySettingsCheckAsync("Virtual camera toggle", async () => - { - bool before = await client.Outputs.IsVirtualCamActiveAsync(cancellationToken).ConfigureAwait(false); + results.Add( + await TrySettingsCheckAsync( + "SetInputVolumeMulAsync", + async () => + { + GetInputVolumeResponseData? before = await client + .Inputs.GetInputVolumeAsync( + new GetInputVolumeRequestData(inputName: inputName), + cancellationToken + ) + .ConfigureAwait(false); + double original = before!.InputVolumeMul; + + await client + .Inputs.SetInputVolumeMulAsync(inputName, 0.5, cancellationToken) + .ConfigureAwait(false); + GetInputVolumeResponseData? after = await client + .Inputs.GetInputVolumeAsync( + new GetInputVolumeRequestData(inputName: inputName), + cancellationToken + ) + .ConfigureAwait(false); + double mul = after!.InputVolumeMul; + + await client + .Inputs.SetInputVolumeMulAsync( + inputName, + original, + cancellationToken + ) + .ConfigureAwait(false); + return (Math.Abs(mul - 0.5) < 0.01, $"mul={mul:0.###}"); + } + ) + .ConfigureAwait(false) + ); - bool? turnedOn = await client - .Outputs.SetVirtualCamActiveAndWaitAsync(!before, cancellationToken: cancellationToken) - .ConfigureAwait(false); - bool observed = await client.Outputs.IsVirtualCamActiveAsync(cancellationToken).ConfigureAwait(false); + results.Add( + await TrySettingsCheckAsync( + "SwitchProgramSceneAsync", + async () => + { + await client + .Scenes.SwitchProgramSceneAsync( + sceneName, + cancellationToken: cancellationToken + ) + .ConfigureAwait(false); + GetSceneListResponseData? mid = await client + .Scenes.GetSceneListAsync( + new GetSceneListRequestData(), + cancellationToken + ) + .ConfigureAwait(false); + bool switched = string.Equals( + mid?.CurrentProgramSceneName, + sceneName, + StringComparison.Ordinal + ); + + await client + .Scenes.SwitchProgramSceneAsync( + originalScene, + cancellationToken: cancellationToken + ) + .ConfigureAwait(false); + GetSceneListResponseData? restored = await client + .Scenes.GetSceneListAsync( + new GetSceneListRequestData(), + cancellationToken + ) + .ConfigureAwait(false); + + return ( + switched + && string.Equals( + restored?.CurrentProgramSceneName, + originalScene, + StringComparison.Ordinal + ), + $"switched={switched}, restored to '{restored?.CurrentProgramSceneName}'" + ); + } + ) + .ConfigureAwait(false) + ); - // Put it back the way it was found. - _ = await client - .Outputs.SetVirtualCamActiveAndWaitAsync(before, cancellationToken: cancellationToken) - .ConfigureAwait(false); - bool restored = await client.Outputs.IsVirtualCamActiveAsync(cancellationToken).ConfigureAwait(false); + results.Add( + await TrySettingsCheckAsync( + "FindSceneItemIdInt32Async", + async () => + { + int? id = await client + .SceneItems.FindSceneItemIdInt32Async( + sceneName, + inputName, + cancellationToken + ) + .ConfigureAwait(false); + int? miss = await client + .SceneItems.FindSceneItemIdInt32Async( + sceneName, + "__absent__", + cancellationToken + ) + .ConfigureAwait(false); + + return ( + id is not null && miss is null, + $"id={id}, miss={(miss is null ? "null" : "unexpected")}" + ); + } + ) + .ConfigureAwait(false) + ); - return ( - turnedOn == !before && observed == !before && restored == before, - $"{before} -> {observed} -> {restored}" - ); - }).ConfigureAwait(false)); + results.Add( + await TrySettingsCheckAsync( + "Screenshot helpers", + async () => + { + byte[]? bytes = await client + .Sources.GetSourceScreenshotBytesAsync( + sceneName, + "png", + cancellationToken: cancellationToken + ) + .ConfigureAwait(false); + + string path = Path.Combine( + Path.GetTempPath(), + $"obsws_{Guid.NewGuid():N}.png" + ); + try + { + await client + .Sources.SaveSourceScreenshotToFileAsync( + sceneName, + path, + "png", + cancellationToken: cancellationToken + ) + .ConfigureAwait(false); + + // A PNG starts with the eight byte signature, so this checks real image data + // rather than merely that the call returned. + byte[] written = await File.ReadAllBytesAsync( + path, + cancellationToken + ) + .ConfigureAwait(false); + bool pngOnDisk = + written.Length > 8 + && written[0] == 0x89 + && written[1] == 0x50 + && written[2] == 0x4E + && written[3] == 0x47; + bool pngInMemory = + bytes is { Length: > 8 } + && bytes[0] == 0x89 + && bytes[1] == 0x50 + && bytes[2] == 0x4E + && bytes[3] == 0x47; + + return ( + pngInMemory && pngOnDisk, + $"{bytes?.Length ?? 0} bytes in memory, {written.Length} on disk" + ); + } + finally + { + if (File.Exists(path)) + { + File.Delete(path); + } + } + } + ) + .ConfigureAwait(false) + ); - results.Add(await TrySettingsCheckAsync("Typed exception on a rejected request", async () => - { - try - { - _ = await client - .SceneItems.GetSceneItemListAsync( - new GetSceneItemListRequestData(sceneName: "__no_such_scene__"), cancellationToken) - .ConfigureAwait(false); - return (false, "no exception"); - } - catch (ObsWebSocketRequestException ex) - { - return ( - ex.Status?.Code == 600 && ex.RequestType == "GetSceneItemList", - $"{ex.RequestType} code {ex.Status?.Code}" - ); - } - }).ConfigureAwait(false)); + results.Add( + await TrySettingsCheckAsync( + "Ensure profile and scene collection", + async () => + { + // Asking for the one already active proves the check without disrupting OBS, + // since switching either of these reloads the whole configuration. + GetProfileListResponseData? profiles = await client + .Config.GetProfileListAsync(cancellationToken) + .ConfigureAwait(false); + GetSceneCollectionListResponseData? collections = await client + .Config.GetSceneCollectionListAsync(cancellationToken) + .ConfigureAwait(false); + + bool profileOk = await client + .Config.EnsureProfileActiveAsync( + profiles!.CurrentProfileName!, + cancellationToken + ) + .ConfigureAwait(false); + bool collectionOk = await client + .Config.EnsureSceneCollectionActiveAsync( + collections!.CurrentSceneCollectionName!, + cancellationToken + ) + .ConfigureAwait(false); + bool absent = await client + .Config.EnsureProfileActiveAsync( + "__no_such_profile__", + cancellationToken + ) + .ConfigureAwait(false); + + return ( + profileOk && collectionOk && !absent, + $"profile={profiles.CurrentProfileName}, collection={collections.CurrentSceneCollectionName}, absent reported {absent}" + ); + } + ) + .ConfigureAwait(false) + ); - results.Add(await TrySettingsCheckAsync("Output state helpers", async () => - { - bool recording = await client.Record.IsRecordActiveAsync(cancellationToken).ConfigureAwait(false); - bool streaming = await client.Stream.IsStreamActiveAsync(cancellationToken).ConfigureAwait(false); - bool virtualCam = await client.Outputs.IsVirtualCamActiveAsync(cancellationToken).ConfigureAwait(false); + results.Add( + await TrySettingsCheckAsync( + "Media transport shorthands", + async () => + { + await client + .MediaInputs.PlayMediaAsync(inputName, cancellationToken) + .ConfigureAwait(false); + await client + .MediaInputs.PauseMediaAsync(inputName, cancellationToken) + .ConfigureAwait(false); + await client + .MediaInputs.RestartMediaAsync(inputName, cancellationToken) + .ConfigureAwait(false); + await client + .MediaInputs.StopMediaAsync(inputName, cancellationToken) + .ConfigureAwait(false); + + GetMediaInputStatusResponseData? status = await client + .MediaInputs.GetMediaInputStatusAsync( + new GetMediaInputStatusRequestData(inputName: inputName), + cancellationToken + ) + .ConfigureAwait(false); + + return (status is not null, $"state={status?.MediaState}"); + } + ) + .ConfigureAwait(false) + ); - // Each helper has to agree with the request it wraps. - GetRecordStatusResponseData? recordStatus = await client - .Record.GetRecordStatusAsync(cancellationToken) - .ConfigureAwait(false); - GetStreamStatusResponseData? streamStatus = await client - .Stream.GetStreamStatusAsync(cancellationToken) - .ConfigureAwait(false); - GetVirtualCamStatusResponseData? camStatus = await client - .Outputs.GetVirtualCamStatusAsync(cancellationToken) - .ConfigureAwait(false); + results.Add( + await TrySettingsCheckAsync( + "Virtual camera toggle", + async () => + { + bool before = await client + .Outputs.IsVirtualCamActiveAsync(cancellationToken) + .ConfigureAwait(false); + + bool? turnedOn = await client + .Outputs.SetVirtualCamActiveAndWaitAsync( + !before, + cancellationToken: cancellationToken + ) + .ConfigureAwait(false); + bool observed = await client + .Outputs.IsVirtualCamActiveAsync(cancellationToken) + .ConfigureAwait(false); + + // Put it back the way it was found. + _ = await client + .Outputs.SetVirtualCamActiveAndWaitAsync( + before, + cancellationToken: cancellationToken + ) + .ConfigureAwait(false); + bool restored = await client + .Outputs.IsVirtualCamActiveAsync(cancellationToken) + .ConfigureAwait(false); + + return ( + turnedOn == !before && observed == !before && restored == before, + $"{before} -> {observed} -> {restored}" + ); + } + ) + .ConfigureAwait(false) + ); - bool agrees = - recording == recordStatus?.OutputActive - && streaming == streamStatus?.OutputActive - && virtualCam == camStatus?.OutputActive; + results.Add( + await TrySettingsCheckAsync( + "Typed exception on a rejected request", + async () => + { + try + { + _ = await client + .SceneItems.GetSceneItemListAsync( + new GetSceneItemListRequestData( + sceneName: "__no_such_scene__" + ), + cancellationToken + ) + .ConfigureAwait(false); + return (false, "no exception"); + } + catch (ObsWebSocketRequestException ex) + { + return ( + ex.Status?.Code == 600 && ex.RequestType == "GetSceneItemList", + $"{ex.RequestType} code {ex.Status?.Code}" + ); + } + } + ) + .ConfigureAwait(false) + ); - return ( - agrees, - $"record={recording}, stream={streaming}, virtualCam={virtualCam}, agrees={agrees}" - ); - }).ConfigureAwait(false)); + results.Add( + await TrySettingsCheckAsync( + "Output state helpers", + async () => + { + bool recording = await client + .Record.IsRecordActiveAsync(cancellationToken) + .ConfigureAwait(false); + bool streaming = await client + .Stream.IsStreamActiveAsync(cancellationToken) + .ConfigureAwait(false); + bool virtualCam = await client + .Outputs.IsVirtualCamActiveAsync(cancellationToken) + .ConfigureAwait(false); + + // Each helper has to agree with the request it wraps. + GetRecordStatusResponseData? recordStatus = await client + .Record.GetRecordStatusAsync(cancellationToken) + .ConfigureAwait(false); + GetStreamStatusResponseData? streamStatus = await client + .Stream.GetStreamStatusAsync(cancellationToken) + .ConfigureAwait(false); + GetVirtualCamStatusResponseData? camStatus = await client + .Outputs.GetVirtualCamStatusAsync(cancellationToken) + .ConfigureAwait(false); + + bool agrees = + recording == recordStatus?.OutputActive + && streaming == streamStatus?.OutputActive + && virtualCam == camStatus?.OutputActive; + + return ( + agrees, + $"record={recording}, stream={streaming}, virtualCam={virtualCam}, agrees={agrees}" + ); + } + ) + .ConfigureAwait(false) + ); } finally { @@ -1790,20 +2322,31 @@ await client { if (!string.IsNullOrEmpty(originalScene)) { - await client.Scenes.SwitchSceneAsync(originalScene, cancellationToken: CancellationToken.None).ConfigureAwait(false); + await client + .Scenes.SwitchSceneAsync( + originalScene, + cancellationToken: CancellationToken.None + ) + .ConfigureAwait(false); } if (inputCreated) { await client - .Inputs.RemoveInputAsync(new RemoveInputRequestData(inputName: inputName), CancellationToken.None) + .Inputs.RemoveInputAsync( + new RemoveInputRequestData(inputName: inputName), + CancellationToken.None + ) .ConfigureAwait(false); } if (sceneCreated) { await client - .Scenes.RemoveSceneAsync(new RemoveSceneRequestData(sceneName: sceneName), CancellationToken.None) + .Scenes.RemoveSceneAsync( + new RemoveSceneRequestData(sceneName: sceneName), + CancellationToken.None + ) .ConfigureAwait(false); } } @@ -1818,7 +2361,8 @@ await client private static async Task<(string Label, bool Pass, string Detail)> TrySettingsCheckAsync( string label, - Func> action) + Func> action + ) { try { @@ -1855,8 +2399,8 @@ SerializationFormat format { List?> extensionBags = [ - ..(scenes?.Scenes ?? []).Select(scene => scene.ExtensionData), - ..(inputs?.Inputs ?? []).Select(input => input.ExtensionData), + .. (scenes?.Scenes ?? []).Select(scene => scene.ExtensionData), + .. (inputs?.Inputs ?? []).Select(input => input.ExtensionData), ]; int extensionBagCount = extensionBags.Count(bag => bag is { Count: > 0 }); @@ -1865,7 +2409,11 @@ SerializationFormat format .Sum(bag => bag!.Count); bool valid = true; - foreach (Dictionary? bag in extensionBags.Where(bag => bag is { Count: > 0 })) + foreach ( + Dictionary? bag in extensionBags.Where(bag => + bag is { Count: > 0 } + ) + ) { foreach ((string _, JsonElement value) in bag!) { @@ -1978,7 +2526,12 @@ out payload foreach (JsonElement element in source.EnumerateArray()) { if ( - TryFindCustomEventPayloadByTestIdCore(element, testId, depth + 1, out payload) + TryFindCustomEventPayloadByTestIdCore( + element, + testId, + depth + 1, + out payload + ) ) { return true; @@ -2066,14 +2619,17 @@ await DumpKindDefaultSettingsAsync( "Filter Kind Defaults", async ct => { - GetSourceFilterKindListResponseData? r = await _obsClient.Filters.GetSourceFilterKindListAsync(cancellationToken: ct); + GetSourceFilterKindListResponseData? r = + await _obsClient.Filters.GetSourceFilterKindListAsync(cancellationToken: ct); return r?.SourceFilterKinds ?? []; }, async (kind, ct) => { - GetSourceFilterDefaultSettingsResponseData? r = await _obsClient.Filters.GetSourceFilterDefaultSettingsAsync(new GetSourceFilterDefaultSettingsRequestData(kind), - cancellationToken: ct - ); + GetSourceFilterDefaultSettingsResponseData? r = + await _obsClient.Filters.GetSourceFilterDefaultSettingsAsync( + new GetSourceFilterDefaultSettingsRequestData(kind), + cancellationToken: ct + ); return r?.DefaultFilterSettings; }, cancellationToken @@ -2091,9 +2647,11 @@ await DumpKindDefaultSettingsAsync( }, async (kind, ct) => { - GetInputDefaultSettingsResponseData? r = await _obsClient.Inputs.GetInputDefaultSettingsAsync(new GetInputDefaultSettingsRequestData(kind), - cancellationToken: ct - ); + GetInputDefaultSettingsResponseData? r = + await _obsClient.Inputs.GetInputDefaultSettingsAsync( + new GetInputDefaultSettingsRequestData(kind), + cancellationToken: ct + ); return r?.DefaultInputSettings; }, cancellationToken @@ -2128,7 +2686,11 @@ CancellationToken cancellationToken return; } - _logger.LogInformation("Found {Count} kinds for '{Panel}'. Fetching defaults...", kinds.Count, panelTitle); + _logger.LogInformation( + "Found {Count} kinds for '{Panel}'. Fetching defaults...", + kinds.Count, + panelTitle + ); Dictionary results = new(StringComparer.OrdinalIgnoreCase); foreach (string kind in kinds) @@ -2186,19 +2748,28 @@ private async Task DumpOutputSettingsAsync(CancellationToken cancellationToken) string key = output.OutputKind is { } kind ? $"{name} ({kind})" : name; try { - GetOutputSettingsResponseData? r = await _obsClient.Outputs.GetOutputSettingsAsync(new GetOutputSettingsRequestData(outputName: name), + GetOutputSettingsResponseData? r = await _obsClient.Outputs.GetOutputSettingsAsync( + new GetOutputSettingsRequestData(outputName: name), cancellationToken: cancellationToken ); results[key] = r?.OutputSettings; } catch (ObsWebSocketException ex) { - _logger.LogWarning("Could not get settings for output '{Name}': {Msg}", name, ex.Message); + _logger.LogWarning( + "Could not get settings for output '{Name}': {Msg}", + name, + ex.Message + ); results[key] = null; } catch (Exception ex) { - _logger.LogError(ex, "Unexpected error getting settings for output '{Name}'.", name); + _logger.LogError( + ex, + "Unexpected error getting settings for output '{Name}'.", + name + ); results[key] = null; } } @@ -2210,8 +2781,10 @@ private async Task DumpStreamServiceSettingsAsync(CancellationToken cancellation { try { - GetStreamServiceSettingsResponseData? response = await _obsClient.Config.GetStreamServiceSettingsAsync(cancellationToken: cancellationToken - ); + GetStreamServiceSettingsResponseData? response = + await _obsClient.Config.GetStreamServiceSettingsAsync( + cancellationToken: cancellationToken + ); ArrayBufferWriter buf = new(); using (Utf8JsonWriter w = new(buf, new JsonWriterOptions { Indented = true })) @@ -2232,7 +2805,10 @@ private async Task DumpStreamServiceSettingsAsync(CancellationToken cancellation w.Flush(); } - RenderJsonPanel("Stream Service Settings", System.Text.Encoding.UTF8.GetString(buf.WrittenSpan)); + RenderJsonPanel( + "Stream Service Settings", + System.Text.Encoding.UTF8.GetString(buf.WrittenSpan) + ); } catch (Exception ex) { @@ -2279,19 +2855,21 @@ private async Task AddBrowserSourceAsync(CancellationToken cancellationToken) string currentProgramScene = sceneList.CurrentProgramSceneName ?? string.Empty; - List sceneNames = [ - ..sceneList.Scenes - .Select(s => s.SceneName) + List sceneNames = + [ + .. sceneList + .Scenes.Select(s => s.SceneName) .Where(n => !string.IsNullOrEmpty(n)) .Select(n => n!), ]; // Place current program scene first, then alphabetically List orderedSceneNames = !string.IsNullOrEmpty(currentProgramScene) - ? [ - ..sceneNames.Where(n => n == currentProgramScene), - ..sceneNames.Where(n => n != currentProgramScene).OrderBy(n => n), - ] + ? + [ + .. sceneNames.Where(n => n == currentProgramScene), + .. sceneNames.Where(n => n != currentProgramScene).OrderBy(n => n), + ] : [.. sceneNames.OrderBy(n => n)]; if (orderedSceneNames.Count == 0) @@ -2317,10 +2895,11 @@ private async Task AddBrowserSourceAsync(CancellationToken cancellationToken) string selectedScene = displayToSceneName[selectedSceneDisplay]; // Step 3: Fetch scene items and all global browser_source inputs in parallel - Task sceneItemsTask = _obsClient.SceneItems.GetSceneItemListAsync( - new GetSceneItemListRequestData(sceneName: selectedScene), - cancellationToken: cancellationToken - ); + Task sceneItemsTask = + _obsClient.SceneItems.GetSceneItemListAsync( + new GetSceneItemListRequestData(sceneName: selectedScene), + cancellationToken: cancellationToken + ); Task browserInputsTask = _obsClient.Inputs.GetInputListAsync( new GetInputListRequestData("browser_source"), cancellationToken: cancellationToken @@ -2332,17 +2911,21 @@ private async Task AddBrowserSourceAsync(CancellationToken cancellationToken) GetInputListResponseData? browserInputList = await browserInputsTask; // Find browser sources that already exist in the selected scene - HashSet sceneSourceNames = sceneItemList?.SceneItems? - .Select(si => si.SourceName ?? string.Empty) - .Where(n => !string.IsNullOrEmpty(n)) - .ToHashSet(StringComparer.OrdinalIgnoreCase) ?? []; - - List existingBrowserSourcesInScene = browserInputList?.Inputs? - .Where(i => sceneSourceNames.Contains(i.InputName ?? string.Empty)) - .Select(i => i.InputName!) - .Where(n => !string.IsNullOrEmpty(n)) - .OrderBy(n => n) - .ToList() ?? []; + HashSet sceneSourceNames = + sceneItemList + ?.SceneItems?.Select(si => si.SourceName ?? string.Empty) + .Where(n => !string.IsNullOrEmpty(n)) + .ToHashSet(StringComparer.OrdinalIgnoreCase) + ?? []; + + List existingBrowserSourcesInScene = + browserInputList + ?.Inputs?.Where(i => sceneSourceNames.Contains(i.InputName ?? string.Empty)) + .Select(i => i.InputName!) + .Where(n => !string.IsNullOrEmpty(n)) + .OrderBy(n => n) + .ToList() + ?? []; // Step 4: Prompt — create new source or update an existing browser source const string CreateNewChoice = "+ Create new browser source"; @@ -2358,12 +2941,11 @@ private async Task AddBrowserSourceAsync(CancellationToken cancellationToken) bool isNewSource = selectedSourceChoice == CreateNewChoice; string sourceName = isNewSource ? AnsiConsole.Prompt( - new TextPrompt("New browser source [cyan]name[/]:") - .Validate(s => - !string.IsNullOrWhiteSpace(s) - ? ValidationResult.Success() - : ValidationResult.Error("[red]Name cannot be empty.[/]") - ) + new TextPrompt("New browser source [cyan]name[/]:").Validate(s => + !string.IsNullOrWhiteSpace(s) + ? ValidationResult.Success() + : ValidationResult.Error("[red]Name cannot be empty.[/]") + ) ) : selectedSourceChoice; @@ -2384,12 +2966,11 @@ private async Task AddBrowserSourceAsync(CancellationToken cancellationToken) // Step 6: Prompt for the overlay URL string url = AnsiConsole.Prompt( - new TextPrompt("Browser source [cyan]URL[/]:") - .Validate(s => - !string.IsNullOrWhiteSpace(s) - ? ValidationResult.Success() - : ValidationResult.Error("[red]URL cannot be empty.[/]") - ) + new TextPrompt("Browser source [cyan]URL[/]:").Validate(s => + !string.IsNullOrWhiteSpace(s) + ? ValidationResult.Success() + : ValidationResult.Error("[red]URL cannot be empty.[/]") + ) ); // Step 7: Build the browser source settings payload @@ -2415,7 +2996,8 @@ private async Task AddBrowserSourceAsync(CancellationToken cancellationToken) { UiInfo($"Creating browser source '{sourceName}' in scene '{selectedScene}'..."); - CreateInputResponseData? createResult = await _obsClient.Inputs.CreateInputAsync(inputKind: "browser_source", + CreateInputResponseData? createResult = await _obsClient.Inputs.CreateInputAsync( + inputKind: "browser_source", inputName: sourceName, settings: browserSettings, sceneName: selectedScene, @@ -2437,7 +3019,8 @@ private async Task AddBrowserSourceAsync(CancellationToken cancellationToken) UiInfo($"Updating browser source '{sourceName}' settings..."); // overlay: false — reset to defaults then apply all new settings cleanly - await _obsClient.Inputs.SetInputSettingsAsync(inputName: sourceName, + await _obsClient.Inputs.SetInputSettingsAsync( + inputName: sourceName, settings: browserSettings, overlay: false, cancellationToken: cancellationToken @@ -2496,15 +3079,15 @@ private static void RenderCommandHelp() Markup.Escape("version"), Markup.Escape("Get OBS and WebSocket version info") ); - _ = commandTable.AddRow( - Markup.Escape("scene"), - Markup.Escape("Get current program scene") - ); + _ = commandTable.AddRow(Markup.Escape("scene"), Markup.Escape("Get current program scene")); _ = commandTable.AddRow( Markup.Escape("mute [input name]"), Markup.Escape("Toggle mute for audio input") ); - _ = commandTable.AddRow(Markup.Escape("unmute [input name]"), Markup.Escape("Alias for mute")); + _ = commandTable.AddRow( + Markup.Escape("unmute [input name]"), + Markup.Escape("Alias for mute") + ); _ = commandTable.AddRow( Markup.Escape("get-input-settings [scene] [input]"), Markup.Escape("Get settings for an input") @@ -2535,7 +3118,9 @@ private static void RenderCommandHelp() ); _ = commandTable.AddRow( Markup.Escape("run-transport-tests"), - Markup.Escape("Run validation cycle for the configured transport (version, scenes, inputs, filters, custom event, batch, settings modes 1/2/3)") + Markup.Escape( + "Run validation cycle for the configured transport (version, scenes, inputs, filters, custom event, batch, settings modes 1/2/3)" + ) ); _ = commandTable.AddRow( Markup.Escape("list-subs"), @@ -2547,7 +3132,9 @@ private static void RenderCommandHelp() ); _ = commandTable.AddRow( Markup.Escape("get-all-settings-types"), - Markup.Escape("Dump default settings for all filter kinds, input kinds, and current stream service") + Markup.Escape( + "Dump default settings for all filter kinds, input kinds, and current stream service" + ) ); _ = commandTable.AddRow( Markup.Escape("add-browser-source"), @@ -2556,7 +3143,10 @@ private static void RenderCommandHelp() AnsiConsole.Write(commandTable); } - private static void RenderKeyValueTable(string title, IReadOnlyList<(string Key, string Value)> rows) + private static void RenderKeyValueTable( + string title, + IReadOnlyList<(string Key, string Value)> rows + ) { Table table = new() { Title = new TableTitle(title) }; _ = table.AddColumn("Property"); @@ -2713,15 +3303,12 @@ internal sealed record WorkerBrowserUrlSettings( [property: JsonPropertyName("url")] string? Url = null ); -internal sealed record WorkerGainDbSettings( - [property: JsonPropertyName("db")] double? Db = null -); +internal sealed record WorkerGainDbSettings([property: JsonPropertyName("db")] double? Db = null); [JsonSerializable(typeof(WorkerBrowserUrlSettings))] [JsonSerializable(typeof(WorkerGainDbSettings))] [JsonSourceGenerationOptions( PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase, - DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault)] + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault +)] internal sealed partial class WorkerSettingsJsonContext : JsonSerializerContext { } - - diff --git a/ObsWebSocket.Tests/BatchBuilderAndEnumTests.cs b/ObsWebSocket.Tests/BatchBuilderAndEnumTests.cs index 1fee663..f5ead61 100644 --- a/ObsWebSocket.Tests/BatchBuilderAndEnumTests.cs +++ b/ObsWebSocket.Tests/BatchBuilderAndEnumTests.cs @@ -123,7 +123,11 @@ public void OutputState_RoundTripsThroughWireValues() foreach (OutputState value in Enum.GetValues()) { string wire = value.ToWireValue(); - Assert.AreEqual(value, OutputStateExtensions.FromWireValue(wire), $"round trip for {value}"); + Assert.AreEqual( + value, + OutputStateExtensions.FromWireValue(wire), + $"round trip for {value}" + ); } } @@ -133,14 +137,21 @@ public void MediaInputAction_RoundTripsThroughWireValues() foreach (MediaInputAction value in Enum.GetValues()) { string wire = value.ToWireValue(); - Assert.AreEqual(value, MediaInputActionExtensions.FromWireValue(wire), $"round trip for {value}"); + Assert.AreEqual( + value, + MediaInputActionExtensions.FromWireValue(wire), + $"round trip for {value}" + ); } } [TestMethod] public void ToWireValue_MatchesTheProtocolConstants() { - Assert.AreEqual(ObsOutputState.OBS_WEBSOCKET_OUTPUT_STARTED, OutputState.Started.ToWireValue()); + Assert.AreEqual( + ObsOutputState.OBS_WEBSOCKET_OUTPUT_STARTED, + OutputState.Started.ToWireValue() + ); Assert.AreEqual( ObsMediaInputAction.OBS_WEBSOCKET_MEDIA_INPUT_ACTION_PLAY, MediaInputAction.Play.ToWireValue() diff --git a/ObsWebSocket.Tests/BatchResultTests.cs b/ObsWebSocket.Tests/BatchResultTests.cs index 8f39672..f0d7685 100644 --- a/ObsWebSocket.Tests/BatchResultTests.cs +++ b/ObsWebSocket.Tests/BatchResultTests.cs @@ -18,13 +18,12 @@ public sealed class BatchResultTests private static RequestResponsePayload JsonResult(string type, object? payload) { - JsonElement element = - payload is null - ? default - : JsonSerializer.SerializeToElement( - payload, - ObsWebSocketJsonContext.Default.Options.GetTypeInfo(payload.GetType()) - ); + JsonElement element = payload is null + ? default + : JsonSerializer.SerializeToElement( + payload, + ObsWebSocketJsonContext.Default.Options.GetTypeInfo(payload.GetType()) + ); return new RequestResponsePayload(type, $"{type}_0", Ok, element); } @@ -87,10 +86,9 @@ public void GetRequiredData_FailedRequest_ThrowsCarryingStatus() null ); - ObsWebSocketRequestException ex = - Assert.ThrowsExactly( - () => result.GetRequiredData() - ); + ObsWebSocketRequestException ex = Assert.ThrowsExactly(() => + result.GetRequiredData() + ); Assert.AreEqual(600, ex.Status?.Code); Assert.AreEqual("GetSceneItemList", ex.RequestType); @@ -115,7 +113,10 @@ public void AllSucceededAndGetFailures_ReflectStatuses() { List> results = [ - JsonResult("GetVersion", new GetVersionResponseData { ObsVersion = "1", RpcVersion = 1 }), + JsonResult( + "GetVersion", + new GetVersionResponseData { ObsVersion = "1", RpcVersion = 1 } + ), new("GetStats", "GetStats_1", new RequestStatus(false, 604, "nope"), null), ]; diff --git a/ObsWebSocket.Tests/EventStreamTests.cs b/ObsWebSocket.Tests/EventStreamTests.cs index edd2a06..fe9080b 100644 --- a/ObsWebSocket.Tests/EventStreamTests.cs +++ b/ObsWebSocket.Tests/EventStreamTests.cs @@ -59,10 +59,11 @@ public async Task Create_RaisedEvents_AreYieldedInOrder() List seen = []; IAsyncEnumerator enumerator = StreamOf( - source, - capacity: 8, - cts.Token - ).GetAsyncEnumerator(cts.Token); + source, + capacity: 8, + cts.Token + ) + .GetAsyncEnumerator(cts.Token); try { @@ -91,10 +92,11 @@ public async Task Create_WhenEnumerationEnds_Unsubscribes() using CancellationTokenSource cts = new(TimeSpan.FromSeconds(10)); IAsyncEnumerator enumerator = StreamOf( - source, - capacity: 4, - cts.Token - ).GetAsyncEnumerator(cts.Token); + source, + capacity: 4, + cts.Token + ) + .GetAsyncEnumerator(cts.Token); ValueTask pending = enumerator.MoveNextAsync(); source.Raise("One"); @@ -112,10 +114,11 @@ public async Task Create_WhenConsumerFallsBehind_DropsOldest() using CancellationTokenSource cts = new(TimeSpan.FromSeconds(10)); IAsyncEnumerator enumerator = StreamOf( - source, - capacity: 2, - cts.Token - ).GetAsyncEnumerator(cts.Token); + source, + capacity: 2, + cts.Token + ) + .GetAsyncEnumerator(cts.Token); try { @@ -148,10 +151,11 @@ public async Task Create_WhenCancelled_StopsAndUnsubscribes() using CancellationTokenSource cts = new(); IAsyncEnumerator enumerator = StreamOf( - source, - capacity: 4, - cts.Token - ).GetAsyncEnumerator(cts.Token); + source, + capacity: 4, + cts.Token + ) + .GetAsyncEnumerator(cts.Token); ValueTask pending = enumerator.MoveNextAsync(); await cts.CancelAsync(); @@ -165,10 +169,6 @@ public async Task Create_WhenCancelled_StopsAndUnsubscribes() [TestMethod] public void Create_WithInvalidCapacity_Throws() => Assert.ThrowsExactly(() => - EventStream.Create( - _ => { }, - _ => { }, - capacity: 0 - ) + EventStream.Create(_ => { }, _ => { }, capacity: 0) ); } diff --git a/ObsWebSocket.Tests/FalsyRequestFieldTests.cs b/ObsWebSocket.Tests/FalsyRequestFieldTests.cs index a26e9f0..588fd91 100644 --- a/ObsWebSocket.Tests/FalsyRequestFieldTests.cs +++ b/ObsWebSocket.Tests/FalsyRequestFieldTests.cs @@ -18,10 +18,12 @@ namespace ObsWebSocket.Tests; public sealed class FalsyRequestFieldTests { private static string Json(object data) => - JsonSerializer.SerializeToElement( - data, - ObsWebSocketJsonContext.Default.Options.GetTypeInfo(data.GetType()) - ).GetRawText(); + JsonSerializer + .SerializeToElement( + data, + ObsWebSocketJsonContext.Default.Options.GetTypeInfo(data.GetType()) + ) + .GetRawText(); [TestMethod] public void Serialize_RequiredFalseBool_KeepsField() diff --git a/ObsWebSocket.Tests/HostingTests.cs b/ObsWebSocket.Tests/HostingTests.cs index 02a67f4..db4b1a2 100644 --- a/ObsWebSocket.Tests/HostingTests.cs +++ b/ObsWebSocket.Tests/HostingTests.cs @@ -63,8 +63,8 @@ public void AddObsWebSocketClient_MissingConnectionString_ExplainsWhat() { HostApplicationBuilder builder = Host.CreateApplicationBuilder(); - InvalidOperationException ex = Assert.ThrowsExactly( - () => builder.AddObsWebSocketClient("obs") + InvalidOperationException ex = Assert.ThrowsExactly(() => + builder.AddObsWebSocketClient("obs") ); StringAssert.Contains(ex.Message, "ConnectionStrings:obs"); @@ -81,7 +81,9 @@ public async Task HealthCheck_WhenNotConnected_ReportsUnhealthy() await using ServiceProvider provider = services.BuildServiceProvider(); HealthCheckService checks = provider.GetRequiredService(); - HealthReport report = await checks.CheckHealthAsync(TestContext.CancellationTokenSource.Token); + HealthReport report = await checks.CheckHealthAsync( + TestContext.CancellationTokenSource.Token + ); Assert.AreEqual(HealthStatus.Unhealthy, report.Status); Assert.IsTrue(report.Entries.ContainsKey("obs-websocket")); diff --git a/ObsWebSocket.Tests/JsonMessageSerializerTests.cs b/ObsWebSocket.Tests/JsonMessageSerializerTests.cs index 1f50c8b..a60da96 100644 --- a/ObsWebSocket.Tests/JsonMessageSerializerTests.cs +++ b/ObsWebSocket.Tests/JsonMessageSerializerTests.cs @@ -32,9 +32,8 @@ public void DeserializePayload_EventPayloadBaseObject_UsesContextBackedBridge() ) .RootElement.Clone(); - EventPayloadBase? result = CreateSerializer().DeserializePayload< - EventPayloadBase - >(payload); + EventPayloadBase? result = CreateSerializer() + .DeserializePayload>(payload); Assert.IsNotNull(result); Assert.AreEqual("SceneListChanged", result.EventType); @@ -60,9 +59,8 @@ public void DeserializePayload_RequestResponsePayloadObject_UsesContextBackedBri ) .RootElement.Clone(); - RequestResponsePayload? result = CreateSerializer().DeserializePayload< - RequestResponsePayload - >(payload); + RequestResponsePayload? result = CreateSerializer() + .DeserializePayload>(payload); Assert.IsNotNull(result); Assert.AreEqual("GetVersion", result.RequestType); @@ -87,9 +85,8 @@ public void DeserializePayload_SceneListChangedPayload_DeserializesSceneStubs() ) .RootElement.Clone(); - SceneListChangedPayload? result = CreateSerializer().DeserializePayload( - payload - ); + SceneListChangedPayload? result = CreateSerializer() + .DeserializePayload(payload); Assert.IsNotNull(result); Assert.IsNotNull(result.Scenes); @@ -122,9 +119,8 @@ public void DeserializePayload_RequestBatchResponsePayloadObject_UsesContextBack ) .RootElement.Clone(); - RequestBatchResponsePayload? result = CreateSerializer().DeserializePayload< - RequestBatchResponsePayload - >(payload); + RequestBatchResponsePayload? result = CreateSerializer() + .DeserializePayload>(payload); Assert.IsNotNull(result); Assert.AreEqual("batch-1", result.RequestId); @@ -156,8 +152,7 @@ public async Task SerializeAsync_OutgoingMessage_UsesExpectedEnvelopeShape() [TestMethod] public async Task DeserializeAsync_ValidIncomingEnvelope_ReturnsIncomingMessage() { - const string incomingJson = - """ + const string incomingJson = """ { "op": 5, "d": { @@ -187,9 +182,8 @@ public void DeserializeValuePayload_WebSocketOpCode_DeserializesEnumValueType() { JsonElement payload = JsonDocument.Parse("5").RootElement.Clone(); - WebSocketOpCode? result = CreateSerializer().DeserializeValuePayload( - payload - ); + WebSocketOpCode? result = CreateSerializer() + .DeserializeValuePayload(payload); Assert.IsTrue(result.HasValue); Assert.AreEqual(WebSocketOpCode.Event, result.Value); @@ -210,9 +204,8 @@ public void DeserializePayload_RequestStatus_DeserializesFields() ) .RootElement.Clone(); - ObsWebSocket.Core.Protocol.RequestStatus? result = CreateSerializer().DeserializePayload< - ObsWebSocket.Core.Protocol.RequestStatus - >(payload); + ObsWebSocket.Core.Protocol.RequestStatus? result = CreateSerializer() + .DeserializePayload(payload); Assert.IsNotNull(result); Assert.IsTrue(result.Result); diff --git a/ObsWebSocket.Tests/ObsWebSocket.Tests.csproj b/ObsWebSocket.Tests/ObsWebSocket.Tests.csproj index 5fffe1b..c983f93 100644 --- a/ObsWebSocket.Tests/ObsWebSocket.Tests.csproj +++ b/ObsWebSocket.Tests/ObsWebSocket.Tests.csproj @@ -20,12 +20,17 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - - - + + diff --git a/ObsWebSocket.Tests/ObsWebSocketClientConnectionTests.cs b/ObsWebSocket.Tests/ObsWebSocketClientConnectionTests.cs index b445c34..76d14a5 100644 --- a/ObsWebSocket.Tests/ObsWebSocketClientConnectionTests.cs +++ b/ObsWebSocket.Tests/ObsWebSocketClientConnectionTests.cs @@ -213,7 +213,8 @@ public async Task ConnectAsync_SuccessfulFirstAttempt_RaisesCorrectEvents() // Mock WebSocket Connection _ = mockConnection .Setup(c => c.ConnectAsync(s_testServerUri, It.IsAny())) - .Callback(() => mockConnection.SetupGet(conn => conn.State).Returns(WebSocketState.Open) + .Callback(() => + mockConnection.SetupGet(conn => conn.State).Returns(WebSocketState.Open) ) .Returns(Task.CompletedTask); @@ -665,7 +666,9 @@ public async Task ConnectAsync_FailAllRetries_RaisesDisconnectedWithLastError() _ = mockConnFailing.SetupGet(c => c.State).Returns(WebSocketState.None); _ = mockConnFailing.SetupGet(c => c.Options).Returns(new ClientWebSocket().Options); _ = mockConnFailing.SetupGet(c => c.SubProtocol).Returns("obswebsocket.json"); - _ = mockConnFailing.SetupGet(c => c.CloseStatus).Returns((WebSocketCloseStatus?)null); + _ = mockConnFailing + .SetupGet(c => c.CloseStatus) + .Returns((WebSocketCloseStatus?)null); _ = mockConnFailing.SetupGet(c => c.CloseStatusDescription).Returns((string?)null); _ = mockConnFailing .Setup(c => c.ConnectAsync(s_testServerUri, It.IsAny())) @@ -677,14 +680,16 @@ public async Task ConnectAsync_FailAllRetries_RaisesDisconnectedWithLastError() // Act & Assert ObsWebSocketException thrownException = - await Assert.ThrowsExactlyAsync( - () => PumpAsync(client.ConnectAsync(), time) + await Assert.ThrowsExactlyAsync(() => + PumpAsync(client.ConnectAsync(), time) ); Assert.IsTrue( thrownException.Message.Contains($"Failed to connect after {maxAttempts} attempts") ); - _ = Assert.IsInstanceOfType(thrownException.InnerException); + _ = Assert.IsInstanceOfType( + thrownException.InnerException + ); Assert.AreEqual(connectException, thrownException.InnerException!.InnerException); _ = await Task.WhenAny(disconnectedSignal.Task, Task.Delay(1000)) @@ -756,7 +761,9 @@ public async Task DisconnectAsync_DuringRetry_StopsRetriesAndDisconnectsGraceful _ = mockConnFailing.SetupGet(c => c.State).Returns(WebSocketState.None); _ = mockConnFailing.SetupGet(c => c.Options).Returns(new ClientWebSocket().Options); _ = mockConnFailing.SetupGet(c => c.SubProtocol).Returns("obswebsocket.json"); - _ = mockConnFailing.SetupGet(c => c.CloseStatus).Returns((WebSocketCloseStatus?)null); + _ = mockConnFailing + .SetupGet(c => c.CloseStatus) + .Returns((WebSocketCloseStatus?)null); _ = mockConnFailing.SetupGet(c => c.CloseStatusDescription).Returns((string?)null); _ = mockConnFailing .Setup(c => c.ConnectAsync(s_testServerUri, It.IsAny())) @@ -839,7 +846,9 @@ public async Task ConnectAsync_InfiniteRetries_AttemptsMultipleTimes() _ = mockConnFailing.SetupGet(c => c.State).Returns(WebSocketState.None); _ = mockConnFailing.SetupGet(c => c.Options).Returns(new ClientWebSocket().Options); _ = mockConnFailing.SetupGet(c => c.SubProtocol).Returns("obswebsocket.json"); - _ = mockConnFailing.SetupGet(c => c.CloseStatus).Returns((WebSocketCloseStatus?)null); + _ = mockConnFailing + .SetupGet(c => c.CloseStatus) + .Returns((WebSocketCloseStatus?)null); _ = mockConnFailing.SetupGet(c => c.CloseStatusDescription).Returns((string?)null); _ = mockConnFailing .Setup(c => c.ConnectAsync(s_testServerUri, It.IsAny())) diff --git a/ObsWebSocket.Tests/ObsWebSocketClientIntegrationTests.cs b/ObsWebSocket.Tests/ObsWebSocketClientIntegrationTests.cs index 1276b32..3330a37 100644 --- a/ObsWebSocket.Tests/ObsWebSocketClientIntegrationTests.cs +++ b/ObsWebSocket.Tests/ObsWebSocketClientIntegrationTests.cs @@ -49,7 +49,9 @@ public static void ClassInitialize(TestContext context) _ = services.AddLogging(builder => builder.AddConsole().SetMinimumLevel(minLogLevel)); // Bind the "ObsIntegration" section from configuration to the options class - _ = services.Configure(configuration.GetSection("ObsIntegration")); + _ = services.Configure( + configuration.GetSection("ObsIntegration") + ); // Configure ObsWebSocketClientOptions based on ObsIntegrationTestOptions _ = services @@ -112,15 +114,13 @@ private static ObsWebSocketClient CreateClient() ILogger logger = s_serviceProvider.GetRequiredService< ILogger >(); - IWebSocketMessageSerializer serializer = s_serviceProvider.GetRequiredService< - IWebSocketMessageSerializer - >(); + IWebSocketMessageSerializer serializer = + s_serviceProvider.GetRequiredService(); IOptions options = s_serviceProvider.GetRequiredService< IOptions >(); - IWebSocketConnectionFactory connectionFactory = s_serviceProvider.GetRequiredService< - IWebSocketConnectionFactory - >(); + IWebSocketConnectionFactory connectionFactory = + s_serviceProvider.GetRequiredService(); return new ObsWebSocketClient(logger, serializer, options, connectionFactory); } @@ -450,7 +450,10 @@ public async Task GetSceneItemTransform_ReturnsTransformStub() // Get the transform GetSceneItemTransformResponseData? transformResponse = await client.SceneItems.GetSceneItemTransformAsync( - new GetSceneItemTransformRequestData(sceneItemId, sceneName: s_testOptions.TestSceneName!) + new GetSceneItemTransformRequestData( + sceneItemId, + sceneName: s_testOptions.TestSceneName! + ) ); Assert.IsNotNull(transformResponse, "GetSceneItemTransform response was null."); @@ -526,7 +529,8 @@ public async Task GetInputSettings_TextGDI_ReturnsJsonElement() GetInputSettingsResponseData? response; try { - response = await client.Inputs.GetInputSettingsAsync(new GetInputSettingsRequestData(s_testOptions.TestInputName!) + response = await client.Inputs.GetInputSettingsAsync( + new GetInputSettingsRequestData(s_testOptions.TestInputName!) ); } catch (ObsWebSocketException ex) when (ex.Message.Contains("ResourceNotFound")) @@ -636,7 +640,8 @@ public async Task GetTransitionList_ReturnsTransitionStubs() await client.ConnectAsync(TestContext.CancellationToken); Assert.IsTrue(client.IsConnected); - GetSceneTransitionListResponseData? response = await client.Transitions.GetSceneTransitionListAsync(); + GetSceneTransitionListResponseData? response = + await client.Transitions.GetSceneTransitionListAsync(); Assert.IsNotNull(response, "GetSceneTransitionList response was null."); Assert.IsNotNull(response.Transitions, "Transitions list was null."); @@ -680,7 +685,10 @@ public async Task GetOutputList_ReturnsOutputStubs() Assert.IsNotNull(response, "GetOutputList response was null."); Assert.IsNotNull(response.Outputs, "Outputs list was null."); - Assert.IsNotEmpty(response.Outputs, "Expected at least one output (e.g., Simple Output or Advanced)."); + Assert.IsNotEmpty( + response.Outputs, + "Expected at least one output (e.g., Simple Output or Advanced)." + ); OutputStub? firstOutput = response.Outputs.FirstOrDefault(); Assert.IsNotNull(firstOutput, "First output stub was null."); @@ -812,7 +820,8 @@ public async Task GetInputDefaultSettings_ReturnsJsonElement_CanDeserialize() GetInputDefaultSettingsResponseData? response; try { - response = await client.Inputs.GetInputDefaultSettingsAsync(new GetInputDefaultSettingsRequestData(inputKind) + response = await client.Inputs.GetInputDefaultSettingsAsync( + new GetInputDefaultSettingsRequestData(inputKind) ); } catch (ObsWebSocketException ex) when (ex.Message.Contains("InvalidInputKind")) @@ -851,10 +860,7 @@ public async Task GetInputDefaultSettings_ReturnsJsonElement_CanDeserialize() ); // Assert some known default properties for text_gdiplus_v3 exist - Assert.IsTrue( - defaultSettingsDict.ContainsKey("font"), - "Expected 'font' default setting." - ); + Assert.IsTrue(defaultSettingsDict.ContainsKey("font"), "Expected 'font' default setting."); Assert.AreEqual( JsonValueKind.Object, defaultSettingsDict["font"].ValueKind, diff --git a/ObsWebSocket.Tests/ObsWebSocketClientRequestTests.cs b/ObsWebSocket.Tests/ObsWebSocketClientRequestTests.cs index c3b113d..faf4103 100644 --- a/ObsWebSocket.Tests/ObsWebSocketClientRequestTests.cs +++ b/ObsWebSocket.Tests/ObsWebSocketClientRequestTests.cs @@ -332,7 +332,9 @@ CancellationToken ct .Returns(responseDto); // Act - GetInputMuteResponseData? actualResponseDto = await client.Inputs.GetInputMuteAsync(requestDto); + GetInputMuteResponseData? actualResponseDto = await client.Inputs.GetInputMuteAsync( + requestDto + ); // Assert Assert.IsNotNull(actualResponseDto, "Response DTO should not be null."); @@ -442,8 +444,8 @@ CancellationToken ct // Act & Assert // Verify that calling the client method throws the correct exception - ObsWebSocketException ex = await Assert.ThrowsAsync( - async () => await client.General.GetVersionAsync() // Call the specific extension method + ObsWebSocketException ex = await Assert.ThrowsAsync(async () => + await client.General.GetVersionAsync() // Call the specific extension method ); // Check the exception details @@ -626,4 +628,3 @@ TRequestData expectedData } // Ignore errors } } - diff --git a/ObsWebSocket.Tests/ObsWebSocketClientTests.cs b/ObsWebSocket.Tests/ObsWebSocketClientTests.cs index 4a5c93c..b7a90a0 100644 --- a/ObsWebSocket.Tests/ObsWebSocketClientTests.cs +++ b/ObsWebSocket.Tests/ObsWebSocketClientTests.cs @@ -86,11 +86,8 @@ await client.CallBatchAsync(requests) public async Task CallBatchAsync_AnonymousRequestData_ThrowsObsWebSocketException() { // Arrange - ( - ObsWebSocketClient client, - _, - Mock mockWebSocket - ) = TestUtils.SetupConnectedClientForceState(); + (ObsWebSocketClient client, _, Mock mockWebSocket) = + TestUtils.SetupConnectedClientForceState(); List requests = [ @@ -107,10 +104,11 @@ await client.CallBatchAsync(requests) "Failed to serialize request data", StringComparison.Ordinal ); - bool innerHasExpectedMessage = ex.InnerException?.Message.Contains( - "Failed to serialize request data", - StringComparison.Ordinal - ) == true; + bool innerHasExpectedMessage = + ex.InnerException?.Message.Contains( + "Failed to serialize request data", + StringComparison.Ordinal + ) == true; Assert.IsTrue( outerHasExpectedMessage || innerHasExpectedMessage, "Exception chain should indicate request data serialization failure." @@ -367,8 +365,8 @@ CancellationToken ct .Returns(ValueTask.CompletedTask); // Act & Assert - ObsWebSocketException ex = await Assert.ThrowsAsync( - async () => await client.CallBatchAsync(requests, timeoutMs: timeoutMs) // Use the timeout override + ObsWebSocketException ex = await Assert.ThrowsAsync(async () => + await client.CallBatchAsync(requests, timeoutMs: timeoutMs) // Use the timeout override ); // Verify exception details @@ -427,4 +425,3 @@ private static bool IsRequestType(ReadOnlyMemory buffer, WebSocketOpCode o } // Ignore deserialization errors } } - diff --git a/ObsWebSocket.Tests/ObsWebSocketDiTests.cs b/ObsWebSocket.Tests/ObsWebSocketDiTests.cs index 9061bcb..1acd0e0 100644 --- a/ObsWebSocket.Tests/ObsWebSocketDiTests.cs +++ b/ObsWebSocket.Tests/ObsWebSocketDiTests.cs @@ -286,4 +286,3 @@ public void Resolve_WithoutServerUri_FailsValidation() ); } } - diff --git a/ObsWebSocket.Tests/ReadmeCompileCheck.cs b/ObsWebSocket.Tests/ReadmeCompileCheck.cs index 5946190..5b12214 100644 --- a/ObsWebSocket.Tests/ReadmeCompileCheck.cs +++ b/ObsWebSocket.Tests/ReadmeCompileCheck.cs @@ -2,8 +2,8 @@ using Microsoft.Extensions.DependencyInjection; using ObsWebSocket.Core; using ObsWebSocket.Core.Events.Generated; -using ObsWebSocket.Core.Protocol.Common.InputSettings; using ObsWebSocket.Core.Protocol; +using ObsWebSocket.Core.Protocol.Common.InputSettings; using ObsWebSocket.Core.Protocol.Generated; using ObsWebSocket.Core.Protocol.Requests; using ObsWebSocket.Core.Protocol.Responses; @@ -41,15 +41,24 @@ internal static async Task ReplayBufferAsync(ObsWebSocketClient client, Cancella internal static async Task BrowserSourceAsync(ObsWebSocketClient client, CancellationToken ct) { - var current = await client.Inputs.GetInputSettingsAsync("StreamOverlay", ct); + var current = await client.Inputs.GetInputSettingsAsync( + "StreamOverlay", + ct + ); _ = current?.Url; - await client.Inputs.SetInputSettingsAsync("StreamOverlay", - new BrowserSourceSettings(Url: "https://myoverlay.example.com", Width: 1920, Height: 1080), + await client.Inputs.SetInputSettingsAsync( + "StreamOverlay", + new BrowserSourceSettings( + Url: "https://myoverlay.example.com", + Width: 1920, + Height: 1080 + ), cancellationToken: ct ); - await client.Inputs.SetInputSettingsAsync("StreamOverlay", + await client.Inputs.SetInputSettingsAsync( + "StreamOverlay", new OverlaySettings(Url: "https://myoverlay.example.com"), MyContext.Default.OverlaySettings, cancellationToken: ct @@ -60,7 +69,8 @@ internal static async Task UtilitiesAsync(ObsWebSocketClient client, Cancellatio { await client.Scenes.SwitchSceneAndWaitAsync("Scene", cancellationToken: ct); _ = await client.Sources.SourceExistsAsync("Source", ct); - await client.Filters.CreateSourceFilterAsync("Source", + await client.Filters.CreateSourceFilterAsync( + "Source", "MyFilter", "gain_filter", new OverlaySettings(), @@ -76,7 +86,9 @@ await client.Filters.CreateSourceFilterAsync("Source", internal static async Task VersionAsync(ObsWebSocketClient client, CancellationToken ct) { - GetVersionResponseData? version = await client.General.GetVersionAsync(cancellationToken: ct); + GetVersionResponseData? version = await client.General.GetVersionAsync( + cancellationToken: ct + ); _ = version?.ObsVersion; } @@ -85,9 +97,15 @@ internal static async Task BatchAsync(ObsWebSocketClient client, CancellationTok List items = [ new("GetVersion", null), - new("SetCurrentProgramScene", new SetCurrentProgramSceneRequestData(sceneName: "Intro")), + new( + "SetCurrentProgramScene", + new SetCurrentProgramSceneRequestData(sceneName: "Intro") + ), new("Sleep", new SleepRequestData(sleepMillis: 100)), - new("SetInputMute", new SetInputMuteRequestData { InputName = "Mic", InputMuted = false }), + new( + "SetInputMute", + new SetInputMuteRequestData { InputName = "Mic", InputMuted = false } + ), ]; var results = await client.CallBatchAsync( @@ -103,21 +121,32 @@ internal static async Task BatchAsync(ObsWebSocketClient client, CancellationTok } } - internal static async Task UtilitiesExtendedAsync(ObsWebSocketClient client, CancellationToken ct) + internal static async Task UtilitiesExtendedAsync( + ObsWebSocketClient client, + CancellationToken ct + ) { await client.Scenes.SwitchSceneAsync("Scene", cancellationToken: ct); _ = await client.SceneItems.SetSceneItemEnabledAsync("Scene", "Source", null, ct); _ = await client.SceneItems.FindSceneItemIdAsync("Scene", "Source", ct); await client.Inputs.SetInputMutesAsync([("Mic", false), ("Desktop Audio", true)], ct); _ = await client.Sources.GetSourceScreenshotBytesAsync("Source", cancellationToken: ct); - _ = await client.Sources.GetSourceScreenshotOnCanvasBytesAsync("Source", cancellationToken: ct); - await client.Sources.SaveSourceScreenshotToFileAsync("Source", "shot.png", cancellationToken: ct); + _ = await client.Sources.GetSourceScreenshotOnCanvasBytesAsync( + "Source", + cancellationToken: ct + ); + await client.Sources.SaveSourceScreenshotToFileAsync( + "Source", + "shot.png", + cancellationToken: ct + ); _ = await client.Config.EnsureProfileActiveAsync("Profile", ct); _ = await client.Config.EnsureSceneCollectionActiveAsync("Collection", ct); _ = await client.Outputs.IsVirtualCamActiveAsync(ct); _ = await client.Outputs.SetVirtualCamActiveAndWaitAsync(true, cancellationToken: ct); await client.General.TriggerHotkeyAsync("OBSBasic.StartRecording", ct); - _ = await client.Inputs.CreateInputAsync("browser_source", + _ = await client.Inputs.CreateInputAsync( + "browser_source", "NewOverlay", new OverlaySettings(), MyContext.Default.OverlaySettings, @@ -127,7 +156,9 @@ internal static async Task UtilitiesExtendedAsync(ObsWebSocketClient client, Can internal static async Task EventStreamsAsync(ObsWebSocketClient client, CancellationToken ct) { - await foreach (var e in client.Scenes.CurrentProgramSceneChangedStream(cancellationToken: ct)) + await foreach ( + var e in client.Scenes.CurrentProgramSceneChangedStream(cancellationToken: ct) + ) { _ = e.EventData.SceneName; break; @@ -208,12 +239,19 @@ internal static async Task NewHelpersAsync(ObsWebSocketClient client, Cancellati await client.Inputs.SetInputVolumeMulAsync("Mic", 0.5, ct); } - internal static async Task TypedBatchResultsAsync(ObsWebSocketClient client, CancellationToken ct) + internal static async Task TypedBatchResultsAsync( + ObsWebSocketClient client, + CancellationToken ct + ) { ObsBatchBuilder batch = new(); - BatchRef intro = batch.SceneItems.GetSceneItemList(new(sceneName: "Intro")); + BatchRef intro = batch.SceneItems.GetSceneItemList( + new(sceneName: "Intro") + ); BatchRef version = batch.General.GetVersion(); - BatchRef outro = batch.SceneItems.GetSceneItemList(new(sceneName: "Outro")); + BatchRef outro = batch.SceneItems.GetSceneItemList( + new(sceneName: "Outro") + ); BatchResults results = await client.CallBatchAsync(batch, cancellationToken: ct); @@ -238,7 +276,9 @@ internal static async Task TypedErrorsAsync(ObsWebSocketClient client, Cancellat } } - internal static void HostIntegration(Microsoft.Extensions.Hosting.IHostApplicationBuilder builder) + internal static void HostIntegration( + Microsoft.Extensions.Hosting.IHostApplicationBuilder builder + ) { _ = builder.AddObsWebSocketClient("obs"); _ = builder.Services.WithAutoConnect(); @@ -247,7 +287,10 @@ internal static void HostIntegration(Microsoft.Extensions.Hosting.IHostApplicati internal static void TelemetryAndKeyedRegistration(IServiceCollection services) { - _ = services.AddObsWebSocketClient("main", o => o.ServerUri = new Uri("ws://localhost:4455")); + _ = services.AddObsWebSocketClient( + "main", + o => o.ServerUri = new Uri("ws://localhost:4455") + ); _ = services.AddObsWebSocketClient("booth", o => o.ServerUri = new Uri("ws://booth:4455")); _ = ObsWebSocketDiagnostics.ActivitySourceName; _ = ObsWebSocketDiagnostics.MeterName; @@ -256,9 +299,17 @@ internal static void TelemetryAndKeyedRegistration(IServiceCollection services) internal static async Task ScreenshotsAsync(ObsWebSocketClient client, CancellationToken ct) { - byte[]? png = await client.Sources.GetSourceScreenshotBytesAsync("Intro", "png", cancellationToken: ct); + byte[]? png = await client.Sources.GetSourceScreenshotBytesAsync( + "Intro", + "png", + cancellationToken: ct + ); _ = png; - await client.Sources.SaveSourceScreenshotToFileAsync("Intro", "shot.png", cancellationToken: ct); + await client.Sources.SaveSourceScreenshotToFileAsync( + "Intro", + "shot.png", + cancellationToken: ct + ); } internal static async Task ReplayBufferAsync2(ObsWebSocketClient client, CancellationToken ct) @@ -280,8 +331,6 @@ internal static async Task StudioModeAsync(ObsWebSocketClient client, Cancellati { _ = $"{ex.RequestType} failed with {ex.Status?.Code}: {ex.Comment}"; } - catch (ObsWebSocketTimeoutException) - { - } + catch (ObsWebSocketTimeoutException) { } } } diff --git a/ObsWebSocket.Tests/ScreenshotDecodeTests.cs b/ObsWebSocket.Tests/ScreenshotDecodeTests.cs index 7e35e37..bac6aec 100644 --- a/ObsWebSocket.Tests/ScreenshotDecodeTests.cs +++ b/ObsWebSocket.Tests/ScreenshotDecodeTests.cs @@ -10,8 +10,7 @@ namespace ObsWebSocket.Tests; public sealed class ScreenshotDecodeTests { // The eight byte PNG signature, which is what a caller checks to know it got an image. - private static readonly byte[] PngSignature = - [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]; + private static readonly byte[] PngSignature = [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]; [TestMethod] public void DecodeImageData_WithDataUriPrefix_ReturnsTheImageBytes() @@ -26,9 +25,7 @@ public void DecodeImageData_WithDataUriPrefix_ReturnsTheImageBytes() [TestMethod] public void DecodeImageData_WithBareBase64_StillDecodes() { - byte[] decoded = SourcesGroup.DecodeImageData( - Convert.ToBase64String(PngSignature) - ); + byte[] decoded = SourcesGroup.DecodeImageData(Convert.ToBase64String(PngSignature)); CollectionAssert.AreEqual(PngSignature, decoded); } diff --git a/ObsWebSocket.Tests/SerializerBehaviorTests.cs b/ObsWebSocket.Tests/SerializerBehaviorTests.cs index 3ae9432..10a67f4 100644 --- a/ObsWebSocket.Tests/SerializerBehaviorTests.cs +++ b/ObsWebSocket.Tests/SerializerBehaviorTests.cs @@ -50,9 +50,8 @@ public void JsonSerializer_DeserializePayload_SceneStubExtensionData_IsAvailable ) .RootElement.Clone(); - SceneListChangedPayload? result = CreateJsonSerializer().DeserializePayload( - payload - ); + SceneListChangedPayload? result = CreateJsonSerializer() + .DeserializePayload(payload); Assert.IsNotNull(result); Assert.IsNotNull(result.Scenes); @@ -102,10 +101,7 @@ public void MsgPackSerializer_DeserializePayload_WithComplexSceneStubBytes_Deser Assert.AreEqual("scene-uuid-1", scenes[0].SceneUuid); Dictionary? extensionData = scenes[0].ExtensionData; Assert.IsNotNull(extensionData); - Assert.AreEqual( - "extension-data-like-field", - extensionData["extraTag"].GetString() - ); + Assert.AreEqual("extension-data-like-field", extensionData["extraTag"].GetString()); } [TestMethod] @@ -129,22 +125,11 @@ public void MsgPackSerializer_DeserializePayload_WithComplexFilterBytes_Deserial Assert.IsTrue(filters[0].FilterEnabled ?? false); Assert.IsTrue(filters[0].FilterSettings.HasValue); JsonElement filterSettings = filters[0].FilterSettings.GetValueOrDefault(); - Assert.AreEqual( - 0.8d, - filterSettings.GetProperty("opacity").GetDouble(), - 0.0001d - ); - Assert.AreEqual( - 1.2d, - filterSettings.GetProperty("gamma").GetDouble(), - 0.0001d - ); + Assert.AreEqual(0.8d, filterSettings.GetProperty("opacity").GetDouble(), 0.0001d); + Assert.AreEqual(1.2d, filterSettings.GetProperty("gamma").GetDouble(), 0.0001d); Dictionary? extensionData = filters[0].ExtensionData; Assert.IsNotNull(extensionData); - Assert.AreEqual( - "present", - extensionData["customExtensionField"].GetString() - ); + Assert.AreEqual("present", extensionData["customExtensionField"].GetString()); JsonElement listExtension = extensionData["listExtension"]; Assert.AreEqual(JsonValueKind.Array, listExtension.ValueKind); Assert.AreEqual(2, listExtension.GetArrayLength()); @@ -251,7 +236,9 @@ public void JsonSerializer_DeserializePayload_InvalidGeneratedShape_ReturnsDefau ) .RootElement.Clone(); - CreateSceneRequestData? data = serializer.DeserializePayload(payload); + CreateSceneRequestData? data = serializer.DeserializePayload( + payload + ); Assert.IsNull(data); } @@ -275,7 +262,8 @@ public async Task MsgPackSerializer_DeserializeAsync_IncomingMapPayload_Captures object? envelope = await serializer.DeserializeAsync(stream); _ = Assert.IsInstanceOfType>>(envelope); - IncomingMessage> incoming = (IncomingMessage>)envelope; + IncomingMessage> incoming = + (IncomingMessage>)envelope; Assert.AreEqual(WebSocketOpCode.Hello, incoming.Op); Assert.IsTrue(incoming.D.Length > 0); @@ -387,7 +375,9 @@ public void MsgPackSerializer_DeserializePayload_SceneItemList_WithTransformAndE byte[] bytes = BuildSceneItemListPayloadBytes(); GetSceneItemListResponseData? payload = - serializer.DeserializePayload(new ReadOnlyMemory(bytes)); + serializer.DeserializePayload( + new ReadOnlyMemory(bytes) + ); Assert.IsNotNull(payload); List? sceneItems = payload.SceneItems; @@ -411,7 +401,9 @@ public void MsgPackSerializer_DeserializePayload_InvalidGeneratedShape_ReturnsDe byte[] bytes = BuildInvalidFilterListPayloadBytes(); GetSourceFilterListResponseData? payload = - serializer.DeserializePayload(new ReadOnlyMemory(bytes)); + serializer.DeserializePayload( + new ReadOnlyMemory(bytes) + ); Assert.IsNull(payload); } @@ -420,29 +412,28 @@ public void MsgPackSerializer_DeserializePayload_InvalidGeneratedShape_ReturnsDe public void MsgPackSerializer_SerializeThenDeserialize_FilterPayload_RoundTripsWithValues() { MsgPackMessageSerializer serializer = CreateMsgPackSerializer(); - GetSourceFilterListResponseData payload = new( - [ - new Core.Protocol.Common.FilterStub - { - FilterName = "Color Correction", - FilterKind = "color_filter_v2", - FilterIndex = 0, - FilterEnabled = true, - }, - new Core.Protocol.Common.FilterStub - { - FilterName = "Limiter", - FilterKind = "limiter_filter_v2", - FilterIndex = 1, - FilterEnabled = false, - }, - ] - ); + GetSourceFilterListResponseData payload = new([ + new Core.Protocol.Common.FilterStub + { + FilterName = "Color Correction", + FilterKind = "color_filter_v2", + FilterIndex = 0, + FilterEnabled = true, + }, + new Core.Protocol.Common.FilterStub + { + FilterName = "Limiter", + FilterKind = "limiter_filter_v2", + FilterIndex = 1, + FilterEnabled = false, + }, + ]); byte[] bytes = MessagePack.MessagePackSerializer.Serialize(payload); - GetSourceFilterListResponseData? roundTrip = serializer.DeserializePayload< - GetSourceFilterListResponseData - >(new ReadOnlyMemory(bytes)); + GetSourceFilterListResponseData? roundTrip = + serializer.DeserializePayload( + new ReadOnlyMemory(bytes) + ); Assert.IsNotNull(roundTrip); Assert.IsNotNull(roundTrip.Filters); @@ -498,27 +489,13 @@ public void MsgPackResolver_CoversGeneratedAndCoreProtocolTypes() ) ); - requiredTypes.Add( - typeof(HelloPayload) - ); - requiredTypes.Add( - typeof(AuthenticationData) - ); - requiredTypes.Add( - typeof(IdentifyPayload) - ); - requiredTypes.Add( - typeof(IdentifiedPayload) - ); - requiredTypes.Add( - typeof(ReidentifyPayload) - ); - requiredTypes.Add( - typeof(RequestPayload) - ); - requiredTypes.Add( - typeof(RequestBatchPayload) - ); + requiredTypes.Add(typeof(HelloPayload)); + requiredTypes.Add(typeof(AuthenticationData)); + requiredTypes.Add(typeof(IdentifyPayload)); + requiredTypes.Add(typeof(IdentifiedPayload)); + requiredTypes.Add(typeof(ReidentifyPayload)); + requiredTypes.Add(typeof(RequestPayload)); + requiredTypes.Add(typeof(RequestBatchPayload)); requiredTypes.Add(typeof(ObsWebSocket.Core.Protocol.RequestStatus)); List missing = []; @@ -729,7 +706,9 @@ private sealed class UnreadableMemoryStream(byte[] buffer) : MemoryStream(buffer private static bool HasFormatter(IFormatterResolver resolver, Type targetType) { - MethodInfo? method = typeof(IFormatterResolver).GetMethod(nameof(IFormatterResolver.GetFormatter)); + MethodInfo? method = typeof(IFormatterResolver).GetMethod( + nameof(IFormatterResolver.GetFormatter) + ); Assert.IsNotNull(method, "Could not locate IFormatterResolver.GetFormatter."); object? formatter = method.MakeGenericMethod(targetType).Invoke(resolver, null); return formatter is not null; diff --git a/ObsWebSocket.Tests/TestUtils.cs b/ObsWebSocket.Tests/TestUtils.cs index c0cc32c..9e772cc 100644 --- a/ObsWebSocket.Tests/TestUtils.cs +++ b/ObsWebSocket.Tests/TestUtils.cs @@ -365,9 +365,10 @@ internal static ConcurrentDictionary< /// /// Serializes an object to a JsonElement using default web options. Clones the element for safe use. /// - internal static JsonElement? ToJsonElement(object? obj) => obj == null - ? null - : obj is JsonElement element ? element.Clone() : JsonSerializer.SerializeToElement(obj, obj.GetType(), s_jsonSerializerOptions); + internal static JsonElement? ToJsonElement(object? obj) => + obj == null ? null + : obj is JsonElement element ? element.Clone() + : JsonSerializer.SerializeToElement(obj, obj.GetType(), s_jsonSerializerOptions); /// /// Gets a delegate for a private instance method using reflection. diff --git a/ObsWebSocket.Tests/TimeProviderTests.cs b/ObsWebSocket.Tests/TimeProviderTests.cs index f289868..027e79d 100644 --- a/ObsWebSocket.Tests/TimeProviderTests.cs +++ b/ObsWebSocket.Tests/TimeProviderTests.cs @@ -36,7 +36,11 @@ Mock mockWebSocket ) .Returns(ValueTask.CompletedTask); - Task pending = client.CallAsync("GetVersion", null, timeoutMs: 5000); + Task pending = client.CallAsync( + "GetVersion", + null, + timeoutMs: 5000 + ); await Task.Delay(50, TestContext.CancellationTokenSource.Token); Assert.IsFalse(pending.IsCompleted, "wall-clock time must not advance the timeout"); @@ -47,9 +51,10 @@ Mock mockWebSocket time.Advance(TimeSpan.FromMilliseconds(2)); - ObsWebSocketTimeoutException ex = await Assert.ThrowsExactlyAsync( - async () => await pending - ); + ObsWebSocketTimeoutException ex = + await Assert.ThrowsExactlyAsync(async () => + await pending + ); StringAssert.Contains(ex.Message, "timed out"); await client.DisposeAsync(); diff --git a/ObsWebSocket.Tests/TypedSettingsTests.cs b/ObsWebSocket.Tests/TypedSettingsTests.cs index 9d7dc66..638ab65 100644 --- a/ObsWebSocket.Tests/TypedSettingsTests.cs +++ b/ObsWebSocket.Tests/TypedSettingsTests.cs @@ -27,7 +27,8 @@ internal sealed record TestConsumerSettings( [JsonSerializable(typeof(TestConsumerSettings))] [JsonSourceGenerationOptions( PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase, - DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault)] + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault +)] internal sealed partial class TestConsumerSettingsJsonContext : JsonSerializerContext { } // ───────────────────────────────────────────────────────────────────────────── @@ -40,13 +41,14 @@ public class TypedSettingsTests private static MsgPackMessageSerializer CreateMsgPackSerializer() => new(NullLogger.Instance); - // ── Section 1: Direct serialization (no client infrastructure) ──────────── [TestMethod] public void BrowserSourceSettings_SerializeToJson_ProducesCorrectKeys() { - JsonTypeInfo typeInfo = ObsWebSocketSettingsJsonContext.Default.BrowserSourceSettings; + JsonTypeInfo typeInfo = ObsWebSocketSettingsJsonContext + .Default + .BrowserSourceSettings; BrowserSourceSettings settings = new( Url: "https://example.com", Width: 1920, @@ -75,7 +77,9 @@ public void BrowserSourceSettings_SerializeToJson_ProducesCorrectKeys() [TestMethod] public void BrowserSourceSettings_NullProperties_AreOmittedFromJson() { - JsonTypeInfo typeInfo = ObsWebSocketSettingsJsonContext.Default.BrowserSourceSettings; + JsonTypeInfo typeInfo = ObsWebSocketSettingsJsonContext + .Default + .BrowserSourceSettings; BrowserSourceSettings settings = new(Url: "https://example.com"); // only Url set JsonElement element = JsonSerializer.SerializeToElement(settings, typeInfo); @@ -83,15 +87,23 @@ public void BrowserSourceSettings_NullProperties_AreOmittedFromJson() Assert.AreEqual("https://example.com", element.GetProperty("url").GetString()); Assert.IsFalse(element.TryGetProperty("width", out _), "Null width should be absent"); Assert.IsFalse(element.TryGetProperty("height", out _), "Null height should be absent"); - Assert.IsFalse(element.TryGetProperty("fps_custom", out _), "Null fps_custom should be absent"); + Assert.IsFalse( + element.TryGetProperty("fps_custom", out _), + "Null fps_custom should be absent" + ); Assert.IsFalse(element.TryGetProperty("css", out _), "Null css should be absent"); - Assert.IsFalse(element.TryGetProperty("reroute_audio", out _), "Null reroute_audio should be absent"); + Assert.IsFalse( + element.TryGetProperty("reroute_audio", out _), + "Null reroute_audio should be absent" + ); } [TestMethod] public void GainFilterSettings_SerializeToJson_ProducesCorrectKey() { - JsonTypeInfo typeInfo = ObsWebSocketSettingsJsonContext.Default.GainFilterSettings; + JsonTypeInfo typeInfo = ObsWebSocketSettingsJsonContext + .Default + .GainFilterSettings; GainFilterSettings settings = new(Db: -6.0); JsonElement element = JsonSerializer.SerializeToElement(settings, typeInfo); @@ -103,7 +115,9 @@ public void GainFilterSettings_SerializeToJson_ProducesCorrectKey() [TestMethod] public void ConsumerSettings_ExplicitTypeInfo_SerializesCorrectly() { - JsonTypeInfo typeInfo = TestConsumerSettingsJsonContext.Default.TestConsumerSettings; + JsonTypeInfo typeInfo = TestConsumerSettingsJsonContext + .Default + .TestConsumerSettings; TestConsumerSettings settings = new(CustomKey: "hello", CustomCount: 42); JsonElement element = JsonSerializer.SerializeToElement(settings, typeInfo); @@ -115,7 +129,9 @@ public void ConsumerSettings_ExplicitTypeInfo_SerializesCorrectly() [TestMethod] public void BrowserSourceSettings_MsgPackRoundtrip_ThroughGetInputSettingsResponse() { - JsonTypeInfo typeInfo = ObsWebSocketSettingsJsonContext.Default.BrowserSourceSettings; + JsonTypeInfo typeInfo = ObsWebSocketSettingsJsonContext + .Default + .BrowserSourceSettings; BrowserSourceSettings original = new( Url: "https://example.com", Width: 1920, @@ -127,9 +143,15 @@ public void BrowserSourceSettings_MsgPackRoundtrip_ThroughGetInputSettingsRespon ); JsonElement settingsElement = JsonSerializer.SerializeToElement(original, typeInfo); - GetInputSettingsResponseData response = new(inputSettings: settingsElement, inputKind: "browser_source"); + GetInputSettingsResponseData response = new( + inputSettings: settingsElement, + inputKind: "browser_source" + ); - byte[] bytes = MessagePackSerializer.Serialize(response, MsgPackMessageSerializer.s_msgPackOptions); + byte[] bytes = MessagePackSerializer.Serialize( + response, + MsgPackMessageSerializer.s_msgPackOptions + ); GetInputSettingsResponseData? roundTripped = CreateMsgPackSerializer() .DeserializePayload(new ReadOnlyMemory(bytes)); @@ -150,7 +172,9 @@ public void BrowserSourceSettings_MsgPackRoundtrip_ThroughGetInputSettingsRespon [TestMethod] public void GainFilterSettings_MsgPackRoundtrip_ThroughGetSourceFilterResponse() { - JsonTypeInfo typeInfo = ObsWebSocketSettingsJsonContext.Default.GainFilterSettings; + JsonTypeInfo typeInfo = ObsWebSocketSettingsJsonContext + .Default + .GainFilterSettings; GainFilterSettings original = new(Db: -3.5); JsonElement settingsElement = JsonSerializer.SerializeToElement(original, typeInfo); @@ -161,7 +185,10 @@ public void GainFilterSettings_MsgPackRoundtrip_ThroughGetSourceFilterResponse() filterKind: "gain_filter" ); - byte[] bytes = MessagePackSerializer.Serialize(response, MsgPackMessageSerializer.s_msgPackOptions); + byte[] bytes = MessagePackSerializer.Serialize( + response, + MsgPackMessageSerializer.s_msgPackOptions + ); GetSourceFilterResponseData? roundTripped = CreateMsgPackSerializer() .DeserializePayload(new ReadOnlyMemory(bytes)); @@ -173,37 +200,65 @@ public void GainFilterSettings_MsgPackRoundtrip_ThroughGetSourceFilterResponse() Assert.AreEqual(-3.5, result.Db!.Value, 0.0001d); } - // ── Section 2: SetInputSettingsAsync helpers ────────────────────────────── [TestMethod] [Timeout(TestTimeout)] public async Task SetInputSettingsAsync_LibraryType_SendsCorrectSettingsElement() { - (ObsWebSocketClient client, Mock mockSerializer, Mock mockConnection) = - TestUtils.SetupConnectedClientForceState(); - _ = mockSerializer.Setup(s => s.DeserializePayload(It.IsAny())).Returns((object?)null); + ( + ObsWebSocketClient client, + Mock mockSerializer, + Mock mockConnection + ) = TestUtils.SetupConnectedClientForceState(); + _ = mockSerializer + .Setup(s => s.DeserializePayload(It.IsAny())) + .Returns((object?)null); BrowserSourceSettings settings = new(Url: "https://test.com", Width: 1280, Height: 720); JsonElement? capturedSettings = null; - _ = mockConnection.Setup(ws => ws.SendAsync( - It.IsAny>(), It.IsAny(), true, It.IsAny())) - .Callback((ReadOnlyMemory buffer, WebSocketMessageType _, bool _, CancellationToken _) => - { - OutgoingMessage? msg = JsonSerializer.Deserialize>( - buffer.Span, TestUtils.s_jsonSerializerOptions); - if (msg?.D?.RequestType == "SetInputSettings") - { - string id = msg.D.RequestId!; - capturedSettings = msg.D.RequestData?.GetProperty("inputSettings"); - _ = TestUtils.SimulateIncomingResponse(client, id, new RequestResponsePayload( - RequestType: "SetInputSettings", RequestId: id, - RequestStatus: new RequestStatus(Result: true, Code: (int)Core.Protocol.Generated.RequestStatus.Success), - ResponseData: null)); - } - }) - .Returns(ValueTask.CompletedTask); + _ = mockConnection + .Setup(ws => + ws.SendAsync( + It.IsAny>(), + It.IsAny(), + true, + It.IsAny() + ) + ) + .Callback( + ( + ReadOnlyMemory buffer, + WebSocketMessageType _, + bool _, + CancellationToken _ + ) => + { + OutgoingMessage? msg = JsonSerializer.Deserialize< + OutgoingMessage + >(buffer.Span, TestUtils.s_jsonSerializerOptions); + if (msg?.D?.RequestType == "SetInputSettings") + { + string id = msg.D.RequestId!; + capturedSettings = msg.D.RequestData?.GetProperty("inputSettings"); + _ = TestUtils.SimulateIncomingResponse( + client, + id, + new RequestResponsePayload( + RequestType: "SetInputSettings", + RequestId: id, + RequestStatus: new RequestStatus( + Result: true, + Code: (int)Core.Protocol.Generated.RequestStatus.Success + ), + ResponseData: null + ) + ); + } + } + ) + .Returns(ValueTask.CompletedTask); await client.Inputs.SetInputSettingsAsync("TestSource", settings, overlay: true); @@ -211,38 +266,72 @@ public async Task SetInputSettingsAsync_LibraryType_SendsCorrectSettingsElement( Assert.AreEqual("https://test.com", capturedSettings.Value.GetProperty("url").GetString()); Assert.AreEqual(1280, capturedSettings.Value.GetProperty("width").GetInt32()); Assert.AreEqual(720, capturedSettings.Value.GetProperty("height").GetInt32()); - Assert.IsFalse(capturedSettings.Value.TryGetProperty("fps_custom", out _), "Null props should be absent"); + Assert.IsFalse( + capturedSettings.Value.TryGetProperty("fps_custom", out _), + "Null props should be absent" + ); } [TestMethod] [Timeout(TestTimeout)] public async Task SetInputSettingsAsync_ConsumerType_WithExplicitTypeInfo_SendsCorrectSettingsElement() { - (ObsWebSocketClient client, Mock mockSerializer, Mock mockConnection) = - TestUtils.SetupConnectedClientForceState(); - _ = mockSerializer.Setup(s => s.DeserializePayload(It.IsAny())).Returns((object?)null); + ( + ObsWebSocketClient client, + Mock mockSerializer, + Mock mockConnection + ) = TestUtils.SetupConnectedClientForceState(); + _ = mockSerializer + .Setup(s => s.DeserializePayload(It.IsAny())) + .Returns((object?)null); TestConsumerSettings settings = new(CustomKey: "abc", CustomCount: 99); - JsonTypeInfo typeInfo = TestConsumerSettingsJsonContext.Default.TestConsumerSettings; + JsonTypeInfo typeInfo = TestConsumerSettingsJsonContext + .Default + .TestConsumerSettings; JsonElement? capturedSettings = null; - _ = mockConnection.Setup(ws => ws.SendAsync( - It.IsAny>(), It.IsAny(), true, It.IsAny())) - .Callback((ReadOnlyMemory buffer, WebSocketMessageType _, bool _, CancellationToken _) => - { - OutgoingMessage? msg = JsonSerializer.Deserialize>( - buffer.Span, TestUtils.s_jsonSerializerOptions); - if (msg?.D?.RequestType == "SetInputSettings") - { - string id = msg.D.RequestId!; - capturedSettings = msg.D.RequestData?.GetProperty("inputSettings"); - _ = TestUtils.SimulateIncomingResponse(client, id, new RequestResponsePayload( - RequestType: "SetInputSettings", RequestId: id, - RequestStatus: new RequestStatus(Result: true, Code: (int)Core.Protocol.Generated.RequestStatus.Success), - ResponseData: null)); - } - }) - .Returns(ValueTask.CompletedTask); + _ = mockConnection + .Setup(ws => + ws.SendAsync( + It.IsAny>(), + It.IsAny(), + true, + It.IsAny() + ) + ) + .Callback( + ( + ReadOnlyMemory buffer, + WebSocketMessageType _, + bool _, + CancellationToken _ + ) => + { + OutgoingMessage? msg = JsonSerializer.Deserialize< + OutgoingMessage + >(buffer.Span, TestUtils.s_jsonSerializerOptions); + if (msg?.D?.RequestType == "SetInputSettings") + { + string id = msg.D.RequestId!; + capturedSettings = msg.D.RequestData?.GetProperty("inputSettings"); + _ = TestUtils.SimulateIncomingResponse( + client, + id, + new RequestResponsePayload( + RequestType: "SetInputSettings", + RequestId: id, + RequestStatus: new RequestStatus( + Result: true, + Code: (int)Core.Protocol.Generated.RequestStatus.Success + ), + ResponseData: null + ) + ); + } + } + ) + .Returns(ValueTask.CompletedTask); await client.Inputs.SetInputSettingsAsync("TestSource", settings, typeInfo, overlay: true); @@ -256,8 +345,12 @@ public async Task SetInputSettingsAsync_UnregisteredType_ThrowsObsWebSocketExcep { (ObsWebSocketClient client, _, _) = TestUtils.SetupConnectedClientForceState(); - _ = await Assert.ThrowsExactlyAsync( - () => client.Inputs.SetInputSettingsAsync("TestSource", new TestConsumerSettings(), overlay: true) + _ = await Assert.ThrowsExactlyAsync(() => + client.Inputs.SetInputSettingsAsync( + "TestSource", + new TestConsumerSettings(), + overlay: true + ) ); } @@ -267,35 +360,74 @@ public async Task SetInputSettingsAsync_UnregisteredType_ThrowsObsWebSocketExcep [Timeout(TestTimeout)] public async Task GetInputSettingsAsync_LibraryType_DeserializesCorrectly() { - (ObsWebSocketClient client, Mock mockSerializer, Mock mockConnection) = - TestUtils.SetupConnectedClientForceState(); - - BrowserSourceSettings expected = new(Url: "https://obs.test", Width: 1920, Height: 1080, RerouteAudio: true); - JsonTypeInfo typeInfo = ObsWebSocketSettingsJsonContext.Default.BrowserSourceSettings; + ( + ObsWebSocketClient client, + Mock mockSerializer, + Mock mockConnection + ) = TestUtils.SetupConnectedClientForceState(); + + BrowserSourceSettings expected = new( + Url: "https://obs.test", + Width: 1920, + Height: 1080, + RerouteAudio: true + ); + JsonTypeInfo typeInfo = ObsWebSocketSettingsJsonContext + .Default + .BrowserSourceSettings; JsonElement settingsElement = JsonSerializer.SerializeToElement(expected, typeInfo); - GetInputSettingsResponseData responseDto = new(inputSettings: settingsElement, inputKind: "browser_source"); - - _ = mockConnection.Setup(ws => ws.SendAsync( - It.IsAny>(), It.IsAny(), true, It.IsAny())) - .Callback((ReadOnlyMemory buffer, WebSocketMessageType _, bool _, CancellationToken _) => - { - OutgoingMessage? msg = JsonSerializer.Deserialize>( - buffer.Span, TestUtils.s_jsonSerializerOptions); - if (msg?.D?.RequestType == "GetInputSettings") - { - JsonElement rawPayload = TestUtils.ToJsonElement(responseDto)!.Value; - _ = TestUtils.SimulateIncomingResponse(client, msg.D.RequestId!, new RequestResponsePayload( - RequestType: "GetInputSettings", RequestId: msg.D.RequestId!, - RequestStatus: new RequestStatus(Result: true, Code: (int)Core.Protocol.Generated.RequestStatus.Success), - ResponseData: rawPayload)); - } - }) - .Returns(ValueTask.CompletedTask); - - _ = mockSerializer.Setup(s => s.DeserializePayload(It.IsAny())) + GetInputSettingsResponseData responseDto = new( + inputSettings: settingsElement, + inputKind: "browser_source" + ); + + _ = mockConnection + .Setup(ws => + ws.SendAsync( + It.IsAny>(), + It.IsAny(), + true, + It.IsAny() + ) + ) + .Callback( + ( + ReadOnlyMemory buffer, + WebSocketMessageType _, + bool _, + CancellationToken _ + ) => + { + OutgoingMessage? msg = JsonSerializer.Deserialize< + OutgoingMessage + >(buffer.Span, TestUtils.s_jsonSerializerOptions); + if (msg?.D?.RequestType == "GetInputSettings") + { + JsonElement rawPayload = TestUtils.ToJsonElement(responseDto)!.Value; + _ = TestUtils.SimulateIncomingResponse( + client, + msg.D.RequestId!, + new RequestResponsePayload( + RequestType: "GetInputSettings", + RequestId: msg.D.RequestId!, + RequestStatus: new RequestStatus( + Result: true, + Code: (int)Core.Protocol.Generated.RequestStatus.Success + ), + ResponseData: rawPayload + ) + ); + } + } + ) + .Returns(ValueTask.CompletedTask); + + _ = mockSerializer + .Setup(s => s.DeserializePayload(It.IsAny())) .Returns(responseDto); - BrowserSourceSettings? result = await client.Inputs.GetInputSettingsAsync("TestInput"); + BrowserSourceSettings? result = + await client.Inputs.GetInputSettingsAsync("TestInput"); Assert.IsNotNull(result); Assert.AreEqual("https://obs.test", result.Url); @@ -309,35 +441,71 @@ public async Task GetInputSettingsAsync_LibraryType_DeserializesCorrectly() [Timeout(TestTimeout)] public async Task GetInputSettingsAsync_ConsumerType_WithExplicitTypeInfo_DeserializesCorrectly() { - (ObsWebSocketClient client, Mock mockSerializer, Mock mockConnection) = - TestUtils.SetupConnectedClientForceState(); + ( + ObsWebSocketClient client, + Mock mockSerializer, + Mock mockConnection + ) = TestUtils.SetupConnectedClientForceState(); TestConsumerSettings expected = new(CustomKey: "xyz", CustomCount: 7); - JsonTypeInfo typeInfo = TestConsumerSettingsJsonContext.Default.TestConsumerSettings; + JsonTypeInfo typeInfo = TestConsumerSettingsJsonContext + .Default + .TestConsumerSettings; JsonElement settingsElement = JsonSerializer.SerializeToElement(expected, typeInfo); - GetInputSettingsResponseData responseDto = new(inputSettings: settingsElement, inputKind: "custom_source"); - - _ = mockConnection.Setup(ws => ws.SendAsync( - It.IsAny>(), It.IsAny(), true, It.IsAny())) - .Callback((ReadOnlyMemory buffer, WebSocketMessageType _, bool _, CancellationToken _) => - { - OutgoingMessage? msg = JsonSerializer.Deserialize>( - buffer.Span, TestUtils.s_jsonSerializerOptions); - if (msg?.D?.RequestType == "GetInputSettings") - { - JsonElement rawPayload = TestUtils.ToJsonElement(responseDto)!.Value; - _ = TestUtils.SimulateIncomingResponse(client, msg.D.RequestId!, new RequestResponsePayload( - RequestType: "GetInputSettings", RequestId: msg.D.RequestId!, - RequestStatus: new RequestStatus(Result: true, Code: (int)Core.Protocol.Generated.RequestStatus.Success), - ResponseData: rawPayload)); - } - }) - .Returns(ValueTask.CompletedTask); - - _ = mockSerializer.Setup(s => s.DeserializePayload(It.IsAny())) + GetInputSettingsResponseData responseDto = new( + inputSettings: settingsElement, + inputKind: "custom_source" + ); + + _ = mockConnection + .Setup(ws => + ws.SendAsync( + It.IsAny>(), + It.IsAny(), + true, + It.IsAny() + ) + ) + .Callback( + ( + ReadOnlyMemory buffer, + WebSocketMessageType _, + bool _, + CancellationToken _ + ) => + { + OutgoingMessage? msg = JsonSerializer.Deserialize< + OutgoingMessage + >(buffer.Span, TestUtils.s_jsonSerializerOptions); + if (msg?.D?.RequestType == "GetInputSettings") + { + JsonElement rawPayload = TestUtils.ToJsonElement(responseDto)!.Value; + _ = TestUtils.SimulateIncomingResponse( + client, + msg.D.RequestId!, + new RequestResponsePayload( + RequestType: "GetInputSettings", + RequestId: msg.D.RequestId!, + RequestStatus: new RequestStatus( + Result: true, + Code: (int)Core.Protocol.Generated.RequestStatus.Success + ), + ResponseData: rawPayload + ) + ); + } + } + ) + .Returns(ValueTask.CompletedTask); + + _ = mockSerializer + .Setup(s => s.DeserializePayload(It.IsAny())) .Returns(responseDto); - TestConsumerSettings? result = await client.Inputs.GetInputSettingsAsync("TestInput", typeInfo); + TestConsumerSettings? result = await client.Inputs.GetInputSettingsAsync( + "TestInput", + typeInfo + ); Assert.IsNotNull(result); Assert.AreEqual("xyz", result.CustomKey); @@ -350,32 +518,66 @@ public async Task GetInputSettingsAsync_ConsumerType_WithExplicitTypeInfo_Deseri [Timeout(TestTimeout)] public async Task SetSourceFilterSettingsAsync_LibraryType_SendsCorrectSettingsElement() { - (ObsWebSocketClient client, Mock mockSerializer, Mock mockConnection) = - TestUtils.SetupConnectedClientForceState(); - _ = mockSerializer.Setup(s => s.DeserializePayload(It.IsAny())).Returns((object?)null); + ( + ObsWebSocketClient client, + Mock mockSerializer, + Mock mockConnection + ) = TestUtils.SetupConnectedClientForceState(); + _ = mockSerializer + .Setup(s => s.DeserializePayload(It.IsAny())) + .Returns((object?)null); GainFilterSettings settings = new(Db: -12.0); JsonElement? capturedSettings = null; - _ = mockConnection.Setup(ws => ws.SendAsync( - It.IsAny>(), It.IsAny(), true, It.IsAny())) - .Callback((ReadOnlyMemory buffer, WebSocketMessageType _, bool _, CancellationToken _) => - { - OutgoingMessage? msg = JsonSerializer.Deserialize>( - buffer.Span, TestUtils.s_jsonSerializerOptions); - if (msg?.D?.RequestType == "SetSourceFilterSettings") - { - string id = msg.D.RequestId!; - capturedSettings = msg.D.RequestData?.GetProperty("filterSettings"); - _ = TestUtils.SimulateIncomingResponse(client, id, new RequestResponsePayload( - RequestType: "SetSourceFilterSettings", RequestId: id, - RequestStatus: new RequestStatus(Result: true, Code: (int)Core.Protocol.Generated.RequestStatus.Success), - ResponseData: null)); - } - }) - .Returns(ValueTask.CompletedTask); - - await client.Filters.SetSourceFilterSettingsAsync("AudioSource", "Gain", settings, overlay: true); + _ = mockConnection + .Setup(ws => + ws.SendAsync( + It.IsAny>(), + It.IsAny(), + true, + It.IsAny() + ) + ) + .Callback( + ( + ReadOnlyMemory buffer, + WebSocketMessageType _, + bool _, + CancellationToken _ + ) => + { + OutgoingMessage? msg = JsonSerializer.Deserialize< + OutgoingMessage + >(buffer.Span, TestUtils.s_jsonSerializerOptions); + if (msg?.D?.RequestType == "SetSourceFilterSettings") + { + string id = msg.D.RequestId!; + capturedSettings = msg.D.RequestData?.GetProperty("filterSettings"); + _ = TestUtils.SimulateIncomingResponse( + client, + id, + new RequestResponsePayload( + RequestType: "SetSourceFilterSettings", + RequestId: id, + RequestStatus: new RequestStatus( + Result: true, + Code: (int)Core.Protocol.Generated.RequestStatus.Success + ), + ResponseData: null + ) + ); + } + } + ) + .Returns(ValueTask.CompletedTask); + + await client.Filters.SetSourceFilterSettingsAsync( + "AudioSource", + "Gain", + settings, + overlay: true + ); Assert.IsNotNull(capturedSettings); Assert.AreEqual(-12.0, capturedSettings.Value.GetProperty("db").GetDouble(), 0.0001d); @@ -385,37 +587,77 @@ public async Task SetSourceFilterSettingsAsync_LibraryType_SendsCorrectSettingsE [Timeout(TestTimeout)] public async Task SetSourceFilterSettingsAsync_ConsumerType_WithExplicitTypeInfo_SendsCorrectSettingsElement() { - (ObsWebSocketClient client, Mock mockSerializer, Mock mockConnection) = - TestUtils.SetupConnectedClientForceState(); - _ = mockSerializer.Setup(s => s.DeserializePayload(It.IsAny())).Returns((object?)null); + ( + ObsWebSocketClient client, + Mock mockSerializer, + Mock mockConnection + ) = TestUtils.SetupConnectedClientForceState(); + _ = mockSerializer + .Setup(s => s.DeserializePayload(It.IsAny())) + .Returns((object?)null); TestConsumerSettings settings = new(CustomKey: "filter-val"); - JsonTypeInfo typeInfo = TestConsumerSettingsJsonContext.Default.TestConsumerSettings; + JsonTypeInfo typeInfo = TestConsumerSettingsJsonContext + .Default + .TestConsumerSettings; JsonElement? capturedSettings = null; - _ = mockConnection.Setup(ws => ws.SendAsync( - It.IsAny>(), It.IsAny(), true, It.IsAny())) - .Callback((ReadOnlyMemory buffer, WebSocketMessageType _, bool _, CancellationToken _) => - { - OutgoingMessage? msg = JsonSerializer.Deserialize>( - buffer.Span, TestUtils.s_jsonSerializerOptions); - if (msg?.D?.RequestType == "SetSourceFilterSettings") - { - string id = msg.D.RequestId!; - capturedSettings = msg.D.RequestData?.GetProperty("filterSettings"); - _ = TestUtils.SimulateIncomingResponse(client, id, new RequestResponsePayload( - RequestType: "SetSourceFilterSettings", RequestId: id, - RequestStatus: new RequestStatus(Result: true, Code: (int)Core.Protocol.Generated.RequestStatus.Success), - ResponseData: null)); - } - }) - .Returns(ValueTask.CompletedTask); - - await client.Filters.SetSourceFilterSettingsAsync("AudioSource", "CustomFilter", settings, typeInfo, overlay: true); + _ = mockConnection + .Setup(ws => + ws.SendAsync( + It.IsAny>(), + It.IsAny(), + true, + It.IsAny() + ) + ) + .Callback( + ( + ReadOnlyMemory buffer, + WebSocketMessageType _, + bool _, + CancellationToken _ + ) => + { + OutgoingMessage? msg = JsonSerializer.Deserialize< + OutgoingMessage + >(buffer.Span, TestUtils.s_jsonSerializerOptions); + if (msg?.D?.RequestType == "SetSourceFilterSettings") + { + string id = msg.D.RequestId!; + capturedSettings = msg.D.RequestData?.GetProperty("filterSettings"); + _ = TestUtils.SimulateIncomingResponse( + client, + id, + new RequestResponsePayload( + RequestType: "SetSourceFilterSettings", + RequestId: id, + RequestStatus: new RequestStatus( + Result: true, + Code: (int)Core.Protocol.Generated.RequestStatus.Success + ), + ResponseData: null + ) + ); + } + } + ) + .Returns(ValueTask.CompletedTask); + + await client.Filters.SetSourceFilterSettingsAsync( + "AudioSource", + "CustomFilter", + settings, + typeInfo, + overlay: true + ); Assert.IsNotNull(capturedSettings); Assert.AreEqual("filter-val", capturedSettings.Value.GetProperty("custom_key").GetString()); - Assert.IsFalse(capturedSettings.Value.TryGetProperty("custom_count", out _), "Null props should be absent"); + Assert.IsFalse( + capturedSettings.Value.TryGetProperty("custom_count", out _), + "Null props should be absent" + ); } // ── Section 5: GetSourceFilterSettingsAsync helpers ─────────────────────── @@ -424,36 +666,74 @@ public async Task SetSourceFilterSettingsAsync_ConsumerType_WithExplicitTypeInfo [Timeout(TestTimeout)] public async Task GetSourceFilterSettingsAsync_LibraryType_DeserializesCorrectly() { - (ObsWebSocketClient client, Mock mockSerializer, Mock mockConnection) = - TestUtils.SetupConnectedClientForceState(); + ( + ObsWebSocketClient client, + Mock mockSerializer, + Mock mockConnection + ) = TestUtils.SetupConnectedClientForceState(); GainFilterSettings expected = new(Db: -6.0); - JsonTypeInfo typeInfo = ObsWebSocketSettingsJsonContext.Default.GainFilterSettings; + JsonTypeInfo typeInfo = ObsWebSocketSettingsJsonContext + .Default + .GainFilterSettings; JsonElement settingsElement = JsonSerializer.SerializeToElement(expected, typeInfo); GetSourceFilterResponseData responseDto = new( - filterSettings: settingsElement, filterEnabled: true, filterIndex: 0, filterKind: "gain_filter"); - - _ = mockConnection.Setup(ws => ws.SendAsync( - It.IsAny>(), It.IsAny(), true, It.IsAny())) - .Callback((ReadOnlyMemory buffer, WebSocketMessageType _, bool _, CancellationToken _) => - { - OutgoingMessage? msg = JsonSerializer.Deserialize>( - buffer.Span, TestUtils.s_jsonSerializerOptions); - if (msg?.D?.RequestType == "GetSourceFilter") - { - JsonElement rawPayload = TestUtils.ToJsonElement(responseDto)!.Value; - _ = TestUtils.SimulateIncomingResponse(client, msg.D.RequestId!, new RequestResponsePayload( - RequestType: "GetSourceFilter", RequestId: msg.D.RequestId!, - RequestStatus: new RequestStatus(Result: true, Code: (int)Core.Protocol.Generated.RequestStatus.Success), - ResponseData: rawPayload)); - } - }) - .Returns(ValueTask.CompletedTask); - - _ = mockSerializer.Setup(s => s.DeserializePayload(It.IsAny())) + filterSettings: settingsElement, + filterEnabled: true, + filterIndex: 0, + filterKind: "gain_filter" + ); + + _ = mockConnection + .Setup(ws => + ws.SendAsync( + It.IsAny>(), + It.IsAny(), + true, + It.IsAny() + ) + ) + .Callback( + ( + ReadOnlyMemory buffer, + WebSocketMessageType _, + bool _, + CancellationToken _ + ) => + { + OutgoingMessage? msg = JsonSerializer.Deserialize< + OutgoingMessage + >(buffer.Span, TestUtils.s_jsonSerializerOptions); + if (msg?.D?.RequestType == "GetSourceFilter") + { + JsonElement rawPayload = TestUtils.ToJsonElement(responseDto)!.Value; + _ = TestUtils.SimulateIncomingResponse( + client, + msg.D.RequestId!, + new RequestResponsePayload( + RequestType: "GetSourceFilter", + RequestId: msg.D.RequestId!, + RequestStatus: new RequestStatus( + Result: true, + Code: (int)Core.Protocol.Generated.RequestStatus.Success + ), + ResponseData: rawPayload + ) + ); + } + } + ) + .Returns(ValueTask.CompletedTask); + + _ = mockSerializer + .Setup(s => s.DeserializePayload(It.IsAny())) .Returns(responseDto); - GainFilterSettings? result = await client.Filters.GetSourceFilterSettingsAsync("AudioSource", "Gain"); + GainFilterSettings? result = + await client.Filters.GetSourceFilterSettingsAsync( + "AudioSource", + "Gain" + ); Assert.IsNotNull(result); Assert.AreEqual(-6.0, result.Db!.Value, 0.0001d); @@ -463,36 +743,74 @@ public async Task GetSourceFilterSettingsAsync_LibraryType_DeserializesCorrectly [Timeout(TestTimeout)] public async Task GetSourceFilterSettingsAsync_ConsumerType_WithExplicitTypeInfo_DeserializesCorrectly() { - (ObsWebSocketClient client, Mock mockSerializer, Mock mockConnection) = - TestUtils.SetupConnectedClientForceState(); + ( + ObsWebSocketClient client, + Mock mockSerializer, + Mock mockConnection + ) = TestUtils.SetupConnectedClientForceState(); TestConsumerSettings expected = new(CustomKey: "my-filter", CustomCount: 3); - JsonTypeInfo typeInfo = TestConsumerSettingsJsonContext.Default.TestConsumerSettings; + JsonTypeInfo typeInfo = TestConsumerSettingsJsonContext + .Default + .TestConsumerSettings; JsonElement settingsElement = JsonSerializer.SerializeToElement(expected, typeInfo); GetSourceFilterResponseData responseDto = new( - filterSettings: settingsElement, filterEnabled: true, filterIndex: 0, filterKind: "custom_filter"); - - _ = mockConnection.Setup(ws => ws.SendAsync( - It.IsAny>(), It.IsAny(), true, It.IsAny())) - .Callback((ReadOnlyMemory buffer, WebSocketMessageType _, bool _, CancellationToken _) => - { - OutgoingMessage? msg = JsonSerializer.Deserialize>( - buffer.Span, TestUtils.s_jsonSerializerOptions); - if (msg?.D?.RequestType == "GetSourceFilter") - { - JsonElement rawPayload = TestUtils.ToJsonElement(responseDto)!.Value; - _ = TestUtils.SimulateIncomingResponse(client, msg.D.RequestId!, new RequestResponsePayload( - RequestType: "GetSourceFilter", RequestId: msg.D.RequestId!, - RequestStatus: new RequestStatus(Result: true, Code: (int)Core.Protocol.Generated.RequestStatus.Success), - ResponseData: rawPayload)); - } - }) - .Returns(ValueTask.CompletedTask); - - _ = mockSerializer.Setup(s => s.DeserializePayload(It.IsAny())) + filterSettings: settingsElement, + filterEnabled: true, + filterIndex: 0, + filterKind: "custom_filter" + ); + + _ = mockConnection + .Setup(ws => + ws.SendAsync( + It.IsAny>(), + It.IsAny(), + true, + It.IsAny() + ) + ) + .Callback( + ( + ReadOnlyMemory buffer, + WebSocketMessageType _, + bool _, + CancellationToken _ + ) => + { + OutgoingMessage? msg = JsonSerializer.Deserialize< + OutgoingMessage + >(buffer.Span, TestUtils.s_jsonSerializerOptions); + if (msg?.D?.RequestType == "GetSourceFilter") + { + JsonElement rawPayload = TestUtils.ToJsonElement(responseDto)!.Value; + _ = TestUtils.SimulateIncomingResponse( + client, + msg.D.RequestId!, + new RequestResponsePayload( + RequestType: "GetSourceFilter", + RequestId: msg.D.RequestId!, + RequestStatus: new RequestStatus( + Result: true, + Code: (int)Core.Protocol.Generated.RequestStatus.Success + ), + ResponseData: rawPayload + ) + ); + } + } + ) + .Returns(ValueTask.CompletedTask); + + _ = mockSerializer + .Setup(s => s.DeserializePayload(It.IsAny())) .Returns(responseDto); - TestConsumerSettings? result = await client.Filters.GetSourceFilterSettingsAsync("AudioSource", "CustomFilter", typeInfo); + TestConsumerSettings? result = await client.Filters.GetSourceFilterSettingsAsync( + "AudioSource", + "CustomFilter", + typeInfo + ); Assert.IsNotNull(result); Assert.AreEqual("my-filter", result.CustomKey); @@ -505,45 +823,78 @@ public async Task GetSourceFilterSettingsAsync_ConsumerType_WithExplicitTypeInfo [Timeout(TestTimeout)] public async Task CreateInputAsync_LibraryType_SendsCorrectSettingsElement() { - (ObsWebSocketClient client, Mock mockSerializer, Mock mockConnection) = - TestUtils.SetupConnectedClientForceState(); + ( + ObsWebSocketClient client, + Mock mockSerializer, + Mock mockConnection + ) = TestUtils.SetupConnectedClientForceState(); BrowserSourceSettings settings = new(Url: "https://create.test", Width: 800, Height: 600); JsonElement? capturedSettings = null; CreateInputResponseData responseDto = new(sceneItemId: 42); - _ = mockConnection.Setup(ws => ws.SendAsync( - It.IsAny>(), It.IsAny(), true, It.IsAny())) - .Callback((ReadOnlyMemory buffer, WebSocketMessageType _, bool _, CancellationToken _) => - { - OutgoingMessage? msg = JsonSerializer.Deserialize>( - buffer.Span, TestUtils.s_jsonSerializerOptions); - if (msg?.D?.RequestType == "CreateInput") - { - string id = msg.D.RequestId!; - capturedSettings = msg.D.RequestData?.GetProperty("inputSettings"); - JsonElement rawPayload = TestUtils.ToJsonElement(responseDto)!.Value; - _ = TestUtils.SimulateIncomingResponse(client, id, new RequestResponsePayload( - RequestType: "CreateInput", RequestId: id, - RequestStatus: new RequestStatus(Result: true, Code: (int)Core.Protocol.Generated.RequestStatus.Success), - ResponseData: rawPayload)); - } - }) - .Returns(ValueTask.CompletedTask); - - _ = mockSerializer.Setup(s => s.DeserializePayload(It.IsAny())) + _ = mockConnection + .Setup(ws => + ws.SendAsync( + It.IsAny>(), + It.IsAny(), + true, + It.IsAny() + ) + ) + .Callback( + ( + ReadOnlyMemory buffer, + WebSocketMessageType _, + bool _, + CancellationToken _ + ) => + { + OutgoingMessage? msg = JsonSerializer.Deserialize< + OutgoingMessage + >(buffer.Span, TestUtils.s_jsonSerializerOptions); + if (msg?.D?.RequestType == "CreateInput") + { + string id = msg.D.RequestId!; + capturedSettings = msg.D.RequestData?.GetProperty("inputSettings"); + JsonElement rawPayload = TestUtils.ToJsonElement(responseDto)!.Value; + _ = TestUtils.SimulateIncomingResponse( + client, + id, + new RequestResponsePayload( + RequestType: "CreateInput", + RequestId: id, + RequestStatus: new RequestStatus( + Result: true, + Code: (int)Core.Protocol.Generated.RequestStatus.Success + ), + ResponseData: rawPayload + ) + ); + } + } + ) + .Returns(ValueTask.CompletedTask); + + _ = mockSerializer + .Setup(s => s.DeserializePayload(It.IsAny())) .Returns(responseDto); - CreateInputResponseData? result = await client.Inputs.CreateInputAsync(inputKind: "browser_source", + CreateInputResponseData? result = await client.Inputs.CreateInputAsync( + inputKind: "browser_source", inputName: "New Browser", settings: settings, sceneName: "Scene A", - sceneItemEnabled: true); + sceneItemEnabled: true + ); Assert.IsNotNull(result); Assert.AreEqual(42, result.SceneItemId); Assert.IsNotNull(capturedSettings); - Assert.AreEqual("https://create.test", capturedSettings.Value.GetProperty("url").GetString()); + Assert.AreEqual( + "https://create.test", + capturedSettings.Value.GetProperty("url").GetString() + ); Assert.AreEqual(800, capturedSettings.Value.GetProperty("width").GetInt32()); } @@ -551,40 +902,72 @@ public async Task CreateInputAsync_LibraryType_SendsCorrectSettingsElement() [Timeout(TestTimeout)] public async Task CreateInputAsync_ConsumerType_WithExplicitTypeInfo_SendsCorrectSettingsElement() { - (ObsWebSocketClient client, Mock mockSerializer, Mock mockConnection) = - TestUtils.SetupConnectedClientForceState(); + ( + ObsWebSocketClient client, + Mock mockSerializer, + Mock mockConnection + ) = TestUtils.SetupConnectedClientForceState(); TestConsumerSettings settings = new(CustomKey: "my-input", CustomCount: 5); - JsonTypeInfo typeInfo = TestConsumerSettingsJsonContext.Default.TestConsumerSettings; + JsonTypeInfo typeInfo = TestConsumerSettingsJsonContext + .Default + .TestConsumerSettings; JsonElement? capturedSettings = null; CreateInputResponseData responseDto = new(sceneItemId: 7); - _ = mockConnection.Setup(ws => ws.SendAsync( - It.IsAny>(), It.IsAny(), true, It.IsAny())) - .Callback((ReadOnlyMemory buffer, WebSocketMessageType _, bool _, CancellationToken _) => - { - OutgoingMessage? msg = JsonSerializer.Deserialize>( - buffer.Span, TestUtils.s_jsonSerializerOptions); - if (msg?.D?.RequestType == "CreateInput") - { - string id = msg.D.RequestId!; - capturedSettings = msg.D.RequestData?.GetProperty("inputSettings"); - JsonElement rawPayload = TestUtils.ToJsonElement(responseDto)!.Value; - _ = TestUtils.SimulateIncomingResponse(client, id, new RequestResponsePayload( - RequestType: "CreateInput", RequestId: id, - RequestStatus: new RequestStatus(Result: true, Code: (int)Core.Protocol.Generated.RequestStatus.Success), - ResponseData: rawPayload)); - } - }) - .Returns(ValueTask.CompletedTask); - - _ = mockSerializer.Setup(s => s.DeserializePayload(It.IsAny())) + _ = mockConnection + .Setup(ws => + ws.SendAsync( + It.IsAny>(), + It.IsAny(), + true, + It.IsAny() + ) + ) + .Callback( + ( + ReadOnlyMemory buffer, + WebSocketMessageType _, + bool _, + CancellationToken _ + ) => + { + OutgoingMessage? msg = JsonSerializer.Deserialize< + OutgoingMessage + >(buffer.Span, TestUtils.s_jsonSerializerOptions); + if (msg?.D?.RequestType == "CreateInput") + { + string id = msg.D.RequestId!; + capturedSettings = msg.D.RequestData?.GetProperty("inputSettings"); + JsonElement rawPayload = TestUtils.ToJsonElement(responseDto)!.Value; + _ = TestUtils.SimulateIncomingResponse( + client, + id, + new RequestResponsePayload( + RequestType: "CreateInput", + RequestId: id, + RequestStatus: new RequestStatus( + Result: true, + Code: (int)Core.Protocol.Generated.RequestStatus.Success + ), + ResponseData: rawPayload + ) + ); + } + } + ) + .Returns(ValueTask.CompletedTask); + + _ = mockSerializer + .Setup(s => s.DeserializePayload(It.IsAny())) .Returns(responseDto); - CreateInputResponseData? result = await client.Inputs.CreateInputAsync(inputKind: "custom_source", + CreateInputResponseData? result = await client.Inputs.CreateInputAsync( + inputKind: "custom_source", inputName: "My Custom", settings: settings, - typeInfo: typeInfo); + typeInfo: typeInfo + ); Assert.IsNotNull(result); Assert.AreEqual(7, result.SceneItemId); @@ -599,35 +982,66 @@ public async Task CreateInputAsync_ConsumerType_WithExplicitTypeInfo_SendsCorrec [Timeout(TestTimeout)] public async Task CreateSourceFilterAsync_LibraryType_SendsCorrectSettingsElement() { - (ObsWebSocketClient client, Mock mockSerializer, Mock mockConnection) = - TestUtils.SetupConnectedClientForceState(); - _ = mockSerializer.Setup(s => s.DeserializePayload(It.IsAny())).Returns((object?)null); + ( + ObsWebSocketClient client, + Mock mockSerializer, + Mock mockConnection + ) = TestUtils.SetupConnectedClientForceState(); + _ = mockSerializer + .Setup(s => s.DeserializePayload(It.IsAny())) + .Returns((object?)null); GainFilterSettings settings = new(Db: -3.0); JsonElement? capturedSettings = null; - _ = mockConnection.Setup(ws => ws.SendAsync( - It.IsAny>(), It.IsAny(), true, It.IsAny())) - .Callback((ReadOnlyMemory buffer, WebSocketMessageType _, bool _, CancellationToken _) => - { - OutgoingMessage? msg = JsonSerializer.Deserialize>( - buffer.Span, TestUtils.s_jsonSerializerOptions); - if (msg?.D?.RequestType == "CreateSourceFilter") - { - string id = msg.D.RequestId!; - capturedSettings = msg.D.RequestData?.GetProperty("filterSettings"); - _ = TestUtils.SimulateIncomingResponse(client, id, new RequestResponsePayload( - RequestType: "CreateSourceFilter", RequestId: id, - RequestStatus: new RequestStatus(Result: true, Code: (int)Core.Protocol.Generated.RequestStatus.Success), - ResponseData: null)); - } - }) - .Returns(ValueTask.CompletedTask); - - await client.Filters.CreateSourceFilterAsync(sourceName: "AudioSource", + _ = mockConnection + .Setup(ws => + ws.SendAsync( + It.IsAny>(), + It.IsAny(), + true, + It.IsAny() + ) + ) + .Callback( + ( + ReadOnlyMemory buffer, + WebSocketMessageType _, + bool _, + CancellationToken _ + ) => + { + OutgoingMessage? msg = JsonSerializer.Deserialize< + OutgoingMessage + >(buffer.Span, TestUtils.s_jsonSerializerOptions); + if (msg?.D?.RequestType == "CreateSourceFilter") + { + string id = msg.D.RequestId!; + capturedSettings = msg.D.RequestData?.GetProperty("filterSettings"); + _ = TestUtils.SimulateIncomingResponse( + client, + id, + new RequestResponsePayload( + RequestType: "CreateSourceFilter", + RequestId: id, + RequestStatus: new RequestStatus( + Result: true, + Code: (int)Core.Protocol.Generated.RequestStatus.Success + ), + ResponseData: null + ) + ); + } + } + ) + .Returns(ValueTask.CompletedTask); + + await client.Filters.CreateSourceFilterAsync( + sourceName: "AudioSource", filterName: "My Gain", filterKind: "gain_filter", - settings: settings); + settings: settings + ); Assert.IsNotNull(capturedSettings); Assert.AreEqual(-3.0, capturedSettings.Value.GetProperty("db").GetDouble(), 0.0001d); @@ -637,37 +1051,70 @@ await client.Filters.CreateSourceFilterAsync(sourceName: "AudioSource", [Timeout(TestTimeout)] public async Task CreateSourceFilterAsync_ConsumerType_WithExplicitTypeInfo_SendsCorrectSettingsElement() { - (ObsWebSocketClient client, Mock mockSerializer, Mock mockConnection) = - TestUtils.SetupConnectedClientForceState(); - _ = mockSerializer.Setup(s => s.DeserializePayload(It.IsAny())).Returns((object?)null); + ( + ObsWebSocketClient client, + Mock mockSerializer, + Mock mockConnection + ) = TestUtils.SetupConnectedClientForceState(); + _ = mockSerializer + .Setup(s => s.DeserializePayload(It.IsAny())) + .Returns((object?)null); TestConsumerSettings settings = new(CustomKey: "my-filter", CustomCount: 1); - JsonTypeInfo typeInfo = TestConsumerSettingsJsonContext.Default.TestConsumerSettings; + JsonTypeInfo typeInfo = TestConsumerSettingsJsonContext + .Default + .TestConsumerSettings; JsonElement? capturedSettings = null; - _ = mockConnection.Setup(ws => ws.SendAsync( - It.IsAny>(), It.IsAny(), true, It.IsAny())) - .Callback((ReadOnlyMemory buffer, WebSocketMessageType _, bool _, CancellationToken _) => - { - OutgoingMessage? msg = JsonSerializer.Deserialize>( - buffer.Span, TestUtils.s_jsonSerializerOptions); - if (msg?.D?.RequestType == "CreateSourceFilter") - { - string id = msg.D.RequestId!; - capturedSettings = msg.D.RequestData?.GetProperty("filterSettings"); - _ = TestUtils.SimulateIncomingResponse(client, id, new RequestResponsePayload( - RequestType: "CreateSourceFilter", RequestId: id, - RequestStatus: new RequestStatus(Result: true, Code: (int)Core.Protocol.Generated.RequestStatus.Success), - ResponseData: null)); - } - }) - .Returns(ValueTask.CompletedTask); - - await client.Filters.CreateSourceFilterAsync(sourceName: "VideoSource", + _ = mockConnection + .Setup(ws => + ws.SendAsync( + It.IsAny>(), + It.IsAny(), + true, + It.IsAny() + ) + ) + .Callback( + ( + ReadOnlyMemory buffer, + WebSocketMessageType _, + bool _, + CancellationToken _ + ) => + { + OutgoingMessage? msg = JsonSerializer.Deserialize< + OutgoingMessage + >(buffer.Span, TestUtils.s_jsonSerializerOptions); + if (msg?.D?.RequestType == "CreateSourceFilter") + { + string id = msg.D.RequestId!; + capturedSettings = msg.D.RequestData?.GetProperty("filterSettings"); + _ = TestUtils.SimulateIncomingResponse( + client, + id, + new RequestResponsePayload( + RequestType: "CreateSourceFilter", + RequestId: id, + RequestStatus: new RequestStatus( + Result: true, + Code: (int)Core.Protocol.Generated.RequestStatus.Success + ), + ResponseData: null + ) + ); + } + } + ) + .Returns(ValueTask.CompletedTask); + + await client.Filters.CreateSourceFilterAsync( + sourceName: "VideoSource", filterName: "CustomFilter", filterKind: "custom_filter_kind", settings: settings, - typeInfo: typeInfo); + typeInfo: typeInfo + ); Assert.IsNotNull(capturedSettings); Assert.AreEqual("my-filter", capturedSettings.Value.GetProperty("custom_key").GetString()); From 876f83bf7fd3c471c6d0e87bcce1d98b5486708b Mon Sep 17 00:00:00 2001 From: Agash Date: Wed, 26 Aug 2026 07:34:30 +0200 Subject: [PATCH 07/12] refactor(core): use the typed surfaces inside the helpers 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. --- ObsWebSocket.Core/Groups/ConfigGroup.cs | 29 +++----- ObsWebSocket.Core/Groups/FiltersGroup.cs | 10 +-- ObsWebSocket.Core/Groups/InputsGroup.cs | 73 +++++++++---------- ObsWebSocket.Core/Groups/SceneItemsGroup.cs | 51 +++---------- ObsWebSocket.Core/Groups/SourcesGroup.cs | 10 +-- .../ObsWebSocketRequestException.cs | 15 ++++ ObsWebSocket.Example/Worker.cs | 6 +- 7 files changed, 82 insertions(+), 112 deletions(-) diff --git a/ObsWebSocket.Core/Groups/ConfigGroup.cs b/ObsWebSocket.Core/Groups/ConfigGroup.cs index 220449d..74201ec 100644 --- a/ObsWebSocket.Core/Groups/ConfigGroup.cs +++ b/ObsWebSocket.Core/Groups/ConfigGroup.cs @@ -11,6 +11,7 @@ using ObsWebSocket.Core.Protocol.Generated; using ObsWebSocket.Core.Protocol.Requests; using ObsWebSocket.Core.Protocol.Responses; +using ObsRequestStatus = ObsWebSocket.Core.Protocol.Generated.RequestStatus; namespace ObsWebSocket.Core; @@ -165,15 +166,11 @@ await client .ConfigureAwait(false); return true; // Switch command sent successfully } - catch (ObsWebSocketException ex) - when (ex.Message.Contains("NotFound", StringComparison.OrdinalIgnoreCase) - || // General not found - ex.Message.Contains( - $"code {(int)Core.Protocol.Generated.RequestStatus.ResourceNotFound}:", - StringComparison.Ordinal - ) - || // Specific code - ex.Message.Contains("InvalidParameter", StringComparison.OrdinalIgnoreCase) // Might be InvalidParameter if name doesn't exist + // OBS answers a name it does not know with either status, depending on the request. + catch (ObsWebSocketRequestException ex) + when (ex.StatusCode + is ObsRequestStatus.ResourceNotFound + or ObsRequestStatus.InvalidRequestField ) { client._logger.LogWarning( @@ -227,15 +224,11 @@ await client .ConfigureAwait(false); return true; // Switch command sent successfully } - catch (ObsWebSocketException ex) - when (ex.Message.Contains("NotFound", StringComparison.OrdinalIgnoreCase) - || // General not found - ex.Message.Contains( - $"code {(int)Core.Protocol.Generated.RequestStatus.ResourceNotFound}:", - StringComparison.Ordinal - ) - || // Specific code - ex.Message.Contains("InvalidParameter", StringComparison.OrdinalIgnoreCase) // Might be InvalidParameter if name doesn't exist + // OBS answers a name it does not know with either status, depending on the request. + catch (ObsWebSocketRequestException ex) + when (ex.StatusCode + is ObsRequestStatus.ResourceNotFound + or ObsRequestStatus.InvalidRequestField ) { client._logger.LogWarning( diff --git a/ObsWebSocket.Core/Groups/FiltersGroup.cs b/ObsWebSocket.Core/Groups/FiltersGroup.cs index b6f29ba..29d998d 100644 --- a/ObsWebSocket.Core/Groups/FiltersGroup.cs +++ b/ObsWebSocket.Core/Groups/FiltersGroup.cs @@ -11,6 +11,7 @@ using ObsWebSocket.Core.Protocol.Generated; using ObsWebSocket.Core.Protocol.Requests; using ObsWebSocket.Core.Protocol.Responses; +using ObsRequestStatus = ObsWebSocket.Core.Protocol.Generated.RequestStatus; namespace ObsWebSocket.Core; @@ -54,13 +55,8 @@ public readonly partial struct FiltersGroup ) .ConfigureAwait(false); } - catch (ObsWebSocketException ex) - when (ex.Message.Contains("NotFound", StringComparison.OrdinalIgnoreCase) - || ex.Message.Contains( - $"code {(int)Core.Protocol.Generated.RequestStatus.ResourceNotFound}:", - StringComparison.Ordinal - ) - ) + catch (ObsWebSocketRequestException ex) + when (ex.StatusCode is ObsRequestStatus.ResourceNotFound) { return null; } diff --git a/ObsWebSocket.Core/Groups/InputsGroup.cs b/ObsWebSocket.Core/Groups/InputsGroup.cs index d0650d3..315b8b5 100644 --- a/ObsWebSocket.Core/Groups/InputsGroup.cs +++ b/ObsWebSocket.Core/Groups/InputsGroup.cs @@ -11,6 +11,7 @@ using ObsWebSocket.Core.Protocol.Generated; using ObsWebSocket.Core.Protocol.Requests; using ObsWebSocket.Core.Protocol.Responses; +using ObsRequestStatus = ObsWebSocket.Core.Protocol.Generated.RequestStatus; namespace ObsWebSocket.Core; @@ -49,15 +50,18 @@ await client } /// - /// Sets the mute state for multiple audio inputs using a single batch request. + /// Sets the mute state for several audio inputs in one batch. /// - /// An enumerable of tuples, where each tuple contains the input name (string) and desired mute state (bool: true=muted, false=unmuted). + /// The inputs to change, each with the mute state to apply. /// A token to cancel the operation. - /// A Task representing the completion of the batch request submission. Inspect logs for individual item failures. - /// Thrown if the batch request itself fails (e.g., timeout). + /// + /// One result per input, in the order given, so a caller can see which inputs OBS rejected. + /// The batch does not halt on a failure, so one unknown input does not skip the rest. + /// + /// Thrown if the batch itself fails. /// Thrown if the client is not connected. - /// Thrown if inputMutes is null. - public async Task SetInputMutesAsync( + /// Thrown if is null. + public async Task SetInputMutesAsync( IEnumerable<(string InputName, bool IsMuted)> inputMutes, CancellationToken cancellationToken = default ) @@ -65,49 +69,47 @@ public async Task SetInputMutesAsync( ArgumentNullException.ThrowIfNull(inputMutes); client.EnsureConnected(); - List batchItems = - [ - .. inputMutes.Select(im => new BatchRequestItem( - RequestType: "SetInputMute", - RequestData: new SetInputMuteRequestData( - inputName: im.InputName, - inputMuted: im.IsMuted - ) - )), - ]; + ObsBatchBuilder batch = new(); + List names = []; + foreach ((string inputName, bool isMuted) in inputMutes) + { + _ = batch.Inputs.SetInputMute( + new SetInputMuteRequestData(inputName: inputName, inputMuted: isMuted) + ); + names.Add(inputName); + } - if (batchItems.Count == 0) + if (names.Count == 0) { - client._logger.LogDebug("SetInputMutesAsync called with empty list, nothing to do."); - return; // Nothing to send + client._logger.LogDebug("SetInputMutesAsync called with an empty list, nothing to do."); + return new BatchResults([]); } - // Send batch, don't halt on failure - List> results = await client + BatchResults results = await client .CallBatchAsync( - requests: batchItems, + batch, + // Serial keeps each result paired with the input at the same position. + executionType: RequestBatchExecutionType.SerialRealtime, haltOnFailure: false, - executionType: RequestBatchExecutionType.SerialRealtime, // Appropriate for simple state changes cancellationToken: cancellationToken ) .ConfigureAwait(false); - // Optional: Log failures from results - foreach (RequestResponsePayload result in results) + for (int i = 0; i < results.Count; i++) { + RequestResponsePayload result = results[i]; if (!result.RequestStatus.Result) { - // Attempt to find original input name (requires parsing RequestId or matching RequestData - complex) - // For now, log the failed request type and ID client._logger.LogWarning( - "Failed batch item in SetInputMutesAsync: RequestType={ReqType}, RequestId={ReqId}, Code={Code}, Comment={Comment}", - result.RequestType, - result.RequestId, + "Failed to set mute state for input '{InputName}': code {Code}, {Comment}", + names[i], result.RequestStatus.Code, - result.RequestStatus.Comment ?? "N/A" + result.RequestStatus.Comment ?? "no comment" ); } } + + return results; } /// @@ -142,13 +144,8 @@ public async Task SetInputMutesAsync( ) .ConfigureAwait(false); } - catch (ObsWebSocketException ex) - when (ex.Message.Contains("NotFound", StringComparison.OrdinalIgnoreCase) - || ex.Message.Contains( - $"code {(int)Core.Protocol.Generated.RequestStatus.ResourceNotFound}:", - StringComparison.Ordinal - ) - ) + catch (ObsWebSocketRequestException ex) + when (ex.StatusCode is ObsRequestStatus.ResourceNotFound) { return null; } diff --git a/ObsWebSocket.Core/Groups/SceneItemsGroup.cs b/ObsWebSocket.Core/Groups/SceneItemsGroup.cs index ef044f7..bb0e1c2 100644 --- a/ObsWebSocket.Core/Groups/SceneItemsGroup.cs +++ b/ObsWebSocket.Core/Groups/SceneItemsGroup.cs @@ -11,6 +11,7 @@ using ObsWebSocket.Core.Protocol.Generated; using ObsWebSocket.Core.Protocol.Requests; using ObsWebSocket.Core.Protocol.Responses; +using ObsRequestStatus = ObsWebSocket.Core.Protocol.Generated.RequestStatus; namespace ObsWebSocket.Core; @@ -117,25 +118,6 @@ public async Task SetSceneItemEnabledAsync( ); } - /// - /// Attempts to get the numeric ID of a scene item within a specific scene. - /// Returns null if the scene or source is not found. - /// - /// The name of the scene to search within. - /// The name of the source corresponding to the scene item. - /// A token to cancel the operation. - /// A Task resulting in the nullable scene item ID (double?). Returns null if the item or scene is not found. - /// Thrown for OBS errors other than 'ResourceNotFound'. - /// Thrown if the client is not connected. - [Obsolete( - "Renamed to FindSceneItemIdAsync. Async methods cannot use the out-parameter Try pattern, so the Try prefix was misleading. This forwarder will be removed in a future release." - )] - public Task TryGetSceneItemIdAsync( - string sceneName, - string sourceName, - CancellationToken cancellationToken = default - ) => client.SceneItems.FindSceneItemIdAsync(sceneName, sourceName, cancellationToken); - /// /// Returns the scene item id for a source within a scene, or when the /// scene does not contain it. @@ -145,7 +127,7 @@ public async Task SetSceneItemEnabledAsync( /// A token to cancel the operation. /// The scene item id, or if the source is not in the scene. /// Thrown if the client is not connected. - public async Task FindSceneItemIdAsync( + public async Task FindSceneItemIdAsync( string sceneName, string sourceName, CancellationToken cancellationToken = default @@ -164,18 +146,12 @@ public async Task SetSceneItemEnabledAsync( ) .ConfigureAwait(false); - // If response is not null, return the ID. The underlying GetSceneItemIdAsync - // should guarantee the response isn't null on success. - return response?.SceneItemId; + // Scene item ids are Number on the wire because the protocol has no integer type, + // but OBS only ever assigns whole numbers. + return response is null ? null : checked((int)response.SceneItemId); } - catch (ObsWebSocketException ex) - when (ex.Message.Contains("NotFound", StringComparison.OrdinalIgnoreCase) - || // General not found - ex.Message.Contains( - $"code {(int)Core.Protocol.Generated.RequestStatus.ResourceNotFound}:", - StringComparison.Ordinal - ) // Specific code check - ) + catch (ObsWebSocketRequestException ex) + when (ex.StatusCode is ObsRequestStatus.ResourceNotFound) { // Item or scene not found, which is the expected 'failure' for a 'TryGet' pattern return null; @@ -211,15 +187,12 @@ public Task SetSceneItemEnabledAsync( /// The name of the scene to search. /// The name of the source to locate. /// A token to cancel the operation. - public async Task FindSceneItemIdInt32Async( + [Obsolete( + "FindSceneItemIdAsync now returns int?, so this variant is redundant. This forwarder will be removed in a future release." + )] + public Task FindSceneItemIdInt32Async( string sceneName, string sourceName, CancellationToken cancellationToken = default - ) - { - double? id = await client - .SceneItems.FindSceneItemIdAsync(sceneName, sourceName, cancellationToken) - .ConfigureAwait(false); - return id is null ? null : checked((int)id.Value); - } + ) => FindSceneItemIdAsync(sceneName, sourceName, cancellationToken); } diff --git a/ObsWebSocket.Core/Groups/SourcesGroup.cs b/ObsWebSocket.Core/Groups/SourcesGroup.cs index 8577792..599bafa 100644 --- a/ObsWebSocket.Core/Groups/SourcesGroup.cs +++ b/ObsWebSocket.Core/Groups/SourcesGroup.cs @@ -11,6 +11,7 @@ using ObsWebSocket.Core.Protocol.Generated; using ObsWebSocket.Core.Protocol.Requests; using ObsWebSocket.Core.Protocol.Responses; +using ObsRequestStatus = ObsWebSocket.Core.Protocol.Generated.RequestStatus; namespace ObsWebSocket.Core; @@ -118,13 +119,8 @@ await client ) .ConfigureAwait(false); } - catch (ObsWebSocketException ex) - when (ex.Message.Contains("NotFound", StringComparison.OrdinalIgnoreCase) - || ex.Message.Contains( - $"code {(int)Core.Protocol.Generated.RequestStatus.ResourceNotFound}:", - StringComparison.Ordinal - ) - ) + catch (ObsWebSocketRequestException ex) + when (ex.StatusCode is ObsRequestStatus.ResourceNotFound) { client._logger.LogWarning( "Source '{SourceName}' not found for screenshot.", diff --git a/ObsWebSocket.Core/ObsWebSocketRequestException.cs b/ObsWebSocket.Core/ObsWebSocketRequestException.cs index 145177c..a7030ff 100644 --- a/ObsWebSocket.Core/ObsWebSocketRequestException.cs +++ b/ObsWebSocket.Core/ObsWebSocketRequestException.cs @@ -1,4 +1,5 @@ using ObsWebSocket.Core.Protocol; +using ObsRequestStatus = ObsWebSocket.Core.Protocol.Generated.RequestStatus; namespace ObsWebSocket.Core; @@ -53,6 +54,20 @@ public ObsWebSocketRequestException(string message, Exception innerException) /// The comment OBS attached, if any. public string? Comment { get; } + + /// + /// The status OBS reported as , + /// so a handler can match on the reason rather than on the text of + /// . Note that the enum and the record + /// share the name RequestStatus in different namespaces. + /// + /// + /// + /// catch (ObsWebSocketRequestException ex) + /// when (ex.StatusCode is RequestStatus.ResourceNotFound) { } + /// + /// + public ObsRequestStatus? StatusCode => Status is null ? null : (ObsRequestStatus)Status.Code; } /// diff --git a/ObsWebSocket.Example/Worker.cs b/ObsWebSocket.Example/Worker.cs index b010c6c..ce2094d 100644 --- a/ObsWebSocket.Example/Worker.cs +++ b/ObsWebSocket.Example/Worker.cs @@ -2042,18 +2042,18 @@ await client results.Add( await TrySettingsCheckAsync( - "FindSceneItemIdInt32Async", + "FindSceneItemIdAsync", async () => { int? id = await client - .SceneItems.FindSceneItemIdInt32Async( + .SceneItems.FindSceneItemIdAsync( sceneName, inputName, cancellationToken ) .ConfigureAwait(false); int? miss = await client - .SceneItems.FindSceneItemIdInt32Async( + .SceneItems.FindSceneItemIdAsync( sceneName, "__absent__", cancellationToken From 3ab80166b768a8d47090ec2a88b5a30d1b3706ec Mon Sep 17 00:00:00 2001 From: Agash Date: Wed, 26 Aug 2026 07:36:08 +0200 Subject: [PATCH 08/12] docs: show the typed status filter in the errors section --- ObsWebSocket.Tests/ReadmeCompileCheck.cs | 18 ++++++++++++++++++ README.md | 10 ++++++++++ 2 files changed, 28 insertions(+) diff --git a/ObsWebSocket.Tests/ReadmeCompileCheck.cs b/ObsWebSocket.Tests/ReadmeCompileCheck.cs index 5b12214..15807d8 100644 --- a/ObsWebSocket.Tests/ReadmeCompileCheck.cs +++ b/ObsWebSocket.Tests/ReadmeCompileCheck.cs @@ -276,6 +276,24 @@ internal static async Task TypedErrorsAsync(ObsWebSocketClient client, Cancellat } } + internal static async Task TypedStatusFilterAsync( + ObsWebSocketClient client, + CancellationToken ct + ) + { + try + { + await client.SceneItems.GetSceneItemListAsync(new("Missing"), ct); + } + catch (ObsWebSocketRequestException ex) + when (ex.StatusCode + is ObsWebSocket.Core.Protocol.Generated.RequestStatus.ResourceNotFound + ) + { + // The scene, input or filter does not exist. + } + } + internal static void HostIntegration( Microsoft.Extensions.Hosting.IHostApplicationBuilder builder ) diff --git a/README.md b/README.md index 15c4516..71dfb81 100644 --- a/README.md +++ b/README.md @@ -343,6 +343,16 @@ catch (ObsWebSocketTimeoutException) } ``` +`StatusCode` reports the same status as the protocol's `RequestStatus` enum, so a filter can name +the reason instead of a number: + +```csharp +catch (ObsWebSocketRequestException ex) when (ex.StatusCode is RequestStatus.ResourceNotFound) +{ + // The scene, input or filter does not exist. +} +``` + `ObsWebSocketSerializationException` covers payloads that cannot be written or read, and all three derive from `ObsWebSocketException`. From 896a090181382d14b066fff6531a6cbf27c3d80a Mon Sep 17 00:00:00 2001 From: Agash Date: Wed, 26 Aug 2026 08:15:51 +0200 Subject: [PATCH 09/12] feat(core)!: map whole number protocol fields to int and long 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. --- .../Generation/Diagnostics.cs | 14 +++ .../Generation/Emitter.Helpers.cs | 19 ++- .../Generation/NumericFieldTable.cs | 112 ++++++++++++++++++ ...ransitionDurationChanged.EventPayload.g.cs | 4 +- ...utAudioSyncOffsetChanged.EventPayload.g.cs | 4 +- .../Events/InputCreated.EventPayload.g.cs | 4 +- .../Events/SceneItemCreated.EventPayload.g.cs | 6 +- ...neItemEnableStateChanged.EventPayload.g.cs | 4 +- ...ceneItemLockStateChanged.EventPayload.g.cs | 4 +- .../Events/SceneItemRemoved.EventPayload.g.cs | 4 +- .../SceneItemSelected.EventPayload.g.cs | 4 +- ...ceneItemTransformChanged.EventPayload.g.cs | 4 +- .../SourceFilterCreated.EventPayload.g.cs | 4 +- .../Requests/DuplicateSceneItem.Request.g.cs | 4 +- .../GetSceneItemBlendMode.Request.g.cs | 4 +- .../Requests/GetSceneItemEnabled.Request.g.cs | 4 +- .../Requests/GetSceneItemId.Request.g.cs | 4 +- .../Requests/GetSceneItemIndex.Request.g.cs | 4 +- .../Requests/GetSceneItemLocked.Request.g.cs | 4 +- .../Requests/GetSceneItemSource.Request.g.cs | 4 +- .../GetSceneItemTransform.Request.g.cs | 4 +- .../Requests/GetSourceScreenshot.Request.g.cs | 8 +- .../OffsetMediaInputCursor.Request.g.cs | 4 +- .../Requests/OpenSourceProjector.Request.g.cs | 4 +- .../OpenVideoMixProjector.Request.g.cs | 4 +- .../Requests/RemoveSceneItem.Request.g.cs | 4 +- .../SaveSourceScreenshot.Request.g.cs | 8 +- ...urrentSceneTransitionDuration.Request.g.cs | 4 +- .../SetInputAudioSyncOffset.Request.g.cs | 4 +- .../Requests/SetMediaInputCursor.Request.g.cs | 4 +- .../SetSceneItemBlendMode.Request.g.cs | 4 +- .../Requests/SetSceneItemEnabled.Request.g.cs | 4 +- .../Requests/SetSceneItemIndex.Request.g.cs | 6 +- .../Requests/SetSceneItemLocked.Request.g.cs | 4 +- .../SetSceneItemTransform.Request.g.cs | 4 +- ...tSceneSceneTransitionOverride.Request.g.cs | 4 +- .../SetSourceFilterIndex.Request.g.cs | 4 +- .../Requests/SetTBarPosition.Request.g.cs | 4 +- .../Requests/SetVideoSettings.Request.g.cs | 14 +-- .../Protocol/Requests/Sleep.Request.g.cs | 6 +- .../Responses/CreateInput.Response.g.cs | 4 +- .../Responses/CreateSceneItem.Response.g.cs | 4 +- .../DuplicateSceneItem.Response.g.cs | 4 +- .../GetCurrentSceneTransition.Response.g.cs | 4 +- .../GetInputAudioSyncOffset.Response.g.cs | 4 +- .../GetMediaInputStatus.Response.g.cs | 6 +- .../Responses/GetOutputStatus.Response.g.cs | 10 +- .../Responses/GetRecordStatus.Response.g.cs | 6 +- .../Responses/GetSceneItemId.Response.g.cs | 4 +- .../Responses/GetSceneItemIndex.Response.g.cs | 4 +- ...SceneSceneTransitionOverride.Response.g.cs | 4 +- .../Responses/GetSourceFilter.Response.g.cs | 4 +- .../Protocol/Responses/GetStats.Response.g.cs | 14 +-- .../Responses/GetStreamStatus.Response.g.cs | 10 +- .../Responses/GetVersion.Response.g.cs | 4 +- .../Responses/GetVideoSettings.Response.g.cs | 14 +-- ObsWebSocket.Core/Groups/SceneItemsGroup.cs | 29 +---- ObsWebSocket.Example/Worker.cs | 88 +++++++------- .../ObsWebSocketClientIntegrationTests.cs | 2 +- 59 files changed, 330 insertions(+), 206 deletions(-) create mode 100644 ObsWebSocket.Codegen.Tasks/Generation/NumericFieldTable.cs diff --git a/ObsWebSocket.Codegen.Tasks/Generation/Diagnostics.cs b/ObsWebSocket.Codegen.Tasks/Generation/Diagnostics.cs index cfc6549..e48f96e 100644 --- a/ObsWebSocket.Codegen.Tasks/Generation/Diagnostics.cs +++ b/ObsWebSocket.Codegen.Tasks/Generation/Diagnostics.cs @@ -113,6 +113,20 @@ internal static class Diagnostics isEnabledByDefault: true ); + /// + /// Reported when the protocol defines a Number field the numeric table does not + /// classify. The field still maps to double, which is the safe fallback, but a whole + /// number field left unclassified reaches callers as a floating point value. + /// + public static readonly DiagnosticDescriptor UnclassifiedNumberField = new( + id: "OBSWSGEN012", + title: "Unclassified Number field", + messageFormat: "Number field '{0}' in '{1}' is not listed in NumericFieldTable. Mapping to 'double'. Add it to the table if it holds whole numbers.", + category: Category, + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true + ); + /// /// Informational diagnostic reported when an optional field that is a value type (struct) is generated as a nullable value type. /// diff --git a/ObsWebSocket.Codegen.Tasks/Generation/Emitter.Helpers.cs b/ObsWebSocket.Codegen.Tasks/Generation/Emitter.Helpers.cs index 6e7ccdc..60054c5 100644 --- a/ObsWebSocket.Codegen.Tasks/Generation/Emitter.Helpers.cs +++ b/ObsWebSocket.Codegen.Tasks/Generation/Emitter.Helpers.cs @@ -244,10 +244,27 @@ string parentDtoName } // --- Basic Type Mapping --- + string? numberType = null; + if (obsType == "Number") + { + numberType = NumericFieldTable.MapNumber(fieldName, out bool classified); + if (!classified) + { + context.ReportDiagnostic( + Diagnostic.Create( + Diagnostics.UnclassifiedNumberField, + Location.None, + fieldName, + parentDtoName + ) + ); + } + } + string? mappedType = obsType switch { "String" => "string", - "Number" => "double", + "Number" => numberType, "Boolean" => "bool", "Uuid" => "string", "Object" or "Any" => "System.Text.Json.JsonElement?", // Fallback for unhandled Object/Any diff --git a/ObsWebSocket.Codegen.Tasks/Generation/NumericFieldTable.cs b/ObsWebSocket.Codegen.Tasks/Generation/NumericFieldTable.cs new file mode 100644 index 0000000..f984c79 --- /dev/null +++ b/ObsWebSocket.Codegen.Tasks/Generation/NumericFieldTable.cs @@ -0,0 +1,112 @@ +// ObsWebSocket.Codegen.Tasks/Generation/NumericFieldTable.cs +namespace ObsWebSocket.Codegen.Tasks.Generation; + +/// +/// Decides which of the protocol's Number fields are whole numbers. +/// +/// +/// The protocol has one numeric type because JSON has one numeric type, so sceneItemId and +/// inputVolumeMul are indistinguishable in the definition: both are Number with a +/// >= 0 restriction. This table is written out by hand rather than inferred from the +/// field name, because a rule that guesses wrong on a volume field truncates it silently, while a +/// field missing from the table only stays double, which is what it would have been anyway. +/// A refresh that introduces an unlisted Number field reports OBSWSGEN012 so it gets +/// classified deliberately instead of drifting in. +/// +internal static class NumericFieldTable +{ + /// Whole-number fields whose values fit comfortably in 32 bits. + private static readonly HashSet s_int32Fields = new(StringComparer.Ordinal) + { + // Identity and ordering. + "sceneItemId", + "sceneItemIndex", + "filterIndex", + "monitorIndex", + "position", + "searchOffset", + // Resolutions, in pixels. + "baseWidth", + "baseHeight", + "outputWidth", + "outputHeight", + "imageWidth", + "imageHeight", + "imageCompressionQuality", + // Frame rate, expressed as a fraction. + "fpsNumerator", + "fpsDenominator", + // Durations and offsets that OBS reports in whole milliseconds or frames. + "inputAudioSyncOffset", + "transitionDuration", + "sleepFrames", + "sleepMillis", + // Counters. + "renderSkippedFrames", + "renderTotalFrames", + "outputSkippedFrames", + "outputTotalFrames", + "webSocketSessionIncomingMessages", + "webSocketSessionOutgoingMessages", + // Protocol version. + "rpcVersion", + }; + + /// + /// Whole-number fields that can exceed 32 bits: byte counts, millisecond durations over a long + /// session, and the input capability bitflag, which OBS defines as an unsigned 32 bit mask. + /// + private static readonly HashSet s_int64Fields = new(StringComparer.Ordinal) + { + "outputBytes", + "outputDuration", + "mediaCursor", + "mediaCursorOffset", + "mediaDuration", + "inputKindCaps", + }; + + /// + /// Fields deliberately left fractional, listed so an unclassified field is distinguishable + /// from one that was considered and left alone. + /// + private static readonly HashSet s_doubleFields = new(StringComparer.Ordinal) + { + "inputVolumeMul", + "inputVolumeDb", + "inputAudioBalance", + "transitionCursor", + "outputCongestion", + "cpuUsage", + "memoryUsage", + "availableDiskSpace", + "activeFps", + "averageFrameRenderTime", + }; + + /// + /// Returns the C# type for a protocol Number field. + /// + /// The protocol field name, matched case sensitively. + /// + /// Whether the field appears in the table at all. An unclassified field still maps to + /// double; the flag lets the caller report it. + /// + public static string MapNumber(string fieldName, out bool classified) + { + if (s_int32Fields.Contains(fieldName)) + { + classified = true; + return "int"; + } + + if (s_int64Fields.Contains(fieldName)) + { + classified = true; + return "long"; + } + + classified = s_doubleFields.Contains(fieldName); + return "double"; + } +} diff --git a/ObsWebSocket.Core/Generated/Protocol/Events/CurrentSceneTransitionDurationChanged.EventPayload.g.cs b/ObsWebSocket.Core/Generated/Protocol/Events/CurrentSceneTransitionDurationChanged.EventPayload.g.cs index b31234b..443a405 100644 --- a/ObsWebSocket.Core/Generated/Protocol/Events/CurrentSceneTransitionDurationChanged.EventPayload.g.cs +++ b/ObsWebSocket.Core/Generated/Protocol/Events/CurrentSceneTransitionDurationChanged.EventPayload.g.cs @@ -29,7 +29,7 @@ public sealed partial record CurrentSceneTransitionDurationChangedPayload /// [JsonPropertyName("transitionDuration")] [Key("transitionDuration")] - public required double TransitionDuration { get; init; } + public required int TransitionDuration { get; init; } /// Initializes a new instance for deserialization via . [JsonConstructor] @@ -40,7 +40,7 @@ public CurrentSceneTransitionDurationChangedPayload() { } /// Parameters are ordered with required properties first, then optional properties (with defaults). Follows protocol definition order where possible. /// [System.Diagnostics.CodeAnalysis.SetsRequiredMembers] - public CurrentSceneTransitionDurationChangedPayload(double transitionDuration) + public CurrentSceneTransitionDurationChangedPayload(int transitionDuration) { this.TransitionDuration = transitionDuration; } diff --git a/ObsWebSocket.Core/Generated/Protocol/Events/InputAudioSyncOffsetChanged.EventPayload.g.cs b/ObsWebSocket.Core/Generated/Protocol/Events/InputAudioSyncOffsetChanged.EventPayload.g.cs index 41ccd26..027ebf4 100644 --- a/ObsWebSocket.Core/Generated/Protocol/Events/InputAudioSyncOffsetChanged.EventPayload.g.cs +++ b/ObsWebSocket.Core/Generated/Protocol/Events/InputAudioSyncOffsetChanged.EventPayload.g.cs @@ -29,7 +29,7 @@ public sealed partial record InputAudioSyncOffsetChangedPayload /// [JsonPropertyName("inputAudioSyncOffset")] [Key("inputAudioSyncOffset")] - public required double InputAudioSyncOffset { get; init; } + public required int InputAudioSyncOffset { get; init; } /// /// Name of the input @@ -54,7 +54,7 @@ public InputAudioSyncOffsetChangedPayload() { } /// Parameters are ordered with required properties first, then optional properties (with defaults). Follows protocol definition order where possible. /// [System.Diagnostics.CodeAnalysis.SetsRequiredMembers] - public InputAudioSyncOffsetChangedPayload(double inputAudioSyncOffset, string? inputName = null, string? inputUuid = null) + public InputAudioSyncOffsetChangedPayload(int inputAudioSyncOffset, string? inputName = null, string? inputUuid = null) { this.InputName = inputName; this.InputUuid = inputUuid; diff --git a/ObsWebSocket.Core/Generated/Protocol/Events/InputCreated.EventPayload.g.cs b/ObsWebSocket.Core/Generated/Protocol/Events/InputCreated.EventPayload.g.cs index 1d86762..a57cb9d 100644 --- a/ObsWebSocket.Core/Generated/Protocol/Events/InputCreated.EventPayload.g.cs +++ b/ObsWebSocket.Core/Generated/Protocol/Events/InputCreated.EventPayload.g.cs @@ -43,7 +43,7 @@ public sealed partial record InputCreatedPayload /// [JsonPropertyName("inputKindCaps")] [Key("inputKindCaps")] - public required double InputKindCaps { get; init; } + public required long InputKindCaps { get; init; } /// /// Name of the input @@ -82,7 +82,7 @@ public InputCreatedPayload() { } /// Parameters are ordered with required properties first, then optional properties (with defaults). Follows protocol definition order where possible. /// [System.Diagnostics.CodeAnalysis.SetsRequiredMembers] - public InputCreatedPayload(double inputKindCaps, string? inputName = null, string? inputUuid = null, string? inputKind = null, string? unversionedInputKind = null, System.Text.Json.JsonElement? inputSettings = null, System.Text.Json.JsonElement? defaultInputSettings = null) + public InputCreatedPayload(long inputKindCaps, string? inputName = null, string? inputUuid = null, string? inputKind = null, string? unversionedInputKind = null, System.Text.Json.JsonElement? inputSettings = null, System.Text.Json.JsonElement? defaultInputSettings = null) { this.InputName = inputName; this.InputUuid = inputUuid; diff --git a/ObsWebSocket.Core/Generated/Protocol/Events/SceneItemCreated.EventPayload.g.cs b/ObsWebSocket.Core/Generated/Protocol/Events/SceneItemCreated.EventPayload.g.cs index 025c25e..76dad2f 100644 --- a/ObsWebSocket.Core/Generated/Protocol/Events/SceneItemCreated.EventPayload.g.cs +++ b/ObsWebSocket.Core/Generated/Protocol/Events/SceneItemCreated.EventPayload.g.cs @@ -29,14 +29,14 @@ public sealed partial record SceneItemCreatedPayload /// [JsonPropertyName("sceneItemId")] [Key("sceneItemId")] - public required double SceneItemId { get; init; } + public required int SceneItemId { get; init; } /// /// Index position of the item /// [JsonPropertyName("sceneItemIndex")] [Key("sceneItemIndex")] - public required double SceneItemIndex { get; init; } + public required int SceneItemIndex { get; init; } /// /// Name of the scene the item was added to @@ -75,7 +75,7 @@ public SceneItemCreatedPayload() { } /// Parameters are ordered with required properties first, then optional properties (with defaults). Follows protocol definition order where possible. /// [System.Diagnostics.CodeAnalysis.SetsRequiredMembers] - public SceneItemCreatedPayload(double sceneItemId, double sceneItemIndex, string? sceneName = null, string? sceneUuid = null, string? sourceName = null, string? sourceUuid = null) + public SceneItemCreatedPayload(int sceneItemId, int sceneItemIndex, string? sceneName = null, string? sceneUuid = null, string? sourceName = null, string? sourceUuid = null) { this.SceneName = sceneName; this.SceneUuid = sceneUuid; diff --git a/ObsWebSocket.Core/Generated/Protocol/Events/SceneItemEnableStateChanged.EventPayload.g.cs b/ObsWebSocket.Core/Generated/Protocol/Events/SceneItemEnableStateChanged.EventPayload.g.cs index 86e8a26..48a8e4e 100644 --- a/ObsWebSocket.Core/Generated/Protocol/Events/SceneItemEnableStateChanged.EventPayload.g.cs +++ b/ObsWebSocket.Core/Generated/Protocol/Events/SceneItemEnableStateChanged.EventPayload.g.cs @@ -36,7 +36,7 @@ public sealed partial record SceneItemEnableStateChangedPayload /// [JsonPropertyName("sceneItemId")] [Key("sceneItemId")] - public required double SceneItemId { get; init; } + public required int SceneItemId { get; init; } /// /// Name of the scene the item is in @@ -61,7 +61,7 @@ public SceneItemEnableStateChangedPayload() { } /// Parameters are ordered with required properties first, then optional properties (with defaults). Follows protocol definition order where possible. /// [System.Diagnostics.CodeAnalysis.SetsRequiredMembers] - public SceneItemEnableStateChangedPayload(double sceneItemId, bool sceneItemEnabled, string? sceneName = null, string? sceneUuid = null) + public SceneItemEnableStateChangedPayload(int sceneItemId, bool sceneItemEnabled, string? sceneName = null, string? sceneUuid = null) { this.SceneName = sceneName; this.SceneUuid = sceneUuid; diff --git a/ObsWebSocket.Core/Generated/Protocol/Events/SceneItemLockStateChanged.EventPayload.g.cs b/ObsWebSocket.Core/Generated/Protocol/Events/SceneItemLockStateChanged.EventPayload.g.cs index 7680fb5..6ce7abd 100644 --- a/ObsWebSocket.Core/Generated/Protocol/Events/SceneItemLockStateChanged.EventPayload.g.cs +++ b/ObsWebSocket.Core/Generated/Protocol/Events/SceneItemLockStateChanged.EventPayload.g.cs @@ -29,7 +29,7 @@ public sealed partial record SceneItemLockStateChangedPayload /// [JsonPropertyName("sceneItemId")] [Key("sceneItemId")] - public required double SceneItemId { get; init; } + public required int SceneItemId { get; init; } /// /// Whether the scene item is locked @@ -61,7 +61,7 @@ public SceneItemLockStateChangedPayload() { } /// Parameters are ordered with required properties first, then optional properties (with defaults). Follows protocol definition order where possible. /// [System.Diagnostics.CodeAnalysis.SetsRequiredMembers] - public SceneItemLockStateChangedPayload(double sceneItemId, bool sceneItemLocked, string? sceneName = null, string? sceneUuid = null) + public SceneItemLockStateChangedPayload(int sceneItemId, bool sceneItemLocked, string? sceneName = null, string? sceneUuid = null) { this.SceneName = sceneName; this.SceneUuid = sceneUuid; diff --git a/ObsWebSocket.Core/Generated/Protocol/Events/SceneItemRemoved.EventPayload.g.cs b/ObsWebSocket.Core/Generated/Protocol/Events/SceneItemRemoved.EventPayload.g.cs index cd52e03..4a4293e 100644 --- a/ObsWebSocket.Core/Generated/Protocol/Events/SceneItemRemoved.EventPayload.g.cs +++ b/ObsWebSocket.Core/Generated/Protocol/Events/SceneItemRemoved.EventPayload.g.cs @@ -31,7 +31,7 @@ public sealed partial record SceneItemRemovedPayload /// [JsonPropertyName("sceneItemId")] [Key("sceneItemId")] - public required double SceneItemId { get; init; } + public required int SceneItemId { get; init; } /// /// Name of the scene the item was removed from @@ -70,7 +70,7 @@ public SceneItemRemovedPayload() { } /// Parameters are ordered with required properties first, then optional properties (with defaults). Follows protocol definition order where possible. /// [System.Diagnostics.CodeAnalysis.SetsRequiredMembers] - public SceneItemRemovedPayload(double sceneItemId, string? sceneName = null, string? sceneUuid = null, string? sourceName = null, string? sourceUuid = null) + public SceneItemRemovedPayload(int sceneItemId, string? sceneName = null, string? sceneUuid = null, string? sourceName = null, string? sourceUuid = null) { this.SceneName = sceneName; this.SceneUuid = sceneUuid; diff --git a/ObsWebSocket.Core/Generated/Protocol/Events/SceneItemSelected.EventPayload.g.cs b/ObsWebSocket.Core/Generated/Protocol/Events/SceneItemSelected.EventPayload.g.cs index 84b6077..762d26b 100644 --- a/ObsWebSocket.Core/Generated/Protocol/Events/SceneItemSelected.EventPayload.g.cs +++ b/ObsWebSocket.Core/Generated/Protocol/Events/SceneItemSelected.EventPayload.g.cs @@ -29,7 +29,7 @@ public sealed partial record SceneItemSelectedPayload /// [JsonPropertyName("sceneItemId")] [Key("sceneItemId")] - public required double SceneItemId { get; init; } + public required int SceneItemId { get; init; } /// /// Name of the scene the item is in @@ -54,7 +54,7 @@ public SceneItemSelectedPayload() { } /// Parameters are ordered with required properties first, then optional properties (with defaults). Follows protocol definition order where possible. /// [System.Diagnostics.CodeAnalysis.SetsRequiredMembers] - public SceneItemSelectedPayload(double sceneItemId, string? sceneName = null, string? sceneUuid = null) + public SceneItemSelectedPayload(int sceneItemId, string? sceneName = null, string? sceneUuid = null) { this.SceneName = sceneName; this.SceneUuid = sceneUuid; diff --git a/ObsWebSocket.Core/Generated/Protocol/Events/SceneItemTransformChanged.EventPayload.g.cs b/ObsWebSocket.Core/Generated/Protocol/Events/SceneItemTransformChanged.EventPayload.g.cs index 545617b..bd2c2e0 100644 --- a/ObsWebSocket.Core/Generated/Protocol/Events/SceneItemTransformChanged.EventPayload.g.cs +++ b/ObsWebSocket.Core/Generated/Protocol/Events/SceneItemTransformChanged.EventPayload.g.cs @@ -29,7 +29,7 @@ public sealed partial record SceneItemTransformChangedPayload /// [JsonPropertyName("sceneItemId")] [Key("sceneItemId")] - public required double SceneItemId { get; init; } + public required int SceneItemId { get; init; } /// /// New transform/crop info of the scene item @@ -61,7 +61,7 @@ public SceneItemTransformChangedPayload() { } /// Parameters are ordered with required properties first, then optional properties (with defaults). Follows protocol definition order where possible. /// [System.Diagnostics.CodeAnalysis.SetsRequiredMembers] - public SceneItemTransformChangedPayload(double sceneItemId, string? sceneName = null, string? sceneUuid = null, ObsWebSocket.Core.Protocol.Common.SceneItemTransformStub? sceneItemTransform = null) + public SceneItemTransformChangedPayload(int sceneItemId, string? sceneName = null, string? sceneUuid = null, ObsWebSocket.Core.Protocol.Common.SceneItemTransformStub? sceneItemTransform = null) { this.SceneName = sceneName; this.SceneUuid = sceneUuid; diff --git a/ObsWebSocket.Core/Generated/Protocol/Events/SourceFilterCreated.EventPayload.g.cs b/ObsWebSocket.Core/Generated/Protocol/Events/SourceFilterCreated.EventPayload.g.cs index a4512ca..7632912 100644 --- a/ObsWebSocket.Core/Generated/Protocol/Events/SourceFilterCreated.EventPayload.g.cs +++ b/ObsWebSocket.Core/Generated/Protocol/Events/SourceFilterCreated.EventPayload.g.cs @@ -36,7 +36,7 @@ public sealed partial record SourceFilterCreatedPayload /// [JsonPropertyName("filterIndex")] [Key("filterIndex")] - public required double FilterIndex { get; init; } + public required int FilterIndex { get; init; } /// /// The kind of the filter @@ -75,7 +75,7 @@ public SourceFilterCreatedPayload() { } /// Parameters are ordered with required properties first, then optional properties (with defaults). Follows protocol definition order where possible. /// [System.Diagnostics.CodeAnalysis.SetsRequiredMembers] - public SourceFilterCreatedPayload(double filterIndex, string? sourceName = null, string? filterName = null, string? filterKind = null, System.Text.Json.JsonElement? filterSettings = null, System.Text.Json.JsonElement? defaultFilterSettings = null) + public SourceFilterCreatedPayload(int filterIndex, string? sourceName = null, string? filterName = null, string? filterKind = null, System.Text.Json.JsonElement? filterSettings = null, System.Text.Json.JsonElement? defaultFilterSettings = null) { this.SourceName = sourceName; this.FilterName = filterName; diff --git a/ObsWebSocket.Core/Generated/Protocol/Requests/DuplicateSceneItem.Request.g.cs b/ObsWebSocket.Core/Generated/Protocol/Requests/DuplicateSceneItem.Request.g.cs index b370bb9..a9f3e2e 100644 --- a/ObsWebSocket.Core/Generated/Protocol/Requests/DuplicateSceneItem.Request.g.cs +++ b/ObsWebSocket.Core/Generated/Protocol/Requests/DuplicateSceneItem.Request.g.cs @@ -68,7 +68,7 @@ public sealed partial record DuplicateSceneItemRequestData /// [JsonPropertyName("sceneItemId")] [Key("sceneItemId")] - public required double SceneItemId { get; init; } + public required int SceneItemId { get; init; } /// /// Name of the scene the item is in @@ -101,7 +101,7 @@ public DuplicateSceneItemRequestData() { } /// Parameters are ordered with required properties first, then optional properties (with defaults). Follows protocol definition order where possible. /// [System.Diagnostics.CodeAnalysis.SetsRequiredMembers] - public DuplicateSceneItemRequestData(double sceneItemId, string? canvasUuid = null, string? sceneName = null, string? sceneUuid = null, string? destinationSceneName = null, string? destinationSceneUuid = null) + public DuplicateSceneItemRequestData(int sceneItemId, string? canvasUuid = null, string? sceneName = null, string? sceneUuid = null, string? destinationSceneName = null, string? destinationSceneUuid = null) { this.CanvasUuid = canvasUuid; this.SceneName = sceneName; diff --git a/ObsWebSocket.Core/Generated/Protocol/Requests/GetSceneItemBlendMode.Request.g.cs b/ObsWebSocket.Core/Generated/Protocol/Requests/GetSceneItemBlendMode.Request.g.cs index 4959de8..bc37f79 100644 --- a/ObsWebSocket.Core/Generated/Protocol/Requests/GetSceneItemBlendMode.Request.g.cs +++ b/ObsWebSocket.Core/Generated/Protocol/Requests/GetSceneItemBlendMode.Request.g.cs @@ -56,7 +56,7 @@ public sealed partial record GetSceneItemBlendModeRequestData /// [JsonPropertyName("sceneItemId")] [Key("sceneItemId")] - public required double SceneItemId { get; init; } + public required int SceneItemId { get; init; } /// /// Name of the scene the item is in @@ -89,7 +89,7 @@ public GetSceneItemBlendModeRequestData() { } /// Parameters are ordered with required properties first, then optional properties (with defaults). Follows protocol definition order where possible. /// [System.Diagnostics.CodeAnalysis.SetsRequiredMembers] - public GetSceneItemBlendModeRequestData(double sceneItemId, string? canvasUuid = null, string? sceneName = null, string? sceneUuid = null) + public GetSceneItemBlendModeRequestData(int sceneItemId, string? canvasUuid = null, string? sceneName = null, string? sceneUuid = null) { this.CanvasUuid = canvasUuid; this.SceneName = sceneName; diff --git a/ObsWebSocket.Core/Generated/Protocol/Requests/GetSceneItemEnabled.Request.g.cs b/ObsWebSocket.Core/Generated/Protocol/Requests/GetSceneItemEnabled.Request.g.cs index 8d11dff..7308cf6 100644 --- a/ObsWebSocket.Core/Generated/Protocol/Requests/GetSceneItemEnabled.Request.g.cs +++ b/ObsWebSocket.Core/Generated/Protocol/Requests/GetSceneItemEnabled.Request.g.cs @@ -46,7 +46,7 @@ public sealed partial record GetSceneItemEnabledRequestData /// [JsonPropertyName("sceneItemId")] [Key("sceneItemId")] - public required double SceneItemId { get; init; } + public required int SceneItemId { get; init; } /// /// Name of the scene the item is in @@ -79,7 +79,7 @@ public GetSceneItemEnabledRequestData() { } /// Parameters are ordered with required properties first, then optional properties (with defaults). Follows protocol definition order where possible. /// [System.Diagnostics.CodeAnalysis.SetsRequiredMembers] - public GetSceneItemEnabledRequestData(double sceneItemId, string? canvasUuid = null, string? sceneName = null, string? sceneUuid = null) + public GetSceneItemEnabledRequestData(int sceneItemId, string? canvasUuid = null, string? sceneName = null, string? sceneUuid = null) { this.CanvasUuid = canvasUuid; this.SceneName = sceneName; diff --git a/ObsWebSocket.Core/Generated/Protocol/Requests/GetSceneItemId.Request.g.cs b/ObsWebSocket.Core/Generated/Protocol/Requests/GetSceneItemId.Request.g.cs index 70659e4..0b1b90f 100644 --- a/ObsWebSocket.Core/Generated/Protocol/Requests/GetSceneItemId.Request.g.cs +++ b/ObsWebSocket.Core/Generated/Protocol/Requests/GetSceneItemId.Request.g.cs @@ -69,7 +69,7 @@ public sealed partial record GetSceneItemIdRequestData /// [JsonPropertyName("searchOffset")] [Key("searchOffset")] - public double? SearchOffset { get; init; } + public int? SearchOffset { get; init; } /// /// Name of the source to find @@ -90,7 +90,7 @@ public GetSceneItemIdRequestData() { } /// Parameters are ordered with required properties first, then optional properties (with defaults). Follows protocol definition order where possible. /// [System.Diagnostics.CodeAnalysis.SetsRequiredMembers] - public GetSceneItemIdRequestData(string sourceName, string? canvasUuid = null, string? sceneName = null, string? sceneUuid = null, double? searchOffset = null) + public GetSceneItemIdRequestData(string sourceName, string? canvasUuid = null, string? sceneName = null, string? sceneUuid = null, int? searchOffset = null) { this.CanvasUuid = canvasUuid; this.SceneName = sceneName; diff --git a/ObsWebSocket.Core/Generated/Protocol/Requests/GetSceneItemIndex.Request.g.cs b/ObsWebSocket.Core/Generated/Protocol/Requests/GetSceneItemIndex.Request.g.cs index bfe0227..c4b96b7 100644 --- a/ObsWebSocket.Core/Generated/Protocol/Requests/GetSceneItemIndex.Request.g.cs +++ b/ObsWebSocket.Core/Generated/Protocol/Requests/GetSceneItemIndex.Request.g.cs @@ -48,7 +48,7 @@ public sealed partial record GetSceneItemIndexRequestData /// [JsonPropertyName("sceneItemId")] [Key("sceneItemId")] - public required double SceneItemId { get; init; } + public required int SceneItemId { get; init; } /// /// Name of the scene the item is in @@ -81,7 +81,7 @@ public GetSceneItemIndexRequestData() { } /// Parameters are ordered with required properties first, then optional properties (with defaults). Follows protocol definition order where possible. /// [System.Diagnostics.CodeAnalysis.SetsRequiredMembers] - public GetSceneItemIndexRequestData(double sceneItemId, string? canvasUuid = null, string? sceneName = null, string? sceneUuid = null) + public GetSceneItemIndexRequestData(int sceneItemId, string? canvasUuid = null, string? sceneName = null, string? sceneUuid = null) { this.CanvasUuid = canvasUuid; this.SceneName = sceneName; diff --git a/ObsWebSocket.Core/Generated/Protocol/Requests/GetSceneItemLocked.Request.g.cs b/ObsWebSocket.Core/Generated/Protocol/Requests/GetSceneItemLocked.Request.g.cs index c06fe7d..6afb955 100644 --- a/ObsWebSocket.Core/Generated/Protocol/Requests/GetSceneItemLocked.Request.g.cs +++ b/ObsWebSocket.Core/Generated/Protocol/Requests/GetSceneItemLocked.Request.g.cs @@ -46,7 +46,7 @@ public sealed partial record GetSceneItemLockedRequestData /// [JsonPropertyName("sceneItemId")] [Key("sceneItemId")] - public required double SceneItemId { get; init; } + public required int SceneItemId { get; init; } /// /// Name of the scene the item is in @@ -79,7 +79,7 @@ public GetSceneItemLockedRequestData() { } /// Parameters are ordered with required properties first, then optional properties (with defaults). Follows protocol definition order where possible. /// [System.Diagnostics.CodeAnalysis.SetsRequiredMembers] - public GetSceneItemLockedRequestData(double sceneItemId, string? canvasUuid = null, string? sceneName = null, string? sceneUuid = null) + public GetSceneItemLockedRequestData(int sceneItemId, string? canvasUuid = null, string? sceneName = null, string? sceneUuid = null) { this.CanvasUuid = canvasUuid; this.SceneName = sceneName; diff --git a/ObsWebSocket.Core/Generated/Protocol/Requests/GetSceneItemSource.Request.g.cs b/ObsWebSocket.Core/Generated/Protocol/Requests/GetSceneItemSource.Request.g.cs index 809bc14..34415b4 100644 --- a/ObsWebSocket.Core/Generated/Protocol/Requests/GetSceneItemSource.Request.g.cs +++ b/ObsWebSocket.Core/Generated/Protocol/Requests/GetSceneItemSource.Request.g.cs @@ -44,7 +44,7 @@ public sealed partial record GetSceneItemSourceRequestData /// [JsonPropertyName("sceneItemId")] [Key("sceneItemId")] - public required double SceneItemId { get; init; } + public required int SceneItemId { get; init; } /// /// Name of the scene the item is in @@ -77,7 +77,7 @@ public GetSceneItemSourceRequestData() { } /// Parameters are ordered with required properties first, then optional properties (with defaults). Follows protocol definition order where possible. /// [System.Diagnostics.CodeAnalysis.SetsRequiredMembers] - public GetSceneItemSourceRequestData(double sceneItemId, string? canvasUuid = null, string? sceneName = null, string? sceneUuid = null) + public GetSceneItemSourceRequestData(int sceneItemId, string? canvasUuid = null, string? sceneName = null, string? sceneUuid = null) { this.CanvasUuid = canvasUuid; this.SceneName = sceneName; diff --git a/ObsWebSocket.Core/Generated/Protocol/Requests/GetSceneItemTransform.Request.g.cs b/ObsWebSocket.Core/Generated/Protocol/Requests/GetSceneItemTransform.Request.g.cs index fc811e2..af05154 100644 --- a/ObsWebSocket.Core/Generated/Protocol/Requests/GetSceneItemTransform.Request.g.cs +++ b/ObsWebSocket.Core/Generated/Protocol/Requests/GetSceneItemTransform.Request.g.cs @@ -46,7 +46,7 @@ public sealed partial record GetSceneItemTransformRequestData /// [JsonPropertyName("sceneItemId")] [Key("sceneItemId")] - public required double SceneItemId { get; init; } + public required int SceneItemId { get; init; } /// /// Name of the scene the item is in @@ -79,7 +79,7 @@ public GetSceneItemTransformRequestData() { } /// Parameters are ordered with required properties first, then optional properties (with defaults). Follows protocol definition order where possible. /// [System.Diagnostics.CodeAnalysis.SetsRequiredMembers] - public GetSceneItemTransformRequestData(double sceneItemId, string? canvasUuid = null, string? sceneName = null, string? sceneUuid = null) + public GetSceneItemTransformRequestData(int sceneItemId, string? canvasUuid = null, string? sceneName = null, string? sceneUuid = null) { this.CanvasUuid = canvasUuid; this.SceneName = sceneName; diff --git a/ObsWebSocket.Core/Generated/Protocol/Requests/GetSourceScreenshot.Request.g.cs b/ObsWebSocket.Core/Generated/Protocol/Requests/GetSourceScreenshot.Request.g.cs index 2ba81c5..5160ad6 100644 --- a/ObsWebSocket.Core/Generated/Protocol/Requests/GetSourceScreenshot.Request.g.cs +++ b/ObsWebSocket.Core/Generated/Protocol/Requests/GetSourceScreenshot.Request.g.cs @@ -50,7 +50,7 @@ public sealed partial record GetSourceScreenshotRequestData /// [JsonPropertyName("imageCompressionQuality")] [Key("imageCompressionQuality")] - public double? ImageCompressionQuality { get; init; } + public int? ImageCompressionQuality { get; init; } /// /// Image compression format to use. Use `GetVersion` to get compatible image formats @@ -72,7 +72,7 @@ public sealed partial record GetSourceScreenshotRequestData /// [JsonPropertyName("imageHeight")] [Key("imageHeight")] - public double? ImageHeight { get; init; } + public int? ImageHeight { get; init; } /// /// Width to scale the screenshot to @@ -84,7 +84,7 @@ public sealed partial record GetSourceScreenshotRequestData /// [JsonPropertyName("imageWidth")] [Key("imageWidth")] - public double? ImageWidth { get; init; } + public int? ImageWidth { get; init; } /// /// Name of the source to take a screenshot of @@ -117,7 +117,7 @@ public GetSourceScreenshotRequestData() { } /// Parameters are ordered with required properties first, then optional properties (with defaults). Follows protocol definition order where possible. /// [System.Diagnostics.CodeAnalysis.SetsRequiredMembers] - public GetSourceScreenshotRequestData(string imageFormat, string? canvasUuid = null, string? sourceName = null, string? sourceUuid = null, double? imageWidth = null, double? imageHeight = null, double? imageCompressionQuality = null) + public GetSourceScreenshotRequestData(string imageFormat, string? canvasUuid = null, string? sourceName = null, string? sourceUuid = null, int? imageWidth = null, int? imageHeight = null, int? imageCompressionQuality = null) { this.CanvasUuid = canvasUuid; this.SourceName = sourceName; diff --git a/ObsWebSocket.Core/Generated/Protocol/Requests/OffsetMediaInputCursor.Request.g.cs b/ObsWebSocket.Core/Generated/Protocol/Requests/OffsetMediaInputCursor.Request.g.cs index e000f03..9cb4b13 100644 --- a/ObsWebSocket.Core/Generated/Protocol/Requests/OffsetMediaInputCursor.Request.g.cs +++ b/ObsWebSocket.Core/Generated/Protocol/Requests/OffsetMediaInputCursor.Request.g.cs @@ -56,7 +56,7 @@ public sealed partial record OffsetMediaInputCursorRequestData /// [JsonPropertyName("mediaCursorOffset")] [Key("mediaCursorOffset")] - public required double MediaCursorOffset { get; init; } + public required long MediaCursorOffset { get; init; } /// Initializes a new instance for deserialization via . [JsonConstructor] @@ -67,7 +67,7 @@ public OffsetMediaInputCursorRequestData() { } /// Parameters are ordered with required properties first, then optional properties (with defaults). Follows protocol definition order where possible. /// [System.Diagnostics.CodeAnalysis.SetsRequiredMembers] - public OffsetMediaInputCursorRequestData(double mediaCursorOffset, string? inputName = null, string? inputUuid = null) + public OffsetMediaInputCursorRequestData(long mediaCursorOffset, string? inputName = null, string? inputUuid = null) { this.InputName = inputName; this.InputUuid = inputUuid; diff --git a/ObsWebSocket.Core/Generated/Protocol/Requests/OpenSourceProjector.Request.g.cs b/ObsWebSocket.Core/Generated/Protocol/Requests/OpenSourceProjector.Request.g.cs index 1d4aa57..248b87c 100644 --- a/ObsWebSocket.Core/Generated/Protocol/Requests/OpenSourceProjector.Request.g.cs +++ b/ObsWebSocket.Core/Generated/Protocol/Requests/OpenSourceProjector.Request.g.cs @@ -46,7 +46,7 @@ public sealed partial record OpenSourceProjectorRequestData /// [JsonPropertyName("monitorIndex")] [Key("monitorIndex")] - public double? MonitorIndex { get; init; } + public int? MonitorIndex { get; init; } /// /// Size/Position data for a windowed projector, in Qt Base64 encoded format. Mutually exclusive with `monitorIndex` @@ -89,7 +89,7 @@ public OpenSourceProjectorRequestData() { } /// Initializes a new instance with all properties specified. /// Parameters are ordered with required properties first, then optional properties (with defaults). Follows protocol definition order where possible. /// - public OpenSourceProjectorRequestData(string? canvasUuid = null, string? sourceName = null, string? sourceUuid = null, double? monitorIndex = null, string? projectorGeometry = null) + public OpenSourceProjectorRequestData(string? canvasUuid = null, string? sourceName = null, string? sourceUuid = null, int? monitorIndex = null, string? projectorGeometry = null) { this.CanvasUuid = canvasUuid; this.SourceName = sourceName; diff --git a/ObsWebSocket.Core/Generated/Protocol/Requests/OpenVideoMixProjector.Request.g.cs b/ObsWebSocket.Core/Generated/Protocol/Requests/OpenVideoMixProjector.Request.g.cs index d0d7277..aebe232 100644 --- a/ObsWebSocket.Core/Generated/Protocol/Requests/OpenVideoMixProjector.Request.g.cs +++ b/ObsWebSocket.Core/Generated/Protocol/Requests/OpenVideoMixProjector.Request.g.cs @@ -41,7 +41,7 @@ public sealed partial record OpenVideoMixProjectorRequestData /// [JsonPropertyName("monitorIndex")] [Key("monitorIndex")] - public double? MonitorIndex { get; init; } + public int? MonitorIndex { get; init; } /// /// Size/Position data for a windowed projector, in Qt Base64 encoded format. Mutually exclusive with `monitorIndex` @@ -73,7 +73,7 @@ public OpenVideoMixProjectorRequestData() { } /// Parameters are ordered with required properties first, then optional properties (with defaults). Follows protocol definition order where possible. /// [System.Diagnostics.CodeAnalysis.SetsRequiredMembers] - public OpenVideoMixProjectorRequestData(string videoMixType, double? monitorIndex = null, string? projectorGeometry = null) + public OpenVideoMixProjectorRequestData(string videoMixType, int? monitorIndex = null, string? projectorGeometry = null) { this.VideoMixType = videoMixType; this.MonitorIndex = monitorIndex; diff --git a/ObsWebSocket.Core/Generated/Protocol/Requests/RemoveSceneItem.Request.g.cs b/ObsWebSocket.Core/Generated/Protocol/Requests/RemoveSceneItem.Request.g.cs index bbdc107..7a3b6ae 100644 --- a/ObsWebSocket.Core/Generated/Protocol/Requests/RemoveSceneItem.Request.g.cs +++ b/ObsWebSocket.Core/Generated/Protocol/Requests/RemoveSceneItem.Request.g.cs @@ -46,7 +46,7 @@ public sealed partial record RemoveSceneItemRequestData /// [JsonPropertyName("sceneItemId")] [Key("sceneItemId")] - public required double SceneItemId { get; init; } + public required int SceneItemId { get; init; } /// /// Name of the scene the item is in @@ -79,7 +79,7 @@ public RemoveSceneItemRequestData() { } /// Parameters are ordered with required properties first, then optional properties (with defaults). Follows protocol definition order where possible. /// [System.Diagnostics.CodeAnalysis.SetsRequiredMembers] - public RemoveSceneItemRequestData(double sceneItemId, string? canvasUuid = null, string? sceneName = null, string? sceneUuid = null) + public RemoveSceneItemRequestData(int sceneItemId, string? canvasUuid = null, string? sceneName = null, string? sceneUuid = null) { this.CanvasUuid = canvasUuid; this.SceneName = sceneName; diff --git a/ObsWebSocket.Core/Generated/Protocol/Requests/SaveSourceScreenshot.Request.g.cs b/ObsWebSocket.Core/Generated/Protocol/Requests/SaveSourceScreenshot.Request.g.cs index 307bb32..a559bd4 100644 --- a/ObsWebSocket.Core/Generated/Protocol/Requests/SaveSourceScreenshot.Request.g.cs +++ b/ObsWebSocket.Core/Generated/Protocol/Requests/SaveSourceScreenshot.Request.g.cs @@ -50,7 +50,7 @@ public sealed partial record SaveSourceScreenshotRequestData /// [JsonPropertyName("imageCompressionQuality")] [Key("imageCompressionQuality")] - public double? ImageCompressionQuality { get; init; } + public int? ImageCompressionQuality { get; init; } /// /// Path to save the screenshot file to. Eg. `C:\Users\user\Desktop\screenshot.png` @@ -82,7 +82,7 @@ public sealed partial record SaveSourceScreenshotRequestData /// [JsonPropertyName("imageHeight")] [Key("imageHeight")] - public double? ImageHeight { get; init; } + public int? ImageHeight { get; init; } /// /// Width to scale the screenshot to @@ -94,7 +94,7 @@ public sealed partial record SaveSourceScreenshotRequestData /// [JsonPropertyName("imageWidth")] [Key("imageWidth")] - public double? ImageWidth { get; init; } + public int? ImageWidth { get; init; } /// /// Name of the source to take a screenshot of @@ -127,7 +127,7 @@ public SaveSourceScreenshotRequestData() { } /// Parameters are ordered with required properties first, then optional properties (with defaults). Follows protocol definition order where possible. /// [System.Diagnostics.CodeAnalysis.SetsRequiredMembers] - public SaveSourceScreenshotRequestData(string imageFormat, string imageFilePath, string? canvasUuid = null, string? sourceName = null, string? sourceUuid = null, double? imageWidth = null, double? imageHeight = null, double? imageCompressionQuality = null) + public SaveSourceScreenshotRequestData(string imageFormat, string imageFilePath, string? canvasUuid = null, string? sourceName = null, string? sourceUuid = null, int? imageWidth = null, int? imageHeight = null, int? imageCompressionQuality = null) { this.CanvasUuid = canvasUuid; this.SourceName = sourceName; diff --git a/ObsWebSocket.Core/Generated/Protocol/Requests/SetCurrentSceneTransitionDuration.Request.g.cs b/ObsWebSocket.Core/Generated/Protocol/Requests/SetCurrentSceneTransitionDuration.Request.g.cs index 6af8b2b..6236fe9 100644 --- a/ObsWebSocket.Core/Generated/Protocol/Requests/SetCurrentSceneTransitionDuration.Request.g.cs +++ b/ObsWebSocket.Core/Generated/Protocol/Requests/SetCurrentSceneTransitionDuration.Request.g.cs @@ -33,7 +33,7 @@ public sealed partial record SetCurrentSceneTransitionDurationRequestData /// [JsonPropertyName("transitionDuration")] [Key("transitionDuration")] - public required double TransitionDuration { get; init; } + public required int TransitionDuration { get; init; } /// Initializes a new instance for deserialization via . [JsonConstructor] @@ -44,7 +44,7 @@ public SetCurrentSceneTransitionDurationRequestData() { } /// Parameters are ordered with required properties first, then optional properties (with defaults). Follows protocol definition order where possible. /// [System.Diagnostics.CodeAnalysis.SetsRequiredMembers] - public SetCurrentSceneTransitionDurationRequestData(double transitionDuration) + public SetCurrentSceneTransitionDurationRequestData(int transitionDuration) { this.TransitionDuration = transitionDuration; } diff --git a/ObsWebSocket.Core/Generated/Protocol/Requests/SetInputAudioSyncOffset.Request.g.cs b/ObsWebSocket.Core/Generated/Protocol/Requests/SetInputAudioSyncOffset.Request.g.cs index 95dbf74..f8038e1 100644 --- a/ObsWebSocket.Core/Generated/Protocol/Requests/SetInputAudioSyncOffset.Request.g.cs +++ b/ObsWebSocket.Core/Generated/Protocol/Requests/SetInputAudioSyncOffset.Request.g.cs @@ -33,7 +33,7 @@ public sealed partial record SetInputAudioSyncOffsetRequestData /// [JsonPropertyName("inputAudioSyncOffset")] [Key("inputAudioSyncOffset")] - public required double InputAudioSyncOffset { get; init; } + public required int InputAudioSyncOffset { get; init; } /// /// Name of the input to set the audio sync offset of @@ -66,7 +66,7 @@ public SetInputAudioSyncOffsetRequestData() { } /// Parameters are ordered with required properties first, then optional properties (with defaults). Follows protocol definition order where possible. /// [System.Diagnostics.CodeAnalysis.SetsRequiredMembers] - public SetInputAudioSyncOffsetRequestData(double inputAudioSyncOffset, string? inputName = null, string? inputUuid = null) + public SetInputAudioSyncOffsetRequestData(int inputAudioSyncOffset, string? inputName = null, string? inputUuid = null) { this.InputName = inputName; this.InputUuid = inputUuid; diff --git a/ObsWebSocket.Core/Generated/Protocol/Requests/SetMediaInputCursor.Request.g.cs b/ObsWebSocket.Core/Generated/Protocol/Requests/SetMediaInputCursor.Request.g.cs index 35adc7f..58a3d4d 100644 --- a/ObsWebSocket.Core/Generated/Protocol/Requests/SetMediaInputCursor.Request.g.cs +++ b/ObsWebSocket.Core/Generated/Protocol/Requests/SetMediaInputCursor.Request.g.cs @@ -57,7 +57,7 @@ public sealed partial record SetMediaInputCursorRequestData /// [JsonPropertyName("mediaCursor")] [Key("mediaCursor")] - public required double MediaCursor { get; init; } + public required long MediaCursor { get; init; } /// Initializes a new instance for deserialization via . [JsonConstructor] @@ -68,7 +68,7 @@ public SetMediaInputCursorRequestData() { } /// Parameters are ordered with required properties first, then optional properties (with defaults). Follows protocol definition order where possible. /// [System.Diagnostics.CodeAnalysis.SetsRequiredMembers] - public SetMediaInputCursorRequestData(double mediaCursor, string? inputName = null, string? inputUuid = null) + public SetMediaInputCursorRequestData(long mediaCursor, string? inputName = null, string? inputUuid = null) { this.InputName = inputName; this.InputUuid = inputUuid; diff --git a/ObsWebSocket.Core/Generated/Protocol/Requests/SetSceneItemBlendMode.Request.g.cs b/ObsWebSocket.Core/Generated/Protocol/Requests/SetSceneItemBlendMode.Request.g.cs index ee4c5e9..2f83328 100644 --- a/ObsWebSocket.Core/Generated/Protocol/Requests/SetSceneItemBlendMode.Request.g.cs +++ b/ObsWebSocket.Core/Generated/Protocol/Requests/SetSceneItemBlendMode.Request.g.cs @@ -56,7 +56,7 @@ public sealed partial record SetSceneItemBlendModeRequestData /// [JsonPropertyName("sceneItemId")] [Key("sceneItemId")] - public required double SceneItemId { get; init; } + public required int SceneItemId { get; init; } /// /// Name of the scene the item is in @@ -89,7 +89,7 @@ public SetSceneItemBlendModeRequestData() { } /// Parameters are ordered with required properties first, then optional properties (with defaults). Follows protocol definition order where possible. /// [System.Diagnostics.CodeAnalysis.SetsRequiredMembers] - public SetSceneItemBlendModeRequestData(double sceneItemId, string sceneItemBlendMode, string? canvasUuid = null, string? sceneName = null, string? sceneUuid = null) + public SetSceneItemBlendModeRequestData(int sceneItemId, string sceneItemBlendMode, string? canvasUuid = null, string? sceneName = null, string? sceneUuid = null) { this.CanvasUuid = canvasUuid; this.SceneName = sceneName; diff --git a/ObsWebSocket.Core/Generated/Protocol/Requests/SetSceneItemEnabled.Request.g.cs b/ObsWebSocket.Core/Generated/Protocol/Requests/SetSceneItemEnabled.Request.g.cs index d09803e..4f488fc 100644 --- a/ObsWebSocket.Core/Generated/Protocol/Requests/SetSceneItemEnabled.Request.g.cs +++ b/ObsWebSocket.Core/Generated/Protocol/Requests/SetSceneItemEnabled.Request.g.cs @@ -56,7 +56,7 @@ public sealed partial record SetSceneItemEnabledRequestData /// [JsonPropertyName("sceneItemId")] [Key("sceneItemId")] - public required double SceneItemId { get; init; } + public required int SceneItemId { get; init; } /// /// Name of the scene the item is in @@ -89,7 +89,7 @@ public SetSceneItemEnabledRequestData() { } /// Parameters are ordered with required properties first, then optional properties (with defaults). Follows protocol definition order where possible. /// [System.Diagnostics.CodeAnalysis.SetsRequiredMembers] - public SetSceneItemEnabledRequestData(double sceneItemId, bool sceneItemEnabled, string? canvasUuid = null, string? sceneName = null, string? sceneUuid = null) + public SetSceneItemEnabledRequestData(int sceneItemId, bool sceneItemEnabled, string? canvasUuid = null, string? sceneName = null, string? sceneUuid = null) { this.CanvasUuid = canvasUuid; this.SceneName = sceneName; diff --git a/ObsWebSocket.Core/Generated/Protocol/Requests/SetSceneItemIndex.Request.g.cs b/ObsWebSocket.Core/Generated/Protocol/Requests/SetSceneItemIndex.Request.g.cs index 4eaf6b6..9b52d13 100644 --- a/ObsWebSocket.Core/Generated/Protocol/Requests/SetSceneItemIndex.Request.g.cs +++ b/ObsWebSocket.Core/Generated/Protocol/Requests/SetSceneItemIndex.Request.g.cs @@ -46,7 +46,7 @@ public sealed partial record SetSceneItemIndexRequestData /// [JsonPropertyName("sceneItemId")] [Key("sceneItemId")] - public required double SceneItemId { get; init; } + public required int SceneItemId { get; init; } /// /// New index position of the scene item @@ -57,7 +57,7 @@ public sealed partial record SetSceneItemIndexRequestData /// [JsonPropertyName("sceneItemIndex")] [Key("sceneItemIndex")] - public required double SceneItemIndex { get; init; } + public required int SceneItemIndex { get; init; } /// /// Name of the scene the item is in @@ -90,7 +90,7 @@ public SetSceneItemIndexRequestData() { } /// Parameters are ordered with required properties first, then optional properties (with defaults). Follows protocol definition order where possible. /// [System.Diagnostics.CodeAnalysis.SetsRequiredMembers] - public SetSceneItemIndexRequestData(double sceneItemId, double sceneItemIndex, string? canvasUuid = null, string? sceneName = null, string? sceneUuid = null) + public SetSceneItemIndexRequestData(int sceneItemId, int sceneItemIndex, string? canvasUuid = null, string? sceneName = null, string? sceneUuid = null) { this.CanvasUuid = canvasUuid; this.SceneName = sceneName; diff --git a/ObsWebSocket.Core/Generated/Protocol/Requests/SetSceneItemLocked.Request.g.cs b/ObsWebSocket.Core/Generated/Protocol/Requests/SetSceneItemLocked.Request.g.cs index aef6429..540613b 100644 --- a/ObsWebSocket.Core/Generated/Protocol/Requests/SetSceneItemLocked.Request.g.cs +++ b/ObsWebSocket.Core/Generated/Protocol/Requests/SetSceneItemLocked.Request.g.cs @@ -46,7 +46,7 @@ public sealed partial record SetSceneItemLockedRequestData /// [JsonPropertyName("sceneItemId")] [Key("sceneItemId")] - public required double SceneItemId { get; init; } + public required int SceneItemId { get; init; } /// /// New lock state of the scene item @@ -89,7 +89,7 @@ public SetSceneItemLockedRequestData() { } /// Parameters are ordered with required properties first, then optional properties (with defaults). Follows protocol definition order where possible. /// [System.Diagnostics.CodeAnalysis.SetsRequiredMembers] - public SetSceneItemLockedRequestData(double sceneItemId, bool sceneItemLocked, string? canvasUuid = null, string? sceneName = null, string? sceneUuid = null) + public SetSceneItemLockedRequestData(int sceneItemId, bool sceneItemLocked, string? canvasUuid = null, string? sceneName = null, string? sceneUuid = null) { this.CanvasUuid = canvasUuid; this.SceneName = sceneName; diff --git a/ObsWebSocket.Core/Generated/Protocol/Requests/SetSceneItemTransform.Request.g.cs b/ObsWebSocket.Core/Generated/Protocol/Requests/SetSceneItemTransform.Request.g.cs index bd9f746..af872c0 100644 --- a/ObsWebSocket.Core/Generated/Protocol/Requests/SetSceneItemTransform.Request.g.cs +++ b/ObsWebSocket.Core/Generated/Protocol/Requests/SetSceneItemTransform.Request.g.cs @@ -44,7 +44,7 @@ public sealed partial record SetSceneItemTransformRequestData /// [JsonPropertyName("sceneItemId")] [Key("sceneItemId")] - public required double SceneItemId { get; init; } + public required int SceneItemId { get; init; } /// /// Object containing scene item transform info to update @@ -87,7 +87,7 @@ public SetSceneItemTransformRequestData() { } /// Parameters are ordered with required properties first, then optional properties (with defaults). Follows protocol definition order where possible. /// [System.Diagnostics.CodeAnalysis.SetsRequiredMembers] - public SetSceneItemTransformRequestData(double sceneItemId, ObsWebSocket.Core.Protocol.Common.SceneItemTransformStub? sceneItemTransform, string? canvasUuid = null, string? sceneName = null, string? sceneUuid = null) + public SetSceneItemTransformRequestData(int sceneItemId, ObsWebSocket.Core.Protocol.Common.SceneItemTransformStub? sceneItemTransform, string? canvasUuid = null, string? sceneName = null, string? sceneUuid = null) { this.CanvasUuid = canvasUuid; this.SceneName = sceneName; diff --git a/ObsWebSocket.Core/Generated/Protocol/Requests/SetSceneSceneTransitionOverride.Request.g.cs b/ObsWebSocket.Core/Generated/Protocol/Requests/SetSceneSceneTransitionOverride.Request.g.cs index bbaf034..ac2de92 100644 --- a/ObsWebSocket.Core/Generated/Protocol/Requests/SetSceneSceneTransitionOverride.Request.g.cs +++ b/ObsWebSocket.Core/Generated/Protocol/Requests/SetSceneSceneTransitionOverride.Request.g.cs @@ -67,7 +67,7 @@ public sealed partial record SetSceneSceneTransitionOverrideRequestData /// [JsonPropertyName("transitionDuration")] [Key("transitionDuration")] - public double? TransitionDuration { get; init; } + public int? TransitionDuration { get; init; } /// /// Name of the scene transition to use as override. Specify `null` to remove @@ -88,7 +88,7 @@ public SetSceneSceneTransitionOverrideRequestData() { } /// Initializes a new instance with all properties specified. /// Parameters are ordered with required properties first, then optional properties (with defaults). Follows protocol definition order where possible. /// - public SetSceneSceneTransitionOverrideRequestData(string? canvasUuid = null, string? sceneName = null, string? sceneUuid = null, string? transitionName = null, double? transitionDuration = null) + public SetSceneSceneTransitionOverrideRequestData(string? canvasUuid = null, string? sceneName = null, string? sceneUuid = null, string? transitionName = null, int? transitionDuration = null) { this.CanvasUuid = canvasUuid; this.SceneName = sceneName; diff --git a/ObsWebSocket.Core/Generated/Protocol/Requests/SetSourceFilterIndex.Request.g.cs b/ObsWebSocket.Core/Generated/Protocol/Requests/SetSourceFilterIndex.Request.g.cs index 60ab30a..ebe980f 100644 --- a/ObsWebSocket.Core/Generated/Protocol/Requests/SetSourceFilterIndex.Request.g.cs +++ b/ObsWebSocket.Core/Generated/Protocol/Requests/SetSourceFilterIndex.Request.g.cs @@ -44,7 +44,7 @@ public sealed partial record SetSourceFilterIndexRequestData /// [JsonPropertyName("filterIndex")] [Key("filterIndex")] - public required double FilterIndex { get; init; } + public required int FilterIndex { get; init; } /// /// Name of the filter @@ -87,7 +87,7 @@ public SetSourceFilterIndexRequestData() { } /// Parameters are ordered with required properties first, then optional properties (with defaults). Follows protocol definition order where possible. /// [System.Diagnostics.CodeAnalysis.SetsRequiredMembers] - public SetSourceFilterIndexRequestData(string filterName, double filterIndex, string? canvasUuid = null, string? sourceName = null, string? sourceUuid = null) + public SetSourceFilterIndexRequestData(string filterName, int filterIndex, string? canvasUuid = null, string? sourceName = null, string? sourceUuid = null) { this.CanvasUuid = canvasUuid; this.SourceName = sourceName; diff --git a/ObsWebSocket.Core/Generated/Protocol/Requests/SetTBarPosition.Request.g.cs b/ObsWebSocket.Core/Generated/Protocol/Requests/SetTBarPosition.Request.g.cs index fffc0c3..411dfd4 100644 --- a/ObsWebSocket.Core/Generated/Protocol/Requests/SetTBarPosition.Request.g.cs +++ b/ObsWebSocket.Core/Generated/Protocol/Requests/SetTBarPosition.Request.g.cs @@ -35,7 +35,7 @@ public sealed partial record SetTBarPositionRequestData /// [JsonPropertyName("position")] [Key("position")] - public required double Position { get; init; } + public required int Position { get; init; } /// /// Whether to release the TBar. Only set `false` if you know that you will be sending another position update @@ -57,7 +57,7 @@ public SetTBarPositionRequestData() { } /// Parameters are ordered with required properties first, then optional properties (with defaults). Follows protocol definition order where possible. /// [System.Diagnostics.CodeAnalysis.SetsRequiredMembers] - public SetTBarPositionRequestData(double position, bool? release = null) + public SetTBarPositionRequestData(int position, bool? release = null) { this.Position = position; this.Release = release; diff --git a/ObsWebSocket.Core/Generated/Protocol/Requests/SetVideoSettings.Request.g.cs b/ObsWebSocket.Core/Generated/Protocol/Requests/SetVideoSettings.Request.g.cs index f37cbdb..5749d84 100644 --- a/ObsWebSocket.Core/Generated/Protocol/Requests/SetVideoSettings.Request.g.cs +++ b/ObsWebSocket.Core/Generated/Protocol/Requests/SetVideoSettings.Request.g.cs @@ -36,7 +36,7 @@ public sealed partial record SetVideoSettingsRequestData /// [JsonPropertyName("baseHeight")] [Key("baseHeight")] - public double? BaseHeight { get; init; } + public int? BaseHeight { get; init; } /// /// Width of the base (canvas) resolution in pixels @@ -48,7 +48,7 @@ public sealed partial record SetVideoSettingsRequestData /// [JsonPropertyName("baseWidth")] [Key("baseWidth")] - public double? BaseWidth { get; init; } + public int? BaseWidth { get; init; } /// /// Denominator of the fractional FPS value @@ -60,7 +60,7 @@ public sealed partial record SetVideoSettingsRequestData /// [JsonPropertyName("fpsDenominator")] [Key("fpsDenominator")] - public double? FpsDenominator { get; init; } + public int? FpsDenominator { get; init; } /// /// Numerator of the fractional FPS value @@ -72,7 +72,7 @@ public sealed partial record SetVideoSettingsRequestData /// [JsonPropertyName("fpsNumerator")] [Key("fpsNumerator")] - public double? FpsNumerator { get; init; } + public int? FpsNumerator { get; init; } /// /// Height of the output resolution in pixels @@ -84,7 +84,7 @@ public sealed partial record SetVideoSettingsRequestData /// [JsonPropertyName("outputHeight")] [Key("outputHeight")] - public double? OutputHeight { get; init; } + public int? OutputHeight { get; init; } /// /// Width of the output resolution in pixels @@ -96,7 +96,7 @@ public sealed partial record SetVideoSettingsRequestData /// [JsonPropertyName("outputWidth")] [Key("outputWidth")] - public double? OutputWidth { get; init; } + public int? OutputWidth { get; init; } /// Initializes a new instance for deserialization via . [JsonConstructor] @@ -106,7 +106,7 @@ public SetVideoSettingsRequestData() { } /// Initializes a new instance with all properties specified. /// Parameters are ordered with required properties first, then optional properties (with defaults). Follows protocol definition order where possible. /// - public SetVideoSettingsRequestData(double? fpsNumerator = null, double? fpsDenominator = null, double? baseWidth = null, double? baseHeight = null, double? outputWidth = null, double? outputHeight = null) + public SetVideoSettingsRequestData(int? fpsNumerator = null, int? fpsDenominator = null, int? baseWidth = null, int? baseHeight = null, int? outputWidth = null, int? outputHeight = null) { this.FpsNumerator = fpsNumerator; this.FpsDenominator = fpsDenominator; diff --git a/ObsWebSocket.Core/Generated/Protocol/Requests/Sleep.Request.g.cs b/ObsWebSocket.Core/Generated/Protocol/Requests/Sleep.Request.g.cs index 02cf715..485e8c5 100644 --- a/ObsWebSocket.Core/Generated/Protocol/Requests/Sleep.Request.g.cs +++ b/ObsWebSocket.Core/Generated/Protocol/Requests/Sleep.Request.g.cs @@ -34,7 +34,7 @@ public sealed partial record SleepRequestData /// [JsonPropertyName("sleepFrames")] [Key("sleepFrames")] - public double? SleepFrames { get; init; } + public int? SleepFrames { get; init; } /// /// Number of milliseconds to sleep for (if `SERIAL_REALTIME` mode) @@ -46,7 +46,7 @@ public sealed partial record SleepRequestData /// [JsonPropertyName("sleepMillis")] [Key("sleepMillis")] - public double? SleepMillis { get; init; } + public int? SleepMillis { get; init; } /// Initializes a new instance for deserialization via . [JsonConstructor] @@ -56,7 +56,7 @@ public SleepRequestData() { } /// Initializes a new instance with all properties specified. /// Parameters are ordered with required properties first, then optional properties (with defaults). Follows protocol definition order where possible. /// - public SleepRequestData(double? sleepMillis = null, double? sleepFrames = null) + public SleepRequestData(int? sleepMillis = null, int? sleepFrames = null) { this.SleepMillis = sleepMillis; this.SleepFrames = sleepFrames; diff --git a/ObsWebSocket.Core/Generated/Protocol/Responses/CreateInput.Response.g.cs b/ObsWebSocket.Core/Generated/Protocol/Responses/CreateInput.Response.g.cs index 2f05650..c032cd3 100644 --- a/ObsWebSocket.Core/Generated/Protocol/Responses/CreateInput.Response.g.cs +++ b/ObsWebSocket.Core/Generated/Protocol/Responses/CreateInput.Response.g.cs @@ -36,7 +36,7 @@ public sealed partial record CreateInputResponseData /// [JsonPropertyName("sceneItemId")] [Key("sceneItemId")] - public required double SceneItemId { get; init; } + public required int SceneItemId { get; init; } /// Initializes a new instance for deserialization via . [JsonConstructor] @@ -47,7 +47,7 @@ public CreateInputResponseData() { } /// Parameters are ordered with required properties first, then optional properties (with defaults). Follows protocol definition order where possible. /// [System.Diagnostics.CodeAnalysis.SetsRequiredMembers] - public CreateInputResponseData(double sceneItemId, string? inputUuid = null) + public CreateInputResponseData(int sceneItemId, string? inputUuid = null) { this.InputUuid = inputUuid; this.SceneItemId = sceneItemId; diff --git a/ObsWebSocket.Core/Generated/Protocol/Responses/CreateSceneItem.Response.g.cs b/ObsWebSocket.Core/Generated/Protocol/Responses/CreateSceneItem.Response.g.cs index cfabee0..d81b54c 100644 --- a/ObsWebSocket.Core/Generated/Protocol/Responses/CreateSceneItem.Response.g.cs +++ b/ObsWebSocket.Core/Generated/Protocol/Responses/CreateSceneItem.Response.g.cs @@ -31,7 +31,7 @@ public sealed partial record CreateSceneItemResponseData /// [JsonPropertyName("sceneItemId")] [Key("sceneItemId")] - public required double SceneItemId { get; init; } + public required int SceneItemId { get; init; } /// Initializes a new instance for deserialization via . [JsonConstructor] @@ -42,7 +42,7 @@ public CreateSceneItemResponseData() { } /// Parameters are ordered with required properties first, then optional properties (with defaults). Follows protocol definition order where possible. /// [System.Diagnostics.CodeAnalysis.SetsRequiredMembers] - public CreateSceneItemResponseData(double sceneItemId) + public CreateSceneItemResponseData(int sceneItemId) { this.SceneItemId = sceneItemId; } diff --git a/ObsWebSocket.Core/Generated/Protocol/Responses/DuplicateSceneItem.Response.g.cs b/ObsWebSocket.Core/Generated/Protocol/Responses/DuplicateSceneItem.Response.g.cs index ecfcf15..cc4c27a 100644 --- a/ObsWebSocket.Core/Generated/Protocol/Responses/DuplicateSceneItem.Response.g.cs +++ b/ObsWebSocket.Core/Generated/Protocol/Responses/DuplicateSceneItem.Response.g.cs @@ -31,7 +31,7 @@ public sealed partial record DuplicateSceneItemResponseData /// [JsonPropertyName("sceneItemId")] [Key("sceneItemId")] - public required double SceneItemId { get; init; } + public required int SceneItemId { get; init; } /// Initializes a new instance for deserialization via . [JsonConstructor] @@ -42,7 +42,7 @@ public DuplicateSceneItemResponseData() { } /// Parameters are ordered with required properties first, then optional properties (with defaults). Follows protocol definition order where possible. /// [System.Diagnostics.CodeAnalysis.SetsRequiredMembers] - public DuplicateSceneItemResponseData(double sceneItemId) + public DuplicateSceneItemResponseData(int sceneItemId) { this.SceneItemId = sceneItemId; } diff --git a/ObsWebSocket.Core/Generated/Protocol/Responses/GetCurrentSceneTransition.Response.g.cs b/ObsWebSocket.Core/Generated/Protocol/Responses/GetCurrentSceneTransition.Response.g.cs index cc42520..54f270a 100644 --- a/ObsWebSocket.Core/Generated/Protocol/Responses/GetCurrentSceneTransition.Response.g.cs +++ b/ObsWebSocket.Core/Generated/Protocol/Responses/GetCurrentSceneTransition.Response.g.cs @@ -36,7 +36,7 @@ public sealed partial record GetCurrentSceneTransitionResponseData /// [JsonPropertyName("transitionDuration")] [Key("transitionDuration")] - public double? TransitionDuration { get; init; } + public int? TransitionDuration { get; init; } /// /// Whether the transition uses a fixed (unconfigurable) duration @@ -82,7 +82,7 @@ public GetCurrentSceneTransitionResponseData() { } /// Parameters are ordered with required properties first, then optional properties (with defaults). Follows protocol definition order where possible. /// [System.Diagnostics.CodeAnalysis.SetsRequiredMembers] - public GetCurrentSceneTransitionResponseData(bool transitionFixed, bool transitionConfigurable, string? transitionName = null, string? transitionUuid = null, string? transitionKind = null, double? transitionDuration = null, System.Text.Json.JsonElement? transitionSettings = null) + public GetCurrentSceneTransitionResponseData(bool transitionFixed, bool transitionConfigurable, string? transitionName = null, string? transitionUuid = null, string? transitionKind = null, int? transitionDuration = null, System.Text.Json.JsonElement? transitionSettings = null) { this.TransitionName = transitionName; this.TransitionUuid = transitionUuid; diff --git a/ObsWebSocket.Core/Generated/Protocol/Responses/GetInputAudioSyncOffset.Response.g.cs b/ObsWebSocket.Core/Generated/Protocol/Responses/GetInputAudioSyncOffset.Response.g.cs index 2d2cb2d..01caf06 100644 --- a/ObsWebSocket.Core/Generated/Protocol/Responses/GetInputAudioSyncOffset.Response.g.cs +++ b/ObsWebSocket.Core/Generated/Protocol/Responses/GetInputAudioSyncOffset.Response.g.cs @@ -31,7 +31,7 @@ public sealed partial record GetInputAudioSyncOffsetResponseData /// [JsonPropertyName("inputAudioSyncOffset")] [Key("inputAudioSyncOffset")] - public required double InputAudioSyncOffset { get; init; } + public required int InputAudioSyncOffset { get; init; } /// Initializes a new instance for deserialization via . [JsonConstructor] @@ -42,7 +42,7 @@ public GetInputAudioSyncOffsetResponseData() { } /// Parameters are ordered with required properties first, then optional properties (with defaults). Follows protocol definition order where possible. /// [System.Diagnostics.CodeAnalysis.SetsRequiredMembers] - public GetInputAudioSyncOffsetResponseData(double inputAudioSyncOffset) + public GetInputAudioSyncOffsetResponseData(int inputAudioSyncOffset) { this.InputAudioSyncOffset = inputAudioSyncOffset; } diff --git a/ObsWebSocket.Core/Generated/Protocol/Responses/GetMediaInputStatus.Response.g.cs b/ObsWebSocket.Core/Generated/Protocol/Responses/GetMediaInputStatus.Response.g.cs index ff95718..54d974d 100644 --- a/ObsWebSocket.Core/Generated/Protocol/Responses/GetMediaInputStatus.Response.g.cs +++ b/ObsWebSocket.Core/Generated/Protocol/Responses/GetMediaInputStatus.Response.g.cs @@ -40,14 +40,14 @@ public sealed partial record GetMediaInputStatusResponseData /// [JsonPropertyName("mediaCursor")] [Key("mediaCursor")] - public double? MediaCursor { get; init; } + public long? MediaCursor { get; init; } /// /// Total duration of the playing media in milliseconds. `null` if not playing /// [JsonPropertyName("mediaDuration")] [Key("mediaDuration")] - public double? MediaDuration { get; init; } + public long? MediaDuration { get; init; } /// /// State of the media input @@ -64,7 +64,7 @@ public GetMediaInputStatusResponseData() { } /// Initializes a new instance with all properties specified. /// Parameters are ordered with required properties first, then optional properties (with defaults). Follows protocol definition order where possible. /// - public GetMediaInputStatusResponseData(string? mediaState = null, double? mediaDuration = null, double? mediaCursor = null) + public GetMediaInputStatusResponseData(string? mediaState = null, long? mediaDuration = null, long? mediaCursor = null) { this.MediaState = mediaState; this.MediaDuration = mediaDuration; diff --git a/ObsWebSocket.Core/Generated/Protocol/Responses/GetOutputStatus.Response.g.cs b/ObsWebSocket.Core/Generated/Protocol/Responses/GetOutputStatus.Response.g.cs index edec618..9b0b776 100644 --- a/ObsWebSocket.Core/Generated/Protocol/Responses/GetOutputStatus.Response.g.cs +++ b/ObsWebSocket.Core/Generated/Protocol/Responses/GetOutputStatus.Response.g.cs @@ -36,7 +36,7 @@ public sealed partial record GetOutputStatusResponseData /// [JsonPropertyName("outputBytes")] [Key("outputBytes")] - public required double OutputBytes { get; init; } + public required long OutputBytes { get; init; } /// /// Congestion of the output @@ -50,7 +50,7 @@ public sealed partial record GetOutputStatusResponseData /// [JsonPropertyName("outputDuration")] [Key("outputDuration")] - public required double OutputDuration { get; init; } + public required long OutputDuration { get; init; } /// /// Whether the output is reconnecting @@ -64,7 +64,7 @@ public sealed partial record GetOutputStatusResponseData /// [JsonPropertyName("outputSkippedFrames")] [Key("outputSkippedFrames")] - public required double OutputSkippedFrames { get; init; } + public required int OutputSkippedFrames { get; init; } /// /// Current formatted timecode string for the output @@ -78,7 +78,7 @@ public sealed partial record GetOutputStatusResponseData /// [JsonPropertyName("outputTotalFrames")] [Key("outputTotalFrames")] - public required double OutputTotalFrames { get; init; } + public required int OutputTotalFrames { get; init; } /// Initializes a new instance for deserialization via . [JsonConstructor] @@ -89,7 +89,7 @@ public GetOutputStatusResponseData() { } /// Parameters are ordered with required properties first, then optional properties (with defaults). Follows protocol definition order where possible. /// [System.Diagnostics.CodeAnalysis.SetsRequiredMembers] - public GetOutputStatusResponseData(bool outputActive, bool outputReconnecting, double outputDuration, double outputCongestion, double outputBytes, double outputSkippedFrames, double outputTotalFrames, string? outputTimecode = null) + public GetOutputStatusResponseData(bool outputActive, bool outputReconnecting, long outputDuration, double outputCongestion, long outputBytes, int outputSkippedFrames, int outputTotalFrames, string? outputTimecode = null) { this.OutputActive = outputActive; this.OutputReconnecting = outputReconnecting; diff --git a/ObsWebSocket.Core/Generated/Protocol/Responses/GetRecordStatus.Response.g.cs b/ObsWebSocket.Core/Generated/Protocol/Responses/GetRecordStatus.Response.g.cs index 0737846..5dbe7bc 100644 --- a/ObsWebSocket.Core/Generated/Protocol/Responses/GetRecordStatus.Response.g.cs +++ b/ObsWebSocket.Core/Generated/Protocol/Responses/GetRecordStatus.Response.g.cs @@ -36,14 +36,14 @@ public sealed partial record GetRecordStatusResponseData /// [JsonPropertyName("outputBytes")] [Key("outputBytes")] - public required double OutputBytes { get; init; } + public required long OutputBytes { get; init; } /// /// Current duration in milliseconds for the output /// [JsonPropertyName("outputDuration")] [Key("outputDuration")] - public required double OutputDuration { get; init; } + public required long OutputDuration { get; init; } /// /// Whether the output is paused @@ -68,7 +68,7 @@ public GetRecordStatusResponseData() { } /// Parameters are ordered with required properties first, then optional properties (with defaults). Follows protocol definition order where possible. /// [System.Diagnostics.CodeAnalysis.SetsRequiredMembers] - public GetRecordStatusResponseData(bool outputActive, bool outputPaused, double outputDuration, double outputBytes, string? outputTimecode = null) + public GetRecordStatusResponseData(bool outputActive, bool outputPaused, long outputDuration, long outputBytes, string? outputTimecode = null) { this.OutputActive = outputActive; this.OutputPaused = outputPaused; diff --git a/ObsWebSocket.Core/Generated/Protocol/Responses/GetSceneItemId.Response.g.cs b/ObsWebSocket.Core/Generated/Protocol/Responses/GetSceneItemId.Response.g.cs index af48b22..4fbba5a 100644 --- a/ObsWebSocket.Core/Generated/Protocol/Responses/GetSceneItemId.Response.g.cs +++ b/ObsWebSocket.Core/Generated/Protocol/Responses/GetSceneItemId.Response.g.cs @@ -31,7 +31,7 @@ public sealed partial record GetSceneItemIdResponseData /// [JsonPropertyName("sceneItemId")] [Key("sceneItemId")] - public required double SceneItemId { get; init; } + public required int SceneItemId { get; init; } /// Initializes a new instance for deserialization via . [JsonConstructor] @@ -42,7 +42,7 @@ public GetSceneItemIdResponseData() { } /// Parameters are ordered with required properties first, then optional properties (with defaults). Follows protocol definition order where possible. /// [System.Diagnostics.CodeAnalysis.SetsRequiredMembers] - public GetSceneItemIdResponseData(double sceneItemId) + public GetSceneItemIdResponseData(int sceneItemId) { this.SceneItemId = sceneItemId; } diff --git a/ObsWebSocket.Core/Generated/Protocol/Responses/GetSceneItemIndex.Response.g.cs b/ObsWebSocket.Core/Generated/Protocol/Responses/GetSceneItemIndex.Response.g.cs index e6e0c80..a7d86c6 100644 --- a/ObsWebSocket.Core/Generated/Protocol/Responses/GetSceneItemIndex.Response.g.cs +++ b/ObsWebSocket.Core/Generated/Protocol/Responses/GetSceneItemIndex.Response.g.cs @@ -33,7 +33,7 @@ public sealed partial record GetSceneItemIndexResponseData /// [JsonPropertyName("sceneItemIndex")] [Key("sceneItemIndex")] - public required double SceneItemIndex { get; init; } + public required int SceneItemIndex { get; init; } /// Initializes a new instance for deserialization via . [JsonConstructor] @@ -44,7 +44,7 @@ public GetSceneItemIndexResponseData() { } /// Parameters are ordered with required properties first, then optional properties (with defaults). Follows protocol definition order where possible. /// [System.Diagnostics.CodeAnalysis.SetsRequiredMembers] - public GetSceneItemIndexResponseData(double sceneItemIndex) + public GetSceneItemIndexResponseData(int sceneItemIndex) { this.SceneItemIndex = sceneItemIndex; } diff --git a/ObsWebSocket.Core/Generated/Protocol/Responses/GetSceneSceneTransitionOverride.Response.g.cs b/ObsWebSocket.Core/Generated/Protocol/Responses/GetSceneSceneTransitionOverride.Response.g.cs index ca5f955..c1cece7 100644 --- a/ObsWebSocket.Core/Generated/Protocol/Responses/GetSceneSceneTransitionOverride.Response.g.cs +++ b/ObsWebSocket.Core/Generated/Protocol/Responses/GetSceneSceneTransitionOverride.Response.g.cs @@ -31,7 +31,7 @@ public sealed partial record GetSceneSceneTransitionOverrideResponseData /// [JsonPropertyName("transitionDuration")] [Key("transitionDuration")] - public double? TransitionDuration { get; init; } + public int? TransitionDuration { get; init; } /// /// Name of the overridden scene transition, else `null` @@ -48,7 +48,7 @@ public GetSceneSceneTransitionOverrideResponseData() { } /// Initializes a new instance with all properties specified. /// Parameters are ordered with required properties first, then optional properties (with defaults). Follows protocol definition order where possible. /// - public GetSceneSceneTransitionOverrideResponseData(string? transitionName = null, double? transitionDuration = null) + public GetSceneSceneTransitionOverrideResponseData(string? transitionName = null, int? transitionDuration = null) { this.TransitionName = transitionName; this.TransitionDuration = transitionDuration; diff --git a/ObsWebSocket.Core/Generated/Protocol/Responses/GetSourceFilter.Response.g.cs b/ObsWebSocket.Core/Generated/Protocol/Responses/GetSourceFilter.Response.g.cs index 8cba357..ef6669c 100644 --- a/ObsWebSocket.Core/Generated/Protocol/Responses/GetSourceFilter.Response.g.cs +++ b/ObsWebSocket.Core/Generated/Protocol/Responses/GetSourceFilter.Response.g.cs @@ -36,7 +36,7 @@ public sealed partial record GetSourceFilterResponseData /// [JsonPropertyName("filterIndex")] [Key("filterIndex")] - public required double FilterIndex { get; init; } + public required int FilterIndex { get; init; } /// /// The kind of filter @@ -61,7 +61,7 @@ public GetSourceFilterResponseData() { } /// Parameters are ordered with required properties first, then optional properties (with defaults). Follows protocol definition order where possible. /// [System.Diagnostics.CodeAnalysis.SetsRequiredMembers] - public GetSourceFilterResponseData(bool filterEnabled, double filterIndex, string? filterKind = null, System.Text.Json.JsonElement? filterSettings = null) + public GetSourceFilterResponseData(bool filterEnabled, int filterIndex, string? filterKind = null, System.Text.Json.JsonElement? filterSettings = null) { this.FilterEnabled = filterEnabled; this.FilterIndex = filterIndex; diff --git a/ObsWebSocket.Core/Generated/Protocol/Responses/GetStats.Response.g.cs b/ObsWebSocket.Core/Generated/Protocol/Responses/GetStats.Response.g.cs index 0942964..07d2863 100644 --- a/ObsWebSocket.Core/Generated/Protocol/Responses/GetStats.Response.g.cs +++ b/ObsWebSocket.Core/Generated/Protocol/Responses/GetStats.Response.g.cs @@ -64,42 +64,42 @@ public sealed partial record GetStatsResponseData /// [JsonPropertyName("outputSkippedFrames")] [Key("outputSkippedFrames")] - public required double OutputSkippedFrames { get; init; } + public required int OutputSkippedFrames { get; init; } /// /// Total number of frames outputted by the output thread /// [JsonPropertyName("outputTotalFrames")] [Key("outputTotalFrames")] - public required double OutputTotalFrames { get; init; } + public required int OutputTotalFrames { get; init; } /// /// Number of frames skipped by OBS in the render thread /// [JsonPropertyName("renderSkippedFrames")] [Key("renderSkippedFrames")] - public required double RenderSkippedFrames { get; init; } + public required int RenderSkippedFrames { get; init; } /// /// Total number of frames outputted by the render thread /// [JsonPropertyName("renderTotalFrames")] [Key("renderTotalFrames")] - public required double RenderTotalFrames { get; init; } + public required int RenderTotalFrames { get; init; } /// /// Total number of messages received by obs-websocket from the client /// [JsonPropertyName("webSocketSessionIncomingMessages")] [Key("webSocketSessionIncomingMessages")] - public required double WebSocketSessionIncomingMessages { get; init; } + public required int WebSocketSessionIncomingMessages { get; init; } /// /// Total number of messages sent by obs-websocket to the client /// [JsonPropertyName("webSocketSessionOutgoingMessages")] [Key("webSocketSessionOutgoingMessages")] - public required double WebSocketSessionOutgoingMessages { get; init; } + public required int WebSocketSessionOutgoingMessages { get; init; } /// Initializes a new instance for deserialization via . [JsonConstructor] @@ -110,7 +110,7 @@ public GetStatsResponseData() { } /// Parameters are ordered with required properties first, then optional properties (with defaults). Follows protocol definition order where possible. /// [System.Diagnostics.CodeAnalysis.SetsRequiredMembers] - public GetStatsResponseData(double cpuUsage, double memoryUsage, double availableDiskSpace, double activeFps, double averageFrameRenderTime, double renderSkippedFrames, double renderTotalFrames, double outputSkippedFrames, double outputTotalFrames, double webSocketSessionIncomingMessages, double webSocketSessionOutgoingMessages) + public GetStatsResponseData(double cpuUsage, double memoryUsage, double availableDiskSpace, double activeFps, double averageFrameRenderTime, int renderSkippedFrames, int renderTotalFrames, int outputSkippedFrames, int outputTotalFrames, int webSocketSessionIncomingMessages, int webSocketSessionOutgoingMessages) { this.CpuUsage = cpuUsage; this.MemoryUsage = memoryUsage; diff --git a/ObsWebSocket.Core/Generated/Protocol/Responses/GetStreamStatus.Response.g.cs b/ObsWebSocket.Core/Generated/Protocol/Responses/GetStreamStatus.Response.g.cs index 52d0d65..c8119b0 100644 --- a/ObsWebSocket.Core/Generated/Protocol/Responses/GetStreamStatus.Response.g.cs +++ b/ObsWebSocket.Core/Generated/Protocol/Responses/GetStreamStatus.Response.g.cs @@ -36,7 +36,7 @@ public sealed partial record GetStreamStatusResponseData /// [JsonPropertyName("outputBytes")] [Key("outputBytes")] - public required double OutputBytes { get; init; } + public required long OutputBytes { get; init; } /// /// Congestion of the output @@ -50,7 +50,7 @@ public sealed partial record GetStreamStatusResponseData /// [JsonPropertyName("outputDuration")] [Key("outputDuration")] - public required double OutputDuration { get; init; } + public required long OutputDuration { get; init; } /// /// Whether the output is currently reconnecting @@ -64,7 +64,7 @@ public sealed partial record GetStreamStatusResponseData /// [JsonPropertyName("outputSkippedFrames")] [Key("outputSkippedFrames")] - public required double OutputSkippedFrames { get; init; } + public required int OutputSkippedFrames { get; init; } /// /// Current formatted timecode string for the output @@ -78,7 +78,7 @@ public sealed partial record GetStreamStatusResponseData /// [JsonPropertyName("outputTotalFrames")] [Key("outputTotalFrames")] - public required double OutputTotalFrames { get; init; } + public required int OutputTotalFrames { get; init; } /// Initializes a new instance for deserialization via . [JsonConstructor] @@ -89,7 +89,7 @@ public GetStreamStatusResponseData() { } /// Parameters are ordered with required properties first, then optional properties (with defaults). Follows protocol definition order where possible. /// [System.Diagnostics.CodeAnalysis.SetsRequiredMembers] - public GetStreamStatusResponseData(bool outputActive, bool outputReconnecting, double outputDuration, double outputCongestion, double outputBytes, double outputSkippedFrames, double outputTotalFrames, string? outputTimecode = null) + public GetStreamStatusResponseData(bool outputActive, bool outputReconnecting, long outputDuration, double outputCongestion, long outputBytes, int outputSkippedFrames, int outputTotalFrames, string? outputTimecode = null) { this.OutputActive = outputActive; this.OutputReconnecting = outputReconnecting; diff --git a/ObsWebSocket.Core/Generated/Protocol/Responses/GetVersion.Response.g.cs b/ObsWebSocket.Core/Generated/Protocol/Responses/GetVersion.Response.g.cs index c8b120f..56eb552 100644 --- a/ObsWebSocket.Core/Generated/Protocol/Responses/GetVersion.Response.g.cs +++ b/ObsWebSocket.Core/Generated/Protocol/Responses/GetVersion.Response.g.cs @@ -64,7 +64,7 @@ public sealed partial record GetVersionResponseData /// [JsonPropertyName("rpcVersion")] [Key("rpcVersion")] - public required double RpcVersion { get; init; } + public required int RpcVersion { get; init; } /// /// Image formats available in `GetSourceScreenshot` and `SaveSourceScreenshot` requests. @@ -82,7 +82,7 @@ public GetVersionResponseData() { } /// Parameters are ordered with required properties first, then optional properties (with defaults). Follows protocol definition order where possible. /// [System.Diagnostics.CodeAnalysis.SetsRequiredMembers] - public GetVersionResponseData(double rpcVersion, string? obsVersion = null, string? obsWebSocketVersion = null, System.Collections.Generic.List? availableRequests = null, System.Collections.Generic.List? supportedImageFormats = null, string? platform = null, string? platformDescription = null) + public GetVersionResponseData(int rpcVersion, string? obsVersion = null, string? obsWebSocketVersion = null, System.Collections.Generic.List? availableRequests = null, System.Collections.Generic.List? supportedImageFormats = null, string? platform = null, string? platformDescription = null) { this.ObsVersion = obsVersion; this.ObsWebSocketVersion = obsWebSocketVersion; diff --git a/ObsWebSocket.Core/Generated/Protocol/Responses/GetVideoSettings.Response.g.cs b/ObsWebSocket.Core/Generated/Protocol/Responses/GetVideoSettings.Response.g.cs index 7fab887..4becccc 100644 --- a/ObsWebSocket.Core/Generated/Protocol/Responses/GetVideoSettings.Response.g.cs +++ b/ObsWebSocket.Core/Generated/Protocol/Responses/GetVideoSettings.Response.g.cs @@ -31,42 +31,42 @@ public sealed partial record GetVideoSettingsResponseData /// [JsonPropertyName("baseHeight")] [Key("baseHeight")] - public required double BaseHeight { get; init; } + public required int BaseHeight { get; init; } /// /// Width of the base (canvas) resolution in pixels /// [JsonPropertyName("baseWidth")] [Key("baseWidth")] - public required double BaseWidth { get; init; } + public required int BaseWidth { get; init; } /// /// Denominator of the fractional FPS value /// [JsonPropertyName("fpsDenominator")] [Key("fpsDenominator")] - public required double FpsDenominator { get; init; } + public required int FpsDenominator { get; init; } /// /// Numerator of the fractional FPS value /// [JsonPropertyName("fpsNumerator")] [Key("fpsNumerator")] - public required double FpsNumerator { get; init; } + public required int FpsNumerator { get; init; } /// /// Height of the output resolution in pixels /// [JsonPropertyName("outputHeight")] [Key("outputHeight")] - public required double OutputHeight { get; init; } + public required int OutputHeight { get; init; } /// /// Width of the output resolution in pixels /// [JsonPropertyName("outputWidth")] [Key("outputWidth")] - public required double OutputWidth { get; init; } + public required int OutputWidth { get; init; } /// Initializes a new instance for deserialization via . [JsonConstructor] @@ -77,7 +77,7 @@ public GetVideoSettingsResponseData() { } /// Parameters are ordered with required properties first, then optional properties (with defaults). Follows protocol definition order where possible. /// [System.Diagnostics.CodeAnalysis.SetsRequiredMembers] - public GetVideoSettingsResponseData(double fpsNumerator, double fpsDenominator, double baseWidth, double baseHeight, double outputWidth, double outputHeight) + public GetVideoSettingsResponseData(int fpsNumerator, int fpsDenominator, int baseWidth, int baseHeight, int outputWidth, int outputHeight) { this.FpsNumerator = fpsNumerator; this.FpsDenominator = fpsDenominator; diff --git a/ObsWebSocket.Core/Groups/SceneItemsGroup.cs b/ObsWebSocket.Core/Groups/SceneItemsGroup.cs index bb0e1c2..0d551b7 100644 --- a/ObsWebSocket.Core/Groups/SceneItemsGroup.cs +++ b/ObsWebSocket.Core/Groups/SceneItemsGroup.cs @@ -32,7 +32,7 @@ public readonly partial struct SceneItemsGroup /// Thrown if the client is not connected. public async Task SetSceneItemEnabledAsync( string sceneName, - double sceneItemId, // Use double as sceneItemId is Number in protocol + int sceneItemId, bool? isEnabled = null, // If null, toggles; otherwise sets to the specified state CancellationToken cancellationToken = default ) @@ -100,7 +100,7 @@ public async Task SetSceneItemEnabledAsync( ArgumentException.ThrowIfNullOrEmpty(sourceName); client.EnsureConnected(); - double? sceneItemId = await client + int? sceneItemId = await client .SceneItems.FindSceneItemIdAsync(sceneName, sourceName, cancellationToken) .ConfigureAwait(false); @@ -146,9 +146,7 @@ public async Task SetSceneItemEnabledAsync( ) .ConfigureAwait(false); - // Scene item ids are Number on the wire because the protocol has no integer type, - // but OBS only ever assigns whole numbers. - return response is null ? null : checked((int)response.SceneItemId); + return response?.SceneItemId; } catch (ObsWebSocketRequestException ex) when (ex.StatusCode is ObsRequestStatus.ResourceNotFound) @@ -159,27 +157,6 @@ public async Task SetSceneItemEnabledAsync( // Let other ObsWebSocketExceptions or different exception types propagate } - /// - /// Sets or toggles a scene item's enabled state using an integer item id. - /// - /// The name of the scene containing the item. - /// The numeric id of the scene item. - /// The desired state, or to toggle. - /// A token to cancel the operation. - /// The resulting enabled state. - public Task SetSceneItemEnabledAsync( - string sceneName, - int sceneItemId, - bool? isEnabled = null, - CancellationToken cancellationToken = default - ) => - client.SceneItems.SetSceneItemEnabledAsync( - sceneName, - (double)sceneItemId, - isEnabled, - cancellationToken - ); - /// /// Returns the scene item id for a source within a scene as an , or /// when the scene does not contain it. diff --git a/ObsWebSocket.Example/Worker.cs b/ObsWebSocket.Example/Worker.cs index ce2094d..0157c15 100644 --- a/ObsWebSocket.Example/Worker.cs +++ b/ObsWebSocket.Example/Worker.cs @@ -345,7 +345,7 @@ await _obsClient.Inputs.ToggleInputMuteAsync( try { // First, find the scene item ID within the specified scene - double sceneItemId = await GetSceneItemIdAsync( + int sceneItemId = await GetSceneItemIdAsync( sceneForGetSettings, inputForGetSettings, cancellationToken @@ -392,7 +392,7 @@ await _obsClient.Inputs.GetInputSettingsAsync( try { // Find the scene item ID first (optional but good practice) - double sceneItemId = await GetSceneItemIdAsync( + int sceneItemId = await GetSceneItemIdAsync( sceneForSetText, inputForSetText, cancellationToken @@ -2207,44 +2207,48 @@ await client .ConfigureAwait(false) ); - results.Add( - await TrySettingsCheckAsync( - "Virtual camera toggle", - async () => - { - bool before = await client - .Outputs.IsVirtualCamActiveAsync(cancellationToken) - .ConfigureAwait(false); - - bool? turnedOn = await client - .Outputs.SetVirtualCamActiveAndWaitAsync( - !before, - cancellationToken: cancellationToken - ) - .ConfigureAwait(false); - bool observed = await client - .Outputs.IsVirtualCamActiveAsync(cancellationToken) - .ConfigureAwait(false); - - // Put it back the way it was found. - _ = await client - .Outputs.SetVirtualCamActiveAndWaitAsync( - before, - cancellationToken: cancellationToken - ) - .ConfigureAwait(false); - bool restored = await client - .Outputs.IsVirtualCamActiveAsync(cancellationToken) - .ConfigureAwait(false); - - return ( - turnedOn == !before && observed == !before && restored == before, - $"{before} -> {observed} -> {restored}" - ); - } - ) - .ConfigureAwait(false) - ); + // Disabled: toggling the virtual camera takes down this OBS install. The fault is in + // the Stream Deck plugin (streamdeckpluginobs32.dll appears at the fault address and + // in every frame above it), not in OBS or in this library. Re-enable once that plugin + // is removed. + // results.Add( + // await TrySettingsCheckAsync( + // "Virtual camera toggle", + // async () => + // { + // bool before = await client + // .Outputs.IsVirtualCamActiveAsync(cancellationToken) + // .ConfigureAwait(false); + + // bool? turnedOn = await client + // .Outputs.SetVirtualCamActiveAndWaitAsync( + // !before, + // cancellationToken: cancellationToken + // ) + // .ConfigureAwait(false); + // bool observed = await client + // .Outputs.IsVirtualCamActiveAsync(cancellationToken) + // .ConfigureAwait(false); + + // // Put it back the way it was found. + // _ = await client + // .Outputs.SetVirtualCamActiveAndWaitAsync( + // before, + // cancellationToken: cancellationToken + // ) + // .ConfigureAwait(false); + // bool restored = await client + // .Outputs.IsVirtualCamActiveAsync(cancellationToken) + // .ConfigureAwait(false); + + // return ( + // turnedOn == !before && observed == !before && restored == before, + // $"{before} -> {observed} -> {restored}" + // ); + // } + // ) + // .ConfigureAwait(false) + // ); results.Add( await TrySettingsCheckAsync( @@ -2593,7 +2597,7 @@ private ObsWebSocketClientOptions CloneOptionsForFormat(SerializationFormat form }; // --- Helper to find Scene Item ID --- - private async Task GetSceneItemIdAsync( + private async Task GetSceneItemIdAsync( string sceneName, string sourceName, CancellationToken cancellationToken @@ -2989,7 +2993,7 @@ .. sceneNames.Where(n => n != currentProgramScene).OrderBy(n => n), RestartWhenActive: true ); - double sceneItemId; + int sceneItemId; // Step 8: Create new input or update existing source settings if (isNewSource) diff --git a/ObsWebSocket.Tests/ObsWebSocketClientIntegrationTests.cs b/ObsWebSocket.Tests/ObsWebSocketClientIntegrationTests.cs index 3330a37..3ae7f22 100644 --- a/ObsWebSocket.Tests/ObsWebSocketClientIntegrationTests.cs +++ b/ObsWebSocket.Tests/ObsWebSocketClientIntegrationTests.cs @@ -445,7 +445,7 @@ public async Task GetSceneItemTransform_ReturnsTransformStub() idResponse, $"Could not get SceneItemId for '{s_testOptions.TestInputName}' in scene '{s_testOptions.TestSceneName}'. Ensure it exists." ); - double sceneItemId = idResponse.SceneItemId; + int sceneItemId = idResponse.SceneItemId; // Get the transform GetSceneItemTransformResponseData? transformResponse = From 9c4966c77684a890a01ad1b56828b8e85e85d507 Mon Sep 17 00:00:00 2001 From: Agash Date: Wed, 26 Aug 2026 08:26:15 +0200 Subject: [PATCH 10/12] feat(core)!: chain client options off the registration 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. --- .../ObsWebSocketClientBuilder.cs | 148 ++++++++++++++++++ ObsWebSocket.Core/ObsWebSocketHosting.cs | 36 ++++- ...ObsWebSocketServiceCollectionExtensions.cs | 12 +- ObsWebSocket.Example/Worker.cs | 110 +++++++++++++ ObsWebSocket.Tests/HostingTests.cs | 15 +- .../ObsWebSocketClientConnectionTests.cs | 18 ++- ObsWebSocket.Tests/ReadmeCompileCheck.cs | 4 +- README.md | 10 +- 8 files changed, 319 insertions(+), 34 deletions(-) create mode 100644 ObsWebSocket.Core/ObsWebSocketClientBuilder.cs diff --git a/ObsWebSocket.Core/ObsWebSocketClientBuilder.cs b/ObsWebSocket.Core/ObsWebSocketClientBuilder.cs new file mode 100644 index 0000000..92241ce --- /dev/null +++ b/ObsWebSocket.Core/ObsWebSocketClientBuilder.cs @@ -0,0 +1,148 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Diagnostics.HealthChecks; +using Microsoft.Extensions.Options; + +namespace ObsWebSocket.Core; + +/// +/// The client returned by AddObsWebSocketClient, so the things that apply to one client are +/// chained off the call that registered it rather than applied to the whole container. +/// +/// +/// This is what lets an application drive two OBS instances and give each its own behaviour, which +/// a bare extension cannot express because it has no way to know +/// which client is meant. +/// +public interface IObsWebSocketClientBuilder +{ + /// The collection the client was registered in. + IServiceCollection Services { get; } + + /// + /// The key this client is registered under, or for the unnamed client. + /// + string? Name { get; } +} + +/// The collection the client was registered in. +/// The key the client is registered under, or null for the unnamed client. +internal sealed class ObsWebSocketClientBuilder(IServiceCollection services, string? name) + : IObsWebSocketClientBuilder +{ + /// + public IServiceCollection Services { get; } = services; + + /// + public string? Name { get; } = name; +} + +/// +/// The per-client options, chained off the registration. +/// +public static class ObsWebSocketClientBuilderExtensions +{ + /// + /// Connects when the host starts and disconnects when it stops, so an application does not + /// need its own background service for the connection. + /// + /// + /// A connection that cannot be established at startup is logged rather than thrown, because + /// OBS is often started after the application; reconnect takes over from there. + /// + /// The client to connect automatically. + /// The same builder, for chaining. + public static IObsWebSocketClientBuilder WithAutoConnect( + this IObsWebSocketClientBuilder builder + ) + { + ArgumentNullException.ThrowIfNull(builder); + + string? name = builder.Name; + _ = builder.Services.AddHostedService(sp => new ObsWebSocketConnectionService( + name is null + ? sp.GetRequiredService() + : sp.GetRequiredKeyedService(name), + sp.GetRequiredService>(), + sp.GetRequiredService>(), + name + )); + + return builder; + } + + /// + /// Adds a health check reporting whether this client is connected. + /// + /// The client to report on. + /// + /// The name to register the check under. Defaults to obs-websocket for the unnamed + /// client, and obs-websocket-{key} for a named one, so two clients do not collide. + /// + /// Status to report when not connected. + /// Tags for filtering checks. + /// The same builder, for chaining. + public static IObsWebSocketClientBuilder WithHealthCheck( + this IObsWebSocketClientBuilder builder, + string? name = null, + HealthStatus? failureStatus = null, + IEnumerable? tags = null + ) + { + ArgumentNullException.ThrowIfNull(builder); + + string? key = builder.Name; + string checkName = name ?? (key is null ? "obs-websocket" : $"obs-websocket-{key}"); + + _ = builder + .Services.AddHealthChecks() + .Add( + new HealthCheckRegistration( + checkName, + sp => new ObsWebSocketHealthCheck( + key is null + ? sp.GetRequiredService() + : sp.GetRequiredKeyedService(key) + ), + failureStatus, + tags + ) + ); + + return builder; + } + + /// + /// Registers the reconnect pipeline for this client. + /// + /// The client to configure. + /// The same builder, for chaining. + public static IObsWebSocketClientBuilder WithReconnectPipeline( + this IObsWebSocketClientBuilder builder + ) + { + ArgumentNullException.ThrowIfNull(builder); + _ = builder.Services.AddObsWebSocketReconnectPipeline(); + return builder; + } + + /// + /// Configures this client's options. + /// + /// The client to configure. + /// Applied to this client's options. + /// The same builder, for chaining. + public static IObsWebSocketClientBuilder Configure( + this IObsWebSocketClientBuilder builder, + Action configure + ) + { + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(configure); + + _ = builder + .Services.AddOptions(builder.Name ?? Options.DefaultName) + .Configure(configure); + + return builder; + } +} diff --git a/ObsWebSocket.Core/ObsWebSocketHosting.cs b/ObsWebSocket.Core/ObsWebSocketHosting.cs index a1a3d2f..2f29c00 100644 --- a/ObsWebSocket.Core/ObsWebSocketHosting.cs +++ b/ObsWebSocket.Core/ObsWebSocketHosting.cs @@ -13,25 +13,44 @@ namespace ObsWebSocket.Core; /// The client to manage. /// Monitored options, watched for endpoint changes. /// Logger for connection outcomes. +/// The key this client is registered under, or null for the unnamed client. internal sealed class ObsWebSocketConnectionService( ObsWebSocketClient client, IOptionsMonitor options, - ILogger logger + ILogger logger, + string? name = null ) : IHostedService, IDisposable { private IDisposable? _optionsWatch; private (Uri? Uri, string? Password, SerializationFormat Format) _connectedWith; + private string OptionsName => name ?? Options.DefaultName; + /// public async Task StartAsync(CancellationToken cancellationToken) { - _optionsWatch = options.OnChange(OnOptionsChanged); + _optionsWatch = options.OnChange( + (updated, changedName) => + { + // OnChange fires for every named instance, so ignore the ones that are not ours. + if ( + string.Equals( + changedName ?? Options.DefaultName, + OptionsName, + StringComparison.Ordinal + ) + ) + { + OnOptionsChanged(updated); + } + } + ); await ConnectAsync(cancellationToken).ConfigureAwait(false); } private async Task ConnectAsync(CancellationToken cancellationToken) { - ObsWebSocketClientOptions current = options.CurrentValue; + ObsWebSocketClientOptions current = options.Get(OptionsName); _connectedWith = (current.ServerUri, current.Password, current.Format); try @@ -135,6 +154,9 @@ public static class ObsWebSocketHostingExtensions /// /// The service collection the client was added to. /// The same collection, for chaining. + [Obsolete( + "Chain WithAutoConnect off AddObsWebSocketClient instead, which also works for a named client. This forwarder will be removed in a future release." + )] public static IServiceCollection WithAutoConnect(this IServiceCollection services) { ArgumentNullException.ThrowIfNull(services); @@ -153,9 +175,9 @@ public static IServiceCollection WithAutoConnect(this IServiceCollection service /// The host application builder. /// The connection string name, also the key for the client. /// An optional action to configure the remaining options. - /// The same builder, for chaining. + /// A builder for configuring this client. /// Thrown if the connection string is missing. - public static IHostApplicationBuilder AddObsWebSocketClient( + public static IObsWebSocketClientBuilder AddObsWebSocketClient( this IHostApplicationBuilder builder, string connectionName, Action? configureOptions = null @@ -170,13 +192,11 @@ public static IHostApplicationBuilder AddObsWebSocketClient( $"No connection string named '{connectionName}' was found. Add ConnectionStrings:{connectionName}, for example ws://localhost:4455." ); - _ = builder.Services.AddObsWebSocketClient(options => + return builder.Services.AddObsWebSocketClient(options => { ApplyConnectionString(options, connectionString); configureOptions?.Invoke(options); }); - - return builder; } /// diff --git a/ObsWebSocket.Core/ObsWebSocketServiceCollectionExtensions.cs b/ObsWebSocket.Core/ObsWebSocketServiceCollectionExtensions.cs index 80a53bd..bd7c019 100644 --- a/ObsWebSocket.Core/ObsWebSocketServiceCollectionExtensions.cs +++ b/ObsWebSocket.Core/ObsWebSocketServiceCollectionExtensions.cs @@ -18,9 +18,9 @@ public static class ObsWebSocketServiceCollectionExtensions /// /// The to add the services to. /// An optional action to configure the . - /// The original for chaining. + /// A builder for configuring this client. /// Thrown if is null. - public static IServiceCollection AddObsWebSocketClient( + public static IObsWebSocketClientBuilder AddObsWebSocketClient( this IServiceCollection services, Action? configureOptions = null ) @@ -97,7 +97,7 @@ public static IServiceCollection AddObsWebSocketClient( ); }); - return services; + return new ObsWebSocketClientBuilder(services, name: null); } /// @@ -108,10 +108,10 @@ public static IServiceCollection AddObsWebSocketClient( /// The to add the services to. /// The key identifying this client. /// An optional action to configure this client's options. - /// The original for chaining. + /// A builder for configuring this client. /// Thrown if is null. /// Thrown if is null or empty. - public static IServiceCollection AddObsWebSocketClient( + public static IObsWebSocketClientBuilder AddObsWebSocketClient( this IServiceCollection services, string name, Action? configureOptions = null @@ -166,7 +166,7 @@ public static IServiceCollection AddObsWebSocketClient( } ); - return services; + return new ObsWebSocketClientBuilder(services, name); } } diff --git a/ObsWebSocket.Example/Worker.cs b/ObsWebSocket.Example/Worker.cs index 0157c15..1d2c59a 100644 --- a/ObsWebSocket.Example/Worker.cs +++ b/ObsWebSocket.Example/Worker.cs @@ -2250,6 +2250,116 @@ await client // .ConfigureAwait(false) // ); + results.Add( + await TrySettingsCheckAsync( + "Integer fields round trip", + async () => + { + // The protocol calls every number "Number", so these fields used to + // arrive as double. Writing one and reading it back proves the + // retype survives the wire in both directions, which matters most + // for MessagePack, where an int and a float are different encodings. + int itemId = + await client + .SceneItems.FindSceneItemIdAsync( + sceneName, + inputName, + cancellationToken + ) + .ConfigureAwait(false) + ?? throw new InvalidOperationException( + $"'{inputName}' is not in '{sceneName}'." + ); + + GetSceneItemIndexResponseData originalIndex = await client + .SceneItems.GetSceneItemIndexAsync( + new GetSceneItemIndexRequestData( + sceneItemId: itemId, + sceneName: sceneName + ), + cancellationToken + ) + .ConfigureAwait(false); + + await client + .SceneItems.SetSceneItemIndexAsync( + new SetSceneItemIndexRequestData( + sceneItemId: itemId, + sceneItemIndex: 0, + sceneName: sceneName + ), + cancellationToken + ) + .ConfigureAwait(false); + + GetSceneItemIndexResponseData afterIndex = await client + .SceneItems.GetSceneItemIndexAsync( + new GetSceneItemIndexRequestData( + sceneItemId: itemId, + sceneName: sceneName + ), + cancellationToken + ) + .ConfigureAwait(false); + + await client + .SceneItems.SetSceneItemIndexAsync( + new SetSceneItemIndexRequestData( + sceneItemId: itemId, + sceneItemIndex: originalIndex.SceneItemIndex, + sceneName: sceneName + ), + cancellationToken + ) + .ConfigureAwait(false); + + // A negative value, since OBS accepts negative sync offsets and a + // sign error would otherwise go unnoticed. + GetInputAudioSyncOffsetResponseData originalOffset = await client + .Inputs.GetInputAudioSyncOffsetAsync( + new GetInputAudioSyncOffsetRequestData(inputName: inputName), + cancellationToken + ) + .ConfigureAwait(false); + + await client + .Inputs.SetInputAudioSyncOffsetAsync( + new SetInputAudioSyncOffsetRequestData( + inputAudioSyncOffset: -125, + inputName: inputName + ), + cancellationToken + ) + .ConfigureAwait(false); + + GetInputAudioSyncOffsetResponseData afterOffset = await client + .Inputs.GetInputAudioSyncOffsetAsync( + new GetInputAudioSyncOffsetRequestData(inputName: inputName), + cancellationToken + ) + .ConfigureAwait(false); + + await client + .Inputs.SetInputAudioSyncOffsetAsync( + new SetInputAudioSyncOffsetRequestData( + inputAudioSyncOffset: originalOffset.InputAudioSyncOffset, + inputName: inputName + ), + cancellationToken + ) + .ConfigureAwait(false); + + return ( + afterIndex.SceneItemIndex == 0 + && afterOffset.InputAudioSyncOffset == -125, + $"index {originalIndex.SceneItemIndex} -> {afterIndex.SceneItemIndex}, " + + $"syncOffset {originalOffset.InputAudioSyncOffset} -> {afterOffset.InputAudioSyncOffset}" + ); + } + ) + .ConfigureAwait(false) + ); + results.Add( await TrySettingsCheckAsync( "Typed exception on a rejected request", diff --git a/ObsWebSocket.Tests/HostingTests.cs b/ObsWebSocket.Tests/HostingTests.cs index db4b1a2..403e8ed 100644 --- a/ObsWebSocket.Tests/HostingTests.cs +++ b/ObsWebSocket.Tests/HostingTests.cs @@ -96,13 +96,14 @@ public async Task WithAutoConnect_WhenObsIsUnreachable_DoesNotPreventStartup() // survivable rather than fatal. HostApplicationBuilder builder = Host.CreateApplicationBuilder(); builder.Logging.SetMinimumLevel(LogLevel.Critical); - _ = builder.Services.AddObsWebSocketClient(o => - { - o.ServerUri = new Uri("ws://127.0.0.1:59999"); - o.AutoReconnectEnabled = false; - o.HandshakeTimeoutMs = 200; - }); - _ = builder.Services.WithAutoConnect(); + _ = builder + .Services.AddObsWebSocketClient(o => + { + o.ServerUri = new Uri("ws://127.0.0.1:59999"); + o.AutoReconnectEnabled = false; + o.HandshakeTimeoutMs = 200; + }) + .WithAutoConnect(); using IHost host = builder.Build(); diff --git a/ObsWebSocket.Tests/ObsWebSocketClientConnectionTests.cs b/ObsWebSocket.Tests/ObsWebSocketClientConnectionTests.cs index 76d14a5..36775f3 100644 --- a/ObsWebSocket.Tests/ObsWebSocketClientConnectionTests.cs +++ b/ObsWebSocket.Tests/ObsWebSocketClientConnectionTests.cs @@ -808,7 +808,7 @@ await connectTask } [TestMethod] - [Timeout(3000)] // Slightly increased timeout + [Timeout(TestTimeout)] public async Task ConnectAsync_InfiniteRetries_AttemptsMultipleTimes() { // Arrange @@ -830,11 +830,18 @@ public async Task ConnectAsync_InfiniteRetries_AttemptsMultipleTimes() int attemptCounter = 0; object counterLock = new(); + TaskCompletionSource attemptsReached = new( + TaskCreationOptions.RunContinuationsAsynchronously + ); client.Connecting += (_, _) => { lock (counterLock) { attemptCounter++; + if (attemptCounter >= minExpectedAttempts) + { + _ = attemptsReached.TrySetResult(); + } } }; @@ -861,12 +868,13 @@ public async Task ConnectAsync_InfiniteRetries_AttemptsMultipleTimes() // Act Task connectTask = client.ConnectAsync(cts.Token); // Pass the cancellation token - // Advance the fake clock until enough attempts have occurred or the task completes. + // Advance the fake clock until enough attempts have occurred, waiting on a signal rather + // than a fixed iteration count. The reconnect delay comes from the fake clock, but the + // continuation after it still needs the scheduler, so each advance is followed by a real + // yield. Bounded so a regression fails here with a count rather than hanging. for ( int i = 0; - i < 2000 - && Volatile.Read(ref attemptCounter) < minExpectedAttempts - && !connectTask.IsCompleted; + i < 200 && !attemptsReached.Task.IsCompleted && !connectTask.IsCompleted; i++ ) { diff --git a/ObsWebSocket.Tests/ReadmeCompileCheck.cs b/ObsWebSocket.Tests/ReadmeCompileCheck.cs index 15807d8..3365c63 100644 --- a/ObsWebSocket.Tests/ReadmeCompileCheck.cs +++ b/ObsWebSocket.Tests/ReadmeCompileCheck.cs @@ -298,9 +298,7 @@ internal static void HostIntegration( Microsoft.Extensions.Hosting.IHostApplicationBuilder builder ) { - _ = builder.AddObsWebSocketClient("obs"); - _ = builder.Services.WithAutoConnect(); - _ = builder.Services.AddHealthChecks().AddObsWebSocket(); + _ = builder.AddObsWebSocketClient("obs").WithAutoConnect().WithHealthCheck(); } internal static void TelemetryAndKeyedRegistration(IServiceCollection services) diff --git a/README.md b/README.md index 71dfb81..045fe55 100644 --- a/README.md +++ b/README.md @@ -39,8 +39,8 @@ using ObsWebSocket.Core; HostApplicationBuilder builder = Host.CreateApplicationBuilder(args); -builder.AddObsWebSocketClient("obs"); // endpoint from ConnectionStrings:obs -builder.Services.WithAutoConnect(); // connect on start, disconnect on stop +builder.AddObsWebSocketClient("obs") // endpoint from ConnectionStrings:obs + .WithAutoConnect(); // connect on start, disconnect on stop builder.Services.AddHostedService(); await builder.Build().RunAsync(); @@ -284,9 +284,9 @@ and `ToWireValue()` converts an enum back. ## Host integration ```csharp -builder.AddObsWebSocketClient("obs"); // reads ConnectionStrings:obs -builder.Services.WithAutoConnect(); // connects on start, disconnects on stop -builder.Services.AddHealthChecks().AddObsWebSocket(); +builder.AddObsWebSocketClient("obs") // reads ConnectionStrings:obs + .WithAutoConnect() // connects on start, disconnects on stop + .WithHealthCheck(); ``` The password may travel in the connection string or be set on the options; either way it is kept off From 9a1825b27c69173d6b0308dc128f9f236008ec68 Mon Sep 17 00:00:00 2001 From: Agash Date: Wed, 26 Aug 2026 08:35:31 +0200 Subject: [PATCH 11/12] refactor(core)!: name the enum and the scene helpers for what they are 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. --- .../Generation/Emitter.cs | 21 ++++- ObsWebSocket.Core/BatchResultExtensions.cs | 7 ++ ....Enum.g.cs => RequestStatusCode.Enum.g.cs} | 4 +- ObsWebSocket.Core/Groups/ConfigGroup.cs | 9 +- ObsWebSocket.Core/Groups/FiltersGroup.cs | 3 +- ObsWebSocket.Core/Groups/InputsGroup.cs | 3 +- ObsWebSocket.Core/Groups/SceneItemsGroup.cs | 3 +- ObsWebSocket.Core/Groups/ScenesGroup.cs | 84 +++++++++++++++++-- ObsWebSocket.Core/Groups/SourcesGroup.cs | 3 +- .../ObsWebSocketRequestException.cs | 13 ++- ObsWebSocket.Example/Worker.cs | 6 +- .../ObsWebSocketClientConnectionTests.cs | 42 ++++++---- .../ObsWebSocketClientRequestTests.cs | 8 +- ObsWebSocket.Tests/ObsWebSocketDiTests.cs | 5 +- ObsWebSocket.Tests/ReadmeCompileCheck.cs | 6 +- ObsWebSocket.Tests/TypedSettingsTests.cs | 24 +++--- 16 files changed, 169 insertions(+), 72 deletions(-) rename ObsWebSocket.Core/Generated/Protocol/Generated/{RequestStatus.Enum.g.cs => RequestStatusCode.Enum.g.cs} (98%) diff --git a/ObsWebSocket.Codegen.Tasks/Generation/Emitter.cs b/ObsWebSocket.Codegen.Tasks/Generation/Emitter.cs index 3a61ae0..f05d358 100644 --- a/ObsWebSocket.Codegen.Tasks/Generation/Emitter.cs +++ b/ObsWebSocket.Codegen.Tasks/Generation/Emitter.cs @@ -48,8 +48,12 @@ public static void GenerateEnums(SourceProductionContext context, ProtocolDefini { string suffix = valueKind == EnumValueKind.Numeric ? ".Enum.g.cs" : ".Class.g.cs"; + string fileStem = + valueKind == EnumValueKind.Numeric + ? MapEnumTypeName(SanitizeIdentifier(enumDef.EnumType)) + : SanitizeIdentifier(enumDef.EnumType); context.AddSource( - $"{SanitizeIdentifier(enumDef.EnumType)}{suffix}", + $"{fileStem}{suffix}", SourceText.From(source, Encoding.UTF8) ); } @@ -88,7 +92,7 @@ public static void GenerateEnums(SourceProductionContext context, ProtocolDefini /// private static string? GenerateNumericEnumSource(EnumDefinition enumDef, string underlyingType) { - string enumName = SanitizeIdentifier(enumDef.EnumType); + string enumName = MapEnumTypeName(SanitizeIdentifier(enumDef.EnumType)); StringBuilder builder = BuildSourceHeader($"// Type: Numeric Enum ({underlyingType})"); builder.AppendLine($"namespace {GeneratedEnumsNamespace};"); builder.AppendLine(); @@ -222,6 +226,19 @@ EnumDefinition enumDef return builder.ToString(); } + /// + /// Renames protocol enums whose name already belongs to a message type, so a caller writing a + /// catch filter does not have to disambiguate two types called RequestStatus in + /// neighbouring namespaces. + /// + private static string MapEnumTypeName(string enumTypeName) => + enumTypeName switch + { + // Protocol.RequestStatus is the record carried on a response; this is the code on it. + "RequestStatus" => "RequestStatusCode", + _ => enumTypeName, + }; + /// /// Drops the leading Obs from a protocol enum type name, so ObsMediaInputAction /// yields MediaInputAction and does not collide with the string-constant class. diff --git a/ObsWebSocket.Core/BatchResultExtensions.cs b/ObsWebSocket.Core/BatchResultExtensions.cs index bb262bb..c7e40c3 100644 --- a/ObsWebSocket.Core/BatchResultExtensions.cs +++ b/ObsWebSocket.Core/BatchResultExtensions.cs @@ -11,6 +11,13 @@ namespace ObsWebSocket.Core; /// /// returns results whose payloads are transport-shaped, because a batch may mix request types. /// These helpers turn a result into the response record for its request. +/// +/// They exist for that low level path. A batch built with comes back +/// as , which addresses results by the reference the builder handed out +/// and carries its own AllSucceeded and GetFailures. Those members win over the +/// extensions of the same name here, so reaching for a builder-built batch gets the typed path +/// either way; prefer it, and use these when holding a raw result list. +/// /// public static class BatchResultExtensions { diff --git a/ObsWebSocket.Core/Generated/Protocol/Generated/RequestStatus.Enum.g.cs b/ObsWebSocket.Core/Generated/Protocol/Generated/RequestStatusCode.Enum.g.cs similarity index 98% rename from ObsWebSocket.Core/Generated/Protocol/Generated/RequestStatus.Enum.g.cs rename to ObsWebSocket.Core/Generated/Protocol/Generated/RequestStatusCode.Enum.g.cs index 83ede42..982201e 100644 --- a/ObsWebSocket.Core/Generated/Protocol/Generated/RequestStatus.Enum.g.cs +++ b/ObsWebSocket.Core/Generated/Protocol/Generated/RequestStatusCode.Enum.g.cs @@ -5,10 +5,10 @@ namespace ObsWebSocket.Core.Protocol.Generated; /// -/// Represents the RequestStatus options defined in the OBS WebSocket protocol. +/// Represents the RequestStatusCode options defined in the OBS WebSocket protocol. /// /// Generated from OBS WebSocket Protocol definition. -public enum RequestStatus : int +public enum RequestStatusCode : int { /// /// Unknown status, should never be used. diff --git a/ObsWebSocket.Core/Groups/ConfigGroup.cs b/ObsWebSocket.Core/Groups/ConfigGroup.cs index 74201ec..db8d7cf 100644 --- a/ObsWebSocket.Core/Groups/ConfigGroup.cs +++ b/ObsWebSocket.Core/Groups/ConfigGroup.cs @@ -11,7 +11,6 @@ using ObsWebSocket.Core.Protocol.Generated; using ObsWebSocket.Core.Protocol.Requests; using ObsWebSocket.Core.Protocol.Responses; -using ObsRequestStatus = ObsWebSocket.Core.Protocol.Generated.RequestStatus; namespace ObsWebSocket.Core; @@ -169,8 +168,8 @@ await client // OBS answers a name it does not know with either status, depending on the request. catch (ObsWebSocketRequestException ex) when (ex.StatusCode - is ObsRequestStatus.ResourceNotFound - or ObsRequestStatus.InvalidRequestField + is RequestStatusCode.ResourceNotFound + or RequestStatusCode.InvalidRequestField ) { client._logger.LogWarning( @@ -227,8 +226,8 @@ await client // OBS answers a name it does not know with either status, depending on the request. catch (ObsWebSocketRequestException ex) when (ex.StatusCode - is ObsRequestStatus.ResourceNotFound - or ObsRequestStatus.InvalidRequestField + is RequestStatusCode.ResourceNotFound + or RequestStatusCode.InvalidRequestField ) { client._logger.LogWarning( diff --git a/ObsWebSocket.Core/Groups/FiltersGroup.cs b/ObsWebSocket.Core/Groups/FiltersGroup.cs index 29d998d..f70830c 100644 --- a/ObsWebSocket.Core/Groups/FiltersGroup.cs +++ b/ObsWebSocket.Core/Groups/FiltersGroup.cs @@ -11,7 +11,6 @@ using ObsWebSocket.Core.Protocol.Generated; using ObsWebSocket.Core.Protocol.Requests; using ObsWebSocket.Core.Protocol.Responses; -using ObsRequestStatus = ObsWebSocket.Core.Protocol.Generated.RequestStatus; namespace ObsWebSocket.Core; @@ -56,7 +55,7 @@ public readonly partial struct FiltersGroup .ConfigureAwait(false); } catch (ObsWebSocketRequestException ex) - when (ex.StatusCode is ObsRequestStatus.ResourceNotFound) + when (ex.StatusCode is RequestStatusCode.ResourceNotFound) { return null; } diff --git a/ObsWebSocket.Core/Groups/InputsGroup.cs b/ObsWebSocket.Core/Groups/InputsGroup.cs index 315b8b5..e68272b 100644 --- a/ObsWebSocket.Core/Groups/InputsGroup.cs +++ b/ObsWebSocket.Core/Groups/InputsGroup.cs @@ -11,7 +11,6 @@ using ObsWebSocket.Core.Protocol.Generated; using ObsWebSocket.Core.Protocol.Requests; using ObsWebSocket.Core.Protocol.Responses; -using ObsRequestStatus = ObsWebSocket.Core.Protocol.Generated.RequestStatus; namespace ObsWebSocket.Core; @@ -145,7 +144,7 @@ public async Task SetInputMutesAsync( .ConfigureAwait(false); } catch (ObsWebSocketRequestException ex) - when (ex.StatusCode is ObsRequestStatus.ResourceNotFound) + when (ex.StatusCode is RequestStatusCode.ResourceNotFound) { return null; } diff --git a/ObsWebSocket.Core/Groups/SceneItemsGroup.cs b/ObsWebSocket.Core/Groups/SceneItemsGroup.cs index 0d551b7..9c682dc 100644 --- a/ObsWebSocket.Core/Groups/SceneItemsGroup.cs +++ b/ObsWebSocket.Core/Groups/SceneItemsGroup.cs @@ -11,7 +11,6 @@ using ObsWebSocket.Core.Protocol.Generated; using ObsWebSocket.Core.Protocol.Requests; using ObsWebSocket.Core.Protocol.Responses; -using ObsRequestStatus = ObsWebSocket.Core.Protocol.Generated.RequestStatus; namespace ObsWebSocket.Core; @@ -149,7 +148,7 @@ public async Task SetSceneItemEnabledAsync( return response?.SceneItemId; } catch (ObsWebSocketRequestException ex) - when (ex.StatusCode is ObsRequestStatus.ResourceNotFound) + when (ex.StatusCode is RequestStatusCode.ResourceNotFound) { // Item or scene not found, which is the expected 'failure' for a 'TryGet' pattern return null; diff --git a/ObsWebSocket.Core/Groups/ScenesGroup.cs b/ObsWebSocket.Core/Groups/ScenesGroup.cs index 6ead653..4ffc9f7 100644 --- a/ObsWebSocket.Core/Groups/ScenesGroup.cs +++ b/ObsWebSocket.Core/Groups/ScenesGroup.cs @@ -30,7 +30,7 @@ public readonly partial struct ScenesGroup /// A token to cancel the operation. /// Thrown if OBS fails to perform any step (e.g., scene/transition not found). /// Thrown if the client is not connected. - public async Task SwitchSceneAsync( + private async Task SwitchSceneCoreAsync( string sceneName, string? transitionName = null, int? transitionDurationMs = null, @@ -107,7 +107,7 @@ await client /// Thrown if the expected event confirming the switch completion is not received within the timeout period. /// Thrown if the client is not connected, or if trying to switch Preview scene when Studio Mode is disabled. /// Thrown if the operation is canceled via the cancellationToken. - public async Task SwitchSceneAndWaitAsync( + private async Task SwitchSceneAndWaitCoreAsync( string sceneName, string? transitionName = null, int? transitionDurationMs = null, @@ -158,8 +158,7 @@ public async Task SwitchSceneAndWaitAsync( { // Trigger the scene switch using the non-waiting helper // This call happens *after* WaitForEventAsync has set up its subscription - await client - .Scenes.SwitchSceneAsync( + await SwitchSceneCoreAsync( sceneName: sceneName, transitionName: switchToProgram ? transitionName : null, transitionDurationMs: switchToProgram ? transitionDurationMs : null, @@ -247,7 +246,7 @@ public Task SwitchProgramSceneAsync( int? transitionDurationMs = null, CancellationToken cancellationToken = default ) => - client.Scenes.SwitchSceneAsync( + SwitchSceneCoreAsync( sceneName, transitionName, transitionDurationMs, @@ -262,7 +261,7 @@ public Task SwitchPreviewSceneAsync( string sceneName, CancellationToken cancellationToken = default ) => - client.Scenes.SwitchSceneAsync( + SwitchSceneCoreAsync( sceneName, switchToProgram: false, cancellationToken: cancellationToken @@ -278,10 +277,81 @@ public Task SwitchProgramSceneAndWaitAsync( TimeSpan? timeout = null, CancellationToken cancellationToken = default ) => - client.Scenes.SwitchSceneAndWaitAsync( + SwitchSceneAndWaitCoreAsync( sceneName, switchToProgram: true, timeout: timeout, cancellationToken: cancellationToken ); + + /// Switches the Preview scene and waits for OBS to confirm it. Requires Studio Mode. + /// The scene to switch to. + /// How long to wait for confirmation. + /// A token to cancel the operation. + public Task SwitchPreviewSceneAndWaitAsync( + string sceneName, + TimeSpan? timeout = null, + CancellationToken cancellationToken = default + ) => + SwitchSceneAndWaitCoreAsync( + sceneName, + switchToProgram: false, + timeout: timeout, + cancellationToken: cancellationToken + ); + + /// + /// Switches the Program or Preview scene, depending on . + /// + /// The scene to switch to. + /// Optional transition to use for this switch only. + /// Optional transition duration for this switch only. + /// Program when true, Preview when false. + /// A token to cancel the operation. + [Obsolete( + "Call SwitchProgramSceneAsync or SwitchPreviewSceneAsync, which say which scene they switch. This forwarder will be removed in a future release." + )] + public Task SwitchSceneAsync( + string sceneName, + string? transitionName = null, + int? transitionDurationMs = null, + bool switchToProgram = true, + CancellationToken cancellationToken = default + ) => + SwitchSceneCoreAsync( + sceneName, + transitionName, + transitionDurationMs, + switchToProgram, + cancellationToken + ); + + /// + /// Switches the Program or Preview scene and waits for OBS to confirm it. + /// + /// The scene to switch to. + /// Optional transition to use for this switch only. + /// Optional transition duration for this switch only. + /// Program when true, Preview when false. + /// How long to wait for confirmation. + /// A token to cancel the operation. + [Obsolete( + "Call SwitchProgramSceneAndWaitAsync or SwitchPreviewSceneAndWaitAsync, which say which scene they switch. This forwarder will be removed in a future release." + )] + public Task SwitchSceneAndWaitAsync( + string sceneName, + string? transitionName = null, + int? transitionDurationMs = null, + bool switchToProgram = true, + TimeSpan? timeout = null, + CancellationToken cancellationToken = default + ) => + SwitchSceneAndWaitCoreAsync( + sceneName, + transitionName, + transitionDurationMs, + switchToProgram, + timeout, + cancellationToken + ); } diff --git a/ObsWebSocket.Core/Groups/SourcesGroup.cs b/ObsWebSocket.Core/Groups/SourcesGroup.cs index 599bafa..898a9dc 100644 --- a/ObsWebSocket.Core/Groups/SourcesGroup.cs +++ b/ObsWebSocket.Core/Groups/SourcesGroup.cs @@ -11,7 +11,6 @@ using ObsWebSocket.Core.Protocol.Generated; using ObsWebSocket.Core.Protocol.Requests; using ObsWebSocket.Core.Protocol.Responses; -using ObsRequestStatus = ObsWebSocket.Core.Protocol.Generated.RequestStatus; namespace ObsWebSocket.Core; @@ -120,7 +119,7 @@ await client .ConfigureAwait(false); } catch (ObsWebSocketRequestException ex) - when (ex.StatusCode is ObsRequestStatus.ResourceNotFound) + when (ex.StatusCode is RequestStatusCode.ResourceNotFound) { client._logger.LogWarning( "Source '{SourceName}' not found for screenshot.", diff --git a/ObsWebSocket.Core/ObsWebSocketRequestException.cs b/ObsWebSocket.Core/ObsWebSocketRequestException.cs index a7030ff..dbfe5a9 100644 --- a/ObsWebSocket.Core/ObsWebSocketRequestException.cs +++ b/ObsWebSocket.Core/ObsWebSocketRequestException.cs @@ -1,5 +1,5 @@ using ObsWebSocket.Core.Protocol; -using ObsRequestStatus = ObsWebSocket.Core.Protocol.Generated.RequestStatus; +using ObsWebSocket.Core.Protocol.Generated; namespace ObsWebSocket.Core; @@ -56,18 +56,17 @@ public ObsWebSocketRequestException(string message, Exception innerException) public string? Comment { get; } /// - /// The status OBS reported as , - /// so a handler can match on the reason rather than on the text of - /// . Note that the enum and the record - /// share the name RequestStatus in different namespaces. + /// The status OBS reported as + /// , so a handler can + /// match on the reason rather than on the text of . /// /// /// /// catch (ObsWebSocketRequestException ex) - /// when (ex.StatusCode is RequestStatus.ResourceNotFound) { } + /// when (ex.StatusCode is RequestStatusCode.ResourceNotFound) { } /// /// - public ObsRequestStatus? StatusCode => Status is null ? null : (ObsRequestStatus)Status.Code; + public RequestStatusCode? StatusCode => Status is null ? null : (RequestStatusCode)Status.Code; } /// diff --git a/ObsWebSocket.Example/Worker.cs b/ObsWebSocket.Example/Worker.cs index 1d2c59a..3ab2246 100644 --- a/ObsWebSocket.Example/Worker.cs +++ b/ObsWebSocket.Example/Worker.cs @@ -1532,7 +1532,7 @@ CurrentProgramSceneChangedEventArgs sceneEvent in client await Task.Delay(250, cancellationToken).ConfigureAwait(false); await client - .Scenes.SwitchSceneAsync( + .Scenes.SwitchProgramSceneAsync( sceneName, cancellationToken: cancellationToken ) @@ -1541,7 +1541,7 @@ await client if (!string.IsNullOrEmpty(originalScene)) { await client - .Scenes.SwitchSceneAsync( + .Scenes.SwitchProgramSceneAsync( originalScene, cancellationToken: cancellationToken ) @@ -2437,7 +2437,7 @@ await TrySettingsCheckAsync( if (!string.IsNullOrEmpty(originalScene)) { await client - .Scenes.SwitchSceneAsync( + .Scenes.SwitchProgramSceneAsync( originalScene, cancellationToken: CancellationToken.None ) diff --git a/ObsWebSocket.Tests/ObsWebSocketClientConnectionTests.cs b/ObsWebSocket.Tests/ObsWebSocketClientConnectionTests.cs index 36775f3..295b10f 100644 --- a/ObsWebSocket.Tests/ObsWebSocketClientConnectionTests.cs +++ b/ObsWebSocket.Tests/ObsWebSocketClientConnectionTests.cs @@ -720,13 +720,20 @@ public async Task DisconnectAsync_DuringRetry_StopsRetriesAndDisconnectsGraceful { // Arrange WebSocketException connectException = new("Server unavailable"); + // The retry delay comes from the fake clock, so the client is definitively still waiting + // it out when DisconnectAsync is called. Racing a real delay makes the condition this + // test is about depend on how loaded the machine is. + FakeTimeProvider time = new(); (ObsWebSocketClient? client, _, _, Mock? mockFactory) = - BuildMockedInfrastructure(opts => - { - opts.AutoReconnectEnabled = true; - opts.MaxReconnectAttempts = 5; // Allow multiple retries - opts.InitialReconnectDelayMs = 300; // Longer delay to allow disconnect call - }); + BuildMockedInfrastructure( + opts => + { + opts.AutoReconnectEnabled = true; + opts.MaxReconnectAttempts = 5; // Allow multiple retries + opts.InitialReconnectDelayMs = 300; + }, + time + ); List eventLog = []; Exception? disconnectedReason = new("Placeholder"); // Start non-null for check later (Simplified 'new') @@ -776,20 +783,21 @@ public async Task DisconnectAsync_DuringRetry_StopsRetriesAndDisconnectsGraceful // Act Task connectTask = client.ConnectAsync(); // Start connection attempts in background - // Wait for the first connection attempt to fail - bool firstFailed = - await Task.WhenAny(firstFailSignal.Task, Task.Delay(TimeSpan.FromSeconds(2))) - == firstFailSignal.Task; - Assert.IsTrue(firstFailed, "First connection attempt did not fail within timeout."); + // Wait for the first connection attempt to fail. The attempt itself does not wait on the + // clock, so this completes without advancing it. + await firstFailSignal + .Task.WaitAsync(TimeSpan.FromSeconds(2)) + .ConfigureAwait(ConfigureAwaitOptions.None); - // Request disconnect *while* the client is likely in the retry delay + // The clock has not moved, so the client is still inside the retry delay here. await client.DisconnectAsync(); - // Wait for the Disconnected event - bool disconnected = - await Task.WhenAny(disconnectedSignal.Task, Task.Delay(TimeSpan.FromSeconds(3))) - == disconnectedSignal.Task; - Assert.IsTrue(disconnected, "Disconnect event did not fire after calling DisconnectAsync."); + // Wait for the Disconnected event. Disconnecting cancels the delay rather than waiting + // it out, so this also needs no clock advance; a regression that waits shows up as the + // timeout below rather than as a pass. + await disconnectedSignal + .Task.WaitAsync(TimeSpan.FromSeconds(3)) + .ConfigureAwait(ConfigureAwaitOptions.None); // Allow original ConnectAsync task to complete/be observed (it should have been cancelled by DisconnectAsync) await connectTask diff --git a/ObsWebSocket.Tests/ObsWebSocketClientRequestTests.cs b/ObsWebSocket.Tests/ObsWebSocketClientRequestTests.cs index faf4103..32551b6 100644 --- a/ObsWebSocket.Tests/ObsWebSocketClientRequestTests.cs +++ b/ObsWebSocket.Tests/ObsWebSocketClientRequestTests.cs @@ -87,7 +87,7 @@ CancellationToken ct RequestId: capturedRequestId, RequestStatus: new RequestStatus( Result: true, - Code: (int)Core.Protocol.Generated.RequestStatus.Success + Code: (int)Core.Protocol.Generated.RequestStatusCode.Success ), ResponseData: rawResponseData.Value // Pass the JsonElement payload ); @@ -205,7 +205,7 @@ CancellationToken ct RequestId: capturedRequestId, RequestStatus: new RequestStatus( Result: true, - Code: (int)Core.Protocol.Generated.RequestStatus.Success + Code: (int)Core.Protocol.Generated.RequestStatusCode.Success ), ResponseData: rawResponseData // null ); @@ -308,7 +308,7 @@ CancellationToken ct RequestId: capturedRequestId, RequestStatus: new RequestStatus( Result: true, - Code: (int)Core.Protocol.Generated.RequestStatus.Success + Code: (int)Core.Protocol.Generated.RequestStatusCode.Success ), ResponseData: rawResponseData.Value // Pass JsonElement payload ); @@ -393,7 +393,7 @@ public async Task RequestExtension_FailureResponse_ThrowsObsWebSocketException() string requestType = "GetVersion"; // Using GetVersion as an example request RequestStatus failureStatus = new( Result: false, - Code: (int)Core.Protocol.Generated.RequestStatus.ResourceNotFound, + Code: (int)Core.Protocol.Generated.RequestStatusCode.ResourceNotFound, Comment: "ResourceNotFound" ); object? rawResponseData = null; // No data s_expectedFailNoRetryLog on failure diff --git a/ObsWebSocket.Tests/ObsWebSocketDiTests.cs b/ObsWebSocket.Tests/ObsWebSocketDiTests.cs index 1acd0e0..6eb804b 100644 --- a/ObsWebSocket.Tests/ObsWebSocketDiTests.cs +++ b/ObsWebSocket.Tests/ObsWebSocketDiTests.cs @@ -265,8 +265,9 @@ public void AddObsWebSocketClient_WithoutConfiguration_UsesDefaults() /// Verifies that omitting ServerUri fails when the client is resolved, rather than later /// when a connection is attempted. /// + // No timeout: nothing here waits on anything, so a wall clock limit only fails the test when + // the machine is busy. [TestMethod] - [Timeout(1000)] public void Resolve_WithoutServerUri_FailsValidation() { ServiceCollection services = CreateServiceCollectionWithLogging(); @@ -274,7 +275,7 @@ public void Resolve_WithoutServerUri_FailsValidation() { opts.Password = "abc"; }); - ServiceProvider provider = services.BuildServiceProvider(); + using ServiceProvider provider = services.BuildServiceProvider(); OptionsValidationException ex = Assert.ThrowsExactly(() => provider.GetRequiredService() diff --git a/ObsWebSocket.Tests/ReadmeCompileCheck.cs b/ObsWebSocket.Tests/ReadmeCompileCheck.cs index 3365c63..392a277 100644 --- a/ObsWebSocket.Tests/ReadmeCompileCheck.cs +++ b/ObsWebSocket.Tests/ReadmeCompileCheck.cs @@ -67,7 +67,7 @@ await client.Inputs.SetInputSettingsAsync( internal static async Task UtilitiesAsync(ObsWebSocketClient client, CancellationToken ct) { - await client.Scenes.SwitchSceneAndWaitAsync("Scene", cancellationToken: ct); + await client.Scenes.SwitchProgramSceneAndWaitAsync("Scene", cancellationToken: ct); _ = await client.Sources.SourceExistsAsync("Source", ct); await client.Filters.CreateSourceFilterAsync( "Source", @@ -126,7 +126,7 @@ internal static async Task UtilitiesExtendedAsync( CancellationToken ct ) { - await client.Scenes.SwitchSceneAsync("Scene", cancellationToken: ct); + await client.Scenes.SwitchProgramSceneAsync("Scene", cancellationToken: ct); _ = await client.SceneItems.SetSceneItemEnabledAsync("Scene", "Source", null, ct); _ = await client.SceneItems.FindSceneItemIdAsync("Scene", "Source", ct); await client.Inputs.SetInputMutesAsync([("Mic", false), ("Desktop Audio", true)], ct); @@ -287,7 +287,7 @@ CancellationToken ct } catch (ObsWebSocketRequestException ex) when (ex.StatusCode - is ObsWebSocket.Core.Protocol.Generated.RequestStatus.ResourceNotFound + is ObsWebSocket.Core.Protocol.Generated.RequestStatusCode.ResourceNotFound ) { // The scene, input or filter does not exist. diff --git a/ObsWebSocket.Tests/TypedSettingsTests.cs b/ObsWebSocket.Tests/TypedSettingsTests.cs index 638ab65..c115a18 100644 --- a/ObsWebSocket.Tests/TypedSettingsTests.cs +++ b/ObsWebSocket.Tests/TypedSettingsTests.cs @@ -250,7 +250,7 @@ CancellationToken _ RequestId: id, RequestStatus: new RequestStatus( Result: true, - Code: (int)Core.Protocol.Generated.RequestStatus.Success + Code: (int)Core.Protocol.Generated.RequestStatusCode.Success ), ResponseData: null ) @@ -323,7 +323,7 @@ CancellationToken _ RequestId: id, RequestStatus: new RequestStatus( Result: true, - Code: (int)Core.Protocol.Generated.RequestStatus.Success + Code: (int)Core.Protocol.Generated.RequestStatusCode.Success ), ResponseData: null ) @@ -412,7 +412,7 @@ CancellationToken _ RequestId: msg.D.RequestId!, RequestStatus: new RequestStatus( Result: true, - Code: (int)Core.Protocol.Generated.RequestStatus.Success + Code: (int)Core.Protocol.Generated.RequestStatusCode.Success ), ResponseData: rawPayload ) @@ -488,7 +488,7 @@ CancellationToken _ RequestId: msg.D.RequestId!, RequestStatus: new RequestStatus( Result: true, - Code: (int)Core.Protocol.Generated.RequestStatus.Success + Code: (int)Core.Protocol.Generated.RequestStatusCode.Success ), ResponseData: rawPayload ) @@ -562,7 +562,7 @@ CancellationToken _ RequestId: id, RequestStatus: new RequestStatus( Result: true, - Code: (int)Core.Protocol.Generated.RequestStatus.Success + Code: (int)Core.Protocol.Generated.RequestStatusCode.Success ), ResponseData: null ) @@ -634,7 +634,7 @@ CancellationToken _ RequestId: id, RequestStatus: new RequestStatus( Result: true, - Code: (int)Core.Protocol.Generated.RequestStatus.Success + Code: (int)Core.Protocol.Generated.RequestStatusCode.Success ), ResponseData: null ) @@ -715,7 +715,7 @@ CancellationToken _ RequestId: msg.D.RequestId!, RequestStatus: new RequestStatus( Result: true, - Code: (int)Core.Protocol.Generated.RequestStatus.Success + Code: (int)Core.Protocol.Generated.RequestStatusCode.Success ), ResponseData: rawPayload ) @@ -792,7 +792,7 @@ CancellationToken _ RequestId: msg.D.RequestId!, RequestStatus: new RequestStatus( Result: true, - Code: (int)Core.Protocol.Generated.RequestStatus.Success + Code: (int)Core.Protocol.Generated.RequestStatusCode.Success ), ResponseData: rawPayload ) @@ -866,7 +866,7 @@ CancellationToken _ RequestId: id, RequestStatus: new RequestStatus( Result: true, - Code: (int)Core.Protocol.Generated.RequestStatus.Success + Code: (int)Core.Protocol.Generated.RequestStatusCode.Success ), ResponseData: rawPayload ) @@ -948,7 +948,7 @@ CancellationToken _ RequestId: id, RequestStatus: new RequestStatus( Result: true, - Code: (int)Core.Protocol.Generated.RequestStatus.Success + Code: (int)Core.Protocol.Generated.RequestStatusCode.Success ), ResponseData: rawPayload ) @@ -1026,7 +1026,7 @@ CancellationToken _ RequestId: id, RequestStatus: new RequestStatus( Result: true, - Code: (int)Core.Protocol.Generated.RequestStatus.Success + Code: (int)Core.Protocol.Generated.RequestStatusCode.Success ), ResponseData: null ) @@ -1098,7 +1098,7 @@ CancellationToken _ RequestId: id, RequestStatus: new RequestStatus( Result: true, - Code: (int)Core.Protocol.Generated.RequestStatus.Success + Code: (int)Core.Protocol.Generated.RequestStatusCode.Success ), ResponseData: null ) From 1cfd72b62033a09d73569d6ff77c0b01e5a8a3f3 Mon Sep 17 00:00:00 2001 From: Agash Date: Wed, 26 Aug 2026 08:52:40 +0200 Subject: [PATCH 12/12] docs: bring the example and README onto the current surface 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. --- ObsWebSocket.Example/Program.cs | 3 +- ObsWebSocket.Example/Worker.cs | 10 +++++-- ObsWebSocket.Tests/ReadmeCompileCheck.cs | 19 +++++++++++-- README.md | 36 ++++++++++++++++++++---- 4 files changed, 55 insertions(+), 13 deletions(-) diff --git a/ObsWebSocket.Example/Program.cs b/ObsWebSocket.Example/Program.cs index 7dbd630..6bfb95e 100644 --- a/ObsWebSocket.Example/Program.cs +++ b/ObsWebSocket.Example/Program.cs @@ -34,8 +34,7 @@ // Add the ObsWebSocketClient and its dependencies. The Worker drives the connection itself so // that it can demonstrate connect and disconnect, which is why WithAutoConnect is not used here; // an ordinary application would call it and skip the ceremony. -builder.Services.AddObsWebSocketClient(); -builder.Services.AddHealthChecks().AddObsWebSocket(); +builder.Services.AddObsWebSocketClient().WithHealthCheck(); // Add our background service builder.Services.AddHostedService(); diff --git a/ObsWebSocket.Example/Worker.cs b/ObsWebSocket.Example/Worker.cs index 3ab2246..dada299 100644 --- a/ObsWebSocket.Example/Worker.cs +++ b/ObsWebSocket.Example/Worker.cs @@ -593,14 +593,16 @@ CurrentProgramSceneChangedEventArgs sceneEvent in _obsClient.Scenes.CurrentProgr // Add remains for anything the generated methods do not cover. _ = exampleBatch.Add("GetStats"); - List> batchResults = ( - await _obsClient.CallBatchAsync( + // BatchResults is itself the list of results, so there is no reason to drop to + // Raw here; keeping it means the typed references stay usable further down. + BatchResults batchResults = await _obsClient + .CallBatchAsync( exampleBatch, executionType: RequestBatchExecutionType.SerialRealtime, haltOnFailure: false, // Continue even if one fails cancellationToken: cancellationToken ) - ).Raw.ToList(); + .ConfigureAwait(false); Table batchTable = new() { @@ -956,6 +958,8 @@ customEvent is not null ); } + // The low level path, on purpose: the raw overload takes request items rather than the + // typed builder, and still works for anyone who needs to hand roll a batch. List> batch = await cycleClient .CallBatchAsync( [new("GetVersion", null), new("GetSceneList", null)], diff --git a/ObsWebSocket.Tests/ReadmeCompileCheck.cs b/ObsWebSocket.Tests/ReadmeCompileCheck.cs index 392a277..5906d2d 100644 --- a/ObsWebSocket.Tests/ReadmeCompileCheck.cs +++ b/ObsWebSocket.Tests/ReadmeCompileCheck.cs @@ -286,14 +286,27 @@ CancellationToken ct await client.SceneItems.GetSceneItemListAsync(new("Missing"), ct); } catch (ObsWebSocketRequestException ex) - when (ex.StatusCode - is ObsWebSocket.Core.Protocol.Generated.RequestStatusCode.ResourceNotFound - ) + when (ex.StatusCode is RequestStatusCode.ResourceNotFound) { // The scene, input or filter does not exist. } } + internal static async Task NumbersAsync(ObsWebSocketClient client, CancellationToken ct) + { + int id = + await client.SceneItems.FindSceneItemIdAsync("Intro", "Logo", ct) + ?? throw new InvalidOperationException(); + await client.SceneItems.SetSceneItemIndexAsync( + new(sceneItemId: id, sceneItemIndex: 0, sceneName: "Intro"), + ct + ); + + long bytes = (await client.Stream.GetStreamStatusAsync(ct)).OutputBytes; + double volume = (await client.Inputs.GetInputVolumeAsync(new("Mic"), ct)).InputVolumeMul; + _ = $"{bytes} {volume}"; + } + internal static void HostIntegration( Microsoft.Extensions.Hosting.IHostApplicationBuilder builder ) diff --git a/README.md b/README.md index 045fe55..964fc22 100644 --- a/README.md +++ b/README.md @@ -281,6 +281,23 @@ await client.MediaInputs.TriggerMediaActionAsync("Stinger", MediaInputAction.Res The wire constants remain available as `const` strings on `ObsOutputState` and `ObsMediaInputAction`, and `ToWireValue()` converts an enum back. +## Numbers + +The protocol has a single `Number` type because JSON does, so a scene item id and a volume +multiplier look identical in the definition. Fields that hold whole numbers are generated as `int` +or `long` rather than `double`: + +```csharp +int id = await client.SceneItems.FindSceneItemIdAsync("Intro", "Logo", ct) ?? throw new(...); +await client.SceneItems.SetSceneItemIndexAsync(new(sceneItemId: id, sceneItemIndex: 0, sceneName: "Intro"), ct); + +long bytes = (await client.Stream.GetStreamStatusAsync(ct)).OutputBytes; +double volume = (await client.Inputs.GetInputVolumeAsync(new("Mic"), ct)).InputVolumeMul; +``` + +Which fields those are is an explicit list in the generator, not a rule over field names, so a +volume can never be silently truncated by a naming coincidence. + ## Host integration ```csharp @@ -316,14 +333,21 @@ startup with the offending option named, rather than on the first connection att Register clients by name and resolve them with `[FromKeyedServices]`: ```csharp -builder.Services.AddObsWebSocketClient("main", o => o.ServerUri = new Uri("ws://localhost:4455")); -builder.Services.AddObsWebSocketClient("booth", o => o.ServerUri = new Uri("ws://booth:4455")); +builder.Services.AddObsWebSocketClient("main", o => o.ServerUri = new Uri("ws://localhost:4455")) + .WithAutoConnect() + .WithHealthCheck(); + +builder.Services.AddObsWebSocketClient("booth", o => o.ServerUri = new Uri("ws://booth:4455")) + .WithAutoConnect(); public sealed class Worker( [FromKeyedServices("main")] ObsWebSocketClient main, [FromKeyedServices("booth")] ObsWebSocketClient booth); ``` +Each client gets its own options instance, its own connection service and a health check named after +its key, so the two do not collide. + ## Errors Failures are typed, so they can be caught by category rather than matched by message: @@ -343,11 +367,13 @@ catch (ObsWebSocketTimeoutException) } ``` -`StatusCode` reports the same status as the protocol's `RequestStatus` enum, so a filter can name -the reason instead of a number: +`StatusCode` reports the status as the `RequestStatusCode` enum, so a filter can name the reason +instead of a number: ```csharp -catch (ObsWebSocketRequestException ex) when (ex.StatusCode is RequestStatus.ResourceNotFound) +using ObsWebSocket.Core.Protocol.Generated; + +catch (ObsWebSocketRequestException ex) when (ex.StatusCode is RequestStatusCode.ResourceNotFound) { // The scene, input or filter does not exist. }