fix: refresh the retained on-air scene
This commit is contained in:
@@ -550,18 +550,33 @@ internal sealed class DynamicK3dSession : IK3dGuardedConnectSession
|
||||
var engine = _engine!;
|
||||
var player = _player!;
|
||||
var outputChannel = _outputChannel;
|
||||
object? playingScene = null;
|
||||
// Tornado2 can return an incomplete GetPlayingScene proxy. The scene loaded,
|
||||
// prepared, and promoted by Play is the only trusted reference for an in-place
|
||||
// refresh, and it remains retained until the normal take-out/unload lifecycle.
|
||||
if (_preparedScene is not null)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"An on-air update cannot run while another scene is prepared.");
|
||||
}
|
||||
|
||||
var currentScene = ReusableScene(_currentScene, cue.SceneName)
|
||||
?? throw new InvalidOperationException(
|
||||
"No matching retained on-air scene is available.");
|
||||
if (SupportsLifecycleCallbacks && _pendingPlayCallbacks != 0)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"An on-air update cannot run while scene playback is pending.");
|
||||
}
|
||||
|
||||
var transactionStarted = false;
|
||||
|
||||
try
|
||||
{
|
||||
playingScene = InvokeRequired(player, "GetPlayingScene", layoutIndex);
|
||||
|
||||
foreach (var mutation in cue.Mutations ?? [])
|
||||
{
|
||||
if (IsBeforeTransactionSceneSetupMutation(mutation))
|
||||
{
|
||||
ApplySceneSetupMutation(playingScene, mutation);
|
||||
ApplySceneSetupMutation(currentScene.Value, mutation);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -570,7 +585,7 @@ internal sealed class DynamicK3dSession : IK3dGuardedConnectSession
|
||||
|
||||
foreach (var field in cue.Fields ?? [])
|
||||
{
|
||||
ApplyField(playingScene, field);
|
||||
ApplyField(currentScene.Value, field);
|
||||
}
|
||||
|
||||
foreach (var mutation in cue.Mutations ?? [])
|
||||
@@ -580,11 +595,11 @@ internal sealed class DynamicK3dSession : IK3dGuardedConnectSession
|
||||
// explicit in-transaction ordering.
|
||||
if (!IsBeforeTransactionSceneSetupMutation(mutation))
|
||||
{
|
||||
ApplyTransactionMutation(playingScene, mutation);
|
||||
ApplyTransactionMutation(currentScene.Value, mutation);
|
||||
}
|
||||
}
|
||||
|
||||
Invoke(playingScene, "QueryVariables");
|
||||
Invoke(currentScene.Value, "QueryVariables");
|
||||
if (outputChannel.HasValue)
|
||||
{
|
||||
Invoke(engine, "EndTransactionOnChannel", outputChannel.GetValueOrDefault());
|
||||
@@ -595,20 +610,8 @@ internal sealed class DynamicK3dSession : IK3dGuardedConnectSession
|
||||
}
|
||||
|
||||
transactionStarted = false;
|
||||
Invoke(player, "Prepare", layoutIndex, playingScene);
|
||||
Invoke(player, "Prepare", layoutIndex, currentScene.Value);
|
||||
InvokeTrackedPlay(layoutIndex);
|
||||
|
||||
// GetPlayingScene can return the same RCW or another RCW for the same COM
|
||||
// scene. An in-place page update must never retire/unload the active scene.
|
||||
if (_currentScene is null)
|
||||
{
|
||||
_currentScene = new SceneReference(playingScene, cue.SceneName);
|
||||
playingScene = null;
|
||||
}
|
||||
else if (ReferenceEquals(_currentScene.Value, playingScene))
|
||||
{
|
||||
playingScene = null;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
@@ -626,10 +629,6 @@ internal sealed class DynamicK3dSession : IK3dGuardedConnectSession
|
||||
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
{
|
||||
ReleaseComObject(playingScene);
|
||||
}
|
||||
}
|
||||
|
||||
public void TakeOut(int layoutIndex, PlayoutTakeOutScope scope)
|
||||
|
||||
@@ -458,7 +458,7 @@ public sealed class DynamicK3dSessionTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UpdateOnAir_UsesPlayingSceneAndAppliesOnlyBeforeTransactionBackgroundSetup()
|
||||
public async Task UpdateOnAir_ReusesRetainedSceneAndIgnoresInvalidPlayingSceneProxy()
|
||||
{
|
||||
using var scenes = TemporarySceneDirectory.Create(
|
||||
"test-scene.t2s",
|
||||
@@ -484,7 +484,7 @@ public sealed class DynamicK3dSessionTests
|
||||
]));
|
||||
var log = new FakeComLog();
|
||||
var scene = new FakeScene(log, "test-scene");
|
||||
var player = new FakePlayer(log) { PlayingScene = scene };
|
||||
var player = new FakePlayer(log) { PlayingScene = new object() };
|
||||
var activator = new FakeActivator(log, new FakeEngine(log, player, scene));
|
||||
await using var dispatcher = new StaDispatcher(capacity: 2);
|
||||
var updateStart = 0;
|
||||
@@ -514,7 +514,6 @@ public sealed class DynamicK3dSessionTests
|
||||
Assert.Equal(
|
||||
new[]
|
||||
{
|
||||
"GetPlayingScene:10",
|
||||
"UseBackground:0",
|
||||
"SetBackgroundTexture:background.vrv",
|
||||
"SetBackgroundVideo:background.vrv:4:1",
|
||||
@@ -531,20 +530,274 @@ public sealed class DynamicK3dSessionTests
|
||||
"Play:10"
|
||||
},
|
||||
updateCalls);
|
||||
Assert.Equal(1, updateCalls.Count(call => call == "Play:10"));
|
||||
Assert.DoesNotContain("GetPlayingScene:10", log.Names);
|
||||
Assert.DoesNotContain(updateCalls, call =>
|
||||
call.StartsWith("LoadScene:", StringComparison.Ordinal) ||
|
||||
call.StartsWith("SetSceneEffectType:", StringComparison.Ordinal));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UpdateOnAir_CallbackSession_TracksItsPlayUntilMatchingCallback()
|
||||
public async Task UpdateOnAir_MismatchedRetainedSceneFailsBeforeAnySdkMutation()
|
||||
{
|
||||
using var scenes = TemporarySceneDirectory.Create(
|
||||
"5001.t2s",
|
||||
"5006.t2s",
|
||||
"Video\\background.vrv");
|
||||
var options = SceneOptions(scenes.Path, "5001", "5006");
|
||||
var initialCue = SceneCue(options, "5001");
|
||||
var mismatchedCue = options.ResolveCue(new PlayoutCue(
|
||||
"5006.t2s",
|
||||
"5006",
|
||||
[new PlayoutField("headline", "must not be sent", true)],
|
||||
Mutations:
|
||||
[
|
||||
new PlayoutUseBackground(false),
|
||||
new PlayoutSetBackgroundTexture("Video/background.vrv")
|
||||
]));
|
||||
var log = new FakeComLog();
|
||||
var retainedScene = new FakeScene(log, "5001");
|
||||
var player = new FakePlayer(log) { PlayingScene = new object() };
|
||||
var activator = new FakeActivator(
|
||||
log,
|
||||
new FakeEngine(log, player, retainedScene));
|
||||
await using var dispatcher = new StaDispatcher(capacity: 2);
|
||||
string[] updateCalls = [];
|
||||
|
||||
await dispatcher.InvokeAsync(
|
||||
() =>
|
||||
{
|
||||
using var session = new DynamicK3dSession(activator);
|
||||
session.Connect(options);
|
||||
session.Prepare(initialCue, options.LayoutIndex);
|
||||
session.Play(options.LayoutIndex);
|
||||
|
||||
var updateStart = log.Names.Length;
|
||||
Assert.Throws<InvalidOperationException>(
|
||||
() => session.UpdateOnAir(mismatchedCue, options.LayoutIndex));
|
||||
updateCalls = log.Names.Skip(updateStart).ToArray();
|
||||
session.Disconnect();
|
||||
return true;
|
||||
},
|
||||
TimeSpan.FromSeconds(5),
|
||||
CancellationToken.None);
|
||||
|
||||
Assert.Empty(updateCalls);
|
||||
Assert.DoesNotContain("GetPlayingScene:10", log.Names);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UpdateOnAir_WithoutRetainedCurrentSceneFailsBeforeAnySdkMutation()
|
||||
{
|
||||
using var scenes = TemporarySceneDirectory.Create("5001.t2s");
|
||||
var options = SceneOptions(scenes.Path, "5001");
|
||||
var cue = SceneCue(options, "5001");
|
||||
var log = new FakeComLog();
|
||||
var player = new FakePlayer(log) { PlayingScene = new object() };
|
||||
var scene = new FakeScene(log, "5001");
|
||||
var activator = new FakeActivator(log, new FakeEngine(log, player, scene));
|
||||
await using var dispatcher = new StaDispatcher(capacity: 2);
|
||||
string[] updateCalls = [];
|
||||
|
||||
await dispatcher.InvokeAsync(
|
||||
() =>
|
||||
{
|
||||
using var session = new DynamicK3dSession(activator);
|
||||
session.Connect(options);
|
||||
|
||||
var updateStart = log.Names.Length;
|
||||
Assert.Throws<InvalidOperationException>(
|
||||
() => session.UpdateOnAir(cue, options.LayoutIndex));
|
||||
updateCalls = log.Names.Skip(updateStart).ToArray();
|
||||
session.Disconnect();
|
||||
return true;
|
||||
},
|
||||
TimeSpan.FromSeconds(5),
|
||||
CancellationToken.None);
|
||||
|
||||
Assert.Empty(updateCalls);
|
||||
Assert.DoesNotContain("GetPlayingScene:10", log.Names);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UpdateOnAir_WithAnotherPreparedSceneFailsBeforeAnySdkMutation()
|
||||
{
|
||||
using var scenes = TemporarySceneDirectory.Create("5001.t2s", "5006.t2s");
|
||||
var options = SceneOptions(scenes.Path, "5001", "5006");
|
||||
var cue5001 = SceneCue(options, "5001");
|
||||
var log = new FakeComLog();
|
||||
var player = new FakePlayer(log) { PlayingScene = new object() };
|
||||
var scene5001 = new FakeScene(log, "5001");
|
||||
var scene5006 = new FakeScene(log, "5006");
|
||||
var engine = new FakeEngine(log, player, scene5001)
|
||||
{
|
||||
SceneResolver = name => name == "5006" ? scene5006 : scene5001
|
||||
};
|
||||
var activator = new FakeActivator(log, engine);
|
||||
await using var dispatcher = new StaDispatcher(capacity: 2);
|
||||
string[] updateCalls = [];
|
||||
|
||||
await dispatcher.InvokeAsync(
|
||||
() =>
|
||||
{
|
||||
using var session = new DynamicK3dSession(activator);
|
||||
session.Connect(options);
|
||||
session.Prepare(cue5001, options.LayoutIndex);
|
||||
session.Play(options.LayoutIndex);
|
||||
session.Prepare(SceneCue(options, "5006"), options.LayoutIndex);
|
||||
|
||||
var updateStart = log.Names.Length;
|
||||
Assert.Throws<InvalidOperationException>(
|
||||
() => session.UpdateOnAir(cue5001, options.LayoutIndex));
|
||||
updateCalls = log.Names.Skip(updateStart).ToArray();
|
||||
session.Disconnect();
|
||||
return true;
|
||||
},
|
||||
TimeSpan.FromSeconds(5),
|
||||
CancellationToken.None);
|
||||
|
||||
Assert.Empty(updateCalls);
|
||||
Assert.DoesNotContain("GetPlayingScene:10", log.Names);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UpdateOnAir_WithPendingInitialPlayFailsBeforeAnySdkMutation()
|
||||
{
|
||||
using var scenes = TemporarySceneDirectory.Create("5001.t2s");
|
||||
var options = SceneOptions(scenes.Path, "5001");
|
||||
var cue = SceneCue(options, "5001");
|
||||
var log = new FakeComLog();
|
||||
var scene = new FakeScene(log, "5001");
|
||||
var player = new FakePlayer(log) { PlayingScene = scene };
|
||||
var player = new FakePlayer(log) { PlayingScene = new object() };
|
||||
var engine = new FakeEngine(log, player, scene);
|
||||
var activator = new FakeActivator(log, engine);
|
||||
var callbacks = new RecordingEventHandlerFactory();
|
||||
var releaser = new FakeReleaser(
|
||||
log,
|
||||
(scene, "5001"),
|
||||
(player, "Player"),
|
||||
(engine, "Engine"),
|
||||
(callbacks.Handler, "EventHandler"));
|
||||
await using var dispatcher = new StaDispatcher(capacity: 2);
|
||||
string[] updateCalls = [];
|
||||
|
||||
await dispatcher.InvokeAsync(
|
||||
() =>
|
||||
{
|
||||
using var session = new DynamicK3dSession(
|
||||
activator,
|
||||
releaser,
|
||||
new InstalledK3dInteropMethodInvoker(),
|
||||
callbacks);
|
||||
session.Connect(options);
|
||||
session.Prepare(cue, options.LayoutIndex);
|
||||
session.Play(options.LayoutIndex);
|
||||
Assert.True(session.HasPendingPlayCallbacks);
|
||||
|
||||
var updateStart = log.Names.Length;
|
||||
Assert.Throws<InvalidOperationException>(
|
||||
() => session.UpdateOnAir(cue, options.LayoutIndex));
|
||||
updateCalls = log.Names.Skip(updateStart).ToArray();
|
||||
|
||||
callbacks.Fire("OnScenePlayed", 1, 9, options.LayoutIndex);
|
||||
session.ProcessPendingCallbacks(options.LayoutIndex);
|
||||
Assert.False(session.HasPendingPlayCallbacks);
|
||||
session.Disconnect();
|
||||
return true;
|
||||
},
|
||||
TimeSpan.FromSeconds(5),
|
||||
CancellationToken.None);
|
||||
|
||||
Assert.Empty(updateCalls);
|
||||
Assert.DoesNotContain("GetPlayingScene:10", log.Names);
|
||||
Assert.Equal(1, log.Names.Count(name => name == "Release:5001"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UpdateOnAir_TransactionFailureRollsBackAndRetainsSceneForStopAllCleanup()
|
||||
{
|
||||
using var scenes = TemporarySceneDirectory.Create("5001.t2s");
|
||||
var options = SceneOptions(scenes.Path, "5001");
|
||||
var initialCue = SceneCue(options, "5001");
|
||||
var updateCue = options.ResolveCue(new PlayoutCue(
|
||||
"5001.t2s",
|
||||
"5001",
|
||||
[new PlayoutField("headline", "refresh", true)],
|
||||
Mutations: [new PlayoutUseBackground(false)]));
|
||||
var log = new FakeComLog();
|
||||
var scene = new FakeScene(log, "5001");
|
||||
var player = new FakePlayer(log) { PlayingScene = new object() };
|
||||
var engine = new FakeEngine(log, player, scene);
|
||||
var activator = new FakeActivator(log, engine);
|
||||
var callbacks = new RecordingEventHandlerFactory();
|
||||
var releaser = new FakeReleaser(
|
||||
log,
|
||||
(scene, "5001"),
|
||||
(player, "Player"),
|
||||
(engine, "Engine"),
|
||||
(callbacks.Handler, "EventHandler"));
|
||||
await using var dispatcher = new StaDispatcher(capacity: 2);
|
||||
|
||||
await dispatcher.InvokeAsync(
|
||||
() =>
|
||||
{
|
||||
using var session = new DynamicK3dSession(
|
||||
activator,
|
||||
releaser,
|
||||
new InstalledK3dInteropMethodInvoker(),
|
||||
callbacks);
|
||||
session.Connect(options);
|
||||
session.Prepare(initialCue, options.LayoutIndex);
|
||||
session.Play(options.LayoutIndex);
|
||||
callbacks.Fire("OnScenePlayed", 1, 9, options.LayoutIndex);
|
||||
session.ProcessPendingCallbacks(options.LayoutIndex);
|
||||
|
||||
scene.QueryVariablesFailure = new InvalidOperationException("fake refresh failure");
|
||||
var updateStart = log.Names.Length;
|
||||
Assert.Throws<InvalidOperationException>(
|
||||
() => session.UpdateOnAir(updateCue, options.LayoutIndex));
|
||||
var updateCalls = log.Names.Skip(updateStart).ToArray();
|
||||
Assert.Equal(
|
||||
new[]
|
||||
{
|
||||
"UseBackground:0",
|
||||
"BeginTransaction",
|
||||
"GetObject:headline",
|
||||
"SetValue:headline:refresh",
|
||||
"SetVisible:headline:1",
|
||||
"QueryVariables",
|
||||
"RollbackTransaction"
|
||||
},
|
||||
updateCalls);
|
||||
Assert.DoesNotContain("Release:5001", log.Names);
|
||||
Assert.DoesNotContain("Unload:5001", log.Names);
|
||||
|
||||
scene.QueryVariablesFailure = null;
|
||||
session.TakeOut(options.LayoutIndex, PlayoutTakeOutScope.All);
|
||||
callbacks.Fire("OnStopAll", 1);
|
||||
Assert.Equal(
|
||||
1,
|
||||
session.ProcessPendingCallbacks(options.LayoutIndex).UnloadedSceneCount);
|
||||
session.Disconnect();
|
||||
return true;
|
||||
},
|
||||
TimeSpan.FromSeconds(5),
|
||||
CancellationToken.None);
|
||||
|
||||
Assert.Equal(1, log.Names.Count(name => name == "Unload:5001"));
|
||||
Assert.Equal(1, log.Names.Count(name => name == "Release:5001"));
|
||||
Assert.DoesNotContain("GetPlayingScene:10", log.Names);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UpdateOnAir_CallbackSession_TracksPlayAndRetainsSceneUntilStopAll()
|
||||
{
|
||||
using var scenes = TemporarySceneDirectory.Create("5001.t2s");
|
||||
var options = SceneOptions(scenes.Path, "5001");
|
||||
var cue = SceneCue(options, "5001");
|
||||
var log = new FakeComLog();
|
||||
var scene = new FakeScene(log, "5001");
|
||||
var player = new FakePlayer(log) { PlayingScene = new object() };
|
||||
var engine = new FakeEngine(log, player, scene);
|
||||
var activator = new FakeActivator(log, engine);
|
||||
var callbacks = new RecordingEventHandlerFactory();
|
||||
@@ -577,6 +830,8 @@ public sealed class DynamicK3dSessionTests
|
||||
|
||||
session.UpdateOnAir(cue, options.LayoutIndex);
|
||||
Assert.True(session.HasPendingLifecycleCallbacks);
|
||||
Assert.DoesNotContain("Release:5001", log.Names);
|
||||
Assert.DoesNotContain("Unload:5001", log.Names);
|
||||
|
||||
callbacks.Fire("OnScenePlayed", 1, 8, 10);
|
||||
session.ProcessPendingCallbacks(options.LayoutIndex);
|
||||
@@ -585,6 +840,13 @@ public sealed class DynamicK3dSessionTests
|
||||
callbacks.Fire("OnScenePlayed", 1, 9, 10);
|
||||
session.ProcessPendingCallbacks(options.LayoutIndex);
|
||||
Assert.False(session.HasPendingLifecycleCallbacks);
|
||||
|
||||
session.TakeOut(options.LayoutIndex, PlayoutTakeOutScope.All);
|
||||
callbacks.Fire("OnStopAll", 1);
|
||||
Assert.Equal(
|
||||
1,
|
||||
session.ProcessPendingCallbacks(options.LayoutIndex).UnloadedSceneCount);
|
||||
Assert.False(session.HasPendingLifecycleCallbacks);
|
||||
session.Disconnect();
|
||||
return true;
|
||||
},
|
||||
@@ -592,8 +854,9 @@ public sealed class DynamicK3dSessionTests
|
||||
CancellationToken.None);
|
||||
|
||||
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);
|
||||
Assert.DoesNotContain("GetPlayingScene:10", log.Names);
|
||||
Assert.Equal(1, log.Names.Count(name => name == "Unload:5001"));
|
||||
Assert.Equal(1, log.Names.Count(name => name == "Release:5001"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -1729,7 +1992,7 @@ public sealed class DynamicK3dSessionTests
|
||||
{
|
||||
private readonly FakeComLog _log;
|
||||
private readonly string? _name;
|
||||
private readonly Exception? _queryVariablesFailure;
|
||||
private Exception? _queryVariablesFailure;
|
||||
private readonly Exception? _unloadFailure;
|
||||
|
||||
public FakeScene(
|
||||
@@ -1773,6 +2036,12 @@ public sealed class DynamicK3dSessionTests
|
||||
}
|
||||
}
|
||||
|
||||
public Exception? QueryVariablesFailure
|
||||
{
|
||||
get => _queryVariablesFailure;
|
||||
set => _queryVariablesFailure = value;
|
||||
}
|
||||
|
||||
public void Unload()
|
||||
{
|
||||
_log.Add(_name is null ? "Unload" : $"Unload:{_name}");
|
||||
|
||||
Reference in New Issue
Block a user