diff options
Diffstat (limited to 'MediaBrowser.Model/Dlna/StreamBuilder.cs')
| -rw-r--r-- | MediaBrowser.Model/Dlna/StreamBuilder.cs | 1900 |
1 files changed, 1900 insertions, 0 deletions
diff --git a/MediaBrowser.Model/Dlna/StreamBuilder.cs b/MediaBrowser.Model/Dlna/StreamBuilder.cs new file mode 100644 index 000000000..840abf618 --- /dev/null +++ b/MediaBrowser.Model/Dlna/StreamBuilder.cs @@ -0,0 +1,1900 @@ +using MediaBrowser.Model.Dto; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Extensions; +using MediaBrowser.Model.Logging; +using MediaBrowser.Model.MediaInfo; +using MediaBrowser.Model.Session; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; + +namespace MediaBrowser.Model.Dlna +{ + public class StreamBuilder + { + private readonly ILogger _logger; + private readonly ITranscoderSupport _transcoderSupport; + + public StreamBuilder(ITranscoderSupport transcoderSupport, ILogger logger) + { + _transcoderSupport = transcoderSupport; + _logger = logger; + } + + public StreamBuilder(ILogger logger) + : this(new FullTranscoderSupport(), logger) + { + } + + public StreamInfo BuildAudioItem(AudioOptions options) + { + ValidateAudioInput(options); + + var mediaSources = new List<MediaSourceInfo>(); + foreach (MediaSourceInfo i in options.MediaSources) + { + if (string.IsNullOrEmpty(options.MediaSourceId) || + StringHelper.EqualsIgnoreCase(i.Id, options.MediaSourceId)) + { + mediaSources.Add(i); + } + } + + var streams = new List<StreamInfo>(); + foreach (MediaSourceInfo i in mediaSources) + { + StreamInfo streamInfo = BuildAudioItem(i, options); + if (streamInfo != null) + { + streams.Add(streamInfo); + } + } + + foreach (StreamInfo stream in streams) + { + stream.DeviceId = options.DeviceId; + stream.DeviceProfileId = options.Profile.Id; + } + + return GetOptimalStream(streams, options.GetMaxBitrate(true) ?? 0); + } + + public StreamInfo BuildVideoItem(VideoOptions options) + { + ValidateInput(options); + + var mediaSources = new List<MediaSourceInfo>(); + foreach (MediaSourceInfo i in options.MediaSources) + { + if (string.IsNullOrEmpty(options.MediaSourceId) || + StringHelper.EqualsIgnoreCase(i.Id, options.MediaSourceId)) + { + mediaSources.Add(i); + } + } + + var streams = new List<StreamInfo>(); + foreach (MediaSourceInfo i in mediaSources) + { + StreamInfo streamInfo = BuildVideoItem(i, options); + if (streamInfo != null) + { + streams.Add(streamInfo); + } + } + + foreach (StreamInfo stream in streams) + { + stream.DeviceId = options.DeviceId; + stream.DeviceProfileId = options.Profile.Id; + } + + return GetOptimalStream(streams, options.GetMaxBitrate(false) ?? 0); + } + + private StreamInfo GetOptimalStream(List<StreamInfo> streams, long maxBitrate) + { + var sorted = SortMediaSources(streams, maxBitrate); + + foreach (StreamInfo stream in sorted) + { + return stream; + } + + return null; + } + + private StreamInfo[] SortMediaSources(List<StreamInfo> streams, long maxBitrate) + { + return streams.OrderBy(i => + { + // Nothing beats direct playing a file + if (i.PlayMethod == PlayMethod.DirectPlay && i.MediaSource.Protocol == MediaProtocol.File) + { + return 0; + } + + return 1; + + }).ThenBy(i => + { + switch (i.PlayMethod) + { + // Let's assume direct streaming a file is just as desirable as direct playing a remote url + case PlayMethod.DirectStream: + case PlayMethod.DirectPlay: + return 0; + default: + return 1; + } + + }).ThenBy(i => + { + switch (i.MediaSource.Protocol) + { + case MediaProtocol.File: + return 0; + default: + return 1; + } + + }).ThenBy(i => + { + if (maxBitrate > 0) + { + if (i.MediaSource.Bitrate.HasValue) + { + return Math.Abs(i.MediaSource.Bitrate.Value - maxBitrate); + } + } + + return 0; + + }).ThenBy(streams.IndexOf).ToArray(); + } + + private TranscodeReason? GetTranscodeReasonForFailedCondition(ProfileCondition condition) + { + switch (condition.Property) + { + case ProfileConditionValue.AudioBitrate: + if (condition.Condition == ProfileConditionType.LessThanEqual) + { + return TranscodeReason.AudioBitrateNotSupported; + } + return TranscodeReason.AudioBitrateNotSupported; + + case ProfileConditionValue.AudioChannels: + if (condition.Condition == ProfileConditionType.LessThanEqual) + { + return TranscodeReason.AudioChannelsNotSupported; + } + return TranscodeReason.AudioChannelsNotSupported; + + case ProfileConditionValue.AudioProfile: + return TranscodeReason.AudioProfileNotSupported; + + case ProfileConditionValue.AudioSampleRate: + return TranscodeReason.AudioSampleRateNotSupported; + + case ProfileConditionValue.Has64BitOffsets: + // TODO + return null; + + case ProfileConditionValue.Height: + return TranscodeReason.VideoResolutionNotSupported; + + case ProfileConditionValue.IsAnamorphic: + return TranscodeReason.AnamorphicVideoNotSupported; + + case ProfileConditionValue.IsAvc: + // TODO + return null; + + case ProfileConditionValue.IsInterlaced: + return TranscodeReason.InterlacedVideoNotSupported; + + case ProfileConditionValue.IsSecondaryAudio: + return TranscodeReason.SecondaryAudioNotSupported; + + case ProfileConditionValue.NumAudioStreams: + // TODO + return null; + + case ProfileConditionValue.NumVideoStreams: + // TODO + return null; + + case ProfileConditionValue.PacketLength: + // TODO + return null; + + case ProfileConditionValue.RefFrames: + return TranscodeReason.RefFramesNotSupported; + + case ProfileConditionValue.VideoBitDepth: + return TranscodeReason.VideoBitDepthNotSupported; + + case ProfileConditionValue.AudioBitDepth: + return TranscodeReason.AudioBitDepthNotSupported; + + case ProfileConditionValue.VideoBitrate: + return TranscodeReason.VideoBitrateNotSupported; + + case ProfileConditionValue.VideoCodecTag: + return TranscodeReason.VideoCodecNotSupported; + + case ProfileConditionValue.VideoFramerate: + return TranscodeReason.VideoFramerateNotSupported; + + case ProfileConditionValue.VideoLevel: + return TranscodeReason.VideoLevelNotSupported; + + case ProfileConditionValue.VideoProfile: + return TranscodeReason.VideoProfileNotSupported; + + case ProfileConditionValue.VideoTimestamp: + // TODO + return null; + + case ProfileConditionValue.Width: + return TranscodeReason.VideoResolutionNotSupported; + + default: + return null; + } + } + + public static string NormalizeMediaSourceFormatIntoSingleContainer(string inputContainer, string unused1, DeviceProfile profile, DlnaProfileType type) + { + if (string.IsNullOrEmpty(inputContainer)) + { + return null; + } + + var formats = ContainerProfile.SplitValue(inputContainer); + + if (formats.Length == 1) + { + return formats[0]; + } + + if (profile != null) + { + foreach (var format in formats) + { + foreach (var directPlayProfile in profile.DirectPlayProfiles) + { + if (directPlayProfile.Type == type) + { + if (directPlayProfile.SupportsContainer(format)) + { + return format; + } + } + } + } + } + + return formats[0]; + } + + private StreamInfo BuildAudioItem(MediaSourceInfo item, AudioOptions options) + { + var transcodeReasons = new List<TranscodeReason>(); + + StreamInfo playlistItem = new StreamInfo + { + ItemId = options.ItemId, + MediaType = DlnaProfileType.Audio, + MediaSource = item, + RunTimeTicks = item.RunTimeTicks, + Context = options.Context, + DeviceProfile = options.Profile + }; + + if (options.ForceDirectPlay) + { + playlistItem.PlayMethod = PlayMethod.DirectPlay; + playlistItem.Container = NormalizeMediaSourceFormatIntoSingleContainer(item.Container, item.Path, options.Profile, DlnaProfileType.Audio); + return playlistItem; + } + + if (options.ForceDirectStream) + { + playlistItem.PlayMethod = PlayMethod.DirectStream; + playlistItem.Container = NormalizeMediaSourceFormatIntoSingleContainer(item.Container, item.Path, options.Profile, DlnaProfileType.Audio); + return playlistItem; + } + + MediaStream audioStream = item.GetDefaultAudioStream(null); + + var directPlayInfo = GetAudioDirectPlayMethods(item, audioStream, options); + + var directPlayMethods = directPlayInfo.Item1; + transcodeReasons.AddRange(directPlayInfo.Item2); + + ConditionProcessor conditionProcessor = new ConditionProcessor(); + + int? inputAudioChannels = audioStream == null ? null : audioStream.Channels; + int? inputAudioBitrate = audioStream == null ? null : audioStream.BitDepth; + int? inputAudioSampleRate = audioStream == null ? null : audioStream.SampleRate; + int? inputAudioBitDepth = audioStream == null ? null : audioStream.BitDepth; + + if (directPlayMethods.Count > 0) + { + string audioCodec = audioStream == null ? null : audioStream.Codec; + + // Make sure audio codec profiles are satisfied + var conditions = new List<ProfileCondition>(); + foreach (CodecProfile 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) + { + foreach (ProfileCondition c in i.Conditions) + { + conditions.Add(c); + } + } + } + } + + bool all = true; + foreach (ProfileCondition c in conditions) + { + 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); + } + all = false; + break; + } + } + + if (all) + { + if (directPlayMethods.Contains(PlayMethod.DirectStream)) + { + playlistItem.PlayMethod = PlayMethod.DirectStream; + } + + playlistItem.Container = NormalizeMediaSourceFormatIntoSingleContainer(item.Container, item.Path, options.Profile, DlnaProfileType.Audio); + + return playlistItem; + } + } + + TranscodingProfile transcodingProfile = null; + foreach (TranscodingProfile i in options.Profile.TranscodingProfiles) + { + if (i.Type == playlistItem.MediaType && i.Context == options.Context) + { + if (_transcoderSupport.CanEncodeToAudioCodec(i.AudioCodec ?? i.Container)) + { + transcodingProfile = i; + break; + } + } + } + + if (transcodingProfile != null) + { + if (!item.SupportsTranscoding) + { + return null; + } + + SetStreamInfoOptionsFromTranscodingProfile(playlistItem, transcodingProfile); + + var audioCodecProfiles = new List<CodecProfile>(); + foreach (CodecProfile 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 (CodecProfile i in audioCodecProfiles) + { + 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) + { + foreach (ProfileCondition c in i.Conditions) + { + audioTranscodingConditions.Add(c); + } + } + } + + ApplyTranscodingConditions(playlistItem, audioTranscodingConditions, null, true, true); + + // Honor requested max channels + playlistItem.GlobalMaxAudioChannels = options.MaxAudioChannels; + + var configuredBitrate = options.GetMaxBitrate(true); + + long transcodingBitrate = options.AudioTranscodingBitrate ?? + (options.Context == EncodingContext.Streaming ? options.Profile.MusicStreamingTranscodingBitrate : null) ?? + configuredBitrate ?? + 128000; + + if (configuredBitrate.HasValue) + { + transcodingBitrate = Math.Min(configuredBitrate.Value, transcodingBitrate); + } + + var longBitrate = Math.Min(transcodingBitrate, playlistItem.AudioBitrate ?? transcodingBitrate); + playlistItem.AudioBitrate = longBitrate > int.MaxValue ? int.MaxValue : Convert.ToInt32(longBitrate); + } + + playlistItem.TranscodeReasons = transcodeReasons.ToArray(); + return playlistItem; + } + + private long? GetBitrateForDirectPlayCheck(MediaSourceInfo item, AudioOptions options, bool isAudio) + { + if (item.Protocol == MediaProtocol.File) + { + return options.Profile.MaxStaticBitrate; + } + + return options.GetMaxBitrate(isAudio); + } + + private Tuple<List<PlayMethod>, List<TranscodeReason>> GetAudioDirectPlayMethods(MediaSourceInfo item, MediaStream audioStream, AudioOptions options) + { + var transcodeReasons = new List<TranscodeReason>(); + + DirectPlayProfile directPlayProfile = null; + foreach (DirectPlayProfile i in options.Profile.DirectPlayProfiles) + { + if (i.Type == DlnaProfileType.Audio && IsAudioDirectPlaySupported(i, item, audioStream)) + { + directPlayProfile = i; + break; + } + } + + var playMethods = new List<PlayMethod>(); + + if (directPlayProfile != null) + { + // 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 (options.EnableDirectStream) + { + playMethods.Add(PlayMethod.DirectStream); + } + } + else + { + transcodeReasons.Add(TranscodeReason.ContainerBitrateExceedsLimit); + } + } + + // The profile describes what the device supports + // 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 (options.EnableDirectPlay) + { + playMethods.Add(PlayMethod.DirectPlay); + } + } + else + { + transcodeReasons.Add(TranscodeReason.ContainerBitrateExceedsLimit); + } + } + } + else + { + transcodeReasons.InsertRange(0, GetTranscodeReasonsFromDirectPlayProfile(item, null, audioStream, options.Profile.DirectPlayProfiles)); + + _logger.Info("Profile: {0}, No direct play profiles found for Path: {1}", + options.Profile.Name ?? "Unknown Profile", + item.Path ?? "Unknown path"); + } + + if (playMethods.Count > 0) + { + transcodeReasons.Clear(); + } + else + { + transcodeReasons = transcodeReasons.Distinct().ToList(); + } + + return new Tuple<List<PlayMethod>, List<TranscodeReason>>(playMethods, transcodeReasons); + } + + private List<TranscodeReason> GetTranscodeReasonsFromDirectPlayProfile(MediaSourceInfo item, MediaStream videoStream, MediaStream audioStream, IEnumerable<DirectPlayProfile> directPlayProfiles) + { + var list = new List<TranscodeReason>(); + var containerSupported = false; + var audioSupported = false; + var videoSupported = false; + + foreach (var profile in directPlayProfiles) + { + audioSupported = false; + videoSupported = false; + + // Check container type + if (profile.SupportsContainer(item.Container)) + { + containerSupported = true; + + if (videoStream != null) + { + if (profile.SupportsVideoCodec(videoStream.Codec)) + { + videoSupported = true; + } + } + + if (audioStream != null) + { + if (profile.SupportsAudioCodec(audioStream.Codec)) + { + audioSupported = true; + } + } + + if (videoSupported && audioSupported) + { + break; + } + } + } + + if (!containerSupported) + { + list.Add(TranscodeReason.ContainerNotSupported); + } + + if (videoStream != null && !videoSupported) + { + list.Add(TranscodeReason.VideoCodecNotSupported); + } + + if (audioStream != null && !audioSupported) + { + list.Add(TranscodeReason.AudioCodecNotSupported); + } + + return list; + } + + private int? GetDefaultSubtitleStreamIndex(MediaSourceInfo item, SubtitleProfile[] subtitleProfiles) + { + int highestScore = -1; + + foreach (MediaStream stream in item.MediaStreams) + { + if (stream.Type == MediaStreamType.Subtitle && stream.Score.HasValue) + { + if (stream.Score.Value > highestScore) + { + highestScore = stream.Score.Value; + } + } + } + + var topStreams = new List<MediaStream>(); + foreach (MediaStream stream in item.MediaStreams) + { + if (stream.Type == MediaStreamType.Subtitle && stream.Score.HasValue && stream.Score.Value == highestScore) + { + topStreams.Add(stream); + } + } + + // If multiple streams have an equal score, try to pick the most efficient one + if (topStreams.Count > 1) + { + foreach (MediaStream stream in topStreams) + { + foreach (SubtitleProfile profile in subtitleProfiles) + { + if (profile.Method == SubtitleDeliveryMethod.External && StringHelper.EqualsIgnoreCase(profile.Format, stream.Codec)) + { + return stream.Index; + } + } + } + } + + // If no optimization panned out, just use the original default + return item.DefaultSubtitleStreamIndex; + } + + private void SetStreamInfoOptionsFromTranscodingProfile(StreamInfo playlistItem, TranscodingProfile transcodingProfile) + { + if (string.IsNullOrEmpty(transcodingProfile.AudioCodec)) + { + playlistItem.AudioCodecs = Array.Empty<string>(); + } + else + { + playlistItem.AudioCodecs = transcodingProfile.AudioCodec.Split(','); + } + + playlistItem.Container = transcodingProfile.Container; + playlistItem.EstimateContentLength = transcodingProfile.EstimateContentLength; + playlistItem.TranscodeSeekInfo = transcodingProfile.TranscodeSeekInfo; + + if (string.IsNullOrEmpty(transcodingProfile.VideoCodec)) + { + playlistItem.VideoCodecs = Array.Empty<string>(); + } + else + { + playlistItem.VideoCodecs = transcodingProfile.VideoCodec.Split(','); + } + + playlistItem.CopyTimestamps = transcodingProfile.CopyTimestamps; + playlistItem.EnableSubtitlesInManifest = transcodingProfile.EnableSubtitlesInManifest; + playlistItem.EnableMpegtsM2TsMode = transcodingProfile.EnableMpegtsM2TsMode; + + playlistItem.BreakOnNonKeyFrames = transcodingProfile.BreakOnNonKeyFrames; + + if (transcodingProfile.MinSegments > 0) + { + playlistItem.MinSegments = transcodingProfile.MinSegments; + } + if (transcodingProfile.SegmentLength > 0) + { + playlistItem.SegmentLength = transcodingProfile.SegmentLength; + } + playlistItem.SubProtocol = transcodingProfile.Protocol; + + if (!string.IsNullOrEmpty(transcodingProfile.MaxAudioChannels)) + { + int transcodingMaxAudioChannels; + if (int.TryParse(transcodingProfile.MaxAudioChannels, NumberStyles.Any, CultureInfo.InvariantCulture, out transcodingMaxAudioChannels)) + { + playlistItem.TranscodingMaxAudioChannels = transcodingMaxAudioChannels; + } + } + } + + private StreamInfo BuildVideoItem(MediaSourceInfo item, VideoOptions options) + { + if (item == null) + { + throw new ArgumentNullException("item"); + } + + var transcodeReasons = new List<TranscodeReason>(); + + StreamInfo playlistItem = new StreamInfo + { + ItemId = options.ItemId, + MediaType = DlnaProfileType.Video, + MediaSource = item, + RunTimeTicks = item.RunTimeTicks, + Context = options.Context, + DeviceProfile = options.Profile + }; + + playlistItem.SubtitleStreamIndex = options.SubtitleStreamIndex ?? GetDefaultSubtitleStreamIndex(item, options.Profile.SubtitleProfiles); + MediaStream subtitleStream = playlistItem.SubtitleStreamIndex.HasValue ? item.GetMediaStream(MediaStreamType.Subtitle, playlistItem.SubtitleStreamIndex.Value) : null; + + MediaStream audioStream = item.GetDefaultAudioStream(options.AudioStreamIndex ?? item.DefaultAudioStreamIndex); + if (audioStream != null) + { + playlistItem.AudioStreamIndex = audioStream.Index; + } + + MediaStream videoStream = item.VideoStream; + + // TODO: This doesn't accout for situation of device being able to handle media bitrate, but wifi connection not fast enough + var directPlayEligibilityResult = IsEligibleForDirectPlay(item, GetBitrateForDirectPlayCheck(item, options, true) ?? 0, subtitleStream, options, PlayMethod.DirectPlay); + var directStreamEligibilityResult = IsEligibleForDirectPlay(item, options.GetMaxBitrate(false) ?? 0, subtitleStream, options, PlayMethod.DirectStream); + bool isEligibleForDirectPlay = options.EnableDirectPlay && (options.ForceDirectPlay || directPlayEligibilityResult.Item1); + bool isEligibleForDirectStream = options.EnableDirectStream && (options.ForceDirectStream || directStreamEligibilityResult.Item1); + + _logger.Info("Profile: {0}, Path: {1}, isEligibleForDirectPlay: {2}, isEligibleForDirectStream: {3}", + options.Profile.Name ?? "Unknown Profile", + item.Path ?? "Unknown path", + isEligibleForDirectPlay, + isEligibleForDirectStream); + + if (isEligibleForDirectPlay || isEligibleForDirectStream) + { + // See if it can be direct played + var directPlayInfo = GetVideoDirectPlayProfile(options, item, videoStream, audioStream, isEligibleForDirectPlay, isEligibleForDirectStream); + var directPlay = directPlayInfo.Item1; + + if (directPlay != null) + { + playlistItem.PlayMethod = directPlay.Value; + playlistItem.Container = NormalizeMediaSourceFormatIntoSingleContainer(item.Container, item.Path, options.Profile, DlnaProfileType.Video); + + if (subtitleStream != null) + { + SubtitleProfile subtitleProfile = GetSubtitleProfile(item, subtitleStream, options.Profile.SubtitleProfiles, directPlay.Value, _transcoderSupport, item.Container, null); + + playlistItem.SubtitleDeliveryMethod = subtitleProfile.Method; + playlistItem.SubtitleFormat = subtitleProfile.Format; + } + + return playlistItem; + } + + transcodeReasons.AddRange(directPlayInfo.Item2); + } + + if (directPlayEligibilityResult.Item2.HasValue) + { + transcodeReasons.Add(directPlayEligibilityResult.Item2.Value); + } + + if (directStreamEligibilityResult.Item2.HasValue) + { + transcodeReasons.Add(directStreamEligibilityResult.Item2.Value); + } + + // Can't direct play, find the transcoding profile + TranscodingProfile transcodingProfile = null; + foreach (TranscodingProfile i in options.Profile.TranscodingProfiles) + { + if (i.Type == playlistItem.MediaType && i.Context == options.Context) + { + transcodingProfile = i; + break; + } + } + + if (transcodingProfile != null) + { + if (!item.SupportsTranscoding) + { + return null; + } + + if (subtitleStream != null) + { + SubtitleProfile 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.PlayMethod = PlayMethod.Transcode; + + SetStreamInfoOptionsFromTranscodingProfile(playlistItem, transcodingProfile); + + ConditionProcessor conditionProcessor = new ConditionProcessor(); + + var isFirstAppliedCodecProfile = true; + foreach (CodecProfile i in options.Profile.CodecProfiles) + { + if (i.Type == CodecType.Video && i.ContainsAnyCodec(transcodingProfile.VideoCodec, transcodingProfile.Container)) + { + bool applyConditions = true; + foreach (ProfileCondition applyCondition in i.ApplyConditions) + { + int? width = videoStream == null ? null : videoStream.Width; + int? height = videoStream == null ? null : videoStream.Height; + int? bitDepth = videoStream == null ? null : videoStream.BitDepth; + int? videoBitrate = videoStream == null ? null : videoStream.BitRate; + double? videoLevel = videoStream == null ? null : videoStream.Level; + string videoProfile = videoStream == null ? null : videoStream.Profile; + float videoFramerate = videoStream == null ? 0 : videoStream.AverageFrameRate ?? videoStream.AverageFrameRate ?? 0; + bool? isAnamorphic = videoStream == null ? null : videoStream.IsAnamorphic; + bool? isInterlaced = videoStream == null ? (bool?)null : videoStream.IsInterlaced; + string videoCodecTag = videoStream == null ? null : videoStream.CodecTag; + bool? isAvc = videoStream == null ? null : videoStream.IsAVC; + + TransportStreamTimestamp? timestamp = videoStream == null ? TransportStreamTimestamp.None : item.Timestamp; + int? packetLength = videoStream == null ? null : videoStream.PacketLength; + int? refFrames = videoStream == null ? null : 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; + } + } + } + } + } + + // Honor requested max channels + playlistItem.GlobalMaxAudioChannels = options.MaxAudioChannels; + + int audioBitrate = GetAudioBitrate(playlistItem.SubProtocol, options.GetMaxBitrate(false) ?? 0, playlistItem.TargetAudioCodec, audioStream, playlistItem); + playlistItem.AudioBitrate = Math.Min(playlistItem.AudioBitrate ?? audioBitrate, audioBitrate); + + isFirstAppliedCodecProfile = true; + foreach (CodecProfile i in options.Profile.CodecProfiles) + { + if (i.Type == CodecType.VideoAudio && i.ContainsAnyCodec(transcodingProfile.AudioCodec, transcodingProfile.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; + } + } + } + } + } + + var maxBitrateSetting = options.GetMaxBitrate(false); + // Honor max rate + if (maxBitrateSetting.HasValue) + { + var availableBitrateForVideo = maxBitrateSetting.Value; + + if (playlistItem.AudioBitrate.HasValue) + { + availableBitrateForVideo -= playlistItem.AudioBitrate.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); + } + } + + playlistItem.TranscodeReasons = transcodeReasons.ToArray(); + + return playlistItem; + } + + private int GetDefaultAudioBitrateIfUnknown(MediaStream audioStream) + { + if ((audioStream.Channels ?? 0) >= 6) + { + return 384000; + } + + return 192000; + } + + private int GetAudioBitrate(string subProtocol, long maxTotalBitrate, string[] targetAudioCodecs, MediaStream audioStream, StreamInfo item) + { + var targetAudioCodec = targetAudioCodecs.Length == 0 ? null : targetAudioCodecs[0]; + + var targetAudioChannels = item.GetTargetAudioChannels(targetAudioCodec); + + int defaultBitrate = audioStream == null ? 192000 : audioStream.BitRate ?? GetDefaultAudioBitrateIfUnknown(audioStream); + + // Reduce the bitrate if we're downmixing + if (targetAudioChannels.HasValue && audioStream != null && audioStream.Channels.HasValue && targetAudioChannels.Value < audioStream.Channels.Value) + { + defaultBitrate = targetAudioChannels.Value <= 2 ? 128000 : 192000; + } + + int encoderAudioBitrateLimit = int.MaxValue; + + if (audioStream != null) + { + // Seeing webm encoding failures when source has 1 audio channel and 22k bitrate. + // Any attempts to transcode over 64k will fail + if (audioStream.Channels.HasValue && + audioStream.Channels.Value == 1) + { + if ((audioStream.BitRate ?? 0) < 64000) + { + encoderAudioBitrateLimit = 64000; + } + } + } + + if (maxTotalBitrate > 0) + { + defaultBitrate = Math.Min(GetMaxAudioBitrateForTotalBitrate(maxTotalBitrate), defaultBitrate); + } + + return Math.Min(defaultBitrate, encoderAudioBitrateLimit); + } + + private int GetMaxAudioBitrateForTotalBitrate(long totalBitrate) + { + if (totalBitrate <= 640000) + { + return 128000; + } + + if (totalBitrate <= 2000000) + { + return 384000; + } + + if (totalBitrate <= 3000000) + { + return 448000; + } + + return 640000; + } + + private Tuple<PlayMethod?, List<TranscodeReason>> GetVideoDirectPlayProfile(VideoOptions options, + MediaSourceInfo mediaSource, + MediaStream videoStream, + MediaStream audioStream, + bool isEligibleForDirectPlay, + bool isEligibleForDirectStream) + { + DeviceProfile profile = options.Profile; + + if (options.ForceDirectPlay) + { + return new Tuple<PlayMethod?, List<TranscodeReason>>(PlayMethod.DirectPlay, new List<TranscodeReason>()); + } + if (options.ForceDirectStream) + { + return new Tuple<PlayMethod?, List<TranscodeReason>>(PlayMethod.DirectStream, new List<TranscodeReason>()); + } + + // See if it can be direct played + DirectPlayProfile directPlay = null; + foreach (DirectPlayProfile i in profile.DirectPlayProfiles) + { + if (i.Type == DlnaProfileType.Video && IsVideoDirectPlaySupported(i, mediaSource, videoStream, audioStream)) + { + directPlay = i; + break; + } + } + + if (directPlay == null) + { + _logger.Info("Profile: {0}, No direct play profiles found for Path: {1}", + profile.Name ?? "Unknown Profile", + mediaSource.Path ?? "Unknown path"); + + return new Tuple<PlayMethod?, List<TranscodeReason>>(null, GetTranscodeReasonsFromDirectPlayProfile(mediaSource, videoStream, audioStream, profile.DirectPlayProfiles)); + } + + string container = mediaSource.Container; + + var conditions = new List<ProfileCondition>(); + foreach (ContainerProfile i in profile.ContainerProfiles) + { + if (i.Type == DlnaProfileType.Video && + i.ContainsContainer(container)) + { + foreach (ProfileCondition c in i.Conditions) + { + conditions.Add(c); + } + } + } + + ConditionProcessor conditionProcessor = new ConditionProcessor(); + + int? width = videoStream == null ? null : videoStream.Width; + int? height = videoStream == null ? null : videoStream.Height; + int? bitDepth = videoStream == null ? null : videoStream.BitDepth; + int? videoBitrate = videoStream == null ? null : videoStream.BitRate; + double? videoLevel = videoStream == null ? null : videoStream.Level; + string videoProfile = videoStream == null ? null : videoStream.Profile; + float videoFramerate = videoStream == null ? 0 : videoStream.AverageFrameRate ?? videoStream.AverageFrameRate ?? 0; + bool? isAnamorphic = videoStream == null ? null : videoStream.IsAnamorphic; + bool? isInterlaced = videoStream == null ? (bool?)null : videoStream.IsInterlaced; + string videoCodecTag = videoStream == null ? null : videoStream.CodecTag; + bool? isAvc = videoStream == null ? null : videoStream.IsAVC; + + int? audioBitrate = audioStream == null ? null : audioStream.BitRate; + int? audioChannels = audioStream == null ? null : audioStream.Channels; + string audioProfile = audioStream == null ? null : audioStream.Profile; + int? audioSampleRate = audioStream == null ? null : audioStream.SampleRate; + int? audioBitDepth = audioStream == null ? null : audioStream.BitDepth; + + TransportStreamTimestamp? timestamp = videoStream == null ? TransportStreamTimestamp.None : mediaSource.Timestamp; + int? packetLength = videoStream == null ? null : videoStream.PacketLength; + int? refFrames = videoStream == null ? null : videoStream.RefFrames; + + 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 transcodeReason = GetTranscodeReasonForFailedCondition(i); + var transcodeReasons = transcodeReason.HasValue + ? new List<TranscodeReason> { transcodeReason.Value } + : new List<TranscodeReason> { }; + + return new Tuple<PlayMethod?, List<TranscodeReason>>(null, transcodeReasons); + } + } + + string videoCodec = videoStream == null ? null : videoStream.Codec; + + conditions = new List<ProfileCondition>(); + foreach (CodecProfile i in profile.CodecProfiles) + { + if (i.Type == CodecType.Video && i.ContainsAnyCodec(videoCodec, container)) + { + bool applyConditions = true; + foreach (ProfileCondition applyCondition in i.ApplyConditions) + { + if (!conditionProcessor.IsVideoConditionSatisfied(applyCondition, width, height, bitDepth, videoBitrate, videoProfile, videoLevel, videoFramerate, packetLength, timestamp, isAnamorphic, isInterlaced, refFrames, numVideoStreams, numAudioStreams, videoCodecTag, isAvc)) + { + //LogConditionFailure(profile, "VideoCodecProfile.ApplyConditions", applyCondition, mediaSource); + applyConditions = false; + break; + } + } + + if (applyConditions) + { + foreach (ProfileCondition c in i.Conditions) + { + conditions.Add(c); + } + } + } + } + + 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, "VideoCodecProfile", i, mediaSource); + + var transcodeReason = GetTranscodeReasonForFailedCondition(i); + var transcodeReasons = transcodeReason.HasValue + ? new List<TranscodeReason> { transcodeReason.Value } + : new List<TranscodeReason> { }; + + return new Tuple<PlayMethod?, List<TranscodeReason>>(null, transcodeReasons); + } + } + + if (audioStream != null) + { + string audioCodec = audioStream.Codec; + + conditions = new List<ProfileCondition>(); + bool? isSecondaryAudio = audioStream == null ? null : mediaSource.IsSecondaryAudio(audioStream); + + foreach (CodecProfile i in profile.CodecProfiles) + { + if (i.Type == CodecType.VideoAudio && i.ContainsAnyCodec(audioCodec, container)) + { + bool applyConditions = true; + foreach (ProfileCondition applyCondition in i.ApplyConditions) + { + if (!conditionProcessor.IsVideoAudioConditionSatisfied(applyCondition, audioChannels, audioBitrate, audioSampleRate, audioBitDepth, audioProfile, isSecondaryAudio)) + { + //LogConditionFailure(profile, "VideoAudioCodecProfile.ApplyConditions", applyCondition, mediaSource); + applyConditions = false; + break; + } + } + + if (applyConditions) + { + foreach (ProfileCondition c in i.Conditions) + { + conditions.Add(c); + } + } + } + } + + foreach (ProfileCondition i in conditions) + { + if (!conditionProcessor.IsVideoAudioConditionSatisfied(i, audioChannels, audioBitrate, audioSampleRate, audioBitDepth, audioProfile, isSecondaryAudio)) + { + LogConditionFailure(profile, "VideoAudioCodecProfile", i, mediaSource); + + var transcodeReason = GetTranscodeReasonForFailedCondition(i); + var transcodeReasons = transcodeReason.HasValue + ? new List<TranscodeReason> { transcodeReason.Value } + : new List<TranscodeReason> { }; + + return new Tuple<PlayMethod?, List<TranscodeReason>>(null, transcodeReasons); + } + } + } + + if (isEligibleForDirectStream && mediaSource.SupportsDirectStream) + { + return new Tuple<PlayMethod?, List<TranscodeReason>>(PlayMethod.DirectStream, new List<TranscodeReason>()); + } + + return new Tuple<PlayMethod?, List<TranscodeReason>>(null, new List<TranscodeReason> { TranscodeReason.ContainerBitrateExceedsLimit }); + } + + private void LogConditionFailure(DeviceProfile profile, string type, ProfileCondition condition, MediaSourceInfo mediaSource) + { + _logger.Info("Profile: {0}, DirectPlay=false. Reason={1}.{2} Condition: {3}. ConditionValue: {4}. IsRequired: {5}. Path: {6}", + type, + profile.Name ?? "Unknown Profile", + condition.Property, + condition.Condition, + condition.Value ?? string.Empty, + condition.IsRequired, + mediaSource.Path ?? "Unknown path"); + } + + private ValueTuple<bool, TranscodeReason?> IsEligibleForDirectPlay(MediaSourceInfo item, + long maxBitrate, + MediaStream subtitleStream, + VideoOptions options, + PlayMethod playMethod) + { + if (subtitleStream != null) + { + SubtitleProfile subtitleProfile = GetSubtitleProfile(item, subtitleStream, options.Profile.SubtitleProfiles, playMethod, _transcoderSupport, item.Container, null); + + if (subtitleProfile.Method != SubtitleDeliveryMethod.External && subtitleProfile.Method != SubtitleDeliveryMethod.Embed) + { + _logger.Info("Not eligible for {0} due to unsupported subtitles", playMethod); + return new ValueTuple<bool, TranscodeReason?>(false, TranscodeReason.SubtitleCodecNotSupported); + } + } + + var result = IsAudioEligibleForDirectPlay(item, maxBitrate, playMethod); + + if (result) + { + return new ValueTuple<bool, TranscodeReason?>(result, null); + } + + return new ValueTuple<bool, TranscodeReason?>(result, TranscodeReason.ContainerBitrateExceedsLimit); + } + + public static SubtitleProfile GetSubtitleProfile(MediaSourceInfo mediaSource, MediaStream subtitleStream, SubtitleProfile[] subtitleProfiles, PlayMethod playMethod, ITranscoderSupport transcoderSupport, string outputContainer, string transcodingSubProtocol) + { + if (!subtitleStream.IsExternal && (playMethod != PlayMethod.Transcode || !string.Equals(transcodingSubProtocol, "hls", StringComparison.OrdinalIgnoreCase))) + { + // Look for supported embedded subs of the same format + foreach (SubtitleProfile profile in subtitleProfiles) + { + if (!profile.SupportsLanguage(subtitleStream.Language)) + { + continue; + } + + if (profile.Method != SubtitleDeliveryMethod.Embed) + { + continue; + } + + if (!ContainerProfile.ContainsContainer(profile.Container, outputContainer)) + { + continue; + } + + if (playMethod == PlayMethod.Transcode && !IsSubtitleEmbedSupported(subtitleStream, profile, transcodingSubProtocol, outputContainer)) + { + continue; + } + + if (subtitleStream.IsTextSubtitleStream == MediaStream.IsTextFormat(profile.Format) && StringHelper.EqualsIgnoreCase(profile.Format, subtitleStream.Codec)) + { + return profile; + } + } + + // Look for supported embedded subs of a convertible format + foreach (SubtitleProfile profile in subtitleProfiles) + { + if (!profile.SupportsLanguage(subtitleStream.Language)) + { + continue; + } + + if (profile.Method != SubtitleDeliveryMethod.Embed) + { + continue; + } + + if (!ContainerProfile.ContainsContainer(profile.Container, outputContainer)) + { + continue; + } + + if (playMethod == PlayMethod.Transcode && !IsSubtitleEmbedSupported(subtitleStream, profile, transcodingSubProtocol, outputContainer)) + { + continue; + } + + if (subtitleStream.IsTextSubtitleStream && subtitleStream.SupportsSubtitleConversionTo(profile.Format)) + { + return profile; + } + } + } + + // Look for an external or hls profile that matches the stream type (text/graphical) and doesn't require conversion + return GetExternalSubtitleProfile(mediaSource, subtitleStream, subtitleProfiles, playMethod, transcoderSupport, false) ?? + GetExternalSubtitleProfile(mediaSource, subtitleStream, subtitleProfiles, playMethod, transcoderSupport, true) ?? + new SubtitleProfile + { + Method = SubtitleDeliveryMethod.Encode, + Format = subtitleStream.Codec + }; + } + + private static bool IsSubtitleEmbedSupported(MediaStream subtitleStream, SubtitleProfile subtitleProfile, string transcodingSubProtocol, string transcodingContainer) + { + if (!string.IsNullOrEmpty(transcodingContainer)) + { + var normalizedContainers = ContainerProfile.SplitValue(transcodingContainer); + + if (ContainerProfile.ContainsContainer(normalizedContainers, "ts")) + { + return false; + } + if (ContainerProfile.ContainsContainer(normalizedContainers, "mpegts")) + { + return false; + } + if (ContainerProfile.ContainsContainer(normalizedContainers, "mp4")) + { + return false; + } + if (ContainerProfile.ContainsContainer(normalizedContainers, "mkv") || + ContainerProfile.ContainsContainer(normalizedContainers, "matroska")) + { + return true; + } + } + + return false; + } + + private static SubtitleProfile GetExternalSubtitleProfile(MediaSourceInfo mediaSource, MediaStream subtitleStream, SubtitleProfile[] subtitleProfiles, PlayMethod playMethod, ITranscoderSupport transcoderSupport, bool allowConversion) + { + foreach (SubtitleProfile profile in subtitleProfiles) + { + if (profile.Method != SubtitleDeliveryMethod.External && profile.Method != SubtitleDeliveryMethod.Hls) + { + continue; + } + + if (profile.Method == SubtitleDeliveryMethod.Hls && playMethod != PlayMethod.Transcode) + { + continue; + } + + if (!profile.SupportsLanguage(subtitleStream.Language)) + { + continue; + } + + if (!subtitleStream.IsExternal && !transcoderSupport.CanExtractSubtitles(subtitleStream.Codec)) + { + continue; + } + + if ((profile.Method == SubtitleDeliveryMethod.External && subtitleStream.IsTextSubtitleStream == MediaStream.IsTextFormat(profile.Format)) || + (profile.Method == SubtitleDeliveryMethod.Hls && subtitleStream.IsTextSubtitleStream)) + { + bool requiresConversion = !StringHelper.EqualsIgnoreCase(subtitleStream.Codec, profile.Format); + + if (!requiresConversion) + { + return profile; + } + + if (!allowConversion) + { + continue; + } + + // TODO: Build this into subtitleStream.SupportsExternalStream + if (mediaSource.IsInfiniteStream) + { + continue; + } + + if (subtitleStream.IsTextSubtitleStream && subtitleStream.SupportsExternalStream && subtitleStream.SupportsSubtitleConversionTo(profile.Format)) + { + return profile; + } + } + } + + return null; + } + + private bool IsAudioEligibleForDirectPlay(MediaSourceInfo item, long maxBitrate, PlayMethod playMethod) + { + // Don't restrict by bitrate if coming from an external domain + if (item.IsRemote) + { + return true; + } + + var requestedMaxBitrate = maxBitrate > 0 ? maxBitrate : 1000000; + + // If we don't know the bitrate, then force a transcode if requested max bitrate is under 40 mbps + var itemBitrate = item.Bitrate ?? + 40000000; + + if (itemBitrate > requestedMaxBitrate) + { + _logger.Info("Bitrate exceeds " + playMethod + " limit: media bitrate: {0}, max bitrate: {1}", itemBitrate.ToString(CultureInfo.InvariantCulture), requestedMaxBitrate.ToString(CultureInfo.InvariantCulture)); + return false; + } + + return true; + } + + private void ValidateInput(VideoOptions options) + { + ValidateAudioInput(options); + + if (options.AudioStreamIndex.HasValue && string.IsNullOrEmpty(options.MediaSourceId)) + { + throw new ArgumentException("MediaSourceId is required when a specific audio stream is requested"); + } + + if (options.SubtitleStreamIndex.HasValue && string.IsNullOrEmpty(options.MediaSourceId)) + { + throw new ArgumentException("MediaSourceId is required when a specific subtitle stream is requested"); + } + } + + private void ValidateAudioInput(AudioOptions options) + { + if (options.ItemId.Equals(Guid.Empty)) + { + throw new ArgumentException("ItemId is required"); + } + if (string.IsNullOrEmpty(options.DeviceId)) + { + throw new ArgumentException("DeviceId is required"); + } + if (options.Profile == null) + { + throw new ArgumentException("Profile is required"); + } + if (options.MediaSources == null) + { + throw new ArgumentException("MediaSources is required"); + } + } + + private void ApplyTranscodingConditions(StreamInfo item, List<CodecProfile> codecProfiles) + { + foreach (var profile in codecProfiles) + { + ApplyTranscodingConditions(item, profile); + } + } + + private void ApplyTranscodingConditions(StreamInfo item, CodecProfile codecProfile) + { + var codecs = ContainerProfile.SplitValue(codecProfile.Codec); + if (codecs.Length == 0) + { + ApplyTranscodingConditions(item, codecProfile.Conditions, null, true, true); + return; + } + + var enableNonQualified = true; + + foreach (var codec in codecs) + { + ApplyTranscodingConditions(item, codecProfile.Conditions, codec, true, enableNonQualified); + enableNonQualified = false; + } + } + + private void ApplyTranscodingConditions(StreamInfo item, IEnumerable<ProfileCondition> conditions, string qualifier, bool enableQualifiedConditions, bool enableNonQualifiedConditions) + { + foreach (ProfileCondition condition in conditions) + { + string value = condition.Value; + + if (string.IsNullOrEmpty(value)) + { + continue; + } + + // No way to express this + if (condition.Condition == ProfileConditionType.GreaterThanEqual) + { + continue; + } + + switch (condition.Property) + { + case ProfileConditionValue.AudioBitrate: + { + if (!enableNonQualifiedConditions) + { + continue; + } + + int num; + if (int.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out num)) + { + if (condition.Condition == ProfileConditionType.Equals) + { + item.AudioBitrate = num; + } + else if (condition.Condition == ProfileConditionType.LessThanEqual) + { + item.AudioBitrate = Math.Min(num, item.AudioBitrate ?? num); + } + else if (condition.Condition == ProfileConditionType.GreaterThanEqual) + { + item.AudioBitrate = Math.Max(num, item.AudioBitrate ?? num); + } + } + break; + } + case ProfileConditionValue.AudioChannels: + { + if (string.IsNullOrEmpty(qualifier)) + { + if (!enableNonQualifiedConditions) + { + continue; + } + } + else + { + if (!enableQualifiedConditions) + { + continue; + } + } + + int num; + if (int.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out num)) + { + if (condition.Condition == ProfileConditionType.Equals) + { + item.SetOption(qualifier, "audiochannels", num.ToString(CultureInfo.InvariantCulture)); + } + else if (condition.Condition == ProfileConditionType.LessThanEqual) + { + item.SetOption(qualifier, "audiochannels", Math.Min(num, item.GetTargetAudioChannels(qualifier) ?? num).ToString(CultureInfo.InvariantCulture)); + } + else if (condition.Condition == ProfileConditionType.GreaterThanEqual) + { + item.SetOption(qualifier, "audiochannels", Math.Max(num, item.GetTargetAudioChannels(qualifier) ?? num).ToString(CultureInfo.InvariantCulture)); + } + } + break; + } + case ProfileConditionValue.IsAvc: + { + if (!enableNonQualifiedConditions) + { + continue; + } + + bool isAvc; + if (bool.TryParse(value, out isAvc)) + { + if (isAvc && condition.Condition == ProfileConditionType.Equals) + { + item.RequireAvc = true; + } + else if (!isAvc && condition.Condition == ProfileConditionType.NotEquals) + { + item.RequireAvc = true; + } + } + break; + } + case ProfileConditionValue.IsAnamorphic: + { + if (!enableNonQualifiedConditions) + { + continue; + } + + bool isAnamorphic; + if (bool.TryParse(value, out isAnamorphic)) + { + if (isAnamorphic && condition.Condition == ProfileConditionType.Equals) + { + item.RequireNonAnamorphic = true; + } + else if (!isAnamorphic && condition.Condition == ProfileConditionType.NotEquals) + { + item.RequireNonAnamorphic = true; + } + } + break; + } + case ProfileConditionValue.IsInterlaced: + { + if (string.IsNullOrEmpty(qualifier)) + { + if (!enableNonQualifiedConditions) + { + continue; + } + } + else + { + if (!enableQualifiedConditions) + { + continue; + } + } + + bool isInterlaced; + if (bool.TryParse(value, out isInterlaced)) + { + if (!isInterlaced && condition.Condition == ProfileConditionType.Equals) + { + item.SetOption(qualifier, "deinterlace", "true"); + } + else if (isInterlaced && condition.Condition == ProfileConditionType.NotEquals) + { + item.SetOption(qualifier, "deinterlace", "true"); + } + } + break; + } + case ProfileConditionValue.AudioProfile: + case ProfileConditionValue.Has64BitOffsets: + case ProfileConditionValue.PacketLength: + case ProfileConditionValue.NumAudioStreams: + case ProfileConditionValue.NumVideoStreams: + case ProfileConditionValue.IsSecondaryAudio: + case ProfileConditionValue.VideoTimestamp: + { + // Not supported yet + break; + } + case ProfileConditionValue.RefFrames: + { + if (string.IsNullOrEmpty(qualifier)) + { + if (!enableNonQualifiedConditions) + { + continue; + } + } + else + { + if (!enableQualifiedConditions) + { + continue; + } + } + + int num; + if (int.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out num)) + { + if (condition.Condition == ProfileConditionType.Equals) + { + item.SetOption(qualifier, "maxrefframes", num.ToString(CultureInfo.InvariantCulture)); + } + else if (condition.Condition == ProfileConditionType.LessThanEqual) + { + item.SetOption(qualifier, "maxrefframes", Math.Min(num, item.GetTargetRefFrames(qualifier) ?? num).ToString(CultureInfo.InvariantCulture)); + } + else if (condition.Condition == ProfileConditionType.GreaterThanEqual) + { + item.SetOption(qualifier, "maxrefframes", Math.Max(num, item.GetTargetRefFrames(qualifier) ?? num).ToString(CultureInfo.InvariantCulture)); + } + } + break; + } + case ProfileConditionValue.VideoBitDepth: + { + if (string.IsNullOrEmpty(qualifier)) + { + if (!enableNonQualifiedConditions) + { + continue; + } + } + else + { + if (!enableQualifiedConditions) + { + continue; + } + } + + int num; + if (int.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out num)) + { + if (condition.Condition == ProfileConditionType.Equals) + { + item.SetOption(qualifier, "videobitdepth", num.ToString(CultureInfo.InvariantCulture)); + } + else if (condition.Condition == ProfileConditionType.LessThanEqual) + { + item.SetOption(qualifier, "videobitdepth", Math.Min(num, item.GetTargetVideoBitDepth(qualifier) ?? num).ToString(CultureInfo.InvariantCulture)); + } + else if (condition.Condition == ProfileConditionType.GreaterThanEqual) + { + item.SetOption(qualifier, "videobitdepth", Math.Max(num, item.GetTargetVideoBitDepth(qualifier) ?? num).ToString(CultureInfo.InvariantCulture)); + } + } + break; + } + case ProfileConditionValue.VideoProfile: + { + if (string.IsNullOrEmpty(qualifier)) + { + continue; + } + + if (!string.IsNullOrEmpty(value)) + { + // change from split by | to comma + + // strip spaces to avoid having to encode + var values = value + .Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries); + + if (condition.Condition == ProfileConditionType.Equals || condition.Condition == ProfileConditionType.EqualsAny) + { + item.SetOption(qualifier, "profile", string.Join(",", values)); + } + } + break; + } + case ProfileConditionValue.Height: + { + if (!enableNonQualifiedConditions) + { + continue; + } + + int num; + if (int.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out num)) + { + if (condition.Condition == ProfileConditionType.Equals) + { + item.MaxHeight = num; + } + else if (condition.Condition == ProfileConditionType.LessThanEqual) + { + item.MaxHeight = Math.Min(num, item.MaxHeight ?? num); + } + else if (condition.Condition == ProfileConditionType.GreaterThanEqual) + { + item.MaxHeight = Math.Max(num, item.MaxHeight ?? num); + } + } + break; + } + case ProfileConditionValue.VideoBitrate: + { + if (!enableNonQualifiedConditions) + { + continue; + } + + int num; + if (int.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out num)) + { + if (condition.Condition == ProfileConditionType.Equals) + { + item.VideoBitrate = num; + } + else if (condition.Condition == ProfileConditionType.LessThanEqual) + { + item.VideoBitrate = Math.Min(num, item.VideoBitrate ?? num); + } + else if (condition.Condition == ProfileConditionType.GreaterThanEqual) + { + item.VideoBitrate = Math.Max(num, item.VideoBitrate ?? num); + } + } + break; + } + case ProfileConditionValue.VideoFramerate: + { + if (!enableNonQualifiedConditions) + { + continue; + } + + float num; + if (float.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out num)) + { + if (condition.Condition == ProfileConditionType.Equals) + { + item.MaxFramerate = num; + } + else if (condition.Condition == ProfileConditionType.LessThanEqual) + { + item.MaxFramerate = Math.Min(num, item.MaxFramerate ?? num); + } + else if (condition.Condition == ProfileConditionType.GreaterThanEqual) + { + item.MaxFramerate = Math.Max(num, item.MaxFramerate ?? num); + } + } + break; + } + case ProfileConditionValue.VideoLevel: + { + if (string.IsNullOrEmpty(qualifier)) + { + continue; + } + + int num; + if (int.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out num)) + { + if (condition.Condition == ProfileConditionType.Equals) + { + item.SetOption(qualifier, "level", num.ToString(CultureInfo.InvariantCulture)); + } + else if (condition.Condition == ProfileConditionType.LessThanEqual) + { + item.SetOption(qualifier, "level", Math.Min(num, item.GetTargetVideoLevel(qualifier) ?? num).ToString(CultureInfo.InvariantCulture)); + } + else if (condition.Condition == ProfileConditionType.GreaterThanEqual) + { + item.SetOption(qualifier, "level", Math.Max(num, item.GetTargetVideoLevel(qualifier) ?? num).ToString(CultureInfo.InvariantCulture)); + } + } + break; + } + case ProfileConditionValue.Width: + { + if (!enableNonQualifiedConditions) + { + continue; + } + + int num; + if (int.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out num)) + { + if (condition.Condition == ProfileConditionType.Equals) + { + item.MaxWidth = num; + } + else if (condition.Condition == ProfileConditionType.LessThanEqual) + { + item.MaxWidth = Math.Min(num, item.MaxWidth ?? num); + } + else if (condition.Condition == ProfileConditionType.GreaterThanEqual) + { + item.MaxWidth = Math.Max(num, item.MaxWidth ?? num); + } + } + break; + } + default: + break; + } + } + } + + private bool IsAudioDirectPlaySupported(DirectPlayProfile profile, MediaSourceInfo item, MediaStream audioStream) + { + // Check container type + if (!profile.SupportsContainer(item.Container)) + { + return false; + } + + // Check audio codec + string audioCodec = audioStream == null ? null : audioStream.Codec; + if (!profile.SupportsAudioCodec(audioCodec)) + { + return false; + } + + return true; + } + + private bool IsVideoDirectPlaySupported(DirectPlayProfile profile, MediaSourceInfo item, MediaStream videoStream, MediaStream audioStream) + { + // Check container type + if (!profile.SupportsContainer(item.Container)) + { + return false; + } + + // Check video codec + string videoCodec = videoStream == null ? null : videoStream.Codec; + if (!profile.SupportsVideoCodec(videoCodec)) + { + return false; + } + + // Check audio codec + if (audioStream != null) + { + string audioCodec = audioStream == null ? null : audioStream.Codec; + if (!profile.SupportsAudioCodec(audioCodec)) + { + return false; + } + } + + return true; + } + } +} |
