فهرست منبع

Rework PR 6203

Shadowghost 8 ماه پیش
والد
کامیت
2351eeba56

+ 2 - 2
Jellyfin.Server.Implementations/Devices/DeviceManager.cs

@@ -135,8 +135,8 @@ namespace Jellyfin.Server.Implementations.Devices
         {
             IEnumerable<Device> devices = _devices.Values
                 .Where(device => !query.UserId.HasValue || device.UserId.Equals(query.UserId.Value))
-                .Where(device => query.DeviceId == null || device.DeviceId == query.DeviceId)
-                .Where(device => query.AccessToken == null || device.AccessToken == query.AccessToken)
+                .Where(device => query.DeviceId is null || device.DeviceId == query.DeviceId)
+                .Where(device => query.AccessToken is null || device.AccessToken == query.AccessToken)
                 .OrderBy(d => d.Id)
                 .ToList();
             var count = devices.Count();

+ 78 - 58
MediaBrowser.Model/Dlna/CodecProfile.cs

@@ -1,74 +1,94 @@
-#nullable disable
-#pragma warning disable CS1591
-
 using System;
+using System.Collections.Generic;
+using System.Linq;
 using System.Xml.Serialization;
-using Jellyfin.Extensions;
+using MediaBrowser.Model.Extensions;
+
+namespace MediaBrowser.Model.Dlna;
 
-namespace MediaBrowser.Model.Dlna
+/// <summary>
+/// Defines the <see cref="CodecProfile"/>.
+/// </summary>
+public class CodecProfile
 {
-    public class CodecProfile
+    /// <summary>
+    /// Initializes a new instance of the <see cref="CodecProfile"/> class.
+    /// </summary>
+    public CodecProfile()
     {
-        public CodecProfile()
-        {
-            Conditions = Array.Empty<ProfileCondition>();
-            ApplyConditions = Array.Empty<ProfileCondition>();
-        }
-
-        [XmlAttribute("type")]
-        public CodecType Type { get; set; }
-
-        public ProfileCondition[] Conditions { get; set; }
-
-        public ProfileCondition[] ApplyConditions { get; set; }
-
-        [XmlAttribute("codec")]
-        public string Codec { get; set; }
+        Conditions = [];
+        ApplyConditions = [];
+    }
 
-        [XmlAttribute("container")]
-        public string Container { get; set; }
+    /// <summary>
+    /// Gets or sets the <see cref="CodecType"/> which this container must meet.
+    /// </summary>
+    [XmlAttribute("type")]
+    public CodecType Type { get; set; }
 
-        [XmlAttribute("subcontainer")]
-        public string SubContainer { get; set; }
+    /// <summary>
+    /// Gets or sets the list of <see cref="ProfileCondition"/> which this profile must meet.
+    /// </summary>
+    public ProfileCondition[] Conditions { get; set; }
 
-        public string[] GetCodecs()
-        {
-            return ContainerProfile.SplitValue(Codec);
-        }
+    /// <summary>
+    /// Gets or sets the list of <see cref="ProfileCondition"/> to apply if this profile is met.
+    /// </summary>
+    public ProfileCondition[] ApplyConditions { get; set; }
 
-        private bool ContainsContainer(string container, bool useSubContainer = false)
-        {
-            var containerToCheck = useSubContainer && string.Equals(Container, "hls", StringComparison.OrdinalIgnoreCase) ? SubContainer : Container;
-            return ContainerProfile.ContainsContainer(containerToCheck, container);
-        }
+    /// <summary>
+    /// Gets or sets the codec(s) that this profile applies to.
+    /// </summary>
+    [XmlAttribute("codec")]
+    public string? Codec { get; set; }
 
-        public bool ContainsAnyCodec(string codec, string container, bool useSubContainer = false)
-        {
-            return ContainsAnyCodec(ContainerProfile.SplitValue(codec), container, useSubContainer);
-        }
+    /// <summary>
+    /// Gets or sets the container(s) which this profile will be applied to.
+    /// </summary>
+    [XmlAttribute("container")]
+    public string? Container { get; set; }
 
-        public bool ContainsAnyCodec(string[] codec, string container, bool useSubContainer = false)
-        {
-            if (!ContainsContainer(container, useSubContainer))
-            {
-                return false;
-            }
+    /// <summary>
+    /// Gets or sets the sub-container(s) which this profile will be applied to.
+    /// </summary>
+    [XmlAttribute("subcontainer")]
+    public string? SubContainer { get; set; }
 
-            var codecs = GetCodecs();
-            if (codecs.Length == 0)
-            {
-                return true;
-            }
+    /// <summary>
+    /// Checks to see whether the codecs and containers contain the given parameters.
+    /// </summary>
+    /// <param name="codecs">The codecs to match.</param>
+    /// <param name="container">The container to match.</param>
+    /// <param name="useSubContainer">Consider sub-containers.</param>
+    /// <returns>True if both conditions are met.</returns>
+    public bool ContainsAnyCodec(IReadOnlyList<string> codecs, string? container, bool useSubContainer = false)
+    {
+        var containerToCheck = useSubContainer && string.Equals(Container, "hls", StringComparison.OrdinalIgnoreCase) ? SubContainer : Container;
+        return ContainerHelper.ContainsContainer(containerToCheck, container) && codecs.Any(c => ContainerHelper.ContainsContainer(Codec, false, c));
+    }
 
-            foreach (var val in codec)
-            {
-                if (codecs.Contains(val, StringComparison.OrdinalIgnoreCase))
-                {
-                    return true;
-                }
-            }
+    /// <summary>
+    /// Checks to see whether the codecs and containers contain the given parameters.
+    /// </summary>
+    /// <param name="codec">The codec to match.</param>
+    /// <param name="container">The container to match.</param>
+    /// <param name="useSubContainer">Consider sub-containers.</param>
+    /// <returns>True if both conditions are met.</returns>
+    public bool ContainsAnyCodec(string? codec, string? container, bool useSubContainer = false)
+    {
+        return ContainsAnyCodec(codec.AsSpan(), container, useSubContainer);
+    }
 
-            return false;
-        }
+    /// <summary>
+    /// Checks to see whether the codecs and containers contain the given parameters.
+    /// </summary>
+    /// <param name="codec">The codec to match.</param>
+    /// <param name="container">The container to match.</param>
+    /// <param name="useSubContainer">Consider sub-containers.</param>
+    /// <returns>True if both conditions are met.</returns>
+    public bool ContainsAnyCodec(ReadOnlySpan<char> codec, string? container, bool useSubContainer = false)
+    {
+        var containerToCheck = useSubContainer && string.Equals(Container, "hls", StringComparison.OrdinalIgnoreCase) ? SubContainer : Container;
+        return ContainerHelper.ContainsContainer(containerToCheck, container) && ContainerHelper.ContainsContainer(Codec, false, codec);
     }
 }

+ 41 - 66
MediaBrowser.Model/Dlna/ContainerProfile.cs

@@ -1,74 +1,49 @@
-#pragma warning disable CS1591
+#pragma warning disable CA1819 // Properties should not return arrays
 
 using System;
+using System.Collections.Generic;
 using System.Xml.Serialization;
-using Jellyfin.Extensions;
+using MediaBrowser.Model.Extensions;
 
-namespace MediaBrowser.Model.Dlna
+namespace MediaBrowser.Model.Dlna;
+
+/// <summary>
+/// Defines the <see cref="ContainerProfile"/>.
+/// </summary>
+public class ContainerProfile
 {
-    public class ContainerProfile
+    /// <summary>
+    /// Gets or sets the <see cref="DlnaProfileType"/> which this container must meet.
+    /// </summary>
+    [XmlAttribute("type")]
+    public DlnaProfileType Type { get; set; }
+
+    /// <summary>
+    /// Gets or sets the list of <see cref="ProfileCondition"/> which this container will be applied to.
+    /// </summary>
+    public ProfileCondition[] Conditions { get; set; } = [];
+
+    /// <summary>
+    /// Gets or sets the container(s) which this container must meet.
+    /// </summary>
+    [XmlAttribute("container")]
+    public string? Container { get; set; }
+
+    /// <summary>
+    /// Gets or sets the sub container(s) which this container must meet.
+    /// </summary>
+    [XmlAttribute("subcontainer")]
+    public string? SubContainer { get; set; }
+
+    /// <summary>
+    /// Returns true if an item in <paramref name="container"/> appears in the <see cref="Container"/> property.
+    /// </summary>
+    /// <param name="container">The item to match.</param>
+    /// <param name="useSubContainer">Consider subcontainers.</param>
+    /// <returns>The result of the operation.</returns>
+    public bool ContainsContainer(ReadOnlySpan<char> container, bool useSubContainer = false)
     {
-        [XmlAttribute("type")]
-        public DlnaProfileType Type { get; set; }
-
-        public ProfileCondition[] Conditions { get; set; } = Array.Empty<ProfileCondition>();
-
-        [XmlAttribute("container")]
-        public string Container { get; set; } = string.Empty;
-
-        public static string[] SplitValue(string? value)
-        {
-            if (string.IsNullOrEmpty(value))
-            {
-                return Array.Empty<string>();
-            }
-
-            return value.Split(',', StringSplitOptions.RemoveEmptyEntries);
-        }
-
-        public bool ContainsContainer(string? container)
-        {
-            var containers = SplitValue(Container);
-
-            return ContainsContainer(containers, container);
-        }
-
-        public static bool ContainsContainer(string? profileContainers, string? inputContainer)
-        {
-            var isNegativeList = false;
-            if (profileContainers is not null && profileContainers.StartsWith('-'))
-            {
-                isNegativeList = true;
-                profileContainers = profileContainers.Substring(1);
-            }
-
-            return ContainsContainer(SplitValue(profileContainers), isNegativeList, inputContainer);
-        }
-
-        public static bool ContainsContainer(string[]? profileContainers, string? inputContainer)
-        {
-            return ContainsContainer(profileContainers, false, inputContainer);
-        }
-
-        public static bool ContainsContainer(string[]? profileContainers, bool isNegativeList, string? inputContainer)
-        {
-            if (profileContainers is null || profileContainers.Length == 0)
-            {
-                // Empty profiles always support all containers/codecs
-                return true;
-            }
-
-            var allInputContainers = SplitValue(inputContainer);
-
-            foreach (var container in allInputContainers)
-            {
-                if (profileContainers.Contains(container, StringComparison.OrdinalIgnoreCase))
-                {
-                    return !isNegativeList;
-                }
-            }
-
-            return isNegativeList;
-        }
+        var containerToCheck = useSubContainer && string.Equals(Container, "hls", StringComparison.OrdinalIgnoreCase) ? SubContainer : Container;
+        return ContainerHelper.ContainsContainer(containerToCheck, container);
     }
 }

+ 53 - 56
MediaBrowser.Model/Dlna/DeviceProfile.cs

@@ -1,74 +1,71 @@
 #pragma warning disable CA1819 // Properties should not return arrays
 
 using System;
-using System.Xml.Serialization;
 
-namespace MediaBrowser.Model.Dlna
+namespace MediaBrowser.Model.Dlna;
+
+/// <summary>
+/// A <see cref="DeviceProfile" /> represents a set of metadata which determines which content a certain device is able to play.
+/// <br/>
+/// Specifically, it defines the supported <see cref="ContainerProfiles">containers</see> and
+/// <see cref="CodecProfiles">codecs</see> (video and/or audio, including codec profiles and levels)
+/// the device is able to direct play (without transcoding or remuxing),
+/// as well as which <see cref="TranscodingProfiles">containers/codecs to transcode to</see> in case it isn't.
+/// </summary>
+public class DeviceProfile
 {
     /// <summary>
-    /// A <see cref="DeviceProfile" /> represents a set of metadata which determines which content a certain device is able to play.
-    /// <br/>
-    /// Specifically, it defines the supported <see cref="ContainerProfiles">containers</see> and
-    /// <see cref="CodecProfiles">codecs</see> (video and/or audio, including codec profiles and levels)
-    /// the device is able to direct play (without transcoding or remuxing),
-    /// as well as which <see cref="TranscodingProfiles">containers/codecs to transcode to</see> in case it isn't.
+    /// Gets or sets the name of this device profile. User profiles must have a unique name.
     /// </summary>
-    public class DeviceProfile
-    {
-        /// <summary>
-        /// Gets or sets the name of this device profile.
-        /// </summary>
-        public string? Name { get; set; }
+    public string? Name { get; set; }
 
-        /// <summary>
-        /// Gets or sets the Id.
-        /// </summary>
-        [XmlIgnore]
-        public string? Id { get; set; }
+    /// <summary>
+    /// Gets or sets the unique internal identifier.
+    /// </summary>
+    public Guid Id { get; set; }
 
-        /// <summary>
-        /// Gets or sets the maximum allowed bitrate for all streamed content.
-        /// </summary>
-        public int? MaxStreamingBitrate { get; set; } = 8000000;
+    /// <summary>
+    /// Gets or sets the maximum allowed bitrate for all streamed content.
+    /// </summary>
+    public int? MaxStreamingBitrate { get; set; } = 8000000;
 
-        /// <summary>
-        /// Gets or sets the maximum allowed bitrate for statically streamed content (= direct played files).
-        /// </summary>
-        public int? MaxStaticBitrate { get; set; } = 8000000;
+    /// <summary>
+    /// Gets or sets the maximum allowed bitrate for statically streamed content (= direct played files).
+    /// </summary>
+    public int? MaxStaticBitrate { get; set; } = 8000000;
 
-        /// <summary>
-        /// Gets or sets the maximum allowed bitrate for transcoded music streams.
-        /// </summary>
-        public int? MusicStreamingTranscodingBitrate { get; set; } = 128000;
+    /// <summary>
+    /// Gets or sets the maximum allowed bitrate for transcoded music streams.
+    /// </summary>
+    public int? MusicStreamingTranscodingBitrate { get; set; } = 128000;
 
-        /// <summary>
-        /// Gets or sets the maximum allowed bitrate for statically streamed (= direct played) music files.
-        /// </summary>
-        public int? MaxStaticMusicBitrate { get; set; } = 8000000;
+    /// <summary>
+    /// Gets or sets the maximum allowed bitrate for statically streamed (= direct played) music files.
+    /// </summary>
+    public int? MaxStaticMusicBitrate { get; set; } = 8000000;
 
-        /// <summary>
-        /// Gets or sets the direct play profiles.
-        /// </summary>
-        public DirectPlayProfile[] DirectPlayProfiles { get; set; } = Array.Empty<DirectPlayProfile>();
+    /// <summary>
+    /// Gets or sets the direct play profiles.
+    /// </summary>
+    public DirectPlayProfile[] DirectPlayProfiles { get; set; } = [];
 
-        /// <summary>
-        /// Gets or sets the transcoding profiles.
-        /// </summary>
-        public TranscodingProfile[] TranscodingProfiles { get; set; } = Array.Empty<TranscodingProfile>();
+    /// <summary>
+    /// Gets or sets the transcoding profiles.
+    /// </summary>
+    public TranscodingProfile[] TranscodingProfiles { get; set; } = [];
 
-        /// <summary>
-        /// Gets or sets the container profiles.
-        /// </summary>
-        public ContainerProfile[] ContainerProfiles { get; set; } = Array.Empty<ContainerProfile>();
+    /// <summary>
+    /// Gets or sets the container profiles. Failing to meet these optional conditions causes transcoding to occur.
+    /// </summary>
+    public ContainerProfile[] ContainerProfiles { get; set; } = [];
 
-        /// <summary>
-        /// Gets or sets the codec profiles.
-        /// </summary>
-        public CodecProfile[] CodecProfiles { get; set; } = Array.Empty<CodecProfile>();
+    /// <summary>
+    /// Gets or sets the codec profiles.
+    /// </summary>
+    public CodecProfile[] CodecProfiles { get; set; } = [];
 
-        /// <summary>
-        /// Gets or sets the subtitle profiles.
-        /// </summary>
-        public SubtitleProfile[] SubtitleProfiles { get; set; } = Array.Empty<SubtitleProfile>();
-    }
+    /// <summary>
+    /// Gets or sets the subtitle profiles.
+    /// </summary>
+    public SubtitleProfile[] SubtitleProfiles { get; set; } = [];
 }

+ 54 - 25
MediaBrowser.Model/Dlna/DirectPlayProfile.cs

@@ -1,36 +1,65 @@
-#pragma warning disable CS1591
-
 using System.Xml.Serialization;
+using MediaBrowser.Model.Extensions;
+
+namespace MediaBrowser.Model.Dlna;
 
-namespace MediaBrowser.Model.Dlna
+/// <summary>
+/// Defines the <see cref="DirectPlayProfile"/>.
+/// </summary>
+public class DirectPlayProfile
 {
-    public class DirectPlayProfile
-    {
-        [XmlAttribute("container")]
-        public string? Container { get; set; }
+    /// <summary>
+    /// Gets or sets the container.
+    /// </summary>
+    [XmlAttribute("container")]
+    public string Container { get; set; } = string.Empty;
 
-        [XmlAttribute("audioCodec")]
-        public string? AudioCodec { get; set; }
+    /// <summary>
+    /// Gets or sets the audio codec.
+    /// </summary>
+    [XmlAttribute("audioCodec")]
+    public string? AudioCodec { get; set; }
 
-        [XmlAttribute("videoCodec")]
-        public string? VideoCodec { get; set; }
+    /// <summary>
+    /// Gets or sets the video codec.
+    /// </summary>
+    [XmlAttribute("videoCodec")]
+    public string? VideoCodec { get; set; }
 
-        [XmlAttribute("type")]
-        public DlnaProfileType Type { get; set; }
+    /// <summary>
+    /// Gets or sets the Dlna profile type.
+    /// </summary>
+    [XmlAttribute("type")]
+    public DlnaProfileType Type { get; set; }
 
-        public bool SupportsContainer(string? container)
-        {
-            return ContainerProfile.ContainsContainer(Container, container);
-        }
+    /// <summary>
+    /// Returns whether the <see cref="Container"/> supports the <paramref name="container"/>.
+    /// </summary>
+    /// <param name="container">The container to match against.</param>
+    /// <returns>True if supported.</returns>
+    public bool SupportsContainer(string? container)
+    {
+        return ContainerHelper.ContainsContainer(Container, container);
+    }
 
-        public bool SupportsVideoCodec(string? codec)
-        {
-            return Type == DlnaProfileType.Video && ContainerProfile.ContainsContainer(VideoCodec, codec);
-        }
+    /// <summary>
+    /// Returns whether the <see cref="VideoCodec"/> supports the <paramref name="codec"/>.
+    /// </summary>
+    /// <param name="codec">The codec to match against.</param>
+    /// <returns>True if supported.</returns>
+    public bool SupportsVideoCodec(string? codec)
+    {
+        return Type == DlnaProfileType.Video && ContainerHelper.ContainsContainer(VideoCodec, codec);
+    }
 
-        public bool SupportsAudioCodec(string? codec)
-        {
-            return (Type == DlnaProfileType.Audio || Type == DlnaProfileType.Video) && ContainerProfile.ContainsContainer(AudioCodec, codec);
-        }
+    /// <summary>
+    /// Returns whether the <see cref="AudioCodec"/> supports the <paramref name="codec"/>.
+    /// </summary>
+    /// <param name="codec">The codec to match against.</param>
+    /// <returns>True if supported.</returns>
+    public bool SupportsAudioCodec(string? codec)
+    {
+        // Video profiles can have audio codec restrictions too, therefore incude Video as valid type.
+        return (Type == DlnaProfileType.Audio || Type == DlnaProfileType.Video) && ContainerHelper.ContainsContainer(AudioCodec, codec);
     }
 }

+ 94 - 98
MediaBrowser.Model/Dlna/StreamBuilder.cs

@@ -6,6 +6,7 @@ using Jellyfin.Data.Enums;
 using Jellyfin.Extensions;
 using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Extensions;
 using MediaBrowser.Model.MediaInfo;
 using MediaBrowser.Model.Session;
 using Microsoft.Extensions.Logging;
@@ -27,9 +28,9 @@ namespace MediaBrowser.Model.Dlna
 
         private readonly ILogger _logger;
         private readonly ITranscoderSupport _transcoderSupport;
-        private static readonly string[] _supportedHlsVideoCodecs = new string[] { "h264", "hevc", "vp9", "av1" };
-        private static readonly string[] _supportedHlsAudioCodecsTs = new string[] { "aac", "ac3", "eac3", "mp3" };
-        private static readonly string[] _supportedHlsAudioCodecsMp4 = new string[] { "aac", "ac3", "eac3", "mp3", "alac", "flac", "opus", "dca", "truehd" };
+        private static readonly string[] _supportedHlsVideoCodecs = ["h264", "hevc", "vp9", "av1"];
+        private static readonly string[] _supportedHlsAudioCodecsTs = ["aac", "ac3", "eac3", "mp3"];
+        private static readonly string[] _supportedHlsAudioCodecsMp4 = ["aac", "ac3", "eac3", "mp3", "alac", "flac", "opus", "dca", "truehd"];
 
         /// <summary>
         /// Initializes a new instance of the <see cref="StreamBuilder"/> class.
@@ -51,7 +52,7 @@ namespace MediaBrowser.Model.Dlna
         {
             ValidateMediaOptions(options, false);
 
-            var streams = new List<StreamInfo>();
+            List<StreamInfo> streams = [];
             foreach (var mediaSource in options.MediaSources)
             {
                 if (!(string.IsNullOrEmpty(options.MediaSourceId)
@@ -64,7 +65,7 @@ namespace MediaBrowser.Model.Dlna
                 if (streamInfo is not null)
                 {
                     streamInfo.DeviceId = options.DeviceId;
-                    streamInfo.DeviceProfileId = options.Profile.Id;
+                    streamInfo.DeviceProfileId = options.Profile.Id.ToString("N", CultureInfo.InvariantCulture);
                     streams.Add(streamInfo);
                 }
             }
@@ -129,7 +130,7 @@ namespace MediaBrowser.Model.Dlna
             if (directPlayMethod is PlayMethod.DirectStream)
             {
                 var remuxContainer = item.TranscodingContainer ?? "ts";
-                var supportedHlsContainers = new[] { "ts", "mp4" };
+                string[] supportedHlsContainers = ["ts", "mp4"];
                 // If the container specified for the profile is an HLS supported container, use that container instead, overriding the preference
                 // The client should be responsible to ensure this container is compatible
                 remuxContainer = Array.Exists(supportedHlsContainers, element => string.Equals(element, directPlayInfo.Profile?.Container, StringComparison.OrdinalIgnoreCase)) ? directPlayInfo.Profile?.Container : remuxContainer;
@@ -226,7 +227,7 @@ namespace MediaBrowser.Model.Dlna
                 ? options.MediaSources
                 : options.MediaSources.Where(x => string.Equals(x.Id, options.MediaSourceId, StringComparison.OrdinalIgnoreCase));
 
-            var streams = new List<StreamInfo>();
+            List<StreamInfo> streams = [];
             foreach (var mediaSourceInfo in mediaSources)
             {
                 var streamInfo = BuildVideoItem(mediaSourceInfo, options);
@@ -239,7 +240,7 @@ namespace MediaBrowser.Model.Dlna
             foreach (var stream in streams)
             {
                 stream.DeviceId = options.DeviceId;
-                stream.DeviceProfileId = options.Profile.Id;
+                stream.DeviceProfileId = options.Profile.Id.ToString("N", CultureInfo.InvariantCulture);
             }
 
             return GetOptimalStream(streams, options.GetMaxBitrate(false) ?? 0);
@@ -388,32 +389,33 @@ namespace MediaBrowser.Model.Dlna
         /// <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 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 (profile is null || !inputContainer.Contains(',', StringComparison.OrdinalIgnoreCase))
             {
-                return null;
+                return inputContainer;
             }
 
-            var formats = ContainerProfile.SplitValue(inputContainer);
-
-            if (profile is not null)
+            var formats = ContainerHelper.Split(inputContainer);
+            var playProfiles = playProfile is null ? profile.DirectPlayProfiles : [playProfile];
+            foreach (var format in formats)
             {
-                var playProfiles = playProfile is null ? profile.DirectPlayProfiles : new[] { playProfile };
-                foreach (var format in formats)
+                foreach (var directPlayProfile in playProfiles)
                 {
-                    foreach (var directPlayProfile in playProfiles)
+                    if (directPlayProfile.Type != type)
                     {
-                        if (directPlayProfile.Type == type
-                            && directPlayProfile.SupportsContainer(format))
-                        {
-                            return format;
-                        }
+                        continue;
+                    }
+
+                    var formatStr = format.ToString();
+                    if (directPlayProfile.SupportsContainer(formatStr))
+                    {
+                        return formatStr;
                     }
                 }
             }
 
-            return formats[0];
+            return inputContainer;
         }
 
         private (DirectPlayProfile? Profile, PlayMethod? PlayMethod, TranscodeReason TranscodeReasons) GetAudioDirectPlayProfile(MediaSourceInfo item, MediaStream audioStream, MediaOptions options)
@@ -533,7 +535,6 @@ namespace MediaBrowser.Model.Dlna
         private static int? GetDefaultSubtitleStreamIndex(MediaSourceInfo item, SubtitleProfile[] subtitleProfiles)
         {
             int highestScore = -1;
-
             foreach (var stream in item.MediaStreams)
             {
                 if (stream.Type == MediaStreamType.Subtitle
@@ -544,7 +545,7 @@ namespace MediaBrowser.Model.Dlna
                 }
             }
 
-            var topStreams = new List<MediaStream>();
+            List<MediaStream> topStreams = [];
             foreach (var stream in item.MediaStreams)
             {
                 if (stream.Type == MediaStreamType.Subtitle && stream.Score.HasValue && stream.Score.Value == highestScore)
@@ -623,8 +624,8 @@ namespace MediaBrowser.Model.Dlna
             playlistItem.Container = container;
             playlistItem.SubProtocol = protocol;
 
-            playlistItem.VideoCodecs = new[] { item.VideoStream.Codec };
-            playlistItem.AudioCodecs = ContainerProfile.SplitValue(directPlayProfile?.AudioCodec);
+            playlistItem.VideoCodecs = [item.VideoStream.Codec];
+            playlistItem.AudioCodecs = ContainerHelper.Split(directPlayProfile?.AudioCodec);
         }
 
         private StreamInfo BuildVideoItem(MediaSourceInfo item, MediaOptions options)
@@ -651,7 +652,7 @@ namespace MediaBrowser.Model.Dlna
             }
 
             // Collect candidate audio streams
-            ICollection<MediaStream> candidateAudioStreams = audioStream is null ? Array.Empty<MediaStream>() : new[] { audioStream };
+            ICollection<MediaStream> candidateAudioStreams = audioStream is null ? [] : [audioStream];
             if (!options.AudioStreamIndex.HasValue || options.AudioStreamIndex < 0)
             {
                 if (audioStream?.IsDefault == true)
@@ -702,7 +703,8 @@ namespace MediaBrowser.Model.Dlna
                     directPlayProfile = directPlayInfo.Profile;
                     playlistItem.PlayMethod = directPlay.Value;
                     playlistItem.Container = NormalizeMediaSourceFormatIntoSingleContainer(item.Container, options.Profile, DlnaProfileType.Video, directPlayProfile);
-                    playlistItem.VideoCodecs = new[] { videoStream.Codec };
+                    var videoCodec = videoStream?.Codec;
+                    playlistItem.VideoCodecs = videoCodec is null ? [] : [videoCodec];
 
                     if (directPlay == PlayMethod.DirectPlay)
                     {
@@ -713,7 +715,7 @@ namespace MediaBrowser.Model.Dlna
                         {
                             playlistItem.AudioStreamIndex = audioStreamIndex;
                             var audioCodec = item.GetMediaStream(MediaStreamType.Audio, audioStreamIndex.Value)?.Codec;
-                            playlistItem.AudioCodecs = audioCodec is null ? Array.Empty<string>() : new[] { audioCodec };
+                            playlistItem.AudioCodecs = audioCodec is null ? [] : [audioCodec];
                         }
                     }
                     else if (directPlay == PlayMethod.DirectStream)
@@ -721,7 +723,7 @@ namespace MediaBrowser.Model.Dlna
                         playlistItem.AudioStreamIndex = audioStream?.Index;
                         if (audioStream is not null)
                         {
-                            playlistItem.AudioCodecs = ContainerProfile.SplitValue(directPlayProfile?.AudioCodec);
+                            playlistItem.AudioCodecs = ContainerHelper.Split(directPlayProfile?.AudioCodec);
                         }
 
                         SetStreamInfoOptionsFromDirectPlayProfile(options, item, playlistItem, directPlayProfile);
@@ -753,7 +755,7 @@ namespace MediaBrowser.Model.Dlna
             {
                 // Can't direct play, find the transcoding profile
                 // If we do this for direct-stream we will overwrite the info
-                var (transcodingProfile, playMethod) = GetVideoTranscodeProfile(item, options, videoStream, audioStream, candidateAudioStreams, subtitleStream, playlistItem);
+                var (transcodingProfile, playMethod) = GetVideoTranscodeProfile(item, options, videoStream, audioStream, playlistItem);
 
                 if (transcodingProfile is not null && playMethod.HasValue)
                 {
@@ -781,7 +783,7 @@ namespace MediaBrowser.Model.Dlna
                         }
 
                         playlistItem.SubtitleFormat = subtitleProfile.Format;
-                        playlistItem.SubtitleCodecs = new[] { subtitleProfile.Format };
+                        playlistItem.SubtitleCodecs = [subtitleProfile.Format];
                     }
 
                     if ((playlistItem.TranscodeReasons & (VideoReasons | TranscodeReason.ContainerBitrateExceedsLimit)) != 0)
@@ -810,8 +812,6 @@ namespace MediaBrowser.Model.Dlna
             MediaOptions options,
             MediaStream? videoStream,
             MediaStream? audioStream,
-            IEnumerable<MediaStream> candidateAudioStreams,
-            MediaStream? subtitleStream,
             StreamInfo playlistItem)
         {
             if (!(item.SupportsTranscoding || item.SupportsDirectStream))
@@ -849,9 +849,7 @@ namespace MediaBrowser.Model.Dlna
 
                     if (options.AllowVideoStreamCopy)
                     {
-                        var videoCodecs = ContainerProfile.SplitValue(transcodingProfile.VideoCodec);
-
-                        if (ContainerProfile.ContainsContainer(videoCodecs, videoCodec))
+                        if (ContainerHelper.ContainsContainer(transcodingProfile.VideoCodec, videoCodec))
                         {
                             var appliedVideoConditions = options.Profile.CodecProfiles
                                 .Where(i => i.Type == CodecType.Video &&
@@ -868,9 +866,7 @@ namespace MediaBrowser.Model.Dlna
 
                     if (options.AllowAudioStreamCopy)
                     {
-                        var audioCodecs = ContainerProfile.SplitValue(transcodingProfile.AudioCodec);
-
-                        if (ContainerProfile.ContainsContainer(audioCodecs, audioCodec))
+                        if (ContainerHelper.ContainsContainer(transcodingProfile.AudioCodec, audioCodec))
                         {
                             var appliedVideoConditions = options.Profile.CodecProfiles
                                 .Where(i => i.Type == CodecType.VideoAudio &&
@@ -913,20 +909,18 @@ namespace MediaBrowser.Model.Dlna
             string? audioCodec)
         {
             // Prefer matching video codecs
-            var videoCodecs = ContainerProfile.SplitValue(videoCodec);
+            var videoCodecs = ContainerHelper.Split(videoCodec).ToList();
 
-            // Enforce HLS video codec restrictions
-            if (playlistItem.SubProtocol == MediaStreamProtocol.hls)
+            if (videoCodecs.Count == 0 && videoStream is not null)
             {
-                videoCodecs = videoCodecs.Where(codec => _supportedHlsVideoCodecs.Contains(codec)).ToArray();
+                // Add the original codec if no codec is specified
+                videoCodecs.Add(videoStream.Codec);
             }
 
-            var directVideoCodec = ContainerProfile.ContainsContainer(videoCodecs, videoStream?.Codec) ? videoStream?.Codec : null;
-            if (directVideoCodec is not null)
+            // Enforce HLS video codec restrictions
+            if (playlistItem.SubProtocol == MediaStreamProtocol.hls)
             {
-                // merge directVideoCodec to videoCodecs
-                Array.Resize(ref videoCodecs, videoCodecs.Length + 1);
-                videoCodecs[^1] = directVideoCodec;
+                videoCodecs = videoCodecs.Where(codec => _supportedHlsVideoCodecs.Contains(codec)).ToList();
             }
 
             playlistItem.VideoCodecs = videoCodecs;
@@ -950,22 +944,28 @@ namespace MediaBrowser.Model.Dlna
             }
 
             // Prefer matching audio codecs, could do better here
-            var audioCodecs = ContainerProfile.SplitValue(audioCodec);
+            var audioCodecs = ContainerHelper.Split(audioCodec).ToList();
+
+            if (audioCodecs.Count == 0 && audioStream is not null)
+            {
+                // Add the original codec if no codec is specified
+                audioCodecs.Add(audioStream.Codec);
+            }
 
             // Enforce HLS audio codec restrictions
             if (playlistItem.SubProtocol == MediaStreamProtocol.hls)
             {
                 if (string.Equals(playlistItem.Container, "mp4", StringComparison.OrdinalIgnoreCase))
                 {
-                    audioCodecs = audioCodecs.Where(codec => _supportedHlsAudioCodecsMp4.Contains(codec)).ToArray();
+                    audioCodecs = audioCodecs.Where(codec => _supportedHlsAudioCodecsMp4.Contains(codec)).ToList();
                 }
                 else
                 {
-                    audioCodecs = audioCodecs.Where(codec => _supportedHlsAudioCodecsTs.Contains(codec)).ToArray();
+                    audioCodecs = audioCodecs.Where(codec => _supportedHlsAudioCodecsTs.Contains(codec)).ToList();
                 }
             }
 
-            var audioStreamWithSupportedCodec = candidateAudioStreams.Where(stream => ContainerProfile.ContainsContainer(audioCodecs, stream.Codec)).FirstOrDefault();
+            var audioStreamWithSupportedCodec = candidateAudioStreams.Where(stream => ContainerHelper.ContainsContainer(audioCodecs, false, stream.Codec)).FirstOrDefault();
 
             var directAudioStream = audioStreamWithSupportedCodec?.Channels is not null && audioStreamWithSupportedCodec.Channels.Value <= (playlistItem.TranscodingMaxAudioChannels ?? int.MaxValue) ? audioStreamWithSupportedCodec : null;
 
@@ -982,7 +982,8 @@ namespace MediaBrowser.Model.Dlna
             {
                 audioStream = directAudioStream;
                 playlistItem.AudioStreamIndex = audioStream.Index;
-                playlistItem.AudioCodecs = audioCodecs = new[] { audioStream.Codec };
+                audioCodecs = [audioStream.Codec];
+                playlistItem.AudioCodecs = audioCodecs;
 
                 // Copy matching audio codec options
                 playlistItem.AudioSampleRate = audioStream.SampleRate;
@@ -1023,18 +1024,17 @@ namespace MediaBrowser.Model.Dlna
 
             var appliedVideoConditions = options.Profile.CodecProfiles
                 .Where(i => i.Type == CodecType.Video &&
-                    i.ContainsAnyCodec(videoCodecs, container, useSubContainer) &&
+                    i.ContainsAnyCodec(playlistItem.VideoCodecs, container, useSubContainer) &&
                     i.ApplyConditions.All(applyCondition => ConditionProcessor.IsVideoConditionSatisfied(applyCondition, width, height, bitDepth, videoBitrate, videoProfile, videoRangeType, videoLevel, videoFramerate, packetLength, timestamp, isAnamorphic, isInterlaced, refFrames, numVideoStreams, numAudioStreams, videoCodecTag, isAvc)))
                 // Reverse codec profiles for backward compatibility - first codec profile has higher priority
                 .Reverse();
-
-            foreach (var i in appliedVideoConditions)
+            foreach (var condition in appliedVideoConditions)
             {
-                foreach (var transcodingVideoCodec in videoCodecs)
+                foreach (var transcodingVideoCodec in playlistItem.VideoCodecs)
                 {
-                    if (i.ContainsAnyCodec(transcodingVideoCodec, container, useSubContainer))
+                    if (condition.ContainsAnyCodec(transcodingVideoCodec, container, useSubContainer))
                     {
-                        ApplyTranscodingConditions(playlistItem, i.Conditions, transcodingVideoCodec, true, true);
+                        ApplyTranscodingConditions(playlistItem, condition.Conditions, transcodingVideoCodec, true, true);
                         continue;
                     }
                 }
@@ -1055,14 +1055,14 @@ namespace MediaBrowser.Model.Dlna
 
             var appliedAudioConditions = options.Profile.CodecProfiles
                 .Where(i => i.Type == CodecType.VideoAudio &&
-                    i.ContainsAnyCodec(audioCodecs, container) &&
+                    i.ContainsAnyCodec(playlistItem.AudioCodecs, container) &&
                     i.ApplyConditions.All(applyCondition => ConditionProcessor.IsVideoAudioConditionSatisfied(applyCondition, audioChannels, inputAudioBitrate, inputAudioSampleRate, inputAudioBitDepth, audioProfile, isSecondaryAudio)))
                 // Reverse codec profiles for backward compatibility - first codec profile has higher priority
                 .Reverse();
 
             foreach (var codecProfile in appliedAudioConditions)
             {
-                foreach (var transcodingAudioCodec in audioCodecs)
+                foreach (var transcodingAudioCodec in playlistItem.AudioCodecs)
                 {
                     if (codecProfile.ContainsAnyCodec(transcodingAudioCodec, container))
                     {
@@ -1132,9 +1132,9 @@ namespace MediaBrowser.Model.Dlna
             return 192000;
         }
 
-        private static int GetAudioBitrate(long maxTotalBitrate, string[] targetAudioCodecs, MediaStream? audioStream, StreamInfo item)
+        private static int GetAudioBitrate(long maxTotalBitrate, IReadOnlyList<string> targetAudioCodecs, MediaStream? audioStream, StreamInfo item)
         {
-            string? targetAudioCodec = targetAudioCodecs.Length == 0 ? null : targetAudioCodecs[0];
+            string? targetAudioCodec = targetAudioCodecs.Count == 0 ? null : targetAudioCodecs[0];
 
             int? targetAudioChannels = item.GetTargetAudioChannels(targetAudioCodec);
 
@@ -1151,7 +1151,7 @@ namespace MediaBrowser.Model.Dlna
                     && audioStream.Channels.HasValue
                     && audioStream.Channels.Value > targetAudioChannels.Value)
                 {
-                    // Reduce the bitrate if we're downmixing.
+                    // Reduce the bitrate if we're down mixing.
                     defaultBitrate = GetDefaultAudioBitrate(targetAudioCodec, targetAudioChannels);
                 }
                 else if (targetAudioChannels.HasValue
@@ -1159,8 +1159,8 @@ namespace MediaBrowser.Model.Dlna
                          && audioStream.Channels.Value <= targetAudioChannels.Value
                          && !string.IsNullOrEmpty(audioStream.Codec)
                          && targetAudioCodecs is not null
-                         && targetAudioCodecs.Length > 0
-                         && !Array.Exists(targetAudioCodecs, elem => string.Equals(audioStream.Codec, elem, StringComparison.OrdinalIgnoreCase)))
+                         && targetAudioCodecs.Count > 0
+                         && !targetAudioCodecs.Any(elem => string.Equals(audioStream.Codec, elem, StringComparison.OrdinalIgnoreCase)))
                 {
                     // Shift the bitrate if we're transcoding to a different audio codec.
                     defaultBitrate = GetDefaultAudioBitrate(targetAudioCodec, audioStream.Channels.Value);
@@ -1299,7 +1299,7 @@ namespace MediaBrowser.Model.Dlna
                         !checkVideoConditions(codecProfile.ApplyConditions).Any())
                     .SelectMany(codecProfile => checkVideoConditions(codecProfile.Conditions)));
 
-            // Check audiocandidates profile conditions
+            // Check audio candidates profile conditions
             var audioStreamMatches = candidateAudioStreams.ToDictionary(s => s, audioStream => CheckVideoAudioStreamDirectPlay(options, mediaSource, container, audioStream));
 
             TranscodeReason subtitleProfileReasons = 0;
@@ -1316,24 +1316,6 @@ namespace MediaBrowser.Model.Dlna
                 }
             }
 
-            var rankings = new[] { TranscodeReason.VideoCodecNotSupported, VideoCodecReasons, TranscodeReason.AudioCodecNotSupported, AudioCodecReasons, ContainerReasons };
-            var rank = (ref TranscodeReason a) =>
-                {
-                    var index = 1;
-                    foreach (var flag in rankings)
-                    {
-                        var reason = a & flag;
-                        if (reason != 0)
-                        {
-                            return index;
-                        }
-
-                        index++;
-                    }
-
-                    return index;
-                };
-
             var containerSupported = false;
 
             // Check DirectPlay profiles to see if it can be direct played
@@ -1400,7 +1382,9 @@ namespace MediaBrowser.Model.Dlna
                         playMethod = PlayMethod.DirectStream;
                     }
 
-                    var ranked = rank(ref failureReasons);
+                    TranscodeReason[] rankings = [TranscodeReason.VideoCodecNotSupported, VideoCodecReasons, TranscodeReason.AudioCodecNotSupported, AudioCodecReasons, ContainerReasons];
+                    var ranked = GetRank(ref failureReasons, rankings);
+
                     return (Result: (Profile: directPlayProfile, PlayMethod: playMethod, AudioStreamIndex: selectedAudioStream?.Index, TranscodeReason: failureReasons), Order: order, Rank: ranked);
                 })
                 .OrderByDescending(analysis => analysis.Result.PlayMethod)
@@ -1475,7 +1459,7 @@ namespace MediaBrowser.Model.Dlna
         /// <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>
+        /// <param name="transcodingSubProtocol">The subtitle transcoding protocol.</param>
         /// <returns>The normalized input container.</returns>
         public static SubtitleProfile GetSubtitleProfile(
             MediaSourceInfo mediaSource,
@@ -1501,7 +1485,7 @@ namespace MediaBrowser.Model.Dlna
                         continue;
                     }
 
-                    if (!ContainerProfile.ContainsContainer(profile.Container, outputContainer))
+                    if (!ContainerHelper.ContainsContainer(profile.Container, outputContainer))
                     {
                         continue;
                     }
@@ -1530,7 +1514,7 @@ namespace MediaBrowser.Model.Dlna
                         continue;
                     }
 
-                    if (!ContainerProfile.ContainsContainer(profile.Container, outputContainer))
+                    if (!ContainerHelper.ContainsContainer(profile.Container, outputContainer))
                     {
                         continue;
                     }
@@ -1561,17 +1545,12 @@ namespace MediaBrowser.Model.Dlna
         {
             if (!string.IsNullOrEmpty(transcodingContainer))
             {
-                string[] normalizedContainers = ContainerProfile.SplitValue(transcodingContainer);
-
-                if (ContainerProfile.ContainsContainer(normalizedContainers, "ts")
-                    || ContainerProfile.ContainsContainer(normalizedContainers, "mpegts")
-                    || ContainerProfile.ContainsContainer(normalizedContainers, "mp4"))
+                if (ContainerHelper.ContainsContainer(transcodingContainer, "ts,mpegts,mp4"))
                 {
                     return false;
                 }
 
-                if (ContainerProfile.ContainsContainer(normalizedContainers, "mkv")
-                    || ContainerProfile.ContainsContainer(normalizedContainers, "matroska"))
+                if (ContainerHelper.ContainsContainer(transcodingContainer, "mkv,matroska"))
                 {
                     return true;
                 }
@@ -2274,5 +2253,22 @@ namespace MediaBrowser.Model.Dlna
 
             return false;
         }
+
+        private int GetRank(ref TranscodeReason a, TranscodeReason[] rankings)
+        {
+            var index = 1;
+            foreach (var flag in rankings)
+            {
+                var reason = a & flag;
+                if (reason != 0)
+                {
+                    return index;
+                }
+
+                index++;
+            }
+
+            return index;
+        }
     }
 }

+ 984 - 691
MediaBrowser.Model/Dlna/StreamInfo.cs

@@ -1,9 +1,6 @@
-#pragma warning disable CS1591
-
 using System;
 using System.Collections.Generic;
 using System.Globalization;
-using System.Linq;
 using Jellyfin.Data.Enums;
 using MediaBrowser.Model.Drawing;
 using MediaBrowser.Model.Dto;
@@ -11,1007 +8,1303 @@ using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.MediaInfo;
 using MediaBrowser.Model.Session;
 
-namespace MediaBrowser.Model.Dlna
+namespace MediaBrowser.Model.Dlna;
+
+/// <summary>
+/// Class holding information on a stream.
+/// </summary>
+public class StreamInfo
 {
     /// <summary>
-    /// Class StreamInfo.
+    /// Initializes a new instance of the <see cref="StreamInfo"/> class.
     /// </summary>
-    public class StreamInfo
+    public StreamInfo()
     {
-        public StreamInfo()
-        {
-            AudioCodecs = Array.Empty<string>();
-            VideoCodecs = Array.Empty<string>();
-            SubtitleCodecs = Array.Empty<string>();
-            StreamOptions = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
-        }
+        AudioCodecs = [];
+        VideoCodecs = [];
+        SubtitleCodecs = [];
+        StreamOptions = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
+    }
 
-        public Guid ItemId { get; set; }
+    /// <summary>
+    /// Gets or sets the item id.
+    /// </summary>
+    /// <value>The item id.</value>
+    public Guid ItemId { get; set; }
 
-        public PlayMethod PlayMethod { get; set; }
+    /// <summary>
+    /// Gets or sets the play method.
+    /// </summary>
+    /// <value>The play method.</value>
+    public PlayMethod PlayMethod { get; set; }
 
-        public EncodingContext Context { get; set; }
+    /// <summary>
+    /// Gets or sets the encoding context.
+    /// </summary>
+    /// <value>The encoding context.</value>
+    public EncodingContext Context { get; set; }
 
-        public DlnaProfileType MediaType { get; set; }
+    /// <summary>
+    /// Gets or sets the media type.
+    /// </summary>
+    /// <value>The media type.</value>
+    public DlnaProfileType MediaType { get; set; }
 
-        public string? Container { get; set; }
+    /// <summary>
+    /// Gets or sets the container.
+    /// </summary>
+    /// <value>The container.</value>
+    public string? Container { get; set; }
 
-        public MediaStreamProtocol SubProtocol { get; set; }
+    /// <summary>
+    /// Gets or sets the sub protocol.
+    /// </summary>
+    /// <value>The sub protocol.</value>
+    public MediaStreamProtocol SubProtocol { get; set; }
 
-        public long StartPositionTicks { get; set; }
+    /// <summary>
+    /// Gets or sets the start position ticks.
+    /// </summary>
+    /// <value>The start position ticks.</value>
+    public long StartPositionTicks { get; set; }
 
-        public int? SegmentLength { get; set; }
+    /// <summary>
+    /// Gets or sets the segment length.
+    /// </summary>
+    /// <value>The segment length.</value>
+    public int? SegmentLength { get; set; }
 
-        public int? MinSegments { get; set; }
+    /// <summary>
+    /// Gets or sets the minimum segments count.
+    /// </summary>
+    /// <value>The minimum segments count.</value>
+    public int? MinSegments { get; set; }
 
-        public bool BreakOnNonKeyFrames { get; set; }
+    /// <summary>
+    /// Gets or sets a value indicating whether the stream can be broken on non-keyframes.
+    /// </summary>
+    public bool BreakOnNonKeyFrames { get; set; }
 
-        public bool RequireAvc { get; set; }
+    /// <summary>
+    /// Gets or sets a value indicating whether the stream requires AVC.
+    /// </summary>
+    public bool RequireAvc { get; set; }
 
-        public bool RequireNonAnamorphic { get; set; }
+    /// <summary>
+    /// Gets or sets a value indicating whether the stream requires AVC.
+    /// </summary>
+    public bool RequireNonAnamorphic { get; set; }
 
-        public bool CopyTimestamps { get; set; }
+    /// <summary>
+    /// Gets or sets a value indicating whether timestamps should be copied.
+    /// </summary>
+    public bool CopyTimestamps { get; set; }
 
-        public bool EnableMpegtsM2TsMode { get; set; }
+    /// <summary>
+    /// Gets or sets a value indicating whether timestamps should be copied.
+    /// </summary>
+    public bool EnableMpegtsM2TsMode { get; set; }
 
-        public bool EnableSubtitlesInManifest { get; set; }
+    /// <summary>
+    /// Gets or sets a value indicating whether the subtitle manifest is enabled.
+    /// </summary>
+    public bool EnableSubtitlesInManifest { get; set; }
 
-        public string[] AudioCodecs { get; set; }
+    /// <summary>
+    /// Gets or sets the audio codecs.
+    /// </summary>
+    /// <value>The audio codecs.</value>
+    public IReadOnlyList<string> AudioCodecs { get; set; }
 
-        public string[] VideoCodecs { get; set; }
+    /// <summary>
+    /// Gets or sets the video codecs.
+    /// </summary>
+    /// <value>The video codecs.</value>
+    public IReadOnlyList<string> VideoCodecs { get; set; }
 
-        public int? AudioStreamIndex { get; set; }
+    /// <summary>
+    /// Gets or sets the audio stream index.
+    /// </summary>
+    /// <value>The audio stream index.</value>
+    public int? AudioStreamIndex { get; set; }
 
-        public int? SubtitleStreamIndex { get; set; }
+    /// <summary>
+    /// Gets or sets the video stream index.
+    /// </summary>
+    /// <value>The subtitle stream index.</value>
+    public int? SubtitleStreamIndex { get; set; }
 
-        public int? TranscodingMaxAudioChannels { get; set; }
+    /// <summary>
+    /// Gets or sets the maximum transcoding audio channels.
+    /// </summary>
+    /// <value>The maximum transcoding audio channels.</value>
+    public int? TranscodingMaxAudioChannels { get; set; }
 
-        public int? GlobalMaxAudioChannels { get; set; }
+    /// <summary>
+    /// Gets or sets the global maximum audio channels.
+    /// </summary>
+    /// <value>The global maximum audio channels.</value>
+    public int? GlobalMaxAudioChannels { get; set; }
 
-        public int? AudioBitrate { get; set; }
+    /// <summary>
+    /// Gets or sets the audio bitrate.
+    /// </summary>
+    /// <value>The audio bitrate.</value>
+    public int? AudioBitrate { get; set; }
 
-        public int? AudioSampleRate { get; set; }
+    /// <summary>
+    /// Gets or sets the audio sample rate.
+    /// </summary>
+    /// <value>The audio sample rate.</value>
+    public int? AudioSampleRate { get; set; }
 
-        public int? VideoBitrate { get; set; }
+    /// <summary>
+    /// Gets or sets the video bitrate.
+    /// </summary>
+    /// <value>The video bitrate.</value>
+    public int? VideoBitrate { get; set; }
 
-        public int? MaxWidth { get; set; }
+    /// <summary>
+    /// Gets or sets the maximum output width.
+    /// </summary>
+    /// <value>The output width.</value>
+    public int? MaxWidth { get; set; }
 
-        public int? MaxHeight { get; set; }
+    /// <summary>
+    /// Gets or sets the maximum output height.
+    /// </summary>
+    /// <value>The maximum output height.</value>
+    public int? MaxHeight { get; set; }
 
-        public float? MaxFramerate { get; set; }
+    /// <summary>
+    /// Gets or sets the maximum framerate.
+    /// </summary>
+    /// <value>The maximum framerate.</value>
+    public float? MaxFramerate { get; set; }
 
-        public required DeviceProfile DeviceProfile { get; set; }
+    /// <summary>
+    /// Gets or sets the device profile.
+    /// </summary>
+    /// <value>The device profile.</value>
+    public required DeviceProfile DeviceProfile { get; set; }
 
-        public string? DeviceProfileId { get; set; }
+    /// <summary>
+    /// Gets or sets the device profile id.
+    /// </summary>
+    /// <value>The device profile id.</value>
+    public string? DeviceProfileId { get; set; }
 
-        public string? DeviceId { get; set; }
+    /// <summary>
+    /// Gets or sets the device id.
+    /// </summary>
+    /// <value>The device id.</value>
+    public string? DeviceId { get; set; }
 
-        public long? RunTimeTicks { get; set; }
+    /// <summary>
+    /// Gets or sets the runtime ticks.
+    /// </summary>
+    /// <value>The runtime ticks.</value>
+    public long? RunTimeTicks { get; set; }
 
-        public TranscodeSeekInfo TranscodeSeekInfo { get; set; }
+    /// <summary>
+    /// Gets or sets the transcode seek info.
+    /// </summary>
+    /// <value>The transcode seek info.</value>
+    public TranscodeSeekInfo TranscodeSeekInfo { get; set; }
 
-        public bool EstimateContentLength { get; set; }
+    /// <summary>
+    /// Gets or sets a value indicating whether content length should be estimated.
+    /// </summary>
+    public bool EstimateContentLength { get; set; }
 
-        public MediaSourceInfo? MediaSource { get; set; }
+    /// <summary>
+    /// Gets or sets the media source info.
+    /// </summary>
+    /// <value>The media source info.</value>
+    public MediaSourceInfo? MediaSource { get; set; }
 
-        public string[] SubtitleCodecs { get; set; }
+    /// <summary>
+    /// Gets or sets the subtitle codecs.
+    /// </summary>
+    /// <value>The subtitle codecs.</value>
+    public IReadOnlyList<string> SubtitleCodecs { get; set; }
 
-        public SubtitleDeliveryMethod SubtitleDeliveryMethod { get; set; }
+    /// <summary>
+    /// Gets or sets the subtitle delivery method.
+    /// </summary>
+    /// <value>The subtitle delivery method.</value>
+    public SubtitleDeliveryMethod SubtitleDeliveryMethod { get; set; }
 
-        public string? SubtitleFormat { get; set; }
+    /// <summary>
+    /// Gets or sets the subtitle format.
+    /// </summary>
+    /// <value>The subtitle format.</value>
+    public string? SubtitleFormat { get; set; }
 
-        public string? PlaySessionId { get; set; }
+    /// <summary>
+    /// Gets or sets the play session id.
+    /// </summary>
+    /// <value>The play session id.</value>
+    public string? PlaySessionId { get; set; }
 
-        public TranscodeReason TranscodeReasons { get; set; }
+    /// <summary>
+    /// Gets or sets the transcode reasons.
+    /// </summary>
+    /// <value>The transcode reasons.</value>
+    public TranscodeReason TranscodeReasons { get; set; }
 
-        public Dictionary<string, string> StreamOptions { get; private set; }
+    /// <summary>
+    /// Gets the stream options.
+    /// </summary>
+    /// <value>The stream options.</value>
+    public Dictionary<string, string> StreamOptions { get; private set; }
 
-        public string? MediaSourceId => MediaSource?.Id;
+    /// <summary>
+    /// Gets the media source id.
+    /// </summary>
+    /// <value>The media source id.</value>
+    public string? MediaSourceId => MediaSource?.Id;
 
-        public bool EnableAudioVbrEncoding { get; set; }
+    /// <summary>
+    /// Gets or sets a value indicating whether audio VBR encoding is enabled.
+    /// </summary>
+    public bool EnableAudioVbrEncoding { get; set; }
 
-        public bool IsDirectStream => MediaSource?.VideoType is not (VideoType.Dvd or VideoType.BluRay)
-            && PlayMethod is PlayMethod.DirectStream or PlayMethod.DirectPlay;
+    /// <summary>
+    /// Gets a value indicating whether the stream is direct.
+    /// </summary>
+    public bool IsDirectStream => MediaSource?.VideoType is not (VideoType.Dvd or VideoType.BluRay)
+        && PlayMethod is PlayMethod.DirectStream or PlayMethod.DirectPlay;
 
-        /// <summary>
-        /// Gets the audio stream that will be used.
-        /// </summary>
-        public MediaStream? TargetAudioStream => MediaSource?.GetDefaultAudioStream(AudioStreamIndex);
+    /// <summary>
+    /// Gets the audio stream that will be used in the output stream.
+    /// </summary>
+    /// <value>The audio stream.</value>
+    public MediaStream? TargetAudioStream => MediaSource?.GetDefaultAudioStream(AudioStreamIndex);
 
-        /// <summary>
-        /// Gets the video stream that will be used.
-        /// </summary>
-        public MediaStream? TargetVideoStream => MediaSource?.VideoStream;
+    /// <summary>
+    /// Gets the video stream that will be used in the output stream.
+    /// </summary>
+    /// <value>The video stream.</value>
+    public MediaStream? TargetVideoStream => MediaSource?.VideoStream;
 
-        /// <summary>
-        /// Gets the audio sample rate that will be in the output stream.
-        /// </summary>
-        public int? TargetAudioSampleRate
+    /// <summary>
+    /// Gets the audio sample rate that will be in the output stream.
+    /// </summary>
+    /// <value>The target audio sample rate.</value>
+    public int? TargetAudioSampleRate
+    {
+        get
         {
-            get
-            {
-                var stream = TargetAudioStream;
-                return AudioSampleRate.HasValue && !IsDirectStream
-                    ? AudioSampleRate
-                    : stream?.SampleRate;
-            }
+            var stream = TargetAudioStream;
+            return AudioSampleRate.HasValue && !IsDirectStream
+                ? AudioSampleRate
+                : stream?.SampleRate;
         }
+    }
 
-        /// <summary>
-        /// Gets the audio sample rate that will be in the output stream.
-        /// </summary>
-        public int? TargetAudioBitDepth
+    /// <summary>
+    /// Gets the audio bit depth that will be in the output stream.
+    /// </summary>
+    /// <value>The target bit depth.</value>
+    public int? TargetAudioBitDepth
+    {
+        get
         {
-            get
+            if (IsDirectStream)
             {
-                if (IsDirectStream)
-                {
-                    return TargetAudioStream?.BitDepth;
-                }
-
-                var targetAudioCodecs = TargetAudioCodec;
-                var audioCodec = targetAudioCodecs.Length == 0 ? null : targetAudioCodecs[0];
-                if (!string.IsNullOrEmpty(audioCodec))
-                {
-                    return GetTargetAudioBitDepth(audioCodec);
-                }
-
                 return TargetAudioStream?.BitDepth;
             }
-        }
 
-        /// <summary>
-        /// Gets the audio sample rate that will be in the output stream.
-        /// </summary>
-        public int? TargetVideoBitDepth
-        {
-            get
+            var targetAudioCodecs = TargetAudioCodec;
+            var audioCodec = targetAudioCodecs.Count == 0 ? null : targetAudioCodecs[0];
+            if (!string.IsNullOrEmpty(audioCodec))
             {
-                if (IsDirectStream)
-                {
-                    return TargetVideoStream?.BitDepth;
-                }
-
-                var targetVideoCodecs = TargetVideoCodec;
-                var videoCodec = targetVideoCodecs.Length == 0 ? null : targetVideoCodecs[0];
-                if (!string.IsNullOrEmpty(videoCodec))
-                {
-                    return GetTargetVideoBitDepth(videoCodec);
-                }
-
-                return TargetVideoStream?.BitDepth;
+                return GetTargetAudioBitDepth(audioCodec);
             }
+
+            return TargetAudioStream?.BitDepth;
         }
+    }
 
-        /// <summary>
-        /// Gets the target reference frames.
-        /// </summary>
-        /// <value>The target reference frames.</value>
-        public int? TargetRefFrames
+    /// <summary>
+    /// Gets the video bit depth that will be in the output stream.
+    /// </summary>
+    /// <value>The target video bit depth.</value>
+    public int? TargetVideoBitDepth
+    {
+        get
         {
-            get
+            if (IsDirectStream)
             {
-                if (IsDirectStream)
-                {
-                    return TargetVideoStream?.RefFrames;
-                }
-
-                var targetVideoCodecs = TargetVideoCodec;
-                var videoCodec = targetVideoCodecs.Length == 0 ? null : targetVideoCodecs[0];
-                if (!string.IsNullOrEmpty(videoCodec))
-                {
-                    return GetTargetRefFrames(videoCodec);
-                }
+                return TargetVideoStream?.BitDepth;
+            }
 
-                return TargetVideoStream?.RefFrames;
+            var targetVideoCodecs = TargetVideoCodec;
+            var videoCodec = targetVideoCodecs.Count == 0 ? null : targetVideoCodecs[0];
+            if (!string.IsNullOrEmpty(videoCodec))
+            {
+                return GetTargetVideoBitDepth(videoCodec);
             }
+
+            return TargetVideoStream?.BitDepth;
         }
+    }
 
-        /// <summary>
-        /// Gets the audio sample rate that will be in the output stream.
-        /// </summary>
-        public float? TargetFramerate
+    /// <summary>
+    /// Gets the target reference frames that will be in the output stream.
+    /// </summary>
+    /// <value>The target reference frames.</value>
+    public int? TargetRefFrames
+    {
+        get
         {
-            get
+            if (IsDirectStream)
             {
-                var stream = TargetVideoStream;
-                return MaxFramerate.HasValue && !IsDirectStream
-                    ? MaxFramerate
-                    : stream?.ReferenceFrameRate;
+                return TargetVideoStream?.RefFrames;
             }
-        }
 
-        /// <summary>
-        /// Gets the audio sample rate that will be in the output stream.
-        /// </summary>
-        public double? TargetVideoLevel
-        {
-            get
+            var targetVideoCodecs = TargetVideoCodec;
+            var videoCodec = targetVideoCodecs.Count == 0 ? null : targetVideoCodecs[0];
+            if (!string.IsNullOrEmpty(videoCodec))
             {
-                if (IsDirectStream)
-                {
-                    return TargetVideoStream?.Level;
-                }
+                return GetTargetRefFrames(videoCodec);
+            }
 
-                var targetVideoCodecs = TargetVideoCodec;
-                var videoCodec = targetVideoCodecs.Length == 0 ? null : targetVideoCodecs[0];
-                if (!string.IsNullOrEmpty(videoCodec))
-                {
-                    return GetTargetVideoLevel(videoCodec);
-                }
+            return TargetVideoStream?.RefFrames;
+        }
+    }
 
-                return TargetVideoStream?.Level;
-            }
+    /// <summary>
+    /// Gets the target framerate that will be in the output stream.
+    /// </summary>
+    /// <value>The target framerate.</value>
+    public float? TargetFramerate
+    {
+        get
+        {
+            var stream = TargetVideoStream;
+            return MaxFramerate.HasValue && !IsDirectStream
+                ? MaxFramerate
+                : stream?.ReferenceFrameRate;
         }
+    }
 
-        /// <summary>
-        /// Gets the audio sample rate that will be in the output stream.
-        /// </summary>
-        public int? TargetPacketLength
+    /// <summary>
+    /// Gets the target video level that will be in the output stream.
+    /// </summary>
+    /// <value>The target video level.</value>
+    public double? TargetVideoLevel
+    {
+        get
         {
-            get
+            if (IsDirectStream)
             {
-                var stream = TargetVideoStream;
-                return !IsDirectStream
-                    ? null
-                    : stream?.PacketLength;
+                return TargetVideoStream?.Level;
             }
-        }
 
-        /// <summary>
-        /// Gets the audio sample rate that will be in the output stream.
-        /// </summary>
-        public string? TargetVideoProfile
-        {
-            get
+            var targetVideoCodecs = TargetVideoCodec;
+            var videoCodec = targetVideoCodecs.Count == 0 ? null : targetVideoCodecs[0];
+            if (!string.IsNullOrEmpty(videoCodec))
             {
-                if (IsDirectStream)
-                {
-                    return TargetVideoStream?.Profile;
-                }
+                return GetTargetVideoLevel(videoCodec);
+            }
 
-                var targetVideoCodecs = TargetVideoCodec;
-                var videoCodec = targetVideoCodecs.Length == 0 ? null : targetVideoCodecs[0];
-                if (!string.IsNullOrEmpty(videoCodec))
-                {
-                    return GetOption(videoCodec, "profile");
-                }
+            return TargetVideoStream?.Level;
+        }
+    }
 
-                return TargetVideoStream?.Profile;
-            }
+    /// <summary>
+    /// Gets the target packet length that will be in the output stream.
+    /// </summary>
+    /// <value>The target packet length.</value>
+    public int? TargetPacketLength
+    {
+        get
+        {
+            var stream = TargetVideoStream;
+            return !IsDirectStream
+                ? null
+                : stream?.PacketLength;
         }
+    }
 
-        /// <summary>
-        /// Gets the target video range type that will be in the output stream.
-        /// </summary>
-        public VideoRangeType TargetVideoRangeType
+    /// <summary>
+    /// Gets the target video profile that will be in the output stream.
+    /// </summary>
+    /// <value>The target video profile.</value>
+    public string? TargetVideoProfile
+    {
+        get
         {
-            get
+            if (IsDirectStream)
             {
-                if (IsDirectStream)
-                {
-                    return TargetVideoStream?.VideoRangeType ?? VideoRangeType.Unknown;
-                }
-
-                var targetVideoCodecs = TargetVideoCodec;
-                var videoCodec = targetVideoCodecs.Length == 0 ? null : targetVideoCodecs[0];
-                if (!string.IsNullOrEmpty(videoCodec)
-                    && Enum.TryParse(GetOption(videoCodec, "rangetype"), true, out VideoRangeType videoRangeType))
-                {
-                    return videoRangeType;
-                }
+                return TargetVideoStream?.Profile;
+            }
 
-                return TargetVideoStream?.VideoRangeType ?? VideoRangeType.Unknown;
+            var targetVideoCodecs = TargetVideoCodec;
+            var videoCodec = targetVideoCodecs.Count == 0 ? null : targetVideoCodecs[0];
+            if (!string.IsNullOrEmpty(videoCodec))
+            {
+                return GetOption(videoCodec, "profile");
             }
+
+            return TargetVideoStream?.Profile;
         }
+    }
 
-        /// <summary>
-        /// Gets the target video codec tag.
-        /// </summary>
-        /// <value>The target video codec tag.</value>
-        public string? TargetVideoCodecTag
+    /// <summary>
+    /// Gets the target video range type that will be in the output stream.
+    /// </summary>
+    /// <value>The video range type.</value>
+    public VideoRangeType TargetVideoRangeType
+    {
+        get
         {
-            get
+            if (IsDirectStream)
             {
-                var stream = TargetVideoStream;
-                return !IsDirectStream
-                    ? null
-                    : stream?.CodecTag;
+                return TargetVideoStream?.VideoRangeType ?? VideoRangeType.Unknown;
             }
-        }
 
-        /// <summary>
-        /// Gets the audio bitrate that will be in the output stream.
-        /// </summary>
-        public int? TargetAudioBitrate
-        {
-            get
+            var targetVideoCodecs = TargetVideoCodec;
+            var videoCodec = targetVideoCodecs.Count == 0 ? null : targetVideoCodecs[0];
+            if (!string.IsNullOrEmpty(videoCodec)
+                && Enum.TryParse(GetOption(videoCodec, "rangetype"), true, out VideoRangeType videoRangeType))
             {
-                var stream = TargetAudioStream;
-                return AudioBitrate.HasValue && !IsDirectStream
-                    ? AudioBitrate
-                    : stream?.BitRate;
+                return videoRangeType;
             }
+
+            return TargetVideoStream?.VideoRangeType ?? VideoRangeType.Unknown;
         }
+    }
 
-        /// <summary>
-        /// Gets the audio channels that will be in the output stream.
-        /// </summary>
-        public int? TargetAudioChannels
+    /// <summary>
+    /// Gets the target video codec tag.
+    /// </summary>
+    /// <value>The video codec tag.</value>
+    public string? TargetVideoCodecTag
+    {
+        get
         {
-            get
-            {
-                if (IsDirectStream)
-                {
-                    return TargetAudioStream?.Channels;
-                }
+            var stream = TargetVideoStream;
+            return !IsDirectStream
+                ? null
+                : stream?.CodecTag;
+        }
+    }
 
-                var targetAudioCodecs = TargetAudioCodec;
-                var codec = targetAudioCodecs.Length == 0 ? null : targetAudioCodecs[0];
-                if (!string.IsNullOrEmpty(codec))
-                {
-                    return GetTargetRefFrames(codec);
-                }
+    /// <summary>
+    /// Gets the audio bitrate that will be in the output stream.
+    /// </summary>
+    /// <value>The audio bitrate.</value>
+    public int? TargetAudioBitrate
+    {
+        get
+        {
+            var stream = TargetAudioStream;
+            return AudioBitrate.HasValue && !IsDirectStream
+                ? AudioBitrate
+                : stream?.BitRate;
+        }
+    }
 
+    /// <summary>
+    /// Gets the amount of audio channels that will be in the output stream.
+    /// </summary>
+    /// <value>The target audio channels.</value>
+    public int? TargetAudioChannels
+    {
+        get
+        {
+            if (IsDirectStream)
+            {
                 return TargetAudioStream?.Channels;
             }
+
+            var targetAudioCodecs = TargetAudioCodec;
+            var codec = targetAudioCodecs.Count == 0 ? null : targetAudioCodecs[0];
+            if (!string.IsNullOrEmpty(codec))
+            {
+                return GetTargetRefFrames(codec);
+            }
+
+            return TargetAudioStream?.Channels;
         }
+    }
 
-        /// <summary>
-        /// Gets the audio codec that will be in the output stream.
-        /// </summary>
-        public string[] TargetAudioCodec
+    /// <summary>
+    /// Gets the audio codec that will be in the output stream.
+    /// </summary>
+    /// <value>The audio codec.</value>
+    public IReadOnlyList<string> TargetAudioCodec
+    {
+        get
         {
-            get
-            {
-                var stream = TargetAudioStream;
+            var stream = TargetAudioStream;
 
-                string? inputCodec = stream?.Codec;
+            string? inputCodec = stream?.Codec;
 
-                if (IsDirectStream)
-                {
-                    return string.IsNullOrEmpty(inputCodec) ? Array.Empty<string>() : new[] { inputCodec };
-                }
+            if (IsDirectStream)
+            {
+                return string.IsNullOrEmpty(inputCodec) ? [] : [inputCodec];
+            }
 
-                foreach (string codec in AudioCodecs)
+            foreach (string codec in AudioCodecs)
+            {
+                if (string.Equals(codec, inputCodec, StringComparison.OrdinalIgnoreCase))
                 {
-                    if (string.Equals(codec, inputCodec, StringComparison.OrdinalIgnoreCase))
-                    {
-                        return string.IsNullOrEmpty(codec) ? Array.Empty<string>() : new[] { codec };
-                    }
+                    return string.IsNullOrEmpty(codec) ? [] : [codec];
                 }
-
-                return AudioCodecs;
             }
+
+            return AudioCodecs;
         }
+    }
 
-        public string[] TargetVideoCodec
+    /// <summary>
+    /// Gets the video codec that will be in the output stream.
+    /// </summary>
+    /// <value>The target video codec.</value>
+    public IReadOnlyList<string> TargetVideoCodec
+    {
+        get
         {
-            get
-            {
-                var stream = TargetVideoStream;
+            var stream = TargetVideoStream;
 
-                string? inputCodec = stream?.Codec;
+            string? inputCodec = stream?.Codec;
 
-                if (IsDirectStream)
-                {
-                    return string.IsNullOrEmpty(inputCodec) ? Array.Empty<string>() : new[] { inputCodec };
-                }
+            if (IsDirectStream)
+            {
+                return string.IsNullOrEmpty(inputCodec) ? [] : [inputCodec];
+            }
 
-                foreach (string codec in VideoCodecs)
+            foreach (string codec in VideoCodecs)
+            {
+                if (string.Equals(codec, inputCodec, StringComparison.OrdinalIgnoreCase))
                 {
-                    if (string.Equals(codec, inputCodec, StringComparison.OrdinalIgnoreCase))
-                    {
-                        return string.IsNullOrEmpty(codec) ? Array.Empty<string>() : new[] { codec };
-                    }
+                    return string.IsNullOrEmpty(codec) ? [] : [codec];
                 }
-
-                return VideoCodecs;
             }
+
+            return VideoCodecs;
         }
+    }
 
-        /// <summary>
-        /// Gets the audio channels that will be in the output stream.
-        /// </summary>
-        public long? TargetSize
+    /// <summary>
+    /// Gets the target size of the output stream.
+    /// </summary>
+    /// <value>The target size.</value>
+    public long? TargetSize
+    {
+        get
         {
-            get
+            if (IsDirectStream)
             {
-                if (IsDirectStream)
-                {
-                    return MediaSource?.Size;
-                }
-
-                if (RunTimeTicks.HasValue)
-                {
-                    int? totalBitrate = TargetTotalBitrate;
+                return MediaSource?.Size;
+            }
 
-                    double totalSeconds = RunTimeTicks.Value;
-                    // Convert to ms
-                    totalSeconds /= 10000;
-                    // Convert to seconds
-                    totalSeconds /= 1000;
+            if (RunTimeTicks.HasValue)
+            {
+                int? totalBitrate = TargetTotalBitrate;
 
-                    return totalBitrate.HasValue ?
-                        Convert.ToInt64(totalBitrate.Value * totalSeconds) :
-                        null;
-                }
+                double totalSeconds = RunTimeTicks.Value;
+                // Convert to ms
+                totalSeconds /= 10000;
+                // Convert to seconds
+                totalSeconds /= 1000;
 
-                return null;
+                return totalBitrate.HasValue ?
+                    Convert.ToInt64(totalBitrate.Value * totalSeconds) :
+                    null;
             }
+
+            return null;
         }
+    }
 
-        public int? TargetVideoBitrate
+    /// <summary>
+    /// Gets the target video bitrate of the output stream.
+    /// </summary>
+    /// <value>The video bitrate.</value>
+    public int? TargetVideoBitrate
+    {
+        get
         {
-            get
-            {
-                var stream = TargetVideoStream;
+            var stream = TargetVideoStream;
 
-                return VideoBitrate.HasValue && !IsDirectStream
-                    ? VideoBitrate
-                    : stream?.BitRate;
-            }
+            return VideoBitrate.HasValue && !IsDirectStream
+                ? VideoBitrate
+                : stream?.BitRate;
         }
+    }
 
-        public TransportStreamTimestamp TargetTimestamp
+    /// <summary>
+    /// Gets the target timestamp of the output stream.
+    /// </summary>
+    /// <value>The target timestamp.</value>
+    public TransportStreamTimestamp TargetTimestamp
+    {
+        get
         {
-            get
-            {
-                var defaultValue = string.Equals(Container, "m2ts", StringComparison.OrdinalIgnoreCase)
-                    ? TransportStreamTimestamp.Valid
-                    : TransportStreamTimestamp.None;
+            var defaultValue = string.Equals(Container, "m2ts", StringComparison.OrdinalIgnoreCase)
+                ? TransportStreamTimestamp.Valid
+                : TransportStreamTimestamp.None;
 
-                return !IsDirectStream
-                    ? defaultValue
-                    : MediaSource is null ? defaultValue : MediaSource.Timestamp ?? TransportStreamTimestamp.None;
-            }
+            return !IsDirectStream
+                ? defaultValue
+                : MediaSource is null ? defaultValue : MediaSource.Timestamp ?? TransportStreamTimestamp.None;
         }
+    }
 
-        public int? TargetTotalBitrate => (TargetAudioBitrate ?? 0) + (TargetVideoBitrate ?? 0);
+    /// <summary>
+    /// Gets the target total bitrate of the output stream.
+    /// </summary>
+    /// <value>The target total bitrate.</value>
+    public int? TargetTotalBitrate => (TargetAudioBitrate ?? 0) + (TargetVideoBitrate ?? 0);
 
-        public bool? IsTargetAnamorphic
+    /// <summary>
+    /// Gets a value indicating whether the output stream is anamorphic.
+    /// </summary>
+    public bool? IsTargetAnamorphic
+    {
+        get
         {
-            get
+            if (IsDirectStream)
             {
-                if (IsDirectStream)
-                {
-                    return TargetVideoStream?.IsAnamorphic;
-                }
-
-                return false;
+                return TargetVideoStream?.IsAnamorphic;
             }
+
+            return false;
         }
+    }
 
-        public bool? IsTargetInterlaced
+    /// <summary>
+    /// Gets a value indicating whether the output stream is interlaced.
+    /// </summary>
+    public bool? IsTargetInterlaced
+    {
+        get
         {
-            get
+            if (IsDirectStream)
             {
-                if (IsDirectStream)
-                {
-                    return TargetVideoStream?.IsInterlaced;
-                }
-
-                var targetVideoCodecs = TargetVideoCodec;
-                var videoCodec = targetVideoCodecs.Length == 0 ? null : targetVideoCodecs[0];
-                if (!string.IsNullOrEmpty(videoCodec))
-                {
-                    if (string.Equals(GetOption(videoCodec, "deinterlace"), "true", StringComparison.OrdinalIgnoreCase))
-                    {
-                        return false;
-                    }
-                }
-
                 return TargetVideoStream?.IsInterlaced;
             }
-        }
 
-        public bool? IsTargetAVC
-        {
-            get
+            var targetVideoCodecs = TargetVideoCodec;
+            var videoCodec = targetVideoCodecs.Count == 0 ? null : targetVideoCodecs[0];
+            if (!string.IsNullOrEmpty(videoCodec))
             {
-                if (IsDirectStream)
+                if (string.Equals(GetOption(videoCodec, "deinterlace"), "true", StringComparison.OrdinalIgnoreCase))
                 {
-                    return TargetVideoStream?.IsAVC;
+                    return false;
                 }
-
-                return true;
             }
+
+            return TargetVideoStream?.IsInterlaced;
         }
+    }
 
-        public int? TargetWidth
+    /// <summary>
+    /// Gets a value indicating whether the output stream is AVC.
+    /// </summary>
+    public bool? IsTargetAVC
+    {
+        get
         {
-            get
+            if (IsDirectStream)
             {
-                var videoStream = TargetVideoStream;
-
-                if (videoStream is not null && videoStream.Width.HasValue && videoStream.Height.HasValue)
-                {
-                    ImageDimensions size = new ImageDimensions(videoStream.Width.Value, videoStream.Height.Value);
-
-                    size = DrawingUtils.Resize(size, 0, 0, MaxWidth ?? 0, MaxHeight ?? 0);
-
-                    return size.Width;
-                }
-
-                return MaxWidth;
+                return TargetVideoStream?.IsAVC;
             }
+
+            return true;
         }
+    }
 
-        public int? TargetHeight
+    /// <summary>
+    /// Gets the target width of the output stream.
+    /// </summary>
+    /// <value>The target width.</value>
+    public int? TargetWidth
+    {
+        get
         {
-            get
-            {
-                var videoStream = TargetVideoStream;
-
-                if (videoStream is not null && videoStream.Width.HasValue && videoStream.Height.HasValue)
-                {
-                    ImageDimensions size = new ImageDimensions(videoStream.Width.Value, videoStream.Height.Value);
+            var videoStream = TargetVideoStream;
 
-                    size = DrawingUtils.Resize(size, 0, 0, MaxWidth ?? 0, MaxHeight ?? 0);
+            if (videoStream is not null && videoStream.Width.HasValue && videoStream.Height.HasValue)
+            {
+                ImageDimensions size = new ImageDimensions(videoStream.Width.Value, videoStream.Height.Value);
 
-                    return size.Height;
-                }
+                size = DrawingUtils.Resize(size, 0, 0, MaxWidth ?? 0, MaxHeight ?? 0);
 
-                return MaxHeight;
+                return size.Width;
             }
+
+            return MaxWidth;
         }
+    }
 
-        public int? TargetVideoStreamCount
+    /// <summary>
+    /// Gets the target height of the output stream.
+    /// </summary>
+    /// <value>The target height.</value>
+    public int? TargetHeight
+    {
+        get
         {
-            get
+            var videoStream = TargetVideoStream;
+
+            if (videoStream is not null && videoStream.Width.HasValue && videoStream.Height.HasValue)
             {
-                if (IsDirectStream)
-                {
-                    return GetMediaStreamCount(MediaStreamType.Video, int.MaxValue);
-                }
+                ImageDimensions size = new ImageDimensions(videoStream.Width.Value, videoStream.Height.Value);
 
-                return GetMediaStreamCount(MediaStreamType.Video, 1);
+                size = DrawingUtils.Resize(size, 0, 0, MaxWidth ?? 0, MaxHeight ?? 0);
+
+                return size.Height;
             }
+
+            return MaxHeight;
         }
+    }
 
-        public int? TargetAudioStreamCount
+    /// <summary>
+    /// Gets the target video stream count of the output stream.
+    /// </summary>
+    /// <value>The target video stream count.</value>
+    public int? TargetVideoStreamCount
+    {
+        get
         {
-            get
+            if (IsDirectStream)
             {
-                if (IsDirectStream)
-                {
-                    return GetMediaStreamCount(MediaStreamType.Audio, int.MaxValue);
-                }
-
-                return GetMediaStreamCount(MediaStreamType.Audio, 1);
+                return GetMediaStreamCount(MediaStreamType.Video, int.MaxValue);
             }
+
+            return GetMediaStreamCount(MediaStreamType.Video, 1);
         }
+    }
 
-        public void SetOption(string? qualifier, string name, string value)
+    /// <summary>
+    /// Gets the target audio stream count of the output stream.
+    /// </summary>
+    /// <value>The target audio stream count.</value>
+    public int? TargetAudioStreamCount
+    {
+        get
         {
-            if (string.IsNullOrEmpty(qualifier))
+            if (IsDirectStream)
             {
-                SetOption(name, value);
-            }
-            else
-            {
-                SetOption(qualifier + "-" + name, value);
+                return GetMediaStreamCount(MediaStreamType.Audio, int.MaxValue);
             }
+
+            return GetMediaStreamCount(MediaStreamType.Audio, 1);
         }
+    }
 
-        public void SetOption(string name, string value)
+    /// <summary>
+    /// Sets a stream option.
+    /// </summary>
+    /// <param name="qualifier">The qualifier.</param>
+    /// <param name="name">The name.</param>
+    /// <param name="value">The value.</param>
+    public void SetOption(string? qualifier, string name, string value)
+    {
+        if (string.IsNullOrEmpty(qualifier))
         {
-            StreamOptions[name] = value;
+            SetOption(name, value);
         }
-
-        public string? GetOption(string? qualifier, string name)
+        else
         {
-            var value = GetOption(qualifier + "-" + name);
+            SetOption(qualifier + "-" + name, value);
+        }
+    }
 
-            if (string.IsNullOrEmpty(value))
-            {
-                value = GetOption(name);
-            }
+    /// <summary>
+    /// Sets a stream option.
+    /// </summary>
+    /// <param name="name">The name.</param>
+    /// <param name="value">The value.</param>
+    public void SetOption(string name, string value)
+    {
+        StreamOptions[name] = value;
+    }
 
-            return value;
-        }
+    /// <summary>
+    /// Gets a stream option.
+    /// </summary>
+    /// <param name="qualifier">The qualifier.</param>
+    /// <param name="name">The name.</param>
+    /// <returns>The value.</returns>
+    public string? GetOption(string? qualifier, string name)
+    {
+        var value = GetOption(qualifier + "-" + name);
 
-        public string? GetOption(string name)
+        if (string.IsNullOrEmpty(value))
         {
-            if (StreamOptions.TryGetValue(name, out var value))
-            {
-                return value;
-            }
-
-            return null;
+            value = GetOption(name);
         }
 
-        public string ToUrl(string baseUrl, string? accessToken)
+        return value;
+    }
+
+    /// <summary>
+    /// Gets a stream option.
+    /// </summary>
+    /// <param name="name">The name.</param>
+    /// <returns>The value.</returns>
+    public string? GetOption(string name)
+    {
+        if (StreamOptions.TryGetValue(name, out var value))
         {
-            ArgumentException.ThrowIfNullOrEmpty(baseUrl);
+            return value;
+        }
 
-            var list = new List<string>();
-            foreach (NameValuePair pair in BuildParams(this, accessToken))
-            {
-                if (string.IsNullOrEmpty(pair.Value))
-                {
-                    continue;
-                }
+        return null;
+    }
 
-                // Try to keep the url clean by omitting defaults
-                if (string.Equals(pair.Name, "StartTimeTicks", StringComparison.OrdinalIgnoreCase)
-                    && string.Equals(pair.Value, "0", StringComparison.OrdinalIgnoreCase))
-                {
-                    continue;
-                }
+    /// <summary>
+    /// Returns this output stream URL for this class.
+    /// </summary>
+    /// <param name="baseUrl">The base Url.</param>
+    /// <param name="accessToken">The access Token.</param>
+    /// <returns>A querystring representation of this object.</returns>
+    public string ToUrl(string baseUrl, string? accessToken)
+    {
+        ArgumentException.ThrowIfNullOrEmpty(baseUrl);
 
-                if (string.Equals(pair.Name, "SubtitleStreamIndex", StringComparison.OrdinalIgnoreCase)
-                    && string.Equals(pair.Value, "-1", StringComparison.OrdinalIgnoreCase))
-                {
-                    continue;
-                }
+        List<string> list = [];
+        foreach (NameValuePair pair in BuildParams(this, accessToken))
+        {
+            if (string.IsNullOrEmpty(pair.Value))
+            {
+                continue;
+            }
 
-                if (string.Equals(pair.Name, "Static", StringComparison.OrdinalIgnoreCase)
-                    && string.Equals(pair.Value, "false", StringComparison.OrdinalIgnoreCase))
-                {
-                    continue;
-                }
+            // Try to keep the url clean by omitting defaults
+            if (string.Equals(pair.Name, "StartTimeTicks", StringComparison.OrdinalIgnoreCase)
+                && string.Equals(pair.Value, "0", StringComparison.OrdinalIgnoreCase))
+            {
+                continue;
+            }
 
-                var encodedValue = pair.Value.Replace(" ", "%20", StringComparison.Ordinal);
+            if (string.Equals(pair.Name, "SubtitleStreamIndex", StringComparison.OrdinalIgnoreCase)
+                && string.Equals(pair.Value, "-1", StringComparison.OrdinalIgnoreCase))
+            {
+                continue;
+            }
 
-                list.Add(string.Format(CultureInfo.InvariantCulture, "{0}={1}", pair.Name, encodedValue));
+            if (string.Equals(pair.Name, "Static", StringComparison.OrdinalIgnoreCase)
+                && string.Equals(pair.Value, "false", StringComparison.OrdinalIgnoreCase))
+            {
+                continue;
             }
 
-            string queryString = string.Join('&', list);
+            var encodedValue = pair.Value.Replace(" ", "%20", StringComparison.Ordinal);
 
-            return GetUrl(baseUrl, queryString);
+            list.Add(string.Format(CultureInfo.InvariantCulture, "{0}={1}", pair.Name, encodedValue));
         }
 
-        private string GetUrl(string baseUrl, string queryString)
-        {
-            ArgumentException.ThrowIfNullOrEmpty(baseUrl);
+        string queryString = string.Join('&', list);
 
-            string extension = string.IsNullOrEmpty(Container) ? string.Empty : "." + Container;
+        return GetUrl(baseUrl, queryString);
+    }
 
-            baseUrl = baseUrl.TrimEnd('/');
+    private string GetUrl(string baseUrl, string queryString)
+    {
+        ArgumentException.ThrowIfNullOrEmpty(baseUrl);
 
-            if (MediaType == DlnaProfileType.Audio)
-            {
-                if (SubProtocol == MediaStreamProtocol.hls)
-                {
-                    return string.Format(CultureInfo.InvariantCulture, "{0}/audio/{1}/master.m3u8?{2}", baseUrl, ItemId, queryString);
-                }
+        string extension = string.IsNullOrEmpty(Container) ? string.Empty : "." + Container;
 
-                return string.Format(CultureInfo.InvariantCulture, "{0}/audio/{1}/stream{2}?{3}", baseUrl, ItemId, extension, queryString);
-            }
+        baseUrl = baseUrl.TrimEnd('/');
 
+        if (MediaType == DlnaProfileType.Audio)
+        {
             if (SubProtocol == MediaStreamProtocol.hls)
             {
-                return string.Format(CultureInfo.InvariantCulture, "{0}/videos/{1}/master.m3u8?{2}", baseUrl, ItemId, queryString);
+                return string.Format(CultureInfo.InvariantCulture, "{0}/audio/{1}/master.m3u8?{2}", baseUrl, ItemId, queryString);
             }
 
-            return string.Format(CultureInfo.InvariantCulture, "{0}/videos/{1}/stream{2}?{3}", baseUrl, ItemId, extension, queryString);
+            return string.Format(CultureInfo.InvariantCulture, "{0}/audio/{1}/stream{2}?{3}", baseUrl, ItemId, extension, queryString);
         }
 
-        private static IEnumerable<NameValuePair> BuildParams(StreamInfo item, string? accessToken)
+        if (SubProtocol == MediaStreamProtocol.hls)
         {
-            var list = new List<NameValuePair>();
+            return string.Format(CultureInfo.InvariantCulture, "{0}/videos/{1}/master.m3u8?{2}", baseUrl, ItemId, queryString);
+        }
 
-            string audioCodecs = item.AudioCodecs.Length == 0 ?
-                string.Empty :
-                string.Join(',', item.AudioCodecs);
+        return string.Format(CultureInfo.InvariantCulture, "{0}/videos/{1}/stream{2}?{3}", baseUrl, ItemId, extension, queryString);
+    }
 
-            string videoCodecs = item.VideoCodecs.Length == 0 ?
-                string.Empty :
-                string.Join(',', item.VideoCodecs);
+    private static List<NameValuePair> BuildParams(StreamInfo item, string? accessToken)
+    {
+        List<NameValuePair> list = [];
+
+        string audioCodecs = item.AudioCodecs.Count == 0 ?
+            string.Empty :
+            string.Join(',', item.AudioCodecs);
+
+        string videoCodecs = item.VideoCodecs.Count == 0 ?
+            string.Empty :
+            string.Join(',', item.VideoCodecs);
+
+        list.Add(new NameValuePair("DeviceProfileId", item.DeviceProfileId ?? string.Empty));
+        list.Add(new NameValuePair("DeviceId", item.DeviceId ?? string.Empty));
+        list.Add(new NameValuePair("MediaSourceId", item.MediaSourceId ?? string.Empty));
+        list.Add(new NameValuePair("Static", item.IsDirectStream.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()));
+        list.Add(new NameValuePair("VideoCodec", videoCodecs));
+        list.Add(new NameValuePair("AudioCodec", audioCodecs));
+        list.Add(new NameValuePair("AudioStreamIndex", item.AudioStreamIndex.HasValue ? item.AudioStreamIndex.Value.ToString(CultureInfo.InvariantCulture) : string.Empty));
+        list.Add(new NameValuePair("SubtitleStreamIndex", item.SubtitleStreamIndex.HasValue && item.SubtitleDeliveryMethod != SubtitleDeliveryMethod.External ? item.SubtitleStreamIndex.Value.ToString(CultureInfo.InvariantCulture) : string.Empty));
+        list.Add(new NameValuePair("VideoBitrate", item.VideoBitrate.HasValue ? item.VideoBitrate.Value.ToString(CultureInfo.InvariantCulture) : string.Empty));
+        list.Add(new NameValuePair("AudioBitrate", item.AudioBitrate.HasValue ? item.AudioBitrate.Value.ToString(CultureInfo.InvariantCulture) : string.Empty));
+        list.Add(new NameValuePair("AudioSampleRate", item.AudioSampleRate.HasValue ? item.AudioSampleRate.Value.ToString(CultureInfo.InvariantCulture) : string.Empty));
+
+        list.Add(new NameValuePair("MaxFramerate", item.MaxFramerate.HasValue ? item.MaxFramerate.Value.ToString(CultureInfo.InvariantCulture) : string.Empty));
+        list.Add(new NameValuePair("MaxWidth", item.MaxWidth.HasValue ? item.MaxWidth.Value.ToString(CultureInfo.InvariantCulture) : string.Empty));
+        list.Add(new NameValuePair("MaxHeight", item.MaxHeight.HasValue ? item.MaxHeight.Value.ToString(CultureInfo.InvariantCulture) : string.Empty));
+
+        long startPositionTicks = item.StartPositionTicks;
+
+        if (item.SubProtocol == MediaStreamProtocol.hls)
+        {
+            list.Add(new NameValuePair("StartTimeTicks", string.Empty));
+        }
+        else
+        {
+            list.Add(new NameValuePair("StartTimeTicks", startPositionTicks.ToString(CultureInfo.InvariantCulture)));
+        }
 
-            list.Add(new NameValuePair("DeviceProfileId", item.DeviceProfileId ?? string.Empty));
-            list.Add(new NameValuePair("DeviceId", item.DeviceId ?? string.Empty));
-            list.Add(new NameValuePair("MediaSourceId", item.MediaSourceId ?? string.Empty));
-            list.Add(new NameValuePair("Static", item.IsDirectStream.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()));
-            list.Add(new NameValuePair("VideoCodec", videoCodecs));
-            list.Add(new NameValuePair("AudioCodec", audioCodecs));
-            list.Add(new NameValuePair("AudioStreamIndex", item.AudioStreamIndex.HasValue ? item.AudioStreamIndex.Value.ToString(CultureInfo.InvariantCulture) : string.Empty));
-            list.Add(new NameValuePair("SubtitleStreamIndex", item.SubtitleStreamIndex.HasValue && item.SubtitleDeliveryMethod != SubtitleDeliveryMethod.External ? item.SubtitleStreamIndex.Value.ToString(CultureInfo.InvariantCulture) : string.Empty));
-            list.Add(new NameValuePair("VideoBitrate", item.VideoBitrate.HasValue ? item.VideoBitrate.Value.ToString(CultureInfo.InvariantCulture) : string.Empty));
-            list.Add(new NameValuePair("AudioBitrate", item.AudioBitrate.HasValue ? item.AudioBitrate.Value.ToString(CultureInfo.InvariantCulture) : string.Empty));
-            list.Add(new NameValuePair("AudioSampleRate", item.AudioSampleRate.HasValue ? item.AudioSampleRate.Value.ToString(CultureInfo.InvariantCulture) : string.Empty));
+        list.Add(new NameValuePair("PlaySessionId", item.PlaySessionId ?? string.Empty));
+        list.Add(new NameValuePair("api_key", accessToken ?? string.Empty));
 
-            list.Add(new NameValuePair("MaxFramerate", item.MaxFramerate.HasValue ? item.MaxFramerate.Value.ToString(CultureInfo.InvariantCulture) : string.Empty));
-            list.Add(new NameValuePair("MaxWidth", item.MaxWidth.HasValue ? item.MaxWidth.Value.ToString(CultureInfo.InvariantCulture) : string.Empty));
-            list.Add(new NameValuePair("MaxHeight", item.MaxHeight.HasValue ? item.MaxHeight.Value.ToString(CultureInfo.InvariantCulture) : string.Empty));
+        string? liveStreamId = item.MediaSource?.LiveStreamId;
+        list.Add(new NameValuePair("LiveStreamId", liveStreamId ?? string.Empty));
 
-            long startPositionTicks = item.StartPositionTicks;
+        list.Add(new NameValuePair("SubtitleMethod", item.SubtitleStreamIndex.HasValue && item.SubtitleDeliveryMethod != SubtitleDeliveryMethod.External ? item.SubtitleDeliveryMethod.ToString() : string.Empty));
 
-            if (item.SubProtocol == MediaStreamProtocol.hls)
-            {
-                list.Add(new NameValuePair("StartTimeTicks", string.Empty));
-            }
-            else
+        if (!item.IsDirectStream)
+        {
+            if (item.RequireNonAnamorphic)
             {
-                list.Add(new NameValuePair("StartTimeTicks", startPositionTicks.ToString(CultureInfo.InvariantCulture)));
+                list.Add(new NameValuePair("RequireNonAnamorphic", item.RequireNonAnamorphic.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()));
             }
 
-            list.Add(new NameValuePair("PlaySessionId", item.PlaySessionId ?? string.Empty));
-            list.Add(new NameValuePair("api_key", accessToken ?? string.Empty));
-
-            string? liveStreamId = item.MediaSource?.LiveStreamId;
-            list.Add(new NameValuePair("LiveStreamId", liveStreamId ?? string.Empty));
+            list.Add(new NameValuePair("TranscodingMaxAudioChannels", item.TranscodingMaxAudioChannels.HasValue ? item.TranscodingMaxAudioChannels.Value.ToString(CultureInfo.InvariantCulture) : string.Empty));
 
-            list.Add(new NameValuePair("SubtitleMethod", item.SubtitleStreamIndex.HasValue && item.SubtitleDeliveryMethod != SubtitleDeliveryMethod.External ? item.SubtitleDeliveryMethod.ToString() : string.Empty));
-
-            if (!item.IsDirectStream)
+            if (item.EnableSubtitlesInManifest)
             {
-                if (item.RequireNonAnamorphic)
-                {
-                    list.Add(new NameValuePair("RequireNonAnamorphic", item.RequireNonAnamorphic.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()));
-                }
-
-                list.Add(new NameValuePair("TranscodingMaxAudioChannels", item.TranscodingMaxAudioChannels.HasValue ? item.TranscodingMaxAudioChannels.Value.ToString(CultureInfo.InvariantCulture) : string.Empty));
-
-                if (item.EnableSubtitlesInManifest)
-                {
-                    list.Add(new NameValuePair("EnableSubtitlesInManifest", item.EnableSubtitlesInManifest.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()));
-                }
-
-                if (item.EnableMpegtsM2TsMode)
-                {
-                    list.Add(new NameValuePair("EnableMpegtsM2TsMode", item.EnableMpegtsM2TsMode.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()));
-                }
-
-                if (item.EstimateContentLength)
-                {
-                    list.Add(new NameValuePair("EstimateContentLength", item.EstimateContentLength.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()));
-                }
+                list.Add(new NameValuePair("EnableSubtitlesInManifest", item.EnableSubtitlesInManifest.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()));
+            }
 
-                if (item.TranscodeSeekInfo != TranscodeSeekInfo.Auto)
-                {
-                    list.Add(new NameValuePair("TranscodeSeekInfo", item.TranscodeSeekInfo.ToString().ToLowerInvariant()));
-                }
+            if (item.EnableMpegtsM2TsMode)
+            {
+                list.Add(new NameValuePair("EnableMpegtsM2TsMode", item.EnableMpegtsM2TsMode.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()));
+            }
 
-                if (item.CopyTimestamps)
-                {
-                    list.Add(new NameValuePair("CopyTimestamps", item.CopyTimestamps.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()));
-                }
+            if (item.EstimateContentLength)
+            {
+                list.Add(new NameValuePair("EstimateContentLength", item.EstimateContentLength.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()));
+            }
 
-                list.Add(new NameValuePair("RequireAvc", item.RequireAvc.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()));
+            if (item.TranscodeSeekInfo != TranscodeSeekInfo.Auto)
+            {
+                list.Add(new NameValuePair("TranscodeSeekInfo", item.TranscodeSeekInfo.ToString().ToLowerInvariant()));
+            }
 
-                list.Add(new NameValuePair("EnableAudioVbrEncoding", item.EnableAudioVbrEncoding.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()));
+            if (item.CopyTimestamps)
+            {
+                list.Add(new NameValuePair("CopyTimestamps", item.CopyTimestamps.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()));
             }
 
-            list.Add(new NameValuePair("Tag", item.MediaSource?.ETag ?? string.Empty));
+            list.Add(new NameValuePair("RequireAvc", item.RequireAvc.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()));
 
-            string subtitleCodecs = item.SubtitleCodecs.Length == 0 ?
-               string.Empty :
-               string.Join(",", item.SubtitleCodecs);
+            list.Add(new NameValuePair("EnableAudioVbrEncoding", item.EnableAudioVbrEncoding.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()));
+        }
 
-            list.Add(new NameValuePair("SubtitleCodec", item.SubtitleStreamIndex.HasValue && item.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Embed ? subtitleCodecs : string.Empty));
+        list.Add(new NameValuePair("Tag", item.MediaSource?.ETag ?? string.Empty));
 
-            if (item.SubProtocol == MediaStreamProtocol.hls)
-            {
-                list.Add(new NameValuePair("SegmentContainer", item.Container ?? string.Empty));
+        string subtitleCodecs = item.SubtitleCodecs.Count == 0 ?
+            string.Empty :
+            string.Join(",", item.SubtitleCodecs);
 
-                if (item.SegmentLength.HasValue)
-                {
-                    list.Add(new NameValuePair("SegmentLength", item.SegmentLength.Value.ToString(CultureInfo.InvariantCulture)));
-                }
+        list.Add(new NameValuePair("SubtitleCodec", item.SubtitleStreamIndex.HasValue && item.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Embed ? subtitleCodecs : string.Empty));
 
-                if (item.MinSegments.HasValue)
-                {
-                    list.Add(new NameValuePair("MinSegments", item.MinSegments.Value.ToString(CultureInfo.InvariantCulture)));
-                }
+        if (item.SubProtocol == MediaStreamProtocol.hls)
+        {
+            list.Add(new NameValuePair("SegmentContainer", item.Container ?? string.Empty));
 
-                list.Add(new NameValuePair("BreakOnNonKeyFrames", item.BreakOnNonKeyFrames.ToString(CultureInfo.InvariantCulture)));
+            if (item.SegmentLength.HasValue)
+            {
+                list.Add(new NameValuePair("SegmentLength", item.SegmentLength.Value.ToString(CultureInfo.InvariantCulture)));
             }
 
-            foreach (var pair in item.StreamOptions)
+            if (item.MinSegments.HasValue)
             {
-                if (string.IsNullOrEmpty(pair.Value))
-                {
-                    continue;
-                }
-
-                // strip spaces to avoid having to encode h264 profile names
-                list.Add(new NameValuePair(pair.Key, pair.Value.Replace(" ", string.Empty, StringComparison.Ordinal)));
+                list.Add(new NameValuePair("MinSegments", item.MinSegments.Value.ToString(CultureInfo.InvariantCulture)));
             }
 
-            if (!item.IsDirectStream)
+            list.Add(new NameValuePair("BreakOnNonKeyFrames", item.BreakOnNonKeyFrames.ToString(CultureInfo.InvariantCulture)));
+        }
+
+        foreach (var pair in item.StreamOptions)
+        {
+            if (string.IsNullOrEmpty(pair.Value))
             {
-                list.Add(new NameValuePair("TranscodeReasons", item.TranscodeReasons.ToString()));
+                continue;
             }
 
-            return list;
+            // strip spaces to avoid having to encode h264 profile names
+            list.Add(new NameValuePair(pair.Key, pair.Value.Replace(" ", string.Empty, StringComparison.Ordinal)));
         }
 
-        public IEnumerable<SubtitleStreamInfo> GetSubtitleProfiles(ITranscoderSupport transcoderSupport, bool includeSelectedTrackOnly, string baseUrl, string? accessToken)
+        if (!item.IsDirectStream)
         {
-            return GetSubtitleProfiles(transcoderSupport, includeSelectedTrackOnly, false, baseUrl, accessToken);
+            list.Add(new NameValuePair("TranscodeReasons", item.TranscodeReasons.ToString()));
         }
 
-        public IEnumerable<SubtitleStreamInfo> GetSubtitleProfiles(ITranscoderSupport transcoderSupport, bool includeSelectedTrackOnly, bool enableAllProfiles, string baseUrl, string? accessToken)
+        return list;
+    }
+
+    /// <summary>
+    /// Gets the subtitle profiles.
+    /// </summary>
+    /// <param name="transcoderSupport">The transcoder support.</param>
+    /// <param name="includeSelectedTrackOnly">If only the selected track should be included.</param>
+    /// <param name="baseUrl">The base URL.</param>
+    /// <param name="accessToken">The access token.</param>
+    /// <returns>The <see cref="SubtitleStreamInfo"/> of the profiles.</returns>
+    public IEnumerable<SubtitleStreamInfo> GetSubtitleProfiles(ITranscoderSupport transcoderSupport, bool includeSelectedTrackOnly, string baseUrl, string? accessToken)
+    {
+        return GetSubtitleProfiles(transcoderSupport, includeSelectedTrackOnly, false, baseUrl, accessToken);
+    }
+
+    /// <summary>
+    /// Gets the subtitle profiles.
+    /// </summary>
+    /// <param name="transcoderSupport">The transcoder support.</param>
+    /// <param name="includeSelectedTrackOnly">If only the selected track should be included.</param>
+    /// <param name="enableAllProfiles">If all profiles are enabled.</param>
+    /// <param name="baseUrl">The base URL.</param>
+    /// <param name="accessToken">The access token.</param>
+    /// <returns>The <see cref="SubtitleStreamInfo"/> of the profiles.</returns>
+    public IEnumerable<SubtitleStreamInfo> GetSubtitleProfiles(ITranscoderSupport transcoderSupport, bool includeSelectedTrackOnly, bool enableAllProfiles, string baseUrl, string? accessToken)
+    {
+        if (MediaSource is null)
         {
-            if (MediaSource is null)
-            {
-                return Enumerable.Empty<SubtitleStreamInfo>();
-            }
+            return [];
+        }
 
-            var list = new List<SubtitleStreamInfo>();
+        List<SubtitleStreamInfo> list = [];
 
-            // HLS will preserve timestamps so we can just grab the full subtitle stream
-            long startPositionTicks = SubProtocol == MediaStreamProtocol.hls
-                ? 0
-                : (PlayMethod == PlayMethod.Transcode && !CopyTimestamps ? StartPositionTicks : 0);
+        // HLS will preserve timestamps so we can just grab the full subtitle stream
+        long startPositionTicks = SubProtocol == MediaStreamProtocol.hls
+            ? 0
+            : (PlayMethod == PlayMethod.Transcode && !CopyTimestamps ? StartPositionTicks : 0);
 
-            // First add the selected track
-            if (SubtitleStreamIndex.HasValue)
+        // First add the selected track
+        if (SubtitleStreamIndex.HasValue)
+        {
+            foreach (var stream in MediaSource.MediaStreams)
             {
-                foreach (var stream in MediaSource.MediaStreams)
+                if (stream.Type == MediaStreamType.Subtitle && stream.Index == SubtitleStreamIndex.Value)
                 {
-                    if (stream.Type == MediaStreamType.Subtitle && stream.Index == SubtitleStreamIndex.Value)
-                    {
-                        AddSubtitleProfiles(list, stream, transcoderSupport, enableAllProfiles, baseUrl, accessToken, startPositionTicks);
-                    }
+                    AddSubtitleProfiles(list, stream, transcoderSupport, enableAllProfiles, baseUrl, accessToken, startPositionTicks);
                 }
             }
+        }
 
-            if (!includeSelectedTrackOnly)
+        if (!includeSelectedTrackOnly)
+        {
+            foreach (var stream in MediaSource.MediaStreams)
             {
-                foreach (var stream in MediaSource.MediaStreams)
+                if (stream.Type == MediaStreamType.Subtitle && (!SubtitleStreamIndex.HasValue || stream.Index != SubtitleStreamIndex.Value))
                 {
-                    if (stream.Type == MediaStreamType.Subtitle && (!SubtitleStreamIndex.HasValue || stream.Index != SubtitleStreamIndex.Value))
-                    {
-                        AddSubtitleProfiles(list, stream, transcoderSupport, enableAllProfiles, baseUrl, accessToken, startPositionTicks);
-                    }
+                    AddSubtitleProfiles(list, stream, transcoderSupport, enableAllProfiles, baseUrl, accessToken, startPositionTicks);
                 }
             }
-
-            return list;
         }
 
-        private void AddSubtitleProfiles(List<SubtitleStreamInfo> list, MediaStream stream, ITranscoderSupport transcoderSupport, bool enableAllProfiles, string baseUrl, string? accessToken, long startPositionTicks)
+        return list;
+    }
+
+    private void AddSubtitleProfiles(List<SubtitleStreamInfo> list, MediaStream stream, ITranscoderSupport transcoderSupport, bool enableAllProfiles, string baseUrl, string? accessToken, long startPositionTicks)
+    {
+        if (enableAllProfiles)
         {
-            if (enableAllProfiles)
-            {
-                foreach (var profile in DeviceProfile.SubtitleProfiles)
-                {
-                    var info = GetSubtitleStreamInfo(stream, baseUrl, accessToken, startPositionTicks, new[] { profile }, transcoderSupport);
-                    if (info is not null)
-                    {
-                        list.Add(info);
-                    }
-                }
-            }
-            else
+            foreach (var profile in DeviceProfile.SubtitleProfiles)
             {
-                var info = GetSubtitleStreamInfo(stream, baseUrl, accessToken, startPositionTicks, DeviceProfile.SubtitleProfiles, transcoderSupport);
+                var info = GetSubtitleStreamInfo(stream, baseUrl, accessToken, startPositionTicks, new[] { profile }, transcoderSupport);
                 if (info is not null)
                 {
                     list.Add(info);
                 }
             }
         }
-
-        private SubtitleStreamInfo? GetSubtitleStreamInfo(MediaStream stream, string baseUrl, string? accessToken, long startPositionTicks, SubtitleProfile[] subtitleProfiles, ITranscoderSupport transcoderSupport)
+        else
         {
-            if (MediaSource is null)
+            var info = GetSubtitleStreamInfo(stream, baseUrl, accessToken, startPositionTicks, DeviceProfile.SubtitleProfiles, transcoderSupport);
+            if (info is not null)
             {
-                return null;
+                list.Add(info);
             }
+        }
+    }
 
-            var subtitleProfile = StreamBuilder.GetSubtitleProfile(MediaSource, stream, subtitleProfiles, PlayMethod, transcoderSupport, Container, SubProtocol);
-            var info = new SubtitleStreamInfo
-            {
-                IsForced = stream.IsForced,
-                Language = stream.Language,
-                Name = stream.Language ?? "Unknown",
-                Format = subtitleProfile.Format,
-                Index = stream.Index,
-                DeliveryMethod = subtitleProfile.Method,
-                DisplayTitle = stream.DisplayTitle
-            };
+    private SubtitleStreamInfo? GetSubtitleStreamInfo(MediaStream stream, string baseUrl, string? accessToken, long startPositionTicks, SubtitleProfile[] subtitleProfiles, ITranscoderSupport transcoderSupport)
+    {
+        if (MediaSource is null)
+        {
+            return null;
+        }
 
-            if (info.DeliveryMethod == SubtitleDeliveryMethod.External)
+        var subtitleProfile = StreamBuilder.GetSubtitleProfile(MediaSource, stream, subtitleProfiles, PlayMethod, transcoderSupport, Container, SubProtocol);
+        var info = new SubtitleStreamInfo
+        {
+            IsForced = stream.IsForced,
+            Language = stream.Language,
+            Name = stream.Language ?? "Unknown",
+            Format = subtitleProfile.Format,
+            Index = stream.Index,
+            DeliveryMethod = subtitleProfile.Method,
+            DisplayTitle = stream.DisplayTitle
+        };
+
+        if (info.DeliveryMethod == SubtitleDeliveryMethod.External)
+        {
+            if (MediaSource.Protocol == MediaProtocol.File || !string.Equals(stream.Codec, subtitleProfile.Format, StringComparison.OrdinalIgnoreCase) || !stream.IsExternal)
             {
-                if (MediaSource.Protocol == MediaProtocol.File || !string.Equals(stream.Codec, subtitleProfile.Format, StringComparison.OrdinalIgnoreCase) || !stream.IsExternal)
+                info.Url = string.Format(
+                    CultureInfo.InvariantCulture,
+                    "{0}/Videos/{1}/{2}/Subtitles/{3}/{4}/Stream.{5}",
+                    baseUrl,
+                    ItemId,
+                    MediaSourceId,
+                    stream.Index.ToString(CultureInfo.InvariantCulture),
+                    startPositionTicks.ToString(CultureInfo.InvariantCulture),
+                    subtitleProfile.Format);
+
+                if (!string.IsNullOrEmpty(accessToken))
                 {
-                    info.Url = string.Format(
-                        CultureInfo.InvariantCulture,
-                        "{0}/Videos/{1}/{2}/Subtitles/{3}/{4}/Stream.{5}",
-                        baseUrl,
-                        ItemId,
-                        MediaSourceId,
-                        stream.Index.ToString(CultureInfo.InvariantCulture),
-                        startPositionTicks.ToString(CultureInfo.InvariantCulture),
-                        subtitleProfile.Format);
-
-                    if (!string.IsNullOrEmpty(accessToken))
-                    {
-                        info.Url += "?api_key=" + accessToken;
-                    }
-
-                    info.IsExternalUrl = false;
+                    info.Url += "?api_key=" + accessToken;
                 }
-                else
-                {
-                    info.Url = stream.Path;
-                    info.IsExternalUrl = true;
-                }
-            }
-
-            return info;
-        }
-
-        public int? GetTargetVideoBitDepth(string? codec)
-        {
-            var value = GetOption(codec, "videobitdepth");
 
-            if (int.TryParse(value, CultureInfo.InvariantCulture, out var result))
+                info.IsExternalUrl = false;
+            }
+            else
             {
-                return result;
+                info.Url = stream.Path;
+                info.IsExternalUrl = true;
             }
-
-            return null;
         }
 
-        public int? GetTargetAudioBitDepth(string? codec)
-        {
-            var value = GetOption(codec, "audiobitdepth");
+        return info;
+    }
 
-            if (int.TryParse(value, CultureInfo.InvariantCulture, out var result))
-            {
-                return result;
-            }
+    /// <summary>
+    /// Gets the target video bit depth.
+    /// </summary>
+    /// <param name="codec">The codec.</param>
+    /// <returns>The target video bit depth.</returns>
+    public int? GetTargetVideoBitDepth(string? codec)
+    {
+        var value = GetOption(codec, "videobitdepth");
 
-            return null;
+        if (int.TryParse(value, CultureInfo.InvariantCulture, out var result))
+        {
+            return result;
         }
 
-        public double? GetTargetVideoLevel(string? codec)
-        {
-            var value = GetOption(codec, "level");
+        return null;
+    }
 
-            if (double.TryParse(value, CultureInfo.InvariantCulture, out var result))
-            {
-                return result;
-            }
+    /// <summary>
+    /// Gets the target audio bit depth.
+    /// </summary>
+    /// <param name="codec">The codec.</param>
+    /// <returns>The target audio bit depth.</returns>
+    public int? GetTargetAudioBitDepth(string? codec)
+    {
+        var value = GetOption(codec, "audiobitdepth");
 
-            return null;
+        if (int.TryParse(value, CultureInfo.InvariantCulture, out var result))
+        {
+            return result;
         }
 
-        public int? GetTargetRefFrames(string? codec)
-        {
-            var value = GetOption(codec, "maxrefframes");
+        return null;
+    }
 
-            if (int.TryParse(value, CultureInfo.InvariantCulture, out var result))
-            {
-                return result;
-            }
+    /// <summary>
+    /// Gets the target video level.
+    /// </summary>
+    /// <param name="codec">The codec.</param>
+    /// <returns>The target video level.</returns>
+    public double? GetTargetVideoLevel(string? codec)
+    {
+        var value = GetOption(codec, "level");
 
-            return null;
+        if (double.TryParse(value, CultureInfo.InvariantCulture, out var result))
+        {
+            return result;
         }
 
-        public int? GetTargetAudioChannels(string? codec)
+        return null;
+    }
+
+    /// <summary>
+    /// Gets the target reference frames.
+    /// </summary>
+    /// <param name="codec">The codec.</param>
+    /// <returns>The target reference frames.</returns>
+    public int? GetTargetRefFrames(string? codec)
+    {
+        var value = GetOption(codec, "maxrefframes");
+
+        if (int.TryParse(value, CultureInfo.InvariantCulture, out var result))
         {
-            var defaultValue = GlobalMaxAudioChannels ?? TranscodingMaxAudioChannels;
+            return result;
+        }
 
-            var value = GetOption(codec, "audiochannels");
-            if (string.IsNullOrEmpty(value))
-            {
-                return defaultValue;
-            }
+        return null;
+    }
 
-            if (int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result))
-            {
-                return Math.Min(result, defaultValue ?? result);
-            }
+    /// <summary>
+    /// Gets the target audio channels.
+    /// </summary>
+    /// <param name="codec">The codec.</param>
+    /// <returns>The target audio channels.</returns>
+    public int? GetTargetAudioChannels(string? codec)
+    {
+        var defaultValue = GlobalMaxAudioChannels ?? TranscodingMaxAudioChannels;
 
+        var value = GetOption(codec, "audiochannels");
+        if (string.IsNullOrEmpty(value))
+        {
             return defaultValue;
         }
 
-        private int? GetMediaStreamCount(MediaStreamType type, int limit)
+        if (int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result))
         {
-            var count = MediaSource?.GetStreamCount(type);
+            return Math.Min(result, defaultValue ?? result);
+        }
 
-            if (count.HasValue)
-            {
-                count = Math.Min(count.Value, limit);
-            }
+        return defaultValue;
+    }
+
+    /// <summary>
+    /// Gets the media stream count.
+    /// </summary>
+    /// <param name="type">The type.</param>
+    /// <param name="limit">The limit.</param>
+    /// <returns>The media stream count.</returns>
+    private int? GetMediaStreamCount(MediaStreamType type, int limit)
+    {
+        var count = MediaSource?.GetStreamCount(type);
 
-            return count;
+        if (count.HasValue)
+        {
+            count = Math.Min(count.Value, limit);
         }
+
+        return count;
     }
 }

+ 49 - 35
MediaBrowser.Model/Dlna/SubtitleProfile.cs

@@ -1,48 +1,62 @@
 #nullable disable
-#pragma warning disable CS1591
 
-using System;
 using System.Xml.Serialization;
-using Jellyfin.Extensions;
+using MediaBrowser.Model.Extensions;
 
-namespace MediaBrowser.Model.Dlna
+namespace MediaBrowser.Model.Dlna;
+
+/// <summary>
+/// A class for subtitle profile information.
+/// </summary>
+public class SubtitleProfile
 {
-    public class SubtitleProfile
+    /// <summary>
+    /// Gets or sets the format.
+    /// </summary>
+    [XmlAttribute("format")]
+    public string Format { get; set; }
+
+    /// <summary>
+    /// Gets or sets the delivery method.
+    /// </summary>
+    [XmlAttribute("method")]
+    public SubtitleDeliveryMethod Method { get; set; }
+
+    /// <summary>
+    /// Gets or sets the DIDL mode.
+    /// </summary>
+    [XmlAttribute("didlMode")]
+    public string DidlMode { get; set; }
+
+    /// <summary>
+    /// Gets or sets the language.
+    /// </summary>
+    [XmlAttribute("language")]
+    public string Language { get; set; }
+
+    /// <summary>
+    /// Gets or sets the container.
+    /// </summary>
+    [XmlAttribute("container")]
+    public string Container { get; set; }
+
+    /// <summary>
+    /// Checks if a language is supported.
+    /// </summary>
+    /// <param name="subLanguage">The language to check for support.</param>
+    /// <returns><c>true</c> if supported.</returns>
+    public bool SupportsLanguage(string subLanguage)
     {
-        [XmlAttribute("format")]
-        public string Format { get; set; }
-
-        [XmlAttribute("method")]
-        public SubtitleDeliveryMethod Method { get; set; }
-
-        [XmlAttribute("didlMode")]
-        public string DidlMode { get; set; }
-
-        [XmlAttribute("language")]
-        public string Language { get; set; }
-
-        [XmlAttribute("container")]
-        public string Container { get; set; }
-
-        public string[] GetLanguages()
+        if (string.IsNullOrEmpty(Language))
         {
-            return ContainerProfile.SplitValue(Language);
+            return true;
         }
 
-        public bool SupportsLanguage(string subLanguage)
+        if (string.IsNullOrEmpty(subLanguage))
         {
-            if (string.IsNullOrEmpty(Language))
-            {
-                return true;
-            }
-
-            if (string.IsNullOrEmpty(subLanguage))
-            {
-                subLanguage = "und";
-            }
-
-            var languages = GetLanguages();
-            return languages.Length == 0 || languages.Contains(subLanguage, StringComparison.OrdinalIgnoreCase);
+            subLanguage = "und";
         }
+
+        return ContainerHelper.ContainsContainer(Language, subLanguage);
     }
 }

+ 122 - 74
MediaBrowser.Model/Dlna/TranscodingProfile.cs

@@ -1,82 +1,130 @@
-#pragma warning disable CS1591
-
-using System;
 using System.ComponentModel;
 using System.Xml.Serialization;
 using Jellyfin.Data.Enums;
 
-namespace MediaBrowser.Model.Dlna
+namespace MediaBrowser.Model.Dlna;
+
+/// <summary>
+/// A class for transcoding profile information.
+/// </summary>
+public class TranscodingProfile
 {
-    public class TranscodingProfile
+    /// <summary>
+    /// Initializes a new instance of the <see cref="TranscodingProfile" /> class.
+    /// </summary>
+    public TranscodingProfile()
     {
-        public TranscodingProfile()
-        {
-            Conditions = Array.Empty<ProfileCondition>();
-        }
-
-        [XmlAttribute("container")]
-        public string Container { get; set; } = string.Empty;
-
-        [XmlAttribute("type")]
-        public DlnaProfileType Type { get; set; }
-
-        [XmlAttribute("videoCodec")]
-        public string VideoCodec { get; set; } = string.Empty;
-
-        [XmlAttribute("audioCodec")]
-        public string AudioCodec { get; set; } = string.Empty;
-
-        [XmlAttribute("protocol")]
-        public MediaStreamProtocol Protocol { get; set; } = MediaStreamProtocol.http;
-
-        [DefaultValue(false)]
-        [XmlAttribute("estimateContentLength")]
-        public bool EstimateContentLength { get; set; }
-
-        [DefaultValue(false)]
-        [XmlAttribute("enableMpegtsM2TsMode")]
-        public bool EnableMpegtsM2TsMode { get; set; }
-
-        [DefaultValue(TranscodeSeekInfo.Auto)]
-        [XmlAttribute("transcodeSeekInfo")]
-        public TranscodeSeekInfo TranscodeSeekInfo { get; set; }
-
-        [DefaultValue(false)]
-        [XmlAttribute("copyTimestamps")]
-        public bool CopyTimestamps { get; set; }
-
-        [DefaultValue(EncodingContext.Streaming)]
-        [XmlAttribute("context")]
-        public EncodingContext Context { get; set; }
-
-        [DefaultValue(false)]
-        [XmlAttribute("enableSubtitlesInManifest")]
-        public bool EnableSubtitlesInManifest { get; set; }
-
-        [XmlAttribute("maxAudioChannels")]
-        public string? MaxAudioChannels { get; set; }
-
-        [DefaultValue(0)]
-        [XmlAttribute("minSegments")]
-        public int MinSegments { get; set; }
-
-        [DefaultValue(0)]
-        [XmlAttribute("segmentLength")]
-        public int SegmentLength { get; set; }
-
-        [DefaultValue(false)]
-        [XmlAttribute("breakOnNonKeyFrames")]
-        public bool BreakOnNonKeyFrames { get; set; }
-
-        public ProfileCondition[] Conditions { get; set; }
-
-        [DefaultValue(true)]
-        [XmlAttribute("enableAudioVbrEncoding")]
-        public bool EnableAudioVbrEncoding { get; set; } = true;
-
-        public string[] GetAudioCodecs()
-        {
-            return ContainerProfile.SplitValue(AudioCodec);
-        }
+        Conditions = [];
     }
+
+    /// <summary>
+    /// Gets or sets the container.
+    /// </summary>
+    [XmlAttribute("container")]
+    public string Container { get; set; } = string.Empty;
+
+    /// <summary>
+    /// Gets or sets the DLNA profile type.
+    /// </summary>
+    [XmlAttribute("type")]
+    public DlnaProfileType Type { get; set; }
+
+    /// <summary>
+    /// Gets or sets the video codec.
+    /// </summary>
+    [XmlAttribute("videoCodec")]
+    public string VideoCodec { get; set; } = string.Empty;
+
+    /// <summary>
+    /// Gets or sets the audio codec.
+    /// </summary>
+    [XmlAttribute("audioCodec")]
+    public string AudioCodec { get; set; } = string.Empty;
+
+    /// <summary>
+    /// Gets or sets the protocol.
+    /// </summary>
+    [XmlAttribute("protocol")]
+    public MediaStreamProtocol Protocol { get; set; } = MediaStreamProtocol.http;
+
+    /// <summary>
+    /// Gets or sets a value indicating whether the content length should be estimated.
+    /// </summary>
+    [DefaultValue(false)]
+    [XmlAttribute("estimateContentLength")]
+    public bool EstimateContentLength { get; set; }
+
+    /// <summary>
+    /// Gets or sets a value indicating whether M2TS mode is enabled.
+    /// </summary>
+    [DefaultValue(false)]
+    [XmlAttribute("enableMpegtsM2TsMode")]
+    public bool EnableMpegtsM2TsMode { get; set; }
+
+    /// <summary>
+    /// Gets or sets the transcoding seek info mode.
+    /// </summary>
+    [DefaultValue(TranscodeSeekInfo.Auto)]
+    [XmlAttribute("transcodeSeekInfo")]
+    public TranscodeSeekInfo TranscodeSeekInfo { get; set; }
+
+    /// <summary>
+    /// Gets or sets a value indicating whether timestamps should be copied.
+    /// </summary>
+    [DefaultValue(false)]
+    [XmlAttribute("copyTimestamps")]
+    public bool CopyTimestamps { get; set; }
+
+    /// <summary>
+    /// Gets or sets the encoding context.
+    /// </summary>
+    [DefaultValue(EncodingContext.Streaming)]
+    [XmlAttribute("context")]
+    public EncodingContext Context { get; set; }
+
+    /// <summary>
+    /// Gets or sets a value indicating whether subtitles are allowed in the manifest.
+    /// </summary>
+    [DefaultValue(false)]
+    [XmlAttribute("enableSubtitlesInManifest")]
+    public bool EnableSubtitlesInManifest { get; set; }
+
+    /// <summary>
+    /// Gets or sets the maximum audio channels.
+    /// </summary>
+    [XmlAttribute("maxAudioChannels")]
+    public string? MaxAudioChannels { get; set; }
+
+    /// <summary>
+    /// Gets or sets the minimum amount of segments.
+    /// </summary>
+    [DefaultValue(0)]
+    [XmlAttribute("minSegments")]
+    public int MinSegments { get; set; }
+
+    /// <summary>
+    /// Gets or sets the segment length.
+    /// </summary>
+    [DefaultValue(0)]
+    [XmlAttribute("segmentLength")]
+    public int SegmentLength { get; set; }
+
+    /// <summary>
+    /// Gets or sets a value indicating whether breaking the video stream on non-keyframes is supported.
+    /// </summary>
+    [DefaultValue(false)]
+    [XmlAttribute("breakOnNonKeyFrames")]
+    public bool BreakOnNonKeyFrames { get; set; }
+
+    /// <summary>
+    /// Gets or sets the profile conditions.
+    /// </summary>
+    public ProfileCondition[] Conditions { get; set; }
+
+    /// <summary>
+    /// Gets or sets a value indicating whether variable bitrate encoding is supported.
+    /// </summary>
+    [DefaultValue(true)]
+    [XmlAttribute("enableAudioVbrEncoding")]
+    public bool EnableAudioVbrEncoding { get; set; } = true;
 }

+ 145 - 0
MediaBrowser.Model/Extensions/ContainerHelper.cs

@@ -0,0 +1,145 @@
+using System;
+using System.Collections.Generic;
+using Jellyfin.Extensions;
+
+namespace MediaBrowser.Model.Extensions;
+
+/// <summary>
+/// Defines the <see cref="ContainerHelper"/> class.
+/// </summary>
+public static class ContainerHelper
+{
+    /// <summary>
+    /// Compares two containers, returning true if an item in <paramref name="inputContainer"/> exists
+    /// in <paramref name="profileContainers"/>.
+    /// </summary>
+    /// <param name="profileContainers">The comma-delimited string being searched.
+    /// If the parameter begins with the <c>-</c> character, the operation is reversed.</param>
+    /// <param name="inputContainer">The comma-delimited string being matched.</param>
+    /// <returns>The result of the operation.</returns>
+    public static bool ContainsContainer(string? profileContainers, string? inputContainer)
+    {
+        var isNegativeList = false;
+        if (profileContainers != null && profileContainers.StartsWith('-'))
+        {
+            isNegativeList = true;
+            profileContainers = profileContainers[1..];
+        }
+
+        return ContainsContainer(profileContainers, isNegativeList, inputContainer);
+    }
+
+    /// <summary>
+    /// Compares two containers, returning true if an item in <paramref name="inputContainer"/> exists
+    /// in <paramref name="profileContainers"/>.
+    /// </summary>
+    /// <param name="profileContainers">The comma-delimited string being searched.
+    /// If the parameter begins with the <c>-</c> character, the operation is reversed.</param>
+    /// <param name="inputContainer">The comma-delimited string being matched.</param>
+    /// <returns>The result of the operation.</returns>
+    public static bool ContainsContainer(string? profileContainers, ReadOnlySpan<char> inputContainer)
+    {
+        var isNegativeList = false;
+        if (profileContainers != null && profileContainers.StartsWith('-'))
+        {
+            isNegativeList = true;
+            profileContainers = profileContainers[1..];
+        }
+
+        return ContainsContainer(profileContainers, isNegativeList, inputContainer);
+    }
+
+    /// <summary>
+    /// Compares two containers, returning <paramref name="isNegativeList"/> if an item in <paramref name="inputContainer"/>
+    /// does not exist in <paramref name="profileContainers"/>.
+    /// </summary>
+    /// <param name="profileContainers">The comma-delimited string being searched.</param>
+    /// <param name="isNegativeList">The boolean result to return if a match is not found.</param>
+    /// <param name="inputContainer">The comma-delimited string being matched.</param>
+    /// <returns>The result of the operation.</returns>
+    public static bool ContainsContainer(string? profileContainers, bool isNegativeList, string? inputContainer)
+    {
+        if (string.IsNullOrEmpty(inputContainer))
+        {
+            return isNegativeList;
+        }
+
+        return ContainsContainer(profileContainers, isNegativeList, inputContainer.AsSpan());
+    }
+
+    /// <summary>
+    /// Compares two containers, returning <paramref name="isNegativeList"/> if an item in <paramref name="inputContainer"/>
+    /// does not exist in <paramref name="profileContainers"/>.
+    /// </summary>
+    /// <param name="profileContainers">The comma-delimited string being searched.</param>
+    /// <param name="isNegativeList">The boolean result to return if a match is not found.</param>
+    /// <param name="inputContainer">The comma-delimited string being matched.</param>
+    /// <returns>The result of the operation.</returns>
+    public static bool ContainsContainer(string? profileContainers, bool isNegativeList, ReadOnlySpan<char> inputContainer)
+    {
+        if (string.IsNullOrEmpty(profileContainers))
+        {
+            // Empty profiles always support all containers/codecs.
+            return true;
+        }
+
+        var allInputContainers = inputContainer.Split(',');
+        var allProfileContainers = profileContainers.SpanSplit(',');
+        foreach (var container in allInputContainers)
+        {
+            if (!container.IsEmpty)
+            {
+                foreach (var profile in allProfileContainers)
+                {
+                    if (container.Equals(profile, StringComparison.OrdinalIgnoreCase))
+                    {
+                        return !isNegativeList;
+                    }
+                }
+            }
+        }
+
+        return isNegativeList;
+    }
+
+    /// <summary>
+    /// Compares two containers, returning <paramref name="isNegativeList"/> if an item in <paramref name="inputContainer"/>
+    /// does not exist in <paramref name="profileContainers"/>.
+    /// </summary>
+    /// <param name="profileContainers">The profile containers being matched searched.</param>
+    /// <param name="isNegativeList">The boolean result to return if a match is not found.</param>
+    /// <param name="inputContainer">The comma-delimited string being matched.</param>
+    /// <returns>The result of the operation.</returns>
+    public static bool ContainsContainer(IReadOnlyList<string>? profileContainers, bool isNegativeList, string inputContainer)
+    {
+        if (profileContainers is null)
+        {
+            // Empty profiles always support all containers/codecs.
+            return true;
+        }
+
+        var allInputContainers = inputContainer.Split(',');
+        foreach (var container in allInputContainers)
+        {
+            foreach (var profile in profileContainers)
+            {
+                if (string.Equals(profile, container, StringComparison.OrdinalIgnoreCase))
+                {
+                    return !isNegativeList;
+                }
+            }
+        }
+
+        return isNegativeList;
+    }
+
+    /// <summary>
+    /// Splits and input string.
+    /// </summary>
+    /// <param name="input">The input string.</param>
+    /// <returns>The result of the operation.</returns>
+    public static string[] Split(string? input)
+    {
+        return input?.Split(',', StringSplitOptions.RemoveEmptyEntries) ?? [];
+    }
+}

+ 1 - 1
MediaBrowser.Providers/MediaInfo/AudioFileProber.cs

@@ -316,7 +316,7 @@ namespace MediaBrowser.Providers.MediaInfo
                     genres = genres.SelectMany(g => SplitWithCustomDelimiter(g, libraryOptions.CustomTagDelimiters, libraryOptions.DelimiterWhitelist)).ToArray();
                 }
 
-                audio.Genres = options.ReplaceAllMetadata || audio.Genres == null || audio.Genres.Length == 0
+                audio.Genres = options.ReplaceAllMetadata || audio.Genres is null || audio.Genres.Length == 0
                     ? genres
                     : audio.Genres;
             }

+ 54 - 0
tests/Jellyfin.Model.Tests/Dlna/ContainerHelperTests.cs

@@ -0,0 +1,54 @@
+using System;
+using MediaBrowser.Model.Dlna;
+using MediaBrowser.Model.Extensions;
+using Xunit;
+
+namespace Jellyfin.Model.Tests.Dlna;
+
+public class ContainerHelperTests
+{
+    private readonly ContainerProfile _emptyContainerProfile = new ContainerProfile();
+
+    [Theory]
+    [InlineData(null)]
+    [InlineData("")]
+    [InlineData("mp4")]
+    public void ContainsContainer_EmptyContainerProfile_ReturnsTrue(string? containers)
+    {
+        Assert.True(_emptyContainerProfile.ContainsContainer(containers));
+    }
+
+    [Theory]
+    [InlineData("mp3,mpeg", "mp3")]
+    [InlineData("mp3,mpeg,avi", "mp3,avi")]
+    [InlineData("-mp3,mpeg", "avi")]
+    [InlineData("-mp3,mpeg,avi", "mp4,jpg")]
+    public void ContainsContainer_InList_ReturnsTrue(string container, string? extension)
+    {
+        Assert.True(ContainerHelper.ContainsContainer(container, extension));
+    }
+
+    [Theory]
+    [InlineData("mp3,mpeg", "avi")]
+    [InlineData("mp3,mpeg,avi", "mp4,jpg")]
+    [InlineData("mp3,mpeg", null)]
+    [InlineData("mp3,mpeg", "")]
+    [InlineData("-mp3,mpeg", "mp3")]
+    [InlineData("-mp3,mpeg,avi", "mpeg,avi")]
+    [InlineData(",mp3,", ",avi,")] // Empty values should be discarded
+    [InlineData("-,mp3,", ",mp3,")] // Empty values should be discarded
+    public void ContainsContainer_NotInList_ReturnsFalse(string container, string? extension)
+    {
+        Assert.False(ContainerHelper.ContainsContainer(container, extension));
+    }
+
+    [Theory]
+    [InlineData("mp3,mpeg", "mp3")]
+    [InlineData("mp3,mpeg,avi", "mp3,avi")]
+    [InlineData("-mp3,mpeg", "avi")]
+    [InlineData("-mp3,mpeg,avi", "mp4,jpg")]
+    public void ContainsContainer_InList_ReturnsTrue_SpanVersion(string container, string? extension)
+    {
+        Assert.True(ContainerHelper.ContainsContainer(container, extension.AsSpan()));
+    }
+}

+ 0 - 19
tests/Jellyfin.Model.Tests/Dlna/ContainerProfileTests.cs

@@ -1,19 +0,0 @@
-using MediaBrowser.Model.Dlna;
-using Xunit;
-
-namespace Jellyfin.Model.Tests.Dlna
-{
-    public class ContainerProfileTests
-    {
-        private readonly ContainerProfile _emptyContainerProfile = new ContainerProfile();
-
-        [Theory]
-        [InlineData(null)]
-        [InlineData("")]
-        [InlineData("mp4")]
-        public void ContainsContainer_EmptyContainerProfile_True(string? containers)
-        {
-            Assert.True(_emptyContainerProfile.ContainsContainer(containers));
-        }
-    }
-}

+ 13 - 8
tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs

@@ -389,18 +389,23 @@ namespace Jellyfin.Model.Tests
             if (playMethod == PlayMethod.DirectPlay)
             {
                 // Check expected container
-                var containers = ContainerProfile.SplitValue(mediaSource.Container);
+                var containers = mediaSource.Container.Split(',');
+                Assert.Contains(uri.Extension, containers);
                 // TODO: Test transcode too
-                // Assert.Contains(uri.Extension, containers);
 
                 // Check expected video codec (1)
-                Assert.Contains(targetVideoStream?.Codec, streamInfo.TargetVideoCodec);
-                Assert.Single(streamInfo.TargetVideoCodec);
+                if (targetVideoStream?.Codec is not null)
+                {
+                    Assert.Contains(targetVideoStream?.Codec, streamInfo.TargetVideoCodec);
+                    Assert.Single(streamInfo.TargetVideoCodec);
+                }
 
-                // Check expected audio codecs (1)
-                Assert.Contains(targetAudioStream?.Codec, streamInfo.TargetAudioCodec);
-                Assert.Single(streamInfo.TargetAudioCodec);
-                // Assert.Single(val.AudioCodecs);
+                if (targetAudioStream?.Codec is not null)
+                {
+                    // Check expected audio codecs (1)
+                    Assert.Contains(targetAudioStream?.Codec, streamInfo.TargetAudioCodec);
+                    Assert.Single(streamInfo.TargetAudioCodec);
+                }
 
                 if (transcodeMode.Equals("DirectStream", StringComparison.Ordinal))
                 {