From 144b3fe80b4f29abfc6b4ffdc7a9dd6655dbf1ac Mon Sep 17 00:00:00 2001 From: Jason Dove <1695733+jasongdove@users.noreply.github.com> Date: Sat, 11 Oct 2025 08:30:24 -0500 Subject: [PATCH] fix remote stream durations in playouts (#2515) --- CHANGELOG.md | 1 + .../Extensions/MediaItemExtensions.cs | 19 ++++++++++++++ .../BlockScheduling/BlockPlayoutBuilder.cs | 13 +--------- .../BlockPlayoutFillerBuilder.cs | 15 ++--------- .../Scheduling/Engine/ISchedulingEngine.cs | 2 -- .../Scheduling/Engine/SchedulingEngine.cs | 15 ++--------- .../Scheduling/PlayoutModeSchedulerBase.cs | 25 +++---------------- .../PlayoutModeSchedulerDuration.cs | 3 ++- .../Scheduling/PlayoutModeSchedulerFlood.cs | 3 ++- .../PlayoutModeSchedulerMultiple.cs | 3 ++- .../Scheduling/PlayoutModeSchedulerOne.cs | 3 ++- .../Handlers/YamlPlayoutAllHandler.cs | 3 ++- .../Handlers/YamlPlayoutContentHandler.cs | 11 -------- .../Handlers/YamlPlayoutCountHandler.cs | 3 ++- .../Handlers/YamlPlayoutDurationHandler.cs | 3 ++- .../Api/ScriptedScheduleController.cs | 3 ++- 16 files changed, 44 insertions(+), 81 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 76595a481..e7d7daf91 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ### Fixed - Fix NVIDIA startup errors on arm64 +- Fix remote stream durations in playouts created using block, sequential or scripted schedules ### Changed - Do not use graphics engine for single, permanent watermark diff --git a/ErsatzTV.Core/Extensions/MediaItemExtensions.cs b/ErsatzTV.Core/Extensions/MediaItemExtensions.cs index 4645b00b2..d77528859 100644 --- a/ErsatzTV.Core/Extensions/MediaItemExtensions.cs +++ b/ErsatzTV.Core/Extensions/MediaItemExtensions.cs @@ -24,6 +24,25 @@ public static class MediaItemExtensions return maybeDuration.Any(duration => duration == TimeSpan.Zero) ? Option.None : maybeDuration; } + public static TimeSpan GetDurationForPlayout(this MediaItem mediaItem) + { + if (mediaItem is Image image) + { + return TimeSpan.FromSeconds(image.ImageMetadata.Head().DurationSeconds ?? Image.DefaultSeconds); + } + + MediaVersion version = mediaItem.GetHeadVersion(); + + if (mediaItem is RemoteStream remoteStream) + { + return version.Duration == TimeSpan.Zero && remoteStream.Duration.HasValue + ? remoteStream.Duration.Value + : version.Duration; + } + + return version.Duration; + } + public static MediaVersion GetHeadVersion(this MediaItem mediaItem) => mediaItem switch { diff --git a/ErsatzTV.Core/Scheduling/BlockScheduling/BlockPlayoutBuilder.cs b/ErsatzTV.Core/Scheduling/BlockScheduling/BlockPlayoutBuilder.cs index 3d1ae1d2c..ee025c2e6 100644 --- a/ErsatzTV.Core/Scheduling/BlockScheduling/BlockPlayoutBuilder.cs +++ b/ErsatzTV.Core/Scheduling/BlockScheduling/BlockPlayoutBuilder.cs @@ -192,7 +192,7 @@ public class BlockPlayoutBuilder( mediaItem.Id, PlayoutBuilder.DisplayTitle(mediaItem)); - TimeSpan itemDuration = DurationForMediaItem(mediaItem); + TimeSpan itemDuration = mediaItem.GetDurationForPlayout(); // item will never fit in block var blockDuration = TimeSpan.FromMinutes(effectiveBlock.Block.Minutes); @@ -439,15 +439,4 @@ public class BlockPlayoutBuilder( return result; } - - private static TimeSpan DurationForMediaItem(MediaItem mediaItem) - { - if (mediaItem is Image image) - { - return TimeSpan.FromSeconds(image.ImageMetadata.Head().DurationSeconds ?? Image.DefaultSeconds); - } - - MediaVersion version = mediaItem.GetHeadVersion(); - return version.Duration; - } } diff --git a/ErsatzTV.Core/Scheduling/BlockScheduling/BlockPlayoutFillerBuilder.cs b/ErsatzTV.Core/Scheduling/BlockScheduling/BlockPlayoutFillerBuilder.cs index d134d6ac1..6ac71e283 100644 --- a/ErsatzTV.Core/Scheduling/BlockScheduling/BlockPlayoutFillerBuilder.cs +++ b/ErsatzTV.Core/Scheduling/BlockScheduling/BlockPlayoutFillerBuilder.cs @@ -203,7 +203,7 @@ public class BlockPlayoutFillerBuilder( { foreach (MediaItem mediaItem in enumerator.Current) { - TimeSpan itemDuration = DurationForMediaItem(mediaItem); + TimeSpan itemDuration = mediaItem.GetDurationForPlayout(); var filler = new PlayoutItem { @@ -348,7 +348,7 @@ public class BlockPlayoutFillerBuilder( { foreach (MediaItem mediaItem in enumerator.Current) { - TimeSpan itemDuration = DurationForMediaItem(mediaItem); + TimeSpan itemDuration = mediaItem.GetDurationForPlayout(); // add filler from deco to unscheduled period var filler = new PlayoutItem @@ -528,16 +528,5 @@ public class BlockPlayoutFillerBuilder( } } - private static TimeSpan DurationForMediaItem(MediaItem mediaItem) - { - if (mediaItem is Image image) - { - return TimeSpan.FromSeconds(image.ImageMetadata.Head().DurationSeconds ?? Image.DefaultSeconds); - } - - MediaVersion version = mediaItem.GetHeadVersion(); - return version.Duration; - } - private record ItemAndHistory(PlayoutItem PlayoutItem, PlayoutHistory History); } diff --git a/ErsatzTV.Core/Scheduling/Engine/ISchedulingEngine.cs b/ErsatzTV.Core/Scheduling/Engine/ISchedulingEngine.cs index 2962dd95d..6887805ad 100644 --- a/ErsatzTV.Core/Scheduling/Engine/ISchedulingEngine.cs +++ b/ErsatzTV.Core/Scheduling/Engine/ISchedulingEngine.cs @@ -143,6 +143,4 @@ public interface ISchedulingEngine PlayoutAnchor GetAnchor(); ISchedulingEngineState GetState(); - - TimeSpan DurationForMediaItem(MediaItem mediaItem); } diff --git a/ErsatzTV.Core/Scheduling/Engine/SchedulingEngine.cs b/ErsatzTV.Core/Scheduling/Engine/SchedulingEngine.cs index 92cd04c3f..953194a42 100644 --- a/ErsatzTV.Core/Scheduling/Engine/SchedulingEngine.cs +++ b/ErsatzTV.Core/Scheduling/Engine/SchedulingEngine.cs @@ -725,7 +725,7 @@ public class SchedulingEngine( foreach (MediaItem mediaItem in enumeratorDetails.Enumerator.Current) { - TimeSpan itemDuration = DurationForMediaItem(mediaItem); + TimeSpan itemDuration = mediaItem.GetDurationForPlayout(); var playoutItem = new PlayoutItem { @@ -906,7 +906,7 @@ public class SchedulingEngine( foreach (MediaItem mediaItem in enumeratorDetails.Enumerator.Current) { - TimeSpan itemDuration = DurationForMediaItem(mediaItem); + TimeSpan itemDuration = mediaItem.GetDurationForPlayout(); // create a playout item var playoutItem = new PlayoutItem @@ -1368,17 +1368,6 @@ public class SchedulingEngine( } } - public TimeSpan DurationForMediaItem(MediaItem mediaItem) - { - if (mediaItem is Image image) - { - return TimeSpan.FromSeconds(image.ImageMetadata.Head().DurationSeconds ?? Image.DefaultSeconds); - } - - MediaVersion version = mediaItem.GetHeadVersion(); - return version.Duration; - } - private List GetHistoryForItem( EnumeratorDetails enumeratorDetails, PlayoutItem playoutItem, diff --git a/ErsatzTV.Core/Scheduling/PlayoutModeSchedulerBase.cs b/ErsatzTV.Core/Scheduling/PlayoutModeSchedulerBase.cs index e63801559..aeac62f15 100644 --- a/ErsatzTV.Core/Scheduling/PlayoutModeSchedulerBase.cs +++ b/ErsatzTV.Core/Scheduling/PlayoutModeSchedulerBase.cs @@ -144,7 +144,7 @@ public abstract class PlayoutModeSchedulerBase : IPlayoutModeScheduler whe { MediaItem mediaItem = enumerator.Current.ValueUnsafe(); - TimeSpan itemDuration = DurationForMediaItem(mediaItem); + TimeSpan itemDuration = mediaItem.GetDurationForPlayout(); TimeSpan inPoint = InPointForMediaItem(mediaItem); if (nextState.CurrentTime + itemDuration > nextItemStart) @@ -230,25 +230,6 @@ public abstract class PlayoutModeSchedulerBase : IPlayoutModeScheduler whe return Tuple(nextState, newItems); } - protected static TimeSpan DurationForMediaItem(MediaItem mediaItem) - { - if (mediaItem is Image image) - { - return TimeSpan.FromSeconds(image.ImageMetadata.Head().DurationSeconds ?? Image.DefaultSeconds); - } - - MediaVersion version = mediaItem.GetHeadVersion(); - - if (mediaItem is RemoteStream remoteStream) - { - return version.Duration == TimeSpan.Zero && remoteStream.Duration.HasValue - ? remoteStream.Duration.Value - : version.Duration; - } - - return version.Duration; - } - private static TimeSpan InPointForMediaItem(MediaItem mediaItem) => mediaItem switch { @@ -784,7 +765,7 @@ public abstract class PlayoutModeSchedulerBase : IPlayoutModeScheduler whe { foreach (MediaItem mediaItem in enumerator.Current) { - TimeSpan itemDuration = DurationForMediaItem(mediaItem); + TimeSpan itemDuration = mediaItem.GetDurationForPlayout(); TimeSpan inPoint = InPointForMediaItem(mediaItem); var playoutItem = new PlayoutItem @@ -828,7 +809,7 @@ public abstract class PlayoutModeSchedulerBase : IPlayoutModeScheduler whe { foreach (MediaItem mediaItem in enumerator.Current) { - TimeSpan itemDuration = DurationForMediaItem(mediaItem); + TimeSpan itemDuration = mediaItem.GetDurationForPlayout(); TimeSpan inPoint = InPointForMediaItem(mediaItem); if (remainingToFill - itemDuration >= TimeSpan.Zero) diff --git a/ErsatzTV.Core/Scheduling/PlayoutModeSchedulerDuration.cs b/ErsatzTV.Core/Scheduling/PlayoutModeSchedulerDuration.cs index 2346055ff..31c327b34 100644 --- a/ErsatzTV.Core/Scheduling/PlayoutModeSchedulerDuration.cs +++ b/ErsatzTV.Core/Scheduling/PlayoutModeSchedulerDuration.cs @@ -1,5 +1,6 @@ using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain.Filler; +using ErsatzTV.Core.Extensions; using ErsatzTV.Core.Interfaces.Scheduling; using LanguageExt.UnsafeValueAccess; using Microsoft.Extensions.Logging; @@ -74,7 +75,7 @@ public class PlayoutModeSchedulerDuration : PlayoutModeSchedulerBase itemChapters = ChaptersForMediaItem(mediaItem); if (itemDuration > scheduleItem.PlayoutDuration) diff --git a/ErsatzTV.Core/Scheduling/PlayoutModeSchedulerFlood.cs b/ErsatzTV.Core/Scheduling/PlayoutModeSchedulerFlood.cs index ceb03dfbd..38c374466 100644 --- a/ErsatzTV.Core/Scheduling/PlayoutModeSchedulerFlood.cs +++ b/ErsatzTV.Core/Scheduling/PlayoutModeSchedulerFlood.cs @@ -1,5 +1,6 @@ using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain.Filler; +using ErsatzTV.Core.Extensions; using ErsatzTV.Core.Interfaces.Scheduling; using LanguageExt.UnsafeValueAccess; using Microsoft.Extensions.Logging; @@ -46,7 +47,7 @@ public class PlayoutModeSchedulerFlood : PlayoutModeSchedulerBase.Some(Logger)); - TimeSpan itemDuration = DurationForMediaItem(mediaItem); + TimeSpan itemDuration = mediaItem.GetDurationForPlayout(); List itemChapters = ChaptersForMediaItem(mediaItem); var playoutItem = new PlayoutItem diff --git a/ErsatzTV.Core/Scheduling/PlayoutModeSchedulerOne.cs b/ErsatzTV.Core/Scheduling/PlayoutModeSchedulerOne.cs index 305130e5a..b2dc07aaa 100644 --- a/ErsatzTV.Core/Scheduling/PlayoutModeSchedulerOne.cs +++ b/ErsatzTV.Core/Scheduling/PlayoutModeSchedulerOne.cs @@ -1,5 +1,6 @@ using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain.Filler; +using ErsatzTV.Core.Extensions; using ErsatzTV.Core.Interfaces.Scheduling; using Microsoft.Extensions.Logging; @@ -34,7 +35,7 @@ public class PlayoutModeSchedulerOne : PlayoutModeSchedulerBase itemChapters = ChaptersForMediaItem(mediaItem); var playoutItem = new PlayoutItem diff --git a/ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutAllHandler.cs b/ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutAllHandler.cs index 2a3db4435..ca541a92b 100644 --- a/ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutAllHandler.cs +++ b/ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutAllHandler.cs @@ -1,6 +1,7 @@ using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain.Filler; using ErsatzTV.Core.Domain.Scheduling; +using ErsatzTV.Core.Extensions; using ErsatzTV.Core.Interfaces.Scheduling; using ErsatzTV.Core.Scheduling.YamlScheduling.Models; using Microsoft.Extensions.Logging; @@ -41,7 +42,7 @@ public class YamlPlayoutAllHandler(EnumeratorCache enumeratorCache) : YamlPlayou foreach (MediaItem mediaItem in enumerator.Current) { - TimeSpan itemDuration = DurationForMediaItem(mediaItem); + TimeSpan itemDuration = mediaItem.GetDurationForPlayout(); // create a playout item var playoutItem = new PlayoutItem diff --git a/ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutContentHandler.cs b/ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutContentHandler.cs index fe2ac962d..99a3b2e5e 100644 --- a/ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutContentHandler.cs +++ b/ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutContentHandler.cs @@ -137,17 +137,6 @@ public abstract class YamlPlayoutContentHandler(EnumeratorCache enumeratorCache) return result; } - protected static TimeSpan DurationForMediaItem(MediaItem mediaItem) - { - if (mediaItem is Image image) - { - return TimeSpan.FromSeconds(image.ImageMetadata.Head().DurationSeconds ?? Image.DefaultSeconds); - } - - MediaVersion version = mediaItem.GetHeadVersion(); - return version.Duration; - } - protected static FillerKind GetFillerKind(YamlPlayoutInstruction instruction, YamlPlayoutContext context) { if (!string.IsNullOrWhiteSpace(instruction.FillerKind) && diff --git a/ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutCountHandler.cs b/ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutCountHandler.cs index 611cb3fa0..7f1731d04 100644 --- a/ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutCountHandler.cs +++ b/ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutCountHandler.cs @@ -1,6 +1,7 @@ using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain.Filler; using ErsatzTV.Core.Domain.Scheduling; +using ErsatzTV.Core.Extensions; using ErsatzTV.Core.Interfaces.Scheduling; using ErsatzTV.Core.Scheduling.YamlScheduling.Models; using Microsoft.Extensions.Logging; @@ -66,7 +67,7 @@ public class YamlPlayoutCountHandler(EnumeratorCache enumeratorCache) : YamlPlay foreach (MediaItem mediaItem in enumerator.Current) { - TimeSpan itemDuration = DurationForMediaItem(mediaItem); + TimeSpan itemDuration = mediaItem.GetDurationForPlayout(); // create a playout item var playoutItem = new PlayoutItem diff --git a/ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutDurationHandler.cs b/ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutDurationHandler.cs index 1b8672f75..4dba4a0f3 100644 --- a/ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutDurationHandler.cs +++ b/ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutDurationHandler.cs @@ -1,6 +1,7 @@ using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain.Filler; using ErsatzTV.Core.Domain.Scheduling; +using ErsatzTV.Core.Extensions; using ErsatzTV.Core.Interfaces.Scheduling; using ErsatzTV.Core.Scheduling.YamlScheduling.Models; using Microsoft.Extensions.Logging; @@ -110,7 +111,7 @@ public class YamlPlayoutDurationHandler(EnumeratorCache enumeratorCache) : YamlP foreach (MediaItem mediaItem in enumerator.Current) { - TimeSpan itemDuration = DurationForMediaItem(mediaItem); + TimeSpan itemDuration = mediaItem.GetDurationForPlayout(); var playoutItem = new PlayoutItem { diff --git a/ErsatzTV/Controllers/Api/ScriptedScheduleController.cs b/ErsatzTV/Controllers/Api/ScriptedScheduleController.cs index 187ec7c83..6019d09af 100644 --- a/ErsatzTV/Controllers/Api/ScriptedScheduleController.cs +++ b/ErsatzTV/Controllers/Api/ScriptedScheduleController.cs @@ -1,6 +1,7 @@ using ErsatzTV.Core.Api.ScriptedPlayout; using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain.Filler; +using ErsatzTV.Core.Extensions; using ErsatzTV.Core.Interfaces.Scheduling; using ErsatzTV.Core.Scheduling.Engine; using Microsoft.AspNetCore.Mvc; @@ -426,7 +427,7 @@ public class ScriptedScheduleController(IScriptedPlayoutBuilderService scriptedP return new PeekItemDuration { Content = content, - Milliseconds = (long)engine.DurationForMediaItem(mediaItem).TotalMilliseconds + Milliseconds = (long)mediaItem.GetDurationForPlayout().TotalMilliseconds }; }