diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a13809cb..d6a1bcd5b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -52,6 +52,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Fix UI error editing watermarks and graphics elements on blocks - Fix showing playout build failure details when resetting a playout - Fix scheduling auto-generated trakt list playlists that contain shows +- Fix playout builder getting stuck (forever) on block item with an empty collection +- Fix HLS Direct playback when using custom stream selector or preferred audio language/title +- Fix selecting embedded subtitles (text and picture) with HLS Direct ### Changed - Do not use graphics engine for single, permanent watermark diff --git a/ErsatzTV.Application/Playouts/Commands/BuildPlayoutHandler.cs b/ErsatzTV.Application/Playouts/Commands/BuildPlayoutHandler.cs index 330880a0e..ec4d86a64 100644 --- a/ErsatzTV.Application/Playouts/Commands/BuildPlayoutHandler.cs +++ b/ErsatzTV.Application/Playouts/Commands/BuildPlayoutHandler.cs @@ -116,6 +116,14 @@ public class BuildPlayoutHandler : IRequestHandler 0) + { + _logger.LogDebug( + "Playout {PlayoutId} skipped {Count} block items due to empty collections", + request.PlayoutId, + playoutBuildResult.Warnings.BlockItemSkippedEmptyCollection); + } } return result.Map(_ => Unit.Default); diff --git a/ErsatzTV.Core.Tests/Scheduling/BlockScheduling/BlockPlayoutBuilderTests.cs b/ErsatzTV.Core.Tests/Scheduling/BlockScheduling/BlockPlayoutBuilderTests.cs index 5ca98bc36..04c47610e 100644 --- a/ErsatzTV.Core.Tests/Scheduling/BlockScheduling/BlockPlayoutBuilderTests.cs +++ b/ErsatzTV.Core.Tests/Scheduling/BlockScheduling/BlockPlayoutBuilderTests.cs @@ -484,8 +484,164 @@ public class BlockPlayoutBuilderTests result.AddedItems[0].StartOffset.TimeOfDay.ShouldBe(TimeSpan.FromHours(9)); } } + + [Test] + [CancelAfter(10_000)] + public async Task Should_Skip_Block_With_Empty_Collection(CancellationToken cancellationToken) + { + var collection = new SmartCollection + { + Id = 1, + Query = "asdf" + }; + + var collection2 = new SmartCollection + { + Id = 2, + Query = "asdf2" + }; + + var block = new Block + { + Id = 1, + Name = "Test Block", + Minutes = 30, + Items = + [ + new BlockItem + { + Id = 1, + CollectionType = CollectionType.SmartCollection, + PlaybackOrder = PlaybackOrder.Chronological, + Index = 1, + SmartCollection = collection, + SmartCollectionId = collection.Id + }, + new BlockItem + { + Id = 2, + CollectionType = CollectionType.SmartCollection, + PlaybackOrder = PlaybackOrder.Chronological, + Index = 2, + SmartCollection = collection2, + SmartCollectionId = collection2.Id + } + ], + StopScheduling = BlockStopScheduling.BeforeDurationEnd + }; + + var template = new Template + { + Id = 1, + Items = [] + }; + + var templateItem = new TemplateItem + { + Block = block, + BlockId = block.Id, + StartTime = TimeSpan.FromHours(9), + Template = template, + TemplateId = template.Id + }; + + template.Items.Add(templateItem); + + var playoutTemplate = new PlayoutTemplate + { + Id = 1, + Index = 1, + Template = template, + TemplateId = template.Id, + DaysOfMonth = PlayoutTemplate.AllDaysOfMonth(), + DaysOfWeek = PlayoutTemplate.AllDaysOfWeek(), + MonthsOfYear = PlayoutTemplate.AllMonthsOfYear() + }; + + var playout = new Playout + { + Id = 1, + Channel = new Channel(Guid.Empty) { Id = 1, Name = "Test Channel" }, + Templates = + [ + playoutTemplate + ], + Items = [], + PlayoutHistory = [] + }; + + var now = HoursAfterMidnight(9).AddMinutes(15); + + var mediaItems = new List(); + var mediaItems2 = new List + { + new Movie + { + Id = 2, + MovieMetadata = [new MovieMetadata { ReleaseDate = DateTime.Today.AddDays(1) }], + MediaVersions = + [ + new MediaVersion + { + Duration = TimeSpan.FromMinutes(25), + MediaFiles = [new MediaFile { Path = "/fake/path/2" }] + } + ] + } + }; + + var collectionRepo = new FakeMediaCollectionRepository( + new Map>( + [ + (collection.Id, mediaItems), + (collection2.Id, mediaItems2) + ]) + ); + + IConfigElementRepository configRepo = Substitute.For(); + configRepo + .GetValue(Arg.Is(ConfigElementKey.PlayoutDaysToBuild), Arg.Any()) + .Returns(Some(1)); + + var builder = new BlockPlayoutBuilder( + configRepo, + collectionRepo, + Substitute.For(), + Substitute.For(), + Substitute.For(), + _logger); + + var referenceData = new PlayoutReferenceData( + playout.Channel, + Option.None, + [], + playout.Templates.ToList(), + null, + [], + [], + TimeSpan.Zero); + + Either buildResult = await builder.Build( + now, + playout, + referenceData, + PlayoutBuildMode.Reset, + cancellationToken); + + buildResult.IsRight.ShouldBeTrue(); + foreach (var result in buildResult.RightToSeq()) + { + // this test only cares about "today" + result.AddedItems.RemoveAll(i => i.StartOffset.Date > now.Date); + + result.AddedItems.Count.ShouldBe(1); + result.AddedItems[0].StartOffset.TimeOfDay.ShouldBe(TimeSpan.FromHours(9)); + } + } } + + protected static DateTimeOffset HoursAfterMidnight(int hours) { DateTimeOffset now = DateTimeOffset.Now; diff --git a/ErsatzTV.Core/FFmpeg/CustomStreamSelector.cs b/ErsatzTV.Core/FFmpeg/CustomStreamSelector.cs index 6b84b8f24..04cdde1e6 100644 --- a/ErsatzTV.Core/FFmpeg/CustomStreamSelector.cs +++ b/ErsatzTV.Core/FFmpeg/CustomStreamSelector.cs @@ -205,7 +205,8 @@ public class CustomStreamSelector(ILocalFileSystem localFileSystem, ILogger AudioFilter.LoudNorm, @@ -269,7 +269,8 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService Option subtitleInputFile = maybeSubtitle.Map>(subtitle => { - if (!subtitle.IsImage && subtitle.SubtitleKind == SubtitleKind.Embedded && + if (channel.StreamingMode != StreamingMode.HttpLiveStreamingDirect && !subtitle.IsImage && + subtitle.SubtitleKind == SubtitleKind.Embedded && (!subtitle.IsExtracted || string.IsNullOrWhiteSpace(subtitle.Path))) { _logger.LogWarning("Subtitles are not yet available for this item"); diff --git a/ErsatzTV.Core/FFmpeg/FFmpegStreamSelector.cs b/ErsatzTV.Core/FFmpeg/FFmpegStreamSelector.cs index 4f00437bd..27ed201f2 100644 --- a/ErsatzTV.Core/FFmpeg/FFmpegStreamSelector.cs +++ b/ErsatzTV.Core/FFmpeg/FFmpegStreamSelector.cs @@ -156,25 +156,28 @@ public class FFmpegStreamSelector : IFFmpegStreamSelector candidateSubtitles = candidateSubtitles.Filter(s => s.SubtitleKind is not SubtitleKind.Embedded).ToList(); } - foreach (Subtitle subtitle in candidateSubtitles - .Filter(s => s.SubtitleKind is SubtitleKind.Embedded && !s.IsImage) - .ToList()) + if (channel.StreamingMode is not StreamingMode.HttpLiveStreamingDirect) { - if (!subtitle.IsExtracted) + foreach (Subtitle subtitle in candidateSubtitles + .Filter(s => s.SubtitleKind is SubtitleKind.Embedded && !s.IsImage) + .ToList()) { - _logger.LogDebug( - "Ignoring embedded subtitle with index {Index} that has not been extracted", - subtitle.StreamIndex); + if (!subtitle.IsExtracted) + { + _logger.LogDebug( + "Ignoring embedded subtitle with index {Index} that has not been extracted", + subtitle.StreamIndex); - candidateSubtitles.Remove(subtitle); - } - else if (string.IsNullOrWhiteSpace(subtitle.Path)) - { - _logger.LogDebug( - "BUG: ignoring embedded subtitle with index {Index} that is missing a path", - subtitle.StreamIndex); + candidateSubtitles.Remove(subtitle); + } + else if (string.IsNullOrWhiteSpace(subtitle.Path)) + { + _logger.LogDebug( + "BUG: ignoring embedded subtitle with index {Index} that is missing a path", + subtitle.StreamIndex); - candidateSubtitles.Remove(subtitle); + candidateSubtitles.Remove(subtitle); + } } } diff --git a/ErsatzTV.Core/Scheduling/BlockScheduling/BlockPlayoutBuilder.cs b/ErsatzTV.Core/Scheduling/BlockScheduling/BlockPlayoutBuilder.cs index ee025c2e6..3829f71b7 100644 --- a/ErsatzTV.Core/Scheduling/BlockScheduling/BlockPlayoutBuilder.cs +++ b/ErsatzTV.Core/Scheduling/BlockScheduling/BlockPlayoutBuilder.cs @@ -180,10 +180,16 @@ public class BlockPlayoutBuilder( historyKey, collectionMediaItems); + if (enumerator.Count == 0) + { + result.Warnings.BlockItemSkippedEmptyCollection++; + continue; + } + var pastTime = false; var done = false; - while (!done && !pastTime) + while (!done && !pastTime && !cancellationToken.IsCancellationRequested) { foreach (MediaItem mediaItem in enumerator.Current) { diff --git a/ErsatzTV.Core/Scheduling/PlayoutBuildWarnings.cs b/ErsatzTV.Core/Scheduling/PlayoutBuildWarnings.cs index b15d63819..322227047 100644 --- a/ErsatzTV.Core/Scheduling/PlayoutBuildWarnings.cs +++ b/ErsatzTV.Core/Scheduling/PlayoutBuildWarnings.cs @@ -7,9 +7,13 @@ public class PlayoutBuildWarnings TailFillerTooLong += warnings.TailFillerTooLong; MidRollContentWithoutChapters += warnings.MidRollContentWithoutChapters; DurationFillerSkipped += warnings.DurationFillerSkipped; + + BlockItemSkippedEmptyCollection += warnings.BlockItemSkippedEmptyCollection; } public int TailFillerTooLong { get; set; } public int MidRollContentWithoutChapters { get; set; } public int DurationFillerSkipped { get; set; } + + public int BlockItemSkippedEmptyCollection { get; set; } }