diff --git a/Jellyfin.Api/Controllers/AudioController.cs b/Jellyfin.Api/Controllers/AudioController.cs index 7405c26fb..d9afbd910 100644 --- a/Jellyfin.Api/Controllers/AudioController.cs +++ b/Jellyfin.Api/Controllers/AudioController.cs @@ -35,13 +35,12 @@ 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; private readonly IDeviceManager _deviceManager; private readonly TranscodingJobHelper _transcodingJobHelper; - private readonly HttpClient _httpClient; + private readonly IHttpClientFactory _httpClientFactory; private readonly TranscodingJobType _transcodingJobType = TranscodingJobType.Progressive; @@ -55,13 +54,12 @@ 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. /// The singleton. - /// Instance of the . + /// Instance of the interface. public AudioController( IDlnaManager dlnaManager, IUserManager userManger, @@ -70,13 +68,12 @@ namespace Jellyfin.Api.Controllers IMediaSourceManager mediaSourceManager, IServerConfigurationManager serverConfigurationManager, IMediaEncoder mediaEncoder, - IStreamHelper streamHelper, IFileSystem fileSystem, ISubtitleEncoder subtitleEncoder, IConfiguration configuration, IDeviceManager deviceManager, TranscodingJobHelper transcodingJobHelper, - HttpClient httpClient) + IHttpClientFactory httpClientFactory) { _dlnaManager = dlnaManager; _authContext = authorizationContext; @@ -85,13 +82,12 @@ namespace Jellyfin.Api.Controllers _mediaSourceManager = mediaSourceManager; _serverConfigurationManager = serverConfigurationManager; _mediaEncoder = mediaEncoder; - _streamHelper = streamHelper; _fileSystem = fileSystem; _subtitleEncoder = subtitleEncoder; _configuration = configuration; _deviceManager = deviceManager; _transcodingJobHelper = transcodingJobHelper; - _httpClient = httpClient; + _httpClientFactory = httpClientFactory; } /// @@ -146,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")] @@ -211,7 +208,7 @@ namespace Jellyfin.Api.Controllers { Id = itemId, Container = container, - Static = @static.HasValue ? @static.Value : true, + Static = @static ?? true, Params = @params, Tag = tag, DeviceProfileId = deviceProfileId, @@ -222,10 +219,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 +232,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 +241,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, @@ -283,8 +280,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")!); @@ -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) @@ -318,8 +319,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); } @@ -338,7 +342,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 bc5446510..9144d6f28 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/MediaInfoController.cs b/Jellyfin.Api/Controllers/MediaInfoController.cs index da400f510..c2c02c02c 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/Controllers/VideosController.cs b/Jellyfin.Api/Controllers/VideosController.cs index e2a44427b..d1ef817eb 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,19 @@ 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 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; /// /// Initializes a new instance of the class. @@ -33,14 +61,47 @@ 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 class. + /// Instance of the interface. public VideosController( ILibraryManager libraryManager, 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; _userManager = userManager; _dtoService = dtoService; + _dlnaManager = dlnaManager; + _authContext = authContext; + _mediaSourceManager = mediaSourceManager; + _serverConfigurationManager = serverConfigurationManager; + _mediaEncoder = mediaEncoder; + _fileSystem = fileSystem; + _subtitleEncoder = subtitleEncoder; + _configuration = configuration; + _deviceManager = deviceManager; + _transcodingJobHelper = transcodingJobHelper; + _httpClientFactory = httpClientFactory; } /// @@ -200,5 +261,263 @@ 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. + /// Video stream returned. + /// 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; + 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); + } } } diff --git a/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs b/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs index 636f47f5f..a463783e0 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; @@ -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); } /// @@ -80,7 +79,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 +89,6 @@ namespace Jellyfin.Api.Helpers public static async Task GetTranscodedFile( StreamState state, bool isHeadRequest, - IStreamHelper streamHelper, ControllerBase controller, TranscodingJobHelper transcodingJobHelper, string ffmpegCommandLineArguments, @@ -116,18 +113,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 e8e6966f4..432df9708 100644 --- a/Jellyfin.Api/Helpers/ProgressiveFileCopier.cs +++ b/Jellyfin.Api/Helpers/ProgressiveFileCopier.cs @@ -1,7 +1,10 @@ using System; +using System.Buffers; using System.IO; +using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; +using Jellyfin.Api.Models.PlaybackDtos; using MediaBrowser.Controller.Library; using MediaBrowser.Model.IO; @@ -12,34 +15,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 +70,106 @@ 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 (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { - 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) + { + 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 + { + if (_job != null) + { + _transcodingJobHelper.OnTranscodeEndRequest(_job); } } } + + private async Task CopyToInternalAsync(Stream source, Stream destination, bool readAsync, CancellationToken cancellationToken) + { + var array = ArrayPool.Shared.Rent(IODefaults.CopyToBufferSize); + int bytesRead; + int totalBytesRead = 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; + + 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); + } + } + + if (readAsync) + { + bytesRead = await source.ReadAsync(array, 0, array.Length, cancellationToken).ConfigureAwait(false); + } + else + { + bytesRead = source.Read(array, 0, array.Length); + } + } + + return totalBytesRead; + } } } diff --git a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs index 76f7c8fde..fc38eacaf 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 @@ -712,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. /// diff --git a/Jellyfin.Api/Jellyfin.Api.csproj b/Jellyfin.Api/Jellyfin.Api.csproj index 572cb1af2..a52b234d4 100644 --- a/Jellyfin.Api/Jellyfin.Api.csproj +++ b/Jellyfin.Api/Jellyfin.Api.csproj @@ -16,6 +16,7 @@ + diff --git a/Jellyfin.Api/Models/VideoDtos/DeviceProfileDto.cs b/Jellyfin.Api/Models/VideoDtos/DeviceProfileDto.cs new file mode 100644 index 000000000..db55dc34b --- /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; } + } +} diff --git a/Jellyfin.Server/Startup.cs b/Jellyfin.Server/Startup.cs index edf023fa2..108d8f881 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(); } /// diff --git a/MediaBrowser.Api/Playback/Progressive/VideoService.cs b/MediaBrowser.Api/Playback/Progressive/VideoService.cs index c3f6b905c..5bc85f42d 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 { }