From 5e779f20ee88de93b75d06b255c93d0196c1255b Mon Sep 17 00:00:00 2001 From: Isaac Gordezky Date: Sun, 23 Jan 2022 23:49:14 +0000 Subject: [PATCH] Series: issue-6450 Issue: https://github.com/jellyfin/jellyfin/issues/6450 Enable DirectPlay responses Rewrite DirectPlay and DirectStream resolution Prefer copy transcode video codec options Enhance condition processor Support DirectStream and Transcode with parity Rework audio stream selection and add tests for ExternalAudio Update MediaInfoHelper to only call StreamBuilder once --- .../Controllers/MediaInfoController.cs | 10 +- Jellyfin.Api/Helpers/MediaInfoHelper.cs | 183 +--- .../MediaEncoding/EncodingHelper.cs | 16 +- .../MediaEncoding/EncodingJobInfo.cs | 5 +- MediaBrowser.Model/Dlna/AudioOptions.cs | 2 + MediaBrowser.Model/Dlna/StreamBuilder.cs | 946 ++++++++++-------- MediaBrowser.Model/Dlna/VideoOptions.cs | 2 + MediaBrowser.Model/Dto/MediaSourceInfo.cs | 2 +- MediaBrowser.Model/Properties/AssemblyInfo.cs | 1 + MediaBrowser.Model/Session/TranscodeReason.cs | 19 +- .../Session/TranscodeReasonExtensions.cs | 24 +- MediaBrowser.Model/Session/TranscodingInfo.cs | 2 +- .../Jellyfin.Dlna.Tests/StreamBuilderTests.cs | 353 ++++--- 13 files changed, 801 insertions(+), 764 deletions(-) diff --git a/Jellyfin.Api/Controllers/MediaInfoController.cs b/Jellyfin.Api/Controllers/MediaInfoController.cs index b422eb78c6..75df18204d 100644 --- a/Jellyfin.Api/Controllers/MediaInfoController.cs +++ b/Jellyfin.Api/Controllers/MediaInfoController.cs @@ -126,7 +126,7 @@ namespace Jellyfin.Api.Controllers var authInfo = await _authContext.GetAuthorizationInfo(Request).ConfigureAwait(false); var profile = playbackInfoDto?.DeviceProfile; - _logger.LogInformation("GetPostedPlaybackInfo profile: {@Profile}", profile); + _logger.LogDebug("GetPostedPlaybackInfo profile: {@Profile}", profile); if (profile == null) { @@ -225,14 +225,6 @@ namespace Jellyfin.Api.Controllers } } - if (info.MediaSources != null) - { - foreach (var mediaSource in info.MediaSources) - { - _mediaInfoHelper.NormalizeMediaSourceContainer(mediaSource, profile!, DlnaProfileType.Video); - } - } - return info; } diff --git a/Jellyfin.Api/Helpers/MediaInfoHelper.cs b/Jellyfin.Api/Helpers/MediaInfoHelper.cs index 3b8dc7e316..1fb7c67078 100644 --- a/Jellyfin.Api/Helpers/MediaInfoHelper.cs +++ b/Jellyfin.Api/Helpers/MediaInfoHelper.cs @@ -191,7 +191,9 @@ namespace Jellyfin.Api.Helpers DeviceId = auth.DeviceId, ItemId = item.Id, Profile = profile, - MaxAudioChannels = maxAudioChannels + MaxAudioChannels = maxAudioChannels, + AllowAudioStreamCopy = allowAudioStreamCopy, + AllowVideoStreamCopy = allowVideoStreamCopy }; if (string.Equals(mediaSourceId, mediaSource.Id, StringComparison.OrdinalIgnoreCase)) @@ -208,7 +210,7 @@ namespace Jellyfin.Api.Helpers mediaSource.SupportsDirectPlay = false; } - if (!enableDirectStream) + if (!enableDirectStream || !allowVideoStreamCopy) { mediaSource.SupportsDirectStream = false; } @@ -235,168 +237,79 @@ namespace Jellyfin.Api.Helpers user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding)); } - // Beginning of Playback Determination: Attempt DirectPlay first - if (mediaSource.SupportsDirectPlay) - { - if (mediaSource.IsRemote && user.HasPermission(PermissionKind.ForceRemoteSourceTranscoding)) - { - mediaSource.SupportsDirectPlay = false; - } - else - { - var supportsDirectStream = mediaSource.SupportsDirectStream; - - // Dummy this up to fool StreamBuilder - mediaSource.SupportsDirectStream = true; - options.MaxBitrate = maxBitrate; + options.MaxBitrate = GetMaxBitrate(maxBitrate, user, ipAddress); - if (item is Audio) - { - if (!user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding)) - { - options.ForceDirectPlay = true; - } - } - else if (item is Video) - { - if (!user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding) - && !user.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding) - && !user.HasPermission(PermissionKind.EnablePlaybackRemuxing)) - { - options.ForceDirectPlay = true; - } - } + if (!options.ForceDirectStream) + { + // direct-stream http streaming is currently broken + options.EnableDirectStream = false; + } - // The MediaSource supports direct stream, now test to see if the client supports it - var streamInfo = string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase) - ? streamBuilder.BuildAudioItem(options) - : streamBuilder.BuildVideoItem(options); + // Beginning of Playback Determination + var streamInfo = string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase) + ? streamBuilder.BuildAudioItem(options) + : streamBuilder.BuildVideoItem(options); - if (streamInfo == null || !streamInfo.IsDirectStream) - { - mediaSource.SupportsDirectPlay = false; - } + if (streamInfo != null) + { + streamInfo.PlaySessionId = playSessionId; + streamInfo.StartPositionTicks = startTimeTicks; - // Set this back to what it was - mediaSource.SupportsDirectStream = supportsDirectStream; + mediaSource.SupportsDirectPlay = streamInfo.PlayMethod == PlayMethod.DirectPlay; + // Players do not handle this being set according to PlayMethod + mediaSource.SupportsDirectStream = options.EnableDirectStream ? streamInfo.PlayMethod == PlayMethod.DirectPlay || streamInfo.PlayMethod == PlayMethod.DirectStream : streamInfo.PlayMethod == PlayMethod.DirectPlay; + mediaSource.SupportsTranscoding = streamInfo.PlayMethod == PlayMethod.DirectStream || mediaSource.TranscodingContainer != null; - if (streamInfo != null) + if (item is Audio) + { + if (!user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding)) { - SetDeviceSpecificSubtitleInfo(streamInfo, mediaSource, auth.Token); - mediaSource.DefaultAudioStreamIndex = streamInfo.AudioStreamIndex; + mediaSource.SupportsTranscoding = false; } } - } - - if (mediaSource.SupportsDirectStream) - { - if (mediaSource.IsRemote && user.HasPermission(PermissionKind.ForceRemoteSourceTranscoding)) + else if (item is Video) { - mediaSource.SupportsDirectStream = false; - } - else - { - options.MaxBitrate = GetMaxBitrate(maxBitrate, user, ipAddress); - - if (item is Audio) + if (!user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding) + && !user.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding) + && !user.HasPermission(PermissionKind.EnablePlaybackRemuxing)) { - if (!user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding)) - { - options.ForceDirectStream = true; - } - } - else if (item is Video) - { - if (!user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding) - && !user.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding) - && user.HasPermission(PermissionKind.EnablePlaybackRemuxing)) - { - options.ForceDirectStream = true; - } - } - - // The MediaSource supports direct stream, now test to see if the client supports it - var streamInfo = string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase) - ? streamBuilder.BuildAudioItem(options) - : streamBuilder.BuildVideoItem(options); - - if (streamInfo == null || !streamInfo.IsDirectStream) - { - mediaSource.SupportsDirectStream = false; - } - - if (streamInfo != null) - { - SetDeviceSpecificSubtitleInfo(streamInfo, mediaSource, auth.Token); - mediaSource.DefaultAudioStreamIndex = streamInfo.AudioStreamIndex; + mediaSource.SupportsTranscoding = false; } } - } - - if (mediaSource.SupportsTranscoding) - { - options.MaxBitrate = GetMaxBitrate(maxBitrate, user, ipAddress); - - // The MediaSource supports direct stream, now test to see if the client supports it - var streamInfo = string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase) - ? streamBuilder.BuildAudioItem(options) - : streamBuilder.BuildVideoItem(options); if (mediaSource.IsRemote && user.HasPermission(PermissionKind.ForceRemoteSourceTranscoding)) { - if (streamInfo != null) - { - streamInfo.PlaySessionId = playSessionId; - streamInfo.StartPositionTicks = startTimeTicks; - mediaSource.TranscodingUrl = streamInfo.ToUrl("-", auth.Token).TrimStart('-'); - mediaSource.TranscodingUrl += "&allowVideoStreamCopy=false"; - mediaSource.TranscodingUrl += "&allowAudioStreamCopy=false"; - mediaSource.TranscodingContainer = streamInfo.Container; - mediaSource.TranscodingSubProtocol = streamInfo.SubProtocol; - - // Do this after the above so that StartPositionTicks is set - SetDeviceSpecificSubtitleInfo(streamInfo, mediaSource, auth.Token); - mediaSource.DefaultAudioStreamIndex = streamInfo.AudioStreamIndex; - } + mediaSource.SupportsDirectPlay = false; + mediaSource.SupportsDirectStream = false; + + mediaSource.TranscodingUrl = streamInfo.ToUrl("-", auth.Token).TrimStart('-'); + mediaSource.TranscodingUrl += "&allowVideoStreamCopy=false"; + mediaSource.TranscodingUrl += "&allowAudioStreamCopy=false"; + mediaSource.TranscodingContainer = streamInfo.Container; + mediaSource.TranscodingSubProtocol = streamInfo.SubProtocol; } else { - if (streamInfo != null) + if (mediaSource.SupportsTranscoding || mediaSource.SupportsDirectStream) { - streamInfo.PlaySessionId = playSessionId; + streamInfo.PlayMethod = PlayMethod.Transcode; + mediaSource.TranscodingUrl = streamInfo.ToUrl("-", auth.Token).TrimStart('-'); - if (streamInfo.PlayMethod == PlayMethod.Transcode) + if (!allowVideoStreamCopy) { - streamInfo.StartPositionTicks = startTimeTicks; - mediaSource.TranscodingUrl = streamInfo.ToUrl("-", auth.Token).TrimStart('-'); - - if (!allowVideoStreamCopy) - { - mediaSource.TranscodingUrl += "&allowVideoStreamCopy=false"; - } - - if (!allowAudioStreamCopy) - { - mediaSource.TranscodingUrl += "&allowAudioStreamCopy=false"; - } - - mediaSource.TranscodingContainer = streamInfo.Container; - mediaSource.TranscodingSubProtocol = streamInfo.SubProtocol; + mediaSource.TranscodingUrl += "&allowVideoStreamCopy=false"; } if (!allowAudioStreamCopy) { mediaSource.TranscodingUrl += "&allowAudioStreamCopy=false"; } - - mediaSource.TranscodingContainer = streamInfo.Container; - mediaSource.TranscodingSubProtocol = streamInfo.SubProtocol; - - // Do this after the above so that StartPositionTicks is set - SetDeviceSpecificSubtitleInfo(streamInfo, mediaSource, auth.Token); - mediaSource.DefaultAudioStreamIndex = streamInfo.AudioStreamIndex; } } + + // Do this after the above so that StartPositionTicks is set + SetDeviceSpecificSubtitleInfo(streamInfo, mediaSource, auth.Token); + mediaSource.DefaultAudioStreamIndex = streamInfo.AudioStreamIndex; } foreach (var attachment in mediaSource.MediaAttachments) diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index f7248acacd..b4aff60e17 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -1798,7 +1798,7 @@ namespace MediaBrowser.Controller.MediaEncoding return false; } - return request.EnableAutoStreamCopy; + return true; } public bool CanStreamCopyAudio(EncodingJobInfo state, MediaStream audioStream, IEnumerable supportedAudioCodecs) @@ -1855,17 +1855,11 @@ namespace MediaBrowser.Controller.MediaEncoding } // Video bitrate must fall within requested value - if (request.AudioBitRate.HasValue) + if (request.AudioBitRate.HasValue + && audioStream.BitDepth.HasValue + && audioStream.BitRate.Value > request.AudioBitRate.Value) { - if (!audioStream.BitRate.HasValue || audioStream.BitRate.Value <= 0) - { - return false; - } - - if (audioStream.BitRate.Value > request.AudioBitRate.Value) - { - return false; - } + return false; } return request.EnableAutoStreamCopy; diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs b/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs index 0f5fdcc3c6..23067c7b18 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs @@ -35,7 +35,7 @@ namespace MediaBrowser.Controller.MediaEncoding SupportedSubtitleCodecs = Array.Empty(); } - public TranscodeReason[] TranscodeReasons { get => TranscodeReason.ToArray(); } + public TranscodeReason[] TranscodeReasons => TranscodeReason.ToArray(); [JsonIgnore] public TranscodeReason TranscodeReason @@ -50,8 +50,7 @@ namespace MediaBrowser.Controller.MediaEncoding return TranscodeReason.None; } - TranscodeReason reason = TranscodeReason.None; - Enum.TryParse(BaseRequest.TranscodeReasons, out reason); + _ = Enum.TryParse(BaseRequest.TranscodeReasons, out var reason); _transcodeReasons = reason; } diff --git a/MediaBrowser.Model/Dlna/AudioOptions.cs b/MediaBrowser.Model/Dlna/AudioOptions.cs index 4d4d8d78cb..33755e7462 100644 --- a/MediaBrowser.Model/Dlna/AudioOptions.cs +++ b/MediaBrowser.Model/Dlna/AudioOptions.cs @@ -27,6 +27,8 @@ namespace MediaBrowser.Model.Dlna public bool ForceDirectStream { get; set; } + public bool AllowAudioStreamCopy { get; set; } + public Guid ItemId { get; set; } public MediaSourceInfo[] MediaSources { get; set; } diff --git a/MediaBrowser.Model/Dlna/StreamBuilder.cs b/MediaBrowser.Model/Dlna/StreamBuilder.cs index 7654337e70..0f48e985d5 100644 --- a/MediaBrowser.Model/Dlna/StreamBuilder.cs +++ b/MediaBrowser.Model/Dlna/StreamBuilder.cs @@ -15,6 +15,12 @@ namespace MediaBrowser.Model.Dlna { public class StreamBuilder { + // Aliases + internal const TranscodeReason ContainerReasons = TranscodeReason.ContainerNotSupported | TranscodeReason.ContainerBitrateExceedsLimit; + internal const TranscodeReason AudioReasons = TranscodeReason.AudioCodecNotSupported | TranscodeReason.AudioBitrateNotSupported | TranscodeReason.AudioChannelsNotSupported | TranscodeReason.AudioProfileNotSupported | TranscodeReason.AudioSampleRateNotSupported | TranscodeReason.SecondaryAudioNotSupported | TranscodeReason.AudioBitDepthNotSupported | TranscodeReason.AudioIsExternal; + internal const TranscodeReason VideoReasons = TranscodeReason.VideoCodecNotSupported | TranscodeReason.VideoResolutionNotSupported | TranscodeReason.AnamorphicVideoNotSupported | TranscodeReason.InterlacedVideoNotSupported | TranscodeReason.VideoBitDepthNotSupported | TranscodeReason.VideoBitrateNotSupported | TranscodeReason.VideoFramerateNotSupported | TranscodeReason.VideoLevelNotSupported | TranscodeReason.RefFramesNotSupported; + internal const TranscodeReason DirectStreamReasons = AudioReasons | TranscodeReason.ContainerNotSupported; + private readonly ILogger _logger; private readonly ITranscoderSupport _transcoderSupport; @@ -227,7 +233,7 @@ namespace MediaBrowser.Model.Dlna } } - public static string NormalizeMediaSourceFormatIntoSingleContainer(string inputContainer, DeviceProfile profile, DlnaProfileType type) + public static string NormalizeMediaSourceFormatIntoSingleContainer(string inputContainer, DeviceProfile profile, DlnaProfileType type, DirectPlayProfile playProfile = null) { if (string.IsNullOrEmpty(inputContainer)) { @@ -236,16 +242,12 @@ namespace MediaBrowser.Model.Dlna var formats = ContainerProfile.SplitValue(inputContainer); - if (formats.Length == 1) - { - return formats[0]; - } - if (profile != null) { + var playProfiles = playProfile == null ? profile.DirectPlayProfiles : new[] { playProfile }; foreach (var format in formats) { - foreach (var directPlayProfile in profile.DirectPlayProfiles) + foreach (var directPlayProfile in playProfiles) { if (directPlayProfile.Type == type && directPlayProfile.SupportsContainer(format)) @@ -287,9 +289,9 @@ namespace MediaBrowser.Model.Dlna var audioStream = item.GetDefaultAudioStream(null); - var directPlayInfo = GetAudioDirectPlayMethods(item, audioStream, options); + var directPlayInfo = GetAudioDirectPlayProfile(item, audioStream, options); - var directPlayMethods = directPlayInfo.PlayMethods; + var directPlayMethod = directPlayInfo.PlayMethod; var transcodeReasons = directPlayInfo.TranscodeReasons; int? inputAudioChannels = audioStream?.Channels; @@ -297,55 +299,17 @@ namespace MediaBrowser.Model.Dlna int? inputAudioSampleRate = audioStream?.SampleRate; int? inputAudioBitDepth = audioStream?.BitDepth; - if (directPlayMethods.Any()) + if (directPlayMethod.HasValue) { - string audioCodec = audioStream?.Codec; - - // Make sure audio codec profiles are satisfied - var conditions = new List(); - foreach (var i in options.Profile.CodecProfiles) - { - if (i.Type == CodecType.Audio && i.ContainsAnyCodec(audioCodec, item.Container)) - { - bool applyConditions = true; - foreach (ProfileCondition applyCondition in i.ApplyConditions) - { - if (!ConditionProcessor.IsAudioConditionSatisfied(applyCondition, inputAudioChannels, inputAudioBitrate, inputAudioSampleRate, inputAudioBitDepth)) - { - LogConditionFailure(options.Profile, "AudioCodecProfile", applyCondition, item); - applyConditions = false; - break; - } - } - - if (applyConditions) - { - conditions.AddRange(i.Conditions); - } - } - } + var profile = options.Profile; + var audioFailureConditions = GetProfileConditionsForAudio(profile.CodecProfiles, item.Container, audioStream?.Codec, inputAudioChannels, inputAudioBitrate, inputAudioSampleRate, inputAudioBitDepth, true); + var audioFailureReasons = AggregateFailureConditions(item, profile, "AudioCodecProfile", audioFailureConditions); + transcodeReasons |= audioFailureReasons; - bool all = true; - foreach (ProfileCondition c in conditions) + if (audioFailureReasons == TranscodeReason.None) { - if (!ConditionProcessor.IsAudioConditionSatisfied(c, inputAudioChannels, inputAudioBitrate, inputAudioSampleRate, inputAudioBitDepth)) - { - LogConditionFailure(options.Profile, "AudioCodecProfile", c, item); - transcodeReasons |= GetTranscodeReasonForFailedCondition(c); - - all = false; - break; - } - } - - if (all) - { - if (directPlayMethods.Contains(PlayMethod.DirectStream)) - { - playlistItem.PlayMethod = PlayMethod.DirectStream; - } - - playlistItem.Container = NormalizeMediaSourceFormatIntoSingleContainer(item.Container, options.Profile, DlnaProfileType.Audio); + playlistItem.PlayMethod = directPlayMethod.Value; + playlistItem.Container = NormalizeMediaSourceFormatIntoSingleContainer(item.Container, options.Profile, DlnaProfileType.Audio, directPlayInfo.Profile); return playlistItem; } @@ -370,45 +334,9 @@ namespace MediaBrowser.Model.Dlna return null; } - SetStreamInfoOptionsFromTranscodingProfile(playlistItem, transcodingProfile); - - var audioCodecProfiles = new List(); - foreach (var i in options.Profile.CodecProfiles) - { - if (i.Type == CodecType.Audio && i.ContainsAnyCodec(transcodingProfile.AudioCodec, transcodingProfile.Container)) - { - audioCodecProfiles.Add(i); - } - - if (audioCodecProfiles.Count >= 1) - { - break; - } - } - - var audioTranscodingConditions = new List(); - foreach (var i in audioCodecProfiles) - { - bool applyConditions = true; - foreach (var applyCondition in i.ApplyConditions) - { - if (!ConditionProcessor.IsAudioConditionSatisfied(applyCondition, inputAudioChannels, inputAudioBitrate, inputAudioSampleRate, inputAudioBitDepth)) - { - LogConditionFailure(options.Profile, "AudioCodecProfile", applyCondition, item); - applyConditions = false; - break; - } - } - - if (applyConditions) - { - foreach (ProfileCondition c in i.Conditions) - { - audioTranscodingConditions.Add(c); - } - } - } + SetStreamInfoOptionsFromTranscodingProfile(item, playlistItem, transcodingProfile); + var audioTranscodingConditions = GetProfileConditionsForAudio(options.Profile.CodecProfiles, transcodingProfile.Container, transcodingProfile.AudioCodec, inputAudioChannels, inputAudioBitrate, inputAudioSampleRate, inputAudioBitDepth, false).ToArray(); ApplyTranscodingConditions(playlistItem, audioTranscodingConditions, null, true, true); // Honor requested max channels @@ -444,9 +372,9 @@ namespace MediaBrowser.Model.Dlna return options.GetMaxBitrate(isAudio); } - private (IEnumerable PlayMethods, TranscodeReason TranscodeReasons) GetAudioDirectPlayMethods(MediaSourceInfo item, MediaStream audioStream, AudioOptions options) + private (DirectPlayProfile Profile, PlayMethod? PlayMethod, TranscodeReason TranscodeReasons) GetAudioDirectPlayProfile(MediaSourceInfo item, MediaStream audioStream, AudioOptions options) { - DirectPlayProfile directPlayProfile = options.Profile.DirectPlayProfiles + var directPlayProfile = options.Profile.DirectPlayProfiles .FirstOrDefault(x => x.Type == DlnaProfileType.Audio && IsAudioDirectPlaySupported(x, item, audioStream)); if (directPlayProfile == null) @@ -457,20 +385,21 @@ namespace MediaBrowser.Model.Dlna item.Path ?? "Unknown path", audioStream.Codec ?? "Unknown codec"); - return (Enumerable.Empty(), GetTranscodeReasonsFromDirectPlayProfile(item, null, audioStream, options.Profile.DirectPlayProfiles)); + return (null, null, GetTranscodeReasonsFromDirectPlayProfile(item, null, audioStream, options.Profile.DirectPlayProfiles)); } var playMethods = new List(); var transcodeReasons = TranscodeReason.None; - // While options takes the network and other factors into account. Only applies to direct stream - if (item.SupportsDirectStream) + // The profile describes what the device supports + // If device requirements are satisfied then allow both direct stream and direct play + if (item.SupportsDirectPlay) { - if (IsItemBitrateEligibleForDirectPlay(item, options.GetMaxBitrate(true) ?? 0, PlayMethod.DirectStream)) + if (IsItemBitrateEligibleForDirectPlay(item, GetBitrateForDirectPlayCheck(item, options, true) ?? 0, PlayMethod.DirectPlay)) { - if (options.EnableDirectStream) + if (options.EnableDirectPlay) { - playMethods.Add(PlayMethod.DirectStream); + return (directPlayProfile, PlayMethod.DirectPlay, TranscodeReason.None); } } else @@ -479,15 +408,14 @@ namespace MediaBrowser.Model.Dlna } } - // The profile describes what the device supports - // If device requirements are satisfied then allow both direct stream and direct play - if (item.SupportsDirectPlay) + // While options takes the network and other factors into account. Only applies to direct stream + if (item.SupportsDirectStream) { - if (IsItemBitrateEligibleForDirectPlay(item, GetBitrateForDirectPlayCheck(item, options, true) ?? 0, PlayMethod.DirectPlay)) + if (IsItemBitrateEligibleForDirectPlay(item, options.GetMaxBitrate(true) ?? 0, PlayMethod.DirectStream)) { - if (options.EnableDirectPlay) + if (options.EnableDirectStream) { - playMethods.Add(PlayMethod.DirectPlay); + return (directPlayProfile, PlayMethod.DirectStream, transcodeReasons); } } else @@ -496,12 +424,7 @@ namespace MediaBrowser.Model.Dlna } } - if (playMethods.Count > 0) - { - transcodeReasons = TranscodeReason.None; - } - - return (playMethods, transcodeReasons); + return (directPlayProfile, null, transcodeReasons); } private static TranscodeReason GetTranscodeReasonsFromDirectPlayProfile(MediaSourceInfo item, MediaStream videoStream, MediaStream audioStream, IEnumerable directPlayProfiles) @@ -592,30 +515,29 @@ namespace MediaBrowser.Model.Dlna return item.DefaultSubtitleStreamIndex; } - private static void SetStreamInfoOptionsFromTranscodingProfile(StreamInfo playlistItem, TranscodingProfile transcodingProfile) + private static void SetStreamInfoOptionsFromTranscodingProfile(MediaSourceInfo item, StreamInfo playlistItem, TranscodingProfile transcodingProfile) { - if (string.IsNullOrEmpty(transcodingProfile.AudioCodec)) - { - playlistItem.AudioCodecs = Array.Empty(); - } - else - { - playlistItem.AudioCodecs = transcodingProfile.AudioCodec.Split(','); - } + var container = transcodingProfile.Container; + var protocol = transcodingProfile.Protocol; - playlistItem.Container = transcodingProfile.Container; - playlistItem.EstimateContentLength = transcodingProfile.EstimateContentLength; - playlistItem.TranscodeSeekInfo = transcodingProfile.TranscodeSeekInfo; + item.TranscodingContainer = container; + item.TranscodingSubProtocol = protocol; - if (string.IsNullOrEmpty(transcodingProfile.VideoCodec)) + if (playlistItem.PlayMethod == PlayMethod.Transcode) { - playlistItem.VideoCodecs = Array.Empty(); + playlistItem.Container = container; + playlistItem.SubProtocol = protocol; } - else + + playlistItem.TranscodeSeekInfo = transcodingProfile.TranscodeSeekInfo; + if (!string.IsNullOrEmpty(transcodingProfile.MaxAudioChannels) + && int.TryParse(transcodingProfile.MaxAudioChannels, NumberStyles.Any, CultureInfo.InvariantCulture, out int transcodingMaxAudioChannels)) { - playlistItem.VideoCodecs = transcodingProfile.VideoCodec.Split(','); + playlistItem.TranscodingMaxAudioChannels = transcodingMaxAudioChannels; } + playlistItem.EstimateContentLength = transcodingProfile.EstimateContentLength; + playlistItem.CopyTimestamps = transcodingProfile.CopyTimestamps; playlistItem.EnableSubtitlesInManifest = transcodingProfile.EnableSubtitlesInManifest; playlistItem.EnableMpegtsM2TsMode = transcodingProfile.EnableMpegtsM2TsMode; @@ -631,14 +553,21 @@ namespace MediaBrowser.Model.Dlna { playlistItem.SegmentLength = transcodingProfile.SegmentLength; } + } - playlistItem.SubProtocol = transcodingProfile.Protocol; + private static void SetStreamInfoOptionsFromDirectPlayProfile(VideoOptions options, MediaSourceInfo item, StreamInfo playlistItem, DirectPlayProfile directPlayProfile) + { + var container = NormalizeMediaSourceFormatIntoSingleContainer(item.Container, options.Profile, DlnaProfileType.Video, directPlayProfile); + var protocol = "http"; - if (!string.IsNullOrEmpty(transcodingProfile.MaxAudioChannels) - && int.TryParse(transcodingProfile.MaxAudioChannels, NumberStyles.Any, CultureInfo.InvariantCulture, out int transcodingMaxAudioChannels)) - { - playlistItem.TranscodingMaxAudioChannels = transcodingMaxAudioChannels; - } + item.TranscodingContainer = container; + item.TranscodingSubProtocol = protocol; + + playlistItem.Container = container; + playlistItem.SubProtocol = protocol; + + playlistItem.VideoCodecs = new[] { item.VideoStream.Codec }; + playlistItem.AudioCodecs = ContainerProfile.SplitValue(directPlayProfile.AudioCodec); } private StreamInfo BuildVideoItem(MediaSourceInfo item, VideoOptions options) @@ -667,11 +596,27 @@ namespace MediaBrowser.Model.Dlna playlistItem.AudioStreamIndex = audioStream.Index; } + // Collect candidate audio streams + IEnumerable candidateAudioStreams = audioStream == null ? Array.Empty() : new[] { audioStream }; + if (!options.AudioStreamIndex.HasValue || options.AudioStreamIndex < 0) + { + if (audioStream?.IsDefault == true) + { + candidateAudioStreams = item.MediaStreams.Where(stream => stream.Type == MediaStreamType.Audio && stream.IsDefault); + } + else + { + candidateAudioStreams = item.MediaStreams.Where(stream => stream.Type == MediaStreamType.Audio && stream.Language == audioStream?.Language); + } + } + + candidateAudioStreams = candidateAudioStreams.ToArray(); + var videoStream = item.VideoStream; // 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); + var directPlayEligibilityResult = IsEligibleForDirectPlay(item, GetBitrateForDirectPlayCheck(item, options, true) ?? 0, options, PlayMethod.DirectPlay); + var directStreamEligibilityResult = IsEligibleForDirectPlay(item, options.GetMaxBitrate(false) ?? 0, options, PlayMethod.DirectStream); bool isEligibleForDirectPlay = options.EnableDirectPlay && (options.ForceDirectPlay || directPlayEligibilityResult == TranscodeReason.None); bool isEligibleForDirectStream = options.EnableDirectStream && (options.ForceDirectStream || directPlayEligibilityResult == TranscodeReason.None); var transcodeReasons = directPlayEligibilityResult | directStreamEligibilityResult; @@ -683,183 +628,292 @@ namespace MediaBrowser.Model.Dlna isEligibleForDirectPlay, isEligibleForDirectStream); + DirectPlayProfile directPlayProfile = null; if (isEligibleForDirectPlay || isEligibleForDirectStream) { // See if it can be direct played - var directPlayInfo = GetVideoDirectPlayProfile(options, item, videoStream, audioStream, isEligibleForDirectStream); + var directPlayInfo = GetVideoDirectPlayProfile(options, item, videoStream, audioStream, candidateAudioStreams, subtitleStream, isEligibleForDirectPlay, isEligibleForDirectStream); var directPlay = directPlayInfo.PlayMethod; + transcodeReasons |= directPlayInfo.TranscodeReasons; if (directPlay != null) { + directPlayProfile = directPlayInfo.Profile; playlistItem.PlayMethod = directPlay.Value; - playlistItem.Container = NormalizeMediaSourceFormatIntoSingleContainer(item.Container, options.Profile, DlnaProfileType.Video); + playlistItem.Container = NormalizeMediaSourceFormatIntoSingleContainer(item.Container, options.Profile, DlnaProfileType.Video, directPlayProfile); + playlistItem.VideoCodecs = new[] { videoStream.Codec }; + + if (directPlay == PlayMethod.DirectPlay) + { + playlistItem.SubProtocol = "http"; + + var audioStreamIndex = directPlayInfo.AudioStreamIndex ?? audioStream?.Index; + if (audioStreamIndex.HasValue) + { + playlistItem.AudioStreamIndex = audioStreamIndex; + playlistItem.AudioCodecs = new[] { item.GetMediaStream(MediaStreamType.Audio, audioStreamIndex.Value)?.Codec }; + } + } + else if (directPlay == PlayMethod.DirectStream) + { + playlistItem.AudioStreamIndex = audioStream?.Index; + if (audioStream != null) + { + playlistItem.AudioCodecs = ContainerProfile.SplitValue(directPlayProfile.AudioCodec); + } + + SetStreamInfoOptionsFromDirectPlayProfile(options, item, playlistItem, directPlayProfile); + BuildStreamVideoItem(playlistItem, options, item, videoStream, audioStream, candidateAudioStreams, directPlayProfile.Container, directPlayProfile.VideoCodec, directPlayProfile.AudioCodec); + } if (subtitleStream != null) { - var subtitleProfile = GetSubtitleProfile(item, subtitleStream, options.Profile.SubtitleProfiles, directPlay.Value, _transcoderSupport, item.Container, null); + var subtitleProfile = GetSubtitleProfile(item, subtitleStream, options.Profile.SubtitleProfiles, directPlay.Value, _transcoderSupport, directPlayProfile.Container, null); playlistItem.SubtitleDeliveryMethod = subtitleProfile.Method; playlistItem.SubtitleFormat = subtitleProfile.Format; } - - return playlistItem; } - transcodeReasons |= directPlayInfo.TranscodeReasons; + _logger.LogInformation( + "DirectPlay Result for Profile: {0}, Path: {1}, PlayMethod: {2}, AudioStreamIndex: {3}, SubtitleStreamIndex: {4}, Reasons: {5}", + options.Profile.Name ?? "Anonymous Profile", + item.Path ?? "Unknown path", + directPlayInfo.PlayMethod, + directPlayInfo.AudioStreamIndex ?? audioStream?.Index, + playlistItem.SubtitleStreamIndex, + directPlayInfo.TranscodeReasons); } - if (playlistItem.PlayMethod != PlayMethod.Transcode) + playlistItem.TranscodeReasons = transcodeReasons; + + if (playlistItem.PlayMethod != PlayMethod.DirectStream || !options.EnableDirectStream) { - playlistItem.TranscodeReasons = transcodeReasons; - return playlistItem; + // Can't direct play, find the transcoding profile + // If we do this for direct-stream we will overwrite the info + var transcodingProfile = GetVideoTranscodeProfile(item, options, videoStream, audioStream, candidateAudioStreams, subtitleStream, playlistItem); + if (transcodingProfile != null) + { + SetStreamInfoOptionsFromTranscodingProfile(item, playlistItem, transcodingProfile); + + BuildStreamVideoItem(playlistItem, options, item, videoStream, audioStream, candidateAudioStreams, transcodingProfile.Container, transcodingProfile.VideoCodec, transcodingProfile.AudioCodec); + + if (subtitleStream != null) + { + var subtitleProfile = GetSubtitleProfile(item, subtitleStream, options.Profile.SubtitleProfiles, PlayMethod.Transcode, _transcoderSupport, transcodingProfile.Container, transcodingProfile.Protocol); + + playlistItem.SubtitleDeliveryMethod = subtitleProfile.Method; + playlistItem.SubtitleFormat = subtitleProfile.Format; + playlistItem.SubtitleCodecs = new[] { subtitleProfile.Format }; + } + + if (playlistItem.PlayMethod != PlayMethod.DirectPlay) + { + playlistItem.PlayMethod = PlayMethod.Transcode; + } + } } - // Can't direct play, find the transcoding profile - TranscodingProfile transcodingProfile = null; - foreach (var i in options.Profile.TranscodingProfiles) + _logger.LogInformation( + "StreamBuilder.BuildVideoItem( Profile={0}, Path={1}, AudioStreamIndex={2}, SubtitleStreamIndex={3} ) => ( PlayMethod={4}, TranscodeReason={5} ) {6}", + options.Profile.Name ?? "Anonymous Profile", + item.Path ?? "Unknown path", + options.AudioStreamIndex, + options.SubtitleStreamIndex, + playlistItem.PlayMethod, + playlistItem.TranscodeReasons, + playlistItem.ToUrl("media:", "")); + + item.Container = NormalizeMediaSourceFormatIntoSingleContainer(item.Container, options.Profile, DlnaProfileType.Video, directPlayProfile); + return playlistItem; + } + + private TranscodingProfile GetVideoTranscodeProfile(MediaSourceInfo item, VideoOptions options, MediaStream videoStream, MediaStream audioStream, IEnumerable candidateAudioStreams, MediaStream subtitleStream, StreamInfo playlistItem) + { + if (!(item.SupportsTranscoding || item.SupportsDirectStream)) + { + return null; + } + + var transcodingProfiles = options.Profile.TranscodingProfiles + .Where(i => i.Type == playlistItem.MediaType && i.Context == options.Context); + + if (options.AllowVideoStreamCopy) { - if (i.Type == playlistItem.MediaType && i.Context == options.Context) + // prefer direct copy profile + float videoFramerate = videoStream == null ? 0 : videoStream.AverageFrameRate ?? videoStream.AverageFrameRate ?? 0; + TransportStreamTimestamp? timestamp = videoStream == null ? TransportStreamTimestamp.None : item.Timestamp; + int? numAudioStreams = item.GetStreamCount(MediaStreamType.Audio); + int? numVideoStreams = item.GetStreamCount(MediaStreamType.Video); + + transcodingProfiles = transcodingProfiles.ToLookup(transcodingProfile => { - transcodingProfile = i; - break; - } + var videoCodecs = ContainerProfile.SplitValue(transcodingProfile.VideoCodec); + var match = ContainerProfile.ContainsContainer(videoCodecs, item.VideoStream.Codec) && + options.Profile.CodecProfiles + .Any(i => i.Type == CodecType.Video && + i.ContainsAnyCodec(transcodingProfile.VideoCodec, transcodingProfile.Container) && + i.ApplyConditions.Any(applyCondition => !ConditionProcessor.IsVideoConditionSatisfied(applyCondition, videoStream?.Width, videoStream?.Height, videoStream?.BitDepth, videoStream?.BitRate, videoStream?.Profile, videoStream?.Level, videoFramerate, videoStream?.PacketLength, timestamp, videoStream?.IsAnamorphic, videoStream?.IsInterlaced, videoStream?.RefFrames, numVideoStreams, numAudioStreams, videoStream?.CodecTag, videoStream?.IsAVC))); + + return match ? 1 : 2; + }) + .OrderBy(lookup => lookup.Key) + .SelectMany(lookup => lookup); } - if (transcodingProfile != null) + return transcodingProfiles.FirstOrDefault(); + } + + private void BuildStreamVideoItem(StreamInfo playlistItem, VideoOptions options, MediaSourceInfo item, MediaStream videoStream, MediaStream audioStream, IEnumerable candidateAudioStreams, string container, string videoCodec, string audioCodec) + { + // prefer matching video codecs + var videoCodecs = ContainerProfile.SplitValue(videoCodec); + var directVideoCodec = ContainerProfile.ContainsContainer(videoCodecs, videoStream.Codec) ? videoStream.Codec : null; + playlistItem.VideoCodecs = directVideoCodec != null ? new[] { directVideoCodec } : videoCodecs; + + // copy video codec options as a starting point, this applies to transcode and direct-stream + playlistItem.MaxFramerate = videoStream.AverageFrameRate; + var qualifier = videoStream.Codec; + if (videoStream.Level.HasValue) { - if (!item.SupportsTranscoding) + playlistItem.SetOption(qualifier, "level", videoStream.Level.Value.ToString(CultureInfo.InvariantCulture)); + } + + if (videoStream.BitDepth.HasValue) + { + playlistItem.SetOption(qualifier, "videobitdepth", videoStream.BitDepth.Value.ToString(CultureInfo.InvariantCulture)); + } + + if (!string.IsNullOrEmpty(videoStream.Profile)) + { + playlistItem.SetOption(qualifier, "profile", videoStream.Profile.ToLowerInvariant()); + } + + if (videoStream.Level != 0) + { + playlistItem.SetOption(qualifier, "level", videoStream.Level.ToString()); + } + + // prefer matching audio codecs, could do beter here + var audioCodecs = ContainerProfile.SplitValue(audioCodec); + var directAudioStream = candidateAudioStreams.FirstOrDefault(stream => ContainerProfile.ContainsContainer(audioCodecs, stream.Codec)); + playlistItem.AudioCodecs = audioCodecs; + if (directAudioStream != null) + { + audioStream = directAudioStream; + playlistItem.AudioStreamIndex = audioStream.Index; + playlistItem.AudioCodecs = new[] { audioStream.Codec }; + + // copy matching audio codec options + playlistItem.AudioSampleRate = audioStream.SampleRate; + playlistItem.SetOption(qualifier, "audiochannels", audioStream.Channels.ToString()); + + if (!string.IsNullOrEmpty(audioStream.Profile)) { - return null; + playlistItem.SetOption(audioStream.Codec, "profile", audioStream.Profile.ToLowerInvariant()); } - if (subtitleStream != null) + if (audioStream.Level != 0) { - var subtitleProfile = GetSubtitleProfile(item, subtitleStream, options.Profile.SubtitleProfiles, PlayMethod.Transcode, _transcoderSupport, transcodingProfile.Container, transcodingProfile.Protocol); - - playlistItem.SubtitleDeliveryMethod = subtitleProfile.Method; - playlistItem.SubtitleFormat = subtitleProfile.Format; - playlistItem.SubtitleCodecs = new[] { subtitleProfile.Format }; + playlistItem.SetOption(audioStream.Codec, "level", audioStream.Level.ToString()); } + } - playlistItem.PlayMethod = PlayMethod.Transcode; + int? width = videoStream?.Width; + int? height = videoStream?.Height; + int? bitDepth = videoStream?.BitDepth; + int? videoBitrate = videoStream?.BitRate; + double? videoLevel = videoStream?.Level; + string videoProfile = videoStream?.Profile; + float videoFramerate = videoStream == null ? 0 : videoStream.AverageFrameRate ?? videoStream.AverageFrameRate ?? 0; + bool? isAnamorphic = videoStream?.IsAnamorphic; + bool? isInterlaced = videoStream?.IsInterlaced; + string videoCodecTag = videoStream?.CodecTag; + bool? isAvc = videoStream?.IsAVC; + + TransportStreamTimestamp? timestamp = videoStream == null ? TransportStreamTimestamp.None : item.Timestamp; + int? packetLength = videoStream?.PacketLength; + int? refFrames = videoStream?.RefFrames; - SetStreamInfoOptionsFromTranscodingProfile(playlistItem, transcodingProfile); + int? numAudioStreams = item.GetStreamCount(MediaStreamType.Audio); + int? numVideoStreams = item.GetStreamCount(MediaStreamType.Video); - var isFirstAppliedCodecProfile = true; - foreach (var i in options.Profile.CodecProfiles) + var appliedVideoConditions = options.Profile.CodecProfiles + .Where(i => i.Type == CodecType.Video && + i.ContainsAnyCodec(videoCodec, container) && + i.ApplyConditions.Any(applyCondition => ConditionProcessor.IsVideoConditionSatisfied(applyCondition, width, height, bitDepth, videoBitrate, videoProfile, videoLevel, videoFramerate, packetLength, timestamp, isAnamorphic, isInterlaced, refFrames, numVideoStreams, numAudioStreams, videoCodecTag, isAvc))); + var isFirstAppliedCodecProfile = true; + foreach (var i in appliedVideoConditions) + { + var transcodingVideoCodecs = ContainerProfile.SplitValue(videoCodec); + foreach (var transcodingVideoCodec in transcodingVideoCodecs) { - if (i.Type == CodecType.Video && i.ContainsAnyCodec(transcodingProfile.VideoCodec, transcodingProfile.Container)) + if (i.ContainsAnyCodec(transcodingVideoCodec, container)) { - bool applyConditions = true; - foreach (ProfileCondition applyCondition in i.ApplyConditions) - { - int? width = videoStream?.Width; - int? height = videoStream?.Height; - int? bitDepth = videoStream?.BitDepth; - int? videoBitrate = videoStream?.BitRate; - double? videoLevel = videoStream?.Level; - string videoProfile = videoStream?.Profile; - float videoFramerate = videoStream == null ? 0 : videoStream.AverageFrameRate ?? videoStream.AverageFrameRate ?? 0; - bool? isAnamorphic = videoStream?.IsAnamorphic; - bool? isInterlaced = videoStream?.IsInterlaced; - string videoCodecTag = videoStream?.CodecTag; - bool? isAvc = videoStream?.IsAVC; - - TransportStreamTimestamp? timestamp = videoStream == null ? TransportStreamTimestamp.None : item.Timestamp; - int? packetLength = videoStream?.PacketLength; - int? refFrames = videoStream?.RefFrames; - - int? numAudioStreams = item.GetStreamCount(MediaStreamType.Audio); - int? numVideoStreams = item.GetStreamCount(MediaStreamType.Video); - - if (!ConditionProcessor.IsVideoConditionSatisfied(applyCondition, width, height, bitDepth, videoBitrate, videoProfile, videoLevel, videoFramerate, packetLength, timestamp, isAnamorphic, isInterlaced, refFrames, numVideoStreams, numAudioStreams, videoCodecTag, isAvc)) - { - // LogConditionFailure(options.Profile, "VideoCodecProfile.ApplyConditions", applyCondition, item); - applyConditions = false; - break; - } - } - - if (applyConditions) - { - var transcodingVideoCodecs = ContainerProfile.SplitValue(transcodingProfile.VideoCodec); - foreach (var transcodingVideoCodec in transcodingVideoCodecs) - { - if (i.ContainsAnyCodec(transcodingVideoCodec, transcodingProfile.Container)) - { - ApplyTranscodingConditions(playlistItem, i.Conditions, transcodingVideoCodec, true, isFirstAppliedCodecProfile); - isFirstAppliedCodecProfile = false; - } - } - } + ApplyTranscodingConditions(playlistItem, i.Conditions, transcodingVideoCodec, true, isFirstAppliedCodecProfile); + isFirstAppliedCodecProfile = false; + continue; } } + } - // Honor requested max channels - playlistItem.GlobalMaxAudioChannels = options.MaxAudioChannels; + // Honor requested max channels + playlistItem.GlobalMaxAudioChannels = options.MaxAudioChannels; - int audioBitrate = GetAudioBitrate(options.GetMaxBitrate(false) ?? 0, playlistItem.TargetAudioCodec, audioStream, playlistItem); - playlistItem.AudioBitrate = Math.Min(playlistItem.AudioBitrate ?? audioBitrate, audioBitrate); + int audioBitrate = GetAudioBitrate(options.GetMaxBitrate(false) ?? 0, playlistItem.TargetAudioCodec, audioStream, playlistItem); + playlistItem.AudioBitrate = Math.Min(playlistItem.AudioBitrate ?? audioBitrate, audioBitrate); - isFirstAppliedCodecProfile = true; - foreach (var i in options.Profile.CodecProfiles) + bool? isSecondaryAudio = audioStream == null ? null : item.IsSecondaryAudio(audioStream); + int? inputAudioBitrate = audioStream == null ? null : audioStream.BitRate; + int? audioChannels = audioStream == null ? null : audioStream.Channels; + string audioProfile = audioStream == null ? null : audioStream.Profile; + int? inputAudioSampleRate = audioStream == null ? null : audioStream.SampleRate; + int? inputAudioBitDepth = audioStream == null ? null : audioStream.BitDepth; + + var appliedAudioConditions = options.Profile.CodecProfiles + .Where(i => i.Type == CodecType.Video && + i.ContainsAnyCodec(audioCodec, container) && + i.ApplyConditions.Any(applyCondition => ConditionProcessor.IsVideoAudioConditionSatisfied(applyCondition, audioChannels, inputAudioBitrate, inputAudioSampleRate, inputAudioBitDepth, audioProfile, isSecondaryAudio))); + isFirstAppliedCodecProfile = true; + foreach (var i in appliedAudioConditions) + { + var transcodingAudioCodecs = ContainerProfile.SplitValue(audioCodec); + foreach (var transcodingAudioCodec in transcodingAudioCodecs) { - if (i.Type == CodecType.VideoAudio && i.ContainsAnyCodec(transcodingProfile.AudioCodec, transcodingProfile.Container)) + if (i.ContainsAnyCodec(transcodingAudioCodec, container)) { - bool applyConditions = true; - foreach (ProfileCondition applyCondition in i.ApplyConditions) - { - bool? isSecondaryAudio = audioStream == null ? null : item.IsSecondaryAudio(audioStream); - int? inputAudioBitrate = audioStream == null ? null : audioStream.BitRate; - int? audioChannels = audioStream == null ? null : audioStream.Channels; - string audioProfile = audioStream == null ? null : audioStream.Profile; - int? inputAudioSampleRate = audioStream == null ? null : audioStream.SampleRate; - int? inputAudioBitDepth = audioStream == null ? null : audioStream.BitDepth; - - if (!ConditionProcessor.IsVideoAudioConditionSatisfied(applyCondition, audioChannels, inputAudioBitrate, inputAudioSampleRate, inputAudioBitDepth, audioProfile, isSecondaryAudio)) - { - // LogConditionFailure(options.Profile, "VideoCodecProfile.ApplyConditions", applyCondition, item); - applyConditions = false; - break; - } - } - - if (applyConditions) - { - var transcodingAudioCodecs = ContainerProfile.SplitValue(transcodingProfile.AudioCodec); - foreach (var transcodingAudioCodec in transcodingAudioCodecs) - { - if (i.ContainsAnyCodec(transcodingAudioCodec, transcodingProfile.Container)) - { - ApplyTranscodingConditions(playlistItem, i.Conditions, transcodingAudioCodec, true, isFirstAppliedCodecProfile); - isFirstAppliedCodecProfile = false; - } - } - } + ApplyTranscodingConditions(playlistItem, i.Conditions, transcodingAudioCodec, true, isFirstAppliedCodecProfile); + isFirstAppliedCodecProfile = false; + break; } } + } - var maxBitrateSetting = options.GetMaxBitrate(false); - // Honor max rate - if (maxBitrateSetting.HasValue) - { - var availableBitrateForVideo = maxBitrateSetting.Value; - - if (playlistItem.AudioBitrate.HasValue) - { - availableBitrateForVideo -= playlistItem.AudioBitrate.Value; - } + var maxBitrateSetting = options.GetMaxBitrate(false); + // Honor max rate + if (maxBitrateSetting.HasValue) + { + var availableBitrateForVideo = maxBitrateSetting.Value; - // Make sure the video bitrate is lower than bitrate settings but at least 64k - long currentValue = playlistItem.VideoBitrate ?? availableBitrateForVideo; - var longBitrate = Math.Max(Math.Min(availableBitrateForVideo, currentValue), 64000); - playlistItem.VideoBitrate = longBitrate >= int.MaxValue ? int.MaxValue : Convert.ToInt32(longBitrate); + if (playlistItem.AudioBitrate.HasValue) + { + availableBitrateForVideo -= playlistItem.AudioBitrate.Value; } - } - playlistItem.TranscodeReasons = transcodeReasons; + // Make sure the video bitrate is lower than bitrate settings but at least 64k + long currentValue = playlistItem.VideoBitrate ?? availableBitrateForVideo; + var longBitrate = Math.Max(Math.Min(availableBitrateForVideo, currentValue), 64000); + playlistItem.VideoBitrate = longBitrate >= int.MaxValue ? int.MaxValue : Convert.ToInt32(longBitrate); + } - return playlistItem; + _logger.LogInformation( + "Transcode Result for Profile: {0}, Path: {1}, PlayMethod: {2}, AudioStreamIndex: {3}, SubtitleStreamIndex: {4}, Reasons: {5}", + options.Profile.Name ?? "Anonymous Profile", + item.Path ?? "Unknown path", + playlistItem.PlayMethod, + audioStream.Index, + playlistItem.SubtitleStreamIndex, + playlistItem.TranscodeReasons); } private static int GetDefaultAudioBitrate(string audioCodec, int? audioChannels) @@ -988,63 +1042,30 @@ namespace MediaBrowser.Model.Dlna return 7168000; } - private (PlayMethod? PlayMethod, TranscodeReason TranscodeReasons) GetVideoDirectPlayProfile( + private (DirectPlayProfile Profile, PlayMethod? PlayMethod, int? AudioStreamIndex, TranscodeReason TranscodeReasons) GetVideoDirectPlayProfile( VideoOptions options, MediaSourceInfo mediaSource, MediaStream videoStream, MediaStream audioStream, + IEnumerable candidateAudioStreams, + MediaStream subtitleStream, + bool isEligibleForDirectPlay, bool isEligibleForDirectStream) { if (options.ForceDirectPlay) { - return (PlayMethod.DirectPlay, TranscodeReason.None); + return (null, PlayMethod.DirectPlay, audioStream?.Index, TranscodeReason.None); } if (options.ForceDirectStream) { - return (PlayMethod.DirectStream, TranscodeReason.None); + return (null, PlayMethod.DirectStream, audioStream?.Index, TranscodeReason.None); } DeviceProfile profile = options.Profile; string container = mediaSource.Container; - // See if it can be direct played - DirectPlayProfile directPlay = null; - foreach (var p in profile.DirectPlayProfiles) - { - if (p.Type == DlnaProfileType.Video && IsVideoDirectPlaySupported(p, container, videoStream, audioStream)) - { - directPlay = p; - break; - } - } - - if (directPlay == null) - { - _logger.LogDebug( - "Container: {Container}, Video: {Video}, Audio: {Audio} cannot be direct played by profile: {Profile} for path: {Path}", - container, - videoStream?.Codec ?? "no video", - audioStream?.Codec ?? "no audio", - profile.Name ?? "unknown profile", - mediaSource.Path ?? "unknown path"); - - return (null, GetTranscodeReasonsFromDirectPlayProfile(mediaSource, videoStream, audioStream, profile.DirectPlayProfiles)); - } - - var conditions = new List(); - foreach (var p in profile.ContainerProfiles) - { - if (p.Type == DlnaProfileType.Video - && p.ContainsContainer(container)) - { - foreach (var c in p.Conditions) - { - conditions.Add(c); - } - } - } - + // video int? width = videoStream?.Width; int? height = videoStream?.Height; int? bitDepth = videoStream?.BitDepth; @@ -1056,12 +1077,9 @@ namespace MediaBrowser.Model.Dlna bool? isInterlaced = videoStream?.IsInterlaced; string videoCodecTag = videoStream?.CodecTag; bool? isAvc = videoStream?.IsAVC; - - int? audioBitrate = audioStream?.BitRate; - int? audioChannels = audioStream?.Channels; - string audioProfile = audioStream?.Profile; - int? audioSampleRate = audioStream?.SampleRate; - int? audioBitDepth = audioStream?.BitDepth; + // audio + var defaultLanguage = audioStream?.Language ?? string.Empty; + var defaultMarked = audioStream?.IsDefault ?? false; TransportStreamTimestamp? timestamp = videoStream == null ? TransportStreamTimestamp.None : mediaSource.Timestamp; int? packetLength = videoStream?.PacketLength; @@ -1070,106 +1088,165 @@ namespace MediaBrowser.Model.Dlna int? numAudioStreams = mediaSource.GetStreamCount(MediaStreamType.Audio); int? numVideoStreams = mediaSource.GetStreamCount(MediaStreamType.Video); - // Check container conditions - foreach (ProfileCondition i in conditions) - { - if (!ConditionProcessor.IsVideoConditionSatisfied(i, width, height, bitDepth, videoBitrate, videoProfile, videoLevel, videoFramerate, packetLength, timestamp, isAnamorphic, isInterlaced, refFrames, numVideoStreams, numAudioStreams, videoCodecTag, isAvc)) - { - LogConditionFailure(profile, "VideoContainerProfile", i, mediaSource); - - var transcodeReasons = GetTranscodeReasonForFailedCondition(i); - return (null, transcodeReasons); - } - } - - string videoCodec = videoStream?.Codec; + var checkVideoConditions = (ProfileCondition[] conditions) => + conditions.Where(applyCondition => !ConditionProcessor.IsVideoConditionSatisfied(applyCondition, width, height, bitDepth, videoBitrate, videoProfile, videoLevel, videoFramerate, packetLength, timestamp, isAnamorphic, isInterlaced, refFrames, numVideoStreams, numAudioStreams, videoCodecTag, isAvc)); - conditions = new List(); - foreach (var i in profile.CodecProfiles) - { - if (i.Type == CodecType.Video && i.ContainsAnyCodec(videoCodec, container)) - { - bool applyConditions = true; - foreach (ProfileCondition applyCondition in i.ApplyConditions) + // Check container conditions + var containerProfileReasons = AggregateFailureConditions( + mediaSource, + profile, + "VideoCodecProfile", + profile.ContainerProfiles + .Where(containerProfile => containerProfile.Type == DlnaProfileType.Video && containerProfile.ContainsContainer(container)) + .SelectMany(containerProfile => checkVideoConditions(containerProfile.Conditions))); + + // Check video conditions + var videoCodecProfileReasons = AggregateFailureConditions( + mediaSource, + profile, + "VideoCodecProfile", + profile.CodecProfiles + .Where(codecProfile => codecProfile.Type == CodecType.Video && codecProfile.ContainsAnyCodec(videoStream?.Codec, container)) + .SelectMany(codecProfile => { - if (!ConditionProcessor.IsVideoConditionSatisfied(applyCondition, width, height, bitDepth, videoBitrate, videoProfile, videoLevel, videoFramerate, packetLength, timestamp, isAnamorphic, isInterlaced, refFrames, numVideoStreams, numAudioStreams, videoCodecTag, isAvc)) + var failedApplyConditions = checkVideoConditions(codecProfile.ApplyConditions); + if (!failedApplyConditions.Any()) { - // LogConditionFailure(profile, "VideoCodecProfile.ApplyConditions", applyCondition, mediaSource); - applyConditions = false; - break; + return Array.Empty(); } - } - if (applyConditions) - { - foreach (ProfileCondition c in i.Conditions) - { - conditions.Add(c); - } - } - } - } + var failedConditions = checkVideoConditions(codecProfile.Conditions); + return failedApplyConditions.Concat(failedConditions); + })); + + // Check audiocandidates profile conditions + var audioStreamMatches = candidateAudioStreams.ToDictionary(s => s, audioStream => CheckVideoAudioStreamDirectPlay(options, mediaSource, container, audioStream, defaultLanguage, defaultMarked)); - foreach (ProfileCondition i in conditions) + TranscodeReason subtitleProfileReasons = TranscodeReason.None; + if (subtitleStream != null) { - if (!ConditionProcessor.IsVideoConditionSatisfied(i, width, height, bitDepth, videoBitrate, videoProfile, videoLevel, videoFramerate, packetLength, timestamp, isAnamorphic, isInterlaced, refFrames, numVideoStreams, numAudioStreams, videoCodecTag, isAvc)) - { - LogConditionFailure(profile, "VideoCodecProfile", i, mediaSource); + var subtitleProfile = GetSubtitleProfile(mediaSource, subtitleStream, options.Profile.SubtitleProfiles, PlayMethod.DirectPlay, _transcoderSupport, container, null); - var transcodeReason = GetTranscodeReasonForFailedCondition(i); - return (null, transcodeReason); + if (subtitleProfile.Method != SubtitleDeliveryMethod.Drop + && subtitleProfile.Method != SubtitleDeliveryMethod.External + && subtitleProfile.Method != SubtitleDeliveryMethod.Embed) + { + _logger.LogDebug("Not eligible for {0} due to unsupported subtitles", PlayMethod.DirectPlay); + subtitleProfileReasons |= TranscodeReason.SubtitleCodecNotSupported; } } - if (audioStream != null) - { - string audioCodec = audioStream.Codec; - conditions = new List(); - bool? isSecondaryAudio = mediaSource.IsSecondaryAudio(audioStream); - - foreach (var i in profile.CodecProfiles) + var rankings = new[] { VideoReasons, AudioReasons, ContainerReasons }; + var rank = (ref TranscodeReason a) => { - if (i.Type == CodecType.VideoAudio && i.ContainsAnyCodec(audioCodec, container)) + var index = 1; + foreach (var flag in rankings) { - bool applyConditions = true; - foreach (ProfileCondition applyCondition in i.ApplyConditions) + var reason = a & flag; + if (reason != TranscodeReason.None) { - if (!ConditionProcessor.IsVideoAudioConditionSatisfied(applyCondition, audioChannels, audioBitrate, audioSampleRate, audioBitDepth, audioProfile, isSecondaryAudio)) - { - // LogConditionFailure(profile, "VideoAudioCodecProfile.ApplyConditions", applyCondition, mediaSource); - applyConditions = false; - break; - } + a = reason; + return index; } - if (applyConditions) - { - foreach (ProfileCondition c in i.Conditions) - { - conditions.Add(c); - } - } + index++; } - } - foreach (ProfileCondition i in conditions) + return index; + }; + + // Check DirectPlay profiles to see if it can be direct played + var analyzedProfiles = profile.DirectPlayProfiles + .Where(directPlayProfile => directPlayProfile.Type == DlnaProfileType.Video) + .Select((directPlayProfile, order) => { - if (!ConditionProcessor.IsVideoAudioConditionSatisfied(i, audioChannels, audioBitrate, audioSampleRate, audioBitDepth, audioProfile, isSecondaryAudio)) + var directPlayProfileReasons = TranscodeReason.None; + var audioCodecProfileReasons = TranscodeReason.None; + + // Check container type + if (!directPlayProfile.SupportsContainer(container)) { - LogConditionFailure(profile, "VideoAudioCodecProfile", i, mediaSource); + directPlayProfileReasons |= TranscodeReason.ContainerNotSupported; + } - var transcodeReasons = GetTranscodeReasonForFailedCondition(i); - return (null, transcodeReasons); + // Check video codec + string videoCodec = videoStream?.Codec; + if (!directPlayProfile.SupportsVideoCodec(videoCodec)) + { + directPlayProfileReasons |= TranscodeReason.VideoCodecNotSupported; } - } + + // Check audio codec + var selectedAudioStream = candidateAudioStreams.FirstOrDefault(audioStream => directPlayProfile.SupportsAudioCodec(audioStream.Codec)); + if (selectedAudioStream == null) + { + directPlayProfileReasons |= TranscodeReason.AudioCodecNotSupported; + } + else + { + audioCodecProfileReasons = audioStreamMatches.GetValueOrDefault(selectedAudioStream); + } + + var failureReasons = directPlayProfileReasons | containerProfileReasons | videoCodecProfileReasons | audioCodecProfileReasons | subtitleProfileReasons; + var directStreamFailureReasons = failureReasons & (~DirectStreamReasons); + + PlayMethod? playMethod = null; + if (failureReasons == TranscodeReason.None && isEligibleForDirectPlay && mediaSource.SupportsDirectPlay) + { + playMethod = PlayMethod.DirectPlay; + } + else if (directStreamFailureReasons == TranscodeReason.None && isEligibleForDirectStream && mediaSource.SupportsDirectStream && directPlayProfile != null) + { + playMethod = PlayMethod.DirectStream; + } + + var ranked = rank(ref failureReasons); + return (Result: (Profile: directPlayProfile, PlayMethod: playMethod, AudioStreamIndex: selectedAudioStream?.Index, TranscodeReason: failureReasons), Order: order, Rank: ranked); + }) + .OrderByDescending(analysis => analysis.Result.PlayMethod) + .ThenBy(analysis => analysis.Order) + .ToArray() + .ToLookup(analysis => analysis.Result.PlayMethod != null); + + var profileMatch = analyzedProfiles[true] + .Select(analysis => analysis.Result) + .FirstOrDefault(); + if (profileMatch.Profile != null) + { + return profileMatch; } - if (isEligibleForDirectStream && mediaSource.SupportsDirectStream) + var failureReasons = analyzedProfiles[false].OrderBy(a => a.Result.TranscodeReason).ThenBy(analysis => analysis.Order).FirstOrDefault().Result.TranscodeReason; + if (failureReasons == TranscodeReason.None) { - return (PlayMethod.DirectStream, TranscodeReason.None); + failureReasons = TranscodeReason.DirectPlayError; } - return (null, TranscodeReason.ContainerBitrateExceedsLimit); + return (Profile: null, PlayMethod: null, AudioStreamIndex: null, TranscodeReasons: failureReasons); + } + + private TranscodeReason CheckVideoAudioStreamDirectPlay(VideoOptions options, MediaSourceInfo mediaSource, string container, MediaStream audioStream, string language, bool isDefault) + { + var profile = options.Profile; + var audioFailureConditions = GetProfileConditionsForVideoAudio(profile.CodecProfiles, container, audioStream.Codec, audioStream.Channels, audioStream.BitRate, audioStream.SampleRate, audioStream.BitDepth, audioStream.Profile, !audioStream.IsDefault); + + var audioStreamFailureReasons = AggregateFailureConditions(mediaSource, profile, "VideoAudioCodecProfile", audioFailureConditions); + if (audioStream?.IsExternal == true) + { + audioStreamFailureReasons |= TranscodeReason.AudioIsExternal; + } + + return audioStreamFailureReasons; + } + + private TranscodeReason AggregateFailureConditions(MediaSourceInfo mediaSource, DeviceProfile profile, string type, IEnumerable conditions) + { + return conditions.Aggregate(TranscodeReason.None, (reasons, i) => + { + LogConditionFailure(profile, type, i, mediaSource); + var transcodeReasons = GetTranscodeReasonForFailedCondition(i); + return reasons | transcodeReasons; + }); } private void LogConditionFailure(DeviceProfile profile, string type, ProfileCondition condition, MediaSourceInfo mediaSource) @@ -1188,38 +1265,18 @@ namespace MediaBrowser.Model.Dlna private TranscodeReason IsEligibleForDirectPlay( MediaSourceInfo item, long maxBitrate, - MediaStream subtitleStream, - MediaStream audioStream, VideoOptions options, PlayMethod playMethod) { - var reason = TranscodeReason.None; - if (subtitleStream != null) - { - var subtitleProfile = GetSubtitleProfile(item, subtitleStream, options.Profile.SubtitleProfiles, playMethod, _transcoderSupport, item.Container, null); - - if (subtitleProfile.Method != SubtitleDeliveryMethod.Drop - && subtitleProfile.Method != SubtitleDeliveryMethod.External - && subtitleProfile.Method != SubtitleDeliveryMethod.Embed) - { - _logger.LogDebug("Not eligible for {0} due to unsupported subtitles", playMethod); - reason |= TranscodeReason.SubtitleCodecNotSupported; - } - } - bool result = IsItemBitrateEligibleForDirectPlay(item, maxBitrate, playMethod); if (!result) { - reason |= TranscodeReason.ContainerBitrateExceedsLimit; + return TranscodeReason.ContainerBitrateExceedsLimit; } - - // TODO:6450 support external audio in DirectStream? - if (audioStream?.IsExternal == true) + else { - reason |= TranscodeReason.AudioIsExternal; + return TranscodeReason.None; } - - return reason; } public static SubtitleProfile GetSubtitleProfile( @@ -1443,6 +1500,47 @@ namespace MediaBrowser.Model.Dlna } } + private static IEnumerable GetProfileConditionsForVideoAudio( + IEnumerable codecProfiles, + string container, + string codec, + int? audioChannels, + int? audioBitrate, + int? audioSampleRate, + int? audioBitDepth, + string audioProfile, + bool? isSecondaryAudio) + { + return codecProfiles + .Where(profile => profile.Type == CodecType.VideoAudio && profile.ContainsAnyCodec(codec, container) && + profile.ApplyConditions.All(applyCondition => ConditionProcessor.IsVideoAudioConditionSatisfied(applyCondition, audioChannels, audioBitrate, audioSampleRate, audioBitDepth, audioProfile, isSecondaryAudio))) + .SelectMany(profile => profile.Conditions) + .Where(condition => !ConditionProcessor.IsVideoAudioConditionSatisfied(condition, audioChannels, audioBitrate, audioSampleRate, audioBitDepth, audioProfile, isSecondaryAudio)); + } + + private static IEnumerable GetProfileConditionsForAudio( + IEnumerable codecProfiles, + string container, + string codec, + int? audioChannels, + int? audioBitrate, + int? audioSampleRate, + int? audioBitDepth, + bool checkConditions) + { + var conditions = codecProfiles + .Where(profile => profile.Type == CodecType.Audio && profile.ContainsAnyCodec(codec, container) && + profile.ApplyConditions.All(applyCondition => ConditionProcessor.IsAudioConditionSatisfied(applyCondition, audioChannels, audioBitrate, audioSampleRate, audioBitDepth))) + .SelectMany(profile => profile.Conditions); + + if (!checkConditions) + { + return conditions; + } + + return conditions.Where(condition => !ConditionProcessor.IsAudioConditionSatisfied(condition, audioChannels, audioBitrate, audioSampleRate, audioBitDepth)); + } + private void ApplyTranscodingConditions(StreamInfo item, IEnumerable conditions, string qualifier, bool enableQualifiedConditions, bool enableNonQualifiedConditions) { foreach (ProfileCondition condition in conditions) @@ -1722,10 +1820,22 @@ namespace MediaBrowser.Model.Dlna var values = value .Split('|', StringSplitOptions.RemoveEmptyEntries); - if (condition.Condition == ProfileConditionType.Equals || condition.Condition == ProfileConditionType.EqualsAny) + if (condition.Condition == ProfileConditionType.Equals) { item.SetOption(qualifier, "profile", string.Join(',', values)); } + else if (condition.Condition == ProfileConditionType.EqualsAny) + { + var currentValue = item.GetOption(qualifier, "profile"); + if (!string.IsNullOrEmpty(currentValue) && values.Any(value => value == currentValue)) + { + item.SetOption(qualifier, "profile", currentValue); + } + else + { + item.SetOption(qualifier, "profile", string.Join(',', values)); + } + } break; } @@ -1883,29 +1993,5 @@ namespace MediaBrowser.Model.Dlna return true; } - - private bool IsVideoDirectPlaySupported(DirectPlayProfile profile, string container, MediaStream videoStream, MediaStream audioStream) - { - // Check container type - if (!profile.SupportsContainer(container)) - { - return false; - } - - // Check video codec - string videoCodec = videoStream?.Codec; - if (!profile.SupportsVideoCodec(videoCodec)) - { - return false; - } - - // Check audio codec - if (audioStream != null && !profile.SupportsAudioCodec(audioStream.Codec)) - { - return false; - } - - return true; - } } } diff --git a/MediaBrowser.Model/Dlna/VideoOptions.cs b/MediaBrowser.Model/Dlna/VideoOptions.cs index 4194f17c6e..0cb80af544 100644 --- a/MediaBrowser.Model/Dlna/VideoOptions.cs +++ b/MediaBrowser.Model/Dlna/VideoOptions.cs @@ -10,5 +10,7 @@ namespace MediaBrowser.Model.Dlna public int? AudioStreamIndex { get; set; } public int? SubtitleStreamIndex { get; set; } + + public bool AllowVideoStreamCopy { get; set; } } } diff --git a/MediaBrowser.Model/Dto/MediaSourceInfo.cs b/MediaBrowser.Model/Dto/MediaSourceInfo.cs index 2281e6ae5d..bb98488480 100644 --- a/MediaBrowser.Model/Dto/MediaSourceInfo.cs +++ b/MediaBrowser.Model/Dto/MediaSourceInfo.cs @@ -161,7 +161,7 @@ namespace MediaBrowser.Model.Dto public MediaStream GetDefaultAudioStream(int? defaultIndex) { - if (defaultIndex.HasValue) + if (defaultIndex.HasValue && defaultIndex != -1) { var val = defaultIndex.Value; diff --git a/MediaBrowser.Model/Properties/AssemblyInfo.cs b/MediaBrowser.Model/Properties/AssemblyInfo.cs index e50baf604e..6bf1eb0c03 100644 --- a/MediaBrowser.Model/Properties/AssemblyInfo.cs +++ b/MediaBrowser.Model/Properties/AssemblyInfo.cs @@ -16,6 +16,7 @@ using System.Runtime.InteropServices; [assembly: AssemblyCulture("")] [assembly: NeutralResourcesLanguage("en")] [assembly: InternalsVisibleTo("Jellyfin.Model.Tests")] +[assembly: InternalsVisibleTo("Jellyfin.Dlna.Tests")] // Setting ComVisible to false makes the types in this assembly not visible // to COM components. If you need to access a type in this assembly from diff --git a/MediaBrowser.Model/Session/TranscodeReason.cs b/MediaBrowser.Model/Session/TranscodeReason.cs index c3570840f9..c99136384e 100644 --- a/MediaBrowser.Model/Session/TranscodeReason.cs +++ b/MediaBrowser.Model/Session/TranscodeReason.cs @@ -31,21 +31,16 @@ namespace MediaBrowser.Model.Session AudioChannelsNotSupported = 1 << 14, AudioProfileNotSupported = 1 << 15, AudioSampleRateNotSupported = 1 << 16, - AudioBitDepthNotSupported = 1 << 20, + AudioBitDepthNotSupported = 1 << 17, // Bitrate Constraints - ContainerBitrateExceedsLimit = 1 << 17, - VideoBitrateNotSupported = 1 << 18, - AudioBitrateNotSupported = 1 << 19, + ContainerBitrateExceedsLimit = 1 << 18, + VideoBitrateNotSupported = 1 << 19, + AudioBitrateNotSupported = 1 << 20, // 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, + UnknownVideoStreamInfo = 1 << 21, + UnknownAudioStreamInfo = 1 << 22, + DirectPlayError = 1 << 23, } } diff --git a/MediaBrowser.Model/Session/TranscodeReasonExtensions.cs b/MediaBrowser.Model/Session/TranscodeReasonExtensions.cs index c7a5095f74..213257b209 100644 --- a/MediaBrowser.Model/Session/TranscodeReasonExtensions.cs +++ b/MediaBrowser.Model/Session/TranscodeReasonExtensions.cs @@ -1,22 +1,34 @@ -#pragma warning disable CS1591 - using System; using System.Linq; namespace MediaBrowser.Model.Session { + /// + /// Extension methods for serializing TranscodeReason. + /// public static class TranscodeReasonExtensions { - private static TranscodeReason[] values = Enum.GetValues(); + private static readonly TranscodeReason[] _values = Enum.GetValues(); - public static string Serialize(this MediaBrowser.Model.Session.TranscodeReason reasons, string sep = ",") + /// + /// Serializes a TranscodeReason into a delimiter-separated string. + /// + /// The enumeration. + /// The string separator to use. defualt ,. + /// string of transcode reasons delimited. + public static string Serialize(this TranscodeReason reasons, string sep = ",") { return string.Join(sep, reasons.ToArray()); } - public static TranscodeReason[] ToArray(this MediaBrowser.Model.Session.TranscodeReason reasons) + /// + /// Serializes a TranscodeReason into an array of individual TranscodeReason bits. + /// + /// The enumeration. + /// Array of TranscodeReason. + public static TranscodeReason[] ToArray(this TranscodeReason reasons) { - return values.Where(r => r != 0 && reasons.HasFlag(r)).ToArray(); + return _values.Where(r => r != 0 && reasons.HasFlag(r)).ToArray(); } } } diff --git a/MediaBrowser.Model/Session/TranscodingInfo.cs b/MediaBrowser.Model/Session/TranscodingInfo.cs index 78e5baad7e..8834856b04 100644 --- a/MediaBrowser.Model/Session/TranscodingInfo.cs +++ b/MediaBrowser.Model/Session/TranscodingInfo.cs @@ -31,7 +31,7 @@ namespace MediaBrowser.Model.Session public HardwareEncodingType? HardwareAccelerationType { get; set; } - public TranscodeReason[] TranscodeReasons { get => TranscodeReason.ToArray(); } + public TranscodeReason[] TranscodeReasons => TranscodeReason.ToArray(); [JsonIgnore] public TranscodeReason TranscodeReason { get; set; } diff --git a/tests/Jellyfin.Dlna.Tests/StreamBuilderTests.cs b/tests/Jellyfin.Dlna.Tests/StreamBuilderTests.cs index ccd95f7502..c3e3324bbd 100644 --- a/tests/Jellyfin.Dlna.Tests/StreamBuilderTests.cs +++ b/tests/Jellyfin.Dlna.Tests/StreamBuilderTests.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Specialized; using System.IO; using System.Linq; +using System.Runtime.Serialization; using System.Text.Json; using System.Threading.Tasks; using Jellyfin.Extensions.Json; @@ -19,97 +20,117 @@ 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, 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-h264-aac-vtt-2600k", PlayMethod.DirectPlay)] // #6450 + [InlineData("Chrome", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectPlay)] // #6450 + [InlineData("Chrome", "mp4-h264-ac3-aacDef-srt-2600k", PlayMethod.DirectPlay)] // #6450 + [InlineData("Chrome", "mp4-h264-ac3-aacExt-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioIsExternal)] // #6450 + [InlineData("Chrome", "mp4-h264-ac3-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450 [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 + [InlineData("Chrome", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported, "Transcode")] + [InlineData("Chrome", "mkv-vp9-aac-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450 + [InlineData("Chrome", "mkv-vp9-ac3-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450 + [InlineData("Chrome", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.DirectPlay, TranscodeReason.None, "Remux")] // #6450 // 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-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-h264-aac-vtt-2600k", PlayMethod.DirectPlay)] // #6450 + [InlineData("Firefox", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectPlay)] // #6450 + [InlineData("Firefox", "mp4-h264-ac3-aacDef-srt-2600k", PlayMethod.DirectPlay)] // #6450 + [InlineData("Firefox", "mp4-h264-ac3-aacExt-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioIsExternal)] // #6450 + [InlineData("Firefox", "mp4-h264-ac3-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450 [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 + [InlineData("Firefox", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported, "Transcode")] + [InlineData("Firefox", "mkv-vp9-aac-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450 + [InlineData("Firefox", "mkv-vp9-ac3-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450 + [InlineData("Firefox", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.DirectPlay, TranscodeReason.None, "Remux")] // #6450 // Safari - [InlineData("SafariNext", "mp4-h264-aac-vtt-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 + [InlineData("SafariNext", "mp4-h264-aac-vtt-2600k", PlayMethod.DirectPlay)] // #6450 + [InlineData("SafariNext", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectPlay)] // #6450 + [InlineData("SafariNext", "mp4-h264-ac3-aacDef-srt-2600k", PlayMethod.DirectPlay)] // #6450 + [InlineData("SafariNext", "mp4-h264-ac3-aacExt-srt-2600k", PlayMethod.DirectPlay)] // #6450 + [InlineData("SafariNext", "mp4-h264-ac3-srt-2600k", PlayMethod.DirectPlay)] // #6450 + [InlineData("SafariNext", "mp4-hevc-aac-srt-15200k", PlayMethod.DirectPlay)] // #6450 + [InlineData("SafariNext", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.DirectPlay)] // #6450 + [InlineData("SafariNext", "mp4-hevc-ac3-aacExt-srt-15200k", PlayMethod.DirectPlay)] // #6450 // AndroidPixel - [InlineData("AndroidPixel", "mp4-h264-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-h264-aac-srt-2600k", PlayMethod.DirectPlay)] // #6450 + [InlineData("AndroidPixel", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectPlay)] // #6450 + [InlineData("AndroidPixel", "mp4-h264-ac3-aacDef-srt-2600k", PlayMethod.DirectPlay)] // #6450 + [InlineData("AndroidPixel", "mp4-h264-ac3-srt-2600k", PlayMethod.DirectPlay)] // #6450 [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-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, TranscodeReason.AudioCodecNotSupported, "Transcode")] // #6450 should be DirectPlay + [InlineData("Yatse", "mp4-h264-aac-srt-2600k", PlayMethod.DirectPlay, TranscodeReason.None, "Remux")] // #6450 + [InlineData("Yatse", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectPlay, TranscodeReason.None, "Remux")] // #6450 + [InlineData("Yatse", "mp4-h264-ac3-aacDef-srt-2600k", PlayMethod.DirectPlay, TranscodeReason.None, "Remux")] // #6450 + [InlineData("Yatse", "mp4-h264-ac3-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] + [InlineData("Yatse", "mp4-hevc-aac-srt-15200k", PlayMethod.DirectPlay, TranscodeReason.None, "Remux")] // #6450 + [InlineData("Yatse", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.DirectPlay, TranscodeReason.None, "Remux")] // #6450 // 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-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, TranscodeReason.AudioCodecNotSupported, "Transcode")] // #6450 should be DirectPlay - [InlineData("RokuSSPlus", "mp4-hevc-ac3-srt-15200k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported, "Transcode")] // #6450 should be DirectStream + [InlineData("RokuSSPlus", "mp4-h264-aac-srt-2600k", PlayMethod.DirectPlay, TranscodeReason.None, "Remux")] // #6450 + [InlineData("RokuSSPlus", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectPlay, TranscodeReason.None, "Remux")] // #6450 should be DirectPlay + [InlineData("RokuSSPlus", "mp4-h264-ac3-aacDef-srt-2600k", PlayMethod.DirectPlay, TranscodeReason.None, "Remux")] // #6450 + [InlineData("RokuSSPlus", "mp4-h264-ac3-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450 + [InlineData("RokuSSPlus", "mp4-hevc-aac-srt-15200k", PlayMethod.DirectPlay, TranscodeReason.None, "Remux")] // #6450 + [InlineData("RokuSSPlus", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.DirectPlay, TranscodeReason.None, "Remux")] // #6450 + [InlineData("RokuSSPlus", "mp4-hevc-ac3-srt-15200k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450 // 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 + [InlineData("JellyfinMediaPlayer", "mp4-h264-aac-vtt-2600k", PlayMethod.DirectPlay, TranscodeReason.None, "Remux")] // #6450 + [InlineData("JellyfinMediaPlayer", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectPlay, TranscodeReason.None, "Remux")] // #6450 + [InlineData("JellyfinMediaPlayer", "mp4-h264-ac3-srt-2600k", PlayMethod.DirectPlay, TranscodeReason.None, "Remux")] // #6450 + [InlineData("JellyfinMediaPlayer", "mp4-hevc-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.ContainerBitrateExceedsLimit, "Transcode")] + [InlineData("JellyfinMediaPlayer", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.ContainerBitrateExceedsLimit, "Transcode")] + [InlineData("JellyfinMediaPlayer", "mkv-vp9-aac-srt-2600k", PlayMethod.DirectPlay)] // #6450 + [InlineData("JellyfinMediaPlayer", "mkv-vp9-ac3-srt-2600k", PlayMethod.DirectPlay)] // #6450 + [InlineData("JellyfinMediaPlayer", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.DirectPlay)] // #6450 + // Chrome-NoHLS + [InlineData("Chrome-NoHLS", "mp4-h264-aac-vtt-2600k", PlayMethod.DirectPlay)] // #6450 + [InlineData("Chrome-NoHLS", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectPlay)] // #6450 + [InlineData("Chrome-NoHLS", "mp4-h264-ac3-aacDef-srt-2600k", PlayMethod.DirectPlay)] // #6450 + [InlineData("Chrome-NoHLS", "mp4-h264-ac3-aacExt-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioIsExternal)] // #6450 + [InlineData("Chrome-NoHLS", "mp4-h264-ac3-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450 + [InlineData("Chrome-NoHLS", "mp4-hevc-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported, "Transcode", "http")] + [InlineData("Chrome-NoHLS", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported, "Transcode", "http")] + [InlineData("Chrome-NoHLS", "mkv-vp9-aac-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450 + [InlineData("Chrome-NoHLS", "mkv-vp9-ac3-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450 + [InlineData("Chrome-NoHLS", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.DirectPlay, TranscodeReason.None, "Remux")] // #6450 // TranscodeMedia - [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 + [InlineData("TranscodeMedia", "mp4-h264-aac-vtt-2600k", PlayMethod.Transcode, TranscodeReason.DirectPlayError, "Transcode")] + [InlineData("TranscodeMedia", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.Transcode, TranscodeReason.DirectPlayError, "Transcode")] + [InlineData("TranscodeMedia", "mp4-h264-ac3-srt-2600k", PlayMethod.Transcode, TranscodeReason.DirectPlayError, "Transcode")] + [InlineData("TranscodeMedia", "mp4-hevc-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.DirectPlayError, "Transcode")] + [InlineData("TranscodeMedia", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.DirectPlayError, "Transcode")] + [InlineData("TranscodeMedia", "mkv-vp9-aac-srt-2600k", PlayMethod.Transcode, TranscodeReason.DirectPlayError, "Transcode")] + [InlineData("TranscodeMedia", "mkv-vp9-ac3-srt-2600k", PlayMethod.Transcode, TranscodeReason.DirectPlayError, "Transcode")] + [InlineData("TranscodeMedia", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.Transcode, TranscodeReason.DirectPlayError, "Transcode")] // 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 - [InlineData("DirectMedia", "mp4-h264-ac3-srt-2600k", PlayMethod.DirectStream)] // #6450 should be DirectPlay - [InlineData("DirectMedia", "mp4-hevc-aac-srt-15200k", PlayMethod.DirectStream)] // #6450 should be DirectPlay - [InlineData("DirectMedia", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.DirectStream)] // #6450 should be DirectPlay - [InlineData("DirectMedia", "mkv-vp9-aac-srt-2600k", PlayMethod.DirectStream)] // #6450 should be DirectPlay - [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 + [InlineData("DirectMedia", "mp4-h264-aac-vtt-2600k", PlayMethod.DirectPlay, TranscodeReason.None, "Remux")] + [InlineData("DirectMedia", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectPlay, TranscodeReason.None, "Remux")] + [InlineData("DirectMedia", "mp4-h264-ac3-aacDef-srt-2600k", PlayMethod.DirectPlay, TranscodeReason.None, "Remux")] + [InlineData("DirectMedia", "mp4-h264-ac3-srt-2600k", PlayMethod.DirectPlay, TranscodeReason.None, "Remux")] + [InlineData("DirectMedia", "mp4-hevc-aac-srt-15200k", PlayMethod.DirectPlay, TranscodeReason.None, "Remux")] + [InlineData("DirectMedia", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.DirectPlay, TranscodeReason.None, "Remux")] + [InlineData("DirectMedia", "mkv-vp9-aac-srt-2600k", PlayMethod.DirectPlay)] + [InlineData("DirectMedia", "mkv-vp9-ac3-srt-2600k", PlayMethod.DirectPlay)] + [InlineData("DirectMedia", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.DirectPlay)] // LowBandwidth - [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 + [InlineData("LowBandwidth", "mp4-h264-aac-vtt-2600k", PlayMethod.Transcode, TranscodeReason.ContainerBitrateExceedsLimit, "Transcode")] + [InlineData("LowBandwidth", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.Transcode, TranscodeReason.ContainerBitrateExceedsLimit, "Transcode")] + [InlineData("LowBandwidth", "mp4-h264-ac3-srt-2600k", PlayMethod.Transcode, TranscodeReason.ContainerBitrateExceedsLimit, "Transcode")] + [InlineData("LowBandwidth", "mp4-hevc-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.ContainerBitrateExceedsLimit, "Transcode")] + [InlineData("LowBandwidth", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.ContainerBitrateExceedsLimit, "Transcode")] + [InlineData("LowBandwidth", "mkv-vp9-aac-srt-2600k", PlayMethod.Transcode, TranscodeReason.ContainerBitrateExceedsLimit, "Transcode")] + [InlineData("LowBandwidth", "mkv-vp9-ac3-srt-2600k", PlayMethod.Transcode, TranscodeReason.ContainerBitrateExceedsLimit, "Transcode")] + [InlineData("LowBandwidth", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.Transcode, TranscodeReason.ContainerBitrateExceedsLimit, "Transcode")] // Null - [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 + [InlineData("Null", "mp4-h264-aac-vtt-2600k", null, TranscodeReason.ContainerBitrateExceedsLimit)] + [InlineData("Null", "mp4-h264-ac3-aac-srt-2600k", null, TranscodeReason.ContainerBitrateExceedsLimit)] + [InlineData("Null", "mp4-h264-ac3-srt-2600k", null, TranscodeReason.ContainerBitrateExceedsLimit)] + [InlineData("Null", "mp4-hevc-aac-srt-15200k", null, TranscodeReason.ContainerBitrateExceedsLimit)] + [InlineData("Null", "mp4-hevc-ac3-aac-srt-15200k", null, TranscodeReason.ContainerBitrateExceedsLimit)] + [InlineData("Null", "mkv-vp9-aac-srt-2600k", null, TranscodeReason.ContainerBitrateExceedsLimit)] + [InlineData("Null", "mkv-vp9-ac3-srt-2600k", null, TranscodeReason.ContainerBitrateExceedsLimit)] + [InlineData("Null", "mkv-vp9-vorbis-vtt-2600k", null, TranscodeReason.ContainerBitrateExceedsLimit)] + + // [InlineData("Chrome", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported | TranscodeReason.AudioCodecNotSupported, "Transcode")] public async Task BuildVideoItemSimple(string deviceName, string mediaSource, PlayMethod? playMethod, TranscodeReason why = TranscodeReason.None, string transcodeMode = "DirectStream", string transcodeProtocol = "") { var options = await GetVideoOptions(deviceName, mediaSource); @@ -118,88 +139,103 @@ 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, TranscodeReason.AudioCodecNotSupported)] // #6450 should be DirectPlay - [InlineData("Chrome", "mp4-h264-ac3-srt-2600k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported)] // #6450 should be DirectStream + [InlineData("Chrome", "mp4-h264-aac-vtt-2600k", PlayMethod.DirectPlay)] // #6450 + [InlineData("Chrome", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450 + [InlineData("Chrome", "mp4-h264-ac3-aacDef-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450 + [InlineData("Chrome", "mp4-h264-ac3-aacExt-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450 + [InlineData("Chrome", "mp4-h264-ac3-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450 [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 + [InlineData("Chrome", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported, "Transcode")] + [InlineData("Chrome", "mkv-vp9-aac-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450 + [InlineData("Chrome", "mkv-vp9-ac3-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450 + [InlineData("Chrome", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.DirectPlay, TranscodeReason.None, "Remux")] // #6450 // 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-h264-aac-vtt-2600k", PlayMethod.DirectPlay)] // #6450 + [InlineData("Firefox", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450 + [InlineData("Firefox", "mp4-h264-ac3-aacDef-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450 + [InlineData("Firefox", "mp4-h264-ac3-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450 [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 + [InlineData("Firefox", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported, "Transcode")] + [InlineData("Firefox", "mkv-vp9-aac-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450 + [InlineData("Firefox", "mkv-vp9-ac3-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450 + [InlineData("Firefox", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.DirectPlay, TranscodeReason.None, "Remux")] // #6450 // 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 + [InlineData("SafariNext", "mp4-h264-aac-vtt-2600k", PlayMethod.DirectPlay)] // #6450 + [InlineData("SafariNext", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectPlay)] // #6450 + [InlineData("SafariNext", "mp4-h264-ac3-aacDef-srt-2600k", PlayMethod.DirectPlay)] // #6450 + [InlineData("SafariNext", "mp4-h264-ac3-aacExt-srt-2600k", PlayMethod.DirectPlay)] // #6450 + [InlineData("SafariNext", "mp4-h264-ac3-srt-2600k", PlayMethod.DirectPlay)] // #6450 + [InlineData("SafariNext", "mp4-hevc-aac-srt-15200k", PlayMethod.DirectPlay)] // #6450 + [InlineData("SafariNext", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.DirectPlay)] // #6450 + [InlineData("SafariNext", "mp4-hevc-ac3-aacExt-srt-15200k", PlayMethod.DirectPlay)] // #6450 // 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-h264-aac-srt-2600k", PlayMethod.DirectPlay)] // #6450 + [InlineData("AndroidPixel", "mp4-h264-ac3-aacDef-srt-2600k", PlayMethod.DirectPlay)] // #6450 + [InlineData("AndroidPixel", "mp4-h264-ac3-srt-2600k", PlayMethod.DirectPlay)] // #6450 [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 + [InlineData("Yatse", "mp4-h264-aac-srt-2600k", PlayMethod.DirectPlay, TranscodeReason.None, "Remux")] // #6450 + [InlineData("Yatse", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450 + [InlineData("Yatse", "mp4-h264-ac3-aacDef-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450 + [InlineData("Yatse", "mp4-h264-ac3-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] + [InlineData("Yatse", "mp4-hevc-aac-srt-15200k", PlayMethod.DirectPlay, TranscodeReason.None, "Remux")] // #6450 + [InlineData("Yatse", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450 // 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 + [InlineData("RokuSSPlus", "mp4-h264-aac-srt-2600k", PlayMethod.DirectPlay, TranscodeReason.None, "Remux")] // #6450 + [InlineData("RokuSSPlus", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450 should be DirectPlay + [InlineData("RokuSSPlus", "mp4-h264-ac3-aacDef-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450 + [InlineData("RokuSSPlus", "mp4-h264-ac3-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450 + [InlineData("RokuSSPlus", "mp4-hevc-aac-srt-15200k", PlayMethod.DirectPlay, TranscodeReason.None, "Remux")] // #6450 + [InlineData("RokuSSPlus", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450 + [InlineData("RokuSSPlus", "mp4-hevc-ac3-srt-15200k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450 // 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 = "") + [InlineData("JellyfinMediaPlayer", "mp4-h264-aac-vtt-2600k", PlayMethod.DirectPlay, TranscodeReason.None, "Remux")] // #6450 + [InlineData("JellyfinMediaPlayer", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectPlay, TranscodeReason.None, "Remux")] // #6450 + [InlineData("JellyfinMediaPlayer", "mp4-h264-ac3-srt-2600k", PlayMethod.DirectPlay, TranscodeReason.None, "Remux")] // #6450 + [InlineData("JellyfinMediaPlayer", "mp4-hevc-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.ContainerBitrateExceedsLimit, "Transcode")] // #6450 + [InlineData("JellyfinMediaPlayer", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.ContainerBitrateExceedsLimit, "Transcode")] // #6450 + [InlineData("JellyfinMediaPlayer", "mkv-vp9-aac-srt-2600k", PlayMethod.DirectPlay)] // #6450 + [InlineData("JellyfinMediaPlayer", "mkv-vp9-ac3-srt-2600k", PlayMethod.DirectPlay)] // #6450 + [InlineData("JellyfinMediaPlayer", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.DirectPlay)] // #6450 + 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); + options.SubtitleStreamIndex = options.MediaSources[0].MediaStreams.Count - 1; + + var streamInfo = BuildVideoItemSimpleTest(options, playMethod, why, transcodeMode, transcodeProtocol); + Assert.Equal(streamInfo?.AudioStreamIndex, options.AudioStreamIndex); + Assert.Equal(streamInfo?.SubtitleStreamIndex, options.SubtitleStreamIndex); } [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? + [InlineData("Chrome", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectPlay)] // #6450 + [InlineData("Chrome", "mp4-h264-ac3-aacExt-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioIsExternal)] // #6450 + [InlineData("Chrome", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported, "Transcode")] // 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? + [InlineData("Firefox", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectPlay)] // #6450 + [InlineData("Firefox", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported, "Transcode")] // 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 + [InlineData("Yatse", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectPlay, TranscodeReason.None, "Remux")] // #6450 + [InlineData("Yatse", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.DirectPlay, TranscodeReason.None, "Remux")] // #6450 // 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 = "") + [InlineData("RokuSSPlus", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectPlay, TranscodeReason.None, "Remux")] // #6450 + [InlineData("RokuSSPlus", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.DirectPlay, TranscodeReason.None, "Remux")] // #6450 + 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(); + var streamCount = options.MediaSources[0].MediaStreams.Count; options.AudioStreamIndex = streamCount - 2; options.SubtitleStreamIndex = streamCount - 1; - BuildVideoItemSimpleTest(options, playMethod, why, transcodeMode, transcodeProtocol); + + var streamInfo = BuildVideoItemSimpleTest(options, playMethod, why, transcodeMode, transcodeProtocol); + Assert.Equal(streamInfo?.AudioStreamIndex, options.AudioStreamIndex); + Assert.Equal(streamInfo?.SubtitleStreamIndex, options.SubtitleStreamIndex); } - private void BuildVideoItemSimpleTest(VideoOptions options, PlayMethod? playMethod, TranscodeReason why, string transcodeMode, string transcodeProtocol) + private StreamInfo? BuildVideoItemSimpleTest(VideoOptions options, PlayMethod? playMethod, TranscodeReason why, string transcodeMode, string transcodeProtocol) { if (string.IsNullOrEmpty(transcodeProtocol)) { @@ -235,7 +271,8 @@ namespace Jellyfin.MediaBrowser.Model.Tests { // check expected container var containers = ContainerProfile.SplitValue(mediaSource.Container); - Assert.Contains(uri.Extension, containers); + // TODO: test transcode too + // Assert.Contains(uri.Extension, containers); // check expected video codec (1) Assert.Contains(targetVideoStream.Codec, val.TargetVideoCodec); @@ -243,15 +280,19 @@ namespace Jellyfin.MediaBrowser.Model.Tests // check expected audio codecs (1) Assert.Contains(targetAudioStream.Codec, val.TargetAudioCodec); - Assert.Single(val.AudioCodecs); + Assert.Single(val.TargetAudioCodec); + // Assert.Single(val.AudioCodecs); - // TODO: validate transcoding options as well + if (transcodeMode == "DirectStream") + { + Assert.Equal(val.Container, uri.Extension); + } } else if (playMethod == PlayMethod.DirectStream || playMethod == PlayMethod.Transcode) { Assert.NotNull(val.Container); - // Assert.NotEmpty(val.VideoCodecs); - // Assert.NotEmpty(val.AudioCodecs); + Assert.NotEmpty(val.VideoCodecs); + Assert.NotEmpty(val.AudioCodecs); // check expected container (todo: this could be a test param) if (transcodeProtocol == "http") @@ -259,7 +300,7 @@ namespace Jellyfin.MediaBrowser.Model.Tests // Assert.Equal("webm", val.Container); Assert.Equal(val.Container, uri.Extension); Assert.Equal("stream", uri.Filename); - // Assert.Equal("http", val.SubProtocol); + Assert.Equal("http", val.SubProtocol); } else { @@ -272,12 +313,11 @@ namespace Jellyfin.MediaBrowser.Model.Tests // Full transcode if (transcodeMode == "Transcode") { - // TODO: what else to validate here - if ((val.TranscodeReasons & TranscodeReason.ContainerReasons) == TranscodeReason.None) + if ((val.TranscodeReasons & (StreamBuilder.ContainerReasons | TranscodeReason.DirectPlayError)) == TranscodeReason.None) { - // Assert.All( - // videoStreams, - // stream => Assert.DoesNotContain(stream.Codec, val.VideoCodecs)); + Assert.All( + videoStreams, + stream => Assert.DoesNotContain(stream.Codec, val.VideoCodecs)); } // todo: fill out tests here @@ -295,7 +335,7 @@ namespace Jellyfin.MediaBrowser.Model.Tests if (!targetAudioStream.IsExternal) { // check expected audio codecs (1) - // Assert.DoesNotContain(targetAudioStream.Codec, val.AudioCodecs); + Assert.DoesNotContain(targetAudioStream.Codec, val.AudioCodecs); } } else if (transcodeMode == "Remux") @@ -309,10 +349,10 @@ namespace Jellyfin.MediaBrowser.Model.Tests var videoStream = targetVideoStream; Assert.False(val.EstimateContentLength); Assert.Equal(TranscodeSeekInfo.Auto, val.TranscodeSeekInfo); - // Assert.Contains(videoStream.Profile?.ToLowerInvariant() ?? string.Empty, val.TargetVideoProfile?.Split(",").Select(s => s.ToLowerInvariant()) ?? new string[0]); - // Assert.Equal(videoStream.Level, val.TargetVideoLevel); - // Assert.Equal(videoStream.BitDepth, val.TargetVideoBitDepth); - // Assert.InRange(val.VideoBitrate.GetValueOrDefault(), videoStream.BitRate.GetValueOrDefault(), int.MaxValue); + Assert.Contains(videoStream.Profile?.ToLowerInvariant() ?? string.Empty, val.TargetVideoProfile?.Split(",").Select(s => s.ToLowerInvariant()) ?? Array.Empty()); + Assert.Equal(videoStream.Level, val.TargetVideoLevel); + Assert.Equal(videoStream.BitDepth, val.TargetVideoBitDepth); + Assert.InRange(val.VideoBitrate.GetValueOrDefault(), videoStream.BitRate.GetValueOrDefault(), int.MaxValue); // audio codec not supported if ((why & TranscodeReason.AudioCodecNotSupported) != TranscodeReason.None) @@ -335,24 +375,23 @@ namespace Jellyfin.MediaBrowser.Model.Tests { if (!stream.IsExternal) { - // Assert.DoesNotContain(stream.Codec, val.AudioCodecs); + Assert.DoesNotContain(stream.Codec, val.AudioCodecs); } }); } } } } - - if (playMethod == null) + else if (playMethod == null) { - // what should the actual result be here? Assert.Null(val.SubProtocol); - Assert.EndsWith("/stream", uri.Path, StringComparison.InvariantCulture); + Assert.Equal("stream", uri.Filename); Assert.False(val.EstimateContentLength); Assert.Equal(TranscodeSeekInfo.Auto, val.TranscodeSeekInfo); - // Assert.True(val.CopyTimestamps); } + + return val; } private static async ValueTask TestData(string name) @@ -366,7 +405,7 @@ namespace Jellyfin.MediaBrowser.Model.Tests return value; } - throw new Exception("Invalid test data: " + name); + throw new SerializationException("Invalid test data: " + name); } } @@ -394,6 +433,8 @@ namespace Jellyfin.MediaBrowser.Model.Tests MediaSources = mediaSources, DeviceId = "test-deviceId", Profile = dp, + AllowAudioStreamCopy = true, + AllowVideoStreamCopy = true, }; }