From: Jellyfin Packaging <noreply@jellyfin.org>
Date: Wed, 8 Jan 2025 00:00:00 +0000
Subject: [PATCH] Progressive download improvements for v10.11.5+

This patch includes all progressive download and transcoding improvements:
- Use /downloads/ folder for full-file progressive downloads
- Fix download transcoding not stopping when client disconnects
- Improve MP4 movflags for better seeking compatibility (mpv/iina/infuse)
- Fix timestamp seeking for progressive downloads
- Use separate transcode paths for HLS vs progressive downloads

---
 Jellyfin.Api/Controllers/VideosController.cs      |  3 ++-
 Jellyfin.Api/Helpers/StreamingHelpers.cs          | 15 +++++++++++++++
 .../MediaEncoding/EncodingHelper.cs               | 22 ++++++++++++++++++++++
 3 files changed, 39 insertions(+), 1 deletion(-)

diff --git a/Jellyfin.Api/Controllers/VideosController.cs b/Jellyfin.Api/Controllers/VideosController.cs
index 97f3239bbc..6f9fbadddb 100644
--- a/Jellyfin.Api/Controllers/VideosController.cs
+++ b/Jellyfin.Api/Controllers/VideosController.cs
@@ -369,7 +369,8 @@ public class VideosController : BaseJellyfinApiController
     {
         var isHeadRequest = Request.Method == System.Net.WebRequestMethods.Http.Head;
         // CTS lifecycle is managed internally.
-        var cancellationTokenSource = new CancellationTokenSource();
+        // Link to HttpContext.RequestAborted so transcode stops when client disconnects
+        var cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(HttpContext.RequestAborted);
         var streamingRequest = new VideoRequestDto
         {
             Id = itemId,
diff --git a/Jellyfin.Api/Helpers/StreamingHelpers.cs b/Jellyfin.Api/Helpers/StreamingHelpers.cs
index b3f5b9a801..f366fd7a23 100644
--- a/Jellyfin.Api/Helpers/StreamingHelpers.cs
+++ b/Jellyfin.Api/Helpers/StreamingHelpers.cs
@@ -378,8 +378,23 @@ public static class StreamingHelpers

         var filename = data.GetMD5().ToString("N", CultureInfo.InvariantCulture);
         var ext = outputFileExtension.ToLowerInvariant();
+
+        // Use different transcode paths for HLS vs Progressive downloads
+        // HLS segments: use RAM-based transcode path (fast, auto-cleanup)
+        // Progressive downloads: use disk-based path (large files OK, slower cleanup)
         var folder = serverConfigurationManager.GetTranscodePath();

+        // Check if this is a full file download (no segments) vs HLS streaming
+        var streamingRequest = state.BaseRequest as StreamingRequestDto;
+
+        if (streamingRequest?.SegmentContainer is null)
+        {
+            // Full file download - use disk-based transcode path
+            var diskTranscodePath = Path.Combine(folder, "downloads");
+            Directory.CreateDirectory(diskTranscodePath);
+            folder = diskTranscodePath;
+        }
+
         return Path.Combine(folder, filename + ext);
     }

diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
index e088cd358d..a31a735bed 100644
--- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
+++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
@@ -20,6 +20,7 @@ using Jellyfin.Extensions;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Controller.Extensions;
 using MediaBrowser.Controller.IO;
+using MediaBrowser.Controller.Streaming;
 using MediaBrowser.Model.Configuration;
 using MediaBrowser.Model.Dlna;
 using MediaBrowser.Model.Dto;
@@ -7475,8 +7476,20 @@ namespace MediaBrowser.Controller.MediaEncoding
             if (Path.GetExtension(outputPath.AsSpan()).Equals(".mp4", StringComparison.OrdinalIgnoreCase)
                 && state.BaseRequest.Context == EncodingContext.Streaming)
             {
-                // Comparison: https://github.com/jansmolders86/mediacenterjs/blob/master/lib/transcoding/desktop.js
-                format = " -f mp4 -movflags frag_keyframe+empty_moov+delay_moov";
+                // Use fragmented MP4 for adaptive streaming (HLS/DASH with segments)
+                // Use faststart for progressive downloads (better seeking and metadata)
+                var streamingRequest = state.BaseRequest as StreamingRequestDto;
+                if (streamingRequest?.SegmentContainer is not null)
+                {
+                    // Fragmented MP4 for HLS/DASH
+                    format = " -f mp4 -movflags frag_keyframe+empty_moov+delay_moov";
+                }
+                else
+                {
+                    // Progressive download - use faststart for proper seeking and duration
+                    // Use frag_keyframe for better seeking compatibility with mpv
+                    format = " -f mp4 -movflags frag_keyframe+faststart+default_base_moof";
+                }
             }

             var threads = GetNumberOfThreads(state, encodingOptions, videoCodec);
@@ -7582,6 +7595,15 @@ namespace MediaBrowser.Controller.MediaEncoding
                         args += " -start_at_zero";
                     }
                 }
+                else if (state.TranscodingType == TranscodingJobType.Progressive && !state.BaseRequest.CopyTimestamps)
+                {
+                    // For progressive downloads without copyTimestamps, ensure timestamps start at 0
+                    // This fixes seeking issues in strict players like mpv, iina, and infuse
+                    args += " -avoid_negative_ts make_zero -start_at_zero";
+                }

                 var qualityParam = GetVideoQualityParam(state, videoCodec, encodingOptions, defaultPreset);

--
2.43.0
