浏览代码

convert dependent controller functions to di helper class

crobibero 4 年之前
父节点
当前提交
460c3dd351

+ 3 - 0
Emby.Server.Implementations/ApplicationHost.cs

@@ -632,6 +632,9 @@ namespace Emby.Server.Implementations
             serviceCollection.AddSingleton<IAttachmentExtractor, MediaBrowser.MediaEncoding.Attachments.AttachmentExtractor>();
             serviceCollection.AddSingleton<IAttachmentExtractor, MediaBrowser.MediaEncoding.Attachments.AttachmentExtractor>();
 
 
             serviceCollection.AddSingleton<TranscodingJobHelper>();
             serviceCollection.AddSingleton<TranscodingJobHelper>();
+            serviceCollection.AddScoped<MediaInfoHelper>();
+            serviceCollection.AddScoped<AudioHelper>();
+            serviceCollection.AddScoped<DynamicHlsHelper>();
         }
         }
 
 
         /// <summary>
         /// <summary>

+ 6 - 161
Jellyfin.Api/Controllers/AudioController.cs

@@ -1,93 +1,32 @@
 using System;
 using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
-using System.Net.Http;
-using System.Threading;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
 using Jellyfin.Api.Helpers;
 using Jellyfin.Api.Helpers;
 using Jellyfin.Api.Models.StreamingDtos;
 using Jellyfin.Api.Models.StreamingDtos;
-using MediaBrowser.Common.Configuration;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Devices;
-using MediaBrowser.Controller.Dlna;
-using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.MediaEncoding;
 using MediaBrowser.Controller.MediaEncoding;
-using MediaBrowser.Controller.Net;
 using MediaBrowser.Model.Dlna;
 using MediaBrowser.Model.Dlna;
-using MediaBrowser.Model.IO;
-using MediaBrowser.Model.MediaInfo;
-using MediaBrowser.Model.Net;
 using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Mvc;
 using Microsoft.AspNetCore.Mvc;
-using Microsoft.Extensions.Configuration;
 
 
 namespace Jellyfin.Api.Controllers
 namespace Jellyfin.Api.Controllers
 {
 {
     /// <summary>
     /// <summary>
     /// The audio controller.
     /// The audio controller.
     /// </summary>
     /// </summary>
-    // TODO: In order to autheneticate this in the future, Dlna playback will require updating
+    // TODO: In order to authenticate this in the future, Dlna playback will require updating
     public class AudioController : BaseJellyfinApiController
     public class AudioController : BaseJellyfinApiController
     {
     {
-        private readonly IDlnaManager _dlnaManager;
-        private readonly IAuthorizationContext _authContext;
-        private readonly IUserManager _userManager;
-        private readonly ILibraryManager _libraryManager;
-        private readonly IMediaSourceManager _mediaSourceManager;
-        private readonly IServerConfigurationManager _serverConfigurationManager;
-        private readonly IMediaEncoder _mediaEncoder;
-        private readonly IFileSystem _fileSystem;
-        private readonly ISubtitleEncoder _subtitleEncoder;
-        private readonly IConfiguration _configuration;
-        private readonly IDeviceManager _deviceManager;
-        private readonly TranscodingJobHelper _transcodingJobHelper;
-        private readonly IHttpClientFactory _httpClientFactory;
+        private readonly AudioHelper _audioHelper;
 
 
         private readonly TranscodingJobType _transcodingJobType = TranscodingJobType.Progressive;
         private readonly TranscodingJobType _transcodingJobType = TranscodingJobType.Progressive;
 
 
         /// <summary>
         /// <summary>
         /// Initializes a new instance of the <see cref="AudioController"/> class.
         /// Initializes a new instance of the <see cref="AudioController"/> class.
         /// </summary>
         /// </summary>
-        /// <param name="dlnaManager">Instance of the <see cref="IDlnaManager"/> interface.</param>
-        /// <param name="userManger">Instance of the <see cref="IUserManager"/> interface.</param>
-        /// <param name="authorizationContext">Instance of the <see cref="IAuthorizationContext"/> interface.</param>
-        /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
-        /// <param name="mediaSourceManager">Instance of the <see cref="IMediaSourceManager"/> interface.</param>
-        /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
-        /// <param name="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> interface.</param>
-        /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
-        /// <param name="subtitleEncoder">Instance of the <see cref="ISubtitleEncoder"/> interface.</param>
-        /// <param name="configuration">Instance of the <see cref="IConfiguration"/> interface.</param>
-        /// <param name="deviceManager">Instance of the <see cref="IDeviceManager"/> interface.</param>
-        /// <param name="transcodingJobHelper">The <see cref="TranscodingJobHelper"/> singleton.</param>
-        /// <param name="httpClientFactory">Instance of the <see cref="IHttpClientFactory"/> interface.</param>
-        public AudioController(
-            IDlnaManager dlnaManager,
-            IUserManager userManger,
-            IAuthorizationContext authorizationContext,
-            ILibraryManager libraryManager,
-            IMediaSourceManager mediaSourceManager,
-            IServerConfigurationManager serverConfigurationManager,
-            IMediaEncoder mediaEncoder,
-            IFileSystem fileSystem,
-            ISubtitleEncoder subtitleEncoder,
-            IConfiguration configuration,
-            IDeviceManager deviceManager,
-            TranscodingJobHelper transcodingJobHelper,
-            IHttpClientFactory httpClientFactory)
+        /// <param name="audioHelper">Instance of <see cref="AudioHelper"/>.</param>
+        public AudioController(AudioHelper audioHelper)
         {
         {
-            _dlnaManager = dlnaManager;
-            _authContext = authorizationContext;
-            _userManager = userManger;
-            _libraryManager = libraryManager;
-            _mediaSourceManager = mediaSourceManager;
-            _serverConfigurationManager = serverConfigurationManager;
-            _mediaEncoder = mediaEncoder;
-            _fileSystem = fileSystem;
-            _subtitleEncoder = subtitleEncoder;
-            _configuration = configuration;
-            _deviceManager = deviceManager;
-            _transcodingJobHelper = transcodingJobHelper;
-            _httpClientFactory = httpClientFactory;
+            _audioHelper = audioHelper;
         }
         }
 
 
         /// <summary>
         /// <summary>
@@ -200,10 +139,6 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] EncodingContext? context,
             [FromQuery] EncodingContext? context,
             [FromQuery] Dictionary<string, string>? streamOptions)
             [FromQuery] Dictionary<string, string>? streamOptions)
         {
         {
-            bool isHeadRequest = Request.Method == System.Net.WebRequestMethods.Http.Head;
-
-            var cancellationTokenSource = new CancellationTokenSource();
-
             StreamingRequestDto streamingRequest = new StreamingRequestDto
             StreamingRequestDto streamingRequest = new StreamingRequestDto
             {
             {
                 Id = itemId,
                 Id = itemId,
@@ -257,97 +192,7 @@ namespace Jellyfin.Api.Controllers
                 StreamOptions = streamOptions
                 StreamOptions = streamOptions
             };
             };
 
 
-            using var state = await StreamingHelpers.GetStreamingState(
-                    streamingRequest,
-                    Request,
-                    _authContext,
-                    _mediaSourceManager,
-                    _userManager,
-                    _libraryManager,
-                    _serverConfigurationManager,
-                    _mediaEncoder,
-                    _fileSystem,
-                    _subtitleEncoder,
-                    _configuration,
-                    _dlnaManager,
-                    _deviceManager,
-                    _transcodingJobHelper,
-                    _transcodingJobType,
-                    cancellationTokenSource.Token)
-                .ConfigureAwait(false);
-
-            if (@static.HasValue && @static.Value && state.DirectStreamProvider != null)
-            {
-                StreamingHelpers.AddDlnaHeaders(state, Response.Headers, true, startTimeTicks, Request, _dlnaManager);
-
-                await new ProgressiveFileCopier(state.DirectStreamProvider, null, _transcodingJobHelper, CancellationToken.None)
-                    {
-                        AllowEndOfFile = false
-                    }.WriteToAsync(Response.Body, CancellationToken.None)
-                    .ConfigureAwait(false);
-
-                // TODO (moved from MediaBrowser.Api): Don't hardcode contentType
-                return File(Response.Body, MimeTypes.GetMimeType("file.ts")!);
-            }
-
-            // Static remote stream
-            if (@static.HasValue && @static.Value && state.InputProtocol == MediaProtocol.Http)
-            {
-                StreamingHelpers.AddDlnaHeaders(state, Response.Headers, true, startTimeTicks, Request, _dlnaManager);
-
-                using var httpClient = _httpClientFactory.CreateClient();
-                return await FileStreamResponseHelpers.GetStaticRemoteStreamResult(state, isHeadRequest, this, httpClient).ConfigureAwait(false);
-            }
-
-            if (@static.HasValue && @static.Value && state.InputProtocol != MediaProtocol.File)
-            {
-                return BadRequest($"Input protocol {state.InputProtocol} cannot be streamed statically");
-            }
-
-            var outputPath = state.OutputFilePath;
-            var outputPathExists = System.IO.File.Exists(outputPath);
-
-            var transcodingJob = _transcodingJobHelper.GetTranscodingJob(outputPath, TranscodingJobType.Progressive);
-            var isTranscodeCached = outputPathExists && transcodingJob != null;
-
-            StreamingHelpers.AddDlnaHeaders(state, Response.Headers, (@static.HasValue && @static.Value) || isTranscodeCached, startTimeTicks, Request, _dlnaManager);
-
-            // Static stream
-            if (@static.HasValue && @static.Value)
-            {
-                var contentType = state.GetMimeType("." + state.OutputContainer, false) ?? state.GetMimeType(state.MediaPath);
-
-                if (state.MediaSource.IsInfiniteStream)
-                {
-                    await new ProgressiveFileCopier(state.MediaPath, null, _transcodingJobHelper, CancellationToken.None)
-                        {
-                            AllowEndOfFile = false
-                        }.WriteToAsync(Response.Body, CancellationToken.None)
-                        .ConfigureAwait(false);
-
-                    return File(Response.Body, contentType);
-                }
-
-                return FileStreamResponseHelpers.GetStaticFileResult(
-                    state.MediaPath,
-                    contentType,
-                    isHeadRequest,
-                    this);
-            }
-
-            // Need to start ffmpeg (because media can't be returned directly)
-            var encodingOptions = _serverConfigurationManager.GetEncodingOptions();
-            var encodingHelper = new EncodingHelper(_mediaEncoder, _fileSystem, _subtitleEncoder, _configuration);
-            var ffmpegCommandLineArguments = encodingHelper.GetProgressiveAudioFullCommandLine(state, encodingOptions, outputPath);
-            return await FileStreamResponseHelpers.GetTranscodedFile(
-                state,
-                isHeadRequest,
-                this,
-                _transcodingJobHelper,
-                ffmpegCommandLineArguments,
-                Request,
-                _transcodingJobType,
-                cancellationTokenSource).ConfigureAwait(false);
+            return await _audioHelper.GetAudioStream(this, _transcodingJobType, streamingRequest).ConfigureAwait(false);
         }
         }
     }
     }
 }
 }

+ 7 - 439
Jellyfin.Api/Controllers/DynamicHlsController.cs

@@ -13,7 +13,6 @@ using Jellyfin.Api.Helpers;
 using Jellyfin.Api.Models.PlaybackDtos;
 using Jellyfin.Api.Models.PlaybackDtos;
 using Jellyfin.Api.Models.StreamingDtos;
 using Jellyfin.Api.Models.StreamingDtos;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Common.Configuration;
-using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Devices;
 using MediaBrowser.Controller.Devices;
 using MediaBrowser.Controller.Dlna;
 using MediaBrowser.Controller.Dlna;
@@ -22,7 +21,6 @@ using MediaBrowser.Controller.MediaEncoding;
 using MediaBrowser.Controller.Net;
 using MediaBrowser.Controller.Net;
 using MediaBrowser.Model.Configuration;
 using MediaBrowser.Model.Configuration;
 using MediaBrowser.Model.Dlna;
 using MediaBrowser.Model.Dlna;
-using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Model.Net;
 using MediaBrowser.Model.Net;
 using Microsoft.AspNetCore.Authorization;
 using Microsoft.AspNetCore.Authorization;
@@ -53,9 +51,9 @@ namespace Jellyfin.Api.Controllers
         private readonly IConfiguration _configuration;
         private readonly IConfiguration _configuration;
         private readonly IDeviceManager _deviceManager;
         private readonly IDeviceManager _deviceManager;
         private readonly TranscodingJobHelper _transcodingJobHelper;
         private readonly TranscodingJobHelper _transcodingJobHelper;
