crobibero 4 роки тому
батько
коміт
b8d327889b

+ 10 - 9
Jellyfin.Api/Controllers/AudioController.cs

@@ -35,7 +35,6 @@ namespace Jellyfin.Api.Controllers
         private readonly IMediaSourceManager _mediaSourceManager;
         private readonly IServerConfigurationManager _serverConfigurationManager;
         private readonly IMediaEncoder _mediaEncoder;
-        private readonly IStreamHelper _streamHelper;
         private readonly IFileSystem _fileSystem;
         private readonly ISubtitleEncoder _subtitleEncoder;
         private readonly IConfiguration _configuration;
@@ -55,7 +54,6 @@ namespace Jellyfin.Api.Controllers
         /// <param name="mediaSourceManager">Instance of the <see cref="IMediaSourceManager"/> interface.</param>
         /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
         /// <param name="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> interface.</param>
-        /// <param name="streamHelper">Instance of the <see cref="IStreamHelper"/> interface.</param>
         /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
         /// <param name="subtitleEncoder">Instance of the <see cref="ISubtitleEncoder"/> interface.</param>
         /// <param name="configuration">Instance of the <see cref="IConfiguration"/> interface.</param>
@@ -70,7 +68,6 @@ namespace Jellyfin.Api.Controllers
             IMediaSourceManager mediaSourceManager,
             IServerConfigurationManager serverConfigurationManager,
             IMediaEncoder mediaEncoder,
-            IStreamHelper streamHelper,
             IFileSystem fileSystem,
             ISubtitleEncoder subtitleEncoder,
             IConfiguration configuration,
@@ -85,7 +82,6 @@ namespace Jellyfin.Api.Controllers
             _mediaSourceManager = mediaSourceManager;
             _serverConfigurationManager = serverConfigurationManager;
             _mediaEncoder = mediaEncoder;
-            _streamHelper = streamHelper;
             _fileSystem = fileSystem;
             _subtitleEncoder = subtitleEncoder;
             _configuration = configuration;
