Browse Source

Merge pull request #3691 from crobibero/api-video

move VideoService.cs to Jellyfin.Api
Cody Robibero 4 years ago
parent
commit
cb31aba5dd

+ 27 - 24
Jellyfin.Api/Controllers/AudioController.cs

@@ -35,13 +35,12 @@ namespace Jellyfin.Api.Controllers
         private readonly IMediaSourceManager _mediaSourceManager;
         private readonly IMediaSourceManager _mediaSourceManager;
         private readonly IServerConfigurationManager _serverConfigurationManager;
         private readonly IServerConfigurationManager _serverConfigurationManager;
         private readonly IMediaEncoder _mediaEncoder;
         private readonly IMediaEncoder _mediaEncoder;
-        private readonly IStreamHelper _streamHelper;
         private readonly IFileSystem _fileSystem;
         private readonly IFileSystem _fileSystem;
         private readonly ISubtitleEncoder _subtitleEncoder;
         private readonly ISubtitleEncoder _subtitleEncoder;
         private readonly IConfiguration _configuration;
         private readonly IConfiguration _configuration;
         private readonly IDeviceManager _deviceManager;
         private readonly IDeviceManager _deviceManager;
         private readonly TranscodingJobHelper _transcodingJobHelper;
         private readonly TranscodingJobHelper _transcodingJobHelper;
-        private readonly HttpClient _httpClient;
+        private readonly IHttpClientFactory _httpClientFactory;
 
 
         private readonly TranscodingJobType _transcodingJobType = TranscodingJobType.Progressive;
         private readonly TranscodingJobType _transcodingJobType = TranscodingJobType.Progressive;
 
 
@@ -55,13 +54,12 @@ namespace Jellyfin.Api.Controllers
         /// <param name="mediaSourceManager">Instance of the <see cref="IMediaSourceManager"/> interface.</param>
         /// <param name="mediaSourceManager">Instance of the <see cref="IMediaSourceManager"/> interface.</param>
         /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> 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="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="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
         /// <param name="subtitleEncoder">Instance of the <see cref="ISubtitleEncoder"/> interface.</param>
         /// <param name="subtitleEncoder">Instance of the <see cref="ISubtitleEncoder"/> interface.</param>
         /// <param name="configuration">Instance of the <see cref="IConfiguration"/> interface.</param>
         /// <param name="configuration">Instance of the <see cref="IConfiguration"/> interface.</param>
         /// <param name="deviceManager">Instance of the <see cref="IDeviceManager"/> interface.</param>
         /// <param name="deviceManager">Instance of the <see cref="IDeviceManager"/> interface.</param>
         /// <param name="transcodingJobHelper">The <see cref="TranscodingJobHelper"/> singleton.</param>
         /// <param name="transcodingJobHelper">The <see cref="TranscodingJobHelper"/> singleton.</param>
-        /// <param name="httpClient">Instance of the <see cref="HttpClient"/>.</param>
+        /// <param name="httpClientFactory">Instance of the <see cref="IHttpClientFactory"/> interface.</param>
         public AudioController(
         public AudioController(
             IDlnaManager dlnaManager,
             IDlnaManager dlnaManager,
             IUserManager userManger,
             IUserManager userManger,
@@ -70,13 +68,12 @@ namespace Jellyfin.Api.Controllers
             IMediaSourceManager mediaSourceManager,
             IMediaSourceManager mediaSourceManager,
             IServerConfigurationManager serverConfigurationManager,
             IServerConfigurationManager serverConfigurationManager,
             IMediaEncoder mediaEncoder,
             IMediaEncoder mediaEncoder,
-            IStreamHelper streamHelper,
             IFileSystem fileSystem,
             IFileSystem fileSystem,
             ISubtitleEncoder subtitleEncoder,
             ISubtitleEncoder subtitleEncoder,
             IConfiguration configuration,
             IConfiguration configuration,
             IDeviceManager deviceManager,
             IDeviceManager deviceManager,
             TranscodingJobHelper transcodingJobHelper,
             TranscodingJobHelper transcodingJobHelper,
-            HttpClient httpClient)
+            IHttpClientFactory httpClientFactory)
         {
         {
             _dlnaManager = dlnaManager;
             _dlnaManager = dlnaManager;
             _authContext = authorizationContext;
             _authContext = authorizationContext;
@@ -85,13 +82,12 @@ namespace Jellyfin.Api.Controllers
             _mediaSourceManager = mediaSourceManager;
             _mediaSourceManager = mediaSourceManager;
             _serverConfigurationManager = serverConfigurationManager;
             _serverConfigurationManager = serverConfigurationManager;
             _mediaEncoder = mediaEncoder;
             _mediaEncoder = mediaEncoder;
-            _streamHelper = streamHelper;
             _fileSystem = fileSystem;
             _fileSystem = fileSystem;
             _subtitleEncoder = subtitleEncoder;
             _subtitleEncoder = subtitleEncoder;
             _configuration = configuration;
             _configuration = configuration;
             _deviceManager = deviceManager;
             _deviceManager = deviceManager;
             _transcodingJobHelper = transcodingJobHelper;
             _transcodingJobHelper = transcodingJobHelper;
-            _httpClient = httpClient;
+            _httpClientFactory = httpClientFactory;
         }
         }
 
 
         /// <summary>
         /// <summary>
@@ -146,6 +142,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="videoStreamIndex">Optional. The index of the video stream to use. If omitted the first video stream will be used.</param>
         /// <param name="videoStreamIndex">Optional. The index of the video stream to use. If omitted the first video stream will be used.</param>
         /// <param name="context">Optional. The <see cref="EncodingContext"/>.</param>
         /// <param name="context">Optional. The <see cref="EncodingContext"/>.</param>
         /// <param name="streamOptions">Optional. The streaming options.</param>
         /// <param name="streamOptions">Optional. The streaming options.</param>
+        /// <response code="200">Audio stream returned.</response>
         /// <returns>A <see cref="FileResult"/> containing the audio file.</returns>
         /// <returns>A <see cref="FileResult"/> containing the audio file.</returns>
         [HttpGet("{itemId}/{stream=stream}.{container?}")]
         [HttpGet("{itemId}/{stream=stream}.{container?}")]
         [HttpGet("{itemId}/stream")]
         [HttpGet("{itemId}/stream")]
