// // // 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; } /// /// Watermark (image/video overlay) to composite on top of the primary content for the /// duration of this item. Omit for no watermark. /// [JsonProperty("watermark")] public Watermark Watermark { 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). /// /// A live stream pulled from an RTSP server (e.g. an IP camera). Always treated as live: it /// is never seeked and cannot work ahead. /// /// An external command whose stdout is an MPEG-TS stream, proxied to ffmpeg over loopback /// HTTP. /// /// A placeholder source resolved at playback time by fetching a `PlayoutItem` JSON document /// over HTTP(S). The returned item replaces this one; its `start` is forced to the current /// transcode position and its `finish` is clamped to the placeholder's `finish`. Because /// `start` advances each tick, the resolver is re-hit while the transcode position remains /// inside the placeholder window, allowing a sequence of distinct items to be returned for a /// single placeholder. The resolved item's `source` may not itself be `dynamic`, but it may /// carry its own `probe_hint`, which is honored exactly as for a directly-specified source. /// /// The channel automatically injects the following request headers (in addition to any /// configured via `headers`), all formatted per RFC 3339 for timestamps: /// /// - `x-etv-channel` — the channel number this request is for. /// - `x-etv-dynamic-id` — a stable identifier for the placeholder; resolvers can use it to /// coalesce or cache decisions across the multiple calls that occur while transcoding stays /// inside one placeholder window. /// - `x-etv-now` — the current transcode position; this is the `start` that will be forced /// onto the resolved item. /// - `x-etv-until` — the placeholder's `finish`; the resolver should not return an item that /// extends beyond this (it will be clamped if it does). /// 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; } /// /// Optional pre-supplied probe metadata. When present, ffprobe is skipped and the source is /// read only once (at playback). See `ProbeHint`. /// /// Optional pre-supplied probe metadata. When present, ffprobe is skipped and the source is /// read only once (at playback). Especially useful here: a scripted source (e.g. a yt-dlp /// pipeline) is otherwise opened twice, once to probe and once to play. See `ProbeHint`. /// [JsonProperty("probe_hint")] public ProbeHint ProbeHint { 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}}"]. /// /// Custom HTTP headers, e.g. ["Authorization: Bearer {{TOKEN}}"]. Supports {{TEMPLATE}} /// expansion. /// [JsonProperty("headers")] public List Headers { get; set; } /// /// Enable persistent connections in ffmpeg. Default: false. /// [JsonProperty("keep_alive")] public bool? KeepAlive { 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. /// /// Request timeout in microseconds. Defaults to 10 seconds. /// [JsonProperty("timeout_us")] public long? TimeoutUs { get; set; } /// /// URI template, e.g. "https://example.com/file.mkv?token={{MY_SECRET}}". /// /// RTSP URI template, e.g. "rtsp://user:{{PASSWORD}}@camera.lan:554/stream". /// /// URI template for the resolver endpoint, e.g. /// "http://localhost:8409/fallback?channel=5&token={{MY_SECRET}}". Must respond with a JSON /// `PlayoutItem`. /// [JsonProperty("uri", NullValueHandling = NullValueHandling.Ignore)] public string Uri { get; set; } /// /// Custom User-Agent string. /// /// Custom User-Agent string. Supports {{TEMPLATE}} expansion. /// [JsonProperty("user_agent")] public string UserAgent { get; set; } /// /// Optional arguments for the command. Supports {{TEMPLATE}} expansion. Defaults to []. /// [JsonProperty("args", NullValueHandling = NullValueHandling.Ignore)] public List Args { get; set; } /// /// Command that writes an MPEG-TS stream to its stdout. Supports {{TEMPLATE}} expansion. /// [JsonProperty("command", NullValueHandling = NullValueHandling.Ignore)] public string Command { get; set; } /// /// Whether the content is live and therefore cannot work ahead. Default: false. /// [JsonProperty("is_live")] public bool? IsLive { get; set; } } /// /// Pre-supplied probe metadata for a source. When present, the server trusts these values /// and skips running ffprobe entirely, so the source is opened only once (at playback) /// instead of twice. This matters most for slow or expensive inputs such as scripted yt-dlp /// pipelines. /// /// The hint fully replaces probing: values are not validated up front, so incorrect metadata /// surfaces as an ffmpeg error during playback rather than a probe failure. Provide an entry /// for every stream the pipeline needs to select — typically one video and one audio, plus /// any subtitle stream a track selects. /// public partial class ProbeHint { /// /// Audio streams in the source. Omit (or use `[]`) for video-only sources; defaults to empty. /// [JsonProperty("audio", NullValueHandling = NullValueHandling.Ignore)] public List Audio { get; set; } /// /// Source duration in milliseconds. Omit for live or unbounded sources. /// [JsonProperty("duration_ms")] public long? DurationMs { get; set; } /// /// Container format name as reported by ffprobe's `format_name` (e.g. "mpegts", "image2", /// "png_pipe"). Used to detect still images (a single video stream in an `image2` or /// `*_pipe` container). Defaults to "mpegts" when omitted. /// [JsonProperty("format_name")] public string FormatName { get; set; } /// /// Subtitle streams in the source. Omit (or use `[]`) for sources without subtitles; /// defaults to empty. Provide an entry for any subtitle stream a track selects, otherwise /// the requested subtitle stream cannot be located and subtitles are dropped. /// [JsonProperty("subtitle", NullValueHandling = NullValueHandling.Ignore)] public List Subtitle { get; set; } /// /// Video (and still-image) streams in the source. Omit (or use `[]`) for audio-only sources; /// defaults to empty. /// [JsonProperty("video", NullValueHandling = NullValueHandling.Ignore)] public List Video { get; set; } } /// /// Probe metadata for a single audio stream. /// public partial class AudioHint { /// /// Number of audio channels. /// [JsonProperty("channels")] public long Channels { get; set; } /// /// Codec name as reported by ffprobe (e.g. "aac", "ac3", "mp2"). Compared case-insensitively. /// [JsonProperty("codec")] public string Codec { get; set; } /// /// Zero-based index of this stream within the source. /// [JsonProperty("stream_index")] public long StreamIndex { get; set; } } /// /// Probe metadata for a single subtitle stream. /// public partial class SubtitleHint { /// /// Codec name as reported by ffprobe (e.g. "subrip", "ass", "hdmv_pgs_subtitle", /// "dvd_subtitle"). Compared case-insensitively; image-based codecs are handled differently /// from text-based ones. /// [JsonProperty("codec")] public string Codec { get; set; } /// /// Zero-based index of this stream within the source. /// [JsonProperty("stream_index")] public long StreamIndex { get; set; } } /// /// Probe metadata for a single video (or still-image) stream. Optional fields left out /// assume progressive, square-pixel, SDR content at 24 fps — the same fallbacks used when /// ffprobe omits a field. /// public partial class VideoHint { /// /// Codec name as reported by ffprobe (e.g. "h264", "hevc", "mpeg2video", "png"). Compared /// case-insensitively. /// [JsonProperty("codec")] public string Codec { get; set; } /// /// Color primaries (e.g. "bt709", "bt2020"). Omit if unknown. /// [JsonProperty("color_primaries")] public string ColorPrimaries { get; set; } /// /// Color range (e.g. "tv", "pc"). Omit if unknown. /// [JsonProperty("color_range")] public string ColorRange { get; set; } /// /// Color space / matrix coefficients (e.g. "bt709", "bt2020nc"). Omit if unknown. /// [JsonProperty("color_space")] public string ColorSpace { get; set; } /// /// Color transfer characteristics (e.g. "bt709", "smpte2084", "arib-std-b67"). The values /// "smpte2084" and "arib-std-b67" mark the stream as HDR. Omit for SDR. /// [JsonProperty("color_transfer")] public string ColorTransfer { get; set; } /// /// Display aspect ratio, e.g. "16:9". Omit if unknown. /// [JsonProperty("display_aspect_ratio")] public string DisplayAspectRatio { get; set; } /// /// Interlacing field order as reported by ffprobe ("progressive", "tt", "bb", "tb", "bt"). /// Omit for progressive. /// [JsonProperty("field_order")] public string FieldOrder { get; set; } /// /// Frame rate as a number or rational string (e.g. "24", "30000/1001"). Defaults to 24 when /// omitted. /// [JsonProperty("frame_rate")] public string FrameRate { get; set; } /// /// Coded frame height in pixels. /// [JsonProperty("height")] public long Height { get; set; } /// /// Pixel format (e.g. "yuv420p", "yuv420p10le"). /// [JsonProperty("pix_fmt")] public string PixFmt { get; set; } /// /// Codec profile (e.g. "high", "main 10"). Compared case-insensitively. Omit if unknown. /// [JsonProperty("profile")] public string Profile { get; set; } /// /// Sample (pixel) aspect ratio, e.g. "1:1". Omit for square pixels. /// [JsonProperty("sample_aspect_ratio")] public string SampleAspectRatio { get; set; } /// /// Zero-based index of this stream within the source. /// [JsonProperty("stream_index")] public long StreamIndex { get; set; } /// /// Coded frame width in pixels. /// [JsonProperty("width")] public long Width { 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; } } /// /// An image or video overlay composited on top of the primary content. Sized and positioned /// relative to the primary content's frame. /// public partial class Watermark { /// /// Horizontal offset from the anchor `location`, as a percent of primary content width /// (0–100). Omit for 0. /// [JsonProperty("horizontal_margin_percent")] [JsonConverter(typeof(MinMaxValueCheckConverter))] public double? HorizontalMarginPercent { get; set; } /// /// Anchor position within the primary content frame. /// [JsonProperty("location")] public WatermarkLocation Location { get; set; } /// /// Opacity as a percent (0–100). Omit for fully opaque (100). /// [JsonProperty("opacity_percent")] [JsonConverter(typeof(MinMaxValueCheckConverter))] public double? OpacityPercent { get; set; } /// /// The source providing the watermark media (typically an image, but any `PlayoutItemSource` /// is accepted). /// [JsonProperty("source")] public PlayoutItemSource Source { get; set; } /// /// Zero-based stream index within the source. If omitted, the server picks the first video /// stream. /// [JsonProperty("stream_index")] public long? StreamIndex { get; set; } /// /// Visibility schedule for the watermark. Omit for an always-on watermark. /// [JsonProperty("timing")] public Timing Timing { get; set; } /// /// Vertical offset from the anchor `location`, as a percent of primary content height /// (0–100). Omit for 0. /// [JsonProperty("vertical_margin_percent")] [JsonConverter(typeof(MinMaxValueCheckConverter))] public double? VerticalMarginPercent { get; set; } /// /// Scale the watermark to this percent of the primary content width (0–100). Omit to use the /// watermark's actual size. /// [JsonProperty("width_percent")] [JsonConverter(typeof(MinMaxValueCheckConverter))] public double? WidthPercent { get; set; } /// /// When true, position margins are measured from the edges of the source content rather than /// the padded output frame, so letterbox/pillarbox bars push the watermark inward and keep /// it inside the visible content. When false, margins are relative to the full padded frame, /// so a 0% margin can land inside the bars. Has no effect when the primary content fills the /// output (crop/stretch). Omit for false. /// [JsonProperty("within_source_content")] public bool? WithinSourceContent { get; set; } } /// /// A media source. Exactly one variant, distinguished by `source_type`. /// /// The source providing the watermark media (typically an image, but any `PlayoutItemSource` /// is accepted). /// /// 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). /// /// A live stream pulled from an RTSP server (e.g. an IP camera). Always treated as live: it /// is never seeked and cannot work ahead. /// /// An external command whose stdout is an MPEG-TS stream, proxied to ffmpeg over loopback /// HTTP. /// /// A placeholder source resolved at playback time by fetching a `PlayoutItem` JSON document /// over HTTP(S). The returned item replaces this one; its `start` is forced to the current /// transcode position and its `finish` is clamped to the placeholder's `finish`. Because /// `start` advances each tick, the resolver is re-hit while the transcode position remains /// inside the placeholder window, allowing a sequence of distinct items to be returned for a /// single placeholder. The resolved item's `source` may not itself be `dynamic`, but it may /// carry its own `probe_hint`, which is honored exactly as for a directly-specified source. /// /// The channel automatically injects the following request headers (in addition to any /// configured via `headers`), all formatted per RFC 3339 for timestamps: /// /// - `x-etv-channel` — the channel number this request is for. /// - `x-etv-dynamic-id` — a stable identifier for the placeholder; resolvers can use it to /// coalesce or cache decisions across the multiple calls that occur while transcoding stays /// inside one placeholder window. /// - `x-etv-now` — the current transcode position; this is the `start` that will be forced /// onto the resolved item. /// - `x-etv-until` — the placeholder's `finish`; the resolver should not return an item that /// extends beyond this (it will be clamped if it does). /// public partial class PlayoutItemSource { /// /// 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; } /// /// Optional pre-supplied probe metadata. When present, ffprobe is skipped and the source is /// read only once (at playback). See `ProbeHint`. /// /// Optional pre-supplied probe metadata. When present, ffprobe is skipped and the source is /// read only once (at playback). Especially useful here: a scripted source (e.g. a yt-dlp /// pipeline) is otherwise opened twice, once to probe and once to play. See `ProbeHint`. /// [JsonProperty("probe_hint")] public ProbeHint ProbeHint { 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}}"]. /// /// Custom HTTP headers, e.g. ["Authorization: Bearer {{TOKEN}}"]. Supports {{TEMPLATE}} /// expansion. /// [JsonProperty("headers")] public List Headers { get; set; } /// /// Enable persistent connections in ffmpeg. Default: false. /// [JsonProperty("keep_alive")] public bool? KeepAlive { 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. /// /// Request timeout in microseconds. Defaults to 10 seconds. /// [JsonProperty("timeout_us")] public long? TimeoutUs { get; set; } /// /// URI template, e.g. "https://example.com/file.mkv?token={{MY_SECRET}}". /// /// RTSP URI template, e.g. "rtsp://user:{{PASSWORD}}@camera.lan:554/stream". /// /// URI template for the resolver endpoint, e.g. /// "http://localhost:8409/fallback?channel=5&token={{MY_SECRET}}". Must respond with a JSON /// `PlayoutItem`. /// [JsonProperty("uri", NullValueHandling = NullValueHandling.Ignore)] public string Uri { get; set; } /// /// Custom User-Agent string. /// /// Custom User-Agent string. Supports {{TEMPLATE}} expansion. /// [JsonProperty("user_agent")] public string UserAgent { get; set; } /// /// Optional arguments for the command. Supports {{TEMPLATE}} expansion. Defaults to []. /// [JsonProperty("args", NullValueHandling = NullValueHandling.Ignore)] public List Args { get; set; } /// /// Command that writes an MPEG-TS stream to its stdout. Supports {{TEMPLATE}} expansion. /// [JsonProperty("command", NullValueHandling = NullValueHandling.Ignore)] public string Command { get; set; } /// /// Whether the content is live and therefore cannot work ahead. Default: false. /// [JsonProperty("is_live")] public bool? IsLive { get; set; } } /// /// Controls when the watermark is shown. Exactly one variant, distinguished by /// `timing_type`. /// /// Cyclical visibility: the watermark fades in, holds, and fades out once every /// `frequency_ms`. Cycle length is start-to-start, so `2 * fade_ms + hold_ms` must be ≤ /// `frequency_ms`, and `fade_ms` must be ≤ `hold_ms`. /// public partial class Timing { /// /// Reference clock used to position cycles. /// [JsonProperty("clock")] public PeriodicClock Clock { get; set; } /// /// Cap on the time, measured from the start of the playout item in milliseconds, during /// which new appearances are allowed to begin. An appearance already in progress at the cap /// is allowed to fade out cleanly. Omit for no cap. /// [JsonProperty("disable_after_ms")] public long? DisableAfterMs { get; set; } /// /// Fade-in and fade-out duration in milliseconds (symmetric). Must be ≤ `hold_ms`. Omit for /// 1000; set to 0 for hard cuts. /// [JsonProperty("fade_ms")] public long? FadeMs { get; set; } /// /// Period of the cycle, start-to-start, in milliseconds. /// [JsonProperty("frequency_ms")] public long FrequencyMs { get; set; } /// /// Time held fully visible between fade-in and fade-out, in milliseconds. /// [JsonProperty("hold_ms")] public long HoldMs { get; set; } /// /// Shift the cycle by this many milliseconds. With `clock: wall` and `frequency_ms: 300000`, /// an offset of 0 puts an appearance start at every wall-clock 5-minute boundary; 120000 /// shifts to :02, :07, :12, etc. Omit for 0. /// [JsonProperty("phase_offset_ms")] public long? PhaseOffsetMs { get; set; } [JsonProperty("timing_type")] public TimingType TimingType { get; set; } } public enum SourceType { Dynamic, Http, Lavfi, Local, Rtsp, Script }; /// /// Anchor position within the primary content frame. /// /// Nine-position anchor within the primary content frame. Read like a 3×3 grid: rows /// top/center/bottom, columns left/center/right; the dead center is `center`. /// public enum WatermarkLocation { BottomCenter, BottomLeft, BottomRight, Center, CenterLeft, CenterRight, TopCenter, TopLeft, TopRight }; /// /// Reference clock used to position cycles. /// /// Reference clock for periodic timing. `wall` aligns cycles to wall-clock time (so a viewer /// tuning in at any moment sees the same phase). `content` measures from the start of the /// containing playout item. /// public enum PeriodicClock { Content, Wall }; public enum TimingType { Periodic }; 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); } public static class Converter { public static readonly JsonSerializerSettings Settings = new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore, MetadataPropertyHandling = MetadataPropertyHandling.Ignore, DateParseHandling = DateParseHandling.None, Converters = { SourceTypeConverter.Singleton, WatermarkLocationConverter.Singleton, PeriodicClockConverter.Singleton, TimingTypeConverter.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 "dynamic": return SourceType.Dynamic; case "http": return SourceType.Http; case "lavfi": return SourceType.Lavfi; case "local": return SourceType.Local; case "rtsp": return SourceType.Rtsp; case "script": return SourceType.Script; } 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.Dynamic: serializer.Serialize(writer, "dynamic"); return; case SourceType.Http: serializer.Serialize(writer, "http"); return; case SourceType.Lavfi: serializer.Serialize(writer, "lavfi"); return; case SourceType.Local: serializer.Serialize(writer, "local"); return; case SourceType.Rtsp: serializer.Serialize(writer, "rtsp"); return; case SourceType.Script: serializer.Serialize(writer, "script"); return; } throw new Exception("Cannot marshal type SourceType"); } public static readonly SourceTypeConverter Singleton = new SourceTypeConverter(); } internal class MinMaxValueCheckConverter : JsonConverter { public override bool CanConvert(Type t) => t == typeof(double) || t == typeof(double?); public override object ReadJson(JsonReader reader, Type t, object existingValue, JsonSerializer serializer) { if (reader.TokenType == JsonToken.Null) return null; var value = serializer.Deserialize(reader); if (value >= 0 && value <= 100) { return value; } throw new Exception("Cannot unmarshal type double"); } public override void WriteJson(JsonWriter writer, object untypedValue, JsonSerializer serializer) { if (untypedValue == null) { serializer.Serialize(writer, null); return; } var value = (double)untypedValue; if (value >= 0 && value <= 100) { serializer.Serialize(writer, value); return; } throw new Exception("Cannot marshal type double"); } public static readonly MinMaxValueCheckConverter Singleton = new MinMaxValueCheckConverter(); } internal class WatermarkLocationConverter : JsonConverter { public override bool CanConvert(Type t) => t == typeof(WatermarkLocation) || t == typeof(WatermarkLocation?); 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 "bottom_center": return WatermarkLocation.BottomCenter; case "bottom_left": return WatermarkLocation.BottomLeft; case "bottom_right": return WatermarkLocation.BottomRight; case "center": return WatermarkLocation.Center; case "center_left": return WatermarkLocation.CenterLeft; case "center_right": return WatermarkLocation.CenterRight; case "top_center": return WatermarkLocation.TopCenter; case "top_left": return WatermarkLocation.TopLeft; case "top_right": return WatermarkLocation.TopRight; } throw new Exception("Cannot unmarshal type WatermarkLocation"); } public override void WriteJson(JsonWriter writer, object untypedValue, JsonSerializer serializer) { if (untypedValue == null) { serializer.Serialize(writer, null); return; } var value = (WatermarkLocation)untypedValue; switch (value) { case WatermarkLocation.BottomCenter: serializer.Serialize(writer, "bottom_center"); return; case WatermarkLocation.BottomLeft: serializer.Serialize(writer, "bottom_left"); return; case WatermarkLocation.BottomRight: serializer.Serialize(writer, "bottom_right"); return; case WatermarkLocation.Center: serializer.Serialize(writer, "center"); return; case WatermarkLocation.CenterLeft: serializer.Serialize(writer, "center_left"); return; case WatermarkLocation.CenterRight: serializer.Serialize(writer, "center_right"); return; case WatermarkLocation.TopCenter: serializer.Serialize(writer, "top_center"); return; case WatermarkLocation.TopLeft: serializer.Serialize(writer, "top_left"); return; case WatermarkLocation.TopRight: serializer.Serialize(writer, "top_right"); return; } throw new Exception("Cannot marshal type WatermarkLocation"); } public static readonly WatermarkLocationConverter Singleton = new WatermarkLocationConverter(); } internal class PeriodicClockConverter : JsonConverter { public override bool CanConvert(Type t) => t == typeof(PeriodicClock) || t == typeof(PeriodicClock?); 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 "content": return PeriodicClock.Content; case "wall": return PeriodicClock.Wall; } throw new Exception("Cannot unmarshal type PeriodicClock"); } public override void WriteJson(JsonWriter writer, object untypedValue, JsonSerializer serializer) { if (untypedValue == null) { serializer.Serialize(writer, null); return; } var value = (PeriodicClock)untypedValue; switch (value) { case PeriodicClock.Content: serializer.Serialize(writer, "content"); return; case PeriodicClock.Wall: serializer.Serialize(writer, "wall"); return; } throw new Exception("Cannot marshal type PeriodicClock"); } public static readonly PeriodicClockConverter Singleton = new PeriodicClockConverter(); } internal class TimingTypeConverter : JsonConverter { public override bool CanConvert(Type t) => t == typeof(TimingType) || t == typeof(TimingType?); public override object ReadJson(JsonReader reader, Type t, object existingValue, JsonSerializer serializer) { if (reader.TokenType == JsonToken.Null) return null; var value = serializer.Deserialize(reader); if (value == "periodic") { return TimingType.Periodic; } throw new Exception("Cannot unmarshal type TimingType"); } public override void WriteJson(JsonWriter writer, object untypedValue, JsonSerializer serializer) { if (untypedValue == null) { serializer.Serialize(writer, null); return; } var value = (TimingType)untypedValue; if (value == TimingType.Periodic) { serializer.Serialize(writer, "periodic"); return; } throw new Exception("Cannot marshal type TimingType"); } public static readonly TimingTypeConverter Singleton = new TimingTypeConverter(); } }