mirror of https://github.com/ErsatzTV/ErsatzTV.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1154 lines
44 KiB
1154 lines
44 KiB
// <auto-generated /> |
|
// |
|
// 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; |
|
|
|
/// <summary> |
|
/// 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. |
|
/// </summary> |
|
public partial class Playout |
|
{ |
|
/// <summary> |
|
/// Ordered list of scheduled items for this window. |
|
/// </summary> |
|
[JsonPropertyName("items")] |
|
public List<PlayoutItem> Items { get; set; } |
|
|
|
/// <summary> |
|
/// URI identifying the schema version, e.g. "https://ersatztv.org/playout/version/0.0.1". |
|
/// </summary> |
|
[JsonPropertyName("version")] |
|
public string Version { get; set; } |
|
} |
|
|
|
/// <summary> |
|
/// 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). |
|
/// </summary> |
|
public partial class PlayoutItem |
|
{ |
|
/// <summary> |
|
/// RFC3339 formatted finish time, e.g. 2026-04-13T00:54:21.527-05:00. |
|
/// </summary> |
|
[JsonPropertyName("finish")] |
|
public DateTimeOffset Finish { get; set; } |
|
|
|
/// <summary> |
|
/// Stable identifier for this item, unique within the playout. |
|
/// </summary> |
|
[JsonPropertyName("id")] |
|
public string Id { get; set; } |
|
|
|
/// <summary> |
|
/// The default source shared by any track that doesn't specify its own. Required unless |
|
/// every selected track in `tracks` provides its own `source`. |
|
/// </summary> |
|
[JsonPropertyName("source")] |
|
public Source? Source { get; set; } |
|
|
|
/// <summary> |
|
/// RFC3339 formatted start time, e.g. 2026-04-13T00:24:21.527-05:00. |
|
/// </summary> |
|
[JsonPropertyName("start")] |
|
public DateTimeOffset Start { get; set; } |
|
|
|
/// <summary> |
|
/// Per-track selection. Omit to let the server pick the first video and first audio track of |
|
/// the item's `source`. |
|
/// </summary> |
|
[JsonPropertyName("tracks")] |
|
public PlayoutItemTracks? Tracks { get; set; } |
|
|
|
/// <summary> |
|
/// Watermark (image/video overlay) to composite on top of the primary content for the |
|
/// duration of this item. Omit for no watermark. |
|
/// </summary> |
|
[JsonPropertyName("watermark")] |
|
public Watermark? Watermark { get; set; } |
|
} |
|
|
|
/// <summary> |
|
/// 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). |
|
/// </summary> |
|
public partial class Source |
|
{ |
|
/// <summary> |
|
/// Optional start offset into the source, in milliseconds. |
|
/// </summary> |
|
[JsonPropertyName("in_point_ms")] |
|
public long? InPointMs { get; set; } |
|
|
|
/// <summary> |
|
/// Optional end offset into the source, in milliseconds. |
|
/// </summary> |
|
[JsonPropertyName("out_point_ms")] |
|
public long? OutPointMs { get; set; } |
|
|
|
/// <summary> |
|
/// Absolute path to the media file. |
|
/// </summary> |
|
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] |
|
[JsonPropertyName("path")] |
|
public string? Path { get; set; } |
|
|
|
/// <summary> |
|
/// 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`. |
|
/// </summary> |
|
[JsonPropertyName("probe_hint")] |
|
public ProbeHint? ProbeHint { get; set; } |
|
|
|
[JsonPropertyName("source_type")] |
|
public SourceType SourceType { get; set; } |
|
|
|
/// <summary> |
|
/// The lavfi filter graph parameters, passed verbatim to ffmpeg's `-f lavfi -i`. |
|
/// </summary> |
|
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] |
|
[JsonPropertyName("params")] |
|
public string? Params { get; set; } |
|
|
|
/// <summary> |
|
/// Custom HTTP headers, e.g. ["Authorization: Bearer {{TOKEN}}"]. |
|
/// |
|
/// Custom HTTP headers, e.g. ["Authorization: Bearer {{TOKEN}}"]. Supports {{TEMPLATE}} |
|
/// expansion. |
|
/// </summary> |
|
[JsonPropertyName("headers")] |
|
public List<string>? Headers { get; set; } |
|
|
|
/// <summary> |
|
/// Enable persistent connections in ffmpeg. Default: false. |
|
/// </summary> |
|
[JsonPropertyName("keep_alive")] |
|
public bool? KeepAlive { get; set; } |
|
|
|
/// <summary> |
|
/// Enable reconnect on failure. Default: true. |
|
/// </summary> |
|
[JsonPropertyName("reconnect")] |
|
public bool? Reconnect { get; set; } |
|
|
|
/// <summary> |
|
/// Maximum reconnect delay in seconds. Maps to ffmpeg's `reconnect_delay_max`. |
|
/// </summary> |
|
[JsonPropertyName("reconnect_delay_max")] |
|
public long? ReconnectDelayMax { get; set; } |
|
|
|
/// <summary> |
|
/// Socket timeout in microseconds. |
|
/// |
|
/// Request timeout in microseconds. Defaults to 10 seconds. |
|
/// </summary> |
|
[JsonPropertyName("timeout_us")] |
|
public long? TimeoutUs { get; set; } |
|
|
|
/// <summary> |
|
/// 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`. |
|
/// </summary> |
|
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] |
|
[JsonPropertyName("uri")] |
|
public string? Uri { get; set; } |
|
|
|
/// <summary> |
|
/// Custom User-Agent string. |
|
/// |
|
/// Custom User-Agent string. Supports {{TEMPLATE}} expansion. |
|
/// </summary> |
|
[JsonPropertyName("user_agent")] |
|
public string? UserAgent { get; set; } |
|
|
|
/// <summary> |
|
/// Optional arguments for the command. Supports {{TEMPLATE}} expansion. Defaults to []. |
|
/// </summary> |
|
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] |
|
[JsonPropertyName("args")] |
|
public List<string>? Args { get; set; } |
|
|
|
/// <summary> |
|
/// Command that writes an MPEG-TS stream to its stdout. Supports {{TEMPLATE}} expansion. |
|
/// </summary> |
|
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] |
|
[JsonPropertyName("command")] |
|
public string? Command { get; set; } |
|
|
|
/// <summary> |
|
/// Whether the content is live and therefore cannot work ahead. Default: false. |
|
/// </summary> |
|
[JsonPropertyName("is_live")] |
|
public bool? IsLive { get; set; } |
|
} |
|
|
|
/// <summary> |
|
/// 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. |
|
/// </summary> |
|
public partial class ProbeHint |
|
{ |
|
/// <summary> |
|
/// Audio streams in the source. Omit (or use `[]`) for video-only sources; defaults to empty. |
|
/// </summary> |
|
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] |
|
[JsonPropertyName("audio")] |
|
public List<AudioHint>? Audio { get; set; } |
|
|
|
/// <summary> |
|
/// Source duration in milliseconds. Omit for live or unbounded sources. |
|
/// </summary> |
|
[JsonPropertyName("duration_ms")] |
|
public long? DurationMs { get; set; } |
|
|
|
/// <summary> |
|
/// 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. |
|
/// </summary> |
|
[JsonPropertyName("format_name")] |
|
public string? FormatName { get; set; } |
|
|
|
/// <summary> |
|
/// 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. |
|
/// </summary> |
|
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] |
|
[JsonPropertyName("subtitle")] |
|
public List<SubtitleHint>? Subtitle { get; set; } |
|
|
|
/// <summary> |
|
/// Video (and still-image) streams in the source. Omit (or use `[]`) for audio-only sources; |
|
/// defaults to empty. |
|
/// </summary> |
|
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] |
|
[JsonPropertyName("video")] |
|
public List<VideoHint>? Video { get; set; } |
|
} |
|
|
|
/// <summary> |
|
/// Probe metadata for a single audio stream. |
|
/// </summary> |
|
public partial class AudioHint |
|
{ |
|
/// <summary> |
|
/// Number of audio channels. |
|
/// </summary> |
|
[JsonPropertyName("channels")] |
|
public long Channels { get; set; } |
|
|
|
/// <summary> |
|
/// Codec name as reported by ffprobe (e.g. "aac", "ac3", "mp2"). Compared case-insensitively. |
|
/// </summary> |
|
[JsonPropertyName("codec")] |
|
public string Codec { get; set; } |
|
|
|
/// <summary> |
|
/// Zero-based index of this stream within the source. |
|
/// </summary> |
|
[JsonPropertyName("stream_index")] |
|
public long StreamIndex { get; set; } |
|
} |
|
|
|
/// <summary> |
|
/// Probe metadata for a single subtitle stream. |
|
/// </summary> |
|
public partial class SubtitleHint |
|
{ |
|
/// <summary> |
|
/// 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. |
|
/// </summary> |
|
[JsonPropertyName("codec")] |
|
public string Codec { get; set; } |
|
|
|
/// <summary> |
|
/// Zero-based index of this stream within the source. |
|
/// </summary> |
|
[JsonPropertyName("stream_index")] |
|
public long StreamIndex { get; set; } |
|
} |
|
|
|
/// <summary> |
|
/// 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. |
|
/// </summary> |
|
public partial class VideoHint |
|
{ |
|
/// <summary> |
|
/// Codec name as reported by ffprobe (e.g. "h264", "hevc", "mpeg2video", "png"). Compared |
|
/// case-insensitively. |
|
/// </summary> |
|
[JsonPropertyName("codec")] |
|
public string Codec { get; set; } |
|
|
|
/// <summary> |
|
/// Color primaries (e.g. "bt709", "bt2020"). Omit if unknown. |
|
/// </summary> |
|
[JsonPropertyName("color_primaries")] |
|
public string? ColorPrimaries { get; set; } |
|
|
|
/// <summary> |
|
/// Color range (e.g. "tv", "pc"). Omit if unknown. |
|
/// </summary> |
|
[JsonPropertyName("color_range")] |
|
public string? ColorRange { get; set; } |
|
|
|
/// <summary> |
|
/// Color space / matrix coefficients (e.g. "bt709", "bt2020nc"). Omit if unknown. |
|
/// </summary> |
|
[JsonPropertyName("color_space")] |
|
public string? ColorSpace { get; set; } |
|
|
|
/// <summary> |
|
/// 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. |
|
/// </summary> |
|
[JsonPropertyName("color_transfer")] |
|
public string? ColorTransfer { get; set; } |
|
|
|
/// <summary> |
|
/// Display aspect ratio, e.g. "16:9". Omit if unknown. |
|
/// </summary> |
|
[JsonPropertyName("display_aspect_ratio")] |
|
public string? DisplayAspectRatio { get; set; } |
|
|
|
/// <summary> |
|
/// Interlacing field order as reported by ffprobe ("progressive", "tt", "bb", "tb", "bt"). |
|
/// Omit for progressive. |
|
/// </summary> |
|
[JsonPropertyName("field_order")] |
|
public string? FieldOrder { get; set; } |
|
|
|
/// <summary> |
|
/// Frame rate as a number or rational string (e.g. "24", "30000/1001"). Defaults to 24 when |
|
/// omitted. |
|
/// </summary> |
|
[JsonPropertyName("frame_rate")] |
|
public string? FrameRate { get; set; } |
|
|
|
/// <summary> |
|
/// Coded frame height in pixels. |
|
/// </summary> |
|
[JsonPropertyName("height")] |
|
public long Height { get; set; } |
|
|
|
/// <summary> |
|
/// Pixel format (e.g. "yuv420p", "yuv420p10le"). |
|
/// </summary> |
|
[JsonPropertyName("pix_fmt")] |
|
public string PixFmt { get; set; } |
|
|
|
/// <summary> |
|
/// Codec profile (e.g. "high", "main 10"). Compared case-insensitively. Omit if unknown. |
|
/// </summary> |
|
[JsonPropertyName("profile")] |
|
public string? Profile { get; set; } |
|
|
|
/// <summary> |
|
/// Sample (pixel) aspect ratio, e.g. "1:1". Omit for square pixels. |
|
/// </summary> |
|
[JsonPropertyName("sample_aspect_ratio")] |
|
public string? SampleAspectRatio { get; set; } |
|
|
|
/// <summary> |
|
/// Zero-based index of this stream within the source. |
|
/// </summary> |
|
[JsonPropertyName("stream_index")] |
|
public long StreamIndex { get; set; } |
|
|
|
/// <summary> |
|
/// Coded frame width in pixels. |
|
/// </summary> |
|
[JsonPropertyName("width")] |
|
public long Width { get; set; } |
|
} |
|
|
|
/// <summary> |
|
/// 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). |
|
/// </summary> |
|
public partial class PlayoutItemTracks |
|
{ |
|
/// <summary> |
|
/// Audio track selection. |
|
/// </summary> |
|
[JsonPropertyName("audio")] |
|
public TrackSelection? Audio { get; set; } |
|
|
|
/// <summary> |
|
/// Subtitle track selection. |
|
/// </summary> |
|
[JsonPropertyName("subtitle")] |
|
public TrackSelection? Subtitle { get; set; } |
|
|
|
/// <summary> |
|
/// Video track selection. |
|
/// </summary> |
|
[JsonPropertyName("video")] |
|
public TrackSelection? Video { get; set; } |
|
} |
|
|
|
/// <summary> |
|
/// 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. |
|
/// </summary> |
|
public partial class TrackSelection |
|
{ |
|
/// <summary> |
|
/// Source to pull this track from. If omitted, inherits from the parent `PlayoutItem.source`. |
|
/// </summary> |
|
[JsonPropertyName("source")] |
|
public Source? Source { get; set; } |
|
|
|
/// <summary> |
|
/// Zero-based stream index within the effective source. If omitted, the server picks the |
|
/// first stream of this track's kind. |
|
/// </summary> |
|
[JsonPropertyName("stream_index")] |
|
public long? StreamIndex { get; set; } |
|
} |
|
|
|
/// <summary> |
|
/// An image or video overlay composited on top of the primary content. Sized and positioned |
|
/// relative to the primary content's frame. |
|
/// </summary> |
|
public partial class Watermark |
|
{ |
|
/// <summary> |
|
/// Horizontal offset from the anchor `location`, as a percent of primary content width |
|
/// (0–100). Omit for 0. |
|
/// </summary> |
|
[JsonPropertyName("horizontal_margin_percent")] |
|
[JsonConverter(typeof(MinMaxValueCheckConverter))] |
|
public double? HorizontalMarginPercent { get; set; } |
|
|
|
/// <summary> |
|
/// Anchor position within the primary content frame. |
|
/// </summary> |
|
[JsonPropertyName("location")] |
|
public WatermarkLocation Location { get; set; } |
|
|
|
/// <summary> |
|
/// Opacity as a percent (0–100). Omit for fully opaque (100). |
|
/// </summary> |
|
[JsonPropertyName("opacity_percent")] |
|
[JsonConverter(typeof(MinMaxValueCheckConverter))] |
|
public double? OpacityPercent { get; set; } |
|
|
|
/// <summary> |
|
/// The source providing the watermark media (typically an image, but any `PlayoutItemSource` |
|
/// is accepted). |
|
/// </summary> |
|
[JsonPropertyName("source")] |
|
public PlayoutItemSource Source { get; set; } |
|
|
|
/// <summary> |
|
/// Zero-based stream index within the source. If omitted, the server picks the first video |
|
/// stream. |
|
/// </summary> |
|
[JsonPropertyName("stream_index")] |
|
public long? StreamIndex { get; set; } |
|
|
|
/// <summary> |
|
/// Visibility schedule for the watermark. Omit for an always-on watermark. |
|
/// </summary> |
|
[JsonPropertyName("timing")] |
|
public Timing? Timing { get; set; } |
|
|
|
/// <summary> |
|
/// Vertical offset from the anchor `location`, as a percent of primary content height |
|
/// (0–100). Omit for 0. |
|
/// </summary> |
|
[JsonPropertyName("vertical_margin_percent")] |
|
[JsonConverter(typeof(MinMaxValueCheckConverter))] |
|
public double? VerticalMarginPercent { get; set; } |
|
|
|
/// <summary> |
|
/// Scale the watermark to this percent of the primary content width (0–100). Omit to use the |
|
/// watermark's actual size. |
|
/// </summary> |
|
[JsonPropertyName("width_percent")] |
|
[JsonConverter(typeof(MinMaxValueCheckConverter))] |
|
public double? WidthPercent { get; set; } |
|
|
|
/// <summary> |
|
/// 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. |
|
/// </summary> |
|
[JsonPropertyName("within_source_content")] |
|
public bool? WithinSourceContent { get; set; } |
|
} |
|
|
|
/// <summary> |
|
/// 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). |
|
/// </summary> |
|
public partial class PlayoutItemSource |
|
{ |
|
/// <summary> |
|
/// Optional start offset into the source, in milliseconds. |
|
/// </summary> |
|
[JsonPropertyName("in_point_ms")] |
|
public long? InPointMs { get; set; } |
|
|
|
/// <summary> |
|
/// Optional end offset into the source, in milliseconds. |
|
/// </summary> |
|
[JsonPropertyName("out_point_ms")] |
|
public long? OutPointMs { get; set; } |
|
|
|
/// <summary> |
|
/// Absolute path to the media file. |
|
/// </summary> |
|
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] |
|
[JsonPropertyName("path")] |
|
public string? Path { get; set; } |
|
|
|
/// <summary> |
|
/// 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`. |
|
/// </summary> |
|
[JsonPropertyName("probe_hint")] |
|
public ProbeHint? ProbeHint { get; set; } |
|
|
|
[JsonPropertyName("source_type")] |
|
public SourceType SourceType { get; set; } |
|
|
|
/// <summary> |
|
/// The lavfi filter graph parameters, passed verbatim to ffmpeg's `-f lavfi -i`. |
|
/// </summary> |
|
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] |
|
[JsonPropertyName("params")] |
|
public string? Params { get; set; } |
|
|
|
/// <summary> |
|
/// Custom HTTP headers, e.g. ["Authorization: Bearer {{TOKEN}}"]. |
|
/// |
|
/// Custom HTTP headers, e.g. ["Authorization: Bearer {{TOKEN}}"]. Supports {{TEMPLATE}} |
|
/// expansion. |
|
/// </summary> |
|
[JsonPropertyName("headers")] |
|
public List<string>? Headers { get; set; } |
|
|
|
/// <summary> |
|
/// Enable persistent connections in ffmpeg. Default: false. |
|
/// </summary> |
|
[JsonPropertyName("keep_alive")] |
|
public bool? KeepAlive { get; set; } |
|
|
|
/// <summary> |
|
/// Enable reconnect on failure. Default: true. |
|
/// </summary> |
|
[JsonPropertyName("reconnect")] |
|
public bool? Reconnect { get; set; } |
|
|
|
/// <summary> |
|
/// Maximum reconnect delay in seconds. Maps to ffmpeg's `reconnect_delay_max`. |
|
/// </summary> |
|
[JsonPropertyName("reconnect_delay_max")] |
|
public long? ReconnectDelayMax { get; set; } |
|
|
|
/// <summary> |
|
/// Socket timeout in microseconds. |
|
/// |
|
/// Request timeout in microseconds. Defaults to 10 seconds. |
|
/// </summary> |
|
[JsonPropertyName("timeout_us")] |
|
public long? TimeoutUs { get; set; } |
|
|
|
/// <summary> |
|
/// 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`. |
|
/// </summary> |
|
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] |
|
[JsonPropertyName("uri")] |
|
public string? Uri { get; set; } |
|
|
|
/// <summary> |
|
/// Custom User-Agent string. |
|
/// |
|
/// Custom User-Agent string. Supports {{TEMPLATE}} expansion. |
|
/// </summary> |
|
[JsonPropertyName("user_agent")] |
|
public string? UserAgent { get; set; } |
|
|
|
/// <summary> |
|
/// Optional arguments for the command. Supports {{TEMPLATE}} expansion. Defaults to []. |
|
/// </summary> |
|
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] |
|
[JsonPropertyName("args")] |
|
public List<string>? Args { get; set; } |
|
|
|
/// <summary> |
|
/// Command that writes an MPEG-TS stream to its stdout. Supports {{TEMPLATE}} expansion. |
|
/// </summary> |
|
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] |
|
[JsonPropertyName("command")] |
|
public string? Command { get; set; } |
|
|
|
/// <summary> |
|
/// Whether the content is live and therefore cannot work ahead. Default: false. |
|
/// </summary> |
|
[JsonPropertyName("is_live")] |
|
public bool? IsLive { get; set; } |
|
} |
|
|
|
/// <summary> |
|
/// 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`. |
|
/// </summary> |
|
public partial class Timing |
|
{ |
|
/// <summary> |
|
/// Reference clock used to position cycles. |
|
/// </summary> |
|
[JsonPropertyName("clock")] |
|
public PeriodicClock Clock { get; set; } |
|
|
|
/// <summary> |
|
/// 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. |
|
/// </summary> |
|
[JsonPropertyName("disable_after_ms")] |
|
public long? DisableAfterMs { get; set; } |
|
|
|
/// <summary> |
|
/// Fade-in and fade-out duration in milliseconds (symmetric). Must be ≤ `hold_ms`. Omit for |
|
/// 1000; set to 0 for hard cuts. |
|
/// </summary> |
|
[JsonPropertyName("fade_ms")] |
|
public long? FadeMs { get; set; } |
|
|
|
/// <summary> |
|
/// Period of the cycle, start-to-start, in milliseconds. |
|
/// </summary> |
|
[JsonPropertyName("frequency_ms")] |
|
public long FrequencyMs { get; set; } |
|
|
|
/// <summary> |
|
/// Time held fully visible between fade-in and fade-out, in milliseconds. |
|
/// </summary> |
|
[JsonPropertyName("hold_ms")] |
|
public long HoldMs { get; set; } |
|
|
|
/// <summary> |
|
/// 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. |
|
/// </summary> |
|
[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 }; |
|
|
|
/// <summary> |
|
/// 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`. |
|
/// </summary> |
|
public enum WatermarkLocation { BottomCenter, BottomLeft, BottomRight, Center, CenterLeft, CenterRight, TopCenter, TopLeft, TopRight }; |
|
|
|
/// <summary> |
|
/// 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. |
|
/// </summary> |
|
public enum PeriodicClock { Content, Wall }; |
|
|
|
public enum TimingType { Periodic }; |
|
|
|
public partial class Playout |
|
{ |
|
public static Playout FromJson(string json) => JsonSerializer.Deserialize<Playout>(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<SourceType> |
|
{ |
|
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<double> |
|
{ |
|
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<WatermarkLocation> |
|
{ |
|
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<PeriodicClock> |
|
{ |
|
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<TimingType> |
|
{ |
|
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<DateOnly> |
|
{ |
|
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<TimeOnly> |
|
{ |
|
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<DateTimeOffset> |
|
{ |
|
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
|
|
|