feat: add safe Tornado K3D playout adapter

This commit is contained in:
2026-07-10 06:41:48 +09:00
parent 39c4504b87
commit 5a8c7028dc
43 changed files with 7341 additions and 92 deletions

View File

@@ -0,0 +1,201 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using MBN_STOCK_WEBVIEW.Core.Playout;
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);
return options;
}
internal static bool IsLiveAuthorizedForThisLaunch() =>
string.Equals(
Environment.GetEnvironmentVariable(LiveAuthorizationEnvironmentVariable),
RequiredLiveAuthorizationValue,
StringComparison.Ordinal);
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", 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);
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(string name, Action<PlayoutMode> apply)
{
var text = Environment.GetEnvironmentVariable(name);
if (string.IsNullOrWhiteSpace(text))
{
return;
}
if (!Enum.TryParse<PlayoutMode>(text, true, out var 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)
{
}
}