소스 검색

Merge pull request #7494 from Shadowghost/streambuilder-cleanup

Bond-009 2 년 전
부모
커밋
817996da4b

+ 2 - 2
Emby.Dlna/Didl/DidlBuilder.cs

@@ -195,7 +195,7 @@ namespace Emby.Dlna.Didl
             {
             {
                 var sources = _mediaSourceManager.GetStaticMediaSources(video, true, _user);
                 var sources = _mediaSourceManager.GetStaticMediaSources(video, true, _user);
 
 
-                streamInfo = new StreamBuilder(_mediaEncoder, _logger).BuildVideoItem(new VideoOptions
+                streamInfo = new StreamBuilder(_mediaEncoder, _logger).GetOptimalVideoStream(new MediaOptions
                 {
                 {
                     ItemId = video.Id,
                     ItemId = video.Id,
                     MediaSources = sources.ToArray(),
                     MediaSources = sources.ToArray(),
@@ -537,7 +537,7 @@ namespace Emby.Dlna.Didl
             {
             {
                 var sources = _mediaSourceManager.GetStaticMediaSources(audio, true, _user);
                 var sources = _mediaSourceManager.GetStaticMediaSources(audio, true, _user);
 
 
-                streamInfo = new StreamBuilder(_mediaEncoder, _logger).BuildAudioItem(new AudioOptions
+                streamInfo = new StreamBuilder(_mediaEncoder, _logger).GetOptimalAudioStream(new MediaOptions
                 {
                 {
                     ItemId = audio.Id,
                     ItemId = audio.Id,
                     MediaSources = sources.ToArray(),
                     MediaSources = sources.ToArray(),

+ 2 - 2
Emby.Dlna/PlayTo/PlayToController.cs

@@ -585,7 +585,7 @@ namespace Emby.Dlna.PlayTo
             {
             {
                 return new PlaylistItem
                 return new PlaylistItem
                 {
                 {
-                    StreamInfo = new StreamBuilder(_mediaEncoder, _logger).BuildVideoItem(new VideoOptions
+                    StreamInfo = new StreamBuilder(_mediaEncoder, _logger).GetOptimalVideoStream(new MediaOptions
                     {
                     {
                         ItemId = item.Id,
                         ItemId = item.Id,
                         MediaSources = mediaSources,
                         MediaSources = mediaSources,
@@ -605,7 +605,7 @@ namespace Emby.Dlna.PlayTo
             {
             {
                 return new PlaylistItem
                 return new PlaylistItem
                 {
                 {
-                    StreamInfo = new StreamBuilder(_mediaEncoder, _logger).BuildAudioItem(new AudioOptions
+                    StreamInfo = new StreamBuilder(_mediaEncoder, _logger).GetOptimalAudioStream(new MediaOptions
                     {
                     {
                         ItemId = item.Id,
                         ItemId = item.Id,
                         MediaSources = mediaSources,
                         MediaSources = mediaSources,

+ 3 - 3
Jellyfin.Api/Helpers/MediaInfoHelper.cs

@@ -181,7 +181,7 @@ namespace Jellyfin.Api.Helpers
         {
         {
             var streamBuilder = new StreamBuilder(_mediaEncoder, _logger);
             var streamBuilder = new StreamBuilder(_mediaEncoder, _logger);
 
 
-            var options = new VideoOptions
+            var options = new MediaOptions
             {
             {
                 MediaSources = new[] { mediaSource },
                 MediaSources = new[] { mediaSource },
                 Context = EncodingContext.Streaming,
                 Context = EncodingContext.Streaming,
@@ -244,8 +244,8 @@ namespace Jellyfin.Api.Helpers
 
 
             // Beginning of Playback Determination
             // Beginning of Playback Determination
             var streamInfo = string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase)
             var streamInfo = string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase)
-                ? streamBuilder.BuildAudioItem(options)
-                : streamBuilder.BuildVideoItem(options);
+                ? streamBuilder.GetOptimalAudioStream(options)
+                : streamBuilder.GetOptimalVideoStream(options);
 
 
             if (streamInfo is not null)
             if (streamInfo is not null)
             {
             {

+ 49 - 5
MediaBrowser.Model/Dlna/AudioOptions.cs → MediaBrowser.Model/Dlna/MediaOptions.cs

@@ -1,5 +1,4 @@
 #nullable disable
 #nullable disable
-#pragma warning disable CS1591
 
 
 using System;
 using System;
 using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Dto;
@@ -7,11 +6,14 @@ using MediaBrowser.Model.Dto;
 namespace MediaBrowser.Model.Dlna
 namespace MediaBrowser.Model.Dlna
 {
 {
     /// <summary>
     /// <summary>
-    /// Class AudioOptions.
+    /// Class MediaOptions.
     /// </summary>
     /// </summary>
-    public class AudioOptions
+    public class MediaOptions
     {
     {
-        public AudioOptions()
+        /// <summary>
+        /// Initializes a new instance of the <see cref="MediaOptions"/> class.
+        /// </summary>
+        public MediaOptions()
         {
         {
             Context = EncodingContext.Streaming;
             Context = EncodingContext.Streaming;
 
 
@@ -19,20 +21,49 @@ namespace MediaBrowser.Model.Dlna
             EnableDirectStream = true;
             EnableDirectStream = true;
         }
         }
 
 
+        /// <summary>
+        /// Gets or sets a value indicating whether direct playback is allowed.
+        /// </summary>
         public bool EnableDirectPlay { get; set; }
         public bool EnableDirectPlay { get; set; }
 
 
+        /// <summary>
+        /// Gets or sets a value indicating whether direct streaming is allowed.
+        /// </summary>
         public bool EnableDirectStream { get; set; }
         public bool EnableDirectStream { get; set; }
 
 
+        /// <summary>
+        /// Gets or sets a value indicating whether direct playback is forced.
+        /// </summary>
         public bool ForceDirectPlay { get; set; }
         public bool ForceDirectPlay { get; set; }
 
 
+        /// <summary>
+        /// Gets or sets a value indicating whether direct streaming is forced.
+        /// </summary>
         public bool ForceDirectStream { get; set; }
         public bool ForceDirectStream { get; set; }
 
 
+        /// <summary>
+        /// Gets or sets a value indicating whether audio stream copy is allowed.
+        /// </summary>
         public bool AllowAudioStreamCopy { get; set; }
         public bool AllowAudioStreamCopy { get; set; }
 
 
+        /// <summary>
+        /// Gets or sets a value indicating whether video stream copy is allowed.
+        /// </summary>
+        public bool AllowVideoStreamCopy { get; set; }
+
+        /// <summary>
+        /// Gets or sets the item id.
+        /// </summary>
         public Guid ItemId { get; set; }
         public Guid ItemId { get; set; }
 
 
+        /// <summary>
+        /// Gets or sets the media sources.
+        /// </summary>
         public MediaSourceInfo[] MediaSources { get; set; }
         public MediaSourceInfo[] MediaSources { get; set; }
 
 
+        /// <summary>
+        /// Gets or sets the device profile.
+        /// </summary>
         public DeviceProfile Profile { get; set; }
         public DeviceProfile Profile { get; set; }
 
 
         /// <summary>
         /// <summary>
@@ -40,6 +71,9 @@ namespace MediaBrowser.Model.Dlna
         /// </summary>
         /// </summary>
         public string MediaSourceId { get; set; }
         public string MediaSourceId { get; set; }
 
 
+        /// <summary>
+        /// Gets or sets the device id.
+        /// </summary>
         public string DeviceId { get; set; }
         public string DeviceId { get; set; }
 
 
         /// <summary>
         /// <summary>
@@ -49,7 +83,7 @@ namespace MediaBrowser.Model.Dlna
         public int? MaxAudioChannels { get; set; }
         public int? MaxAudioChannels { get; set; }
 
 
         /// <summary>
         /// <summary>
-        /// Gets or sets the application's configured quality setting.
+        /// Gets or sets the application's configured maximum bitrate.
         /// </summary>
         /// </summary>
         public int? MaxBitrate { get; set; }
         public int? MaxBitrate { get; set; }
 
 
@@ -65,6 +99,16 @@ namespace MediaBrowser.Model.Dlna
         /// <value>The audio transcoding bitrate.</value>
         /// <value>The audio transcoding bitrate.</value>
         public int? AudioTranscodingBitrate { get; set; }
         public int? AudioTranscodingBitrate { get; set; }
 
 
+        /// <summary>
+        /// Gets or sets an override for the audio stream index.
+        /// </summary>
+        public int? AudioStreamIndex { get; set; }
+
+        /// <summary>
+        /// Gets or sets an override for the subtitle stream index.
+        /// </summary>
+        public int? SubtitleStreamIndex { get; set; }
+
         /// <summary>
         /// <summary>
         /// Gets the maximum bitrate.
         /// Gets the maximum bitrate.
         /// </summary>
         /// </summary>

+ 204 - 182
MediaBrowser.Model/Dlna/StreamBuilder.cs

@@ -1,5 +1,4 @@
 #nullable disable
 #nullable disable
-#pragma warning disable CS1591
 
 
 using System;
 using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
@@ -13,6 +12,9 @@ using Microsoft.Extensions.Logging;
 
 
 namespace MediaBrowser.Model.Dlna
 namespace MediaBrowser.Model.Dlna
 {
 {
+    /// <summary>
+    /// Class StreamBuilder.
+    /// </summary>
     public class StreamBuilder
     public class StreamBuilder
     {
     {
         // Aliases
         // Aliases
@@ -24,42 +26,56 @@ namespace MediaBrowser.Model.Dlna
         private readonly ILogger _logger;
         private readonly ILogger _logger;
         private readonly ITranscoderSupport _transcoderSupport;
         private readonly ITranscoderSupport _transcoderSupport;
 
 
+        /// <summary>
+        /// Initializes a new instance of the <see cref="StreamBuilder"/> class.
+        /// </summary>
+        /// <param name="transcoderSupport">The <see cref="ITranscoderSupport"/> object.</param>
+        /// <param name="logger">The <see cref="ILogger"/> object.</param>
         public StreamBuilder(ITranscoderSupport transcoderSupport, ILogger logger)
         public StreamBuilder(ITranscoderSupport transcoderSupport, ILogger logger)
         {
         {
             _transcoderSupport = transcoderSupport;
             _transcoderSupport = transcoderSupport;
             _logger = logger;
             _logger = logger;
         }
         }
 
 
+        /// <summary>
+        /// Initializes a new instance of the <see cref="StreamBuilder"/> class.
+        /// </summary>
+        /// <param name="logger">The <see cref="ILogger"/> object.</param>
         public StreamBuilder(ILogger<StreamBuilder> logger)
         public StreamBuilder(ILogger<StreamBuilder> logger)
             : this(new FullTranscoderSupport(), logger)
             : this(new FullTranscoderSupport(), logger)
         {
         {
         }
         }
 
 
-        public StreamInfo BuildAudioItem(AudioOptions options)
+        /// <summary>
+        /// Gets the optimal audio stream.
+        /// </summary>
+        /// <param name="options">The <see cref="MediaOptions"/> object to get the audio stream from.</param>
+        /// <returns>The <see cref="StreamInfo"/> of the optimal audio stream.</returns>
+        public StreamInfo GetOptimalAudioStream(MediaOptions options)
         {
         {
-            ValidateAudioInput(options);
+            ValidateMediaOptions(options, false);
 
 
             var mediaSources = new List<MediaSourceInfo>();
             var mediaSources = new List<MediaSourceInfo>();
-            foreach (MediaSourceInfo i in options.MediaSources)
+            foreach (var mediaSource in options.MediaSources)
             {
             {
                 if (string.IsNullOrEmpty(options.MediaSourceId) ||
                 if (string.IsNullOrEmpty(options.MediaSourceId) ||
-                    string.Equals(i.Id, options.MediaSourceId, StringComparison.OrdinalIgnoreCase))
+                    string.Equals(mediaSource.Id, options.MediaSourceId, StringComparison.OrdinalIgnoreCase))
                 {
                 {
-                    mediaSources.Add(i);
+                    mediaSources.Add(mediaSource);
                 }
                 }
             }
             }
 
 
             var streams = new List<StreamInfo>();
             var streams = new List<StreamInfo>();
-            foreach (MediaSourceInfo i in mediaSources)
+            foreach (var mediaSourceInfo in mediaSources)
             {
             {
-                StreamInfo streamInfo = BuildAudioItem(i, options);
+                StreamInfo streamInfo = GetOptimalAudioStream(mediaSourceInfo, options);
                 if (streamInfo is not null)
                 if (streamInfo is not null)
                 {
                 {
                     streams.Add(streamInfo);
                     streams.Add(streamInfo);
                 }
                 }
             }
             }
 
 
-            foreach (StreamInfo stream in streams)
+            foreach (var stream in streams)
             {
             {
                 stream.DeviceId = options.DeviceId;
                 stream.DeviceId = options.DeviceId;
                 stream.DeviceProfileId = options.Profile.Id;
                 stream.DeviceProfileId = options.Profile.Id;
@@ -68,31 +84,137 @@ namespace MediaBrowser.Model.Dlna
             return GetOptimalStream(streams, options.GetMaxBitrate(true) ?? 0);
             return GetOptimalStream(streams, options.GetMaxBitrate(true) ?? 0);
         }
         }
 
 
-        public StreamInfo BuildVideoItem(VideoOptions options)
+        private StreamInfo GetOptimalAudioStream(MediaSourceInfo item, MediaOptions options)
         {
         {
-            ValidateInput(options);
+            var playlistItem = new StreamInfo
+            {
+                ItemId = options.ItemId,
+                MediaType = DlnaProfileType.Audio,
+                MediaSource = item,
+                RunTimeTicks = item.RunTimeTicks,
+                Context = options.Context,
+                DeviceProfile = options.Profile
+            };
+
+            if (options.ForceDirectPlay)
+            {
+                playlistItem.PlayMethod = PlayMethod.DirectPlay;
+                playlistItem.Container = NormalizeMediaSourceFormatIntoSingleContainer(item.Container, options.Profile, DlnaProfileType.Audio);
+                return playlistItem;
+            }
+
+            if (options.ForceDirectStream)
+            {
+                playlistItem.PlayMethod = PlayMethod.DirectStream;
+                playlistItem.Container = NormalizeMediaSourceFormatIntoSingleContainer(item.Container, options.Profile, DlnaProfileType.Audio);
+                return playlistItem;
+            }
+
+            MediaStream audioStream = item.GetDefaultAudioStream(null);
+
+            var directPlayInfo = GetAudioDirectPlayProfile(item, audioStream, options);
+
+            var directPlayMethod = directPlayInfo.PlayMethod;
+            var transcodeReasons = directPlayInfo.TranscodeReasons;
+
+            var inputAudioChannels = audioStream?.Channels;
+            var inputAudioBitrate = audioStream?.BitDepth;
+            var inputAudioSampleRate = audioStream?.SampleRate;
+            var inputAudioBitDepth = audioStream?.BitDepth;
+
+            if (directPlayMethod.HasValue)
+            {
+                var profile = options.Profile;
+                var audioFailureConditions = GetProfileConditionsForAudio(profile.CodecProfiles, item.Container, audioStream?.Codec, inputAudioChannels, inputAudioBitrate, inputAudioSampleRate, inputAudioBitDepth, true);
+                var audioFailureReasons = AggregateFailureConditions(item, profile, "AudioCodecProfile", audioFailureConditions);
+                transcodeReasons |= audioFailureReasons;
+
+                if (audioFailureReasons == 0)
+                {
+                    playlistItem.PlayMethod = directPlayMethod.Value;
+                    playlistItem.Container = NormalizeMediaSourceFormatIntoSingleContainer(item.Container, options.Profile, DlnaProfileType.Audio, directPlayInfo.Profile);
+
+                    return playlistItem;
+                }
+            }
+
+            TranscodingProfile transcodingProfile = null;
+            foreach (var tcProfile in options.Profile.TranscodingProfiles)
+            {
+                if (tcProfile.Type == playlistItem.MediaType
+                    && tcProfile.Context == options.Context
+                    && _transcoderSupport.CanEncodeToAudioCodec(transcodingProfile.AudioCodec ?? tcProfile.Container))
+                {
+                    transcodingProfile = tcProfile;
+                    break;
+                }
+            }
+
+            if (transcodingProfile != null)
+            {
+                if (!item.SupportsTranscoding)
+                {
+                    return null;
+                }
+
+                SetStreamInfoOptionsFromTranscodingProfile(item, playlistItem, transcodingProfile);
+
+                var audioTranscodingConditions = GetProfileConditionsForAudio(options.Profile.CodecProfiles, transcodingProfile.Container, transcodingProfile.AudioCodec, inputAudioChannels, inputAudioBitrate, inputAudioSampleRate, inputAudioBitDepth, false).ToArray();
+                ApplyTranscodingConditions(playlistItem, audioTranscodingConditions, null, true, true);
+
+                // Honor requested max channels
+                playlistItem.GlobalMaxAudioChannels = options.MaxAudioChannels;
+
+                var configuredBitrate = options.GetMaxBitrate(true);
+
+                long transcodingBitrate = options.AudioTranscodingBitrate
+                    ?? (options.Context == EncodingContext.Streaming ? options.Profile.MusicStreamingTranscodingBitrate : null)
+                    ?? configuredBitrate
+                    ?? 128000;
+
+                if (configuredBitrate.HasValue)
+                {
+                    transcodingBitrate = Math.Min(configuredBitrate.Value, transcodingBitrate);
+                }
+
+                var longBitrate = Math.Min(transcodingBitrate, playlistItem.AudioBitrate ?? transcodingBitrate);
+                playlistItem.AudioBitrate = longBitrate > int.MaxValue ? int.MaxValue : Convert.ToInt32(longBitrate);
+            }
+
+            playlistItem.TranscodeReasons = transcodeReasons;
+            return playlistItem;
+        }
+
+        /// <summary>
+        /// Gets the optimal video stream.
+        /// </summary>
+        /// <param name="options">The <see cref="MediaOptions"/> object to get the video stream from.</param>
+        /// <returns>The <see cref="StreamInfo"/> of the optimal video stream.</returns>
+        public StreamInfo GetOptimalVideoStream(MediaOptions options)
+        {
+            ValidateMediaOptions(options, true);
 
 
             var mediaSources = new List<MediaSourceInfo>();
             var mediaSources = new List<MediaSourceInfo>();
-            foreach (MediaSourceInfo i in options.MediaSources)
+            foreach (var mediaSourceInfo in options.MediaSources)
             {
             {
                 if (string.IsNullOrEmpty(options.MediaSourceId) ||
                 if (string.IsNullOrEmpty(options.MediaSourceId) ||
-                    string.Equals(i.Id, options.MediaSourceId, StringComparison.OrdinalIgnoreCase))
+                    string.Equals(mediaSourceInfo.Id, options.MediaSourceId, StringComparison.OrdinalIgnoreCase))
                 {
                 {
-                    mediaSources.Add(i);
+                    mediaSources.Add(mediaSourceInfo);
                 }
                 }
             }
             }
 
 
             var streams = new List<StreamInfo>();
             var streams = new List<StreamInfo>();
-            foreach (MediaSourceInfo i in mediaSources)
+            foreach (var mediaSourceInfo in mediaSources)
             {
             {
-                var streamInfo = BuildVideoItem(i, options);
+                var streamInfo = BuildVideoItem(mediaSourceInfo, options);
                 if (streamInfo is not null)
                 if (streamInfo is not null)
                 {
                 {
                     streams.Add(streamInfo);
                     streams.Add(streamInfo);
                 }
                 }
             }
             }
 
 
-            foreach (StreamInfo stream in streams)
+            foreach (var stream in streams)
             {
             {
                 stream.DeviceId = options.DeviceId;
                 stream.DeviceId = options.DeviceId;
                 stream.DeviceProfileId = options.Profile.Id;
                 stream.DeviceProfileId = options.Profile.Id;
@@ -236,6 +358,14 @@ namespace MediaBrowser.Model.Dlna
             }
             }
         }
         }
 
 
+        /// <summary>
+        /// Normalizes input container.
+        /// </summary>
+        /// <param name="inputContainer">The input container.</param>
+        /// <param name="profile">The <see cref="DeviceProfile"/>.</param>
+        /// <param name="type">The <see cref="DlnaProfileType"/>.</param>
+        /// <param name="playProfile">The <see cref="DirectPlayProfile"/> object to get the video stream from.</param>
+        /// <returns>The the normalized input container.</returns>
         public static string NormalizeMediaSourceFormatIntoSingleContainer(string inputContainer, DeviceProfile profile, DlnaProfileType type, DirectPlayProfile playProfile = null)
         public static string NormalizeMediaSourceFormatIntoSingleContainer(string inputContainer, DeviceProfile profile, DlnaProfileType type, DirectPlayProfile playProfile = null)
         {
         {
             if (string.IsNullOrEmpty(inputContainer))
             if (string.IsNullOrEmpty(inputContainer))
@@ -264,108 +394,7 @@ namespace MediaBrowser.Model.Dlna
             return formats[0];
             return formats[0];
         }
         }
 
 
-        private StreamInfo BuildAudioItem(MediaSourceInfo item, AudioOptions options)
-        {
-            StreamInfo playlistItem = new StreamInfo
-            {
-                ItemId = options.ItemId,
-                MediaType = DlnaProfileType.Audio,
-                MediaSource = item,
-                RunTimeTicks = item.RunTimeTicks,
-                Context = options.Context,
-                DeviceProfile = options.Profile
-            };
-
-            if (options.ForceDirectPlay)
-            {
-                playlistItem.PlayMethod = PlayMethod.DirectPlay;
-                playlistItem.Container = NormalizeMediaSourceFormatIntoSingleContainer(item.Container, options.Profile, DlnaProfileType.Audio);
-                return playlistItem;
-            }
-
-            if (options.ForceDirectStream)
-            {
-                playlistItem.PlayMethod = PlayMethod.DirectStream;
-                playlistItem.Container = NormalizeMediaSourceFormatIntoSingleContainer(item.Container, options.Profile, DlnaProfileType.Audio);
-                return playlistItem;
-            }
-
-            var audioStream = item.GetDefaultAudioStream(null);
-
-            var directPlayInfo = GetAudioDirectPlayProfile(item, audioStream, options);
-
-            var directPlayMethod = directPlayInfo.PlayMethod;
-            var transcodeReasons = directPlayInfo.TranscodeReasons;
-
-            int? inputAudioChannels = audioStream?.Channels;
-            int? inputAudioBitrate = audioStream?.BitDepth;
-            int? inputAudioSampleRate = audioStream?.SampleRate;
-            int? inputAudioBitDepth = audioStream?.BitDepth;
-
-            if (directPlayMethod.HasValue)
-            {
-                var profile = options.Profile;
-                var audioFailureConditions = GetProfileConditionsForAudio(profile.CodecProfiles, item.Container, audioStream?.Codec, inputAudioChannels, inputAudioBitrate, inputAudioSampleRate, inputAudioBitDepth, true);
-                var audioFailureReasons = AggregateFailureConditions(item, profile, "AudioCodecProfile", audioFailureConditions);
-                transcodeReasons |= audioFailureReasons;
-
-                if (audioFailureReasons == 0)
-                {
-                    playlistItem.PlayMethod = directPlayMethod.Value;
-                    playlistItem.Container = NormalizeMediaSourceFormatIntoSingleContainer(item.Container, options.Profile, DlnaProfileType.Audio, directPlayInfo.Profile);
-
-                    return playlistItem;
-                }
-            }
-
-            TranscodingProfile transcodingProfile = null;
-            foreach (var i in options.Profile.TranscodingProfiles)
-            {
-                if (i.Type == playlistItem.MediaType
-                    && i.Context == options.Context
-                    && _transcoderSupport.CanEncodeToAudioCodec(i.AudioCodec ?? i.Container))
-                {
-                    transcodingProfile = i;
-                    break;
-                }
-            }
-
-            if (transcodingProfile is not null)
-            {
-                if (!item.SupportsTranscoding)
-                {
-                    return null;
-                }
-
-                SetStreamInfoOptionsFromTranscodingProfile(item, playlistItem, transcodingProfile);
-
-                var audioTranscodingConditions = GetProfileConditionsForAudio(options.Profile.CodecProfiles, transcodingProfile.Container, transcodingProfile.AudioCodec, inputAudioChannels, inputAudioBitrate, inputAudioSampleRate, inputAudioBitDepth, false).ToArray();
-                ApplyTranscodingConditions(playlistItem, audioTranscodingConditions, null, true, true);
-
-                // Honor requested max channels
-                playlistItem.GlobalMaxAudioChannels = options.MaxAudioChannels;
-
-                var configuredBitrate = options.GetMaxBitrate(true);
-
-                long transcodingBitrate = options.AudioTranscodingBitrate ??
-                    (options.Context == EncodingContext.Streaming ? options.Profile.MusicStreamingTranscodingBitrate : null) ??
-                    configuredBitrate ??
-                    128000;
-
-                if (configuredBitrate.HasValue)
-                {
-                    transcodingBitrate = Math.Min(configuredBitrate.Value, transcodingBitrate);
-                }
-
-                var longBitrate = Math.Min(transcodingBitrate, playlistItem.AudioBitrate ?? transcodingBitrate);
-                playlistItem.AudioBitrate = longBitrate > int.MaxValue ? int.MaxValue : Convert.ToInt32(longBitrate);
-            }
-
-            playlistItem.TranscodeReasons = transcodeReasons;
-            return playlistItem;
-        }
-
-        private (DirectPlayProfile Profile, PlayMethod? PlayMethod, TranscodeReason TranscodeReasons) GetAudioDirectPlayProfile(MediaSourceInfo item, MediaStream audioStream, AudioOptions options)
+        private (DirectPlayProfile Profile, PlayMethod? PlayMethod, TranscodeReason TranscodeReasons) GetAudioDirectPlayProfile(MediaSourceInfo item, MediaStream audioStream, MediaOptions options)
         {
         {
             var directPlayProfile = options.Profile.DirectPlayProfiles
             var directPlayProfile = options.Profile.DirectPlayProfiles
                 .FirstOrDefault(x => x.Type == DlnaProfileType.Audio && IsAudioDirectPlaySupported(x, item, audioStream));
                 .FirstOrDefault(x => x.Type == DlnaProfileType.Audio && IsAudioDirectPlaySupported(x, item, audioStream));
@@ -388,7 +417,7 @@ namespace MediaBrowser.Model.Dlna
             // If device requirements are satisfied then allow both direct stream and direct play
             // If device requirements are satisfied then allow both direct stream and direct play
             if (item.SupportsDirectPlay)
             if (item.SupportsDirectPlay)
             {
             {
-                if (IsItemBitrateEligibleForDirectPlayback(item, options.GetMaxBitrate(true) ?? 0, PlayMethod.DirectPlay))
+                if (!IsBitrateLimitExceeded(item, options.GetMaxBitrate(true) ?? 0))
                 {
                 {
                     if (options.EnableDirectPlay)
                     if (options.EnableDirectPlay)
                     {
                     {
@@ -404,7 +433,7 @@ namespace MediaBrowser.Model.Dlna
             // While options takes the network and other factors into account. Only applies to direct stream
             // While options takes the network and other factors into account. Only applies to direct stream
             if (item.SupportsDirectStream)
             if (item.SupportsDirectStream)
             {
             {
-                if (IsItemBitrateEligibleForDirectPlayback(item, options.GetMaxBitrate(true) ?? 0, PlayMethod.DirectStream))
+                if (!IsBitrateLimitExceeded(item, options.GetMaxBitrate(true) ?? 0))
                 {
                 {
                     if (options.EnableDirectStream)
                     if (options.EnableDirectStream)
                     {
                     {
@@ -427,7 +456,6 @@ namespace MediaBrowser.Model.Dlna
             var containerSupported = false;
             var containerSupported = false;
             var audioSupported = false;
             var audioSupported = false;
             var videoSupported = false;
             var videoSupported = false;
-            TranscodeReason reasons = 0;
 
 
             foreach (var profile in directPlayProfiles)
             foreach (var profile in directPlayProfiles)
             {
             {
@@ -447,6 +475,7 @@ namespace MediaBrowser.Model.Dlna
                 }
                 }
             }
             }
 
 
+            TranscodeReason reasons = 0;
             if (!containerSupported)
             if (!containerSupported)
             {
             {
                 reasons |= TranscodeReason.ContainerNotSupported;
                 reasons |= TranscodeReason.ContainerNotSupported;
@@ -547,7 +576,7 @@ namespace MediaBrowser.Model.Dlna
             }
             }
         }
         }
 
 
-        private static void SetStreamInfoOptionsFromDirectPlayProfile(VideoOptions options, MediaSourceInfo item, StreamInfo playlistItem, DirectPlayProfile directPlayProfile)
+        private static void SetStreamInfoOptionsFromDirectPlayProfile(MediaOptions options, MediaSourceInfo item, StreamInfo playlistItem, DirectPlayProfile directPlayProfile)
         {
         {
             var container = NormalizeMediaSourceFormatIntoSingleContainer(item.Container, options.Profile, DlnaProfileType.Video, directPlayProfile);
             var container = NormalizeMediaSourceFormatIntoSingleContainer(item.Container, options.Profile, DlnaProfileType.Video, directPlayProfile);
             var protocol = "http";
             var protocol = "http";
@@ -562,7 +591,7 @@ namespace MediaBrowser.Model.Dlna
             playlistItem.AudioCodecs = ContainerProfile.SplitValue(directPlayProfile.AudioCodec);
             playlistItem.AudioCodecs = ContainerProfile.SplitValue(directPlayProfile.AudioCodec);
         }
         }
 
 
-        private StreamInfo BuildVideoItem(MediaSourceInfo item, VideoOptions options)
+        private StreamInfo BuildVideoItem(MediaSourceInfo item, MediaOptions options)
         {
         {
             ArgumentNullException.ThrowIfNull(item);
             ArgumentNullException.ThrowIfNull(item);
 
 
@@ -601,11 +630,15 @@ namespace MediaBrowser.Model.Dlna
 
 
             var videoStream = item.VideoStream;
             var videoStream = item.VideoStream;
 
 
-            var directPlayBitrateEligibility = IsBitrateEligibleForDirectPlayback(item, options.GetMaxBitrate(false) ?? 0, options, PlayMethod.DirectPlay);
-            var directStreamBitrateEligibility = IsBitrateEligibleForDirectPlayback(item, options.GetMaxBitrate(false) ?? 0, options, PlayMethod.DirectStream);
-            bool isEligibleForDirectPlay = options.EnableDirectPlay && (options.ForceDirectPlay || directPlayBitrateEligibility == 0);
-            bool isEligibleForDirectStream = options.EnableDirectStream && (options.ForceDirectStream || directStreamBitrateEligibility == 0);
-            var transcodeReasons = directPlayBitrateEligibility | directStreamBitrateEligibility;
+            var bitrateLimitExceeded = IsBitrateLimitExceeded(item, options.GetMaxBitrate(false) ?? 0);
+            var isEligibleForDirectPlay = options.EnableDirectPlay && (options.ForceDirectPlay || !bitrateLimitExceeded);
+            var isEligibleForDirectStream = options.EnableDirectStream && (options.ForceDirectStream || !bitrateLimitExceeded);
+            TranscodeReason transcodeReasons = 0;
+
+            if (bitrateLimitExceeded)
+            {
+                transcodeReasons = TranscodeReason.ContainerBitrateExceedsLimit;
+            }
 
 
             _logger.LogDebug(
             _logger.LogDebug(
                 "Profile: {0}, Path: {1}, isEligibleForDirectPlay: {2}, isEligibleForDirectStream: {3}",
                 "Profile: {0}, Path: {1}, isEligibleForDirectPlay: {2}, isEligibleForDirectStream: {3}",
@@ -702,7 +735,7 @@ namespace MediaBrowser.Model.Dlna
                 }
                 }
             }
             }
 
 
-            _logger.LogInformation(
+            _logger.LogDebug(
                 "StreamBuilder.BuildVideoItem( Profile={0}, Path={1}, AudioStreamIndex={2}, SubtitleStreamIndex={3} ) => ( PlayMethod={4}, TranscodeReason={5} ) {6}",
                 "StreamBuilder.BuildVideoItem( Profile={0}, Path={1}, AudioStreamIndex={2}, SubtitleStreamIndex={3} ) => ( PlayMethod={4}, TranscodeReason={5} ) {6}",
                 options.Profile.Name ?? "Anonymous Profile",
                 options.Profile.Name ?? "Anonymous Profile",
                 item.Path ?? "Unknown path",
                 item.Path ?? "Unknown path",
@@ -716,7 +749,7 @@ namespace MediaBrowser.Model.Dlna
             return playlistItem;
             return playlistItem;
         }
         }
 
 
-        private TranscodingProfile GetVideoTranscodeProfile(MediaSourceInfo item, VideoOptions options, MediaStream videoStream, MediaStream audioStream, IEnumerable<MediaStream> candidateAudioStreams, MediaStream subtitleStream, StreamInfo playlistItem)
+        private TranscodingProfile GetVideoTranscodeProfile(MediaSourceInfo item, MediaOptions options, MediaStream videoStream, MediaStream audioStream, IEnumerable<MediaStream> candidateAudioStreams, MediaStream subtitleStream, StreamInfo playlistItem)
         {
         {
             if (!(item.SupportsTranscoding || item.SupportsDirectStream))
             if (!(item.SupportsTranscoding || item.SupportsDirectStream))
             {
             {
@@ -763,7 +796,7 @@ namespace MediaBrowser.Model.Dlna
             return transcodingProfiles.FirstOrDefault();
             return transcodingProfiles.FirstOrDefault();
         }
         }
 
 
-        private void BuildStreamVideoItem(StreamInfo playlistItem, VideoOptions options, MediaSourceInfo item, MediaStream videoStream, MediaStream audioStream, IEnumerable<MediaStream> candidateAudioStreams, string container, string videoCodec, string audioCodec)
+        private void BuildStreamVideoItem(StreamInfo playlistItem, MediaOptions options, MediaSourceInfo item, MediaStream videoStream, MediaStream audioStream, IEnumerable<MediaStream> candidateAudioStreams, string container, string videoCodec, string audioCodec)
         {
         {
             // Prefer matching video codecs
             // Prefer matching video codecs
             var videoCodecs = ContainerProfile.SplitValue(videoCodec);
             var videoCodecs = ContainerProfile.SplitValue(videoCodec);
@@ -867,7 +900,7 @@ namespace MediaBrowser.Model.Dlna
             // Honor requested max channels
             // Honor requested max channels
             playlistItem.GlobalMaxAudioChannels = options.MaxAudioChannels;
             playlistItem.GlobalMaxAudioChannels = options.MaxAudioChannels;
 
 
-            int audioBitrate = GetAudioBitrate(options.GetMaxBitrate(false) ?? 0, playlistItem.TargetAudioCodec, audioStream, playlistItem);
+            int audioBitrate = GetAudioBitrate(options.GetMaxBitrate(true) ?? 0, playlistItem.TargetAudioCodec, audioStream, playlistItem);
             playlistItem.AudioBitrate = Math.Min(playlistItem.AudioBitrate ?? audioBitrate, audioBitrate);
             playlistItem.AudioBitrate = Math.Min(playlistItem.AudioBitrate ?? audioBitrate, audioBitrate);
 
 
             bool? isSecondaryAudio = audioStream is null ? null : item.IsSecondaryAudio(audioStream);
             bool? isSecondaryAudio = audioStream is null ? null : item.IsSecondaryAudio(audioStream);
@@ -882,14 +915,14 @@ namespace MediaBrowser.Model.Dlna
                     i.ContainsAnyCodec(audioCodec, container) &&
                     i.ContainsAnyCodec(audioCodec, container) &&
                     i.ApplyConditions.All(applyCondition => ConditionProcessor.IsVideoAudioConditionSatisfied(applyCondition, audioChannels, inputAudioBitrate, inputAudioSampleRate, inputAudioBitDepth, audioProfile, isSecondaryAudio)));
                     i.ApplyConditions.All(applyCondition => ConditionProcessor.IsVideoAudioConditionSatisfied(applyCondition, audioChannels, inputAudioBitrate, inputAudioSampleRate, inputAudioBitDepth, audioProfile, isSecondaryAudio)));
             isFirstAppliedCodecProfile = true;
             isFirstAppliedCodecProfile = true;
-            foreach (var i in appliedAudioConditions)
+            foreach (var codecProfile in appliedAudioConditions)
             {
             {
                 var transcodingAudioCodecs = ContainerProfile.SplitValue(audioCodec);
                 var transcodingAudioCodecs = ContainerProfile.SplitValue(audioCodec);
                 foreach (var transcodingAudioCodec in transcodingAudioCodecs)
                 foreach (var transcodingAudioCodec in transcodingAudioCodecs)
                 {
                 {
-                    if (i.ContainsAnyCodec(transcodingAudioCodec, container))
+                    if (codecProfile.ContainsAnyCodec(transcodingAudioCodec, container))
                     {
                     {
-                        ApplyTranscodingConditions(playlistItem, i.Conditions, transcodingAudioCodec, true, isFirstAppliedCodecProfile);
+                        ApplyTranscodingConditions(playlistItem, codecProfile.Conditions, transcodingAudioCodec, true, isFirstAppliedCodecProfile);
                         isFirstAppliedCodecProfile = false;
                         isFirstAppliedCodecProfile = false;
                         break;
                         break;
                     }
                     }
@@ -1050,7 +1083,7 @@ namespace MediaBrowser.Model.Dlna
         }
         }
 
 
         private (DirectPlayProfile Profile, PlayMethod? PlayMethod, int? AudioStreamIndex, TranscodeReason TranscodeReasons) GetVideoDirectPlayProfile(
         private (DirectPlayProfile Profile, PlayMethod? PlayMethod, int? AudioStreamIndex, TranscodeReason TranscodeReasons) GetVideoDirectPlayProfile(
-            VideoOptions options,
+            MediaOptions options,
             MediaSourceInfo mediaSource,
             MediaSourceInfo mediaSource,
             MediaStream videoStream,
             MediaStream videoStream,
             MediaStream audioStream,
             MediaStream audioStream,
@@ -1237,7 +1270,7 @@ namespace MediaBrowser.Model.Dlna
             return (Profile: null, PlayMethod: null, AudioStreamIndex: null, TranscodeReasons: failureReasons);
             return (Profile: null, PlayMethod: null, AudioStreamIndex: null, TranscodeReasons: failureReasons);
         }
         }
 
 
-        private TranscodeReason CheckVideoAudioStreamDirectPlay(VideoOptions options, MediaSourceInfo mediaSource, string container, MediaStream audioStream)
+        private TranscodeReason CheckVideoAudioStreamDirectPlay(MediaOptions options, MediaSourceInfo mediaSource, string container, MediaStream audioStream)
         {
         {
             var profile = options.Profile;
             var profile = options.Profile;
             var audioFailureConditions = GetProfileConditionsForVideoAudio(profile.CodecProfiles, container, audioStream.Codec, audioStream.Channels, audioStream.BitRate, audioStream.SampleRate, audioStream.BitDepth, audioStream.Profile, mediaSource.IsSecondaryAudio(audioStream));
             var audioFailureConditions = GetProfileConditionsForVideoAudio(profile.CodecProfiles, container, audioStream.Codec, audioStream.Channels, audioStream.BitRate, audioStream.SampleRate, audioStream.BitDepth, audioStream.Profile, mediaSource.IsSecondaryAudio(audioStream));
@@ -1274,23 +1307,17 @@ namespace MediaBrowser.Model.Dlna
                 mediaSource.Path ?? "Unknown path");
                 mediaSource.Path ?? "Unknown path");
         }
         }
 
 
-        private TranscodeReason IsBitrateEligibleForDirectPlayback(
-            MediaSourceInfo item,
-            long maxBitrate,
-            VideoOptions options,
-            PlayMethod playMethod)
-        {
-            bool result = IsItemBitrateEligibleForDirectPlayback(item, maxBitrate, playMethod);
-            if (!result)
-            {
-                return TranscodeReason.ContainerBitrateExceedsLimit;
-            }
-            else
-            {
-                return 0;
-            }
-        }
-
+        /// <summary>
+        /// Normalizes input container.
+        /// </summary>
+        /// <param name="mediaSource">The <see cref="MediaSourceInfo"/>.</param>
+        /// <param name="subtitleStream">The <see cref="MediaStream"/> of the subtitle stream.</param>
+        /// <param name="subtitleProfiles">The list of supported <see cref="SubtitleProfile"/>s.</param>
+        /// <param name="playMethod">The <see cref="PlayMethod"/>.</param>
+        /// <param name="transcoderSupport">The <see cref="ITranscoderSupport"/>.</param>
+        /// <param name="outputContainer">The output container.</param>
+        /// <param name="transcodingSubProtocol">The subtitle transoding protocol.</param>
+        /// <returns>The the normalized input container.</returns>
         public static SubtitleProfile GetSubtitleProfile(
         public static SubtitleProfile GetSubtitleProfile(
             MediaSourceInfo mediaSource,
             MediaSourceInfo mediaSource,
             MediaStream subtitleStream,
             MediaStream subtitleStream,
@@ -1448,12 +1475,12 @@ namespace MediaBrowser.Model.Dlna
             return null;
             return null;
         }
         }
 
 
-        private bool IsItemBitrateEligibleForDirectPlayback(MediaSourceInfo item, long maxBitrate, PlayMethod playMethod)
+        private bool IsBitrateLimitExceeded(MediaSourceInfo item, long maxBitrate)
         {
         {
             // Don't restrict bitrate if item is remote.
             // Don't restrict bitrate if item is remote.
             if (item.IsRemote)
             if (item.IsRemote)
             {
             {
-                return true;
+                return false;
             }
             }
 
 
             // If no maximum bitrate is set, default to no maximum bitrate.
             // If no maximum bitrate is set, default to no maximum bitrate.
@@ -1465,40 +1492,22 @@ namespace MediaBrowser.Model.Dlna
             if (itemBitrate > requestedMaxBitrate)
             if (itemBitrate > requestedMaxBitrate)
             {
             {
                 _logger.LogDebug(
                 _logger.LogDebug(
-                    "Bitrate exceeds {PlayBackMethod} limit: media bitrate: {MediaBitrate}, max bitrate: {MaxBitrate}",
-                    playMethod,
+                    "Bitrate exceeds limit: media bitrate: {MediaBitrate}, max bitrate: {MaxBitrate}",
                     itemBitrate,
                     itemBitrate,
                     requestedMaxBitrate);
                     requestedMaxBitrate);
-                return false;
-            }
-
-            return true;
-        }
-
-        private static void ValidateInput(VideoOptions options)
-        {
-            ValidateAudioInput(options);
-
-            if (options.AudioStreamIndex.HasValue && string.IsNullOrEmpty(options.MediaSourceId))
-            {
-                throw new ArgumentException("MediaSourceId is required when a specific audio stream is requested");
+                return true;
             }
             }
 
 
-            if (options.SubtitleStreamIndex.HasValue && string.IsNullOrEmpty(options.MediaSourceId))
-            {
-                throw new ArgumentException("MediaSourceId is required when a specific subtitle stream is requested");
-            }
+            return false;
         }
         }
 
 
-        private static void ValidateAudioInput(AudioOptions options)
+        private static void ValidateMediaOptions(MediaOptions options, bool isMediaSource)
         {
         {
             if (options.ItemId.Equals(default))
             if (options.ItemId.Equals(default))
             {
             {
-                throw new ArgumentException("ItemId is required");
+                ArgumentException.ThrowIfNullOrEmpty(options.DeviceId);
             }
             }
 
 
-            ArgumentException.ThrowIfNullOrEmpty(options.DeviceId);
-
             if (options.Profile is null)
             if (options.Profile is null)
             {
             {
                 throw new ArgumentException("Profile is required");
                 throw new ArgumentException("Profile is required");
@@ -1508,6 +1517,19 @@ namespace MediaBrowser.Model.Dlna
             {
             {
                 throw new ArgumentException("MediaSources is required");
                 throw new ArgumentException("MediaSources is required");
             }
             }
+
+            if (isMediaSource)
+            {
+                if (options.AudioStreamIndex.HasValue && string.IsNullOrEmpty(options.MediaSourceId))
+                {
+                    throw new ArgumentException("MediaSourceId is required when a specific audio stream is requested");
+                }
+
+                if (options.SubtitleStreamIndex.HasValue && string.IsNullOrEmpty(options.MediaSourceId))
+                {
+                    throw new ArgumentException("MediaSourceId is required when a specific subtitle stream is requested");
+                }
+            }
         }
         }
 
 
         private static IEnumerable<ProfileCondition> GetProfileConditionsForVideoAudio(
         private static IEnumerable<ProfileCondition> GetProfileConditionsForVideoAudio(
@@ -1825,8 +1847,8 @@ namespace MediaBrowser.Model.Dlna
                                 continue;
                                 continue;
                             }
                             }
 
 
-                            // change from split by | to comma
-                            // strip spaces to avoid having to encode
+                            // Change from split by | to comma
+                            // Strip spaces to avoid having to encode
                             var values = value
                             var values = value
                                 .Split('|', StringSplitOptions.RemoveEmptyEntries);
                                 .Split('|', StringSplitOptions.RemoveEmptyEntries);
 
 

+ 0 - 16
MediaBrowser.Model/Dlna/VideoOptions.cs

@@ -1,16 +0,0 @@
-#pragma warning disable CS1591
-
-namespace MediaBrowser.Model.Dlna
-{
-    /// <summary>
-    /// Class VideoOptions.
-    /// </summary>
-    public class VideoOptions : AudioOptions
-    {
-        public int? AudioStreamIndex { get; set; }
-
-        public int? SubtitleStreamIndex { get; set; }
-
-        public bool AllowVideoStreamCopy { get; set; }
-    }
-}

+ 51 - 50
tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs

@@ -164,7 +164,7 @@ namespace Jellyfin.Model.Tests
         [InlineData("Tizen4-4K-5.1", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.DirectPlay)]
         [InlineData("Tizen4-4K-5.1", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.DirectPlay)]
         public async Task BuildVideoItemSimple(string deviceName, string mediaSource, PlayMethod? playMethod, TranscodeReason why = (TranscodeReason)0, string transcodeMode = "DirectStream", string transcodeProtocol = "")
         public async Task BuildVideoItemSimple(string deviceName, string mediaSource, PlayMethod? playMethod, TranscodeReason why = (TranscodeReason)0, string transcodeMode = "DirectStream", string transcodeProtocol = "")
         {
         {
-            var options = await GetVideoOptions(deviceName, mediaSource);
+            var options = await GetMediaOptions(deviceName, mediaSource);
             BuildVideoItemSimpleTest(options, playMethod, why, transcodeMode, transcodeProtocol);
             BuildVideoItemSimpleTest(options, playMethod, why, transcodeMode, transcodeProtocol);
         }
         }
 
 
@@ -262,7 +262,7 @@ namespace Jellyfin.Model.Tests
         [InlineData("Tizen4-4K-5.1", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.DirectPlay)]
         [InlineData("Tizen4-4K-5.1", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.DirectPlay)]
         public async Task BuildVideoItemWithFirstExplicitStream(string deviceName, string mediaSource, PlayMethod? playMethod, TranscodeReason why = (TranscodeReason)0, string transcodeMode = "DirectStream", string transcodeProtocol = "")
         public async Task BuildVideoItemWithFirstExplicitStream(string deviceName, string mediaSource, PlayMethod? playMethod, TranscodeReason why = (TranscodeReason)0, string transcodeMode = "DirectStream", string transcodeProtocol = "")
         {
         {
-            var options = await GetVideoOptions(deviceName, mediaSource);
+            var options = await GetMediaOptions(deviceName, mediaSource);
             options.AudioStreamIndex = 1;
             options.AudioStreamIndex = 1;
             options.SubtitleStreamIndex = options.MediaSources[0].MediaStreams.Count - 1;
             options.SubtitleStreamIndex = options.MediaSources[0].MediaStreams.Count - 1;
 
 
@@ -298,7 +298,7 @@ namespace Jellyfin.Model.Tests
         [InlineData("Tizen4-4K-5.1", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")]
         [InlineData("Tizen4-4K-5.1", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")]
         public async Task BuildVideoItemWithDirectPlayExplicitStreams(string deviceName, string mediaSource, PlayMethod? playMethod, TranscodeReason why = (TranscodeReason)0, string transcodeMode = "DirectStream", string transcodeProtocol = "")
         public async Task BuildVideoItemWithDirectPlayExplicitStreams(string deviceName, string mediaSource, PlayMethod? playMethod, TranscodeReason why = (TranscodeReason)0, string transcodeMode = "DirectStream", string transcodeProtocol = "")
         {
         {
-            var options = await GetVideoOptions(deviceName, mediaSource);
+            var options = await GetMediaOptions(deviceName, mediaSource);
             var streamCount = options.MediaSources[0].MediaStreams.Count;
             var streamCount = options.MediaSources[0].MediaStreams.Count;
             if (streamCount > 0)
             if (streamCount > 0)
             {
             {
@@ -311,7 +311,7 @@ namespace Jellyfin.Model.Tests
             Assert.Equal(streamInfo?.SubtitleStreamIndex, options.SubtitleStreamIndex);
             Assert.Equal(streamInfo?.SubtitleStreamIndex, options.SubtitleStreamIndex);
         }
         }
 
 
-        private StreamInfo? BuildVideoItemSimpleTest(VideoOptions options, PlayMethod? playMethod, TranscodeReason why, string transcodeMode, string transcodeProtocol)
+        private StreamInfo? BuildVideoItemSimpleTest(MediaOptions options, PlayMethod? playMethod, TranscodeReason why, string transcodeMode, string transcodeProtocol)
         {
         {
             if (string.IsNullOrEmpty(transcodeProtocol))
             if (string.IsNullOrEmpty(transcodeProtocol))
             {
             {
@@ -320,28 +320,28 @@ namespace Jellyfin.Model.Tests
 
 
             var builder = GetStreamBuilder();
             var builder = GetStreamBuilder();
 
 
-            var val = builder.BuildVideoItem(options);
-            Assert.NotNull(val);
+            var streamInfo = builder.GetOptimalVideoStream(options);
+            Assert.NotNull(streamInfo);
 
 
             if (playMethod is not null)
             if (playMethod is not null)
             {
             {
-                Assert.Equal(playMethod, val.PlayMethod);
+                Assert.Equal(playMethod, streamInfo.PlayMethod);
             }
             }
 
 
-            Assert.Equal(why, val.TranscodeReasons);
+            Assert.Equal(why, streamInfo.TranscodeReasons);
 
 
             var audioStreamIndexInput = options.AudioStreamIndex;
             var audioStreamIndexInput = options.AudioStreamIndex;
-            var targetVideoStream = val.TargetVideoStream;
-            var targetAudioStream = val.TargetAudioStream;
+            var targetVideoStream = streamInfo.TargetVideoStream;
+            var targetAudioStream = streamInfo.TargetAudioStream;
 
 
-            var mediaSource = options.MediaSources.First(source => source.Id == val.MediaSourceId);
+            var mediaSource = options.MediaSources.First(source => source.Id == streamInfo.MediaSourceId);
             Assert.NotNull(mediaSource);
             Assert.NotNull(mediaSource);
             var videoStreams = mediaSource.MediaStreams.Where(stream => stream.Type == MediaStreamType.Video);
             var videoStreams = mediaSource.MediaStreams.Where(stream => stream.Type == MediaStreamType.Video);
             var audioStreams = mediaSource.MediaStreams.Where(stream => stream.Type == MediaStreamType.Audio);
             var audioStreams = mediaSource.MediaStreams.Where(stream => stream.Type == MediaStreamType.Audio);
             // TODO: Check AudioStreamIndex vs options.AudioStreamIndex
             // TODO: Check AudioStreamIndex vs options.AudioStreamIndex
             var inputAudioStream = mediaSource.GetDefaultAudioStream(audioStreamIndexInput ?? mediaSource.DefaultAudioStreamIndex);
             var inputAudioStream = mediaSource.GetDefaultAudioStream(audioStreamIndexInput ?? mediaSource.DefaultAudioStreamIndex);
 
 
-            var uri = ParseUri(val);
+            var uri = ParseUri(streamInfo);
 
 
             if (playMethod == PlayMethod.DirectPlay)
             if (playMethod == PlayMethod.DirectPlay)
             {
             {
@@ -351,98 +351,99 @@ namespace Jellyfin.Model.Tests
                 // Assert.Contains(uri.Extension, containers);
                 // Assert.Contains(uri.Extension, containers);
 
 
                 // Check expected video codec (1)
                 // Check expected video codec (1)
-                Assert.Contains(targetVideoStream.Codec, val.TargetVideoCodec);
-                Assert.Single(val.TargetVideoCodec);
+                Assert.Contains(targetVideoStream.Codec, streamInfo.TargetVideoCodec);
+                Assert.Single(streamInfo.TargetVideoCodec);
 
 
                 // Check expected audio codecs (1)
                 // Check expected audio codecs (1)
-                Assert.Contains(targetAudioStream.Codec, val.TargetAudioCodec);
-                Assert.Single(val.TargetAudioCodec);
+                Assert.Contains(targetAudioStream.Codec, streamInfo.TargetAudioCodec);
+                Assert.Single(streamInfo.TargetAudioCodec);
                 // Assert.Single(val.AudioCodecs);
                 // Assert.Single(val.AudioCodecs);
 
 
                 if (transcodeMode.Equals("DirectStream", StringComparison.Ordinal))
                 if (transcodeMode.Equals("DirectStream", StringComparison.Ordinal))
                 {
                 {
-                    Assert.Equal(val.Container, uri.Extension);
+                    Assert.Equal(streamInfo.Container, uri.Extension);
                 }
                 }
             }
             }
             else if (playMethod == PlayMethod.DirectStream || playMethod == PlayMethod.Transcode)
             else if (playMethod == PlayMethod.DirectStream || playMethod == PlayMethod.Transcode)
             {
             {
-                Assert.NotNull(val.Container);
-                Assert.NotEmpty(val.VideoCodecs);
-                Assert.NotEmpty(val.AudioCodecs);
+                Assert.NotNull(streamInfo.Container);
+                Assert.NotEmpty(streamInfo.VideoCodecs);
+                Assert.NotEmpty(streamInfo.AudioCodecs);
 
 
                 // Check expected container (todo: this could be a test param)
                 // Check expected container (todo: this could be a test param)
                 if (transcodeProtocol.Equals("http", StringComparison.Ordinal))
                 if (transcodeProtocol.Equals("http", StringComparison.Ordinal))
                 {
                 {
                     // Assert.Equal("webm", val.Container);
                     // Assert.Equal("webm", val.Container);
-                    Assert.Equal(val.Container, uri.Extension);
+                    Assert.Equal(streamInfo.Container, uri.Extension);
                     Assert.Equal("stream", uri.Filename);
                     Assert.Equal("stream", uri.Filename);
-                    Assert.Equal("http", val.SubProtocol);
+                    Assert.Equal("http", streamInfo.SubProtocol);
                 }
                 }
                 else if (transcodeProtocol.Equals("HLS.mp4", StringComparison.Ordinal))
                 else if (transcodeProtocol.Equals("HLS.mp4", StringComparison.Ordinal))
                 {
                 {
-                    Assert.Equal("mp4", val.Container);
+                    Assert.Equal("mp4", streamInfo.Container);
                     Assert.Equal("m3u8", uri.Extension);
                     Assert.Equal("m3u8", uri.Extension);
                     Assert.Equal("master", uri.Filename);
                     Assert.Equal("master", uri.Filename);
-                    Assert.Equal("hls", val.SubProtocol);
+                    Assert.Equal("hls", streamInfo.SubProtocol);
                 }
                 }
                 else
                 else
                 {
                 {
-                    Assert.Equal("ts", val.Container);
+                    Assert.Equal("ts", streamInfo.Container);
                     Assert.Equal("m3u8", uri.Extension);
                     Assert.Equal("m3u8", uri.Extension);
                     Assert.Equal("master", uri.Filename);
                     Assert.Equal("master", uri.Filename);
-                    Assert.Equal("hls", val.SubProtocol);
+                    Assert.Equal("hls", streamInfo.SubProtocol);
                 }
                 }
 
 
                 // Full transcode
                 // Full transcode
                 if (transcodeMode.Equals("Transcode", StringComparison.Ordinal))
                 if (transcodeMode.Equals("Transcode", StringComparison.Ordinal))
                 {
                 {
-                    if ((val.TranscodeReasons & (StreamBuilder.ContainerReasons | TranscodeReason.DirectPlayError)) == 0)
+                    if ((streamInfo.TranscodeReasons & (StreamBuilder.ContainerReasons | TranscodeReason.DirectPlayError)) == 0)
                     {
                     {
                         Assert.All(
                         Assert.All(
                             videoStreams,
                             videoStreams,
-                            stream => Assert.DoesNotContain(stream.Codec, val.VideoCodecs));
+                            stream => Assert.DoesNotContain(stream.Codec, streamInfo.VideoCodecs));
                     }
                     }
 
 
-                    // TODO: Fill out tests here
+                    // TODO: fill out tests here
                 }
                 }
 
 
                 // DirectStream and Remux
                 // DirectStream and Remux
                 else
                 else
                 {
                 {
                     // Check expected video codec (1)
                     // Check expected video codec (1)
-                    Assert.Contains(targetVideoStream.Codec, val.TargetVideoCodec);
-                    Assert.Single(val.TargetVideoCodec);
+                    Assert.Contains(targetVideoStream.Codec, streamInfo.TargetVideoCodec);
+                    Assert.Single(streamInfo.TargetVideoCodec);
 
 
                     if (transcodeMode.Equals("DirectStream", StringComparison.Ordinal))
                     if (transcodeMode.Equals("DirectStream", StringComparison.Ordinal))
                     {
                     {
                         // Check expected audio codecs (1)
                         // Check expected audio codecs (1)
                         if (!targetAudioStream.IsExternal)
                         if (!targetAudioStream.IsExternal)
                         {
                         {
-                            if (val.TranscodeReasons.HasFlag(TranscodeReason.ContainerNotSupported))
+                            // Check expected audio codecs (1)
+                            if (streamInfo.TranscodeReasons.HasFlag(TranscodeReason.ContainerNotSupported))
                             {
                             {
-                                Assert.Contains(targetAudioStream.Codec, val.AudioCodecs);
+                                Assert.Contains(targetAudioStream.Codec, streamInfo.AudioCodecs);
                             }
                             }
                             else
                             else
                             {
                             {
-                                Assert.DoesNotContain(targetAudioStream.Codec, val.AudioCodecs);
+                                Assert.DoesNotContain(targetAudioStream.Codec, streamInfo.AudioCodecs);
                             }
                             }
                         }
                         }
                     }
                     }
                     else if (transcodeMode.Equals("Remux", StringComparison.Ordinal))
                     else if (transcodeMode.Equals("Remux", StringComparison.Ordinal))
                     {
                     {
                         // Check expected audio codecs (1)
                         // Check expected audio codecs (1)
-                        Assert.Contains(targetAudioStream.Codec, val.AudioCodecs);
-                        Assert.Single(val.AudioCodecs);
+                        Assert.Contains(targetAudioStream.Codec, streamInfo.AudioCodecs);
+                        Assert.Single(streamInfo.AudioCodecs);
                     }
                     }
 
 
                     // Video details
                     // Video details
                     var videoStream = targetVideoStream;
                     var videoStream = targetVideoStream;
-                    Assert.False(val.EstimateContentLength);
-                    Assert.Equal(TranscodeSeekInfo.Auto, val.TranscodeSeekInfo);
-                    Assert.Contains(videoStream.Profile?.ToLowerInvariant() ?? string.Empty, val.TargetVideoProfile?.Split(",").Select(s => s.ToLowerInvariant()) ?? Array.Empty<string>());
-                    Assert.Equal(videoStream.Level, val.TargetVideoLevel);
-                    Assert.Equal(videoStream.BitDepth, val.TargetVideoBitDepth);
-                    Assert.InRange(val.VideoBitrate.GetValueOrDefault(), videoStream.BitRate.GetValueOrDefault(), int.MaxValue);
+                    Assert.False(streamInfo.EstimateContentLength);
+                    Assert.Equal(TranscodeSeekInfo.Auto, streamInfo.TranscodeSeekInfo);
+                    Assert.Contains(videoStream.Profile?.ToLowerInvariant() ?? string.Empty, streamInfo.TargetVideoProfile?.Split(",").Select(s => s.ToLowerInvariant()) ?? Array.Empty<string>());
+                    Assert.Equal(videoStream.Level, streamInfo.TargetVideoLevel);
+                    Assert.Equal(videoStream.BitDepth, streamInfo.TargetVideoBitDepth);
+                    Assert.InRange(streamInfo.VideoBitrate.GetValueOrDefault(), videoStream.BitRate.GetValueOrDefault(), int.MaxValue);
 
 
                     // Audio codec not supported
                     // Audio codec not supported
                     if ((why & TranscodeReason.AudioCodecNotSupported) != 0)
                     if ((why & TranscodeReason.AudioCodecNotSupported) != 0)
@@ -453,7 +454,7 @@ namespace Jellyfin.Model.Tests
                             // TODO:fixme
                             // TODO:fixme
                             if (!targetAudioStream.IsExternal)
                             if (!targetAudioStream.IsExternal)
                             {
                             {
-                                Assert.DoesNotContain(targetAudioStream.Codec, val.AudioCodecs);
+                                Assert.DoesNotContain(targetAudioStream.Codec, streamInfo.AudioCodecs);
                             }
                             }
                         }
                         }
 
 
@@ -465,7 +466,7 @@ namespace Jellyfin.Model.Tests
                             {
                             {
                                 if (!stream.IsExternal)
                                 if (!stream.IsExternal)
                                 {
                                 {
-                                    Assert.DoesNotContain(stream.Codec, val.AudioCodecs);
+                                    Assert.DoesNotContain(stream.Codec, streamInfo.AudioCodecs);
                                 }
                                 }
                             });
                             });
                         }
                         }
@@ -474,14 +475,14 @@ namespace Jellyfin.Model.Tests
             }
             }
             else if (playMethod is null)
             else if (playMethod is null)
             {
             {
-                Assert.Null(val.SubProtocol);
+                Assert.Null(streamInfo.SubProtocol);
                 Assert.Equal("stream", uri.Filename);
                 Assert.Equal("stream", uri.Filename);
 
 
-                Assert.False(val.EstimateContentLength);
-                Assert.Equal(TranscodeSeekInfo.Auto, val.TranscodeSeekInfo);
+                Assert.False(streamInfo.EstimateContentLength);
+                Assert.Equal(TranscodeSeekInfo.Auto, streamInfo.TranscodeSeekInfo);
             }
             }
 
 
-            return val;
+            return streamInfo;
         }
         }
 
 
         private static async ValueTask<T> TestData<T>(string name)
         private static async ValueTask<T> TestData<T>(string name)
@@ -507,7 +508,7 @@ namespace Jellyfin.Model.Tests
             return new StreamBuilder(transcodeSupport.Object, logger);
             return new StreamBuilder(transcodeSupport.Object, logger);
         }
         }
 
 
-        private static async ValueTask<VideoOptions> GetVideoOptions(string deviceProfile, params string[] sources)
+        private static async ValueTask<MediaOptions> GetMediaOptions(string deviceProfile, params string[] sources)
         {
         {
             var mediaSources = sources.Select(src => TestData<MediaSourceInfo>(src))
             var mediaSources = sources.Select(src => TestData<MediaSourceInfo>(src))
                 .Select(val => val.Result)
                 .Select(val => val.Result)
@@ -516,7 +517,7 @@ namespace Jellyfin.Model.Tests
 
 
             var dp = await TestData<DeviceProfile>(deviceProfile);
             var dp = await TestData<DeviceProfile>(deviceProfile);
 
 
-            return new VideoOptions()
+            return new MediaOptions()
             {
             {
                 ItemId = new Guid("11D229B7-2D48-4B95-9F9B-49F6AB75E613"),
                 ItemId = new Guid("11D229B7-2D48-4B95-9F9B-49F6AB75E613"),
                 MediaSourceId = mediaSourceId,
                 MediaSourceId = mediaSourceId,