Complete legacy operator UI and playout migration
This commit is contained in:
280
MainWindow.Background.cs
Normal file
280
MainWindow.Background.cs
Normal file
@@ -0,0 +1,280 @@
|
||||
using System.Text.Json;
|
||||
using MBN_STOCK_WEBVIEW.Core.Playout.Scenes;
|
||||
using MBN_STOCK_WEBVIEW.Playout.Configuration;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Windows.Storage.Pickers;
|
||||
using WinRT.Interop;
|
||||
|
||||
namespace MBN_STOCK_WEBVIEW;
|
||||
|
||||
public sealed partial class MainWindow
|
||||
{
|
||||
private static readonly IReadOnlySet<string> OperatorBackgroundExtensions =
|
||||
new HashSet<string>([".vrv", ".jpg", ".jpeg", ".png"],
|
||||
StringComparer.OrdinalIgnoreCase);
|
||||
private int _backgroundSelectionInFlight;
|
||||
|
||||
private void HandleBackgroundStatusRequest(JsonElement payload)
|
||||
{
|
||||
if (!TryParseBackgroundRequest(payload, out var requestId))
|
||||
{
|
||||
PostBackgroundError(string.Empty, "올바르지 않은 배경 상태 요청입니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
PostBackgroundStatus(requestId);
|
||||
}
|
||||
|
||||
private async Task HandleChooseBackgroundAsync(JsonElement payload)
|
||||
{
|
||||
if (!TryParseBackgroundRequest(payload, out var requestId))
|
||||
{
|
||||
PostBackgroundError(string.Empty, "올바르지 않은 배경 파일 선택 요청입니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!await _playoutCommandGate.WaitAsync(0))
|
||||
{
|
||||
PostBackgroundError(requestId, "송출 명령 처리 중에는 배경을 변경할 수 없습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
string? error = null;
|
||||
var message = "배경 파일 선택을 취소했습니다.";
|
||||
Interlocked.Exchange(ref _backgroundSelectionInFlight, 1);
|
||||
try
|
||||
{
|
||||
if (!CanChangeLegacyBackground(allowBackgroundOperation: true))
|
||||
{
|
||||
error = "배경은 IDLE 상태에서 다음 PREPARE 전에만 변경할 수 있습니다.";
|
||||
return;
|
||||
}
|
||||
|
||||
var picker = new FileOpenPicker
|
||||
{
|
||||
SuggestedStartLocation = PickerLocationId.DocumentsLibrary,
|
||||
ViewMode = PickerViewMode.List
|
||||
};
|
||||
picker.FileTypeFilter.Add(".vrv");
|
||||
picker.FileTypeFilter.Add(".jpg");
|
||||
picker.FileTypeFilter.Add(".jpeg");
|
||||
picker.FileTypeFilter.Add(".png");
|
||||
InitializeWithWindow.Initialize(picker, WindowNative.GetWindowHandle(this));
|
||||
|
||||
var selected = await picker.PickSingleFileAsync();
|
||||
if (selected is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!CanChangeLegacyBackground(allowBackgroundOperation: true))
|
||||
{
|
||||
error = "배경 파일 선택 중 송출 상태가 바뀌어 변경을 취소했습니다.";
|
||||
return;
|
||||
}
|
||||
|
||||
var composition = CreateOperatorBackgroundComposition(selected.Path);
|
||||
_legacyCompositionOptionsSource!.Update(composition);
|
||||
_lastEnabledLegacyComposition = composition;
|
||||
message = "선택한 배경을 다음 PREPARE부터 사용합니다.";
|
||||
}
|
||||
catch (Exception exception) when (
|
||||
exception is ArgumentException or IOException or UnauthorizedAccessException)
|
||||
{
|
||||
error = "배경 파일은 설정된 컷 루트 안의 vrv/jpg/jpeg/png 파일만 선택할 수 있습니다.";
|
||||
}
|
||||
catch
|
||||
{
|
||||
error = "배경 파일을 선택하지 못했습니다.";
|
||||
}
|
||||
finally
|
||||
{
|
||||
Interlocked.Exchange(ref _backgroundSelectionInFlight, 0);
|
||||
_playoutCommandGate.Release();
|
||||
|
||||
if (error is null)
|
||||
{
|
||||
PostBackgroundStatus(requestId, message);
|
||||
}
|
||||
else
|
||||
{
|
||||
PostBackgroundError(requestId, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task HandleToggleBackgroundAsync(JsonElement payload)
|
||||
{
|
||||
if (!TryParseBackgroundRequest(payload, out var requestId))
|
||||
{
|
||||
PostBackgroundError(string.Empty, "올바르지 않은 배경 토글 요청입니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!await _playoutCommandGate.WaitAsync(0))
|
||||
{
|
||||
PostBackgroundError(requestId, "송출 명령 처리 중에는 배경을 변경할 수 없습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
string? error = null;
|
||||
var message = string.Empty;
|
||||
Interlocked.Exchange(ref _backgroundSelectionInFlight, 1);
|
||||
try
|
||||
{
|
||||
if (!CanChangeLegacyBackground(allowBackgroundOperation: true))
|
||||
{
|
||||
error = "배경은 IDLE 상태에서 다음 PREPARE 전에만 변경할 수 있습니다.";
|
||||
return;
|
||||
}
|
||||
|
||||
var source = _legacyCompositionOptionsSource ??
|
||||
throw new InvalidOperationException("Background composition is unavailable.");
|
||||
var current = source.Current;
|
||||
if (current.BackgroundKind == LegacySceneBackgroundKind.None)
|
||||
{
|
||||
if (_lastEnabledLegacyComposition is null)
|
||||
{
|
||||
error = "켜 둘 배경 파일이 없습니다. 먼저 F2로 배경 파일을 선택하세요.";
|
||||
return;
|
||||
}
|
||||
|
||||
source.Update(_lastEnabledLegacyComposition);
|
||||
message = "배경 사용을 켰습니다. 다음 PREPARE부터 반영됩니다.";
|
||||
}
|
||||
else
|
||||
{
|
||||
_lastEnabledLegacyComposition = current;
|
||||
source.Update(new LegacySceneCueCompositionOptions(
|
||||
current.FadeDuration,
|
||||
LegacySceneBackgroundKind.None));
|
||||
message = "배경 사용을 껐습니다. 다음 PREPARE부터 반영됩니다.";
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
error = "배경 사용 상태를 변경하지 못했습니다.";
|
||||
}
|
||||
finally
|
||||
{
|
||||
Interlocked.Exchange(ref _backgroundSelectionInFlight, 0);
|
||||
_playoutCommandGate.Release();
|
||||
|
||||
if (error is null)
|
||||
{
|
||||
PostBackgroundStatus(requestId, message);
|
||||
}
|
||||
else
|
||||
{
|
||||
PostBackgroundError(requestId, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private LegacySceneCueCompositionOptions CreateOperatorBackgroundComposition(
|
||||
string selectedPath)
|
||||
{
|
||||
var options = _playoutOptions ??
|
||||
throw new InvalidOperationException("Playout options are unavailable.");
|
||||
var source = _legacyCompositionOptionsSource ??
|
||||
throw new InvalidOperationException("Background composition is unavailable.");
|
||||
var rootText = options.SceneDirectory?.Trim();
|
||||
if (string.IsNullOrWhiteSpace(rootText) || !Path.IsPathFullyQualified(rootText) ||
|
||||
string.IsNullOrWhiteSpace(selectedPath) || !Path.IsPathFullyQualified(selectedPath))
|
||||
{
|
||||
throw new ArgumentException("A trusted scene root and background are required.");
|
||||
}
|
||||
|
||||
var root = Path.TrimEndingDirectorySeparator(Path.GetFullPath(rootText));
|
||||
var candidate = Path.GetFullPath(selectedPath);
|
||||
var rootPrefix = root + Path.DirectorySeparatorChar;
|
||||
var extension = Path.GetExtension(candidate);
|
||||
if (!candidate.StartsWith(rootPrefix, StringComparison.OrdinalIgnoreCase) ||
|
||||
!OperatorBackgroundExtensions.Contains(extension))
|
||||
{
|
||||
throw new ArgumentException("The selected background is outside the trusted scene root.");
|
||||
}
|
||||
|
||||
var kind = string.Equals(extension, ".vrv", StringComparison.OrdinalIgnoreCase)
|
||||
? LegacySceneBackgroundKind.Video
|
||||
: LegacySceneBackgroundKind.Texture;
|
||||
var validationOptions = new PlayoutOptions
|
||||
{
|
||||
SceneDirectory = root,
|
||||
LegacySceneFadeDuration = source.Current.FadeDuration,
|
||||
LegacySceneBackgroundKind = kind,
|
||||
LegacySceneBackgroundAssetPath = Path.GetRelativePath(root, candidate),
|
||||
LegacySceneBackgroundVideoLoopCount = options.LegacySceneBackgroundVideoLoopCount,
|
||||
LegacySceneBackgroundVideoLoopInfinite = options.LegacySceneBackgroundVideoLoopInfinite
|
||||
};
|
||||
return PlayoutSceneCompositionFactory.Create(validationOptions);
|
||||
}
|
||||
|
||||
private bool CanChangeLegacyBackground(bool allowBackgroundOperation = false)
|
||||
{
|
||||
if (_playoutOptions is null || _legacyCompositionOptionsSource is null ||
|
||||
string.IsNullOrWhiteSpace(_playoutOptions.SceneDirectory) ||
|
||||
IsBrowserCorrelationQuarantined() ||
|
||||
(!allowBackgroundOperation &&
|
||||
Volatile.Read(ref _backgroundSelectionInFlight) != 0) ||
|
||||
Volatile.Read(ref _playoutCommandInFlight) != 0 ||
|
||||
Volatile.Read(ref _playoutShutdownStarted) != 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var status = _playoutEngine?.Status;
|
||||
var workflow = _playoutWorkflow?.State;
|
||||
return status is not null && workflow is not null &&
|
||||
!status.IsPlayCompletionPending &&
|
||||
string.IsNullOrWhiteSpace(status.PreparedSceneName) &&
|
||||
string.IsNullOrWhiteSpace(status.OnAirSceneName) &&
|
||||
string.IsNullOrWhiteSpace(workflow.PreparedCutCode) &&
|
||||
string.IsNullOrWhiteSpace(workflow.OnAirCutCode) &&
|
||||
(_legacyRefreshTask is null || _legacyRefreshTask.IsCompleted);
|
||||
}
|
||||
|
||||
private static bool TryParseBackgroundRequest(JsonElement payload, out string requestId)
|
||||
{
|
||||
requestId = string.Empty;
|
||||
return payload.ValueKind == JsonValueKind.Object &&
|
||||
HasOnlyProperties(payload, "requestId") &&
|
||||
TryGetSafeToken(
|
||||
payload,
|
||||
"requestId",
|
||||
MaximumPlayoutRequestIdLength,
|
||||
out requestId);
|
||||
}
|
||||
|
||||
private void PostBackgroundStatus(string requestId, string? message = null)
|
||||
{
|
||||
var current = _legacyCompositionOptionsSource?.Current ??
|
||||
LegacySceneCueCompositionOptions.Default;
|
||||
var enabled = current.BackgroundKind != LegacySceneBackgroundKind.None;
|
||||
PostMessage("background-status", new
|
||||
{
|
||||
requestId,
|
||||
enabled,
|
||||
kind = current.BackgroundKind switch
|
||||
{
|
||||
LegacySceneBackgroundKind.Texture => "texture",
|
||||
LegacySceneBackgroundKind.Video => "video",
|
||||
_ => "none"
|
||||
},
|
||||
fileName = enabled ? Path.GetFileName(current.BackgroundAssetPath) : string.Empty,
|
||||
canChange = CanChangeLegacyBackground(),
|
||||
message = message ?? (enabled
|
||||
? "선택한 배경은 다음 PREPARE부터 적용됩니다."
|
||||
: "배경 사용 안 함")
|
||||
});
|
||||
}
|
||||
|
||||
private void PostBackgroundError(string requestId, string message)
|
||||
{
|
||||
PostMessage("background-error", new
|
||||
{
|
||||
requestId,
|
||||
message
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user