@@ -211,7 +208,7 @@ namespace Jellyfin.Api.Controllers
             {
             {
                 Id = itemId,
                 Id = itemId,
                 Container = container,
                 Container = container,
-                Static = @static.HasValue ? @static.Value : true,
+                Static = @static ?? true,
                 Params = @params,
                 Params = @params,
                 Tag = tag,
                 Tag = tag,
                 DeviceProfileId = deviceProfileId,
                 DeviceProfileId = deviceProfileId,
@@ -222,10 +219,10 @@ namespace Jellyfin.Api.Controllers
                 MediaSourceId = mediaSourceId,
                 MediaSourceId = mediaSourceId,
                 DeviceId = deviceId,
                 DeviceId = deviceId,
                 AudioCodec = audioCodec,
                 AudioCodec = audioCodec,
-                EnableAutoStreamCopy = enableAutoStreamCopy.HasValue ? enableAutoStreamCopy.Value : true,
-                AllowAudioStreamCopy = allowAudioStreamCopy.HasValue ? allowAudioStreamCopy.Value : true,
-                AllowVideoStreamCopy = allowVideoStreamCopy.HasValue ? allowVideoStreamCopy.Value : true,
-                BreakOnNonKeyFrames = breakOnNonKeyFrames.HasValue ? breakOnNonKeyFrames.Value : false,
+                EnableAutoStreamCopy = enableAutoStreamCopy ?? true,
+                AllowAudioStreamCopy = allowAudioStreamCopy ?? true,
+                AllowVideoStreamCopy = allowVideoStreamCopy ?? true,
+                BreakOnNonKeyFrames = breakOnNonKeyFrames ?? false,
                 AudioSampleRate = audioSampleRate,
                 AudioSampleRate = audioSampleRate,
                 MaxAudioChannels = maxAudioChannels,
                 MaxAudioChannels = maxAudioChannels,
                 AudioBitRate = audioBitRate,
                 AudioBitRate = audioBitRate,
@@ -235,7 +232,7 @@ namespace Jellyfin.Api.Controllers
                 Level = level,
                 Level = level,
                 Framerate = framerate,
                 Framerate = framerate,
                 MaxFramerate = maxFramerate,
                 MaxFramerate = maxFramerate,
-                CopyTimestamps = copyTimestamps.HasValue ? copyTimestamps.Value : true,
+                CopyTimestamps = copyTimestamps ?? true,
                 StartTimeTicks = startTimeTicks,
                 StartTimeTicks = startTimeTicks,
                 Width = width,
                 Width = width,
                 Height = height,
                 Height = height,
@@ -244,13 +241,13 @@ namespace Jellyfin.Api.Controllers
                 SubtitleMethod = subtitleMethod,
                 SubtitleMethod = subtitleMethod,
                 MaxRefFrames = maxRefFrames,
                 MaxRefFrames = maxRefFrames,
                 MaxVideoBitDepth = maxVideoBitDepth,
                 MaxVideoBitDepth = maxVideoBitDepth,
-                RequireAvc = requireAvc.HasValue ? requireAvc.Value : true,
-                DeInterlace = deInterlace.HasValue ? deInterlace.Value : true,
-                RequireNonAnamorphic = requireNonAnamorphic.HasValue ? requireNonAnamorphic.Value : true,
+                RequireAvc = requireAvc ?? true,
+                DeInterlace = deInterlace ?? true,
+                RequireNonAnamorphic = requireNonAnamorphic ?? true,
                 TranscodingMaxAudioChannels = transcodingMaxAudioChannels,
                 TranscodingMaxAudioChannels = transcodingMaxAudioChannels,
                 CpuCoreLimit = cpuCoreLimit,
                 CpuCoreLimit = cpuCoreLimit,
                 LiveStreamId = liveStreamId,
                 LiveStreamId = liveStreamId,
-                EnableMpegtsM2TsMode = enableMpegtsM2TsMode.HasValue ? enableMpegtsM2TsMode.Value : true,
+                EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? true,
                 VideoCodec = videoCodec,
                 VideoCodec = videoCodec,
                 SubtitleCodec = subtitleCodec,
                 SubtitleCodec = subtitleCodec,
                 TranscodeReasons = transcodingReasons,
                 TranscodeReasons = transcodingReasons,
@@ -283,8 +280,11 @@ namespace Jellyfin.Api.Controllers
             {
             {
                 StreamingHelpers.AddDlnaHeaders(state, Response.Headers, true, startTimeTicks, Request, _dlnaManager);
                 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
                 // TODO (moved from MediaBrowser.Api): Don't hardcode contentType
                 return File(Response.Body, MimeTypes.GetMimeType("file.ts")!);
                 return File(Response.Body, MimeTypes.GetMimeType("file.ts")!);
@@ -295,7 +295,8 @@ namespace Jellyfin.Api.Controllers
             {
             {
                 StreamingHelpers.AddDlnaHeaders(state, Response.Headers, true, startTimeTicks, Request, _dlnaManager);
                 StreamingHelpers.AddDlnaHeaders(state, Response.Headers, true, startTimeTicks, Request, _dlnaManager);
 
 
-                return await FileStreamResponseHelpers.GetStaticRemoteStreamResult(state, isHeadRequest, this, _httpClient).ConfigureAwait(false);
+                using var httpClient = _httpClientFactory.CreateClient();
+                return await FileStreamResponseHelpers.GetStaticRemoteStreamResult(state, isHeadRequest, this, httpClient).ConfigureAwait(false);
             }
             }
 
 
             if (@static.HasValue && @static.Value && state.InputProtocol != MediaProtocol.File)
             if (@static.HasValue && @static.Value && state.InputProtocol != MediaProtocol.File)
@@ -318,8 +319,11 @@ namespace Jellyfin.Api.Controllers
 
 
                 if (state.MediaSource.IsInfiniteStream)
                 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);
                     return File(Response.Body, contentType);
                 }
                 }
@@ -338,7 +342,6 @@ namespace Jellyfin.Api.Controllers
             return await FileStreamResponseHelpers.GetTranscodedFile(
             return await FileStreamResponseHelpers.GetTranscodedFile(
                 state,
                 state,
                 isHeadRequest,
                 isHeadRequest,
-                _streamHelper,
                 this,
                 this,
                 _transcodingJobHelper,
                 _transcodingJobHelper,
                 ffmpegCommandLineArguments,
                 ffmpegCommandLineArguments,

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

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

+ 4 - 3
Jellyfin.Api/Controllers/MediaInfoController.cs

@@ -7,6 +7,7 @@ using System.Text.Json;
 using System.Threading;
 using System.Threading;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
 using Jellyfin.Api.Constants;
 using Jellyfin.Api.Constants;
+using Jellyfin.Api.Models.VideoDtos;
 using Jellyfin.Data.Entities;
 using Jellyfin.Data.Entities;
 using Jellyfin.Data.Enums;
 using Jellyfin.Data.Enums;
 using MediaBrowser.Common.Net;
 using MediaBrowser.Common.Net;
@@ -126,7 +127,7 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] int? maxAudioChannels,
             [FromQuery] int? maxAudioChannels,
             [FromQuery] string? mediaSourceId,
             [FromQuery] string? mediaSourceId,
             [FromQuery] string? liveStreamId,
             [FromQuery] string? liveStreamId,
-            [FromQuery] DeviceProfile? deviceProfile,
+            [FromBody] DeviceProfileDto? deviceProfile,
             [FromQuery] bool autoOpenLiveStream = false,
             [FromQuery] bool autoOpenLiveStream = false,
             [FromQuery] bool enableDirectPlay = true,
             [FromQuery] bool enableDirectPlay = true,
             [FromQuery] bool enableDirectStream = true,
             [FromQuery] bool enableDirectStream = true,
@@ -136,7 +137,7 @@ namespace Jellyfin.Api.Controllers
         {
         {
             var authInfo = _authContext.GetAuthorizationInfo(Request);
             var authInfo = _authContext.GetAuthorizationInfo(Request);
 
 
-            var profile = deviceProfile;
+            var profile = deviceProfile?.DeviceProfile;
 
 
             _logger.LogInformation("GetPostedPlaybackInfo profile: {@Profile}", profile);
             _logger.LogInformation("GetPostedPlaybackInfo profile: {@Profile}", profile);
 
 
@@ -190,7 +191,7 @@ namespace Jellyfin.Api.Controllers
                     var openStreamResult = await OpenMediaSource(new LiveStreamRequest
                     var openStreamResult = await OpenMediaSource(new LiveStreamRequest
                     {
                     {
                         AudioStreamIndex = audioStreamIndex,
                         AudioStreamIndex = audioStreamIndex,
-                        DeviceProfile = deviceProfile,
+                        DeviceProfile = deviceProfile?.DeviceProfile,
                         EnableDirectPlay = enableDirectPlay,
                         EnableDirectPlay = enableDirectPlay,
                         EnableDirectStream = enableDirectStream,
                         EnableDirectStream = enableDirectStream,
                         ItemId = itemId,
                         ItemId = itemId,

+ 320 - 1
Jellyfin.Api/Controllers/VideosController.cs

@@ -1,19 +1,34 @@
 using System;
 using System;
+using System.Collections.Generic;
 using System.Globalization;
 using System.Globalization;
 using System.Linq;
 using System.Linq;
+using System.Net.Http;
 using System.Threading;
 using System.Threading;
+using System.Threading.Tasks;
 using Jellyfin.Api.Constants;
 using Jellyfin.Api.Constants;
 using Jellyfin.Api.Extensions;
 using Jellyfin.Api.Extensions;
 using Jellyfin.Api.Helpers;
 using Jellyfin.Api.Helpers;
+using Jellyfin.Api.Models.StreamingDtos;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Devices;
+using MediaBrowser.Controller.Dlna;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.MediaEncoding;
+using MediaBrowser.Controller.Net;
+using MediaBrowser.Model.Dlna;
 using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.IO;
+using MediaBrowser.Model.MediaInfo;
+using MediaBrowser.Model.Net;
 using MediaBrowser.Model.Querying;
 using MediaBrowser.Model.Querying;
 using Microsoft.AspNetCore.Authorization;
 using Microsoft.AspNetCore.Authorization;
 using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Mvc;
 using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.Configuration;
 
 
 namespace Jellyfin.Api.Controllers
 namespace Jellyfin.Api.Controllers
 {
 {
@@ -26,6 +41,19 @@ namespace Jellyfin.Api.Controllers
         private readonly ILibraryManager _libraryManager;
         private readonly ILibraryManager _libraryManager;
         private readonly IUserManager _userManager;
         private readonly IUserManager _userManager;
         private readonly IDtoService _dtoService;
         private readonly IDtoService _dtoService;
+        private readonly IDlnaManager _dlnaManager;
+        private readonly IAuthorizationContext _authContext;
+        private readonly IMediaSourceManager _mediaSourceManager;
+        private readonly IServerConfigurationManager _serverConfigurationManager;
+        private readonly IMediaEncoder _mediaEncoder;
+        private readonly IFileSystem _fileSystem;
+        private readonly ISubtitleEncoder _subtitleEncoder;
+        private readonly IConfiguration _configuration;
+        private readonly IDeviceManager _deviceManager;
+        private readonly TranscodingJobHelper _transcodingJobHelper;
+        private readonly IHttpClientFactory _httpClientFactory;
+
+        private readonly TranscodingJobType _transcodingJobType = TranscodingJobType.Progressive;
 
 
         /// <summary>
         /// <summary>
         /// Initializes a new instance of the <see cref="VideosController"/> class.
         /// Initializes a new instance of the <see cref="VideosController"/> class.
@@ -33,14 +61,47 @@ namespace Jellyfin.Api.Controllers
         /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
         /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
         /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
         /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
         /// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param>
         /// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param>
+        /// <param name="dlnaManager">Instance of the <see cref="IDlnaManager"/> interface.</param>
+        /// <param name="authContext">Instance of the <see cref="IAuthorizationContext"/> interface.</param>
+        /// <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="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>
+        /// <param name="deviceManager">Instance of the <see cref="IDeviceManager"/> interface.</param>
+        /// <param name="transcodingJobHelper">Instance of the <see cref="TranscodingJobHelper"/> class.</param>
+        /// <param name="httpClientFactory">Instance of the <see cref="IHttpClientFactory"/> interface.</param>
         public VideosController(
         public VideosController(
             ILibraryManager libraryManager,
             ILibraryManager libraryManager,
             IUserManager userManager,
             IUserManager userManager,
-            IDtoService dtoService)
+            IDtoService dtoService,
+            IDlnaManager dlnaManager,
+            IAuthorizationContext authContext,
+            IMediaSourceManager mediaSourceManager,
+            IServerConfigurationManager serverConfigurationManager,
+            IMediaEncoder mediaEncoder,
+            IFileSystem fileSystem,
+            ISubtitleEncoder subtitleEncoder,
+            IConfiguration configuration,
+            IDeviceManager deviceManager,
+            TranscodingJobHelper transcodingJobHelper,
+            IHttpClientFactory httpClientFactory)
         {
         {
             _libraryManager = libraryManager;
             _libraryManager = libraryManager;
             _userManager = userManager;
             _userManager = userManager;
             _dtoService = dtoService;
             _dtoService = dtoService;
+            _dlnaManager = dlnaManager;
+            _authContext = authContext;
+            _mediaSourceManager = mediaSourceManager;
+            _serverConfigurationManager = serverConfigurationManager;
+            _mediaEncoder = mediaEncoder;
+            _fileSystem = fileSystem;
+            _subtitleEncoder = subtitleEncoder;
+            _configuration = configuration;
+            _deviceManager = deviceManager;
+            _transcodingJobHelper = transcodingJobHelper;
+            _httpClientFactory = httpClientFactory;
         }
         }
 
 
         /// <summary>
         /// <summary>
@@ -200,5 +261,263 @@ namespace Jellyfin.Api.Controllers
             primaryVersion.UpdateToRepository(ItemUpdateType.MetadataEdit, CancellationToken.None);
             primaryVersion.UpdateToRepository(ItemUpdateType.MetadataEdit, CancellationToken.None);
             return NoContent();
             return NoContent();
         }
         }
+
+        /// <summary>
+        /// Gets a video stream.
+        /// </summary>
+        /// <param name="itemId">The item id.</param>
+        /// <param name="container">The video container. Possible values are: ts, webm, asf, wmv, ogv, mp4, m4v, mkv, mpeg, mpg, avi, 3gp, wmv, wtv, m2ts, mov, iso, flv. </param>
+        /// <param name="static">Optional. If true, the original file will be streamed statically without any encoding. Use either no url extension or the original file extension. true/false.</param>
+        /// <param name="params">The streaming parameters.</param>
+        /// <param name="tag">The tag.</param>
+        /// <param name="deviceProfileId">Optional. The dlna device profile id to utilize.</param>
+        /// <param name="playSessionId">The play session id.</param>
+        /// <param name="segmentContainer">The segment container.</param>
+        /// <param name="segmentLength">The segment lenght.</param>
+        /// <param name="minSegments">The minimum number of segments.</param>
+        /// <param name="mediaSourceId">The media version id, if playing an alternate version.</param>
+        /// <param name="deviceId">The device id of the client requesting. Used to stop encoding processes when needed.</param>
+        /// <param name="audioCodec">Optional. Specify a audio codec to encode to, e.g. mp3. If omitted the server will auto-select using the url's extension. Options: aac, mp3, vorbis, wma.</param>
+        /// <param name="enableAutoStreamCopy">Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.</param>
+        /// <param name="allowVideoStreamCopy">Whether or not to allow copying of the video stream url.</param>
+        /// <param name="allowAudioStreamCopy">Whether or not to allow copying of the audio stream url.</param>
+        /// <param name="breakOnNonKeyFrames">Optional. Whether to break on non key frames.</param>
+        /// <param name="audioSampleRate">Optional. Specify a specific audio sample rate, e.g. 44100.</param>
+        /// <param name="maxAudioBitDepth">Optional. The maximum audio bit depth.</param>
+        /// <param name="audioBitRate">Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.</param>
+        /// <param name="audioChannels">Optional. Specify a specific number of audio channels to encode to, e.g. 2.</param>
+        /// <param name="maxAudioChannels">Optional. Specify a maximum number of audio channels to encode to, e.g. 2.</param>
+        /// <param name="profile">Optional. Specify a specific an encoder profile (varies by encoder), e.g. main, baseline, high.</param>
+        /// <param name="level">Optional. Specify a level for the encoder profile (varies by encoder), e.g. 3, 3.1.</param>
+        /// <param name="framerate">Optional. A specific video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.</param>
+        /// <param name="maxFramerate">Optional. A specific maximum video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.</param>
+        /// <param name="copyTimestamps">Whether or not to copy timestamps when transcoding with an offset. Defaults to false.</param>
+        /// <param name="startTimeTicks">Optional. Specify a starting offset, in ticks. 1 tick = 10000 ms.</param>
+        /// <param name="width">Optional. The fixed horizontal resolution of the encoded video.</param>
+        /// <param name="height">Optional. The fixed vertical resolution of the encoded video.</param>
+        /// <param name="videoBitRate">Optional. Specify a video bitrate to encode to, e.g. 500000. If omitted this will be left to encoder defaults.</param>
+        /// <param name="subtitleStreamIndex">Optional. The index of the subtitle stream to use. If omitted no subtitles will be used.</param>
+        /// <param name="subtitleMethod">Optional. Specify the subtitle delivery method.</param>
+        /// <param name="maxRefFrames">Optional.</param>
+        /// <param name="maxVideoBitDepth">Optional. The maximum video bit depth.</param>
+        /// <param name="requireAvc">Optional. Whether to require avc.</param>
+        /// <param name="deInterlace">Optional. Whether to deinterlace the video.</param>
+        /// <param name="requireNonAnamorphic">Optional. Whether to require a non anamporphic stream.</param>
+        /// <param name="transcodingMaxAudioChannels">Optional. The maximum number of audio channels to transcode.</param>
+        /// <param name="cpuCoreLimit">Optional. The limit of how many cpu cores to use.</param>
+        /// <param name="liveStreamId">The live stream id.</param>
+        /// <param name="enableMpegtsM2TsMode">Optional. Whether to enable the MpegtsM2Ts mode.</param>
+        /// <param name="videoCodec">Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension. Options: h265, h264, mpeg4, theora, vpx, wmv.</param>
+        /// <param name="subtitleCodec">Optional. Specify a subtitle codec to encode to.</param>
+        /// <param name="transcodingReasons">Optional. The transcoding reason.</param>
+        /// <param name="audioStreamIndex">Optional. The index of the audio stream to use. If omitted the first audio stream will be used.</param>
+        /// <param name="videoStreamIndex">Optional. The index of the video stream to use. If omitted the first video stream will be used.</param>
+        /// <param name="context">Optional. The <see cref="EncodingContext"/>.</param>
+        /// <param name="streamOptions">Optional. The streaming options.</param>
+        /// <response code="200">Video stream returned.</response>
+        /// <returns>A <see cref="FileResult"/> containing the audio file.</returns>
+        [HttpGet("{itemId}/{stream=stream}.{container?}")]
+        [HttpGet("{itemId}/stream")]
+        [HttpHead("{itemId}/{stream=stream}.{container?}")]
+        [HttpHead("{itemId}/stream")]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        public async Task<ActionResult> GetVideoStream(
+            [FromRoute] Guid itemId,
+            [FromRoute] string? container,
+            [FromQuery] bool? @static,
+            [FromQuery] string? @params,
+            [FromQuery] string? tag,
+            [FromQuery] string? deviceProfileId,
+            [FromQuery] string? playSessionId,
+            [FromQuery] string? segmentContainer,
+            [FromQuery] int? segmentLength,
+            [FromQuery] int? minSegments,
+            [FromQuery] string? mediaSourceId,
+            [FromQuery] string? deviceId,
+            [FromQuery] string? audioCodec,
+            [FromQuery] bool? enableAutoStreamCopy,
+            [FromQuery] bool? allowVideoStreamCopy,
+            [FromQuery] bool? allowAudioStreamCopy,
+            [FromQuery] bool? breakOnNonKeyFrames,
+            [FromQuery] int? audioSampleRate,
+            [FromQuery] int? maxAudioBitDepth,
+            [FromQuery] int? audioBitRate,
+            [FromQuery] int? audioChannels,
+            [FromQuery] int? maxAudioChannels,
+            [FromQuery] string? profile,
+            [FromQuery] string? level,
+            [FromQuery] float? framerate,
+            [FromQuery] float? maxFramerate,
+            [FromQuery] bool? copyTimestamps,
+            [FromQuery] long? startTimeTicks,
+            [FromQuery] int? width,
+            [FromQuery] int? height,
+            [FromQuery] int? videoBitRate,
+            [FromQuery] int? subtitleStreamIndex,
+            [FromQuery] SubtitleDeliveryMethod subtitleMethod,
+            [FromQuery] int? maxRefFrames,
+            [FromQuery] int? maxVideoBitDepth,
+            [FromQuery] bool? requireAvc,
+            [FromQuery] bool? deInterlace,
+            [FromQuery] bool? requireNonAnamorphic,
+            [FromQuery] int? transcodingMaxAudioChannels,
+            [FromQuery] int? cpuCoreLimit,
+            [FromQuery] string? liveStreamId,
+            [FromQuery] bool? enableMpegtsM2TsMode,
+            [FromQuery] string? videoCodec,
+            [FromQuery] string? subtitleCodec,
+            [FromQuery] string? transcodingReasons,
+            [FromQuery] int? audioStreamIndex,
+            [FromQuery] int? videoStreamIndex,
+            [FromQuery] EncodingContext context,
+            [FromQuery] Dictionary<string, string> streamOptions)
+        {
+            var isHeadRequest = Request.Method == System.Net.WebRequestMethods.Http.Head;
+            var cancellationTokenSource = new CancellationTokenSource();
+            var streamingRequest = new VideoRequestDto
+            {
+                Id = itemId,
+                Container = container,
+                Static = @static ?? true,
+                Params = @params,
+                Tag = tag,
+                DeviceProfileId = deviceProfileId,
+                PlaySessionId = playSessionId,
+                SegmentContainer = segmentContainer,
+                SegmentLength = segmentLength,
+                MinSegments = minSegments,
+                MediaSourceId = mediaSourceId,
+                DeviceId = deviceId,
+                AudioCodec = audioCodec,
+                EnableAutoStreamCopy = enableAutoStreamCopy ?? true,
+                AllowAudioStreamCopy = allowAudioStreamCopy ?? true,
+                AllowVideoStreamCopy = allowVideoStreamCopy ?? true,
+                BreakOnNonKeyFrames = breakOnNonKeyFrames ?? false,
+                AudioSampleRate = audioSampleRate,
+                MaxAudioChannels = maxAudioChannels,
+                AudioBitRate = audioBitRate,
+                MaxAudioBitDepth = maxAudioBitDepth,
+                AudioChannels = audioChannels,
+                Profile = profile,
+                Level = level,
+                Framerate = framerate,
+                MaxFramerate = maxFramerate,
+                CopyTimestamps = copyTimestamps ?? true,
+                StartTimeTicks = startTimeTicks,
+                Width = width,
+                Height = height,
+                VideoBitRate = videoBitRate,
+                SubtitleStreamIndex = subtitleStreamIndex,
+                SubtitleMethod = subtitleMethod,
+                MaxRefFrames = maxRefFrames,
+                MaxVideoBitDepth = maxVideoBitDepth,
+                RequireAvc = requireAvc ?? true,
+                DeInterlace = deInterlace ?? true,
+                RequireNonAnamorphic = requireNonAnamorphic ?? true,
+                TranscodingMaxAudioChannels = transcodingMaxAudioChannels,
+                CpuCoreLimit = cpuCoreLimit,
+                LiveStreamId = liveStreamId,
+                EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? true,
+                VideoCodec = videoCodec,
+                SubtitleCodec = subtitleCodec,
+                TranscodeReasons = transcodingReasons,
+                AudioStreamIndex = audioStreamIndex,
+                VideoStreamIndex = videoStreamIndex,
+                Context = context,
+                StreamOptions = streamOptions
+            };
+
+            using var state = await StreamingHelpers.GetStreamingState(
+                    streamingRequest,
+                    Request,
+                    _authContext,
+                    _mediaSourceManager,
+                    _userManager,
+                    _libraryManager,
+                    _serverConfigurationManager,
+                    _mediaEncoder,
+                    _fileSystem,
+                    _subtitleEncoder,
+                    _configuration,
+                    _dlnaManager,
+                    _deviceManager,
+                    _transcodingJobHelper,
+                    _transcodingJobType,
+                    cancellationTokenSource.Token)
+                .ConfigureAwait(false);
+
+            if (@static.HasValue && @static.Value && state.DirectStreamProvider != null)
+            {
+                StreamingHelpers.AddDlnaHeaders(state, Response.Headers, true, startTimeTicks, Request, _dlnaManager);
+
+                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")!);
+            }
+
+            // Static remote stream
+            if (@static.HasValue && @static.Value && state.InputProtocol == MediaProtocol.Http)
+            {
+                StreamingHelpers.AddDlnaHeaders(state, Response.Headers, true, startTimeTicks, Request, _dlnaManager);
+
+                using var httpClient = _httpClientFactory.CreateClient();
+                return await FileStreamResponseHelpers.GetStaticRemoteStreamResult(state, isHeadRequest, this, httpClient).ConfigureAwait(false);
+            }
+
+            if (@static.HasValue && @static.Value && state.InputProtocol != MediaProtocol.File)
+            {
+                return BadRequest($"Input protocol {state.InputProtocol} cannot be streamed statically");
+            }
+
+            var outputPath = state.OutputFilePath;
+            var outputPathExists = System.IO.File.Exists(outputPath);
+
+            var transcodingJob = _transcodingJobHelper.GetTranscodingJob(outputPath, TranscodingJobType.Progressive);
+            var isTranscodeCached = outputPathExists && transcodingJob != null;
+
+            StreamingHelpers.AddDlnaHeaders(state, Response.Headers, (@static.HasValue && @static.Value) || isTranscodeCached, startTimeTicks, Request, _dlnaManager);
+
+            // Static stream
+            if (@static.HasValue && @static.Value)
+            {
+                var contentType = state.GetMimeType("." + state.OutputContainer, false) ?? state.GetMimeType(state.MediaPath);
+
+                if (state.MediaSource.IsInfiniteStream)
+                {
+                    await new ProgressiveFileCopier(state.MediaPath, null, _transcodingJobHelper, CancellationToken.None)
+                        {
+                            AllowEndOfFile = false
+                        }.WriteToAsync(Response.Body, CancellationToken.None)
+                        .ConfigureAwait(false);
+
+                    return File(Response.Body, contentType);
+                }
+
+                return FileStreamResponseHelpers.GetStaticFileResult(
+                    state.MediaPath,
+                    contentType,
+                    isHeadRequest,
+                    this);
+            }
+
+            // Need to start ffmpeg (because media can't be returned directly)
+            var encodingOptions = _serverConfigurationManager.GetEncodingOptions();
+            var encodingHelper = new EncodingHelper(_mediaEncoder, _fileSystem, _subtitleEncoder, _configuration);
+            var ffmpegCommandLineArguments = encodingHelper.GetProgressiveVideoFullCommandLine(state, encodingOptions, outputPath, "superfast");
+            return await FileStreamResponseHelpers.GetTranscodedFile(
+                state,
+                isHeadRequest,
+                this,
+                _transcodingJobHelper,
+                ffmpegCommandLineArguments,
+                Request,
+                _transcodingJobType,
+                cancellationTokenSource).ConfigureAwait(false);
+        }
     }
     }
 }
 }

+ 8 - 9
Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs

@@ -3,9 +3,9 @@ using System.IO;
 using System.Net.Http;
 using System.Net.Http;
 using System.Threading;
 using System.Threading;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
+using Jellyfin.Api.Models.PlaybackDtos;
 using Jellyfin.Api.Models.StreamingDtos;
 using Jellyfin.Api.Models.StreamingDtos;
 using MediaBrowser.Controller.MediaEncoding;
 using MediaBrowser.Controller.MediaEncoding;
-using MediaBrowser.Model.IO;
 using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Mvc;
 using Microsoft.AspNetCore.Mvc;
 using Microsoft.Net.Http.Headers;
 using Microsoft.Net.Http.Headers;
@@ -71,8 +71,7 @@ namespace Jellyfin.Api.Helpers
                 return controller.NoContent();
                 return controller.NoContent();
             }
             }
 
 
-            using var stream = new FileStream(path, FileMode.Open, FileAccess.Read);
-            return controller.File(stream, contentType);
+            return controller.PhysicalFile(path, contentType);
         }
         }
 
 
         /// <summary>
         /// <summary>
