fix: await playout callbacks before refresh

This commit is contained in:
2026-07-11 05:03:31 +09:00
parent d8b9641a75
commit 3e3292b8a9
6 changed files with 358 additions and 32 deletions

View File

@@ -250,6 +250,7 @@ public sealed partial class MainWindow
message = result.Message,
preparedCode = status.PreparedSceneName,
onAirCode = status.OnAirSceneName,
playCompletionPending = status.IsPlayCompletionPending,
currentCueIndex = workflowState.CurrentCueIndexZeroBased,
currentEntryId = workflowState.CurrentEntryId,
builderKey = workflowState.BuilderKey,
@@ -414,6 +415,7 @@ public sealed partial class MainWindow
networkMonitoringCheckRequired = false,
commandAvailable = false,
liveTakeInAllowed = false,
playCompletionPending = false,
outcomeUnknown = browserCorrelationQuarantined,
browserCorrelationQuarantined,
operationTimeoutMilliseconds = 5000,
@@ -460,6 +462,7 @@ public sealed partial class MainWindow
networkMonitoringCheckRequired = false,
commandAvailable = false,
liveTakeInAllowed = false,
playCompletionPending = false,
outcomeUnknown = browserCorrelationQuarantined,
browserCorrelationQuarantined,
operationTimeoutMilliseconds = 5000,
@@ -515,6 +518,7 @@ public sealed partial class MainWindow
networkMonitoringCheckRequired = status.NetworkMonitoringCheckRequired,
commandAvailable = status.IsCommandAvailable && !browserCorrelationQuarantined,
liveTakeInAllowed = status.LiveTakeInAllowed && !browserCorrelationQuarantined,
playCompletionPending = status.IsPlayCompletionPending,
outcomeUnknown = browserCorrelationQuarantined ||
status.State == PlayoutConnectionState.OutcomeUnknown,
browserCorrelationQuarantined,
@@ -569,6 +573,7 @@ public sealed partial class MainWindow
networkMonitoringCheckRequired = status.NetworkMonitoringCheckRequired,
commandAvailable = status.IsCommandAvailable && !browserCorrelationQuarantined,
liveTakeInAllowed = status.LiveTakeInAllowed && !browserCorrelationQuarantined,
playCompletionPending = status.IsPlayCompletionPending,
outcomeUnknown = browserCorrelationQuarantined ||
status.State == PlayoutConnectionState.OutcomeUnknown,
browserCorrelationQuarantined,
@@ -768,6 +773,28 @@ public sealed partial class MainWindow
while (true)
{
await Task.Delay(nextDelay, token);
var engine = _playoutEngine;
if (engine is null ||
!ReferenceEquals(_playoutWorkflow, workflow) ||
!ReferenceEquals(_legacyRefreshCancellation, cancellation) ||
Volatile.Read(ref _playoutShutdownStarted) != 0)
{
return;
}
// Play returns before Tornado necessarily delivers OnScenePlayed. Waiting
// outside the app command gate keeps emergency TAKE OUT available and does
// not dispatch, repeat, or probe any SDK mutation.
if (!await WaitForPlayCompletionAsync(engine, token))
{
SetLegacyRefreshFault(
"PLAY_CALLBACK_TIMEOUT",
"Tornado 재생 완료 이벤트를 제한 시간 안에 확인하지 못해 자동 장면 갱신을 중단했습니다. TAKE OUT 후 실제 화면을 확인하세요.");
QueuePlayoutStatus();
return;
}
await _playoutCommandGate.WaitAsync(token);
var databaseActivityEntered = false;
try
@@ -856,6 +883,64 @@ public sealed partial class MainWindow
}
}
private static async Task<bool> WaitForPlayCompletionAsync(
IPlayoutEngine engine,
CancellationToken cancellationToken)
{
if (!engine.Status.IsPlayCompletionPending)
{
return true;
}
var timeoutMilliseconds = Math.Clamp(
engine.Status.OperationTimeoutMilliseconds,
100,
300_000);
using var timeoutCancellation = CancellationTokenSource.CreateLinkedTokenSource(
cancellationToken);
timeoutCancellation.CancelAfter(timeoutMilliseconds);
while (engine.Status.IsPlayCompletionPending)
{
var statusChanged = new TaskCompletionSource(
TaskCreationOptions.RunContinuationsAsynchronously);
void OnStatusChanged(object? sender, PlayoutStatusChangedEventArgs args)
{
if (!args.Current.IsPlayCompletionPending)
{
statusChanged.TrySetResult();
}
}
engine.StatusChanged += OnStatusChanged;
try
{
// Close the subscribe-after-read race without causing an engine call.
if (!engine.Status.IsPlayCompletionPending)
{
return true;
}
try
{
await statusChanged.Task.WaitAsync(timeoutCancellation.Token);
}
catch (OperationCanceledException)
when (!cancellationToken.IsCancellationRequested)
{
return false;
}
}
finally
{
engine.StatusChanged -= OnStatusChanged;
}
}
return true;
}
private LegacyRefreshRuntimeStatus GetLegacyRefreshStatus()
{
lock (_legacyRefreshStatusGate)