using System.Text.Json; using System.Text.Json.Serialization; using MBN_STOCK_WEBVIEW.Core.Playout; using MBN_STOCK_WEBVIEW.Core.Playout.Scenes; namespace MBN_STOCK_WEBVIEW.Playout.Configuration; public static class PlayoutOptionsLoader { public const string LiveAuthorizationEnvironmentVariable = "MBN_STOCK_PLAYOUT_AUTHORIZE_LIVE_OUTPUT"; public const string RequiredLiveAuthorizationValue = "I_AUTHORIZE_LIVE_PROGRAM_OUTPUT_FOR_THIS_LAUNCH"; public static string DefaultPath => Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "MBN_STOCK_WEBVIEW", "Config", "playout.local.json"); public static PlayoutOptions Load(string? path = null) { var options = LoadJson(path ?? DefaultPath); ApplyEnvironment(options); ApplyExecutableAssetDefaults(options, AppContext.BaseDirectory); return options; } /// /// Loads only the specified JSON file and deliberately ignores all environment /// overrides. Intended for explicit, reproducible diagnostic commands. /// public static PlayoutOptions LoadFileOnly(string path) { ArgumentException.ThrowIfNullOrWhiteSpace(path); return LoadJson(path); } internal static bool IsLiveAuthorizedForThisLaunch() => string.Equals( Environment.GetEnvironmentVariable(LiveAuthorizationEnvironmentVariable), RequiredLiveAuthorizationValue, StringComparison.Ordinal); internal static void ApplyExecutableAssetDefaults( PlayoutOptions options, string? baseDirectory) { ArgumentNullException.ThrowIfNull(options); if (!string.IsNullOrWhiteSpace(options.SceneDirectory) || string.IsNullOrWhiteSpace(baseDirectory)) { return; } try { var cutsDirectory = Path.Combine( Path.GetFullPath(baseDirectory), "Cuts"); if (Directory.Exists(cutsDirectory)) { options.SceneDirectory = cutsDirectory; } } catch (Exception exception) when ( exception is ArgumentException or IOException or NotSupportedException or System.Security.SecurityException) { // A missing or invalid executable asset root leaves the existing safe // configuration unchanged; validation reports it if a command needs it. } } private static PlayoutOptions LoadJson(string path) { if (!File.Exists(path)) { return new PlayoutOptions(); } try { var json = File.ReadAllText(path); return JsonSerializer.Deserialize(json, JsonOptions()) ?? new PlayoutOptions(); } catch (JsonException exception) { throw new PlayoutConfigurationException( "송출 설정 파일 형식이 올바르지 않습니다.", exception); } catch (IOException exception) { throw new PlayoutConfigurationException( "송출 설정 파일을 읽을 수 없습니다.", exception); } catch (UnauthorizedAccessException exception) { throw new PlayoutConfigurationException( "송출 설정 파일에 접근할 수 없습니다.", exception); } } private static void ApplyEnvironment(PlayoutOptions options) { ApplyEnum( "MBN_STOCK_PLAYOUT_MODE", (PlayoutMode value) => options.Mode = value); ApplyString("MBN_STOCK_PLAYOUT_HOST", value => options.Host = value); ApplyInt("MBN_STOCK_PLAYOUT_PORT", value => options.Port = value); ApplyInt("MBN_STOCK_PLAYOUT_TCP_MODE", value => options.TcpMode = value); ApplyInt("MBN_STOCK_PLAYOUT_CLIENT_PORT", value => options.ClientPort = value); ApplyNullableInt("MBN_STOCK_PLAYOUT_OUTPUT_CHANNEL", value => options.OutputChannel = value); ApplyInt("MBN_STOCK_PLAYOUT_LAYOUT_INDEX", value => options.LayoutIndex = value); ApplyInt( "MBN_STOCK_PLAYOUT_LEGACY_FADE_DURATION", value => options.LegacySceneFadeDuration = value); ApplyEnum( "MBN_STOCK_PLAYOUT_LEGACY_BACKGROUND_KIND", (LegacySceneBackgroundKind value) => options.LegacySceneBackgroundKind = value); ApplyString( "MBN_STOCK_PLAYOUT_LEGACY_BACKGROUND_ASSET", value => options.LegacySceneBackgroundAssetPath = value); ApplyInt( "MBN_STOCK_PLAYOUT_LEGACY_BACKGROUND_VIDEO_LOOP_COUNT", value => options.LegacySceneBackgroundVideoLoopCount = value); ApplyBool( "MBN_STOCK_PLAYOUT_LEGACY_BACKGROUND_VIDEO_LOOP_INFINITE", value => options.LegacySceneBackgroundVideoLoopInfinite = value); ApplyString( "MBN_STOCK_PLAYOUT_LEGACY_BACKGROUND_DIRECTORY", value => options.LegacyBackgroundDirectory = value); ApplyString("MBN_STOCK_PLAYOUT_SCENE_DIRECTORY", value => options.SceneDirectory = value); ApplyString( "MBN_STOCK_PLAYOUT_TEST_WINDOW_TITLE_PATTERN", value => options.TestProcessWindowTitlePattern = value); ApplyInt("MBN_STOCK_PLAYOUT_QUEUE_CAPACITY", value => options.QueueCapacity = value); ApplyInt( "MBN_STOCK_PLAYOUT_CONNECT_TIMEOUT_MS", value => options.ConnectTimeoutMilliseconds = value); ApplyInt( "MBN_STOCK_PLAYOUT_OPERATION_TIMEOUT_MS", value => options.OperationTimeoutMilliseconds = value); ApplyInt( "MBN_STOCK_PLAYOUT_DISCONNECT_TIMEOUT_MS", value => options.DisconnectTimeoutMilliseconds = value); ApplyInt( "MBN_STOCK_PLAYOUT_PROCESS_POLL_INTERVAL_MS", value => options.ProcessPollIntervalMilliseconds = value); ApplyInt( "MBN_STOCK_PLAYOUT_RECONNECT_DELAY_MS", value => options.ReconnectDelayMilliseconds = value); ApplyInt( "MBN_STOCK_PLAYOUT_MAXIMUM_RECONNECT_ATTEMPTS", value => options.MaximumReconnectAttempts = value); ApplyBool( "MBN_STOCK_PLAYOUT_RECONNECT_ENABLED", value => options.ReconnectEnabled = value); } private static JsonSerializerOptions JsonOptions() { var result = new JsonSerializerOptions { PropertyNameCaseInsensitive = true, ReadCommentHandling = JsonCommentHandling.Skip, AllowTrailingCommas = true }; result.Converters.Add(new JsonStringEnumConverter()); return result; } private static void ApplyString(string name, Action apply) { var value = Environment.GetEnvironmentVariable(name); if (!string.IsNullOrWhiteSpace(value)) { apply(value.Trim()); } } private static void ApplyInt(string name, Action apply) { var text = Environment.GetEnvironmentVariable(name); if (string.IsNullOrWhiteSpace(text)) { return; } if (!int.TryParse(text, out var value)) { throw InvalidEnvironment(name); } apply(value); } private static void ApplyNullableInt(string name, Action apply) { var text = Environment.GetEnvironmentVariable(name); if (string.IsNullOrWhiteSpace(text)) { return; } if (!int.TryParse(text, out var value)) { throw InvalidEnvironment(name); } apply(value); } private static void ApplyBool(string name, Action apply) { var text = Environment.GetEnvironmentVariable(name); if (string.IsNullOrWhiteSpace(text)) { return; } if (!bool.TryParse(text, out var value)) { throw InvalidEnvironment(name); } apply(value); } private static void ApplyEnum(string name, Action apply) where TEnum : struct, Enum { var text = Environment.GetEnvironmentVariable(name); if (string.IsNullOrWhiteSpace(text)) { return; } if (!Enum.TryParse(text, true, out var value) || !Enum.IsDefined(value)) { throw InvalidEnvironment(name); } apply(value); } private static PlayoutConfigurationException InvalidEnvironment(string name) => new($"환경 변수 {name} 값이 올바르지 않습니다."); } public sealed class PlayoutConfigurationException : Exception { public PlayoutConfigurationException(string message) : base(message) { } public PlayoutConfigurationException(string message, Exception innerException) : base(message, innerException) { } }