@@ -80,7 +79,6 @@ namespace Jellyfin.Api.Helpers
         /// </summary>
         /// </summary>
         /// <param name="state">The current <see cref="StreamState"/>.</param>
         /// <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="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="controller">The <see cref="ControllerBase"/> managing the response.</param>
         /// <param name="transcodingJobHelper">The <see cref="TranscodingJobHelper"/> singleton.</param>
         /// <param name="transcodingJobHelper">The <see cref="TranscodingJobHelper"/> singleton.</param>
         /// <param name="ffmpegCommandLineArguments">The command line arguments to start ffmpeg.</param>
         /// <param name="ffmpegCommandLineArguments">The command line arguments to start ffmpeg.</param>
@@ -91,7 +89,6 @@ namespace Jellyfin.Api.Helpers
         public static async Task<ActionResult> GetTranscodedFile(
         public static async Task<ActionResult> GetTranscodedFile(
             StreamState state,
             StreamState state,
             bool isHeadRequest,
             bool isHeadRequest,
-            IStreamHelper streamHelper,
             ControllerBase controller,
             ControllerBase controller,
             TranscodingJobHelper transcodingJobHelper,
             TranscodingJobHelper transcodingJobHelper,
             string ffmpegCommandLineArguments,
             string ffmpegCommandLineArguments,
@@ -116,18 +113,20 @@ namespace Jellyfin.Api.Helpers
             await transcodingLock.WaitAsync(cancellationTokenSource.Token).ConfigureAwait(false);
             await transcodingLock.WaitAsync(cancellationTokenSource.Token).ConfigureAwait(false);
             try
             try
             {
             {
+                TranscodingJobDto? job;
                 if (!File.Exists(outputPath))
                 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
                 else
                 {
                 {
-                    transcodingJobHelper.OnTranscodeBeginRequest(outputPath, TranscodingJobType.Progressive);
+                    job = transcodingJobHelper.OnTranscodeBeginRequest(outputPath, TranscodingJobType.Progressive);
                     state.Dispose();
                     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);
                 return controller.File(memoryStream, contentType);
             }
             }
             finally
             finally

+ 117 - 26
Jellyfin.Api/Helpers/ProgressiveFileCopier.cs

@@ -1,7 +1,10 @@
 using System;
 using System;
+using System.Buffers;
 using System.IO;
 using System.IO;
+using System.Runtime.InteropServices;
 using System.Threading;
 using System.Threading;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
+using Jellyfin.Api.Models.PlaybackDtos;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Model.IO;
 
 
@@ -12,34 +15,53 @@ namespace Jellyfin.Api.Helpers
     /// </summary>
     /// </summary>
     public class ProgressiveFileCopier
     public class ProgressiveFileCopier
     {
     {
+        private readonly TranscodingJobDto? _job;
         private readonly string? _path;
         private readonly string? _path;
+        private readonly CancellationToken _cancellationToken;
         private readonly IDirectStreamProvider? _directStreamProvider;
         private readonly IDirectStreamProvider? _directStreamProvider;
-        private readonly IStreamHelper _streamHelper;
+        private readonly TranscodingJobHelper _transcodingJobHelper;
+        private long _bytesWritten;
 
 
         /// <summary>
         /// <summary>
         /// Initializes a new instance of the <see cref="ProgressiveFileCopier"/> class.
         /// Initializes a new instance of the <see cref="ProgressiveFileCopier"/> class.
         /// </summary>
         /// </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;
             _path = path;
-            _streamHelper = streamHelper;
-            _directStreamProvider = null;
+            _job = job;
+            _cancellationToken = cancellationToken;
+            _transcodingJobHelper = transcodingJobHelper;
         }
         }
 
 
         /// <summary>
         /// <summary>
         /// Initializes a new instance of the <see cref="ProgressiveFileCopier"/> class.
         /// Initializes a new instance of the <see cref="ProgressiveFileCopier"/> class.
         /// </summary>
         /// </summary>
-        /// <param name="streamHelper">Instance of the <see cref="IStreamHelper"/> interface.</param>
         /// <param name="directStreamProvider">Instance of the <see cref="IDirectStreamProvider"/> 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;
             _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>
         /// <summary>
         /// Write source stream to output.
         /// Write source stream to output.
         /// </summary>
         /// </summary>
@@ -48,37 +70,106 @@ namespace Jellyfin.Api.Helpers
         /// <returns>A <see cref="Task"/>.</returns>
         /// <returns>A <see cref="Task"/>.</returns>
         public async Task WriteToAsync(Stream outputStream, CancellationToken cancellationToken)
         public async Task WriteToAsync(Stream outputStream, CancellationToken cancellationToken)
         {
         {
-            if (_directStreamProvider != null)
+            cancellationToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, _cancellationToken).Token;
+
+            try
+            {
+                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 (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+                {
+                    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)
+                {
+                    var bytesRead = await CopyToInternalAsync(inputStream, outputStream, allowAsyncFileRead, cancellationToken).ConfigureAwait(false);
+
+                    if (bytesRead == 0)
+                    {
+                        if (_job == null || _job.HasExited)
+                        {
+                            eofCount++;
+                        }
+
+                        await Task.Delay(100, cancellationToken).ConfigureAwait(false);
+                    }
+                    else
+                    {
+                        eofCount = 0;
+                    }
+                }
+            }
+            finally
             {
             {
-                await _directStreamProvider.CopyToAsync(outputStream, cancellationToken).ConfigureAwait(false);
-                return;
+                if (_job != null)
+                {
+                    _transcodingJobHelper.OnTranscodeEndRequest(_job);
+                }
             }
             }
+        }
 
 
-            var fileOptions = FileOptions.SequentialScan;
+        private async Task<int> CopyToInternalAsync(Stream source, Stream destination, bool readAsync, CancellationToken cancellationToken)
+        {
+            var array = ArrayPool<byte>.Shared.Rent(IODefaults.CopyToBufferSize);
+            int bytesRead;
+            int totalBytesRead = 0;
 
 
-            // use non-async filestream along with read due to https://github.com/dotnet/corefx/issues/6039
-            if (Environment.OSVersion.Platform != PlatformID.Win32NT)
+            if (readAsync)
+            {
+                bytesRead = await source.ReadAsync(array, 0, array.Length, cancellationToken).ConfigureAwait(false);
+            }
+            else
             {
             {
-                fileOptions |= FileOptions.Asynchronous;
+                bytesRead = source.Read(array, 0, array.Length);
             }
             }
 
 
-            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)
+            while (bytesRead != 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);
+                    }
+                }
+
+                if (readAsync)
+                {
+                    bytesRead = await source.ReadAsync(array, 0, array.Length, cancellationToken).ConfigureAwait(false);
                 }
                 }
                 else
                 else
                 {
                 {
-                    eofCount = 0;
+                    bytesRead = source.Read(array, 0, array.Length);
                 }
                 }
             }
             }
