Browse Source

Convert TranscodeReason to Flags

Isaac Gordezky 3 years ago
parent
commit
d871dded9f

+ 3 - 2
Jellyfin.Api/Controllers/UniversalAudioController.cs

@@ -16,6 +16,7 @@ using MediaBrowser.Controller.MediaEncoding;
 using MediaBrowser.Controller.Net;
 using MediaBrowser.Model.Dlna;
 using MediaBrowser.Model.MediaInfo;
+using MediaBrowser.Model.Session;
 using Microsoft.AspNetCore.Authorization;
 using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Mvc;
@@ -223,7 +224,7 @@ namespace Jellyfin.Api.Controllers
                     DeInterlace = false,
                     RequireNonAnamorphic = false,
                     EnableMpegtsM2TsMode = false,
-                    TranscodeReasons = mediaSource.TranscodeReasons == null ? null : string.Join(',', mediaSource.TranscodeReasons.Select(i => i.ToString()).ToArray()),
+                    TranscodeReasons = mediaSource.TranscodeReasons == MediaBrowser.Model.Session.TranscodeReason.None ? null : mediaSource.TranscodeReasons.Serialize(),
                     Context = EncodingContext.Static,
                     StreamOptions = new Dictionary<string, string>(),
                     EnableAdaptiveBitrateStreaming = true
@@ -254,7 +255,7 @@ namespace Jellyfin.Api.Controllers
                 CopyTimestamps = true,
                 StartTimeTicks = startTimeTicks,
                 SubtitleMethod = SubtitleDeliveryMethod.Embed,
-                TranscodeReasons = mediaSource.TranscodeReasons == null ? null : string.Join(',', mediaSource.TranscodeReasons.Select(i => i.ToString()).ToArray()),
+                TranscodeReasons = mediaSource.TranscodeReasons == MediaBrowser.Model.Session.TranscodeReason.None ? null : mediaSource.TranscodeReasons.Serialize(),
                 Context = EncodingContext.Static
             };
 

+ 1 - 1
Jellyfin.Api/Helpers/TranscodingJobHelper.cs

@@ -479,7 +479,7 @@ namespace Jellyfin.Api.Helpers
                     IsAudioDirect = EncodingHelper.IsCopyCodec(state.OutputAudioCodec),
                     IsVideoDirect = EncodingHelper.IsCopyCodec(state.OutputVideoCodec),
                     HardwareAccelerationType = hardwareAccelerationType,
-                    TranscodeReasons = state.TranscodeReasons
+                    TranscodeReason = state.TranscodeReason
                 });
             }
         }

+ 13 - 10
MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs

@@ -6,6 +6,7 @@ using System;
 using System.Collections.Generic;
 using System.Globalization;
 using System.Linq;
+using System.Text.Json.Serialization;
 using Jellyfin.Data.Entities;
 using MediaBrowser.Model.Dlna;
 using MediaBrowser.Model.Drawing;
@@ -23,7 +24,7 @@ namespace MediaBrowser.Controller.MediaEncoding
         public int? OutputAudioBitrate;
         public int? OutputAudioChannels;
 
-        private TranscodeReason[] _transcodeReasons = null;
+        private TranscodeReason? _transcodeReasons = null;
 
         public EncodingJobInfo(TranscodingJobType jobType)
         {
@@ -34,25 +35,27 @@ namespace MediaBrowser.Controller.MediaEncoding
             SupportedSubtitleCodecs = Array.Empty<string>();
         }
 
-        public TranscodeReason[] TranscodeReasons
+        public TranscodeReason[] TranscodeReasons { get => TranscodeReason.ToArray(); }
+
+        [JsonIgnore]
+        public TranscodeReason TranscodeReason
         {
             get
             {
-                if (_transcodeReasons == null)
+                if (!_transcodeReasons.HasValue)
                 {
                     if (BaseRequest.TranscodeReasons == null)
                     {
-                        return Array.Empty<TranscodeReason>();
+                        _transcodeReasons = TranscodeReason.None;
+                        return TranscodeReason.None;
                     }
 
-                    _transcodeReasons = BaseRequest.TranscodeReasons
-                        .Split(',')
-                        .Where(i => !string.IsNullOrEmpty(i))
-                        .Select(v => (TranscodeReason)Enum.Parse(typeof(TranscodeReason), v, true))
-                        .ToArray();
+                    TranscodeReason reason = TranscodeReason.None;
+                    Enum.TryParse<TranscodeReason>(BaseRequest.TranscodeReasons, out reason);
+                    _transcodeReasons = reason;
                 }
 
-                return _transcodeReasons;
+                return _transcodeReasons.Value;
             }
         }
 

+ 49 - 71
MediaBrowser.Model/Dlna/StreamBuilder.cs

@@ -143,7 +143,7 @@ namespace MediaBrowser.Model.Dlna
             }).ThenBy(streams.IndexOf);
         }
 
-        private static TranscodeReason? GetTranscodeReasonForFailedCondition(ProfileCondition condition)
+        private static TranscodeReason GetTranscodeReasonForFailedCondition(ProfileCondition condition)
         {
             switch (condition.Property)
             {
@@ -161,7 +161,7 @@ namespace MediaBrowser.Model.Dlna
 
                 case ProfileConditionValue.Has64BitOffsets:
                     // TODO
-                    return null;
+                    return TranscodeReason.None;
 
                 case ProfileConditionValue.Height:
                     return TranscodeReason.VideoResolutionNotSupported;
@@ -171,7 +171,7 @@ namespace MediaBrowser.Model.Dlna
 
                 case ProfileConditionValue.IsAvc:
                     // TODO
-                    return null;
+                    return TranscodeReason.None;
 
                 case ProfileConditionValue.IsInterlaced:
                     return TranscodeReason.InterlacedVideoNotSupported;
@@ -181,15 +181,15 @@ namespace MediaBrowser.Model.Dlna
 
                 case ProfileConditionValue.NumAudioStreams:
                     // TODO
-                    return null;
+                    return TranscodeReason.None;
 
                 case ProfileConditionValue.NumVideoStreams:
                     // TODO
-                    return null;
+                    return TranscodeReason.None;
 
                 case ProfileConditionValue.PacketLength:
                     // TODO
-                    return null;
+                    return TranscodeReason.None;
 
                 case ProfileConditionValue.RefFrames:
                     return TranscodeReason.RefFramesNotSupported;
@@ -217,13 +217,13 @@ namespace MediaBrowser.Model.Dlna
 
                 case ProfileConditionValue.VideoTimestamp:
                     // TODO
-                    return null;
+                    return TranscodeReason.None;
 
                 case ProfileConditionValue.Width:
                     return TranscodeReason.VideoResolutionNotSupported;
 
                 default:
-                    return null;
+                    return TranscodeReason.None;
             }
         }
 
@@ -290,7 +290,7 @@ namespace MediaBrowser.Model.Dlna
             var directPlayInfo = GetAudioDirectPlayMethods(item, audioStream, options);
 
             var directPlayMethods = directPlayInfo.PlayMethods;
-            var transcodeReasons = directPlayInfo.TranscodeReasons.ToList();
+            var transcodeReasons = directPlayInfo.TranscodeReasons;
 
             int? inputAudioChannels = audioStream?.Channels;
             int? inputAudioBitrate = audioStream?.BitDepth;
@@ -331,11 +331,7 @@ namespace MediaBrowser.Model.Dlna
                     if (!ConditionProcessor.IsAudioConditionSatisfied(c, inputAudioChannels, inputAudioBitrate, inputAudioSampleRate, inputAudioBitDepth))
                     {
                         LogConditionFailure(options.Profile, "AudioCodecProfile", c, item);
-                        var transcodeReason = GetTranscodeReasonForFailedCondition(c);
-                        if (transcodeReason.HasValue)
-                        {
-                            transcodeReasons.Add(transcodeReason.Value);
-                        }
+                        transcodeReasons |= GetTranscodeReasonForFailedCondition(c);
 
                         all = false;
                         break;
@@ -434,7 +430,7 @@ namespace MediaBrowser.Model.Dlna
                 playlistItem.AudioBitrate = longBitrate > int.MaxValue ? int.MaxValue : Convert.ToInt32(longBitrate);
             }
 
-            playlistItem.TranscodeReasons = transcodeReasons.ToArray();
+            playlistItem.TranscodeReasons = transcodeReasons;
             return playlistItem;
         }
 
