From b1ce607cea7265e09e9d9ed3a925a9a19eef983a Mon Sep 17 00:00:00 2001 From: Jason Dove <1695733+jasongdove@users.noreply.github.com> Date: Mon, 4 May 2026 12:03:50 -0500 Subject: [PATCH] feat: convert subtitles from media servers (#2882) --- .../Commands/SyncNextPlayoutHandler.cs | 20 +++++++++++------ ErsatzTV.Core/Next/Playout.cs | 22 +++++++++++++++++++ 2 files changed, 35 insertions(+), 7 deletions(-) diff --git a/ErsatzTV.Application/Playouts/Commands/SyncNextPlayoutHandler.cs b/ErsatzTV.Application/Playouts/Commands/SyncNextPlayoutHandler.cs index d0cdae501..d32d379ed 100644 --- a/ErsatzTV.Application/Playouts/Commands/SyncNextPlayoutHandler.cs +++ b/ErsatzTV.Application/Playouts/Commands/SyncNextPlayoutHandler.cs @@ -423,6 +423,7 @@ public partial class SyncNextPlayoutHandler( VerticalMarginPercent = watermarkOptions.Watermark.VerticalMarginPercent, OpacityPercent = watermarkOptions.Watermark.Opacity, StreamIndex = await watermarkOptions.ImageStreamIndex.IfNoneAsync(0), + WithinSourceContent = watermarkOptions.Watermark.PlaceWithinSourceContent, }; if (watermarkOptions.Watermark.Size is WatermarkSize.Scaled) @@ -470,7 +471,8 @@ public partial class SyncNextPlayoutHandler( plexPathReplacementService, jellyfinPathReplacementService, embyPathReplacementService, - cancellationToken); + cancellationToken, + log: false); // check filesystem first if (fileSystem.File.Exists(path)) @@ -489,7 +491,9 @@ public partial class SyncNextPlayoutHandler( return new Core.Next.Source { SourceType = Core.Next.SourceType.Http, - Uri = $"http://localhost:{Settings.StreamingPort}/media/plex/{mediaSourceId}/{pmf.Key}" + Uri = $"http://localhost:{Settings.StreamingPort}/media/plex/{mediaSourceId}/{pmf.Key}", + KeepAlive = false, + Reconnect = true }; } @@ -505,7 +509,9 @@ public partial class SyncNextPlayoutHandler( return new Core.Next.Source { SourceType = Core.Next.SourceType.Http, - Uri = $"http://localhost:{Settings.StreamingPort}/media/jellyfin/{itemId}" + Uri = $"http://localhost:{Settings.StreamingPort}/media/jellyfin/{itemId}", + KeepAlive = false, + Reconnect = true }; } @@ -522,7 +528,9 @@ public partial class SyncNextPlayoutHandler( return new Core.Next.Source { SourceType = Core.Next.SourceType.Http, - Uri = $"http://localhost:{Settings.StreamingPort}/media/emby/{itemId}" + Uri = $"http://localhost:{Settings.StreamingPort}/media/emby/{itemId}", + KeepAlive = false, + Reconnect = true }; } @@ -611,10 +619,8 @@ public partial class SyncNextPlayoutHandler( if (isMediaServer) { - return []; - // closed captions are currently unsupported - //allSubtitles.RemoveAll(s => s.Codec == "eia_608"); + allSubtitles.RemoveAll(s => s.Codec == "eia_608"); } // TODO: external image subtitles diff --git a/ErsatzTV.Core/Next/Playout.cs b/ErsatzTV.Core/Next/Playout.cs index 5abcb6821..9d4dfba6b 100644 --- a/ErsatzTV.Core/Next/Playout.cs +++ b/ErsatzTV.Core/Next/Playout.cs @@ -135,6 +135,12 @@ namespace ErsatzTV.Core.Next [JsonProperty("headers")] public List Headers { get; set; } + /// + /// Enable persistent connections in ffmpeg. Default: false. + /// + [JsonProperty("keep_alive")] + public bool? KeepAlive { get; set; } + /// /// Enable reconnect on failure. Default: true. /// @@ -278,6 +284,16 @@ namespace ErsatzTV.Core.Next [JsonProperty("width_percent")] [JsonConverter(typeof(MinMaxValueCheckConverter))] public double? WidthPercent { get; set; } + + /// + /// When true, position margins are measured from the edges of the source content rather than + /// the padded output frame, so letterbox/pillarbox bars push the watermark inward and keep + /// it inside the visible content. When false, margins are relative to the full padded frame, + /// so a 0% margin can land inside the bars. Has no effect when the primary content fills the + /// output (crop/stretch). Omit for false. + /// + [JsonProperty("within_source_content")] + public bool? WithinSourceContent { get; set; } } /// @@ -327,6 +343,12 @@ namespace ErsatzTV.Core.Next [JsonProperty("headers")] public List Headers { get; set; } + /// + /// Enable persistent connections in ffmpeg. Default: false. + /// + [JsonProperty("keep_alive")] + public bool? KeepAlive { get; set; } + /// /// Enable reconnect on failure. Default: true. ///