Files
MBN_STOCK_WEBVIEW/MainWindow.PlaylistPagePlans.cs

252 lines
8.5 KiB
C#

using System.Text.Json;
using MBN_STOCK_WEBVIEW.Core.Playout;
using MBN_STOCK_WEBVIEW.Core.Playout.Scenes;
using MBN_STOCK_WEBVIEW.Infrastructure;
namespace MBN_STOCK_WEBVIEW;
public sealed partial class MainWindow
{
private async Task HandlePlaylistPagePlansRequestAsync(JsonElement payload)
{
var parsed = PlayoutBridgeProtocol.ParsePagePlanRequest(payload);
var request = parsed.Request;
if (!parsed.IsValid)
{
PostPlaylistPagePlansError(
request.RequestId,
"INVALID_REQUEST",
parsed.Error,
retryable: false);
return;
}
var workflow = _playoutWorkflow;
var engine = _playoutEngine;
if (workflow is null || engine is null)
{
PostPlaylistPagePlansError(
request.RequestId,
"PLAYOUT_UNAVAILABLE",
_playoutInitializationError ??
"The playout workflow is not available for page preflight.",
retryable: false);
return;
}
var requestCancellation = CancellationTokenSource.CreateLinkedTokenSource(
_lifetimeCancellation.Token);
var requestToken = requestCancellation.Token;
var browserGeneration = Volatile.Read(ref _playlistPagePlanBrowserGeneration);
var previousCancellation = Interlocked.Exchange(
ref _playlistPagePlanCancellation,
requestCancellation);
CancelRequest(previousCancellation);
var databaseActivityEntered = false;
try
{
if (Volatile.Read(ref _playoutCommandInFlight) != 0)
{
PostPlaylistPagePlansErrorIfCurrent(
request.RequestId,
"PLAYOUT_PRIORITY",
"A playout command has priority over page preflight.",
retryable: true,
requestCancellation,
browserGeneration);
return;
}
if (!IsPlaylistPagePlanIdle(workflow, engine))
{
PostPlaylistPagePlansErrorIfCurrent(
request.RequestId,
"PLAYOUT_NOT_IDLE",
"Page preflight is available only while no scene is prepared or on air.",
retryable: true,
requestCancellation,
browserGeneration);
return;
}
await _databaseActivityGate.WaitAsync(requestToken);
databaseActivityEntered = true;
requestToken.ThrowIfCancellationRequested();
if (Volatile.Read(ref _playoutCommandInFlight) != 0)
{
PostPlaylistPagePlansErrorIfCurrent(
request.RequestId,
"PLAYOUT_PRIORITY",
"A playout command cancelled page preflight before the database read.",
retryable: true,
requestCancellation,
browserGeneration);
return;
}
if (!IsPlaylistPagePlanIdle(workflow, engine))
{
PostPlaylistPagePlansErrorIfCurrent(
request.RequestId,
"PLAYOUT_NOT_IDLE",
"Playout became active before page preflight could start.",
retryable: true,
requestCancellation,
browserGeneration);
return;
}
var plans = await workflow.PreflightPagePlansAsync(
request.Entries!,
requestToken);
requestToken.ThrowIfCancellationRequested();
if (!IsCurrentPlaylistPagePlanRequest(requestCancellation, browserGeneration))
{
return;
}
PostMessage("playlist-page-plans-results", new
{
requestId = request.RequestId,
calculatedAt = DateTimeOffset.UtcNow,
plans = plans.Select(plan => new
{
entryId = plan.EntryId,
itemCount = plan.ItemCount,
pageSize = plan.PageSize,
pageCount = plan.PageCount,
accessibleItemCount = plan.AccessibleItemCount,
isTruncated = plan.IsTruncated,
builderKey = plan.BuilderKey
})
});
}
catch (OperationCanceledException) when (requestToken.IsCancellationRequested)
{
if (Volatile.Read(ref _playoutCommandInFlight) != 0)
{
PostPlaylistPagePlansErrorIfCurrent(
request.RequestId,
"PLAYOUT_PRIORITY",
"A playout command cancelled the read-only page preflight.",
retryable: true,
requestCancellation,
browserGeneration);
}
}
catch (DatabaseOperationException exception)
{
PostPlaylistPagePlansErrorIfCurrent(
request.RequestId,
"DATABASE_UNAVAILABLE",
exception.Message,
retryable: true,
requestCancellation,
browserGeneration);
}
catch (LegacySceneDataException exception)
{
PostPlaylistPagePlansErrorIfCurrent(
request.RequestId,
"SCENE_DATA_INVALID",
exception.Message,
retryable: false,
requestCancellation,
browserGeneration);
}
catch (InvalidOperationException exception)
{
PostPlaylistPagePlansErrorIfCurrent(
request.RequestId,
"PLAYOUT_NOT_IDLE",
exception.Message,
retryable: true,
requestCancellation,
browserGeneration);
}
catch (Exception exception)
{
PostPlaylistPagePlansErrorIfCurrent(
request.RequestId,
"PAGE_PREFLIGHT_FAILED",
exception.Message,
retryable: false,
requestCancellation,
browserGeneration);
}
finally
{
if (databaseActivityEntered)
{
_databaseActivityGate.Release();
}
Interlocked.CompareExchange(
ref _playlistPagePlanCancellation,
null,
requestCancellation);
requestCancellation.Dispose();
}
}
private static bool IsPlaylistPagePlanIdle(
LegacyPlayoutWorkflow workflow,
IPlayoutEngine engine)
{
var workflowState = workflow.State;
var engineStatus = engine.Status;
return string.IsNullOrWhiteSpace(workflowState.PreparedCutCode) &&
string.IsNullOrWhiteSpace(workflowState.OnAirCutCode) &&
string.IsNullOrWhiteSpace(engineStatus.PreparedSceneName) &&
string.IsNullOrWhiteSpace(engineStatus.OnAirSceneName);
}
private bool IsCurrentPlaylistPagePlanRequest(
CancellationTokenSource requestCancellation,
int browserGeneration) =>
ReferenceEquals(
Volatile.Read(ref _playlistPagePlanCancellation),
requestCancellation) &&
browserGeneration == Volatile.Read(ref _playlistPagePlanBrowserGeneration) &&
!_lifetimeCancellation.IsCancellationRequested;
private void PostPlaylistPagePlansErrorIfCurrent(
string requestId,
string code,
string message,
bool retryable,
CancellationTokenSource requestCancellation,
int browserGeneration)
{
if (!IsCurrentPlaylistPagePlanRequest(requestCancellation, browserGeneration))
{
return;
}
PostPlaylistPagePlansError(requestId, code, message, retryable);
}
private void PostPlaylistPagePlansError(
string requestId,
string code,
string message,
bool retryable)
{
PostMessage("playlist-page-plans-error", new
{
requestId,
code,
message,
retryable
});
}
private void InvalidatePlaylistPagePlanBrowserRequest()
{
Interlocked.Increment(ref _playlistPagePlanBrowserGeneration);
CancelRequest(Interlocked.Exchange(ref _playlistPagePlanCancellation, null));
}
}