@@ -448,7 +444,7 @@ namespace MediaBrowser.Model.Dlna
             return options.GetMaxBitrate(isAudio);
         }
 
-        private (IEnumerable<PlayMethod> PlayMethods, IEnumerable<TranscodeReason> TranscodeReasons) GetAudioDirectPlayMethods(MediaSourceInfo item, MediaStream audioStream, AudioOptions options)
+        private (IEnumerable<PlayMethod> PlayMethods, TranscodeReason TranscodeReasons) GetAudioDirectPlayMethods(MediaSourceInfo item, MediaStream audioStream, AudioOptions options)
         {
             DirectPlayProfile directPlayProfile = options.Profile.DirectPlayProfiles
                 .FirstOrDefault(x => x.Type == DlnaProfileType.Audio && IsAudioDirectPlaySupported(x, item, audioStream));
@@ -465,12 +461,12 @@ namespace MediaBrowser.Model.Dlna
             }
 
             var playMethods = new List<PlayMethod>();
-            var transcodeReasons = new List<TranscodeReason>();
+            var transcodeReasons = TranscodeReason.None;
 
             // While options takes the network and other factors into account. Only applies to direct stream
             if (item.SupportsDirectStream)
             {
-                if (IsAudioEligibleForDirectPlay(item, options.GetMaxBitrate(true) ?? 0, PlayMethod.DirectStream))
+                if (IsItemBitrateEligibleForDirectPlay(item, options.GetMaxBitrate(true) ?? 0, PlayMethod.DirectStream))
                 {
                     if (options.EnableDirectStream)
                     {
@@ -479,7 +475,7 @@ namespace MediaBrowser.Model.Dlna
                 }
                 else
                 {
-                    transcodeReasons.Add(TranscodeReason.ContainerBitrateExceedsLimit);
+                    transcodeReasons |= TranscodeReason.ContainerBitrateExceedsLimit;
                 }
             }
 
@@ -487,7 +483,7 @@ namespace MediaBrowser.Model.Dlna
             // If device requirements are satisfied then allow both direct stream and direct play
             if (item.SupportsDirectPlay)
             {
-                if (IsAudioEligibleForDirectPlay(item, GetBitrateForDirectPlayCheck(item, options, true) ?? 0, PlayMethod.DirectPlay))
+                if (IsItemBitrateEligibleForDirectPlay(item, GetBitrateForDirectPlayCheck(item, options, true) ?? 0, PlayMethod.DirectPlay))
                 {
                     if (options.EnableDirectPlay)
                     {
@@ -496,29 +492,26 @@ namespace MediaBrowser.Model.Dlna
                 }
                 else
                 {
-                    transcodeReasons.Add(TranscodeReason.ContainerBitrateExceedsLimit);
+                    transcodeReasons |= TranscodeReason.ContainerBitrateExceedsLimit;
                 }
             }
 
             if (playMethods.Count > 0)
             {
-                transcodeReasons.Clear();
-            }
-            else
-            {
-                transcodeReasons = transcodeReasons.Distinct().ToList();
+                transcodeReasons = TranscodeReason.None;
             }
 
             return (playMethods, transcodeReasons);
         }
 
-        private static List<TranscodeReason> GetTranscodeReasonsFromDirectPlayProfile(MediaSourceInfo item, MediaStream videoStream, MediaStream audioStream, IEnumerable<DirectPlayProfile> directPlayProfiles)
+        private static TranscodeReason GetTranscodeReasonsFromDirectPlayProfile(MediaSourceInfo item, MediaStream videoStream, MediaStream audioStream, IEnumerable<DirectPlayProfile> directPlayProfiles)
         {
             var mediaType = videoStream == null ? DlnaProfileType.Audio : DlnaProfileType.Video;
 
             var containerSupported = false;
             var audioSupported = false;
             var videoSupported = false;
+            var reasons = TranscodeReason.None;
 
             foreach (var profile in directPlayProfiles)
             {
@@ -541,20 +534,20 @@ namespace MediaBrowser.Model.Dlna
             var list = new List<TranscodeReason>();
             if (!containerSupported)
             {
-                list.Add(TranscodeReason.ContainerNotSupported);
+                reasons |= TranscodeReason.ContainerNotSupported;
             }
 
             if (videoStream != null && !videoSupported)
             {
-                list.Add(TranscodeReason.VideoCodecNotSupported);
+                reasons |= TranscodeReason.VideoCodecNotSupported;
             }
 
             if (audioStream != null && !audioSupported)
             {
-                list.Add(TranscodeReason.AudioCodecNotSupported);
+                reasons |= TranscodeReason.AudioCodecNotSupported;
             }
 
-            return list;
+            return reasons;
         }
 
         private static int? GetDefaultSubtitleStreamIndex(MediaSourceInfo item, SubtitleProfile[] subtitleProfiles)
@@ -679,8 +672,9 @@ namespace MediaBrowser.Model.Dlna
             // TODO: This doesn't account for situations where the device is able to handle the media's bitrate, but the connection isn't fast enough
             var directPlayEligibilityResult = IsEligibleForDirectPlay(item, GetBitrateForDirectPlayCheck(item, options, true) ?? 0, subtitleStream, audioStream, options, PlayMethod.DirectPlay);
             var directStreamEligibilityResult = IsEligibleForDirectPlay(item, options.GetMaxBitrate(false) ?? 0, subtitleStream, audioStream, options, PlayMethod.DirectStream);
-            bool isEligibleForDirectPlay = options.EnableDirectPlay && (options.ForceDirectPlay || directPlayEligibilityResult.DirectPlay);
-            bool isEligibleForDirectStream = options.EnableDirectStream && (options.ForceDirectStream || directStreamEligibilityResult.DirectPlay);
+            bool isEligibleForDirectPlay = options.EnableDirectPlay && (options.ForceDirectPlay || directPlayEligibilityResult == TranscodeReason.None);
+            bool isEligibleForDirectStream = options.EnableDirectStream && (options.ForceDirectStream || directPlayEligibilityResult == TranscodeReason.None);
+            var transcodeReasons = directPlayEligibilityResult | directStreamEligibilityResult;
 
             _logger.LogDebug(
                 "Profile: {0}, Path: {1}, isEligibleForDirectPlay: {2}, isEligibleForDirectStream: {3}",
@@ -689,8 +683,6 @@ namespace MediaBrowser.Model.Dlna
                 isEligibleForDirectPlay,
                 isEligibleForDirectStream);
 
-            var transcodeReasons = new List<TranscodeReason>();
-
             if (isEligibleForDirectPlay || isEligibleForDirectStream)
             {
                 // See if it can be direct played
@@ -713,17 +705,13 @@ namespace MediaBrowser.Model.Dlna
                     return playlistItem;
                 }
 
-                transcodeReasons.AddRange(directPlayInfo.TranscodeReasons);
-            }
-
-            if (directPlayEligibilityResult.Reason.HasValue)
-            {
-                transcodeReasons.Add(directPlayEligibilityResult.Reason.Value);
+                transcodeReasons |= directPlayInfo.TranscodeReasons;
             }
 
-            if (directStreamEligibilityResult.Reason.HasValue)
+            if (playlistItem.PlayMethod != PlayMethod.Transcode)
             {
-                transcodeReasons.Add(directStreamEligibilityResult.Reason.Value);
+                playlistItem.TranscodeReasons = transcodeReasons;
+                return playlistItem;
             }
 
             // Can't direct play, find the transcoding profile
@@ -869,7 +857,7 @@ namespace MediaBrowser.Model.Dlna
                 }
             }
 
-            playlistItem.TranscodeReasons = transcodeReasons.ToArray();
+            playlistItem.TranscodeReasons = transcodeReasons;
 
             return playlistItem;
         }
@@ -1000,7 +988,7 @@ namespace MediaBrowser.Model.Dlna
             return 7168000;
         }
 
