// // // To parse this JSON data, add NuGet 'System.Text.Json' then do: // // using ErsatzTV.Core.Next; // // var playout = Playout.FromJson(jsonString); #nullable enable #pragma warning disable CS8618 #pragma warning disable CS8601 #pragma warning disable CS8602 #pragma warning disable CS8603 namespace ErsatzTV.Core.Next { using System; using System.Collections.Generic; using System.Text.Json; using System.Text.Json.Serialization; using System.Globalization; /// /// 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. /// [JsonPropertyName("items")] public List Items { get; set; } /// /// URI identifying the schema version, e.g. "https://ersatztv.org/playout/version/0.0.1". /// [JsonPropertyName("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. /// [JsonPropertyName("finish")] public DateTimeOffset Finish { get; set; } /// /// Stable identifier for this item, unique within the playout. /// [JsonPropertyName("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`. /// [JsonPropertyName("source")] public Source? Source { get; set; } /// /// RFC3339 formatted start time, e.g. 2026-04-13T00:24:21.527-05:00. /// [JsonPropertyName("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`. /// [JsonPropertyName("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. /// [JsonPropertyName("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. /// [JsonPropertyName("in_point_ms")] public long? InPointMs { get; set; } /// /// Optional end offset into the source, in milliseconds. /// [JsonPropertyName("out_point_ms")] public long? OutPointMs { get; set; } /// /// Absolute path to the media file. /// [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("path")] 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`. /// [JsonPropertyName("probe_hint")] public ProbeHint? ProbeHint { get; set; } [JsonPropertyName("source_type")] public SourceType SourceType { get; set; } /// /// The lavfi filter graph parameters, passed verbatim to ffmpeg's `-f lavfi -i`. /// [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("params")] public string? Params { get; set; } /// /// Custom HTTP headers, e.g. ["Authorization: Bearer {{TOKEN}}"]. /// /// Custom HTTP headers, e.g. ["Authorization: Bearer {{TOKEN}}"]. Supports {{TEMPLATE}} /// expansion. /// [JsonPropertyName("headers")] public List? Headers { get; set; } /// /// Enable persistent connections in ffmpeg. Default: false. /// [JsonPropertyName("keep_alive")] public bool? KeepAlive { get; set; } /// /// Enable reconnect on failure. Default: true. /// [JsonPropertyName("reconnect")] public bool? Reconnect { get; set; } /// /// Maximum reconnect delay in seconds. Maps to ffmpeg's `reconnect_delay_max`. /// [JsonPropertyName("reconnect_delay_max")] public long? ReconnectDelayMax { get; set; } /// /// Socket timeout in microseconds. /// /// Request timeout in microseconds. Defaults to 10 seconds. /// [JsonPropertyName("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`. /// [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("uri")] public string? Uri { get; set; } /// /// Custom User-Agent string. /// /// Custom User-Agent string. Supports {{TEMPLATE}} expansion. /// [JsonPropertyName("user_agent")] public string? UserAgent { get; set; } /// /// Optional arguments for the command. Supports {{TEMPLATE}} expansion. Defaults to []. /// [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("args")] public List? Args { get; set; } /// /// Command that writes an MPEG-TS stream to its stdout. Supports {{TEMPLATE}} expansion. /// [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("command")] public string? Command { get; set; } /// /// Whether the content is live and therefore cannot work ahead. Default: false. /// [JsonPropertyName("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. /// [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("audio")] public List? Audio { get; set; } /// /// Source duration in milliseconds. Omit for live or unbounded sources. /// [JsonPropertyName("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. /// [JsonPropertyName("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. /// [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("subtitle")] public List? Subtitle { get; set; } /// /// Video (and still-image) streams in the source. Omit (or use `[]`) for audio-only sources; /// defaults to empty. /// [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("video")] public List? Video { get; set; } } /// /// Probe metadata for a single audio stream. /// public partial class AudioHint { /// /// Number of audio channels. /// [JsonPropertyName("channels")] public long Channels { get; set; } /// /// Codec name as reported by ffprobe (e.g. "aac", "ac3", "mp2"). Compared case-insensitively. /// [JsonPropertyName("codec")] public string Codec { get; set; } /// /// Zero-based index of this stream within the source. /// [JsonPropertyName("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. /// [JsonPropertyName("codec")] public string Codec { get; set; } /// /// Zero-based index of this stream within the source. /// [JsonPropertyName("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. /// [JsonPropertyName("codec")] public string Codec { get; set; } /// /// Color primaries (e.g. "bt709", "bt2020"). Omit if unknown. /// [JsonPropertyName("color_primaries")] public string? ColorPrimaries { get; set; } /// /// Color range (e.g. "tv", "pc"). Omit if unknown. /// [JsonPropertyName("color_range")] public string? ColorRange { get; set; } /// /// Color space / matrix coefficients (e.g. "bt709", "bt2020nc"). Omit if unknown. /// [JsonPropertyName("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. /// [JsonPropertyName("color_transfer")] public string? ColorTransfer { get; set; } /// /// Display aspect ratio, e.g. "16:9". Omit if unknown. /// [JsonPropertyName("display_aspect_ratio")] public string? DisplayAspectRatio { get; set; } /// /// Interlacing field order as reported by ffprobe ("progressive", "tt", "bb", "tb", "bt"). /// Omit for progressive. /// [JsonPropertyName("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. /// [JsonPropertyName("frame_rate")] public string? FrameRate { get; set; } /// /// Coded frame height in pixels. /// [JsonPropertyName("height")] public long Height { get; set; } /// /// Pixel format (e.g. "yuv420p", "yuv420p10le"). /// [JsonPropertyName("pix_fmt")] public string PixFmt { get; set; } /// /// Codec profile (e.g. "high", "main 10"). Compared case-insensitively. Omit if unknown. /// [JsonPropertyName("profile")] public string? Profile { get; set; } /// /// Sample (pixel) aspect ratio, e.g. "1:1". Omit for square pixels. /// [JsonPropertyName("sample_aspect_ratio")] public string? SampleAspectRatio { get; set; } /// /// Zero-based index of this stream within the source. /// [JsonPropertyName("stream_index")] public long StreamIndex { get; set; } /// /// Coded frame width in pixels. /// [JsonPropertyName("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. /// [JsonPropertyName("audio")] public TrackSelection? Audio { get; set; } /// /// Subtitle track selection. /// [JsonPropertyName("subtitle")] public TrackSelection? Subtitle { get; set; } /// /// Video track selection. /// [JsonPropertyName("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`. /// [JsonPropertyName("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. /// [JsonPropertyName("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. /// [JsonPropertyName("horizontal_margin_percent")] [JsonConverter(typeof(MinMaxValueCheckConverter))] public double? HorizontalMarginPercent { get; set; } /// /// Anchor position within the primary content frame. /// [JsonPropertyName("location")] public WatermarkLocation Location { get; set; } /// /// Opacity as a percent (0–100). Omit for fully opaque (100). /// [JsonPropertyName("opacity_percent")] [JsonConverter(typeof(MinMaxValueCheckConverter))] public double? OpacityPercent { get; set; } /// /// The source providing the watermark media (typically an image, but any `PlayoutItemSource` /// is accepted). /// [JsonPropertyName("source")] public PlayoutItemSource Source { get; set; } /// /// Zero-based stream index within the source. If omitted, the server picks the first video /// stream. /// [JsonPropertyName("stream_index")] public long? StreamIndex { get; set; } /// /// Visibility schedule for the watermark. Omit for an always-on watermark. /// [JsonPropertyName("timing")] public Timing? Timing { get; set; } /// /// Vertical offset from the anchor `location`, as a percent of primary content height /// (0–100). Omit for 0. /// [JsonPropertyName("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. /// [JsonPropertyName("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. /// [JsonPropertyName("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. /// [JsonPropertyName("in_point_ms")] public long? InPointMs { get; set; } /// /// Optional end offset into the source, in milliseconds. /// [JsonPropertyName("out_point_ms")] public long? OutPointMs { get; set; } /// /// Absolute path to the media file. /// [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("path")] 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`. /// [JsonPropertyName("probe_hint")] public ProbeHint? ProbeHint { get; set; } [JsonPropertyName("source_type")] public SourceType SourceType { get; set; } /// /// The lavfi filter graph parameters, passed verbatim to ffmpeg's `-f lavfi -i`. /// [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("params")] public string? Params { get; set; } /// /// Custom HTTP headers, e.g. ["Authorization: Bearer {{TOKEN}}"]. /// /// Custom HTTP headers, e.g. ["Authorization: Bearer {{TOKEN}}"]. Supports {{TEMPLATE}} /// expansion. /// [JsonPropertyName("headers")] public List? Headers { get; set; } /// /// Enable persistent connections in ffmpeg. Default: false. /// [JsonPropertyName("keep_alive")] public bool? KeepAlive { get; set; } /// /// Enable reconnect on failure. Default: true. /// [JsonPropertyName("reconnect")] public bool? Reconnect { get; set; } /// /// Maximum reconnect delay in seconds. Maps to ffmpeg's `reconnect_delay_max`. /// [JsonPropertyName("reconnect_delay_max")] public long? ReconnectDelayMax { get; set; } /// /// Socket timeout in microseconds. /// /// Request timeout in microseconds. Defaults to 10 seconds. /// [JsonPropertyName("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`. /// [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("uri")] public string? Uri { get; set; } /// /// Custom User-Agent string. /// /// Custom User-Agent string. Supports {{TEMPLATE}} expansion. /// [JsonPropertyName("user_agent")] public string? UserAgent { get; set; } /// /// Optional arguments for the command. Supports {{TEMPLATE}} expansion. Defaults to []. /// [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("args")] public List? Args { get; set; } /// /// Command that writes an MPEG-TS stream to its stdout. Supports {{TEMPLATE}} expansion. /// [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("command")] public string? Command { get; set; } /// /// Whether the content is live and therefore cannot work ahead. Default: false. /// [JsonPropertyName("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. /// [JsonPropertyName("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. /// [JsonPropertyName("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. /// [JsonPropertyName("fade_ms")] public long? FadeMs { get; set; } /// /// Period of the cycle, start-to-start, in milliseconds. /// [JsonPropertyName("frequency_ms")] public long FrequencyMs { get; set; } /// /// Time held fully visible between fade-in and fade-out, in milliseconds. /// [JsonPropertyName("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. /// [JsonPropertyName("phase_offset_ms")] public long? PhaseOffsetMs { get; set; } [JsonPropertyName("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) => JsonSerializer.Deserialize(json, ErsatzTV.Core.Next.Converter.Settings); } public static class Serialize { public static string ToJson(this Playout self) => JsonSerializer.Serialize(self, ErsatzTV.Core.Next.Converter.Settings); } public static class Converter { public static readonly JsonSerializerOptions Settings = new(JsonSerializerDefaults.General) { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, Converters = { SourceTypeConverter.Singleton, WatermarkLocationConverter.Singleton, PeriodicClockConverter.Singleton, TimingTypeConverter.Singleton, new DateOnlyConverter(), new TimeOnlyConverter(), IsoDateTimeOffsetConverter.Singleton }, }; } internal class SourceTypeConverter : JsonConverter { public override bool CanConvert(Type t) => t == typeof(SourceType); public override SourceType Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { var value = reader.GetString(); 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 Write(Utf8JsonWriter writer, SourceType value, JsonSerializerOptions options) { switch (value) { case SourceType.Dynamic: JsonSerializer.Serialize(writer, "dynamic", options); return; case SourceType.Http: JsonSerializer.Serialize(writer, "http", options); return; case SourceType.Lavfi: JsonSerializer.Serialize(writer, "lavfi", options); return; case SourceType.Local: JsonSerializer.Serialize(writer, "local", options); return; case SourceType.Rtsp: JsonSerializer.Serialize(writer, "rtsp", options); return; case SourceType.Script: JsonSerializer.Serialize(writer, "script", options); 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); public override double Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { var value = reader.GetDouble(); if (value >= 0 && value <= 100) { return value; } throw new Exception("Cannot unmarshal type double"); } public override void Write(Utf8JsonWriter writer, double value, JsonSerializerOptions options) { if (value >= 0 && value <= 100) { JsonSerializer.Serialize(writer, value, options); 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); public override WatermarkLocation Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { var value = reader.GetString(); 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 Write(Utf8JsonWriter writer, WatermarkLocation value, JsonSerializerOptions options) { switch (value) { case WatermarkLocation.BottomCenter: JsonSerializer.Serialize(writer, "bottom_center", options); return; case WatermarkLocation.BottomLeft: JsonSerializer.Serialize(writer, "bottom_left", options); return; case WatermarkLocation.BottomRight: JsonSerializer.Serialize(writer, "bottom_right", options); return; case WatermarkLocation.Center: JsonSerializer.Serialize(writer, "center", options); return; case WatermarkLocation.CenterLeft: JsonSerializer.Serialize(writer, "center_left", options); return; case WatermarkLocation.CenterRight: JsonSerializer.Serialize(writer, "center_right", options); return; case WatermarkLocation.TopCenter: JsonSerializer.Serialize(writer, "top_center", options); return; case WatermarkLocation.TopLeft: JsonSerializer.Serialize(writer, "top_left", options); return; case WatermarkLocation.TopRight: JsonSerializer.Serialize(writer, "top_right", options); 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); public override PeriodicClock Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { var value = reader.GetString(); switch (value) { case "content": return PeriodicClock.Content; case "wall": return PeriodicClock.Wall; } throw new Exception("Cannot unmarshal type PeriodicClock"); } public override void Write(Utf8JsonWriter writer, PeriodicClock value, JsonSerializerOptions options) { switch (value) { case PeriodicClock.Content: JsonSerializer.Serialize(writer, "content", options); return; case PeriodicClock.Wall: JsonSerializer.Serialize(writer, "wall", options); 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); public override TimingType Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { var value = reader.GetString(); if (value == "periodic") { return TimingType.Periodic; } throw new Exception("Cannot unmarshal type TimingType"); } public override void Write(Utf8JsonWriter writer, TimingType value, JsonSerializerOptions options) { if (value == TimingType.Periodic) { JsonSerializer.Serialize(writer, "periodic", options); return; } throw new Exception("Cannot marshal type TimingType"); } public static readonly TimingTypeConverter Singleton = new TimingTypeConverter(); } public class DateOnlyConverter : JsonConverter { private readonly string serializationFormat; public DateOnlyConverter() : this(null) { } public DateOnlyConverter(string? serializationFormat) { this.serializationFormat = serializationFormat ?? "yyyy-MM-dd"; } public override DateOnly Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { var value = reader.GetString(); return DateOnly.Parse(value!); } public override void Write(Utf8JsonWriter writer, DateOnly value, JsonSerializerOptions options) => writer.WriteStringValue(value.ToString(serializationFormat)); } public class TimeOnlyConverter : JsonConverter { private readonly string serializationFormat; public TimeOnlyConverter() : this(null) { } public TimeOnlyConverter(string? serializationFormat) { this.serializationFormat = serializationFormat ?? "HH:mm:ss.fff"; } public override TimeOnly Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { var value = reader.GetString(); return TimeOnly.Parse(value!); } public override void Write(Utf8JsonWriter writer, TimeOnly value, JsonSerializerOptions options) => writer.WriteStringValue(value.ToString(serializationFormat)); } internal class IsoDateTimeOffsetConverter : JsonConverter { public override bool CanConvert(Type t) => t == typeof(DateTimeOffset); private const string DefaultDateTimeFormat = "yyyy'-'MM'-'dd'T'HH':'mm':'ss.FFFFFFFK"; private DateTimeStyles _dateTimeStyles = DateTimeStyles.RoundtripKind; private string? _dateTimeFormat; private CultureInfo? _culture; public DateTimeStyles DateTimeStyles { get => _dateTimeStyles; set => _dateTimeStyles = value; } public string? DateTimeFormat { get => _dateTimeFormat ?? string.Empty; set => _dateTimeFormat = (string.IsNullOrEmpty(value)) ? null : value; } public CultureInfo Culture { get => _culture ?? CultureInfo.CurrentCulture; set => _culture = value; } public override void Write(Utf8JsonWriter writer, DateTimeOffset value, JsonSerializerOptions options) { string text; if ((_dateTimeStyles & DateTimeStyles.AdjustToUniversal) == DateTimeStyles.AdjustToUniversal || (_dateTimeStyles & DateTimeStyles.AssumeUniversal) == DateTimeStyles.AssumeUniversal) { value = value.ToUniversalTime(); } text = value.ToString(_dateTimeFormat ?? DefaultDateTimeFormat, Culture); writer.WriteStringValue(text); } public override DateTimeOffset Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { string? dateText = reader.GetString(); if (string.IsNullOrEmpty(dateText) == false) { if (!string.IsNullOrEmpty(_dateTimeFormat)) { return DateTimeOffset.ParseExact(dateText, _dateTimeFormat, Culture, _dateTimeStyles); } else { return DateTimeOffset.Parse(dateText, Culture, _dateTimeStyles); } } else { return default(DateTimeOffset); } } public static readonly IsoDateTimeOffsetConverter Singleton = new IsoDateTimeOffsetConverter(); } } #pragma warning restore CS8618 #pragma warning restore CS8601 #pragma warning restore CS8602 #pragma warning restore CS8603