diff --git a/.run/Stryker for PR.run.xml b/.run/Stryker for PR.run.xml
new file mode 100644
index 0000000..04a111a
--- /dev/null
+++ b/.run/Stryker for PR.run.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/AtemSharp.CodeGenerators/Analyzers/SerializationFieldAccessibilityAnalyzer.cs b/AtemSharp.CodeGenerators/Analyzers/SerializationFieldAccessibilityAnalyzer.cs
index b29c360..9c493b9 100644
--- a/AtemSharp.CodeGenerators/Analyzers/SerializationFieldAccessibilityAnalyzer.cs
+++ b/AtemSharp.CodeGenerators/Analyzers/SerializationFieldAccessibilityAnalyzer.cs
@@ -10,7 +10,6 @@ public class SerializationFieldAccessibilityAnalyzer : DiagnosticAnalyzer
{
public override ImmutableArray SupportedDiagnostics =>
ImmutableArray.Create(
- DiagnosticDescriptors.FieldCannotBeReadonly,
DiagnosticDescriptors.FieldCannotBePublic
);
@@ -37,15 +36,6 @@ private static void AnalyzeField(SymbolAnalysisContext context)
var attrName = attribute.AttributeClass?.Name;
if (attrName == "DeserializedFieldAttribute" || attrName == "SerializedFieldAttribute")
{
- if (fieldSymbol.IsReadOnly)
- {
- var diag = Diagnostic.Create(
- DiagnosticDescriptors.FieldCannotBeReadonly,
- fieldSymbol.Locations[0],
- fieldSymbol.Name
- );
- context.ReportDiagnostic(diag);
- }
if (fieldSymbol.DeclaredAccessibility == Accessibility.Public)
{
var diag = Diagnostic.Create(
diff --git a/AtemSharp.CodeGenerators/Analyzers/SerializationFieldAccessibilityCodeFixProvider.cs b/AtemSharp.CodeGenerators/Analyzers/SerializationFieldAccessibilityCodeFixProvider.cs
index 143fc0a..ed9768e 100644
--- a/AtemSharp.CodeGenerators/Analyzers/SerializationFieldAccessibilityCodeFixProvider.cs
+++ b/AtemSharp.CodeGenerators/Analyzers/SerializationFieldAccessibilityCodeFixProvider.cs
@@ -15,7 +15,7 @@ namespace AtemSharp.CodeGenerators.Analyzers
public class SerializationFieldAccessibilityCodeFixProvider : CodeFixProvider
{
public override ImmutableArray FixableDiagnosticIds =>
- ImmutableArray.Create("GEN008", "GEN009");
+ ImmutableArray.Create("GEN009");
public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer;
@@ -35,16 +35,7 @@ public override async Task RegisterCodeFixesAsync(CodeFixContext context)
return;
}
- if (diagnostic.Id == "GEN008")
- {
- context.RegisterCodeFix(
- CodeAction.Create(
- "Remove readonly modifier",
- c => RemoveReadonlyAsync(context.Document, fieldDecl, c),
- nameof(SerializationFieldAccessibilityCodeFixProvider) + ".RemoveReadonly"),
- diagnostic);
- }
- else if (diagnostic.Id == "GEN009")
+ if (diagnostic.Id == "GEN009")
{
// Offer to make private, internal, or protected
context.RegisterCodeFix(
@@ -68,20 +59,6 @@ public override async Task RegisterCodeFixesAsync(CodeFixContext context)
}
}
- private async Task RemoveReadonlyAsync(Document document, FieldDeclarationSyntax fieldDecl, CancellationToken cancellationToken)
- {
- var newModifiers = fieldDecl.Modifiers.Where(m => !m.IsKind(SyntaxKind.ReadOnlyKeyword));
- var newField = fieldDecl.WithModifiers(SyntaxFactory.TokenList(newModifiers));
- var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
- if (root == null)
- {
- return document;
- }
-
- var newRoot = root.ReplaceNode(fieldDecl, newField);
- return document.WithSyntaxRoot(newRoot);
- }
-
private async Task ChangeAccessibilityAsync(Document document, FieldDeclarationSyntax fieldDecl, SyntaxKind newAccessibility, CancellationToken cancellationToken)
{
var modifiers = fieldDecl.Modifiers;
diff --git a/AtemSharp.CodeGenerators/DiagnosticDescriptors.cs b/AtemSharp.CodeGenerators/DiagnosticDescriptors.cs
index 61bd3d2..4715c48 100644
--- a/AtemSharp.CodeGenerators/DiagnosticDescriptors.cs
+++ b/AtemSharp.CodeGenerators/DiagnosticDescriptors.cs
@@ -93,17 +93,6 @@ public static DiagnosticDescriptor CreateFieldTypeError(IFieldSymbol f)
isEnabledByDefault: true,
description: "Custom serialization methods named SerializeInternal must have a first parameter of type byte[] and optionally a second parameter of type ProtocolVersion and return void.");
-
- public static readonly DiagnosticDescriptor FieldCannotBeReadonly = new(
- "GEN008",
- "Field with Serialization Attribute Cannot Be Readonly",
- "Field '{0}' is marked with DeserializedFieldAttribute or SerializedFieldAttribute and cannot be readonly",
- "Usage",
- DiagnosticSeverity.Error,
- isEnabledByDefault: true,
- description: "Fields marked with DeserializedFieldAttribute or SerializedFieldAttribute must not be readonly."
- );
-
public static readonly DiagnosticDescriptor FieldCannotBePublic = new(
"GEN009",
"Field with Serialization Attribute Cannot Be Public",
diff --git a/AtemSharp.CodeGenerators/Serialization/SerializedCommandGenerator.cs b/AtemSharp.CodeGenerators/Serialization/SerializedCommandGenerator.cs
index 3696da2..d577e7a 100644
--- a/AtemSharp.CodeGenerators/Serialization/SerializedCommandGenerator.cs
+++ b/AtemSharp.CodeGenerators/Serialization/SerializedCommandGenerator.cs
@@ -125,18 +125,25 @@ private static string GetPropertyCode(IFieldSymbol f)
var validationCode = validationMethod is null ? string.Empty : $"{validationMethod}(value);";
var fieldType = Helpers.GetFieldType(f);
var docComment = Helpers.GetFieldMsDocComment(f);
+ var visibility = f.GetAttributes().Any(a => a.AttributeClass?.Name == "InternalPropertyAttribute") ? "internal" : "public";
+
+ var setter = f.IsReadOnly
+ ? string.Empty
+ : $$"""
+ set {
+ {{validationCode}}
+ {{f.Name}} = value;
+ {{flagCode}}
+ }
+ """;
return $$"""
{{docComment}}
- public {{fieldType}} {{propertyName}}
+ {{visibility}} {{fieldType}} {{propertyName}}
{
- [ExcludeFromCodeCoverage]
+ [ExcludeFromCodeCoverage]
get => {{f.Name}};
- set {
- {{validationCode}}
- {{f.Name}} = value;
- {{flagCode}}
- }
+ {{setter}}
}
""";
}
diff --git a/AtemSharp.CodeGenerators/State/StateGenerator.cs b/AtemSharp.CodeGenerators/State/StateGenerator.cs
index bd90723..59e58f4 100644
--- a/AtemSharp.CodeGenerators/State/StateGenerator.cs
+++ b/AtemSharp.CodeGenerators/State/StateGenerator.cs
@@ -20,6 +20,7 @@ protected override void ProcessClass(SourceProductionContext spc, INamedTypeSymb
.OfType()
.Where(f => f.AssociatedSymbol is null)
.Where(f => !f.GetAttributes().Any(a => a.AttributeClass?.Name is "IgnoreDataMember" or "IgnoreDataMemberAttribute"))
+ .Where(f => !f.Name.Contains("<"))
.Select(ProcessField)
.ToArray();
@@ -53,7 +54,7 @@ protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName
private ProcessedField ProcessField(IFieldSymbol field)
{
var propertyName = Helpers.GetPropertyName(field);
- var isReadOnly = field.GetAttributes().Any(a => a.AttributeClass?.Name is "ReadOnly" or "ReadOnlyAttribute");
+ var isReadOnly = field.IsReadOnly || field.GetAttributes().Any(a => a.AttributeClass?.Name is "ReadOnly" or "ReadOnlyAttribute");
var type = Helpers.GetFieldType(field);
var setterCode = isReadOnly
diff --git a/AtemSharp.Tests/AtemSwitcherTests.cs b/AtemSharp.Tests/AtemSwitcherTests.cs
index 77d3ce0..6c33196 100644
--- a/AtemSharp.Tests/AtemSwitcherTests.cs
+++ b/AtemSharp.Tests/AtemSwitcherTests.cs
@@ -304,7 +304,7 @@ public async Task ReceiveCommand_ShouldUpdateState()
var evt = new SemaphoreSlim(0);
- data.Atem.Macros.CurrentlyPlayingChanged += (_, _) => evt.Release();
+ data.Atem.Macros.Player.CurrentlyPlayingChanged += (_, _) => evt.Release();
data.Services.ClientFake.SimulateReceivedCommand(new MacroRunStatusUpdateCommand()
{
@@ -319,9 +319,9 @@ public async Task ReceiveCommand_ShouldUpdateState()
Assert.Multiple(() =>
{
- Assert.That(data.Atem.Macros.CurrentlyPlaying, Is.SameAs(data.Atem.Macros[2]));
- Assert.That(data.Atem.Macros.PlayLooped, Is.True);
- Assert.That(data.Atem.Macros.PlaybackIsWaiting, Is.False);
+ Assert.That(data.Atem.Macros.Player.CurrentlyPlaying, Is.SameAs(data.Atem.Macros[2]));
+ Assert.That(data.Atem.Macros.Player.PlayLooped, Is.True);
+ Assert.That(data.Atem.Macros.Player.PlaybackIsWaiting, Is.False);
});
}
diff --git a/AtemSharp.Tests/Commands/Macro/MacroPropertiesCommandTests.cs b/AtemSharp.Tests/Commands/Macro/MacroPropertiesCommandTests.cs
index 5d4a846..e6540dd 100644
--- a/AtemSharp.Tests/Commands/Macro/MacroPropertiesCommandTests.cs
+++ b/AtemSharp.Tests/Commands/Macro/MacroPropertiesCommandTests.cs
@@ -14,11 +14,14 @@ public class CommandData : CommandDataBase
protected override MacroPropertiesCommand CreateSut(TestUtilities.CommandTests.TestCaseData testCase)
{
- return new MacroPropertiesCommand(new AtemSharp.State.Macro.Macro(Substitute.For())
+ var macro = new AtemSharp.State.Macro.Macro(Substitute.For())
{
Id = testCase.Command.Index,
- Name = testCase.Command.Name,
- Description = testCase.Command.Description
- });
+ };
+
+ macro.UpdateName(testCase.Command.Name);
+ macro.UpdateDescription(testCase.Command.Description);
+
+ return new MacroPropertiesCommand(macro);
}
}
diff --git a/AtemSharp.Tests/Commands/Macro/MacroPropertiesUpdateCommandTests.cs b/AtemSharp.Tests/Commands/Macro/MacroPropertiesUpdateCommandTests.cs
index a1ef920..be4cac0 100644
--- a/AtemSharp.Tests/Commands/Macro/MacroPropertiesUpdateCommandTests.cs
+++ b/AtemSharp.Tests/Commands/Macro/MacroPropertiesUpdateCommandTests.cs
@@ -72,10 +72,10 @@ public void EmptyNameAndDescription()
var state = new TestStateHolder();
var macro = state.Macros.GetOrCreate(0);
macro.Id = 0;
- macro.IsUsed = true;
- macro.HasUnsupportedOps = true;
- macro.Name = "WrongName";
- macro.Description = "WrongDescription";
+ macro.UpdateIsUsed(true);
+ macro.UpdateHasUnsupportedOps(true);
+ macro.UpdateName("WrongName");
+ macro.UpdateDescription("WrongDescription");
command.Apply(state);
Assert.That(state.Macros.Count, Is.EqualTo(1));
diff --git a/AtemSharp.Tests/Commands/Macro/MacroRecordCommandTests.cs b/AtemSharp.Tests/Commands/Macro/MacroRecordCommandTests.cs
index 855b392..fc8c8dd 100644
--- a/AtemSharp.Tests/Commands/Macro/MacroRecordCommandTests.cs
+++ b/AtemSharp.Tests/Commands/Macro/MacroRecordCommandTests.cs
@@ -14,11 +14,14 @@ public class CommandData : CommandDataBase
protected override MacroRecordCommand CreateSut(TestUtilities.CommandTests.TestCaseData testCase)
{
- return new MacroRecordCommand(new AtemSharp.State.Macro.Macro(Substitute.For())
+ var macro = new AtemSharp.State.Macro.Macro(Substitute.For())
{
Id = testCase.Command.Index,
- Name = testCase.Command.Name,
- Description = testCase.Command.Description,
- });
+ };
+
+ macro.UpdateName(testCase.Command.Name);
+ macro.UpdateDescription(testCase.Command.Description);
+
+ return new MacroRecordCommand(macro);
}
}
diff --git a/AtemSharp.Tests/Commands/Macro/MacroRecordingStatusCommandTests.cs b/AtemSharp.Tests/Commands/Macro/MacroRecordingStatusCommandTests.cs
index 494e0e7..836ef6e 100644
--- a/AtemSharp.Tests/Commands/Macro/MacroRecordingStatusCommandTests.cs
+++ b/AtemSharp.Tests/Commands/Macro/MacroRecordingStatusCommandTests.cs
@@ -32,11 +32,11 @@ protected override void CompareStateProperties(IStateHolder state, CommandData e
{
if (expectedData.IsRecording)
{
- Assert.That(state.Macros.CurrentlyRecording, Is.SameAs(state.Macros[expectedData.Index]));
+ Assert.That(state.Macros.Recorder.CurrentlyRecording, Is.SameAs(state.Macros[expectedData.Index]));
}
else
{
- Assert.That(state.Macros.CurrentlyRecording, Is.Null);
+ Assert.That(state.Macros.Recorder.CurrentlyRecording, Is.Null);
}
}
}
diff --git a/AtemSharp.Tests/Commands/Macro/MacroRunStatusUpdateCommandTests.cs b/AtemSharp.Tests/Commands/Macro/MacroRunStatusUpdateCommandTests.cs
index 49fae8f..7cd592e 100644
--- a/AtemSharp.Tests/Commands/Macro/MacroRunStatusUpdateCommandTests.cs
+++ b/AtemSharp.Tests/Commands/Macro/MacroRunStatusUpdateCommandTests.cs
@@ -36,14 +36,14 @@ protected override void CompareStateProperties(IStateHolder state, CommandData e
{
if (expectedData.IsRunning)
{
- Assert.That(state.Macros.CurrentlyPlaying, Is.SameAs(state.Macros[expectedData.Index]));
+ Assert.That(state.Macros.Player.CurrentlyPlaying, Is.SameAs(state.Macros[expectedData.Index]));
}
else
{
- Assert.That(state.Macros.CurrentlyPlaying, Is.Null);
+ Assert.That(state.Macros.Player.CurrentlyPlaying, Is.Null);
}
- Assert.That(state.Macros.PlayLooped, Is.EqualTo(expectedData.Loop));
- Assert.That(state.Macros.PlaybackIsWaiting, Is.EqualTo(expectedData.IsWaiting));
+ Assert.That(state.Macros.Player.PlayLooped, Is.EqualTo(expectedData.Loop));
+ Assert.That(state.Macros.Player.PlaybackIsWaiting, Is.EqualTo(expectedData.IsWaiting));
}
}
diff --git a/AtemSharp.Tests/State/MacroSystemTests.cs b/AtemSharp.Tests/State/MacroSystemTests.cs
index f06f2a6..fd34bfb 100644
--- a/AtemSharp.Tests/State/MacroSystemTests.cs
+++ b/AtemSharp.Tests/State/MacroSystemTests.cs
@@ -1,6 +1,8 @@
using AtemSharp.Commands.Macro;
+using AtemSharp.State;
using AtemSharp.State.Macro;
-using NSubstitute;
+using AtemSharp.State.Settings;
+using AtemSharp.Tests.TestUtilities;
namespace AtemSharp.Tests.State;
@@ -12,14 +14,16 @@ public class MacroSystemTests
[TestCase(false)]
public void ChangeIsLooping(bool newValue)
{
- var switcher = Substitute.For();
+ var switcher = new AtemSwitcherFake();
var sut = new MacroSystem(switcher);
sut.Populate(5);
- sut.UpdatePlayLooped(!newValue);
- sut.PlayLooped = newValue;
+ sut.Player.UpdatePlayLooped(!newValue);
+ sut.Player.PlayLooped = newValue;
- switcher.Received(1).SendCommandAsync(Arg.Is(x => x.Loop == newValue));
+ Assert.That(switcher.SentCommands, Has.Count.EqualTo(1));
+ var command = switcher.SentCommands[0].As();
+ Assert.That(command.Loop, Is.EqualTo(newValue));
}
[Test]
@@ -27,13 +31,218 @@ public void ChangeIsLooping(bool newValue)
[TestCase(false)]
public void NoChangeIsLooping(bool newValue)
{
- var switcher = Substitute.For();
+ var switcher = new AtemSwitcherFake();
var sut = new MacroSystem(switcher);
sut.Populate(5);
- sut.UpdatePlayLooped(newValue);
- sut.PlayLooped = newValue;
+ sut.Player.UpdatePlayLooped(newValue);
+ sut.Player.PlayLooped = newValue;
- switcher.DidNotReceiveWithAnyArgs().SendCommandAsync(null!);
+ Assert.That(switcher.SentCommands, Has.Count.EqualTo(0));
+ }
+
+ [Test]
+ public async Task RunMacro()
+ {
+ var switcher = new AtemSwitcherFake();
+
+ var sut = new MacroSystem(switcher);
+ sut.Populate(5);
+ sut[2].UpdateIsUsed(true);
+ await sut[2].Run().WithTimeout();
+
+ Assert.That(switcher.SentCommands, Has.Count.EqualTo(1));
+
+ Assert.That(switcher.SentCommands, Has.Count.EqualTo(1));
+ var command = switcher.SentCommands[0].As();
+ Assert.Multiple(() =>
+ {
+ Assert.That(command.Index, Is.EqualTo(2));
+ Assert.That(command.Action, Is.EqualTo(MacroAction.Run));
+ });
+ }
+
+ [Test]
+ public void RunUnusedMacro()
+ {
+ var switcher = new AtemSwitcherFake();
+
+ var sut = new MacroSystem(switcher);
+ sut.Populate(5);
+ sut[2].UpdateIsUsed(false);
+
+ Assert.ThrowsAsync(() => sut[2].Run().WithTimeout());
+ Assert.That(switcher.SentCommands, Has.Count.EqualTo(0));
+ }
+
+ [Test]
+ public async Task StopMacro()
+ {
+ var switcher = new AtemSwitcherFake();
+ var sut = new MacroSystem(switcher);
+ sut.Populate(5);
+
+ sut.Player.UpdateCurrentlyPlaying(sut[2]);
+
+ await sut.Player.StopPlayback().WithTimeout();
+
+ Assert.That(switcher.SentCommands, Has.Count.EqualTo(1));
+ var command = switcher.SentCommands[0].As();
+ Assert.Multiple(() =>
+ {
+ Assert.That(command.Index, Is.EqualTo(2));
+ Assert.That(command.Action, Is.EqualTo(MacroAction.Stop));
+ });
+ }
+
+ [Test]
+ public async Task StopNonRunningMacro()
+ {
+ var switcher = new AtemSwitcherFake();
+ var sut = new MacroSystem(switcher);
+ sut.Populate(5);
+
+ sut.Player.UpdateCurrentlyPlaying(null);
+
+ await sut.Player.StopPlayback().WithTimeout();
+
+ Assert.That(switcher.SentCommands, Has.Count.EqualTo(0));
+ }
+
+ [Test]
+ public async Task StopMacroRecord()
+ {
+ var switcher = new AtemSwitcherFake();
+ var sut = new MacroSystem(switcher);
+ sut.Populate(5);
+
+ sut.Recorder.UpdateCurrentlyRecording(sut[2]);
+
+ await sut.Recorder.StopRecording().WithTimeout();
+
+ Assert.That(switcher.SentCommands, Has.Count.EqualTo(1));
+ var command = switcher.SentCommands[0].As();
+ Assert.Multiple(() =>
+ {
+ Assert.That(command.Index, Is.EqualTo(2));
+ Assert.That(command.Action, Is.EqualTo(MacroAction.StopRecord));
+ });
+ }
+
+ [Test]
+ public async Task StopNonRecordingMacro()
+ {
+ var switcher = new AtemSwitcherFake();
+ var sut = new MacroSystem(switcher);
+ sut.Populate(5);
+
+ sut.Recorder.UpdateCurrentlyRecording(null);
+
+ await sut.Recorder.StopRecording().WithTimeout();
+
+ Assert.That(switcher.SentCommands, Has.Count.EqualTo(0));
+ }
+
+ [Test]
+ public async Task AddPauseInFrames()
+ {
+ var switcher = new AtemSwitcherFake();
+ var sut = new MacroSystem(switcher);
+ sut.Populate(5);
+ sut.Recorder.UpdateCurrentlyRecording(sut[2]);
+
+ await sut.Recorder.AddPause(12).WithTimeout();
+
+ Assert.That(switcher.SentCommands, Has.Count.EqualTo(1));
+ var command = switcher.SentCommands[0].As();
+ Assert.That(command.Frames, Is.EqualTo(12));
+ }
+
+ [Test]
+ public void AddPauseWhileNotRecording()
+ {
+ var switcher = new AtemSwitcherFake();
+ var sut = new MacroSystem(switcher);
+ sut.Populate(5);
+ sut.Recorder.UpdateCurrentlyRecording(null);
+
+ var ex = Assert.ThrowsAsync(() => sut.Recorder.AddPause(12).WithTimeout());
+ Assert.Multiple(() =>
+ {
+ Assert.That(ex.Message, Does.Contain("Can only add pause while recording"));
+ Assert.That(switcher.SentCommands, Has.Count.EqualTo(0));
+ });
+ }
+
+ [Test]
+ [TestCase(1, (ushort)24)]
+ [TestCase(0.5, (ushort)12)]
+ public async Task AddPauseInTimeSpan(double durationInSeconds, ushort expectedFrames)
+ {
+ var switcher = new AtemSwitcherFake();
+ switcher.State = new AtemState
+ {
+ Settings =
+ {
+ VideoMode = VideoMode.N4KHDp24
+ }
+ };
+
+ var sut = new MacroSystem(switcher);
+ sut.Populate(5);
+ sut.Recorder.UpdateCurrentlyRecording(sut[2]);
+
+ await sut.Recorder.AddPause(TimeSpan.FromSeconds(durationInSeconds)).WithTimeout();
+
+ Assert.That(switcher.SentCommands, Has.Count.EqualTo(1));
+ var command = switcher.SentCommands[0].As();
+ Assert.That(command.Frames, Is.EqualTo(expectedFrames));
+ }
+
+ [Test]
+ public void ChangeMacroName()
+ {
+ var switcher = new AtemSwitcherFake();
+ var sut = new MacroSystem(switcher);
+ sut.Populate(5);
+
+ sut[2].Name = "New Name";
+
+ Assert.That(switcher.SentCommands, Has.Count.EqualTo(1));
+ var command = switcher.SentCommands[0].As();
+ Assert.That(command.Name, Is.EqualTo("New Name"));
+ }
+
+ [Test]
+ public void ChangeMacroDescription()
+ {
+ var switcher = new AtemSwitcherFake();
+ var sut = new MacroSystem(switcher);
+ sut.Populate(5);
+
+ sut[2].Description = "New Description";
+
+ Assert.That(switcher.SentCommands, Has.Count.EqualTo(1));
+ var command = switcher.SentCommands[0].As();
+ Assert.That(command.Description, Is.EqualTo("New Description"));
+ }
+
+ [Test]
+ public async Task RecordMacro()
+ {
+ var switcher = new AtemSwitcherFake();
+ var sut = new MacroSystem(switcher);
+ sut.Populate(5);
+
+ await sut[2].Record("My Macro", "My Description").WithTimeout();
+
+ Assert.That(switcher.SentCommands, Has.Count.EqualTo(1));
+ var command = switcher.SentCommands[0].As();
+ Assert.Multiple(() =>
+ {
+ Assert.That(command.Index, Is.EqualTo(2));
+ Assert.That(command.Name, Is.EqualTo("My Macro"));
+ Assert.That(command.Description, Is.EqualTo("My Description"));
+ });
}
}
diff --git a/AtemSharp.Tests/TestUtilities/AtemSwitcherFake.cs b/AtemSharp.Tests/TestUtilities/AtemSwitcherFake.cs
new file mode 100644
index 0000000..ead965d
--- /dev/null
+++ b/AtemSharp.Tests/TestUtilities/AtemSwitcherFake.cs
@@ -0,0 +1,43 @@
+using AtemSharp.Commands;
+using AtemSharp.State;
+using AtemSharp.State.Macro;
+
+namespace AtemSharp.Tests.TestUtilities;
+
+public class AtemSwitcherFake : IAtemSwitcher
+{
+ public readonly List SentCommands = [];
+
+ public ValueTask DisposeAsync()
+ {
+ return ValueTask.CompletedTask;
+ }
+
+ public AtemState State { get; internal set; } = null!;
+
+ public MacroSystem Macros { get; internal set; } = null!;
+
+ public ConnectionState ConnectionState { get; internal set; } = ConnectionState.Connected;
+
+ public Task ConnectAsync(CancellationToken cancellationToken = default)
+ {
+ return Task.CompletedTask;
+ }
+
+ public Task DisconnectAsync()
+ {
+ return Task.CompletedTask;
+ }
+
+ public Task SendCommandAsync(SerializedCommand command)
+ {
+ SendCommandsAsync([command]);
+ return Task.CompletedTask;
+ }
+
+ public Task SendCommandsAsync(IEnumerable commands)
+ {
+ SentCommands.AddRange(commands);
+ return Task.CompletedTask;
+ }
+}
diff --git a/AtemSharp/Attributes/NoPropertyAttribute.cs b/AtemSharp/Attributes/NoPropertyAttribute.cs
index f26450c..f636509 100644
--- a/AtemSharp/Attributes/NoPropertyAttribute.cs
+++ b/AtemSharp/Attributes/NoPropertyAttribute.cs
@@ -14,3 +14,10 @@ internal class NoPropertyAttribute : Attribute
{
}
+
+[AttributeUsage(AttributeTargets.Field)]
+[ExcludeFromCodeCoverage]
+internal class InternalPropertyAttribute : Attribute
+{
+
+}
diff --git a/AtemSharp/Commands/Macro/MacroActionCommand.cs b/AtemSharp/Commands/Macro/MacroActionCommand.cs
index 8385f63..aa62872 100644
--- a/AtemSharp/Commands/Macro/MacroActionCommand.cs
+++ b/AtemSharp/Commands/Macro/MacroActionCommand.cs
@@ -1,3 +1,5 @@
+using System.Diagnostics.CodeAnalysis;
+
namespace AtemSharp.Commands.Macro;
@@ -9,9 +11,9 @@ namespace AtemSharp.Commands.Macro;
[BufferSize(4)]
public partial class MacroActionCommand(State.Macro.Macro macro, MacroAction action) : SerializedCommand
{
- [SerializedField(0)] [NoProperty] private readonly ushort _index = macro.Id;
+ [SerializedField(0)] [InternalProperty] private readonly ushort _index = macro.Id;
- [SerializedField(2)] [NoProperty] private readonly MacroAction _action = action;
+ [SerializedField(2)] [InternalProperty] private readonly MacroAction _action = action;
private void SerializeInternal(byte[] buffer)
{
diff --git a/AtemSharp/Commands/Macro/MacroPropertiesUpdateCommand.cs b/AtemSharp/Commands/Macro/MacroPropertiesUpdateCommand.cs
index 0be6776..8c38aab 100644
--- a/AtemSharp/Commands/Macro/MacroPropertiesUpdateCommand.cs
+++ b/AtemSharp/Commands/Macro/MacroPropertiesUpdateCommand.cs
@@ -37,9 +37,9 @@ public void ApplyToState(AtemState state)
public void Apply(IStateHolder state)
{
var macro = state.Macros[Id];
- macro.Name = Name;
- macro.Description = Description;
- macro.IsUsed = IsUsed;
- macro.HasUnsupportedOps = HasUnsupportedOps;
+ macro.UpdateName(Name);
+ macro.UpdateDescription(_description);
+ macro.UpdateIsUsed(IsUsed);
+ macro.UpdateHasUnsupportedOps(HasUnsupportedOps);
}
}
diff --git a/AtemSharp/Commands/Macro/MacroRecordCommand.cs b/AtemSharp/Commands/Macro/MacroRecordCommand.cs
index e7f78db..9485d51 100644
--- a/AtemSharp/Commands/Macro/MacroRecordCommand.cs
+++ b/AtemSharp/Commands/Macro/MacroRecordCommand.cs
@@ -9,7 +9,7 @@ namespace AtemSharp.Commands.Macro;
[Command("MSRc")]
public class MacroRecordCommand(AtemSharp.State.Macro.Macro targetSlot) : SerializedCommand
{
- private readonly ushort _index = targetSlot.Id;
+ internal readonly ushort Index = targetSlot.Id;
public string Name { get; set; } = targetSlot.Name;
public string Description { get; set; } = targetSlot.Description;
@@ -18,7 +18,7 @@ public class MacroRecordCommand(AtemSharp.State.Macro.Macro targetSlot) : Serial
public override byte[] Serialize(ProtocolVersion version)
{
var buffer = new byte[SerializationExtensions.PadToMultiple(8 + Name.Length + Description.Length, 4)];
- buffer.WriteUInt16BigEndian(_index, 0);
+ buffer.WriteUInt16BigEndian(Index, 0);
buffer.WriteUInt16BigEndian((ushort)Name.Length, 2);
buffer.WriteUInt16BigEndian((ushort)Description.Length, 4);
buffer.WriteString(Name, 6);
diff --git a/AtemSharp/Commands/Macro/MacroRecordingStatusCommand.cs b/AtemSharp/Commands/Macro/MacroRecordingStatusCommand.cs
index 25279af..877a42f 100644
--- a/AtemSharp/Commands/Macro/MacroRecordingStatusCommand.cs
+++ b/AtemSharp/Commands/Macro/MacroRecordingStatusCommand.cs
@@ -19,6 +19,6 @@ public void ApplyToState(AtemState state)
///
public void Apply(IStateHolder state)
{
- state.Macros.UpdateCurrentlyRecording(IsRecording ? state.Macros[_macroIndex] : null);
+ state.Macros.Recorder.UpdateCurrentlyRecording(IsRecording ? state.Macros[_macroIndex] : null);
}
}
diff --git a/AtemSharp/Commands/Macro/MacroRunStatusUpdateCommand.cs b/AtemSharp/Commands/Macro/MacroRunStatusUpdateCommand.cs
index 94739fc..c03958b 100644
--- a/AtemSharp/Commands/Macro/MacroRunStatusUpdateCommand.cs
+++ b/AtemSharp/Commands/Macro/MacroRunStatusUpdateCommand.cs
@@ -31,9 +31,9 @@ private void DeserializeInternal(ReadOnlySpan rawCommand)
public void Apply(IStateHolder switcher)
{
var macroSystem = switcher.Macros;
- macroSystem.UpdatePlayLooped(Loop);
- macroSystem.UpdateCurrentlyPlaying(_isRunning ? macroSystem[_macroIndex] : null);
- macroSystem.UpdatePlaybackIsWaiting(IsWaiting);
+ macroSystem.Player.UpdatePlayLooped(Loop);
+ macroSystem.Player.UpdateCurrentlyPlaying(_isRunning ? macroSystem[_macroIndex] : null);
+ macroSystem.Player.UpdatePlaybackIsWaiting(IsWaiting);
}
[ExcludeFromCodeCoverage(Justification = "Obsolete")]
diff --git a/AtemSharp/Extensions/VideoModeExtensions.cs b/AtemSharp/Extensions/VideoModeExtensions.cs
new file mode 100644
index 0000000..d5795d5
--- /dev/null
+++ b/AtemSharp/Extensions/VideoModeExtensions.cs
@@ -0,0 +1,42 @@
+using AtemSharp.State.Settings;
+
+namespace AtemSharp.Extensions;
+
+public static class VideoModeExtensions
+{
+ public static ushort FramesPerSecond(this VideoMode videoMode)
+ {
+ return videoMode switch
+ {
+ VideoMode.N525i5994NTSC => 60,
+ VideoMode.P625i50PAL => 50,
+ VideoMode.N525i5994169 => 60,
+ VideoMode.P625i50169 => 50,
+ VideoMode.P720p50 => 50,
+ VideoMode.N720p5994 => 60,
+ VideoMode.P1080i50 => 50,
+ VideoMode.N1080i5994 => 60,
+ VideoMode.N1080p2398 => 24,
+ VideoMode.N1080p24 => 24,
+ VideoMode.P1080p25 => 25,
+ VideoMode.N1080p2997 => 30,
+ VideoMode.P1080p50 => 50,
+ VideoMode.N1080p5994 => 60,
+ VideoMode.N4KHDp2398 => 24,
+ VideoMode.N4KHDp24 => 24,
+ VideoMode.P4KHDp25 => 25,
+ VideoMode.N4KHDp2997 => 30,
+ VideoMode.P4KHDp5000 => 50,
+ VideoMode.N4KHDp5994 => 60,
+ VideoMode.N8KHDp2398 => 24,
+ VideoMode.N8KHDp24 => 24,
+ VideoMode.P8KHDp25 => 25,
+ VideoMode.N8KHDp2997 => 30,
+ VideoMode.P8KHDp50 => 50,
+ VideoMode.N8KHDp5994 => 60,
+ VideoMode.N1080p30 => 30,
+ VideoMode.N1080p60 => 60,
+ _ => throw new ArgumentOutOfRangeException()
+ };
+ }
+}
diff --git a/AtemSharp/State/Macro/Macro.cs b/AtemSharp/State/Macro/Macro.cs
index 7e1d3cc..ffab090 100644
--- a/AtemSharp/State/Macro/Macro.cs
+++ b/AtemSharp/State/Macro/Macro.cs
@@ -1,24 +1,47 @@
+using System.ComponentModel;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using AtemSharp.Commands.Macro;
+using AtemSharp.Lib;
namespace AtemSharp.State.Macro;
[ExcludeFromCodeCoverage(Justification="Auto-Properties aren't tested")]
[DebuggerDisplay("{" + nameof(ToString) + ",nq}")]
-public class Macro(IAtemSwitcher switcher)
+public partial class Macro(IAtemSwitcher switcher)
{
public ushort Id { get; internal set; }
- public string Name { get; internal set; } = string.Empty;
- public string Description { get; internal set; } = string.Empty;
- public bool IsUsed { get; internal set; }
- public bool HasUnsupportedOps { get; internal set; }
+
+ private string _name = string.Empty;
+ private string _description = string.Empty;
+ [ReadOnly(true)] private bool _isUsed;
+ [ReadOnly(true)] private bool _hasUnsupportedOps;
[ExcludeFromCodeCoverage]
- public override string ToString() => $"{GetType().Name} #{Id}";
+ public override string ToString() => $"{GetType().Name} #{Id} ({Name})";
public async Task Run()
{
+ if (!IsUsed)
+ {
+ throw new InvalidOperationException("Cannot run macros that are unused");
+ }
+
await switcher.SendCommandAsync(new MacroActionCommand(this, MacroAction.Run));
}
+
+ public async Task Record(string name, string description)
+ {
+ await switcher.SendCommandAsync(new MacroRecordCommand(this) { Name = name, Description = description });
+ }
+
+ private partial void SendNameUpdateCommand(string value)
+ {
+ switcher.SendCommandAsync(new MacroPropertiesCommand(this) { Name = value }).FireAndForget();
+ }
+
+ private partial void SendDescriptionUpdateCommand(string value)
+ {
+ switcher.SendCommandAsync(new MacroPropertiesCommand(this) { Description = value }).FireAndForget();
+ }
}
diff --git a/AtemSharp/State/Macro/MacroPlayer.cs b/AtemSharp/State/Macro/MacroPlayer.cs
index 5c82ca1..dcaf734 100644
--- a/AtemSharp/State/Macro/MacroPlayer.cs
+++ b/AtemSharp/State/Macro/MacroPlayer.cs
@@ -1,12 +1,31 @@
-using System.Diagnostics.CodeAnalysis;
+using System.ComponentModel;
+using AtemSharp.Commands.Macro;
+using AtemSharp.Lib;
namespace AtemSharp.State.Macro;
-[ExcludeFromCodeCoverage(Justification="Auto-Properties aren't tested")]
-public class MacroPlayer
+public partial class MacroPlayer(IAtemSwitcher switcher)
{
- public ushort MacroIndex { get; internal set; }
- public bool IsRunning { get; internal set; }
- public bool IsLooping { get; internal set; }
- public bool IsWaiting { get; internal set; }
+ private bool _playLooped;
+
+ [ReadOnly(true)] private bool _playbackIsWaiting;
+ [ReadOnly(true)] private Macro? _currentlyPlaying;
+
+
+ private partial void SendPlayLoopedUpdateCommand(bool value)
+ {
+ switcher.SendCommandAsync(new MacroRunStatusCommand { Loop = value }).FireAndForget();
+ }
+
+ public async Task StopPlayback()
+ {
+ var macroToStop = _currentlyPlaying;
+
+ if (macroToStop is null)
+ {
+ return;
+ }
+
+ await switcher.SendCommandAsync(new MacroActionCommand(macroToStop, MacroAction.Stop));
+ }
}
diff --git a/AtemSharp/State/Macro/MacroRecorder.cs b/AtemSharp/State/Macro/MacroRecorder.cs
index c2fd148..ccc3802 100644
--- a/AtemSharp/State/Macro/MacroRecorder.cs
+++ b/AtemSharp/State/Macro/MacroRecorder.cs
@@ -1,10 +1,37 @@
-using System.Diagnostics.CodeAnalysis;
+using System.ComponentModel;
+using AtemSharp.Commands.Macro;
+using AtemSharp.Extensions;
namespace AtemSharp.State.Macro;
-[ExcludeFromCodeCoverage(Justification="Auto-Properties aren't tested")]
-public class MacroRecorder
+public partial class MacroRecorder(IAtemSwitcher switcher)
{
- public bool IsRecording { get; internal set; }
- public ushort MacroIndex { get; internal set; }
+ [ReadOnly(true)] private Macro? _currentlyRecording;
+
+ public async Task StopRecording()
+ {
+ var macroToStop = _currentlyRecording;
+
+ if (macroToStop is null)
+ {
+ return;
+ }
+
+ await switcher.SendCommandAsync(new MacroActionCommand(macroToStop, MacroAction.StopRecord));
+ }
+
+ public async Task AddPause(ushort frameCount)
+ {
+ if (_currentlyRecording is null)
+ {
+ throw new InvalidOperationException("Can only add pause while recording.");
+ }
+
+ await switcher.SendCommandAsync(new MacroAddTimedPauseCommand { Frames = frameCount });
+ }
+
+ public async Task AddPause(TimeSpan duration)
+ {
+ await AddPause((ushort)(duration.TotalSeconds * switcher.State.Settings.VideoMode.FramesPerSecond()));
+ }
}
diff --git a/AtemSharp/State/Macro/MacroSystem.cs b/AtemSharp/State/Macro/MacroSystem.cs
index 5dddfb6..b8a9dab 100644
--- a/AtemSharp/State/Macro/MacroSystem.cs
+++ b/AtemSharp/State/Macro/MacroSystem.cs
@@ -1,34 +1,16 @@
-using System.ComponentModel;
-using System.Runtime.Serialization;
-using AtemSharp.Commands.Macro;
-using AtemSharp.Lib;
using AtemSharp.Types;
namespace AtemSharp.State.Macro;
public partial class MacroSystem : ItemCollection
{
- [IgnoreDataMember]
- private readonly IAtemSwitcher _switcher;
-
internal MacroSystem(IAtemSwitcher switcher) : base(id => new Macro(switcher) { Id = id })
{
- _switcher = switcher;
+ Player = new MacroPlayer(switcher);
+ Recorder = new MacroRecorder(switcher);
}
- private bool _playLooped;
-
- [ReadOnly(true)] private bool _playbackIsWaiting;
-
- [ReadOnly(true)]
- private Macro? _currentlyPlaying;
-
- [ReadOnly(true)]
- private Macro? _currentlyRecording;
+ public MacroPlayer Player { get; }
- private partial void SendPlayLoopedUpdateCommand(bool value)
- {
- _switcher.SendCommandAsync(new MacroRunStatusCommand { Loop = value }).FireAndForget();
- }
+ public MacroRecorder Recorder { get; }
}
-