-        private readonly INetworkManager _networkManager;
         private readonly ILogger<DynamicHlsController> _logger;
         private readonly ILogger<DynamicHlsController> _logger;
         private readonly EncodingHelper _encodingHelper;
         private readonly EncodingHelper _encodingHelper;
+        private readonly DynamicHlsHelper _dynamicHlsHelper;
 
 
         private readonly TranscodingJobType _transcodingJobType = TranscodingJobType.Hls;
         private readonly TranscodingJobType _transcodingJobType = TranscodingJobType.Hls;
 
 
@@ -74,8 +72,8 @@ namespace Jellyfin.Api.Controllers
         /// <param name="configuration">Instance of the <see cref="IConfiguration"/> interface.</param>
         /// <param name="configuration">Instance of the <see cref="IConfiguration"/> interface.</param>
         /// <param name="deviceManager">Instance of the <see cref="IDeviceManager"/> interface.</param>
         /// <param name="deviceManager">Instance of the <see cref="IDeviceManager"/> interface.</param>
         /// <param name="transcodingJobHelper">Instance of the <see cref="TranscodingJobHelper"/> class.</param>
         /// <param name="transcodingJobHelper">Instance of the <see cref="TranscodingJobHelper"/> class.</param>
-        /// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param>
         /// <param name="logger">Instance of the <see cref="ILogger{DynamicHlsController}"/> interface.</param>
         /// <param name="logger">Instance of the <see cref="ILogger{DynamicHlsController}"/> interface.</param>
+        /// <param name="dynamicHlsHelper">Instance of <see cref="DynamicHlsHelper"/>.</param>
         public DynamicHlsController(
         public DynamicHlsController(
             ILibraryManager libraryManager,
             ILibraryManager libraryManager,
             IUserManager userManager,
             IUserManager userManager,
@@ -89,8 +87,8 @@ namespace Jellyfin.Api.Controllers
             IConfiguration configuration,
             IConfiguration configuration,
             IDeviceManager deviceManager,
             IDeviceManager deviceManager,
             TranscodingJobHelper transcodingJobHelper,
             TranscodingJobHelper transcodingJobHelper,
-            INetworkManager networkManager,
-            ILogger<DynamicHlsController> logger)
+            ILogger<DynamicHlsController> logger,
+            DynamicHlsHelper dynamicHlsHelper)
         {
         {
             _libraryManager = libraryManager;
             _libraryManager = libraryManager;
             _userManager = userManager;
             _userManager = userManager;
@@ -104,8 +102,8 @@ namespace Jellyfin.Api.Controllers
             _configuration = configuration;
             _configuration = configuration;
             _deviceManager = deviceManager;
             _deviceManager = deviceManager;
             _transcodingJobHelper = transcodingJobHelper;
             _transcodingJobHelper = transcodingJobHelper;
-            _networkManager = networkManager;
             _logger = logger;
             _logger = logger;
+            _dynamicHlsHelper = dynamicHlsHelper;
 
 
             _encodingHelper = new EncodingHelper(_mediaEncoder, _fileSystem, _subtitleEncoder, _configuration);
             _encodingHelper = new EncodingHelper(_mediaEncoder, _fileSystem, _subtitleEncoder, _configuration);
         }
         }
@@ -220,8 +218,6 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] Dictionary<string, string> streamOptions,
             [FromQuery] Dictionary<string, string> streamOptions,
             [FromQuery] bool enableAdaptiveBitrateStreaming = true)
             [FromQuery] bool enableAdaptiveBitrateStreaming = true)
         {
         {
-            var isHeadRequest = Request.Method == System.Net.WebRequestMethods.Http.Head;
-            var cancellationTokenSource = new CancellationTokenSource();
             var streamingRequest = new HlsVideoRequestDto
             var streamingRequest = new HlsVideoRequestDto
             {
             {
                 Id = itemId,
                 Id = itemId,
@@ -276,8 +272,7 @@ namespace Jellyfin.Api.Controllers
                 EnableAdaptiveBitrateStreaming = enableAdaptiveBitrateStreaming
                 EnableAdaptiveBitrateStreaming = enableAdaptiveBitrateStreaming
             };
             };
 
 
-            return await GetMasterPlaylistInternal(streamingRequest, isHeadRequest, enableAdaptiveBitrateStreaming, cancellationTokenSource)
-                .ConfigureAwait(false);
+            return await _dynamicHlsHelper.GetMasterHlsPlaylist(this, _transcodingJobType, streamingRequest, enableAdaptiveBitrateStreaming).ConfigureAwait(false);
         }
         }
 
 
         /// <summary>
         /// <summary>
@@ -390,8 +385,6 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] Dictionary<string, string> streamOptions,
             [FromQuery] Dictionary<string, string> streamOptions,
             [FromQuery] bool enableAdaptiveBitrateStreaming = true)
             [FromQuery] bool enableAdaptiveBitrateStreaming = true)
         {
         {
-            var isHeadRequest = Request.Method == System.Net.WebRequestMethods.Http.Head;
-            var cancellationTokenSource = new CancellationTokenSource();
             var streamingRequest = new HlsAudioRequestDto
             var streamingRequest = new HlsAudioRequestDto
             {
             {
                 Id = itemId,
                 Id = itemId,
@@ -446,8 +439,7 @@ namespace Jellyfin.Api.Controllers
                 EnableAdaptiveBitrateStreaming = enableAdaptiveBitrateStreaming
                 EnableAdaptiveBitrateStreaming = enableAdaptiveBitrateStreaming
             };
             };
 
 
-            return await GetMasterPlaylistInternal(streamingRequest, isHeadRequest, enableAdaptiveBitrateStreaming, cancellationTokenSource)
-                .ConfigureAwait(false);
+            return await _dynamicHlsHelper.GetMasterHlsPlaylist(this, _transcodingJobType, streamingRequest, enableAdaptiveBitrateStreaming).ConfigureAwait(false);
         }
         }
 
 
         /// <summary>
         /// <summary>
@@ -1118,106 +1110,6 @@ namespace Jellyfin.Api.Controllers
                 .ConfigureAwait(false);
                 .ConfigureAwait(false);
         }
         }
 
 
-        private async Task<ActionResult> GetMasterPlaylistInternal(
-            StreamingRequestDto streamingRequest,
-            bool isHeadRequest,
-            bool enableAdaptiveBitrateStreaming,
-            CancellationTokenSource cancellationTokenSource)
-        {
-            using var state = await StreamingHelpers.GetStreamingState(
-                    streamingRequest,
-                    Request,
-                    _authContext,
-                    _mediaSourceManager,
-                    _userManager,
-                    _libraryManager,
-                    _serverConfigurationManager,
-                    _mediaEncoder,
-                    _fileSystem,
-                    _subtitleEncoder,
-                    _configuration,
-                    _dlnaManager,
-                    _deviceManager,
-                    _transcodingJobHelper,
-                    _transcodingJobType,
-                    cancellationTokenSource.Token)
-                .ConfigureAwait(false);
-
-            Response.Headers.Add(HeaderNames.Expires, "0");
-            if (isHeadRequest)
-            {
-                return new FileContentResult(Array.Empty<byte>(), 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 = 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);
-            }
-
-            AppendPlaylist(builder, state, playlistUrl, totalBitrate, subtitleGroup);
-
-            if (EnableAdaptiveBitrateStreaming(state, isLiveStream, enableAdaptiveBitrateStreaming))
-            {
-                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 async Task<ActionResult> GetVariantPlaylistInternal(StreamingRequestDto streamingRequest, string name, CancellationTokenSource cancellationTokenSource)
         private async Task<ActionResult> GetVariantPlaylistInternal(StreamingRequestDto streamingRequest, string name, CancellationTokenSource cancellationTokenSource)
         {
         {
             using var state = await StreamingHelpers.GetStreamingState(
             using var state = await StreamingHelpers.GetStreamingState(
@@ -1411,330 +1303,6 @@ namespace Jellyfin.Api.Controllers
             return await GetSegmentResult(state, playlistPath, segmentPath, segmentExtension, segmentId, job, cancellationToken).ConfigureAwait(false);
             return await GetSegmentResult(state, playlistPath, segmentPath, segmentExtension, segmentId, job, cancellationToken).ConfigureAwait(false);
         }
         }
 
 
-        private void AddSubtitles(StreamState state, IEnumerable<MediaStream> subtitles, StringBuilder builder)
-        {
-            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(Request.HttpContext.User));
-
-                var line = string.Format(
-                    CultureInfo.InvariantCulture,
-                    Format,
-                    name,
-                    isDefault ? "YES" : "NO",
-                    isForced ? "YES" : "NO",
-                    url,
-                    stream.Language ?? "Unknown");
-
-                builder.AppendLine(line);
-            }
-        }
-
-        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);
-        }
-
-        /// <summary>
-        /// Appends a CODECS field containing formatted strings of
-        /// the active streams output video and audio codecs.
-        /// </summary>
-        /// <seealso cref="AppendPlaylist(StringBuilder, StreamState, string, int, string)"/>
-        /// <seealso cref="GetPlaylistVideoCodecs(StreamState, string, int)"/>
-        /// <seealso cref="GetPlaylistAudioCodecs(StreamState)"/>
-        /// <param name="builder">StringBuilder to append the field to.</param>
-        /// <param name="state">StreamState of the current stream.</param>
-        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('"');
-            }
-        }
-
-        /// <summary>
-        /// Appends a RESOLUTION field containing the resolution of the output stream.
-        /// </summary>
-        /// <seealso cref="AppendPlaylist(StringBuilder, StreamState, string, int, string)"/>
-        /// <param name="builder">StringBuilder to append the field to.</param>
-        /// <param name="state">StreamState of the current stream.</param>
-        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());
-            }
-        }
-
-        /// <summary>
-        /// Appends a FRAME-RATE field containing the framerate of the output stream.
-        /// </summary>
-        /// <seealso cref="AppendPlaylist(StringBuilder, StreamState, string, int, string)"/>
-        /// <param name="builder">StringBuilder to append the field to.</param>
-        /// <param name="state">StreamState of the current stream.</param>
-        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)
-        {
-            // Within the local network this will likely do more harm than good.
-            var ip = RequestHelpers.NormalizeIp(Request.HttpContext.Connection.RemoteIpAddress).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;
-        }
-
-        /// <summary>
-        /// Get the H.26X level of the output video stream.
-        /// </summary>
-        /// <param name="state">StreamState of the current stream.</param>
-        /// <returns>H.26X level of the output video stream.</returns>
-        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;
-        }
-
-        /// <summary>
-        /// Gets a formatted string of the output audio codec, for use in the CODECS field.
-        /// </summary>
-        /// <seealso cref="AppendPlaylistCodecsField(StringBuilder, StreamState)"/>
-        /// <seealso cref="GetPlaylistVideoCodecs(StreamState, string, int)"/>
-        /// <param name="state">StreamState of the current stream.</param>
-        /// <returns>Formatted audio codec string.</returns>
-        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;
-        }
-
-        /// <summary>
-        /// Gets a formatted string of the output video codec, for use in the CODECS field.
-        /// </summary>
-        /// <seealso cref="AppendPlaylistCodecsField(StringBuilder, StreamState)"/>
-        /// <seealso cref="GetPlaylistAudioCodecs(StreamState)"/>
-        /// <param name="state">StreamState of the current stream.</param>
-        /// <param name="codec">Video codec.</param>
-        /// <param name="level">Video level.</param>
-        /// <returns>Formatted video codec string.</returns>
-        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);
-        }
-
         private double[] GetSegmentLengths(StreamState state)
         private double[] GetSegmentLengths(StreamState state)
         {
         {
             var result = new List<double>();
             var result = new List<double>();

+ 38 - 500
Jellyfin.Api/Controllers/MediaInfoController.cs

@@ -1,30 +1,18 @@
 using System;
 using System;
 using System.Buffers;
 using System.Buffers;
 using System.ComponentModel.DataAnnotations;
 using System.ComponentModel.DataAnnotations;
-using System.Globalization;
 using System.Linq;
 using System.Linq;
 using System.Net.Mime;
 using System.Net.Mime;
-using System.Text.Json;
-using System.Threading;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
 using Jellyfin.Api.Constants;
 using Jellyfin.Api.Constants;
+using Jellyfin.Api.Helpers;
 using Jellyfin.Api.Models.MediaInfoDtos;
 using Jellyfin.Api.Models.MediaInfoDtos;
 using Jellyfin.Api.Models.VideoDtos;
 using Jellyfin.Api.Models.VideoDtos;
-using Jellyfin.Data.Entities;
-using Jellyfin.Data.Enums;
-using MediaBrowser.Common.Net;
-using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Devices;
 using MediaBrowser.Controller.Devices;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Entities.Audio;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.MediaEncoding;
 using MediaBrowser.Controller.Net;
 using MediaBrowser.Controller.Net;
 using MediaBrowser.Model.Dlna;
 using MediaBrowser.Model.Dlna;
-using MediaBrowser.Model.Dto;
-using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.MediaInfo;
 using MediaBrowser.Model.MediaInfo;
-using MediaBrowser.Model.Session;
 using Microsoft.AspNetCore.Authorization;
 using Microsoft.AspNetCore.Authorization;
 using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Mvc;
 using Microsoft.AspNetCore.Mvc;
@@ -42,12 +30,9 @@ namespace Jellyfin.Api.Controllers
         private readonly IMediaSourceManager _mediaSourceManager;
         private readonly IMediaSourceManager _mediaSourceManager;
         private readonly IDeviceManager _deviceManager;
         private readonly IDeviceManager _deviceManager;
         private readonly ILibraryManager _libraryManager;
         private readonly ILibraryManager _libraryManager;
-        private readonly INetworkManager _networkManager;
-        private readonly IMediaEncoder _mediaEncoder;
-        private readonly IUserManager _userManager;
         private readonly IAuthorizationContext _authContext;
         private readonly IAuthorizationContext _authContext;
         private readonly ILogger<MediaInfoController> _logger;
         private readonly ILogger<MediaInfoController> _logger;
-        private readonly IServerConfigurationManager _serverConfigurationManager;
+        private readonly MediaInfoHelper _mediaInfoHelper;
 
 
         /// <summary>
         /// <summary>
         /// Initializes a new instance of the <see cref="MediaInfoController"/> class.
         /// Initializes a new instance of the <see cref="MediaInfoController"/> class.
@@ -55,32 +40,23 @@ namespace Jellyfin.Api.Controllers
         /// <param name="mediaSourceManager">Instance of the <see cref="IMediaSourceManager"/> interface.</param>
         /// <param name="mediaSourceManager">Instance of the <see cref="IMediaSourceManager"/> interface.</param>
         /// <param name="deviceManager">Instance of the <see cref="IDeviceManager"/> interface.</param>
         /// <param name="deviceManager">Instance of the <see cref="IDeviceManager"/> interface.</param>
         /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
         /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
-        /// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param>
-        /// <param name="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> interface.</param>
-        /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
         /// <param name="authContext">Instance of the <see cref="IAuthorizationContext"/> interface.</param>
         /// <param name="authContext">Instance of the <see cref="IAuthorizationContext"/> interface.</param>
         /// <param name="logger">Instance of the <see cref="ILogger{MediaInfoController}"/> interface.</param>
         /// <param name="logger">Instance of the <see cref="ILogger{MediaInfoController}"/> interface.</param>
-        /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
+        /// <param name="mediaInfoHelper">Instance of the <see cref="MediaInfoHelper"/>.</param>
         public MediaInfoController(
         public MediaInfoController(
             IMediaSourceManager mediaSourceManager,
             IMediaSourceManager mediaSourceManager,
             IDeviceManager deviceManager,
             IDeviceManager deviceManager,
             ILibraryManager libraryManager,
             ILibraryManager libraryManager,
-            INetworkManager networkManager,
-            IMediaEncoder mediaEncoder,
-            IUserManager userManager,
             IAuthorizationContext authContext,
             IAuthorizationContext authContext,
             ILogger<MediaInfoController> logger,
             ILogger<MediaInfoController> logger,
-            IServerConfigurationManager serverConfigurationManager)
+            MediaInfoHelper mediaInfoHelper)
         {
         {
             _mediaSourceManager = mediaSourceManager;
             _mediaSourceManager = mediaSourceManager;
             _deviceManager = deviceManager;
             _deviceManager = deviceManager;
             _libraryManager = libraryManager;
             _libraryManager = libraryManager;
-            _networkManager = networkManager;
-            _mediaEncoder = mediaEncoder;
-            _userManager = userManager;
             _authContext = authContext;
             _authContext = authContext;
             _logger = logger;
             _logger = logger;
-            _serverConfigurationManager = serverConfigurationManager;
+            _mediaInfoHelper = mediaInfoHelper;
         }
         }
 
 
         /// <summary>
         /// <summary>
@@ -94,7 +70,10 @@ namespace Jellyfin.Api.Controllers
         [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public async Task<ActionResult<PlaybackInfoResponse>> GetPlaybackInfo([FromRoute] Guid itemId, [FromQuery, Required] Guid? userId)
         public async Task<ActionResult<PlaybackInfoResponse>> GetPlaybackInfo([FromRoute] Guid itemId, [FromQuery, Required] Guid? userId)
         {
         {
-            return await GetPlaybackInfoInternal(itemId, userId).ConfigureAwait(false);
+            return await _mediaInfoHelper.GetPlaybackInfo(
+                    itemId,
+                    userId)
+                .ConfigureAwait(false);
         }
         }
 
 
         /// <summary>
         /// <summary>
@@ -153,7 +132,12 @@ namespace Jellyfin.Api.Controllers
                 }
                 }
             }
             }
 
 
