Browse Source

block playout fixes; hls direct fixes (#2566)

* fix block playout builder with empty collection

* fix hls direct when selecting audio

* allow embedded subtitles with hls direct
pull/2569/head
Jason Dove 7 months ago committed by GitHub
parent
commit
1f8834c280
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 3
      CHANGELOG.md
  2. 8
      ErsatzTV.Application/Playouts/Commands/BuildPlayoutHandler.cs
  3. 156
      ErsatzTV.Core.Tests/Scheduling/BlockScheduling/BlockPlayoutBuilderTests.cs
  4. 3
      ErsatzTV.Core/FFmpeg/CustomStreamSelector.cs
  5. 5
      ErsatzTV.Core/FFmpeg/FFmpegLibraryProcessService.cs
  6. 33
      ErsatzTV.Core/FFmpeg/FFmpegStreamSelector.cs
  7. 8
      ErsatzTV.Core/Scheduling/BlockScheduling/BlockPlayoutBuilder.cs
  8. 4
      ErsatzTV.Core/Scheduling/PlayoutBuildWarnings.cs

3
CHANGELOG.md

@ -52,6 +52,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). @@ -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

8
ErsatzTV.Application/Playouts/Commands/BuildPlayoutHandler.cs

@ -116,6 +116,14 @@ public class BuildPlayoutHandler : IRequestHandler<BuildPlayout, Either<BaseErro @@ -116,6 +116,14 @@ public class BuildPlayoutHandler : IRequestHandler<BuildPlayout, Either<BaseErro
request.PlayoutId,
playoutBuildResult.Warnings.DurationFillerSkipped);
}
if (playoutBuildResult.Warnings.BlockItemSkippedEmptyCollection > 0)
{
_logger.LogDebug(
"Playout {PlayoutId} skipped {Count} block items due to empty collections",
request.PlayoutId,
playoutBuildResult.Warnings.BlockItemSkippedEmptyCollection);
}
}
return result.Map(_ => Unit.Default);

156
ErsatzTV.Core.Tests/Scheduling/BlockScheduling/BlockPlayoutBuilderTests.cs

@ -484,8 +484,164 @@ public class BlockPlayoutBuilderTests @@ -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<MediaItem>();
var mediaItems2 = new List<MediaItem>
{
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<int, List<MediaItem>>(
[
(collection.Id, mediaItems),
(collection2.Id, mediaItems2)
])
);
IConfigElementRepository configRepo = Substitute.For<IConfigElementRepository>();
configRepo
.GetValue<int>(Arg.Is(ConfigElementKey.PlayoutDaysToBuild), Arg.Any<CancellationToken>())
.Returns(Some(1));
var builder = new BlockPlayoutBuilder(
configRepo,
collectionRepo,
Substitute.For<ITelevisionRepository>(),
Substitute.For<IArtistRepository>(),
Substitute.For<ICollectionEtag>(),
_logger);
var referenceData = new PlayoutReferenceData(
playout.Channel,
Option<Deco>.None,
[],
playout.Templates.ToList(),
null,
[],
[],
TimeSpan.Zero);
Either<BaseError, PlayoutBuildResult> 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;

3
ErsatzTV.Core/FFmpeg/CustomStreamSelector.cs

@ -205,7 +205,8 @@ public class CustomStreamSelector(ILocalFileSystem localFileSystem, ILogger<Cust @@ -205,7 +205,8 @@ public class CustomStreamSelector(ILocalFileSystem localFileSystem, ILogger<Cust
}
}
if (subtitle.SubtitleKind is SubtitleKind.Embedded && !subtitle.IsImage &&
if (channel.StreamingMode != StreamingMode.HttpLiveStreamingDirect &&
subtitle.SubtitleKind is SubtitleKind.Embedded && !subtitle.IsImage &&
!subtitle.IsExtracted)
{
candidateSubtitles.Remove(subtitle);

5
ErsatzTV.Core/FFmpeg/FFmpegLibraryProcessService.cs

@ -183,7 +183,7 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService @@ -183,7 +183,7 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
playbackSettings.AudioBitrate,
playbackSettings.AudioBufferSize,
playbackSettings.AudioSampleRate,
videoPath == audioPath,
audioFormat != AudioFormat.Copy && videoPath == audioPath,
playbackSettings.NormalizeLoudnessMode switch
{
NormalizeLoudnessMode.LoudNorm => AudioFilter.LoudNorm,
@ -269,7 +269,8 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService @@ -269,7 +269,8 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
Option<SubtitleInputFile> subtitleInputFile = maybeSubtitle.Map<Option<SubtitleInputFile>>(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");

33
ErsatzTV.Core/FFmpeg/FFmpegStreamSelector.cs

@ -156,25 +156,28 @@ public class FFmpegStreamSelector : IFFmpegStreamSelector @@ -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);
}
}
}

8
ErsatzTV.Core/Scheduling/BlockScheduling/BlockPlayoutBuilder.cs

@ -180,10 +180,16 @@ public class BlockPlayoutBuilder( @@ -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)
{

4
ErsatzTV.Core/Scheduling/PlayoutBuildWarnings.cs

@ -7,9 +7,13 @@ public class PlayoutBuildWarnings @@ -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; }
}

Loading…
Cancel
Save