+
+            return totalBytesRead;
         }
         }
     }
     }
 }
 }

+ 14 - 14
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>
         /// <summary>
         /// <summary>
         /// The progressive
         /// The progressive
@@ -712,20 +726,6 @@ namespace Jellyfin.Api.Helpers
             }
             }
         }
         }
 
 
-        /// <summary>
-        /// Transcoding video finished. Decrement the active request counter.
-        /// </summary>
-        /// <param name="job">The <see cref="TranscodingJobDto"/> which ended.</param>
-        public void OnTranscodeEndRequest(TranscodingJobDto job)
-        {
-            job.ActiveRequestCount--;
-            _logger.LogDebug("OnTranscodeEndRequest job.ActiveRequestCount={0}", job.ActiveRequestCount);
-            if (job.ActiveRequestCount <= 0)
-            {
-                PingTimer(job, false);
-            }
-        }
-
         /// <summary>
         /// <summary>
         /// Processes the exited.
         /// Processes the exited.
         /// </summary>
         /// </summary>

+ 1 - 0
Jellyfin.Api/Jellyfin.Api.csproj

@@ -16,6 +16,7 @@
     <PackageReference Include="Microsoft.AspNetCore.Authentication" Version="2.2.0" />
     <PackageReference Include="Microsoft.AspNetCore.Authentication" Version="2.2.0" />
     <PackageReference Include="Microsoft.AspNetCore.Authorization" Version="3.1.6" />
     <PackageReference Include="Microsoft.AspNetCore.Authorization" Version="3.1.6" />
     <PackageReference Include="Microsoft.AspNetCore.Mvc" Version="2.2.0" />
     <PackageReference Include="Microsoft.AspNetCore.Mvc" Version="2.2.0" />
