// // // 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 { /// /// Ordered list of scheduled items for this window. /// [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; } } /// /// A single scheduled item in the playout. /// /// An item must supply media for its tracks. You can do this two ways, and they can be /// combined: /// /// 1. Set `source` to use one shared source for every track. Track details (which stream, /// etc.) are chosen naively, or may be refined via `tracks`. /// 2. Set `tracks` to specify each track (video, audio) individually. A track may provide /// its own `source`; if it doesn't, the item's top-level `source` is used. /// /// At least one of `source` or `tracks` must be present, and every track that is selected /// must have an effective source (either its own or the item's). /// public partial class PlayoutItem { /// /// RFC3339 formatted finish time, e.g. 2026-04-13T00:54:21.527-05:00. /// [JsonProperty("finish")] public DateTimeOffset Finish { get; set; } /// /// Stable identifier for this item, unique within the playout. /// [JsonProperty("id")] public string Id { get; set; } /// /// The default source shared by any track that doesn't specify its own. Required unless /// every selected track in `tracks` provides its own `source`. /// [JsonProperty("source")] public Source Source { get; set; } /// /// RFC3339 formatted start time, e.g. 2026-04-13T00:24:21.527-05:00. /// [JsonProperty("start")] public DateTimeOffset Start { get; set; } /// /// Per-track selection. Omit to let the server pick the first video and first audio track of /// the item's `source`. /// [JsonProperty("tracks")] public PlayoutItemTracks Tracks { get; set; } } /// /// A file on the local filesystem reachable by the server. /// /// A synthetic source produced by an ffmpeg lavfi filter graph. /// /// A remote source fetched over HTTP(S). /// public partial class Source { /// /// Optional start offset into the source, in milliseconds. /// [JsonProperty("in_point_ms")] public long? InPointMs { get; set; } /// /// Optional end offset into the source, in milliseconds. /// [JsonProperty("out_point_ms")] public long? OutPointMs { get; set; } /// /// Absolute path to the media file. /// [JsonProperty("path", NullValueHandling = NullValueHandling.Ignore)] public string Path { get; set; } [JsonProperty("source_type")] public SourceType SourceType { get; set; } /// /// The lavfi filter graph parameters, passed verbatim to ffmpeg's `-f lavfi -i`. /// [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; } /// /// Maximum reconnect delay in seconds. Maps to ffmpeg's `reconnect_delay_max`. /// [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; } } /// /// Per-track overrides for a playout item. Omit a field to use the server default for that /// track kind (first stream of that kind in the item's `source`, if any). /// public partial class PlayoutItemTracks { /// /// Audio track selection. /// [JsonProperty("audio")] public TrackSelection Audio { get; set; } /// /// Subtitle track selection. /// [JsonProperty("subtitle")] public TrackSelection Subtitle { get; set; } /// /// Video track selection. /// [JsonProperty("video")] public TrackSelection Video { get; set; } } /// /// Selects a single track (video or audio). /// /// - `source` absent: inherit the parent `PlayoutItem.source`. /// - `source` present: use this source for this track, overriding the parent. /// - `stream_index` absent: server picks the first stream of the track's kind in the /// effective source. /// - `stream_index` present: use this specific stream within the effective source. /// public partial class TrackSelection { /// /// Source to pull this track from. If omitted, inherits from the parent `PlayoutItem.source`. /// [JsonProperty("source")] public Source Source { get; set; } /// /// Zero-based stream index within the effective source. If omitted, the server picks the /// first stream of this track's kind. /// [JsonProperty("stream_index")] public long? StreamIndex { 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(); } }