diff --git a/Jellyfin.Api/Controllers/UniversalAudioController.cs b/Jellyfin.Api/Controllers/UniversalAudioController.cs index bc9527a0bc..962f637d4e 100644 --- a/Jellyfin.Api/Controllers/UniversalAudioController.cs +++ b/Jellyfin.Api/Controllers/UniversalAudioController.cs @@ -16,6 +16,7 @@ using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Controller.Net; using MediaBrowser.Model.Dlna; using MediaBrowser.Model.MediaInfo; +using MediaBrowser.Model.Session; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; @@ -223,7 +224,7 @@ namespace Jellyfin.Api.Controllers DeInterlace = false, RequireNonAnamorphic = false, EnableMpegtsM2TsMode = false, - TranscodeReasons = mediaSource.TranscodeReasons == null ? null : string.Join(',', mediaSource.TranscodeReasons.Select(i => i.ToString()).ToArray()), + TranscodeReasons = mediaSource.TranscodeReasons == MediaBrowser.Model.Session.TranscodeReason.None ? null : mediaSource.TranscodeReasons.Serialize(), Context = EncodingContext.Static, StreamOptions = new Dictionary(), EnableAdaptiveBitrateStreaming = true @@ -254,7 +255,7 @@ namespace Jellyfin.Api.Controllers CopyTimestamps = true, StartTimeTicks = startTimeTicks, SubtitleMethod = SubtitleDeliveryMethod.Embed, - TranscodeReasons = mediaSource.TranscodeReasons == null ? null : string.Join(',', mediaSource.TranscodeReasons.Select(i => i.ToString()).ToArray()), + TranscodeReasons = mediaSource.TranscodeReasons == MediaBrowser.Model.Session.TranscodeReason.None ? null : mediaSource.TranscodeReasons.Serialize(), Context = EncodingContext.Static }; diff --git a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs index c8762b7c54..49a3948688 100644 --- a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs +++ b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs @@ -479,7 +479,7 @@ namespace Jellyfin.Api.Helpers IsAudioDirect = EncodingHelper.IsCopyCodec(state.OutputAudioCodec), IsVideoDirect = EncodingHelper.IsCopyCodec(state.OutputVideoCodec), HardwareAccelerationType = hardwareAccelerationType, - TranscodeReasons = state.TranscodeReasons + TranscodeReason = state.TranscodeReason }); } } diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs b/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs index c4affa5678..0f5fdcc3c6 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs @@ -6,6 +6,7 @@ using System; using System.Collections.Generic; using System.Globalization; using System.Linq; +using System.Text.Json.Serialization; using Jellyfin.Data.Entities; using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Drawing; @@ -23,7 +24,7 @@ namespace MediaBrowser.Controller.MediaEncoding public int? OutputAudioBitrate; public int? OutputAudioChannels; - private TranscodeReason[] _transcodeReasons = null; + private TranscodeReason? _transcodeReasons = null; public EncodingJobInfo(TranscodingJobType jobType) { @@ -34,25 +35,27 @@ namespace MediaBrowser.Controller.MediaEncoding SupportedSubtitleCodecs = Array.Empty(); } - public TranscodeReason[] TranscodeReasons + public TranscodeReason[] TranscodeReasons { get => TranscodeReason.ToArray(); } + + [JsonIgnore] + public TranscodeReason TranscodeReason { get { - if (_transcodeReasons == null) + if (!_transcodeReasons.HasValue) { if (BaseRequest.TranscodeReasons == null) { - return Array.Empty(); + _transcodeReasons = TranscodeReason.None; + return TranscodeReason.None; } - _transcodeReasons = BaseRequest.TranscodeReasons - .Split(',') - .Where(i => !string.IsNullOrEmpty(i)) - .Select(v => (TranscodeReason)Enum.Parse(typeof(TranscodeReason), v, true)) - .ToArray(); + TranscodeReason reason = TranscodeReason.None; + Enum.TryParse(BaseRequest.TranscodeReasons, out reason); + _transcodeReasons = reason; } - return _transcodeReasons; + return _transcodeReasons.Value; } } diff --git a/MediaBrowser.Model/Dlna/StreamBuilder.cs b/MediaBrowser.Model/Dlna/StreamBuilder.cs index d2ca211505..7654337e70 100644 --- a/MediaBrowser.Model/Dlna/StreamBuilder.cs +++ b/MediaBrowser.Model/Dlna/StreamBuilder.cs @@ -143,7 +143,7 @@ namespace MediaBrowser.Model.Dlna }).ThenBy(streams.IndexOf); } - private static TranscodeReason? GetTranscodeReasonForFailedCondition(ProfileCondition condition) + private static TranscodeReason GetTranscodeReasonForFailedCondition(ProfileCondition condition) { switch (condition.Property) { @@ -161,7 +161,7 @@ namespace MediaBrowser.Model.Dlna case ProfileConditionValue.Has64BitOffsets: // TODO - return null; + return TranscodeReason.None; case ProfileConditionValue.Height: return TranscodeReason.VideoResolutionNotSupported; @@ -171,7 +171,7 @@ namespace MediaBrowser.Model.Dlna case ProfileConditionValue.IsAvc: // TODO - return null; + return TranscodeReason.None; case ProfileConditionValue.IsInterlaced: return TranscodeReason.InterlacedVideoNotSupported; @@ -181,15 +181,15 @@ namespace MediaBrowser.Model.Dlna case ProfileConditionValue.NumAudioStreams: // TODO - return null; + return TranscodeReason.None; case ProfileConditionValue.NumVideoStreams: // TODO - return null; + return TranscodeReason.None; case ProfileConditionValue.PacketLength: // TODO - return null; + return TranscodeReason.None; case ProfileConditionValue.RefFrames: return TranscodeReason.RefFramesNotSupported; @@ -217,13 +217,13 @@ namespace MediaBrowser.Model.Dlna case ProfileConditionValue.VideoTimestamp: // TODO - return null; + return TranscodeReason.None; case ProfileConditionValue.Width: return TranscodeReason.VideoResolutionNotSupported; default: - return null; + return TranscodeReason.None; } } @@ -290,7 +290,7 @@ namespace MediaBrowser.Model.Dlna var directPlayInfo = GetAudioDirectPlayMethods(item, audioStream, options); var directPlayMethods = directPlayInfo.PlayMethods; - var transcodeReasons = directPlayInfo.TranscodeReasons.ToList(); + var transcodeReasons = directPlayInfo.TranscodeReasons; int? inputAudioChannels = audioStream?.Channels; int? inputAudioBitrate = audioStream?.BitDepth; @@ -331,11 +331,7 @@ namespace MediaBrowser.Model.Dlna if (!ConditionProcessor.IsAudioConditionSatisfied(c, inputAudioChannels, inputAudioBitrate, inputAudioSampleRate, inputAudioBitDepth)) { LogConditionFailure(options.Profile, "AudioCodecProfile", c, item); - var transcodeReason = GetTranscodeReasonForFailedCondition(c); - if (transcodeReason.HasValue) - { - transcodeReasons.Add(transcodeReason.Value); - } + transcodeReasons |= GetTranscodeReasonForFailedCondition(c); all = false; break; @@ -434,7 +430,7 @@ namespace MediaBrowser.Model.Dlna playlistItem.AudioBitrate = longBitrate > int.MaxValue ? int.MaxValue : Convert.ToInt32(longBitrate); } - playlistItem.TranscodeReasons = transcodeReasons.ToArray(); + playlistItem.TranscodeReasons = transcodeReasons; return playlistItem; } @@ -448,7 +444,7 @@ namespace MediaBrowser.Model.Dlna return options.GetMaxBitrate(isAudio); } - private (IEnumerable PlayMethods, IEnumerable TranscodeReasons) GetAudioDirectPlayMethods(MediaSourceInfo item, MediaStream audioStream, AudioOptions options) + private (IEnumerable PlayMethods, TranscodeReason TranscodeReasons) GetAudioDirectPlayMethods(MediaSourceInfo item, MediaStream audioStream, AudioOptions options) { DirectPlayProfile directPlayProfile = options.Profile.DirectPlayProfiles .FirstOrDefault(x => x.Type == DlnaProfileType.Audio && IsAudioDirectPlaySupported(x, item, audioStream)); @@ -465,12 +461,12 @@ namespace MediaBrowser.Model.Dlna } var playMethods = new List(); - var transcodeReasons = new List(); + var transcodeReasons = TranscodeReason.None; // While options takes the network and other factors into account. Only applies to direct stream if (item.SupportsDirectStream) { - if (IsAudioEligibleForDirectPlay(item, options.GetMaxBitrate(true) ?? 0, PlayMethod.DirectStream)) + if (IsItemBitrateEligibleForDirectPlay(item, options.GetMaxBitrate(true) ?? 0, PlayMethod.DirectStream)) { if (options.EnableDirectStream) { @@ -479,7 +475,7 @@ namespace MediaBrowser.Model.Dlna } else { - transcodeReasons.Add(TranscodeReason.ContainerBitrateExceedsLimit); + transcodeReasons |= TranscodeReason.ContainerBitrateExceedsLimit; } } @@ -487,7 +483,7 @@ namespace MediaBrowser.Model.Dlna // If device requirements are satisfied then allow both direct stream and direct play if (item.SupportsDirectPlay) { - if (IsAudioEligibleForDirectPlay(item, GetBitrateForDirectPlayCheck(item, options, true) ?? 0, PlayMethod.DirectPlay)) + if (IsItemBitrateEligibleForDirectPlay(item, GetBitrateForDirectPlayCheck(item, options, true) ?? 0, PlayMethod.DirectPlay)) { if (options.EnableDirectPlay) { @@ -496,29 +492,26 @@ namespace MediaBrowser.Model.Dlna } else { - transcodeReasons.Add(TranscodeReason.ContainerBitrateExceedsLimit); + transcodeReasons |= TranscodeReason.ContainerBitrateExceedsLimit; } } if (playMethods.Count > 0) { - transcodeReasons.Clear(); - } - else - { - transcodeReasons = transcodeReasons.Distinct().ToList(); + transcodeReasons = TranscodeReason.None; } return (playMethods, transcodeReasons); } - private static List GetTranscodeReasonsFromDirectPlayProfile(MediaSourceInfo item, MediaStream videoStream, MediaStream audioStream, IEnumerable directPlayProfiles) + private static TranscodeReason GetTranscodeReasonsFromDirectPlayProfile(MediaSourceInfo item, MediaStream videoStream, MediaStream audioStream, IEnumerable directPlayProfiles) { var mediaType = videoStream == null ? DlnaProfileType.Audio : DlnaProfileType.Video; var containerSupported = false; var audioSupported = false; var videoSupported = false; + var reasons = TranscodeReason.None; foreach (var profile in directPlayProfiles) { @@ -541,20 +534,20 @@ namespace MediaBrowser.Model.Dlna var list = new List(); if (!containerSupported) { - list.Add(TranscodeReason.ContainerNotSupported); + reasons |= TranscodeReason.ContainerNotSupported; } if (videoStream != null && !videoSupported) { - list.Add(TranscodeReason.VideoCodecNotSupported); + reasons |= TranscodeReason.VideoCodecNotSupported; } if (audioStream != null && !audioSupported) { - list.Add(TranscodeReason.AudioCodecNotSupported); + reasons |= TranscodeReason.AudioCodecNotSupported; } - return list; + return reasons; } private static int? GetDefaultSubtitleStreamIndex(MediaSourceInfo item, SubtitleProfile[] subtitleProfiles) @@ -679,8 +672,9 @@ namespace MediaBrowser.Model.Dlna // TODO: This doesn't account for situations where the device is able to handle the media's bitrate, but the connection isn't fast enough var directPlayEligibilityResult = IsEligibleForDirectPlay(item, GetBitrateForDirectPlayCheck(item, options, true) ?? 0, subtitleStream, audioStream, options, PlayMethod.DirectPlay); var directStreamEligibilityResult = IsEligibleForDirectPlay(item, options.GetMaxBitrate(false) ?? 0, subtitleStream, audioStream, options, PlayMethod.DirectStream); - bool isEligibleForDirectPlay = options.EnableDirectPlay && (options.ForceDirectPlay || directPlayEligibilityResult.DirectPlay); - bool isEligibleForDirectStream = options.EnableDirectStream && (options.ForceDirectStream || directStreamEligibilityResult.DirectPlay); + bool isEligibleForDirectPlay = options.EnableDirectPlay && (options.ForceDirectPlay || directPlayEligibilityResult == TranscodeReason.None); + bool isEligibleForDirectStream = options.EnableDirectStream && (options.ForceDirectStream || directPlayEligibilityResult == TranscodeReason.None); + var transcodeReasons = directPlayEligibilityResult | directStreamEligibilityResult; _logger.LogDebug( "Profile: {0}, Path: {1}, isEligibleForDirectPlay: {2}, isEligibleForDirectStream: {3}", @@ -689,8 +683,6 @@ namespace MediaBrowser.Model.Dlna isEligibleForDirectPlay, isEligibleForDirectStream); - var transcodeReasons = new List(); - if (isEligibleForDirectPlay || isEligibleForDirectStream) { // See if it can be direct played @@ -713,17 +705,13 @@ namespace MediaBrowser.Model.Dlna return playlistItem; } - transcodeReasons.AddRange(directPlayInfo.TranscodeReasons); - } - - if (directPlayEligibilityResult.Reason.HasValue) - { - transcodeReasons.Add(directPlayEligibilityResult.Reason.Value); + transcodeReasons |= directPlayInfo.TranscodeReasons; } - if (directStreamEligibilityResult.Reason.HasValue) + if (playlistItem.PlayMethod != PlayMethod.Transcode) { - transcodeReasons.Add(directStreamEligibilityResult.Reason.Value); + playlistItem.TranscodeReasons = transcodeReasons; + return playlistItem; } // Can't direct play, find the transcoding profile @@ -869,7 +857,7 @@ namespace MediaBrowser.Model.Dlna } } - playlistItem.TranscodeReasons = transcodeReasons.ToArray(); + playlistItem.TranscodeReasons = transcodeReasons; return playlistItem; } @@ -1000,7 +988,7 @@ namespace MediaBrowser.Model.Dlna return 7168000; } - private (PlayMethod? PlayMethod, List TranscodeReasons) GetVideoDirectPlayProfile( + private (PlayMethod? PlayMethod, TranscodeReason TranscodeReasons) GetVideoDirectPlayProfile( VideoOptions options, MediaSourceInfo mediaSource, MediaStream videoStream, @@ -1009,12 +997,12 @@ namespace MediaBrowser.Model.Dlna { if (options.ForceDirectPlay) { - return (PlayMethod.DirectPlay, new List()); + return (PlayMethod.DirectPlay, TranscodeReason.None); } if (options.ForceDirectStream) { - return (PlayMethod.DirectStream, new List()); + return (PlayMethod.DirectStream, TranscodeReason.None); } DeviceProfile profile = options.Profile; @@ -1089,11 +1077,7 @@ namespace MediaBrowser.Model.Dlna { LogConditionFailure(profile, "VideoContainerProfile", i, mediaSource); - var transcodeReason = GetTranscodeReasonForFailedCondition(i); - var transcodeReasons = transcodeReason.HasValue - ? new List { transcodeReason.Value } - : new List(); - + var transcodeReasons = GetTranscodeReasonForFailedCondition(i); return (null, transcodeReasons); } } @@ -1133,11 +1117,7 @@ namespace MediaBrowser.Model.Dlna LogConditionFailure(profile, "VideoCodecProfile", i, mediaSource); var transcodeReason = GetTranscodeReasonForFailedCondition(i); - var transcodeReasons = transcodeReason.HasValue - ? new List { transcodeReason.Value } - : new List(); - - return (null, transcodeReasons); + return (null, transcodeReason); } } @@ -1178,11 +1158,7 @@ namespace MediaBrowser.Model.Dlna { LogConditionFailure(profile, "VideoAudioCodecProfile", i, mediaSource); - var transcodeReason = GetTranscodeReasonForFailedCondition(i); - var transcodeReasons = transcodeReason.HasValue - ? new List { transcodeReason.Value } - : new List(); - + var transcodeReasons = GetTranscodeReasonForFailedCondition(i); return (null, transcodeReasons); } } @@ -1190,10 +1166,10 @@ namespace MediaBrowser.Model.Dlna if (isEligibleForDirectStream && mediaSource.SupportsDirectStream) { - return (PlayMethod.DirectStream, new List()); + return (PlayMethod.DirectStream, TranscodeReason.None); } - return (null, new List { TranscodeReason.ContainerBitrateExceedsLimit }); + return (null, TranscodeReason.ContainerBitrateExceedsLimit); } private void LogConditionFailure(DeviceProfile profile, string type, ProfileCondition condition, MediaSourceInfo mediaSource) @@ -1209,7 +1185,7 @@ namespace MediaBrowser.Model.Dlna mediaSource.Path ?? "Unknown path"); } - private (bool DirectPlay, TranscodeReason? Reason) IsEligibleForDirectPlay( + private TranscodeReason IsEligibleForDirectPlay( MediaSourceInfo item, long maxBitrate, MediaStream subtitleStream, @@ -1217,6 +1193,7 @@ namespace MediaBrowser.Model.Dlna VideoOptions options, PlayMethod playMethod) { + var reason = TranscodeReason.None; if (subtitleStream != null) { var subtitleProfile = GetSubtitleProfile(item, subtitleStream, options.Profile.SubtitleProfiles, playMethod, _transcoderSupport, item.Container, null); @@ -1226,22 +1203,23 @@ namespace MediaBrowser.Model.Dlna && subtitleProfile.Method != SubtitleDeliveryMethod.Embed) { _logger.LogDebug("Not eligible for {0} due to unsupported subtitles", playMethod); - return (false, TranscodeReason.SubtitleCodecNotSupported); + reason |= TranscodeReason.SubtitleCodecNotSupported; } } - bool result = IsAudioEligibleForDirectPlay(item, maxBitrate, playMethod); + bool result = IsItemBitrateEligibleForDirectPlay(item, maxBitrate, playMethod); if (!result) { - return (false, TranscodeReason.ContainerBitrateExceedsLimit); + reason |= TranscodeReason.ContainerBitrateExceedsLimit; } + // TODO:6450 support external audio in DirectStream? if (audioStream?.IsExternal == true) { - return (false, TranscodeReason.AudioIsExternal); + reason |= TranscodeReason.AudioIsExternal; } - return (true, null); + return reason; } public static SubtitleProfile GetSubtitleProfile( @@ -1401,7 +1379,7 @@ namespace MediaBrowser.Model.Dlna return null; } - private bool IsAudioEligibleForDirectPlay(MediaSourceInfo item, long maxBitrate, PlayMethod playMethod) + private bool IsItemBitrateEligibleForDirectPlay(MediaSourceInfo item, long maxBitrate, PlayMethod playMethod) { // Don't restrict by bitrate if coming from an external domain if (item.IsRemote) diff --git a/MediaBrowser.Model/Dlna/StreamInfo.cs b/MediaBrowser.Model/Dlna/StreamInfo.cs index a678c54e7f..3b86d5f42c 100644 --- a/MediaBrowser.Model/Dlna/StreamInfo.cs +++ b/MediaBrowser.Model/Dlna/StreamInfo.cs @@ -23,7 +23,6 @@ namespace MediaBrowser.Model.Dlna AudioCodecs = Array.Empty(); VideoCodecs = Array.Empty(); SubtitleCodecs = Array.Empty(); - TranscodeReasons = Array.Empty(); StreamOptions = new Dictionary(StringComparer.OrdinalIgnoreCase); } @@ -103,7 +102,7 @@ namespace MediaBrowser.Model.Dlna public string PlaySessionId { get; set; } - public TranscodeReason[] TranscodeReasons { get; set; } + public TranscodeReason TranscodeReasons { get; set; } public Dictionary StreamOptions { get; private set; } @@ -799,7 +798,7 @@ namespace MediaBrowser.Model.Dlna if (!item.IsDirectStream) { - list.Add(new NameValuePair("TranscodeReasons", string.Join(',', item.TranscodeReasons.Distinct()))); + list.Add(new NameValuePair("TranscodeReasons", item.TranscodeReasons.Serialize())); } return list; diff --git a/MediaBrowser.Model/Dto/MediaSourceInfo.cs b/MediaBrowser.Model/Dto/MediaSourceInfo.cs index 049e14333f..2281e6ae5d 100644 --- a/MediaBrowser.Model/Dto/MediaSourceInfo.cs +++ b/MediaBrowser.Model/Dto/MediaSourceInfo.cs @@ -109,7 +109,7 @@ namespace MediaBrowser.Model.Dto public int? AnalyzeDurationMs { get; set; } [JsonIgnore] - public TranscodeReason[] TranscodeReasons { get; set; } + public TranscodeReason TranscodeReasons { get; set; } public int? DefaultAudioStreamIndex { get; set; } diff --git a/MediaBrowser.Model/Session/TranscodeReason.cs b/MediaBrowser.Model/Session/TranscodeReason.cs index 3c95df66d5..c3570840f9 100644 --- a/MediaBrowser.Model/Session/TranscodeReason.cs +++ b/MediaBrowser.Model/Session/TranscodeReason.cs @@ -1,32 +1,51 @@ #pragma warning disable CS1591 +using System; + namespace MediaBrowser.Model.Session { + [Flags] public enum TranscodeReason { - ContainerNotSupported = 0, - VideoCodecNotSupported = 1, - AudioCodecNotSupported = 2, - ContainerBitrateExceedsLimit = 3, - AudioBitrateNotSupported = 4, - AudioChannelsNotSupported = 5, - VideoResolutionNotSupported = 6, - UnknownVideoStreamInfo = 7, - UnknownAudioStreamInfo = 8, - AudioProfileNotSupported = 9, - AudioSampleRateNotSupported = 10, - AnamorphicVideoNotSupported = 11, - InterlacedVideoNotSupported = 12, - SecondaryAudioNotSupported = 13, - RefFramesNotSupported = 14, - VideoBitDepthNotSupported = 15, - VideoBitrateNotSupported = 16, - VideoFramerateNotSupported = 17, - VideoLevelNotSupported = 18, - VideoProfileNotSupported = 19, - AudioBitDepthNotSupported = 20, - SubtitleCodecNotSupported = 21, - DirectPlayError = 22, - AudioIsExternal = 23 + None = 0, + + // Primary + ContainerNotSupported = 1 << 0, + VideoCodecNotSupported = 1 << 1, + AudioCodecNotSupported = 1 << 2, + SubtitleCodecNotSupported = 1 << 3, + AudioIsExternal = 1 << 4, + SecondaryAudioNotSupported = 1 << 5, + + // Video Constraints + VideoProfileNotSupported = 1 << 6, + VideoLevelNotSupported = 1 << 7, + VideoResolutionNotSupported = 1 << 8, + VideoBitDepthNotSupported = 1 << 9, + VideoFramerateNotSupported = 1 << 10, + RefFramesNotSupported = 1 << 11, + AnamorphicVideoNotSupported = 1 << 12, + InterlacedVideoNotSupported = 1 << 13, + + // Audio Constraints + AudioChannelsNotSupported = 1 << 14, + AudioProfileNotSupported = 1 << 15, + AudioSampleRateNotSupported = 1 << 16, + AudioBitDepthNotSupported = 1 << 20, + + // Bitrate Constraints + ContainerBitrateExceedsLimit = 1 << 17, + VideoBitrateNotSupported = 1 << 18, + AudioBitrateNotSupported = 1 << 19, + + // Errors + UnknownVideoStreamInfo = 1 << 20, + UnknownAudioStreamInfo = 1 << 21, + DirectPlayError = 1 << 22, + + // Aliases + ContainerReasons = ContainerNotSupported | ContainerBitrateExceedsLimit, + AudioReasons = AudioCodecNotSupported | AudioBitrateNotSupported | AudioChannelsNotSupported | AudioProfileNotSupported | AudioSampleRateNotSupported | SecondaryAudioNotSupported | AudioBitDepthNotSupported | AudioIsExternal, + VideoReasons = VideoCodecNotSupported | VideoResolutionNotSupported | AnamorphicVideoNotSupported | InterlacedVideoNotSupported | VideoBitDepthNotSupported | VideoBitrateNotSupported | VideoFramerateNotSupported | VideoLevelNotSupported | RefFramesNotSupported, } } diff --git a/MediaBrowser.Model/Session/TranscodeReasonExtensions.cs b/MediaBrowser.Model/Session/TranscodeReasonExtensions.cs new file mode 100644 index 0000000000..c7a5095f74 --- /dev/null +++ b/MediaBrowser.Model/Session/TranscodeReasonExtensions.cs @@ -0,0 +1,22 @@ +#pragma warning disable CS1591 + +using System; +using System.Linq; + +namespace MediaBrowser.Model.Session +{ + public static class TranscodeReasonExtensions + { + private static TranscodeReason[] values = Enum.GetValues(); + + public static string Serialize(this MediaBrowser.Model.Session.TranscodeReason reasons, string sep = ",") + { + return string.Join(sep, reasons.ToArray()); + } + + public static TranscodeReason[] ToArray(this MediaBrowser.Model.Session.TranscodeReason reasons) + { + return values.Where(r => r != 0 && reasons.HasFlag(r)).ToArray(); + } + } +} diff --git a/MediaBrowser.Model/Session/TranscodingInfo.cs b/MediaBrowser.Model/Session/TranscodingInfo.cs index 68ab691f88..78e5baad7e 100644 --- a/MediaBrowser.Model/Session/TranscodingInfo.cs +++ b/MediaBrowser.Model/Session/TranscodingInfo.cs @@ -1,17 +1,12 @@ #nullable disable #pragma warning disable CS1591 -using System; +using System.Text.Json.Serialization; namespace MediaBrowser.Model.Session { public class TranscodingInfo { - public TranscodingInfo() - { - TranscodeReasons = Array.Empty(); - } - public string AudioCodec { get; set; } public string VideoCodec { get; set; } @@ -36,6 +31,9 @@ namespace MediaBrowser.Model.Session public HardwareEncodingType? HardwareAccelerationType { get; set; } - public TranscodeReason[] TranscodeReasons { get; set; } + public TranscodeReason[] TranscodeReasons { get => TranscodeReason.ToArray(); } + + [JsonIgnore] + public TranscodeReason TranscodeReason { get; set; } } } diff --git a/tests/Jellyfin.Dlna.Tests/StreamBuilderTests.cs b/tests/Jellyfin.Dlna.Tests/StreamBuilderTests.cs index abb853b2ab..ccd95f7502 100644 --- a/tests/Jellyfin.Dlna.Tests/StreamBuilderTests.cs +++ b/tests/Jellyfin.Dlna.Tests/StreamBuilderTests.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Specialized; using System.IO; using System.Linq; using System.Text.Json; @@ -19,65 +20,69 @@ namespace Jellyfin.MediaBrowser.Model.Tests [Theory] // Chrome [InlineData("Chrome", "mp4-h264-aac-vtt-2600k", PlayMethod.DirectStream)] // #6450 should be DirectPlay - [InlineData("Chrome", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.Transcode)] // #6450 should be DirectPlay - [InlineData("Chrome", "mp4-h264-ac3-srt-2600k", PlayMethod.Transcode)] // #6450 should be DirectStream - [InlineData("Chrome", "mp4-hevc-aac-srt-15200k", PlayMethod.Transcode, true)] - [InlineData("Chrome", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, true)] - [InlineData("Chrome", "mkv-vp9-aac-srt-2600k", PlayMethod.Transcode, true)] // #6450 should be 'false' - [InlineData("Chrome", "mkv-vp9-ac3-srt-2600k", PlayMethod.Transcode, true)] // #6450 should be 'false' + [InlineData("Chrome", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported)] // #6450 should be DirectPlay + [InlineData("Chrome", "mp4-h264-ac3-aacDef-srt-2600k", PlayMethod.Transcode, TranscodeReason.SecondaryAudioNotSupported)] // #6450 should be DirectPlay + [InlineData("Chrome", "mp4-h264-ac3-srt-2600k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported)] // #6450 should be DirectStream + [InlineData("Chrome", "mp4-hevc-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported, "Transcode")] + [InlineData("Chrome", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported | TranscodeReason.AudioCodecNotSupported, "Transcode")] + [InlineData("Chrome", "mkv-vp9-aac-srt-2600k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported, "Transcode")] // #6450 should be 'false' + [InlineData("Chrome", "mkv-vp9-ac3-srt-2600k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported, "Transcode")] // #6450 should be 'false' [InlineData("Chrome", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.DirectStream)] // #6450 should be DirectPlay // Firefox [InlineData("Firefox", "mp4-h264-aac-vtt-2600k", PlayMethod.DirectStream)] // #6450 should be DirectPlay - [InlineData("Firefox", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.Transcode)] // #6450 should be DirectPlay - [InlineData("Firefox", "mp4-h264-ac3-srt-2600k", PlayMethod.Transcode)] // #6450 should be DirectStream - [InlineData("Firefox", "mp4-hevc-aac-srt-15200k", PlayMethod.Transcode, true)] - [InlineData("Firefox", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, true)] - [InlineData("Firefox", "mkv-vp9-aac-srt-2600k", PlayMethod.Transcode, true)] // #6450 should be 'false' - [InlineData("Firefox", "mkv-vp9-ac3-srt-2600k", PlayMethod.Transcode, true)] // #6450 should be 'false' + [InlineData("Firefox", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported)] // #6450 should be DirectPlay + [InlineData("Firefox", "mp4-h264-ac3-aacDef-srt-2600k", PlayMethod.Transcode, TranscodeReason.SecondaryAudioNotSupported)] // #6450 should be DirectPlay + [InlineData("Firefox", "mp4-h264-ac3-srt-2600k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported)] // #6450 should be DirectStream + [InlineData("Firefox", "mp4-hevc-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported, "Transcode")] + [InlineData("Firefox", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported | TranscodeReason.AudioCodecNotSupported, "Transcode")] + [InlineData("Firefox", "mkv-vp9-aac-srt-2600k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported, "Transcode")] // #6450 should be 'false' + [InlineData("Firefox", "mkv-vp9-ac3-srt-2600k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported, "Transcode")] // #6450 should be 'false' [InlineData("Firefox", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.DirectStream)] // #6450 should be DirectPlay // Safari [InlineData("SafariNext", "mp4-h264-aac-vtt-2600k", PlayMethod.DirectStream)] // #6450 should be DirectPlay - [InlineData("SafariNext", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectStream)] // #6450 should be DirectPlay + [InlineData("SafariNext", "mp4-h264-ac3-aacDef-srt-2600k", PlayMethod.DirectStream)] // #6450 should be DirectPlay [InlineData("SafariNext", "mp4-h264-ac3-srt-2600k", PlayMethod.DirectStream)] // #6450 should be DirectPlay [InlineData("SafariNext", "mp4-hevc-aac-srt-15200k", PlayMethod.DirectStream)] // #6450 should probably be DirectPlay [InlineData("SafariNext", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.DirectStream)] // #6450 should probably be DirectPlay // AndroidPixel [InlineData("AndroidPixel", "mp4-h264-aac-srt-2600k", PlayMethod.DirectStream)] // #6450 should be DirectPlay - [InlineData("AndroidPixel", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectStream)] // #6450 should be DirectPlay + [InlineData("AndroidPixel", "mp4-h264-ac3-aacDef-srt-2600k", PlayMethod.DirectStream)] // #6450 should be DirectPlay [InlineData("AndroidPixel", "mp4-h264-ac3-srt-2600k", PlayMethod.DirectStream)] // #6450 should be DirectPlay - [InlineData("AndroidPixel", "mp4-hevc-aac-srt-15200k", PlayMethod.Transcode, true)] - [InlineData("AndroidPixel", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, true)] + [InlineData("AndroidPixel", "mp4-hevc-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.ContainerBitrateExceedsLimit, "Transcode")] + [InlineData("AndroidPixel", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.ContainerBitrateExceedsLimit, "Transcode")] // Yatse [InlineData("Yatse", "mp4-h264-aac-srt-2600k", PlayMethod.DirectStream)] // #6450 should be DirectPlay - [InlineData("Yatse", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.Transcode)] // #6450 should be DirectPlay - [InlineData("Yatse", "mp4-h264-ac3-srt-2600k", PlayMethod.Transcode, true)] + [InlineData("Yatse", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported)] // #6450 should be DirectPlay + [InlineData("Yatse", "mp4-h264-ac3-aacDef-srt-2600k", PlayMethod.Transcode, TranscodeReason.SecondaryAudioNotSupported)] // #6450 should be DirectPlay + [InlineData("Yatse", "mp4-h264-ac3-srt-2600k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported, "Transcode")] [InlineData("Yatse", "mp4-hevc-aac-srt-15200k", PlayMethod.DirectStream)] // #6450 should be DirectPlay - [InlineData("Yatse", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, true)] // #6450 should be DirectPlay + [InlineData("Yatse", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported, "Transcode")] // #6450 should be DirectPlay // RokuSSPlus [InlineData("RokuSSPlus", "mp4-h264-aac-srt-2600k", PlayMethod.DirectStream)] // #6450 should be DirectPlay - [InlineData("RokuSSPlus", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.Transcode)] // #6450 should be DirectPlay - [InlineData("RokuSSPlus", "mp4-h264-ac3-srt-2600k", PlayMethod.Transcode)] // #6450 should be DirectStream + [InlineData("RokuSSPlus", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported)] // #6450 should be DirectPlay + [InlineData("RokuSSPlus", "mp4-h264-ac3-aacDef-srt-2600k", PlayMethod.DirectStream)] // #6450 should be DirectPlay + [InlineData("RokuSSPlus", "mp4-h264-ac3-srt-2600k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported)] // #6450 should be DirectStream [InlineData("RokuSSPlus", "mp4-hevc-aac-srt-15200k", PlayMethod.DirectStream)] // #6450 should be DirectPlay - [InlineData("RokuSSPlus", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, true)] // #6450 should be DirectPlay - [InlineData("RokuSSPlus", "mp4-hevc-ac3-srt-15200k", PlayMethod.Transcode, true)] // #6450 should be DirectStream + [InlineData("RokuSSPlus", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported, "Transcode")] // #6450 should be DirectPlay + [InlineData("RokuSSPlus", "mp4-hevc-ac3-srt-15200k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported, "Transcode")] // #6450 should be DirectStream // JellyfinMediaPlayer [InlineData("JellyfinMediaPlayer", "mp4-h264-aac-vtt-2600k", PlayMethod.DirectStream)] // #6450 should be DirectPlay [InlineData("JellyfinMediaPlayer", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectStream)] // #6450 should be DirectPlay [InlineData("JellyfinMediaPlayer", "mp4-h264-ac3-srt-2600k", PlayMethod.DirectStream)] // #6450 should be DirectPlay - [InlineData("JellyfinMediaPlayer", "mp4-hevc-aac-srt-15200k", PlayMethod.Transcode)] // #6450 should be DirectPlay - [InlineData("JellyfinMediaPlayer", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode)] // #6450 should be DirectPlay + [InlineData("JellyfinMediaPlayer", "mp4-hevc-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.ContainerBitrateExceedsLimit)] // #6450 should be DirectPlay + [InlineData("JellyfinMediaPlayer", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.ContainerBitrateExceedsLimit)] // #6450 should be DirectPlay [InlineData("JellyfinMediaPlayer", "mkv-vp9-aac-srt-2600k", PlayMethod.DirectStream)] // #6450 should be DirectPlay [InlineData("JellyfinMediaPlayer", "mkv-vp9-ac3-srt-2600k", PlayMethod.DirectStream)] // #6450 should be DirectPlay [InlineData("JellyfinMediaPlayer", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.DirectStream)] // #6450 should be DirectPlay // TranscodeMedia - [InlineData("TranscodeMedia", "mp4-h264-aac-vtt-2600k", PlayMethod.Transcode)] // #6450 should be DirectPlay - [InlineData("TranscodeMedia", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.Transcode)] // #6450 should be DirectPlay - [InlineData("TranscodeMedia", "mp4-h264-ac3-srt-2600k", PlayMethod.Transcode)] // #6450 should be DirectPlay - [InlineData("TranscodeMedia", "mp4-hevc-aac-srt-15200k", PlayMethod.Transcode, true)] // #6450 should be DirectPlay - [InlineData("TranscodeMedia", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, true)] // #6450 should be DirectPlay - [InlineData("TranscodeMedia", "mkv-vp9-aac-srt-2600k", PlayMethod.Transcode, true)] // #6450 should be DirectPlay - [InlineData("TranscodeMedia", "mkv-vp9-ac3-srt-2600k", PlayMethod.Transcode, true)] // #6450 should be DirectPlay - [InlineData("TranscodeMedia", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.Transcode, true)] // #6450 should be DirectPlay + [InlineData("TranscodeMedia", "mp4-h264-aac-vtt-2600k", PlayMethod.Transcode, TranscodeReason.ContainerNotSupported | TranscodeReason.VideoCodecNotSupported | TranscodeReason.AudioCodecNotSupported)] // #6450 should be DirectPlay + [InlineData("TranscodeMedia", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.Transcode, TranscodeReason.ContainerNotSupported | TranscodeReason.VideoCodecNotSupported | TranscodeReason.AudioCodecNotSupported)] // #6450 should be DirectPlay + [InlineData("TranscodeMedia", "mp4-h264-ac3-srt-2600k", PlayMethod.Transcode, TranscodeReason.ContainerNotSupported | TranscodeReason.VideoCodecNotSupported | TranscodeReason.AudioCodecNotSupported)] // #6450 should be DirectPlay + [InlineData("TranscodeMedia", "mp4-hevc-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.ContainerNotSupported | TranscodeReason.VideoCodecNotSupported | TranscodeReason.AudioCodecNotSupported, "Transcode")] // #6450 should be DirectPlay + [InlineData("TranscodeMedia", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.ContainerNotSupported | TranscodeReason.VideoCodecNotSupported | TranscodeReason.AudioCodecNotSupported, "Transcode")] // #6450 should be DirectPlay + [InlineData("TranscodeMedia", "mkv-vp9-aac-srt-2600k", PlayMethod.Transcode, TranscodeReason.ContainerNotSupported | TranscodeReason.VideoCodecNotSupported | TranscodeReason.AudioCodecNotSupported, "Transcode")] // #6450 should be DirectPlay + [InlineData("TranscodeMedia", "mkv-vp9-ac3-srt-2600k", PlayMethod.Transcode, TranscodeReason.ContainerNotSupported | TranscodeReason.VideoCodecNotSupported | TranscodeReason.AudioCodecNotSupported, "Transcode")] // #6450 should be DirectPlay + [InlineData("TranscodeMedia", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.Transcode, TranscodeReason.ContainerNotSupported | TranscodeReason.VideoCodecNotSupported | TranscodeReason.AudioCodecNotSupported, "Transcode")] // #6450 should be DirectPlay // DirectMedia [InlineData("DirectMedia", "mp4-h264-aac-vtt-2600k", PlayMethod.DirectStream)] // #6450 should be DirectPlay [InlineData("DirectMedia", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectStream)] // #6450 should be DirectPlay @@ -88,27 +93,120 @@ namespace Jellyfin.MediaBrowser.Model.Tests [InlineData("DirectMedia", "mkv-vp9-ac3-srt-2600k", PlayMethod.DirectStream)] // #6450 should be DirectPlay [InlineData("DirectMedia", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.DirectStream)] // #6450 should be DirectPlay // LowBandwidth - [InlineData("LowBandwidth", "mp4-h264-aac-vtt-2600k", PlayMethod.Transcode)] // #6450 should be DirectPlay - [InlineData("LowBandwidth", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.Transcode)] // #6450 should be DirectPlay - [InlineData("LowBandwidth", "mp4-h264-ac3-srt-2600k", PlayMethod.Transcode)] // #6450 should be DirectPlay - [InlineData("LowBandwidth", "mp4-hevc-aac-srt-15200k", PlayMethod.Transcode, true)] // #6450 should be DirectPlay - [InlineData("LowBandwidth", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, true)] // #6450 should be DirectPlay - [InlineData("LowBandwidth", "mkv-vp9-aac-srt-2600k", PlayMethod.Transcode, true)] // #6450 should be DirectPlay - [InlineData("LowBandwidth", "mkv-vp9-ac3-srt-2600k", PlayMethod.Transcode, true)] // #6450 should be DirectPlay - [InlineData("LowBandwidth", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.Transcode, true)] // #6450 should be DirectPlay + [InlineData("LowBandwidth", "mp4-h264-aac-vtt-2600k", PlayMethod.Transcode, TranscodeReason.ContainerBitrateExceedsLimit)] // #6450 should be DirectPlay + [InlineData("LowBandwidth", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.Transcode, TranscodeReason.ContainerBitrateExceedsLimit)] // #6450 should be DirectPlay + [InlineData("LowBandwidth", "mp4-h264-ac3-srt-2600k", PlayMethod.Transcode, TranscodeReason.ContainerBitrateExceedsLimit)] // #6450 should be DirectPlay + [InlineData("LowBandwidth", "mp4-hevc-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.ContainerBitrateExceedsLimit, "Transcode")] // #6450 should be DirectPlay + [InlineData("LowBandwidth", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.ContainerBitrateExceedsLimit, "Transcode")] // #6450 should be DirectPlay + [InlineData("LowBandwidth", "mkv-vp9-aac-srt-2600k", PlayMethod.Transcode, TranscodeReason.ContainerBitrateExceedsLimit, "Transcode")] // #6450 should be DirectPlay + [InlineData("LowBandwidth", "mkv-vp9-ac3-srt-2600k", PlayMethod.Transcode, TranscodeReason.ContainerBitrateExceedsLimit, "Transcode")] // #6450 should be DirectPlay + [InlineData("LowBandwidth", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.Transcode, TranscodeReason.ContainerBitrateExceedsLimit, "Transcode")] // #6450 should be DirectPlay // Null - [InlineData("Null", "mp4-h264-aac-vtt-2600k", null)] // #6450 should be DirectPlay - [InlineData("Null", "mp4-h264-ac3-aac-srt-2600k", null)] // #6450 should be DirectPlay - [InlineData("Null", "mp4-h264-ac3-srt-2600k", null)] // #6450 should be DirectPlay - [InlineData("Null", "mp4-hevc-aac-srt-15200k", null)] // #6450 should be DirectPlay - [InlineData("Null", "mp4-hevc-ac3-aac-srt-15200k", null)] // #6450 should be DirectPlay - [InlineData("Null", "mkv-vp9-aac-srt-2600k", null)] // #6450 should be DirectPlay - [InlineData("Null", "mkv-vp9-ac3-srt-2600k", null)] // #6450 should be DirectPlay - [InlineData("Null", "mkv-vp9-vorbis-vtt-2600k", null)] // #6450 should be DirectPlay - public async Task BuildVideoItemSimple(string deviceName, string mediaSource, PlayMethod? playMethod, bool fullTranscode = false) + [InlineData("Null", "mp4-h264-aac-vtt-2600k", null, TranscodeReason.ContainerBitrateExceedsLimit | TranscodeReason.SubtitleCodecNotSupported)] // #6450 should be DirectPlay + [InlineData("Null", "mp4-h264-ac3-aac-srt-2600k", null, TranscodeReason.ContainerBitrateExceedsLimit | TranscodeReason.SubtitleCodecNotSupported)] // #6450 should be DirectPlay + [InlineData("Null", "mp4-h264-ac3-srt-2600k", null, TranscodeReason.ContainerBitrateExceedsLimit | TranscodeReason.SubtitleCodecNotSupported)] // #6450 should be DirectPlay + [InlineData("Null", "mp4-hevc-aac-srt-15200k", null, TranscodeReason.ContainerBitrateExceedsLimit | TranscodeReason.SubtitleCodecNotSupported)] // #6450 should be DirectPlay + [InlineData("Null", "mp4-hevc-ac3-aac-srt-15200k", null, TranscodeReason.ContainerBitrateExceedsLimit | TranscodeReason.SubtitleCodecNotSupported)] // #6450 should be DirectPlay + [InlineData("Null", "mkv-vp9-aac-srt-2600k", null, TranscodeReason.ContainerBitrateExceedsLimit | TranscodeReason.SubtitleCodecNotSupported)] // #6450 should be DirectPlay + [InlineData("Null", "mkv-vp9-ac3-srt-2600k", null, TranscodeReason.ContainerBitrateExceedsLimit | TranscodeReason.SubtitleCodecNotSupported)] // #6450 should be DirectPlay + [InlineData("Null", "mkv-vp9-vorbis-vtt-2600k", null, TranscodeReason.ContainerBitrateExceedsLimit | TranscodeReason.SubtitleCodecNotSupported)] // #6450 should be DirectPlay + public async Task BuildVideoItemSimple(string deviceName, string mediaSource, PlayMethod? playMethod, TranscodeReason why = TranscodeReason.None, string transcodeMode = "DirectStream", string transcodeProtocol = "") { - var builder = GetStreamBuilder(); var options = await GetVideoOptions(deviceName, mediaSource); + BuildVideoItemSimpleTest(options, playMethod, why, transcodeMode, transcodeProtocol); + } + + [Theory] + // Chrome + [InlineData("Chrome", "mp4-h264-aac-vtt-2600k", PlayMethod.DirectStream)] // #6450 should be DirectPlay + [InlineData("Chrome", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported)] // #6450 should be DirectPlay + [InlineData("Chrome", "mp4-h264-ac3-srt-2600k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported)] // #6450 should be DirectStream + [InlineData("Chrome", "mp4-hevc-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported, "Transcode")] + [InlineData("Chrome", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported | TranscodeReason.AudioCodecNotSupported, "Transcode")] + [InlineData("Chrome", "mkv-vp9-aac-srt-2600k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported, "Transcode")] // #6450 should be 'false' + [InlineData("Chrome", "mkv-vp9-ac3-srt-2600k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported, "Transcode")] // #6450 should be 'false' + [InlineData("Chrome", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.DirectStream)] // #6450 should be DirectPlay + // Firefox + [InlineData("Firefox", "mp4-h264-aac-vtt-2600k", PlayMethod.DirectStream)] // #6450 should be DirectPlay + [InlineData("Firefox", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported)] // #6450 should be DirectPlay + [InlineData("Firefox", "mp4-h264-ac3-srt-2600k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported)] // #6450 should be DirectStream + [InlineData("Firefox", "mp4-hevc-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported, "Transcode")] + [InlineData("Firefox", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported | TranscodeReason.AudioCodecNotSupported, "Transcode")] + [InlineData("Firefox", "mkv-vp9-aac-srt-2600k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported, "Transcode")] // #6450 should be 'false' + [InlineData("Firefox", "mkv-vp9-ac3-srt-2600k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported, "Transcode")] // #6450 should be 'false' + [InlineData("Firefox", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.DirectStream)] // #6450 should be DirectPlay + // Safari + [InlineData("SafariNext", "mp4-h264-aac-vtt-2600k", PlayMethod.DirectStream)] // #6450 should be DirectPlay + [InlineData("SafariNext", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectStream)] // #6450 should be DirectPlay + [InlineData("SafariNext", "mp4-h264-ac3-srt-2600k", PlayMethod.DirectStream)] // #6450 should be DirectPlay + [InlineData("SafariNext", "mp4-hevc-aac-srt-15200k", PlayMethod.DirectStream)] // #6450 should probably be DirectPlay + [InlineData("SafariNext", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.DirectStream)] // #6450 should probably be DirectPlay + // AndroidPixel + [InlineData("AndroidPixel", "mp4-h264-aac-srt-2600k", PlayMethod.DirectStream)] // #6450 should be DirectPlay + [InlineData("AndroidPixel", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectStream)] // #6450 should be DirectPlay + [InlineData("AndroidPixel", "mp4-h264-ac3-srt-2600k", PlayMethod.DirectStream)] // #6450 should be DirectPlay + [InlineData("AndroidPixel", "mp4-hevc-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.ContainerBitrateExceedsLimit, "Transcode")] + [InlineData("AndroidPixel", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.ContainerBitrateExceedsLimit, "Transcode")] + // Yatse + [InlineData("Yatse", "mp4-h264-aac-srt-2600k", PlayMethod.DirectStream)] // #6450 should be DirectPlay + [InlineData("Yatse", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported)] // #6450 should be DirectPlay + [InlineData("Yatse", "mp4-h264-ac3-srt-2600k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported, "Transcode")] + [InlineData("Yatse", "mp4-hevc-aac-srt-15200k", PlayMethod.DirectStream)] // #6450 should be DirectPlay + [InlineData("Yatse", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported, "Transcode")] // #6450 should be DirectPlay + // RokuSSPlus + [InlineData("RokuSSPlus", "mp4-h264-aac-srt-2600k", PlayMethod.DirectStream)] // #6450 should be DirectPlay + [InlineData("RokuSSPlus", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported)] // #6450 should be DirectPlay + [InlineData("RokuSSPlus", "mp4-h264-ac3-srt-2600k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported)] // #6450 should be DirectStream + [InlineData("RokuSSPlus", "mp4-hevc-aac-srt-15200k", PlayMethod.DirectStream)] // #6450 should be DirectPlay + [InlineData("RokuSSPlus", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported, "Transcode")] // #6450 should be DirectPlay + [InlineData("RokuSSPlus", "mp4-hevc-ac3-srt-15200k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported, "Transcode")] // #6450 should be DirectStream + // JellyfinMediaPlayer + [InlineData("JellyfinMediaPlayer", "mp4-h264-aac-vtt-2600k", PlayMethod.DirectStream)] // #6450 should be DirectPlay + [InlineData("JellyfinMediaPlayer", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectStream)] // #6450 should be DirectPlay + [InlineData("JellyfinMediaPlayer", "mp4-h264-ac3-srt-2600k", PlayMethod.DirectStream)] // #6450 should be DirectPlay + [InlineData("JellyfinMediaPlayer", "mp4-hevc-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.ContainerBitrateExceedsLimit)] // #6450 should be DirectPlay + [InlineData("JellyfinMediaPlayer", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.ContainerBitrateExceedsLimit)] // #6450 should be DirectPlay + [InlineData("JellyfinMediaPlayer", "mkv-vp9-aac-srt-2600k", PlayMethod.DirectStream)] // #6450 should be DirectPlay + [InlineData("JellyfinMediaPlayer", "mkv-vp9-ac3-srt-2600k", PlayMethod.DirectStream)] // #6450 should be DirectPlay + [InlineData("JellyfinMediaPlayer", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.DirectStream)] // #6450 should be DirectPlay + public async Task BuildVideoItemWithFirstExplicitStream(string deviceName, string mediaSource, PlayMethod?playMethod, TranscodeReason why = TranscodeReason.None, string transcodeMode = "DirectStream", string transcodeProtocol = "") + { + var options = await GetVideoOptions(deviceName, mediaSource); + options.AudioStreamIndex = 1; + options.SubtitleStreamIndex = options.MediaSources[0].MediaStreams.Count() - 1; + BuildVideoItemSimpleTest(options, playMethod, why, transcodeMode, transcodeProtocol); + } + + [Theory] + // Chrome + [InlineData("Chrome", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.Transcode, TranscodeReason.SecondaryAudioNotSupported)] // #6450 should be DirectPlay + [InlineData("Chrome", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported, "Transcode")] // #6450 should have container & profile video reasons? + // Firefox + [InlineData("Firefox", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.Transcode, TranscodeReason.SecondaryAudioNotSupported)] // #6450 should be DirectPlay + [InlineData("Firefox", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported, "Transcode")] // #6450 should have container & profile video reasons? + // Yatse + [InlineData("Yatse", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.Transcode, TranscodeReason.SecondaryAudioNotSupported)] // #6450 should be DirectPlay + [InlineData("Yatse", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.SecondaryAudioNotSupported, "Transcode")] // #6450 should be DirectPlay + // RokuSSPlus + [InlineData("RokuSSPlus", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectStream)] // #6450 should be DirectPlay + [InlineData("RokuSSPlus", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.DirectStream)] // #6450 should be DirectPlay + public async Task BuildVideoItemWithDirectPlayExplicitStreams(string deviceName, string mediaSource, PlayMethod playMethod, TranscodeReason why = TranscodeReason.None, string transcodeMode = "DirectStream", string transcodeProtocol = "") + { + var options = await GetVideoOptions(deviceName, mediaSource); + var streamCount = options.MediaSources[0].MediaStreams.Count(); + options.AudioStreamIndex = streamCount - 2; + options.SubtitleStreamIndex = streamCount - 1; + BuildVideoItemSimpleTest(options, playMethod, why, transcodeMode, transcodeProtocol); + } + + private void BuildVideoItemSimpleTest(VideoOptions options, PlayMethod? playMethod, TranscodeReason why, string transcodeMode, string transcodeProtocol) + { + if (string.IsNullOrEmpty(transcodeProtocol)) + { + transcodeProtocol = playMethod == PlayMethod.DirectStream ? "http" : "hls"; + } + + var builder = GetStreamBuilder(); var val = builder.BuildVideoItem(options); Assert.NotNull(val); @@ -118,63 +216,130 @@ namespace Jellyfin.MediaBrowser.Model.Tests Assert.Equal(playMethod, val.PlayMethod); } - var videoStreams = options.MediaSources.SelectMany(source => source.MediaStreams).Where(stream => stream.Type == MediaStreamType.Video); - var audioStreams = options.MediaSources.SelectMany(source => source.MediaStreams).Where(stream => stream.Type == MediaStreamType.Audio); + Assert.Equal(why, val.TranscodeReasons); + + var audioStreamIndexInput = options.AudioStreamIndex; + var targetVideoStream = val.TargetVideoStream; + var targetAudioStream = val.TargetAudioStream; - var url = new UriBuilder(val.ToUrl("https://server/", "ACCESSTOKEN")); - var query = System.Web.HttpUtility.ParseQueryString(url.Query); + var mediaSource = options.MediaSources.First(source => source.Id == val.MediaSourceId); + Assert.NotNull(mediaSource); + var videoStreams = mediaSource.MediaStreams.Where(stream => stream.Type == MediaStreamType.Video); + var audioStreams = mediaSource.MediaStreams.Where(stream => stream.Type == MediaStreamType.Audio); + // TODO: check AudioStreamIndex vs options.AudioStreamIndex + var inputAudioStream = mediaSource.GetDefaultAudioStream(audioStreamIndexInput ?? mediaSource.DefaultAudioStreamIndex); + + var uri = ParseUri(val); if (playMethod == PlayMethod.DirectPlay) { - // Assert.Contains(query.Get("VidoeCodec"), videoStreams.Select(stream => stream.Codec)); - // Assert.Contains(query.Get("AudioCodec"), audioStreams.Select(stream => stream.Codec)); - Assert.Contains( - videoStreams, - stream => val.TargetVideoCodec.Contains(stream.Codec)); - Assert.Contains( - audioStreams, - stream => val.TargetAudioCodec.Contains(stream.Codec)); - } + // check expected container + var containers = ContainerProfile.SplitValue(mediaSource.Container); + Assert.Contains(uri.Extension, containers); - if (playMethod == PlayMethod.DirectStream) - { - Assert.Matches("stream[.][^.]+$", url.Path); - } + // check expected video codec (1) + Assert.Contains(targetVideoStream.Codec, val.TargetVideoCodec); + Assert.Single(val.TargetVideoCodec); + + // check expected audio codecs (1) + Assert.Contains(targetAudioStream.Codec, val.TargetAudioCodec); + Assert.Single(val.AudioCodecs); - if (playMethod == PlayMethod.Transcode) + // TODO: validate transcoding options as well + } + else if (playMethod == PlayMethod.DirectStream || playMethod == PlayMethod.Transcode) { - if (fullTranscode) + Assert.NotNull(val.Container); + // Assert.NotEmpty(val.VideoCodecs); + // Assert.NotEmpty(val.AudioCodecs); + + // check expected container (todo: this could be a test param) + if (transcodeProtocol == "http") { + // Assert.Equal("webm", val.Container); + Assert.Equal(val.Container, uri.Extension); + Assert.Equal("stream", uri.Filename); + // Assert.Equal("http", val.SubProtocol); + } + else + { + Assert.Equal("ts", val.Container); + Assert.Equal("m3u8", uri.Extension); + Assert.Equal("master", uri.Filename); Assert.Equal("hls", val.SubProtocol); - Assert.EndsWith("master.m3u8", url.Path, StringComparison.InvariantCulture); + } + + // Full transcode + if (transcodeMode == "Transcode") + { + // TODO: what else to validate here + if ((val.TranscodeReasons & TranscodeReason.ContainerReasons) == TranscodeReason.None) + { + // Assert.All( + // videoStreams, + // stream => Assert.DoesNotContain(stream.Codec, val.VideoCodecs)); + } - // Assert.All( - // videoStreams, - // stream => Assert.DoesNotContain(stream.Codec, val.TargetVideoCodec)); + // todo: fill out tests here } + + // DirectStream and Remux else { - Assert.Equal("hls", val.SubProtocol); - Assert.EndsWith("master.m3u8", url.Path, StringComparison.InvariantCulture); + // check expected video codec (1) + Assert.Contains(targetVideoStream.Codec, val.TargetVideoCodec); + Assert.Single(val.TargetVideoCodec); - Assert.Contains( - videoStreams, - stream => val.TargetVideoCodec.Contains(stream.Codec)); - // Assert.All( - // audioStreams, - // stream => Assert.DoesNotContain(stream.Codec, val.TargetAudioCodec)); + if (transcodeMode == "DirectStream") + { + if (!targetAudioStream.IsExternal) + { + // check expected audio codecs (1) + // Assert.DoesNotContain(targetAudioStream.Codec, val.AudioCodecs); + } + } + else if (transcodeMode == "Remux") + { + // check expected audio codecs (1) + Assert.Contains(targetAudioStream.Codec, val.AudioCodecs); + Assert.Single(val.AudioCodecs); + } + // video details + var videoStream = targetVideoStream; Assert.False(val.EstimateContentLength); Assert.Equal(TranscodeSeekInfo.Auto, val.TranscodeSeekInfo); - // Assert.True(val.CopyTimestamps); - - var videoStream = videoStreams.First(stream => val.TargetVideoCodec.Contains(stream.Codec)); - - Assert.Contains(videoStream.Codec, val.TargetVideoCodec); - // Assert.Contains(videoStream.Profile.ToLowerInvariant(), val.TargetVideoProfile.Split(",")); + // Assert.Contains(videoStream.Profile?.ToLowerInvariant() ?? string.Empty, val.TargetVideoProfile?.Split(",").Select(s => s.ToLowerInvariant()) ?? new string[0]); // Assert.Equal(videoStream.Level, val.TargetVideoLevel); // Assert.Equal(videoStream.BitDepth, val.TargetVideoBitDepth); - // Assert.Equal(videoStream.BitRate, val.VideoBitrate); + // Assert.InRange(val.VideoBitrate.GetValueOrDefault(), videoStream.BitRate.GetValueOrDefault(), int.MaxValue); + + // audio codec not supported + if ((why & TranscodeReason.AudioCodecNotSupported) != TranscodeReason.None) + { + // audio stream specified + if (options.AudioStreamIndex >= 0) + { + // TODO:fixme + if (!targetAudioStream.IsExternal) + { + Assert.DoesNotContain(targetAudioStream.Codec, val.AudioCodecs); + } + } + + // audio stream not specified + else + { + // TODO:fixme + Assert.All(audioStreams, stream => + { + if (!stream.IsExternal) + { + // Assert.DoesNotContain(stream.Codec, val.AudioCodecs); + } + }); + } + } } } @@ -182,7 +347,7 @@ namespace Jellyfin.MediaBrowser.Model.Tests { // what should the actual result be here? Assert.Null(val.SubProtocol); - Assert.EndsWith("/stream", url.Path, StringComparison.InvariantCulture); + Assert.EndsWith("/stream", uri.Path, StringComparison.InvariantCulture); Assert.False(val.EstimateContentLength); Assert.Equal(TranscodeSeekInfo.Auto, val.TranscodeSeekInfo); @@ -231,5 +396,23 @@ namespace Jellyfin.MediaBrowser.Model.Tests Profile = dp, }; } + + private static (string Path, NameValueCollection Query, string Filename, string Extension) ParseUri(StreamInfo val) + { + var href = val.ToUrl("media:", "ACCESSTOKEN").Split("?", 2); + var path = href[0]; + + var queryString = href.ElementAtOrDefault(1); + var query = string.IsNullOrEmpty(queryString) ? System.Web.HttpUtility.ParseQueryString(queryString ?? string.Empty) : new NameValueCollection(); + + var filename = System.IO.Path.GetFileNameWithoutExtension(path); + var extension = System.IO.Path.GetExtension(path); + if (extension.Length > 0) + { + extension = extension.Substring(1); + } + + return (path, query, filename, extension); + } } }