diff options
| author | tikuf <admin@nyalindee.com> | 2014-04-02 16:53:53 +1100 |
|---|---|---|
| committer | tikuf <admin@nyalindee.com> | 2014-04-02 16:53:53 +1100 |
| commit | 9ff262bc2ac24be2931505aa877b0c500fcb988a (patch) | |
| tree | 75ef8f32fc4b7359cc48f45be58216491d624b93 /MediaBrowser.Model | |
| parent | 8882925dab080eb236a5cc896f48ed99711d76a8 (diff) | |
| parent | 6fe7264f7828aff196e19cfa3497835c5921a7f9 (diff) | |
Merge branch 'master' of https://github.com/MediaBrowser/MediaBrowser
Diffstat (limited to 'MediaBrowser.Model')
| -rw-r--r-- | MediaBrowser.Model/Dlna/CodecProfile.cs | 85 | ||||
| -rw-r--r-- | MediaBrowser.Model/Dlna/ContainerProfile.cs | 26 | ||||
| -rw-r--r-- | MediaBrowser.Model/Dlna/DeviceIdentification.cs | 82 | ||||
| -rw-r--r-- | MediaBrowser.Model/Dlna/DeviceProfile.cs | 230 | ||||
| -rw-r--r-- | MediaBrowser.Model/Dlna/DirectPlayProfile.cs | 43 | ||||
| -rw-r--r-- | MediaBrowser.Model/Dlna/ResponseProfile.cs | 49 | ||||
| -rw-r--r-- | MediaBrowser.Model/Dlna/StreamBuilder.cs | 533 | ||||
| -rw-r--r-- | MediaBrowser.Model/Dlna/StreamInfo.cs | 124 | ||||
| -rw-r--r-- | MediaBrowser.Model/Dlna/TranscodingProfile.cs | 47 | ||||
| -rw-r--r-- | MediaBrowser.Model/Dto/MediaVersionInfo.cs | 2 | ||||
| -rw-r--r-- | MediaBrowser.Model/Extensions/ModelExtensions.cs | 20 | ||||
| -rw-r--r-- | MediaBrowser.Model/MediaBrowser.Model.csproj | 11 | ||||
| -rw-r--r-- | MediaBrowser.Model/Updates/PackageVersionInfo.cs | 16 |
13 files changed, 1244 insertions, 24 deletions
diff --git a/MediaBrowser.Model/Dlna/CodecProfile.cs b/MediaBrowser.Model/Dlna/CodecProfile.cs new file mode 100644 index 000000000..b15dfc809 --- /dev/null +++ b/MediaBrowser.Model/Dlna/CodecProfile.cs @@ -0,0 +1,85 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Xml.Serialization; + +namespace MediaBrowser.Model.Dlna +{ + public class CodecProfile + { + [XmlAttribute("type")] + public CodecType Type { get; set; } + + public ProfileCondition[] Conditions { get; set; } + + [XmlAttribute("codec")] + public string Codec { get; set; } + + public CodecProfile() + { + Conditions = new ProfileCondition[] {}; + } + + public List<string> GetCodecs() + { + return (Codec ?? string.Empty).Split(',').Where(i => !string.IsNullOrEmpty(i)).ToList(); + } + + public bool ContainsCodec(string codec) + { + var codecs = GetCodecs(); + + return codecs.Count == 0 || codecs.Contains(codec, StringComparer.OrdinalIgnoreCase); + } + } + + public enum CodecType + { + Video = 0, + VideoAudio = 1, + Audio = 2 + } + + public class ProfileCondition + { + [XmlAttribute("condition")] + public ProfileConditionType Condition { get; set; } + + [XmlAttribute("property")] + public ProfileConditionValue Property { get; set; } + + [XmlAttribute("value")] + public string Value { get; set; } + + [XmlAttribute("isRequired")] + public bool IsRequired { get; set; } + + public ProfileCondition() + { + IsRequired = true; + } + } + + public enum ProfileConditionType + { + Equals = 0, + NotEquals = 1, + LessThanEqual = 2, + GreaterThanEqual = 3 + } + + public enum ProfileConditionValue + { + AudioChannels, + AudioBitrate, + AudioProfile, + Width, + Height, + Has64BitOffsets, + VideoBitDepth, + VideoBitrate, + VideoFramerate, + VideoLevel, + VideoProfile + } +} diff --git a/MediaBrowser.Model/Dlna/ContainerProfile.cs b/MediaBrowser.Model/Dlna/ContainerProfile.cs new file mode 100644 index 000000000..3a5fe3bd5 --- /dev/null +++ b/MediaBrowser.Model/Dlna/ContainerProfile.cs @@ -0,0 +1,26 @@ +using System.Collections.Generic; +using System.Linq; +using System.Xml.Serialization; + +namespace MediaBrowser.Model.Dlna +{ + public class ContainerProfile + { + [XmlAttribute("type")] + public DlnaProfileType Type { get; set; } + public ProfileCondition[] Conditions { get; set; } + + [XmlAttribute("container")] + public string Container { get; set; } + + public ContainerProfile() + { + Conditions = new ProfileCondition[] { }; + } + + public List<string> GetContainers() + { + return (Container ?? string.Empty).Split(',').Where(i => !string.IsNullOrEmpty(i)).ToList(); + } + } +} diff --git a/MediaBrowser.Model/Dlna/DeviceIdentification.cs b/MediaBrowser.Model/Dlna/DeviceIdentification.cs new file mode 100644 index 000000000..87cf000b1 --- /dev/null +++ b/MediaBrowser.Model/Dlna/DeviceIdentification.cs @@ -0,0 +1,82 @@ +using System.Xml.Serialization; + +namespace MediaBrowser.Model.Dlna +{ + public class DeviceIdentification + { + /// <summary> + /// Gets or sets the name of the friendly. + /// </summary> + /// <value>The name of the friendly.</value> + public string FriendlyName { get; set; } + /// <summary> + /// Gets or sets the model number. + /// </summary> + /// <value>The model number.</value> + public string ModelNumber { get; set; } + /// <summary> + /// Gets or sets the serial number. + /// </summary> + /// <value>The serial number.</value> + public string SerialNumber { get; set; } + /// <summary> + /// Gets or sets the name of the model. + /// </summary> + /// <value>The name of the model.</value> + public string ModelName { get; set; } + /// <summary> + /// Gets or sets the model description. + /// </summary> + /// <value>The model description.</value> + public string ModelDescription { get; set; } + /// <summary> + /// Gets or sets the device description. + /// </summary> + /// <value>The device description.</value> + public string DeviceDescription { get; set; } + /// <summary> + /// Gets or sets the model URL. + /// </summary> + /// <value>The model URL.</value> + public string ModelUrl { get; set; } + /// <summary> + /// Gets or sets the manufacturer. + /// </summary> + /// <value>The manufacturer.</value> + public string Manufacturer { get; set; } + /// <summary> + /// Gets or sets the manufacturer URL. + /// </summary> + /// <value>The manufacturer URL.</value> + public string ManufacturerUrl { get; set; } + /// <summary> + /// Gets or sets the headers. + /// </summary> + /// <value>The headers.</value> + public HttpHeaderInfo[] Headers { get; set; } + + public DeviceIdentification() + { + Headers = new HttpHeaderInfo[] {}; + } + } + + public class HttpHeaderInfo + { + [XmlAttribute("name")] + public string Name { get; set; } + + [XmlAttribute("value")] + public string Value { get; set; } + + [XmlAttribute("match")] + public HeaderMatchType Match { get; set; } + } + + public enum HeaderMatchType + { + Equals = 0, + Regex = 1, + Substring = 2 + } +} diff --git a/MediaBrowser.Model/Dlna/DeviceProfile.cs b/MediaBrowser.Model/Dlna/DeviceProfile.cs new file mode 100644 index 000000000..53cd2da12 --- /dev/null +++ b/MediaBrowser.Model/Dlna/DeviceProfile.cs @@ -0,0 +1,230 @@ +using MediaBrowser.Model.Entities; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Xml.Serialization; + +namespace MediaBrowser.Model.Dlna +{ + [XmlRoot("Profile")] + public class DeviceProfile + { + /// <summary> + /// Gets or sets the name. + /// </summary> + /// <value>The name.</value> + public string Name { get; set; } + + [XmlIgnore] + public string Id { get; set; } + + [XmlIgnore] + public DeviceProfileType ProfileType { get; set; } + + /// <summary> + /// Gets or sets the identification. + /// </summary> + /// <value>The identification.</value> + public DeviceIdentification Identification { get; set; } + + public string FriendlyName { get; set; } + public string Manufacturer { get; set; } + public string ManufacturerUrl { get; set; } + public string ModelName { get; set; } + public string ModelDescription { get; set; } + public string ModelNumber { get; set; } + public string ModelUrl { get; set; } + public bool IgnoreTranscodeByteRangeRequests { get; set; } + public bool EnableAlbumArtInDidl { get; set; } + + public string SupportedMediaTypes { get; set; } + + /// <summary> + /// Controls the content of the X_DLNADOC element in the urn:schemas-dlna-org:device-1-0 namespace. + /// </summary> + public string XDlnaDoc { get; set; } + /// <summary> + /// Controls the content of the X_DLNACAP element in the urn:schemas-dlna-org:device-1-0 namespace. + /// </summary> + public string XDlnaCap { get; set; } + /// <summary> + /// Controls the content of the aggregationFlags element in the urn:schemas-sonycom:av. + /// </summary> + public string SonyAggregationFlags { get; set; } + + public string ProtocolInfo { get; set; } + + public int TimelineOffsetSeconds { get; set; } + public bool RequiresPlainVideoItems { get; set; } + public bool RequiresPlainFolders { get; set; } + + /// <summary> + /// Gets or sets the direct play profiles. + /// </summary> + /// <value>The direct play profiles.</value> + public DirectPlayProfile[] DirectPlayProfiles { get; set; } + + /// <summary> + /// Gets or sets the transcoding profiles. + /// </summary> + /// <value>The transcoding profiles.</value> + public TranscodingProfile[] TranscodingProfiles { get; set; } + + public ContainerProfile[] ContainerProfiles { get; set; } + + public CodecProfile[] CodecProfiles { get; set; } + public ResponseProfile[] ResponseProfiles { get; set; } + + public DeviceProfile() + { + DirectPlayProfiles = new DirectPlayProfile[] { }; + TranscodingProfiles = new TranscodingProfile[] { }; + ResponseProfiles = new ResponseProfile[] { }; + CodecProfiles = new CodecProfile[] { }; + ContainerProfiles = new ContainerProfile[] { }; + + SupportedMediaTypes = "Audio,Photo,Video"; + } + + public List<string> GetSupportedMediaTypes() + { + return (SupportedMediaTypes ?? string.Empty).Split(',').Where(i => !string.IsNullOrEmpty(i)).ToList(); + } + + public TranscodingProfile GetAudioTranscodingProfile(string container, string audioCodec) + { + container = (container ?? string.Empty).TrimStart('.'); + + return TranscodingProfiles.FirstOrDefault(i => + { + if (i.Type != DlnaProfileType.Audio) + { + return false; + } + + if (!string.Equals(container, i.Container, StringComparison.OrdinalIgnoreCase)) + { + return false; + } + + if (!i.GetAudioCodecs().Contains(audioCodec ?? string.Empty)) + { + return false; + } + + return true; + }); + } + + public TranscodingProfile GetVideoTranscodingProfile(string container, string audioCodec, string videoCodec) + { + container = (container ?? string.Empty).TrimStart('.'); + + return TranscodingProfiles.FirstOrDefault(i => + { + if (i.Type != DlnaProfileType.Video) + { + return false; + } + + if (!string.Equals(container, i.Container, StringComparison.OrdinalIgnoreCase)) + { + return false; + } + + if (!i.GetAudioCodecs().Contains(audioCodec ?? string.Empty)) + { + return false; + } + + if (!string.Equals(videoCodec, i.VideoCodec, StringComparison.OrdinalIgnoreCase)) + { + return false; + } + + return true; + }); + } + + public ResponseProfile GetAudioMediaProfile(string container, string audioCodec, MediaStream audioStream) + { + container = (container ?? string.Empty).TrimStart('.'); + + return ResponseProfiles.FirstOrDefault(i => + { + if (i.Type != DlnaProfileType.Audio) + { + return false; + } + + var containers = i.GetContainers().ToList(); + if (containers.Count > 0 && !containers.Contains(container)) + { + return false; + } + + var audioCodecs = i.GetAudioCodecs().ToList(); + if (audioCodecs.Count > 0 && !audioCodecs.Contains(audioCodec ?? string.Empty)) + { + return false; + } + + return true; + }); + } + + public ResponseProfile GetVideoMediaProfile(string container, string audioCodec, string videoCodec, MediaStream audioStream, MediaStream videoStream) + { + container = (container ?? string.Empty).TrimStart('.'); + + return ResponseProfiles.FirstOrDefault(i => + { + if (i.Type != DlnaProfileType.Video) + { + return false; + } + + var containers = i.GetContainers().ToList(); + if (containers.Count > 0 && !containers.Contains(container)) + { + return false; + } + + var audioCodecs = i.GetAudioCodecs().ToList(); + if (audioCodecs.Count > 0 && !audioCodecs.Contains(audioCodec ?? string.Empty)) + { + return false; + } + + var videoCodecs = i.GetVideoCodecs().ToList(); + if (videoCodecs.Count > 0 && !videoCodecs.Contains(videoCodec ?? string.Empty)) + { + return false; + } + + return true; + }); + } + + public ResponseProfile GetPhotoMediaProfile(string container) + { + container = (container ?? string.Empty).TrimStart('.'); + + return ResponseProfiles.FirstOrDefault(i => + { + if (i.Type != DlnaProfileType.Photo) + { + return false; + } + + var containers = i.GetContainers().ToList(); + if (containers.Count > 0 && !containers.Contains(container)) + { + return false; + } + + return true; + }); + } + } +} diff --git a/MediaBrowser.Model/Dlna/DirectPlayProfile.cs b/MediaBrowser.Model/Dlna/DirectPlayProfile.cs new file mode 100644 index 000000000..c7ecdc908 --- /dev/null +++ b/MediaBrowser.Model/Dlna/DirectPlayProfile.cs @@ -0,0 +1,43 @@ +using System.Collections.Generic; +using System.Linq; +using System.Xml.Serialization; + +namespace MediaBrowser.Model.Dlna +{ + public class DirectPlayProfile + { + [XmlAttribute("container")] + public string Container { get; set; } + + [XmlAttribute("audioCodec")] + public string AudioCodec { get; set; } + + [XmlAttribute("videoCodec")] + public string VideoCodec { get; set; } + + [XmlAttribute("type")] + public DlnaProfileType Type { get; set; } + + public List<string> GetContainers() + { + return (Container ?? string.Empty).Split(',').Where(i => !string.IsNullOrEmpty(i)).ToList(); + } + + public List<string> GetAudioCodecs() + { + return (AudioCodec ?? string.Empty).Split(',').Where(i => !string.IsNullOrEmpty(i)).ToList(); + } + + public List<string> GetVideoCodecs() + { + return (VideoCodec ?? string.Empty).Split(',').Where(i => !string.IsNullOrEmpty(i)).ToList(); + } + } + + public enum DlnaProfileType + { + Audio = 0, + Video = 1, + Photo = 2 + } +} diff --git a/MediaBrowser.Model/Dlna/ResponseProfile.cs b/MediaBrowser.Model/Dlna/ResponseProfile.cs new file mode 100644 index 000000000..e84095ffe --- /dev/null +++ b/MediaBrowser.Model/Dlna/ResponseProfile.cs @@ -0,0 +1,49 @@ +using System.Collections.Generic; +using System.Linq; +using System.Xml.Serialization; + +namespace MediaBrowser.Model.Dlna +{ + public class ResponseProfile + { + [XmlAttribute("container")] + public string Container { get; set; } + + [XmlAttribute("audioCodec")] + public string AudioCodec { get; set; } + + [XmlAttribute("videoCodec")] + public string VideoCodec { get; set; } + + [XmlAttribute("type")] + public DlnaProfileType Type { get; set; } + + [XmlAttribute("orgPn")] + public string OrgPn { get; set; } + + [XmlAttribute("mimeType")] + public string MimeType { get; set; } + + public ProfileCondition[] Conditions { get; set; } + + public ResponseProfile() + { + Conditions = new ProfileCondition[] {}; + } + + public List<string> GetContainers() + { + return (Container ?? string.Empty).Split(',').Where(i => !string.IsNullOrEmpty(i)).ToList(); + } + + public List<string> GetAudioCodecs() + { + return (AudioCodec ?? string.Empty).Split(',').Where(i => !string.IsNullOrEmpty(i)).ToList(); + } + + public List<string> GetVideoCodecs() + { + return (VideoCodec ?? string.Empty).Split(',').Where(i => !string.IsNullOrEmpty(i)).ToList(); + } + } +} diff --git a/MediaBrowser.Model/Dlna/StreamBuilder.cs b/MediaBrowser.Model/Dlna/StreamBuilder.cs new file mode 100644 index 000000000..d03567ffc --- /dev/null +++ b/MediaBrowser.Model/Dlna/StreamBuilder.cs @@ -0,0 +1,533 @@ +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; + } + } + +} diff --git a/MediaBrowser.Model/Dlna/StreamInfo.cs b/MediaBrowser.Model/Dlna/StreamInfo.cs new file mode 100644 index 000000000..21e4dae7b --- /dev/null +++ b/MediaBrowser.Model/Dlna/StreamInfo.cs @@ -0,0 +1,124 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using MediaBrowser.Model.Dto; + +namespace MediaBrowser.Model.Dlna +{ + /// <summary> + /// Class StreamInfo. + /// </summary> + public class StreamInfo + { + public string ItemId { get; set; } + + public string MediaSourceId { get; set; } + + public bool IsDirectStream { get; set; } + + public DlnaProfileType MediaType { get; set; } + + public string Container { get; set; } + + public long StartPositionTicks { get; set; } + + public string VideoCodec { get; set; } + + public string AudioCodec { get; set; } + + public int? AudioStreamIndex { get; set; } + + public int? SubtitleStreamIndex { get; set; } + + public int? MaxAudioChannels { get; set; } + + public int? AudioBitrate { get; set; } + + public int? VideoBitrate { get; set; } + + public int? VideoLevel { get; set; } + + public int? MaxWidth { get; set; } + public int? MaxHeight { get; set; } + + public int? MaxFramerate { get; set; } + + public string DeviceProfileId { get; set; } + public string DeviceId { get; set; } + + public string ToUrl(string baseUrl) + { + return ToDlnaUrl(baseUrl); + } + + public string ToDlnaUrl(string baseUrl) + { + if (string.IsNullOrEmpty(baseUrl)) + { + throw new ArgumentNullException(baseUrl); + } + + var dlnaCommand = BuildDlnaParam(this); + + var extension = string.IsNullOrEmpty(Container) ? string.Empty : "." + Container; + + baseUrl = baseUrl.TrimEnd('/'); + + if (MediaType == DlnaProfileType.Audio) + { + return string.Format("{0}/audio/{1}/stream{2}?{3}", baseUrl, ItemId, extension, dlnaCommand); + } + return string.Format("{0}/videos/{1}/stream{2}?{3}", baseUrl, ItemId, extension, dlnaCommand); + } + + private static string BuildDlnaParam(StreamInfo item) + { + var usCulture = new CultureInfo("en-US"); + + var list = new List<string> + { + item.DeviceProfileId ?? string.Empty, + item.DeviceId ?? string.Empty, + item.MediaSourceId ?? string.Empty, + (item.IsDirectStream).ToString().ToLower(), + item.VideoCodec ?? string.Empty, + item.AudioCodec ?? string.Empty, + item.AudioStreamIndex.HasValue ? item.AudioStreamIndex.Value.ToString(usCulture) : string.Empty, + item.SubtitleStreamIndex.HasValue ? item.SubtitleStreamIndex.Value.ToString(usCulture) : string.Empty, + item.VideoBitrate.HasValue ? item.VideoBitrate.Value.ToString(usCulture) : string.Empty, + item.AudioBitrate.HasValue ? item.AudioBitrate.Value.ToString(usCulture) : string.Empty, + item.MaxAudioChannels.HasValue ? item.MaxAudioChannels.Value.ToString(usCulture) : string.Empty, + item.MaxFramerate.HasValue ? item.MaxFramerate.Value.ToString(usCulture) : string.Empty, + item.MaxWidth.HasValue ? item.MaxWidth.Value.ToString(usCulture) : string.Empty, + item.MaxHeight.HasValue ? item.MaxHeight.Value.ToString(usCulture) : string.Empty, + item.StartPositionTicks.ToString(usCulture), + item.VideoLevel.HasValue ? item.VideoLevel.Value.ToString(usCulture) : string.Empty + }; + + return string.Format("Params={0}", string.Join(";", list.ToArray())); + } + + } + + /// <summary> + /// Class AudioOptions. + /// </summary> + public class AudioOptions + { + public string ItemId { get; set; } + public List<MediaSourceInfo> MediaSources { get; set; } + public int? MaxBitrateSetting { get; set; } + public DeviceProfile Profile { get; set; } + public string MediaSourceId { get; set; } + public string DeviceId { get; set; } + } + + /// <summary> + /// Class VideoOptions. + /// </summary> + public class VideoOptions : AudioOptions + { + public int? AudioStreamIndex { get; set; } + public int? SubtitleStreamIndex { get; set; } + } +} diff --git a/MediaBrowser.Model/Dlna/TranscodingProfile.cs b/MediaBrowser.Model/Dlna/TranscodingProfile.cs new file mode 100644 index 000000000..ba02e9be2 --- /dev/null +++ b/MediaBrowser.Model/Dlna/TranscodingProfile.cs @@ -0,0 +1,47 @@ +using System.Collections.Generic; +using System.Linq; +using System.Xml.Serialization; + +namespace MediaBrowser.Model.Dlna +{ + public class TranscodingProfile + { + [XmlAttribute("container")] + public string Container { get; set; } + + [XmlAttribute("type")] + public DlnaProfileType Type { get; set; } + + [XmlAttribute("videoCodec")] + public string VideoCodec { get; set; } + + [XmlAttribute("audioCodec")] + public string AudioCodec { get; set; } + + [XmlAttribute("protocol")] + public string Protocol { get; set; } + + [XmlAttribute("estimateContentLength")] + public bool EstimateContentLength { get; set; } + + [XmlAttribute("enableMpegtsM2TsMode")] + public bool EnableMpegtsM2TsMode { get; set; } + + [XmlAttribute("transcodeSeekInfo")] + public TranscodeSeekInfo TranscodeSeekInfo { get; set; } + + [XmlAttribute("videoProfile")] + public string VideoProfile { get; set; } + + public List<string> GetAudioCodecs() + { + return (AudioCodec ?? string.Empty).Split(',').Where(i => !string.IsNullOrEmpty(i)).ToList(); + } + } + + public enum TranscodeSeekInfo + { + Auto = 0, + Bytes = 1 + } +} diff --git a/MediaBrowser.Model/Dto/MediaVersionInfo.cs b/MediaBrowser.Model/Dto/MediaVersionInfo.cs index ddb340787..37aefae55 100644 --- a/MediaBrowser.Model/Dto/MediaVersionInfo.cs +++ b/MediaBrowser.Model/Dto/MediaVersionInfo.cs @@ -9,6 +9,8 @@ namespace MediaBrowser.Model.Dto public string Path { get; set; } + public string Container { get; set; } + public LocationType LocationType { get; set; } public string Name { get; set; } diff --git a/MediaBrowser.Model/Extensions/ModelExtensions.cs b/MediaBrowser.Model/Extensions/ModelExtensions.cs deleted file mode 100644 index 6717be8d9..000000000 --- a/MediaBrowser.Model/Extensions/ModelExtensions.cs +++ /dev/null @@ -1,20 +0,0 @@ - -namespace MediaBrowser.Model.Extensions -{ - /// <summary> - /// Class ModelExtensions - /// </summary> - static class ModelExtensions - { - /// <summary> - /// Values the or default. - /// </summary> - /// <param name="str">The STR.</param> - /// <param name="def">The def.</param> - /// <returns>System.String.</returns> - internal static string ValueOrDefault(this string str, string def = "") - { - return string.IsNullOrEmpty(str) ? def : str; - } - } -} diff --git a/MediaBrowser.Model/MediaBrowser.Model.csproj b/MediaBrowser.Model/MediaBrowser.Model.csproj index 0859f6999..ec78a2b1b 100644 --- a/MediaBrowser.Model/MediaBrowser.Model.csproj +++ b/MediaBrowser.Model/MediaBrowser.Model.csproj @@ -66,7 +66,16 @@ <Compile Include="Configuration\MetadataPlugin.cs" /> <Compile Include="Configuration\MetadataOptions.cs" /> <Compile Include="Configuration\ServerConfiguration.cs" /> + <Compile Include="Dlna\CodecProfile.cs" /> + <Compile Include="Dlna\ContainerProfile.cs" /> + <Compile Include="Dlna\DeviceIdentification.cs" /> + <Compile Include="Dlna\DeviceProfile.cs" /> <Compile Include="Dlna\DeviceProfileInfo.cs" /> + <Compile Include="Dlna\DirectPlayProfile.cs" /> + <Compile Include="Dlna\ResponseProfile.cs" /> + <Compile Include="Dlna\StreamBuilder.cs" /> + <Compile Include="Dlna\StreamInfo.cs" /> + <Compile Include="Dlna\TranscodingProfile.cs" /> <Compile Include="Drawing\ImageOutputFormat.cs" /> <Compile Include="Dto\BaseItemPerson.cs" /> <Compile Include="Dto\ChapterInfoDto.cs" /> @@ -155,7 +164,6 @@ <Compile Include="Entities\ParentalRating.cs" /> <Compile Include="Dto\StreamOptions.cs" /> <Compile Include="Entities\VirtualFolderInfo.cs" /> - <Compile Include="Extensions\ModelExtensions.cs" /> <Compile Include="IO\IZipClient.cs" /> <Compile Include="Logging\ILogger.cs" /> <Compile Include="Logging\LogSeverity.cs" /> @@ -223,6 +231,7 @@ <HintPath>..\packages\PropertyChanged.Fody.1.41.0.0\Lib\NET35\PropertyChanged.dll</HintPath> <Private>False</Private> </Reference> + <Reference Include="System.XML" /> </ItemGroup> <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" /> <PropertyGroup> diff --git a/MediaBrowser.Model/Updates/PackageVersionInfo.cs b/MediaBrowser.Model/Updates/PackageVersionInfo.cs index 8b7b65f0c..3b0a94019 100644 --- a/MediaBrowser.Model/Updates/PackageVersionInfo.cs +++ b/MediaBrowser.Model/Updates/PackageVersionInfo.cs @@ -1,5 +1,4 @@ -using MediaBrowser.Model.Extensions; -using System; +using System; using System.Runtime.Serialization; namespace MediaBrowser.Model.Updates @@ -39,7 +38,18 @@ namespace MediaBrowser.Model.Updates [IgnoreDataMember] public Version version { - get { return _version ?? (_version = new Version(versionStr.ValueOrDefault("0.0.0.1"))); } + get { return _version ?? (_version = new Version(ValueOrDefault(versionStr, "0.0.0.1"))); } + } + + /// <summary> + /// Values the or default. + /// </summary> + /// <param name="str">The STR.</param> + /// <param name="def">The def.</param> + /// <returns>System.String.</returns> + private static string ValueOrDefault(string str, string def = "") + { + return string.IsNullOrEmpty(str) ? def : str; } /// <summary> |
