From e53a6d3e6ae29cb05c55c7b8edf2d237a1f75c16 Mon Sep 17 00:00:00 2001 From: Speenah Date: Tue, 13 Oct 2026 13:43:53 -0400 Subject: Merge pull request #17299 from Speenah/fix/hls-subtitle-timing Fix fMP4 HLS subtitle timestamp map offset --- CONTRIBUTORS.md | 1 + Jellyfin.Api/Controllers/SubtitleController.cs | 58 ++++++++++++++++++---- Jellyfin.Api/Helpers/DynamicHlsHelper.cs | 20 +++++++- .../Controllers/SubtitleControllerTests.cs | 40 +++++++++++++++ .../Helpers/DynamicHlsHelperTests.cs | 26 ++++++++++ 5 files changed, 134 insertions(+), 11 deletions(-) create mode 100644 tests/Jellyfin.Api.Tests/Helpers/DynamicHlsHelperTests.cs diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index ac10184a09..a01162a3b4 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -147,6 +147,7 @@ - [Soumyadip Auddy](https://github.com/SoumyadipAuddy) - [st7105](https://github.com/st7105) - [sparky8251](https://github.com/sparky8251) + - [Speenah](https://github.com/Speenah) - [spookbits](https://github.com/spookbits) - [ssenart](https://github.com/ssenart) - [stanionascu](https://github.com/stanionascu) diff --git a/Jellyfin.Api/Controllers/SubtitleController.cs b/Jellyfin.Api/Controllers/SubtitleController.cs index 6721cd6c7e..3c20bda6a6 100644 --- a/Jellyfin.Api/Controllers/SubtitleController.cs +++ b/Jellyfin.Api/Controllers/SubtitleController.cs @@ -49,6 +49,12 @@ public class SubtitleController : BaseJellyfinApiController private readonly IFileSystem _fileSystem; private readonly ILogger _logger; + /// + /// The default MPEGTS value used in the WebVTT X-TIMESTAMP-MAP header, matching the legacy MPEG-TS + /// segment container's 10 second PTS offset. + /// + internal const long DefaultVttTimestampMapMpegts = 900000; + /// /// Initializes a new instance of the class. /// @@ -202,6 +208,7 @@ public class SubtitleController : BaseJellyfinApiController /// Optional. The end position of the subtitle in ticks. /// Optional. Whether to copy the timestamps. /// Optional. Whether to add a VTT time map. + /// Optional. The VTT timestamp map MPEGTS value. /// The start position of the subtitle in ticks. /// File returned. /// A with the subtitle file. @@ -220,6 +227,7 @@ public class SubtitleController : BaseJellyfinApiController [FromQuery] long? endPositionTicks, [FromQuery] bool copyTimestamps = false, [FromQuery] bool addVttTimeMap = false, + [FromQuery][Range(typeof(long), "0", "9223372036854775807")] long vttTimestampMapMpegts = DefaultVttTimestampMapMpegts, [FromQuery] long startPositionTicks = 0) { // Set parameters to route value if not provided via query. @@ -255,7 +263,7 @@ public class SubtitleController : BaseJellyfinApiController var text = await reader.ReadToEndAsync().ConfigureAwait(false); - text = text.Replace("WEBVTT", "WEBVTT\nX-TIMESTAMP-MAP=MPEGTS:900000,LOCAL:00:00:00.000", StringComparison.Ordinal); + text = text.Replace("WEBVTT", "WEBVTT\n" + GetVttTimestampMap(vttTimestampMapMpegts), StringComparison.Ordinal); return File(Encoding.UTF8.GetBytes(text), MimeTypes.GetMimeType("file." + format)); } @@ -289,6 +297,7 @@ public class SubtitleController : BaseJellyfinApiController /// Optional. The end position of the subtitle in ticks. /// Optional. Whether to copy the timestamps. /// Optional. Whether to add a VTT time map. + /// Optional. The VTT timestamp map MPEGTS value. /// File returned. /// A with the subtitle file. [HttpGet("Videos/{routeItemId}/{routeMediaSourceId}/Subtitles/{routeIndex}/{routeStartPositionTicks}/Stream.{routeFormat}")] @@ -307,7 +316,8 @@ public class SubtitleController : BaseJellyfinApiController [FromQuery, ParameterObsolete] string? format, [FromQuery] long? endPositionTicks, [FromQuery] bool copyTimestamps = false, - [FromQuery] bool addVttTimeMap = false) + [FromQuery] bool addVttTimeMap = false, + [FromQuery][Range(typeof(long), "0", "9223372036854775807")] long vttTimestampMapMpegts = DefaultVttTimestampMapMpegts) { return GetSubtitle( routeItemId, @@ -321,6 +331,7 @@ public class SubtitleController : BaseJellyfinApiController endPositionTicks, copyTimestamps, addVttTimeMap, + vttTimestampMapMpegts, startPositionTicks ?? routeStartPositionTicks); } @@ -331,6 +342,7 @@ public class SubtitleController : BaseJellyfinApiController /// The subtitle stream index. /// The media source id. /// The subtitle segment length. + /// Optional. The VTT timestamp map MPEGTS value. /// Subtitle playlist retrieved. /// Item not found. /// A with the HLS subtitle playlist. @@ -344,7 +356,8 @@ public class SubtitleController : BaseJellyfinApiController [FromRoute, Required] Guid itemId, [FromRoute, Required] int index, [FromRoute, Required] string mediaSourceId, - [FromQuery, Required] int segmentLength) + [FromQuery, Required] int segmentLength, + [FromQuery][Range(typeof(long), "0", "9223372036854775807")] long vttTimestampMapMpegts = DefaultVttTimestampMapMpegts) { var item = _libraryManager.GetItemById