-        private (PlayMethod? PlayMethod, List<TranscodeReason> TranscodeReasons) GetVideoDirectPlayProfile(
+        private (PlayMethod? PlayMethod, TranscodeReason TranscodeReasons) GetVideoDirectPlayProfile(
             VideoOptions options,
             MediaSourceInfo mediaSource,
             MediaStream videoStream,
@@ -1009,12 +997,12 @@ namespace MediaBrowser.Model.Dlna
         {
             if (options.ForceDirectPlay)
             {
-                return (PlayMethod.DirectPlay, new List<TranscodeReason>());
+                return (PlayMethod.DirectPlay, TranscodeReason.None);
             }
 
             if (options.ForceDirectStream)
             {
-                return (PlayMethod.DirectStream, new List<TranscodeReason>());
+                return (PlayMethod.DirectStream, TranscodeReason.None);
             }
 
             DeviceProfile profile = options.Profile;
@@ -1089,11 +1077,7 @@ namespace MediaBrowser.Model.Dlna
                 {
                     LogConditionFailure(profile, "VideoContainerProfile", i, mediaSource);
 
-                    var transcodeReason = GetTranscodeReasonForFailedCondition(i);
-                    var transcodeReasons = transcodeReason.HasValue
-                        ? new List<TranscodeReason> { transcodeReason.Value }
-                        : new List<TranscodeReason>();
-
+                    var transcodeReasons = GetTranscodeReasonForFailedCondition(i);
                     return (null, transcodeReasons);
                 }
             }
@@ -1133,11 +1117,7 @@ namespace MediaBrowser.Model.Dlna
                     LogConditionFailure(profile, "VideoCodecProfile", i, mediaSource);
 
                     var transcodeReason = GetTranscodeReasonForFailedCondition(i);
-                    var transcodeReasons = transcodeReason.HasValue
-                        ? new List<TranscodeReason> { transcodeReason.Value }
-                        : new List<TranscodeReason>();
-
-                    return (null, transcodeReasons);
+                    return (null, transcodeReason);
                 }
             }
 
@@ -1178,11 +1158,7 @@ namespace MediaBrowser.Model.Dlna
                     {
                         LogConditionFailure(profile, "VideoAudioCodecProfile", i, mediaSource);
 
-                        var transcodeReason = GetTranscodeReasonForFailedCondition(i);
-                        var transcodeReasons = transcodeReason.HasValue
-                            ? new List<TranscodeReason> { transcodeReason.Value }
-                            : new List<TranscodeReason>();
-
+                        var transcodeReasons = GetTranscodeReasonForFailedCondition(i);
                         return (null, transcodeReasons);
                     }
                 }
@@ -1190,10 +1166,10 @@ namespace MediaBrowser.Model.Dlna
 
             if (isEligibleForDirectStream && mediaSource.SupportsDirectStream)
             {
-                return (PlayMethod.DirectStream, new List<TranscodeReason>());
+                return (PlayMethod.DirectStream, TranscodeReason.None);
             }
 
-            return (null, new List<TranscodeReason> { TranscodeReason.ContainerBitrateExceedsLimit });
+            return (null, TranscodeReason.ContainerBitrateExceedsLimit);
         }
 
         private void LogConditionFailure(DeviceProfile profile, string type, ProfileCondition condition, MediaSourceInfo mediaSource)
@@ -1209,7 +1185,7 @@ namespace MediaBrowser.Model.Dlna
                 mediaSource.Path ?? "Unknown path");
         }
 
-        private (bool DirectPlay, TranscodeReason? Reason) IsEligibleForDirectPlay(
+        private TranscodeReason IsEligibleForDirectPlay(
             MediaSourceInfo item,
             long maxBitrate,
             MediaStream subtitleStream,
@@ -1217,6 +1193,7 @@ namespace MediaBrowser.Model.Dlna
             VideoOptions options,
             PlayMethod playMethod)
         {
+            var reason = TranscodeReason.None;
             if (subtitleStream != null)
             {
                 var subtitleProfile = GetSubtitleProfile(item, subtitleStream, options.Profile.SubtitleProfiles, playMethod, _transcoderSupport, item.Container, null);
@@ -1226,22 +1203,23 @@ namespace MediaBrowser.Model.Dlna
                     && subtitleProfile.Method != SubtitleDeliveryMethod.Embed)
                 {
                     _logger.LogDebug("Not eligible for {0} due to unsupported subtitles", playMethod);
-                    return (false, TranscodeReason.SubtitleCodecNotSupported);
+                    reason |= TranscodeReason.SubtitleCodecNotSupported;
                 }
             }
 
-            bool result = IsAudioEligibleForDirectPlay(item, maxBitrate, playMethod);
+            bool result = IsItemBitrateEligibleForDirectPlay(item, maxBitrate, playMethod);
             if (!result)
             {
-                return (false, TranscodeReason.ContainerBitrateExceedsLimit);
+                reason |= TranscodeReason.ContainerBitrateExceedsLimit;
             }
 
+            // TODO:6450 support external audio in DirectStream?
             if (audioStream?.IsExternal == true)
             {
-                return (false, TranscodeReason.AudioIsExternal);
+                reason |= TranscodeReason.AudioIsExternal;
             }
 
-            return (true, null);
+            return reason;
         }
 
         public static SubtitleProfile GetSubtitleProfile(
@@ -1401,7 +1379,7 @@ namespace MediaBrowser.Model.Dlna
             return null;
         }
 
-        private bool IsAudioEligibleForDirectPlay(MediaSourceInfo item, long maxBitrate, PlayMethod playMethod)
+        private bool IsItemBitrateEligibleForDirectPlay(MediaSourceInfo item, long maxBitrate, PlayMethod playMethod)
         {
             // Don't restrict by bitrate if coming from an external domain
             if (item.IsRemote)

+ 2 - 3
MediaBrowser.Model/Dlna/StreamInfo.cs

@@ -23,7 +23,6 @@ namespace MediaBrowser.Model.Dlna
             AudioCodecs = Array.Empty<string>();
             VideoCodecs = Array.Empty<string>();
             SubtitleCodecs = Array.Empty<string>();
-            TranscodeReasons = Array.Empty<TranscodeReason>();
             StreamOptions = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
         }
 
@@ -103,7 +102,7 @@ namespace MediaBrowser.Model.Dlna
 
         public string PlaySessionId { get; set; }
 
-        public TranscodeReason[] TranscodeReasons { get; set; }
+        public TranscodeReason TranscodeReasons { get; set; }
 
         public Dictionary<string, string> StreamOptions { get; private set; }
 
@@ -799,7 +798,7 @@ namespace MediaBrowser.Model.Dlna
 
             if (!item.IsDirectStream)
             {
-                list.Add(new NameValuePair("TranscodeReasons", string.Join(',', item.TranscodeReasons.Distinct())));
+                list.Add(new NameValuePair("TranscodeReasons", item.TranscodeReasons.Serialize()));
             }
 
             return list;

+ 1 - 1
MediaBrowser.Model/Dto/MediaSourceInfo.cs

@@ -109,7 +109,7 @@ namespace MediaBrowser.Model.Dto
         public int? AnalyzeDurationMs { get; set; }
 
         [JsonIgnore]
-        public TranscodeReason[] TranscodeReasons { get; set; }
+        public TranscodeReason TranscodeReasons { get; set; }
 
         public int? DefaultAudioStreamIndex { get; set; }
 

+ 43 - 24
MediaBrowser.Model/Session/TranscodeReason.cs

@@ -1,32 +1,51 @@
 #pragma warning disable CS1591
 
