diff options
Diffstat (limited to 'MediaBrowser.Model/Dlna/StreamBuilder.cs')
| -rw-r--r-- | MediaBrowser.Model/Dlna/StreamBuilder.cs | 946 |
1 files changed, 516 insertions, 430 deletions
diff --git a/MediaBrowser.Model/Dlna/StreamBuilder.cs b/MediaBrowser.Model/Dlna/StreamBuilder.cs index 7654337e7..0f48e985d 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<ProfileCondition>(); - 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<CodecProfile>(); - 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<ProfileCondition>(); - 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<PlayMethod> 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<PlayMethod>(), GetTranscodeReasonsFromDirectPlayProfile(item, null, audioStream, options.Profile.DirectPlayProfiles)); + return (null, null, GetTranscodeReasonsFromDirectPlayProfile(item, null, audioStream, options.Profile.DirectPlayProfiles)); } var playMethods = new List<PlayMethod>(); 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<DirectPlayProfile> 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<string>(); - } - 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<string>(); + 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<MediaStream> candidateAudioStreams = audioStream == null ? Array.Empty<MediaStream>() : 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:", "<token>")); + + item.Container = NormalizeMediaSourceFormatIntoSingleContainer(item.Container, options.Profile, DlnaProfileType.Video, directPlayProfile); + return playlistItem; + } + + private TranscodingProfile GetVideoTranscodeProfile(MediaSourceInfo item, VideoOptions options, MediaStream videoStream, MediaStream audioStream, IEnumerable<MediaStream> 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<MediaStream> 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<MediaStream> 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<ProfileCondition>(); - 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<ProfileCondition>(); - 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<ProfileCondition>(); } - } - 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<ProfileCondition>(); - 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<ProfileCondition> 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<ProfileCondition> GetProfileConditionsForVideoAudio( + IEnumerable<CodecProfile> 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<ProfileCondition> GetProfileConditionsForAudio( + IEnumerable<CodecProfile> 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<ProfileCondition> 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; - } } } |
