From 3514813eb4eda997a0ea722cc2ed41979419c6dd Mon Sep 17 00:00:00 2001 From: David Date: Sun, 12 Jul 2020 11:14:38 +0200 Subject: Continute work --- Jellyfin.Api/Models/StreamingDtos/StreamState.cs | 207 +++++++++++++++++++++++ 1 file changed, 207 insertions(+) create mode 100644 Jellyfin.Api/Models/StreamingDtos/StreamState.cs (limited to 'Jellyfin.Api/Models/StreamingDtos/StreamState.cs') diff --git a/Jellyfin.Api/Models/StreamingDtos/StreamState.cs b/Jellyfin.Api/Models/StreamingDtos/StreamState.cs new file mode 100644 index 000000000..b962e0ac7 --- /dev/null +++ b/Jellyfin.Api/Models/StreamingDtos/StreamState.cs @@ -0,0 +1,207 @@ +using System; +using Jellyfin.Api.Helpers; +using Jellyfin.Api.Models.PlaybackDtos; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.MediaEncoding; +using MediaBrowser.Model.Dlna; + +namespace Jellyfin.Api.Models.StreamingDtos +{ + /// + /// The stream state dto. + /// + public class StreamState : EncodingJobInfo, IDisposable + { + private readonly IMediaSourceManager _mediaSourceManager; + private readonly TranscodingJobHelper _transcodingJobHelper; + private bool _disposed; + + /// + /// Initializes a new instance of the class. + /// + /// Instance of the interface. + /// The . + /// The singleton. + public StreamState(IMediaSourceManager mediaSourceManager, TranscodingJobType transcodingType, TranscodingJobHelper transcodingJobHelper) + : base(transcodingType) + { + _mediaSourceManager = mediaSourceManager; + _transcodingJobHelper = transcodingJobHelper; + } + + /// + /// Gets or sets the requested url. + /// + public string? RequestedUrl { get; set; } + + // /// + // /// Gets or sets the request. + // /// + // public StreamRequest Request + // { + // get => (StreamRequest)BaseRequest; + // set + // { + // BaseRequest = value; + // + // IsVideoRequest = VideoRequest != null; + // } + // } + + /// + /// Gets or sets the transcoding throttler. + /// + public TranscodingThrottler? TranscodingThrottler { get; set; } + + /// + /// Gets the video request. + /// + public VideoStreamRequest VideoRequest => Request as VideoStreamRequest; + + /// + /// Gets or sets the direct stream provicer. + /// + public IDirectStreamProvider? DirectStreamProvider { get; set; } + + /// + /// Gets or sets the path to wait for. + /// + public string? WaitForPath { get; set; } + + /// + /// Gets a value indicating whether the request outputs video. + /// + public bool IsOutputVideo => Request is VideoStreamRequest; + + /// + /// Gets the segment length. + /// + public int SegmentLength + { + get + { + if (Request.SegmentLength.HasValue) + { + return Request.SegmentLength.Value; + } + + if (EncodingHelper.IsCopyCodec(OutputVideoCodec)) + { + var userAgent = UserAgent ?? string.Empty; + + if (userAgent.IndexOf("AppleTV", StringComparison.OrdinalIgnoreCase) != -1 || + userAgent.IndexOf("cfnetwork", StringComparison.OrdinalIgnoreCase) != -1 || + userAgent.IndexOf("ipad", StringComparison.OrdinalIgnoreCase) != -1 || + userAgent.IndexOf("iphone", StringComparison.OrdinalIgnoreCase) != -1 || + userAgent.IndexOf("ipod", StringComparison.OrdinalIgnoreCase) != -1) + { + if (IsSegmentedLiveStream) + { + return 6; + } + + return 6; + } + + if (IsSegmentedLiveStream) + { + return 3; + } + + return 6; + } + + return 3; + } + } + + /// + /// Gets the minimum number of segments. + /// + public int MinSegments + { + get + { + if (Request.MinSegments.HasValue) + { + return Request.MinSegments.Value; + } + + return SegmentLength >= 10 ? 2 : 3; + } + } + + /// + /// Gets or sets the user agent. + /// + public string? UserAgent { get; set; } + + /// + /// Gets or sets a value indicating whether to estimate the content length. + /// + public bool EstimateContentLength { get; set; } + + /// + /// Gets or sets the transcode seek info. + /// + public TranscodeSeekInfo TranscodeSeekInfo { get; set; } + + /// + /// Gets or sets a value indicating whether to enable dlna headers. + /// + public bool EnableDlnaHeaders { get; set; } + + /// + /// Gets or sets the device profile. + /// + public DeviceProfile? DeviceProfile { get; set; } + + /// + /// Gets or sets the transcoding job. + /// + public TranscodingJobDto? TranscodingJob { get; set; } + + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + public override void ReportTranscodingProgress(TimeSpan? transcodingPosition, float? framerate, double? percentComplete, long? bytesTranscoded, int? bitRate) + { + _transcodingJobHelper.ReportTranscodingProgress(TranscodingJob!, this, transcodingPosition, framerate, percentComplete, bytesTranscoded, bitRate); + } + + /// + /// Disposes the stream state. + /// + /// Whether the object is currently beeing disposed. + protected virtual void Dispose(bool disposing) + { + if (_disposed) + { + return; + } + + if (disposing) + { + // REVIEW: Is this the right place for this? + if (MediaSource.RequiresClosing + && string.IsNullOrWhiteSpace(Request.LiveStreamId) + && !string.IsNullOrWhiteSpace(MediaSource.LiveStreamId)) + { + _mediaSourceManager.CloseLiveStream(MediaSource.LiveStreamId).GetAwaiter().GetResult(); + } + + TranscodingThrottler?.Dispose(); + } + + TranscodingThrottler = null; + TranscodingJob = null; + + _disposed = true; + } + } +} -- cgit v1.2.3 From eae665a9c410540bdbf3880e340fa1a7fb19be92 Mon Sep 17 00:00:00 2001 From: David Date: Wed, 22 Jul 2020 10:57:27 +0200 Subject: Add properties to StreamState to fix some errors --- Jellyfin.Api/Helpers/StreamingHelpers.cs | 35 +++++++++++---------- Jellyfin.Api/Helpers/TranscodingJobHelper.cs | 18 +++++------ Jellyfin.Api/Models/StreamingDtos/StreamState.cs | 40 +++++++++++++++++++++--- 3 files changed, 62 insertions(+), 31 deletions(-) (limited to 'Jellyfin.Api/Models/StreamingDtos/StreamState.cs') diff --git a/Jellyfin.Api/Helpers/StreamingHelpers.cs b/Jellyfin.Api/Helpers/StreamingHelpers.cs index c88ec0b2f..ee1f1efce 100644 --- a/Jellyfin.Api/Helpers/StreamingHelpers.cs +++ b/Jellyfin.Api/Helpers/StreamingHelpers.cs @@ -92,7 +92,10 @@ namespace Jellyfin.Api.Helpers var state = new StreamState(mediaSourceManager, transcodingJobType, transcodingJobHelper) { // TODO request was the StreamingRequest living in MediaBrowser.Api.Playback.Progressive - Request = request, + // Request = request, + DeviceId = deviceId, + PlaySessionId = playSessionId, + LiveStreamId = liveStreamId, RequestedUrl = url, UserAgent = request.Headers[HeaderNames.UserAgent], EnableDlnaHeaders = enableDlnaHeaders @@ -113,23 +116,23 @@ namespace Jellyfin.Api.Helpers } */ - if (state.VideoRequest != null && !string.IsNullOrWhiteSpace(state.VideoRequest.VideoCodec)) + if (state.IsVideoRequest && !string.IsNullOrWhiteSpace(state.VideoCodec)) { - state.SupportedVideoCodecs = state.VideoRequest.VideoCodec.Split(',').Where(i => !string.IsNullOrWhiteSpace(i)).ToArray(); - state.VideoRequest.VideoCodec = state.SupportedVideoCodecs.FirstOrDefault(); + state.SupportedVideoCodecs = state.VideoCodec.Split(',').Where(i => !string.IsNullOrWhiteSpace(i)).ToArray(); + state.VideoCodec = state.SupportedVideoCodecs.FirstOrDefault(); } if (!string.IsNullOrWhiteSpace(audioCodec)) { state.SupportedAudioCodecs = audioCodec.Split(',').Where(i => !string.IsNullOrWhiteSpace(i)).ToArray(); - state.Request.AudioCodec = state.SupportedAudioCodecs.FirstOrDefault(i => mediaEncoder.CanEncodeToAudioCodec(i)) + state.AudioCodec = state.SupportedAudioCodecs.FirstOrDefault(i => mediaEncoder.CanEncodeToAudioCodec(i)) ?? state.SupportedAudioCodecs.FirstOrDefault(); } if (!string.IsNullOrWhiteSpace(subtitleCodec)) { state.SupportedSubtitleCodecs = subtitleCodec.Split(',').Where(i => !string.IsNullOrWhiteSpace(i)).ToArray(); - state.Request.SubtitleCodec = state.SupportedSubtitleCodecs.FirstOrDefault(i => mediaEncoder.CanEncodeToSubtitleCodec(i)) + state.SubtitleCodec = state.SupportedSubtitleCodecs.FirstOrDefault(i => mediaEncoder.CanEncodeToSubtitleCodec(i)) ?? state.SupportedSubtitleCodecs.FirstOrDefault(); } @@ -203,7 +206,7 @@ namespace Jellyfin.Api.Helpers if (isVideoRequest) { - state.OutputVideoCodec = state.VideoRequest.VideoCodec; + state.OutputVideoCodec = state.VideoCodec; state.OutputVideoBitrate = EncodingHelper.GetVideoBitrateParamValue(state.VideoRequest, state.VideoStream, state.OutputVideoCodec); encodingHelper.TryStreamCopy(state); @@ -288,7 +291,7 @@ namespace Jellyfin.Api.Helpers var audioCodec = state.ActualOutputAudioCodec; - if (state.VideoRequest == null) + if (!state.IsVideoRequest) { responseHeaders.Add("contentFeatures.dlna.org", new ContentFeatureBuilder(profile).BuildAudioHeader( state.OutputContainer, @@ -426,12 +429,10 @@ namespace Jellyfin.Api.Helpers return ext; } - var isVideoRequest = state.VideoRequest != null; - // Try to infer based on the desired video codec - if (isVideoRequest) + if (state.IsVideoRequest) { - var videoCodec = state.VideoRequest.VideoCodec; + var videoCodec = state.VideoCodec; if (string.Equals(videoCodec, "h264", StringComparison.OrdinalIgnoreCase) || string.Equals(videoCodec, "h265", StringComparison.OrdinalIgnoreCase)) @@ -456,9 +457,9 @@ namespace Jellyfin.Api.Helpers } // Try to infer based on the desired audio codec - if (!isVideoRequest) + if (!state.IsVideoRequest) { - var audioCodec = state.Request.AudioCodec; + var audioCodec = state.AudioCodec; if (string.Equals("aac", audioCodec, StringComparison.OrdinalIgnoreCase)) { @@ -531,7 +532,7 @@ namespace Jellyfin.Api.Helpers var audioCodec = state.ActualOutputAudioCodec; var videoCodec = state.ActualOutputVideoCodec; - var mediaProfile = state.VideoRequest == null + var mediaProfile = !state.IsVideoRequest ? profile.GetAudioMediaProfile(state.OutputContainer, audioCodec, state.OutputAudioChannels, state.OutputAudioBitrate, state.OutputAudioSampleRate, state.OutputAudioBitDepth) : profile.GetVideoMediaProfile( state.OutputContainer, @@ -561,7 +562,7 @@ namespace Jellyfin.Api.Helpers if (!(@static.HasValue && @static.Value)) { - var transcodingProfile = state.VideoRequest == null ? profile.GetAudioTranscodingProfile(state.OutputContainer, audioCodec) : profile.GetVideoTranscodingProfile(state.OutputContainer, audioCodec, videoCodec); + var transcodingProfile = !state.IsVideoRequest ? profile.GetAudioTranscodingProfile(state.OutputContainer, audioCodec) : profile.GetVideoTranscodingProfile(state.OutputContainer, audioCodec, videoCodec); if (transcodingProfile != null) { @@ -569,7 +570,7 @@ namespace Jellyfin.Api.Helpers // state.EnableMpegtsM2TsMode = transcodingProfile.EnableMpegtsM2TsMode; state.TranscodeSeekInfo = transcodingProfile.TranscodeSeekInfo; - if (state.VideoRequest != null) + if (!state.IsVideoRequest) { state.VideoRequest.CopyTimestamps = transcodingProfile.CopyTimestamps; state.VideoRequest.EnableSubtitlesInManifest = transcodingProfile.EnableSubtitlesInManifest; diff --git a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs index 9fbd5ec2d..4605c0183 100644 --- a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs +++ b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs @@ -443,7 +443,7 @@ namespace Jellyfin.Api.Helpers job.BitRate = bitRate; } - var deviceId = state.Request.DeviceId; + var deviceId = state.DeviceId; if (!string.IsNullOrWhiteSpace(deviceId)) { @@ -525,12 +525,12 @@ namespace Jellyfin.Api.Helpers var transcodingJob = this.OnTranscodeBeginning( outputPath, - state.Request.PlaySessionId, + state.PlaySessionId, state.MediaSource.LiveStreamId, Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture), transcodingJobType, process, - state.Request.DeviceId, + state.DeviceId, state, cancellationTokenSource); @@ -647,12 +647,12 @@ namespace Jellyfin.Api.Helpers /// TranscodingJob. public TranscodingJobDto OnTranscodeBeginning( string path, - string playSessionId, - string liveStreamId, + string? playSessionId, + string? liveStreamId, string transcodingJobId, TranscodingJobType type, Process process, - string deviceId, + string? deviceId, StreamState state, CancellationTokenSource cancellationTokenSource) { @@ -706,9 +706,9 @@ namespace Jellyfin.Api.Helpers _transcodingLocks.Remove(path); } - if (!string.IsNullOrWhiteSpace(state.Request.DeviceId)) + if (!string.IsNullOrWhiteSpace(state.DeviceId)) { - _sessionManager.ClearTranscodingInfo(state.Request.DeviceId); + _sessionManager.ClearTranscodingInfo(state.DeviceId); } } @@ -747,7 +747,7 @@ namespace Jellyfin.Api.Helpers state.IsoMount = await _isoManager.Mount(state.MediaPath, cancellationTokenSource.Token).ConfigureAwait(false); } - if (state.MediaSource.RequiresOpening && string.IsNullOrWhiteSpace(state.Request.LiveStreamId)) + if (state.MediaSource.RequiresOpening && string.IsNullOrWhiteSpace(state.LiveStreamId)) { var liveStreamResponse = await _mediaSourceManager.OpenLiveStream( new LiveStreamRequest { OpenToken = state.MediaSource.OpenToken }, diff --git a/Jellyfin.Api/Models/StreamingDtos/StreamState.cs b/Jellyfin.Api/Models/StreamingDtos/StreamState.cs index b962e0ac7..db7cc6a75 100644 --- a/Jellyfin.Api/Models/StreamingDtos/StreamState.cs +++ b/Jellyfin.Api/Models/StreamingDtos/StreamState.cs @@ -53,10 +53,10 @@ namespace Jellyfin.Api.Models.StreamingDtos /// public TranscodingThrottler? TranscodingThrottler { get; set; } - /// + /*/// /// Gets the video request. /// - public VideoStreamRequest VideoRequest => Request as VideoStreamRequest; + public VideoStreamRequest VideoRequest => Request as VideoStreamRequest;*/ /// /// Gets or sets the direct stream provicer. @@ -68,10 +68,10 @@ namespace Jellyfin.Api.Models.StreamingDtos /// public string? WaitForPath { get; set; } - /// + /*/// /// Gets a value indicating whether the request outputs video. /// - public bool IsOutputVideo => Request is VideoStreamRequest; + public bool IsOutputVideo => Request is VideoStreamRequest;*/ /// /// Gets the segment length. @@ -161,6 +161,36 @@ namespace Jellyfin.Api.Models.StreamingDtos /// public TranscodingJobDto? TranscodingJob { get; set; } + /// + /// Gets or sets the device id. + /// + public string? DeviceId { get; set; } + + /// + /// Gets or sets the play session id. + /// + public string? PlaySessionId { get; set; } + + /// + /// Gets or sets the live stream id. + /// + public string? LiveStreamId { get; set; } + + /// + /// Gets or sets the video coded. + /// + public string? VideoCodec { get; set; } + + /// + /// Gets or sets the audio codec. + /// + public string? AudioCodec { get; set; } + + /// + /// Gets or sets the subtitle codec. + /// + public string? SubtitleCodec { get; set; } + /// public void Dispose() { @@ -189,7 +219,7 @@ namespace Jellyfin.Api.Models.StreamingDtos { // REVIEW: Is this the right place for this? if (MediaSource.RequiresClosing - && string.IsNullOrWhiteSpace(Request.LiveStreamId) + && string.IsNullOrWhiteSpace(LiveStreamId) && !string.IsNullOrWhiteSpace(MediaSource.LiveStreamId)) { _mediaSourceManager.CloseLiveStream(MediaSource.LiveStreamId).GetAwaiter().GetResult(); -- cgit v1.2.3 From 2ce97c022e9ceadea4b9b72053626eff7439ff91 Mon Sep 17 00:00:00 2001 From: David Date: Wed, 22 Jul 2020 16:57:06 +0200 Subject: Move AudioService to Jellyfin.Api --- Jellyfin.Api/Controllers/AudioController.cs | 80 ++++++++--- Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs | 17 +-- Jellyfin.Api/Helpers/StreamingHelpers.cs | 158 +++++++++++---------- Jellyfin.Api/Helpers/TranscodingJobHelper.cs | 14 +- Jellyfin.Api/Models/StreamingDtos/StreamState.cs | 67 +++------ .../Models/StreamingDtos/StreamingRequestDto.cs | 45 ++++++ .../Models/StreamingDtos/VideoRequestDto.cs | 19 +++ 7 files changed, 236 insertions(+), 164 deletions(-) create mode 100644 Jellyfin.Api/Models/StreamingDtos/StreamingRequestDto.cs create mode 100644 Jellyfin.Api/Models/StreamingDtos/VideoRequestDto.cs (limited to 'Jellyfin.Api/Models/StreamingDtos/StreamState.cs') diff --git a/Jellyfin.Api/Controllers/AudioController.cs b/Jellyfin.Api/Controllers/AudioController.cs index 4d29d3880..81492ed4a 100644 --- a/Jellyfin.Api/Controllers/AudioController.cs +++ b/Jellyfin.Api/Controllers/AudioController.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using Jellyfin.Api.Helpers; +using Jellyfin.Api.Models.StreamingDtos; using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Devices; @@ -141,10 +142,10 @@ namespace Jellyfin.Api.Controllers /// Optional. The . /// Optional. The streaming options. /// A containing the audio file. - [HttpGet("{itemId}/stream.{container}")] - [HttpGet("{itemId}/stream")] - [HttpHead("{itemId}/stream.{container}")] + [HttpGet("{itemId}/{stream=stream}.{container?}")] [HttpGet("{itemId}/stream")] + [HttpHead("{itemId}/{stream=stream}.{container?}")] + [HttpHead("{itemId}/stream")] [ProducesResponseType(StatusCodes.Status200OK)] public async Task GetAudioStream( [FromRoute] Guid itemId, @@ -201,21 +202,61 @@ namespace Jellyfin.Api.Controllers var cancellationTokenSource = new CancellationTokenSource(); + StreamingRequestDto streamingRequest = new StreamingRequestDto + { + Id = itemId, + Container = container, + Static = @static.HasValue ? @static.Value : true, + Params = @params, + Tag = tag, + DeviceProfileId = deviceProfileId, + PlaySessionId = playSessionId, + SegmentContainer = segmentContainer, + SegmentLength = segmentLength, + MinSegments = minSegments, + 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, + AudioSampleRate = audioSampleRate, + MaxAudioChannels = maxAudioChannels, + AudioBitRate = audioBitRate, + MaxAudioBitDepth = maxAudioBitDepth, + AudioChannels = audioChannels, + Profile = profile, + Level = level, + Framerate = framerate, + MaxFramerate = maxFramerate, + CopyTimestamps = copyTimestamps.HasValue ? copyTimestamps.Value : true, + StartTimeTicks = startTimeTicks, + Width = width, + Height = height, + VideoBitRate = videoBitRate, + SubtitleStreamIndex = subtitleStreamIndex, + SubtitleMethod = subtitleMethod, + MaxRefFrames = maxRefFrames, + MaxVideoBitDepth = maxVideoBitDepth, + RequireAvc = requireAvc.HasValue ? requireAvc.Value : true, + DeInterlace = deInterlace.HasValue ? deInterlace.Value : true, + RequireNonAnamorphic = requireNonAnamorphic.HasValue ? requireNonAnamorphic.Value : true, + TranscodingMaxAudioChannels = transcodingMaxAudioChannels, + CpuCoreLimit = cpuCoreLimit, + LiveStreamId = liveStreamId, + EnableMpegtsM2TsMode = enableMpegtsM2TsMode.HasValue ? enableMpegtsM2TsMode.Value : true, + VideoCodec = videoCodec, + SubtitleCodec = subtitleCodec, + TranscodeReasons = transcodingReasons, + AudioStreamIndex = audioStreamIndex, + VideoStreamIndex = videoStreamIndex, + Context = context, + StreamOptions = streamOptions + }; + var state = await StreamingHelpers.GetStreamingState( - itemId, - startTimeTicks, - audioCodec, - subtitleCodec, - videoCodec, - @params, - @static, - container, - liveStreamId, - playSessionId, - mediaSourceId, - deviceId, - deviceProfileId, - audioBitRate, + streamingRequest, Request, _authContext, _mediaSourceManager, @@ -230,7 +271,6 @@ namespace Jellyfin.Api.Controllers _deviceManager, _transcodingJobHelper, _transcodingJobType, - false, cancellationTokenSource.Token) .ConfigureAwait(false); @@ -255,7 +295,7 @@ namespace Jellyfin.Api.Controllers using (state) { - return await FileStreamResponseHelpers.GetStaticRemoteStreamResult(state, isHeadRequest, this, cancellationTokenSource).ConfigureAwait(false); + return await FileStreamResponseHelpers.GetStaticRemoteStreamResult(state, isHeadRequest, this).ConfigureAwait(false); } } @@ -297,8 +337,6 @@ namespace Jellyfin.Api.Controllers return FileStreamResponseHelpers.GetStaticFileResult( state.MediaPath, contentType, - _fileSystem.GetLastWriteTimeUtc(state.MediaPath), - cacheDuration, isHeadRequest, this); } diff --git a/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs b/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs index 6ba74d590..9f16b5323 100644 --- a/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs +++ b/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs @@ -23,13 +23,11 @@ namespace Jellyfin.Api.Helpers /// The current . /// Whether the current request is a HTTP HEAD request so only the headers get returned. /// The managing the response. - /// The . /// A containing the API response. public static async Task GetStaticRemoteStreamResult( StreamState state, bool isHeadRequest, - ControllerBase controller, - CancellationTokenSource cancellationTokenSource) + ControllerBase controller) { HttpClient httpClient = new HttpClient(); @@ -59,16 +57,12 @@ namespace Jellyfin.Api.Helpers /// /// The path to the file. /// The content type of the file. - /// The of the last modification of the file. - /// The cache duration of the file. /// Whether the current request is a HTTP HEAD request so only the headers get returned. /// The managing the response. /// An the file. public static ActionResult GetStaticFileResult( string path, string contentType, - DateTime dateLastModified, - TimeSpan? cacheDuration, bool isHeadRequest, ControllerBase controller) { @@ -135,10 +129,11 @@ namespace Jellyfin.Api.Helpers state.Dispose(); } - Stream stream = new MemoryStream(); - - await new ProgressiveFileCopier(streamHelper, outputPath).WriteToAsync(stream, CancellationToken.None).ConfigureAwait(false); - return controller.File(stream, contentType); + using (var memoryStream = new MemoryStream()) + { + await new ProgressiveFileCopier(streamHelper, outputPath).WriteToAsync(memoryStream, CancellationToken.None).ConfigureAwait(false); + return controller.File(memoryStream, contentType); + } } finally { diff --git a/Jellyfin.Api/Helpers/StreamingHelpers.cs b/Jellyfin.Api/Helpers/StreamingHelpers.cs index ee1f1efce..71bf053f5 100644 --- a/Jellyfin.Api/Helpers/StreamingHelpers.cs +++ b/Jellyfin.Api/Helpers/StreamingHelpers.cs @@ -30,22 +30,29 @@ namespace Jellyfin.Api.Helpers /// public static class StreamingHelpers { + /// + /// Gets the current streaming state. + /// + /// The . + /// The . + /// 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. + /// Initialized . + /// The . + /// The . + /// A containing the current . public static async Task GetStreamingState( - Guid itemId, - long? startTimeTicks, - string? audioCodec, - string? subtitleCodec, - string? videoCodec, - string? @params, - bool? @static, - string? container, - string? liveStreamId, - string? playSessionId, - string? mediaSourceId, - string? deviceId, - string? deviceProfileId, - int? audioBitRate, - HttpRequest request, + StreamingRequestDto streamingRequest, + HttpRequest httpRequest, IAuthorizationContext authorizationContext, IMediaSourceManager mediaSourceManager, IUserManager userManager, @@ -59,49 +66,43 @@ namespace Jellyfin.Api.Helpers IDeviceManager deviceManager, TranscodingJobHelper transcodingJobHelper, TranscodingJobType transcodingJobType, - bool isVideoRequest, CancellationToken cancellationToken) { EncodingHelper encodingHelper = new EncodingHelper(mediaEncoder, fileSystem, subtitleEncoder, configuration); // Parse the DLNA time seek header - if (!startTimeTicks.HasValue) + if (!streamingRequest.StartTimeTicks.HasValue) { - var timeSeek = request.Headers["TimeSeekRange.dlna.org"]; + var timeSeek = httpRequest.Headers["TimeSeekRange.dlna.org"]; - startTimeTicks = ParseTimeSeekHeader(timeSeek); + streamingRequest.StartTimeTicks = ParseTimeSeekHeader(timeSeek); } - if (!string.IsNullOrWhiteSpace(@params)) + if (!string.IsNullOrWhiteSpace(streamingRequest.Params)) { - // What is this? - ParseParams(request); + ParseParams(streamingRequest); } - var streamOptions = ParseStreamOptions(request.Query); + streamingRequest.StreamOptions = ParseStreamOptions(httpRequest.Query); - var url = request.Path.Value.Split('.').Last(); + var url = httpRequest.Path.Value.Split('.').Last(); - if (string.IsNullOrEmpty(audioCodec)) + if (string.IsNullOrEmpty(streamingRequest.AudioCodec)) { - audioCodec = encodingHelper.InferAudioCodec(url); + streamingRequest.AudioCodec = encodingHelper.InferAudioCodec(url); } - var enableDlnaHeaders = !string.IsNullOrWhiteSpace(@params) || - string.Equals(request.Headers["GetContentFeatures.DLNA.ORG"], "1", StringComparison.OrdinalIgnoreCase); + var enableDlnaHeaders = !string.IsNullOrWhiteSpace(streamingRequest.Params) || + string.Equals(httpRequest.Headers["GetContentFeatures.DLNA.ORG"], "1", StringComparison.OrdinalIgnoreCase); var state = new StreamState(mediaSourceManager, transcodingJobType, transcodingJobHelper) { - // TODO request was the StreamingRequest living in MediaBrowser.Api.Playback.Progressive - // Request = request, - DeviceId = deviceId, - PlaySessionId = playSessionId, - LiveStreamId = liveStreamId, + Request = streamingRequest, RequestedUrl = url, - UserAgent = request.Headers[HeaderNames.UserAgent], + UserAgent = httpRequest.Headers[HeaderNames.UserAgent], EnableDlnaHeaders = enableDlnaHeaders }; - var auth = authorizationContext.GetAuthorizationInfo(request); + var auth = authorizationContext.GetAuthorizationInfo(httpRequest); if (!auth.UserId.Equals(Guid.Empty)) { state.User = userManager.GetUserById(auth.UserId); @@ -116,27 +117,27 @@ namespace Jellyfin.Api.Helpers } */ - if (state.IsVideoRequest && !string.IsNullOrWhiteSpace(state.VideoCodec)) + if (state.IsVideoRequest && !string.IsNullOrWhiteSpace(state.Request.VideoCodec)) { - state.SupportedVideoCodecs = state.VideoCodec.Split(',').Where(i => !string.IsNullOrWhiteSpace(i)).ToArray(); - state.VideoCodec = state.SupportedVideoCodecs.FirstOrDefault(); + state.SupportedVideoCodecs = state.Request.VideoCodec.Split(',').Where(i => !string.IsNullOrWhiteSpace(i)).ToArray(); + state.Request.VideoCodec = state.SupportedVideoCodecs.FirstOrDefault(); } - if (!string.IsNullOrWhiteSpace(audioCodec)) + if (!string.IsNullOrWhiteSpace(streamingRequest.AudioCodec)) { - state.SupportedAudioCodecs = audioCodec.Split(',').Where(i => !string.IsNullOrWhiteSpace(i)).ToArray(); - state.AudioCodec = state.SupportedAudioCodecs.FirstOrDefault(i => mediaEncoder.CanEncodeToAudioCodec(i)) + state.SupportedAudioCodecs = streamingRequest.AudioCodec.Split(',').Where(i => !string.IsNullOrWhiteSpace(i)).ToArray(); + state.Request.AudioCodec = state.SupportedAudioCodecs.FirstOrDefault(i => mediaEncoder.CanEncodeToAudioCodec(i)) ?? state.SupportedAudioCodecs.FirstOrDefault(); } - if (!string.IsNullOrWhiteSpace(subtitleCodec)) + if (!string.IsNullOrWhiteSpace(streamingRequest.SubtitleCodec)) { - state.SupportedSubtitleCodecs = subtitleCodec.Split(',').Where(i => !string.IsNullOrWhiteSpace(i)).ToArray(); - state.SubtitleCodec = state.SupportedSubtitleCodecs.FirstOrDefault(i => mediaEncoder.CanEncodeToSubtitleCodec(i)) + state.SupportedSubtitleCodecs = streamingRequest.SubtitleCodec.Split(',').Where(i => !string.IsNullOrWhiteSpace(i)).ToArray(); + state.Request.SubtitleCodec = state.SupportedSubtitleCodecs.FirstOrDefault(i => mediaEncoder.CanEncodeToSubtitleCodec(i)) ?? state.SupportedSubtitleCodecs.FirstOrDefault(); } - var item = libraryManager.GetItemById(itemId); + var item = libraryManager.GetItemById(streamingRequest.Id); state.IsInputVideo = string.Equals(item.MediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase); @@ -150,10 +151,10 @@ namespace Jellyfin.Api.Helpers */ MediaSourceInfo? mediaSource = null; - if (string.IsNullOrWhiteSpace(liveStreamId)) + if (string.IsNullOrWhiteSpace(streamingRequest.LiveStreamId)) { - var currentJob = !string.IsNullOrWhiteSpace(playSessionId) - ? transcodingJobHelper.GetTranscodingJob(playSessionId) + var currentJob = !string.IsNullOrWhiteSpace(streamingRequest.PlaySessionId) + ? transcodingJobHelper.GetTranscodingJob(streamingRequest.PlaySessionId) : null; if (currentJob != null) @@ -163,13 +164,13 @@ namespace Jellyfin.Api.Helpers if (mediaSource == null) { - var mediaSources = await mediaSourceManager.GetPlaybackMediaSources(libraryManager.GetItemById(itemId), null, false, false, cancellationToken).ConfigureAwait(false); + var mediaSources = await mediaSourceManager.GetPlaybackMediaSources(libraryManager.GetItemById(streamingRequest.Id), null, false, false, cancellationToken).ConfigureAwait(false); - mediaSource = string.IsNullOrEmpty(mediaSourceId) + mediaSource = string.IsNullOrEmpty(streamingRequest.MediaSourceId) ? mediaSources[0] - : mediaSources.Find(i => string.Equals(i.Id, mediaSourceId, StringComparison.InvariantCulture)); + : mediaSources.Find(i => string.Equals(i.Id, streamingRequest.MediaSourceId, StringComparison.InvariantCulture)); - if (mediaSource == null && Guid.Parse(mediaSourceId) == itemId) + if (mediaSource == null && Guid.Parse(streamingRequest.MediaSourceId) == streamingRequest.Id) { mediaSource = mediaSources[0]; } @@ -177,7 +178,7 @@ namespace Jellyfin.Api.Helpers } else { - var liveStreamInfo = await mediaSourceManager.GetLiveStreamWithDirectStreamProvider(liveStreamId, cancellationToken).ConfigureAwait(false); + var liveStreamInfo = await mediaSourceManager.GetLiveStreamWithDirectStreamProvider(streamingRequest.LiveStreamId, cancellationToken).ConfigureAwait(false); mediaSource = liveStreamInfo.Item1; state.DirectStreamProvider = liveStreamInfo.Item2; } @@ -186,28 +187,28 @@ namespace Jellyfin.Api.Helpers var containerInternal = Path.GetExtension(state.RequestedUrl); - if (string.IsNullOrEmpty(container)) + if (string.IsNullOrEmpty(streamingRequest.Container)) { - containerInternal = container; + containerInternal = streamingRequest.Container; } if (string.IsNullOrEmpty(containerInternal)) { - containerInternal = (@static.HasValue && @static.Value) ? StreamBuilder.NormalizeMediaSourceFormatIntoSingleContainer(state.InputContainer, state.MediaPath, null, DlnaProfileType.Audio) : GetOutputFileExtension(state); + containerInternal = (streamingRequest.Static && streamingRequest.Static) ? StreamBuilder.NormalizeMediaSourceFormatIntoSingleContainer(state.InputContainer, state.MediaPath, null, DlnaProfileType.Audio) : GetOutputFileExtension(state); } state.OutputContainer = (containerInternal ?? string.Empty).TrimStart('.'); - state.OutputAudioBitrate = encodingHelper.GetAudioBitrateParam(audioBitRate, state.AudioStream); + state.OutputAudioBitrate = encodingHelper.GetAudioBitrateParam(streamingRequest.AudioBitRate, state.AudioStream); - state.OutputAudioCodec = audioCodec; + state.OutputAudioCodec = streamingRequest.AudioCodec; state.OutputAudioChannels = encodingHelper.GetNumAudioChannelsParam(state, state.AudioStream, state.OutputAudioCodec); - if (isVideoRequest) + if (state.VideoRequest != null) { - state.OutputVideoCodec = state.VideoCodec; - state.OutputVideoBitrate = EncodingHelper.GetVideoBitrateParamValue(state.VideoRequest, state.VideoStream, state.OutputVideoCodec); + state.OutputVideoCodec = state.Request.VideoCodec; + state.OutputVideoBitrate = encodingHelper.GetVideoBitrateParamValue(state.VideoRequest, state.VideoStream, state.OutputVideoCodec); encodingHelper.TryStreamCopy(state); @@ -220,21 +221,21 @@ namespace Jellyfin.Api.Helpers state.OutputVideoBitrate.Value, state.VideoStream?.Codec, state.OutputVideoCodec, - videoRequest.MaxWidth, - videoRequest.MaxHeight); + state.VideoRequest.MaxWidth, + state.VideoRequest.MaxHeight); - videoRequest.MaxWidth = resolution.MaxWidth; - videoRequest.MaxHeight = resolution.MaxHeight; + state.VideoRequest.MaxWidth = resolution.MaxWidth; + state.VideoRequest.MaxHeight = resolution.MaxHeight; } } - ApplyDeviceProfileSettings(state, dlnaManager, deviceManager, request, deviceProfileId, @static); + ApplyDeviceProfileSettings(state, dlnaManager, deviceManager, httpRequest, streamingRequest.DeviceProfileId, streamingRequest.Static); var ext = string.IsNullOrWhiteSpace(state.OutputContainer) ? GetOutputFileExtension(state) : ('.' + state.OutputContainer); - state.OutputFilePath = GetOutputFilePath(state, ext!, serverConfigurationManager, deviceId, playSessionId); + state.OutputFilePath = GetOutputFilePath(state, ext!, serverConfigurationManager, streamingRequest.DeviceId, streamingRequest.PlaySessionId); return state; } @@ -319,7 +320,7 @@ namespace Jellyfin.Api.Helpers /// /// The time seek header string. /// A nullable representing the seek time in ticks. - public static long? ParseTimeSeekHeader(string value) + private static long? ParseTimeSeekHeader(string value) { if (string.IsNullOrWhiteSpace(value)) { @@ -375,7 +376,7 @@ namespace Jellyfin.Api.Helpers /// /// The query string. /// A containing the stream options. - public static Dictionary ParseStreamOptions(IQueryCollection queryString) + private static Dictionary ParseStreamOptions(IQueryCollection queryString) { Dictionary streamOptions = new Dictionary(); foreach (var param in queryString) @@ -398,7 +399,7 @@ namespace Jellyfin.Api.Helpers /// The current . /// The of the response. /// The start time in ticks. - public static void AddTimeSeekResponseHeaders(StreamState state, IHeaderDictionary responseHeaders, long? startTimeTicks) + private static void AddTimeSeekResponseHeaders(StreamState state, IHeaderDictionary responseHeaders, long? startTimeTicks) { var runtimeSeconds = TimeSpan.FromTicks(state.RunTimeTicks!.Value).TotalSeconds.ToString(CultureInfo.InvariantCulture); var startSeconds = TimeSpan.FromTicks(startTimeTicks ?? 0).TotalSeconds.ToString(CultureInfo.InvariantCulture); @@ -420,7 +421,7 @@ namespace Jellyfin.Api.Helpers /// /// The state. /// System.String. - public static string? GetOutputFileExtension(StreamState state) + private static string? GetOutputFileExtension(StreamState state) { var ext = Path.GetExtension(state.RequestedUrl); @@ -432,7 +433,7 @@ namespace Jellyfin.Api.Helpers // Try to infer based on the desired video codec if (state.IsVideoRequest) { - var videoCodec = state.VideoCodec; + var videoCodec = state.Request.VideoCodec; if (string.Equals(videoCodec, "h264", StringComparison.OrdinalIgnoreCase) || string.Equals(videoCodec, "h265", StringComparison.OrdinalIgnoreCase)) @@ -459,7 +460,7 @@ namespace Jellyfin.Api.Helpers // Try to infer based on the desired audio codec if (!state.IsVideoRequest) { - var audioCodec = state.AudioCodec; + var audioCodec = state.Request.AudioCodec; if (string.Equals("aac", audioCodec, StringComparison.OrdinalIgnoreCase)) { @@ -570,7 +571,7 @@ namespace Jellyfin.Api.Helpers // state.EnableMpegtsM2TsMode = transcodingProfile.EnableMpegtsM2TsMode; state.TranscodeSeekInfo = transcodingProfile.TranscodeSeekInfo; - if (!state.IsVideoRequest) + if (state.VideoRequest != null) { state.VideoRequest.CopyTimestamps = transcodingProfile.CopyTimestamps; state.VideoRequest.EnableSubtitlesInManifest = transcodingProfile.EnableSubtitlesInManifest; @@ -583,11 +584,16 @@ namespace Jellyfin.Api.Helpers /// Parses the parameters. /// /// The request. - private void ParseParams(StreamRequest request) + private static void ParseParams(StreamingRequestDto request) { + if (string.IsNullOrEmpty(request.Params)) + { + return; + } + var vals = request.Params.Split(';'); - var videoRequest = request as VideoStreamRequest; + var videoRequest = request as VideoRequestDto; for (var i = 0; i < vals.Length; i++) { diff --git a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs index 4605c0183..c84135085 100644 --- a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs +++ b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs @@ -443,7 +443,7 @@ namespace Jellyfin.Api.Helpers job.BitRate = bitRate; } - var deviceId = state.DeviceId; + var deviceId = state.Request.DeviceId; if (!string.IsNullOrWhiteSpace(deviceId)) { @@ -486,7 +486,7 @@ namespace Jellyfin.Api.Helpers HttpRequest request, TranscodingJobType transcodingJobType, CancellationTokenSource cancellationTokenSource, - string workingDirectory = null) + string? workingDirectory = null) { Directory.CreateDirectory(Path.GetDirectoryName(outputPath)); @@ -525,12 +525,12 @@ namespace Jellyfin.Api.Helpers var transcodingJob = this.OnTranscodeBeginning( outputPath, - state.PlaySessionId, + state.Request.PlaySessionId, state.MediaSource.LiveStreamId, Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture), transcodingJobType, process, - state.DeviceId, + state.Request.DeviceId, state, cancellationTokenSource); @@ -706,9 +706,9 @@ namespace Jellyfin.Api.Helpers _transcodingLocks.Remove(path); } - if (!string.IsNullOrWhiteSpace(state.DeviceId)) + if (!string.IsNullOrWhiteSpace(state.Request.DeviceId)) { - _sessionManager.ClearTranscodingInfo(state.DeviceId); + _sessionManager.ClearTranscodingInfo(state.Request.DeviceId); } } @@ -747,7 +747,7 @@ namespace Jellyfin.Api.Helpers state.IsoMount = await _isoManager.Mount(state.MediaPath, cancellationTokenSource.Token).ConfigureAwait(false); } - if (state.MediaSource.RequiresOpening && string.IsNullOrWhiteSpace(state.LiveStreamId)) + if (state.MediaSource.RequiresOpening && string.IsNullOrWhiteSpace(state.Request.LiveStreamId)) { var liveStreamResponse = await _mediaSourceManager.OpenLiveStream( new LiveStreamRequest { OpenToken = state.MediaSource.OpenToken }, diff --git a/Jellyfin.Api/Models/StreamingDtos/StreamState.cs b/Jellyfin.Api/Models/StreamingDtos/StreamState.cs index db7cc6a75..70a13d745 100644 --- a/Jellyfin.Api/Models/StreamingDtos/StreamState.cs +++ b/Jellyfin.Api/Models/StreamingDtos/StreamState.cs @@ -19,7 +19,7 @@ namespace Jellyfin.Api.Models.StreamingDtos /// /// Initializes a new instance of the class. /// - /// Instance of the interface. + /// Instance of the interface. /// The . /// The singleton. public StreamState(IMediaSourceManager mediaSourceManager, TranscodingJobType transcodingType, TranscodingJobHelper transcodingJobHelper) @@ -34,29 +34,28 @@ namespace Jellyfin.Api.Models.StreamingDtos /// public string? RequestedUrl { get; set; } - // /// - // /// Gets or sets the request. - // /// - // public StreamRequest Request - // { - // get => (StreamRequest)BaseRequest; - // set - // { - // BaseRequest = value; - // - // IsVideoRequest = VideoRequest != null; - // } - // } + /// + /// Gets or sets the request. + /// + public StreamingRequestDto Request + { + get => (StreamingRequestDto)BaseRequest; + set + { + BaseRequest = value; + IsVideoRequest = VideoRequest != null; + } + } /// /// Gets or sets the transcoding throttler. /// public TranscodingThrottler? TranscodingThrottler { get; set; } - /*/// + /// /// Gets the video request. /// - public VideoStreamRequest VideoRequest => Request as VideoStreamRequest;*/ + public VideoRequestDto? VideoRequest => Request! as VideoRequestDto; /// /// Gets or sets the direct stream provicer. @@ -68,10 +67,10 @@ namespace Jellyfin.Api.Models.StreamingDtos /// public string? WaitForPath { get; set; } - /*/// + /// /// Gets a value indicating whether the request outputs video. /// - public bool IsOutputVideo => Request is VideoStreamRequest;*/ + public bool IsOutputVideo => Request is VideoRequestDto; /// /// Gets the segment length. @@ -161,36 +160,6 @@ namespace Jellyfin.Api.Models.StreamingDtos /// public TranscodingJobDto? TranscodingJob { get; set; } - /// - /// Gets or sets the device id. - /// - public string? DeviceId { get; set; } - - /// - /// Gets or sets the play session id. - /// - public string? PlaySessionId { get; set; } - - /// - /// Gets or sets the live stream id. - /// - public string? LiveStreamId { get; set; } - - /// - /// Gets or sets the video coded. - /// - public string? VideoCodec { get; set; } - - /// - /// Gets or sets the audio codec. - /// - public string? AudioCodec { get; set; } - - /// - /// Gets or sets the subtitle codec. - /// - public string? SubtitleCodec { get; set; } - /// public void Dispose() { @@ -219,7 +188,7 @@ namespace Jellyfin.Api.Models.StreamingDtos { // REVIEW: Is this the right place for this? if (MediaSource.RequiresClosing - && string.IsNullOrWhiteSpace(LiveStreamId) + && string.IsNullOrWhiteSpace(Request.LiveStreamId) && !string.IsNullOrWhiteSpace(MediaSource.LiveStreamId)) { _mediaSourceManager.CloseLiveStream(MediaSource.LiveStreamId).GetAwaiter().GetResult(); diff --git a/Jellyfin.Api/Models/StreamingDtos/StreamingRequestDto.cs b/Jellyfin.Api/Models/StreamingDtos/StreamingRequestDto.cs new file mode 100644 index 000000000..1791b0370 --- /dev/null +++ b/Jellyfin.Api/Models/StreamingDtos/StreamingRequestDto.cs @@ -0,0 +1,45 @@ +using MediaBrowser.Controller.MediaEncoding; + +namespace Jellyfin.Api.Models.StreamingDtos +{ + /// + /// The audio streaming request dto. + /// + public class StreamingRequestDto : BaseEncodingJobOptions + { + /// + /// Gets or sets the device profile. + /// + public string? DeviceProfileId { get; set; } + + /// + /// Gets or sets the params. + /// + public string? Params { get; set; } + + /// + /// Gets or sets the play session id. + /// + public string? PlaySessionId { get; set; } + + /// + /// Gets or sets the tag. + /// + public string? Tag { get; set; } + + /// + /// Gets or sets the segment container. + /// + public string? SegmentContainer { get; set; } + + /// + /// Gets or sets the segment length. + /// + public int? SegmentLength { get; set; } + + /// + /// Gets or sets the min segments. + /// + public int? MinSegments { get; set; } + } +} diff --git a/Jellyfin.Api/Models/StreamingDtos/VideoRequestDto.cs b/Jellyfin.Api/Models/StreamingDtos/VideoRequestDto.cs new file mode 100644 index 000000000..cce2a89d4 --- /dev/null +++ b/Jellyfin.Api/Models/StreamingDtos/VideoRequestDto.cs @@ -0,0 +1,19 @@ +namespace Jellyfin.Api.Models.StreamingDtos +{ + /// + /// The video request dto. + /// + public class VideoRequestDto : StreamingRequestDto + { + /// + /// Gets a value indicating whether this instance has fixed resolution. + /// + /// true if this instance has fixed resolution; otherwise, false. + public bool HasFixedResolution => Width.HasValue || Height.HasValue; + + /// + /// Gets or sets a value indicating whether to enable subtitles in the manifest. + /// + public bool EnableSubtitlesInManifest { get; set; } + } +} -- cgit v1.2.3 From d39f481a5c723dcbd97a578dc8f390e7d0b4e984 Mon Sep 17 00:00:00 2001 From: David Date: Thu, 23 Jul 2020 12:46:54 +0200 Subject: Apply suggestions from review --- Jellyfin.Api/Controllers/AudioController.cs | 16 +++++++--------- Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs | 6 +++--- Jellyfin.Api/Models/StreamingDtos/StreamState.cs | 5 ----- 3 files changed, 10 insertions(+), 17 deletions(-) (limited to 'Jellyfin.Api/Models/StreamingDtos/StreamState.cs') diff --git a/Jellyfin.Api/Controllers/AudioController.cs b/Jellyfin.Api/Controllers/AudioController.cs index 81492ed4a..d8c67cc24 100644 --- a/Jellyfin.Api/Controllers/AudioController.cs +++ b/Jellyfin.Api/Controllers/AudioController.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Net.Http; using System.Threading; using System.Threading.Tasks; using Jellyfin.Api.Helpers; @@ -40,6 +41,7 @@ namespace Jellyfin.Api.Controllers private readonly IConfiguration _configuration; private readonly IDeviceManager _deviceManager; private readonly TranscodingJobHelper _transcodingJobHelper; + private readonly HttpClient _httpClient; private readonly TranscodingJobType _transcodingJobType = TranscodingJobType.Progressive; @@ -59,6 +61,7 @@ namespace Jellyfin.Api.Controllers /// Instance of the interface. /// Instance of the interface. /// The singleton. + /// Instance of the . public AudioController( IDlnaManager dlnaManager, IUserManager userManger, @@ -72,7 +75,8 @@ namespace Jellyfin.Api.Controllers ISubtitleEncoder subtitleEncoder, IConfiguration configuration, IDeviceManager deviceManager, - TranscodingJobHelper transcodingJobHelper) + TranscodingJobHelper transcodingJobHelper, + HttpClient httpClient) { _dlnaManager = dlnaManager; _authContext = authorizationContext; @@ -87,6 +91,7 @@ namespace Jellyfin.Api.Controllers _configuration = configuration; _deviceManager = deviceManager; _transcodingJobHelper = transcodingJobHelper; + _httpClient = httpClient; } /// @@ -295,7 +300,7 @@ namespace Jellyfin.Api.Controllers using (state) { - return await FileStreamResponseHelpers.GetStaticRemoteStreamResult(state, isHeadRequest, this).ConfigureAwait(false); + return await FileStreamResponseHelpers.GetStaticRemoteStreamResult(state, isHeadRequest, this, _httpClient).ConfigureAwait(false); } } @@ -327,13 +332,6 @@ namespace Jellyfin.Api.Controllers return File(Response.Body, contentType); } - TimeSpan? cacheDuration = null; - - if (!string.IsNullOrEmpty(tag)) - { - cacheDuration = TimeSpan.FromDays(365); - } - return FileStreamResponseHelpers.GetStaticFileResult( state.MediaPath, contentType, diff --git a/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs b/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs index 9f16b5323..ddca2f1ae 100644 --- a/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs +++ b/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs @@ -23,14 +23,14 @@ namespace Jellyfin.Api.Helpers /// The current . /// Whether the current request is a HTTP HEAD request so only the headers get returned. /// The managing the response. + /// The making the remote request. /// A containing the API response. public static async Task GetStaticRemoteStreamResult( StreamState state, bool isHeadRequest, - ControllerBase controller) + ControllerBase controller, + HttpClient httpClient) { - HttpClient httpClient = new HttpClient(); - if (state.RemoteHttpHeaders.TryGetValue(HeaderNames.UserAgent, out var useragent)) { httpClient.DefaultRequestHeaders.Add(HeaderNames.UserAgent, useragent); diff --git a/Jellyfin.Api/Models/StreamingDtos/StreamState.cs b/Jellyfin.Api/Models/StreamingDtos/StreamState.cs index 70a13d745..df5e21dac 100644 --- a/Jellyfin.Api/Models/StreamingDtos/StreamState.cs +++ b/Jellyfin.Api/Models/StreamingDtos/StreamState.cs @@ -94,11 +94,6 @@ namespace Jellyfin.Api.Models.StreamingDtos userAgent.IndexOf("iphone", StringComparison.OrdinalIgnoreCase) != -1 || userAgent.IndexOf("ipod", StringComparison.OrdinalIgnoreCase) != -1) { - if (IsSegmentedLiveStream) - { - return 6; - } - return 6; } -- cgit v1.2.3 From ca3dcc3db03d531457b4b60cc3ecdebd57a0157e Mon Sep 17 00:00:00 2001 From: David Date: Fri, 24 Jul 2020 19:14:53 +0200 Subject: Fix suggestions from review --- Jellyfin.Api/Controllers/AudioController.cs | 107 ++++++---------------- Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs | 17 ++-- Jellyfin.Api/Helpers/StreamingHelpers.cs | 68 ++++---------- Jellyfin.Api/Models/StreamingDtos/StreamState.cs | 10 +- 4 files changed, 58 insertions(+), 144 deletions(-) (limited to 'Jellyfin.Api/Models/StreamingDtos/StreamState.cs') diff --git a/Jellyfin.Api/Controllers/AudioController.cs b/Jellyfin.Api/Controllers/AudioController.cs index d8c67cc24..7405c26fb 100644 --- a/Jellyfin.Api/Controllers/AudioController.cs +++ b/Jellyfin.Api/Controllers/AudioController.cs @@ -260,7 +260,7 @@ namespace Jellyfin.Api.Controllers StreamOptions = streamOptions }; - var state = await StreamingHelpers.GetStreamingState( + using var state = await StreamingHelpers.GetStreamingState( streamingRequest, Request, _authContext, @@ -283,14 +283,11 @@ namespace Jellyfin.Api.Controllers { StreamingHelpers.AddDlnaHeaders(state, Response.Headers, true, startTimeTicks, Request, _dlnaManager); - using (state) - { - // TODO AllowEndOfFile = false - await new ProgressiveFileCopier(_streamHelper, state.DirectStreamProvider).WriteToAsync(Response.Body, CancellationToken.None).ConfigureAwait(false); + // 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")!); - } + // TODO (moved from MediaBrowser.Api): Don't hardcode contentType + return File(Response.Body, MimeTypes.GetMimeType("file.ts")!); } // Static remote stream @@ -298,10 +295,7 @@ namespace Jellyfin.Api.Controllers { StreamingHelpers.AddDlnaHeaders(state, Response.Headers, true, startTimeTicks, Request, _dlnaManager); - using (state) - { - return await FileStreamResponseHelpers.GetStaticRemoteStreamResult(state, isHeadRequest, this, _httpClient).ConfigureAwait(false); - } + return await FileStreamResponseHelpers.GetStaticRemoteStreamResult(state, isHeadRequest, this, _httpClient).ConfigureAwait(false); } if (@static.HasValue && @static.Value && state.InputProtocol != MediaProtocol.File) @@ -322,80 +316,35 @@ namespace Jellyfin.Api.Controllers { var contentType = state.GetMimeType("." + state.OutputContainer, false) ?? state.GetMimeType(state.MediaPath); - using (state) + if (state.MediaSource.IsInfiniteStream) { - 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); - } + // TODO AllowEndOfFile = false + await new ProgressiveFileCopier(_streamHelper, state.MediaPath).WriteToAsync(Response.Body, CancellationToken.None).ConfigureAwait(false); - return FileStreamResponseHelpers.GetStaticFileResult( - state.MediaPath, - contentType, - isHeadRequest, - this); + return File(Response.Body, contentType); } - } - /* - // Not static but transcode cache file exists - if (isTranscodeCached && state.VideoRequest == null) - { - var contentType = state.GetMimeType(outputPath) - try - { - if (transcodingJob != null) - { - ApiEntryPoint.Instance.OnTranscodeBeginRequest(transcodingJob); - } - return await ResultFactory.GetStaticFileResult(Request, new StaticFileResultOptions - { - ResponseHeaders = responseHeaders, - ContentType = contentType, - IsHeadRequest = isHeadRequest, - Path = outputPath, - FileShare = FileShare.ReadWrite, - OnComplete = () => - { - if (transcodingJob != null) - { - ApiEntryPoint.Instance.OnTranscodeEndRequest(transcodingJob); - } - }).ConfigureAwait(false); - } - finally - { - state.Dispose(); - } - } - */ - - // Need to start ffmpeg (because media can't be returned directly) - try - { - var encodingOptions = _serverConfigurationManager.GetEncodingOptions(); - var encodingHelper = new EncodingHelper(_mediaEncoder, _fileSystem, _subtitleEncoder, _configuration); - var ffmpegCommandLineArguments = encodingHelper.GetProgressiveAudioFullCommandLine(state, encodingOptions, outputPath); - return await FileStreamResponseHelpers.GetTranscodedFile( - state, + return FileStreamResponseHelpers.GetStaticFileResult( + state.MediaPath, + contentType, isHeadRequest, - _streamHelper, - this, - _transcodingJobHelper, - ffmpegCommandLineArguments, - Request, - _transcodingJobType, - cancellationTokenSource).ConfigureAwait(false); + this); } - catch - { - state.Dispose(); - throw; - } + // 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.GetProgressiveAudioFullCommandLine(state, encodingOptions, outputPath); + return await FileStreamResponseHelpers.GetTranscodedFile( + state, + isHeadRequest, + _streamHelper, + this, + _transcodingJobHelper, + ffmpegCommandLineArguments, + Request, + _transcodingJobType, + cancellationTokenSource).ConfigureAwait(false); } } } diff --git a/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs b/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs index ddca2f1ae..636f47f5f 100644 --- a/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs +++ b/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs @@ -36,17 +36,14 @@ namespace Jellyfin.Api.Helpers httpClient.DefaultRequestHeaders.Add(HeaderNames.UserAgent, useragent); } - var response = await httpClient.GetAsync(state.MediaPath).ConfigureAwait(false); + using var response = await httpClient.GetAsync(state.MediaPath).ConfigureAwait(false); var contentType = response.Content.Headers.ContentType.ToString(); controller.Response.Headers[HeaderNames.AcceptRanges] = "none"; if (isHeadRequest) { - using (response) - { - return controller.File(Array.Empty(), contentType); - } + return controller.File(Array.Empty(), contentType); } return controller.File(await response.Content.ReadAsStreamAsync().ConfigureAwait(false), contentType); @@ -74,7 +71,7 @@ namespace Jellyfin.Api.Helpers return controller.NoContent(); } - var stream = new FileStream(path, FileMode.Open, FileAccess.Read); + using var stream = new FileStream(path, FileMode.Open, FileAccess.Read); return controller.File(stream, contentType); } @@ -129,11 +126,9 @@ namespace Jellyfin.Api.Helpers state.Dispose(); } - using (var memoryStream = new MemoryStream()) - { - await new ProgressiveFileCopier(streamHelper, outputPath).WriteToAsync(memoryStream, CancellationToken.None).ConfigureAwait(false); - return controller.File(memoryStream, contentType); - } + await using var memoryStream = new MemoryStream(); + await new ProgressiveFileCopier(streamHelper, outputPath).WriteToAsync(memoryStream, CancellationToken.None).ConfigureAwait(false); + return controller.File(memoryStream, contentType); } finally { diff --git a/Jellyfin.Api/Helpers/StreamingHelpers.cs b/Jellyfin.Api/Helpers/StreamingHelpers.cs index 0b18756d6..b12590080 100644 --- a/Jellyfin.Api/Helpers/StreamingHelpers.cs +++ b/Jellyfin.Api/Helpers/StreamingHelpers.cs @@ -74,7 +74,7 @@ namespace Jellyfin.Api.Helpers { var timeSeek = httpRequest.Headers["TimeSeekRange.dlna.org"]; - streamingRequest.StartTimeTicks = ParseTimeSeekHeader(timeSeek); + streamingRequest.StartTimeTicks = ParseTimeSeekHeader(timeSeek.ToString()); } if (!string.IsNullOrWhiteSpace(streamingRequest.Params)) @@ -108,31 +108,22 @@ namespace Jellyfin.Api.Helpers state.User = userManager.GetUserById(auth.UserId); } - /* - if ((Request.UserAgent ?? string.Empty).IndexOf("iphone", StringComparison.OrdinalIgnoreCase) != -1 || - (Request.UserAgent ?? string.Empty).IndexOf("ipad", StringComparison.OrdinalIgnoreCase) != -1 || - (Request.UserAgent ?? string.Empty).IndexOf("ipod", StringComparison.OrdinalIgnoreCase) != -1) - { - state.SegmentLength = 6; - } - */ - if (state.IsVideoRequest && !string.IsNullOrWhiteSpace(state.Request.VideoCodec)) { - state.SupportedVideoCodecs = state.Request.VideoCodec.Split(',').Where(i => !string.IsNullOrWhiteSpace(i)).ToArray(); + state.SupportedVideoCodecs = state.Request.VideoCodec.Split(',', StringSplitOptions.RemoveEmptyEntries); state.Request.VideoCodec = state.SupportedVideoCodecs.FirstOrDefault(); } if (!string.IsNullOrWhiteSpace(streamingRequest.AudioCodec)) { - state.SupportedAudioCodecs = streamingRequest.AudioCodec.Split(',').Where(i => !string.IsNullOrWhiteSpace(i)).ToArray(); + state.SupportedAudioCodecs = streamingRequest.AudioCodec.Split(',', StringSplitOptions.RemoveEmptyEntries); state.Request.AudioCodec = state.SupportedAudioCodecs.FirstOrDefault(i => mediaEncoder.CanEncodeToAudioCodec(i)) ?? state.SupportedAudioCodecs.FirstOrDefault(); } if (!string.IsNullOrWhiteSpace(streamingRequest.SubtitleCodec)) { - state.SupportedSubtitleCodecs = streamingRequest.SubtitleCodec.Split(',').Where(i => !string.IsNullOrWhiteSpace(i)).ToArray(); + state.SupportedSubtitleCodecs = streamingRequest.SubtitleCodec.Split(',', StringSplitOptions.RemoveEmptyEntries); state.Request.SubtitleCodec = state.SupportedSubtitleCodecs.FirstOrDefault(i => mediaEncoder.CanEncodeToSubtitleCodec(i)) ?? state.SupportedSubtitleCodecs.FirstOrDefault(); } @@ -141,15 +132,6 @@ namespace Jellyfin.Api.Helpers state.IsInputVideo = string.Equals(item.MediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase); - /* - var primaryImage = item.GetImageInfo(ImageType.Primary, 0) ?? - item.Parents.Select(i => i.GetImageInfo(ImageType.Primary, 0)).FirstOrDefault(i => i != null); - if (primaryImage != null) - { - state.AlbumCoverPath = primaryImage.Path; - } - */ - MediaSourceInfo? mediaSource = null; if (string.IsNullOrWhiteSpace(streamingRequest.LiveStreamId)) { @@ -322,25 +304,24 @@ namespace Jellyfin.Api.Helpers /// /// The time seek header string. /// A nullable representing the seek time in ticks. - private static long? ParseTimeSeekHeader(string value) + private static long? ParseTimeSeekHeader(ReadOnlySpan value) { - if (string.IsNullOrWhiteSpace(value)) + if (value.IsEmpty) { return null; } - const string Npt = "npt="; - if (!value.StartsWith(Npt, StringComparison.OrdinalIgnoreCase)) + const string npt = "npt="; + if (!value.StartsWith(npt, StringComparison.OrdinalIgnoreCase)) { throw new ArgumentException("Invalid timeseek header"); } - int index = value.IndexOf('-', StringComparison.InvariantCulture); + var index = value.IndexOf('-'); value = index == -1 - ? value.Substring(Npt.Length) - : value.Substring(Npt.Length, index - Npt.Length); - - if (value.IndexOf(':', StringComparison.InvariantCulture) == -1) + ? value.Slice(npt.Length) + : value.Slice(npt.Length, index - npt.Length); + if (value.IndexOf(':') == -1) { // Parses npt times in the format of '417.33' if (double.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var seconds)) @@ -351,26 +332,15 @@ namespace Jellyfin.Api.Helpers throw new ArgumentException("Invalid timeseek header"); } - // Parses npt times in the format of '10:19:25.7' - var tokens = value.Split(new[] { ':' }, 3); - double secondsSum = 0; - var timeFactor = 3600; - - foreach (var time in tokens) + try { - if (double.TryParse(time, NumberStyles.Any, CultureInfo.InvariantCulture, out var digit)) - { - secondsSum += digit * timeFactor; - } - else - { - throw new ArgumentException("Invalid timeseek header"); - } - - timeFactor /= 60; + // Parses npt times in the format of '10:19:25.7' + return TimeSpan.Parse(value).Ticks; + } + catch + { + throw new ArgumentException("Invalid timeseek header"); } - - return TimeSpan.FromSeconds(secondsSum).Ticks; } /// diff --git a/Jellyfin.Api/Models/StreamingDtos/StreamState.cs b/Jellyfin.Api/Models/StreamingDtos/StreamState.cs index df5e21dac..e95f2d1f4 100644 --- a/Jellyfin.Api/Models/StreamingDtos/StreamState.cs +++ b/Jellyfin.Api/Models/StreamingDtos/StreamState.cs @@ -88,11 +88,11 @@ namespace Jellyfin.Api.Models.StreamingDtos { var userAgent = UserAgent ?? string.Empty; - if (userAgent.IndexOf("AppleTV", StringComparison.OrdinalIgnoreCase) != -1 || - userAgent.IndexOf("cfnetwork", StringComparison.OrdinalIgnoreCase) != -1 || - userAgent.IndexOf("ipad", StringComparison.OrdinalIgnoreCase) != -1 || - userAgent.IndexOf("iphone", StringComparison.OrdinalIgnoreCase) != -1 || - userAgent.IndexOf("ipod", StringComparison.OrdinalIgnoreCase) != -1) + if (userAgent.IndexOf("AppleTV", StringComparison.OrdinalIgnoreCase) != -1 + || userAgent.IndexOf("cfnetwork", StringComparison.OrdinalIgnoreCase) != -1 + || userAgent.IndexOf("ipad", StringComparison.OrdinalIgnoreCase) != -1 + || userAgent.IndexOf("iphone", StringComparison.OrdinalIgnoreCase) != -1 + || userAgent.IndexOf("ipod", StringComparison.OrdinalIgnoreCase) != -1) { return 6; } -- cgit v1.2.3