From 9e531a82d79b7a2f2b002d09f5b92adaad0bfdea Mon Sep 17 00:00:00 2001 From: Jason Dove Date: Sun, 10 Oct 2021 12:33:02 -0500 Subject: [PATCH] add some hls playlist filter tests (#411) --- .../Streaming/HlsSessionWorker.cs | 2 +- .../FFmpeg/HlsPlaylistFilterTests.cs | 223 ++++++++++++++++++ ErsatzTV.Core/FFmpeg/FFmpegProcessBuilder.cs | 13 +- ErsatzTV.Core/FFmpeg/HlsPlaylistFilter.cs | 8 +- 4 files changed, 233 insertions(+), 13 deletions(-) create mode 100644 ErsatzTV.Core.Tests/FFmpeg/HlsPlaylistFilterTests.cs diff --git a/ErsatzTV.Application/Streaming/HlsSessionWorker.cs b/ErsatzTV.Application/Streaming/HlsSessionWorker.cs index 6dacb719f..e9a380931 100644 --- a/ErsatzTV.Application/Streaming/HlsSessionWorker.cs +++ b/ErsatzTV.Application/Streaming/HlsSessionWorker.cs @@ -194,7 +194,7 @@ namespace ErsatzTV.Application.Streaming { string fileName = Path.GetFileName(file); if (fileName.StartsWith("live") && int.Parse(fileName.Replace("live", string.Empty).Split('.')[0]) < - int.Parse(trimResult.Sequence)) + trimResult.Sequence) { File.Delete(file); } diff --git a/ErsatzTV.Core.Tests/FFmpeg/HlsPlaylistFilterTests.cs b/ErsatzTV.Core.Tests/FFmpeg/HlsPlaylistFilterTests.cs new file mode 100644 index 000000000..db0c2583b --- /dev/null +++ b/ErsatzTV.Core.Tests/FFmpeg/HlsPlaylistFilterTests.cs @@ -0,0 +1,223 @@ +using System; +using ErsatzTV.Core.FFmpeg; +using FluentAssertions; +using NUnit.Framework; + +namespace ErsatzTV.Core.Tests.FFmpeg +{ + [TestFixture] + public class HlsPlaylistFilterTests + { + [Test] + public void HlsPlaylistFilter_ShouldRewriteProgramDateTime() + { + var start = new DateTimeOffset(2021, 10, 9, 8, 0, 0, TimeSpan.FromHours(-5)); + string[] input = @"#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 +#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("\n"); + + TrimPlaylistResult result = HlsPlaylistFilter.TrimPlaylist(start, start.AddSeconds(-30), input); + + result.PlaylistStart.Should().Be(start); + result.Sequence.Should().Be(1137); + result.Playlist.Should().Be( + @"#EXTM3U +#EXT-X-VERSION:6 +#EXT-X-TARGETDURATION:4 +#EXT-X-MEDIA-SEQUENCE:1137 +#EXT-X-DISCONTINUITY-SEQUENCE:0 +#EXT-X-INDEPENDENT-SEGMENTS +#EXT-X-DISCONTINUITY +#EXTINF:4.000000, +#EXT-X-PROGRAM-DATE-TIME:2021-10-09T08:00:00.000-0500 +live001137.ts +#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_ShouldLimitSegments() + { + var start = new DateTimeOffset(2021, 10, 9, 8, 0, 0, TimeSpan.FromHours(-5)); + string[] input = @"#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 +#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("\n"); + + TrimPlaylistResult result = HlsPlaylistFilter.TrimPlaylist(start, start.AddSeconds(-30), input, 2); + + result.PlaylistStart.Should().Be(start); + result.Sequence.Should().Be(1137); + result.Playlist.Should().Be( + @"#EXTM3U +#EXT-X-VERSION:6 +#EXT-X-TARGETDURATION:4 +#EXT-X-MEDIA-SEQUENCE:1137 +#EXT-X-DISCONTINUITY-SEQUENCE:0 +#EXT-X-INDEPENDENT-SEGMENTS +#EXT-X-DISCONTINUITY +#EXTINF:4.000000, +#EXT-X-PROGRAM-DATE-TIME:2021-10-09T08:00:00.000-0500 +live001137.ts +#EXTINF:4.000000, +#EXT-X-PROGRAM-DATE-TIME:2021-10-09T08:00:04.000-0500 +live001138.ts +"); + } + + [Test] + public void HlsPlaylistFilter_ShouldAddDiscontinuity() + { + var start = new DateTimeOffset(2021, 10, 9, 8, 0, 0, TimeSpan.FromHours(-5)); + string[] input = @"#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 +#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("\n"); + + TrimPlaylistResult result = HlsPlaylistFilter.TrimPlaylist( + start, + start.AddSeconds(-30), + input, + int.MaxValue, + true); + + result.PlaylistStart.Should().Be(start); + result.Sequence.Should().Be(1137); + result.Playlist.Should().Be( + @"#EXTM3U +#EXT-X-VERSION:6 +#EXT-X-TARGETDURATION:4 +#EXT-X-MEDIA-SEQUENCE:1137 +#EXT-X-DISCONTINUITY-SEQUENCE:0 +#EXT-X-INDEPENDENT-SEGMENTS +#EXT-X-DISCONTINUITY +#EXTINF:4.000000, +#EXT-X-PROGRAM-DATE-TIME:2021-10-09T08:00:00.000-0500 +live001137.ts +#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 +#EXT-X-DISCONTINUITY +"); + } + + [Test] + public void HlsPlaylistFilter_ShouldFilterOldSegments() + { + var start = new DateTimeOffset(2021, 10, 9, 8, 0, 0, TimeSpan.FromHours(-5)); + string[] input = @"#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 +#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("\n"); + + 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( + @"#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 = @"#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("\n"); + + 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( + @"#EXTM3U +#EXT-X-VERSION:6 +#EXT-X-TARGETDURATION:4 +#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 +"); + } + } +} diff --git a/ErsatzTV.Core/FFmpeg/FFmpegProcessBuilder.cs b/ErsatzTV.Core/FFmpeg/FFmpegProcessBuilder.cs index acebcb06f..eb027a9c5 100644 --- a/ErsatzTV.Core/FFmpeg/FFmpegProcessBuilder.cs +++ b/ErsatzTV.Core/FFmpeg/FFmpegProcessBuilder.cs @@ -319,8 +319,7 @@ namespace ErsatzTV.Core.FFmpeg public FFmpegProcessBuilder WithHls(string channelNumber, MediaVersion mediaVersion, bool startAtZero) { - const int INITIAL_SEGMENT_SECONDS = 4; - const int SUBSEQUENT_SEGMENT_SECONDS = 4; + const int SEGMENT_SECONDS = 4; if (!int.TryParse(mediaVersion.RFrameRate, out int frameRate)) { @@ -336,17 +335,15 @@ namespace ErsatzTV.Core.FFmpeg } } - int segmentSeconds = startAtZero ? SUBSEQUENT_SEGMENT_SECONDS : INITIAL_SEGMENT_SECONDS; - _arguments.AddRange( new[] { "-use_wallclock_as_timestamps", "1", - "-g", $"{frameRate * segmentSeconds}", - "-keyint_min", $"{frameRate * segmentSeconds}", - "-force_key_frames", $"expr:gte(t,n_forced*{segmentSeconds})", + "-g", $"{frameRate * SEGMENT_SECONDS}", + "-keyint_min", $"{frameRate * SEGMENT_SECONDS}", + "-force_key_frames", $"expr:gte(t,n_forced*{SEGMENT_SECONDS})", "-f", "hls", - "-hls_time", $"{segmentSeconds}", + "-hls_time", $"{SEGMENT_SECONDS}", "-hls_list_size", "0", "-segment_list_flags", "+live", "-hls_segment_filename", diff --git a/ErsatzTV.Core/FFmpeg/HlsPlaylistFilter.cs b/ErsatzTV.Core/FFmpeg/HlsPlaylistFilter.cs index b7dde91f6..1745da12c 100644 --- a/ErsatzTV.Core/FFmpeg/HlsPlaylistFilter.cs +++ b/ErsatzTV.Core/FFmpeg/HlsPlaylistFilter.cs @@ -16,7 +16,7 @@ namespace ErsatzTV.Core.FFmpeg DateTimeOffset nextPlaylistStart = DateTimeOffset.MaxValue; var discontinuitySequence = 0; - var startSequence = "0"; + var startSequence = 0; var output = new StringBuilder(); var started = false; var i = 0; @@ -67,10 +67,10 @@ namespace ErsatzTV.Core.FFmpeg if (!started) { - startSequence = lines[i + 2].Replace("live", string.Empty).Split('.')[0]; + startSequence = int.Parse(lines[i + 2].Replace("live", string.Empty).Split('.')[0]); output.AppendLine("#EXTM3U"); - output.AppendLine("#EXT-X-VERSION:3"); + 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}"); @@ -107,5 +107,5 @@ namespace ErsatzTV.Core.FFmpeg } } - public record TrimPlaylistResult(DateTimeOffset PlaylistStart, string Sequence, string Playlist); + public record TrimPlaylistResult(DateTimeOffset PlaylistStart, int Sequence, string Playlist); }