diff --git a/CHANGELOG.md b/CHANGELOG.md index 187fa6708..0011c846f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ### Fixed - Additional fix for duplicate `Other Videos` entries; trash may need to be emptied one last time after upgrading - Fix watermark opacity in cultures where `,` is a decimal separator +- Rework playlist filtering to avoid empty playlist responses ### Added - Enable QSV hardware acceleration for vaapi docker images diff --git a/ErsatzTV.Application/Streaming/HlsSessionWorker.cs b/ErsatzTV.Application/Streaming/HlsSessionWorker.cs index c4f36fb50..49c05c05f 100644 --- a/ErsatzTV.Application/Streaming/HlsSessionWorker.cs +++ b/ErsatzTV.Application/Streaming/HlsSessionWorker.cs @@ -190,7 +190,7 @@ public class HlsSessionWorker : IHlsSessionWorker IMediator mediator = scope.ServiceProvider.GetRequiredService(); - long ptsOffset = await GetPtsOffset(mediator, _channelNumber, cancellationToken); + long ptsOffset = await GetPtsOffset(mediator, _channelNumber, _firstProcess, cancellationToken); // _logger.LogInformation("PTS offset: {PtsOffset}", ptsOffset); var request = new GetPlayoutItemProcessByChannelNumber( @@ -374,12 +374,19 @@ public class HlsSessionWorker : IHlsSessionWorker private async Task GetPtsOffset( IMediator mediator, string channelNumber, + bool firstProcess, CancellationToken cancellationToken) { await Slim.WaitAsync(cancellationToken); try { long result = 0; + + // the first process always starts at zero + if (firstProcess) + { + return result; + } Either queryResult = await mediator.Send( new GetLastPtsDuration(channelNumber), diff --git a/ErsatzTV.Core.Tests/FFmpeg/HlsPlaylistFilterTests.cs b/ErsatzTV.Core.Tests/FFmpeg/HlsPlaylistFilterTests.cs index 3a81db7d5..3e160c1f1 100644 --- a/ErsatzTV.Core.Tests/FFmpeg/HlsPlaylistFilterTests.cs +++ b/ErsatzTV.Core.Tests/FFmpeg/HlsPlaylistFilterTests.cs @@ -20,7 +20,7 @@ public class HlsPlaylistFilterTests private HlsPlaylistFilter _hlsPlaylistFilter; [Test] - public void _hlsPlaylistFilter_ShouldRewriteProgramDateTime() + public void HlsPlaylistFilter_ShouldRewriteProgramDateTime() { var start = new DateTimeOffset(2021, 10, 9, 8, 0, 0, TimeSpan.FromHours(-5)); string[] input = NormalizeLineEndings( @@ -50,9 +50,8 @@ live001139.ts").Split(Environment.NewLine); #EXT-X-VERSION:6 #EXT-X-TARGETDURATION:4 #EXT-X-MEDIA-SEQUENCE:1137 -#EXT-X-DISCONTINUITY-SEQUENCE:0 +#EXT-X-DISCONTINUITY-SEQUENCE:1 #EXT-X-INDEPENDENT-SEGMENTS -#EXT-X-DISCONTINUITY #EXTINF:4.000000, #EXT-X-PROGRAM-DATE-TIME:2021-10-09T08:00:00.000-0500 live001137.ts @@ -66,7 +65,7 @@ live001139.ts } [Test] - public void _hlsPlaylistFilter_ShouldLimitSegments() + public void HlsPlaylistFilter_ShouldLimitSegments() { var start = new DateTimeOffset(2021, 10, 9, 8, 0, 0, TimeSpan.FromHours(-5)); string[] input = NormalizeLineEndings( @@ -96,9 +95,8 @@ live001139.ts").Split(Environment.NewLine); #EXT-X-VERSION:6 #EXT-X-TARGETDURATION:4 #EXT-X-MEDIA-SEQUENCE:1137 -#EXT-X-DISCONTINUITY-SEQUENCE:0 +#EXT-X-DISCONTINUITY-SEQUENCE:1 #EXT-X-INDEPENDENT-SEGMENTS -#EXT-X-DISCONTINUITY #EXTINF:4.000000, #EXT-X-PROGRAM-DATE-TIME:2021-10-09T08:00:00.000-0500 live001137.ts @@ -109,7 +107,7 @@ live001138.ts } [Test] - public void _hlsPlaylistFilter_ShouldAddDiscontinuity() + public void HlsPlaylistFilter_ShouldAddDiscontinuity() { var start = new DateTimeOffset(2021, 10, 9, 8, 0, 0, TimeSpan.FromHours(-5)); string[] input = NormalizeLineEndings( @@ -144,9 +142,8 @@ live001139.ts").Split(Environment.NewLine); #EXT-X-VERSION:6 #EXT-X-TARGETDURATION:4 #EXT-X-MEDIA-SEQUENCE:1137 -#EXT-X-DISCONTINUITY-SEQUENCE:0 +#EXT-X-DISCONTINUITY-SEQUENCE:1 #EXT-X-INDEPENDENT-SEGMENTS -#EXT-X-DISCONTINUITY #EXTINF:4.000000, #EXT-X-PROGRAM-DATE-TIME:2021-10-09T08:00:00.000-0500 live001137.ts @@ -161,7 +158,7 @@ live001139.ts } [Test] - public void _hlsPlaylistFilter_ShouldFilterOldSegments() + public void HlsPlaylistFilter_ShouldFilterOldSegmentsBeyondMax() { var start = new DateTimeOffset(2021, 10, 9, 8, 0, 0, TimeSpan.FromHours(-5)); string[] input = NormalizeLineEndings( @@ -181,48 +178,7 @@ live001138.ts #EXT-X-PROGRAM-DATE-TIME:2021-10-08T08:34:57.320-0500 live001139.ts").Split(Environment.NewLine); - TrimPlaylistResult result = _hlsPlaylistFilter.TrimPlaylist(start, start.AddSeconds(6), input); - - result.PlaylistStart.Should().Be(start.AddSeconds(8)); - result.Sequence.Should().Be(1139); - result.Playlist.Should().Be( - NormalizeLineEndings( - @"#EXTM3U -#EXT-X-VERSION:6 -#EXT-X-TARGETDURATION:4 -#EXT-X-MEDIA-SEQUENCE:1139 -#EXT-X-DISCONTINUITY-SEQUENCE:0 -#EXT-X-INDEPENDENT-SEGMENTS -#EXT-X-DISCONTINUITY -#EXTINF:4.000000, -#EXT-X-PROGRAM-DATE-TIME:2021-10-09T08:00:08.000-0500 -live001139.ts -")); - } - - [Test] - public void _hlsPlaylistFilter_ShouldFilterOldDiscontinuity() - { - var start = new DateTimeOffset(2021, 10, 9, 8, 0, 0, TimeSpan.FromHours(-5)); - string[] input = NormalizeLineEndings( - @"#EXTM3U -#EXT-X-VERSION:6 -#EXT-X-TARGETDURATION:4 -#EXT-X-MEDIA-SEQUENCE:1137 -#EXT-X-INDEPENDENT-SEGMENTS -#EXT-X-DISCONTINUITY -#EXTINF:4.000000, -#EXT-X-PROGRAM-DATE-TIME:2021-10-08T08:34:49.320-0500 -live001137.ts -#EXT-X-DISCONTINUITY -#EXTINF:4.000000, -#EXT-X-PROGRAM-DATE-TIME:2021-10-08T08:34:53.320-0500 -live001138.ts -#EXTINF:4.000000, -#EXT-X-PROGRAM-DATE-TIME:2021-10-08T08:34:57.320-0500 -live001139.ts").Split(Environment.NewLine); - - TrimPlaylistResult result = _hlsPlaylistFilter.TrimPlaylist(start, start.AddSeconds(6), input); + TrimPlaylistResult result = _hlsPlaylistFilter.TrimPlaylist(start, start.AddSeconds(6), input, 1); result.PlaylistStart.Should().Be(start.AddSeconds(8)); result.Sequence.Should().Be(1139); @@ -234,13 +190,367 @@ live001139.ts").Split(Environment.NewLine); #EXT-X-MEDIA-SEQUENCE:1139 #EXT-X-DISCONTINUITY-SEQUENCE:1 #EXT-X-INDEPENDENT-SEGMENTS -#EXT-X-DISCONTINUITY #EXTINF:4.000000, #EXT-X-PROGRAM-DATE-TIME:2021-10-09T08:00:08.000-0500 live001139.ts ")); } + [Test] + public void HlsPlaylistFilter_ShouldFilterOldDiscontinuity() + { + var start = new DateTimeOffset(2021, 10, 9, 8, 0, 0, TimeSpan.FromHours(-5)); + string[] input = NormalizeLineEndings( + @"#EXTM3U +#EXT-X-VERSION:6 +#EXT-X-TARGETDURATION:4 +#EXT-X-MEDIA-SEQUENCE:1137 +#EXT-X-INDEPENDENT-SEGMENTS +#EXT-X-DISCONTINUITY +#EXTINF:4.000000, +#EXT-X-PROGRAM-DATE-TIME:2021-10-08T08:34:49.320-0500 +live001137.ts +#EXT-X-DISCONTINUITY +#EXTINF:4.000000, +#EXT-X-PROGRAM-DATE-TIME:2021-10-08T08:34:53.320-0500 +live001138.ts +#EXTINF:4.000000, +#EXT-X-PROGRAM-DATE-TIME:2021-10-08T08:34:57.320-0500 +live001139.ts").Split(Environment.NewLine); + + TrimPlaylistResult result = _hlsPlaylistFilter.TrimPlaylist(start, start.AddSeconds(6), input); + + result.PlaylistStart.Should().Be(start); + result.Sequence.Should().Be(1137); + result.Playlist.Should().Be( + NormalizeLineEndings( + @"#EXTM3U +#EXT-X-VERSION:6 +#EXT-X-TARGETDURATION:4 +#EXT-X-MEDIA-SEQUENCE:1137 +#EXT-X-DISCONTINUITY-SEQUENCE:1 +#EXT-X-INDEPENDENT-SEGMENTS +#EXTINF:4.000000, +#EXT-X-PROGRAM-DATE-TIME:2021-10-09T08:00:00.000-0500 +live001137.ts +#EXT-X-DISCONTINUITY +#EXTINF:4.000000, +#EXT-X-PROGRAM-DATE-TIME:2021-10-09T08:00:04.000-0500 +live001138.ts +#EXTINF:4.000000, +#EXT-X-PROGRAM-DATE-TIME:2021-10-09T08:00:08.000-0500 +live001139.ts +")); + } + + [Test] + public void HlsPlaylistFilter_Should_Increment_DiscontinuityCount() + { + var start = new DateTimeOffset(2022, 5, 25, 20, 8, 0, TimeSpan.FromHours(-5)); + string[] input = NormalizeLineEndings( + @"#EXTM3U +#EXT-X-VERSION:6 +#EXT-X-TARGETDURATION:4 +#EXT-X-MEDIA-SEQUENCE:0 +#EXT-X-DISCONTINUITY +#EXT-X-INDEPENDENT-SEGMENTS +#EXT-X-DISCONTINUITY +#EXTINF:4.004000, +#EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:08:55.981-0500 +live000000.ts +#EXTINF:4.004000, +#EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:08:59.985-0500 +live000001.ts +#EXTINF:4.004000, +#EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:09:03.989-0500 +live000002.ts +#EXTINF:4.004000, +#EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:09:07.993-0500 +live000003.ts +#EXTINF:4.004000, +#EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:09:11.997-0500 +live000004.ts +#EXTINF:4.004000, +#EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:09:16.001-0500 +live000005.ts +#EXTINF:4.004000, +#EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:09:20.005-0500 +live000006.ts +#EXTINF:4.004000, +#EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:09:24.009-0500 +live000007.ts +#EXTINF:3.970633, +#EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:09:28.013-0500 +live000008.ts +#EXTINF:4.004000, +#EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:09:31.983-0500 +live000009.ts +#EXTINF:4.004000, +#EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:09:35.987-0500 +live000010.ts +#EXTINF:4.004000, +#EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:09:39.991-0500 +live000011.ts +#EXTINF:4.004000, +#EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:09:43.995-0500 +live000012.ts +#EXTINF:4.004000, +#EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:09:47.999-0500 +live000013.ts +#EXTINF:4.004000, +#EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:09:52.003-0500 +live000014.ts +#EXTINF:4.004000, +#EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:09:56.007-0500 +live000015.ts +#EXTINF:3.970633, +#EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:10:00.011-0500 +live000016.ts +#EXTINF:4.004000, +#EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:10:03.982-0500 +live000017.ts +#EXTINF:4.004000, +#EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:10:07.986-0500 +live000018.ts +#EXTINF:4.004000, +#EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:10:11.990-0500 +live000019.ts +#EXTINF:4.004000, +#EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:10:15.994-0500 +live000020.ts +#EXTINF:4.004000, +#EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:10:19.998-0500 +live000021.ts +#EXTINF:4.004000, +#EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:10:24.002-0500 +live000022.ts +#EXTINF:4.004000, +#EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:10:28.006-0500 +live000023.ts +#EXTINF:4.004000, +#EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:10:32.010-0500 +live000024.ts +#EXTINF:3.970633, +#EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:10:36.014-0500 +live000025.ts +#EXTINF:4.004000, +#EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:10:39.985-0500 +live000026.ts +#EXTINF:4.004000, +#EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:10:43.989-0500 +live000027.ts +#EXTINF:4.004000, +#EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:10:47.993-0500 +live000028.ts +#EXTINF:4.004000, +#EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:10:51.997-0500 +live000029.ts +#EXTINF:4.004000, +#EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:10:56.001-0500 +live000030.ts +#EXTINF:4.004000, +#EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:11:00.005-0500 +live000031.ts +#EXTINF:4.004000, +#EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:11:04.009-0500 +live000032.ts +#EXTINF:3.970633, +#EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:11:08.013-0500 +live000033.ts +#EXTINF:4.004000, +#EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:11:11.983-0500 +live000034.ts +#EXTINF:4.004000, +#EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:11:15.987-0500 +live000035.ts +#EXTINF:4.004000, +#EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:11:19.991-0500 +live000036.ts +#EXTINF:4.004000, +#EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:11:23.995-0500 +live000037.ts +#EXTINF:4.004000, +#EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:11:27.999-0500 +live000038.ts +#EXTINF:4.004000, +#EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:11:32.003-0500 +live000039.ts +#EXTINF:4.004000, +#EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:11:36.007-0500 +live000040.ts +#EXTINF:3.970633, +#EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:11:40.011-0500 +live000041.ts +#EXTINF:4.004000, +#EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:11:43.982-0500 +live000042.ts +#EXTINF:4.004000, +#EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:11:47.986-0500 +live000043.ts +#EXTINF:4.004000, +#EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:11:51.990-0500 +live000044.ts +#EXTINF:4.004000, +#EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:11:55.994-0500 +live000045.ts +#EXTINF:4.004000, +#EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:11:59.998-0500 +live000046.ts +#EXTINF:4.004000, +#EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:12:04.002-0500 +live000047.ts +#EXTINF:4.004000, +#EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:12:08.006-0500 +live000048.ts +#EXTINF:4.004000, +#EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:12:12.010-0500 +live000049.ts +#EXTINF:3.970633, +#EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:12:16.014-0500 +live000050.ts +#EXTINF:4.004000, +#EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:12:19.985-0500 +live000051.ts +#EXTINF:0.433767, +#EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:12:23.989-0500 +live000052.ts +#EXT-X-DISCONTINUITY +#EXTINF:4.000000, +#EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:11:30.007-0500 +live000053.ts +#EXTINF:4.000000, +#EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:11:34.007-0500 +live000054.ts +#EXTINF:4.000000, +#EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:11:38.007-0500 +live000055.ts +#EXTINF:4.000000, +#EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:11:42.007-0500 +live000056.ts +#EXTINF:4.000000, +#EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:11:46.007-0500 +live000057.ts +#EXTINF:4.000000, +#EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:11:50.007-0500 +live000058.ts +#EXTINF:4.000000, +#EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:11:54.007-0500 +live000059.ts +#EXTINF:4.000000, +#EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:11:58.007-0500 +live000060.ts +#EXTINF:4.000000, +#EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:12:02.007-0500 +live000061.ts +#EXTINF:4.000000, +#EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:12:06.007-0500 +live000062.ts +#EXTINF:4.000000, +#EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:12:10.007-0500 +live000063.ts +#EXTINF:4.000000, +#EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:12:14.007-0500 +live000064.ts +#EXTINF:4.000000, +#EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:12:18.007-0500 +live000065.ts +#EXTINF:4.000000, +#EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:12:22.007-0500 +live000066.ts +#EXTINF:4.000000, +#EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:12:26.007-0500 +live000067.ts +#EXTINF:4.000000, +#EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:12:30.007-0500 +live000068.ts +#EXTINF:4.000000, +#EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:12:34.007-0500 +live000069.ts +#EXTINF:4.000000, +#EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:12:38.007-0500 +live000070.ts +#EXTINF:4.000000, +#EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:12:42.007-0500 +live000071.ts +#EXTINF:4.000000, +#EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:12:46.007-0500 +live000072.ts +#EXTINF:4.000000, +#EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:12:50.007-0500 +live000073.ts +#EXTINF:4.000000, +#EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:12:54.007-0500 +live000074.ts +#EXTINF:4.000000, +#EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:12:58.007-0500 +live000075.ts +#EXTINF:4.000000, +#EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:13:02.007-0500 +live000076.ts +#EXTINF:4.000000, +#EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:13:06.007-0500 +live000077.ts +#EXTINF:4.000000, +#EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:13:10.007-0500 +live000078.ts +#EXTINF:4.000000, +#EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:13:14.007-0500 +live000079.ts +#EXTINF:4.000000, +#EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:13:18.007-0500 +live000080.ts +#EXTINF:4.000000, +#EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:13:22.007-0500 +live000081.ts +#EXTINF:4.000000, +#EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:13:26.007-0500 +live000082.ts").Split(Environment.NewLine); + + TrimPlaylistResult result = _hlsPlaylistFilter.TrimPlaylist(start, start.AddSeconds(220), input); + + // result.PlaylistStart.Should().Be(start); + result.Sequence.Should().Be(56); + result.Playlist.Should().Be( + NormalizeLineEndings( + @"#EXTM3U +#EXT-X-VERSION:6 +#EXT-X-TARGETDURATION:4 +#EXT-X-MEDIA-SEQUENCE:56 +#EXT-X-DISCONTINUITY-SEQUENCE:2 +#EXT-X-INDEPENDENT-SEGMENTS +#EXTINF:4.000000, +#EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:11:40.441-0500 +live000056.ts +#EXTINF:4.000000, +#EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:11:44.441-0500 +live000057.ts +#EXTINF:4.000000, +#EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:11:48.441-0500 +live000058.ts +#EXTINF:4.000000, +#EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:11:52.441-0500 +live000059.ts +#EXTINF:4.000000, +#EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:11:56.441-0500 +live000060.ts +#EXTINF:4.000000, +#EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:12:00.441-0500 +live000061.ts +#EXTINF:4.000000, +#EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:12:04.441-0500 +live000062.ts +#EXTINF:4.000000, +#EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:12:08.441-0500 +live000063.ts +#EXTINF:4.000000, +#EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:12:12.441-0500 +live000064.ts +#EXTINF:4.000000, +#EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:12:16.441-0500 +live000065.ts +")); + } + private static string NormalizeLineEndings(string str) => str .Replace("\r\n", "\n") diff --git a/ErsatzTV.Core/FFmpeg/HlsPlaylistFilter.cs b/ErsatzTV.Core/FFmpeg/HlsPlaylistFilter.cs index 08834e9e0..4bf30d5bc 100644 --- a/ErsatzTV.Core/FFmpeg/HlsPlaylistFilter.cs +++ b/ErsatzTV.Core/FFmpeg/HlsPlaylistFilter.cs @@ -25,52 +25,38 @@ public class HlsPlaylistFilter : IHlsPlaylistFilter { try { - // _logger.LogDebug( - // "TrimPlaylist - Start {PlaylistStart}, FilterBefore {FilterBefore}, MaxSegments {MaxSegments}, EndWithDiscontinuity {EndWithDiscontinuity}", - // playlistStart, - // filterBefore, - // maxSegments, - // endWithDiscontinuity); + List items = new(); DateTimeOffset currentTime = playlistStart; - DateTimeOffset nextPlaylistStart = DateTimeOffset.MaxValue; var discontinuitySequence = 0; - var startSequence = 0; - var output = new StringBuilder(); - var started = false; var i = 0; - var segments = 0; while (!lines[i].StartsWith("#EXTINF:")) { if (lines[i].StartsWith("#EXT-X-DISCONTINUITY-SEQUENCE")) { discontinuitySequence = int.Parse(lines[i].Split(':')[1]); } + else if (lines[i].StartsWith("#EXT-X-DISCONTINUITY")) + { + items.Add(new PlaylistDiscontinuity()); + } i++; } while (i < lines.Length) { - if (segments >= maxSegments) + string line = lines[i]; + if (string.IsNullOrWhiteSpace(line)) { - break; + i++; + continue; } - string line = lines[i]; - // _logger.LogInformation("Line: {Line}", line); if (line.StartsWith("#EXT-X-DISCONTINUITY")) { - if (started) - { - output.AppendLine("#EXT-X-DISCONTINUITY"); - } - else - { - discontinuitySequence++; - } - + items.Add(new PlaylistDiscontinuity()); i++; continue; } @@ -80,50 +66,23 @@ public class HlsPlaylistFilter : IHlsPlaylistFilter lines[i].TrimEnd(',').Split(':')[1], NumberStyles.Number, CultureInfo.InvariantCulture)); - if (currentTime < filterBefore) - { - currentTime += duration; - i += 3; - continue; - } - nextPlaylistStart = currentTime < nextPlaylistStart ? currentTime : nextPlaylistStart; - - if (!started) - { - startSequence = int.Parse(lines[i + 2].Replace("live", string.Empty).Split('.')[0]); - - output.AppendLine("#EXTM3U"); - output.AppendLine("#EXT-X-VERSION:6"); - output.AppendLine("#EXT-X-TARGETDURATION:4"); - output.AppendLine($"#EXT-X-MEDIA-SEQUENCE:{startSequence}"); - output.AppendLine($"#EXT-X-DISCONTINUITY-SEQUENCE:{discontinuitySequence}"); - output.AppendLine("#EXT-X-INDEPENDENT-SEGMENTS"); - output.AppendLine("#EXT-X-DISCONTINUITY"); - - started = true; - } - - output.AppendLine(lines[i]); - string offset = currentTime.ToString("zzz").Replace(":", string.Empty); - output.AppendLine($"#EXT-X-PROGRAM-DATE-TIME:{currentTime:yyyy-MM-ddTHH:mm:ss.fff}{offset}"); - output.AppendLine(lines[i + 2]); + items.Add(new PlaylistSegment(currentTime, lines[i], lines[i + 2])); currentTime += duration; - segments++; i += 3; } - var playlist = output.ToString(); - if (endWithDiscontinuity && !playlist.EndsWith($"#EXT-X-DISCONTINUITY{Environment.NewLine}")) + if (endWithDiscontinuity && items[^1] is not PlaylistDiscontinuity) { - playlist += "#EXT-X-DISCONTINUITY" + Environment.NewLine; + items.Add(new PlaylistDiscontinuity()); } - if (playlist.Trim().Split(Environment.NewLine).All(l => string.IsNullOrWhiteSpace(l) || l.StartsWith('#'))) - { - throw new Exception("Trimming playlist to nothing"); - } + (string playlist, DateTimeOffset nextPlaylistStart, int startSequence, int segments) = GeneratePlaylist( + items, + filterBefore, + discontinuitySequence, + maxSegments); return new TrimPlaylistResult(nextPlaylistStart, startSequence, playlist, segments); } @@ -153,6 +112,98 @@ public class HlsPlaylistFilter : IHlsPlaylistFilter DateTimeOffset filterBefore, string[] lines) => TrimPlaylist(playlistStart, filterBefore, lines, int.MaxValue, true); + + private static Tuple GeneratePlaylist( + List items, + DateTimeOffset filterBefore, + int discontinuitySequence, + int maxSegments) + { + if (items.Any() && items[0] is PlaylistDiscontinuity) + { + discontinuitySequence++; + } + + while (items.Any() && items[0] is PlaylistDiscontinuity) + { + items.RemoveAt(0); + } + + var allSegments = items.OfType().ToList(); + // only filter if we have more than requested + if (allSegments.Count > maxSegments) + { + var afterFilter = allSegments.Filter(s => s.StartTime >= filterBefore).ToList(); + + // if there are enough new segments after filtering, use those + // otherwise return the last maxSegments + allSegments = afterFilter.Count >= maxSegments + ? afterFilter.Take(maxSegments).ToList() + : allSegments.TakeLast(maxSegments).ToList(); + } + + int startSequence = allSegments + .HeadOrNone() + .Map(s => s.StartSequence) + .IfNone(0); + + // count all discontinuities that were filtered out + if (allSegments.Any()) + { + int index = items.IndexOf(allSegments.Head()); + int count = items.Take(index + 1).OfType().Count(); + discontinuitySequence += count; + } + + var output = new StringBuilder(); + output.AppendLine("#EXTM3U"); + output.AppendLine("#EXT-X-VERSION:6"); + output.AppendLine("#EXT-X-TARGETDURATION:4"); + output.AppendLine($"#EXT-X-MEDIA-SEQUENCE:{startSequence}"); + output.AppendLine($"#EXT-X-DISCONTINUITY-SEQUENCE:{discontinuitySequence}"); + output.AppendLine("#EXT-X-INDEPENDENT-SEGMENTS"); + + for (var i = 0; i < items.Count; i++) + { + switch (items[i]) + { + case PlaylistDiscontinuity: + if (i == items.Count - 1 || allSegments.Contains(items[i + 1])) + { + output.AppendLine("#EXT-X-DISCONTINUITY"); + } + + break; + case PlaylistSegment segment: + if (allSegments.Contains(segment)) + { + output.AppendLine(segment.ExtInf); + string offset = segment.StartTime.ToString("zzz").Replace(":", string.Empty); + output.AppendLine( + $"#EXT-X-PROGRAM-DATE-TIME:{segment.StartTime:yyyy-MM-ddTHH:mm:ss.fff}{offset}"); + output.AppendLine(segment.Line); + } + + break; + } + } + + var playlist = output.ToString(); + DateTimeOffset nextPlaylistStart = allSegments.HeadOrNone() + .Map(s => s.StartTime) + .IfNone(DateTimeOffset.MaxValue); + + return Tuple(playlist, nextPlaylistStart, startSequence, allSegments.Count); + } + + private abstract record PlaylistItem; + + private record PlaylistSegment(DateTimeOffset StartTime, string ExtInf, string Line) : PlaylistItem + { + public int StartSequence => int.Parse(Line.Replace("live", string.Empty).Split('.')[0]); + } + + private record PlaylistDiscontinuity : PlaylistItem; } public record TrimPlaylistResult(DateTimeOffset PlaylistStart, int Sequence, string Playlist, int SegmentCount);