265 lines
8.9 KiB
C#
265 lines
8.9 KiB
C#
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Loads only the specified JSON file and deliberately ignores all environment
|
|
/// overrides. Intended for explicit, reproducible diagnostic commands.
|
|
/// </summary>
|
|
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<PlayoutOptions>(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<string> apply)
|
|
{
|
|
var value = Environment.GetEnvironmentVariable(name);
|
|
if (!string.IsNullOrWhiteSpace(value))
|
|
{
|
|
apply(value.Trim());
|
|
}
|
|
}
|
|
|
|
private static void ApplyInt(string name, Action<int> 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<int?> 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<bool> 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<TEnum>(string name, Action<TEnum> apply)
|
|
where TEnum : struct, Enum
|
|
{
|
|
var text = Environment.GetEnvironmentVariable(name);
|
|
if (string.IsNullOrWhiteSpace(text))
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!Enum.TryParse<TEnum>(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)
|
|
{
|
|
}
|
|
}
|