-            var info = await GetPlaybackInfoInternal(itemId, userId, mediaSourceId, liveStreamId).ConfigureAwait(false);
+            var info = await _mediaInfoHelper.GetPlaybackInfo(
+                    itemId,
+                    userId,
+                    mediaSourceId,
+                    liveStreamId)
+                .ConfigureAwait(false);
 
 
             if (profile != null)
             if (profile != null)
             {
             {
@@ -162,7 +146,7 @@ namespace Jellyfin.Api.Controllers
 
 
                 foreach (var mediaSource in info.MediaSources)
                 foreach (var mediaSource in info.MediaSources)
                 {
                 {
-                    SetDeviceSpecificData(
+                    _mediaInfoHelper.SetDeviceSpecificData(
                         item,
                         item,
                         mediaSource,
                         mediaSource,
                         profile,
                         profile,
@@ -179,10 +163,11 @@ namespace Jellyfin.Api.Controllers
                         enableDirectStream,
                         enableDirectStream,
                         enableTranscoding,
                         enableTranscoding,
                         allowVideoStreamCopy,
                         allowVideoStreamCopy,
-                        allowAudioStreamCopy);
+                        allowAudioStreamCopy,
+                        Request.HttpContext.Connection.RemoteIpAddress.ToString());
                 }
                 }
 
 
-                SortMediaSources(info, maxStreamingBitrate);
+                _mediaInfoHelper.SortMediaSources(info, maxStreamingBitrate);
             }
             }
 
 
             if (autoOpenLiveStream)
             if (autoOpenLiveStream)
@@ -191,21 +176,23 @@ namespace Jellyfin.Api.Controllers
 
 
                 if (mediaSource != null && mediaSource.RequiresOpening && string.IsNullOrWhiteSpace(mediaSource.LiveStreamId))
                 if (mediaSource != null && mediaSource.RequiresOpening && string.IsNullOrWhiteSpace(mediaSource.LiveStreamId))
                 {
                 {
-                    var openStreamResult = await OpenMediaSource(new LiveStreamRequest
-                    {
-                        AudioStreamIndex = audioStreamIndex,
-                        DeviceProfile = deviceProfile?.DeviceProfile,
-                        EnableDirectPlay = enableDirectPlay,
-                        EnableDirectStream = enableDirectStream,
-                        ItemId = itemId,
-                        MaxAudioChannels = maxAudioChannels,
-                        MaxStreamingBitrate = maxStreamingBitrate,
-                        PlaySessionId = info.PlaySessionId,
-                        StartTimeTicks = startTimeTicks,
-                        SubtitleStreamIndex = subtitleStreamIndex,
-                        UserId = userId ?? Guid.Empty,
-                        OpenToken = mediaSource.OpenToken
-                    }).ConfigureAwait(false);
+                    var openStreamResult = await _mediaInfoHelper.OpenMediaSource(
+                        Request,
+                        new LiveStreamRequest
+                        {
+                            AudioStreamIndex = audioStreamIndex,
+                            DeviceProfile = deviceProfile?.DeviceProfile,
+                            EnableDirectPlay = enableDirectPlay,
+                            EnableDirectStream = enableDirectStream,
+                            ItemId = itemId,
+                            MaxAudioChannels = maxAudioChannels,
+                            MaxStreamingBitrate = maxStreamingBitrate,
+                            PlaySessionId = info.PlaySessionId,
+                            StartTimeTicks = startTimeTicks,
+                            SubtitleStreamIndex = subtitleStreamIndex,
+                            UserId = userId ?? Guid.Empty,
+                            OpenToken = mediaSource.OpenToken
+                        }).ConfigureAwait(false);
 
 
                     info.MediaSources = new[] { openStreamResult.MediaSource };
                     info.MediaSources = new[] { openStreamResult.MediaSource };
                 }
                 }
@@ -215,7 +202,7 @@ namespace Jellyfin.Api.Controllers
             {
             {
                 foreach (var mediaSource in info.MediaSources)
                 foreach (var mediaSource in info.MediaSources)
                 {
                 {
-                    NormalizeMediaSourceContainer(mediaSource, profile!, DlnaProfileType.Video);
+                    _mediaInfoHelper.NormalizeMediaSourceContainer(mediaSource, profile!, DlnaProfileType.Video);
                 }
                 }
             }
             }
 
 
@@ -271,7 +258,7 @@ namespace Jellyfin.Api.Controllers
                 EnableDirectStream = enableDirectStream,
                 EnableDirectStream = enableDirectStream,
                 DirectPlayProtocols = openLiveStreamDto?.DirectPlayProtocols ?? new[] { MediaProtocol.Http }
                 DirectPlayProtocols = openLiveStreamDto?.DirectPlayProtocols ?? new[] { MediaProtocol.Http }
             };
             };
-            return await OpenMediaSource(request).ConfigureAwait(false);
+            return await _mediaInfoHelper.OpenMediaSource(Request, request).ConfigureAwait(false);
         }
         }
 
 
         /// <summary>
         /// <summary>
@@ -324,454 +311,5 @@ namespace Jellyfin.Api.Controllers
                 ArrayPool<byte>.Shared.Return(buffer);
                 ArrayPool<byte>.Shared.Return(buffer);
             }
             }
         }
         }