+using System;
+
 namespace MediaBrowser.Model.Session
 {
+    [Flags]
     public enum TranscodeReason
     {
-        ContainerNotSupported = 0,
-        VideoCodecNotSupported = 1,
-        AudioCodecNotSupported = 2,
-        ContainerBitrateExceedsLimit = 3,
-        AudioBitrateNotSupported = 4,
-        AudioChannelsNotSupported = 5,
-        VideoResolutionNotSupported = 6,
-        UnknownVideoStreamInfo = 7,
-        UnknownAudioStreamInfo = 8,
-        AudioProfileNotSupported = 9,
-        AudioSampleRateNotSupported = 10,
-        AnamorphicVideoNotSupported = 11,
-        InterlacedVideoNotSupported = 12,
-        SecondaryAudioNotSupported = 13,
-        RefFramesNotSupported = 14,
-        VideoBitDepthNotSupported = 15,
-        VideoBitrateNotSupported = 16,
-        VideoFramerateNotSupported = 17,
-        VideoLevelNotSupported = 18,
-        VideoProfileNotSupported = 19,
-        AudioBitDepthNotSupported = 20,
-        SubtitleCodecNotSupported = 21,
-        DirectPlayError = 22,
-        AudioIsExternal = 23
+        None = 0,
+
+        // Primary
+        ContainerNotSupported = 1 << 0,
+        VideoCodecNotSupported = 1 << 1,
+        AudioCodecNotSupported = 1 << 2,
+        SubtitleCodecNotSupported = 1 << 3,
+        AudioIsExternal = 1 << 4,
+        SecondaryAudioNotSupported = 1 << 5,
+
+        // Video Constraints
+        VideoProfileNotSupported = 1 << 6,
+        VideoLevelNotSupported = 1 << 7,
+        VideoResolutionNotSupported = 1 << 8,
+        VideoBitDepthNotSupported = 1 << 9,
+        VideoFramerateNotSupported = 1 << 10,
+        RefFramesNotSupported = 1 << 11,
+        AnamorphicVideoNotSupported = 1 << 12,
+        InterlacedVideoNotSupported = 1 << 13,
+
+        // Audio Constraints
+        AudioChannelsNotSupported = 1 << 14,
+        AudioProfileNotSupported = 1 << 15,
+        AudioSampleRateNotSupported = 1 << 16,
+        AudioBitDepthNotSupported = 1 << 20,
+
+        // Bitrate Constraints
+        ContainerBitrateExceedsLimit = 1 << 17,
+        VideoBitrateNotSupported = 1 << 18,
+        AudioBitrateNotSupported = 1 << 19,
+
+        // Errors
+        UnknownVideoStreamInfo = 1 << 20,
+        UnknownAudioStreamInfo = 1 << 21,
+        DirectPlayError = 1 << 22,
+
+        // Aliases
+        ContainerReasons = ContainerNotSupported | ContainerBitrateExceedsLimit,
+        AudioReasons = AudioCodecNotSupported | AudioBitrateNotSupported | AudioChannelsNotSupported | AudioProfileNotSupported | AudioSampleRateNotSupported | SecondaryAudioNotSupported | AudioBitDepthNotSupported | AudioIsExternal,
+        VideoReasons = VideoCodecNotSupported | VideoResolutionNotSupported | AnamorphicVideoNotSupported | InterlacedVideoNotSupported | VideoBitDepthNotSupported | VideoBitrateNotSupported | VideoFramerateNotSupported | VideoLevelNotSupported | RefFramesNotSupported,
     }
 }

+ 22 - 0
MediaBrowser.Model/Session/TranscodeReasonExtensions.cs

@@ -0,0 +1,22 @@
+#pragma warning disable CS1591
+
+using System;
+using System.Linq;
+
+namespace MediaBrowser.Model.Session
+{
+    public static class TranscodeReasonExtensions
+    {
+        private static TranscodeReason[] values = Enum.GetValues<TranscodeReason>();
+
+        public static string Serialize(this MediaBrowser.Model.Session.TranscodeReason reasons, string sep = ",")
+        {
+            return string.Join(sep, reasons.ToArray());
+        }
+
+        public static TranscodeReason[] ToArray(this MediaBrowser.Model.Session.TranscodeReason reasons)
+        {
+            return values.Where(r => r != 0 && reasons.HasFlag(r)).ToArray();
+        }
+    }
+}

+ 5 - 7
MediaBrowser.Model/Session/TranscodingInfo.cs

@@ -1,17 +1,12 @@
 #nullable disable
 #pragma warning disable CS1591
 
-using System;
+using System.Text.Json.Serialization;
 
 namespace MediaBrowser.Model.Session
 {
     public class TranscodingInfo
     {
-        public TranscodingInfo()
-        {
-            TranscodeReasons = Array.Empty<TranscodeReason>();
-        }
-
         public string AudioCodec { get; set; }
 
         public string VideoCodec { get; set; }
@@ -36,6 +31,9 @@ namespace MediaBrowser.Model.Session
 
         public HardwareEncodingType? HardwareAccelerationType { get; set; }
 
-        public TranscodeReason[] TranscodeReasons { get; set; }
+        public TranscodeReason[] TranscodeReasons { get => TranscodeReason.ToArray(); }
+
+        [JsonIgnore]
+        public TranscodeReason TranscodeReason { get; set; }
     }
 }

+ 273 - 90
tests/Jellyfin.Dlna.Tests/StreamBuilderTests.cs

@@ -1,4 +1,5 @@
 using System;
+using System.Collections.Specialized;
 using System.IO;
 using System.Linq;
 using System.Text.Json;
@@ -19,65 +20,69 @@ namespace Jellyfin.MediaBrowser.Model.Tests
         [Theory]
         // Chrome
         [InlineData("Chrome", "mp4-h264-aac-vtt-2600k", PlayMethod.DirectStream)] // #6450 should be DirectPlay
