feat: align legacy operator and playout behavior

This commit is contained in:
2026-07-15 21:04:05 +09:00
parent 06a16e5741
commit 83398044c6
77 changed files with 6377 additions and 391 deletions

View File

@@ -325,7 +325,7 @@ public sealed class DynamicK3dSessionTests
"GetScenePlayerOnChannel:9",
"LoadScene:test-scene",
"SetOutputChannelIndex:9",
"SetSceneEffectType:1:7:12",
"SetSceneEffectType:10:7:12",
"BeginTransaction",
"GetObject:headline",
"SetValue:headline:safe value",
@@ -360,12 +360,12 @@ public sealed class DynamicK3dSessionTests
[
new PlayoutUseBackground(true),
new PlayoutSetBackgroundTexture("Video/background.vrv"),
new PlayoutSetBackgroundVideo("Video/background.vrv", 2, true),
new PlayoutSetBackgroundVideo("Video/background.vrv", 2, 10),
new PlayoutSetValue("headline", "value"),
new PlayoutSetBackgroundVideo(
"Video/background.vrv",
3,
true,
1,
PlayoutMutationTiming.InTransaction),
new PlayoutSetAssetValue("badge", "Images/badge.png"),
new PlayoutSetVisible("badge", false),
@@ -406,10 +406,10 @@ public sealed class DynamicK3dSessionTests
{
"LoadScene:test-scene",
"SetOutputChannelIndex:9",
"SetSceneEffectType:1:7:4",
"SetSceneEffectType:10:7:4",
"UseBackground:1",
"SetBackgroundTexture:background.vrv",
"SetBackgroundVideo:background.vrv:2:1",
"SetBackgroundVideo:background.vrv:2:10",
"BeginTransaction",
"GetObject:headline",
"SetValue:headline:value",
@@ -469,16 +469,16 @@ public sealed class DynamicK3dSessionTests
"test-scene.t2s",
"test-scene",
[new PlayoutField("headline", "page two", true)],
FadeDuration: 59,
FadeDuration: 19,
Mutations:
[
new PlayoutUseBackground(false),
new PlayoutSetBackgroundTexture("Video/background.vrv"),
new PlayoutSetBackgroundVideo("Video/background.vrv", 4, true),
new PlayoutSetBackgroundVideo("Video/background.vrv", 4, 10),
new PlayoutSetBackgroundVideo(
"Video/background.vrv",
8,
true,
1,
PlayoutMutationTiming.InTransaction),
new PlayoutSetVisible("badge", false)
]));
@@ -516,7 +516,7 @@ public sealed class DynamicK3dSessionTests
{
"UseBackground:0",
"SetBackgroundTexture:background.vrv",
"SetBackgroundVideo:background.vrv:4:1",
"SetBackgroundVideo:background.vrv:4:10",
"BeginTransaction",
"GetObject:headline",
"SetValue:headline:page two",
@@ -774,6 +774,7 @@ public sealed class DynamicK3dSessionTests
scene.QueryVariablesFailure = null;
session.TakeOut(options.LayoutIndex, PlayoutTakeOutScope.All);
Assert.True(session.HasPendingTakeOutCallbacks);
callbacks.Fire("OnStopAll", 1);
Assert.Equal(
1,
@@ -847,6 +848,7 @@ public sealed class DynamicK3dSessionTests
1,
session.ProcessPendingCallbacks(options.LayoutIndex).UnloadedSceneCount);
Assert.False(session.HasPendingLifecycleCallbacks);
Assert.False(session.HasPendingTakeOutCallbacks);
session.Disconnect();
return true;
},
@@ -1300,7 +1302,7 @@ public sealed class DynamicK3dSessionTests
"KTAPConnect:1:127.0.0.1:30001:0",
"GetScenePlayer",
"LoadScene:test-scene",
"SetSceneEffectType:1:7:12",
"SetSceneEffectType:10:7:12",
"BeginTransaction",
"QueryVariables",
"EndTransaction",

