From 460c3dd35166c9a48db83db62a3f0a8742956408 Mon Sep 17 00:00:00 2001 From: crobibero Date: Sun, 9 Aug 2020 17:20:14 -0600 Subject: convert dependent controller functions to di helper class --- Jellyfin.Api/Helpers/DynamicHlsHelper.cs | 549 +++++++++++++++++++++++++++++++ 1 file changed, 549 insertions(+) create mode 100644 Jellyfin.Api/Helpers/DynamicHlsHelper.cs (limited to 'Jellyfin.Api/Helpers/DynamicHlsHelper.cs') diff --git a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs new file mode 100644 index 000000000..127d91430 --- /dev/null +++ b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs @@ -0,0 +1,549 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Net; +using System.Security.Claims; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Jellyfin.Api.Models.StreamingDtos; +using MediaBrowser.Common.Net; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Devices; +using MediaBrowser.Controller.Dlna; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.MediaEncoding; +using MediaBrowser.Controller.Net; +using MediaBrowser.Model.Dlna; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.IO; +using MediaBrowser.Model.Net; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using Microsoft.Net.Http.Headers; + +namespace Jellyfin.Api.Helpers +{ + /// + /// Dynamic hls helper. + /// + public class DynamicHlsHelper + { + private readonly ILibraryManager _libraryManager; + private readonly IUserManager _userManager; + 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 INetworkManager _networkManager; + private readonly ILogger _logger; + + /// + /// Initializes a new instance of the class. + /// + /// 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 . + /// Instance of the interface. + /// Instance of the interface. + public DynamicHlsHelper( + ILibraryManager libraryManager, + IUserManager userManager, + IDlnaManager dlnaManager, + IAuthorizationContext authContext, + IMediaSourceManager mediaSourceManager, + IServerConfigurationManager serverConfigurationManager, + IMediaEncoder mediaEncoder, + IFileSystem fileSystem, + ISubtitleEncoder subtitleEncoder, + IConfiguration configuration, + IDeviceManager deviceManager, + TranscodingJobHelper transcodingJobHelper, + INetworkManager networkManager, + ILogger logger) + { + _libraryManager = libraryManager; + _userManager = userManager; + _dlnaManager = dlnaManager; + _authContext = authContext; + _mediaSourceManager = mediaSourceManager; + _serverConfigurationManager = serverConfigurationManager; + _mediaEncoder = mediaEncoder; + _fileSystem = fileSystem; + _subtitleEncoder = subtitleEncoder; + _configuration = configuration; + _deviceManager = deviceManager; + _transcodingJobHelper = transcodingJobHelper; + _networkManager = networkManager; + _logger = logger; + } + + /// + /// Get master hls playlist. + /// + /// Requesting controller. + /// Transcoding job type. + /// Streaming request dto. + /// Enable adaptive bitrate streaming. + /// A containing the resulting . + public async Task GetMasterHlsPlaylist( + BaseJellyfinApiController controller, + TranscodingJobType transcodingJobType, + StreamingRequestDto streamingRequest, + bool enableAdaptiveBitrateStreaming) + { + var isHeadRequest = controller.Request.Method == WebRequestMethods.Http.Head; + var cancellationTokenSource = new CancellationTokenSource(); + return await GetMasterPlaylistInternal( + controller, + streamingRequest, + isHeadRequest, + enableAdaptiveBitrateStreaming, + transcodingJobType, + cancellationTokenSource).ConfigureAwait(false); + } + + private async Task GetMasterPlaylistInternal( + BaseJellyfinApiController controller, + StreamingRequestDto streamingRequest, + bool isHeadRequest, + bool enableAdaptiveBitrateStreaming, + TranscodingJobType transcodingJobType, + CancellationTokenSource cancellationTokenSource) + { + using var state = await StreamingHelpers.GetStreamingState( + streamingRequest, + controller.Request, + _authContext, + _mediaSourceManager, + _userManager, + _libraryManager, + _serverConfigurationManager, + _mediaEncoder, + _fileSystem, + _subtitleEncoder, + _configuration, + _dlnaManager, + _deviceManager, + _transcodingJobHelper, + transcodingJobType, + cancellationTokenSource.Token) + .ConfigureAwait(false); + + controller.Response.Headers.Add(HeaderNames.Expires, "0"); + if (isHeadRequest) + { + return new FileContentResult(Array.Empty(), MimeTypes.GetMimeType("playlist.m3u8")); + } + + var totalBitrate = state.OutputAudioBitrate ?? 0 + state.OutputVideoBitrate ?? 0; + + var builder = new StringBuilder(); + + builder.AppendLine("#EXTM3U"); + + var isLiveStream = state.IsSegmentedLiveStream; + + var queryString = controller.Request.QueryString.ToString(); + + // from universal audio service + if (queryString.IndexOf("SegmentContainer", StringComparison.OrdinalIgnoreCase) == -1 && !string.IsNullOrWhiteSpace(state.Request.SegmentContainer)) + { + queryString += "&SegmentContainer=" + state.Request.SegmentContainer; + } + + // from universal audio service + if (!string.IsNullOrWhiteSpace(state.Request.TranscodeReasons) && queryString.IndexOf("TranscodeReasons=", StringComparison.OrdinalIgnoreCase) == -1) + { + queryString += "&TranscodeReasons=" + state.Request.TranscodeReasons; + } + + // Main stream + var playlistUrl = isLiveStream ? "live.m3u8" : "main.m3u8"; + + playlistUrl += queryString; + + var subtitleStreams = state.MediaSource + .MediaStreams + .Where(i => i.IsTextSubtitleStream) + .ToList(); + + var subtitleGroup = subtitleStreams.Count > 0 && (state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Hls || state.VideoRequest!.EnableSubtitlesInManifest) + ? "subs" + : null; + + // If we're burning in subtitles then don't add additional subs to the manifest + if (state.SubtitleStream != null && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode) + { + subtitleGroup = null; + } + + if (!string.IsNullOrWhiteSpace(subtitleGroup)) + { + AddSubtitles(state, subtitleStreams, builder, controller.Request.HttpContext.User); + } + + AppendPlaylist(builder, state, playlistUrl, totalBitrate, subtitleGroup); + + if (EnableAdaptiveBitrateStreaming(state, isLiveStream, enableAdaptiveBitrateStreaming, controller.Request.HttpContext.Connection.RemoteIpAddress)) + { + var requestedVideoBitrate = state.VideoRequest == null ? 0 : state.VideoRequest.VideoBitRate ?? 0; + + // By default, vary by just 200k + var variation = GetBitrateVariation(totalBitrate); + + var newBitrate = totalBitrate - variation; + var variantUrl = ReplaceBitrate(playlistUrl, requestedVideoBitrate, requestedVideoBitrate - variation); + AppendPlaylist(builder, state, variantUrl, newBitrate, subtitleGroup); + + variation *= 2; + newBitrate = totalBitrate - variation; + variantUrl = ReplaceBitrate(playlistUrl, requestedVideoBitrate, requestedVideoBitrate - variation); + AppendPlaylist(builder, state, variantUrl, newBitrate, subtitleGroup); + } + + return new FileContentResult(Encoding.UTF8.GetBytes(builder.ToString()), MimeTypes.GetMimeType("playlist.m3u8")); + } + + private void AppendPlaylist(StringBuilder builder, StreamState state, string url, int bitrate, string? subtitleGroup) + { + builder.Append("#EXT-X-STREAM-INF:BANDWIDTH=") + .Append(bitrate.ToString(CultureInfo.InvariantCulture)) + .Append(",AVERAGE-BANDWIDTH=") + .Append(bitrate.ToString(CultureInfo.InvariantCulture)); + + AppendPlaylistCodecsField(builder, state); + + AppendPlaylistResolutionField(builder, state); + + AppendPlaylistFramerateField(builder, state); + + if (!string.IsNullOrWhiteSpace(subtitleGroup)) + { + builder.Append(",SUBTITLES=\"") + .Append(subtitleGroup) + .Append('"'); + } + + builder.Append(Environment.NewLine); + builder.AppendLine(url); + } + + /// + /// Appends a CODECS field containing formatted strings of + /// the active streams output video and audio codecs. + /// + /// + /// + /// + /// StringBuilder to append the field to. + /// StreamState of the current stream. + private void AppendPlaylistCodecsField(StringBuilder builder, StreamState state) + { + // Video + string videoCodecs = string.Empty; + int? videoCodecLevel = GetOutputVideoCodecLevel(state); + if (!string.IsNullOrEmpty(state.ActualOutputVideoCodec) && videoCodecLevel.HasValue) + { + videoCodecs = GetPlaylistVideoCodecs(state, state.ActualOutputVideoCodec, videoCodecLevel.Value); + } + + // Audio + string audioCodecs = string.Empty; + if (!string.IsNullOrEmpty(state.ActualOutputAudioCodec)) + { + audioCodecs = GetPlaylistAudioCodecs(state); + } + + StringBuilder codecs = new StringBuilder(); + + codecs.Append(videoCodecs); + + if (!string.IsNullOrEmpty(videoCodecs) && !string.IsNullOrEmpty(audioCodecs)) + { + codecs.Append(','); + } + + codecs.Append(audioCodecs); + + if (codecs.Length > 1) + { + builder.Append(",CODECS=\"") + .Append(codecs) + .Append('"'); + } + } + + /// + /// Appends a RESOLUTION field containing the resolution of the output stream. + /// + /// + /// StringBuilder to append the field to. + /// StreamState of the current stream. + private void AppendPlaylistResolutionField(StringBuilder builder, StreamState state) + { + if (state.OutputWidth.HasValue && state.OutputHeight.HasValue) + { + builder.Append(",RESOLUTION=") + .Append(state.OutputWidth.GetValueOrDefault()) + .Append('x') + .Append(state.OutputHeight.GetValueOrDefault()); + } + } + + /// + /// Appends a FRAME-RATE field containing the framerate of the output stream. + /// + /// + /// StringBuilder to append the field to. + /// StreamState of the current stream. + private void AppendPlaylistFramerateField(StringBuilder builder, StreamState state) + { + double? framerate = null; + if (state.TargetFramerate.HasValue) + { + framerate = Math.Round(state.TargetFramerate.GetValueOrDefault(), 3); + } + else if (state.VideoStream?.RealFrameRate != null) + { + framerate = Math.Round(state.VideoStream.RealFrameRate.GetValueOrDefault(), 3); + } + + if (framerate.HasValue) + { + builder.Append(",FRAME-RATE=") + .Append(framerate.Value); + } + } + + private bool EnableAdaptiveBitrateStreaming(StreamState state, bool isLiveStream, bool enableAdaptiveBitrateStreaming, IPAddress ipAddress) + { + // Within the local network this will likely do more harm than good. + var ip = RequestHelpers.NormalizeIp(ipAddress).ToString(); + if (_networkManager.IsInLocalNetwork(ip)) + { + return false; + } + + if (!enableAdaptiveBitrateStreaming) + { + return false; + } + + if (isLiveStream || string.IsNullOrWhiteSpace(state.MediaPath)) + { + // Opening live streams is so slow it's not even worth it + return false; + } + + if (EncodingHelper.IsCopyCodec(state.OutputVideoCodec)) + { + return false; + } + + if (EncodingHelper.IsCopyCodec(state.OutputAudioCodec)) + { + return false; + } + + if (!state.IsOutputVideo) + { + return false; + } + + // Having problems in android + return false; + // return state.VideoRequest.VideoBitRate.HasValue; + } + + private void AddSubtitles(StreamState state, IEnumerable subtitles, StringBuilder builder, ClaimsPrincipal user) + { + var selectedIndex = state.SubtitleStream == null || state.SubtitleDeliveryMethod != SubtitleDeliveryMethod.Hls ? (int?)null : state.SubtitleStream.Index; + const string Format = "#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID=\"subs\",NAME=\"{0}\",DEFAULT={1},FORCED={2},AUTOSELECT=YES,URI=\"{3}\",LANGUAGE=\"{4}\""; + + foreach (var stream in subtitles) + { + var name = stream.DisplayTitle; + + var isDefault = selectedIndex.HasValue && selectedIndex.Value == stream.Index; + var isForced = stream.IsForced; + + var url = string.Format( + CultureInfo.InvariantCulture, + "{0}/Subtitles/{1}/subtitles.m3u8?SegmentLength={2}&api_key={3}", + state.Request.MediaSourceId, + stream.Index.ToString(CultureInfo.InvariantCulture), + 30.ToString(CultureInfo.InvariantCulture), + ClaimHelpers.GetToken(user)); + + var line = string.Format( + CultureInfo.InvariantCulture, + Format, + name, + isDefault ? "YES" : "NO", + isForced ? "YES" : "NO", + url, + stream.Language ?? "Unknown"); + + builder.AppendLine(line); + } + } + + /// + /// Get the H.26X level of the output video stream. + /// + /// StreamState of the current stream. + /// H.26X level of the output video stream. + private int? GetOutputVideoCodecLevel(StreamState state) + { + string? levelString; + if (EncodingHelper.IsCopyCodec(state.OutputVideoCodec) + && state.VideoStream.Level.HasValue) + { + levelString = state.VideoStream?.Level.ToString(); + } + else + { + levelString = state.GetRequestedLevel(state.ActualOutputVideoCodec); + } + + if (int.TryParse(levelString, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsedLevel)) + { + return parsedLevel; + } + + return null; + } + + /// + /// Gets a formatted string of the output audio codec, for use in the CODECS field. + /// + /// + /// + /// StreamState of the current stream. + /// Formatted audio codec string. + private string GetPlaylistAudioCodecs(StreamState state) + { + if (string.Equals(state.ActualOutputAudioCodec, "aac", StringComparison.OrdinalIgnoreCase)) + { + string? profile = state.GetRequestedProfiles("aac").FirstOrDefault(); + return HlsCodecStringHelpers.GetAACString(profile); + } + + if (string.Equals(state.ActualOutputAudioCodec, "mp3", StringComparison.OrdinalIgnoreCase)) + { + return HlsCodecStringHelpers.GetMP3String(); + } + + if (string.Equals(state.ActualOutputAudioCodec, "ac3", StringComparison.OrdinalIgnoreCase)) + { + return HlsCodecStringHelpers.GetAC3String(); + } + + if (string.Equals(state.ActualOutputAudioCodec, "eac3", StringComparison.OrdinalIgnoreCase)) + { + return HlsCodecStringHelpers.GetEAC3String(); + } + + return string.Empty; + } + + /// + /// Gets a formatted string of the output video codec, for use in the CODECS field. + /// + /// + /// + /// StreamState of the current stream. + /// Video codec. + /// Video level. + /// Formatted video codec string. + private string GetPlaylistVideoCodecs(StreamState state, string codec, int level) + { + if (level == 0) + { + // This is 0 when there's no requested H.26X level in the device profile + // and the source is not encoded in H.26X + _logger.LogError("Got invalid H.26X level when building CODECS field for HLS master playlist"); + return string.Empty; + } + + if (string.Equals(codec, "h264", StringComparison.OrdinalIgnoreCase)) + { + string profile = state.GetRequestedProfiles("h264").FirstOrDefault(); + return HlsCodecStringHelpers.GetH264String(profile, level); + } + + if (string.Equals(codec, "h265", StringComparison.OrdinalIgnoreCase) + || string.Equals(codec, "hevc", StringComparison.OrdinalIgnoreCase)) + { + string profile = state.GetRequestedProfiles("h265").FirstOrDefault(); + + return HlsCodecStringHelpers.GetH265String(profile, level); + } + + return string.Empty; + } + + private int GetBitrateVariation(int bitrate) + { + // By default, vary by just 50k + var variation = 50000; + + if (bitrate >= 10000000) + { + variation = 2000000; + } + else if (bitrate >= 5000000) + { + variation = 1500000; + } + else if (bitrate >= 3000000) + { + variation = 1000000; + } + else if (bitrate >= 2000000) + { + variation = 500000; + } + else if (bitrate >= 1000000) + { + variation = 300000; + } + else if (bitrate >= 600000) + { + variation = 200000; + } + else if (bitrate >= 400000) + { + variation = 100000; + } + + return variation; + } + + private string ReplaceBitrate(string url, int oldValue, int newValue) + { + return url.Replace( + "videobitrate=" + oldValue.ToString(CultureInfo.InvariantCulture), + "videobitrate=" + newValue.ToString(CultureInfo.InvariantCulture), + StringComparison.OrdinalIgnoreCase); + } + } +} -- cgit v1.2.3 From c5e9cf15f6476d96eadc1d32e38687c3a5a98a17 Mon Sep 17 00:00:00 2001 From: crobibero Date: Mon, 10 Aug 2020 07:53:32 -0600 Subject: Use proper IHttpContextAccessor --- Jellyfin.Api/Controllers/AudioController.cs | 2 +- Jellyfin.Api/Controllers/DynamicHlsController.cs | 6 ++-- Jellyfin.Api/Controllers/HlsSegmentController.cs | 4 +-- .../Controllers/UniversalAudioController.cs | 5 ++- Jellyfin.Api/Controllers/VideosController.cs | 7 ++--- Jellyfin.Api/Helpers/AudioHelper.cs | 36 ++++++++++++---------- Jellyfin.Api/Helpers/DynamicHlsHelper.cs | 23 +++++++------- Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs | 36 ++++++++++------------ 8 files changed, 59 insertions(+), 60 deletions(-) (limited to 'Jellyfin.Api/Helpers/DynamicHlsHelper.cs') diff --git a/Jellyfin.Api/Controllers/AudioController.cs b/Jellyfin.Api/Controllers/AudioController.cs index 107db9571..d300b4891 100644 --- a/Jellyfin.Api/Controllers/AudioController.cs +++ b/Jellyfin.Api/Controllers/AudioController.cs @@ -192,7 +192,7 @@ namespace Jellyfin.Api.Controllers StreamOptions = streamOptions }; - return await _audioHelper.GetAudioStream(this, _transcodingJobType, streamingRequest).ConfigureAwait(false); + return await _audioHelper.GetAudioStream(_transcodingJobType, streamingRequest).ConfigureAwait(false); } } } diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs index 1a85a07b8..b4fe3bc8f 100644 --- a/Jellyfin.Api/Controllers/DynamicHlsController.cs +++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs @@ -272,7 +272,7 @@ namespace Jellyfin.Api.Controllers EnableAdaptiveBitrateStreaming = enableAdaptiveBitrateStreaming }; - return await _dynamicHlsHelper.GetMasterHlsPlaylist(this, _transcodingJobType, streamingRequest, enableAdaptiveBitrateStreaming).ConfigureAwait(false); + return await _dynamicHlsHelper.GetMasterHlsPlaylist(_transcodingJobType, streamingRequest, enableAdaptiveBitrateStreaming).ConfigureAwait(false); } /// @@ -439,7 +439,7 @@ namespace Jellyfin.Api.Controllers EnableAdaptiveBitrateStreaming = enableAdaptiveBitrateStreaming }; - return await _dynamicHlsHelper.GetMasterHlsPlaylist(this, _transcodingJobType, streamingRequest, enableAdaptiveBitrateStreaming).ConfigureAwait(false); + return await _dynamicHlsHelper.GetMasterHlsPlaylist(_transcodingJobType, streamingRequest, enableAdaptiveBitrateStreaming).ConfigureAwait(false); } /// @@ -1657,7 +1657,7 @@ namespace Jellyfin.Api.Controllers return Task.CompletedTask; }); - return FileStreamResponseHelpers.GetStaticFileResult(segmentPath, MimeTypes.GetMimeType(segmentPath)!, false, this); + return FileStreamResponseHelpers.GetStaticFileResult(segmentPath, MimeTypes.GetMimeType(segmentPath)!, false, HttpContext); } private long GetEndPositionTicks(StreamState state, int requestedIndex) diff --git a/Jellyfin.Api/Controllers/HlsSegmentController.cs b/Jellyfin.Api/Controllers/HlsSegmentController.cs index e4a6842bc..816252f80 100644 --- a/Jellyfin.Api/Controllers/HlsSegmentController.cs +++ b/Jellyfin.Api/Controllers/HlsSegmentController.cs @@ -61,7 +61,7 @@ namespace Jellyfin.Api.Controllers var file = segmentId + Path.GetExtension(Request.Path); file = Path.Combine(_serverConfigurationManager.GetTranscodePath(), file); - return FileStreamResponseHelpers.GetStaticFileResult(file, MimeTypes.GetMimeType(file)!, false, this); + return FileStreamResponseHelpers.GetStaticFileResult(file, MimeTypes.GetMimeType(file)!, false, HttpContext); } /// @@ -148,7 +148,7 @@ namespace Jellyfin.Api.Controllers return Task.CompletedTask; }); - return FileStreamResponseHelpers.GetStaticFileResult(path, MimeTypes.GetMimeType(path)!, false, this); + return FileStreamResponseHelpers.GetStaticFileResult(path, MimeTypes.GetMimeType(path)!, false, HttpContext); } } } diff --git a/Jellyfin.Api/Controllers/UniversalAudioController.cs b/Jellyfin.Api/Controllers/UniversalAudioController.cs index 2ea462488..2a788e03a 100644 --- a/Jellyfin.Api/Controllers/UniversalAudioController.cs +++ b/Jellyfin.Api/Controllers/UniversalAudioController.cs @@ -110,7 +110,6 @@ namespace Jellyfin.Api.Controllers [FromQuery] bool breakOnNonKeyFrames, [FromQuery] bool enableRedirection = true) { - bool isHeadRequest = Request.Method == System.Net.WebRequestMethods.Http.Head; var deviceProfile = GetDeviceProfile(container, transcodingContainer, audioCodec, transcodingProtocol, breakOnNonKeyFrames, transcodingAudioChannels, maxAudioSampleRate, maxAudioBitDepth, maxAudioChannels); _authorizationContext.GetAuthorizationInfo(Request).DeviceId = deviceId; @@ -222,7 +221,7 @@ namespace Jellyfin.Api.Controllers EnableAdaptiveBitrateStreaming = true }; - return await _dynamicHlsHelper.GetMasterHlsPlaylist(this, TranscodingJobType.Hls, dynamicHlsRequestDto, true) + return await _dynamicHlsHelper.GetMasterHlsPlaylist(TranscodingJobType.Hls, dynamicHlsRequestDto, true) .ConfigureAwait(false); } @@ -251,7 +250,7 @@ namespace Jellyfin.Api.Controllers Context = EncodingContext.Static }; - return await _audioHelper.GetAudioStream(this, TranscodingJobType.Progressive, audioStreamingDto).ConfigureAwait(false); + return await _audioHelper.GetAudioStream(TranscodingJobType.Progressive, audioStreamingDto).ConfigureAwait(false); } private DeviceProfile GetDeviceProfile( diff --git a/Jellyfin.Api/Controllers/VideosController.cs b/Jellyfin.Api/Controllers/VideosController.cs index fafa722c5..8eee51c2c 100644 --- a/Jellyfin.Api/Controllers/VideosController.cs +++ b/Jellyfin.Api/Controllers/VideosController.cs @@ -471,7 +471,7 @@ namespace Jellyfin.Api.Controllers StreamingHelpers.AddDlnaHeaders(state, Response.Headers, true, startTimeTicks, Request, _dlnaManager); using var httpClient = _httpClientFactory.CreateClient(); - return await FileStreamResponseHelpers.GetStaticRemoteStreamResult(state, isHeadRequest, this, httpClient).ConfigureAwait(false); + return await FileStreamResponseHelpers.GetStaticRemoteStreamResult(state, isHeadRequest, httpClient, HttpContext).ConfigureAwait(false); } if (@static.HasValue && @static.Value && state.InputProtocol != MediaProtocol.File) @@ -507,7 +507,7 @@ namespace Jellyfin.Api.Controllers state.MediaPath, contentType, isHeadRequest, - this); + HttpContext); } // Need to start ffmpeg (because media can't be returned directly) @@ -517,10 +517,9 @@ namespace Jellyfin.Api.Controllers return await FileStreamResponseHelpers.GetTranscodedFile( state, isHeadRequest, - this, + HttpContext, _transcodingJobHelper, ffmpegCommandLineArguments, - Request, _transcodingJobType, cancellationTokenSource).ConfigureAwait(false); } diff --git a/Jellyfin.Api/Helpers/AudioHelper.cs b/Jellyfin.Api/Helpers/AudioHelper.cs index 001028432..694e1f0dd 100644 --- a/Jellyfin.Api/Helpers/AudioHelper.cs +++ b/Jellyfin.Api/Helpers/AudioHelper.cs @@ -12,6 +12,7 @@ using MediaBrowser.Controller.Net; using MediaBrowser.Model.IO; using MediaBrowser.Model.MediaInfo; using MediaBrowser.Model.Net; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Configuration; @@ -35,6 +36,7 @@ namespace Jellyfin.Api.Helpers private readonly IDeviceManager _deviceManager; private readonly TranscodingJobHelper _transcodingJobHelper; private readonly IHttpClientFactory _httpClientFactory; + private readonly IHttpContextAccessor _httpContextAccessor; /// /// Initializes a new instance of the class. @@ -52,6 +54,7 @@ namespace Jellyfin.Api.Helpers /// Instance of the interface. /// Instance of . /// Instance of the interface. + /// Instance of the interface. public AudioHelper( IDlnaManager dlnaManager, IAuthorizationContext authContext, @@ -65,7 +68,8 @@ namespace Jellyfin.Api.Helpers IConfiguration configuration, IDeviceManager deviceManager, TranscodingJobHelper transcodingJobHelper, - IHttpClientFactory httpClientFactory) + IHttpClientFactory httpClientFactory, + IHttpContextAccessor httpContextAccessor) { _dlnaManager = dlnaManager; _authContext = authContext; @@ -80,26 +84,25 @@ namespace Jellyfin.Api.Helpers _deviceManager = deviceManager; _transcodingJobHelper = transcodingJobHelper; _httpClientFactory = httpClientFactory; + _httpContextAccessor = httpContextAccessor; } /// /// Get audio stream. /// - /// Requesting controller. /// Transcoding job type. /// Streaming controller.Request dto. /// A containing the resulting . public async Task GetAudioStream( - BaseJellyfinApiController controller, TranscodingJobType transcodingJobType, StreamingRequestDto streamingRequest) { - bool isHeadRequest = controller.Request.Method == System.Net.WebRequestMethods.Http.Head; + bool isHeadRequest = _httpContextAccessor.HttpContext.Request.Method == System.Net.WebRequestMethods.Http.Head; var cancellationTokenSource = new CancellationTokenSource(); using var state = await StreamingHelpers.GetStreamingState( streamingRequest, - controller.Request, + _httpContextAccessor.HttpContext.Request, _authContext, _mediaSourceManager, _userManager, @@ -118,30 +121,30 @@ namespace Jellyfin.Api.Helpers if (streamingRequest.Static && state.DirectStreamProvider != null) { - StreamingHelpers.AddDlnaHeaders(state, controller.Response.Headers, true, streamingRequest.StartTimeTicks, controller.Request, _dlnaManager); + StreamingHelpers.AddDlnaHeaders(state, _httpContextAccessor.HttpContext.Response.Headers, true, streamingRequest.StartTimeTicks, _httpContextAccessor.HttpContext.Request, _dlnaManager); await new ProgressiveFileCopier(state.DirectStreamProvider, null, _transcodingJobHelper, CancellationToken.None) { AllowEndOfFile = false - }.WriteToAsync(controller.Response.Body, CancellationToken.None) + }.WriteToAsync(_httpContextAccessor.HttpContext.Response.Body, CancellationToken.None) .ConfigureAwait(false); // TODO (moved from MediaBrowser.Api): Don't hardcode contentType - return controller.File(controller.Response.Body, MimeTypes.GetMimeType("file.ts")!); + return new FileStreamResult(_httpContextAccessor.HttpContext.Response.Body, MimeTypes.GetMimeType("file.ts")!); } // Static remote stream if (streamingRequest.Static && state.InputProtocol == MediaProtocol.Http) { - StreamingHelpers.AddDlnaHeaders(state, controller.Response.Headers, true, streamingRequest.StartTimeTicks, controller.Request, _dlnaManager); + StreamingHelpers.AddDlnaHeaders(state, _httpContextAccessor.HttpContext.Response.Headers, true, streamingRequest.StartTimeTicks, _httpContextAccessor.HttpContext.Request, _dlnaManager); using var httpClient = _httpClientFactory.CreateClient(); - return await FileStreamResponseHelpers.GetStaticRemoteStreamResult(state, isHeadRequest, controller, httpClient).ConfigureAwait(false); + return await FileStreamResponseHelpers.GetStaticRemoteStreamResult(state, isHeadRequest, httpClient, _httpContextAccessor.HttpContext).ConfigureAwait(false); } if (streamingRequest.Static && state.InputProtocol != MediaProtocol.File) { - return controller.BadRequest($"Input protocol {state.InputProtocol} cannot be streamed statically"); + return new BadRequestObjectResult($"Input protocol {state.InputProtocol} cannot be streamed statically"); } var outputPath = state.OutputFilePath; @@ -150,7 +153,7 @@ namespace Jellyfin.Api.Helpers var transcodingJob = _transcodingJobHelper.GetTranscodingJob(outputPath, TranscodingJobType.Progressive); var isTranscodeCached = outputPathExists && transcodingJob != null; - StreamingHelpers.AddDlnaHeaders(state, controller.Response.Headers, streamingRequest.Static || isTranscodeCached, streamingRequest.StartTimeTicks, controller.Request, _dlnaManager); + StreamingHelpers.AddDlnaHeaders(state, _httpContextAccessor.HttpContext.Response.Headers, streamingRequest.Static || isTranscodeCached, streamingRequest.StartTimeTicks, _httpContextAccessor.HttpContext.Request, _dlnaManager); // Static stream if (streamingRequest.Static) @@ -162,17 +165,17 @@ namespace Jellyfin.Api.Helpers await new ProgressiveFileCopier(state.MediaPath, null, _transcodingJobHelper, CancellationToken.None) { AllowEndOfFile = false - }.WriteToAsync(controller.Response.Body, CancellationToken.None) + }.WriteToAsync(_httpContextAccessor.HttpContext.Response.Body, CancellationToken.None) .ConfigureAwait(false); - return controller.File(controller.Response.Body, contentType); + return new FileStreamResult(_httpContextAccessor.HttpContext.Response.Body, contentType); } return FileStreamResponseHelpers.GetStaticFileResult( state.MediaPath, contentType, isHeadRequest, - controller); + _httpContextAccessor.HttpContext); } // Need to start ffmpeg (because media can't be returned directly) @@ -182,10 +185,9 @@ namespace Jellyfin.Api.Helpers return await FileStreamResponseHelpers.GetTranscodedFile( state, isHeadRequest, - controller, + _httpContextAccessor.HttpContext, _transcodingJobHelper, ffmpegCommandLineArguments, - controller.Request, transcodingJobType, cancellationTokenSource).ConfigureAwait(false); } diff --git a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs index 127d91430..6a8829d46 100644 --- a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs +++ b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs @@ -19,6 +19,7 @@ using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; using MediaBrowser.Model.Net; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; @@ -45,6 +46,7 @@ namespace Jellyfin.Api.Helpers private readonly TranscodingJobHelper _transcodingJobHelper; private readonly INetworkManager _networkManager; private readonly ILogger _logger; + private readonly IHttpContextAccessor _httpContextAccessor; /// /// Initializes a new instance of the class. @@ -63,6 +65,7 @@ namespace Jellyfin.Api.Helpers /// Instance of . /// Instance of the interface. /// Instance of the interface. + /// Instance of the interface. public DynamicHlsHelper( ILibraryManager libraryManager, IUserManager userManager, @@ -77,7 +80,8 @@ namespace Jellyfin.Api.Helpers IDeviceManager deviceManager, TranscodingJobHelper transcodingJobHelper, INetworkManager networkManager, - ILogger logger) + ILogger logger, + IHttpContextAccessor httpContextAccessor) { _libraryManager = libraryManager; _userManager = userManager; @@ -93,26 +97,24 @@ namespace Jellyfin.Api.Helpers _transcodingJobHelper = transcodingJobHelper; _networkManager = networkManager; _logger = logger; + _httpContextAccessor = httpContextAccessor; } /// /// Get master hls playlist. /// - /// Requesting controller. /// Transcoding job type. /// Streaming request dto. /// Enable adaptive bitrate streaming. /// A containing the resulting . public async Task GetMasterHlsPlaylist( - BaseJellyfinApiController controller, TranscodingJobType transcodingJobType, StreamingRequestDto streamingRequest, bool enableAdaptiveBitrateStreaming) { - var isHeadRequest = controller.Request.Method == WebRequestMethods.Http.Head; + var isHeadRequest = _httpContextAccessor.HttpContext.Request.Method == WebRequestMethods.Http.Head; var cancellationTokenSource = new CancellationTokenSource(); return await GetMasterPlaylistInternal( - controller, streamingRequest, isHeadRequest, enableAdaptiveBitrateStreaming, @@ -121,7 +123,6 @@ namespace Jellyfin.Api.Helpers } private async Task GetMasterPlaylistInternal( - BaseJellyfinApiController controller, StreamingRequestDto streamingRequest, bool isHeadRequest, bool enableAdaptiveBitrateStreaming, @@ -130,7 +131,7 @@ namespace Jellyfin.Api.Helpers { using var state = await StreamingHelpers.GetStreamingState( streamingRequest, - controller.Request, + _httpContextAccessor.HttpContext.Request, _authContext, _mediaSourceManager, _userManager, @@ -147,7 +148,7 @@ namespace Jellyfin.Api.Helpers cancellationTokenSource.Token) .ConfigureAwait(false); - controller.Response.Headers.Add(HeaderNames.Expires, "0"); + _httpContextAccessor.HttpContext.Response.Headers.Add(HeaderNames.Expires, "0"); if (isHeadRequest) { return new FileContentResult(Array.Empty(), MimeTypes.GetMimeType("playlist.m3u8")); @@ -161,7 +162,7 @@ namespace Jellyfin.Api.Helpers var isLiveStream = state.IsSegmentedLiveStream; - var queryString = controller.Request.QueryString.ToString(); + var queryString = _httpContextAccessor.HttpContext.Request.QueryString.ToString(); // from universal audio service if (queryString.IndexOf("SegmentContainer", StringComparison.OrdinalIgnoreCase) == -1 && !string.IsNullOrWhiteSpace(state.Request.SegmentContainer)) @@ -197,12 +198,12 @@ namespace Jellyfin.Api.Helpers if (!string.IsNullOrWhiteSpace(subtitleGroup)) { - AddSubtitles(state, subtitleStreams, builder, controller.Request.HttpContext.User); + AddSubtitles(state, subtitleStreams, builder, _httpContextAccessor.HttpContext.Request.HttpContext.User); } AppendPlaylist(builder, state, playlistUrl, totalBitrate, subtitleGroup); - if (EnableAdaptiveBitrateStreaming(state, isLiveStream, enableAdaptiveBitrateStreaming, controller.Request.HttpContext.Connection.RemoteIpAddress)) + if (EnableAdaptiveBitrateStreaming(state, isLiveStream, enableAdaptiveBitrateStreaming, _httpContextAccessor.HttpContext.Request.HttpContext.Connection.RemoteIpAddress)) { var requestedVideoBitrate = state.VideoRequest == null ? 0 : state.VideoRequest.VideoBitRate ?? 0; diff --git a/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs b/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs index a463783e0..e2f82b2de 100644 --- a/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs +++ b/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs @@ -22,14 +22,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. + /// The current http context. /// A containing the API response. public static async Task GetStaticRemoteStreamResult( StreamState state, bool isHeadRequest, - ControllerBase controller, - HttpClient httpClient) + HttpClient httpClient, + HttpContext httpContext) { if (state.RemoteHttpHeaders.TryGetValue(HeaderNames.UserAgent, out var useragent)) { @@ -39,14 +39,14 @@ namespace Jellyfin.Api.Helpers using var response = await httpClient.GetAsync(state.MediaPath).ConfigureAwait(false); var contentType = response.Content.Headers.ContentType.ToString(); - controller.Response.Headers[HeaderNames.AcceptRanges] = "none"; + httpContext.Response.Headers[HeaderNames.AcceptRanges] = "none"; if (isHeadRequest) { - return controller.File(Array.Empty(), contentType); + return new FileContentResult(Array.Empty(), contentType); } - return controller.File(await response.Content.ReadAsStreamAsync().ConfigureAwait(false), contentType); + return new FileStreamResult(await response.Content.ReadAsStreamAsync().ConfigureAwait(false), contentType); } /// @@ -55,23 +55,23 @@ namespace Jellyfin.Api.Helpers /// The path to the file. /// The content type of the file. /// Whether the current request is a HTTP HEAD request so only the headers get returned. - /// The managing the response. + /// The current http context. /// An the file. public static ActionResult GetStaticFileResult( string path, string contentType, bool isHeadRequest, - ControllerBase controller) + HttpContext httpContext) { - controller.Response.ContentType = contentType; + httpContext.Response.ContentType = contentType; // if the request is a head request, return a NoContent result with the same headers as it would with a GET request if (isHeadRequest) { - return controller.NoContent(); + return new NoContentResult(); } - return controller.PhysicalFile(path, contentType); + return new PhysicalFileResult(path, contentType); } /// @@ -79,34 +79,32 @@ 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 current http context. /// The singleton. /// The command line arguments to start ffmpeg. - /// The starting the transcoding. /// The . /// The . /// A containing the transcoded file. public static async Task GetTranscodedFile( StreamState state, bool isHeadRequest, - ControllerBase controller, + HttpContext httpContext, TranscodingJobHelper transcodingJobHelper, string ffmpegCommandLineArguments, - HttpRequest request, TranscodingJobType transcodingJobType, CancellationTokenSource cancellationTokenSource) { // Use the command line args with a dummy playlist path var outputPath = state.OutputFilePath; - controller.Response.Headers[HeaderNames.AcceptRanges] = "none"; + httpContext.Response.Headers[HeaderNames.AcceptRanges] = "none"; var contentType = state.GetMimeType(outputPath); // Headers only if (isHeadRequest) { - return controller.File(Array.Empty(), contentType); + return new FileContentResult(Array.Empty(), contentType); } var transcodingLock = transcodingJobHelper.GetTranscodingLock(outputPath); @@ -116,7 +114,7 @@ namespace Jellyfin.Api.Helpers TranscodingJobDto? job; if (!File.Exists(outputPath)) { - job = await transcodingJobHelper.StartFfMpeg(state, outputPath, ffmpegCommandLineArguments, request, transcodingJobType, cancellationTokenSource).ConfigureAwait(false); + job = await transcodingJobHelper.StartFfMpeg(state, outputPath, ffmpegCommandLineArguments, httpContext.Request, transcodingJobType, cancellationTokenSource).ConfigureAwait(false); } else { @@ -127,7 +125,7 @@ namespace Jellyfin.Api.Helpers var memoryStream = new MemoryStream(); await new ProgressiveFileCopier(outputPath, job, transcodingJobHelper, CancellationToken.None).WriteToAsync(memoryStream, CancellationToken.None).ConfigureAwait(false); memoryStream.Position = 0; - return controller.File(memoryStream, contentType); + return new FileStreamResult(memoryStream, contentType); } finally { -- cgit v1.2.3