diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs index 15b04051f4..1c44024753 100644 --- a/Jellyfin.Api/Controllers/DynamicHlsController.cs +++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs @@ -1399,6 +1399,18 @@ public class DynamicHlsController : BaseJellyfinApiController TranscodingJobType, cancellationTokenSource.Token) .ConfigureAwait(false); + + // Calculate the starting segment index from current transcoding state + var playlistPath = Path.ChangeExtension(state.OutputFilePath, ".m3u8"); + var segmentExtension = EncodingHelper.GetSegmentFileExtension(state.Request.SegmentContainer); + var startSegmentIndex = 0; + + var currentTranscodingIndex = GetCurrentTranscodingIndex(playlistPath, segmentExtension); + if (currentTranscodingIndex.HasValue) + { + startSegmentIndex = currentTranscodingIndex.Value; + } + var mediaSourceId = state.BaseRequest.MediaSourceId; var request = new CreateMainPlaylistRequest( mediaSourceId is null ? null : Guid.Parse(mediaSourceId), @@ -1408,7 +1420,8 @@ public class DynamicHlsController : BaseJellyfinApiController state.Request.SegmentContainer ?? string.Empty, "hls1/main/", Request.QueryString.ToString(), - EncodingHelper.IsCopyCodec(state.OutputVideoCodec)); + EncodingHelper.IsCopyCodec(state.OutputVideoCodec), + startSegmentIndex); var playlist = _dynamicHlsPlaylistGenerator.CreateMainPlaylist(request); return new FileContentResult(Encoding.UTF8.GetBytes(playlist), MimeTypes.GetMimeType("playlist.m3u8")); diff --git a/src/Jellyfin.MediaEncoding.Hls/Playlist/CreateMainPlaylistRequest.cs b/src/Jellyfin.MediaEncoding.Hls/Playlist/CreateMainPlaylistRequest.cs index f5af500626..ee3d966275 100644 --- a/src/Jellyfin.MediaEncoding.Hls/Playlist/CreateMainPlaylistRequest.cs +++ b/src/Jellyfin.MediaEncoding.Hls/Playlist/CreateMainPlaylistRequest.cs @@ -18,7 +18,8 @@ public class CreateMainPlaylistRequest /// The URI prefix for the relative URL in the playlist. /// The desired query string to append (must start with ?). /// Whether the video is being remuxed. - public CreateMainPlaylistRequest(Guid? mediaSourceId, string filePath, int desiredSegmentLengthMs, long totalRuntimeTicks, string segmentContainer, string endpointPrefix, string queryString, bool isRemuxingVideo) + /// The starting segment index for the playlist. + public CreateMainPlaylistRequest(Guid? mediaSourceId, string filePath, int desiredSegmentLengthMs, long totalRuntimeTicks, string segmentContainer, string endpointPrefix, string queryString, bool isRemuxingVideo, int startSegmentIndex = 0) { MediaSourceId = mediaSourceId; FilePath = filePath; @@ -28,6 +29,7 @@ public class CreateMainPlaylistRequest EndpointPrefix = endpointPrefix; QueryString = queryString; IsRemuxingVideo = isRemuxingVideo; + StartSegmentIndex = startSegmentIndex; } /// @@ -69,4 +71,9 @@ public class CreateMainPlaylistRequest /// Gets a value indicating whether the video is being remuxed. /// public bool IsRemuxingVideo { get; } + + /// + /// Gets the starting segment index for the playlist. + /// + public int StartSegmentIndex { get; } } diff --git a/src/Jellyfin.MediaEncoding.Hls/Playlist/DynamicHlsPlaylistGenerator.cs b/src/Jellyfin.MediaEncoding.Hls/Playlist/DynamicHlsPlaylistGenerator.cs index fb5027e5b5..63d9ce44fe 100644 --- a/src/Jellyfin.MediaEncoding.Hls/Playlist/DynamicHlsPlaylistGenerator.cs +++ b/src/Jellyfin.MediaEncoding.Hls/Playlist/DynamicHlsPlaylistGenerator.cs @@ -62,9 +62,9 @@ public class DynamicHlsPlaylistGenerator : IDynamicHlsPlaylistGenerator .Append("#EXT-X-TARGETDURATION:") .Append(Math.Ceiling(segments.Count > 0 ? segments.Max() : request.DesiredSegmentLengthMs)) .AppendLine() - .AppendLine("#EXT-X-MEDIA-SEQUENCE:0"); + .AppendLine(CultureInfo.InvariantCulture, $"#EXT-X-MEDIA-SEQUENCE:{request.StartSegmentIndex}"); - var index = 0; + var index = request.StartSegmentIndex; if (isHlsInFmp4) { diff --git a/tests/Jellyfin.MediaEncoding.Hls.Tests/Jellyfin.MediaEncoding.Hls.Tests.csproj b/tests/Jellyfin.MediaEncoding.Hls.Tests/Jellyfin.MediaEncoding.Hls.Tests.csproj index eab003715c..6e7e36a1d1 100644 --- a/tests/Jellyfin.MediaEncoding.Hls.Tests/Jellyfin.MediaEncoding.Hls.Tests.csproj +++ b/tests/Jellyfin.MediaEncoding.Hls.Tests/Jellyfin.MediaEncoding.Hls.Tests.csproj @@ -11,6 +11,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all + diff --git a/tests/Jellyfin.MediaEncoding.Hls.Tests/Playlist/DynamicHlsPlaylistGeneratorTests.cs b/tests/Jellyfin.MediaEncoding.Hls.Tests/Playlist/DynamicHlsPlaylistGeneratorTests.cs index fc969527e8..8b63d96616 100644 --- a/tests/Jellyfin.MediaEncoding.Hls.Tests/Playlist/DynamicHlsPlaylistGeneratorTests.cs +++ b/tests/Jellyfin.MediaEncoding.Hls.Tests/Playlist/DynamicHlsPlaylistGeneratorTests.cs @@ -1,6 +1,9 @@ using System; +using Jellyfin.MediaEncoding.Hls.Extractors; using Jellyfin.MediaEncoding.Hls.Playlist; using Jellyfin.MediaEncoding.Keyframes; +using MediaBrowser.Controller.Configuration; +using Moq; using Xunit; namespace Jellyfin.MediaEncoding.Hls.Tests.Playlist @@ -96,6 +99,29 @@ namespace Jellyfin.MediaEncoding.Hls.Tests.Playlist return data; } + [Fact] + public void CreateMainPlaylist_WithStartSegmentIndex_GeneratesCorrectSequence() + { + var configManager = new Mock(); + var generator = new DynamicHlsPlaylistGenerator(configManager.Object, Array.Empty()); + var request = new CreateMainPlaylistRequest( + null, + "/path/to/file.mp4", + 6000, + 600000000000, + "mp4", + "hls1/main/", + "?params=1", + false, + startSegmentIndex: 1187); + + var playlist = generator.CreateMainPlaylist(request); + + Assert.Contains("#EXT-X-MEDIA-SEQUENCE:1187", playlist, StringComparison.Ordinal); + Assert.Contains("hls1/main/1187.mp4", playlist, StringComparison.Ordinal); + Assert.Contains("hls1/main/1188.mp4", playlist, StringComparison.Ordinal); + } + private static long MsToTicks(int value) => TimeSpan.FromMilliseconds(value).Ticks; } }