-        [InlineData("Chrome", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.Transcode)] // #6450 should be DirectPlay
-        [InlineData("Chrome", "mp4-h264-ac3-srt-2600k", PlayMethod.Transcode)] // #6450 should be DirectStream
-        [InlineData("Chrome", "mp4-hevc-aac-srt-15200k", PlayMethod.Transcode, true)]
-        [InlineData("Chrome", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, true)]
-        [InlineData("Chrome", "mkv-vp9-aac-srt-2600k", PlayMethod.Transcode, true)] // #6450 should be 'false'
-        [InlineData("Chrome", "mkv-vp9-ac3-srt-2600k", PlayMethod.Transcode, true)] // #6450 should be 'false'
+        [InlineData("Chrome", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported)] // #6450 should be DirectPlay
+        [InlineData("Chrome", "mp4-h264-ac3-aacDef-srt-2600k", PlayMethod.Transcode, TranscodeReason.SecondaryAudioNotSupported)] // #6450 should be DirectPlay
+        [InlineData("Chrome", "mp4-h264-ac3-srt-2600k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported)] // #6450 should be DirectStream
+        [InlineData("Chrome", "mp4-hevc-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported, "Transcode")]
+        [InlineData("Chrome", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported | TranscodeReason.AudioCodecNotSupported, "Transcode")]
+        [InlineData("Chrome", "mkv-vp9-aac-srt-2600k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported, "Transcode")] // #6450 should be 'false'
+        [InlineData("Chrome", "mkv-vp9-ac3-srt-2600k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported, "Transcode")] // #6450 should be 'false'
         [InlineData("Chrome", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.DirectStream)] // #6450 should be DirectPlay
         // Firefox
         [InlineData("Firefox", "mp4-h264-aac-vtt-2600k", PlayMethod.DirectStream)] // #6450 should be DirectPlay
-        [InlineData("Firefox", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.Transcode)] // #6450 should be DirectPlay
-        [InlineData("Firefox", "mp4-h264-ac3-srt-2600k", PlayMethod.Transcode)] // #6450 should be DirectStream
-        [InlineData("Firefox", "mp4-hevc-aac-srt-15200k", PlayMethod.Transcode, true)]
-        [InlineData("Firefox", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, true)]
-        [InlineData("Firefox", "mkv-vp9-aac-srt-2600k", PlayMethod.Transcode, true)] // #6450 should be 'false'
-        [InlineData("Firefox", "mkv-vp9-ac3-srt-2600k", PlayMethod.Transcode, true)] // #6450 should be 'false'
+        [InlineData("Firefox", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported)] // #6450 should be DirectPlay
+        [InlineData("Firefox", "mp4-h264-ac3-aacDef-srt-2600k", PlayMethod.Transcode, TranscodeReason.SecondaryAudioNotSupported)] // #6450 should be DirectPlay
+        [InlineData("Firefox", "mp4-h264-ac3-srt-2600k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported)] // #6450 should be DirectStream
+        [InlineData("Firefox", "mp4-hevc-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported, "Transcode")]
+        [InlineData("Firefox", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported | TranscodeReason.AudioCodecNotSupported, "Transcode")]
+        [InlineData("Firefox", "mkv-vp9-aac-srt-2600k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported, "Transcode")] // #6450 should be 'false'
+        [InlineData("Firefox", "mkv-vp9-ac3-srt-2600k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported, "Transcode")] // #6450 should be 'false'
         [InlineData("Firefox", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.DirectStream)] // #6450 should be DirectPlay
         // Safari
         [InlineData("SafariNext", "mp4-h264-aac-vtt-2600k", PlayMethod.DirectStream)] // #6450 should be DirectPlay
-        [InlineData("SafariNext", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectStream)] // #6450 should be DirectPlay
+        [InlineData("SafariNext", "mp4-h264-ac3-aacDef-srt-2600k", PlayMethod.DirectStream)] // #6450 should be DirectPlay
         [InlineData("SafariNext", "mp4-h264-ac3-srt-2600k", PlayMethod.DirectStream)] // #6450 should be DirectPlay
         [InlineData("SafariNext", "mp4-hevc-aac-srt-15200k", PlayMethod.DirectStream)] // #6450 should probably be DirectPlay
         [InlineData("SafariNext", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.DirectStream)] // #6450 should probably be DirectPlay
         // AndroidPixel
         [InlineData("AndroidPixel", "mp4-h264-aac-srt-2600k", PlayMethod.DirectStream)] // #6450 should be DirectPlay
-        [InlineData("AndroidPixel", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectStream)] // #6450 should be DirectPlay
+        [InlineData("AndroidPixel", "mp4-h264-ac3-aacDef-srt-2600k", PlayMethod.DirectStream)] // #6450 should be DirectPlay
         [InlineData("AndroidPixel", "mp4-h264-ac3-srt-2600k", PlayMethod.DirectStream)] // #6450 should be DirectPlay
-        [InlineData("AndroidPixel", "mp4-hevc-aac-srt-15200k", PlayMethod.Transcode, true)]
-        [InlineData("AndroidPixel", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, true)]
+        [InlineData("AndroidPixel", "mp4-hevc-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.ContainerBitrateExceedsLimit, "Transcode")]
+        [InlineData("AndroidPixel", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.ContainerBitrateExceedsLimit, "Transcode")]
         // Yatse
         [InlineData("Yatse", "mp4-h264-aac-srt-2600k", PlayMethod.DirectStream)] // #6450 should be DirectPlay
-        [InlineData("Yatse", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.Transcode)] // #6450 should be DirectPlay
-        [InlineData("Yatse", "mp4-h264-ac3-srt-2600k", PlayMethod.Transcode, true)]
+        [InlineData("Yatse", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported)] // #6450 should be DirectPlay
+        [InlineData("Yatse", "mp4-h264-ac3-aacDef-srt-2600k", PlayMethod.Transcode, TranscodeReason.SecondaryAudioNotSupported)] // #6450 should be DirectPlay
+        [InlineData("Yatse", "mp4-h264-ac3-srt-2600k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported, "Transcode")]
         [InlineData("Yatse", "mp4-hevc-aac-srt-15200k", PlayMethod.DirectStream)] // #6450 should be DirectPlay
-        [InlineData("Yatse", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, true)] // #6450 should be DirectPlay
+        [InlineData("Yatse", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported, "Transcode")] // #6450 should be DirectPlay
         // RokuSSPlus
         [InlineData("RokuSSPlus", "mp4-h264-aac-srt-2600k", PlayMethod.DirectStream)] // #6450 should be DirectPlay
-        [InlineData("RokuSSPlus", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.Transcode)] // #6450 should be DirectPlay
-        [InlineData("RokuSSPlus", "mp4-h264-ac3-srt-2600k", PlayMethod.Transcode)] // #6450 should be DirectStream
+        [InlineData("RokuSSPlus", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported)] // #6450 should be DirectPlay
+        [InlineData("RokuSSPlus", "mp4-h264-ac3-aacDef-srt-2600k", PlayMethod.DirectStream)] // #6450 should be DirectPlay
+        [InlineData("RokuSSPlus", "mp4-h264-ac3-srt-2600k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported)] // #6450 should be DirectStream
         [InlineData("RokuSSPlus", "mp4-hevc-aac-srt-15200k", PlayMethod.DirectStream)] // #6450 should be DirectPlay
-        [InlineData("RokuSSPlus", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, true)] // #6450 should be DirectPlay
-        [InlineData("RokuSSPlus", "mp4-hevc-ac3-srt-15200k", PlayMethod.Transcode, true)] // #6450 should be DirectStream
+        [InlineData("RokuSSPlus", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported, "Transcode")] // #6450 should be DirectPlay
+        [InlineData("RokuSSPlus", "mp4-hevc-ac3-srt-15200k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported, "Transcode")] // #6450 should be DirectStream
         // JellyfinMediaPlayer
         [InlineData("JellyfinMediaPlayer", "mp4-h264-aac-vtt-2600k", PlayMethod.DirectStream)] // #6450 should be DirectPlay
         [InlineData("JellyfinMediaPlayer", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectStream)] // #6450 should be DirectPlay
         [InlineData("JellyfinMediaPlayer", "mp4-h264-ac3-srt-2600k", PlayMethod.DirectStream)] // #6450 should be DirectPlay
-        [InlineData("JellyfinMediaPlayer", "mp4-hevc-aac-srt-15200k", PlayMethod.Transcode)] // #6450 should be DirectPlay
-        [InlineData("JellyfinMediaPlayer", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode)] // #6450 should be DirectPlay
+        [InlineData("JellyfinMediaPlayer", "mp4-hevc-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.ContainerBitrateExceedsLimit)] // #6450 should be DirectPlay
+        [InlineData("JellyfinMediaPlayer", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.ContainerBitrateExceedsLimit)] // #6450 should be DirectPlay
         [InlineData("JellyfinMediaPlayer", "mkv-vp9-aac-srt-2600k", PlayMethod.DirectStream)] // #6450 should be DirectPlay
         [InlineData("JellyfinMediaPlayer", "mkv-vp9-ac3-srt-2600k", PlayMethod.DirectStream)] // #6450 should be DirectPlay
         [InlineData("JellyfinMediaPlayer", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.DirectStream)] // #6450 should be DirectPlay
         // TranscodeMedia
-        [InlineData("TranscodeMedia", "mp4-h264-aac-vtt-2600k", PlayMethod.Transcode)] // #6450 should be DirectPlay
-        [InlineData("TranscodeMedia", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.Transcode)] // #6450 should be DirectPlay
-        [InlineData("TranscodeMedia", "mp4-h264-ac3-srt-2600k", PlayMethod.Transcode)] // #6450 should be DirectPlay
-        [InlineData("TranscodeMedia", "mp4-hevc-aac-srt-15200k", PlayMethod.Transcode, true)] // #6450 should be DirectPlay
-        [InlineData("TranscodeMedia", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, true)] // #6450 should be DirectPlay
-        [InlineData("TranscodeMedia", "mkv-vp9-aac-srt-2600k", PlayMethod.Transcode, true)] // #6450 should be DirectPlay
-        [InlineData("TranscodeMedia", "mkv-vp9-ac3-srt-2600k", PlayMethod.Transcode, true)] // #6450 should be DirectPlay
-        [InlineData("TranscodeMedia", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.Transcode, true)] // #6450 should be DirectPlay
+        [InlineData("TranscodeMedia", "mp4-h264-aac-vtt-2600k", PlayMethod.Transcode, TranscodeReason.ContainerNotSupported | TranscodeReason.VideoCodecNotSupported | TranscodeReason.AudioCodecNotSupported)] // #6450 should be DirectPlay
+        [InlineData("TranscodeMedia", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.Transcode, TranscodeReason.ContainerNotSupported | TranscodeReason.VideoCodecNotSupported | TranscodeReason.AudioCodecNotSupported)] // #6450 should be DirectPlay
+        [InlineData("TranscodeMedia", "mp4-h264-ac3-srt-2600k", PlayMethod.Transcode, TranscodeReason.ContainerNotSupported | TranscodeReason.VideoCodecNotSupported | TranscodeReason.AudioCodecNotSupported)] // #6450 should be DirectPlay
+        [InlineData("TranscodeMedia", "mp4-hevc-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.ContainerNotSupported | TranscodeReason.VideoCodecNotSupported | TranscodeReason.AudioCodecNotSupported, "Transcode")] // #6450 should be DirectPlay
+        [InlineData("TranscodeMedia", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.ContainerNotSupported | TranscodeReason.VideoCodecNotSupported | TranscodeReason.AudioCodecNotSupported, "Transcode")] // #6450 should be DirectPlay
+        [InlineData("TranscodeMedia", "mkv-vp9-aac-srt-2600k", PlayMethod.Transcode, TranscodeReason.ContainerNotSupported | TranscodeReason.VideoCodecNotSupported | TranscodeReason.AudioCodecNotSupported, "Transcode")] // #6450 should be DirectPlay
+        [InlineData("TranscodeMedia", "mkv-vp9-ac3-srt-2600k", PlayMethod.Transcode, TranscodeReason.ContainerNotSupported | TranscodeReason.VideoCodecNotSupported | TranscodeReason.AudioCodecNotSupported, "Transcode")] // #6450 should be DirectPlay
+        [InlineData("TranscodeMedia", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.Transcode, TranscodeReason.ContainerNotSupported | TranscodeReason.VideoCodecNotSupported | TranscodeReason.AudioCodecNotSupported, "Transcode")] // #6450 should be DirectPlay
         // DirectMedia
         [InlineData("DirectMedia", "mp4-h264-aac-vtt-2600k", PlayMethod.DirectStream)] // #6450 should be DirectPlay
         [InlineData("DirectMedia", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectStream)] // #6450 should be DirectPlay
@@ -88,27 +93,120 @@ namespace Jellyfin.MediaBrowser.Model.Tests
         [InlineData("DirectMedia", "mkv-vp9-ac3-srt-2600k", PlayMethod.DirectStream)] // #6450 should be DirectPlay
         [InlineData("DirectMedia", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.DirectStream)] // #6450 should be DirectPlay
         // LowBandwidth
-        [InlineData("LowBandwidth", "mp4-h264-aac-vtt-2600k", PlayMethod.Transcode)] // #6450 should be DirectPlay
-        [InlineData("LowBandwidth", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.Transcode)] // #6450 should be DirectPlay
-        [InlineData("LowBandwidth", "mp4-h264-ac3-srt-2600k", PlayMethod.Transcode)] // #6450 should be DirectPlay
-        [InlineData("LowBandwidth", "mp4-hevc-aac-srt-15200k", PlayMethod.Transcode, true)] // #6450 should be DirectPlay
-        [InlineData("LowBandwidth", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, true)] // #6450 should be DirectPlay
-        [InlineData("LowBandwidth", "mkv-vp9-aac-srt-2600k", PlayMethod.Transcode, true)] // #6450 should be DirectPlay
-        [InlineData("LowBandwidth", "mkv-vp9-ac3-srt-2600k", PlayMethod.Transcode, true)] // #6450 should be DirectPlay
-        [InlineData("LowBandwidth", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.Transcode, true)] // #6450 should be DirectPlay
+        [InlineData("LowBandwidth", "mp4-h264-aac-vtt-2600k", PlayMethod.Transcode, TranscodeReason.ContainerBitrateExceedsLimit)] // #6450 should be DirectPlay
+        [InlineData("LowBandwidth", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.Transcode, TranscodeReason.ContainerBitrateExceedsLimit)] // #6450 should be DirectPlay
+        [InlineData("LowBandwidth", "mp4-h264-ac3-srt-2600k", PlayMethod.Transcode, TranscodeReason.ContainerBitrateExceedsLimit)] // #6450 should be DirectPlay
+        [InlineData("LowBandwidth", "mp4-hevc-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.ContainerBitrateExceedsLimit, "Transcode")] // #6450 should be DirectPlay
+        [InlineData("LowBandwidth", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.ContainerBitrateExceedsLimit, "Transcode")] // #6450 should be DirectPlay
+        [InlineData("LowBandwidth", "mkv-vp9-aac-srt-2600k", PlayMethod.Transcode, TranscodeReason.ContainerBitrateExceedsLimit, "Transcode")] // #6450 should be DirectPlay
+        [InlineData("LowBandwidth", "mkv-vp9-ac3-srt-2600k", PlayMethod.Transcode, TranscodeReason.ContainerBitrateExceedsLimit, "Transcode")] // #6450 should be DirectPlay
+        [InlineData("LowBandwidth", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.Transcode, TranscodeReason.ContainerBitrateExceedsLimit, "Transcode")] // #6450 should be DirectPlay
         // Null
-        [InlineData("Null", "mp4-h264-aac-vtt-2600k", null)] // #6450 should be DirectPlay
-        [InlineData("Null", "mp4-h264-ac3-aac-srt-2600k", null)] // #6450 should be DirectPlay
-        [InlineData("Null", "mp4-h264-ac3-srt-2600k", null)] // #6450 should be DirectPlay
-        [InlineData("Null", "mp4-hevc-aac-srt-15200k", null)] // #6450 should be DirectPlay
-        [InlineData("Null", "mp4-hevc-ac3-aac-srt-15200k", null)] // #6450 should be DirectPlay
-        [InlineData("Null", "mkv-vp9-aac-srt-2600k", null)] // #6450 should be DirectPlay
-        [InlineData("Null", "mkv-vp9-ac3-srt-2600k", null)] // #6450 should be DirectPlay
-        [InlineData("Null", "mkv-vp9-vorbis-vtt-2600k", null)] // #6450 should be DirectPlay
-        public async Task BuildVideoItemSimple(string deviceName, string mediaSource, PlayMethod? playMethod, bool fullTranscode = false)
+        [InlineData("Null", "mp4-h264-aac-vtt-2600k", null, TranscodeReason.ContainerBitrateExceedsLimit | TranscodeReason.SubtitleCodecNotSupported)] // #6450 should be DirectPlay
+        [InlineData("Null", "mp4-h264-ac3-aac-srt-2600k", null, TranscodeReason.ContainerBitrateExceedsLimit | TranscodeReason.SubtitleCodecNotSupported)] // #6450 should be DirectPlay
+        [InlineData("Null", "mp4-h264-ac3-srt-2600k", null, TranscodeReason.ContainerBitrateExceedsLimit | TranscodeReason.SubtitleCodecNotSupported)] // #6450 should be DirectPlay
+        [InlineData("Null", "mp4-hevc-aac-srt-15200k", null, TranscodeReason.ContainerBitrateExceedsLimit | TranscodeReason.SubtitleCodecNotSupported)] // #6450 should be DirectPlay
+        [InlineData("Null", "mp4-hevc-ac3-aac-srt-15200k", null, TranscodeReason.ContainerBitrateExceedsLimit | TranscodeReason.SubtitleCodecNotSupported)] // #6450 should be DirectPlay
+        [InlineData("Null", "mkv-vp9-aac-srt-2600k", null, TranscodeReason.ContainerBitrateExceedsLimit | TranscodeReason.SubtitleCodecNotSupported)] // #6450 should be DirectPlay
+        [InlineData("Null", "mkv-vp9-ac3-srt-2600k", null, TranscodeReason.ContainerBitrateExceedsLimit | TranscodeReason.SubtitleCodecNotSupported)] // #6450 should be DirectPlay
+        [InlineData("Null", "mkv-vp9-vorbis-vtt-2600k", null, TranscodeReason.ContainerBitrateExceedsLimit | TranscodeReason.SubtitleCodecNotSupported)] // #6450 should be DirectPlay
+        public async Task BuildVideoItemSimple(string deviceName, string mediaSource, PlayMethod? playMethod, TranscodeReason why = TranscodeReason.None, string transcodeMode = "DirectStream", string transcodeProtocol = "")
         {
-            var builder = GetStreamBuilder();
             var options = await GetVideoOptions(deviceName, mediaSource);
+            BuildVideoItemSimpleTest(options, playMethod, why, transcodeMode, transcodeProtocol);
+        }
+
+        [Theory]
+        // Chrome
+        [InlineData("Chrome", "mp4-h264-aac-vtt-2600k", PlayMethod.DirectStream)] // #6450 should be DirectPlay
+        [InlineData("Chrome", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported)] // #6450 should be DirectPlay
+        [InlineData("Chrome", "mp4-h264-ac3-srt-2600k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported)] // #6450 should be DirectStream
+        [InlineData("Chrome", "mp4-hevc-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported, "Transcode")]
+        [InlineData("Chrome", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported | TranscodeReason.AudioCodecNotSupported, "Transcode")]
+        [InlineData("Chrome", "mkv-vp9-aac-srt-2600k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported, "Transcode")] // #6450 should be 'false'
+        [InlineData("Chrome", "mkv-vp9-ac3-srt-2600k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported, "Transcode")] // #6450 should be 'false'
+        [InlineData("Chrome", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.DirectStream)] // #6450 should be DirectPlay
+        // Firefox
+        [InlineData("Firefox", "mp4-h264-aac-vtt-2600k", PlayMethod.DirectStream)] // #6450 should be DirectPlay
+        [InlineData("Firefox", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported)] // #6450 should be DirectPlay
+        [InlineData("Firefox", "mp4-h264-ac3-srt-2600k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported)] // #6450 should be DirectStream
+        [InlineData("Firefox", "mp4-hevc-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported, "Transcode")]
+        [InlineData("Firefox", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported | TranscodeReason.AudioCodecNotSupported, "Transcode")]
+        [InlineData("Firefox", "mkv-vp9-aac-srt-2600k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported, "Transcode")] // #6450 should be 'false'
+        [InlineData("Firefox", "mkv-vp9-ac3-srt-2600k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported, "Transcode")] // #6450 should be 'false'
+        [InlineData("Firefox", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.DirectStream)] // #6450 should be DirectPlay
+        // Safari
+        [InlineData("SafariNext", "mp4-h264-aac-vtt-2600k", PlayMethod.DirectStream)] // #6450 should be DirectPlay
+        [InlineData("SafariNext", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectStream)] // #6450 should be DirectPlay
+        [InlineData("SafariNext", "mp4-h264-ac3-srt-2600k", PlayMethod.DirectStream)] // #6450 should be DirectPlay
+        [InlineData("SafariNext", "mp4-hevc-aac-srt-15200k", PlayMethod.DirectStream)] // #6450 should probably be DirectPlay
+        [InlineData("SafariNext", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.DirectStream)] // #6450 should probably be DirectPlay
+        // AndroidPixel
+        [InlineData("AndroidPixel", "mp4-h264-aac-srt-2600k", PlayMethod.DirectStream)] // #6450 should be DirectPlay
+        [InlineData("AndroidPixel", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectStream)] // #6450 should be DirectPlay
+        [InlineData("AndroidPixel", "mp4-h264-ac3-srt-2600k", PlayMethod.DirectStream)] // #6450 should be DirectPlay
+        [InlineData("AndroidPixel", "mp4-hevc-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.ContainerBitrateExceedsLimit, "Transcode")]
+        [InlineData("AndroidPixel", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.ContainerBitrateExceedsLimit, "Transcode")]
+        // Yatse
+        [InlineData("Yatse", "mp4-h264-aac-srt-2600k", PlayMethod.DirectStream)] // #6450 should be DirectPlay
+        [InlineData("Yatse", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported)] // #6450 should be DirectPlay
+        [InlineData("Yatse", "mp4-h264-ac3-srt-2600k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported, "Transcode")]
+        [InlineData("Yatse", "mp4-hevc-aac-srt-15200k", PlayMethod.DirectStream)] // #6450 should be DirectPlay
+        [InlineData("Yatse", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported, "Transcode")] // #6450 should be DirectPlay
+        // RokuSSPlus
+        [InlineData("RokuSSPlus", "mp4-h264-aac-srt-2600k", PlayMethod.DirectStream)] // #6450 should be DirectPlay
+        [InlineData("RokuSSPlus", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported)] // #6450 should be DirectPlay
+        [InlineData("RokuSSPlus", "mp4-h264-ac3-srt-2600k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported)] // #6450 should be DirectStream
+        [InlineData("RokuSSPlus", "mp4-hevc-aac-srt-15200k", PlayMethod.DirectStream)] // #6450 should be DirectPlay
+        [InlineData("RokuSSPlus", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported, "Transcode")] // #6450 should be DirectPlay
+        [InlineData("RokuSSPlus", "mp4-hevc-ac3-srt-15200k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported, "Transcode")] // #6450 should be DirectStream
+        // JellyfinMediaPlayer
+        [InlineData("JellyfinMediaPlayer", "mp4-h264-aac-vtt-2600k", PlayMethod.DirectStream)] // #6450 should be DirectPlay
+        [InlineData("JellyfinMediaPlayer", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectStream)] // #6450 should be DirectPlay
+        [InlineData("JellyfinMediaPlayer", "mp4-h264-ac3-srt-2600k", PlayMethod.DirectStream)] // #6450 should be DirectPlay
+        [InlineData("JellyfinMediaPlayer", "mp4-hevc-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.ContainerBitrateExceedsLimit)] // #6450 should be DirectPlay
+        [InlineData("JellyfinMediaPlayer", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.ContainerBitrateExceedsLimit)] // #6450 should be DirectPlay
+        [InlineData("JellyfinMediaPlayer", "mkv-vp9-aac-srt-2600k", PlayMethod.DirectStream)] // #6450 should be DirectPlay
+        [InlineData("JellyfinMediaPlayer", "mkv-vp9-ac3-srt-2600k", PlayMethod.DirectStream)] // #6450 should be DirectPlay
+        [InlineData("JellyfinMediaPlayer", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.DirectStream)] // #6450 should be DirectPlay
+        public async Task BuildVideoItemWithFirstExplicitStream(string deviceName, string mediaSource, PlayMethod?playMethod, TranscodeReason why = TranscodeReason.None, string transcodeMode = "DirectStream", string transcodeProtocol = "")
+        {
+            var options = await GetVideoOptions(deviceName, mediaSource);
+            options.AudioStreamIndex = 1;
+            options.SubtitleStreamIndex = options.MediaSources[0].MediaStreams.Count() - 1;
+            BuildVideoItemSimpleTest(options, playMethod, why, transcodeMode, transcodeProtocol);
+        }
+
+        [Theory]
+        // Chrome
+        [InlineData("Chrome", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.Transcode, TranscodeReason.SecondaryAudioNotSupported)] // #6450 should be DirectPlay
+        [InlineData("Chrome", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported, "Transcode")] // #6450 should have container & profile video reasons?
+        // Firefox
+        [InlineData("Firefox", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.Transcode, TranscodeReason.SecondaryAudioNotSupported)] // #6450 should be DirectPlay
+        [InlineData("Firefox", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported, "Transcode")] // #6450 should have container & profile video reasons?
+        // Yatse
+        [InlineData("Yatse", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.Transcode, TranscodeReason.SecondaryAudioNotSupported)] // #6450 should be DirectPlay
+        [InlineData("Yatse", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.SecondaryAudioNotSupported, "Transcode")] // #6450 should be DirectPlay
+        // RokuSSPlus
+        [InlineData("RokuSSPlus", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectStream)] // #6450 should be DirectPlay
+        [InlineData("RokuSSPlus", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.DirectStream)] // #6450 should be DirectPlay
+        public async Task BuildVideoItemWithDirectPlayExplicitStreams(string deviceName, string mediaSource, PlayMethod playMethod, TranscodeReason why = TranscodeReason.None, string transcodeMode = "DirectStream", string transcodeProtocol = "")
+        {
+            var options = await GetVideoOptions(deviceName, mediaSource);
+            var streamCount = options.MediaSources[0].MediaStreams.Count();
+            options.AudioStreamIndex = streamCount - 2;
+            options.SubtitleStreamIndex = streamCount - 1;
+            BuildVideoItemSimpleTest(options, playMethod, why, transcodeMode, transcodeProtocol);
+        }
+
+        private void BuildVideoItemSimpleTest(VideoOptions options, PlayMethod? playMethod, TranscodeReason why, string transcodeMode, string transcodeProtocol)
+        {
+            if (string.IsNullOrEmpty(transcodeProtocol))
+            {
+                transcodeProtocol = playMethod == PlayMethod.DirectStream ? "http" : "hls";
+            }
+
+            var builder = GetStreamBuilder();
 
             var val = builder.BuildVideoItem(options);
             Assert.NotNull(val);
@@ -118,63 +216,130 @@ namespace Jellyfin.MediaBrowser.Model.Tests
                 Assert.Equal(playMethod, val.PlayMethod);
             }
 
-            var videoStreams = options.MediaSources.SelectMany(source => source.MediaStreams).Where(stream => stream.Type == MediaStreamType.Video);
-            var audioStreams = options.MediaSources.SelectMany(source => source.MediaStreams).Where(stream => stream.Type == MediaStreamType.Audio);
+            Assert.Equal(why, val.TranscodeReasons);
+
+            var audioStreamIndexInput = options.AudioStreamIndex;
+            var targetVideoStream = val.TargetVideoStream;
+            var targetAudioStream = val.TargetAudioStream;
 
-            var url = new UriBuilder(val.ToUrl("https://server/", "ACCESSTOKEN"));
-            var query = System.Web.HttpUtility.ParseQueryString(url.Query);
+            var mediaSource = options.MediaSources.First(source => source.Id == val.MediaSourceId);
+            Assert.NotNull(mediaSource);
+            var videoStreams = mediaSource.MediaStreams.Where(stream => stream.Type == MediaStreamType.Video);
+            var audioStreams = mediaSource.MediaStreams.Where(stream => stream.Type == MediaStreamType.Audio);
+            // TODO: check AudioStreamIndex vs options.AudioStreamIndex
+            var inputAudioStream = mediaSource.GetDefaultAudioStream(audioStreamIndexInput ?? mediaSource.DefaultAudioStreamIndex);
+
+            var uri = ParseUri(val);
 
             if (playMethod == PlayMethod.DirectPlay)
             {
-                // Assert.Contains(query.Get("VidoeCodec"), videoStreams.Select(stream => stream.Codec));
-                // Assert.Contains(query.Get("AudioCodec"), audioStreams.Select(stream => stream.Codec));
-                Assert.Contains(
-                    videoStreams,
-                    stream => val.TargetVideoCodec.Contains(stream.Codec));
-                Assert.Contains(
-                    audioStreams,
-                    stream => val.TargetAudioCodec.Contains(stream.Codec));
-            }
+                // check expected container
+                var containers = ContainerProfile.SplitValue(mediaSource.Container);
+                Assert.Contains(uri.Extension, containers);
 
-            if (playMethod == PlayMethod.DirectStream)
-            {
-                Assert.Matches("stream[.][^.]+$", url.Path);
-            }
+                // check expected video codec (1)
+                Assert.Contains(targetVideoStream.Codec, val.TargetVideoCodec);
+                Assert.Single(val.TargetVideoCodec);
+
+                // check expected audio codecs (1)
+                Assert.Contains(targetAudioStream.Codec, val.TargetAudioCodec);
+                Assert.Single(val.AudioCodecs);
 
-            if (playMethod == PlayMethod.Transcode)
+                // TODO: validate transcoding options as well
+            }
+            else if (playMethod == PlayMethod.DirectStream || playMethod == PlayMethod.Transcode)
             {
-                if (fullTranscode)
+                Assert.NotNull(val.Container);
+                // Assert.NotEmpty(val.VideoCodecs);
+                // Assert.NotEmpty(val.AudioCodecs);
+
+                // check expected container (todo: this could be a test param)
+                if (transcodeProtocol == "http")
                 {
+                    // Assert.Equal("webm", val.Container);
+                    Assert.Equal(val.Container, uri.Extension);
+                    Assert.Equal("stream", uri.Filename);
+                    // Assert.Equal("http", val.SubProtocol);
+                }
+                else
+                {
+                    Assert.Equal("ts", val.Container);
+                    Assert.Equal("m3u8", uri.Extension);
+                    Assert.Equal("master", uri.Filename);
                     Assert.Equal("hls", val.SubProtocol);
-                    Assert.EndsWith("master.m3u8", url.Path, StringComparison.InvariantCulture);
+                }
+
+                // Full transcode
+                if (transcodeMode == "Transcode")
+                {
+                    // TODO: what else to validate here
+                    if ((val.TranscodeReasons & TranscodeReason.ContainerReasons) == TranscodeReason.None)
+                    {
+                        // Assert.All(
+                        //     videoStreams,
+                        //     stream => Assert.DoesNotContain(stream.Codec, val.VideoCodecs));
+                    }
 
-                    // Assert.All(
-                    //     videoStreams,
-                    //     stream => Assert.DoesNotContain(stream.Codec, val.TargetVideoCodec));
+                    // todo: fill out tests here
                 }
+
+                // DirectStream and Remux
                 else
                 {
-                    Assert.Equal("hls", val.SubProtocol);
-                    Assert.EndsWith("master.m3u8", url.Path, StringComparison.InvariantCulture);
+                    // check expected video codec (1)
+                    Assert.Contains(targetVideoStream.Codec, val.TargetVideoCodec);
+                    Assert.Single(val.TargetVideoCodec);
 
-                    Assert.Contains(
-                        videoStreams,
-                        stream => val.TargetVideoCodec.Contains(stream.Codec));
-                    // Assert.All(
-                    //     audioStreams,
-                    //     stream => Assert.DoesNotContain(stream.Codec, val.TargetAudioCodec));
+                    if (transcodeMode == "DirectStream")
+                    {
+                        if (!targetAudioStream.IsExternal)
+                        {
+                            // check expected audio codecs (1)
+                            // Assert.DoesNotContain(targetAudioStream.Codec, val.AudioCodecs);
+                        }
+                    }
+                    else if (transcodeMode == "Remux")
+                    {
+                        // check expected audio codecs (1)
+                        Assert.Contains(targetAudioStream.Codec, val.AudioCodecs);
+                        Assert.Single(val.AudioCodecs);
+                    }
 
+                    // video details
+                    var videoStream = targetVideoStream;
                     Assert.False(val.EstimateContentLength);
                     Assert.Equal(TranscodeSeekInfo.Auto, val.TranscodeSeekInfo);
-                    // Assert.True(val.CopyTimestamps);
-
-                    var videoStream = videoStreams.First(stream => val.TargetVideoCodec.Contains(stream.Codec));
-
-                    Assert.Contains(videoStream.Codec, val.TargetVideoCodec);
-                    // Assert.Contains(videoStream.Profile.ToLowerInvariant(), val.TargetVideoProfile.Split(","));
+                    // Assert.Contains(videoStream.Profile?.ToLowerInvariant() ?? string.Empty, val.TargetVideoProfile?.Split(",").Select(s => s.ToLowerInvariant()) ?? new string[0]);
                     // Assert.Equal(videoStream.Level, val.TargetVideoLevel);
                     // Assert.Equal(videoStream.BitDepth, val.TargetVideoBitDepth);
-                    // Assert.Equal(videoStream.BitRate, val.VideoBitrate);
+                    // Assert.InRange(val.VideoBitrate.GetValueOrDefault(), videoStream.BitRate.GetValueOrDefault(), int.MaxValue);
+
+                    // audio codec not supported
+                    if ((why & TranscodeReason.AudioCodecNotSupported) != TranscodeReason.None)
+                    {
+                        // audio stream specified
+                        if (options.AudioStreamIndex >= 0)
+                        {
+                            // TODO:fixme
+                            if (!targetAudioStream.IsExternal)
+                            {
+                                Assert.DoesNotContain(targetAudioStream.Codec, val.AudioCodecs);
+                            }
+                        }
+
+                        // audio stream not specified
+                        else
+                        {
+                            // TODO:fixme
+                            Assert.All(audioStreams, stream =>
+                            {
+                                if (!stream.IsExternal)
+                                {
+                                    // Assert.DoesNotContain(stream.Codec, val.AudioCodecs);
+                                }
+                            });
+                        }
+                    }
                 }
             }
 
@@ -182,7 +347,7 @@ namespace Jellyfin.MediaBrowser.Model.Tests
             {
                 // what should the actual result be here?
                 Assert.Null(val.SubProtocol);
-                Assert.EndsWith("/stream", url.Path, StringComparison.InvariantCulture);
+                Assert.EndsWith("/stream", uri.Path, StringComparison.InvariantCulture);
 
                 Assert.False(val.EstimateContentLength);
                 Assert.Equal(TranscodeSeekInfo.Auto, val.TranscodeSeekInfo);
@@ -231,5 +396,23 @@ namespace Jellyfin.MediaBrowser.Model.Tests
                 Profile = dp,
             };
         }
+
+        private static (string Path, NameValueCollection Query, string Filename, string Extension) ParseUri(StreamInfo val)
+        {
+            var href = val.ToUrl("media:", "ACCESSTOKEN").Split("?", 2);
+            var path = href[0];
+
+            var queryString = href.ElementAtOrDefault(1);
+            var query = string.IsNullOrEmpty(queryString) ? System.Web.HttpUtility.ParseQueryString(queryString ?? string.Empty) : new NameValueCollection();
+
+            var filename = System.IO.Path.GetFileNameWithoutExtension(path);
+            var extension = System.IO.Path.GetExtension(path);
+            if (extension.Length > 0)
+            {
+                extension = extension.Substring(1);
+            }
+
+            return (path, query, filename, extension);
+        }
     }
 }