View File

@@ -8,7 +8,9 @@ public sealed class LegacyParityStartupCompositionResolverTests
[Fact]
public void AbsentLocalConfiguration_UsesValidatedLegacyDefaultAtConfiguredFade()
{
using var scenes = TemporarySceneDirectory.Create("5001.t2s");
using var scenes = TemporarySceneDirectory.Create(
"5001.t2s",
PlayoutSceneCompositionFactory.LegacyStartupDefaultBackgroundAssetPath);
CreateBackground(scenes, PlayoutSceneCompositionFactory.LegacyDefaultBackgroundFileName);
var options = new PlayoutOptions
{
@@ -23,9 +25,9 @@ public sealed class LegacyParityStartupCompositionResolverTests
Assert.False(result.HasWarning);
Assert.Equal(11, result.Composition.FadeDuration);
Assert.Equal(LegacySceneBackgroundKind.Video, result.Composition.BackgroundKind);
Assert.Equal("기본.vrv", result.Composition.BackgroundAssetPath);
Assert.Equal(@"video\기본.vrv", result.Composition.BackgroundAssetPath);
Assert.Equal(
PlayoutAssetRoot.OperatorBackgroundDirectory,
PlayoutAssetRoot.SceneDirectory,
result.Composition.BackgroundAssetRoot);
}
@@ -33,6 +35,7 @@ public sealed class LegacyParityStartupCompositionResolverTests
public void AbsentLocalConfigurationAndMissingDefault_RemainsNoneWithBoundedWarning()
{
using var scenes = TemporarySceneDirectory.Create("5001.t2s");
CreateBackground(scenes, PlayoutSceneCompositionFactory.LegacyDefaultBackgroundFileName);
var options = new PlayoutOptions
{
SceneDirectory = scenes.Path,
@@ -57,8 +60,9 @@ public sealed class LegacyParityStartupCompositionResolverTests
[Fact]
public void ExplicitLocalNone_IsHonoredEvenWhenLegacyDefaultExists()
{
using var scenes = TemporarySceneDirectory.Create("5001.t2s");
CreateBackground(scenes, PlayoutSceneCompositionFactory.LegacyDefaultBackgroundFileName);
using var scenes = TemporarySceneDirectory.Create(
"5001.t2s",
PlayoutSceneCompositionFactory.LegacyStartupDefaultBackgroundAssetPath);
var options = new PlayoutOptions
{
SceneDirectory = scenes.Path,

View File

@@ -61,6 +61,8 @@ internal sealed class FakeK3dSession : IK3dGuardedConnectSession
public bool HasPendingPlayCallbacks { get; set; }
public bool HasPendingTakeOutCallbacks { get; set; }
public bool ClearPendingLifecycleCallbacksAfterProcess { get; set; } = true;
public ConcurrentQueue<string> Calls { get; } = new();
@@ -196,6 +198,7 @@ internal sealed class FakeK3dSession : IK3dGuardedConnectSession
{
HasPendingLifecycleCallbacks = false;
HasPendingPlayCallbacks = false;
HasPendingTakeOutCallbacks = false;
}
return result;

View File

@@ -384,7 +384,7 @@ public sealed class PlayoutSafetyValidationTests : IDisposable
new PlayoutSetBackgroundVideo(
"studio.vrv",
LoopCount: 2004,
LoopInfinite: true,
LoopInfinite: 10,
AssetRoot: PlayoutAssetRoot.OperatorBackgroundDirectory)
]));
@@ -412,7 +412,7 @@ public sealed class PlayoutSafetyValidationTests : IDisposable
new PlayoutSetBackgroundVideo(
"Video\\background.vrv",
LoopCount: 2004,
LoopInfinite: true,
LoopInfinite: 10,
AssetRoot: PlayoutAssetRoot.OperatorBackgroundDirectory)
])));
}
@@ -436,7 +436,7 @@ public sealed class PlayoutSafetyValidationTests : IDisposable
new PlayoutSetBackgroundVideo(
"studio.vrv",
LoopCount: 2004,
LoopInfinite: true,
LoopInfinite: 10,
AssetRoot: PlayoutAssetRoot.OperatorBackgroundDirectory)
]));
@@ -460,7 +460,7 @@ public sealed class PlayoutSafetyValidationTests : IDisposable
new PlayoutSetBackgroundVideo(
"Video/background.vrv",
LoopCount: 2004,
LoopInfinite: true)
LoopInfinite: 10)
]));
var video = Assert.IsType<PlayoutSetBackgroundVideo>(Assert.Single(resolved.Mutations!));
@@ -471,6 +471,27 @@ public sealed class PlayoutSafetyValidationTests : IDisposable
ignoreCase: true);
}
[Theory]
[InlineData(-1)]
[InlineData(2)]
[InlineData(9)]
[InlineData(11)]
public void ResolveCue_RejectsBackgroundVideoLoopModesNotUsedByMainForm(int loopMode)
{
var validated = ValidatedPlayoutOptions.Create(ValidTestOptions());
Assert.Throws<PlayoutRequestException>(() => validated.ResolveCue(new PlayoutCue(
"test-scene.t2s",
"test-scene",
Mutations:
[
new PlayoutSetBackgroundVideo(
"Video/background.vrv",
LoopCount: 2004,
LoopInfinite: loopMode)
])));
}
[Theory]
[InlineData("../outside.vrv")]
[InlineData("Video/missing.vrv")]