+    <PackageReference Include="Microsoft.Extensions.Http" Version="3.1.6" />
     <PackageReference Include="Swashbuckle.AspNetCore" Version="5.5.1" />
     <PackageReference Include="Swashbuckle.AspNetCore" Version="5.5.1" />
     <PackageReference Include="Swashbuckle.AspNetCore.ReDoc" Version="5.3.3" />
     <PackageReference Include="Swashbuckle.AspNetCore.ReDoc" Version="5.3.3" />
   </ItemGroup>
   </ItemGroup>

+ 15 - 0
Jellyfin.Api/Models/VideoDtos/DeviceProfileDto.cs

@@ -0,0 +1,15 @@
+using MediaBrowser.Model.Dlna;
+
+namespace Jellyfin.Api.Models.VideoDtos
+{
+    /// <summary>
+    /// Device profile dto.
+    /// </summary>
+    public class DeviceProfileDto
+    {
+        /// <summary>
+        /// Gets or sets device profile.
+        /// </summary>
+        public DeviceProfile? DeviceProfile { get; set; }
+    }
+}

+ 2 - 0
Jellyfin.Server/Startup.cs

@@ -1,3 +1,4 @@
+using System.Net.Http;
 using Jellyfin.Server.Extensions;
 using Jellyfin.Server.Extensions;
 using Jellyfin.Server.Middleware;
 using Jellyfin.Server.Middleware;
 using Jellyfin.Server.Models;
 using Jellyfin.Server.Models;
