From 37496e958f08d2f81976dc976b6d5de648de958f Mon Sep 17 00:00:00 2001 From: crobibero Date: Fri, 24 Jul 2020 16:49:43 -0600 Subject: [PATCH 01/10] move VideoService.cs to Jellyfin.Api --- Jellyfin.Api/Controllers/AudioController.cs | 22 +- Jellyfin.Api/Controllers/VideosController.cs | 318 +++++++++++++++++- .../Playback/Progressive/VideoService.cs | 43 --- 3 files changed, 328 insertions(+), 55 deletions(-) diff --git a/Jellyfin.Api/Controllers/AudioController.cs b/Jellyfin.Api/Controllers/AudioController.cs index 7405c26fb8..6e3d898bfa 100644 --- a/Jellyfin.Api/Controllers/AudioController.cs +++ b/Jellyfin.Api/Controllers/AudioController.cs @@ -205,13 +205,13 @@ namespace Jellyfin.Api.Controllers { bool isHeadRequest = Request.Method == System.Net.WebRequestMethods.Http.Head; - var cancellationTokenSource = new CancellationTokenSource(); + using var cancellationTokenSource = new CancellationTokenSource(); StreamingRequestDto streamingRequest = new StreamingRequestDto { Id = itemId, Container = container, - Static = @static.HasValue ? @static.Value : true, + Static = @static ?? true, Params = @params, Tag = tag, DeviceProfileId = deviceProfileId, @@ -222,10 +222,10 @@ namespace Jellyfin.Api.Controllers MediaSourceId = mediaSourceId, DeviceId = deviceId, 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, MaxAudioChannels = maxAudioChannels, AudioBitRate = audioBitRate, @@ -235,7 +235,7 @@ namespace Jellyfin.Api.Controllers Level = level, Framerate = framerate, MaxFramerate = maxFramerate, - CopyTimestamps = copyTimestamps.HasValue ? copyTimestamps.Value : true, + CopyTimestamps = copyTimestamps ?? true, StartTimeTicks = startTimeTicks, Width = width, Height = height, @@ -244,13 +244,13 @@ namespace Jellyfin.Api.Controllers SubtitleMethod = subtitleMethod, MaxRefFrames = maxRefFrames, 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, CpuCoreLimit = cpuCoreLimit, LiveStreamId = liveStreamId, - EnableMpegtsM2TsMode = enableMpegtsM2TsMode.HasValue ? enableMpegtsM2TsMode.Value : true, + EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? true, VideoCodec = videoCodec, SubtitleCodec = subtitleCodec, TranscodeReasons = transcodingReasons, diff --git a/Jellyfin.Api/Controllers/VideosController.cs b/Jellyfin.Api/Controllers/VideosController.cs index e2a44427b8..734198d9aa 100644 --- a/Jellyfin.Api/Controllers/VideosController.cs +++ b/Jellyfin.Api/Controllers/VideosController.cs @@ -1,19 +1,34 @@ using System; +using System.Collections.Generic; using System.Globalization; using System.Linq; +using System.Net.Http; using System.Threading; +using System.Threading.Tasks; using Jellyfin.Api.Constants; using Jellyfin.Api.Extensions; 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.Entities; using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.MediaEncoding; +using MediaBrowser.Controller.Net; +using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; +using MediaBrowser.Model.IO; +using MediaBrowser.Model.MediaInfo; +using MediaBrowser.Model.Net; using MediaBrowser.Model.Querying; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Configuration; namespace Jellyfin.Api.Controllers { @@ -26,6 +41,20 @@ namespace Jellyfin.Api.Controllers private readonly ILibraryManager _libraryManager; private readonly IUserManager _userManager; 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 IStreamHelper _streamHelper; + private readonly IFileSystem _fileSystem; + private readonly ISubtitleEncoder _subtitleEncoder; + private readonly IConfiguration _configuration; + private readonly IDeviceManager _deviceManager; + private readonly TranscodingJobHelper _transcodingJobHelper; + private readonly HttpClient _httpClient; + + private readonly TranscodingJobType _transcodingJobType = TranscodingJobType.Progressive; /// /// Initializes a new instance of the class. @@ -33,14 +62,50 @@ namespace Jellyfin.Api.Controllers /// Instance of the interface. /// Instance of the interface. /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the class. + /// Instance of the class. public VideosController( ILibraryManager libraryManager, IUserManager userManager, - IDtoService dtoService) + IDtoService dtoService, + IDlnaManager dlnaManager, + IAuthorizationContext authContext, + IMediaSourceManager mediaSourceManager, + IServerConfigurationManager serverConfigurationManager, + IMediaEncoder mediaEncoder, + IStreamHelper streamHelper, + IFileSystem fileSystem, + ISubtitleEncoder subtitleEncoder, + IConfiguration configuration, + IDeviceManager deviceManager, + TranscodingJobHelper transcodingJobHelper, + HttpClient httpClient) { _libraryManager = libraryManager; _userManager = userManager; _dtoService = dtoService; + _dlnaManager = dlnaManager; + _authContext = authContext; + _mediaSourceManager = mediaSourceManager; + _serverConfigurationManager = serverConfigurationManager; + _mediaEncoder = mediaEncoder; + _streamHelper = streamHelper; + _fileSystem = fileSystem; + _subtitleEncoder = subtitleEncoder; + _configuration = configuration; + _deviceManager = deviceManager; + _transcodingJobHelper = transcodingJobHelper; + _httpClient = httpClient; } /// @@ -200,5 +265,256 @@ namespace Jellyfin.Api.Controllers primaryVersion.UpdateToRepository(ItemUpdateType.MetadataEdit, CancellationToken.None); return NoContent(); } + + /// + /// Gets a video stream. + /// + /// The item id. + /// The video container. Possible values are: ts, webm, asf, wmv, ogv, mp4, m4v, mkv, mpeg, mpg, avi, 3gp, wmv, wtv, m2ts, mov, iso, flv. + /// 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. + /// The streaming parameters. + /// The tag. + /// Optional. The dlna device profile id to utilize. + /// The play session id. + /// The segment container. + /// The segment lenght. + /// The minimum number of segments. + /// The media version id, if playing an alternate version. + /// The device id of the client requesting. Used to stop encoding processes when needed. + /// 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. + /// Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true. + /// Whether or not to allow copying of the video stream url. + /// Whether or not to allow copying of the audio stream url. + /// Optional. Whether to break on non key frames. + /// Optional. Specify a specific audio sample rate, e.g. 44100. + /// Optional. The maximum audio bit depth. + /// Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults. + /// Optional. Specify a specific number of audio channels to encode to, e.g. 2. + /// Optional. Specify a maximum number of audio channels to encode to, e.g. 2. + /// Optional. Specify a specific an encoder profile (varies by encoder), e.g. main, baseline, high. + /// Optional. Specify a level for the encoder profile (varies by encoder), e.g. 3, 3.1. + /// Optional. A specific video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements. + /// Optional. A specific maximum video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements. + /// Whether or not to copy timestamps when transcoding with an offset. Defaults to false. + /// Optional. Specify a starting offset, in ticks. 1 tick = 10000 ms. + /// Optional. The fixed horizontal resolution of the encoded video. + /// Optional. The fixed vertical resolution of the encoded video. + /// Optional. Specify a video bitrate to encode to, e.g. 500000. If omitted this will be left to encoder defaults. + /// Optional. The index of the subtitle stream to use. If omitted no subtitles will be used. + /// Optional. Specify the subtitle delivery method. + /// Optional. + /// Optional. The maximum video bit depth. + /// Optional. Whether to require avc. + /// Optional. Whether to deinterlace the video. + /// Optional. Whether to require a non anamporphic stream. + /// Optional. The maximum number of audio channels to transcode. + /// Optional. The limit of how many cpu cores to use. + /// The live stream id. + /// Optional. Whether to enable the MpegtsM2Ts mode. + /// 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. + /// Optional. Specify a subtitle codec to encode to. + /// Optional. The transcoding reason. + /// Optional. The index of the audio stream to use. If omitted the first audio stream will be used. + /// Optional. The index of the video stream to use. If omitted the first video stream will be used. + /// Optional. The . + /// Optional. The streaming options. + /// A containing the audio file. + [HttpGet("{itemId}/{stream=stream}.{container?}")] + [HttpGet("{itemId}/stream")] + [HttpHead("{itemId}/{stream=stream}.{container?}")] + [HttpHead("{itemId}/stream")] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task 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 streamOptions) + { + var isHeadRequest = Request.Method == System.Net.WebRequestMethods.Http.Head; + using var cancellationTokenSource = new CancellationTokenSource(); + var streamingRequest = new StreamingRequestDto + { + 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); + + // TODO AllowEndOfFile = false + await new ProgressiveFileCopier(_streamHelper, state.DirectStreamProvider).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); + + 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) + { + // TODO AllowEndOfFile = false + await new ProgressiveFileCopier(_streamHelper, state.MediaPath).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, + _streamHelper, + this, + _transcodingJobHelper, + ffmpegCommandLineArguments, + Request, + _transcodingJobType, + cancellationTokenSource).ConfigureAwait(false); + } } } diff --git a/MediaBrowser.Api/Playback/Progressive/VideoService.cs b/MediaBrowser.Api/Playback/Progressive/VideoService.cs index c3f6b905cb..5bc85f42d2 100644 --- a/MediaBrowser.Api/Playback/Progressive/VideoService.cs +++ b/MediaBrowser.Api/Playback/Progressive/VideoService.cs @@ -14,49 +14,6 @@ using Microsoft.Extensions.Logging; namespace MediaBrowser.Api.Playback.Progressive { - /// - /// Class GetVideoStream. - /// - [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 { } From 7bb34fc9e7e480e7048a1e15e1f463afab2198eb Mon Sep 17 00:00:00 2001 From: crobibero Date: Sat, 25 Jul 2020 17:21:40 -0600 Subject: [PATCH 02/10] use proper HttpClient DI --- Jellyfin.Api/Controllers/AudioController.cs | 11 ++++++----- Jellyfin.Api/Controllers/VideosController.cs | 11 ++++++----- Jellyfin.Api/Jellyfin.Api.csproj | 1 + Jellyfin.Server/Startup.cs | 2 ++ 4 files changed, 15 insertions(+), 10 deletions(-) diff --git a/Jellyfin.Api/Controllers/AudioController.cs b/Jellyfin.Api/Controllers/AudioController.cs index 6e3d898bfa..86577411f2 100644 --- a/Jellyfin.Api/Controllers/AudioController.cs +++ b/Jellyfin.Api/Controllers/AudioController.cs @@ -41,7 +41,7 @@ namespace Jellyfin.Api.Controllers private readonly IConfiguration _configuration; private readonly IDeviceManager _deviceManager; private readonly TranscodingJobHelper _transcodingJobHelper; - private readonly HttpClient _httpClient; + private readonly IHttpClientFactory _httpClientFactory; private readonly TranscodingJobType _transcodingJobType = TranscodingJobType.Progressive; @@ -61,7 +61,7 @@ namespace Jellyfin.Api.Controllers /// Instance of the interface. /// Instance of the interface. /// The singleton. - /// Instance of the . + /// Instance of the interface. public AudioController( IDlnaManager dlnaManager, IUserManager userManger, @@ -76,7 +76,7 @@ namespace Jellyfin.Api.Controllers IConfiguration configuration, IDeviceManager deviceManager, TranscodingJobHelper transcodingJobHelper, - HttpClient httpClient) + IHttpClientFactory httpClientFactory) { _dlnaManager = dlnaManager; _authContext = authorizationContext; @@ -91,7 +91,7 @@ namespace Jellyfin.Api.Controllers _configuration = configuration; _deviceManager = deviceManager; _transcodingJobHelper = transcodingJobHelper; - _httpClient = httpClient; + _httpClientFactory = httpClientFactory; } /// @@ -295,7 +295,8 @@ namespace Jellyfin.Api.Controllers { 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) diff --git a/Jellyfin.Api/Controllers/VideosController.cs b/Jellyfin.Api/Controllers/VideosController.cs index 734198d9aa..5050c3d4fe 100644 --- a/Jellyfin.Api/Controllers/VideosController.cs +++ b/Jellyfin.Api/Controllers/VideosController.cs @@ -52,7 +52,7 @@ namespace Jellyfin.Api.Controllers private readonly IConfiguration _configuration; private readonly IDeviceManager _deviceManager; private readonly TranscodingJobHelper _transcodingJobHelper; - private readonly HttpClient _httpClient; + private readonly IHttpClientFactory _httpClientFactory; private readonly TranscodingJobType _transcodingJobType = TranscodingJobType.Progressive; @@ -73,7 +73,7 @@ namespace Jellyfin.Api.Controllers /// Instance of the interface. /// Instance of the interface. /// Instance of the class. - /// Instance of the class. + /// Instance of the interface. public VideosController( ILibraryManager libraryManager, IUserManager userManager, @@ -89,7 +89,7 @@ namespace Jellyfin.Api.Controllers IConfiguration configuration, IDeviceManager deviceManager, TranscodingJobHelper transcodingJobHelper, - HttpClient httpClient) + IHttpClientFactory httpClientFactory) { _libraryManager = libraryManager; _userManager = userManager; @@ -105,7 +105,7 @@ namespace Jellyfin.Api.Controllers _configuration = configuration; _deviceManager = deviceManager; _transcodingJobHelper = transcodingJobHelper; - _httpClient = httpClient; + _httpClientFactory = httpClientFactory; } /// @@ -465,7 +465,8 @@ namespace Jellyfin.Api.Controllers { 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) diff --git a/Jellyfin.Api/Jellyfin.Api.csproj b/Jellyfin.Api/Jellyfin.Api.csproj index 572cb1af25..a52b234d48 100644 --- a/Jellyfin.Api/Jellyfin.Api.csproj +++ b/Jellyfin.Api/Jellyfin.Api.csproj @@ -16,6 +16,7 @@ + diff --git a/Jellyfin.Server/Startup.cs b/Jellyfin.Server/Startup.cs index edf023fa24..108d8f881e 100644 --- a/Jellyfin.Server/Startup.cs +++ b/Jellyfin.Server/Startup.cs @@ -1,3 +1,4 @@ +using System.Net.Http; using Jellyfin.Server.Extensions; using Jellyfin.Server.Middleware; using Jellyfin.Server.Models; @@ -43,6 +44,7 @@ namespace Jellyfin.Server services.AddCustomAuthentication(); services.AddJellyfinApiAuthorization(); + services.AddHttpClient(); } /// From b8d327889b96b820249ddf80ee023b189f67f4a3 Mon Sep 17 00:00:00 2001 From: crobibero Date: Mon, 27 Jul 2020 13:42:40 -0600 Subject: [PATCH 03/10] Add missing functions --- Jellyfin.Api/Controllers/AudioController.cs | 19 +- Jellyfin.Api/Controllers/LiveTvController.cs | 19 +- Jellyfin.Api/Controllers/VideosController.cs | 21 ++- .../Helpers/FileStreamResponseHelpers.cs | 14 +- Jellyfin.Api/Helpers/ProgressiveFileCopier.cs | 176 ++++++++++++++---- Jellyfin.Api/Helpers/TranscodingJobHelper.cs | 14 ++ 6 files changed, 194 insertions(+), 69 deletions(-) diff --git a/Jellyfin.Api/Controllers/AudioController.cs b/Jellyfin.Api/Controllers/AudioController.cs index 86577411f2..e63868339f 100644 --- a/Jellyfin.Api/Controllers/AudioController.cs +++ b/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 /// Instance of the interface. /// Instance of the interface. /// Instance of the interface. - /// Instance of the interface. /// Instance of the interface. /// Instance of the interface. /// Instance of the interface. @@ -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, diff --git a/Jellyfin.Api/Controllers/LiveTvController.cs b/Jellyfin.Api/Controllers/LiveTvController.cs index bc5446510a..9144d6f285 100644 --- a/Jellyfin.Api/Controllers/LiveTvController.cs +++ b/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; /// /// Initializes a new instance of the class. @@ -58,9 +57,9 @@ namespace Jellyfin.Api.Controllers /// Instance of the interface. /// Instance of the interface. /// Instance of the interface. - /// Instance of the interface. /// Instance of the interface. /// Instance of the interface. + /// Instance of the class. 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; } /// @@ -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)); } diff --git a/Jellyfin.Api/Controllers/VideosController.cs b/Jellyfin.Api/Controllers/VideosController.cs index 5050c3d4fe..0ce62186b0 100644 --- a/Jellyfin.Api/Controllers/VideosController.cs +++ b/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 /// Instance of the interface. /// Instance of the interface. /// Instance of the interface. - /// Instance of the interface. /// Instance of the interface. /// Instance of the interface. /// Instance of the interface. @@ -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 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, diff --git a/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs b/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs index 636f47f5f1..96e90d38fc 100644 --- a/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs +++ b/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 /// /// The current . /// Whether the current request is a HTTP HEAD request so only the headers get returned. - /// Instance of the interface. /// The managing the response. /// The singleton. /// The command line arguments to start ffmpeg. @@ -91,7 +90,6 @@ namespace Jellyfin.Api.Helpers public static async Task 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 diff --git a/Jellyfin.Api/Helpers/ProgressiveFileCopier.cs b/Jellyfin.Api/Helpers/ProgressiveFileCopier.cs index e8e6966f45..acaccc77ae 100644 --- a/Jellyfin.Api/Helpers/ProgressiveFileCopier.cs +++ b/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 /// 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; /// /// Initializes a new instance of the class. /// - /// Instance of the interface. - /// Filepath to stream from. - public ProgressiveFileCopier(IStreamHelper streamHelper, string path) + /// The path to copy from. + /// The transcoding job. + /// Instance of the . + /// The cancellation token. + public ProgressiveFileCopier(string path, TranscodingJobDto? job, TranscodingJobHelper transcodingJobHelper, CancellationToken cancellationToken) { _path = path; - _streamHelper = streamHelper; - _directStreamProvider = null; + _job = job; + _cancellationToken = cancellationToken; + _transcodingJobHelper = transcodingJobHelper; } /// /// Initializes a new instance of the class. /// - /// Instance of the interface. /// Instance of the interface. - public ProgressiveFileCopier(IStreamHelper streamHelper, IDirectStreamProvider directStreamProvider) + /// The transcoding job. + /// Instance of the . + /// The cancellation token. + public ProgressiveFileCopier(IDirectStreamProvider directStreamProvider, TranscodingJobDto? job, TranscodingJobHelper transcodingJobHelper, CancellationToken cancellationToken) { _directStreamProvider = directStreamProvider; - _streamHelper = streamHelper; - _path = null; + _job = job; + _cancellationToken = cancellationToken; + _transcodingJobHelper = transcodingJobHelper; } + /// + /// Gets or sets a value indicating whether allow read end of file. + /// + public bool AllowEndOfFile { get; set; } = true; + + /// + /// Gets or sets copy start position. + /// + public long StartPosition { get; set; } + /// /// Write source stream to output. /// @@ -48,37 +68,123 @@ namespace Jellyfin.Api.Helpers /// A . 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; - } - - var fileOptions = FileOptions.SequentialScan; - - // 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; - } - - 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) - { - var bytesRead = await _streamHelper.CopyToAsync(inputStream, outputStream, cancellationToken).ConfigureAwait(false); - - if (bytesRead == 0) + if (_directStreamProvider != null) { - eofCount++; - await Task.Delay(100, cancellationToken).ConfigureAwait(false); + await _directStreamProvider.CopyToAsync(outputStream, cancellationToken).ConfigureAwait(false); + return; } - else + + 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) { - eofCount = 0; + 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); + } + + if (bytesRead == 0) + { + if (_job == null || _job.HasExited) + { + eofCount++; + } + + await Task.Delay(100, cancellationToken).ConfigureAwait(false); + } + else + { + eofCount = 0; + } + } + } + finally + { + if (_job != null) + { + _transcodingJobHelper.OnTranscodeEndRequest(_job); } } } + + private async Task 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 bytesToWrite = bytesRead; + + if (bytesToWrite > 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; + } + + private async Task 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) + { + 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; + } } } diff --git a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs index c84135085f..fc38eacafd 100644 --- a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs +++ b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs @@ -680,6 +680,20 @@ namespace Jellyfin.Api.Helpers } } + /// + /// Called when [transcode end]. + /// + /// The transcode job. + public void OnTranscodeEndRequest(TranscodingJobDto job) + { + job.ActiveRequestCount--; + _logger.LogDebug("OnTranscodeEndRequest job.ActiveRequestCount={ActiveRequestCount}", job.ActiveRequestCount); + if (job.ActiveRequestCount <= 0) + { + PingTimer(job, false); + } + } + /// /// /// The progressive From 03f15fc0eff3a8937e2c710781016eab9e6b9776 Mon Sep 17 00:00:00 2001 From: crobibero Date: Wed, 29 Jul 2020 09:28:53 -0600 Subject: [PATCH 04/10] use proper os comparison --- Jellyfin.Api/Helpers/ProgressiveFileCopier.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Jellyfin.Api/Helpers/ProgressiveFileCopier.cs b/Jellyfin.Api/Helpers/ProgressiveFileCopier.cs index acaccc77ae..d8b1828f5d 100644 --- a/Jellyfin.Api/Helpers/ProgressiveFileCopier.cs +++ b/Jellyfin.Api/Helpers/ProgressiveFileCopier.cs @@ -1,5 +1,7 @@ using System; +using System.Buffers; using System.IO; +using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; using Jellyfin.Api.Models.PlaybackDtos; @@ -82,7 +84,7 @@ namespace Jellyfin.Api.Helpers 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) + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { fileOptions |= FileOptions.Asynchronous; allowAsyncFileRead = true; From d0ce239e3e71d52fd405cd7981cecb5ee49983a5 Mon Sep 17 00:00:00 2001 From: crobibero Date: Wed, 29 Jul 2020 12:12:54 -0600 Subject: [PATCH 05/10] Use ArrayPool and reduce duplicate code --- Jellyfin.Api/Helpers/ProgressiveFileCopier.cs | 59 +++++++------------ 1 file changed, 21 insertions(+), 38 deletions(-) diff --git a/Jellyfin.Api/Helpers/ProgressiveFileCopier.cs b/Jellyfin.Api/Helpers/ProgressiveFileCopier.cs index d8b1828f5d..71197d931f 100644 --- a/Jellyfin.Api/Helpers/ProgressiveFileCopier.cs +++ b/Jellyfin.Api/Helpers/ProgressiveFileCopier.cs @@ -93,23 +93,15 @@ namespace Jellyfin.Api.Helpers await using var inputStream = new FileStream(_path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, IODefaults.FileStreamBufferSize, fileOptions); var eofCount = 0; - const int emptyReadLimit = 20; + const int EmptyReadLimit = 20; if (StartPosition > 0) { inputStream.Position = StartPosition; } - while (eofCount < emptyReadLimit || !AllowEndOfFile) + 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 bytesRead = await CopyToInternalAsync(inputStream, outputStream, allowAsyncFileRead, cancellationToken).ConfigureAwait(false); if (bytesRead == 0) { @@ -135,13 +127,22 @@ namespace Jellyfin.Api.Helpers } } - private async Task CopyToInternalAsyncWithSyncRead(Stream source, Stream destination, CancellationToken cancellationToken) + private async Task CopyToInternalAsync(Stream source, Stream destination, bool readAsync, CancellationToken cancellationToken) { - var array = new byte[IODefaults.CopyToBufferSize]; + var array = ArrayPool.Shared.Rent(IODefaults.CopyToBufferSize); int bytesRead; int totalBytesRead = 0; - while ((bytesRead = source.Read(array, 0, array.Length)) != 0) + if (readAsync) + { + bytesRead = await source.ReadAsync(array, 0, array.Length, cancellationToken).ConfigureAwait(false); + } + else + { + bytesRead = source.Read(array, 0, array.Length); + } + + while (bytesRead != 0) { var bytesToWrite = bytesRead; @@ -157,32 +158,14 @@ namespace Jellyfin.Api.Helpers _job.BytesDownloaded = Math.Max(_job.BytesDownloaded ?? _bytesWritten, _bytesWritten); } } - } - return totalBytesRead; - } - - private async Task 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) + if (readAsync) { - 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); - } + bytesRead = await source.ReadAsync(array, 0, array.Length, cancellationToken).ConfigureAwait(false); + } + else + { + bytesRead = source.Read(array, 0, array.Length); } } From 5c4b342323bdf637738c3c87efcce625ae748f34 Mon Sep 17 00:00:00 2001 From: crobibero Date: Wed, 29 Jul 2020 14:21:32 -0600 Subject: [PATCH 06/10] fix boolean --- Jellyfin.Api/Helpers/ProgressiveFileCopier.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jellyfin.Api/Helpers/ProgressiveFileCopier.cs b/Jellyfin.Api/Helpers/ProgressiveFileCopier.cs index 71197d931f..432df97086 100644 --- a/Jellyfin.Api/Helpers/ProgressiveFileCopier.cs +++ b/Jellyfin.Api/Helpers/ProgressiveFileCopier.cs @@ -84,7 +84,7 @@ namespace Jellyfin.Api.Helpers var allowAsyncFileRead = false; // use non-async filestream along with read due to https://github.com/dotnet/corefx/issues/6039 - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { fileOptions |= FileOptions.Asynchronous; allowAsyncFileRead = true; From df5d43132cd7a59bd912ba7bf6e67fdc1605acc8 Mon Sep 17 00:00:00 2001 From: crobibero Date: Wed, 29 Jul 2020 16:42:01 -0600 Subject: [PATCH 07/10] Fix PlaybackInfo endpoint --- Jellyfin.Api/Controllers/MediaInfoController.cs | 7 ++++--- Jellyfin.Api/Models/VideoDtos/DeviceProfileDto.cs | 15 +++++++++++++++ 2 files changed, 19 insertions(+), 3 deletions(-) create mode 100644 Jellyfin.Api/Models/VideoDtos/DeviceProfileDto.cs diff --git a/Jellyfin.Api/Controllers/MediaInfoController.cs b/Jellyfin.Api/Controllers/MediaInfoController.cs index da400f5106..c2c02c02ca 100644 --- a/Jellyfin.Api/Controllers/MediaInfoController.cs +++ b/Jellyfin.Api/Controllers/MediaInfoController.cs @@ -7,6 +7,7 @@ using System.Text.Json; using System.Threading; using System.Threading.Tasks; using Jellyfin.Api.Constants; +using Jellyfin.Api.Models.VideoDtos; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; using MediaBrowser.Common.Net; @@ -126,7 +127,7 @@ namespace Jellyfin.Api.Controllers [FromQuery] int? maxAudioChannels, [FromQuery] string? mediaSourceId, [FromQuery] string? liveStreamId, - [FromQuery] DeviceProfile? deviceProfile, + [FromBody] DeviceProfileDto? deviceProfile, [FromQuery] bool autoOpenLiveStream = false, [FromQuery] bool enableDirectPlay = true, [FromQuery] bool enableDirectStream = true, @@ -136,7 +137,7 @@ namespace Jellyfin.Api.Controllers { var authInfo = _authContext.GetAuthorizationInfo(Request); - var profile = deviceProfile; + var profile = deviceProfile?.DeviceProfile; _logger.LogInformation("GetPostedPlaybackInfo profile: {@Profile}", profile); @@ -190,7 +191,7 @@ namespace Jellyfin.Api.Controllers var openStreamResult = await OpenMediaSource(new LiveStreamRequest { AudioStreamIndex = audioStreamIndex, - DeviceProfile = deviceProfile, + DeviceProfile = deviceProfile?.DeviceProfile, EnableDirectPlay = enableDirectPlay, EnableDirectStream = enableDirectStream, ItemId = itemId, diff --git a/Jellyfin.Api/Models/VideoDtos/DeviceProfileDto.cs b/Jellyfin.Api/Models/VideoDtos/DeviceProfileDto.cs new file mode 100644 index 0000000000..db55dc34b5 --- /dev/null +++ b/Jellyfin.Api/Models/VideoDtos/DeviceProfileDto.cs @@ -0,0 +1,15 @@ +using MediaBrowser.Model.Dlna; + +namespace Jellyfin.Api.Models.VideoDtos +{ + /// + /// Device profile dto. + /// + public class DeviceProfileDto + { + /// + /// Gets or sets device profile. + /// + public DeviceProfile? DeviceProfile { get; set; } + } +} From bb1fec9da1a1495eb1a259802b9fcd27f69b40fb Mon Sep 17 00:00:00 2001 From: crobibero Date: Wed, 29 Jul 2020 16:43:53 -0600 Subject: [PATCH 08/10] remove extra using --- Jellyfin.Api/Controllers/AudioController.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jellyfin.Api/Controllers/AudioController.cs b/Jellyfin.Api/Controllers/AudioController.cs index e63868339f..1d81def725 100644 --- a/Jellyfin.Api/Controllers/AudioController.cs +++ b/Jellyfin.Api/Controllers/AudioController.cs @@ -201,7 +201,7 @@ namespace Jellyfin.Api.Controllers { bool isHeadRequest = Request.Method == System.Net.WebRequestMethods.Http.Head; - using var cancellationTokenSource = new CancellationTokenSource(); + var cancellationTokenSource = new CancellationTokenSource(); StreamingRequestDto streamingRequest = new StreamingRequestDto { From f543a17d1b8a4e1e2f420831de586e9ecc015166 Mon Sep 17 00:00:00 2001 From: crobibero Date: Thu, 30 Jul 2020 06:29:06 -0600 Subject: [PATCH 09/10] Apply review fixes --- Jellyfin.Api/Controllers/VideosController.cs | 2 +- Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/Jellyfin.Api/Controllers/VideosController.cs b/Jellyfin.Api/Controllers/VideosController.cs index 0ce62186b0..fb02ec9617 100644 --- a/Jellyfin.Api/Controllers/VideosController.cs +++ b/Jellyfin.Api/Controllers/VideosController.cs @@ -373,7 +373,7 @@ namespace Jellyfin.Api.Controllers { var isHeadRequest = Request.Method == System.Net.WebRequestMethods.Http.Head; var cancellationTokenSource = new CancellationTokenSource(); - var streamingRequest = new StreamingRequestDto + var streamingRequest = new VideoRequestDto { Id = itemId, Container = container, diff --git a/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs b/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs index 96e90d38fc..a463783e00 100644 --- a/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs +++ b/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs @@ -71,8 +71,7 @@ namespace Jellyfin.Api.Helpers return controller.NoContent(); } - using var stream = new FileStream(path, FileMode.Open, FileAccess.Read); - return controller.File(stream, contentType); + return controller.PhysicalFile(path, contentType); } /// From c97372a1334bb18a6cd2b5a3abb0c385a30329ac Mon Sep 17 00:00:00 2001 From: crobibero Date: Fri, 31 Jul 2020 09:21:33 -0600 Subject: [PATCH 10/10] Add missing docs and remove duplicate function --- Jellyfin.Api/Controllers/AudioController.cs | 1 + Jellyfin.Api/Controllers/VideosController.cs | 1 + Jellyfin.Api/Helpers/TranscodingJobHelper.cs | 14 -------------- 3 files changed, 2 insertions(+), 14 deletions(-) diff --git a/Jellyfin.Api/Controllers/AudioController.cs b/Jellyfin.Api/Controllers/AudioController.cs index 1d81def725..d9afbd9104 100644 --- a/Jellyfin.Api/Controllers/AudioController.cs +++ b/Jellyfin.Api/Controllers/AudioController.cs @@ -142,6 +142,7 @@ namespace Jellyfin.Api.Controllers /// Optional. The index of the video stream to use. If omitted the first video stream will be used. /// Optional. The . /// Optional. The streaming options. + /// Audio stream returned. /// A containing the audio file. [HttpGet("{itemId}/{stream=stream}.{container?}")] [HttpGet("{itemId}/stream")] diff --git a/Jellyfin.Api/Controllers/VideosController.cs b/Jellyfin.Api/Controllers/VideosController.cs index fb02ec9617..d1ef817eb6 100644 --- a/Jellyfin.Api/Controllers/VideosController.cs +++ b/Jellyfin.Api/Controllers/VideosController.cs @@ -314,6 +314,7 @@ namespace Jellyfin.Api.Controllers /// Optional. The index of the video stream to use. If omitted the first video stream will be used. /// Optional. The . /// Optional. The streaming options. + /// Video stream returned. /// A containing the audio file. [HttpGet("{itemId}/{stream=stream}.{container?}")] [HttpGet("{itemId}/stream")] diff --git a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs index 16272c37a6..fc38eacafd 100644 --- a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs +++ b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs @@ -726,20 +726,6 @@ namespace Jellyfin.Api.Helpers } } - /// - /// Transcoding video finished. Decrement the active request counter. - /// - /// The which ended. - public void OnTranscodeEndRequest(TranscodingJobDto job) - { - job.ActiveRequestCount--; - _logger.LogDebug("OnTranscodeEndRequest job.ActiveRequestCount={0}", job.ActiveRequestCount); - if (job.ActiveRequestCount <= 0) - { - PingTimer(job, false); - } - } - /// /// Processes the exited. ///