-
-        private async Task<PlaybackInfoResponse> GetPlaybackInfoInternal(
-            Guid id,
-            Guid? userId,
-            string? mediaSourceId = null,
-            string? liveStreamId = null)
-        {
-            var user = userId.HasValue && !userId.Equals(Guid.Empty)
-                ? _userManager.GetUserById(userId.Value)
-                : null;
-            var item = _libraryManager.GetItemById(id);
-            var result = new PlaybackInfoResponse();
-
-            MediaSourceInfo[] mediaSources;
-            if (string.IsNullOrWhiteSpace(liveStreamId))
-            {
-                // TODO (moved from MediaBrowser.Api) handle supportedLiveMediaTypes?
-                var mediaSourcesList = await _mediaSourceManager.GetPlaybackMediaSources(item, user, true, true, CancellationToken.None).ConfigureAwait(false);
-
-                if (string.IsNullOrWhiteSpace(mediaSourceId))
-                {
-                    mediaSources = mediaSourcesList.ToArray();
-                }
-                else
-                {
-                    mediaSources = mediaSourcesList
-                        .Where(i => string.Equals(i.Id, mediaSourceId, StringComparison.OrdinalIgnoreCase))
-                        .ToArray();
-                }
-            }
-            else
-            {
-                var mediaSource = await _mediaSourceManager.GetLiveStream(liveStreamId, CancellationToken.None).ConfigureAwait(false);
-
-                mediaSources = new[] { mediaSource };
-            }
-
-            if (mediaSources.Length == 0)
-            {
-                result.MediaSources = Array.Empty<MediaSourceInfo>();
-
-                result.ErrorCode ??= PlaybackErrorCode.NoCompatibleStream;
-            }
-            else
-            {
-                // Since we're going to be setting properties on MediaSourceInfos that come out of _mediaSourceManager, we should clone it
-                // Should we move this directly into MediaSourceManager?
-                result.MediaSources = JsonSerializer.Deserialize<MediaSourceInfo[]>(JsonSerializer.SerializeToUtf8Bytes(mediaSources));
-
-                result.PlaySessionId = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture);
-            }
-
-            return result;
-        }
-
-        private void NormalizeMediaSourceContainer(MediaSourceInfo mediaSource, DeviceProfile profile, DlnaProfileType type)
-        {
-            mediaSource.Container = StreamBuilder.NormalizeMediaSourceFormatIntoSingleContainer(mediaSource.Container, mediaSource.Path, profile, type);
-        }
-
-        private void SetDeviceSpecificData(
-            BaseItem item,
-            MediaSourceInfo mediaSource,
-            DeviceProfile profile,
-            AuthorizationInfo auth,
-            long? maxBitrate,
-            long startTimeTicks,
-            string mediaSourceId,
-            int? audioStreamIndex,
-            int? subtitleStreamIndex,
-            int? maxAudioChannels,
-            string playSessionId,
-            Guid userId,
-            bool enableDirectPlay,
-            bool enableDirectStream,
-            bool enableTranscoding,
-            bool allowVideoStreamCopy,
-            bool allowAudioStreamCopy)
-        {
-            var streamBuilder = new StreamBuilder(_mediaEncoder, _logger);
-
-            var options = new VideoOptions
-            {
-                MediaSources = new[] { mediaSource },
-                Context = EncodingContext.Streaming,
-                DeviceId = auth.DeviceId,
-                ItemId = item.Id,
-                Profile = profile,
-                MaxAudioChannels = maxAudioChannels
-            };
-
-            if (string.Equals(mediaSourceId, mediaSource.Id, StringComparison.OrdinalIgnoreCase))
-            {
-                options.MediaSourceId = mediaSourceId;
-                options.AudioStreamIndex = audioStreamIndex;
-                options.SubtitleStreamIndex = subtitleStreamIndex;
-            }
-
-            var user = _userManager.GetUserById(userId);
-
-            if (!enableDirectPlay)
-            {
-                mediaSource.SupportsDirectPlay = false;
-            }
-
-            if (!enableDirectStream)
-            {
-                mediaSource.SupportsDirectStream = false;
-            }
-
-            if (!enableTranscoding)
-            {
-                mediaSource.SupportsTranscoding = false;
-            }
-
-            if (item is Audio)
-            {
-                _logger.LogInformation(
-                    "User policy for {0}. EnableAudioPlaybackTranscoding: {1}",
-                    user.Username,
-                    user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding));
-            }
-            else
-            {
-                _logger.LogInformation(
-                    "User policy for {0}. EnablePlaybackRemuxing: {1} EnableVideoPlaybackTranscoding: {2} EnableAudioPlaybackTranscoding: {3}",
-                    user.Username,
-                    user.HasPermission(PermissionKind.EnablePlaybackRemuxing),
-                    user.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding),
-                    user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding));
-            }
-
-            // Beginning of Playback Determination: Attempt DirectPlay first
-            if (mediaSource.SupportsDirectPlay)
-            {
-                if (mediaSource.IsRemote && user.HasPermission(PermissionKind.ForceRemoteSourceTranscoding))
-                {
-                    mediaSource.SupportsDirectPlay = false;
-                }
-                else
-                {
-                    var supportsDirectStream = mediaSource.SupportsDirectStream;
-
-                    // Dummy this up to fool StreamBuilder
-                    mediaSource.SupportsDirectStream = true;
-                    options.MaxBitrate = maxBitrate;
-
-                    if (item is Audio)
-                    {
-                        if (!user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding))
-                        {
-                            options.ForceDirectPlay = true;
-                        }
-                    }
-                    else if (item is Video)
-                    {
-                        if (!user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding)
-                            && !user.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding)
-                            && !user.HasPermission(PermissionKind.EnablePlaybackRemuxing))
-                        {
-                            options.ForceDirectPlay = true;
-                        }
-                    }
-
-                    // The MediaSource supports direct stream, now test to see if the client supports it
-                    var streamInfo = string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase)
-                        ? streamBuilder.BuildAudioItem(options)
-                        : streamBuilder.BuildVideoItem(options);
-
-                    if (streamInfo == null || !streamInfo.IsDirectStream)
-                    {
-                        mediaSource.SupportsDirectPlay = false;
-                    }
-
-                    // Set this back to what it was
-                    mediaSource.SupportsDirectStream = supportsDirectStream;
-
-                    if (streamInfo != null)
-                    {
-                        SetDeviceSpecificSubtitleInfo(streamInfo, mediaSource, auth.Token);
-                    }
-                }
-            }
-
-            if (mediaSource.SupportsDirectStream)
-            {
-                if (mediaSource.IsRemote && user.HasPermission(PermissionKind.ForceRemoteSourceTranscoding))
-                {
-                    mediaSource.SupportsDirectStream = false;
-                }
-                else
-                {
-                    options.MaxBitrate = GetMaxBitrate(maxBitrate, user);
-
-                    if (item is Audio)
-                    {
-                        if (!user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding))
-                        {
-                            options.ForceDirectStream = true;
-                        }
-                    }
-                    else if (item is Video)
-                    {
-                        if (!user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding)
-                            && !user.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding)
-                            && !user.HasPermission(PermissionKind.EnablePlaybackRemuxing))
-                        {
-                            options.ForceDirectStream = true;
-                        }
-                    }
-
-                    // The MediaSource supports direct stream, now test to see if the client supports it
-                    var streamInfo = string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase)
-                        ? streamBuilder.BuildAudioItem(options)
-                        : streamBuilder.BuildVideoItem(options);
-
-                    if (streamInfo == null || !streamInfo.IsDirectStream)
-                    {
-                        mediaSource.SupportsDirectStream = false;
-                    }
-
-                    if (streamInfo != null)
-                    {
-                        SetDeviceSpecificSubtitleInfo(streamInfo, mediaSource, auth.Token);
-                    }
-                }
-            }
-
-            if (mediaSource.SupportsTranscoding)
-            {
-                options.MaxBitrate = GetMaxBitrate(maxBitrate, user);
-
-                // The MediaSource supports direct stream, now test to see if the client supports it
-                var streamInfo = string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase)
-                    ? streamBuilder.BuildAudioItem(options)
-                    : streamBuilder.BuildVideoItem(options);
-
-                if (mediaSource.IsRemote && user.HasPermission(PermissionKind.ForceRemoteSourceTranscoding))
-                {
-                    if (streamInfo != null)
-                    {
-                        streamInfo.PlaySessionId = playSessionId;
-                        streamInfo.StartPositionTicks = startTimeTicks;
-                        mediaSource.TranscodingUrl = streamInfo.ToUrl("-", auth.Token).TrimStart('-');
-                        mediaSource.TranscodingUrl += "&allowVideoStreamCopy=false";
-                        mediaSource.TranscodingUrl += "&allowAudioStreamCopy=false";
-                        mediaSource.TranscodingContainer = streamInfo.Container;
-                        mediaSource.TranscodingSubProtocol = streamInfo.SubProtocol;
-
-                        // Do this after the above so that StartPositionTicks is set
-                        SetDeviceSpecificSubtitleInfo(streamInfo, mediaSource, auth.Token);
-                    }
-                }
-                else
-                {
-                    if (streamInfo != null)
-                    {
-                        streamInfo.PlaySessionId = playSessionId;
-
-                        if (streamInfo.PlayMethod == PlayMethod.Transcode)
-                        {
-                            streamInfo.StartPositionTicks = startTimeTicks;
-                            mediaSource.TranscodingUrl = streamInfo.ToUrl("-", auth.Token).TrimStart('-');
-
-                            if (!allowVideoStreamCopy)
-                            {
-                                mediaSource.TranscodingUrl += "&allowVideoStreamCopy=false";
-                            }
-
-                            if (!allowAudioStreamCopy)
-                            {
-                                mediaSource.TranscodingUrl += "&allowAudioStreamCopy=false";
-                            }
-
-                            mediaSource.TranscodingContainer = streamInfo.Container;
-                            mediaSource.TranscodingSubProtocol = streamInfo.SubProtocol;
-                        }
-
-                        if (!allowAudioStreamCopy)
-                        {
-                            mediaSource.TranscodingUrl += "&allowAudioStreamCopy=false";
-                        }
-
-                        mediaSource.TranscodingContainer = streamInfo.Container;
-                        mediaSource.TranscodingSubProtocol = streamInfo.SubProtocol;
-
-                        // Do this after the above so that StartPositionTicks is set
-                        SetDeviceSpecificSubtitleInfo(streamInfo, mediaSource, auth.Token);
-                    }
-                }
-            }
-
-            foreach (var attachment in mediaSource.MediaAttachments)
-            {
-                attachment.DeliveryUrl = string.Format(
-                    CultureInfo.InvariantCulture,
-                    "/Videos/{0}/{1}/Attachments/{2}",
-                    item.Id,
-                    mediaSource.Id,
-                    attachment.Index);
-            }
-        }
-
-        private async Task<LiveStreamResponse> OpenMediaSource(LiveStreamRequest request)
-        {
-            var authInfo = _authContext.GetAuthorizationInfo(Request);
-
-            var result = await _mediaSourceManager.OpenLiveStream(request, CancellationToken.None).ConfigureAwait(false);
-
-            var profile = request.DeviceProfile;
-            if (profile == null)
-            {
-                var caps = _deviceManager.GetCapabilities(authInfo.DeviceId);
-                if (caps != null)
-                {
-                    profile = caps.DeviceProfile;
-                }
-            }
-
-            if (profile != null)
-            {
-                var item = _libraryManager.GetItemById(request.ItemId);
-
-                SetDeviceSpecificData(
-                    item,
-                    result.MediaSource,
-                    profile,
-                    authInfo,
-                    request.MaxStreamingBitrate,
-                    request.StartTimeTicks ?? 0,
-                    result.MediaSource.Id,
-                    request.AudioStreamIndex,
-                    request.SubtitleStreamIndex,
-                    request.MaxAudioChannels,
-                    request.PlaySessionId,
-                    request.UserId,
-                    request.EnableDirectPlay,
-                    request.EnableDirectStream,
-                    true,
-                    true,
-                    true);
-            }
-            else
-            {
-                if (!string.IsNullOrWhiteSpace(result.MediaSource.TranscodingUrl))
-                {
-                    result.MediaSource.TranscodingUrl += "&LiveStreamId=" + result.MediaSource.LiveStreamId;
-                }
-            }
-
-            // here was a check if (result.MediaSource != null) but Rider said it will never be null
-            NormalizeMediaSourceContainer(result.MediaSource, profile!, DlnaProfileType.Video);
-
-            return result;
-        }
-
-        private void SetDeviceSpecificSubtitleInfo(StreamInfo info, MediaSourceInfo mediaSource, string accessToken)
-        {
-            var profiles = info.GetSubtitleProfiles(_mediaEncoder, false, "-", accessToken);
-            mediaSource.DefaultSubtitleStreamIndex = info.SubtitleStreamIndex;
-
-            mediaSource.TranscodeReasons = info.TranscodeReasons;
-
-            foreach (var profile in profiles)
-            {
-                foreach (var stream in mediaSource.MediaStreams)
-                {
-                    if (stream.Type == MediaStreamType.Subtitle && stream.Index == profile.Index)
-                    {
-                        stream.DeliveryMethod = profile.DeliveryMethod;
-
-                        if (profile.DeliveryMethod == SubtitleDeliveryMethod.External)
-                        {
-                            stream.DeliveryUrl = profile.Url.TrimStart('-');
-                            stream.IsExternalUrl = profile.IsExternalUrl;
-                        }
-                    }
-                }
-            }
-        }
-
-        private long? GetMaxBitrate(long? clientMaxBitrate, User user)
-        {
-            var maxBitrate = clientMaxBitrate;
-            var remoteClientMaxBitrate = user?.RemoteClientBitrateLimit ?? 0;
-
-            if (remoteClientMaxBitrate <= 0)
-            {
-                remoteClientMaxBitrate = _serverConfigurationManager.Configuration.RemoteClientBitrateLimit;
-            }
-
-            if (remoteClientMaxBitrate > 0)
-            {
-                var isInLocalNetwork = _networkManager.IsInLocalNetwork(Request.HttpContext.Connection.RemoteIpAddress.ToString());
-
-                _logger.LogInformation("RemoteClientBitrateLimit: {0}, RemoteIp: {1}, IsInLocalNetwork: {2}", remoteClientMaxBitrate, Request.HttpContext.Connection.RemoteIpAddress.ToString(), isInLocalNetwork);
-                if (!isInLocalNetwork)
-                {
-                    maxBitrate = Math.Min(maxBitrate ?? remoteClientMaxBitrate, remoteClientMaxBitrate);
-                }
-            }
-
-            return maxBitrate;
-        }
-
-        private void SortMediaSources(PlaybackInfoResponse result, long? maxBitrate)
-        {
-            var originalList = result.MediaSources.ToList();
-
-            result.MediaSources = result.MediaSources.OrderBy(i =>
-                {
-                    // Nothing beats direct playing a file
-                    if (i.SupportsDirectPlay && i.Protocol == MediaProtocol.File)
-                    {
-                        return 0;
-                    }
-
-                    return 1;
-                })
-                .ThenBy(i =>
-                {
-                    // Let's assume direct streaming a file is just as desirable as direct playing a remote url
-                    if (i.SupportsDirectPlay || i.SupportsDirectStream)
-                    {
-                        return 0;
-                    }
-
-                    return 1;
-                })
-                .ThenBy(i =>
-                {
-                    return i.Protocol switch
-                    {
-                        MediaProtocol.File => 0,
-                        _ => 1,
-                    };
-                })
-                .ThenBy(i =>
-                {
-                    if (maxBitrate.HasValue && i.Bitrate.HasValue)
-                    {
-                        return i.Bitrate.Value <= maxBitrate.Value ? 0 : 2;
-                    }
-
-                    return 1;
-                })
-                .ThenBy(originalList.IndexOf)
-                .ToArray();
-        }
     }
     }
 }
 }