@@ -43,6 +44,7 @@ namespace Jellyfin.Server
             services.AddCustomAuthentication();
             services.AddCustomAuthentication();
 
 
             services.AddJellyfinApiAuthorization();
             services.AddJellyfinApiAuthorization();
+            services.AddHttpClient();
         }
         }
 
 
         /// <summary>
         /// <summary>

+ 0 - 43
MediaBrowser.Api/Playback/Progressive/VideoService.cs

@@ -14,49 +14,6 @@ using Microsoft.Extensions.Logging;
 
 
 namespace MediaBrowser.Api.Playback.Progressive
 namespace MediaBrowser.Api.Playback.Progressive
 {
 {
-    /// <summary>
-    /// Class GetVideoStream.
-    /// </summary>
-    [Route("/Videos/{Id}/stream.mpegts", "GET")]
-    [Route("/Videos/{Id}/stream.ts", "GET")]
-    [Route("/Videos/{Id}/stream.webm", "GET")]
-    [Route("/Videos/{Id}/stream.asf", "GET")]
-    [Route("/Videos/{Id}/stream.wmv", "GET")]
-    [Route("/Videos/{Id}/stream.ogv", "GET")]
-    [Route("/Videos/{Id}/stream.mp4", "GET")]
-    [Route("/Videos/{Id}/stream.m4v", "GET")]
-    [Route("/Videos/{Id}/stream.mkv", "GET")]
-    [Route("/Videos/{Id}/stream.mpeg", "GET")]
-    [Route("/Videos/{Id}/stream.mpg", "GET")]
-    [Route("/Videos/{Id}/stream.avi", "GET")]
-    [Route("/Videos/{Id}/stream.m2ts", "GET")]
-    [Route("/Videos/{Id}/stream.3gp", "GET")]
-    [Route("/Videos/{Id}/stream.wmv", "GET")]
-    [Route("/Videos/{Id}/stream.wtv", "GET")]
-    [Route("/Videos/{Id}/stream.mov", "GET")]
-    [Route("/Videos/{Id}/stream.iso", "GET")]
-    [Route("/Videos/{Id}/stream.flv", "GET")]
-    [Route("/Videos/{Id}/stream.rm", "GET")]
-    [Route("/Videos/{Id}/stream", "GET")]
-    [Route("/Videos/{Id}/stream.ts", "HEAD")]
-    [Route("/Videos/{Id}/stream.webm", "HEAD")]
-    [Route("/Videos/{Id}/stream.asf", "HEAD")]
-    [Route("/Videos/{Id}/stream.wmv", "HEAD")]
-    [Route("/Videos/{Id}/stream.ogv", "HEAD")]
-    [Route("/Videos/{Id}/stream.mp4", "HEAD")]
-    [Route("/Videos/{Id}/stream.m4v", "HEAD")]
-    [Route("/Videos/{Id}/stream.mkv", "HEAD")]
-    [Route("/Videos/{Id}/stream.mpeg", "HEAD")]
-    [Route("/Videos/{Id}/stream.mpg", "HEAD")]
-    [Route("/Videos/{Id}/stream.avi", "HEAD")]
-    [Route("/Videos/{Id}/stream.3gp", "HEAD")]
-    [Route("/Videos/{Id}/stream.wmv", "HEAD")]
-    [Route("/Videos/{Id}/stream.wtv", "HEAD")]
-    [Route("/Videos/{Id}/stream.m2ts", "HEAD")]
-    [Route("/Videos/{Id}/stream.mov", "HEAD")]
-    [Route("/Videos/{Id}/stream.iso", "HEAD")]
-    [Route("/Videos/{Id}/stream.flv", "HEAD")]
-    [Route("/Videos/{Id}/stream", "HEAD")]
     public class GetVideoStream : VideoStreamRequest
     public class GetVideoStream : VideoStreamRequest
     {
     {
     }
     }