// // // To parse this JSON data, add NuGet 'Newtonsoft.Json' then do: // // using ErsatzTV.Core.Next; // // var playout = Playout.FromJson(jsonString); namespace ErsatzTV.Core.Next { using System; using System.Collections.Generic; using System.Globalization; using Newtonsoft.Json; using Newtonsoft.Json.Converters; /// /// A playout schedule for a single time window. /// /// Files should be named `{start}_{finish}.json` using compact ISO 8601 /// (no separators), e.g. /// `20260413T000000.000000000-0500_20260414T002131.620000000-0500.json`, /// so that the channel can locate the correct file for the current time. /// public partial class Playout { [JsonProperty("items")] public List Items { get; set; } /// /// URI identifying the schema version, e.g. "https://ersatztv.org/playout/version/0.0.1" /// [JsonProperty("version")] public string Version { get; set; } } public partial class ItemElement { /// /// RFC3339 formatted date/time, e.g. 2026-04-13T00:24:21.527-05:00 /// [JsonProperty("finish")] public string Finish { get; set; } [JsonProperty("id")] public string Id { get; set; } [JsonProperty("source")] public ItemSource Source { get; set; } /// /// RFC3339 formatted date/time, e.g. 2026-04-13T00:24:21.527-05:00 /// [JsonProperty("start")] public string Start { get; set; } [JsonProperty("tracks")] public TracksClass Tracks { get; set; } } public partial class ItemSource { [JsonProperty("in_point_ms")] public long? InPointMs { get; set; } [JsonProperty("out_point_ms")] public long? OutPointMs { get; set; } [JsonProperty("path", NullValueHandling = NullValueHandling.Ignore)] public string Path { get; set; } [JsonProperty("source_type")] public SourceType SourceType { get; set; } [JsonProperty("params", NullValueHandling = NullValueHandling.Ignore)] public string Params { get; set; } /// /// Custom HTTP headers, e.g. ["Authorization: Bearer {{TOKEN}}"] /// [JsonProperty("headers")] public List Headers { get; set; } /// /// Enable reconnect on failure (default: true) /// [JsonProperty("reconnect")] public bool? Reconnect { get; set; } /// /// Max reconnect delay in seconds /// [JsonProperty("reconnect_delay_max")] public long? ReconnectDelayMax { get; set; } /// /// Socket timeout in microseconds /// [JsonProperty("timeout_us")] public long? TimeoutUs { get; set; } /// /// URI template, e.g. "https://example.com/file.mkv?token={{MY_SECRET}}" /// [JsonProperty("uri", NullValueHandling = NullValueHandling.Ignore)] public string Uri { get; set; } /// /// Custom user-agent string /// [JsonProperty("user_agent")] public string UserAgent { get; set; } } public partial class TracksClass { [JsonProperty("audio")] public AudioClass Audio { get; set; } [JsonProperty("video")] public AudioClass Video { get; set; } } public partial class AudioClass { [JsonProperty("source", NullValueHandling = NullValueHandling.Ignore)] public AudioSource Source { get; set; } [JsonProperty("stream_index", NullValueHandling = NullValueHandling.Ignore)] public long? StreamIndex { get; set; } } public partial class AudioSource { [JsonProperty("in_point_ms")] public long? InPointMs { get; set; } [JsonProperty("out_point_ms")] public long? OutPointMs { get; set; } [JsonProperty("path", NullValueHandling = NullValueHandling.Ignore)] public string Path { get; set; } [JsonProperty("source_type")] public SourceType SourceType { get; set; } [JsonProperty("params", NullValueHandling = NullValueHandling.Ignore)] public string Params { get; set; } /// /// Custom HTTP headers, e.g. ["Authorization: Bearer {{TOKEN}}"] /// [JsonProperty("headers")] public List Headers { get; set; } /// /// Enable reconnect on failure (default: true) /// [JsonProperty("reconnect")] public bool? Reconnect { get; set; } /// /// Max reconnect delay in seconds /// [JsonProperty("reconnect_delay_max")] public long? ReconnectDelayMax { get; set; } /// /// Socket timeout in microseconds /// [JsonProperty("timeout_us")] public long? TimeoutUs { get; set; } /// /// URI template, e.g. "https://example.com/file.mkv?token={{MY_SECRET}}" /// [JsonProperty("uri", NullValueHandling = NullValueHandling.Ignore)] public string Uri { get; set; } /// /// Custom user-agent string /// [JsonProperty("user_agent")] public string UserAgent { get; set; } } public enum SourceType { Http, Lavfi, Local }; public partial class Playout { public static Playout FromJson(string json) => JsonConvert.DeserializeObject(json, ErsatzTV.Core.Next.Converter.Settings); } public static class Serialize { public static string ToJson(this Playout self) => JsonConvert.SerializeObject(self, ErsatzTV.Core.Next.Converter.Settings); } internal static class Converter { public static readonly JsonSerializerSettings Settings = new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore, MetadataPropertyHandling = MetadataPropertyHandling.Ignore, DateParseHandling = DateParseHandling.None, Converters = { SourceTypeConverter.Singleton, new IsoDateTimeConverter { DateTimeStyles = DateTimeStyles.AssumeUniversal } }, }; } internal class SourceTypeConverter : JsonConverter { public override bool CanConvert(Type t) => t == typeof(SourceType) || t == typeof(SourceType?); public override object ReadJson(JsonReader reader, Type t, object existingValue, JsonSerializer serializer) { if (reader.TokenType == JsonToken.Null) return null; var value = serializer.Deserialize(reader); switch (value) { case "http": return SourceType.Http; case "lavfi": return SourceType.Lavfi; case "local": return SourceType.Local; } throw new Exception("Cannot unmarshal type SourceType"); } public override void WriteJson(JsonWriter writer, object untypedValue, JsonSerializer serializer) { if (untypedValue == null) { serializer.Serialize(writer, null); return; } var value = (SourceType)untypedValue; switch (value) { case SourceType.Http: serializer.Serialize(writer, "http"); return; case SourceType.Lavfi: serializer.Serialize(writer, "lavfi"); return; case SourceType.Local: serializer.Serialize(writer, "local"); return; } throw new Exception("Cannot marshal type SourceType"); } public static readonly SourceTypeConverter Singleton = new SourceTypeConverter(); } }