+ 141 - 139
Jellyfin.Api/Controllers/UniversalAudioController.cs

@@ -2,17 +2,20 @@
 using System.Collections.Generic;
 using System.Collections.Generic;
 using System.Globalization;
 using System.Globalization;
 using System.Linq;
 using System.Linq;
-using System.Net.Http;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
 using Jellyfin.Api.Constants;
 using Jellyfin.Api.Constants;
 using Jellyfin.Api.Helpers;
 using Jellyfin.Api.Helpers;
-using Jellyfin.Api.Models.VideoDtos;
+using Jellyfin.Api.Models.StreamingDtos;
+using MediaBrowser.Controller.Devices;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.MediaEncoding;
 using MediaBrowser.Controller.Net;
 using MediaBrowser.Controller.Net;
 using MediaBrowser.Model.Dlna;
 using MediaBrowser.Model.Dlna;
 using MediaBrowser.Model.MediaInfo;
 using MediaBrowser.Model.MediaInfo;
 using Microsoft.AspNetCore.Authorization;
 using Microsoft.AspNetCore.Authorization;
 using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Mvc;
 using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.Logging;
 
 
 namespace Jellyfin.Api.Controllers
 namespace Jellyfin.Api.Controllers
 {
 {
@@ -23,27 +26,39 @@ namespace Jellyfin.Api.Controllers
     public class UniversalAudioController : BaseJellyfinApiController
     public class UniversalAudioController : BaseJellyfinApiController
     {
     {
         private readonly IAuthorizationContext _authorizationContext;
         private readonly IAuthorizationContext _authorizationContext;
-        private readonly MediaInfoController _mediaInfoController;
-        private readonly DynamicHlsController _dynamicHlsController;
-        private readonly AudioController _audioController;
+        private readonly IDeviceManager _deviceManager;
+        private readonly ILibraryManager _libraryManager;
+        private readonly ILogger<UniversalAudioController> _logger;
+        private readonly MediaInfoHelper _mediaInfoHelper;
+        private readonly AudioHelper _audioHelper;
+        private readonly DynamicHlsHelper _dynamicHlsHelper;
 
 
         /// <summary>
         /// <summary>
         /// Initializes a new instance of the <see cref="UniversalAudioController"/> class.
         /// Initializes a new instance of the <see cref="UniversalAudioController"/> class.
         /// </summary>
         /// </summary>
         /// <param name="authorizationContext">Instance of the <see cref="IAuthorizationContext"/> interface.</param>
         /// <param name="authorizationContext">Instance of the <see cref="IAuthorizationContext"/> interface.</param>
-        /// <param name="mediaInfoController">Instance of the <see cref="MediaInfoController"/>.</param>
-        /// <param name="dynamicHlsController">Instance of the <see cref="DynamicHlsController"/>.</param>
-        /// <param name="audioController">Instance of the <see cref="AudioController"/>.</param>
+        /// <param name="deviceManager">Instance of the <see cref="IDeviceManager"/> interface.</param>
+        /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
+        /// <param name="logger">Instance of the <see cref="ILogger{UniversalAudioController}"/> interface.</param>
+        /// <param name="mediaInfoHelper">Instance of <see cref="MediaInfoHelper"/>.</param>
+        /// <param name="audioHelper">Instance of <see cref="AudioHelper"/>.</param>
+        /// <param name="dynamicHlsHelper">Instance of <see cref="DynamicHlsHelper"/>.</param>
         public UniversalAudioController(
         public UniversalAudioController(
             IAuthorizationContext authorizationContext,
             IAuthorizationContext authorizationContext,
-            MediaInfoController mediaInfoController,
-            DynamicHlsController dynamicHlsController,
-            AudioController audioController)
+            IDeviceManager deviceManager,
+            ILibraryManager libraryManager,
+            ILogger<UniversalAudioController> logger,
+            MediaInfoHelper mediaInfoHelper,
+            AudioHelper audioHelper,
+            DynamicHlsHelper dynamicHlsHelper)
         {
         {
             _authorizationContext = authorizationContext;
             _authorizationContext = authorizationContext;
-            _mediaInfoController = mediaInfoController;
-            _dynamicHlsController = dynamicHlsController;
-            _audioController = audioController;
+            _deviceManager = deviceManager;
+            _libraryManager = libraryManager;
+            _logger = logger;
+            _mediaInfoHelper = mediaInfoHelper;
+            _audioHelper = audioHelper;
+            _dynamicHlsHelper = dynamicHlsHelper;
         }
         }
 
 
         /// <summary>
         /// <summary>
@@ -99,20 +114,65 @@ namespace Jellyfin.Api.Controllers
             var deviceProfile = GetDeviceProfile(container, transcodingContainer, audioCodec, transcodingProtocol, breakOnNonKeyFrames, transcodingAudioChannels, maxAudioSampleRate, maxAudioBitDepth, maxAudioChannels);
             var deviceProfile = GetDeviceProfile(container, transcodingContainer, audioCodec, transcodingProtocol, breakOnNonKeyFrames, transcodingAudioChannels, maxAudioSampleRate, maxAudioBitDepth, maxAudioChannels);
             _authorizationContext.GetAuthorizationInfo(Request).DeviceId = deviceId;
             _authorizationContext.GetAuthorizationInfo(Request).DeviceId = deviceId;
 
 
-            var playbackInfoResult = await _mediaInfoController.GetPostedPlaybackInfo(
-                itemId,
-                userId,
-                maxStreamingBitrate,
-                startTimeTicks,
-                null,
-                null,
-                maxAudioChannels,
-                mediaSourceId,
-                null,
-                new DeviceProfileDto { DeviceProfile = deviceProfile })
+            var authInfo = _authorizationContext.GetAuthorizationInfo(Request);
+
+            _logger.LogInformation("GetPostedPlaybackInfo profile: {@Profile}", deviceProfile);
+
+            if (deviceProfile == null)
+            {
+                var caps = _deviceManager.GetCapabilities(authInfo.DeviceId);
+                if (caps != null)
+                {
+                    deviceProfile = caps.DeviceProfile;
+                }
+            }
+
+            var info = await _mediaInfoHelper.GetPlaybackInfo(
+                    itemId,
+                    userId,
+                    mediaSourceId)
                 .ConfigureAwait(false);
                 .ConfigureAwait(false);
-            var mediaSource = playbackInfoResult.Value.MediaSources[0];
 
 
+            if (deviceProfile != null)
+            {
+                // set device specific data
+                var item = _libraryManager.GetItemById(itemId);
+
+                foreach (var sourceInfo in info.MediaSources)
+                {
+                    _mediaInfoHelper.SetDeviceSpecificData(
+                        item,
+                        sourceInfo,
+                        deviceProfile,
+                        authInfo,
+                        maxStreamingBitrate ?? deviceProfile.MaxStreamingBitrate,
+                        startTimeTicks ?? 0,
+                        mediaSourceId ?? string.Empty,
+                        null,
+                        null,
+                        maxAudioChannels,
+                        info!.PlaySessionId!,
+                        userId ?? Guid.Empty,
+                        true,
+                        true,
+                        true,
+                        true,
+                        true,
+                        Request.HttpContext.Connection.RemoteIpAddress.ToString());
+                }
+
+                _mediaInfoHelper.SortMediaSources(info, maxStreamingBitrate);
+            }
+
+            if (info.MediaSources != null)
+            {
+                foreach (var source in info.MediaSources)
+                {
+                    _mediaInfoHelper.NormalizeMediaSourceContainer(source, deviceProfile!, DlnaProfileType.Video);
+                }
+            }
+
+            var mediaSource = info.MediaSources![0];
             if (mediaSource.SupportsDirectPlay && mediaSource.Protocol == MediaProtocol.Http)
             if (mediaSource.SupportsDirectPlay && mediaSource.Protocol == MediaProtocol.Http)
             {
             {
                 if (enableRedirection)
                 if (enableRedirection)
@@ -127,129 +187,71 @@ namespace Jellyfin.Api.Controllers
             var isStatic = mediaSource.SupportsDirectStream;
             var isStatic = mediaSource.SupportsDirectStream;
             if (!isStatic && string.Equals(mediaSource.TranscodingSubProtocol, "hls", StringComparison.OrdinalIgnoreCase))
             if (!isStatic && string.Equals(mediaSource.TranscodingSubProtocol, "hls", StringComparison.OrdinalIgnoreCase))
             {
             {
-                var transcodingProfile = deviceProfile.TranscodingProfiles[0];
-
                 // hls segment container can only be mpegts or fmp4 per ffmpeg documentation
                 // hls segment container can only be mpegts or fmp4 per ffmpeg documentation
                 // TODO: remove this when we switch back to the segment muxer
                 // TODO: remove this when we switch back to the segment muxer
                 var supportedHlsContainers = new[] { "mpegts", "fmp4" };
                 var supportedHlsContainers = new[] { "mpegts", "fmp4" };
 
 
-                if (isHeadRequest)
+                var dynamicHlsRequestDto = new HlsAudioRequestDto
                 {
                 {
-                    _dynamicHlsController.Request.Method = HttpMethod.Head.Method;
-                }
-
-                return await _dynamicHlsController.GetMasterHlsAudioPlaylist(
-                    itemId,
-                    ".m3u8",
-                    isStatic,
-                    null,
-                    null,
-                    null,
-                    playbackInfoResult.Value.PlaySessionId,
+                    Id = itemId,
+                    Container = ".m3u8",
+                    Static = isStatic,
+                    PlaySessionId = info.PlaySessionId,
                     // fallback to mpegts if device reports some weird value unsupported by hls
                     // fallback to mpegts if device reports some weird value unsupported by hls
-                    Array.Exists(supportedHlsContainers, element => element == transcodingContainer) ? transcodingContainer : "mpegts",
-                    null,
-                    null,
-                    mediaSource.Id,
-                    deviceId,
-                    transcodingProfile.AudioCodec,
-                    null,
-                    null,
-                    null,
-                    transcodingProfile.BreakOnNonKeyFrames,
-                    maxAudioSampleRate,
-                    maxAudioBitDepth,
-                    null,
-                    isStatic ? (int?)null : Convert.ToInt32(Math.Min(maxStreamingBitrate ?? 192000, int.MaxValue)),
-                    maxAudioChannels,
-                    null,
-                    null,
-                    null,
-                    null,
-                    null,
-                    startTimeTicks,
-                    null,
-                    null,
-                    null,
-                    null,
-                    SubtitleDeliveryMethod.Hls,
-                    null,
-                    null,
-                    null,
-                    null,
-                    null,
-                    null,
-                    null,
-                    null,
-                    null,
-                    null,
-                    null,
-                    mediaSource.TranscodeReasons == null ? null : string.Join(",", mediaSource.TranscodeReasons.Select(i => i.ToString()).ToArray()),
-                    null,
-                    null,
-                    EncodingContext.Static,
-                    new Dictionary<string, string>())
+                    SegmentContainer = Array.Exists(supportedHlsContainers, element => element == transcodingContainer) ? transcodingContainer : "mpegts",
+                    MediaSourceId = mediaSourceId,
+                    DeviceId = deviceId,
+                    AudioCodec = audioCodec,
+                    EnableAutoStreamCopy = true,
+                    AllowAudioStreamCopy = true,
+                    AllowVideoStreamCopy = true,
+                    BreakOnNonKeyFrames = breakOnNonKeyFrames,
+                    AudioSampleRate = maxAudioSampleRate,
+                    MaxAudioChannels = maxAudioChannels,
+                    MaxAudioBitDepth = maxAudioBitDepth,
+                    AudioChannels = isStatic ? (int?)null : Convert.ToInt32(Math.Min(maxStreamingBitrate ?? 192000, int.MaxValue)),
+                    StartTimeTicks = startTimeTicks,
+                    SubtitleMethod = SubtitleDeliveryMethod.Hls,
+                    RequireAvc = true,
+                    DeInterlace = true,
+                    RequireNonAnamorphic = true,
+                    EnableMpegtsM2TsMode = true,
+                    TranscodeReasons = mediaSource.TranscodeReasons == null ? null : string.Join(",", mediaSource.TranscodeReasons.Select(i => i.ToString()).ToArray()),
+                    Context = EncodingContext.Static,
+                    StreamOptions = new Dictionary<string, string>(),
+                    EnableAdaptiveBitrateStreaming = true
+                };
+
+                return await _dynamicHlsHelper.GetMasterHlsPlaylist(this, TranscodingJobType.Hls, dynamicHlsRequestDto, true)
                     .ConfigureAwait(false);
                     .ConfigureAwait(false);
             }
             }
-            else
+
+            var audioStreamingDto = new StreamingRequestDto
             {
             {
-                if (isHeadRequest)
-                {
-                    _audioController.Request.Method = HttpMethod.Head.Method;
-                }
+                Id = itemId,
+                Container = isStatic ? null : ("." + mediaSource.TranscodingContainer),
+                Static = isStatic,
+                PlaySessionId = info.PlaySessionId,
+                MediaSourceId = mediaSourceId,
+                DeviceId = deviceId,
+                AudioCodec = audioCodec,
+                EnableAutoStreamCopy = true,
+                AllowAudioStreamCopy = true,
+                AllowVideoStreamCopy = true,
+                BreakOnNonKeyFrames = breakOnNonKeyFrames,
+                AudioSampleRate = maxAudioSampleRate,
+                MaxAudioChannels = maxAudioChannels,
+                AudioBitRate = isStatic ? (int?)null : Convert.ToInt32(Math.Min(maxStreamingBitrate ?? 192000, int.MaxValue)),
+                MaxAudioBitDepth = maxAudioBitDepth,
+                AudioChannels = maxAudioChannels,
+                CopyTimestamps = true,
+                StartTimeTicks = startTimeTicks,
+                SubtitleMethod = SubtitleDeliveryMethod.Embed,
+                TranscodeReasons = mediaSource.TranscodeReasons == null ? null : string.Join(",", mediaSource.TranscodeReasons.Select(i => i.ToString()).ToArray()),
+                Context = EncodingContext.Static
+            };
 
 
-                return await _audioController.GetAudioStream(
-                    itemId,
-                    isStatic ? null : ("." + mediaSource.TranscodingContainer),
-                    isStatic,
-                    null,
-                    null,
-                    null,
-                    playbackInfoResult.Value.PlaySessionId,
-                    null,
-                    null,
-                    null,
-                    mediaSource.Id,
-                    deviceId,
-                    audioCodec,
-                    null,
-                    null,
-                    null,
-                    breakOnNonKeyFrames,
-                    maxAudioSampleRate,
-                    maxAudioBitDepth,
-                    isStatic ? (int?)null : Convert.ToInt32(Math.Min(maxStreamingBitrate ?? 192000, int.MaxValue)),
-                    null,
-                    maxAudioChannels,
-                    null,
-                    null,
-                    null,
-                    null,
-                    null,
-                    startTimeTicks,
-                    null,
-                    null,
-                    null,
-                    null,
-                    SubtitleDeliveryMethod.Embed,
-                    null,
-                    null,
-                    null,
-                    null,
-                    null,
-                    null,
-                    null,
-                    null,
-                    null,
-                    null,
-                    null,
-                    mediaSource.TranscodeReasons == null ? null : string.Join(",", mediaSource.TranscodeReasons.Select(i => i.ToString()).ToArray()),
-                    null,
-                    null,
-                    null,
-                    null)
-                    .ConfigureAwait(false);
-            }
+            return await _audioHelper.GetAudioStream(this, TranscodingJobType.Progressive, audioStreamingDto).ConfigureAwait(false);
         }
         }
 
 
         private DeviceProfile GetDeviceProfile(
         private DeviceProfile GetDeviceProfile(

+ 193 - 0
Jellyfin.Api/Helpers/AudioHelper.cs

@@ -0,0 +1,193 @@
+using System.Net.Http;
+using System.Threading;
+using System.Threading.Tasks;
+using Jellyfin.Api.Models.StreamingDtos;
+using MediaBrowser.Common.Configuration;
+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.IO;
+using MediaBrowser.Model.MediaInfo;
+using MediaBrowser.Model.Net;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.Configuration;
+
+namespace Jellyfin.Api.Helpers
+{
+    /// <summary>
+    /// Audio helper.
+    /// </summary>
+    public class AudioHelper
+    {
+        private readonly IDlnaManager _dlnaManager;
+        private readonly IAuthorizationContext _authContext;
+        private readonly IUserManager _userManager;
+        private readonly ILibraryManager _libraryManager;
+        private readonly IMediaSourceManager _mediaSourceManager;
+        private readonly IServerConfigurationManager _serverConfigurationManager;
+        private readonly IMediaEncoder _mediaEncoder;
+        private readonly IFileSystem _fileSystem;
+        private readonly ISubtitleEncoder _subtitleEncoder;
+        private readonly IConfiguration _configuration;
+        private readonly IDeviceManager _deviceManager;
+        private readonly TranscodingJobHelper _transcodingJobHelper;
+        private readonly IHttpClientFactory _httpClientFactory;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="AudioHelper"/> class.
+        /// </summary>
+        /// <param name="dlnaManager">Instance of the <see cref="IDlnaManager"/> interface.</param>
+        /// <param name="authContext">Instance of the <see cref="IAuthorizationContext"/> interface.</param>
+        /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
+        /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
+        /// <param name="mediaSourceManager">Instance of the <see cref="IMediaSourceManager"/> interface.</param>
+        /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
+        /// <param name="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> interface.</param>
+        /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
+        /// <param name="subtitleEncoder">Instance of the <see cref="ISubtitleEncoder"/> interface.</param>
+        /// <param name="configuration">Instance of the <see cref="IConfiguration"/> interface.</param>
+        /// <param name="deviceManager">Instance of the <see cref="IDeviceManager"/> interface.</param>
+        /// <param name="transcodingJobHelper">Instance of <see cref="TranscodingJobHelper"/>.</param>
+        /// <param name="httpClientFactory">Instance of the <see cref="IHttpClientFactory"/> interface.</param>
+        public AudioHelper(
+            IDlnaManager dlnaManager,
+            IAuthorizationContext authContext,
+            IUserManager userManager,
+            ILibraryManager libraryManager,
+            IMediaSourceManager mediaSourceManager,
+            IServerConfigurationManager serverConfigurationManager,
+            IMediaEncoder mediaEncoder,
+            IFileSystem fileSystem,
+            ISubtitleEncoder subtitleEncoder,
+            IConfiguration configuration,
+            IDeviceManager deviceManager,
+            TranscodingJobHelper transcodingJobHelper,
+            IHttpClientFactory httpClientFactory)
+        {
+            _dlnaManager = dlnaManager;
+            _authContext = authContext;
+            _userManager = userManager;
+            _libraryManager = libraryManager;
+            _mediaSourceManager = mediaSourceManager;
+            _serverConfigurationManager = serverConfigurationManager;
+            _mediaEncoder = mediaEncoder;
+            _fileSystem = fileSystem;
+            _subtitleEncoder = subtitleEncoder;
+            _configuration = configuration;
+            _deviceManager = deviceManager;
+            _transcodingJobHelper = transcodingJobHelper;
+            _httpClientFactory = httpClientFactory;
+        }
+
+        /// <summary>
+        /// Get audio stream.
+        /// </summary>
+        /// <param name="controller">Requesting controller.</param>
+        /// <param name="transcodingJobType">Transcoding job type.</param>
+        /// <param name="streamingRequest">Streaming controller.Request dto.</param>
+        /// <returns>A <see cref="Task"/> containing the resulting <see cref="ActionResult"/>.</returns>
+        public async Task<ActionResult> GetAudioStream(
+            BaseJellyfinApiController controller,
+            TranscodingJobType transcodingJobType,
+            StreamingRequestDto streamingRequest)
+        {
+            bool isHeadRequest = controller.Request.Method == System.Net.WebRequestMethods.Http.Head;
+            var cancellationTokenSource = new 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);
+
+            if (streamingRequest.Static && state.DirectStreamProvider != null)
+            {
+                StreamingHelpers.AddDlnaHeaders(state, controller.Response.Headers, true, streamingRequest.StartTimeTicks, controller.Request, _dlnaManager);
+
+                await new ProgressiveFileCopier(state.DirectStreamProvider, null, _transcodingJobHelper, CancellationToken.None)
+                    {
+                        AllowEndOfFile = false
+                    }.WriteToAsync(controller.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")!);
+            }
+
+            // Static remote stream
+            if (streamingRequest.Static && state.InputProtocol == MediaProtocol.Http)
+            {
+                StreamingHelpers.AddDlnaHeaders(state, controller.Response.Headers, true, streamingRequest.StartTimeTicks, controller.Request, _dlnaManager);
+
+                using var httpClient = _httpClientFactory.CreateClient();
+                return await FileStreamResponseHelpers.GetStaticRemoteStreamResult(state, isHeadRequest, controller, httpClient).ConfigureAwait(false);
+            }
+
+            if (streamingRequest.Static && state.InputProtocol != MediaProtocol.File)
+            {
+                return controller.BadRequest($"Input protocol {state.InputProtocol} cannot be streamed statically");
+            }
+
+            var outputPath = state.OutputFilePath;
+            var outputPathExists = System.IO.File.Exists(outputPath);
+
+            var transcodingJob = _transcodingJobHelper.GetTranscodingJob(outputPath, TranscodingJobType.Progressive);
+            var isTranscodeCached = outputPathExists && transcodingJob != null;
+
+            StreamingHelpers.AddDlnaHeaders(state, controller.Response.Headers, streamingRequest.Static || isTranscodeCached, streamingRequest.StartTimeTicks, controller.Request, _dlnaManager);
+
+            // Static stream
+            if (streamingRequest.Static)
+            {
+                var contentType = state.GetMimeType("." + state.OutputContainer, false) ?? state.GetMimeType(state.MediaPath);
+
+                if (state.MediaSource.IsInfiniteStream)
+                {
+                    await new ProgressiveFileCopier(state.MediaPath, null, _transcodingJobHelper, CancellationToken.None)
+                        {
+                            AllowEndOfFile = false
+                        }.WriteToAsync(controller.Response.Body, CancellationToken.None)
+                        .ConfigureAwait(false);
+
+                    return controller.File(controller.Response.Body, contentType);
+                }
+
+                return FileStreamResponseHelpers.GetStaticFileResult(
+                    state.MediaPath,
+                    contentType,
+                    isHeadRequest,
+                    controller);
+            }
+
+            // 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,
+                controller,
+                _transcodingJobHelper,
+                ffmpegCommandLineArguments,
+                controller.Request,
+                transcodingJobType,
+                cancellationTokenSource).ConfigureAwait(false);
+        }
+    }
+}

+ 549 - 0
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
+{
+    /// <summary>
+    /// Dynamic hls helper.
+    /// </summary>
+    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<DynamicHlsHelper> _logger;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="DynamicHlsHelper"/> class.
+        /// </summary>
+        /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
+        /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
+        /// <param name="dlnaManager">Instance of the <see cref="IDlnaManager"/> interface.</param>
+        /// <param name="authContext">Instance of the <see cref="IAuthorizationContext"/> interface.</param>
+        /// <param name="mediaSourceManager">Instance of the <see cref="IMediaSourceManager"/> interface.</param>
+        /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
+        /// <param name="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> interface.</param>
+        /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
+        /// <param name="subtitleEncoder">Instance of the <see cref="ISubtitleEncoder"/> interface.</param>
+        /// <param name="configuration">Instance of the <see cref="IConfiguration"/> interface.</param>
+        /// <param name="deviceManager">Instance of the <see cref="IDeviceManager"/> interface.</param>
+        /// <param name="transcodingJobHelper">Instance of <see cref="TranscodingJobHelper"/>.</param>
+        /// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param>
+        /// <param name="logger">Instance of the <see cref="ILogger{DynamicHlsHelper}"/> interface.</param>
+        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<DynamicHlsHelper> 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;
+        }
+
+        /// <summary>
+        /// Get master hls playlist.
+        /// </summary>
+        /// <param name="controller">Requesting controller.</param>
+        /// <param name="transcodingJobType">Transcoding job type.</param>
+        /// <param name="streamingRequest">Streaming request dto.</param>
+        /// <param name="enableAdaptiveBitrateStreaming">Enable adaptive bitrate streaming.</param>
+        /// <returns>A <see cref="Task"/> containing the resulting <see cref="ActionResult"/>.</returns>
+        public async Task<ActionResult> 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<ActionResult> 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<byte>(), 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);
+        }
+
+        /// <summary>
+        /// Appends a CODECS field containing formatted strings of
+        /// the active streams output video and audio codecs.
+        /// </summary>
+        /// <seealso cref="AppendPlaylist(StringBuilder, StreamState, string, int, string)"/>
+        /// <seealso cref="GetPlaylistVideoCodecs(StreamState, string, int)"/>
+        /// <seealso cref="GetPlaylistAudioCodecs(StreamState)"/>
+        /// <param name="builder">StringBuilder to append the field to.</param>
+        /// <param name="state">StreamState of the current stream.</param>
+        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('"');
+            }
+        }
+
+        /// <summary>
+        /// Appends a RESOLUTION field containing the resolution of the output stream.
+        /// </summary>
+        /// <seealso cref="AppendPlaylist(StringBuilder, StreamState, string, int, string)"/>
+        /// <param name="builder">StringBuilder to append the field to.</param>
+        /// <param name="state">StreamState of the current stream.</param>
+        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());
+            }
+        }
+
+        /// <summary>
+        /// Appends a FRAME-RATE field containing the framerate of the output stream.
+        /// </summary>
+        /// <seealso cref="AppendPlaylist(StringBuilder, StreamState, string, int, string)"/>
+        /// <param name="builder">StringBuilder to append the field to.</param>
+        /// <param name="state">StreamState of the current stream.</param>
+        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<MediaStream> 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);
+            }
+        }
+
+        /// <summary>
+        /// Get the H.26X level of the output video stream.
+        /// </summary>
+        /// <param name="state">StreamState of the current stream.</param>
+        /// <returns>H.26X level of the output video stream.</returns>
+        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;
+        }
+
+        /// <summary>
+        /// Gets a formatted string of the output audio codec, for use in the CODECS field.
+        /// </summary>
+        /// <seealso cref="AppendPlaylistCodecsField(StringBuilder, StreamState)"/>
+        /// <seealso cref="GetPlaylistVideoCodecs(StreamState, string, int)"/>
+        /// <param name="state">StreamState of the current stream.</param>
+        /// <returns>Formatted audio codec string.</returns>
+        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;
+        }
+
+        /// <summary>
+        /// Gets a formatted string of the output video codec, for use in the CODECS field.
+        /// </summary>
+        /// <seealso cref="AppendPlaylistCodecsField(StringBuilder, StreamState)"/>
+        /// <seealso cref="GetPlaylistAudioCodecs(StreamState)"/>
+        /// <param name="state">StreamState of the current stream.</param>
+        /// <param name="codec">Video codec.</param>
+        /// <param name="level">Video level.</param>
+        /// <returns>Formatted video codec string.</returns>
+        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);
+        }
+    }
+}

