123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533 |
- using System;
- using System.Globalization;
- using System.IO;
- using System.Linq;
- using MediaBrowser.Model.Dto;
- using System.Collections.Generic;
- using MediaBrowser.Model.Entities;
- namespace MediaBrowser.Model.Dlna
- {
- public class StreamBuilder
- {
- private readonly CultureInfo _usCulture = new CultureInfo("en-US");
- public StreamInfo BuildAudioItem(AudioOptions options)
- {
- ValidateAudioInput(options);
-
- var mediaSources = options.MediaSources;
- // If the client wants a specific media soure, filter now
- if (!string.IsNullOrEmpty(options.MediaSourceId))
- {
- // Avoid implicitly captured closure
- var mediaSourceId = options.MediaSourceId;
- mediaSources = mediaSources
- .Where(i => string.Equals(i.Id, mediaSourceId, StringComparison.OrdinalIgnoreCase))
- .ToList();
- }
- var streams = mediaSources.Select(i => BuildAudioItem(options.ItemId, i, options.Profile)).ToList();
- foreach (var stream in streams)
- {
- stream.DeviceId = options.DeviceId;
- stream.DeviceProfileId = options.Profile.Id;
- }
- return GetOptimalStream(streams);
- }
- public StreamInfo BuildVideoItem(VideoOptions options)
- {
- ValidateInput(options);
- var mediaSources = options.MediaSources;
- // If the client wants a specific media soure, filter now
- if (!string.IsNullOrEmpty(options.MediaSourceId))
- {
- // Avoid implicitly captured closure
- var mediaSourceId = options.MediaSourceId;
- mediaSources = mediaSources
- .Where(i => string.Equals(i.Id, mediaSourceId, StringComparison.OrdinalIgnoreCase))
- .ToList();
- }
- var streams = mediaSources.Select(i => BuildVideoItem(i, options)).ToList();
- foreach (var stream in streams)
- {
- stream.DeviceId = options.DeviceId;
- stream.DeviceProfileId = options.Profile.Id;
- }
- return GetOptimalStream(streams);
- }
- private StreamInfo GetOptimalStream(List<StreamInfo> streams)
- {
- // Grab the first one that can be direct streamed
- // If that doesn't produce anything, just take the first
- return streams.FirstOrDefault(i => i.IsDirectStream) ??
- streams.FirstOrDefault();
- }
- private StreamInfo BuildAudioItem(string itemId, MediaSourceInfo item, DeviceProfile profile)
- {
- var playlistItem = new StreamInfo
- {
- ItemId = itemId,
- MediaType = DlnaProfileType.Audio,
- MediaSourceId = item.Id
- };
- var audioStream = item.MediaStreams.FirstOrDefault(i => i.Type == MediaStreamType.Audio);
- var directPlay = profile.DirectPlayProfiles
- .FirstOrDefault(i => i.Type == playlistItem.MediaType && IsAudioProfileSupported(i, item, audioStream));
- if (directPlay != null)
- {
- var audioCodec = audioStream == null ? null : audioStream.Codec;
- // Make sure audio codec profiles are satisfied
- if (!string.IsNullOrEmpty(audioCodec) && profile.CodecProfiles.Where(i => i.Type == CodecType.Audio && i.ContainsCodec(audioCodec))
- .All(i => AreConditionsSatisfied(i.Conditions, item.Path, null, audioStream)))
- {
- playlistItem.IsDirectStream = true;
- playlistItem.Container = item.Container;
- return playlistItem;
- }
- }
- var transcodingProfile = profile.TranscodingProfiles
- .FirstOrDefault(i => i.Type == playlistItem.MediaType);
- if (transcodingProfile != null)
- {
- playlistItem.IsDirectStream = false;
- playlistItem.Container = transcodingProfile.Container;
- playlistItem.AudioCodec = transcodingProfile.AudioCodec;
- var audioTranscodingConditions = profile.CodecProfiles
- .Where(i => i.Type == CodecType.Audio && i.ContainsCodec(transcodingProfile.AudioCodec))
- .Take(1)
- .SelectMany(i => i.Conditions);
- ApplyTranscodingConditions(playlistItem, audioTranscodingConditions);
- }
- return playlistItem;
- }
- private StreamInfo BuildVideoItem(MediaSourceInfo item, VideoOptions options)
- {
- var playlistItem = new StreamInfo
- {
- ItemId = options.ItemId,
- MediaType = DlnaProfileType.Video,
- MediaSourceId = item.Id
- };
- var audioStream = item.MediaStreams.FirstOrDefault(i => i.Type == MediaStreamType.Audio);
- var videoStream = item.MediaStreams.FirstOrDefault(i => i.Type == MediaStreamType.Video);
- if (IsEligibleForDirectPlay(item, options))
- {
- // See if it can be direct played
- var directPlay = options.Profile.DirectPlayProfiles
- .FirstOrDefault(i => i.Type == playlistItem.MediaType && IsVideoProfileSupported(i, item, videoStream, audioStream));
- if (directPlay != null)
- {
- var videoCodec = videoStream == null ? null : videoStream.Codec;
- // Make sure video codec profiles are satisfied
- if (!string.IsNullOrEmpty(videoCodec) && options.Profile.CodecProfiles.Where(i => i.Type == CodecType.Video && i.ContainsCodec(videoCodec))
- .All(i => AreConditionsSatisfied(i.Conditions, item.Path, videoStream, audioStream)))
- {
- var audioCodec = audioStream == null ? null : audioStream.Codec;
- // Make sure audio codec profiles are satisfied
- if (string.IsNullOrEmpty(audioCodec) || options.Profile.CodecProfiles.Where(i => i.Type == CodecType.VideoAudio && i.ContainsCodec(audioCodec))
- .All(i => AreConditionsSatisfied(i.Conditions, item.Path, videoStream, audioStream)))
- {
- playlistItem.IsDirectStream = true;
- playlistItem.Container = item.Container;
- return playlistItem;
- }
- }
- }
- }
- // Can't direct play, find the transcoding profile
- var transcodingProfile = options.Profile.TranscodingProfiles
- .FirstOrDefault(i => i.Type == playlistItem.MediaType);
- if (transcodingProfile != null)
- {
- playlistItem.IsDirectStream = false;
- playlistItem.Container = transcodingProfile.Container;
- playlistItem.AudioCodec = transcodingProfile.AudioCodec.Split(',').FirstOrDefault();
- playlistItem.VideoCodec = transcodingProfile.VideoCodec;
- var videoTranscodingConditions = options.Profile.CodecProfiles
- .Where(i => i.Type == CodecType.Video && i.ContainsCodec(transcodingProfile.VideoCodec))
- .Take(1)
- .SelectMany(i => i.Conditions);
- ApplyTranscodingConditions(playlistItem, videoTranscodingConditions);
- var audioTranscodingConditions = options.Profile.CodecProfiles
- .Where(i => i.Type == CodecType.VideoAudio && i.ContainsCodec(transcodingProfile.AudioCodec))
- .Take(1)
- .SelectMany(i => i.Conditions);
- ApplyTranscodingConditions(playlistItem, audioTranscodingConditions);
- }
- return playlistItem;
- }
- private bool IsEligibleForDirectPlay(MediaSourceInfo item, VideoOptions options)
- {
- if (options.SubtitleStreamIndex.HasValue)
- {
- return false;
- }
- if (options.AudioStreamIndex.HasValue &&
- item.MediaStreams.Count(i => i.Type == MediaStreamType.Audio) > 1)
- {
- 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 (string.IsNullOrEmpty(options.ItemId))
- {
- 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, IEnumerable<ProfileCondition> conditions)
- {
- foreach (var condition in conditions
- .Where(i => !string.IsNullOrEmpty(i.Value)))
- {
- var value = condition.Value;
- switch (condition.Property)
- {
- case ProfileConditionValue.AudioBitrate:
- {
- int num;
- if (int.TryParse(value, NumberStyles.Any, _usCulture, out num))
- {
- item.AudioBitrate = num;
- }
- break;
- }
- case ProfileConditionValue.AudioChannels:
- {
- int num;
- if (int.TryParse(value, NumberStyles.Any, _usCulture, out num))
- {
- item.MaxAudioChannels = num;
- }
- break;
- }
- case ProfileConditionValue.AudioProfile:
- case ProfileConditionValue.Has64BitOffsets:
- case ProfileConditionValue.VideoBitDepth:
- case ProfileConditionValue.VideoProfile:
- {
- // Not supported yet
- break;
- }
- case ProfileConditionValue.Height:
- {
- int num;
- if (int.TryParse(value, NumberStyles.Any, _usCulture, out num))
- {
- item.MaxHeight = num;
- }
- break;
- }
- case ProfileConditionValue.VideoBitrate:
- {
- int num;
- if (int.TryParse(value, NumberStyles.Any, _usCulture, out num))
- {
- item.VideoBitrate = num;
- }
- break;
- }
- case ProfileConditionValue.VideoFramerate:
- {
- int num;
- if (int.TryParse(value, NumberStyles.Any, _usCulture, out num))
- {
- item.MaxFramerate = num;
- }
- break;
- }
- case ProfileConditionValue.VideoLevel:
- {
- int num;
- if (int.TryParse(value, NumberStyles.Any, _usCulture, out num))
- {
- item.VideoLevel = num;
- }
- break;
- }
- case ProfileConditionValue.Width:
- {
- int num;
- if (int.TryParse(value, NumberStyles.Any, _usCulture, out num))
- {
- item.MaxWidth = num;
- }
- break;
- }
- default:
- throw new ArgumentException("Unrecognized ProfileConditionValue");
- }
- }
- }
- private bool IsAudioProfileSupported(DirectPlayProfile profile, MediaSourceInfo item, MediaStream audioStream)
- {
- if (profile.Container.Length > 0)
- {
- // Check container type
- var mediaContainer = item.Container ?? string.Empty;
- if (!profile.GetContainers().Any(i => string.Equals(i, mediaContainer, StringComparison.OrdinalIgnoreCase)))
- {
- return false;
- }
- }
- return true;
- }
- private bool IsVideoProfileSupported(DirectPlayProfile profile, MediaSourceInfo item, MediaStream videoStream, MediaStream audioStream)
- {
- // Only plain video files can be direct played
- if (item.VideoType != VideoType.VideoFile)
- {
- return false;
- }
- if (profile.Container.Length > 0)
- {
- // Check container type
- var mediaContainer = item.Container ?? string.Empty;
- if (!profile.GetContainers().Any(i => string.Equals(i, mediaContainer, StringComparison.OrdinalIgnoreCase)))
- {
- return false;
- }
- }
- // Check video codec
- var videoCodecs = profile.GetVideoCodecs();
- if (videoCodecs.Count > 0)
- {
- var videoCodec = videoStream == null ? null : videoStream.Codec;
- if (string.IsNullOrEmpty(videoCodec) || !videoCodecs.Contains(videoCodec, StringComparer.OrdinalIgnoreCase))
- {
- return false;
- }
- }
- var audioCodecs = profile.GetAudioCodecs();
- if (audioCodecs.Count > 0)
- {
- // Check audio codecs
- var audioCodec = audioStream == null ? null : audioStream.Codec;
- if (string.IsNullOrEmpty(audioCodec) || !audioCodecs.Contains(audioCodec, StringComparer.OrdinalIgnoreCase))
- {
- return false;
- }
- }
- return true;
- }
- private bool AreConditionsSatisfied(IEnumerable<ProfileCondition> conditions, string mediaPath, MediaStream videoStream, MediaStream audioStream)
- {
- return conditions.All(i => IsConditionSatisfied(i, mediaPath, videoStream, audioStream));
- }
- /// <summary>
- /// Determines whether [is condition satisfied] [the specified condition].
- /// </summary>
- /// <param name="condition">The condition.</param>
- /// <param name="mediaPath">The media path.</param>
- /// <param name="videoStream">The video stream.</param>
- /// <param name="audioStream">The audio stream.</param>
- /// <returns><c>true</c> if [is condition satisfied] [the specified condition]; otherwise, <c>false</c>.</returns>
- /// <exception cref="System.InvalidOperationException">Unexpected ProfileConditionType</exception>
- private bool IsConditionSatisfied(ProfileCondition condition, string mediaPath, MediaStream videoStream, MediaStream audioStream)
- {
- if (condition.Property == ProfileConditionValue.Has64BitOffsets)
- {
- // TODO: Determine how to evaluate this
- }
- if (condition.Property == ProfileConditionValue.VideoProfile)
- {
- var profile = videoStream == null ? null : videoStream.Profile;
- if (!string.IsNullOrEmpty(profile))
- {
- switch (condition.Condition)
- {
- case ProfileConditionType.Equals:
- return string.Equals(profile, condition.Value, StringComparison.OrdinalIgnoreCase);
- case ProfileConditionType.NotEquals:
- return !string.Equals(profile, condition.Value, StringComparison.OrdinalIgnoreCase);
- default:
- throw new InvalidOperationException("Unexpected ProfileConditionType");
- }
- }
- }
- else if (condition.Property == ProfileConditionValue.AudioProfile)
- {
- var profile = audioStream == null ? null : audioStream.Profile;
- if (!string.IsNullOrEmpty(profile))
- {
- switch (condition.Condition)
- {
- case ProfileConditionType.Equals:
- return string.Equals(profile, condition.Value, StringComparison.OrdinalIgnoreCase);
- case ProfileConditionType.NotEquals:
- return !string.Equals(profile, condition.Value, StringComparison.OrdinalIgnoreCase);
- default:
- throw new InvalidOperationException("Unexpected ProfileConditionType");
- }
- }
- }
- else
- {
- var actualValue = GetConditionValue(condition, mediaPath, videoStream, audioStream);
- if (actualValue.HasValue)
- {
- long expected;
- if (long.TryParse(condition.Value, NumberStyles.Any, _usCulture, out expected))
- {
- switch (condition.Condition)
- {
- case ProfileConditionType.Equals:
- return actualValue.Value == expected;
- case ProfileConditionType.GreaterThanEqual:
- return actualValue.Value >= expected;
- case ProfileConditionType.LessThanEqual:
- return actualValue.Value <= expected;
- case ProfileConditionType.NotEquals:
- return actualValue.Value != expected;
- default:
- throw new InvalidOperationException("Unexpected ProfileConditionType");
- }
- }
- }
- }
- // Value doesn't exist in metadata. Fail it if required.
- return !condition.IsRequired;
- }
- /// <summary>
- /// Gets the condition value.
- /// </summary>
- /// <param name="condition">The condition.</param>
- /// <param name="mediaPath">The media path.</param>
- /// <param name="videoStream">The video stream.</param>
- /// <param name="audioStream">The audio stream.</param>
- /// <returns>System.Nullable{System.Int64}.</returns>
- /// <exception cref="System.InvalidOperationException">Unexpected Property</exception>
- private long? GetConditionValue(ProfileCondition condition, string mediaPath, MediaStream videoStream, MediaStream audioStream)
- {
- switch (condition.Property)
- {
- case ProfileConditionValue.AudioBitrate:
- return audioStream == null ? null : audioStream.BitRate;
- case ProfileConditionValue.AudioChannels:
- return audioStream == null ? null : audioStream.Channels;
- case ProfileConditionValue.VideoBitrate:
- return videoStream == null ? null : videoStream.BitRate;
- case ProfileConditionValue.VideoFramerate:
- return videoStream == null ? null : (ConvertToLong(videoStream.AverageFrameRate ?? videoStream.RealFrameRate));
- case ProfileConditionValue.Height:
- return videoStream == null ? null : videoStream.Height;
- case ProfileConditionValue.Width:
- return videoStream == null ? null : videoStream.Width;
- case ProfileConditionValue.VideoLevel:
- return videoStream == null ? null : ConvertToLong(videoStream.Level);
- default:
- throw new InvalidOperationException("Unexpected Property");
- }
- }
- /// <summary>
- /// Converts to long.
- /// </summary>
- /// <param name="val">The value.</param>
- /// <returns>System.Nullable{System.Int64}.</returns>
- private long? ConvertToLong(float? val)
- {
- return val.HasValue ? Convert.ToInt64(val.Value) : (long?)null;
- }
- /// <summary>
- /// Converts to long.
- /// </summary>
- /// <param name="val">The value.</param>
- /// <returns>System.Nullable{System.Int64}.</returns>
- private long? ConvertToLong(double? val)
- {
- return val.HasValue ? Convert.ToInt64(val.Value) : (long?)null;
- }
- }
- }
|