View File

@@ -15,6 +15,33 @@ public sealed class PlayoutSceneCompositionFactoryTests
Assert.Null(result.BackgroundAssetPath);
}
[Theory]
[InlineData(0)]
[InlineData(6)]
[InlineData(19)]
public void Fade_AcceptsOriginalDissolveComboIndexRange(int fadeDuration)
{
var result = PlayoutSceneCompositionFactory.Create(new PlayoutOptions
{
LegacySceneFadeDuration = fadeDuration
});
Assert.Equal(fadeDuration, result.FadeDuration);
}
[Theory]
[InlineData(-1)]
[InlineData(20)]
[InlineData(60)]
public void Fade_RejectsValuesOutsideOriginalDissolveComboIndexRange(int fadeDuration)
{
Assert.Throws<PlayoutConfigurationException>(() =>
PlayoutSceneCompositionFactory.Create(new PlayoutOptions
{
LegacySceneFadeDuration = fadeDuration
}));
}
[Fact]
public void TrustedRelativeVideo_IsVerifiedEvenInDryRun()
{
@@ -92,4 +119,44 @@ public sealed class PlayoutSceneCompositionFactoryTests
new PlayoutOptions { SceneDirectory = scenes.Path },
fadeDuration: 6));
}
[Fact]
public void LegacyStartupDefault_UsesCutsVideo_NotSiblingBackgroundRoot()
{
using var scenes = TemporarySceneDirectory.Create(
"5001.t2s",
PlayoutSceneCompositionFactory.LegacyStartupDefaultBackgroundAssetPath);
var backgroundRoot = Path.Combine(Directory.GetParent(scenes.Path)!.FullName, "배경");
Directory.CreateDirectory(backgroundRoot);
File.WriteAllText(
Path.Combine(
backgroundRoot,
PlayoutSceneCompositionFactory.LegacyDefaultBackgroundFileName),
"trusted F2/F3 background");
var result = PlayoutSceneCompositionFactory.CreateLegacyStartupDefault(
new PlayoutOptions { SceneDirectory = scenes.Path },
fadeDuration: 6);
Assert.Equal(@"video\기본.vrv", result.BackgroundAssetPath);
Assert.Equal(PlayoutAssetRoot.SceneDirectory, result.BackgroundAssetRoot);
}
[Fact]
public void LegacyStartupDefault_MissingCutsVideo_FailsEvenWhenSiblingDefaultExists()
{
using var scenes = TemporarySceneDirectory.Create("5001.t2s");
var backgroundRoot = Path.Combine(Directory.GetParent(scenes.Path)!.FullName, "배경");
Directory.CreateDirectory(backgroundRoot);
File.WriteAllText(
Path.Combine(
backgroundRoot,
PlayoutSceneCompositionFactory.LegacyDefaultBackgroundFileName),
"trusted F2/F3 background");
Assert.Throws<PlayoutConfigurationException>(() =>
PlayoutSceneCompositionFactory.CreateLegacyStartupDefault(
new PlayoutOptions { SceneDirectory = scenes.Path },
fadeDuration: 6));
}
}