+ 573 - 0
Jellyfin.Api/Helpers/MediaInfoHelper.cs

@@ -0,0 +1,573 @@
+using System;
+using System.Globalization;
+using System.Linq;
+using System.Text.Json;
+using System.Threading;
+using System.Threading.Tasks;
+using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
+using MediaBrowser.Common.Net;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Devices;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.MediaEncoding;
+using MediaBrowser.Controller.Net;
+using MediaBrowser.Model.Dlna;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.MediaInfo;
+using MediaBrowser.Model.Session;
+using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.Logging;
+
+namespace Jellyfin.Api.Helpers
+{
+    /// <summary>
+    /// Media info helper.
+    /// </summary>
+    public class MediaInfoHelper
+    {
+        private readonly IUserManager _userManager;
+        private readonly ILibraryManager _libraryManager;
+        private readonly IMediaSourceManager _mediaSourceManager;
+        private readonly IMediaEncoder _mediaEncoder;
+        private readonly IServerConfigurationManager _serverConfigurationManager;
+        private readonly ILogger<MediaInfoHelper> _logger;
+        private readonly INetworkManager _networkManager;
+        private readonly IDeviceManager _deviceManager;
+        private readonly IAuthorizationContext _authContext;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="MediaInfoHelper"/> class.
+        /// </summary>
+        /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
+        /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
+        /// <param name="mediaSourceManager">Instance of the <see cref="IMediaSourceManager"/> interface.</param>
+        /// <param name="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> interface.</param>
+        /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
+        /// <param name="logger">Instance of the <see cref="ILogger{MediaInfoHelper}"/> interface.</param>
+        /// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param>
+        /// <param name="deviceManager">Instance of the <see cref="IDeviceManager"/> interface.</param>
+        /// <param name="authContext">Instance of the <see cref="IAuthorizationContext"/> interface.</param>
+        public MediaInfoHelper(
+            IUserManager userManager,
+            ILibraryManager libraryManager,
+            IMediaSourceManager mediaSourceManager,
+            IMediaEncoder mediaEncoder,
+            IServerConfigurationManager serverConfigurationManager,
+            ILogger<MediaInfoHelper> logger,
+            INetworkManager networkManager,
+            IDeviceManager deviceManager,
+            IAuthorizationContext authContext)
+        {
+            _userManager = userManager;
+            _libraryManager = libraryManager;
+            _mediaSourceManager = mediaSourceManager;
+            _mediaEncoder = mediaEncoder;
+            _serverConfigurationManager = serverConfigurationManager;
+            _logger = logger;
+            _networkManager = networkManager;
+            _deviceManager = deviceManager;
+            _authContext = authContext;
+        }
+
+        /// <summary>
+        /// Get playback info.
+        /// </summary>
+        /// <param name="id">Item id.</param>
+        /// <param name="userId">User Id.</param>
+        /// <param name="mediaSourceId">Media source id.</param>
+        /// <param name="liveStreamId">Live stream id.</param>
+        /// <returns>A <see cref="Task"/> containing the <see cref="PlaybackInfoResponse"/>.</returns>
+        public async Task<PlaybackInfoResponse> GetPlaybackInfo(
+            Guid id,
+            Guid? userId,
+            string? mediaSourceId = null,
+            string? liveStreamId = null)
+        {
+            var user = userId.HasValue && !userId.Equals(Guid.Empty)
+                ? _userManager.GetUserById(userId.Value)
+                : null;
+            var item = _libraryManager.GetItemById(id);
+            var result = new PlaybackInfoResponse();
+
+            MediaSourceInfo[] mediaSources;
+            if (string.IsNullOrWhiteSpace(liveStreamId))
+            {
+                // TODO (moved from MediaBrowser.Api) handle supportedLiveMediaTypes?
+                var mediaSourcesList = await _mediaSourceManager.GetPlaybackMediaSources(item, user, true, true, CancellationToken.None).ConfigureAwait(false);
+
+                if (string.IsNullOrWhiteSpace(mediaSourceId))
+                {
+                    mediaSources = mediaSourcesList.ToArray();
+                }
+                else
+                {
+                    mediaSources = mediaSourcesList
+                        .Where(i => string.Equals(i.Id, mediaSourceId, StringComparison.OrdinalIgnoreCase))
+                        .ToArray();
+                }
+            }
+            else
+            {
+                var mediaSource = await _mediaSourceManager.GetLiveStream(liveStreamId, CancellationToken.None).ConfigureAwait(false);
+
+                mediaSources = new[] { mediaSource };
+            }
+
+            if (mediaSources.Length == 0)
+            {
+                result.MediaSources = Array.Empty<MediaSourceInfo>();
+
+                result.ErrorCode ??= PlaybackErrorCode.NoCompatibleStream;
+            }
+            else
+            {
+                // Since we're going to be setting properties on MediaSourceInfos that come out of _mediaSourceManager, we should clone it
+                // Should we move this directly into MediaSourceManager?
+                result.MediaSources = JsonSerializer.Deserialize<MediaSourceInfo[]>(JsonSerializer.SerializeToUtf8Bytes(mediaSources));
+
+                result.PlaySessionId = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture);
+            }
+
+            return result;
+        }
+
+        /// <summary>
+        /// SetDeviceSpecificData.
+        /// </summary>
+        /// <param name="item">Item to set data for.</param>
+        /// <param name="mediaSource">Media source info.</param>
+        /// <param name="profile">Device profile.</param>
+        /// <param name="auth">Authorization info.</param>
+        /// <param name="maxBitrate">Max bitrate.</param>
+        /// <param name="startTimeTicks">Start time ticks.</param>
+        /// <param name="mediaSourceId">Media source id.</param>
+        /// <param name="audioStreamIndex">Audio stream index.</param>
+        /// <param name="subtitleStreamIndex">Subtitle stream index.</param>
+        /// <param name="maxAudioChannels">Max audio channels.</param>
+        /// <param name="playSessionId">Play session id.</param>
+        /// <param name="userId">User id.</param>
+        /// <param name="enableDirectPlay">Enable direct play.</param>
+        /// <param name="enableDirectStream">Enable direct stream.</param>
+        /// <param name="enableTranscoding">Enable transcoding.</param>
+        /// <param name="allowVideoStreamCopy">Allow video stream copy.</param>
+        /// <param name="allowAudioStreamCopy">Allow audio stream copy.</param>
+        /// <param name="ipAddress">Requesting IP address.</param>
+        public void SetDeviceSpecificData(
+            BaseItem item,
+            MediaSourceInfo mediaSource,
+            DeviceProfile profile,
+            AuthorizationInfo auth,
+            long? maxBitrate,
+            long startTimeTicks,
+            string mediaSourceId,
+            int? audioStreamIndex,
+            int? subtitleStreamIndex,
+            int? maxAudioChannels,
+            string playSessionId,
+            Guid userId,
+            bool enableDirectPlay,
+            bool enableDirectStream,
+            bool enableTranscoding,
+            bool allowVideoStreamCopy,
+            bool allowAudioStreamCopy,
+            string ipAddress)
+        {
+            var streamBuilder = new StreamBuilder(_mediaEncoder, _logger);
+
+            var options = new VideoOptions
+            {
+                MediaSources = new[] { mediaSource },
+                Context = EncodingContext.Streaming,
+                DeviceId = auth.DeviceId,
+                ItemId = item.Id,
+                Profile = profile,
+                MaxAudioChannels = maxAudioChannels
+            };
+
+            if (string.Equals(mediaSourceId, mediaSource.Id, StringComparison.OrdinalIgnoreCase))
+            {
+                options.MediaSourceId = mediaSourceId;
+                options.AudioStreamIndex = audioStreamIndex;
+                options.SubtitleStreamIndex = subtitleStreamIndex;
+            }
+
+            var user = _userManager.GetUserById(userId);
+
+            if (!enableDirectPlay)
+            {
+                mediaSource.SupportsDirectPlay = false;
+            }
+
+            if (!enableDirectStream)
+            {
+                mediaSource.SupportsDirectStream = false;
+            }
+
+            if (!enableTranscoding)
+            {
+                mediaSource.SupportsTranscoding = false;
+            }
+
+            if (item is Audio)
+            {
+                _logger.LogInformation(
+                    "User policy for {0}. EnableAudioPlaybackTranscoding: {1}",
+                    user.Username,
+                    user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding));
+            }
+            else
+            {
+                _logger.LogInformation(
+                    "User policy for {0}. EnablePlaybackRemuxing: {1} EnableVideoPlaybackTranscoding: {2} EnableAudioPlaybackTranscoding: {3}",
+                    user.Username,
+                    user.HasPermission(PermissionKind.EnablePlaybackRemuxing),
+                    user.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding),
+                    user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding));
+            }
+
+            // Beginning of Playback Determination: Attempt DirectPlay first
+            if (mediaSource.SupportsDirectPlay)
+            {
+                if (mediaSource.IsRemote && user.HasPermission(PermissionKind.ForceRemoteSourceTranscoding))
+                {
+                    mediaSource.SupportsDirectPlay = false;
+                }
+                else
+                {
+                    var supportsDirectStream = mediaSource.SupportsDirectStream;
+
+                    // Dummy this up to fool StreamBuilder
+                    mediaSource.SupportsDirectStream = true;
+                    options.MaxBitrate = maxBitrate;
+
+                    if (item is Audio)
+                    {
+                        if (!user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding))
+                        {
+                            options.ForceDirectPlay = true;
+                        }
+                    }
+                    else if (item is Video)
+                    {
+                        if (!user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding)
+                            && !user.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding)
+                            && !user.HasPermission(PermissionKind.EnablePlaybackRemuxing))
+                        {
+                            options.ForceDirectPlay = true;
+                        }
+                    }
+
+                    // The MediaSource supports direct stream, now test to see if the client supports it
+                    var streamInfo = string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase)
+                        ? streamBuilder.BuildAudioItem(options)
+                        : streamBuilder.BuildVideoItem(options);
+
+                    if (streamInfo == null || !streamInfo.IsDirectStream)
+                    {
+                        mediaSource.SupportsDirectPlay = false;
+                    }
+
+                    // Set this back to what it was
+                    mediaSource.SupportsDirectStream = supportsDirectStream;
+
+                    if (streamInfo != null)
+                    {
+                        SetDeviceSpecificSubtitleInfo(streamInfo, mediaSource, auth.Token);
+                    }
+                }
+            }
+
+            if (mediaSource.SupportsDirectStream)
+            {
+                if (mediaSource.IsRemote && user.HasPermission(PermissionKind.ForceRemoteSourceTranscoding))
+                {
+                    mediaSource.SupportsDirectStream = false;
+                }
+                else
+                {
+                    options.MaxBitrate = GetMaxBitrate(maxBitrate, user, ipAddress);
+
+                    if (item is Audio)
+                    {
+                        if (!user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding))
+                        {
+                            options.ForceDirectStream = true;
+                        }
+                    }
+                    else if (item is Video)
+                    {
+                        if (!user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding)
+                            && !user.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding)
+                            && !user.HasPermission(PermissionKind.EnablePlaybackRemuxing))
+                        {
+                            options.ForceDirectStream = true;
+                        }
+                    }
+
+                    // The MediaSource supports direct stream, now test to see if the client supports it
+                    var streamInfo = string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase)
+                        ? streamBuilder.BuildAudioItem(options)
+                        : streamBuilder.BuildVideoItem(options);
+
+                    if (streamInfo == null || !streamInfo.IsDirectStream)
+                    {
+                        mediaSource.SupportsDirectStream = false;
+                    }
+
+                    if (streamInfo != null)
+                    {
+                        SetDeviceSpecificSubtitleInfo(streamInfo, mediaSource, auth.Token);
+                    }
+                }
+            }
+
+            if (mediaSource.SupportsTranscoding)
+            {
+                options.MaxBitrate = GetMaxBitrate(maxBitrate, user, ipAddress);
+
+                // The MediaSource supports direct stream, now test to see if the client supports it
+                var streamInfo = string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase)
+                    ? streamBuilder.BuildAudioItem(options)
+                    : streamBuilder.BuildVideoItem(options);
+
+                if (mediaSource.IsRemote && user.HasPermission(PermissionKind.ForceRemoteSourceTranscoding))
+                {
+                    if (streamInfo != null)
+                    {
+                        streamInfo.PlaySessionId = playSessionId;
+                        streamInfo.StartPositionTicks = startTimeTicks;
+                        mediaSource.TranscodingUrl = streamInfo.ToUrl("-", auth.Token).TrimStart('-');
+                        mediaSource.TranscodingUrl += "&allowVideoStreamCopy=false";
+                        mediaSource.TranscodingUrl += "&allowAudioStreamCopy=false";
+                        mediaSource.TranscodingContainer = streamInfo.Container;
+                        mediaSource.TranscodingSubProtocol = streamInfo.SubProtocol;
+
+                        // Do this after the above so that StartPositionTicks is set
+                        SetDeviceSpecificSubtitleInfo(streamInfo, mediaSource, auth.Token);
+                    }
+                }
+                else
+                {
+                    if (streamInfo != null)
+                    {
+                        streamInfo.PlaySessionId = playSessionId;
+
+                        if (streamInfo.PlayMethod == PlayMethod.Transcode)
+                        {
+                            streamInfo.StartPositionTicks = startTimeTicks;
+                            mediaSource.TranscodingUrl = streamInfo.ToUrl("-", auth.Token).TrimStart('-');
+
+                            if (!allowVideoStreamCopy)
+                            {
+                                mediaSource.TranscodingUrl += "&allowVideoStreamCopy=false";
+                            }
+
+                            if (!allowAudioStreamCopy)
+                            {
+                                mediaSource.TranscodingUrl += "&allowAudioStreamCopy=false";
+                            }
+
+                            mediaSource.TranscodingContainer = streamInfo.Container;
+                            mediaSource.TranscodingSubProtocol = streamInfo.SubProtocol;
+                        }
+
+                        if (!allowAudioStreamCopy)
+                        {
+                            mediaSource.TranscodingUrl += "&allowAudioStreamCopy=false";
+                        }
+
+                        mediaSource.TranscodingContainer = streamInfo.Container;
+                        mediaSource.TranscodingSubProtocol = streamInfo.SubProtocol;
+
+                        // Do this after the above so that StartPositionTicks is set
+                        SetDeviceSpecificSubtitleInfo(streamInfo, mediaSource, auth.Token);
+                    }
+                }
+            }
+
+            foreach (var attachment in mediaSource.MediaAttachments)
+            {
+                attachment.DeliveryUrl = string.Format(
+                    CultureInfo.InvariantCulture,
+                    "/Videos/{0}/{1}/Attachments/{2}",
+                    item.Id,
+                    mediaSource.Id,
+                    attachment.Index);
+            }
+        }
+
+        /// <summary>
+        /// Sort media source.
+        /// </summary>
+        /// <param name="result">Playback info response.</param>
+        /// <param name="maxBitrate">Max bitrate.</param>
+        public void SortMediaSources(PlaybackInfoResponse result, long? maxBitrate)
+        {
+            var originalList = result.MediaSources.ToList();
+
+            result.MediaSources = result.MediaSources.OrderBy(i =>
+                {
+                    // Nothing beats direct playing a file
+                    if (i.SupportsDirectPlay && i.Protocol == MediaProtocol.File)
+                    {
+                        return 0;
+                    }
+
+                    return 1;
+                })
+                .ThenBy(i =>
+                {
+                    // Let's assume direct streaming a file is just as desirable as direct playing a remote url
+                    if (i.SupportsDirectPlay || i.SupportsDirectStream)
+                    {
+                        return 0;
+                    }
+
+                    return 1;
+                })
+                .ThenBy(i =>
+                {
+                    return i.Protocol switch
+                    {
+                        MediaProtocol.File => 0,
+                        _ => 1,
+                    };
+                })
+                .ThenBy(i =>
+                {
+                    if (maxBitrate.HasValue && i.Bitrate.HasValue)
+                    {
+                        return i.Bitrate.Value <= maxBitrate.Value ? 0 : 2;
+                    }
+
+                    return 1;
+                })
+                .ThenBy(originalList.IndexOf)
+                .ToArray();
+        }
+
+        /// <summary>
+        /// Open media source.
+        /// </summary>
+        /// <param name="httpRequest">Http Request.</param>
+        /// <param name="request">Live stream request.</param>
+        /// <returns>A <see cref="Task"/> containing the <see cref="LiveStreamResponse"/>.</returns>
+        public async Task<LiveStreamResponse> OpenMediaSource(HttpRequest httpRequest, LiveStreamRequest request)
+        {
+            var authInfo = _authContext.GetAuthorizationInfo(httpRequest);
+
+            var result = await _mediaSourceManager.OpenLiveStream(request, CancellationToken.None).ConfigureAwait(false);
+
+            var profile = request.DeviceProfile;
+            if (profile == null)
+            {
+                var caps = _deviceManager.GetCapabilities(authInfo.DeviceId);
+                if (caps != null)
+                {
+                    profile = caps.DeviceProfile;
+                }
+            }
+
+            if (profile != null)
+            {
+                var item = _libraryManager.GetItemById(request.ItemId);
+
+                SetDeviceSpecificData(
+                    item,
+                    result.MediaSource,
+                    profile,
+                    authInfo,
+                    request.MaxStreamingBitrate,
+                    request.StartTimeTicks ?? 0,
+                    result.MediaSource.Id,
+                    request.AudioStreamIndex,
+                    request.SubtitleStreamIndex,
+                    request.MaxAudioChannels,
+                    request.PlaySessionId,
+                    request.UserId,
+                    request.EnableDirectPlay,
+                    request.EnableDirectStream,
+                    true,
+                    true,
+                    true,
+                    httpRequest.HttpContext.Connection.RemoteIpAddress.ToString());
+            }
+            else
+            {
+                if (!string.IsNullOrWhiteSpace(result.MediaSource.TranscodingUrl))
+                {
+                    result.MediaSource.TranscodingUrl += "&LiveStreamId=" + result.MediaSource.LiveStreamId;
+                }
+            }
+
+            // here was a check if (result.MediaSource != null) but Rider said it will never be null
+            NormalizeMediaSourceContainer(result.MediaSource, profile!, DlnaProfileType.Video);
+
+            return result;
+        }
+
+        /// <summary>
+        /// Normalize media source container.
+        /// </summary>
+        /// <param name="mediaSource">Media source.</param>
+        /// <param name="profile">Device profile.</param>
+        /// <param name="type">Dlna profile type.</param>
+        public void NormalizeMediaSourceContainer(MediaSourceInfo mediaSource, DeviceProfile profile, DlnaProfileType type)
+        {
+            mediaSource.Container = StreamBuilder.NormalizeMediaSourceFormatIntoSingleContainer(mediaSource.Container, mediaSource.Path, profile, type);
+        }
+
+        private void SetDeviceSpecificSubtitleInfo(StreamInfo info, MediaSourceInfo mediaSource, string accessToken)
+        {
+            var profiles = info.GetSubtitleProfiles(_mediaEncoder, false, "-", accessToken);
+            mediaSource.DefaultSubtitleStreamIndex = info.SubtitleStreamIndex;
+
+            mediaSource.TranscodeReasons = info.TranscodeReasons;
+
+            foreach (var profile in profiles)
+            {
+                foreach (var stream in mediaSource.MediaStreams)
+                {
+                    if (stream.Type == MediaStreamType.Subtitle && stream.Index == profile.Index)
+                    {
+                        stream.DeliveryMethod = profile.DeliveryMethod;
+
+                        if (profile.DeliveryMethod == SubtitleDeliveryMethod.External)
+                        {
+                            stream.DeliveryUrl = profile.Url.TrimStart('-');
+                            stream.IsExternalUrl = profile.IsExternalUrl;
+                        }
+                    }
+                }
+            }
+        }
+
+        private long? GetMaxBitrate(long? clientMaxBitrate, User user, string ipAddress)
+        {
+            var maxBitrate = clientMaxBitrate;
+            var remoteClientMaxBitrate = user?.RemoteClientBitrateLimit ?? 0;
+
+            if (remoteClientMaxBitrate <= 0)
+            {
+                remoteClientMaxBitrate = _serverConfigurationManager.Configuration.RemoteClientBitrateLimit;
+            }
+
+            if (remoteClientMaxBitrate > 0)
+            {
+                var isInLocalNetwork = _networkManager.IsInLocalNetwork(ipAddress);
+
+                _logger.LogInformation("RemoteClientBitrateLimit: {0}, RemoteIp: {1}, IsInLocalNetwork: {2}", remoteClientMaxBitrate, ipAddress, isInLocalNetwork);
+                if (!isInLocalNetwork)
+                {
+                    maxBitrate = Math.Min(maxBitrate ?? remoteClientMaxBitrate, remoteClientMaxBitrate);
+                }
+            }
+
+            return maxBitrate;
+        }
+    }
+}