diff --git a/CHANGELOG.md b/CHANGELOG.md index 7d466992a..33619da3a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -46,6 +46,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Fix playback of Jellyfin content with unknown color range - Block schedules: skip collections (block items) that will never fit in block duration - Block schedules: skip media items that will never fit in block duration +- Fix HLS playlist generation for clients that actually care about discontinuities (like hls.js) + - This should resolve most playback issues with built-in channel preview ## [25.6.0] - 2025-09-14 ### Added diff --git a/ErsatzTV.Core.Tests/FFmpeg/HlsPlaylistFilterTests.cs b/ErsatzTV.Core.Tests/FFmpeg/HlsPlaylistFilterTests.cs index ece9b624e..e70e16553 100644 --- a/ErsatzTV.Core.Tests/FFmpeg/HlsPlaylistFilterTests.cs +++ b/ErsatzTV.Core.Tests/FFmpeg/HlsPlaylistFilterTests.cs @@ -551,6 +551,54 @@ live000065.ts ")); } + [Test] + public void HlsPlaylistFilter_ShouldHandleDiscontinuitySequenceOnSegmentRemoval() + { + var start = new DateTimeOffset(2025, 9, 17, 10, 11, 5, 31, TimeSpan.FromHours(-5)); + string[] input = NormalizeLineEndings( + @"#EXTM3U +#EXT-X-VERSION:6 +#EXT-X-TARGETDURATION:4 +#EXT-X-MEDIA-SEQUENCE:46 +#EXT-X-DISCONTINUITY-SEQUENCE:2 +#EXT-X-INDEPENDENT-SEGMENTS +#EXTINF:1.734000, +#EXT-X-PROGRAM-DATE-TIME:2025-09-17T10:11:05.031-0500 +live000046.ts +#EXT-X-DISCONTINUITY +#EXTINF:4.004000, +#EXT-X-PROGRAM-DATE-TIME:2025-09-17T10:11:06.765-0500 +live000047.ts +#EXTINF:4.004000, +#EXT-X-PROGRAM-DATE-TIME:2025-09-17T10:11:10.769-0500 +live000048.ts +").Split(Environment.NewLine); + + // filter 'live000046.ts' + var filterBefore = start.AddSeconds(2); + + TrimPlaylistResult result = _hlsPlaylistFilter.TrimPlaylist(start, filterBefore, input, 2); + + result.Sequence.ShouldBe(47); + + string expectedPlaylist = NormalizeLineEndings( + @"#EXTM3U +#EXT-X-VERSION:6 +#EXT-X-TARGETDURATION:4 +#EXT-X-MEDIA-SEQUENCE:47 +#EXT-X-DISCONTINUITY-SEQUENCE:3 +#EXT-X-INDEPENDENT-SEGMENTS +#EXTINF:4.004000, +#EXT-X-PROGRAM-DATE-TIME:2025-09-17T10:11:06.765-0500 +live000047.ts +#EXTINF:4.004000, +#EXT-X-PROGRAM-DATE-TIME:2025-09-17T10:11:10.769-0500 +live000048.ts +"); + + result.Playlist.ShouldBe(expectedPlaylist); + } + 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 fa29e7e50..c261410c7 100644 --- a/ErsatzTV.Core/FFmpeg/HlsPlaylistFilter.cs +++ b/ErsatzTV.Core/FFmpeg/HlsPlaylistFilter.cs @@ -61,11 +61,11 @@ public class HlsPlaylistFilter : IHlsPlaylistFilter continue; } - var duration = TimeSpan.FromSeconds( - double.Parse( - lines[i].TrimEnd(',').Split(':')[1], - NumberStyles.Number, - CultureInfo.InvariantCulture)); + var durationDecimal = decimal.Parse( + lines[i].TrimEnd(',').Split(':')[1], + NumberStyles.Number, + CultureInfo.InvariantCulture); + var duration = TimeSpan.FromTicks((long)(durationDecimal * TimeSpan.TicksPerSecond)); items.Add(new PlaylistSegment(currentTime, lines[i], lines[i + 2])); @@ -168,11 +168,17 @@ public class HlsPlaylistFilter : IHlsPlaylistFilter switch (items[i]) { case PlaylistDiscontinuity: - if (i == items.Count - 1 || allSegments.Contains(items[i + 1])) + if (i < items.Count - 1 && allSegments.Contains(items[i + 1])) + { + if (items[i + 1] is PlaylistSegment nextSegment && allSegments.Head() != nextSegment) + { + output.AppendLine("#EXT-X-DISCONTINUITY"); + } + } + else if (i == items.Count - 1 && allSegments.Count > 0) // discontinuity at the end { output.AppendLine("#EXT-X-DISCONTINUITY"); } - break; case PlaylistSegment segment: if (allSegments.Contains(segment))