View File

@@ -2448,6 +2448,71 @@ public sealed class TornadoPlayoutEngineTests
Assert.True(engine.Status.IsPlayCompletionPending);
}
[Fact]
public async Task PendingTakeOutCallback_BlocksEveryFurtherSdkCommandUntilCompletion()
{
using var scenes = TemporarySceneDirectory.Create("5001.t2s", "5006.t2s");
var completionReady = false;
var session = new FakeK3dSession
{
SupportsLifecycleCallbacks = true,
ClearPendingLifecycleCallbacksAfterProcess = false
};
session.TakeOutAction = (_, _) =>
{
session.HasPendingLifecycleCallbacks = true;
session.HasPendingTakeOutCallbacks = true;
};
session.ProcessPendingCallbacksAction = _ =>
{
if (!completionReady)
{
return new K3dCallbackDrainResult(0, 0, false);
}
session.HasPendingLifecycleCallbacks = false;
session.HasPendingTakeOutCallbacks = false;
return new K3dCallbackDrainResult(1, 1, false);
};
await using var engine = CreateEngine(
TestOptions(scenes.Path, "5001", "5006"),
new FakeK3dSessionFactory(() => session),
new FakeRegistrationProbe(),
new FakeTornadoProcessProbe(new TornadoProcessSnapshot(1, 1, 0)),
liveAuthorized: false);
Assert.True((await engine.ConnectAsync(CancellationToken.None)).IsSuccess);
Assert.True((await engine.PrepareAsync(
Cue("5001"),
CancellationToken.None)).IsSuccess);
Assert.True((await engine.TakeOutAsync(
PlayoutTakeOutScope.All,
CancellationToken.None)).IsSuccess);
Assert.True(engine.Status.IsTakeOutCompletionPending);
Assert.False(engine.Status.IsCommandAvailable);
var prepare = await engine.PrepareAsync(Cue("5006"), CancellationToken.None);
var repeatedTakeOut = await engine.TakeOutAsync(
PlayoutTakeOutScope.All,
CancellationToken.None);
Assert.Equal(PlayoutResultCode.Unavailable, prepare.Code);
Assert.Equal(PlayoutResultCode.Unavailable, repeatedTakeOut.Code);
Assert.Contains("CutOut/StopAll", prepare.Message, StringComparison.Ordinal);
Assert.Equal(1, session.Calls.Count(call => call == "TakeOut:All"));
Assert.DoesNotContain("Prepare:5006", session.Calls);
completionReady = true;
await engine.PollProcessOnceAsync(CancellationToken.None);
Assert.False(engine.Status.IsTakeOutCompletionPending);
Assert.True(engine.Status.IsCommandAvailable);
Assert.True((await engine.PrepareAsync(
Cue("5006"),
CancellationToken.None)).IsSuccess);
Assert.Equal(1, session.Calls.Count(call => call == "Prepare:5006"));
}
[Theory]
[InlineData(PlayoutTakeOutScope.All)]
[InlineData(PlayoutTakeOutScope.Layout)]