From a2f54ef1bb3e6972c6ae612c400defd354de5cda Mon Sep 17 00:00:00 2001 From: Jason Dove <1695733+jasongdove@users.noreply.github.com> Date: Sun, 26 Jul 2026 13:30:35 -0500 Subject: [PATCH] fix: block playout build loop (#2957) --- CHANGELOG.md | 3 ++- .../BlockScheduling/BlockPlayoutBuilder.cs | 26 +++++++++---------- .../BlockPlayoutFillerBuilder.cs | 23 ++++++++++++++-- 3 files changed, 36 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 545587b1c..3f1170dd0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,11 +29,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Content that can never play was still counted as waiting to play, so that never happened and the order stayed the same forever - This was caused by specials (season 0), which are skipped by `Season, Episode` order; marathons grouped by show or season use that order unless `Marathon Shuffle Items` is enabled - Affected playouts also became slower to build the longer they ran -- Fix shuffled playlists and marathon groups changing order when a playout is rebuilt +- Fix shuffled playlists and marathon groups changing order when a playout is rebuilt (continued or refreshed) - Existing playlists and marathons will change order once after updating - Fix playlists used as filler skipping content - When something doesn't fit, ETV puts the filler back where it was and tries again, but playlists were only partly put back - This could skip playlist content, and could cause the next playout build to resume in the wrong place +- Fix case where block playouts would occasionally get stuck building forever - Fix green line sometimes seen with NVIDIA and AMD/VAAPI encoding - Both bugs were in ffmpeg, and ETV's patched ffmpeg 8.1.2 is required for the fixes diff --git a/ErsatzTV.Core/Scheduling/BlockScheduling/BlockPlayoutBuilder.cs b/ErsatzTV.Core/Scheduling/BlockScheduling/BlockPlayoutBuilder.cs index aa79c8555..5cfc811dc 100644 --- a/ErsatzTV.Core/Scheduling/BlockScheduling/BlockPlayoutBuilder.cs +++ b/ErsatzTV.Core/Scheduling/BlockScheduling/BlockPlayoutBuilder.cs @@ -217,21 +217,21 @@ public class BlockPlayoutBuilder( if (effectiveBlock.Block.StopScheduling is BlockStopScheduling.BeforeDurationEnd && itemDuration > blockDuration) { - foreach (TimeSpan minimumDuration in enumerator.MinimumDuration) + // enumerator.MinimumDuration is none for images and remote streams, which + // would skip forever, so use the same duration source as the check above + TimeSpan minimumDuration = collectionMediaItems[CollectionKey.ForBlockItem(blockItem)] + .Map(i => i.GetDurationForPlayout()) + .OrderBy(identity) + .Head(); + + if (minimumDuration > blockDuration) { - if (minimumDuration > blockDuration) - { - Logger.LogError( - "Collection with minimum duration {Duration:hh\\:mm\\:ss} will never fit in block with duration {BlockDuration:hh\\:mm\\:ss}; skipping this block item!", - minimumDuration, - blockDuration); - - done = true; - } - } + Logger.LogError( + "Collection with minimum duration {Duration:hh\\:mm\\:ss} will never fit in block with duration {BlockDuration:hh\\:mm\\:ss}; skipping this block item!", + minimumDuration, + blockDuration); - if (done) - { + done = true; break; } diff --git a/ErsatzTV.Core/Scheduling/BlockScheduling/BlockPlayoutFillerBuilder.cs b/ErsatzTV.Core/Scheduling/BlockScheduling/BlockPlayoutFillerBuilder.cs index 48a151a48..60c1302be 100644 --- a/ErsatzTV.Core/Scheduling/BlockScheduling/BlockPlayoutFillerBuilder.cs +++ b/ErsatzTV.Core/Scheduling/BlockScheduling/BlockPlayoutFillerBuilder.cs @@ -34,13 +34,19 @@ public class BlockPlayoutFillerBuilder( var allItems = result.AddedItems.ToList(); var removeBefore = await result.RemoveBefore.IfNoneAsync(DateTimeOffset.MaxValue); + // existing items that finish before this are deleted from the database by this build, + // so they must not be treated as existing items here + DateTimeOffset deletedBefore = await result.RemoveBefore + .Map(rb => rb - referenceData.MaxPlayoutOffset) + .IfNoneAsync(DateTimeOffset.MinValue); + if (mode is PlayoutBuildMode.Reset) { // remove all playout items with type filler // except block items that are hidden from the guide (guide mode) foreach (PlayoutItem item in filteredExistingItems) { - if (item.Finish < removeBefore) + if (item.FinishOffset < removeBefore) { continue; } @@ -90,6 +96,7 @@ public class BlockPlayoutFillerBuilder( allItems = referenceData.ExistingItems .Where(i => !result.ItemsToRemove.Contains(i.Id)) + .Where(i => i.FinishOffset >= deletedBefore) .ToList(); allItems.AddRange(result.AddedItems); @@ -348,12 +355,24 @@ public class BlockPlayoutFillerBuilder( DateTimeOffset current = start; var pastTime = false; - while (current < finish) + while (current < finish && !cancellationToken.IsCancellationRequested) { foreach (MediaItem mediaItem in enumerator.Current) { TimeSpan itemDuration = mediaItem.GetDurationForPlayout(); + // without a duration this loop can never reach the end of the gap + if (itemDuration <= TimeSpan.Zero) + { + logger.LogWarning( + "Default filler item {Item} has no duration; will stop filling gap at {Time}", + mediaItem.Id, + current); + + pastTime = true; + break; + } + // add filler from deco to unscheduled period var filler = new PlayoutItem {