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 --- Jellyfin.Api/Controllers/SubtitleController.cs | 58 ++++++++++++++++++++++---- 1 file changed, 49 insertions(+), 9 deletions(-) (limited to 'Jellyfin.Api/Controllers/SubtitleController.cs') 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