@@ -283,8 +279,11 @@ namespace Jellyfin.Api.Controllers
             {
                 StreamingHelpers.AddDlnaHeaders(state, Response.Headers, true, startTimeTicks, Request, _dlnaManager);
 
-                // TODO AllowEndOfFile = false
-                await new ProgressiveFileCopier(_streamHelper, state.DirectStreamProvider).WriteToAsync(Response.Body, CancellationToken.None).ConfigureAwait(false);
+                await new ProgressiveFileCopier(state.DirectStreamProvider, null, _transcodingJobHelper, CancellationToken.None)
+                    {
+                        AllowEndOfFile = false
+                    }.WriteToAsync(Response.Body, CancellationToken.None)
+                    .ConfigureAwait(false);
 
                 // TODO (moved from MediaBrowser.Api): Don't hardcode contentType
                 return File(Response.Body, MimeTypes.GetMimeType("file.ts")!);
@@ -319,8 +318,11 @@ namespace Jellyfin.Api.Controllers
 
                 if (state.MediaSource.IsInfiniteStream)
                 {
-                    // TODO AllowEndOfFile = false
-                    await new ProgressiveFileCopier(_streamHelper, state.MediaPath).WriteToAsync(Response.Body, CancellationToken.None).ConfigureAwait(false);
+                    await new ProgressiveFileCopier(state.MediaPath, null, _transcodingJobHelper, CancellationToken.None)
+                        {
+                            AllowEndOfFile = false
+                        }.WriteToAsync(Response.Body, CancellationToken.None)
+                        .ConfigureAwait(false);
 
                     return File(Response.Body, contentType);
                 }
@@ -339,7 +341,6 @@ namespace Jellyfin.Api.Controllers
             return await FileStreamResponseHelpers.GetTranscodedFile(
                 state,
                 isHeadRequest,
-                _streamHelper,
                 this,
                 _transcodingJobHelper,
                 ffmpegCommandLineArguments,

+ 11 - 8
Jellyfin.Api/Controllers/LiveTvController.cs

@@ -24,7 +24,6 @@ using MediaBrowser.Controller.LiveTv;
 using MediaBrowser.Controller.Net;
 using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.IO;
 using MediaBrowser.Model.LiveTv;
 using MediaBrowser.Model.Net;
 using MediaBrowser.Model.Querying;
@@ -45,9 +44,9 @@ namespace Jellyfin.Api.Controllers
         private readonly ILibraryManager _libraryManager;
         private readonly IDtoService _dtoService;
         private readonly ISessionContext _sessionContext;
-        private readonly IStreamHelper _streamHelper;
         private readonly IMediaSourceManager _mediaSourceManager;
         private readonly IConfigurationManager _configurationManager;
+        private readonly TranscodingJobHelper _transcodingJobHelper;
 
         /// <summary>
         /// Initializes a new instance of the <see cref="LiveTvController"/> class.
@@ -58,9 +57,9 @@ namespace Jellyfin.Api.Controllers
         /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
         /// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param>
         /// <param name="sessionContext">Instance of the <see cref="ISessionContext"/> interface.</param>
-        /// <param name="streamHelper">Instance of the <see cref="IStreamHelper"/> interface.</param>
         /// <param name="mediaSourceManager">Instance of the <see cref="IMediaSourceManager"/> interface.</param>
         /// <param name="configurationManager">Instance of the <see cref="IConfigurationManager"/> interface.</param>
+        /// <param name="transcodingJobHelper">Instance of the <see cref="TranscodingJobHelper"/> class.</param>
         public LiveTvController(
             ILiveTvManager liveTvManager,
             IUserManager userManager,
@@ -68,9 +67,9 @@ namespace Jellyfin.Api.Controllers
             ILibraryManager libraryManager,
             IDtoService dtoService,
             ISessionContext sessionContext,
-            IStreamHelper streamHelper,
             IMediaSourceManager mediaSourceManager,
-            IConfigurationManager configurationManager)
+            IConfigurationManager configurationManager,
+            TranscodingJobHelper transcodingJobHelper)
         {
             _liveTvManager = liveTvManager;
             _userManager = userManager;
@@ -78,9 +77,9 @@ namespace Jellyfin.Api.Controllers
             _libraryManager = libraryManager;
             _dtoService = dtoService;
             _sessionContext = sessionContext;
-            _streamHelper = streamHelper;
             _mediaSourceManager = mediaSourceManager;
             _configurationManager = configurationManager;
+            _transcodingJobHelper = transcodingJobHelper;
         }
 
         /// <summary>
@@ -1187,7 +1186,9 @@ namespace Jellyfin.Api.Controllers
             }
 
             await using var memoryStream = new MemoryStream();
-            await new ProgressiveFileCopier(_streamHelper, path).WriteToAsync(memoryStream, CancellationToken.None).ConfigureAwait(false);
+            await new ProgressiveFileCopier(path, null, _transcodingJobHelper, CancellationToken.None)
+                .WriteToAsync(memoryStream, CancellationToken.None)
+                .ConfigureAwait(false);
             return File(memoryStream, MimeTypes.GetMimeType(path));
         }
 
@@ -1214,7 +1215,9 @@ namespace Jellyfin.Api.Controllers
             }
 
             await using var memoryStream = new MemoryStream();
-            await new ProgressiveFileCopier(_streamHelper, liveStreamInfo).WriteToAsync(memoryStream, CancellationToken.None).ConfigureAwait(false);
+            await new ProgressiveFileCopier(liveStreamInfo, null, _transcodingJobHelper, CancellationToken.None)
+                .WriteToAsync(memoryStream, CancellationToken.None)
+                .ConfigureAwait(false);
             return File(memoryStream, MimeTypes.GetMimeType("file." + container));
         }
 

+ 11 - 10
Jellyfin.Api/Controllers/VideosController.cs

@@ -46,7 +46,6 @@ namespace Jellyfin.Api.Controllers
         private readonly IMediaSourceManager _mediaSourceManager;
         private readonly IServerConfigurationManager _serverConfigurationManager;
         private readonly IMediaEncoder _mediaEncoder;
