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; } } -