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 OperatorBackgroundExtensions = new HashSet([".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 }); } }