fix: reuse scenes for on-air refresh

This commit is contained in:
2026-07-11 12:13:30 +09:00
parent 3e3292b8a9
commit d11abfa567
2 changed files with 100 additions and 52 deletions

View File

@@ -413,9 +413,16 @@ internal sealed class DynamicK3dSession : IK3dGuardedConnectSession
try
{
nextScene = new SceneReference(
InvokeRequired(engine, "LoadScene", cue.SceneFile, cue.SceneName),
cue.SceneName);
// MainForm refreshes the selected DTO immediately before TAKE IN and
// Page NEXT can target the alias already on air. Tornado2 rejects a
// second LoadScene for an alias it still owns, so mutate and re-Prepare
// the retained loaded scene instead of creating a duplicate alias.
nextScene =
ReusableScene(_preparedScene, cue.SceneName) ??
ReusableScene(_currentScene, cue.SceneName) ??
new SceneReference(
InvokeRequired(engine, "LoadScene", cue.SceneFile, cue.SceneName),
cue.SceneName);
if (outputChannel.HasValue)
{
Invoke(nextScene.Value, "SetOutputChannelIndex", outputChannel.GetValueOrDefault());
@@ -548,10 +555,16 @@ internal sealed class DynamicK3dSession : IK3dGuardedConnectSession
try
{
// MainForm.timer1_Tick replays the current layout before entering
// Show_PlayList(idx: 1), then prepares and plays the mutated scene again.
InvokeTrackedPlay(layoutIndex);
playingScene = InvokeRequired(player, "GetPlayingScene", layoutIndex);
foreach (var mutation in cue.Mutations ?? [])
{
if (IsBeforeTransactionSceneSetupMutation(mutation))
{
ApplySceneSetupMutation(playingScene, mutation);
}
}
Invoke(engine, "BeginTransaction");
transactionStarted = true;
@@ -562,11 +575,12 @@ internal sealed class DynamicK3dSession : IK3dGuardedConnectSession
foreach (var mutation in cue.Mutations ?? [])
{
// The legacy idx == 1 path updates the playing scene in place. It does not
// reapply scene-level background setup or the scene transition effect.
if (!IsSceneLevelMutation(mutation))
// Match Show_PlayList(idx: 1): common background setup runs before the
// transaction, while builder-owned setup and object changes keep their
// explicit in-transaction ordering.
if (!IsBeforeTransactionSceneSetupMutation(mutation))
{
ApplyMutation(playingScene, mutation);
ApplyTransactionMutation(playingScene, mutation);
}
}
@@ -1080,6 +1094,14 @@ internal sealed class DynamicK3dSession : IK3dGuardedConnectSession
}
}
private static SceneReference? ReusableScene(
SceneReference? scene,
string sceneName) =>
scene is not null &&
string.Equals(scene.Name, sceneName, StringComparison.OrdinalIgnoreCase)
? scene
: null;
private void MoveAllScenesToPendingTakeOut()
{
AddDistinctScene(_pendingTakeOutScenes, _preparedScene);

View File

@@ -458,7 +458,7 @@ public sealed class DynamicK3dSessionTests
}
[Fact]
public async Task UpdateOnAir_UsesPlayingSceneWithoutReloadingOrReapplyingSceneSetup()
public async Task UpdateOnAir_UsesPlayingSceneAndAppliesOnlyBeforeTransactionBackgroundSetup()
{
using var scenes = TemporarySceneDirectory.Create(
"test-scene.t2s",
@@ -475,6 +475,11 @@ public sealed class DynamicK3dSessionTests
new PlayoutUseBackground(false),
new PlayoutSetBackgroundTexture("Video/background.vrv"),
new PlayoutSetBackgroundVideo("Video/background.vrv", 4, true),
new PlayoutSetBackgroundVideo(
"Video/background.vrv",
8,
true,
PlayoutMutationTiming.InTransaction),
new PlayoutSetVisible("badge", false)
]));
var log = new FakeComLog();
@@ -509,12 +514,15 @@ public sealed class DynamicK3dSessionTests
Assert.Equal(
new[]
{
"Play:10",
"GetPlayingScene:10",
"UseBackground:0",
"SetBackgroundTexture:background.vrv",
"SetBackgroundVideo:background.vrv:4:1",
"BeginTransaction",
"GetObject:headline",
"SetValue:headline:page two",
"SetVisible:headline:1",
"SetBackgroundVideo:background.vrv:8:1",
"GetObject:badge",
"SetVisible:badge:0",
"QueryVariables",
@@ -525,10 +533,7 @@ public sealed class DynamicK3dSessionTests
updateCalls);
Assert.DoesNotContain(updateCalls, call =>
call.StartsWith("LoadScene:", StringComparison.Ordinal) ||
call.StartsWith("SetSceneEffectType:", StringComparison.Ordinal) ||
call.StartsWith("UseBackground:", StringComparison.Ordinal) ||
call.StartsWith("SetBackgroundTexture:", StringComparison.Ordinal) ||
call.StartsWith("SetBackgroundVideo:", StringComparison.Ordinal));
call.StartsWith("SetSceneEffectType:", StringComparison.Ordinal));
}
[Fact]
@@ -577,10 +582,6 @@ public sealed class DynamicK3dSessionTests
session.ProcessPendingCallbacks(options.LayoutIndex);
Assert.True(session.HasPendingLifecycleCallbacks);
callbacks.Fire("OnScenePlayed", 1, 9, 10);
session.ProcessPendingCallbacks(options.LayoutIndex);
Assert.True(session.HasPendingLifecycleCallbacks);
callbacks.Fire("OnScenePlayed", 1, 9, 10);
session.ProcessPendingCallbacks(options.LayoutIndex);
Assert.False(session.HasPendingLifecycleCallbacks);
@@ -590,11 +591,48 @@ public sealed class DynamicK3dSessionTests
TimeSpan.FromSeconds(5),
CancellationToken.None);
Assert.Equal(3, log.Names.Count(name => name == "Play:10"));
Assert.Equal(2, log.Names.Count(name => name == "Play:10"));
Assert.Equal(1, log.Names.Count(name => name == "GetPlayingScene:10"));
Assert.DoesNotContain("Unload:5001", log.Names);
}
[Fact]
public async Task Prepare_SamePreparedScene_ReusesSceneWithoutSecondLoad()
{
using var scenes = TemporarySceneDirectory.Create("5001.t2s");
var options = SceneOptions(scenes.Path, "5001");
var log = new FakeComLog();
var player = new FakePlayer(log);
var scene = new FakeScene(log, "5001");
var engine = new FakeEngine(log, player, scene);
var activator = new FakeActivator(log, engine);
var releaser = new FakeReleaser(
log,
(scene, "5001"),
(player, "Player"),
(engine, "Engine"),
(activator.EventHandler, "EventHandler"));
await using var dispatcher = new StaDispatcher(capacity: 2);
await dispatcher.InvokeAsync(
() =>
{
using var session = new DynamicK3dSession(activator, releaser);
session.Connect(options);
session.Prepare(SceneCue(options, "5001"), options.LayoutIndex);
session.Prepare(SceneCue(options, "5001"), options.LayoutIndex);
session.Disconnect();
return true;
},
TimeSpan.FromSeconds(5),
CancellationToken.None);
Assert.Equal(1, log.Names.Count(name => name == "LoadScene:5001"));
Assert.Equal(2, log.Names.Count(name => name == "Prepare:10"));
Assert.DoesNotContain("Unload:5001", log.Names);
Assert.Equal(1, log.Names.Count(name => name == "Release:5001"));
}
[Fact]
public async Task LifecycleCallbacks_UnloadOnlyRetiredScenesAfterMatchingSuccess()
{
@@ -682,25 +720,19 @@ public sealed class DynamicK3dSessionTests
}
[Fact]
public async Task LifecycleCallbacks_SameNameReplacement_UnloadsOnlyAfterStopAll()
public async Task LifecycleCallbacks_CurrentSameNamePrepare_ReusesActiveSceneUntilStopAll()
{
using var scenes = TemporarySceneDirectory.Create("5001.t2s");
var options = SceneOptions(scenes.Path, "5001");
var log = new FakeComLog();
var player = new FakePlayer(log);
var retiredScene = new FakeScene(log, "5001");
var activeScene = new FakeScene(log, "5001");
var scenesToLoad = new Queue<FakeScene>([retiredScene, activeScene]);
var engine = new FakeEngine(log, player, retiredScene)
{
SceneResolver = _ => scenesToLoad.Dequeue()
};
var engine = new FakeEngine(log, player, activeScene);
var activator = new FakeActivator(log, engine);
var callbacks = new RecordingEventHandlerFactory();
var releaser = new FakeReleaser(
log,
(retiredScene, "5001-retired"),
(activeScene, "5001-active"),
(activeScene, "5001"),
(player, "Player"),
(engine, "Engine"),
(callbacks.Handler, "EventHandler"));
@@ -730,8 +762,8 @@ public sealed class DynamicK3dSessionTests
0,
session.ProcessPendingCallbacks(options.LayoutIndex).UnloadedSceneCount);
Assert.DoesNotContain("Unload:5001", log.Names);
Assert.Contains("Release:5001-retired", log.Names);
Assert.DoesNotContain("Release:5001-active", log.Names);
Assert.DoesNotContain("Release:5001", log.Names);
Assert.Equal(1, log.Names.Count(name => name == "LoadScene:5001"));
session.TakeOut(options.LayoutIndex, PlayoutTakeOutScope.All);
callbacks.Fire("OnStopAll", 1);
@@ -739,39 +771,32 @@ public sealed class DynamicK3dSessionTests
1,
session.ProcessPendingCallbacks(options.LayoutIndex).UnloadedSceneCount);
Assert.Equal(1, log.Names.Count(name => name == "Unload:5001"));
Assert.Contains("Release:5001-active", log.Names);
Assert.Contains("Release:5001", log.Names);
session.Disconnect();
return true;
},
TimeSpan.FromSeconds(5),
CancellationToken.None);
Assert.Empty(scenesToLoad);
Assert.Equal(1, log.Names.Count(name => name == "LoadScene:5001"));
Assert.Equal(1, log.Names.Count(name => name == "Unload:5001"));
Assert.Equal(1, log.Names.Count(name => name == "Release:5001-retired"));
Assert.Equal(1, log.Names.Count(name => name == "Release:5001-active"));
Assert.Equal(1, log.Names.Count(name => name == "Release:5001"));
}
[Fact]
public async Task LifecycleCallbacks_SameNameReplacement_IgnoresSceneNameCasing()
public async Task Prepare_CurrentSceneReuse_IgnoresSceneNameCasing()
{
using var scenes = TemporarySceneDirectory.Create("SceneA.t2s");
var options = SceneOptions(scenes.Path, "SceneA");
var log = new FakeComLog();
var player = new FakePlayer(log);
var retiredScene = new FakeScene(log, "SceneA");
var activeScene = new FakeScene(log, "scenea");
var scenesToLoad = new Queue<FakeScene>([retiredScene, activeScene]);
var engine = new FakeEngine(log, player, retiredScene)
{
SceneResolver = _ => scenesToLoad.Dequeue()
};
var activeScene = new FakeScene(log, "SceneA");
var engine = new FakeEngine(log, player, activeScene);
var activator = new FakeActivator(log, engine);
var callbacks = new RecordingEventHandlerFactory();
var releaser = new FakeReleaser(
log,
(retiredScene, "SceneA-retired"),
(activeScene, "scenea-active"),
(activeScene, "SceneA"),
(player, "Player"),
(engine, "Engine"),
(callbacks.Handler, "EventHandler"));
@@ -799,7 +824,9 @@ public sealed class DynamicK3dSessionTests
0,
session.ProcessPendingCallbacks(options.LayoutIndex).UnloadedSceneCount);
Assert.DoesNotContain("Unload:SceneA", log.Names);
Assert.Contains("Release:SceneA-retired", log.Names);
Assert.DoesNotContain("Release:SceneA", log.Names);
Assert.Equal(1, log.Names.Count(name => name == "LoadScene:SceneA"));
Assert.DoesNotContain("LoadScene:scenea", log.Names);
session.TakeOut(options.LayoutIndex, PlayoutTakeOutScope.All);
callbacks.Fire("OnStopAll", 1);
@@ -812,11 +839,10 @@ public sealed class DynamicK3dSessionTests
TimeSpan.FromSeconds(5),
CancellationToken.None);
Assert.Empty(scenesToLoad);
Assert.DoesNotContain("Unload:SceneA", log.Names);
Assert.Equal(1, log.Names.Count(name => name == "Unload:scenea"));
Assert.Equal(1, log.Names.Count(name => name == "Release:SceneA-retired"));
Assert.Equal(1, log.Names.Count(name => name == "Release:scenea-active"));
Assert.Equal(1, log.Names.Count(name => name == "LoadScene:SceneA"));
Assert.DoesNotContain("LoadScene:scenea", log.Names);
Assert.Equal(1, log.Names.Count(name => name == "Unload:SceneA"));
Assert.Equal(1, log.Names.Count(name => name == "Release:SceneA"));
}
[Fact]