-        private readonly IStreamHelper _streamHelper;
         private readonly IFileSystem _fileSystem;
         private readonly ISubtitleEncoder _subtitleEncoder;
         private readonly IConfiguration _configuration;
@@ -67,7 +66,6 @@ namespace Jellyfin.Api.Controllers
         /// <param name="mediaSourceManager">Instance of the <see cref="IMediaSourceManager"/> interface.</param>
         /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
         /// <param name="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> interface.</param>
-        /// <param name="streamHelper">Instance of the <see cref="IStreamHelper"/> interface.</param>
         /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
         /// <param name="subtitleEncoder">Instance of the <see cref="ISubtitleEncoder"/> interface.</param>
         /// <param name="configuration">Instance of the <see cref="IConfiguration"/> interface.</param>
@@ -83,7 +81,6 @@ namespace Jellyfin.Api.Controllers
             IMediaSourceManager mediaSourceManager,
             IServerConfigurationManager serverConfigurationManager,
             IMediaEncoder mediaEncoder,
-            IStreamHelper streamHelper,
             IFileSystem fileSystem,
             ISubtitleEncoder subtitleEncoder,
             IConfiguration configuration,
@@ -99,7 +96,6 @@ namespace Jellyfin.Api.Controllers
             _mediaSourceManager = mediaSourceManager;
             _serverConfigurationManager = serverConfigurationManager;
             _mediaEncoder = mediaEncoder;
-            _streamHelper = streamHelper;
             _fileSystem = fileSystem;
             _subtitleEncoder = subtitleEncoder;
             _configuration = configuration;
