diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml
index 6ce1224..023c835 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.yml
+++ b/.github/ISSUE_TEMPLATE/bug_report.yml
@@ -6,7 +6,7 @@ body:
attributes:
value: |
Thanks for taking the time to file this. Please do not report security
- vulnerabilities here — use a [private advisory](../../security/advisories/new) instead.
+ vulnerabilities here. Use a [private advisory](../../security/advisories/new) instead.
- type: textarea
id: what-happened
attributes:
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 4cee75a..e039b8d 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -111,7 +111,7 @@ Thank you again for your interest in contributing!
- Name tests `{Method}_{Scenario}_{ExpectedResult}`.
- Prefer the purpose-built MSTest assertions (`Assert.HasCount`, `Assert.Contains`,
- `Assert.AreSequenceEqual`) over hand-rolled equality checks — the analyzers will point you at them.
+ `Assert.AreSequenceEqual`) over hand-rolled equality checks; the analyzers will point you at them.
- No `Thread.Sleep`. Use `TaskCompletionSource`, channels, or a fake clock.
- New behaviour needs a test. Bug fixes need a test that fails before the fix.
@@ -124,7 +124,7 @@ fix(webhooks): reject a signature computed over the decoded body
```
Keep the subject under 50 characters and in the imperative mood. Add a body only when the reason for
-the change would not be obvious to the next reader — explain *why*, not *what*.
+the change would not be obvious to the next reader. Explain *why*, not *what*.
One logical change per commit. Rebase rather than merge when updating a branch.
diff --git a/ObsWebSocket.Codegen.Tasks/Generation/Emitter.HandleOverloads.cs b/ObsWebSocket.Codegen.Tasks/Generation/Emitter.HandleOverloads.cs
new file mode 100644
index 0000000..e653f9c
--- /dev/null
+++ b/ObsWebSocket.Codegen.Tasks/Generation/Emitter.HandleOverloads.cs
@@ -0,0 +1,388 @@
+using System.Text;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.Text;
+
+namespace ObsWebSocket.Codegen.Tasks.Generation;
+
+internal static partial class Emitter
+{
+ ///
+ /// Emits, for each kind of thing OBS can address, a type carrying every request about it, with
+ /// the name, uuid and canvas fields already supplied by the handle.
+ ///
+ ///
+ /// These are deliberately not overloads of the existing request methods. A method pair of
+ /// M(XRequestData) and M(XHandle) is ambiguous the moment a caller writes
+ /// M(new(...)), because a target typed new is convertible to either, and that
+ /// idiom is all over the existing surface and the README. Giving the handle form its own type
+ /// keeps both usable.
+ ///
+ /// The set is derived from the field shape rather than listed, so a request added upstream
+ /// that takes a scene appears here the day the definition lands.
+ ///
+ ///
+ public static void GenerateHandleOverloads(
+ SourceProductionContext context,
+ ProtocolDefinition protocol
+ )
+ {
+ if (protocol.Requests is null)
+ {
+ return;
+ }
+
+ // Each request belongs to the thing it is primarily about, which is its first reference.
+ Dictionary<
+ string,
+ List<(RequestDefinition Request, IReadOnlyList Refs)>
+ > byKind = new(StringComparer.Ordinal);
+
+ foreach (RequestDefinition request in protocol.Requests)
+ {
+ IReadOnlyList references = EntityReferenceTable.Find(
+ request.RequestFields
+ );
+ if (references.Count == 0)
+ {
+ continue;
+ }
+
+ string kind = references[0].Kind;
+ if (!byKind.TryGetValue(kind, out var list))
+ {
+ list = [];
+ byKind[kind] = list;
+ }
+
+ list.Add((request, references));
+ }
+
+ StringBuilder builder = new();
+ builder.AppendLine("// ");
+ builder.AppendLine("#nullable enable");
+ builder.AppendLine();
+ builder.AppendLine("using System;");
+ builder.AppendLine("using System.Threading;");
+ builder.AppendLine("using System.Threading.Tasks;");
+ builder.AppendLine("using ObsWebSocket.Core.Protocol.Requests;");
+ builder.AppendLine("using ObsWebSocket.Core.Protocol.Responses;");
+ builder.AppendLine($"using {GeneratedCommonNamespace};");
+ builder.AppendLine();
+ builder.AppendLine($"namespace {ExtensionsNamespace};");
+ builder.AppendLine();
+
+ int emitted = 0;
+ foreach (string kind in byKind.Keys.OrderBy(k => k, StringComparer.Ordinal))
+ {
+ string opsType = OpsTypeName(kind);
+ string handleType = HandleTypeForKind(kind);
+
+ builder.AppendLine("/// ");
+ builder.AppendLine($"/// Every request about one {DescribeKind(kind)}.");
+ builder.AppendLine("/// ");
+ builder.AppendLine(
+ "/// The client these requests are sent on."
+ );
+ builder.AppendLine(
+ $"/// The {DescribeKind(kind)} they are about."
+ );
+ builder.AppendLine(
+ $"public readonly partial struct {opsType}(ObsWebSocketClient client, {handleType} handle)"
+ );
+ builder.AppendLine("{");
+ builder.AppendLine(
+ $" /// The {DescribeKind(kind)} these requests address."
+ );
+ builder.AppendLine($" public {handleType} Handle => handle;");
+ builder.AppendLine();
+
+ var ordered = byKind[kind]
+ .OrderBy(r => r.Request.RequestType, StringComparer.Ordinal)
+ .ToList();
+ Dictionary methodNames = ShortMethodNames(
+ kind,
+ ordered.ConvertAll(r => r.Request.RequestType)
+ );
+
+ foreach ((RequestDefinition request, IReadOnlyList refs) in ordered)
+ {
+ EmitScopedRequest(
+ context,
+ builder,
+ request,
+ refs,
+ methodNames[request.RequestType]
+ );
+ builder.AppendLine();
+ emitted++;
+ }
+
+ builder.AppendLine("}");
+ builder.AppendLine();
+ }
+
+ builder.AppendLine("/// ");
+ builder.AppendLine(
+ "/// Addresses one thing in OBS, so the requests about it need not restate which."
+ );
+ builder.AppendLine("/// ");
+ builder.AppendLine("public static class ObsWebSocketHandleExtensions");
+ builder.AppendLine("{");
+ foreach (string kind in byKind.Keys.OrderBy(k => k, StringComparer.Ordinal))
+ {
+ builder.AppendLine(" extension(ObsWebSocketClient client)");
+ builder.AppendLine(" {");
+ builder.AppendLine(
+ $" /// Every request about one {DescribeKind(kind)}."
+ );
+ builder.AppendLine(
+ $" /// The {DescribeKind(kind)}, which a name or a uuid converts to."
+ );
+ builder.AppendLine(
+ $" public {OpsTypeName(kind)} {AccessorName(kind)}({HandleTypeForKind(kind)} handle) => new(client, handle);"
+ );
+ builder.AppendLine(" }");
+ builder.AppendLine();
+ }
+
+ builder.AppendLine("}");
+ builder.AppendLine();
+ builder.AppendLine($"// Requests reachable through a handle: {emitted}");
+
+ context.AddSource(
+ "ObsWebSocketClient.HandleOverloads.g.cs",
+ SourceText.From(builder.ToString(), Encoding.UTF8)
+ );
+ }
+
+ ///
+ /// The name each request takes on the operations type, with the entity it is already scoped to
+ /// removed.
+ ///
+ ///
+ /// client.SceneItem(logo).SetSceneItemEnabledAsync(false) says scene item twice, once in
+ /// the thing being addressed and once in the verb. Dropping the second reads better and loses
+ /// nothing, because the protocol name stays in the documentation and on the category group.
+ ///
+ /// The whole set is named at once so a collision can be detected: two requests that shorten to
+ /// the same thing both keep their full names rather than one silently shadowing the other. No
+ /// collision exists today; this is here for the refresh that introduces one.
+ ///
+ ///
+ private static Dictionary ShortMethodNames(
+ string kind,
+ List requestTypes
+ )
+ {
+ string[] tokens = kind switch
+ {
+ "scene" => ["Scene"],
+ "input" => ["Input"],
+ "source" => ["Source"],
+ "sceneItem" => ["SceneItem"],
+ "filter" => ["SourceFilter", "Filter"],
+ _ => [],
+ };
+
+ Dictionary shortened = new(StringComparer.Ordinal);
+ Dictionary counts = new(StringComparer.Ordinal);
+
+ foreach (string requestType in requestTypes)
+ {
+ string candidate = requestType;
+ foreach (string token in tokens)
+ {
+ int at = candidate.IndexOf(token, StringComparison.Ordinal);
+ if (at >= 0)
+ {
+ candidate = candidate.Remove(at, token.Length);
+ break;
+ }
+ }
+
+ if (candidate.Length == 0)
+ {
+ candidate = requestType;
+ }
+
+ shortened[requestType] = candidate;
+ counts[candidate] = counts.TryGetValue(candidate, out int n) ? n + 1 : 1;
+ }
+
+ foreach (string requestType in requestTypes)
+ {
+ if (counts[shortened[requestType]] > 1)
+ {
+ shortened[requestType] = requestType;
+ }
+ }
+
+ return shortened;
+ }
+
+ private static string OpsTypeName(string kind) =>
+ kind switch
+ {
+ "sceneItem" => "SceneItemOperations",
+ "filter" => "FilterOperations",
+ _ => ToPascalCase(kind) + "Operations",
+ };
+
+ private static string HandleTypeForKind(string kind) =>
+ kind switch
+ {
+ "sceneItem" => "SceneItemHandle",
+ "filter" => "FilterHandle",
+ _ => EntityReferenceTable.HandleTypeFor(kind)!,
+ };
+
+ private static string AccessorName(string kind) =>
+ kind switch
+ {
+ "sceneItem" => "SceneItem",
+ "filter" => "Filter",
+ _ => ToPascalCase(kind),
+ };
+
+ private static string DescribeKind(string kind) =>
+ kind switch
+ {
+ "sceneItem" => "scene item",
+ _ => kind,
+ };
+
+ private static void EmitScopedRequest(
+ SourceProductionContext context,
+ StringBuilder builder,
+ RequestDefinition request,
+ IReadOnlyList references,
+ string shortName
+ )
+ {
+ string baseName = SanitizeIdentifier(request.RequestType);
+ string methodName = SanitizeIdentifier(shortName) + "Async";
+ string requestDto = $"{GeneratedRequestsNamespace}.{baseName}RequestData";
+ bool hasResponse = request.ResponseFields?.Count > 0;
+ string returnType = hasResponse
+ ? $"Task<{GeneratedResponsesNamespace}.{baseName}ResponseData>"
+ : "Task";
+ string groupName = ToGroupName(request.Category ?? "general");
+
+ HashSet consumed = new(StringComparer.Ordinal);
+ foreach (EntityReference reference in references)
+ {
+ foreach (string field in reference.Fields)
+ {
+ _ = consumed.Add(field);
+ }
+ }
+
+ List> arguments =
+ [
+ .. EntityReferenceTable.ArgumentsFor(references[0], "handle"),
+ ];
+
+ List required = [];
+ List optional = [];
+ List docs = [];
+
+ // A second reference, such as the destination of a duplicated scene item, stays a
+ // parameter: only one thing can be the subject.
+ foreach (EntityReference extra in references.Skip(1))
+ {
+ string paramName = EntityReferenceTable.ParameterNameForReference(extra);
+ required.Add($"{EntityReferenceTable.HandleTypeForReference(extra)} {paramName}");
+ docs.Add(
+ $" /// The {DescribeKind(extra.Kind)} to use."
+ );
+ arguments.AddRange(EntityReferenceTable.ArgumentsFor(extra, paramName));
+ }
+
+ foreach (FieldDefinition field in request.RequestFields!)
+ {
+ if (consumed.Contains(field.ValueName))
+ {
+ continue;
+ }
+
+ (string? csharpType, _) = MapProtocolTypeToCSharp(
+ context,
+ field,
+ $"{baseName}RequestData",
+ reportDiagnostics: false
+ );
+ if (csharpType is null)
+ {
+ continue;
+ }
+
+ string paramName = ToCamelCase(SanitizeIdentifier(ToPascalCase(field.ValueName)));
+ arguments.Add(new(field.ValueName, paramName));
+ docs.Add(
+ $" /// {System.Security.SecurityElement.Escape(FirstLine(field.ValueDescription))}"
+ );
+
+ if (field.ValueOptional == true)
+ {
+ string suffix = csharpType.EndsWith("?", StringComparison.Ordinal) ? "" : "?";
+ optional.Add($"{csharpType}{suffix} {paramName} = null");
+ }
+ else
+ {
+ required.Add($"{csharpType} {paramName}");
+ }
+ }
+
+ builder.AppendLine(" /// ");
+ AppendMultiLineXmlDoc(builder, request.Description, " ///");
+ builder.AppendLine(" /// ");
+ builder.AppendLine(" /// ");
+ builder.AppendLine(
+ $" /// Sends the {request.RequestType} request, with the identity supplied by the handle."
+ );
+ builder.AppendLine(" /// ");
+ foreach (string doc in docs)
+ {
+ builder.AppendLine(doc);
+ }
+
+ builder.AppendLine(
+ " /// A token to cancel the asynchronous operation."
+ );
+ builder.AppendLine(
+ hasResponse
+ ? " /// A task yielding the response data."
+ : " /// A task that completes when OBS has processed the request."
+ );
+
+ List parameters =
+ [
+ .. required,
+ .. optional,
+ "CancellationToken cancellationToken = default",
+ ];
+
+ builder.AppendLine($" public {returnType} {methodName}(");
+ builder.AppendLine(" " + string.Join(",\n ", parameters));
+ builder.AppendLine(" ) =>");
+ builder.AppendLine($" client.{groupName}.{baseName}Async(");
+ builder.AppendLine($" new {requestDto}(");
+ builder.AppendLine(
+ " "
+ + string.Join(
+ ",\n ",
+ arguments.Select(a =>
+ $"{ToCamelCase(SanitizeIdentifier(ToPascalCase(a.Key)))}: {a.Value}"
+ )
+ )
+ );
+ builder.AppendLine(" ),");
+ builder.AppendLine(" cancellationToken");
+ builder.AppendLine(" );");
+ }
+
+ private static string FirstLine(string? text) =>
+ string.IsNullOrWhiteSpace(text)
+ ? "The value to send."
+ : text!.Split(['\n', '\r'], StringSplitOptions.RemoveEmptyEntries)[0].Trim();
+}
diff --git a/ObsWebSocket.Codegen.Tasks/Generation/Emitter.Helpers.cs b/ObsWebSocket.Codegen.Tasks/Generation/Emitter.Helpers.cs
index 24ee3c4..636e3e0 100644
--- a/ObsWebSocket.Codegen.Tasks/Generation/Emitter.Helpers.cs
+++ b/ObsWebSocket.Codegen.Tasks/Generation/Emitter.Helpers.cs
@@ -219,10 +219,15 @@ private static StringBuilder BuildSourceHeader(string? fileTypeComment = null)
[System.Text.RegularExpressions.GeneratedRegex(@"\d\.\d")]
private static partial System.Text.RegularExpressions.Regex FractionalRestriction();
+ ///
+ /// False when a second emitter is asking about a field the DTO emitter has already reported
+ /// on, so the same finding is not raised twice for one field.
+ ///
private static (string? CSharpType, bool IsValueType) MapProtocolTypeToCSharp(
SourceProductionContext context,
FieldDefinition field,
- string parentDtoName
+ string parentDtoName,
+ bool reportDiagnostics = true
)
{
string obsType = field.ValueType;
@@ -266,7 +271,9 @@ string parentDtoName
&& FractionalRestriction().IsMatch(restrictions)
)
{
- context.ReportDiagnostic(
+ ReportIf(
+ reportDiagnostics,
+ context,
Diagnostic.Create(
Diagnostics.FractionalFieldClassifiedAsWhole,
Location.None,
@@ -278,7 +285,9 @@ string parentDtoName
if (!classified)
{
- context.ReportDiagnostic(
+ ReportIf(
+ reportDiagnostics,
+ context,
Diagnostic.Create(
Diagnostics.UnclassifiedNumberField,
Location.None,
@@ -310,7 +319,9 @@ string parentDtoName
// Only warn if it fell back to JsonElement and wasn't handled by specific rules above
if (mappedType == "System.Text.Json.JsonElement?")
{
- context.ReportDiagnostic(
+ ReportIf(
+ reportDiagnostics,
+ context,
Diagnostic.Create(
Diagnostics.NestedObjectNotSupportedWarning,
Location.None,
@@ -325,7 +336,9 @@ string parentDtoName
if (isOptional && IsValueType(mappedType) && !mappedType.EndsWith("?"))
{
// Report diagnostic for optional value types that need nullable annotation
- context.ReportDiagnostic(
+ ReportIf(
+ reportDiagnostics,
+ context,
Diagnostic.Create(
Diagnostics.OptionalValueTypeWarning,
Location.None,
@@ -378,7 +391,9 @@ string parentDtoName
}
else // Fallback for an array whose item type is not mapped to a stub.
{
- context.ReportDiagnostic(
+ ReportIf(
+ reportDiagnostics,
+ context,
Diagnostic.Create(
Diagnostics.ArrayItemTypeUnknownWarning,
Location.None,
@@ -417,7 +432,9 @@ string parentDtoName
}
else // Inner primitive type could not be mapped
{
- context.ReportDiagnostic(
+ ReportIf(
+ reportDiagnostics,
+ context,
Diagnostic.Create(
Diagnostics.ArrayItemTypeUnknownWarning,
Location.None,
@@ -433,7 +450,9 @@ string parentDtoName
// --- Handle Unmappable Type ---
// If we reach here, the obsType is not a basic type, not Object/Any, and not Array
- context.ReportDiagnostic(
+ ReportIf(
+ reportDiagnostics,
+ context,
Diagnostic.Create(
Diagnostics.UnmappableTypeError,
Location.None,
@@ -846,4 +865,17 @@ EnumDefinition enumDef
return (overallKind, null);
}
}
+
+ /// Reports a diagnostic unless a second pass is asking about the same field.
+ private static void ReportIf(
+ bool report,
+ SourceProductionContext context,
+ Diagnostic diagnostic
+ )
+ {
+ if (report)
+ {
+ context.ReportDiagnostic(diagnostic);
+ }
+ }
}
diff --git a/ObsWebSocket.Codegen.Tasks/Generation/Emitter.PayloadHandles.cs b/ObsWebSocket.Codegen.Tasks/Generation/Emitter.PayloadHandles.cs
new file mode 100644
index 0000000..198d968
--- /dev/null
+++ b/ObsWebSocket.Codegen.Tasks/Generation/Emitter.PayloadHandles.cs
@@ -0,0 +1,246 @@
+using System.Text;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.Text;
+
+namespace ObsWebSocket.Codegen.Tasks.Generation;
+
+internal static partial class Emitter
+{
+ ///
+ /// Emits handle accessors on every event payload and response that already carries a uuid.
+ ///
+ ///
+ /// These are the handles that cost nothing. An event announcing a scene change already says
+ /// which scene, by uuid, so acting on it needs no lookup and the result is immune to a rename
+ /// that happens between the event arriving and the next request going out. Without them the
+ /// caller reads e.EventData.SceneName and addresses the scene by name again, which is
+ /// the round trip and the race the uuid was there to avoid.
+ ///
+ public static void GeneratePayloadHandles(
+ SourceProductionContext context,
+ ProtocolDefinition protocol
+ )
+ {
+ StringBuilder builder = new();
+ builder.AppendLine("// ");
+ builder.AppendLine("#nullable enable");
+ builder.AppendLine();
+ builder.AppendLine("using System;");
+ builder.AppendLine($"using {GeneratedCommonNamespace};");
+ builder.AppendLine();
+ builder.AppendLine($"namespace {ExtensionsNamespace};");
+ builder.AppendLine();
+ builder.AppendLine("/// ");
+ builder.AppendLine(
+ "/// Handles for the things an event or a response already identifies by uuid."
+ );
+ builder.AppendLine("/// ");
+ builder.AppendLine("public static class ObsWebSocketPayloadHandles");
+ builder.AppendLine("{");
+
+ int emitted = 0;
+
+ if (protocol.Events is not null)
+ {
+ foreach (
+ OBSEvent definition in protocol.Events.OrderBy(
+ e => e.EventType,
+ StringComparer.Ordinal
+ )
+ )
+ {
+ emitted += EmitAccessors(
+ builder,
+ $"{GeneratedEventsNamespace}.{SanitizeIdentifier(definition.EventType)}Payload",
+ "payload",
+ definition.DataFields
+ );
+ }
+ }
+
+ if (protocol.Requests is not null)
+ {
+ foreach (
+ RequestDefinition request in protocol.Requests.OrderBy(
+ r => r.RequestType,
+ StringComparer.Ordinal
+ )
+ )
+ {
+ if (request.ResponseFields is null || request.ResponseFields.Count == 0)
+ {
+ continue;
+ }
+
+ emitted += EmitAccessors(
+ builder,
+ $"{GeneratedResponsesNamespace}.{SanitizeIdentifier(request.RequestType)}ResponseData",
+ "response",
+ request.ResponseFields
+ );
+ }
+ }
+
+ builder.AppendLine("}");
+ builder.AppendLine();
+ builder.AppendLine($"// Handles reachable without a lookup: {emitted}");
+
+ context.AddSource(
+ "ObsWebSocketClient.PayloadHandles.g.cs",
+ SourceText.From(builder.ToString(), Encoding.UTF8)
+ );
+ }
+
+ ///
+ /// Emits one extension block for a payload, holding an accessor per uuid it carries.
+ ///
+ private static int EmitAccessors(
+ StringBuilder builder,
+ string payloadType,
+ string parameterName,
+ IReadOnlyList? fields
+ )
+ {
+ if (fields is null || fields.Count == 0)
+ {
+ return 0;
+ }
+
+ Dictionary byName = new(StringComparer.Ordinal);
+ foreach (FieldDefinition field in fields)
+ {
+ byName[field.ValueName] = field;
+ }
+
+ List<(
+ string Accessor,
+ string HandleType,
+ string Expression,
+ bool Nullable,
+ string Doc
+ )> accessors = [];
+
+ foreach (FieldDefinition field in fields)
+ {
+ string name = field.ValueName;
+ if (!name.EndsWith("Uuid", StringComparison.Ordinal))
+ {
+ continue;
+ }
+
+ string role = name.Substring(0, name.Length - "Uuid".Length);
+ string? handleType = HandleTypeForRole(role);
+ if (handleType is null)
+ {
+ continue;
+ }
+
+ string property = SanitizeIdentifier(ToPascalCase(name));
+ string accessor = SanitizeIdentifier(ToPascalCase(role));
+ bool nullable =
+ field.ValueOptional == true || DescriptionAllowsNull(field.ValueDescription);
+
+ accessors.Add(
+ (
+ accessor,
+ handleType,
+ $"{handleType}.FromUuid({parameterName}.{property})",
+ nullable,
+ $"The {DescribeRole(role)} this message identifies, addressed by uuid so a rename cannot move it."
+ )
+ );
+ }
+
+ // A scene item needs the scene as well as the id, and both travel together when they
+ // travel at all.
+ if (byName.ContainsKey("sceneItemId") && byName.ContainsKey("sceneUuid"))
+ {
+ FieldDefinition idField = byName["sceneItemId"];
+ bool nullable =
+ idField.ValueOptional == true || DescriptionAllowsNull(idField.ValueDescription);
+ accessors.Add(
+ (
+ "SceneItem",
+ "SceneItemHandle",
+ $"SceneItemHandle.For(SceneHandle.FromUuid({parameterName}.SceneUuid), {parameterName}.SceneItemId)",
+ nullable,
+ "The scene item this message is about, ready to act on without a lookup."
+ )
+ );
+ }
+
+ if (accessors.Count == 0)
+ {
+ return 0;
+ }
+
+ builder.AppendLine($" extension({payloadType} {parameterName})");
+ builder.AppendLine(" {");
+ foreach (
+ (
+ string accessor,
+ string handleType,
+ string expression,
+ bool nullable,
+ string doc
+ ) in accessors
+ )
+ {
+ builder.AppendLine($" /// {doc}");
+ if (nullable)
+ {
+ string source = expression[(expression.IndexOf('(') + 1)..^1];
+ builder.AppendLine($" public {handleType}? {accessor} =>");
+ builder.AppendLine(
+ $" string.IsNullOrEmpty({FirstArgument(source)}) ? null : {expression};"
+ );
+ }
+ else
+ {
+ builder.AppendLine($" public {handleType} {accessor} => {expression};");
+ }
+
+ builder.AppendLine();
+ }
+
+ builder.AppendLine(" }");
+ builder.AppendLine();
+ return accessors.Count;
+ }
+
+ /// The uuid expression a null check has to look at.
+ private static string FirstArgument(string arguments)
+ {
+ int comma = arguments.IndexOf(',');
+ return comma < 0 ? arguments : arguments[..comma].Trim();
+ }
+
+ ///
+ /// The handle type a {role}Uuid field yields, or null when the role is not something a
+ /// request can address.
+ ///
+ ///
+ /// transitionUuid appears on four events and one response, but no request accepts one,
+ /// so a handle for it could not be used for anything. It is left out until the protocol grows
+ /// somewhere to send it.
+ ///
+ private static string? HandleTypeForRole(string role) =>
+ role switch
+ {
+ "scene" or "currentProgramScene" or "currentPreviewScene" or "destinationScene" =>
+ "SceneHandle",
+ "input" => "InputHandle",
+ "source" => "SourceHandle",
+ "canvas" => "CanvasHandle",
+ _ => null,
+ };
+
+ private static string DescribeRole(string role) =>
+ role switch
+ {
+ "currentProgramScene" => "program scene",
+ "currentPreviewScene" => "preview scene",
+ "destinationScene" => "destination scene",
+ _ => role,
+ };
+}
diff --git a/ObsWebSocket.Codegen.Tasks/Generation/EntityReference.cs b/ObsWebSocket.Codegen.Tasks/Generation/EntityReference.cs
new file mode 100644
index 0000000..187c05d
--- /dev/null
+++ b/ObsWebSocket.Codegen.Tasks/Generation/EntityReference.cs
@@ -0,0 +1,193 @@
+namespace ObsWebSocket.Codegen.Tasks.Generation;
+
+///
+/// One thing a request addresses, and the protocol fields that address it.
+///
+/// The entity kind, such as scene or input.
+///
+/// Which reference this is within the request. Usually the same as the kind, but
+/// DuplicateSceneItem names a second scene destinationScene.
+///
+/// The protocol fields this reference consumes.
+internal sealed record EntityReference(string Kind, string Role, IReadOnlyList Fields);
+
+///
+/// Finds the things a request addresses, from the shape of its fields rather than a list of
+/// request names.
+///
+///
+/// The protocol never says "this request takes a scene". It says the request has an optional
+/// sceneName and an optional sceneUuid, which is the same statement made twice per
+/// request across 68 of them. Reading that shape back out is what lets the handle overloads be
+/// generated instead of transcribed, and it is why DuplicateSceneItem's second scene falls
+/// out without anyone thinking about it.
+///
+internal static class EntityReferenceTable
+{
+ ///
+ /// The kinds that are addressed by a name-or-uuid pair, mapped to the handle type that carries
+ /// them. A kind not listed here is a field that happens to end in Name, not an entity.
+ ///
+ private static readonly Dictionary s_handleTypes = new(StringComparer.Ordinal)
+ {
+ ["scene"] = "SceneHandle",
+ ["destinationScene"] = "SceneHandle",
+ ["input"] = "InputHandle",
+ ["source"] = "SourceHandle",
+ };
+
+ /// Returns the handle type for a kind, or if it is not one.
+ public static string? HandleTypeFor(string kind) =>
+ s_handleTypes.TryGetValue(kind, out string? type) ? type : null;
+
+ ///
+ /// Reads the entity references out of a request's fields.
+ ///
+ ///
+ /// A {X}Name and {X}Uuid pair, both optional, is a reference to X. Both optional
+ /// matters: CreateInput takes a required inputName for the input it is about to
+ /// make, which is a value, not a reference to something that already exists.
+ ///
+ /// canvasUuid is folded into the scene or source reference it scopes, because OBS reads
+ /// it only when resolving a name and ignores it beside a uuid. A request with a canvas and no
+ /// name-addressable entity keeps it as an ordinary parameter.
+ ///
+ ///
+ /// A sceneItemId beside a scene reference, and a filterName beside a source
+ /// reference, are composite references: both fields together address one thing.
+ ///
+ ///
+ public static IReadOnlyList Find(IReadOnlyList? fields)
+ {
+ if (fields is null || fields.Count == 0)
+ {
+ return [];
+ }
+
+ Dictionary byName = new(StringComparer.Ordinal);
+ foreach (FieldDefinition f in fields)
+ {
+ byName[f.ValueName] = f;
+ }
+
+ List found = [];
+ foreach (FieldDefinition field in fields)
+ {
+ string name = field.ValueName;
+ if (!name.EndsWith("Name", StringComparison.Ordinal) || field.ValueOptional != true)
+ {
+ continue;
+ }
+
+ string role = name.Substring(0, name.Length - "Name".Length);
+ string uuid = role + "Uuid";
+ if (
+ !byName.TryGetValue(uuid, out FieldDefinition? uuidField)
+ || uuidField.ValueOptional != true
+ )
+ {
+ continue;
+ }
+
+ string kind = role;
+ if (HandleTypeFor(kind) is null)
+ {
+ continue;
+ }
+
+ found.Add(new EntityReference(kind, role, [name, uuid]));
+ }
+
+ if (found.Count == 0)
+ {
+ return found;
+ }
+
+ // The canvas scopes the first name-addressed reference in the request. Only one reference
+ // can own it, and in every request that has both it is the primary scene or source.
+ if (byName.ContainsKey("canvasUuid"))
+ {
+ EntityReference primary = found[0];
+ found[0] = primary with { Fields = [.. primary.Fields, "canvasUuid"] };
+ }
+
+ // Composite references: the id or the filter name plus the entity it hangs off.
+ EntityReference? scene = found.Find(r => r.Kind == "scene");
+ if (scene is not null && byName.ContainsKey("sceneItemId"))
+ {
+ found[found.IndexOf(scene)] = new EntityReference(
+ "sceneItem",
+ "sceneItem",
+ [.. scene.Fields, "sceneItemId"]
+ );
+ }
+
+ EntityReference? source = found.Find(r => r.Kind == "source");
+ if (source is not null && byName.ContainsKey("filterName"))
+ {
+ found[found.IndexOf(source)] = new EntityReference(
+ "filter",
+ "filter",
+ [.. source.Fields, "filterName"]
+ );
+ }
+
+ return found;
+ }
+
+ /// The handle type that carries a reference, including the composite kinds.
+ public static string HandleTypeForReference(EntityReference reference) =>
+ reference.Kind switch
+ {
+ "sceneItem" => "SceneItemHandle",
+ "filter" => "FilterHandle",
+ _ => HandleTypeFor(reference.Kind)!,
+ };
+
+ /// The parameter name a reference is given in a generated overload.
+ public static string ParameterNameForReference(EntityReference reference) =>
+ reference.Role switch
+ {
+ "destinationScene" => "destinationScene",
+ "sceneItem" => "sceneItem",
+ "filter" => "filter",
+ _ => reference.Kind,
+ };
+
+ ///
+ /// The arguments a reference contributes to the generated request record, keyed by protocol
+ /// field name.
+ ///
+ ///
+ /// A handle holds either a name or a uuid, never both, so writing both fields sends exactly
+ /// one of them and the other stays null. That is the shape OBS resolves, without the caller
+ /// having to know the order it resolves in.
+ ///
+ public static IEnumerable> ArgumentsFor(
+ EntityReference reference,
+ string parameterName
+ )
+ {
+ string root = reference.Kind switch
+ {
+ "sceneItem" => $"{parameterName}.Scene",
+ "filter" => $"{parameterName}.Source",
+ _ => parameterName,
+ };
+
+ foreach (string field in reference.Fields)
+ {
+ yield return field switch
+ {
+ "canvasUuid" => new("canvasUuid", $"{root}.Canvas.Uuid"),
+ "sceneItemId" => new("sceneItemId", $"{parameterName}.SceneItemId"),
+ "filterName" => new("filterName", $"{parameterName}.FilterName"),
+ _ when field.EndsWith("Uuid", StringComparison.Ordinal) => new(
+ field,
+ $"{root}.Uuid"
+ ),
+ _ => new(field, $"{root}.Name"),
+ };
+ }
+ }
+}
diff --git a/ObsWebSocket.Codegen.Tasks/Generation/NumericFieldTable.cs b/ObsWebSocket.Codegen.Tasks/Generation/NumericFieldTable.cs
index 1f3dee3..6a54e48 100644
--- a/ObsWebSocket.Codegen.Tasks/Generation/NumericFieldTable.cs
+++ b/ObsWebSocket.Codegen.Tasks/Generation/NumericFieldTable.cs
@@ -12,6 +12,15 @@ namespace ObsWebSocket.Codegen.Tasks.Generation;
/// 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.
+///
+/// The rule for choosing between int and long is not how large the number looks. It
+/// is whether obs-websocket bounds it. A field validated with ValidateNumber or
+/// ValidateOptionalNumber has a stated range and is safe at its natural width; a field
+/// copied straight out of libobs or a settings blob is as wide as the C type behind it, and most of
+/// those are uint32_t or int64_t. Getting this wrong is not a truncated field: the
+/// response fails to deserialize, so one out-of-range pixel count takes the whole message with it.
+/// Check the request handler before adding a field here.
+///
///
internal static class NumericFieldTable
{
@@ -19,7 +28,6 @@ internal static class NumericFieldTable
private static readonly HashSet s_int32Fields = new(StringComparer.Ordinal)
{
// Identity and ordering.
- "sceneItemId",
"sceneItemIndex",
"filterIndex",
"monitorIndex",
@@ -37,16 +45,8 @@ internal static class NumericFieldTable
"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",
};
@@ -55,6 +55,17 @@ internal static class NumericFieldTable
/// 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.
///
+ ///
+ /// The frame and message counters are here because of what fills them, not because the numbers
+ /// look large. A field is safe as an int only when obs-websocket bounds it. The
+ /// resolutions are all validated to 8..4096, and the indices are container positions. These
+ /// are copied out of libobs and the session with no clamp in between:
+ /// obs_get_total_frames and obs_get_lagged_frames return uint32_t,
+ /// video_output_get_skipped_frames returns uint32_t, and the session counters are
+ /// uint64_t. A monotonic frame counter passes after roughly
+ /// 414 days at 60fps, which a 24/7 instance reaches, and the whole response fails to
+ /// deserialize when it does.
+ ///
private static readonly HashSet s_int64Fields = new(StringComparer.Ordinal)
{
"outputBytes",
@@ -63,6 +74,21 @@ internal static class NumericFieldTable
"mediaCursorOffset",
"mediaDuration",
"inputKindCaps",
+ // Counters, unclamped from uint32_t (frames) and uint64_t (session messages).
+ "renderSkippedFrames",
+ "renderTotalFrames",
+ "outputSkippedFrames",
+ "outputTotalFrames",
+ "webSocketSessionIncomingMessages",
+ "webSocketSessionOutgoingMessages",
+ // A scene item id is int64_t at every point OBS touches it, and the only bound
+ // obs-websocket applies is >= 0. Sequential assignment keeps real ids small, but the
+ // counter they come from is restored out of the scene collection file, and a plugin can
+ // choose an id outright, so neither the type nor the protocol makes 32 bits safe.
+ "sceneItemId",
+ // Read back out of a scene's private settings with obs_data_get_int, which is int64_t and
+ // is not revalidated on the way out. Only the write side is bounded to 50..20000.
+ "transitionDuration",
};
///
diff --git a/ObsWebSocket.Codegen.Tasks/Generation/ProtocolCodeGenerator.cs b/ObsWebSocket.Codegen.Tasks/Generation/ProtocolCodeGenerator.cs
index 3693ac3..4b636c1 100644
--- a/ObsWebSocket.Codegen.Tasks/Generation/ProtocolCodeGenerator.cs
+++ b/ObsWebSocket.Codegen.Tasks/Generation/ProtocolCodeGenerator.cs
@@ -43,6 +43,8 @@ IReadOnlyList Diagnostics
Emitter.GenerateResponseDtos(context, protocol);
Emitter.GeneratePayloadSchema(context, protocol);
Emitter.GenerateClientExtensions(context, protocol);
+ Emitter.GenerateHandleOverloads(context, protocol);
+ Emitter.GeneratePayloadHandles(context, protocol);
Emitter.GenerateEventPayloads(context, protocol);
Emitter.GenerateEventArgs(context, protocol);
Emitter.GenerateClientEventInfrastructure(context, protocol);
diff --git a/ObsWebSocket.Core/AuthenticationFailureException.cs b/ObsWebSocket.Core/AuthenticationFailureException.cs
index 74d8383..b99ee87 100644
--- a/ObsWebSocket.Core/AuthenticationFailureException.cs
+++ b/ObsWebSocket.Core/AuthenticationFailureException.cs
@@ -1,7 +1,7 @@
namespace ObsWebSocket.Core;
///
-/// Thrown when authentication against the OBS WebSocket server fails — typically because the
+/// Thrown when authentication against the OBS WebSocket server fails, typically because the
/// configured password is wrong, missing, or otherwise rejected by the server's challenge.
///
///
diff --git a/ObsWebSocket.Core/ConnectionAttemptFailedException.cs b/ObsWebSocket.Core/ConnectionAttemptFailedException.cs
index 02061f0..69c55d0 100644
--- a/ObsWebSocket.Core/ConnectionAttemptFailedException.cs
+++ b/ObsWebSocket.Core/ConnectionAttemptFailedException.cs
@@ -2,7 +2,7 @@ namespace ObsWebSocket.Core;
///
/// Thrown when a single connection attempt to the OBS WebSocket server fails for a non-auth
-/// reason — protocol mismatch, transport error, handshake timeout, or a server-rejected
+/// reason: protocol mismatch, transport error, handshake timeout, or a server-rejected
/// configuration. Distinct from so consumers can
/// decide whether to retry or stop.
///
diff --git a/ObsWebSocket.Core/Generated/Client/ObsWebSocketClient.HandleOverloads.g.cs b/ObsWebSocket.Core/Generated/Client/ObsWebSocketClient.HandleOverloads.g.cs
new file mode 100644
index 0000000..01e0823
--- /dev/null
+++ b/ObsWebSocket.Core/Generated/Client/ObsWebSocketClient.HandleOverloads.g.cs
@@ -0,0 +1,1703 @@
+//
+#nullable enable
+
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using ObsWebSocket.Core.Protocol.Requests;
+using ObsWebSocket.Core.Protocol.Responses;
+using ObsWebSocket.Core.Protocol.Common;
+
+namespace ObsWebSocket.Core;
+
+///
+/// Every request about one filter.
+///
+/// The client these requests are sent on.
+/// The filter they are about.
+public readonly partial struct FilterOperations(ObsWebSocketClient client, FilterHandle handle)
+{
+ /// The filter these requests address.
+ public FilterHandle Handle => handle;
+
+ ///
+ /// Creates a new filter, adding it to the specified source.
+ ///
+ ///
+ /// Sends the CreateSourceFilter request, with the identity supplied by the handle.
+ ///
+ /// The kind of filter to be created
+ /// Settings object to initialize the filter with
+ /// A token to cancel the asynchronous operation.
+ /// A task that completes when OBS has processed the request.
+ public Task CreateAsync(
+ string filterKind,
+ System.Text.Json.JsonElement? filterSettings = null,
+ CancellationToken cancellationToken = default
+ ) =>
+ client.Filters.CreateSourceFilterAsync(
+ new ObsWebSocket.Core.Protocol.Requests.CreateSourceFilterRequestData(
+ sourceName: handle.Source.Name,
+ sourceUuid: handle.Source.Uuid,
+ canvasUuid: handle.Source.Canvas.Uuid,
+ filterName: handle.FilterName,
+ filterKind: filterKind,
+ filterSettings: filterSettings
+ ),
+ cancellationToken
+ );
+
+ ///
+ /// Gets the info for a specific source filter.
+ ///
+ ///
+ /// Sends the GetSourceFilter request, with the identity supplied by the handle.
+ ///
+ /// A token to cancel the asynchronous operation.
+ /// A task yielding the response data.
+ public Task GetAsync(
+ CancellationToken cancellationToken = default
+ ) =>
+ client.Filters.GetSourceFilterAsync(
+ new ObsWebSocket.Core.Protocol.Requests.GetSourceFilterRequestData(
+ sourceName: handle.Source.Name,
+ sourceUuid: handle.Source.Uuid,
+ canvasUuid: handle.Source.Canvas.Uuid,
+ filterName: handle.FilterName
+ ),
+ cancellationToken
+ );
+
+ ///
+ /// Removes a filter from a source.
+ ///
+ ///
+ /// Sends the RemoveSourceFilter request, with the identity supplied by the handle.
+ ///
+ /// A token to cancel the asynchronous operation.
+ /// A task that completes when OBS has processed the request.
+ public Task RemoveAsync(
+ CancellationToken cancellationToken = default
+ ) =>
+ client.Filters.RemoveSourceFilterAsync(
+ new ObsWebSocket.Core.Protocol.Requests.RemoveSourceFilterRequestData(
+ sourceName: handle.Source.Name,
+ sourceUuid: handle.Source.Uuid,
+ canvasUuid: handle.Source.Canvas.Uuid,
+ filterName: handle.FilterName
+ ),
+ cancellationToken
+ );
+
+ ///
+ /// Sets the enable state of a source filter.
+ ///
+ ///
+ /// Sends the SetSourceFilterEnabled request, with the identity supplied by the handle.
+ ///
+ /// New enable state of the filter
+ /// A token to cancel the asynchronous operation.
+ /// A task that completes when OBS has processed the request.
+ public Task SetEnabledAsync(
+ bool filterEnabled,
+ CancellationToken cancellationToken = default
+ ) =>
+ client.Filters.SetSourceFilterEnabledAsync(
+ new ObsWebSocket.Core.Protocol.Requests.SetSourceFilterEnabledRequestData(
+ sourceName: handle.Source.Name,
+ sourceUuid: handle.Source.Uuid,
+ canvasUuid: handle.Source.Canvas.Uuid,
+ filterName: handle.FilterName,
+ filterEnabled: filterEnabled
+ ),
+ cancellationToken
+ );
+
+ ///
+ /// Sets the index position of a filter on a source.
+ ///
+ ///
+ /// Sends the SetSourceFilterIndex request, with the identity supplied by the handle.
+ ///
+ /// New index position of the filter
+ /// A token to cancel the asynchronous operation.
+ /// A task that completes when OBS has processed the request.
+ public Task SetIndexAsync(
+ int filterIndex,
+ CancellationToken cancellationToken = default
+ ) =>
+ client.Filters.SetSourceFilterIndexAsync(
+ new ObsWebSocket.Core.Protocol.Requests.SetSourceFilterIndexRequestData(
+ sourceName: handle.Source.Name,
+ sourceUuid: handle.Source.Uuid,
+ canvasUuid: handle.Source.Canvas.Uuid,
+ filterName: handle.FilterName,
+ filterIndex: filterIndex
+ ),
+ cancellationToken
+ );
+
+ ///
+ /// Sets the name of a source filter (rename).
+ ///
+ ///
+ /// Sends the SetSourceFilterName request, with the identity supplied by the handle.
+ ///
+ /// New name for the filter
+ /// A token to cancel the asynchronous operation.
+ /// A task that completes when OBS has processed the request.
+ public Task SetNameAsync(
+ string newFilterName,
+ CancellationToken cancellationToken = default
+ ) =>
+ client.Filters.SetSourceFilterNameAsync(
+ new ObsWebSocket.Core.Protocol.Requests.SetSourceFilterNameRequestData(
+ sourceName: handle.Source.Name,
+ sourceUuid: handle.Source.Uuid,
+ canvasUuid: handle.Source.Canvas.Uuid,
+ filterName: handle.FilterName,
+ newFilterName: newFilterName
+ ),
+ cancellationToken
+ );
+
+ ///
+ /// Sets the settings of a source filter.
+ ///
+ ///
+ /// Sends the SetSourceFilterSettings request, with the identity supplied by the handle.
+ ///
+ /// Object of settings to apply
+ /// True == apply the settings on top of existing ones, False == reset the input to its defaults, then apply settings.
+ /// A token to cancel the asynchronous operation.
+ /// A task that completes when OBS has processed the request.
+ public Task SetSettingsAsync(
+ System.Text.Json.JsonElement? filterSettings,
+ bool? overlay = null,
+ CancellationToken cancellationToken = default
+ ) =>
+ client.Filters.SetSourceFilterSettingsAsync(
+ new ObsWebSocket.Core.Protocol.Requests.SetSourceFilterSettingsRequestData(
+ sourceName: handle.Source.Name,
+ sourceUuid: handle.Source.Uuid,
+ canvasUuid: handle.Source.Canvas.Uuid,
+ filterName: handle.FilterName,
+ filterSettings: filterSettings,
+ overlay: overlay
+ ),
+ cancellationToken
+ );
+
+}
+
+///
+/// Every request about one input.
+///
+/// The client these requests are sent on.
+/// The input they are about.
+public readonly partial struct InputOperations(ObsWebSocketClient client, InputHandle handle)
+{
+ /// The input these requests address.
+ public InputHandle Handle => handle;
+
+ ///
+ /// Gets the audio balance of an input.
+ ///
+ ///
+ /// Sends the GetInputAudioBalance request, with the identity supplied by the handle.
+ ///
+ /// A token to cancel the asynchronous operation.
+ /// A task yielding the response data.
+ public Task GetAudioBalanceAsync(
+ CancellationToken cancellationToken = default
+ ) =>
+ client.Inputs.GetInputAudioBalanceAsync(
+ new ObsWebSocket.Core.Protocol.Requests.GetInputAudioBalanceRequestData(
+ inputName: handle.Name,
+ inputUuid: handle.Uuid
+ ),
+ cancellationToken
+ );
+
+ ///
+ /// Gets the audio monitor type of an input.
+ ///
+ /// The available audio monitor types are:
+ ///
+ /// - `OBS_MONITORING_TYPE_NONE`
+ /// - `OBS_MONITORING_TYPE_MONITOR_ONLY`
+ /// - `OBS_MONITORING_TYPE_MONITOR_AND_OUTPUT`
+ ///
+ ///
+ /// Sends the GetInputAudioMonitorType request, with the identity supplied by the handle.
+ ///
+ /// A token to cancel the asynchronous operation.
+ /// A task yielding the response data.
+ public Task GetAudioMonitorTypeAsync(
+ CancellationToken cancellationToken = default
+ ) =>
+ client.Inputs.GetInputAudioMonitorTypeAsync(
+ new ObsWebSocket.Core.Protocol.Requests.GetInputAudioMonitorTypeRequestData(
+ inputName: handle.Name,
+ inputUuid: handle.Uuid
+ ),
+ cancellationToken
+ );
+
+ ///
+ /// Gets the audio sync offset of an input.
+ ///
+ /// Note: The audio sync offset can be negative too!
+ ///
+ ///
+ /// Sends the GetInputAudioSyncOffset request, with the identity supplied by the handle.
+ ///
+ /// A token to cancel the asynchronous operation.
+ /// A task yielding the response data.
+ public Task GetAudioSyncOffsetAsync(
+ CancellationToken cancellationToken = default
+ ) =>
+ client.Inputs.GetInputAudioSyncOffsetAsync(
+ new ObsWebSocket.Core.Protocol.Requests.GetInputAudioSyncOffsetRequestData(
+ inputName: handle.Name,
+ inputUuid: handle.Uuid
+ ),
+ cancellationToken
+ );
+
+ ///
+ /// Gets the enable state of all audio tracks of an input.
+ ///
+ ///
+ /// Sends the GetInputAudioTracks request, with the identity supplied by the handle.
+ ///
+ /// A token to cancel the asynchronous operation.
+ /// A task yielding the response data.
+ public Task GetAudioTracksAsync(
+ CancellationToken cancellationToken = default
+ ) =>
+ client.Inputs.GetInputAudioTracksAsync(
+ new ObsWebSocket.Core.Protocol.Requests.GetInputAudioTracksRequestData(
+ inputName: handle.Name,
+ inputUuid: handle.Uuid
+ ),
+ cancellationToken
+ );
+
+ ///
+ /// Gets the deinterlace field order of an input.
+ ///
+ /// Deinterlace Field Orders:
+ ///
+ /// - `OBS_DEINTERLACE_FIELD_ORDER_TOP`
+ /// - `OBS_DEINTERLACE_FIELD_ORDER_BOTTOM`
+ ///
+ /// Note: Deinterlacing functionality is restricted to async inputs only.
+ ///
+ ///
+ /// Sends the GetInputDeinterlaceFieldOrder request, with the identity supplied by the handle.
+ ///
+ /// A token to cancel the asynchronous operation.
+ /// A task yielding the response data.
+ public Task GetDeinterlaceFieldOrderAsync(
+ CancellationToken cancellationToken = default
+ ) =>
+ client.Inputs.GetInputDeinterlaceFieldOrderAsync(
+ new ObsWebSocket.Core.Protocol.Requests.GetInputDeinterlaceFieldOrderRequestData(
+ inputName: handle.Name,
+ inputUuid: handle.Uuid
+ ),
+ cancellationToken
+ );
+
+ ///
+ /// Gets the deinterlace mode of an input.
+ ///
+ /// Deinterlace Modes:
+ ///
+ /// - `OBS_DEINTERLACE_MODE_DISABLE`
+ /// - `OBS_DEINTERLACE_MODE_DISCARD`
+ /// - `OBS_DEINTERLACE_MODE_RETRO`
+ /// - `OBS_DEINTERLACE_MODE_BLEND`
+ /// - `OBS_DEINTERLACE_MODE_BLEND_2X`
+ /// - `OBS_DEINTERLACE_MODE_LINEAR`
+ /// - `OBS_DEINTERLACE_MODE_LINEAR_2X`
+ /// - `OBS_DEINTERLACE_MODE_YADIF`
+ /// - `OBS_DEINTERLACE_MODE_YADIF_2X`
+ ///
+ /// Note: Deinterlacing functionality is restricted to async inputs only.
+ ///
+ ///
+ /// Sends the GetInputDeinterlaceMode request, with the identity supplied by the handle.
+ ///
+ /// A token to cancel the asynchronous operation.
+ /// A task yielding the response data.
+ public Task GetDeinterlaceModeAsync(
+ CancellationToken cancellationToken = default
+ ) =>
+ client.Inputs.GetInputDeinterlaceModeAsync(
+ new ObsWebSocket.Core.Protocol.Requests.GetInputDeinterlaceModeRequestData(
+ inputName: handle.Name,
+ inputUuid: handle.Uuid
+ ),
+ cancellationToken
+ );
+
+ ///
+ /// Gets the audio mute state of an input.
+ ///
+ ///
+ /// Sends the GetInputMute request, with the identity supplied by the handle.
+ ///
+ /// A token to cancel the asynchronous operation.
+ /// A task yielding the response data.
+ public Task GetMuteAsync(
+ CancellationToken cancellationToken = default
+ ) =>
+ client.Inputs.GetInputMuteAsync(
+ new ObsWebSocket.Core.Protocol.Requests.GetInputMuteRequestData(
+ inputName: handle.Name,
+ inputUuid: handle.Uuid
+ ),
+ cancellationToken
+ );
+
+ ///
+ /// Gets the items of a list property from an input's properties.
+ ///
+ /// Note: Use this in cases where an input provides a dynamic, selectable list of items. For example, display capture, where it provides a list of available displays.
+ ///
+ ///
+ /// Sends the GetInputPropertiesListPropertyItems request, with the identity supplied by the handle.
+ ///
+ /// Name of the list property to get the items of
+ /// A token to cancel the asynchronous operation.
+ /// A task yielding the response data.
+ public Task GetPropertiesListPropertyItemsAsync(
+ string propertyName,
+ CancellationToken cancellationToken = default
+ ) =>
+ client.Inputs.GetInputPropertiesListPropertyItemsAsync(
+ new ObsWebSocket.Core.Protocol.Requests.GetInputPropertiesListPropertyItemsRequestData(
+ inputName: handle.Name,
+ inputUuid: handle.Uuid,
+ propertyName: propertyName
+ ),
+ cancellationToken
+ );
+
+ ///
+ /// Gets the settings of an input.
+ ///
+ /// Note: Does not include defaults. To create the entire settings object, overlay `inputSettings` over the `defaultInputSettings` provided by `GetInputDefaultSettings`.
+ ///
+ ///
+ /// Sends the GetInputSettings request, with the identity supplied by the handle.
+ ///
+ /// A token to cancel the asynchronous operation.
+ /// A task yielding the response data.
+ public Task GetSettingsAsync(
+ CancellationToken cancellationToken = default
+ ) =>
+ client.Inputs.GetInputSettingsAsync(
+ new ObsWebSocket.Core.Protocol.Requests.GetInputSettingsRequestData(
+ inputName: handle.Name,
+ inputUuid: handle.Uuid
+ ),
+ cancellationToken
+ );
+
+ ///
+ /// Gets the current volume setting of an input.
+ ///
+ ///
+ /// Sends the GetInputVolume request, with the identity supplied by the handle.
+ ///
+ /// A token to cancel the asynchronous operation.
+ /// A task yielding the response data.
+ public Task GetVolumeAsync(
+ CancellationToken cancellationToken = default
+ ) =>
+ client.Inputs.GetInputVolumeAsync(
+ new ObsWebSocket.Core.Protocol.Requests.GetInputVolumeRequestData(
+ inputName: handle.Name,
+ inputUuid: handle.Uuid
+ ),
+ cancellationToken
+ );
+
+ ///
+ /// Gets the status of a media input.
+ ///
+ /// Media States:
+ ///
+ /// - `OBS_MEDIA_STATE_NONE`
+ /// - `OBS_MEDIA_STATE_PLAYING`
+ /// - `OBS_MEDIA_STATE_OPENING`
+ /// - `OBS_MEDIA_STATE_BUFFERING`
+ /// - `OBS_MEDIA_STATE_PAUSED`
+ /// - `OBS_MEDIA_STATE_STOPPED`
+ /// - `OBS_MEDIA_STATE_ENDED`
+ /// - `OBS_MEDIA_STATE_ERROR`
+ ///
+ ///
+ /// Sends the GetMediaInputStatus request, with the identity supplied by the handle.
+ ///
+ /// A token to cancel the asynchronous operation.
+ /// A task yielding the response data.
+ public Task GetMediaStatusAsync(
+ CancellationToken cancellationToken = default
+ ) =>
+ client.MediaInputs.GetMediaInputStatusAsync(
+ new ObsWebSocket.Core.Protocol.Requests.GetMediaInputStatusRequestData(
+ inputName: handle.Name,
+ inputUuid: handle.Uuid
+ ),
+ cancellationToken
+ );
+
+ ///
+ /// Offsets the current cursor position of a media input by the specified value.
+ ///
+ /// This request does not perform bounds checking of the cursor position.
+ ///
+ ///
+ /// Sends the OffsetMediaInputCursor request, with the identity supplied by the handle.
+ ///
+ /// Value to offset the current cursor position by
+ /// A token to cancel the asynchronous operation.
+ /// A task that completes when OBS has processed the request.
+ public Task OffsetMediaCursorAsync(
+ long mediaCursorOffset,
+ CancellationToken cancellationToken = default
+ ) =>
+ client.MediaInputs.OffsetMediaInputCursorAsync(
+ new ObsWebSocket.Core.Protocol.Requests.OffsetMediaInputCursorRequestData(
+ inputName: handle.Name,
+ inputUuid: handle.Uuid,
+ mediaCursorOffset: mediaCursorOffset
+ ),
+ cancellationToken
+ );
+
+ ///
+ /// Opens the filters dialog of an input.
+ ///
+ ///
+ /// Sends the OpenInputFiltersDialog request, with the identity supplied by the handle.
+ ///
+ /// A token to cancel the asynchronous operation.
+ /// A task that completes when OBS has processed the request.
+ public Task OpenFiltersDialogAsync(
+ CancellationToken cancellationToken = default
+ ) =>
+ client.Ui.OpenInputFiltersDialogAsync(
+ new ObsWebSocket.Core.Protocol.Requests.OpenInputFiltersDialogRequestData(
+ inputName: handle.Name,
+ inputUuid: handle.Uuid
+ ),
+ cancellationToken
+ );
+
+ ///
+ /// Opens the interact dialog of an input.
+ ///
+ ///
+ /// Sends the OpenInputInteractDialog request, with the identity supplied by the handle.
+ ///
+ /// A token to cancel the asynchronous operation.
+ /// A task that completes when OBS has processed the request.
+ public Task OpenInteractDialogAsync(
+ CancellationToken cancellationToken = default
+ ) =>
+ client.Ui.OpenInputInteractDialogAsync(
+ new ObsWebSocket.Core.Protocol.Requests.OpenInputInteractDialogRequestData(
+ inputName: handle.Name,
+ inputUuid: handle.Uuid
+ ),
+ cancellationToken
+ );
+
+ ///
+ /// Opens the properties dialog of an input.
+ ///
+ ///
+ /// Sends the OpenInputPropertiesDialog request, with the identity supplied by the handle.
+ ///
+ /// A token to cancel the asynchronous operation.
+ /// A task that completes when OBS has processed the request.
+ public Task OpenPropertiesDialogAsync(
+ CancellationToken cancellationToken = default
+ ) =>
+ client.Ui.OpenInputPropertiesDialogAsync(
+ new ObsWebSocket.Core.Protocol.Requests.OpenInputPropertiesDialogRequestData(
+ inputName: handle.Name,
+ inputUuid: handle.Uuid
+ ),
+ cancellationToken
+ );
+
+ ///
+ /// Presses a button in the properties of an input.
+ ///
+ /// Some known `propertyName` values are:
+ ///
+ /// - `refreshnocache` - Browser source reload button
+ ///
+ /// Note: Use this in cases where there is a button in the properties of an input that cannot be accessed in any other way. For example, browser sources, where there is a refresh button.
+ ///
+ ///
+ /// Sends the PressInputPropertiesButton request, with the identity supplied by the handle.
+ ///
+ /// Name of the button property to press
+ /// A token to cancel the asynchronous operation.
+ /// A task that completes when OBS has processed the request.
+ public Task PressPropertiesButtonAsync(
+ string propertyName,
+ CancellationToken cancellationToken = default
+ ) =>
+ client.Inputs.PressInputPropertiesButtonAsync(
+ new ObsWebSocket.Core.Protocol.Requests.PressInputPropertiesButtonRequestData(
+ inputName: handle.Name,
+ inputUuid: handle.Uuid,
+ propertyName: propertyName
+ ),
+ cancellationToken
+ );
+
+ ///
+ /// Removes an existing input.
+ ///
+ /// Note: Will immediately remove all associated scene items.
+ ///
+ ///
+ /// Sends the RemoveInput request, with the identity supplied by the handle.
+ ///
+ /// A token to cancel the asynchronous operation.
+ /// A task that completes when OBS has processed the request.
+ public Task RemoveAsync(
+ CancellationToken cancellationToken = default
+ ) =>
+ client.Inputs.RemoveInputAsync(
+ new ObsWebSocket.Core.Protocol.Requests.RemoveInputRequestData(
+ inputName: handle.Name,
+ inputUuid: handle.Uuid
+ ),
+ cancellationToken
+ );
+
+ ///
+ /// Sets the audio balance of an input.
+ ///
+ ///
+ /// Sends the SetInputAudioBalance request, with the identity supplied by the handle.
+ ///
+ /// New audio balance value
+ /// A token to cancel the asynchronous operation.
+ /// A task that completes when OBS has processed the request.
+ public Task SetAudioBalanceAsync(
+ double inputAudioBalance,
+ CancellationToken cancellationToken = default
+ ) =>
+ client.Inputs.SetInputAudioBalanceAsync(
+ new ObsWebSocket.Core.Protocol.Requests.SetInputAudioBalanceRequestData(
+ inputName: handle.Name,
+ inputUuid: handle.Uuid,
+ inputAudioBalance: inputAudioBalance
+ ),
+ cancellationToken
+ );
+
+ ///
+ /// Sets the audio monitor type of an input.
+ ///
+ ///
+ /// Sends the SetInputAudioMonitorType request, with the identity supplied by the handle.
+ ///
+ /// Audio monitor type
+ /// A token to cancel the asynchronous operation.
+ /// A task that completes when OBS has processed the request.
+ public Task SetAudioMonitorTypeAsync(
+ string monitorType,
+ CancellationToken cancellationToken = default
+ ) =>
+ client.Inputs.SetInputAudioMonitorTypeAsync(
+ new ObsWebSocket.Core.Protocol.Requests.SetInputAudioMonitorTypeRequestData(
+ inputName: handle.Name,
+ inputUuid: handle.Uuid,
+ monitorType: monitorType
+ ),
+ cancellationToken
+ );
+
+ ///
+ /// Sets the audio sync offset of an input.
+ ///
+ ///
+ /// Sends the SetInputAudioSyncOffset request, with the identity supplied by the handle.
+ ///
+ /// New audio sync offset in milliseconds
+ /// A token to cancel the asynchronous operation.
+ /// A task that completes when OBS has processed the request.
+ public Task SetAudioSyncOffsetAsync(
+ int inputAudioSyncOffset,
+ CancellationToken cancellationToken = default
+ ) =>
+ client.Inputs.SetInputAudioSyncOffsetAsync(
+ new ObsWebSocket.Core.Protocol.Requests.SetInputAudioSyncOffsetRequestData(
+ inputName: handle.Name,
+ inputUuid: handle.Uuid,
+ inputAudioSyncOffset: inputAudioSyncOffset
+ ),
+ cancellationToken
+ );
+
+ ///
+ /// Sets the enable state of audio tracks of an input.
+ ///
+ ///
+ /// Sends the SetInputAudioTracks request, with the identity supplied by the handle.
+ ///
+ /// Track settings to apply
+ /// A token to cancel the asynchronous operation.
+ /// A task that completes when OBS has processed the request.
+ public Task SetAudioTracksAsync(
+ System.Collections.Generic.Dictionary? inputAudioTracks,
+ CancellationToken cancellationToken = default
+ ) =>
+ client.Inputs.SetInputAudioTracksAsync(
+ new ObsWebSocket.Core.Protocol.Requests.SetInputAudioTracksRequestData(
+ inputName: handle.Name,
+ inputUuid: handle.Uuid,
+ inputAudioTracks: inputAudioTracks
+ ),
+ cancellationToken
+ );
+
+ ///
+ /// Sets the deinterlace field order of an input.
+ ///
+ /// Note: Deinterlacing functionality is restricted to async inputs only.
+ ///
+ ///
+ /// Sends the SetInputDeinterlaceFieldOrder request, with the identity supplied by the handle.
+ ///
+ /// Deinterlace field order for the input
+ /// A token to cancel the asynchronous operation.
+ /// A task that completes when OBS has processed the request.
+ public Task SetDeinterlaceFieldOrderAsync(
+ string inputDeinterlaceFieldOrder,
+ CancellationToken cancellationToken = default
+ ) =>
+ client.Inputs.SetInputDeinterlaceFieldOrderAsync(
+ new ObsWebSocket.Core.Protocol.Requests.SetInputDeinterlaceFieldOrderRequestData(
+ inputName: handle.Name,
+ inputUuid: handle.Uuid,
+ inputDeinterlaceFieldOrder: inputDeinterlaceFieldOrder
+ ),
+ cancellationToken
+ );
+
+ ///
+ /// Sets the deinterlace mode of an input.
+ ///
+ /// Note: Deinterlacing functionality is restricted to async inputs only.
+ ///
+ ///
+ /// Sends the SetInputDeinterlaceMode request, with the identity supplied by the handle.
+ ///
+ /// Deinterlace mode for the input
+ /// A token to cancel the asynchronous operation.
+ /// A task that completes when OBS has processed the request.
+ public Task SetDeinterlaceModeAsync(
+ string inputDeinterlaceMode,
+ CancellationToken cancellationToken = default
+ ) =>
+ client.Inputs.SetInputDeinterlaceModeAsync(
+ new ObsWebSocket.Core.Protocol.Requests.SetInputDeinterlaceModeRequestData(
+ inputName: handle.Name,
+ inputUuid: handle.Uuid,
+ inputDeinterlaceMode: inputDeinterlaceMode
+ ),
+ cancellationToken
+ );
+
+ ///
+ /// Sets the audio mute state of an input.
+ ///
+ ///
+ /// Sends the SetInputMute request, with the identity supplied by the handle.
+ ///
+ /// Whether to mute the input or not
+ /// A token to cancel the asynchronous operation.
+ /// A task that completes when OBS has processed the request.
+ public Task SetMuteAsync(
+ bool inputMuted,
+ CancellationToken cancellationToken = default
+ ) =>
+ client.Inputs.SetInputMuteAsync(
+ new ObsWebSocket.Core.Protocol.Requests.SetInputMuteRequestData(
+ inputName: handle.Name,
+ inputUuid: handle.Uuid,
+ inputMuted: inputMuted
+ ),
+ cancellationToken
+ );
+
+ ///
+ /// Sets the name of an input (rename).
+ ///
+ ///
+ /// Sends the SetInputName request, with the identity supplied by the handle.
+ ///
+ /// New name for the input
+ /// A token to cancel the asynchronous operation.
+ /// A task that completes when OBS has processed the request.
+ public Task SetNameAsync(
+ string newInputName,
+ CancellationToken cancellationToken = default
+ ) =>
+ client.Inputs.SetInputNameAsync(
+ new ObsWebSocket.Core.Protocol.Requests.SetInputNameRequestData(
+ inputName: handle.Name,
+ inputUuid: handle.Uuid,
+ newInputName: newInputName
+ ),
+ cancellationToken
+ );
+
+ ///
+ /// Sets the settings of an input.
+ ///
+ ///
+ /// Sends the SetInputSettings request, with the identity supplied by the handle.
+ ///
+ /// Object of settings to apply
+ /// True == apply the settings on top of existing ones, False == reset the input to its defaults, then apply settings.
+ /// A token to cancel the asynchronous operation.
+ /// A task that completes when OBS has processed the request.
+ public Task SetSettingsAsync(
+ System.Text.Json.JsonElement? inputSettings,
+ bool? overlay = null,
+ CancellationToken cancellationToken = default
+ ) =>
+ client.Inputs.SetInputSettingsAsync(
+ new ObsWebSocket.Core.Protocol.Requests.SetInputSettingsRequestData(
+ inputName: handle.Name,
+ inputUuid: handle.Uuid,
+ inputSettings: inputSettings,
+ overlay: overlay
+ ),
+ cancellationToken
+ );
+
+ ///
+ /// Sets the volume setting of an input.
+ ///
+ ///
+ /// Sends the SetInputVolume request, with the identity supplied by the handle.
+ ///
+ /// Volume setting in mul
+ /// Volume setting in dB
+ /// A token to cancel the asynchronous operation.
+ /// A task that completes when OBS has processed the request.
+ public Task SetVolumeAsync(
+ double? inputVolumeMul = null,
+ double? inputVolumeDb = null,
+ CancellationToken cancellationToken = default
+ ) =>
+ client.Inputs.SetInputVolumeAsync(
+ new ObsWebSocket.Core.Protocol.Requests.SetInputVolumeRequestData(
+ inputName: handle.Name,
+ inputUuid: handle.Uuid,
+ inputVolumeMul: inputVolumeMul,
+ inputVolumeDb: inputVolumeDb
+ ),
+ cancellationToken
+ );
+
+ ///
+ /// Sets the cursor position of a media input.
+ ///
+ /// This request does not perform bounds checking of the cursor position.
+ ///
+ ///
+ /// Sends the SetMediaInputCursor request, with the identity supplied by the handle.
+ ///
+ /// New cursor position to set
+ /// A token to cancel the asynchronous operation.
+ /// A task that completes when OBS has processed the request.
+ public Task SetMediaCursorAsync(
+ long mediaCursor,
+ CancellationToken cancellationToken = default
+ ) =>
+ client.MediaInputs.SetMediaInputCursorAsync(
+ new ObsWebSocket.Core.Protocol.Requests.SetMediaInputCursorRequestData(
+ inputName: handle.Name,
+ inputUuid: handle.Uuid,
+ mediaCursor: mediaCursor
+ ),
+ cancellationToken
+ );
+
+ ///
+ /// Toggles the audio mute state of an input.
+ ///
+ ///
+ /// Sends the ToggleInputMute request, with the identity supplied by the handle.
+ ///
+ /// A token to cancel the asynchronous operation.
+ /// A task yielding the response data.
+ public Task ToggleMuteAsync(
+ CancellationToken cancellationToken = default
+ ) =>
+ client.Inputs.ToggleInputMuteAsync(
+ new ObsWebSocket.Core.Protocol.Requests.ToggleInputMuteRequestData(
+ inputName: handle.Name,
+ inputUuid: handle.Uuid
+ ),
+ cancellationToken
+ );
+
+ ///
+ /// Triggers an action on a media input.
+ ///
+ ///
+ /// Sends the TriggerMediaInputAction request, with the identity supplied by the handle.
+ ///
+ /// Identifier of the `ObsMediaInputAction` enum
+ /// A token to cancel the asynchronous operation.
+ /// A task that completes when OBS has processed the request.
+ public Task TriggerMediaActionAsync(
+ ObsWebSocket.Core.Protocol.Generated.MediaInputAction mediaAction,
+ CancellationToken cancellationToken = default
+ ) =>
+ client.MediaInputs.TriggerMediaInputActionAsync(
+ new ObsWebSocket.Core.Protocol.Requests.TriggerMediaInputActionRequestData(
+ inputName: handle.Name,
+ inputUuid: handle.Uuid,
+ mediaAction: mediaAction
+ ),
+ cancellationToken
+ );
+
+}
+
+///
+/// Every request about one scene.
+///
+/// The client these requests are sent on.
+/// The scene they are about.
+public readonly partial struct SceneOperations(ObsWebSocketClient client, SceneHandle handle)
+{
+ /// The scene these requests address.
+ public SceneHandle Handle => handle;
+
+ ///
+ /// Creates a new input, adding it as a scene item to the specified scene.
+ ///
+ ///
+ /// Sends the CreateInput request, with the identity supplied by the handle.
+ ///
+ /// Name of the new input to created
+ /// The kind of input to be created
+ /// Settings object to initialize the input with
+ /// Whether to set the created scene item to enabled or disabled
+ /// A token to cancel the asynchronous operation.
+ /// A task yielding the response data.
+ public Task CreateInputAsync(
+ string inputName,
+ string inputKind,
+ System.Text.Json.JsonElement? inputSettings = null,
+ bool? sceneItemEnabled = null,
+ CancellationToken cancellationToken = default
+ ) =>
+ client.Inputs.CreateInputAsync(
+ new ObsWebSocket.Core.Protocol.Requests.CreateInputRequestData(
+ sceneName: handle.Name,
+ sceneUuid: handle.Uuid,
+ canvasUuid: handle.Canvas.Uuid,
+ inputName: inputName,
+ inputKind: inputKind,
+ inputSettings: inputSettings,
+ sceneItemEnabled: sceneItemEnabled
+ ),
+ cancellationToken
+ );
+
+ ///
+ /// Creates a new scene item using a source.
+ ///
+ /// Scenes only
+ ///
+ ///
+ /// Sends the CreateSceneItem request, with the identity supplied by the handle.
+ ///
+ /// The source to use.
+ /// Enable state to apply to the scene item on creation
+ /// A token to cancel the asynchronous operation.
+ /// A task yielding the response data.
+ public Task CreateItemAsync(
+ SourceHandle source,
+ bool? sceneItemEnabled = null,
+ CancellationToken cancellationToken = default
+ ) =>
+ client.SceneItems.CreateSceneItemAsync(
+ new ObsWebSocket.Core.Protocol.Requests.CreateSceneItemRequestData(
+ sceneName: handle.Name,
+ sceneUuid: handle.Uuid,
+ canvasUuid: handle.Canvas.Uuid,
+ sourceName: source.Name,
+ sourceUuid: source.Uuid,
+ sceneItemEnabled: sceneItemEnabled
+ ),
+ cancellationToken
+ );
+
+ ///
+ /// Basically GetSceneItemList, but for groups.
+ ///
+ /// Using groups at all in OBS is discouraged, as they are very broken under the hood. Please use nested scenes instead.
+ ///
+ /// Groups only
+ ///
+ ///
+ /// Sends the GetGroupSceneItemList request, with the identity supplied by the handle.
+ ///
+ /// A token to cancel the asynchronous operation.
+ /// A task yielding the response data.
+ public Task GetGroupItemListAsync(
+ CancellationToken cancellationToken = default
+ ) =>
+ client.SceneItems.GetGroupSceneItemListAsync(
+ new ObsWebSocket.Core.Protocol.Requests.GetGroupSceneItemListRequestData(
+ sceneName: handle.Name,
+ sceneUuid: handle.Uuid,
+ canvasUuid: handle.Canvas.Uuid
+ ),
+ cancellationToken
+ );
+
+ ///
+ /// Searches a scene for a source, and returns its id.
+ ///
+ /// Scenes and Groups
+ ///
+ ///
+ /// Sends the GetSceneItemId request, with the identity supplied by the handle.
+ ///
+ /// Name of the source to find
+ /// Number of matches to skip during search. >= 0 means first forward. -1 means last (top) item
+ /// A token to cancel the asynchronous operation.
+ /// A task yielding the response data.
+ public Task GetItemIdAsync(
+ string sourceName,
+ int? searchOffset = null,
+ CancellationToken cancellationToken = default
+ ) =>
+ client.SceneItems.GetSceneItemIdAsync(
+ new ObsWebSocket.Core.Protocol.Requests.GetSceneItemIdRequestData(
+ sceneName: handle.Name,
+ sceneUuid: handle.Uuid,
+ canvasUuid: handle.Canvas.Uuid,
+ sourceName: sourceName,
+ searchOffset: searchOffset
+ ),
+ cancellationToken
+ );
+
+ ///
+ /// Gets a list of all scene items in a scene.
+ ///
+ /// Scenes only
+ ///
+ ///
+ /// Sends the GetSceneItemList request, with the identity supplied by the handle.
+ ///
+ /// A token to cancel the asynchronous operation.
+ /// A task yielding the response data.
+ public Task GetItemListAsync(
+ CancellationToken cancellationToken = default
+ ) =>
+ client.SceneItems.GetSceneItemListAsync(
+ new ObsWebSocket.Core.Protocol.Requests.GetSceneItemListRequestData(
+ sceneName: handle.Name,
+ sceneUuid: handle.Uuid,
+ canvasUuid: handle.Canvas.Uuid
+ ),
+ cancellationToken
+ );
+
+ ///
+ /// Gets the scene transition overridden for a scene.
+ ///
+ /// Note: A transition UUID response field is not currently able to be implemented as of 2024-1-18.
+ ///
+ ///
+ /// Sends the GetSceneSceneTransitionOverride request, with the identity supplied by the handle.
+ ///
+ /// A token to cancel the asynchronous operation.
+ /// A task yielding the response data.
+ public Task GetSceneTransitionOverrideAsync(
+ CancellationToken cancellationToken = default
+ ) =>
+ client.Scenes.GetSceneSceneTransitionOverrideAsync(
+ new ObsWebSocket.Core.Protocol.Requests.GetSceneSceneTransitionOverrideRequestData(
+ sceneName: handle.Name,
+ sceneUuid: handle.Uuid,
+ canvasUuid: handle.Canvas.Uuid
+ ),
+ cancellationToken
+ );
+
+ ///
+ /// Removes a scene from OBS.
+ ///
+ ///
+ /// Sends the RemoveScene request, with the identity supplied by the handle.
+ ///
+ /// A token to cancel the asynchronous operation.
+ /// A task that completes when OBS has processed the request.
+ public Task RemoveAsync(
+ CancellationToken cancellationToken = default
+ ) =>
+ client.Scenes.RemoveSceneAsync(
+ new ObsWebSocket.Core.Protocol.Requests.RemoveSceneRequestData(
+ sceneName: handle.Name,
+ sceneUuid: handle.Uuid,
+ canvasUuid: handle.Canvas.Uuid
+ ),
+ cancellationToken
+ );
+
+ ///
+ /// Sets the current preview scene.
+ ///
+ /// Only available when studio mode is enabled.
+ ///
+ ///
+ /// Sends the SetCurrentPreviewScene request, with the identity supplied by the handle.
+ ///
+ /// A token to cancel the asynchronous operation.
+ /// A task that completes when OBS has processed the request.
+ public Task SetCurrentPreviewAsync(
+ CancellationToken cancellationToken = default
+ ) =>
+ client.Scenes.SetCurrentPreviewSceneAsync(
+ new ObsWebSocket.Core.Protocol.Requests.SetCurrentPreviewSceneRequestData(
+ sceneName: handle.Name,
+ sceneUuid: handle.Uuid
+ ),
+ cancellationToken
+ );
+
+ ///
+ /// Sets the current program scene.
+ ///
+ ///
+ /// Sends the SetCurrentProgramScene request, with the identity supplied by the handle.
+ ///
+ /// A token to cancel the asynchronous operation.
+ /// A task that completes when OBS has processed the request.
+ public Task SetCurrentProgramAsync(
+ CancellationToken cancellationToken = default
+ ) =>
+ client.Scenes.SetCurrentProgramSceneAsync(
+ new ObsWebSocket.Core.Protocol.Requests.SetCurrentProgramSceneRequestData(
+ sceneName: handle.Name,
+ sceneUuid: handle.Uuid
+ ),
+ cancellationToken
+ );
+
+ ///
+ /// Sets the name of a scene (rename).
+ ///
+ ///
+ /// Sends the SetSceneName request, with the identity supplied by the handle.
+ ///
+ /// New name for the scene
+ /// A token to cancel the asynchronous operation.
+ /// A task that completes when OBS has processed the request.
+ public Task SetNameAsync(
+ string newSceneName,
+ CancellationToken cancellationToken = default
+ ) =>
+ client.Scenes.SetSceneNameAsync(
+ new ObsWebSocket.Core.Protocol.Requests.SetSceneNameRequestData(
+ sceneName: handle.Name,
+ sceneUuid: handle.Uuid,
+ canvasUuid: handle.Canvas.Uuid,
+ newSceneName: newSceneName
+ ),
+ cancellationToken
+ );
+
+ ///
+ /// Sets the scene transition overridden for a scene.
+ ///
+ ///
+ /// Sends the SetSceneSceneTransitionOverride request, with the identity supplied by the handle.
+ ///
+ /// Name of the scene transition to use as override. Specify `null` to remove
+ /// Duration to use for any overridden transition. Specify `null` to remove
+ /// A token to cancel the asynchronous operation.
+ /// A task that completes when OBS has processed the request.
+ public Task SetSceneTransitionOverrideAsync(
+ string? transitionName = null,
+ long? transitionDuration = null,
+ CancellationToken cancellationToken = default
+ ) =>
+ client.Scenes.SetSceneSceneTransitionOverrideAsync(
+ new ObsWebSocket.Core.Protocol.Requests.SetSceneSceneTransitionOverrideRequestData(
+ sceneName: handle.Name,
+ sceneUuid: handle.Uuid,
+ canvasUuid: handle.Canvas.Uuid,
+ transitionName: transitionName,
+ transitionDuration: transitionDuration
+ ),
+ cancellationToken
+ );
+
+}
+
+///
+/// Every request about one scene item.
+///
+/// The client these requests are sent on.
+/// The scene item they are about.
+public readonly partial struct SceneItemOperations(ObsWebSocketClient client, SceneItemHandle handle)
+{
+ /// The scene item these requests address.
+ public SceneItemHandle Handle => handle;
+
+ ///
+ /// Duplicates a scene item, copying all transform and crop info.
+ ///
+ /// Scenes only
+ ///
+ ///
+ /// Sends the DuplicateSceneItem request, with the identity supplied by the handle.
+ ///
+ /// The destinationScene to use.
+ /// A token to cancel the asynchronous operation.
+ /// A task yielding the response data.
+ public Task DuplicateAsync(
+ SceneHandle destinationScene,
+ CancellationToken cancellationToken = default
+ ) =>
+ client.SceneItems.DuplicateSceneItemAsync(
+ new ObsWebSocket.Core.Protocol.Requests.DuplicateSceneItemRequestData(
+ sceneName: handle.Scene.Name,
+ sceneUuid: handle.Scene.Uuid,
+ canvasUuid: handle.Scene.Canvas.Uuid,
+ sceneItemId: handle.SceneItemId,
+ destinationSceneName: destinationScene.Name,
+ destinationSceneUuid: destinationScene.Uuid
+ ),
+ cancellationToken
+ );
+
+ ///
+ /// Gets the blend mode of a scene item.
+ ///
+ /// Blend modes:
+ ///
+ /// - `OBS_BLEND_NORMAL`
+ /// - `OBS_BLEND_ADDITIVE`
+ /// - `OBS_BLEND_SUBTRACT`
+ /// - `OBS_BLEND_SCREEN`
+ /// - `OBS_BLEND_MULTIPLY`
+ /// - `OBS_BLEND_LIGHTEN`
+ /// - `OBS_BLEND_DARKEN`
+ ///
+ /// Scenes and Groups
+ ///
+ ///
+ /// Sends the GetSceneItemBlendMode request, with the identity supplied by the handle.
+ ///
+ /// A token to cancel the asynchronous operation.
+ /// A task yielding the response data.
+ public Task GetBlendModeAsync(
+ CancellationToken cancellationToken = default
+ ) =>
+ client.SceneItems.GetSceneItemBlendModeAsync(
+ new ObsWebSocket.Core.Protocol.Requests.GetSceneItemBlendModeRequestData(
+ sceneName: handle.Scene.Name,
+ sceneUuid: handle.Scene.Uuid,
+ canvasUuid: handle.Scene.Canvas.Uuid,
+ sceneItemId: handle.SceneItemId
+ ),
+ cancellationToken
+ );
+
+ ///
+ /// Gets the enable state of a scene item.
+ ///
+ /// Scenes and Groups
+ ///
+ ///
+ /// Sends the GetSceneItemEnabled request, with the identity supplied by the handle.
+ ///
+ /// A token to cancel the asynchronous operation.
+ /// A task yielding the response data.
+ public Task GetEnabledAsync(
+ CancellationToken cancellationToken = default
+ ) =>
+ client.SceneItems.GetSceneItemEnabledAsync(
+ new ObsWebSocket.Core.Protocol.Requests.GetSceneItemEnabledRequestData(
+ sceneName: handle.Scene.Name,
+ sceneUuid: handle.Scene.Uuid,
+ canvasUuid: handle.Scene.Canvas.Uuid,
+ sceneItemId: handle.SceneItemId
+ ),
+ cancellationToken
+ );
+
+ ///
+ /// Gets the index position of a scene item in a scene.
+ ///
+ /// An index of 0 is at the bottom of the source list in the UI.
+ ///
+ /// Scenes and Groups
+ ///
+ ///
+ /// Sends the GetSceneItemIndex request, with the identity supplied by the handle.
+ ///
+ /// A token to cancel the asynchronous operation.
+ /// A task yielding the response data.
+ public Task GetIndexAsync(
+ CancellationToken cancellationToken = default
+ ) =>
+ client.SceneItems.GetSceneItemIndexAsync(
+ new ObsWebSocket.Core.Protocol.Requests.GetSceneItemIndexRequestData(
+ sceneName: handle.Scene.Name,
+ sceneUuid: handle.Scene.Uuid,
+ canvasUuid: handle.Scene.Canvas.Uuid,
+ sceneItemId: handle.SceneItemId
+ ),
+ cancellationToken
+ );
+
+ ///
+ /// Gets the lock state of a scene item.
+ ///
+ /// Scenes and Groups
+ ///
+ ///
+ /// Sends the GetSceneItemLocked request, with the identity supplied by the handle.
+ ///
+ /// A token to cancel the asynchronous operation.
+ /// A task yielding the response data.
+ public Task GetLockedAsync(
+ CancellationToken cancellationToken = default
+ ) =>
+ client.SceneItems.GetSceneItemLockedAsync(
+ new ObsWebSocket.Core.Protocol.Requests.GetSceneItemLockedRequestData(
+ sceneName: handle.Scene.Name,
+ sceneUuid: handle.Scene.Uuid,
+ canvasUuid: handle.Scene.Canvas.Uuid,
+ sceneItemId: handle.SceneItemId
+ ),
+ cancellationToken
+ );
+
+ ///
+ /// Gets the source associated with a scene item.
+ ///
+ ///
+ /// Sends the GetSceneItemSource request, with the identity supplied by the handle.
+ ///
+ /// A token to cancel the asynchronous operation.
+ /// A task yielding the response data.
+ public Task GetSourceAsync(
+ CancellationToken cancellationToken = default
+ ) =>
+ client.SceneItems.GetSceneItemSourceAsync(
+ new ObsWebSocket.Core.Protocol.Requests.GetSceneItemSourceRequestData(
+ sceneName: handle.Scene.Name,
+ sceneUuid: handle.Scene.Uuid,
+ canvasUuid: handle.Scene.Canvas.Uuid,
+ sceneItemId: handle.SceneItemId
+ ),
+ cancellationToken
+ );
+
+ ///
+ /// Gets the transform and crop info of a scene item.
+ ///
+ /// Scenes and Groups
+ ///
+ ///
+ /// Sends the GetSceneItemTransform request, with the identity supplied by the handle.
+ ///
+ /// A token to cancel the asynchronous operation.
+ /// A task yielding the response data.
+ public Task GetTransformAsync(
+ CancellationToken cancellationToken = default
+ ) =>
+ client.SceneItems.GetSceneItemTransformAsync(
+ new ObsWebSocket.Core.Protocol.Requests.GetSceneItemTransformRequestData(
+ sceneName: handle.Scene.Name,
+ sceneUuid: handle.Scene.Uuid,
+ canvasUuid: handle.Scene.Canvas.Uuid,
+ sceneItemId: handle.SceneItemId
+ ),
+ cancellationToken
+ );
+
+ ///
+ /// Removes a scene item from a scene.
+ ///
+ /// Scenes only
+ ///
+ ///
+ /// Sends the RemoveSceneItem request, with the identity supplied by the handle.
+ ///
+ /// A token to cancel the asynchronous operation.
+ /// A task that completes when OBS has processed the request.
+ public Task RemoveAsync(
+ CancellationToken cancellationToken = default
+ ) =>
+ client.SceneItems.RemoveSceneItemAsync(
+ new ObsWebSocket.Core.Protocol.Requests.RemoveSceneItemRequestData(
+ sceneName: handle.Scene.Name,
+ sceneUuid: handle.Scene.Uuid,
+ canvasUuid: handle.Scene.Canvas.Uuid,
+ sceneItemId: handle.SceneItemId
+ ),
+ cancellationToken
+ );
+
+ ///
+ /// Sets the blend mode of a scene item.
+ ///
+ /// Scenes and Groups
+ ///
+ ///
+ /// Sends the SetSceneItemBlendMode request, with the identity supplied by the handle.
+ ///
+ /// New blend mode
+ /// A token to cancel the asynchronous operation.
+ /// A task that completes when OBS has processed the request.
+ public Task SetBlendModeAsync(
+ string sceneItemBlendMode,
+ CancellationToken cancellationToken = default
+ ) =>
+ client.SceneItems.SetSceneItemBlendModeAsync(
+ new ObsWebSocket.Core.Protocol.Requests.SetSceneItemBlendModeRequestData(
+ sceneName: handle.Scene.Name,
+ sceneUuid: handle.Scene.Uuid,
+ canvasUuid: handle.Scene.Canvas.Uuid,
+ sceneItemId: handle.SceneItemId,
+ sceneItemBlendMode: sceneItemBlendMode
+ ),
+ cancellationToken
+ );
+
+ ///
+ /// Sets the enable state of a scene item.
+ ///
+ /// Scenes and Groups
+ ///
+ ///
+ /// Sends the SetSceneItemEnabled request, with the identity supplied by the handle.
+ ///
+ /// New enable state of the scene item
+ /// A token to cancel the asynchronous operation.
+ /// A task that completes when OBS has processed the request.
+ public Task SetEnabledAsync(
+ bool sceneItemEnabled,
+ CancellationToken cancellationToken = default
+ ) =>
+ client.SceneItems.SetSceneItemEnabledAsync(
+ new ObsWebSocket.Core.Protocol.Requests.SetSceneItemEnabledRequestData(
+ sceneName: handle.Scene.Name,
+ sceneUuid: handle.Scene.Uuid,
+ canvasUuid: handle.Scene.Canvas.Uuid,
+ sceneItemId: handle.SceneItemId,
+ sceneItemEnabled: sceneItemEnabled
+ ),
+ cancellationToken
+ );
+
+ ///
+ /// Sets the index position of a scene item in a scene.
+ ///
+ /// Scenes and Groups
+ ///
+ ///
+ /// Sends the SetSceneItemIndex request, with the identity supplied by the handle.
+ ///
+ /// New index position of the scene item
+ /// A token to cancel the asynchronous operation.
+ /// A task that completes when OBS has processed the request.
+ public Task SetIndexAsync(
+ int sceneItemIndex,
+ CancellationToken cancellationToken = default
+ ) =>
+ client.SceneItems.SetSceneItemIndexAsync(
+ new ObsWebSocket.Core.Protocol.Requests.SetSceneItemIndexRequestData(
+ sceneName: handle.Scene.Name,
+ sceneUuid: handle.Scene.Uuid,
+ canvasUuid: handle.Scene.Canvas.Uuid,
+ sceneItemId: handle.SceneItemId,
+ sceneItemIndex: sceneItemIndex
+ ),
+ cancellationToken
+ );
+
+ ///
+ /// Sets the lock state of a scene item.
+ ///
+ /// Scenes and Group
+ ///
+ ///
+ /// Sends the SetSceneItemLocked request, with the identity supplied by the handle.
+ ///
+ /// New lock state of the scene item
+ /// A token to cancel the asynchronous operation.
+ /// A task that completes when OBS has processed the request.
+ public Task SetLockedAsync(
+ bool sceneItemLocked,
+ CancellationToken cancellationToken = default
+ ) =>
+ client.SceneItems.SetSceneItemLockedAsync(
+ new ObsWebSocket.Core.Protocol.Requests.SetSceneItemLockedRequestData(
+ sceneName: handle.Scene.Name,
+ sceneUuid: handle.Scene.Uuid,
+ canvasUuid: handle.Scene.Canvas.Uuid,
+ sceneItemId: handle.SceneItemId,
+ sceneItemLocked: sceneItemLocked
+ ),
+ cancellationToken
+ );
+
+ ///
+ /// Sets the transform and crop info of a scene item.
+ ///
+ ///
+ /// Sends the SetSceneItemTransform request, with the identity supplied by the handle.
+ ///
+ /// Object containing scene item transform info to update
+ /// A token to cancel the asynchronous operation.
+ /// A task that completes when OBS has processed the request.
+ public Task SetTransformAsync(
+ ObsWebSocket.Core.Protocol.Common.SceneItemTransformPatchStub? sceneItemTransform,
+ CancellationToken cancellationToken = default
+ ) =>
+ client.SceneItems.SetSceneItemTransformAsync(
+ new ObsWebSocket.Core.Protocol.Requests.SetSceneItemTransformRequestData(
+ sceneName: handle.Scene.Name,
+ sceneUuid: handle.Scene.Uuid,
+ canvasUuid: handle.Scene.Canvas.Uuid,
+ sceneItemId: handle.SceneItemId,
+ sceneItemTransform: sceneItemTransform
+ ),
+ cancellationToken
+ );
+
+}
+
+///
+/// Every request about one source.
+///
+/// The client these requests are sent on.
+/// The source they are about.
+public readonly partial struct SourceOperations(ObsWebSocketClient client, SourceHandle handle)
+{
+ /// The source these requests address.
+ public SourceHandle Handle => handle;
+
+ ///
+ /// Gets the active and show state of a source.
+ ///
+ /// **Compatible with inputs and scenes.**
+ ///
+ ///
+ /// Sends the GetSourceActive request, with the identity supplied by the handle.
+ ///
+ /// A token to cancel the asynchronous operation.
+ /// A task yielding the response data.
+ public Task GetActiveAsync(
+ CancellationToken cancellationToken = default
+ ) =>
+ client.Sources.GetSourceActiveAsync(
+ new ObsWebSocket.Core.Protocol.Requests.GetSourceActiveRequestData(
+ sourceName: handle.Name,
+ sourceUuid: handle.Uuid,
+ canvasUuid: handle.Canvas.Uuid
+ ),
+ cancellationToken
+ );
+
+ ///
+ /// Gets an array of all of a source's filters.
+ ///
+ ///
+ /// Sends the GetSourceFilterList request, with the identity supplied by the handle.
+ ///
+ /// A token to cancel the asynchronous operation.
+ /// A task yielding the response data.
+ public Task GetFilterListAsync(
+ CancellationToken cancellationToken = default
+ ) =>
+ client.Filters.GetSourceFilterListAsync(
+ new ObsWebSocket.Core.Protocol.Requests.GetSourceFilterListRequestData(
+ sourceName: handle.Name,
+ sourceUuid: handle.Uuid,
+ canvasUuid: handle.Canvas.Uuid
+ ),
+ cancellationToken
+ );
+
+ ///
+ /// Gets a Base64-encoded screenshot of a source.
+ ///
+ /// The `imageWidth` and `imageHeight` parameters are treated as "scale to inner", meaning the smallest ratio will be used and the aspect ratio of the original resolution is kept.
+ /// If `imageWidth` and `imageHeight` are not specified, the compressed image will use the full resolution of the source.
+ ///
+ /// **Compatible with inputs and scenes.**
+ ///
+ ///
+ /// Sends the GetSourceScreenshot request, with the identity supplied by the handle.
+ ///
+ /// Image compression format to use. Use `GetVersion` to get compatible image formats
+ /// Width to scale the screenshot to
+ /// Height to scale the screenshot to
+ /// Compression quality to use. 0 for high compression, 100 for uncompressed. -1 to use "default" (whatever that means, idk)
+ /// A token to cancel the asynchronous operation.
+ /// A task yielding the response data.
+ public Task GetScreenshotAsync(
+ string imageFormat,
+ int? imageWidth = null,
+ int? imageHeight = null,
+ int? imageCompressionQuality = null,
+ CancellationToken cancellationToken = default
+ ) =>
+ client.Sources.GetSourceScreenshotAsync(
+ new ObsWebSocket.Core.Protocol.Requests.GetSourceScreenshotRequestData(
+ sourceName: handle.Name,
+ sourceUuid: handle.Uuid,
+ canvasUuid: handle.Canvas.Uuid,
+ imageFormat: imageFormat,
+ imageWidth: imageWidth,
+ imageHeight: imageHeight,
+ imageCompressionQuality: imageCompressionQuality
+ ),
+ cancellationToken
+ );
+
+ ///
+ /// Opens a projector for a source.
+ ///
+ /// Note: This request serves to provide feature parity with 4.x. It is very likely to be changed/deprecated in a future release.
+ ///
+ ///
+ /// Sends the OpenSourceProjector request, with the identity supplied by the handle.
+ ///
+ /// Monitor index, use `GetMonitorList` to obtain index
+ /// Size/Position data for a windowed projector, in Qt Base64 encoded format. Mutually exclusive with `monitorIndex`
+ /// A token to cancel the asynchronous operation.
+ /// A task that completes when OBS has processed the request.
+ public Task OpenProjectorAsync(
+ int? monitorIndex = null,
+ string? projectorGeometry = null,
+ CancellationToken cancellationToken = default
+ ) =>
+ client.Ui.OpenSourceProjectorAsync(
+ new ObsWebSocket.Core.Protocol.Requests.OpenSourceProjectorRequestData(
+ sourceName: handle.Name,
+ sourceUuid: handle.Uuid,
+ canvasUuid: handle.Canvas.Uuid,
+ monitorIndex: monitorIndex,
+ projectorGeometry: projectorGeometry
+ ),
+ cancellationToken
+ );
+
+ ///
+ /// Saves a screenshot of a source to the filesystem.
+ ///
+ /// The `imageWidth` and `imageHeight` parameters are treated as "scale to inner", meaning the smallest ratio will be used and the aspect ratio of the original resolution is kept.
+ /// If `imageWidth` and `imageHeight` are not specified, the compressed image will use the full resolution of the source.
+ ///
+ /// **Compatible with inputs and scenes.**
+ ///
+ ///
+ /// Sends the SaveSourceScreenshot request, with the identity supplied by the handle.
+ ///
+ /// Image compression format to use. Use `GetVersion` to get compatible image formats
+ /// Path to save the screenshot file to. Eg. `C:\Users\user\Desktop\screenshot.png`
+ /// Width to scale the screenshot to
+ /// Height to scale the screenshot to
+ /// Compression quality to use. 0 for high compression, 100 for uncompressed. -1 to use "default" (whatever that means, idk)
+ /// A token to cancel the asynchronous operation.
+ /// A task that completes when OBS has processed the request.
+ public Task SaveScreenshotAsync(
+ string imageFormat,
+ string imageFilePath,
+ int? imageWidth = null,
+ int? imageHeight = null,
+ int? imageCompressionQuality = null,
+ CancellationToken cancellationToken = default
+ ) =>
+ client.Sources.SaveSourceScreenshotAsync(
+ new ObsWebSocket.Core.Protocol.Requests.SaveSourceScreenshotRequestData(
+ sourceName: handle.Name,
+ sourceUuid: handle.Uuid,
+ canvasUuid: handle.Canvas.Uuid,
+ imageFormat: imageFormat,
+ imageFilePath: imageFilePath,
+ imageWidth: imageWidth,
+ imageHeight: imageHeight,
+ imageCompressionQuality: imageCompressionQuality
+ ),
+ cancellationToken
+ );
+
+}
+
+///
+/// Addresses one thing in OBS, so the requests about it need not restate which.
+///
+public static class ObsWebSocketHandleExtensions
+{
+ extension(ObsWebSocketClient client)
+ {
+ /// Every request about one filter.
+ /// The filter, which a name or a uuid converts to.
+ public FilterOperations Filter(FilterHandle handle) => new(client, handle);
+ }
+
+ extension(ObsWebSocketClient client)
+ {
+ /// Every request about one input.
+ /// The input, which a name or a uuid converts to.
+ public InputOperations Input(InputHandle handle) => new(client, handle);
+ }
+
+ extension(ObsWebSocketClient client)
+ {
+ /// Every request about one scene.
+ /// The scene, which a name or a uuid converts to.
+ public SceneOperations Scene(SceneHandle handle) => new(client, handle);
+ }
+
+ extension(ObsWebSocketClient client)
+ {
+ /// Every request about one scene item.
+ /// The scene item, which a name or a uuid converts to.
+ public SceneItemOperations SceneItem(SceneItemHandle handle) => new(client, handle);
+ }
+
+ extension(ObsWebSocketClient client)
+ {
+ /// Every request about one source.
+ /// The source, which a name or a uuid converts to.
+ public SourceOperations Source(SourceHandle handle) => new(client, handle);
+ }
+
+}
+
+// Requests reachable through a handle: 66
diff --git a/ObsWebSocket.Core/Generated/Client/ObsWebSocketClient.PayloadHandles.g.cs b/ObsWebSocket.Core/Generated/Client/ObsWebSocketClient.PayloadHandles.g.cs
new file mode 100644
index 0000000..361fe77
--- /dev/null
+++ b/ObsWebSocket.Core/Generated/Client/ObsWebSocketClient.PayloadHandles.g.cs
@@ -0,0 +1,303 @@
+//
+#nullable enable
+
+using System;
+using ObsWebSocket.Core.Protocol.Common;
+
+namespace ObsWebSocket.Core;
+
+///
+/// Handles for the things an event or a response already identifies by uuid.
+///
+public static class ObsWebSocketPayloadHandles
+{
+ extension(ObsWebSocket.Core.Protocol.Events.CanvasCreatedPayload payload)
+ {
+ /// The canvas this message identifies, addressed by uuid so a rename cannot move it.
+ public CanvasHandle Canvas => CanvasHandle.FromUuid(payload.CanvasUuid);
+
+ }
+
+ extension(ObsWebSocket.Core.Protocol.Events.CanvasNameChangedPayload payload)
+ {
+ /// The canvas this message identifies, addressed by uuid so a rename cannot move it.
+ public CanvasHandle Canvas => CanvasHandle.FromUuid(payload.CanvasUuid);
+
+ }
+
+ extension(ObsWebSocket.Core.Protocol.Events.CanvasRemovedPayload payload)
+ {
+ /// The canvas this message identifies, addressed by uuid so a rename cannot move it.
+ public CanvasHandle Canvas => CanvasHandle.FromUuid(payload.CanvasUuid);
+
+ }
+
+ extension(ObsWebSocket.Core.Protocol.Events.CurrentPreviewSceneChangedPayload payload)
+ {
+ /// The scene this message identifies, addressed by uuid so a rename cannot move it.
+ public SceneHandle Scene => SceneHandle.FromUuid(payload.SceneUuid);
+
+ }
+
+ extension(ObsWebSocket.Core.Protocol.Events.CurrentProgramSceneChangedPayload payload)
+ {
+ /// The scene this message identifies, addressed by uuid so a rename cannot move it.
+ public SceneHandle Scene => SceneHandle.FromUuid(payload.SceneUuid);
+
+ }
+
+ extension(ObsWebSocket.Core.Protocol.Events.InputActiveStateChangedPayload payload)
+ {
+ /// The input this message identifies, addressed by uuid so a rename cannot move it.
+ public InputHandle Input => InputHandle.FromUuid(payload.InputUuid);
+
+ }
+
+ extension(ObsWebSocket.Core.Protocol.Events.InputAudioBalanceChangedPayload payload)
+ {
+ /// The input this message identifies, addressed by uuid so a rename cannot move it.
+ public InputHandle Input => InputHandle.FromUuid(payload.InputUuid);
+
+ }
+
+ extension(ObsWebSocket.Core.Protocol.Events.InputAudioMonitorTypeChangedPayload payload)
+ {
+ /// The input this message identifies, addressed by uuid so a rename cannot move it.
+ public InputHandle Input => InputHandle.FromUuid(payload.InputUuid);
+
+ }
+
+ extension(ObsWebSocket.Core.Protocol.Events.InputAudioSyncOffsetChangedPayload payload)
+ {
+ /// The input this message identifies, addressed by uuid so a rename cannot move it.
+ public InputHandle Input => InputHandle.FromUuid(payload.InputUuid);
+
+ }
+
+ extension(ObsWebSocket.Core.Protocol.Events.InputAudioTracksChangedPayload payload)
+ {
+ /// The input this message identifies, addressed by uuid so a rename cannot move it.
+ public InputHandle Input => InputHandle.FromUuid(payload.InputUuid);
+
+ }
+
+ extension(ObsWebSocket.Core.Protocol.Events.InputCreatedPayload payload)
+ {
+ /// The input this message identifies, addressed by uuid so a rename cannot move it.
+ public InputHandle Input => InputHandle.FromUuid(payload.InputUuid);
+
+ }
+
+ extension(ObsWebSocket.Core.Protocol.Events.InputMuteStateChangedPayload payload)
+ {
+ /// The input this message identifies, addressed by uuid so a rename cannot move it.
+ public InputHandle Input => InputHandle.FromUuid(payload.InputUuid);
+
+ }
+
+ extension(ObsWebSocket.Core.Protocol.Events.InputNameChangedPayload payload)
+ {
+ /// The input this message identifies, addressed by uuid so a rename cannot move it.
+ public InputHandle Input => InputHandle.FromUuid(payload.InputUuid);
+
+ }
+
+ extension(ObsWebSocket.Core.Protocol.Events.InputRemovedPayload payload)
+ {
+ /// The input this message identifies, addressed by uuid so a rename cannot move it.
+ public InputHandle Input => InputHandle.FromUuid(payload.InputUuid);
+
+ }
+
+ extension(ObsWebSocket.Core.Protocol.Events.InputSettingsChangedPayload payload)
+ {
+ /// The input this message identifies, addressed by uuid so a rename cannot move it.
+ public InputHandle Input => InputHandle.FromUuid(payload.InputUuid);
+
+ }
+
+ extension(ObsWebSocket.Core.Protocol.Events.InputShowStateChangedPayload payload)
+ {
+ /// The input this message identifies, addressed by uuid so a rename cannot move it.
+ public InputHandle Input => InputHandle.FromUuid(payload.InputUuid);
+
+ }
+
+ extension(ObsWebSocket.Core.Protocol.Events.InputVolumeChangedPayload payload)
+ {
+ /// The input this message identifies, addressed by uuid so a rename cannot move it.
+ public InputHandle Input => InputHandle.FromUuid(payload.InputUuid);
+
+ }
+
+ extension(ObsWebSocket.Core.Protocol.Events.MediaInputActionTriggeredPayload payload)
+ {
+ /// The input this message identifies, addressed by uuid so a rename cannot move it.
+ public InputHandle Input => InputHandle.FromUuid(payload.InputUuid);
+
+ }
+
+ extension(ObsWebSocket.Core.Protocol.Events.MediaInputPlaybackEndedPayload payload)
+ {
+ /// The input this message identifies, addressed by uuid so a rename cannot move it.
+ public InputHandle Input => InputHandle.FromUuid(payload.InputUuid);
+
+ }
+
+ extension(ObsWebSocket.Core.Protocol.Events.MediaInputPlaybackStartedPayload payload)
+ {
+ /// The input this message identifies, addressed by uuid so a rename cannot move it.
+ public InputHandle Input => InputHandle.FromUuid(payload.InputUuid);
+
+ }
+
+ extension(ObsWebSocket.Core.Protocol.Events.SceneCreatedPayload payload)
+ {
+ /// The scene this message identifies, addressed by uuid so a rename cannot move it.
+ public SceneHandle Scene => SceneHandle.FromUuid(payload.SceneUuid);
+
+ }
+
+ extension(ObsWebSocket.Core.Protocol.Events.SceneItemCreatedPayload payload)
+ {
+ /// The scene this message identifies, addressed by uuid so a rename cannot move it.
+ public SceneHandle Scene => SceneHandle.FromUuid(payload.SceneUuid);
+
+ /// The source this message identifies, addressed by uuid so a rename cannot move it.
+ public SourceHandle Source => SourceHandle.FromUuid(payload.SourceUuid);
+
+ /// The scene item this message is about, ready to act on without a lookup.
+ public SceneItemHandle SceneItem => SceneItemHandle.For(SceneHandle.FromUuid(payload.SceneUuid), payload.SceneItemId);
+
+ }
+
+ extension(ObsWebSocket.Core.Protocol.Events.SceneItemEnableStateChangedPayload payload)
+ {
+ /// The scene this message identifies, addressed by uuid so a rename cannot move it.
+ public SceneHandle Scene => SceneHandle.FromUuid(payload.SceneUuid);
+
+ /// The scene item this message is about, ready to act on without a lookup.
+ public SceneItemHandle SceneItem => SceneItemHandle.For(SceneHandle.FromUuid(payload.SceneUuid), payload.SceneItemId);
+
+ }
+
+ extension(ObsWebSocket.Core.Protocol.Events.SceneItemListReindexedPayload payload)
+ {
+ /// The scene this message identifies, addressed by uuid so a rename cannot move it.
+ public SceneHandle Scene => SceneHandle.FromUuid(payload.SceneUuid);
+
+ }
+
+ extension(ObsWebSocket.Core.Protocol.Events.SceneItemLockStateChangedPayload payload)
+ {
+ /// The scene this message identifies, addressed by uuid so a rename cannot move it.
+ public SceneHandle Scene => SceneHandle.FromUuid(payload.SceneUuid);
+
+ /// The scene item this message is about, ready to act on without a lookup.
+ public SceneItemHandle SceneItem => SceneItemHandle.For(SceneHandle.FromUuid(payload.SceneUuid), payload.SceneItemId);
+
+ }
+
+ extension(ObsWebSocket.Core.Protocol.Events.SceneItemRemovedPayload payload)
+ {
+ /// The scene this message identifies, addressed by uuid so a rename cannot move it.
+ public SceneHandle Scene => SceneHandle.FromUuid(payload.SceneUuid);
+
+ /// The source this message identifies, addressed by uuid so a rename cannot move it.
+ public SourceHandle Source => SourceHandle.FromUuid(payload.SourceUuid);
+
+ /// The scene item this message is about, ready to act on without a lookup.
+ public SceneItemHandle SceneItem => SceneItemHandle.For(SceneHandle.FromUuid(payload.SceneUuid), payload.SceneItemId);
+
+ }
+
+ extension(ObsWebSocket.Core.Protocol.Events.SceneItemSelectedPayload payload)
+ {
+ /// The scene this message identifies, addressed by uuid so a rename cannot move it.
+ public SceneHandle Scene => SceneHandle.FromUuid(payload.SceneUuid);
+
+ /// The scene item this message is about, ready to act on without a lookup.
+ public SceneItemHandle SceneItem => SceneItemHandle.For(SceneHandle.FromUuid(payload.SceneUuid), payload.SceneItemId);
+
+ }
+
+ extension(ObsWebSocket.Core.Protocol.Events.SceneItemTransformChangedPayload payload)
+ {
+ /// The scene this message identifies, addressed by uuid so a rename cannot move it.
+ public SceneHandle Scene => SceneHandle.FromUuid(payload.SceneUuid);
+
+ /// The scene item this message is about, ready to act on without a lookup.
+ public SceneItemHandle SceneItem => SceneItemHandle.For(SceneHandle.FromUuid(payload.SceneUuid), payload.SceneItemId);
+
+ }
+
+ extension(ObsWebSocket.Core.Protocol.Events.SceneNameChangedPayload payload)
+ {
+ /// The scene this message identifies, addressed by uuid so a rename cannot move it.
+ public SceneHandle Scene => SceneHandle.FromUuid(payload.SceneUuid);
+
+ }
+
+ extension(ObsWebSocket.Core.Protocol.Events.SceneRemovedPayload payload)
+ {
+ /// The scene this message identifies, addressed by uuid so a rename cannot move it.
+ public SceneHandle Scene => SceneHandle.FromUuid(payload.SceneUuid);
+
+ }
+
+ extension(ObsWebSocket.Core.Protocol.Responses.CreateInputResponseData response)
+ {
+ /// The input this message identifies, addressed by uuid so a rename cannot move it.
+ public InputHandle Input => InputHandle.FromUuid(response.InputUuid);
+
+ }
+
+ extension(ObsWebSocket.Core.Protocol.Responses.CreateSceneResponseData response)
+ {
+ /// The scene this message identifies, addressed by uuid so a rename cannot move it.
+ public SceneHandle Scene => SceneHandle.FromUuid(response.SceneUuid);
+
+ }
+
+ extension(ObsWebSocket.Core.Protocol.Responses.GetCurrentPreviewSceneResponseData response)
+ {
+ /// The scene this message identifies, addressed by uuid so a rename cannot move it.
+ public SceneHandle Scene => SceneHandle.FromUuid(response.SceneUuid);
+
+ /// The preview scene this message identifies, addressed by uuid so a rename cannot move it.
+ public SceneHandle CurrentPreviewScene => SceneHandle.FromUuid(response.CurrentPreviewSceneUuid);
+
+ }
+
+ extension(ObsWebSocket.Core.Protocol.Responses.GetCurrentProgramSceneResponseData response)
+ {
+ /// The scene this message identifies, addressed by uuid so a rename cannot move it.
+ public SceneHandle Scene => SceneHandle.FromUuid(response.SceneUuid);
+
+ /// The program scene this message identifies, addressed by uuid so a rename cannot move it.
+ public SceneHandle CurrentProgramScene => SceneHandle.FromUuid(response.CurrentProgramSceneUuid);
+
+ }
+
+ extension(ObsWebSocket.Core.Protocol.Responses.GetSceneItemSourceResponseData response)
+ {
+ /// The source this message identifies, addressed by uuid so a rename cannot move it.
+ public SourceHandle Source => SourceHandle.FromUuid(response.SourceUuid);
+
+ }
+
+ extension(ObsWebSocket.Core.Protocol.Responses.GetSceneListResponseData response)
+ {
+ /// The program scene this message identifies, addressed by uuid so a rename cannot move it.
+ public SceneHandle? CurrentProgramScene =>
+ string.IsNullOrEmpty(response.CurrentProgramSceneUuid) ? null : SceneHandle.FromUuid(response.CurrentProgramSceneUuid);
+
+ /// The preview scene this message identifies, addressed by uuid so a rename cannot move it.
+ public SceneHandle? CurrentPreviewScene =>
+ string.IsNullOrEmpty(response.CurrentPreviewSceneUuid) ? null : SceneHandle.FromUuid(response.CurrentPreviewSceneUuid);
+
+ }
+
+}
+
+// Handles reachable without a lookup: 47
diff --git a/ObsWebSocket.Core/Generated/Protocol/Events/CurrentSceneTransitionDurationChanged.EventPayload.g.cs b/ObsWebSocket.Core/Generated/Protocol/Events/CurrentSceneTransitionDurationChanged.EventPayload.g.cs
index 443a405..670e30e 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 int TransitionDuration { get; init; }
+ public required long 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(int transitionDuration)
+ public CurrentSceneTransitionDurationChangedPayload(long transitionDuration)
{
this.TransitionDuration = transitionDuration;
}
diff --git a/ObsWebSocket.Core/Generated/Protocol/Events/SceneItemCreated.EventPayload.g.cs b/ObsWebSocket.Core/Generated/Protocol/Events/SceneItemCreated.EventPayload.g.cs
index 406822e..c171dfd 100644
--- a/ObsWebSocket.Core/Generated/Protocol/Events/SceneItemCreated.EventPayload.g.cs
+++ b/ObsWebSocket.Core/Generated/Protocol/Events/SceneItemCreated.EventPayload.g.cs
@@ -29,7 +29,7 @@ public sealed partial record SceneItemCreatedPayload
///
[JsonPropertyName("sceneItemId")]
[Key("sceneItemId")]
- public required int SceneItemId { get; init; }
+ public required long SceneItemId { get; init; }
///
/// Index position of the item
@@ -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(string sceneName, string sceneUuid, string sourceName, string sourceUuid, int sceneItemId, int sceneItemIndex)
+ public SceneItemCreatedPayload(string sceneName, string sceneUuid, string sourceName, string sourceUuid, long sceneItemId, int sceneItemIndex)
{
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 99acf10..9dc05e7 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 int SceneItemId { get; init; }
+ public required long 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(string sceneName, string sceneUuid, int sceneItemId, bool sceneItemEnabled)
+ public SceneItemEnableStateChangedPayload(string sceneName, string sceneUuid, long sceneItemId, bool sceneItemEnabled)
{
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 b13fc77..e343624 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 int SceneItemId { get; init; }
+ public required long 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(string sceneName, string sceneUuid, int sceneItemId, bool sceneItemLocked)
+ public SceneItemLockStateChangedPayload(string sceneName, string sceneUuid, long sceneItemId, bool sceneItemLocked)
{
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 5d69c8b..8f128c1 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 int SceneItemId { get; init; }
+ public required long 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(string sceneName, string sceneUuid, string sourceName, string sourceUuid, int sceneItemId)
+ public SceneItemRemovedPayload(string sceneName, string sceneUuid, string sourceName, string sourceUuid, long sceneItemId)
{
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 defb464..f5c93a5 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 int SceneItemId { get; init; }
+ public required long 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(string sceneName, string sceneUuid, int sceneItemId)
+ public SceneItemSelectedPayload(string sceneName, string sceneUuid, long sceneItemId)
{
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 636a70d..8d1ee0a 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 int SceneItemId { get; init; }
+ public required long 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(string sceneName, string sceneUuid, int sceneItemId, ObsWebSocket.Core.Protocol.Common.SceneItemTransformStub sceneItemTransform)
+ public SceneItemTransformChangedPayload(string sceneName, string sceneUuid, long sceneItemId, ObsWebSocket.Core.Protocol.Common.SceneItemTransformStub sceneItemTransform)
{
this.SceneName = sceneName;
this.SceneUuid = sceneUuid;
diff --git a/ObsWebSocket.Core/Generated/Protocol/Requests/DuplicateSceneItem.Request.g.cs b/ObsWebSocket.Core/Generated/Protocol/Requests/DuplicateSceneItem.Request.g.cs
index a9f3e2e..fe51c9a 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 int SceneItemId { get; init; }
+ public required long 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(int sceneItemId, string? canvasUuid = null, string? sceneName = null, string? sceneUuid = null, string? destinationSceneName = null, string? destinationSceneUuid = null)
+ public DuplicateSceneItemRequestData(long 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 bc37f79..ee54579 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 int SceneItemId { get; init; }
+ public required long 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(int sceneItemId, string? canvasUuid = null, string? sceneName = null, string? sceneUuid = null)
+ public GetSceneItemBlendModeRequestData(long 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 7308cf6..9f10718 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 int SceneItemId { get; init; }
+ public required long 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(int sceneItemId, string? canvasUuid = null, string? sceneName = null, string? sceneUuid = null)
+ public GetSceneItemEnabledRequestData(long sceneItemId, string? canvasUuid = null, string? sceneName = null, string? sceneUuid = 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 c4b96b7..b3194da 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 int SceneItemId { get; init; }
+ public required long 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(int sceneItemId, string? canvasUuid = null, string? sceneName = null, string? sceneUuid = null)
+ public GetSceneItemIndexRequestData(long 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 6afb955..2436b6b 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 int SceneItemId { get; init; }
+ public required long 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(int sceneItemId, string? canvasUuid = null, string? sceneName = null, string? sceneUuid = null)
+ public GetSceneItemLockedRequestData(long 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 34415b4..9416886 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 int SceneItemId { get; init; }
+ public required long 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(int sceneItemId, string? canvasUuid = null, string? sceneName = null, string? sceneUuid = null)
+ public GetSceneItemSourceRequestData(long 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 af05154..8ef3d56 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 int SceneItemId { get; init; }
+ public required long 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(int sceneItemId, string? canvasUuid = null, string? sceneName = null, string? sceneUuid = null)
+ public GetSceneItemTransformRequestData(long sceneItemId, string? canvasUuid = null, string? sceneName = null, string? sceneUuid = null)
{
this.CanvasUuid = canvasUuid;
this.SceneName = sceneName;
diff --git a/ObsWebSocket.Core/Generated/Protocol/Requests/RemoveSceneItem.Request.g.cs b/ObsWebSocket.Core/Generated/Protocol/Requests/RemoveSceneItem.Request.g.cs
index 7a3b6ae..938fc95 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 int SceneItemId { get; init; }
+ public required long 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(int sceneItemId, string? canvasUuid = null, string? sceneName = null, string? sceneUuid = null)
+ public RemoveSceneItemRequestData(long sceneItemId, string? canvasUuid = null, string? sceneName = null, string? sceneUuid = null)
{
this.CanvasUuid = canvasUuid;
this.SceneName = sceneName;
diff --git a/ObsWebSocket.Core/Generated/Protocol/Requests/SetCurrentSceneTransitionDuration.Request.g.cs b/ObsWebSocket.Core/Generated/Protocol/Requests/SetCurrentSceneTransitionDuration.Request.g.cs
index 6236fe9..c5aca9b 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 int TransitionDuration { get; init; }
+ public required long 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(int transitionDuration)
+ public SetCurrentSceneTransitionDurationRequestData(long transitionDuration)
{
this.TransitionDuration = transitionDuration;
}
diff --git a/ObsWebSocket.Core/Generated/Protocol/Requests/SetSceneItemBlendMode.Request.g.cs b/ObsWebSocket.Core/Generated/Protocol/Requests/SetSceneItemBlendMode.Request.g.cs
index 2f83328..6c62eae 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 int SceneItemId { get; init; }
+ public required long 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(int sceneItemId, string sceneItemBlendMode, string? canvasUuid = null, string? sceneName = null, string? sceneUuid = null)
+ public SetSceneItemBlendModeRequestData(long 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 4f488fc..ce9870d 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 int SceneItemId { get; init; }
+ public required long 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(int sceneItemId, bool sceneItemEnabled, string? canvasUuid = null, string? sceneName = null, string? sceneUuid = null)
+ public SetSceneItemEnabledRequestData(long 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 9b52d13..8505b1b 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 int SceneItemId { get; init; }
+ public required long SceneItemId { get; init; }
///
/// New index position of the scene item
@@ -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(int sceneItemId, int sceneItemIndex, string? canvasUuid = null, string? sceneName = null, string? sceneUuid = null)
+ public SetSceneItemIndexRequestData(long 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 540613b..7c96467 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 int SceneItemId { get; init; }
+ public required long 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(int sceneItemId, bool sceneItemLocked, string? canvasUuid = null, string? sceneName = null, string? sceneUuid = null)
+ public SetSceneItemLockedRequestData(long 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 431ab4d..e3c662a 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 int SceneItemId { get; init; }
+ public required long 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(int sceneItemId, ObsWebSocket.Core.Protocol.Common.SceneItemTransformPatchStub? sceneItemTransform, string? canvasUuid = null, string? sceneName = null, string? sceneUuid = null)
+ public SetSceneItemTransformRequestData(long sceneItemId, ObsWebSocket.Core.Protocol.Common.SceneItemTransformPatchStub? 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 ac2de92..5a14346 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 int? TransitionDuration { get; init; }
+ public long? 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, int? transitionDuration = null)
+ public SetSceneSceneTransitionOverrideRequestData(string? canvasUuid = null, string? sceneName = null, string? sceneUuid = null, string? transitionName = null, long? transitionDuration = null)
{
this.CanvasUuid = canvasUuid;
this.SceneName = sceneName;
diff --git a/ObsWebSocket.Core/Generated/Protocol/Responses/CreateInput.Response.g.cs b/ObsWebSocket.Core/Generated/Protocol/Responses/CreateInput.Response.g.cs
index feaba6e..150deb4 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 int SceneItemId { get; init; }
+ public required long 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(string inputUuid, int sceneItemId)
+ public CreateInputResponseData(string inputUuid, long sceneItemId)
{
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 d81b54c..275bd4b 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 int SceneItemId { get; init; }
+ public required long 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(int sceneItemId)
+ public CreateSceneItemResponseData(long 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 cc4c27a..22cb4cd 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 int SceneItemId { get; init; }
+ public required long 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(int sceneItemId)
+ public DuplicateSceneItemResponseData(long 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 2e1ebc2..0edd818 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 int? TransitionDuration { get; init; }
+ public long? 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(string transitionName, string transitionUuid, string transitionKind, bool transitionFixed, bool transitionConfigurable, int? transitionDuration = null, System.Text.Json.JsonElement? transitionSettings = default)
+ public GetCurrentSceneTransitionResponseData(string transitionName, string transitionUuid, string transitionKind, bool transitionFixed, bool transitionConfigurable, long? transitionDuration = null, System.Text.Json.JsonElement? transitionSettings = default)
{
this.TransitionName = transitionName;
this.TransitionUuid = transitionUuid;
diff --git a/ObsWebSocket.Core/Generated/Protocol/Responses/GetOutputStatus.Response.g.cs b/ObsWebSocket.Core/Generated/Protocol/Responses/GetOutputStatus.Response.g.cs
index bb89ae2..ff0ca93 100644
--- a/ObsWebSocket.Core/Generated/Protocol/Responses/GetOutputStatus.Response.g.cs
+++ b/ObsWebSocket.Core/Generated/Protocol/Responses/GetOutputStatus.Response.g.cs
@@ -64,7 +64,7 @@ public sealed partial record GetOutputStatusResponseData
///
[JsonPropertyName("outputSkippedFrames")]
[Key("outputSkippedFrames")]
- public required int OutputSkippedFrames { get; init; }
+ public required long 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 int OutputTotalFrames { get; init; }
+ public required long 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, string outputTimecode, long outputDuration, double outputCongestion, long outputBytes, int outputSkippedFrames, int outputTotalFrames)
+ public GetOutputStatusResponseData(bool outputActive, bool outputReconnecting, string outputTimecode, long outputDuration, double outputCongestion, long outputBytes, long outputSkippedFrames, long outputTotalFrames)
{
this.OutputActive = outputActive;
this.OutputReconnecting = outputReconnecting;
diff --git a/ObsWebSocket.Core/Generated/Protocol/Responses/GetSceneItemId.Response.g.cs b/ObsWebSocket.Core/Generated/Protocol/Responses/GetSceneItemId.Response.g.cs
index 4fbba5a..dbf827b 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 int SceneItemId { get; init; }
+ public required long 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(int sceneItemId)
+ public GetSceneItemIdResponseData(long sceneItemId)
{
this.SceneItemId = sceneItemId;
}
diff --git a/ObsWebSocket.Core/Generated/Protocol/Responses/GetSceneSceneTransitionOverride.Response.g.cs b/ObsWebSocket.Core/Generated/Protocol/Responses/GetSceneSceneTransitionOverride.Response.g.cs
index c1cece7..2dde8b6 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 int? TransitionDuration { get; init; }
+ public long? 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, int? transitionDuration = null)
+ public GetSceneSceneTransitionOverrideResponseData(string? transitionName = null, long? transitionDuration = null)
{
this.TransitionName = transitionName;
this.TransitionDuration = transitionDuration;
diff --git a/ObsWebSocket.Core/Generated/Protocol/Responses/GetStats.Response.g.cs b/ObsWebSocket.Core/Generated/Protocol/Responses/GetStats.Response.g.cs
index 07d2863..0b4fa0d 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 int OutputSkippedFrames { get; init; }
+ public required long OutputSkippedFrames { get; init; }
///
/// Total number of frames outputted by the output thread
///
[JsonPropertyName("outputTotalFrames")]
[Key("outputTotalFrames")]
- public required int OutputTotalFrames { get; init; }
+ public required long OutputTotalFrames { get; init; }
///
/// Number of frames skipped by OBS in the render thread
///
[JsonPropertyName("renderSkippedFrames")]
[Key("renderSkippedFrames")]
- public required int RenderSkippedFrames { get; init; }
+ public required long RenderSkippedFrames { get; init; }
///
/// Total number of frames outputted by the render thread
///
[JsonPropertyName("renderTotalFrames")]
[Key("renderTotalFrames")]
- public required int RenderTotalFrames { get; init; }
+ public required long RenderTotalFrames { get; init; }
///
/// Total number of messages received by obs-websocket from the client
///
[JsonPropertyName("webSocketSessionIncomingMessages")]
[Key("webSocketSessionIncomingMessages")]
- public required int WebSocketSessionIncomingMessages { get; init; }
+ public required long WebSocketSessionIncomingMessages { get; init; }
///
/// Total number of messages sent by obs-websocket to the client
///
[JsonPropertyName("webSocketSessionOutgoingMessages")]
[Key("webSocketSessionOutgoingMessages")]
- public required int WebSocketSessionOutgoingMessages { get; init; }
+ public required long 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, int renderSkippedFrames, int renderTotalFrames, int outputSkippedFrames, int outputTotalFrames, int webSocketSessionIncomingMessages, int webSocketSessionOutgoingMessages)
+ public GetStatsResponseData(double cpuUsage, double memoryUsage, double availableDiskSpace, double activeFps, double averageFrameRenderTime, long renderSkippedFrames, long renderTotalFrames, long outputSkippedFrames, long outputTotalFrames, long webSocketSessionIncomingMessages, long 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 47912f4..221d672 100644
--- a/ObsWebSocket.Core/Generated/Protocol/Responses/GetStreamStatus.Response.g.cs
+++ b/ObsWebSocket.Core/Generated/Protocol/Responses/GetStreamStatus.Response.g.cs
@@ -64,7 +64,7 @@ public sealed partial record GetStreamStatusResponseData
///
[JsonPropertyName("outputSkippedFrames")]
[Key("outputSkippedFrames")]
- public required int OutputSkippedFrames { get; init; }
+ public required long 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 int OutputTotalFrames { get; init; }
+ public required long 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, string outputTimecode, long outputDuration, double outputCongestion, long outputBytes, int outputSkippedFrames, int outputTotalFrames)
+ public GetStreamStatusResponseData(bool outputActive, bool outputReconnecting, string outputTimecode, long outputDuration, double outputCongestion, long outputBytes, long outputSkippedFrames, long outputTotalFrames)
{
this.OutputActive = outputActive;
this.OutputReconnecting = outputReconnecting;
diff --git a/ObsWebSocket.Core/Groups/SceneItemsGroup.cs b/ObsWebSocket.Core/Groups/SceneItemsGroup.cs
index 9c682dc..d862b25 100644
--- a/ObsWebSocket.Core/Groups/SceneItemsGroup.cs
+++ b/ObsWebSocket.Core/Groups/SceneItemsGroup.cs
@@ -31,7 +31,7 @@ public readonly partial struct SceneItemsGroup
/// Thrown if the client is not connected.
public async Task SetSceneItemEnabledAsync(
string sceneName,
- int sceneItemId,
+ long sceneItemId,
bool? isEnabled = null, // If null, toggles; otherwise sets to the specified state
CancellationToken cancellationToken = default
)
@@ -99,7 +99,7 @@ public async Task SetSceneItemEnabledAsync(
ArgumentException.ThrowIfNullOrEmpty(sourceName);
client.EnsureConnected();
- int? sceneItemId = await client
+ long? sceneItemId = await client
.SceneItems.FindSceneItemIdAsync(sceneName, sourceName, cancellationToken)
.ConfigureAwait(false);
@@ -126,7 +126,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
@@ -155,20 +155,4 @@ public async Task SetSceneItemEnabledAsync(
}
// Let other ObsWebSocketExceptions or different exception types propagate
}
-
- ///
- /// 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.
- [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
- ) => FindSceneItemIdAsync(sceneName, sourceName, cancellationToken);
}
diff --git a/ObsWebSocket.Core/Groups/SourcesGroup.cs b/ObsWebSocket.Core/Groups/SourcesGroup.cs
index 898a9dc..76b3586 100644
--- a/ObsWebSocket.Core/Groups/SourcesGroup.cs
+++ b/ObsWebSocket.Core/Groups/SourcesGroup.cs
@@ -163,7 +163,7 @@ await client
/// Optional output width. uses the source width.
/// Optional output height. uses the source height.
///
- /// JPEG compression quality 0–100 (-1 uses the OBS default).
+ /// JPEG compression quality 0 to 100 (-1 uses the OBS default).
/// Ignored for lossless formats.
///
///
@@ -222,7 +222,7 @@ public async Task GetSourceScreenshotOnCanvasBytesAsync(
/// Image format: "png", "jpg", or "bmp".
/// Optional output width. uses the source width.
/// Optional output height. uses the source height.
- /// JPEG compression quality 0–100 (-1 uses the OBS default).
+ /// JPEG compression quality 0 to 100 (-1 uses the OBS default).
///
/// Optional source UUID for unambiguous identification.
/// When the lookup is by alone.
diff --git a/ObsWebSocket.Core/Handles/ObsHandleResolution.cs b/ObsWebSocket.Core/Handles/ObsHandleResolution.cs
new file mode 100644
index 0000000..6fa3bbc
--- /dev/null
+++ b/ObsWebSocket.Core/Handles/ObsHandleResolution.cs
@@ -0,0 +1,284 @@
+using ObsWebSocket.Core.Protocol.Common;
+using ObsWebSocket.Core.Protocol.Requests;
+using ObsWebSocket.Core.Protocol.Responses;
+
+namespace ObsWebSocket.Core;
+
+// Turning a name into a uuid is a round trip, so it is never done implicitly.
+//
+// Resolution lives on the category group rather than on the handle, because a handle holds no
+// client: it has to be constructible from a bare string for the request overloads to accept one.
+//
+// There is no narrow lookup in the protocol. Nothing answers "what is the uuid of the scene called
+// X", so resolving a scene means GetSceneList and resolving an input means GetInputList. Both are
+// cheap by construction: OBS builds each entry from a handful of field reads, and the websocket
+// frame costs more than the enumeration. The list also pays for itself, because a miss can say
+// what does exist.
+
+public readonly partial struct ScenesGroup
+{
+ ///
+ /// Looks up a scene's uuid, so the handle survives a rename and needs no canvas.
+ ///
+ /// The scene to resolve. Already-resolved handles are returned unchanged.
+ /// A token to cancel the lookup.
+ ///
+ /// Thrown when no scene has that name, listing the scenes that do exist.
+ ///
+ public async ValueTask ResolveAsync(
+ SceneHandle scene,
+ CancellationToken cancellationToken = default
+ )
+ {
+ ArgumentNullException.ThrowIfNull(scene);
+ if (scene.IsResolved)
+ {
+ return scene;
+ }
+
+ GetSceneListResponseData scenes = await this.GetSceneListAsync(
+ new GetSceneListRequestData(canvasUuid: scene.Canvas.Uuid),
+ cancellationToken
+ )
+ .ConfigureAwait(false);
+
+ SceneStub? match = scenes.Scenes.Find(s =>
+ string.Equals(s.SceneName, scene.Name, StringComparison.Ordinal)
+ );
+
+ return match is not null
+ ? SceneHandle.FromUuid(match.SceneUuid)
+ : throw ObsWebSocketResourceNotFoundException.For(
+ "scene",
+ scene.Name!,
+ scenes.Scenes.ConvertAll(s => s.SceneName),
+ scene.Canvas
+ );
+ }
+}
+
+public readonly partial struct InputsGroup
+{
+ ///
+ /// Looks up an input's uuid, so the handle survives a rename.
+ ///
+ /// The input to resolve. Already-resolved handles are returned unchanged.
+ /// A token to cancel the lookup.
+ ///
+ /// Thrown when no input has that name, listing the inputs that do exist.
+ ///
+ public async ValueTask ResolveAsync(
+ InputHandle input,
+ CancellationToken cancellationToken = default
+ )
+ {
+ ArgumentNullException.ThrowIfNull(input);
+ if (input.IsResolved)
+ {
+ return input;
+ }
+
+ GetInputListResponseData inputs = await this.GetInputListAsync(
+ new GetInputListRequestData(),
+ cancellationToken
+ )
+ .ConfigureAwait(false);
+
+ InputStub? match = inputs.Inputs.Find(i =>
+ string.Equals(i.InputName, input.Name, StringComparison.Ordinal)
+ );
+
+ return match is not null
+ ? InputHandle.FromUuid(match.InputUuid)
+ : throw ObsWebSocketResourceNotFoundException.For(
+ "input",
+ input.Name!,
+ inputs.Inputs.ConvertAll(i => i.InputName),
+ null
+ );
+ }
+}
+
+public readonly partial struct CanvasesGroup
+{
+ ///
+ /// Looks up a canvas's uuid.
+ ///
+ ///
+ /// The one lookup the protocol cannot express itself: no request takes a canvas name, so this
+ /// is the only way to address a canvas you know by name. It is what obs-websocket-js was
+ /// considering adding as a helper for the same reason.
+ ///
+ /// The canvas to resolve. The main canvas and resolved handles come back unchanged.
+ /// A token to cancel the lookup.
+ ///
+ /// Thrown when no canvas has that name, listing the canvases that do exist.
+ ///
+ public async ValueTask ResolveAsync(
+ CanvasHandle canvas,
+ CancellationToken cancellationToken = default
+ )
+ {
+ ArgumentNullException.ThrowIfNull(canvas);
+ if (canvas.IsResolved || canvas.Name is null)
+ {
+ return canvas;
+ }
+
+ GetCanvasListResponseData canvases = await this.GetCanvasListAsync(cancellationToken)
+ .ConfigureAwait(false);
+
+ CanvasStub? match = canvases.Canvases.Find(c =>
+ string.Equals(c.CanvasName, canvas.Name, StringComparison.Ordinal)
+ );
+
+ return match is not null
+ ? CanvasHandle.FromUuid(match.CanvasUuid)
+ : throw ObsWebSocketResourceNotFoundException.For(
+ "canvas",
+ canvas.Name,
+ canvases.Canvases.ConvertAll(c => c.CanvasName),
+ null
+ );
+ }
+}
+
+public readonly partial struct SourcesGroup
+{
+ ///
+ /// Looks up a source's uuid. A source is a scene or an input, so this may take two lookups.
+ ///
+ ///
+ /// Inputs are checked first, because the requests that take a bare source are mostly about
+ /// inputs, so the scene list is usually never fetched at all.
+ ///
+ /// The source to resolve. Already-resolved handles are returned unchanged.
+ /// A token to cancel the lookup.
+ ///
+ /// Thrown when neither the inputs nor the scenes have that name.
+ ///
+ public async ValueTask ResolveAsync(
+ SourceHandle source,
+ CancellationToken cancellationToken = default
+ )
+ {
+ ArgumentNullException.ThrowIfNull(source);
+ if (source.IsResolved)
+ {
+ return source;
+ }
+
+ GetInputListResponseData inputs = await client
+ .Inputs.GetInputListAsync(new GetInputListRequestData(), cancellationToken)
+ .ConfigureAwait(false);
+
+ InputStub? input = inputs.Inputs.Find(i =>
+ string.Equals(i.InputName, source.Name, StringComparison.Ordinal)
+ );
+ if (input is not null)
+ {
+ return SourceHandle.FromUuid(input.InputUuid);
+ }
+
+ GetSceneListResponseData scenes = await client
+ .Scenes.GetSceneListAsync(
+ new GetSceneListRequestData(canvasUuid: source.Canvas.Uuid),
+ cancellationToken
+ )
+ .ConfigureAwait(false);
+
+ SceneStub? scene = scenes.Scenes.Find(s =>
+ string.Equals(s.SceneName, source.Name, StringComparison.Ordinal)
+ );
+
+ return scene is not null
+ ? SourceHandle.FromUuid(scene.SceneUuid)
+ : throw ObsWebSocketResourceNotFoundException.For(
+ "source",
+ source.Name!,
+ [
+ .. inputs.Inputs.ConvertAll(i => i.InputName),
+ .. scenes.Scenes.ConvertAll(s => s.SceneName),
+ ],
+ source.Canvas
+ );
+ }
+}
+
+public readonly partial struct SceneItemsGroup
+{
+ ///
+ /// Looks up the numeric id OBS gave a source inside a scene.
+ ///
+ ///
+ /// The one resolution that is not a convenience. OBS addresses scene items by a number nothing
+ /// else tells you, so GetSceneItemId is the only way in.
+ ///
+ /// The scene and source name to look up.
+ ///
+ /// Which match to take when a source appears more than once in the scene. 0 is the first from
+ /// the bottom; -1 is the topmost.
+ ///
+ /// A token to cancel the lookup.
+ ///
+ /// Thrown when the scene holds no such source, listing the sources it does hold.
+ ///
+ public async ValueTask ResolveAsync(
+ UnresolvedSceneItem item,
+ int searchOffset = 0,
+ CancellationToken cancellationToken = default
+ )
+ {
+ ArgumentNullException.ThrowIfNull(item);
+
+ try
+ {
+ GetSceneItemIdResponseData found = await this.GetSceneItemIdAsync(
+ new GetSceneItemIdRequestData(
+ sourceName: item.SourceName,
+ canvasUuid: item.Scene.Canvas.Uuid,
+ sceneName: item.Scene.Name,
+ sceneUuid: item.Scene.Uuid,
+ searchOffset: searchOffset
+ ),
+ cancellationToken
+ )
+ .ConfigureAwait(false);
+
+ return SceneItemHandle.For(item.Scene, found.SceneItemId);
+ }
+ catch (ObsWebSocketRequestException ex)
+ when (ex.StatusCode == Protocol.Generated.RequestStatusCode.ResourceNotFound)
+ {
+ // OBS says only that it did not find it. The scene's contents are one more request and
+ // turn that into something the caller can act on.
+ List present = [];
+ try
+ {
+ GetSceneItemListResponseData items = await this.GetSceneItemListAsync(
+ new GetSceneItemListRequestData(
+ canvasUuid: item.Scene.Canvas.Uuid,
+ sceneName: item.Scene.Name,
+ sceneUuid: item.Scene.Uuid
+ ),
+ cancellationToken
+ )
+ .ConfigureAwait(false);
+ present = items.SceneItems.ConvertAll(i => i.SourceName);
+ }
+ catch (ObsWebSocketRequestException)
+ {
+ // Deliberately not logged: the scene may be gone too, and the original failure is
+ // the one worth reporting.
+ }
+
+ throw ObsWebSocketResourceNotFoundException.For(
+ $"source in {item.Scene}",
+ item.SourceName,
+ present,
+ item.Scene.Canvas,
+ ex
+ );
+ }
+ }
+}
diff --git a/ObsWebSocket.Core/Handles/ObsHandles.cs b/ObsWebSocket.Core/Handles/ObsHandles.cs
new file mode 100644
index 0000000..1975adb
--- /dev/null
+++ b/ObsWebSocket.Core/Handles/ObsHandles.cs
@@ -0,0 +1,365 @@
+using System.Diagnostics.CodeAnalysis;
+
+namespace ObsWebSocket.Core;
+
+///
+/// How a request addresses one thing in OBS: by name, or by uuid.
+///
+///
+/// The protocol takes both as optional fields and resolves them in a fixed order. From
+/// Request::AcquireSource: a uuid wins outright, a name is only read when no uuid was sent,
+/// the canvas is only consulted on the name path, and neither field present is
+/// MissingRequestField. So sending both is not an error, it silently ignores the name, and
+/// sending neither compiles today and fails at runtime.
+///
+/// A handle is that choice made once and carried, rather than restated on every call. It holds
+/// identity only: OBS state drifts, and a handle that cached a name or a scene item id would go
+/// stale without saying so.
+///
+///
+public interface IObsHandle
+{
+ /// The name this handle addresses by, or when it holds a uuid.
+ string? Name { get; }
+
+ /// The uuid this handle addresses by, or when it holds a name.
+ ///
+ /// Kept as the wire string and never parsed. OBS produces RFC 4122 uuids, but a response is
+ /// not the place to discover that one day it did not.
+ ///
+ string? Uuid { get; }
+
+ ///
+ /// Whether this handle addresses by uuid, and so survives a rename.
+ ///
+ [MemberNotNullWhen(true, nameof(Uuid))]
+ bool IsResolved { get; }
+}
+
+///
+/// A canvas. Every canvas-scoped request takes a uuid and there is no canvasName field in
+/// the protocol at all, so a name has to be resolved before it can be used.
+///
+///
+/// Omitting the canvas means the main canvas, which is why is a value rather
+/// than a null check at every call site.
+///
+public sealed record CanvasHandle : IObsHandle
+{
+ private CanvasHandle(string? name, string? uuid)
+ {
+ Name = name;
+ Uuid = uuid;
+ }
+
+ /// The main canvas, which is what OBS uses when no canvas uuid is sent.
+ public static CanvasHandle Main { get; } = new(null, null);
+
+ ///
+ public string? Name { get; }
+
+ ///
+ public string? Uuid { get; }
+
+ ///
+ [MemberNotNullWhen(true, nameof(Uuid))]
+ public bool IsResolved => Uuid is not null;
+
+ /// Addresses a canvas by name, which needs resolving before any request accepts it.
+ public static CanvasHandle FromName(string name)
+ {
+ ArgumentException.ThrowIfNullOrEmpty(name);
+ return new CanvasHandle(name, null);
+ }
+
+ /// Addresses a canvas by uuid, with no lookup.
+ public static CanvasHandle FromUuid(Guid uuid) => new(null, uuid.ToString("D"));
+
+ /// Addresses a canvas by uuid as OBS wrote it.
+ public static CanvasHandle FromUuid(string uuid)
+ {
+ ArgumentException.ThrowIfNullOrEmpty(uuid);
+ return new CanvasHandle(null, uuid);
+ }
+
+ /// Addresses a canvas by name.
+ public static implicit operator CanvasHandle(string name) => FromName(name);
+
+ /// Addresses a canvas by uuid.
+ public static implicit operator CanvasHandle(Guid uuid) => FromUuid(uuid);
+
+ /// A scene on this canvas, addressed by name.
+ public SceneHandle Scene(string name) => SceneHandle.FromName(name, this);
+
+ ///
+ public override string ToString() =>
+ IsResolved ? $"canvas {Uuid}"
+ : Name is not null ? $"canvas '{Name}'"
+ : "the main canvas";
+}
+
+///
+/// A scene, addressed by name or by uuid.
+///
+///
+/// A name is only unique within a canvas, which is why the canvas travels with a name handle and
+/// is dropped from a uuid handle: OBS reads canvasUuid only on the name path and ignores it
+/// otherwise.
+///
+public sealed record SceneHandle : IObsHandle
+{
+ private SceneHandle(string? name, string? uuid, CanvasHandle canvas)
+ {
+ Name = name;
+ Uuid = uuid;
+ Canvas = canvas;
+ }
+
+ ///
+ public string? Name { get; }
+
+ ///
+ public string? Uuid { get; }
+
+ ///
+ /// The canvas a name is looked up in. Meaningless once the handle is resolved, because OBS
+ /// does not read the canvas field when a uuid is present.
+ ///
+ public CanvasHandle Canvas { get; }
+
+ ///
+ [MemberNotNullWhen(true, nameof(Uuid))]
+ public bool IsResolved => Uuid is not null;
+
+ /// Addresses a scene by name, optionally on a canvas other than the main one.
+ public static SceneHandle FromName(string name, CanvasHandle? canvas = null)
+ {
+ ArgumentException.ThrowIfNullOrEmpty(name);
+ return new SceneHandle(name, null, canvas ?? CanvasHandle.Main);
+ }
+
+ /// Addresses a scene by uuid, with no lookup.
+ public static SceneHandle FromUuid(Guid uuid) => FromUuid(uuid.ToString("D"));
+
+ /// Addresses a scene by uuid as OBS wrote it.
+ public static SceneHandle FromUuid(string uuid)
+ {
+ ArgumentException.ThrowIfNullOrEmpty(uuid);
+ return new SceneHandle(null, uuid, CanvasHandle.Main);
+ }
+
+ /// Addresses a scene by name on the main canvas.
+ public static implicit operator SceneHandle(string name) => FromName(name);
+
+ /// Addresses a scene by uuid.
+ public static implicit operator SceneHandle(Guid uuid) => FromUuid(uuid);
+
+ ///
+ /// A scene item in this scene, by the numeric id OBS assigned it.
+ ///
+ public SceneItemHandle Item(long sceneItemId) => SceneItemHandle.For(this, sceneItemId);
+
+ ///
+ /// A scene item in this scene, by the name of the source it shows. Needs resolving, because
+ /// only GetSceneItemId knows the id.
+ ///
+ public UnresolvedSceneItem Item(string sourceName) =>
+ new(this, sourceName ?? throw new ArgumentNullException(nameof(sourceName)));
+
+ /// This scene addressed as a source, for the requests that take any source.
+ public SourceHandle AsSource() =>
+ IsResolved ? SourceHandle.FromUuid(Uuid) : SourceHandle.FromName(Name!, Canvas);
+
+ ///
+ public override string ToString() => IsResolved ? $"scene {Uuid}" : $"scene '{Name}'";
+}
+
+///
+/// An input, addressed by name or by uuid.
+///
+///
+/// Input requests carry no canvas field: an input is not scoped to one.
+///
+public sealed record InputHandle : IObsHandle
+{
+ private InputHandle(string? name, string? uuid)
+ {
+ Name = name;
+ Uuid = uuid;
+ }
+
+ ///
+ public string? Name { get; }
+
+ ///
+ public string? Uuid { get; }
+
+ ///
+ [MemberNotNullWhen(true, nameof(Uuid))]
+ public bool IsResolved => Uuid is not null;
+
+ /// Addresses an input by name.
+ public static InputHandle FromName(string name)
+ {
+ ArgumentException.ThrowIfNullOrEmpty(name);
+ return new InputHandle(name, null);
+ }
+
+ /// Addresses an input by uuid, with no lookup.
+ public static InputHandle FromUuid(Guid uuid) => FromUuid(uuid.ToString("D"));
+
+ /// Addresses an input by uuid as OBS wrote it.
+ public static InputHandle FromUuid(string uuid)
+ {
+ ArgumentException.ThrowIfNullOrEmpty(uuid);
+ return new InputHandle(null, uuid);
+ }
+
+ /// Addresses an input by name.
+ public static implicit operator InputHandle(string name) => FromName(name);
+
+ /// Addresses an input by uuid.
+ public static implicit operator InputHandle(Guid uuid) => FromUuid(uuid);
+
+ /// A filter on this input, by name. Filter names are the identity; nothing to resolve.
+ public FilterHandle Filter(string filterName) => FilterHandle.For(AsSource(), filterName);
+
+ /// This input addressed as a source, for the requests that take any source.
+ public SourceHandle AsSource() =>
+ IsResolved ? SourceHandle.FromUuid(Uuid) : SourceHandle.FromName(Name!);
+
+ ///
+ public override string ToString() => IsResolved ? $"input {Uuid}" : $"input '{Name}'";
+}
+
+///
+/// A source, which in OBS means either a scene or an input. The requests that take a source accept
+/// both, and validate the kind themselves.
+///
+public sealed record SourceHandle : IObsHandle
+{
+ private SourceHandle(string? name, string? uuid, CanvasHandle canvas)
+ {
+ Name = name;
+ Uuid = uuid;
+ Canvas = canvas;
+ }
+
+ ///
+ public string? Name { get; }
+
+ ///
+ public string? Uuid { get; }
+
+ /// The canvas a name is looked up in.
+ public CanvasHandle Canvas { get; }
+
+ ///
+ [MemberNotNullWhen(true, nameof(Uuid))]
+ public bool IsResolved => Uuid is not null;
+
+ /// Addresses a source by name.
+ public static SourceHandle FromName(string name, CanvasHandle? canvas = null)
+ {
+ ArgumentException.ThrowIfNullOrEmpty(name);
+ return new SourceHandle(name, null, canvas ?? CanvasHandle.Main);
+ }
+
+ /// Addresses a source by uuid, with no lookup.
+ public static SourceHandle FromUuid(Guid uuid) => FromUuid(uuid.ToString("D"));
+
+ /// Addresses a source by uuid as OBS wrote it.
+ public static SourceHandle FromUuid(string uuid)
+ {
+ ArgumentException.ThrowIfNullOrEmpty(uuid);
+ return new SourceHandle(null, uuid, CanvasHandle.Main);
+ }
+
+ /// Addresses a source by name.
+ public static implicit operator SourceHandle(string name) => FromName(name);
+
+ /// Addresses a source by uuid.
+ public static implicit operator SourceHandle(Guid uuid) => FromUuid(uuid);
+
+ /// A filter on this source, by name.
+ public FilterHandle Filter(string filterName) => FilterHandle.For(this, filterName);
+
+ ///
+ public override string ToString() => IsResolved ? $"source {Uuid}" : $"source '{Name}'";
+}
+
+///
+/// A scene item: a scene, and the numeric id OBS gave one source inside it.
+///
+///
+/// The id is only meaningful within its scene, and only while the item exists. Removing the item
+/// and adding it back gives a new one.
+///
+public sealed record SceneItemHandle
+{
+ private SceneItemHandle(SceneHandle scene, long sceneItemId)
+ {
+ Scene = scene;
+ SceneItemId = sceneItemId;
+ }
+
+ /// The scene the item lives in.
+ public SceneHandle Scene { get; }
+
+ /// The numeric id OBS assigned the item.
+ public long SceneItemId { get; }
+
+ /// Builds a handle for an id already known.
+ public static SceneItemHandle For(SceneHandle scene, long sceneItemId)
+ {
+ ArgumentNullException.ThrowIfNull(scene);
+ ArgumentOutOfRangeException.ThrowIfNegative(sceneItemId);
+ return new SceneItemHandle(scene, sceneItemId);
+ }
+
+ ///
+ public override string ToString() => $"item {SceneItemId} in {Scene}";
+}
+
+///
+/// A scene item named by its source rather than its id, which OBS cannot act on until the id is
+/// looked up.
+///
+///
+/// A separate type rather than a nullable id, so a scene item that has not been resolved cannot be
+/// passed to a request that needs one.
+///
+public sealed record UnresolvedSceneItem(SceneHandle Scene, string SourceName);
+
+///
+/// A filter on a source, addressed by name.
+///
+///
+/// Filters have no uuid in the protocol, so the name is the identity and there is nothing to
+/// resolve. A rename moves the filter out from under the handle.
+///
+public sealed record FilterHandle
+{
+ private FilterHandle(SourceHandle source, string filterName)
+ {
+ Source = source;
+ FilterName = filterName;
+ }
+
+ /// The source the filter is on.
+ public SourceHandle Source { get; }
+
+ /// The filter's name, which is its identity.
+ public string FilterName { get; }
+
+ /// Builds a handle for a filter on a source.
+ public static FilterHandle For(SourceHandle source, string filterName)
+ {
+ ArgumentNullException.ThrowIfNull(source);
+ ArgumentException.ThrowIfNullOrEmpty(filterName);
+ return new FilterHandle(source, filterName);
+ }
+
+ ///
+ public override string ToString() => $"filter '{FilterName}' on {Source}";
+}
diff --git a/ObsWebSocket.Core/Handles/ObsOperationsNavigation.cs b/ObsWebSocket.Core/Handles/ObsOperationsNavigation.cs
new file mode 100644
index 0000000..397b577
--- /dev/null
+++ b/ObsWebSocket.Core/Handles/ObsOperationsNavigation.cs
@@ -0,0 +1,119 @@
+namespace ObsWebSocket.Core;
+
+// Getting from one addressed thing to another without going back through the client.
+//
+// The operations types are generated from the protocol, which knows nothing about a scene
+// containing items or an input carrying filters. That relationship is real and worth navigating,
+// so it is written here rather than inferred.
+
+public readonly partial struct SceneOperations
+{
+ ///
+ /// Looks up this scene's uuid, so later requests survive a rename.
+ ///
+ /// A token to cancel the lookup.
+ ///
+ /// Thrown when no scene has that name, listing the scenes that do.
+ ///
+ public async ValueTask ResolveAsync(
+ CancellationToken cancellationToken = default
+ ) =>
+ new(
+ client,
+ await client.Scenes.ResolveAsync(handle, cancellationToken).ConfigureAwait(false)
+ );
+
+ /// A scene item in this scene, by the numeric id OBS assigned it.
+ /// The id, which GetSceneItemList or an event reports.
+ public SceneItemOperations Item(long sceneItemId) => new(client, handle.Item(sceneItemId));
+
+ ///
+ /// A scene item in this scene, by the name of the source it shows.
+ ///
+ ///
+ /// Unlike the other lookups this one is not a convenience: OBS addresses scene items by a
+ /// number that only GetSceneItemId reports.
+ ///
+ /// The name of the source the item shows.
+ ///
+ /// Which match to take when the source appears more than once. 0 is the first from the bottom;
+ /// -1 is the topmost.
+ ///
+ /// A token to cancel the lookup.
+ ///
+ /// Thrown when the scene holds no such source, listing the sources it does hold.
+ ///
+ public async ValueTask ItemAsync(
+ string sourceName,
+ int searchOffset = 0,
+ CancellationToken cancellationToken = default
+ ) =>
+ new(
+ client,
+ await client
+ .SceneItems.ResolveAsync(handle.Item(sourceName), searchOffset, cancellationToken)
+ .ConfigureAwait(false)
+ );
+
+ /// This scene addressed as a source, for the requests that take any source.
+ public SourceOperations AsSource() => new(client, handle.AsSource());
+}
+
+public readonly partial struct InputOperations
+{
+ ///
+ /// Looks up this input's uuid, so later requests survive a rename.
+ ///
+ /// A token to cancel the lookup.
+ ///
+ /// Thrown when no input has that name, listing the inputs that do.
+ ///
+ public async ValueTask ResolveAsync(
+ CancellationToken cancellationToken = default
+ ) =>
+ new(
+ client,
+ await client.Inputs.ResolveAsync(handle, cancellationToken).ConfigureAwait(false)
+ );
+
+ /// A filter on this input, by name, which is a filter's whole identity.
+ /// The filter's name.
+ public FilterOperations Filter(string filterName) => new(client, handle.Filter(filterName));
+
+ /// This input addressed as a source, for the requests that take any source.
+ public SourceOperations AsSource() => new(client, handle.AsSource());
+}
+
+public readonly partial struct SourceOperations
+{
+ ///
+ /// Looks up this source's uuid. A source is a scene or an input, so this may take two lookups.
+ ///
+ /// A token to cancel the lookup.
+ ///
+ /// Thrown when neither the inputs nor the scenes have that name.
+ ///
+ public async ValueTask ResolveAsync(
+ CancellationToken cancellationToken = default
+ ) =>
+ new(
+ client,
+ await client.Sources.ResolveAsync(handle, cancellationToken).ConfigureAwait(false)
+ );
+
+ /// A filter on this source, by name.
+ /// The filter's name.
+ public FilterOperations Filter(string filterName) => new(client, handle.Filter(filterName));
+}
+
+public readonly partial struct SceneItemOperations
+{
+ /// The scene this item lives in.
+ public SceneOperations Scene => new(client, handle.Scene);
+}
+
+public readonly partial struct FilterOperations
+{
+ /// The source this filter is on.
+ public SourceOperations Source => new(client, handle.Source);
+}
diff --git a/ObsWebSocket.Core/Handles/ObsWebSocketResourceNotFoundException.cs b/ObsWebSocket.Core/Handles/ObsWebSocketResourceNotFoundException.cs
new file mode 100644
index 0000000..b8f6a9b
--- /dev/null
+++ b/ObsWebSocket.Core/Handles/ObsWebSocketResourceNotFoundException.cs
@@ -0,0 +1,83 @@
+namespace ObsWebSocket.Core;
+
+///
+/// Thrown when a name does not match anything in OBS.
+///
+///
+/// Resolving a name means fetching the list it would have been in, so the list is already in hand
+/// when the lookup misses. Saying what does exist costs nothing and turns a typo from a puzzle
+/// into an answer, which is more than OBS itself can offer: its own reply is
+/// ResourceNotFound and the name you already knew.
+///
+public sealed class ObsWebSocketResourceNotFoundException : ObsWebSocketException
+{
+ /// Initializes a new instance.
+ public ObsWebSocketResourceNotFoundException() { }
+
+ /// Initializes a new instance with a message.
+ /// The message.
+ public ObsWebSocketResourceNotFoundException(string message)
+ : base(message) { }
+
+ /// Initializes a new instance with a message and an inner exception.
+ /// The message.
+ /// The underlying failure.
+ public ObsWebSocketResourceNotFoundException(string message, Exception innerException)
+ : base(message, innerException) { }
+
+ private ObsWebSocketResourceNotFoundException(
+ string message,
+ string kind,
+ string requestedName,
+ IReadOnlyList available,
+ Exception? innerException
+ )
+ : base(message, innerException!)
+ {
+ Kind = kind;
+ RequestedName = requestedName;
+ Available = available;
+ }
+
+ /// What was being looked for, such as scene or input.
+ public string? Kind { get; }
+
+ /// The name that did not match.
+ public string? RequestedName { get; }
+
+ /// The names that did exist when the lookup ran.
+ public IReadOnlyList Available { get; } = [];
+
+ /// Builds the exception, including the available names in the message.
+ /// What was being looked for.
+ /// The name that did not match.
+ /// The names that did exist.
+ /// The canvas searched, when the search was canvas scoped.
+ /// The underlying failure, when there was one.
+ internal static ObsWebSocketResourceNotFoundException For(
+ string kind,
+ string requestedName,
+ IReadOnlyList available,
+ CanvasHandle? canvas,
+ Exception? innerException = null
+ )
+ {
+ string scope =
+ canvas is null || ReferenceEquals(canvas, CanvasHandle.Main)
+ ? string.Empty
+ : $" on {canvas}";
+
+ string names =
+ available.Count == 0
+ ? "There are none."
+ : $"Available: {string.Join(", ", available.Select(n => $"'{n}'"))}.";
+
+ return new ObsWebSocketResourceNotFoundException(
+ $"No {kind} named '{requestedName}'{scope}. {names}",
+ kind,
+ requestedName,
+ available,
+ innerException
+ );
+ }
+}
diff --git a/ObsWebSocket.Core/Protocol/Common/FilterSettings/CommonFilterSettings.cs b/ObsWebSocket.Core/Protocol/Common/FilterSettings/CommonFilterSettings.cs
index 9fee89e..45b0328 100644
--- a/ObsWebSocket.Core/Protocol/Common/FilterSettings/CommonFilterSettings.cs
+++ b/ObsWebSocket.Core/Protocol/Common/FilterSettings/CommonFilterSettings.cs
@@ -3,7 +3,7 @@
namespace ObsWebSocket.Core.Protocol.Common.FilterSettings;
/// Settings for the 'mask_filter' or 'mask_filter_v2' filter (Image Mask/Blend).
-/// v1 uses opacity 0–100; v2 uses 0.0–1.0.
+/// v1 uses opacity 0 to 100; v2 uses 0.0 to 1.0.
public sealed record ImageMaskBlendFilterSettings(
[property: JsonPropertyName("type")] string? Type = null,
[property: JsonPropertyName("color")] long? Color = null,
@@ -46,8 +46,8 @@ public sealed record HdrTonemapFilterSettings(
///
/// Settings for the 'color_filter' (Color Correction v1) or 'color_filter_v2' filter.
-/// v1 includes and uses opacity on a 0–100 scale.
-/// v2 replaces with / and uses opacity 0.0–1.0.
+/// v1 includes and uses opacity on a 0 to 100 scale.
+/// v2 replaces with / and uses opacity 0.0 to 1.0.
/// Null properties are skipped on overlay writes, so this type works for both versions.
///
public sealed record ColorCorrectionFilterSettings(
@@ -86,7 +86,7 @@ public sealed record GpuDelayFilterSettings;
///
/// Settings for the 'color_key_filter' or 'color_key_filter_v2' filter.
-/// v1 uses opacity 0–100; v2 uses 0.0–1.0.
+/// v1 uses opacity 0 to 100; v2 uses 0.0 to 1.0.
///
public sealed record ColorKeyFilterSettings(
[property: JsonPropertyName("key_color_type")] string? KeyColorType = null,
@@ -113,7 +113,7 @@ public sealed record SharpnessFilterSettings(
///
/// Settings for the 'chroma_key_filter' or 'chroma_key_filter_v2' filter.
-/// v1 uses opacity 0–100; v2 uses 0.0–1.0.
+/// v1 uses opacity 0 to 100; v2 uses 0.0 to 1.0.
///
public sealed record ChromaKeyFilterSettings(
[property: JsonPropertyName("key_color_type")] string? KeyColorType = null,
@@ -188,10 +188,10 @@ public static class DetectorValues
/// Known values for the setting.
public static class PresetsValues
{
- /// Expander — increases dynamic range below the threshold.
+ /// Expander. Increases dynamic range below the threshold.
public const string Expander = "expander";
- /// Gate — silences audio below the threshold.
+ /// Gate. Silences audio below the threshold.
public const string Gate = "gate";
}
}
diff --git a/ObsWebSocket.Core/Protocol/Common/InputSettings/CommonInputSettings.cs b/ObsWebSocket.Core/Protocol/Common/InputSettings/CommonInputSettings.cs
index 1f590cb..fe524ce 100644
--- a/ObsWebSocket.Core/Protocol/Common/InputSettings/CommonInputSettings.cs
+++ b/ObsWebSocket.Core/Protocol/Common/InputSettings/CommonInputSettings.cs
@@ -269,7 +269,7 @@ public sealed record WasapiOutputCaptureSettings(
);
/// Settings for the 'dshow_input' (Video Capture Device / DirectShow) input.
-/// Device-identifying properties (device, device_id, video_device_id, audio_device_id) are not included — use a consumer-defined type to target a specific device.
+/// Device-identifying properties (device, device_id, video_device_id, audio_device_id) are not included. Use a consumer-defined type to target a specific device.
public sealed record DShowInputSettings(
[property: JsonPropertyName("active")] bool? Active = null,
[property: JsonPropertyName("hw_decode")] bool? HwDecode = null,
diff --git a/ObsWebSocket.Core/Protocol/Common/StubTypes.cs b/ObsWebSocket.Core/Protocol/Common/StubTypes.cs
index a46b78c..dd441b2 100644
--- a/ObsWebSocket.Core/Protocol/Common/StubTypes.cs
+++ b/ObsWebSocket.Core/Protocol/Common/StubTypes.cs
@@ -22,9 +22,15 @@ public sealed class SceneStub
public required string SceneUuid { get; init; }
/// Scene index position.
+ ///
+ /// Nullable because OBS sends null, not because the protocol says so. The main scene list
+ /// numbers its entries, but GetCanvasSceneList enumerates through a callback that has no index
+ /// to report and writes null in its place. As a non-nullable int that failed the whole
+ /// response rather than the one field.
+ ///
[JsonPropertyName("sceneIndex")]
[Key("sceneIndex")]
- public required int SceneIndex { get; init; }
+ public int? SceneIndex { get; init; }
/// Captures any extra fields not explicitly defined in the stub.
[IgnoreMember]
@@ -50,7 +56,7 @@ public sealed class SceneItemOrderStub
/// Numeric ID of the scene item.
[JsonPropertyName("sceneItemId")]
[Key("sceneItemId")]
- public required int SceneItemId { get; init; }
+ public required long SceneItemId { get; init; }
/// Index of the scene item, counted from the bottom of the list.
[JsonPropertyName("sceneItemIndex")]
@@ -306,11 +312,17 @@ public sealed class SceneItemTransformStub
public required double SourceHeight { get; init; }
///
- /// Alignment value.
+ /// Alignment of the scene item, as an OBS_ALIGN_* bit mask.
///
+ ///
+ /// A mask, not a count, and OBS treats it as the full width of one: the field is
+ /// uint32_t in obs_transform_info, and obs-websocket validates writes to
+ /// 0 .. uint32_t max rather than to the flags it defines. A value past
+ /// is accepted on the way in and handed back on the way out.
+ ///
[JsonPropertyName("alignment")]
[Key("alignment")]
- public required int Alignment { get; init; }
+ public required long Alignment { get; init; }
///
/// Bounds type value.
@@ -320,11 +332,12 @@ public sealed class SceneItemTransformStub
public required string BoundsType { get; init; }
///
- /// Bounds alignment value.
+ /// Alignment of the bounding box, as an OBS_ALIGN_* bit mask. See
+ /// for why this is 64 bits wide.
///
[JsonPropertyName("boundsAlignment")]
[Key("boundsAlignment")]
- public required int BoundsAlignment { get; init; }
+ public required long BoundsAlignment { get; init; }
///
/// Bounds width value.
@@ -368,6 +381,11 @@ public sealed class SceneItemTransformStub
[Key("cropBottom")]
public required int CropBottom { get; init; }
+ /// Whether the crop is applied relative to the bounding box.
+ [JsonPropertyName("cropToBounds")]
+ [Key("cropToBounds")]
+ public bool? CropToBounds { get; init; }
+
/// Captures any extra fields not explicitly defined in the stub.
[IgnoreMember]
[JsonExtensionData]
@@ -454,11 +472,11 @@ public sealed class SceneItemTransformPatchStub
public double? SourceHeight { get; init; }
///
- /// Alignment value.
+ /// Alignment of the scene item, as an OBS_ALIGN_* bit mask.
///
[JsonPropertyName("alignment")]
[Key("alignment")]
- public int? Alignment { get; init; }
+ public long? Alignment { get; init; }
///
/// Bounds type value.
@@ -468,11 +486,11 @@ public sealed class SceneItemTransformPatchStub
public string? BoundsType { get; init; }
///
- /// Bounds alignment value.
+ /// Alignment of the bounding box, as an OBS_ALIGN_* bit mask.
///
[JsonPropertyName("boundsAlignment")]
[Key("boundsAlignment")]
- public int? BoundsAlignment { get; init; }
+ public long? BoundsAlignment { get; init; }
///
/// Bounds width value.
@@ -516,6 +534,11 @@ public sealed class SceneItemTransformPatchStub
[Key("cropBottom")]
public int? CropBottom { get; init; }
+ /// Whether the crop is applied relative to the bounding box.
+ [JsonPropertyName("cropToBounds")]
+ [Key("cropToBounds")]
+ public bool? CropToBounds { get; init; }
+
/// Captures any extra fields not explicitly defined in the stub.
[IgnoreMember]
[JsonExtensionData]
@@ -536,7 +559,7 @@ public sealed class SceneItemStub
/// Scene item ID.
[JsonPropertyName("sceneItemId")]
[Key("sceneItemId")]
- public required int SceneItemId { get; init; }
+ public required long SceneItemId { get; init; }
/// Scene item index position.
[JsonPropertyName("sceneItemIndex")]
@@ -563,11 +586,48 @@ public sealed class SceneItemStub
[Key("sceneItemLocked")]
public required bool SceneItemLocked { get; init; }
- /// Whether the source is a group.
+ ///
+ /// Kind of the input the item shows, or when the item is a scene or a
+ /// group rather than an input.
+ ///
+ [JsonPropertyName("inputKind")]
+ [Key("inputKind")]
+ public string? InputKind { get; init; }
+
+ ///
+ /// Type of the source the item shows, as an OBS_SOURCE_TYPE_* value.
+ ///
+ [JsonPropertyName("sourceType")]
+ [Key("sourceType")]
+ public string? SourceType { get; init; }
+
+ ///
+ /// Whether the source is a group, or when the item shows an input.
+ ///
+ ///
+ /// OBS answers this only for items that could be a group. An input gets null rather than
+ /// false, so a null here means "not applicable", not "unknown".
+ ///
[JsonPropertyName("isGroup")]
[Key("isGroup")]
public bool? IsGroup { get; init; }
+ /// Blend mode of the scene item, as an OBS_BLEND_* value.
+ [JsonPropertyName("sceneItemBlendMode")]
+ [Key("sceneItemBlendMode")]
+ public string? SceneItemBlendMode { get; init; }
+
+ ///
+ /// Blend method of the scene item, as an OBS_BLEND_METHOD_* value.
+ ///
+ ///
+ /// Nullable because it is newer than the rest: OBS 32.2.2 does not send it, so requiring it
+ /// would make the scene item list unreadable on every build that predates the field.
+ ///
+ [JsonPropertyName("sceneItemBlendMethod")]
+ [Key("sceneItemBlendMethod")]
+ public string? SceneItemBlendMethod { get; init; }
+
/// Transform data for the scene item.
[JsonPropertyName("sceneItemTransform")]
[Key("sceneItemTransform")]
@@ -652,6 +712,15 @@ public sealed class InputStub
[Key("unversionedInputKind")]
public required string UnversionedInputKind { get; init; }
+ /// Capability flags for the input's kind, as an OBS_SOURCE_* bit mask.
+ ///
+ /// 64 bits wide because the wire value is: OBS fills it from
+ /// obs_source_get_output_flags, which returns uint32_t and is not clamped.
+ ///
+ [JsonPropertyName("inputKindCaps")]
+ [Key("inputKindCaps")]
+ public long? InputKindCaps { get; init; }
+
/// Captures any extra fields not explicitly defined in the stub.
[IgnoreMember]
[JsonExtensionData]
@@ -726,20 +795,37 @@ public sealed class OutputStub
[Key("outputActive")]
public required bool OutputActive { get; init; }
- /// Output width.
+ ///
+ /// Output width in pixels, which an output that has never started may report as garbage.
+ ///
+ ///
+ /// Wider than a pixel count needs to be, because the wire value is wider. OBS fills this from
+ /// obs_output_get_width, which returns uint32_t and is passed through unclamped,
+ /// and an idle output can report a value above . A live OBS 32.2.2
+ /// sent 2586032160 for an inactive virtual camera. As an that made the whole
+ /// GetOutputList response unreadable, intermittently, and only for whoever happened to
+ /// have such an output installed. Treat a value over 4096 as "not meaningful", not as a size.
+ ///
[JsonPropertyName("outputWidth")]
[Key("outputWidth")]
- public required int OutputWidth { get; init; }
+ public required long OutputWidth { get; init; }
- /// Output height.
+ ///
+ /// Output height in pixels, which an output that has never started may report as garbage. See
+ /// for why this is 64 bits wide.
+ ///
[JsonPropertyName("outputHeight")]
[Key("outputHeight")]
- public required int OutputHeight { get; init; }
-
- /// Output settings.
- [JsonPropertyName("outputSettings")]
- [Key("outputSettings")]
- public JsonElement? OutputSettings { get; init; }
+ public required long OutputHeight { get; init; }
+
+ /// Capability flags for the output, keyed by OBS_OUTPUT_* name.
+ ///
+ /// Replaces an outputSettings member that was here for no reason: nothing in
+ /// GetOutputList writes one. Settings come from GetOutputSettings, per output.
+ ///
+ [JsonPropertyName("outputFlags")]
+ [Key("outputFlags")]
+ public Dictionary? OutputFlags { get; init; }
/// Captures any extra fields not explicitly defined in the stub.
[IgnoreMember]
diff --git a/ObsWebSocket.Example/Worker.cs b/ObsWebSocket.Example/Worker.cs
index 18fce6c..19afeac 100644
--- a/ObsWebSocket.Example/Worker.cs
+++ b/ObsWebSocket.Example/Worker.cs
@@ -1,4 +1,5 @@
using System.Buffers;
+using System.Globalization;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Text.Json.Serialization.Metadata;
@@ -307,6 +308,20 @@ await _obsClient.Scenes.GetCurrentProgramSceneAsync(
);
return false;
+ case "resolve":
+ if (args.Length == 0)
+ {
+ UiWarn("Usage: resolve [scene name] [source name in that scene]");
+ return false;
+ }
+
+ await ResolveDemoAsync(
+ args[0],
+ args.Length > 1 ? string.Join(" ", args[1..]) : null,
+ cancellationToken
+ );
+ return false;
+
case "mute":
case "unmute":
if (args.Length == 0)
@@ -317,11 +332,12 @@ 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
- );
+
+ // Handle form. One call about one input, so the identity is said once and the
+ // method name drops the word the handle already carries.
+ ToggleInputMuteResponseData? muteState = await _obsClient
+ .Input(inputNameToMute)
+ .ToggleMuteAsync(cancellationToken);
if (muteState is null)
{
UiWarn($"Could not toggle mute state for {inputNameToMute}. Does it exist?");
@@ -346,7 +362,7 @@ await _obsClient.Inputs.ToggleInputMuteAsync(
try
{
// First, find the scene item ID within the specified scene
- int sceneItemId = await GetSceneItemIdAsync(
+ long sceneItemId = await GetSceneItemIdAsync(
sceneForGetSettings,
inputForGetSettings,
cancellationToken
@@ -393,7 +409,7 @@ await _obsClient.Inputs.GetInputSettingsAsync(
try
{
// Find the scene item ID first (optional but good practice)
- int sceneItemId = await GetSceneItemIdAsync(
+ long sceneItemId = await GetSceneItemIdAsync(
sceneForSetText,
inputForSetText,
cancellationToken
@@ -405,7 +421,9 @@ await _obsClient.Inputs.GetInputSettingsAsync(
sceneForSetText
);
- // Uses SetInputTextAsync helper which serializes TextGdiPlusInputSettings internally.
+ // Protocol level on purpose. SetInputTextAsync is a hand-written group helper
+ // that serializes TextGdiPlusInputSettings internally; the handles are
+ // generated from the protocol, so nothing hand-written appears on them.
await _obsClient.Inputs.SetInputTextAsync(
inputForSetText,
newText,
@@ -438,11 +456,12 @@ await _obsClient.Inputs.SetInputTextAsync(
}
string sourceForFilters = string.Join(" ", args);
- GetSourceFilterListResponseData? filterList =
- await _obsClient.Filters.GetSourceFilterListAsync(
- new GetSourceFilterListRequestData(sourceName: sourceForFilters),
- cancellationToken: cancellationToken
- );
+
+ // A filter list belongs to the source, not to the Filters category, and the handle
+ // says so: client.Source(x).GetFilterListAsync, not Filters.GetSourceFilterList.
+ GetSourceFilterListResponseData? filterList = await _obsClient
+ .Source(sourceForFilters)
+ .GetFilterListAsync(cancellationToken);
if (filterList?.Filters is not null && filterList.Filters.Count > 0)
{
Table table = new()
@@ -456,7 +475,7 @@ await _obsClient.Filters.GetSourceFilterListAsync(
foreach (Core.Protocol.Common.FilterStub filterElement in filterList.Filters)
{
string filterIndex = filterElement.FilterIndex.ToString(
- System.Globalization.CultureInfo.InvariantCulture
+ CultureInfo.InvariantCulture
);
string filterName =
Markup.Escape(filterElement.FilterName ?? "N/A") ?? "N/A";
@@ -489,16 +508,14 @@ await _obsClient.Filters.GetSourceFilterListAsync(
string sourceForToggle = args[0];
string filterToToggle = string.Join(" ", args[1..]);
- // 1. Get current filter state
- GetSourceFilterResponseData? currentFilterState =
- await _obsClient.Filters.GetSourceFilterAsync(
- new GetSourceFilterRequestData
- {
- SourceName = sourceForToggle,
- FilterName = filterToToggle,
- },
- cancellationToken: cancellationToken
- );
+ // Read then write, both about the same filter. Through the category group that is
+ // four strings across two request records, any one of which can be misspelled into
+ // a ResourceNotFound. Held as a handle it is two strings, once.
+ FilterOperations filter = _obsClient.Source(sourceForToggle).Filter(filterToToggle);
+
+ GetSourceFilterResponseData? currentFilterState = await filter.GetAsync(
+ cancellationToken
+ );
if (currentFilterState is null)
{
@@ -508,17 +525,8 @@ await _obsClient.Filters.GetSourceFilterAsync(
return false;
}
- // 2. Toggle the state
bool newState = !currentFilterState.FilterEnabled;
- await _obsClient.Filters.SetSourceFilterEnabledAsync(
- new SetSourceFilterEnabledRequestData
- {
- SourceName = sourceForToggle,
- FilterName = filterToToggle,
- FilterEnabled = newState,
- },
- cancellationToken: cancellationToken
- );
+ await filter.SetEnabledAsync(newState, cancellationToken);
UiSuccess(
$"Filter '{filterToToggle}' on '{sourceForToggle}' toggled to {(newState ? "ENABLED" : "DISABLED")}"
@@ -530,6 +538,10 @@ 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.
+ //
+ // The event already says which scene, by uuid, so acting on it needs no lookup.
+ // Reading SceneName back off it and addressing the scene by name would add a round
+ // trip and reintroduce the rename race the uuid exists to close.
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.");
@@ -546,7 +558,14 @@ CurrentProgramSceneChangedEventArgs sceneEvent in _obsClient.Scenes.CurrentProgr
)
)
{
- UiSuccess($"Program scene is now '{sceneEvent.EventData.SceneName}'");
+ GetSceneItemListResponseData items = await _obsClient
+ .Scene(sceneEvent.EventData.Scene)
+ .GetItemListAsync(watchCts.Token);
+
+ UiSuccess(
+ $"Program scene is now '{sceneEvent.EventData.SceneName}' "
+ + $"({items.SceneItems.Count} item(s))"
+ );
}
}
catch (OperationCanceledException)
@@ -1115,8 +1134,8 @@ await SweepEveryWriteRequestAsync(cycleClient, cancellationToken)
_ = summary.AddRow(
Markup.Escape(label),
pass
- ? $"[green]Pass[/] — {Markup.Escape(detail)}"
- : $"[red]Fail[/] — {Markup.Escape(detail)}"
+ ? $"[green]Pass[/]: {Markup.Escape(detail)}"
+ : $"[red]Fail[/]: {Markup.Escape(detail)}"
);
}
foreach ((string label, bool pass, string detail) in modernResults)
@@ -1143,9 +1162,13 @@ await cycleClient
///
/// Validates all three settings API modes for both InputSettings and FilterSettings.
- /// 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.
///
+ ///
+ /// The browser source and the gain filter are created here and removed again, so a fresh OBS
+ /// install exercises the same checks as a populated one. Discovering an existing input instead
+ /// made the result depend on the machine: the run reported the modes as failing when all that
+ /// was missing was a source to try them on.
+ ///
private static async Task<
List<(string Label, bool Pass, string Detail)>
> ValidateSettingsModesAsync(
@@ -1162,16 +1185,110 @@ CancellationToken cancellationToken
}
// ── InputSettings ─────────────────────────────────────────────────────
- string? browserInputName = inputs
- .Inputs?.FirstOrDefault(i =>
- string.Equals(i.InputKind, "browser_source", StringComparison.OrdinalIgnoreCase)
- )
- ?.InputName;
+ const string FixtureInputName = "__obsws_settings_browser";
+ const string FixtureFilter = "__obsws_settings_gain";
+
+ string? browserInputName = null;
+ string? filterSourceName = null;
+ string? gainFilterName = null;
+
+ try
+ {
+ GetCurrentProgramSceneResponseData programScene = await client
+ .Scenes.GetCurrentProgramSceneAsync(cancellationToken)
+ .ConfigureAwait(false);
+
+ await client
+ .Inputs.CreateInputAsync(
+ inputKind: "browser_source",
+ inputName: FixtureInputName,
+ settings: new BrowserSourceSettings(
+ Url: "https://obsproject.com",
+ Width: 800,
+ Height: 600
+ ),
+ sceneName: programScene.SceneName,
+ sceneItemEnabled: true,
+ cancellationToken: cancellationToken
+ )
+ .ConfigureAwait(false);
+ browserInputName = FixtureInputName;
+
+ await client
+ .Filters.CreateSourceFilterAsync(
+ new CreateSourceFilterRequestData(
+ filterKind: "gain_filter",
+ filterName: FixtureFilter,
+ sourceName: FixtureInputName
+ ),
+ cancellationToken
+ )
+ .ConfigureAwait(false);
+ filterSourceName = FixtureInputName;
+ gainFilterName = FixtureFilter;
+ }
+ catch (ObsWebSocketException ex)
+ {
+ results.Add(("Settings fixtures", false, $"could not be created: {ex.Message}"));
+ }
+
+ try
+ {
+ results.AddRange(
+ await RunSettingsModeChecksAsync(
+ client,
+ browserInputName,
+ filterSourceName,
+ gainFilterName,
+ cancellationToken
+ )
+ .ConfigureAwait(false)
+ );
+ }
+ finally
+ {
+ // Removing the input takes its filter and its scene item with it.
+ if (browserInputName is not null)
+ {
+ try
+ {
+ await client
+ .Inputs.RemoveInputAsync(
+ new(inputName: browserInputName),
+ CancellationToken.None
+ )
+ .ConfigureAwait(false);
+ }
+ catch (ObsWebSocketException)
+ {
+ // Nothing useful to do about a fixture that will not go away. The next run
+ // recreates it by the same name and OBS rejects the duplicate visibly.
+ }
+ }
+ }
+
+ return results;
+ }
+
+ ///
+ /// The settings-mode checks themselves, against fixtures the caller creates and removes.
+ ///
+ private static async Task<
+ List<(string Label, bool Pass, string Detail)>
+ > RunSettingsModeChecksAsync(
+ ObsWebSocketClient client,
+ string? browserInputName,
+ string? filterSourceName,
+ string? gainFilterName,
+ CancellationToken cancellationToken
+ )
+ {
+ List<(string Label, bool Pass, string Detail)> results = [];
if (string.IsNullOrEmpty(browserInputName))
{
results.Add(
- ("InputSettings [all modes]", false, "No browser_source in OBS — add one to test")
+ ("InputSettings [all modes]", false, "fixture browser source was not created")
);
}
else
@@ -1267,45 +1384,10 @@ await client.Inputs.SetInputSettingsAsync(
}
// ── 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)
- ?? []
- )
- {
- try
- {
- GetSourceFilterListResponseData? fl = await client.Filters.GetSourceFilterListAsync(
- new GetSourceFilterListRequestData(sourceName: input.InputName!),
- cancellationToken
- );
- Core.Protocol.Common.FilterStub? gain = fl?.Filters?.FirstOrDefault(f =>
- string.Equals(f.FilterKind, "gain_filter", StringComparison.OrdinalIgnoreCase)
- );
- if (gain?.FilterName is not null)
- {
- filterSourceName = input.InputName;
- gainFilterName = gain.FilterName;
- break;
- }
- }
- 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"
- )
+ ("FilterSettings [all modes]", false, "fixture gain filter was not created")
);
}
else
@@ -2150,14 +2232,14 @@ await TrySettingsCheckAsync(
"FindSceneItemIdAsync",
async () =>
{
- int? id = await client
+ long? id = await client
.SceneItems.FindSceneItemIdAsync(
sceneName,
inputName,
cancellationToken
)
.ConfigureAwait(false);
- int? miss = await client
+ long? miss = await client
.SceneItems.FindSceneItemIdAsync(
sceneName,
"__absent__",
@@ -2364,7 +2446,7 @@ await TrySettingsCheckAsync(
// 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 =
+ long itemId =
await client
.SceneItems.FindSceneItemIdAsync(
sceneName,
@@ -2859,7 +2941,7 @@ await TrySettingsCheckAsync(
return (false, "no scene items to reindex");
}
- int id = items.SceneItems[0].SceneItemId;
+ long id = items.SceneItems[0].SceneItemId;
int index = items.SceneItems[0].SceneItemIndex;
Task reindexed =
@@ -3013,6 +3095,147 @@ await client
.ConfigureAwait(false)
);
+ results.Add(
+ await TrySettingsCheckAsync(
+ "Handles address by name and by uuid",
+ async () =>
+ {
+ // The point of a uuid handle is that it survives a rename. Both forms
+ // have to reach the same scene for that to be worth anything.
+ SceneHandle byName = sceneName;
+ SceneOperations resolved = await client
+ .Scene(byName)
+ .ResolveAsync(cancellationToken)
+ .ConfigureAwait(false);
+ SceneHandle byUuid = resolved.Handle;
+
+ GetSceneItemListResponseData viaName = await client
+ .Scene(byName)
+ .GetItemListAsync(cancellationToken)
+ .ConfigureAwait(false);
+ GetSceneItemListResponseData viaUuid = await client
+ .Scene(byUuid)
+ .GetItemListAsync(cancellationToken)
+ .ConfigureAwait(false);
+
+ string renamed = sceneName + "_renamed";
+ await client
+ .Scene(byUuid)
+ .SetNameAsync(renamed, cancellationToken)
+ .ConfigureAwait(false);
+
+ bool uuidStillWorks;
+ try
+ {
+ _ = await client
+ .Scene(byUuid)
+ .GetItemListAsync(cancellationToken)
+ .ConfigureAwait(false);
+ uuidStillWorks = true;
+ }
+ catch (ObsWebSocketRequestException)
+ {
+ uuidStillWorks = false;
+ }
+
+ bool nameNowMisses;
+ try
+ {
+ _ = await client
+ .Scene(byName)
+ .GetItemListAsync(cancellationToken)
+ .ConfigureAwait(false);
+ nameNowMisses = false;
+ }
+ catch (ObsWebSocketRequestException)
+ {
+ nameNowMisses = true;
+ }
+
+ await client
+ .Scene(byUuid)
+ .SetNameAsync(sceneName, CancellationToken.None)
+ .ConfigureAwait(false);
+
+ return (
+ byUuid.IsResolved
+ && viaName.SceneItems.Count == viaUuid.SceneItems.Count
+ && uuidStillWorks
+ && nameNowMisses,
+ $"resolved to {byUuid.Uuid}, both read {viaUuid.SceneItems.Count} "
+ + $"item(s); after a rename the uuid still resolves "
+ + $"({uuidStillWorks}) and the name does not ({nameNowMisses})"
+ );
+ }
+ )
+ .ConfigureAwait(false)
+ );
+
+ results.Add(
+ await TrySettingsCheckAsync(
+ "A miss says what does exist",
+ async () =>
+ {
+ // OBS answers ResourceNotFound and the name you already gave it. The
+ // list is in hand from the lookup, so the client can do better.
+ ObsWebSocketResourceNotFoundException ex =
+ await ExpectThrowAsync(() =>
+ client
+ .Scenes.ResolveAsync(
+ "__obsws_no_such_scene",
+ cancellationToken
+ )
+ .AsTask()
+ )
+ .ConfigureAwait(false);
+
+ return (
+ ex.Available.Count > 0
+ && ex.Message.Contains(
+ "__obsws_no_such_scene",
+ StringComparison.Ordinal
+ )
+ && ex.Available.Contains(sceneName, StringComparer.Ordinal),
+ $"named {ex.Available.Count} scene(s), including the one the run made"
+ );
+ }
+ )
+ .ConfigureAwait(false)
+ );
+
+ results.Add(
+ await TrySettingsCheckAsync(
+ "A scene item resolves by source name",
+ async () =>
+ {
+ // The one lookup that is not a convenience: OBS addresses scene items
+ // by a number nothing else reports.
+ SceneItemOperations item = await client
+ .Scene(sceneName)
+ .ItemAsync(inputName, cancellationToken: cancellationToken)
+ .ConfigureAwait(false);
+
+ GetSceneItemEnabledResponseData enabled = await item.GetEnabledAsync(
+ cancellationToken
+ )
+ .ConfigureAwait(false);
+
+ // Navigating back up reaches the scene the item is in.
+ GetSceneItemListResponseData siblings = await item
+ .Scene.GetItemListAsync(cancellationToken)
+ .ConfigureAwait(false);
+
+ return (
+ item.Handle.SceneItemId >= 0 && siblings.SceneItems.Count > 0,
+ $"'{inputName}' is item {item.Handle.SceneItemId}, enabled "
+ + $"{enabled.SceneItemEnabled}, among {siblings.SceneItems.Count} "
+ + "in its scene"
+ );
+ }
+ )
+ .ConfigureAwait(false)
+ );
+
results.Add(
await TrySettingsCheckAsync(
"Canvases category",
@@ -3708,7 +3931,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
@@ -3726,6 +3949,76 @@ CancellationToken cancellationToken
: response.SceneItemId;
}
+ ///
+ /// Shows what resolving a handle costs, what it buys, and what a miss reports.
+ ///
+ ///
+ /// The rest of the interactive commands address things by name, which is right for a name the
+ /// operator just typed. This one is the counterpart: it turns a name into a uuid once, and
+ /// everything after that survives a rename in OBS.
+ ///
+ private async Task ResolveDemoAsync(
+ string sceneName,
+ string? sourceName,
+ CancellationToken cancellationToken
+ )
+ {
+ // One round trip. The protocol has no "uuid of the scene called X" request, so this is
+ // GetSceneList and a scan.
+ SceneOperations resolved = await _obsClient
+ .Scene(sceneName)
+ .ResolveAsync(cancellationToken);
+
+ RenderKeyValueTable(
+ "Resolved scene",
+ [
+ ("Given", sceneName),
+ ("UUID", resolved.Handle.Uuid ?? "N/A"),
+ ("Survives a rename", resolved.Handle.IsResolved ? "yes" : "no"),
+ ]
+ );
+
+ // Held by uuid, so this reads the same scene even if it is renamed between the two calls.
+ GetSceneItemListResponseData items = await resolved.GetItemListAsync(cancellationToken);
+ UiInfo($"'{sceneName}' holds {items.SceneItems.Count} scene item(s).");
+
+ if (sourceName is not null)
+ {
+ // The one lookup that is not a convenience: OBS addresses scene items by a number that
+ // only GetSceneItemId reports, so an item known by source name cannot be acted on until
+ // it has been resolved. The type system says so: ItemAsync returns the actable type.
+ SceneItemOperations item = await resolved.ItemAsync(
+ sourceName,
+ cancellationToken: cancellationToken
+ );
+ GetSceneItemEnabledResponseData enabled = await item.GetEnabledAsync(cancellationToken);
+
+ RenderKeyValueTable(
+ "Resolved scene item",
+ [
+ ("Source", sourceName),
+ ("Item id", item.Handle.SceneItemId.ToString(CultureInfo.InvariantCulture)),
+ ("Enabled", enabled.SceneItemEnabled ? "yes" : "no"),
+ ("Back up to", item.Scene.Handle.Uuid ?? item.Scene.Handle.Name ?? "N/A"),
+ ]
+ );
+ }
+
+ // A miss is worth showing: the lookup already fetched the list, so the client can name what
+ // does exist. OBS itself can only answer ResourceNotFound and the name you gave it.
+ try
+ {
+ _ = await _obsClient.Scenes.ResolveAsync(
+ sceneName + "__no_such_scene",
+ cancellationToken
+ );
+ }
+ catch (ObsWebSocketResourceNotFoundException ex)
+ {
+ UiInfo($"A miss reports what exists: {ex.Message}");
+ }
+ }
+
private async Task GetAllSettingsTypesAsync(CancellationToken cancellationToken)
{
_logger.LogInformation("Fetching all settings type schemas from OBS...");
@@ -4042,7 +4335,7 @@ .. sceneNames.Where(n => n != currentProgramScene).OrderBy(n => n),
.ToList()
?? [];
- // Step 4: Prompt — create new source or update an existing browser source
+ // Step 4: Prompt to create a new source or update an existing browser source
const string CreateNewChoice = "+ Create new browser source";
List sourceChoices = [CreateNewChoice, .. existingBrowserSourcesInScene];
@@ -4104,13 +4397,19 @@ .. sceneNames.Where(n => n != currentProgramScene).OrderBy(n => n),
RestartWhenActive: true
);
- int sceneItemId;
+ // The two paths below reach the same scene item by different routes: creating one answers
+ // with its id, finding an existing one costs the lookup only OBS can answer. From here on
+ // the rest of the method does not care which, because both produce the same handle.
+ SceneItemOperations item;
// Step 8: Create new input or update existing source settings
if (isNewSource)
{
UiInfo($"Creating browser source '{sourceName}' in scene '{selectedScene}'...");
+ // Protocol level: the typed-settings CreateInput is a hand-written group helper, so it
+ // has no handle form. Its response carries the new scene item's id, which is what the
+ // handle below is built from.
CreateInputResponseData? createResult = await _obsClient.Inputs.CreateInputAsync(
inputKind: "browser_source",
inputName: sourceName,
@@ -4126,14 +4425,14 @@ .. sceneNames.Where(n => n != currentProgramScene).OrderBy(n => n),
return;
}
- sceneItemId = createResult.SceneItemId;
- UiSuccess($"Created '{sourceName}' (scene item ID: {sceneItemId}).");
+ item = _obsClient.Scene(selectedScene).Item(createResult.SceneItemId);
+ UiSuccess($"Created '{sourceName}' (scene item ID: {createResult.SceneItemId}).");
}
else
{
UiInfo($"Updating browser source '{sourceName}' settings...");
- // overlay: false — reset to defaults then apply all new settings cleanly
+ // overlay: false resets to defaults, then applies all new settings cleanly
await _obsClient.Inputs.SetInputSettingsAsync(
inputName: sourceName,
settings: browserSettings,
@@ -4141,19 +4440,14 @@ await _obsClient.Inputs.SetInputSettingsAsync(
cancellationToken: cancellationToken
);
- sceneItemId = await GetSceneItemIdAsync(selectedScene, sourceName, cancellationToken);
- UiSuccess($"Updated '{sourceName}' (scene item ID: {sceneItemId}).");
+ item = await _obsClient
+ .Scene(selectedScene)
+ .ItemAsync(sourceName, cancellationToken: cancellationToken);
+ UiSuccess($"Updated '{sourceName}' (scene item ID: {item.Handle.SceneItemId}).");
}
// Step 9: Set Blend Mode to Normal (explicit, even though it is the default)
- await _obsClient.SceneItems.SetSceneItemBlendModeAsync(
- new SetSceneItemBlendModeRequestData(
- sceneItemId: sceneItemId,
- sceneItemBlendMode: "OBS_BLEND_NORMAL",
- sceneName: selectedScene
- ),
- cancellationToken: cancellationToken
- );
+ await item.SetBlendModeAsync("OBS_BLEND_NORMAL", cancellationToken);
// The obs-websocket v5 protocol does not expose SetSceneItemPrivateSettings,
// so Blending Method (SRGB Off) cannot be set programmatically via this API.
@@ -4165,7 +4459,7 @@ await _obsClient.SceneItems.SetSceneItemBlendModeAsync(
);
RenderKeyValueTable(
- $"Browser Source — {(isNewSource ? "Created" : "Updated")}",
+ $"Browser Source: {(isNewSource ? "Created" : "Updated")}",
[
("Name", sourceName),
("Scene", selectedScene),
@@ -4177,7 +4471,7 @@ await _obsClient.SceneItems.SetSceneItemBlendModeAsync(
("OBS Control Level", "Full (webpage_control_level: 5)"),
("Refresh on Scene Active", "Yes (restart_when_active: true)"),
("Blend Mode", "Normal (OBS_BLEND_NORMAL)"),
- ("Blending Method", "sRGB Off — set manually in OBS (not exposed by WebSocket v5)"),
+ ("Blending Method", "sRGB Off, set manually in OBS (not exposed by WebSocket v5)"),
]
);
}
@@ -4195,9 +4489,13 @@ private static void RenderCommandHelp()
Markup.Escape("Get OBS and WebSocket version info")
);
_ = commandTable.AddRow(Markup.Escape("scene"), Markup.Escape("Get current program scene"));
+ _ = commandTable.AddRow(
+ Markup.Escape("resolve [scene] [source]"),
+ Markup.Escape("Resolve a name to a uuid handle, and show what a miss reports")
+ );
_ = commandTable.AddRow(
Markup.Escape("mute [input name]"),
- Markup.Escape("Toggle mute for audio input")
+ Markup.Escape("Toggle mute for audio input, through an input handle")
);
_ = commandTable.AddRow(
Markup.Escape("unmute [input name]"),
@@ -4213,11 +4511,11 @@ private static void RenderCommandHelp()
);
_ = commandTable.AddRow(
Markup.Escape("list-filters [source]"),
- Markup.Escape("List filters for source")
+ Markup.Escape("List filters for source, through a source handle")
);
_ = commandTable.AddRow(
Markup.Escape("toggle-filter [source] [filter]"),
- Markup.Escape("Toggle filter enabled state")
+ Markup.Escape("Toggle filter enabled state, through one filter handle")
);
_ = commandTable.AddRow(
Markup.Escape("media [input] [action]"),
@@ -4225,7 +4523,9 @@ private static void RenderCommandHelp()
);
_ = commandTable.AddRow(
Markup.Escape("watch [seconds]"),
- Markup.Escape("Stream scene changes with await foreach (default 15s)")
+ Markup.Escape(
+ "Stream scene changes with await foreach, acting on the event's free handle (default 15s)"
+ )
);
_ = commandTable.AddRow(
Markup.Escape("batch-example"),
@@ -4312,7 +4612,7 @@ async Task Probe(string name, Func call)
GetSceneItemListResponseData items = await client
.SceneItems.GetSceneItemListAsync(new(sceneName: sceneName), cancellationToken)
.ConfigureAwait(false);
- int sceneItemId = items.SceneItems.Count > 0 ? items.SceneItems[0].SceneItemId : -1;
+ long sceneItemId = items.SceneItems.Count > 0 ? items.SceneItems[0].SceneItemId : -1;
string? itemSourceName = items.SceneItems.Count > 0 ? items.SceneItems[0].SourceName : null;
GetInputKindListResponseData inputKinds = await client
@@ -4962,7 +5262,7 @@ await Probe(
GetSceneItemListResponseData fixtureItems = await client
.SceneItems.GetSceneItemListAsync(new(sceneName: sceneName), cancellationToken)
.ConfigureAwait(false);
- int itemId = fixtureItems.SceneItems[0].SceneItemId;
+ long itemId = fixtureItems.SceneItems[0].SceneItemId;
// ── Scenes ───────────────────────────────────────────────────────
await Probe(
@@ -5058,7 +5358,7 @@ await Probe(
)
.ConfigureAwait(false);
- int? addedItemId = null;
+ long? addedItemId = null;
try
{
CreateSceneItemResponseData added = await client
@@ -5075,7 +5375,7 @@ await Probe(
declined.Add($"CreateSceneItem ({ex.StatusCode})");
}
- int? duplicatedItemId = null;
+ long? duplicatedItemId = null;
try
{
DuplicateSceneItemResponseData duplicated = await client
@@ -5094,7 +5394,7 @@ await Probe(
// Only the two this sweep added. Removing the last scene item that references an input
// destroys the input, which took the audio and media fixtures with it.
- foreach (int extra in new[] { addedItemId, duplicatedItemId }.OfType())
+ foreach (long extra in new[] { addedItemId, duplicatedItemId }.OfType())
{
await Probe(
"RemoveSceneItem",
@@ -5664,6 +5964,26 @@ await Probe(
];
}
+ ///
+ /// Runs an action expected to throw, and hands back the exception it threw.
+ ///
+ private static async Task ExpectThrowAsync(Func action)
+ where TException : Exception
+ {
+ try
+ {
+ await action().ConfigureAwait(false);
+ }
+ catch (TException expected)
+ {
+ return expected;
+ }
+
+ throw new InvalidOperationException(
+ $"Expected {typeof(TException).Name}, but the call succeeded."
+ );
+ }
+
private static void RenderKeyValueTable(
string title,
IReadOnlyList<(string Key, string Value)> rows
diff --git a/ObsWebSocket.Tests/FalsyRequestFieldTests.cs b/ObsWebSocket.Tests/FalsyRequestFieldTests.cs
index 588fd91..56fdd3a 100644
--- a/ObsWebSocket.Tests/FalsyRequestFieldTests.cs
+++ b/ObsWebSocket.Tests/FalsyRequestFieldTests.cs
@@ -51,7 +51,7 @@ public void Serialize_RequiredZeroNumber_KeepsField()
public void Serialize_UnsetOptionalField_IsStillOmitted()
{
// Optional protocol fields are generated as nullable, so they must stay absent
- // rather than being sent as explicit nulls — OBS applies its own defaults for
+ // rather than being sent as explicit nulls. OBS applies its own defaults for
// fields that are not present.
string json = Json(new CreateSceneItemRequestData(sceneName: "Scene", sourceName: "Src"));
Assert.IsFalse(json.Contains("null", StringComparison.Ordinal), json);
diff --git a/ObsWebSocket.Tests/HandleOperationTests.cs b/ObsWebSocket.Tests/HandleOperationTests.cs
new file mode 100644
index 0000000..b1b2a93
--- /dev/null
+++ b/ObsWebSocket.Tests/HandleOperationTests.cs
@@ -0,0 +1,190 @@
+using System.Net.WebSockets;
+using System.Text.Json;
+using Microsoft.Extensions.Logging;
+using Moq;
+using ObsWebSocket.Core;
+using ObsWebSocket.Core.Networking;
+using ObsWebSocket.Core.Protocol;
+using ObsWebSocket.Core.Serialization;
+
+namespace ObsWebSocket.Tests;
+
+///
+/// The generated operations exist to send the right identity field and no other. OBS resolves a
+/// uuid before a name and reads the canvas only on the name path, so a handle that sent both would
+/// be sending a field the server ignores, and one that sent neither would be
+/// MissingRequestField.
+///
+[TestClass]
+public sealed class HandleOperationTests
+{
+ private static readonly Guid s_uuid = new("5d5db648-93a5-4985-bff8-45f4c9fe15f7");
+
+ [TestMethod]
+ public async Task ANameHandleSendsTheNameAndNoUuid()
+ {
+ JsonElement sent = await CaptureAsync(
+ (client, ct) => client.Scene("Intro").SetCurrentProgramAsync(ct)
+ );
+
+ Assert.AreEqual("Intro", sent.GetProperty("sceneName").GetString());
+ Assert.IsFalse(
+ sent.TryGetProperty("sceneUuid", out JsonElement uuid)
+ && uuid.ValueKind is not JsonValueKind.Null,
+ "a name handle must not also send a uuid"
+ );
+ }
+
+ [TestMethod]
+ public async Task AUuidHandleSendsTheUuidAndNoName()
+ {
+ JsonElement sent = await CaptureAsync(
+ (client, ct) => client.Scene(s_uuid).SetCurrentProgramAsync(ct)
+ );
+
+ Assert.AreEqual(
+ "5d5db648-93a5-4985-bff8-45f4c9fe15f7",
+ sent.GetProperty("sceneUuid").GetString()
+ );
+ Assert.IsFalse(
+ sent.TryGetProperty("sceneName", out JsonElement name)
+ && name.ValueKind is not JsonValueKind.Null,
+ "a uuid handle must not also send a name"
+ );
+ }
+
+ ///
+ /// The canvas travels with a name, because that is the only path OBS reads it on.
+ ///
+ [TestMethod]
+ public async Task ACanvasScopedNameSendsTheCanvas()
+ {
+ CanvasHandle vertical = CanvasHandle.FromUuid(s_uuid);
+
+ JsonElement sent = await CaptureAsync(
+ (client, ct) => client.Scene(vertical.Scene("Intro")).GetItemListAsync(ct)
+ );
+
+ Assert.AreEqual("Intro", sent.GetProperty("sceneName").GetString());
+ Assert.AreEqual(
+ "5d5db648-93a5-4985-bff8-45f4c9fe15f7",
+ sent.GetProperty("canvasUuid").GetString()
+ );
+ }
+
+ ///
+ /// A composite handle supplies both halves of the identity: the scene, and the id inside it.
+ ///
+ [TestMethod]
+ public async Task ASceneItemSendsItsSceneAndItsId()
+ {
+ JsonElement sent = await CaptureAsync(
+ (client, ct) =>
+ client.SceneItem(SceneHandle.FromName("Intro").Item(7)).SetEnabledAsync(false, ct)
+ );
+
+ Assert.AreEqual("Intro", sent.GetProperty("sceneName").GetString());
+ Assert.AreEqual(7, sent.GetProperty("sceneItemId").GetInt32());
+ Assert.IsFalse(sent.GetProperty("sceneItemEnabled").GetBoolean());
+ }
+
+ [TestMethod]
+ public async Task AFilterSendsItsSourceAndItsName()
+ {
+ JsonElement sent = await CaptureAsync(
+ (client, ct) =>
+ client.Filter(InputHandle.FromName("Mic").Filter("EQ")).SetEnabledAsync(true, ct)
+ );
+
+ Assert.AreEqual("Mic", sent.GetProperty("sourceName").GetString());
+ Assert.AreEqual("EQ", sent.GetProperty("filterName").GetString());
+ Assert.IsTrue(sent.GetProperty("filterEnabled").GetBoolean());
+ }
+
+ ///
+ /// A second reference in one request stays a parameter, because only one thing can be the
+ /// subject. DuplicateSceneItem is the only request in the protocol shaped this way.
+ ///
+ [TestMethod]
+ public async Task ASecondReferenceIsAParameter()
+ {
+ JsonElement sent = await CaptureAsync(
+ (client, ct) =>
+ client
+ .SceneItem(SceneHandle.FromName("Intro").Item(7))
+ .DuplicateAsync(destinationScene: "Outro", cancellationToken: ct)
+ );
+
+ Assert.AreEqual("Intro", sent.GetProperty("sceneName").GetString());
+ Assert.AreEqual(7, sent.GetProperty("sceneItemId").GetInt32());
+ Assert.AreEqual("Outro", sent.GetProperty("destinationSceneName").GetString());
+ }
+
+ /// A bare string is a name, which is the whole point of the implicit conversion.
+ [TestMethod]
+ public async Task AStringReachesTheOperationsWithoutCeremony()
+ {
+ JsonElement sent = await CaptureAsync(
+ (client, ct) => client.Input("Mic").ToggleMuteAsync(ct)
+ );
+
+ Assert.AreEqual("Mic", sent.GetProperty("inputName").GetString());
+ }
+
+ ///
+ /// Captures the requestData of the request the action sends.
+ ///
+ ///
+ /// The call is cancelled the moment the bytes are on the wire, which is everything this test
+ /// class is about. Letting it run to completion would mean canning a response per request
+ /// shape, and the response is not what is under test.
+ ///
+ private static async Task CaptureAsync(
+ Func act
+ )
+ {
+ (ObsWebSocketClient? client, _, Mock? socket) =
+ TestUtils.SetupConnectedClientForceState();
+
+ JsonElement? captured = null;
+ using CancellationTokenSource stopOnceSent = new();
+
+ _ = socket
+ .Setup(ws =>
+ ws.SendAsync(
+ It.IsAny>(),
+ It.IsAny(),
+ true,
+ It.IsAny()
+ )
+ )
+ .Callback(
+ (
+ ReadOnlyMemory buffer,
+ WebSocketMessageType type,
+ bool end,
+ CancellationToken token
+ ) =>
+ {
+ OutgoingMessage? message = JsonSerializer.Deserialize<
+ OutgoingMessage
+ >(buffer.Span, TestUtils.s_jsonSerializerOptions);
+ captured = message?.D?.RequestData;
+ stopOnceSent.Cancel();
+ }
+ )
+ .Returns(ValueTask.CompletedTask);
+
+ try
+ {
+ await act(client, stopOnceSent.Token);
+ }
+ catch (OperationCanceledException)
+ {
+ // Expected: the request was cancelled as soon as it had been sent.
+ }
+
+ Assert.IsNotNull(captured, "no request was sent");
+ return captured.Value;
+ }
+}
diff --git a/ObsWebSocket.Tests/HandleTests.cs b/ObsWebSocket.Tests/HandleTests.cs
new file mode 100644
index 0000000..c946d18
--- /dev/null
+++ b/ObsWebSocket.Tests/HandleTests.cs
@@ -0,0 +1,239 @@
+using ObsWebSocket.Core;
+using ObsWebSocket.Core.Protocol.Events;
+using ObsWebSocket.Core.Protocol.Responses;
+
+namespace ObsWebSocket.Tests;
+
+///
+/// A handle encodes the choice OBS makes for you: a uuid wins outright, a name is read only when
+/// no uuid was sent, the canvas is consulted only on the name path, and neither is
+/// MissingRequestField. The type has to make the last one impossible and the middle two
+/// automatic.
+///
+[TestClass]
+public sealed class HandleTests
+{
+ private static readonly Guid s_uuid = new("5d5db648-93a5-4985-bff8-45f4c9fe15f7");
+
+ [TestMethod]
+ public void AStringIsAName_AndAGuidIsAUuid()
+ {
+ SceneHandle byName = "Intro";
+ SceneHandle byUuid = s_uuid;
+
+ Assert.AreEqual("Intro", byName.Name);
+ Assert.IsNull(byName.Uuid);
+ Assert.IsFalse(byName.IsResolved);
+
+ Assert.IsNull(byUuid.Name);
+ Assert.AreEqual("5d5db648-93a5-4985-bff8-45f4c9fe15f7", byUuid.Uuid);
+ Assert.IsTrue(byUuid.IsResolved);
+ }
+
+ ///
+ /// OBS writes uuids lowercase and hyphenated, on Windows through UuidCreate and elsewhere
+ /// through uuid_unparse_lower, which is what Guid's "D" format produces.
+ ///
+ [TestMethod]
+ public void AGuidIsFormattedTheWayObsWritesOne()
+ {
+ Assert.AreEqual("5d5db648-93a5-4985-bff8-45f4c9fe15f7", SceneHandle.FromUuid(s_uuid).Uuid);
+ Assert.AreEqual(
+ SceneHandle.FromUuid("5d5db648-93a5-4985-bff8-45f4c9fe15f7"),
+ SceneHandle.FromUuid(s_uuid)
+ );
+ }
+
+ ///
+ /// A uuid read off the wire is never parsed, so a value OBS sends that is not a Guid cannot
+ /// break a response.
+ ///
+ [TestMethod]
+ public void AUuidFromTheWireIsCarriedVerbatim()
+ {
+ Assert.AreEqual("not-a-guid", SceneHandle.FromUuid("not-a-guid").Uuid);
+ }
+
+ ///
+ /// OBS reads canvasUuid only on the name path, so carrying it on a resolved handle would be
+ /// carrying a field the server ignores.
+ ///
+ [TestMethod]
+ public void ACanvasScopesANameAndIsDroppedByAUuid()
+ {
+ CanvasHandle vertical = CanvasHandle.FromUuid(s_uuid);
+
+ Assert.AreEqual(vertical, SceneHandle.FromName("Intro", vertical).Canvas);
+ Assert.AreEqual(CanvasHandle.Main, SceneHandle.FromUuid(s_uuid).Canvas);
+ Assert.AreEqual(vertical, vertical.Scene("Intro").Canvas);
+ }
+
+ [TestMethod]
+ public void TheMainCanvasCarriesNoUuid_WhichIsWhatOmittingTheFieldMeans()
+ {
+ Assert.IsNull(CanvasHandle.Main.Uuid);
+ Assert.IsFalse(CanvasHandle.Main.IsResolved);
+ Assert.IsNull(CanvasHandle.Main.Name);
+ }
+
+ ///
+ /// A scene is a source in OBS, and the requests that take a bare source accept either.
+ ///
+ [TestMethod]
+ public void ASceneAndAnInputBothNarrowToASource()
+ {
+ Assert.AreEqual("Intro", SceneHandle.FromName("Intro").AsSource().Name);
+ Assert.AreEqual(
+ "5d5db648-93a5-4985-bff8-45f4c9fe15f7",
+ SceneHandle.FromUuid(s_uuid).AsSource().Uuid
+ );
+ Assert.AreEqual("Mic", InputHandle.FromName("Mic").AsSource().Name);
+ Assert.AreEqual(
+ "5d5db648-93a5-4985-bff8-45f4c9fe15f7",
+ InputHandle.FromUuid(s_uuid).AsSource().Uuid
+ );
+ }
+
+ ///
+ /// A scene item known only by its source name cannot be passed where an id is required, so
+ /// the missing lookup is a compile error rather than a runtime one.
+ ///
+ [TestMethod]
+ public void ASceneItemByIdIsAHandle_ButBySourceNameItIsNotYet()
+ {
+ SceneHandle intro = "Intro";
+
+ SceneItemHandle byId = intro.Item(3);
+ Assert.AreEqual(3, byId.SceneItemId);
+ Assert.AreEqual(intro, byId.Scene);
+
+ UnresolvedSceneItem byName = intro.Item("Logo");
+ Assert.AreEqual("Logo", byName.SourceName);
+ Assert.AreEqual(intro, byName.Scene);
+ }
+
+ /// Filters have no uuid in the protocol, so the name is the identity.
+ [TestMethod]
+ public void AFilterIsNamedOnItsSource()
+ {
+ FilterHandle eq = InputHandle.FromName("Mic").Filter("EQ");
+
+ Assert.AreEqual("EQ", eq.FilterName);
+ Assert.AreEqual("Mic", eq.Source.Name);
+ }
+
+ [TestMethod]
+ public void AnEmptyNameIsRefusedRatherThanSentAsOne()
+ {
+ _ = Assert.ThrowsExactly(() => SceneHandle.FromName(string.Empty));
+ _ = Assert.ThrowsExactly(() => InputHandle.FromName(string.Empty));
+ _ = Assert.ThrowsExactly(() => CanvasHandle.FromName(string.Empty));
+ _ = Assert.ThrowsExactly(() =>
+ InputHandle.FromName("Mic").Filter(string.Empty)
+ );
+ }
+
+ [TestMethod]
+ public void ANegativeSceneItemIdIsRefused()
+ {
+ _ = Assert.ThrowsExactly(() =>
+ SceneHandle.FromName("Intro").Item(-1)
+ );
+ }
+
+ ///
+ /// Handles are compared by what they address, so one built from a response equals one written
+ /// by hand.
+ ///
+ [TestMethod]
+ public void TwoHandlesForTheSameThingAreEqual()
+ {
+ Assert.AreEqual(SceneHandle.FromName("Intro"), (SceneHandle)"Intro");
+ Assert.AreEqual(SceneHandle.FromUuid(s_uuid), (SceneHandle)s_uuid);
+ Assert.AreNotEqual(SceneHandle.FromName("Intro"), SceneHandle.FromName("Outro"));
+
+ // A name and a uuid are different addresses even when they point at one scene, because
+ // nothing here has asked OBS which scene that is.
+ Assert.AreNotEqual(SceneHandle.FromName("Intro"), SceneHandle.FromUuid(s_uuid));
+ }
+
+ [TestMethod]
+ public void AHandleSaysWhatItAddressesWhenPrinted()
+ {
+ Assert.AreEqual("scene 'Intro'", SceneHandle.FromName("Intro").ToString());
+ Assert.AreEqual("the main canvas", CanvasHandle.Main.ToString());
+ Assert.AreEqual(
+ "filter 'EQ' on source 'Mic'",
+ InputHandle.FromName("Mic").Filter("EQ").ToString()
+ );
+ Assert.AreEqual(
+ "item 3 in scene 'Intro'",
+ SceneHandle.FromName("Intro").Item(3).ToString()
+ );
+ }
+
+ ///
+ /// An event already says which scene, by uuid. Reading the name back off it and addressing by
+ /// name again is the round trip and the race the uuid was there to avoid.
+ ///
+ [TestMethod]
+ public void AnEventCarriesAResolvedHandle()
+ {
+ CurrentProgramSceneChangedPayload changed = new(
+ sceneName: "Intro",
+ sceneUuid: "5d5db648-93a5-4985-bff8-45f4c9fe15f7"
+ );
+
+ Assert.IsTrue(changed.Scene.IsResolved);
+ Assert.AreEqual("5d5db648-93a5-4985-bff8-45f4c9fe15f7", changed.Scene.Uuid);
+ Assert.IsNull(changed.Scene.Name);
+ }
+
+ ///
+ /// A scene item needs both halves, and an event that reports one carries both.
+ ///
+ [TestMethod]
+ public void AnEventCarriesAResolvedSceneItem()
+ {
+ SceneItemEnableStateChangedPayload changed = new(
+ sceneName: "Intro",
+ sceneUuid: "5d5db648-93a5-4985-bff8-45f4c9fe15f7",
+ sceneItemId: 7,
+ sceneItemEnabled: true
+ );
+
+ Assert.AreEqual(7, changed.SceneItem.SceneItemId);
+ Assert.IsTrue(changed.SceneItem.Scene.IsResolved);
+ }
+
+ ///
+ /// Creating something answers with its uuid, so the handle for it costs no second request.
+ ///
+ [TestMethod]
+ public void CreatingSomethingAnswersWithAHandle()
+ {
+ CreateSceneResponseData created = new(sceneUuid: "5d5db648-93a5-4985-bff8-45f4c9fe15f7");
+
+ Assert.IsTrue(created.Scene.IsResolved);
+ Assert.AreEqual("5d5db648-93a5-4985-bff8-45f4c9fe15f7", created.Scene.Uuid);
+ }
+
+ ///
+ /// The preview scene is null outside studio mode, and the protocol says so in prose rather
+ /// than by marking the field optional. The accessor has to be nullable to match.
+ ///
+ [TestMethod]
+ public void AUuidTheProtocolSaysCanBeNullYieldsANullHandle()
+ {
+ GetSceneListResponseData outsideStudioMode = new(
+ scenes: [],
+ currentProgramSceneName: "Intro",
+ currentProgramSceneUuid: "5d5db648-93a5-4985-bff8-45f4c9fe15f7",
+ currentPreviewSceneName: null,
+ currentPreviewSceneUuid: null
+ );
+
+ Assert.IsNotNull(outsideStudioMode.CurrentProgramScene);
+ Assert.IsNull(outsideStudioMode.CurrentPreviewScene);
+ }
+}
diff --git a/ObsWebSocket.Tests/ObsWebSocketClientIntegrationTests.cs b/ObsWebSocket.Tests/ObsWebSocketClientIntegrationTests.cs
index 0bfa367..166d12c 100644
--- a/ObsWebSocket.Tests/ObsWebSocketClientIntegrationTests.cs
+++ b/ObsWebSocket.Tests/ObsWebSocketClientIntegrationTests.cs
@@ -441,7 +441,7 @@ public async Task GetSceneItemTransform_ReturnsTransformStub()
idResponse,
$"Could not get SceneItemId for '{s_testOptions.TestInputName}' in scene '{s_testOptions.TestSceneName}'. Ensure it exists."
);
- int sceneItemId = idResponse.SceneItemId;
+ long sceneItemId = idResponse.SceneItemId;
// Get the transform
GetSceneItemTransformResponseData? transformResponse =
diff --git a/ObsWebSocket.Tests/ReadmeCompileCheck.cs b/ObsWebSocket.Tests/ReadmeCompileCheck.cs
index bec11b9..5a4967b 100644
--- a/ObsWebSocket.Tests/ReadmeCompileCheck.cs
+++ b/ObsWebSocket.Tests/ReadmeCompileCheck.cs
@@ -299,7 +299,7 @@ CancellationToken ct
internal static async Task NumbersAsync(ObsWebSocketClient client, CancellationToken ct)
{
- int id =
+ long id =
await client.SceneItems.FindSceneItemIdAsync("Intro", "Logo", ct)
?? throw new InvalidOperationException();
await client.SceneItems.SetSceneItemIndexAsync(
@@ -415,6 +415,59 @@ internal static async Task LowLevelAsync(ObsWebSocketClient client, Cancellation
_ = $"{v?.ObsVersion} {raw}";
}
+ internal static async Task HandlesAsync(
+ ObsWebSocketClient client,
+ Guid sceneGuid,
+ CancellationToken ct
+ )
+ {
+ await client.Scene("Intro").SetCurrentProgramAsync(ct);
+ await client.Scene(sceneGuid).SetNameAsync("Outro", ct);
+ await client.Input("Mic").SetMuteAsync(true, ct);
+ await client.Input("Mic").Filter("EQ").SetEnabledAsync(false, ct);
+ }
+
+ internal static async Task HandleResolveAsync(ObsWebSocketClient client, CancellationToken ct)
+ {
+ SceneOperations intro = await client.Scene("Intro").ResolveAsync(ct);
+ _ = intro.Handle.IsResolved;
+ }
+
+ internal static void FreeSceneHandle(ObsWebSocketClient client)
+ {
+ client.Scenes.CurrentProgramSceneChanged += async (_, e) =>
+ await client.Scene(e.EventData.Scene).GetItemListAsync();
+ }
+
+ internal static async Task FreeResponseHandleAsync(
+ ObsWebSocketClient client,
+ CancellationToken ct
+ )
+ {
+ CreateSceneResponseData created = await client.Scenes.CreateSceneAsync(new("Intro"), ct);
+ await client.Scene(created.Scene).SetCurrentProgramAsync(ct);
+ }
+
+ internal static async Task SceneItemHandlesAsync(
+ ObsWebSocketClient client,
+ CancellationToken ct
+ )
+ {
+ SceneItemOperations logo = await client
+ .Scene("Intro")
+ .ItemAsync("Logo", cancellationToken: ct);
+ await logo.SetEnabledAsync(false, ct);
+ await logo.Scene.GetItemListAsync(ct);
+
+ await client.Scene("Intro").Item(3).SetIndexAsync(0, ct);
+ }
+
+ internal static async Task CanvasHandlesAsync(ObsWebSocketClient client, CancellationToken ct)
+ {
+ CanvasHandle vertical = await client.Canvases.ResolveAsync("Vertical", ct);
+ await client.Scene(vertical.Scene("Intro")).GetItemListAsync(ct);
+ }
+
internal static async Task GroupedSurfaceAsync(ObsWebSocketClient client, CancellationToken ct)
{
await client.Scenes.GetSceneListAsync(new(), ct);
diff --git a/ObsWebSocket.Tests/StubArrayTests.cs b/ObsWebSocket.Tests/StubArrayTests.cs
index 7092e6e..35f2bc3 100644
--- a/ObsWebSocket.Tests/StubArrayTests.cs
+++ b/ObsWebSocket.Tests/StubArrayTests.cs
@@ -254,6 +254,178 @@ public void MeterItem_ReadAsInputStub_FailsOnTheKindFieldsItNeverSends()
StringAssert.Contains(ex.InnerException!.Message, "inputKind");
}
+ ///
+ /// OBS copies the output dimensions straight out of obs_output_get_width and
+ /// obs_output_get_height, which return uint32_t and are not clamped on the way
+ /// out. An output that has never started can report a value past ; a
+ /// live OBS 32.2.2 sent 2586032160 for an idle virtual camera. As an int that
+ /// took the whole GetOutputList response down, not just the one field.
+ ///
+ [TestMethod]
+ public void OutputList_HeightAboveInt32Max_ReadsOverJson()
+ {
+ JsonElement payload = JsonDocument
+ .Parse(
+ """
+ {
+ "outputs": [
+ {
+ "outputName": "virtualcam_output",
+ "outputKind": "virtualcam_output",
+ "outputActive": false,
+ "outputWidth": 0,
+ "outputHeight": 2586032160,
+ "outputFlags": { "OBS_OUTPUT_VIDEO": true }
+ }
+ ]
+ }
+ """
+ )
+ .RootElement.Clone();
+
+ GetOutputListResponseData? read = CreateJsonSerializer()
+ .DeserializePayload(payload);
+
+ Assert.IsNotNull(read);
+ Assert.AreEqual(1, read.Outputs.Count);
+ Assert.AreEqual(2586032160L, read.Outputs[0].OutputHeight);
+ }
+
+ [TestMethod]
+ public void OutputList_HeightAboveInt32Max_RoundTripsOverMsgPack()
+ {
+ GetOutputListResponseData original = new()
+ {
+ Outputs =
+ [
+ new OutputStub
+ {
+ OutputName = "virtualcam_output",
+ OutputKind = "virtualcam_output",
+ OutputActive = false,
+ OutputWidth = 0,
+ OutputHeight = 2586032160L,
+ },
+ ],
+ };
+
+ byte[] packed = MessagePackSerializer.Serialize(
+ original,
+ MsgPackMessageSerializer.s_msgPackOptions
+ );
+ GetOutputListResponseData read =
+ MessagePackSerializer.Deserialize(
+ packed,
+ MsgPackMessageSerializer.s_msgPackOptions
+ );
+
+ Assert.AreEqual(2586032160L, read.Outputs[0].OutputHeight);
+ }
+
+ ///
+ /// The same defect in the counters. The frame counts come from uint32_t and the session
+ /// message counts from uint64_t, none of them clamped, and a monotonic frame counter
+ /// passes after roughly 414 days at 60fps.
+ ///
+ [TestMethod]
+ public void Stats_CountersAboveInt32Max_ReadOverJson()
+ {
+ JsonElement payload = JsonDocument
+ .Parse(
+ """
+ {
+ "cpuUsage": 1.5,
+ "memoryUsage": 512.0,
+ "availableDiskSpace": 1024.0,
+ "activeFps": 60.0,
+ "averageFrameRenderTime": 1.2,
+ "renderSkippedFrames": 4294967295,
+ "renderTotalFrames": 3000000000,
+ "outputSkippedFrames": 2147483648,
+ "outputTotalFrames": 4000000000,
+ "webSocketSessionIncomingMessages": 5000000000,
+ "webSocketSessionOutgoingMessages": 6000000000
+ }
+ """
+ )
+ .RootElement.Clone();
+
+ GetStatsResponseData? read = CreateJsonSerializer()
+ .DeserializePayload(payload);
+
+ Assert.IsNotNull(read);
+ Assert.AreEqual(4294967295L, read.RenderSkippedFrames);
+ Assert.AreEqual(3000000000L, read.RenderTotalFrames);
+ Assert.AreEqual(6000000000L, read.WebSocketSessionOutgoingMessages);
+ }
+
+ ///
+ /// A canvas-scoped scene list has no index to report, so OBS sends sceneIndex as null
+ /// rather than omitting it. As a non-nullable member that failed the whole response.
+ ///
+ [TestMethod]
+ public void SceneList_NullSceneIndex_ReadsOverJson()
+ {
+ JsonElement payload = JsonDocument
+ .Parse(
+ """
+ {
+ "scenes": [
+ {
+ "sceneName": "Intro",
+ "sceneUuid": "0e57ad4c-2b2d-4f5b-9d05-3f4b0f4f1f10",
+ "sceneIndex": null
+ }
+ ]
+ }
+ """
+ )
+ .RootElement.Clone();
+
+ GetSceneListResponseData? read = CreateJsonSerializer()
+ .DeserializePayload(payload);
+
+ Assert.IsNotNull(read);
+ Assert.AreEqual(1, read.Scenes.Count);
+ Assert.IsNull(read.Scenes[0].SceneIndex);
+ }
+
+ ///
+ /// The alignment fields are uint32_t masks in obs_transform_info, and
+ /// obs-websocket validates writes to the whole unsigned range rather than to the flags it
+ /// defines, so a value past reaches the client.
+ ///
+ [TestMethod]
+ public void SceneItemTransform_AlignmentAboveInt32Max_ReadsOverJson()
+ {
+ JsonElement payload = JsonDocument
+ .Parse(
+ """
+ {
+ "sceneItemTransform": {
+ "positionX": 0, "positionY": 0, "rotation": 0,
+ "scaleX": 1, "scaleY": 1, "width": 100, "height": 100,
+ "sourceWidth": 100, "sourceHeight": 100,
+ "alignment": 4294967295,
+ "boundsType": "OBS_BOUNDS_NONE",
+ "boundsAlignment": 3000000000,
+ "boundsWidth": 0, "boundsHeight": 0,
+ "cropLeft": 0, "cropTop": 0, "cropRight": 0, "cropBottom": 0,
+ "cropToBounds": false
+ }
+ }
+ """
+ )
+ .RootElement.Clone();
+
+ GetSceneItemTransformResponseData? read = CreateJsonSerializer()
+ .DeserializePayload(payload);
+
+ Assert.IsNotNull(read);
+ Assert.AreEqual(4294967295L, read.SceneItemTransform.Alignment);
+ Assert.AreEqual(3000000000L, read.SceneItemTransform.BoundsAlignment);
+ }
+
///
/// The formatter that made an unmapped array readable at all. Nothing generated uses it now
/// that every array is mapped, but it is what stands between a future unmapped array and a
diff --git a/README.md b/README.md
index 1b35124..9e0efe2 100644
--- a/README.md
+++ b/README.md
@@ -1,16 +1,13 @@
# ObsWebSocket.Core
-Modern .NET client for OBS Studio WebSocket v5, with generated protocol types and DI-first integration.
+A .NET client for the OBS Studio WebSocket v5 protocol, with generated request types and DI-first
+integration.
[](https://github.com/Agash/ObsWebSocket/actions)
[](https://www.nuget.org/packages/ObsWebSocket.Core/)
[](https://opensource.org/licenses/MIT)
-## Targets
-
-- `net11.0`
-- `net10.0`
-- `net9.0`
+Targets `net11.0`, `net10.0` and `net9.0`.
## Install
@@ -18,7 +15,8 @@ Modern .NET client for OBS Studio WebSocket v5, with generated protocol types an
dotnet add package ObsWebSocket.Core
```
-> **OBS WebSocket v5 only** (OBS Studio 28+). Enable the server via *Tools → WebSocket Server Settings* in OBS.
+Requires OBS Studio 28 or newer with obs-websocket v5. Enable the server under
+*Tools > WebSocket Server Settings*.
## Quick start
@@ -59,6 +57,8 @@ public sealed class Worker(ObsWebSocketClient client) : BackgroundService
var version = await client.General.GetVersionAsync(ct);
Console.WriteLine($"Connected to OBS {version.ObsVersion}");
+ await client.Input("Mic").SetMuteAsync(true, ct);
+
await foreach (var e in client.Scenes.CurrentProgramSceneChangedStream(cancellationToken: ct))
{
Console.WriteLine($"Scene changed: {e.EventData.SceneName}");
@@ -67,102 +67,188 @@ public sealed class Worker(ObsWebSocketClient client) : BackgroundService
}
```
-## Everything is grouped by category
+## Three ways to call OBS
+
+| | Example | Use it for |
+|---|---|---|
+| [Handles](#handles) | `client.Input("Mic").SetMuteAsync(true, ct)` | Requests about one scene, input, source, scene item or filter |
+| [Category groups](#category-groups) | `client.Inputs.SetInputMuteAsync(new("Mic", true), ct)` | Everything. One method per protocol request, plus helpers |
+| [Raw requests](#raw-requests) | `client.CallAsync("SetInputMute", data, ct)` | Requests this build does not model |
+
+Each forwards to the one below it, so they mix freely.
+
+## Category groups
-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:
+The client mirrors the categories the protocol defines. Requests, event streams and the helpers this
+library adds sit in the group their category owns:
```csharp
-await client.Scenes.GetSceneListAsync(new(), ct); // generated request
-await client.Scenes.SwitchProgramSceneAndWaitAsync("Intro", cancellationToken: ct); // convenience
+await client.Scenes.GetSceneListAsync(new(), ct);
+await client.Scenes.SwitchProgramSceneAndWaitAsync("Intro", cancellationToken: ct);
await client.Inputs.SetInputVolumeDbAsync("Mic", -6, ct);
await client.SceneItems.SetSceneItemEnabledAsync("Intro", "Logo", false, ct);
-client.Scenes.CurrentProgramSceneChanged += (_, e) => { }; // classic event
+client.Scenes.CurrentProgramSceneChanged += (_, e) => { };
await foreach (var e in client.Scenes.CurrentProgramSceneChangedStream(cancellationToken: ct)) { break; }
```
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.
+`Record`, `SceneItems`, `Scenes`, `Sources`, `Stream`, `Transitions` and `Ui`.
-`WaitForEventAsync` and `CallBatchAsync` stay directly on the client, since neither belongs to one
+`WaitForEventAsync` and `CallBatchAsync` sit on the client itself, since neither belongs to a
category.
-## The helper set
+## Handles
+
+Most OBS requests identify their target by name or by uuid. A handle carries that identity so it is
+not repeated on every call. A string is a name, a `Guid` is a uuid:
+
+```csharp
+await client.Scene("Intro").SetCurrentProgramAsync(ct);
+await client.Scene(sceneGuid).SetNameAsync("Outro", ct);
+await client.Input("Mic").SetMuteAsync(true, ct);
+await client.Input("Mic").Filter("EQ").SetEnabledAsync(false, ct);
+```
+
+The entry points are `Scene`, `Input`, `Source`, `SceneItem` and `Filter`. Each carries the requests
+the protocol defines for that kind of thing, with the entity dropped from the method name:
+`SetSceneItemEnabled` is `SetEnabledAsync` on a scene item, `GetInputMute` is `GetMuteAsync` on an
+input. The protocol name is in the XML docs and on the category group.
+
+Requests that are not about a particular thing, such as `GetVersion`, `GetStats` and the record and
+stream controls, are on their group only.
+
+### Names and uuids
+
+A name works, but breaks if the thing is renamed. Resolving a name to a uuid costs one round trip:
+
+```csharp
+SceneOperations intro = await client.Scene("Intro").ResolveAsync(ct);
+// intro.Handle.IsResolved is true, and a rename no longer affects it
+```
+
+The protocol has no lookup for a single uuid, so this reads the scene list. When the name is not
+found, the exception lists the names that were:
+
+```
+ObsWebSocketResourceNotFoundException: No scene named 'Intor'. Available: 'Intro', 'Gameplay', 'BRB'.
+```
+
+### Handles from events and responses
+
+Events and responses that carry a uuid expose a handle for it, so acting on one needs no lookup:
+
+```csharp
+client.Scenes.CurrentProgramSceneChanged += async (_, e) =>
+ await client.Scene(e.EventData.Scene).GetItemListAsync();
+
+CreateSceneResponseData created = await client.Scenes.CreateSceneAsync(new("Intro"), ct);
+await client.Scene(created.Scene).SetCurrentProgramAsync(ct);
+```
+
+### Scene items
-Alongside the generated request per protocol request, each group carries conveniences for things
-that otherwise take several calls or a lookup. 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.
+OBS addresses scene items by a numeric id that only `GetSceneItemId` reports, so an item known by
+source name has to be resolved before it can be used:
-**Settings read and write**
+```csharp
+SceneItemOperations logo = await client.Scene("Intro").ItemAsync("Logo", cancellationToken: ct);
+await logo.SetEnabledAsync(false, ct);
+await logo.Scene.GetItemListAsync(ct);
+
+await client.Scene("Intro").Item(3).SetIndexAsync(0, ct); // an id needs no lookup
+```
+
+`Item(long)` and `Filter(string)` send nothing, since an id and a filter name are the whole
+identity.
+
+### Canvases
+
+Canvas-scoped requests take a uuid; `canvasName` appears only in `GetCanvasList`. Resolve a canvas
+by name to use it:
+
+```csharp
+CanvasHandle vertical = await client.Canvases.ResolveAsync("Vertical", ct);
+await client.Scene(vertical.Scene("Intro")).GetItemListAsync(ct);
+```
+
+Omitting the canvas means the main one, which is `CanvasHandle.Main`. A canvas scopes a name only,
+so a resolved handle drops it.
+
+## Helpers
+
+Each group carries helpers for things that otherwise take several calls or a lookup. They are
+hand-written, so they are on the group rather than on a handle.
+
+Typed settings helpers have two overloads: an implicit one for library-registered types, and an
+explicit one taking a `JsonTypeInfo` for your own types. Use the explicit overload under
+Native AOT.
+
+**Settings**
| Helper | Notes |
|---|---|
| `Inputs.GetInputSettingsAsync` / `SetInputSettingsAsync` | Input settings; Set supports `overlay` |
-| `Inputs.GetInputDefaultSettingsAsync` | Defaults for a given input kind |
+| `Inputs.GetInputDefaultSettingsAsync` | Defaults for an input kind |
| `Filters.GetSourceFilterSettingsAsync` / `SetSourceFilterSettingsAsync` | Filter settings; Set supports `overlay` |
-| `Filters.GetSourceFilterDefaultSettingsAsync` | Defaults for a given filter kind |
+| `Filters.GetSourceFilterDefaultSettingsAsync` | Defaults for a filter kind |
| `Transitions.GetCurrentSceneTransitionSettingsAsync` / `SetCurrentSceneTransitionSettingsAsync` | Transition settings |
| `Outputs.GetOutputSettingsAsync` / `SetOutputSettingsAsync` | Output settings |
| `Config.GetStreamServiceSettingsAsync` / `SetStreamServiceSettingsAsync` | Stream service settings |
-Most take optional parameters ahead of the cancellation token, so pass it as `cancellationToken: ct`.
+Most take optional parameters before the cancellation token, so pass it as `cancellationToken: ct`.
**Scenes and scene items**
-- `Scenes.SwitchProgramSceneAsync(scene, ct)` and `Scenes.SwitchPreviewSceneAsync(scene, ct)` switch
- a scene. Optional `transitionName` and `transitionDurationMs` apply to that switch only.
-- `Scenes.SwitchProgramSceneAndWaitAsync` and `Scenes.SwitchPreviewSceneAndWaitAsync` do the same
- and wait for the event confirming it.
+- `Scenes.SwitchProgramSceneAsync(scene, ct)` and `Scenes.SwitchPreviewSceneAsync(scene, ct)`.
+ Optional `transitionName` and `transitionDurationMs` apply to that switch only.
+- `Scenes.SwitchProgramSceneAndWaitAsync` and `Scenes.SwitchPreviewSceneAndWaitAsync` also wait for
+ the confirming event.
- `SceneItems.SetSceneItemEnabledAsync(scene, sourceName, isEnabled, ct)` returns the resulting
- state. Leave `isEnabled` null to toggle. An overload takes the numeric item id instead.
-- `SceneItems.FindSceneItemIdAsync(scene, sourceName, ct)` returns `int?`, null rather than throwing
- when the item is not in the scene.
-- `Sources.SourceExistsAsync(name, ct)` and `Scenes.SceneExistsAsync(name, ct)` check existence.
+ state. Pass null for `isEnabled` to toggle. An overload takes the numeric item id.
+- `SceneItems.FindSceneItemIdAsync(scene, sourceName, ct)` returns `long?`, null when the item is
+ not in the scene.
+- `Sources.SourceExistsAsync(name, ct)` and `Scenes.SceneExistsAsync(name, ct)`.
**Inputs and filters**
-- `Inputs.SetInputTextAsync(name, text, ct)` is shorthand for updating text source content.
+- `Inputs.SetInputTextAsync(name, text, ct)` updates text source content.
- `Inputs.SetInputVolumeDbAsync(name, db, ct)` and `Inputs.SetInputVolumeMulAsync(name, mul, ct)`
each pick one unit. The underlying request accepts either and fails when given neither.
- `Inputs.SetInputMutesAsync(inputMutes, ct)` sets many mute states in one batch and returns the
- results, so a caller sees which inputs OBS rejected.
+ per-input results.
- `Inputs.CreateInputAsync(kind, name, settings, ...)` creates an input with typed settings.
- `Filters.CreateSourceFilterAsync(source, filterName, kind, settings, ct)` adds a typed filter.
**Media**
-- `MediaInputs.PlayMediaAsync`, `PauseMediaAsync`, `StopMediaAsync` and `RestartMediaAsync` are
- shorthands over `TriggerMediaActionAsync(name, MediaInputAction, ct)`.
+- `MediaInputs.PlayMediaAsync`, `PauseMediaAsync`, `StopMediaAsync` and `RestartMediaAsync` wrap
+ `TriggerMediaActionAsync(name, MediaInputAction, ct)`.
**Screenshots**
- `Sources.GetSourceScreenshotBytesAsync(source, ...)` returns decoded image bytes.
- `Sources.GetSourceScreenshotOnCanvasBytesAsync(source, ...)` does the same at canvas dimensions.
-- `Sources.SaveSourceScreenshotToFileAsync(source, filePath, ...)` writes straight to disk.
+- `Sources.SaveSourceScreenshotToFileAsync(source, filePath, ...)` writes to disk.
**Outputs**
- `Record.SetRecordActiveAndWaitAsync(activate, timeout, ct)`,
`Stream.SetStreamActiveAndWaitAsync(...)` and `Outputs.SetVirtualCamActiveAndWaitAsync(...)` start
- or stop the output and wait for OBS to confirm, returning the resulting `OutputState`.
+ or stop the output and wait for confirmation, returning the resulting `OutputState`.
- `Record.IsRecordActiveAsync(ct)`, `Stream.IsStreamActiveAsync(ct)` and
`Outputs.IsVirtualCamActiveAsync(ct)` read current state.
**Application state**
- `Config.EnsureProfileActiveAsync(name, ct)` and `Config.EnsureSceneCollectionActiveAsync(name, ct)`
- switch only if needed, returning whether the target is active rather than throwing when it does
- not exist.
+ switch only if needed, returning whether the target is active.
- `General.TriggerHotkeyAsync(hotkeyName, ct)` fires a hotkey by name.
-## Observing events
+## Events
-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:
+Every event is available as an async sequence on its group. The stream subscribes for the lifetime
+of the loop and unsubscribes when it ends:
```csharp
await foreach (var e in client.Scenes.CurrentProgramSceneChangedStream(cancellationToken: ct))
@@ -171,26 +257,23 @@ await foreach (var e in client.Scenes.CurrentProgramSceneChangedStream(cancellat
}
```
-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.
+Streams buffer a bounded number of events and drop the oldest when a consumer falls behind. Pass
+`capacity` to change that.
-The classic handler sits on the same group, so subscribing and streaming read alike and there is no
-second place to look:
+The classic handler is on the same group:
```csharp
client.Scenes.CurrentProgramSceneChanged += (_, e) =>
Console.WriteLine($"Program scene is now {e.EventData.SceneName}");
```
-Both work over the same event at once. The group's event is the client's event, so a handler added
-through one can be removed through the other; `client.CurrentProgramSceneChanged` remains for the
-low-level path, the way `CallAsync` remains alongside the generated requests.
+The group's event is the client's event, so both work at once.
-Connection lifecycle events stay on the client, since `Connected`, `Disconnected`,
-`ConnectionFailed` and `AuthenticationFailure` belong to no protocol category.
+`Connected`, `Disconnected`, `ConnectionFailed` and `AuthenticationFailure` are on the client, since
+they belong to no protocol category.
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:
+start the wait and then trigger the action:
```csharp
var changed = await client.WaitForEventAsync(ct);
@@ -202,26 +285,24 @@ var intro = await client.WaitForEventAsync(
);
```
-It throws `ObsWebSocketTimeoutException` when the wait elapses, the same type a request
-timeout raises, so one `catch (ObsWebSocketException)` covers both.
+It throws `ObsWebSocketTimeoutException` when the wait elapses.
-## Common use cases
+## Common tasks
### Update a text source
```csharp
await client.Inputs.SetInputTextAsync("NewsTicker", "Breaking: Live now!", ct);
-// 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, as are `TextFreetype2InputSettings`,
-`BrowserSourceSettings` and the filter settings types, which live in
-`ObsWebSocket.Core.Protocol.Common.InputSettings` and `.FilterSettings`.
+`TextGdiPlusInputSettings`, `TextFreetype2InputSettings`, `BrowserSourceSettings` and the filter
+settings types are built in, under `ObsWebSocket.Core.Protocol.Common.InputSettings` and
+`.FilterSettings`.
-### Check and save the replay buffer
+### Save the replay buffer
```csharp
var status = await client.Outputs.GetReplayBufferStatusAsync(ct);
@@ -244,7 +325,7 @@ await client.Inputs.SetInputSettingsAsync(
);
```
-Define your own type to target exactly what you need, and stay AOT-safe by passing its `JsonTypeInfo`:
+For settings this library does not model, define your own type and pass its `JsonTypeInfo`:
```csharp
[JsonSerializable(typeof(OverlaySettings))]
@@ -263,8 +344,8 @@ await client.Inputs.SetInputSettingsAsync(
);
```
-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.
+`overlay` comes before the cancellation token and defaults to true, merging your values onto the
+existing settings. Pass `overlay: false` to replace them.
### Screenshots
@@ -295,13 +376,11 @@ Console.WriteLine(results.Get(version).ObsVersion);
Console.WriteLine(results.Get(scenes).Scenes?.Count);
```
-`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.
+A request type may appear several times in one batch; each reference resolves to its own result.
+`Sleep` is valid only inside a batch.
-`Sleep` is only valid inside a batch, and pairs with `SerialRealtime` to pace a sequence.
-
-`TryGet` reports a failed or missing result instead of throwing, and `Get` throws
-`ObsWebSocketRequestException` carrying the OBS status code when that request was rejected:
+`TryGet` reports a failed or missing result instead of throwing. `Get` throws
+`ObsWebSocketRequestException` carrying the OBS status code:
```csharp
if (!results.AllSucceeded())
@@ -313,176 +392,110 @@ 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` covers anything the generated methods do not, including a raw `JsonElement`, and an overload
-taking a `JsonTypeInfo` keeps a custom payload AOT-safe:
+`Add` covers anything the generated methods do not, including a raw `JsonElement`, with an overload
+taking a `JsonTypeInfo`:
```csharp
batch.Add("GetStats");
batch.Add("SetInputSettings", myJsonElement);
```
-### Running requests in parallel
-
-`RequestBatchExecutionType.Parallel` works, but OBS mislabels what comes back. It collects results
-in completion order and labels them from the submission order, so on any one row the
-`requestType` and `requestId` belong to a different request than the `requestStatus` and
-`responseData` beside them. That happens before the response leaves OBS, so it cannot be corrected
-here. See [#16](https://github.com/Agash/ObsWebSocket/issues/16).
+### Parallel batches
-Only the labelling is wrong. `requestStatus` and `responseData` come from the same object, so each
-row's status does belong to the payload beside it; it is the `requestType` and `requestId` on that
-row that name a different request. `Get` and the indexer therefore throw rather than hand back data
-under the wrong reference, and `TryGet` reports `false`.
+`RequestBatchExecutionType.Parallel` works, but OBS labels the results incorrectly. It collects them
+in completion order and labels them from the submission order, so `requestType` and `requestId` on a
+row may not match the `requestStatus` and `responseData` beside them. This happens inside OBS and
+cannot be corrected here. See [#16](https://github.com/Agash/ObsWebSocket/issues/16).
-Nothing is lost, though, and `Raw` still reaches all of it. `GetData` reads the payload without
-consulting the label, so every response is recoverable as a set:
+Status and payload do come from the same object, so `Get` and the indexer throw rather than return
+data under the wrong reference, and `TryGet` returns false. Results that do not depend on ordering
+are still exact:
```csharp
-BatchResults results = await client.CallBatchAsync(
- batch, executionType: RequestBatchExecutionType.Parallel, haltOnFailure: false, cancellationToken: ct);
-
-foreach (RequestResponsePayload