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.
546 lines
20 KiB
546 lines
20 KiB
// <auto-generated /> |
|
// |
|
// 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; |
|
|
|
/// <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> |
|
[JsonProperty("items")] |
|
public List<PlayoutItem> Items { get; set; } |
|
|
|
/// <summary> |
|
/// URI identifying the schema version, e.g. "https://ersatztv.org/playout/version/0.0.1". |
|
/// </summary> |
|
[JsonProperty("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> |
|
[JsonProperty("finish")] |
|
public DateTimeOffset Finish { get; set; } |
|
|
|
/// <summary> |
|
/// Stable identifier for this item, unique within the playout. |
|
/// </summary> |
|
[JsonProperty("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> |
|
[JsonProperty("source")] |
|
public Source Source { get; set; } |
|
|
|
/// <summary> |
|
/// RFC3339 formatted start time, e.g. 2026-04-13T00:24:21.527-05:00. |
|
/// </summary> |
|
[JsonProperty("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> |
|
[JsonProperty("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> |
|
[JsonProperty("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). |
|
/// </summary> |
|
public partial class Source |
|
{ |
|
/// <summary> |
|
/// Optional start offset into the source, in milliseconds. |
|
/// </summary> |
|
[JsonProperty("in_point_ms")] |
|
public long? InPointMs { get; set; } |
|
|
|
/// <summary> |
|
/// Optional end offset into the source, in milliseconds. |
|
/// </summary> |
|
[JsonProperty("out_point_ms")] |
|
public long? OutPointMs { get; set; } |
|
|
|
/// <summary> |
|
/// Absolute path to the media file. |
|
/// </summary> |
|
[JsonProperty("path", NullValueHandling = NullValueHandling.Ignore)] |
|
public string Path { get; set; } |
|
|
|
[JsonProperty("source_type")] |
|
public SourceType SourceType { get; set; } |
|
|
|
/// <summary> |
|
/// The lavfi filter graph parameters, passed verbatim to ffmpeg's `-f lavfi -i`. |
|
/// </summary> |
|
[JsonProperty("params", NullValueHandling = NullValueHandling.Ignore)] |
|
public string Params { get; set; } |
|
|
|
/// <summary> |
|
/// Custom HTTP headers, e.g. ["Authorization: Bearer {{TOKEN}}"]. |
|
/// </summary> |
|
[JsonProperty("headers")] |
|
public List<string> Headers { get; set; } |
|
|
|
/// <summary> |
|
/// Enable reconnect on failure. Default: true. |
|
/// </summary> |
|
[JsonProperty("reconnect")] |
|
public bool? Reconnect { get; set; } |
|
|
|
/// <summary> |
|
/// Maximum reconnect delay in seconds. Maps to ffmpeg's `reconnect_delay_max`. |
|
/// </summary> |
|
[JsonProperty("reconnect_delay_max")] |
|
public long? ReconnectDelayMax { get; set; } |
|
|
|
/// <summary> |
|
/// Socket timeout in microseconds. |
|
/// </summary> |
|
[JsonProperty("timeout_us")] |
|
public long? TimeoutUs { get; set; } |
|
|
|
/// <summary> |
|
/// URI template, e.g. "https://example.com/file.mkv?token={{MY_SECRET}}". |
|
/// </summary> |
|
[JsonProperty("uri", NullValueHandling = NullValueHandling.Ignore)] |
|
public string Uri { get; set; } |
|
|
|
/// <summary> |
|
/// Custom User-Agent string. |
|
/// </summary> |
|
[JsonProperty("user_agent")] |
|
public string UserAgent { 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> |
|
[JsonProperty("audio")] |
|
public TrackSelection Audio { get; set; } |
|
|
|
/// <summary> |
|
/// Subtitle track selection. |
|
/// </summary> |
|
[JsonProperty("subtitle")] |
|
public TrackSelection Subtitle { get; set; } |
|
|
|
/// <summary> |
|
/// Video track selection. |
|
/// </summary> |
|
[JsonProperty("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> |
|
[JsonProperty("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> |
|
[JsonProperty("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> |
|
[JsonProperty("horizontal_margin_percent")] |
|
[JsonConverter(typeof(MinMaxValueCheckConverter))] |
|
public double? HorizontalMarginPercent { get; set; } |
|
|
|
/// <summary> |
|
/// Anchor position within the primary content frame. |
|
/// </summary> |
|
[JsonProperty("location")] |
|
public WatermarkLocation Location { get; set; } |
|
|
|
/// <summary> |
|
/// Opacity as a percent (0–100). Omit for fully opaque (100). |
|
/// </summary> |
|
[JsonProperty("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> |
|
[JsonProperty("source")] |
|
public PlayoutItemSource Source { get; set; } |
|
|
|
/// <summary> |
|
/// Zero-based stream index within the source. If omitted, the server picks the first video |
|
/// stream. |
|
/// </summary> |
|
[JsonProperty("stream_index")] |
|
public long? StreamIndex { get; set; } |
|
|
|
/// <summary> |
|
/// Vertical offset from the anchor `location`, as a percent of primary content height |
|
/// (0–100). Omit for 0. |
|
/// </summary> |
|
[JsonProperty("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> |
|
[JsonProperty("width_percent")] |
|
[JsonConverter(typeof(MinMaxValueCheckConverter))] |
|
public double? WidthPercent { 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). |
|
/// </summary> |
|
public partial class PlayoutItemSource |
|
{ |
|
/// <summary> |
|
/// Optional start offset into the source, in milliseconds. |
|
/// </summary> |
|
[JsonProperty("in_point_ms")] |
|
public long? InPointMs { get; set; } |
|
|
|
/// <summary> |
|
/// Optional end offset into the source, in milliseconds. |
|
/// </summary> |
|
[JsonProperty("out_point_ms")] |
|
public long? OutPointMs { get; set; } |
|
|
|
/// <summary> |
|
/// Absolute path to the media file. |
|
/// </summary> |
|
[JsonProperty("path", NullValueHandling = NullValueHandling.Ignore)] |
|
public string Path { get; set; } |
|
|
|
[JsonProperty("source_type")] |
|
public SourceType SourceType { get; set; } |
|
|
|
/// <summary> |
|
/// The lavfi filter graph parameters, passed verbatim to ffmpeg's `-f lavfi -i`. |
|
/// </summary> |
|
[JsonProperty("params", NullValueHandling = NullValueHandling.Ignore)] |
|
public string Params { get; set; } |
|
|
|
/// <summary> |
|
/// Custom HTTP headers, e.g. ["Authorization: Bearer {{TOKEN}}"]. |
|
/// </summary> |
|
[JsonProperty("headers")] |
|
public List<string> Headers { get; set; } |
|
|
|
/// <summary> |
|
/// Enable reconnect on failure. Default: true. |
|
/// </summary> |
|
[JsonProperty("reconnect")] |
|
public bool? Reconnect { get; set; } |
|
|
|
/// <summary> |
|
/// Maximum reconnect delay in seconds. Maps to ffmpeg's `reconnect_delay_max`. |
|
/// </summary> |
|
[JsonProperty("reconnect_delay_max")] |
|
public long? ReconnectDelayMax { get; set; } |
|
|
|
/// <summary> |
|
/// Socket timeout in microseconds. |
|
/// </summary> |
|
[JsonProperty("timeout_us")] |
|
public long? TimeoutUs { get; set; } |
|
|
|
/// <summary> |
|
/// URI template, e.g. "https://example.com/file.mkv?token={{MY_SECRET}}". |
|
/// </summary> |
|
[JsonProperty("uri", NullValueHandling = NullValueHandling.Ignore)] |
|
public string Uri { get; set; } |
|
|
|
/// <summary> |
|
/// Custom User-Agent string. |
|
/// </summary> |
|
[JsonProperty("user_agent")] |
|
public string UserAgent { get; set; } |
|
} |
|
|
|
public enum SourceType { Http, Lavfi, Local }; |
|
|
|
/// <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 }; |
|
|
|
public partial class Playout |
|
{ |
|
public static Playout FromJson(string json) => JsonConvert.DeserializeObject<Playout>(json, ErsatzTV.Core.Next.Converter.Settings); |
|
} |
|
|
|
public static class Serialize |
|
{ |
|
public static string ToJson(this Playout self) => JsonConvert.SerializeObject(self, ErsatzTV.Core.Next.Converter.Settings); |
|
} |
|
|
|
internal static class Converter |
|
{ |
|
public static readonly JsonSerializerSettings Settings = new JsonSerializerSettings |
|
{ |
|
NullValueHandling = NullValueHandling.Ignore, |
|
MetadataPropertyHandling = MetadataPropertyHandling.Ignore, |
|
DateParseHandling = DateParseHandling.None, |
|
Converters = |
|
{ |
|
SourceTypeConverter.Singleton, |
|
WatermarkLocationConverter.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<string>(reader); |
|
switch (value) |
|
{ |
|
case "http": |
|
return SourceType.Http; |
|
case "lavfi": |
|
return SourceType.Lavfi; |
|
case "local": |
|
return SourceType.Local; |
|
} |
|
throw new Exception("Cannot unmarshal type SourceType"); |
|
} |
|
|
|
public override void WriteJson(JsonWriter writer, object untypedValue, JsonSerializer serializer) |
|
{ |
|
if (untypedValue == null) |
|
{ |
|
serializer.Serialize(writer, null); |
|
return; |
|
} |
|
var value = (SourceType)untypedValue; |
|
switch (value) |
|
{ |
|
case SourceType.Http: |
|
serializer.Serialize(writer, "http"); |
|
return; |
|
case SourceType.Lavfi: |
|
serializer.Serialize(writer, "lavfi"); |
|
return; |
|
case SourceType.Local: |
|
serializer.Serialize(writer, "local"); |
|
return; |
|
} |
|
throw new Exception("Cannot marshal type SourceType"); |
|
} |
|
|
|
public static readonly SourceTypeConverter Singleton = new SourceTypeConverter(); |
|
} |
|
|
|
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<double>(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<string>(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(); |
|
} |
|
}
|
|
|