@@ -376,7 +372,7 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] Dictionary<string, string> streamOptions)
         {
             var isHeadRequest = Request.Method == System.Net.WebRequestMethods.Http.Head;
-            using var cancellationTokenSource = new CancellationTokenSource();
+            var cancellationTokenSource = new CancellationTokenSource();
             var streamingRequest = new StreamingRequestDto
             {
                 Id = itemId,
@@ -453,8 +449,11 @@ namespace Jellyfin.Api.Controllers
             {
                 StreamingHelpers.AddDlnaHeaders(state, Response.Headers, true, startTimeTicks, Request, _dlnaManager);
 
-                // TODO AllowEndOfFile = false
-                await new ProgressiveFileCopier(_streamHelper, state.DirectStreamProvider).WriteToAsync(Response.Body, CancellationToken.None).ConfigureAwait(false);
+                await new ProgressiveFileCopier(state.DirectStreamProvider, null, _transcodingJobHelper, CancellationToken.None)
+                    {
+                        AllowEndOfFile = false
+                    }.WriteToAsync(Response.Body, CancellationToken.None)
+                    .ConfigureAwait(false);
 
                 // TODO (moved from MediaBrowser.Api): Don't hardcode contentType
                 return File(Response.Body, MimeTypes.GetMimeType("file.ts")!);
@@ -489,8 +488,11 @@ namespace Jellyfin.Api.Controllers
 
                 if (state.MediaSource.IsInfiniteStream)
                 {
-                    // TODO AllowEndOfFile = false
-                    await new ProgressiveFileCopier(_streamHelper, state.MediaPath).WriteToAsync(Response.Body, CancellationToken.None).ConfigureAwait(false);
+                    await new ProgressiveFileCopier(state.MediaPath, null, _transcodingJobHelper, CancellationToken.None)
+                        {
+                            AllowEndOfFile = false
+                        }.WriteToAsync(Response.Body, CancellationToken.None)
+                        .ConfigureAwait(false);
 
                     return File(Response.Body, contentType);
                 }
@@ -509,7 +511,6 @@ namespace Jellyfin.Api.Controllers
             return await FileStreamResponseHelpers.GetTranscodedFile(
                 state,
                 isHeadRequest,
-                _streamHelper,
                 this,
                 _transcodingJobHelper,
                 ffmpegCommandLineArguments,

+ 7 - 7
Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs

@@ -3,9 +3,9 @@ using System.IO;
 using System.Net.Http;
 using System.Threading;
 using System.Threading.Tasks;
+using Jellyfin.Api.Models.PlaybackDtos;
 using Jellyfin.Api.Models.StreamingDtos;
 using MediaBrowser.Controller.MediaEncoding;
-using MediaBrowser.Model.IO;
 using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Mvc;
 using Microsoft.Net.Http.Headers;
@@ -80,7 +80,6 @@ namespace Jellyfin.Api.Helpers
         /// </summary>
         /// <param name="state">The current <see cref="StreamState"/>.</param>
         /// <param name="isHeadRequest">Whether the current request is a HTTP HEAD request so only the headers get returned.</param>
-        /// <param name="streamHelper">Instance of the <see cref="IStreamHelper"/> interface.</param>
         /// <param name="controller">The <see cref="ControllerBase"/> managing the response.</param>
         /// <param name="transcodingJobHelper">The <see cref="TranscodingJobHelper"/> singleton.</param>
         /// <param name="ffmpegCommandLineArguments">The command line arguments to start ffmpeg.</param>
@@ -91,7 +90,6 @@ namespace Jellyfin.Api.Helpers
         public static async Task<ActionResult> GetTranscodedFile(
             StreamState state,
             bool isHeadRequest,
-            IStreamHelper streamHelper,
             ControllerBase controller,
             TranscodingJobHelper transcodingJobHelper,
             string ffmpegCommandLineArguments,
@@ -116,18 +114,20 @@ namespace Jellyfin.Api.Helpers
             await transcodingLock.WaitAsync(cancellationTokenSource.Token).ConfigureAwait(false);
             try
             {
+                TranscodingJobDto? job;
                 if (!File.Exists(outputPath))
                 {
-                    await transcodingJobHelper.StartFfMpeg(state, outputPath, ffmpegCommandLineArguments, request, transcodingJobType, cancellationTokenSource).ConfigureAwait(false);
+                    job = await transcodingJobHelper.StartFfMpeg(state, outputPath, ffmpegCommandLineArguments, request, transcodingJobType, cancellationTokenSource).ConfigureAwait(false);
                 }
                 else
                 {
-                    transcodingJobHelper.OnTranscodeBeginRequest(outputPath, TranscodingJobType.Progressive);
+                    job = transcodingJobHelper.OnTranscodeBeginRequest(outputPath, TranscodingJobType.Progressive);
                     state.Dispose();
                 }
 
-                await using var memoryStream = new MemoryStream();
-                await new ProgressiveFileCopier(streamHelper, outputPath).WriteToAsync(memoryStream, CancellationToken.None).ConfigureAwait(false);
+                var memoryStream = new MemoryStream();
+                await new ProgressiveFileCopier(outputPath, job, transcodingJobHelper, CancellationToken.None).WriteToAsync(memoryStream, CancellationToken.None).ConfigureAwait(false);
+                memoryStream.Position = 0;
                 return controller.File(memoryStream, contentType);
             }
             finally

+ 134 - 28
Jellyfin.Api/Helpers/ProgressiveFileCopier.cs

@@ -2,6 +2,7 @@ using System;
 using System.IO;
 using System.Threading;
 using System.Threading.Tasks;
+using Jellyfin.Api.Models.PlaybackDtos;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Model.IO;
 
@@ -12,34 +13,53 @@ namespace Jellyfin.Api.Helpers
     /// </summary>
     public class ProgressiveFileCopier
     {
+        private readonly TranscodingJobDto? _job;
         private readonly string? _path;
+        private readonly CancellationToken _cancellationToken;
         private readonly IDirectStreamProvider? _directStreamProvider;
-        private readonly IStreamHelper _streamHelper;
+        private readonly TranscodingJobHelper _transcodingJobHelper;
+        private long _bytesWritten;
 
         /// <summary>
         /// Initializes a new instance of the <see cref="ProgressiveFileCopier"/> class.
         /// </summary>
-        /// <param name="streamHelper">Instance of the <see cref="IStreamHelper"/> interface.</param>
-        /// <param name="path">Filepath to stream from.</param>
-        public ProgressiveFileCopier(IStreamHelper streamHelper, string path)
+        /// <param name="path">The path to copy from.</param>
+        /// <param name="job">The transcoding job.</param>
+        /// <param name="transcodingJobHelper">Instance of the <see cref="TranscodingJobHelper"/>.</param>
+        /// <param name="cancellationToken">The cancellation token.</param>
+        public ProgressiveFileCopier(string path, TranscodingJobDto? job, TranscodingJobHelper transcodingJobHelper, CancellationToken cancellationToken)
         {
             _path = path;
-            _streamHelper = streamHelper;
-            _directStreamProvider = null;
+            _job = job;
+            _cancellationToken = cancellationToken;
+            _transcodingJobHelper = transcodingJobHelper;
         }
 
         /// <summary>
         /// Initializes a new instance of the <see cref="ProgressiveFileCopier"/> class.
         /// </summary>
-        /// <param name="streamHelper">Instance of the <see cref="IStreamHelper"/> interface.</param>
         /// <param name="directStreamProvider">Instance of the <see cref="IDirectStreamProvider"/> interface.</param>
-        public ProgressiveFileCopier(IStreamHelper streamHelper, IDirectStreamProvider directStreamProvider)
+        /// <param name="job">The transcoding job.</param>
+        /// <param name="transcodingJobHelper">Instance of the <see cref="TranscodingJobHelper"/>.</param>
+        /// <param name="cancellationToken">The cancellation token.</param>
+        public ProgressiveFileCopier(IDirectStreamProvider directStreamProvider, TranscodingJobDto? job, TranscodingJobHelper transcodingJobHelper, CancellationToken cancellationToken)
         {
             _directStreamProvider = directStreamProvider;
-            _streamHelper = streamHelper;
-            _path = null;
+            _job = job;
+            _cancellationToken = cancellationToken;
+            _transcodingJobHelper = transcodingJobHelper;
         }
 
+        /// <summary>
+        /// Gets or sets a value indicating whether allow read end of file.
+        /// </summary>
+        public bool AllowEndOfFile { get; set; } = true;
+
+        /// <summary>
+        /// Gets or sets copy start position.
+        /// </summary>
+        public long StartPosition { get; set; }
+
         /// <summary>
         /// Write source stream to output.
         /// </summary>
@@ -48,37 +68,123 @@ namespace Jellyfin.Api.Helpers
         /// <returns>A <see cref="Task"/>.</returns>
         public async Task WriteToAsync(Stream outputStream, CancellationToken cancellationToken)
         {
-            if (_directStreamProvider != null)
+            cancellationToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, _cancellationToken).Token;
+
+            try
             {
-                await _directStreamProvider.CopyToAsync(outputStream, cancellationToken).ConfigureAwait(false);
-                return;
-            }
+                if (_directStreamProvider != null)
+                {
+                    await _directStreamProvider.CopyToAsync(outputStream, cancellationToken).ConfigureAwait(false);
+                    return;
+                }
+
+                var fileOptions = FileOptions.SequentialScan;
+                var allowAsyncFileRead = false;
+
+                // use non-async filestream along with read due to https://github.com/dotnet/corefx/issues/6039
+                if (Environment.OSVersion.Platform != PlatformID.Win32NT)
+                {
+                    fileOptions |= FileOptions.Asynchronous;
+                    allowAsyncFileRead = true;
+                }
+
+                await using var inputStream = new FileStream(_path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, IODefaults.FileStreamBufferSize, fileOptions);
+
+                var eofCount = 0;
+                const int emptyReadLimit = 20;
+                if (StartPosition > 0)
+                {
+                    inputStream.Position = StartPosition;
+                }
+
+                while (eofCount < emptyReadLimit || !AllowEndOfFile)
+                {
+                    int bytesRead;
+                    if (allowAsyncFileRead)
+                    {
+                        bytesRead = await CopyToInternalAsync(inputStream, outputStream, cancellationToken).ConfigureAwait(false);
+                    }
+                    else
+                    {
+                        bytesRead = await CopyToInternalAsyncWithSyncRead(inputStream, outputStream, cancellationToken).ConfigureAwait(false);
+                    }
 
-            var fileOptions = FileOptions.SequentialScan;
+                    if (bytesRead == 0)
+                    {
+                        if (_job == null || _job.HasExited)
+                        {
+                            eofCount++;
+                        }
 
-            // use non-async filestream along with read due to https://github.com/dotnet/corefx/issues/6039
-            if (Environment.OSVersion.Platform != PlatformID.Win32NT)
+                        await Task.Delay(100, cancellationToken).ConfigureAwait(false);
+                    }
+                    else
+                    {
+                        eofCount = 0;
+                    }
+                }
+            }
+            finally
             {
-                fileOptions |= FileOptions.Asynchronous;
+                if (_job != null)
+                {
+                    _transcodingJobHelper.OnTranscodeEndRequest(_job);
+                }
             }
+        }
 
-            await using var inputStream = new FileStream(_path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, 4096, fileOptions);
-            const int emptyReadLimit = 100;
-            var eofCount = 0;
-            while (eofCount < emptyReadLimit)
+        private async Task<int> CopyToInternalAsyncWithSyncRead(Stream source, Stream destination, CancellationToken cancellationToken)
+        {
+            var array = new byte[IODefaults.CopyToBufferSize];
+            int bytesRead;
+            int totalBytesRead = 0;
+
+            while ((bytesRead = source.Read(array, 0, array.Length)) != 0)
             {
-                var bytesRead = await _streamHelper.CopyToAsync(inputStream, outputStream, cancellationToken).ConfigureAwait(false);
+                var bytesToWrite = bytesRead;
 
-                if (bytesRead == 0)
+                if (bytesToWrite > 0)
                 {
-                    eofCount++;
-                    await Task.Delay(100, cancellationToken).ConfigureAwait(false);
+                    await destination.WriteAsync(array, 0, Convert.ToInt32(bytesToWrite), cancellationToken).ConfigureAwait(false);
+
+                    _bytesWritten += bytesRead;
+                    totalBytesRead += bytesRead;
+
+                    if (_job != null)
+                    {
+                        _job.BytesDownloaded = Math.Max(_job.BytesDownloaded ?? _bytesWritten, _bytesWritten);
+                    }
                 }
-                else
+            }
+
+            return totalBytesRead;
+        }
+
+        private async Task<int> CopyToInternalAsync(Stream source, Stream destination, CancellationToken cancellationToken)
+        {
+            var array = new byte[IODefaults.CopyToBufferSize];
+            int bytesRead;
+            int totalBytesRead = 0;
+
+            while ((bytesRead = await source.ReadAsync(array, 0, array.Length, cancellationToken).ConfigureAwait(false)) != 0)
+            {
+                var bytesToWrite = bytesRead;
+
+                if (bytesToWrite > 0)
                 {
-                    eofCount = 0;
+                    await destination.WriteAsync(array, 0, Convert.ToInt32(bytesToWrite), cancellationToken).ConfigureAwait(false);
+
+                    _bytesWritten += bytesRead;
+                    totalBytesRead += bytesRead;
+
+                    if (_job != null)
+                    {
+                        _job.BytesDownloaded = Math.Max(_job.BytesDownloaded ?? _bytesWritten, _bytesWritten);
+                    }
                 }
             }
+
+            return totalBytesRead;
         }
     }
 }

+ 14 - 0
Jellyfin.Api/Helpers/TranscodingJobHelper.cs

@@ -680,6 +680,20 @@ namespace Jellyfin.Api.Helpers
             }
         }
 
+        /// <summary>
+        /// Called when [transcode end].
+        /// </summary>
+        /// <param name="job">The transcode job.</param>
+        public void OnTranscodeEndRequest(TranscodingJobDto job)
+        {
+            job.ActiveRequestCount--;
+            _logger.LogDebug("OnTranscodeEndRequest job.ActiveRequestCount={ActiveRequestCount}", job.ActiveRequestCount);
+            if (job.ActiveRequestCount <= 0)
+            {
+                PingTimer(job, false);
+            }
+        }
+
         /// <summary>
         /// <summary>
         /// The progressive