diff options
| author | Andrew Rabert <ar@nullsum.net> | 2018-12-27 18:27:57 -0500 |
|---|---|---|
| committer | Andrew Rabert <ar@nullsum.net> | 2018-12-27 18:27:57 -0500 |
| commit | a86b71899ec52c44ddc6c3018e8cc5e9d7ff4d62 (patch) | |
| tree | a74f6ea4a8abfa1664a605d31d48bc38245ccf58 /MediaBrowser.Model/Dlna | |
| parent | 9bac3ac616b01f67db98381feb09d34ebe821f9a (diff) | |
Add GPL modules
Diffstat (limited to 'MediaBrowser.Model/Dlna')
42 files changed, 5518 insertions, 0 deletions
diff --git a/MediaBrowser.Model/Dlna/AudioOptions.cs b/MediaBrowser.Model/Dlna/AudioOptions.cs new file mode 100644 index 0000000000..189f646350 --- /dev/null +++ b/MediaBrowser.Model/Dlna/AudioOptions.cs @@ -0,0 +1,87 @@ +using MediaBrowser.Model.Dto; +using System.Collections.Generic; +using System; + +namespace MediaBrowser.Model.Dlna +{ + /// <summary> + /// Class AudioOptions. + /// </summary> + public class AudioOptions + { + public AudioOptions() + { + Context = EncodingContext.Streaming; + + EnableDirectPlay = true; + EnableDirectStream = true; + } + + public bool EnableDirectPlay { get; set; } + public bool EnableDirectStream { get; set; } + public bool ForceDirectPlay { get; set; } + public bool ForceDirectStream { get; set; } + + public Guid ItemId { get; set; } + public MediaSourceInfo[] MediaSources { get; set; } + public DeviceProfile Profile { get; set; } + + /// <summary> + /// Optional. Only needed if a specific AudioStreamIndex or SubtitleStreamIndex are requested. + /// </summary> + public string MediaSourceId { get; set; } + + public string DeviceId { get; set; } + + /// <summary> + /// Allows an override of supported number of audio channels + /// Example: DeviceProfile supports five channel, but user only has stereo speakers + /// </summary> + public int? MaxAudioChannels { get; set; } + + /// <summary> + /// The application's configured quality setting + /// </summary> + public long? MaxBitrate { get; set; } + + /// <summary> + /// Gets or sets the context. + /// </summary> + /// <value>The context.</value> + public EncodingContext Context { get; set; } + + /// <summary> + /// Gets or sets the audio transcoding bitrate. + /// </summary> + /// <value>The audio transcoding bitrate.</value> + public int? AudioTranscodingBitrate { get; set; } + + /// <summary> + /// Gets the maximum bitrate. + /// </summary> + /// <returns>System.Nullable<System.Int32>.</returns> + public long? GetMaxBitrate(bool isAudio) + { + if (MaxBitrate.HasValue) + { + return MaxBitrate; + } + + if (Profile != null) + { + if (Context == EncodingContext.Static) + { + if (isAudio && Profile.MaxStaticMusicBitrate.HasValue) + { + return Profile.MaxStaticMusicBitrate; + } + return Profile.MaxStaticBitrate; + } + + return Profile.MaxStreamingBitrate; + } + + return null; + } + } +}
\ No newline at end of file diff --git a/MediaBrowser.Model/Dlna/CodecProfile.cs b/MediaBrowser.Model/Dlna/CodecProfile.cs new file mode 100644 index 0000000000..6d143962dd --- /dev/null +++ b/MediaBrowser.Model/Dlna/CodecProfile.cs @@ -0,0 +1,68 @@ +using MediaBrowser.Model.Extensions; +using System.Collections.Generic; +using System.Xml.Serialization; +using MediaBrowser.Model.Dlna; + +namespace MediaBrowser.Model.Dlna +{ + public class CodecProfile + { + [XmlAttribute("type")] + public CodecType Type { get; set; } + + public ProfileCondition[] Conditions { get; set; } + + public ProfileCondition[] ApplyConditions { get; set; } + + [XmlAttribute("codec")] + public string Codec { get; set; } + + [XmlAttribute("container")] + public string Container { get; set; } + + public CodecProfile() + { + Conditions = new ProfileCondition[] {}; + ApplyConditions = new ProfileCondition[] { }; + } + + public string[] GetCodecs() + { + return ContainerProfile.SplitValue(Codec); + } + + private bool ContainsContainer(string container) + { + return ContainerProfile.ContainsContainer(Container, container); + } + + public bool ContainsAnyCodec(string codec, string container) + { + return ContainsAnyCodec(ContainerProfile.SplitValue(codec), container); + } + + public bool ContainsAnyCodec(string[] codec, string container) + { + if (!ContainsContainer(container)) + { + return false; + } + + var codecs = GetCodecs(); + if (codecs.Length == 0) + { + return true; + } + + foreach (var val in codec) + { + if (ListHelper.ContainsIgnoreCase(codecs, val)) + { + return true; + } + } + + return false; + } + } +} diff --git a/MediaBrowser.Model/Dlna/CodecType.cs b/MediaBrowser.Model/Dlna/CodecType.cs new file mode 100644 index 0000000000..415cae7acf --- /dev/null +++ b/MediaBrowser.Model/Dlna/CodecType.cs @@ -0,0 +1,9 @@ +namespace MediaBrowser.Model.Dlna +{ + public enum CodecType + { + Video = 0, + VideoAudio = 1, + Audio = 2 + } +}
\ No newline at end of file diff --git a/MediaBrowser.Model/Dlna/ConditionProcessor.cs b/MediaBrowser.Model/Dlna/ConditionProcessor.cs new file mode 100644 index 0000000000..a550ee9826 --- /dev/null +++ b/MediaBrowser.Model/Dlna/ConditionProcessor.cs @@ -0,0 +1,284 @@ +using MediaBrowser.Model.Extensions; +using MediaBrowser.Model.MediaInfo; +using System; +using System.Collections.Generic; +using System.Globalization; + +namespace MediaBrowser.Model.Dlna +{ + public class ConditionProcessor + { + public bool IsVideoConditionSatisfied(ProfileCondition condition, + int? width, + int? height, + int? videoBitDepth, + int? videoBitrate, + string videoProfile, + double? videoLevel, + float? videoFramerate, + int? packetLength, + TransportStreamTimestamp? timestamp, + bool? isAnamorphic, + bool? isInterlaced, + int? refFrames, + int? numVideoStreams, + int? numAudioStreams, + string videoCodecTag, + bool? isAvc ) + { + switch (condition.Property) + { + case ProfileConditionValue.IsInterlaced: + return IsConditionSatisfied(condition, isInterlaced); + case ProfileConditionValue.IsAnamorphic: + return IsConditionSatisfied(condition, isAnamorphic); + case ProfileConditionValue.IsAvc: + return IsConditionSatisfied(condition, isAvc); + case ProfileConditionValue.VideoFramerate: + return IsConditionSatisfied(condition, videoFramerate); + case ProfileConditionValue.VideoLevel: + return IsConditionSatisfied(condition, videoLevel); + case ProfileConditionValue.VideoProfile: + return IsConditionSatisfied(condition, videoProfile); + case ProfileConditionValue.VideoCodecTag: + return IsConditionSatisfied(condition, videoCodecTag); + case ProfileConditionValue.PacketLength: + return IsConditionSatisfied(condition, packetLength); + case ProfileConditionValue.VideoBitDepth: + return IsConditionSatisfied(condition, videoBitDepth); + case ProfileConditionValue.VideoBitrate: + return IsConditionSatisfied(condition, videoBitrate); + case ProfileConditionValue.Height: + return IsConditionSatisfied(condition, height); + case ProfileConditionValue.Width: + return IsConditionSatisfied(condition, width); + case ProfileConditionValue.RefFrames: + return IsConditionSatisfied(condition, refFrames); + case ProfileConditionValue.NumAudioStreams: + return IsConditionSatisfied(condition, numAudioStreams); + case ProfileConditionValue.NumVideoStreams: + return IsConditionSatisfied(condition, numVideoStreams); + case ProfileConditionValue.VideoTimestamp: + return IsConditionSatisfied(condition, timestamp); + default: + return true; + } + } + + public bool IsImageConditionSatisfied(ProfileCondition condition, int? width, int? height) + { + switch (condition.Property) + { + case ProfileConditionValue.Height: + return IsConditionSatisfied(condition, height); + case ProfileConditionValue.Width: + return IsConditionSatisfied(condition, width); + default: + throw new ArgumentException("Unexpected condition on image file: " + condition.Property); + } + } + + public bool IsAudioConditionSatisfied(ProfileCondition condition, int? audioChannels, int? audioBitrate, int? audioSampleRate, int? audioBitDepth) + { + switch (condition.Property) + { + case ProfileConditionValue.AudioBitrate: + return IsConditionSatisfied(condition, audioBitrate); + case ProfileConditionValue.AudioChannels: + return IsConditionSatisfied(condition, audioChannels); + case ProfileConditionValue.AudioSampleRate: + return IsConditionSatisfied(condition, audioSampleRate); + case ProfileConditionValue.AudioBitDepth: + return IsConditionSatisfied(condition, audioBitDepth); + default: + throw new ArgumentException("Unexpected condition on audio file: " + condition.Property); + } + } + + public bool IsVideoAudioConditionSatisfied(ProfileCondition condition, + int? audioChannels, + int? audioBitrate, + int? audioSampleRate, + int? audioBitDepth, + string audioProfile, + bool? isSecondaryTrack) + { + switch (condition.Property) + { + case ProfileConditionValue.AudioProfile: + return IsConditionSatisfied(condition, audioProfile); + case ProfileConditionValue.AudioBitrate: + return IsConditionSatisfied(condition, audioBitrate); + case ProfileConditionValue.AudioChannels: + return IsConditionSatisfied(condition, audioChannels); + case ProfileConditionValue.IsSecondaryAudio: + return IsConditionSatisfied(condition, isSecondaryTrack); + case ProfileConditionValue.AudioSampleRate: + return IsConditionSatisfied(condition, audioSampleRate); + case ProfileConditionValue.AudioBitDepth: + return IsConditionSatisfied(condition, audioBitDepth); + default: + throw new ArgumentException("Unexpected condition on audio file: " + condition.Property); + } + } + + private bool IsConditionSatisfied(ProfileCondition condition, int? currentValue) + { + if (!currentValue.HasValue) + { + // If the value is unknown, it satisfies if not marked as required + return !condition.IsRequired; + } + + int expected; + if (int.TryParse(condition.Value, NumberStyles.Any, CultureInfo.InvariantCulture, out expected)) + { + switch (condition.Condition) + { + case ProfileConditionType.Equals: + case ProfileConditionType.EqualsAny: + return currentValue.Value.Equals(expected); + case ProfileConditionType.GreaterThanEqual: + return currentValue.Value >= expected; + case ProfileConditionType.LessThanEqual: + return currentValue.Value <= expected; + case ProfileConditionType.NotEquals: + return !currentValue.Value.Equals(expected); + default: + throw new InvalidOperationException("Unexpected ProfileConditionType: " + condition.Condition); + } + } + + return false; + } + + private bool IsConditionSatisfied(ProfileCondition condition, string currentValue) + { + if (string.IsNullOrEmpty(currentValue)) + { + // If the value is unknown, it satisfies if not marked as required + return !condition.IsRequired; + } + + string expected = condition.Value; + + switch (condition.Condition) + { + case ProfileConditionType.EqualsAny: + { + return ListHelper.ContainsIgnoreCase(expected.Split('|'), currentValue); + } + case ProfileConditionType.Equals: + return StringHelper.EqualsIgnoreCase(currentValue, expected); + case ProfileConditionType.NotEquals: + return !StringHelper.EqualsIgnoreCase(currentValue, expected); + default: + throw new InvalidOperationException("Unexpected ProfileConditionType: " + condition.Condition); + } + } + + private bool IsConditionSatisfied(ProfileCondition condition, bool? currentValue) + { + if (!currentValue.HasValue) + { + // If the value is unknown, it satisfies if not marked as required + return !condition.IsRequired; + } + + bool expected; + if (bool.TryParse(condition.Value, out expected)) + { + switch (condition.Condition) + { + case ProfileConditionType.Equals: + return currentValue.Value == expected; + case ProfileConditionType.NotEquals: + return currentValue.Value != expected; + default: + throw new InvalidOperationException("Unexpected ProfileConditionType: " + condition.Condition); + } + } + + return false; + } + + private bool IsConditionSatisfied(ProfileCondition condition, float currentValue) + { + if (currentValue <= 0) + { + // If the value is unknown, it satisfies if not marked as required + return !condition.IsRequired; + } + + float expected; + if (float.TryParse(condition.Value, NumberStyles.Any, CultureInfo.InvariantCulture, out expected)) + { + switch (condition.Condition) + { + case ProfileConditionType.Equals: + return currentValue.Equals(expected); + case ProfileConditionType.GreaterThanEqual: + return currentValue >= expected; + case ProfileConditionType.LessThanEqual: + return currentValue <= expected; + case ProfileConditionType.NotEquals: + return !currentValue.Equals(expected); + default: + throw new InvalidOperationException("Unexpected ProfileConditionType: " + condition.Condition); + } + } + + return false; + } + + private bool IsConditionSatisfied(ProfileCondition condition, double? currentValue) + { + if (!currentValue.HasValue) + { + // If the value is unknown, it satisfies if not marked as required + return !condition.IsRequired; + } + + double expected; + if (double.TryParse(condition.Value, NumberStyles.Any, CultureInfo.InvariantCulture, out expected)) + { + switch (condition.Condition) + { + case ProfileConditionType.Equals: + return currentValue.Value.Equals(expected); + case ProfileConditionType.GreaterThanEqual: + return currentValue.Value >= expected; + case ProfileConditionType.LessThanEqual: + return currentValue.Value <= expected; + case ProfileConditionType.NotEquals: + return !currentValue.Value.Equals(expected); + default: + throw new InvalidOperationException("Unexpected ProfileConditionType: " + condition.Condition); + } + } + + return false; + } + + private bool IsConditionSatisfied(ProfileCondition condition, TransportStreamTimestamp? timestamp) + { + if (!timestamp.HasValue) + { + // If the value is unknown, it satisfies if not marked as required + return !condition.IsRequired; + } + + TransportStreamTimestamp expected = (TransportStreamTimestamp)Enum.Parse(typeof(TransportStreamTimestamp), condition.Value, true); + + switch (condition.Condition) + { + case ProfileConditionType.Equals: + return timestamp == expected; + case ProfileConditionType.NotEquals: + return timestamp != expected; + default: + throw new InvalidOperationException("Unexpected ProfileConditionType: " + condition.Condition); + } + } + } +} diff --git a/MediaBrowser.Model/Dlna/ContainerProfile.cs b/MediaBrowser.Model/Dlna/ContainerProfile.cs new file mode 100644 index 0000000000..3fb0682b03 --- /dev/null +++ b/MediaBrowser.Model/Dlna/ContainerProfile.cs @@ -0,0 +1,101 @@ +using System; +using System.Collections.Generic; +using System.Xml.Serialization; +using MediaBrowser.Model.Dlna; +using MediaBrowser.Model.Extensions; + +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 string[] GetContainers() + { + return SplitValue(Container); + } + + private static readonly string[] EmptyStringArray = Array.Empty<string>(); + + public static string[] SplitValue(string value) + { + if (string.IsNullOrEmpty(value)) + { + return EmptyStringArray; + } + + return value.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries); + } + + public bool ContainsContainer(string container) + { + var containers = GetContainers(); + + return ContainsContainer(containers, container); + } + + public static bool ContainsContainer(string profileContainers, string inputContainer) + { + var isNegativeList = false; + if (profileContainers != null && profileContainers.StartsWith("-")) + { + isNegativeList = true; + profileContainers = profileContainers.Substring(1); + } + + return ContainsContainer(SplitValue(profileContainers), isNegativeList, inputContainer); + } + + public static bool ContainsContainer(string[] profileContainers, string inputContainer) + { + return ContainsContainer(profileContainers, false, inputContainer); + } + + public static bool ContainsContainer(string[] profileContainers, bool isNegativeList, string inputContainer) + { + if (profileContainers.Length == 0) + { + return true; + } + + if (isNegativeList) + { + var allInputContainers = SplitValue(inputContainer); + + foreach (var container in allInputContainers) + { + if (ListHelper.ContainsIgnoreCase(profileContainers, container)) + { + return false; + } + } + + return true; + } + else + { + var allInputContainers = SplitValue(inputContainer); + + foreach (var container in allInputContainers) + { + if (ListHelper.ContainsIgnoreCase(profileContainers, container)) + { + return true; + } + } + + return false; + } + } + } +} diff --git a/MediaBrowser.Model/Dlna/ContentFeatureBuilder.cs b/MediaBrowser.Model/Dlna/ContentFeatureBuilder.cs new file mode 100644 index 0000000000..966c4a8ce2 --- /dev/null +++ b/MediaBrowser.Model/Dlna/ContentFeatureBuilder.cs @@ -0,0 +1,236 @@ +using MediaBrowser.Model.MediaInfo; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace MediaBrowser.Model.Dlna +{ + public class ContentFeatureBuilder + { + private readonly DeviceProfile _profile; + + public ContentFeatureBuilder(DeviceProfile profile) + { + _profile = profile; + } + + public string BuildImageHeader(string container, + int? width, + int? height, + bool isDirectStream, + string orgPn = null) + { + string orgOp = ";DLNA.ORG_OP=" + DlnaMaps.GetImageOrgOpValue(); + + // 0 = native, 1 = transcoded + var orgCi = isDirectStream ? ";DLNA.ORG_CI=0" : ";DLNA.ORG_CI=1"; + + DlnaFlags flagValue = DlnaFlags.BackgroundTransferMode | + DlnaFlags.InteractiveTransferMode | + DlnaFlags.DlnaV15; + + string dlnaflags = string.Format(";DLNA.ORG_FLAGS={0}", + DlnaMaps.FlagsToString(flagValue)); + + ResponseProfile mediaProfile = _profile.GetImageMediaProfile(container, + width, + height); + + if (string.IsNullOrEmpty(orgPn)) + { + orgPn = mediaProfile == null ? null : mediaProfile.OrgPn; + } + + if (string.IsNullOrEmpty(orgPn)) + { + orgPn = GetImageOrgPnValue(container, width, height); + } + + string contentFeatures = string.IsNullOrEmpty(orgPn) ? string.Empty : "DLNA.ORG_PN=" + orgPn; + + return (contentFeatures + orgOp + orgCi + dlnaflags).Trim(';'); + } + + public string BuildAudioHeader(string container, + string audioCodec, + int? audioBitrate, + int? audioSampleRate, + int? audioChannels, + int? audioBitDepth, + bool isDirectStream, + long? runtimeTicks, + TranscodeSeekInfo transcodeSeekInfo) + { + // first bit means Time based seek supported, second byte range seek supported (not sure about the order now), so 01 = only byte seek, 10 = time based, 11 = both, 00 = none + string orgOp = ";DLNA.ORG_OP=" + DlnaMaps.GetOrgOpValue(runtimeTicks > 0, isDirectStream, transcodeSeekInfo); + + // 0 = native, 1 = transcoded + string orgCi = isDirectStream ? ";DLNA.ORG_CI=0" : ";DLNA.ORG_CI=1"; + + DlnaFlags flagValue = DlnaFlags.StreamingTransferMode | + DlnaFlags.BackgroundTransferMode | + DlnaFlags.InteractiveTransferMode | + DlnaFlags.DlnaV15; + + //if (isDirectStream) + //{ + // flagValue = flagValue | DlnaFlags.ByteBasedSeek; + //} + //else if (runtimeTicks.HasValue) + //{ + // flagValue = flagValue | DlnaFlags.TimeBasedSeek; + //} + + string dlnaflags = string.Format(";DLNA.ORG_FLAGS={0}", + DlnaMaps.FlagsToString(flagValue)); + + ResponseProfile mediaProfile = _profile.GetAudioMediaProfile(container, + audioCodec, + audioChannels, + audioBitrate, + audioSampleRate, + audioBitDepth); + + string orgPn = mediaProfile == null ? null : mediaProfile.OrgPn; + + if (string.IsNullOrEmpty(orgPn)) + { + orgPn = GetAudioOrgPnValue(container, audioBitrate, audioSampleRate, audioChannels); + } + + string contentFeatures = string.IsNullOrEmpty(orgPn) ? string.Empty : "DLNA.ORG_PN=" + orgPn; + + return (contentFeatures + orgOp + orgCi + dlnaflags).Trim(';'); + } + + public List<string> BuildVideoHeader(string container, + string videoCodec, + string audioCodec, + int? width, + int? height, + int? bitDepth, + int? videoBitrate, + TransportStreamTimestamp timestamp, + bool isDirectStream, + long? runtimeTicks, + string videoProfile, + double? videoLevel, + float? videoFramerate, + int? packetLength, + TranscodeSeekInfo transcodeSeekInfo, + bool? isAnamorphic, + bool? isInterlaced, + int? refFrames, + int? numVideoStreams, + int? numAudioStreams, + string videoCodecTag, + bool? isAvc) + { + // first bit means Time based seek supported, second byte range seek supported (not sure about the order now), so 01 = only byte seek, 10 = time based, 11 = both, 00 = none + string orgOp = ";DLNA.ORG_OP=" + DlnaMaps.GetOrgOpValue(runtimeTicks > 0, isDirectStream, transcodeSeekInfo); + + // 0 = native, 1 = transcoded + string orgCi = isDirectStream ? ";DLNA.ORG_CI=0" : ";DLNA.ORG_CI=1"; + + DlnaFlags flagValue = DlnaFlags.StreamingTransferMode | + DlnaFlags.BackgroundTransferMode | + DlnaFlags.InteractiveTransferMode | + DlnaFlags.DlnaV15; + + //if (isDirectStream) + //{ + // flagValue = flagValue | DlnaFlags.ByteBasedSeek; + //} + //else if (runtimeTicks.HasValue) + //{ + // flagValue = flagValue | DlnaFlags.TimeBasedSeek; + //} + + string dlnaflags = string.Format(";DLNA.ORG_FLAGS={0}", + DlnaMaps.FlagsToString(flagValue)); + + ResponseProfile mediaProfile = _profile.GetVideoMediaProfile(container, + audioCodec, + videoCodec, + width, + height, + bitDepth, + videoBitrate, + videoProfile, + videoLevel, + videoFramerate, + packetLength, + timestamp, + isAnamorphic, + isInterlaced, + refFrames, + numVideoStreams, + numAudioStreams, + videoCodecTag, + isAvc); + + List<string> orgPnValues = new List<string>(); + + if (mediaProfile != null && !string.IsNullOrEmpty(mediaProfile.OrgPn)) + { + orgPnValues.AddRange(mediaProfile.OrgPn.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)); + } + else + { + foreach (string s in GetVideoOrgPnValue(container, videoCodec, audioCodec, width, height, timestamp)) + { + orgPnValues.Add(s); + break; + } + } + + List<string> contentFeatureList = new List<string>(); + + foreach (string orgPn in orgPnValues) + { + string contentFeatures = string.IsNullOrEmpty(orgPn) ? string.Empty : "DLNA.ORG_PN=" + orgPn; + + var value = (contentFeatures + orgOp + orgCi + dlnaflags).Trim(';'); + + contentFeatureList.Add(value); + } + + if (orgPnValues.Count == 0) + { + string contentFeatures = string.Empty; + + var value = (contentFeatures + orgOp + orgCi + dlnaflags).Trim(';'); + + contentFeatureList.Add(value); + } + + return contentFeatureList; + } + + private string GetImageOrgPnValue(string container, int? width, int? height) + { + MediaFormatProfile? format = new MediaFormatProfileResolver() + .ResolveImageFormat(container, + width, + height); + + return format.HasValue ? format.Value.ToString() : null; + } + + private string GetAudioOrgPnValue(string container, int? audioBitrate, int? audioSampleRate, int? audioChannels) + { + MediaFormatProfile? format = new MediaFormatProfileResolver() + .ResolveAudioFormat(container, + audioBitrate, + audioSampleRate, + audioChannels); + + return format.HasValue ? format.Value.ToString() : null; + } + + private string[] GetVideoOrgPnValue(string container, string videoCodec, string audioCodec, int? width, int? height, TransportStreamTimestamp timestamp) + { + return new MediaFormatProfileResolver().ResolveVideoFormat(container, videoCodec, audioCodec, width, height, timestamp); + } + } +} diff --git a/MediaBrowser.Model/Dlna/DeviceIdentification.cs b/MediaBrowser.Model/Dlna/DeviceIdentification.cs new file mode 100644 index 0000000000..97f4409daf --- /dev/null +++ b/MediaBrowser.Model/Dlna/DeviceIdentification.cs @@ -0,0 +1,61 @@ +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[] {}; + } + } +} diff --git a/MediaBrowser.Model/Dlna/DeviceProfile.cs b/MediaBrowser.Model/Dlna/DeviceProfile.cs new file mode 100644 index 0000000000..4bf7d6b8d5 --- /dev/null +++ b/MediaBrowser.Model/Dlna/DeviceProfile.cs @@ -0,0 +1,327 @@ +using MediaBrowser.Model.Extensions; +using MediaBrowser.Model.MediaInfo; +using System.Collections.Generic; +using System.Xml.Serialization; +using System; + +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; } + + /// <summary> + /// Gets or sets the identification. + /// </summary> + /// <value>The identification.</value> + public MediaBrowser.Model.Dlna.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 string SerialNumber { get; set; } + + public bool EnableAlbumArtInDidl { get; set; } + public bool EnableSingleAlbumArtLimit { get; set; } + public bool EnableSingleSubtitleLimit { get; set; } + + public string SupportedMediaTypes { get; set; } + + public string UserId { get; set; } + + public string AlbumArtPn { get; set; } + + public int MaxAlbumArtWidth { get; set; } + public int MaxAlbumArtHeight { get; set; } + + public int? MaxIconWidth { get; set; } + public int? MaxIconHeight { get; set; } + + public long? MaxStreamingBitrate { get; set; } + public long? MaxStaticBitrate { get; set; } + + public int? MusicStreamingTranscodingBitrate { get; set; } + public int? MaxStaticMusicBitrate { get; set; } + + /// <summary> + /// Controls the content of the aggregationFlags element in the urn:schemas-sonycom:av namespace. + /// </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; } + + public bool EnableMSMediaReceiverRegistrar { get; set; } + public bool IgnoreTranscodeByteRangeRequests { get; set; } + + public XmlAttribute[] XmlRootAttributes { 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 SubtitleProfile[] SubtitleProfiles { get; set; } + + public DeviceProfile() + { + DirectPlayProfiles = new DirectPlayProfile[] { }; + TranscodingProfiles = new TranscodingProfile[] { }; + ResponseProfiles = new ResponseProfile[] { }; + CodecProfiles = new CodecProfile[] { }; + ContainerProfiles = new ContainerProfile[] { }; + SubtitleProfiles = Array.Empty<SubtitleProfile>(); + + XmlRootAttributes = new XmlAttribute[] { }; + + SupportedMediaTypes = "Audio,Photo,Video"; + MaxStreamingBitrate = 8000000; + MaxStaticBitrate = 8000000; + MusicStreamingTranscodingBitrate = 128000; + } + + public string[] GetSupportedMediaTypes() + { + return ContainerProfile.SplitValue(SupportedMediaTypes); + } + + public TranscodingProfile GetAudioTranscodingProfile(string container, string audioCodec) + { + container = (container ?? string.Empty).TrimStart('.'); + + foreach (var i in TranscodingProfiles) + { + if (i.Type != MediaBrowser.Model.Dlna.DlnaProfileType.Audio) + { + continue; + } + + if (!StringHelper.EqualsIgnoreCase(container, i.Container)) + { + continue; + } + + if (!ListHelper.ContainsIgnoreCase(i.GetAudioCodecs(), audioCodec ?? string.Empty)) + { + continue; + } + + return i; + } + return null; + } + + public TranscodingProfile GetVideoTranscodingProfile(string container, string audioCodec, string videoCodec) + { + container = (container ?? string.Empty).TrimStart('.'); + + foreach (var i in TranscodingProfiles) + { + if (i.Type != MediaBrowser.Model.Dlna.DlnaProfileType.Video) + { + continue; + } + + if (!StringHelper.EqualsIgnoreCase(container, i.Container)) + { + continue; + } + + if (!ListHelper.ContainsIgnoreCase(i.GetAudioCodecs(), audioCodec ?? string.Empty)) + { + continue; + } + + if (!StringHelper.EqualsIgnoreCase(videoCodec, i.VideoCodec ?? string.Empty)) + { + continue; + } + + return i; + } + return null; + } + + public ResponseProfile GetAudioMediaProfile(string container, string audioCodec, int? audioChannels, int? audioBitrate, int? audioSampleRate, int? audioBitDepth) + { + foreach (var i in ResponseProfiles) + { + if (i.Type != DlnaProfileType.Audio) + { + continue; + } + + if (!ContainerProfile.ContainsContainer(i.GetContainers(), container)) + { + continue; + } + + var audioCodecs = i.GetAudioCodecs(); + if (audioCodecs.Length > 0 && !ListHelper.ContainsIgnoreCase(audioCodecs, audioCodec ?? string.Empty)) + { + continue; + } + + var conditionProcessor = new ConditionProcessor(); + + var anyOff = false; + foreach (ProfileCondition c in i.Conditions) + { + if (!conditionProcessor.IsAudioConditionSatisfied(GetModelProfileCondition(c), audioChannels, audioBitrate, audioSampleRate, audioBitDepth)) + { + anyOff = true; + break; + } + } + + if (anyOff) + { + continue; + } + + return i; + } + return null; + } + + private ProfileCondition GetModelProfileCondition(ProfileCondition c) + { + return new ProfileCondition + { + Condition = c.Condition, + IsRequired = c.IsRequired, + Property = c.Property, + Value = c.Value + }; + } + + public ResponseProfile GetImageMediaProfile(string container, int? width, int? height) + { + foreach (var i in ResponseProfiles) + { + if (i.Type != DlnaProfileType.Photo) + { + continue; + } + + if (!ContainerProfile.ContainsContainer(i.GetContainers(), container)) + { + continue; + } + + var conditionProcessor = new ConditionProcessor(); + + var anyOff = false; + foreach (ProfileCondition c in i.Conditions) + { + if (!conditionProcessor.IsImageConditionSatisfied(GetModelProfileCondition(c), width, height)) + { + anyOff = true; + break; + } + } + + if (anyOff) + { + continue; + } + + return i; + } + return null; + } + + public ResponseProfile GetVideoMediaProfile(string container, + string audioCodec, + string videoCodec, + int? width, + int? height, + int? bitDepth, + int? videoBitrate, + string videoProfile, + double? videoLevel, + float? videoFramerate, + int? packetLength, + TransportStreamTimestamp timestamp, + bool? isAnamorphic, + bool? isInterlaced, + int? refFrames, + int? numVideoStreams, + int? numAudioStreams, + string videoCodecTag, + bool? isAvc) + { + foreach (var i in ResponseProfiles) + { + if (i.Type != DlnaProfileType.Video) + { + continue; + } + + if (!ContainerProfile.ContainsContainer(i.GetContainers(), container)) + { + continue; + } + + var audioCodecs = i.GetAudioCodecs(); + if (audioCodecs.Length > 0 && !ListHelper.ContainsIgnoreCase(audioCodecs, audioCodec ?? string.Empty)) + { + continue; + } + + var videoCodecs = i.GetVideoCodecs(); + if (videoCodecs.Length > 0 && !ListHelper.ContainsIgnoreCase(videoCodecs, videoCodec ?? string.Empty)) + { + continue; + } + + var conditionProcessor = new ConditionProcessor(); + + var anyOff = false; + foreach (ProfileCondition c in i.Conditions) + { + if (!conditionProcessor.IsVideoConditionSatisfied(GetModelProfileCondition(c), width, height, bitDepth, videoBitrate, videoProfile, videoLevel, videoFramerate, packetLength, timestamp, isAnamorphic, isInterlaced, refFrames, numVideoStreams, numAudioStreams, videoCodecTag, isAvc)) + { + anyOff = true; + break; + } + } + + if (anyOff) + { + continue; + } + + return i; + } + return null; + } + } +} diff --git a/MediaBrowser.Model/Dlna/DeviceProfileInfo.cs b/MediaBrowser.Model/Dlna/DeviceProfileInfo.cs new file mode 100644 index 0000000000..b2afdf2924 --- /dev/null +++ b/MediaBrowser.Model/Dlna/DeviceProfileInfo.cs @@ -0,0 +1,24 @@ + +namespace MediaBrowser.Model.Dlna +{ + public class DeviceProfileInfo + { + /// <summary> + /// Gets or sets the identifier. + /// </summary> + /// <value>The identifier.</value> + public string Id { get; set; } + + /// <summary> + /// Gets or sets the name. + /// </summary> + /// <value>The name.</value> + public string Name { get; set; } + + /// <summary> + /// Gets or sets the type. + /// </summary> + /// <value>The type.</value> + public DeviceProfileType Type { get; set; } + } +} diff --git a/MediaBrowser.Model/Dlna/DeviceProfileType.cs b/MediaBrowser.Model/Dlna/DeviceProfileType.cs new file mode 100644 index 0000000000..f881a45395 --- /dev/null +++ b/MediaBrowser.Model/Dlna/DeviceProfileType.cs @@ -0,0 +1,8 @@ +namespace MediaBrowser.Model.Dlna +{ + public enum DeviceProfileType + { + System = 0, + User = 1 + } +}
\ No newline at end of file diff --git a/MediaBrowser.Model/Dlna/DirectPlayProfile.cs b/MediaBrowser.Model/Dlna/DirectPlayProfile.cs new file mode 100644 index 0000000000..4279b545d6 --- /dev/null +++ b/MediaBrowser.Model/Dlna/DirectPlayProfile.cs @@ -0,0 +1,36 @@ +using System; +using System.Collections.Generic; +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 bool SupportsContainer(string container) + { + return ContainerProfile.ContainsContainer(Container, container); + } + + public bool SupportsVideoCodec(string codec) + { + return ContainerProfile.ContainsContainer(VideoCodec, codec); + } + + public bool SupportsAudioCodec(string codec) + { + return ContainerProfile.ContainsContainer(AudioCodec, codec); + } + } +} diff --git a/MediaBrowser.Model/Dlna/DlnaFlags.cs b/MediaBrowser.Model/Dlna/DlnaFlags.cs new file mode 100644 index 0000000000..b981e8455c --- /dev/null +++ b/MediaBrowser.Model/Dlna/DlnaFlags.cs @@ -0,0 +1,48 @@ +using System; + +namespace MediaBrowser.Model.Dlna +{ + [Flags] + public enum DlnaFlags : ulong + { + /*! <i>Background</i> transfer mode. + For use with upload and download transfers to and from the server. + The primary difference between \ref DH_TransferMode_Interactive and + \ref DH_TransferMode_Bulk is that the latter assumes that the user + is not relying on the transfer for immediately rendering the content + and there are no issues with causing a buffer overflow if the + receiver uses TCP flow control to reduce total throughput. + */ + BackgroundTransferMode = 1 << 22, + + ByteBasedSeek = 1 << 29, + ConnectionStall = 1 << 21, + + DlnaV15 = 1 << 20, + + /*! <i>Interactive</i> transfer mode. + For best effort transfer of images and non-real-time transfers. + URIs with image content usually support \ref DH_TransferMode_Bulk too. + The primary difference between \ref DH_TransferMode_Interactive and + \ref DH_TransferMode_Bulk is that the former assumes that the + transfer is intended for immediate rendering. + */ + InteractiveTransferMode = 1 << 23, + + PlayContainer = 1 << 28, + RtspPause = 1 << 25, + S0Increase = 1 << 27, + SenderPaced = 1L << 31, + SnIncrease = 1 << 26, + + /*! <i>Streaming</i> transfer mode. + The server transmits at a throughput sufficient for real-time playback of + audio or video. URIs with audio or video often support the + \ref DH_TransferMode_Interactive and \ref DH_TransferMode_Bulk transfer modes. + The most well-known exception to this general claim is for live streams. + */ + StreamingTransferMode = 1 << 24, + + TimeBasedSeek = 1 << 30 + } +}
\ No newline at end of file diff --git a/MediaBrowser.Model/Dlna/DlnaMaps.cs b/MediaBrowser.Model/Dlna/DlnaMaps.cs new file mode 100644 index 0000000000..8dadc32d60 --- /dev/null +++ b/MediaBrowser.Model/Dlna/DlnaMaps.cs @@ -0,0 +1,56 @@ +namespace MediaBrowser.Model.Dlna +{ + public class DlnaMaps + { + private static readonly string DefaultStreaming = + FlagsToString(DlnaFlags.StreamingTransferMode | + DlnaFlags.BackgroundTransferMode | + DlnaFlags.ConnectionStall | + DlnaFlags.ByteBasedSeek | + DlnaFlags.DlnaV15); + + private static readonly string DefaultInteractive = + FlagsToString(DlnaFlags.InteractiveTransferMode | + DlnaFlags.BackgroundTransferMode | + DlnaFlags.ConnectionStall | + DlnaFlags.ByteBasedSeek | + DlnaFlags.DlnaV15); + + public static string FlagsToString(DlnaFlags flags) + { + return string.Format("{0:X8}{1:D24}", (ulong)flags, 0); + } + + public static string GetOrgOpValue(bool hasKnownRuntime, bool isDirectStream, TranscodeSeekInfo profileTranscodeSeekInfo) + { + if (hasKnownRuntime) + { + string orgOp = string.Empty; + + // Time-based seeking currently only possible when transcoding + orgOp += isDirectStream ? "0" : "1"; + + // Byte-based seeking only possible when not transcoding + orgOp += isDirectStream || profileTranscodeSeekInfo == TranscodeSeekInfo.Bytes ? "1" : "0"; + + return orgOp; + } + + // No seeking is available if we don't know the content runtime + return "00"; + } + + public static string GetImageOrgOpValue() + { + string orgOp = string.Empty; + + // Time-based seeking currently only possible when transcoding + orgOp += "0"; + + // Byte-based seeking only possible when not transcoding + orgOp += "0"; + + return orgOp; + } + } +} diff --git a/MediaBrowser.Model/Dlna/DlnaProfileType.cs b/MediaBrowser.Model/Dlna/DlnaProfileType.cs new file mode 100644 index 0000000000..1bad14081a --- /dev/null +++ b/MediaBrowser.Model/Dlna/DlnaProfileType.cs @@ -0,0 +1,9 @@ +namespace MediaBrowser.Model.Dlna +{ + public enum DlnaProfileType + { + Audio = 0, + Video = 1, + Photo = 2 + } +}
\ No newline at end of file diff --git a/MediaBrowser.Model/Dlna/EncodingContext.cs b/MediaBrowser.Model/Dlna/EncodingContext.cs new file mode 100644 index 0000000000..f83d8ddc82 --- /dev/null +++ b/MediaBrowser.Model/Dlna/EncodingContext.cs @@ -0,0 +1,8 @@ +namespace MediaBrowser.Model.Dlna +{ + public enum EncodingContext + { + Streaming = 0, + Static = 1 + } +}
\ No newline at end of file diff --git a/MediaBrowser.Model/Dlna/HeaderMatchType.cs b/MediaBrowser.Model/Dlna/HeaderMatchType.cs new file mode 100644 index 0000000000..7a0d5c24f9 --- /dev/null +++ b/MediaBrowser.Model/Dlna/HeaderMatchType.cs @@ -0,0 +1,9 @@ +namespace MediaBrowser.Model.Dlna +{ + public enum HeaderMatchType + { + Equals = 0, + Regex = 1, + Substring = 2 + } +}
\ No newline at end of file diff --git a/MediaBrowser.Model/Dlna/HttpHeaderInfo.cs b/MediaBrowser.Model/Dlna/HttpHeaderInfo.cs new file mode 100644 index 0000000000..b4fa4e0afd --- /dev/null +++ b/MediaBrowser.Model/Dlna/HttpHeaderInfo.cs @@ -0,0 +1,17 @@ +using System.Xml.Serialization; +using MediaBrowser.Model.Dlna; + +namespace MediaBrowser.Model.Dlna +{ + public class HttpHeaderInfo + { + [XmlAttribute("name")] + public string Name { get; set; } + + [XmlAttribute("value")] + public string Value { get; set; } + + [XmlAttribute("match")] + public HeaderMatchType Match { get; set; } + } +}
\ No newline at end of file diff --git a/MediaBrowser.Model/Dlna/IDeviceDiscovery.cs b/MediaBrowser.Model/Dlna/IDeviceDiscovery.cs new file mode 100644 index 0000000000..70191ff23c --- /dev/null +++ b/MediaBrowser.Model/Dlna/IDeviceDiscovery.cs @@ -0,0 +1,11 @@ +using System; +using MediaBrowser.Model.Events; + +namespace MediaBrowser.Model.Dlna +{ + public interface IDeviceDiscovery + { + event EventHandler<GenericEventArgs<UpnpDeviceInfo>> DeviceDiscovered; + event EventHandler<GenericEventArgs<UpnpDeviceInfo>> DeviceLeft; + } +} diff --git a/MediaBrowser.Model/Dlna/ITranscoderSupport.cs b/MediaBrowser.Model/Dlna/ITranscoderSupport.cs new file mode 100644 index 0000000000..14723bd273 --- /dev/null +++ b/MediaBrowser.Model/Dlna/ITranscoderSupport.cs @@ -0,0 +1,25 @@ +namespace MediaBrowser.Model.Dlna +{ + public interface ITranscoderSupport + { + bool CanEncodeToAudioCodec(string codec); + bool CanEncodeToSubtitleCodec(string codec); + bool CanExtractSubtitles(string codec); + } + + public class FullTranscoderSupport : ITranscoderSupport + { + public bool CanEncodeToAudioCodec(string codec) + { + return true; + } + public bool CanEncodeToSubtitleCodec(string codec) + { + return true; + } + public bool CanExtractSubtitles(string codec) + { + return true; + } + } +} diff --git a/MediaBrowser.Model/Dlna/MediaFormatProfile.cs b/MediaBrowser.Model/Dlna/MediaFormatProfile.cs new file mode 100644 index 0000000000..f3d04335fb --- /dev/null +++ b/MediaBrowser.Model/Dlna/MediaFormatProfile.cs @@ -0,0 +1,113 @@ + +namespace MediaBrowser.Model.Dlna +{ + public enum MediaFormatProfile + { + MP3, + WMA_BASE, + WMA_FULL, + LPCM16_44_MONO, + LPCM16_44_STEREO, + LPCM16_48_MONO, + LPCM16_48_STEREO, + AAC_ISO, + AAC_ISO_320, + AAC_ADTS, + AAC_ADTS_320, + FLAC, + OGG, + + JPEG_SM, + JPEG_MED, + JPEG_LRG, + JPEG_TN, + PNG_LRG, + PNG_TN, + GIF_LRG, + RAW, + + MPEG1, + MPEG_PS_PAL, + MPEG_PS_NTSC, + MPEG_TS_SD_EU, + MPEG_TS_SD_EU_ISO, + MPEG_TS_SD_EU_T, + MPEG_TS_SD_NA, + MPEG_TS_SD_NA_ISO, + MPEG_TS_SD_NA_T, + MPEG_TS_SD_KO, + MPEG_TS_SD_KO_ISO, + MPEG_TS_SD_KO_T, + MPEG_TS_JP_T, + AVI, + MATROSKA, + FLV, + DVR_MS, + WTV, + OGV, + AVC_MP4_MP_SD_AAC_MULT5, + AVC_MP4_MP_SD_MPEG1_L3, + AVC_MP4_MP_SD_AC3, + AVC_MP4_MP_HD_720p_AAC, + AVC_MP4_MP_HD_1080i_AAC, + AVC_MP4_HP_HD_AAC, + AVC_TS_MP_HD_AAC_MULT5, + AVC_TS_MP_HD_AAC_MULT5_T, + AVC_TS_MP_HD_AAC_MULT5_ISO, + AVC_TS_MP_HD_MPEG1_L3, + AVC_TS_MP_HD_MPEG1_L3_T, + AVC_TS_MP_HD_MPEG1_L3_ISO, + AVC_TS_MP_HD_AC3, + AVC_TS_MP_HD_AC3_T, + AVC_TS_MP_HD_AC3_ISO, + AVC_TS_HP_HD_MPEG1_L2_T, + AVC_TS_HP_HD_MPEG1_L2_ISO, + AVC_TS_MP_SD_AAC_MULT5, + AVC_TS_MP_SD_AAC_MULT5_T, + AVC_TS_MP_SD_AAC_MULT5_ISO, + AVC_TS_MP_SD_MPEG1_L3, + AVC_TS_MP_SD_MPEG1_L3_T, + AVC_TS_MP_SD_MPEG1_L3_ISO, + AVC_TS_HP_SD_MPEG1_L2_T, + AVC_TS_HP_SD_MPEG1_L2_ISO, + AVC_TS_MP_SD_AC3, + AVC_TS_MP_SD_AC3_T, + AVC_TS_MP_SD_AC3_ISO, + AVC_TS_HD_DTS_T, + AVC_TS_HD_DTS_ISO, + WMVMED_BASE, + WMVMED_FULL, + WMVMED_PRO, + WMVHIGH_FULL, + WMVHIGH_PRO, + VC1_ASF_AP_L1_WMA, + VC1_ASF_AP_L2_WMA, + VC1_ASF_AP_L3_WMA, + VC1_TS_AP_L1_AC3_ISO, + VC1_TS_AP_L2_AC3_ISO, + VC1_TS_HD_DTS_ISO, + VC1_TS_HD_DTS_T, + MPEG4_P2_MP4_ASP_AAC, + MPEG4_P2_MP4_SP_L6_AAC, + MPEG4_P2_MP4_NDSD, + MPEG4_P2_TS_ASP_AAC, + MPEG4_P2_TS_ASP_AAC_T, + MPEG4_P2_TS_ASP_AAC_ISO, + MPEG4_P2_TS_ASP_MPEG1_L3, + MPEG4_P2_TS_ASP_MPEG1_L3_T, + MPEG4_P2_TS_ASP_MPEG1_L3_ISO, + MPEG4_P2_TS_ASP_MPEG2_L2, + MPEG4_P2_TS_ASP_MPEG2_L2_T, + MPEG4_P2_TS_ASP_MPEG2_L2_ISO, + MPEG4_P2_TS_ASP_AC3, + MPEG4_P2_TS_ASP_AC3_T, + MPEG4_P2_TS_ASP_AC3_ISO, + AVC_TS_HD_50_LPCM_T, + AVC_MP4_LPCM, + MPEG4_P2_3GPP_SP_L0B_AAC, + MPEG4_P2_3GPP_SP_L0B_AMR, + AVC_3GPP_BL_QCIF15_AAC, + MPEG4_H263_3GPP_P0_L10_AMR, + MPEG4_H263_MP4_P0_L10_AAC + } +} diff --git a/MediaBrowser.Model/Dlna/MediaFormatProfileResolver.cs b/MediaBrowser.Model/Dlna/MediaFormatProfileResolver.cs new file mode 100644 index 0000000000..b6f3293874 --- /dev/null +++ b/MediaBrowser.Model/Dlna/MediaFormatProfileResolver.cs @@ -0,0 +1,439 @@ +using MediaBrowser.Model.Extensions; +using MediaBrowser.Model.MediaInfo; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace MediaBrowser.Model.Dlna +{ + public class MediaFormatProfileResolver + { + public string[] ResolveVideoFormat(string container, string videoCodec, string audioCodec, int? width, int? height, TransportStreamTimestamp timestampType) + { + return ResolveVideoFormatInternal(container, videoCodec, audioCodec, width, height, timestampType) + .Select(i => i.ToString()) + .ToArray(); + } + + private MediaFormatProfile[] ResolveVideoFormatInternal(string container, string videoCodec, string audioCodec, int? width, int? height, TransportStreamTimestamp timestampType) + { + if (StringHelper.EqualsIgnoreCase(container, "asf")) + { + MediaFormatProfile? val = ResolveVideoASFFormat(videoCodec, audioCodec, width, height); + return val.HasValue ? new MediaFormatProfile[] { val.Value } : new MediaFormatProfile[] { }; + } + + if (StringHelper.EqualsIgnoreCase(container, "mp4")) + { + MediaFormatProfile? val = ResolveVideoMP4Format(videoCodec, audioCodec, width, height); + return val.HasValue ? new MediaFormatProfile[] { val.Value } : new MediaFormatProfile[] { }; + } + + if (StringHelper.EqualsIgnoreCase(container, "avi")) + return new MediaFormatProfile[] { MediaFormatProfile.AVI }; + + if (StringHelper.EqualsIgnoreCase(container, "mkv")) + return new MediaFormatProfile[] { MediaFormatProfile.MATROSKA }; + + if (StringHelper.EqualsIgnoreCase(container, "mpeg2ps") || + StringHelper.EqualsIgnoreCase(container, "ts")) + + return new MediaFormatProfile[] { MediaFormatProfile.MPEG_PS_NTSC, MediaFormatProfile.MPEG_PS_PAL }; + + if (StringHelper.EqualsIgnoreCase(container, "mpeg1video")) + return new MediaFormatProfile[] { MediaFormatProfile.MPEG1 }; + + if (StringHelper.EqualsIgnoreCase(container, "mpeg2ts") || + StringHelper.EqualsIgnoreCase(container, "mpegts") || + StringHelper.EqualsIgnoreCase(container, "m2ts")) + { + + return ResolveVideoMPEG2TSFormat(videoCodec, audioCodec, width, height, timestampType); + } + + if (StringHelper.EqualsIgnoreCase(container, "flv")) + return new MediaFormatProfile[] { MediaFormatProfile.FLV }; + + if (StringHelper.EqualsIgnoreCase(container, "wtv")) + return new MediaFormatProfile[] { MediaFormatProfile.WTV }; + + if (StringHelper.EqualsIgnoreCase(container, "3gp")) + { + MediaFormatProfile? val = ResolveVideo3GPFormat(videoCodec, audioCodec); + return val.HasValue ? new MediaFormatProfile[] { val.Value } : new MediaFormatProfile[] { }; + } + + if (StringHelper.EqualsIgnoreCase(container, "ogv") || StringHelper.EqualsIgnoreCase(container, "ogg")) + return new MediaFormatProfile[] { MediaFormatProfile.OGV }; + + return new MediaFormatProfile[] { }; + } + + private MediaFormatProfile[] ResolveVideoMPEG2TSFormat(string videoCodec, string audioCodec, int? width, int? height, TransportStreamTimestamp timestampType) + { + string suffix = ""; + + switch (timestampType) + { + case TransportStreamTimestamp.None: + suffix = "_ISO"; + break; + case TransportStreamTimestamp.Valid: + suffix = "_T"; + break; + } + + string resolution = "S"; + if ((width.HasValue && width.Value > 720) || (height.HasValue && height.Value > 576)) + { + resolution = "H"; + } + + if (StringHelper.EqualsIgnoreCase(videoCodec, "mpeg2video")) + { + List<MediaFormatProfile> list = new List<MediaFormatProfile>(); + + list.Add(ValueOf("MPEG_TS_SD_NA" + suffix)); + list.Add(ValueOf("MPEG_TS_SD_EU" + suffix)); + list.Add(ValueOf("MPEG_TS_SD_KO" + suffix)); + + if ((timestampType == TransportStreamTimestamp.Valid) && StringHelper.EqualsIgnoreCase(audioCodec, "aac")) + { + list.Add(MediaFormatProfile.MPEG_TS_JP_T); + } + return list.ToArray(list.Count); + } + if (StringHelper.EqualsIgnoreCase(videoCodec, "h264")) + { + if (StringHelper.EqualsIgnoreCase(audioCodec, "lpcm")) + return new MediaFormatProfile[] { MediaFormatProfile.AVC_TS_HD_50_LPCM_T }; + + if (StringHelper.EqualsIgnoreCase(audioCodec, "dts")) + { + if (timestampType == TransportStreamTimestamp.None) + { + return new MediaFormatProfile[] { MediaFormatProfile.AVC_TS_HD_DTS_ISO }; + } + return new MediaFormatProfile[] { MediaFormatProfile.AVC_TS_HD_DTS_T }; + } + + if (StringHelper.EqualsIgnoreCase(audioCodec, "mp2")) + { + if (timestampType == TransportStreamTimestamp.None) + { + return new MediaFormatProfile[] { ValueOf(string.Format("AVC_TS_HP_{0}D_MPEG1_L2_ISO", resolution)) }; + } + + return new MediaFormatProfile[] { ValueOf(string.Format("AVC_TS_HP_{0}D_MPEG1_L2_T", resolution)) }; + } + + if (StringHelper.EqualsIgnoreCase(audioCodec, "aac")) + return new MediaFormatProfile[] { ValueOf(string.Format("AVC_TS_MP_{0}D_AAC_MULT5{1}", resolution, suffix)) }; + + if (StringHelper.EqualsIgnoreCase(audioCodec, "mp3")) + return new MediaFormatProfile[] { ValueOf(string.Format("AVC_TS_MP_{0}D_MPEG1_L3{1}", resolution, suffix)) }; + + if (string.IsNullOrEmpty(audioCodec) || + StringHelper.EqualsIgnoreCase(audioCodec, "ac3")) + return new MediaFormatProfile[] { ValueOf(string.Format("AVC_TS_MP_{0}D_AC3{1}", resolution, suffix)) }; + } + else if (StringHelper.EqualsIgnoreCase(videoCodec, "vc1")) + { + if (string.IsNullOrEmpty(audioCodec) || StringHelper.EqualsIgnoreCase(audioCodec, "ac3")) + { + if ((width.HasValue && width.Value > 720) || (height.HasValue && height.Value > 576)) + { + return new MediaFormatProfile[] { MediaFormatProfile.VC1_TS_AP_L2_AC3_ISO }; + } + return new MediaFormatProfile[] { MediaFormatProfile.VC1_TS_AP_L1_AC3_ISO }; + } + if (StringHelper.EqualsIgnoreCase(audioCodec, "dts")) + { + suffix = StringHelper.EqualsIgnoreCase(suffix, "_ISO") ? suffix : "_T"; + + return new MediaFormatProfile[] { ValueOf(string.Format("VC1_TS_HD_DTS{0}", suffix)) }; + } + + } + else if (StringHelper.EqualsIgnoreCase(videoCodec, "mpeg4") || StringHelper.EqualsIgnoreCase(videoCodec, "msmpeg4")) + { + if (StringHelper.EqualsIgnoreCase(audioCodec, "aac")) + return new MediaFormatProfile[] { ValueOf(string.Format("MPEG4_P2_TS_ASP_AAC{0}", suffix)) }; + if (StringHelper.EqualsIgnoreCase(audioCodec, "mp3")) + return new MediaFormatProfile[] { ValueOf(string.Format("MPEG4_P2_TS_ASP_MPEG1_L3{0}", suffix)) }; + if (StringHelper.EqualsIgnoreCase(audioCodec, "mp2")) + return new MediaFormatProfile[] { ValueOf(string.Format("MPEG4_P2_TS_ASP_MPEG2_L2{0}", suffix)) }; + if (StringHelper.EqualsIgnoreCase(audioCodec, "ac3")) + return new MediaFormatProfile[] { ValueOf(string.Format("MPEG4_P2_TS_ASP_AC3{0}", suffix)) }; + } + + return new MediaFormatProfile[]{}; + } + + private MediaFormatProfile ValueOf(string value) + { + return (MediaFormatProfile)Enum.Parse(typeof(MediaFormatProfile), value, true); + } + + private MediaFormatProfile? ResolveVideoMP4Format(string videoCodec, string audioCodec, int? width, int? height) + { + if (StringHelper.EqualsIgnoreCase(videoCodec, "h264")) + { + if (StringHelper.EqualsIgnoreCase(audioCodec, "lpcm")) + return MediaFormatProfile.AVC_MP4_LPCM; + if (string.IsNullOrEmpty(audioCodec) || + StringHelper.EqualsIgnoreCase(audioCodec, "ac3")) + { + return MediaFormatProfile.AVC_MP4_MP_SD_AC3; + } + if (StringHelper.EqualsIgnoreCase(audioCodec, "mp3")) + { + return MediaFormatProfile.AVC_MP4_MP_SD_MPEG1_L3; + } + if (width.HasValue && height.HasValue) + { + if ((width.Value <= 720) && (height.Value <= 576)) + { + if (StringHelper.EqualsIgnoreCase(audioCodec, "aac")) + return MediaFormatProfile.AVC_MP4_MP_SD_AAC_MULT5; + } + else if ((width.Value <= 1280) && (height.Value <= 720)) + { + if (StringHelper.EqualsIgnoreCase(audioCodec, "aac")) + return MediaFormatProfile.AVC_MP4_MP_HD_720p_AAC; + } + else if ((width.Value <= 1920) && (height.Value <= 1080)) + { + if (StringHelper.EqualsIgnoreCase(audioCodec, "aac")) + { + return MediaFormatProfile.AVC_MP4_MP_HD_1080i_AAC; + } + } + } + } + else if (StringHelper.EqualsIgnoreCase(videoCodec, "mpeg4") || + StringHelper.EqualsIgnoreCase(videoCodec, "msmpeg4")) + { + if (width.HasValue && height.HasValue && width.Value <= 720 && height.Value <= 576) + { + if (string.IsNullOrEmpty(audioCodec) || StringHelper.EqualsIgnoreCase(audioCodec, "aac")) + return MediaFormatProfile.MPEG4_P2_MP4_ASP_AAC; + if (StringHelper.EqualsIgnoreCase(audioCodec, "ac3") || StringHelper.EqualsIgnoreCase(audioCodec, "mp3")) + { + return MediaFormatProfile.MPEG4_P2_MP4_NDSD; + } + } + else if (string.IsNullOrEmpty(audioCodec) || StringHelper.EqualsIgnoreCase(audioCodec, "aac")) + { + return MediaFormatProfile.MPEG4_P2_MP4_SP_L6_AAC; + } + } + else if (StringHelper.EqualsIgnoreCase(videoCodec, "h263") && StringHelper.EqualsIgnoreCase(audioCodec, "aac")) + { + return MediaFormatProfile.MPEG4_H263_MP4_P0_L10_AAC; + } + + return null; + } + + private MediaFormatProfile? ResolveVideo3GPFormat(string videoCodec, string audioCodec) + { + if (StringHelper.EqualsIgnoreCase(videoCodec, "h264")) + { + if (string.IsNullOrEmpty(audioCodec) || StringHelper.EqualsIgnoreCase(audioCodec, "aac")) + return MediaFormatProfile.AVC_3GPP_BL_QCIF15_AAC; + } + else if (StringHelper.EqualsIgnoreCase(videoCodec, "mpeg4") || + StringHelper.EqualsIgnoreCase(videoCodec, "msmpeg4")) + { + if (string.IsNullOrEmpty(audioCodec) || StringHelper.EqualsIgnoreCase(audioCodec, "wma")) + return MediaFormatProfile.MPEG4_P2_3GPP_SP_L0B_AAC; + if (StringHelper.EqualsIgnoreCase(audioCodec, "amrnb")) + return MediaFormatProfile.MPEG4_P2_3GPP_SP_L0B_AMR; + } + else if (StringHelper.EqualsIgnoreCase(videoCodec, "h263") && StringHelper.EqualsIgnoreCase(audioCodec, "amrnb")) + { + return MediaFormatProfile.MPEG4_H263_3GPP_P0_L10_AMR; + } + + return null; + } + + private MediaFormatProfile? ResolveVideoASFFormat(string videoCodec, string audioCodec, int? width, int? height) + { + if (StringHelper.EqualsIgnoreCase(videoCodec, "wmv") && + (string.IsNullOrEmpty(audioCodec) || StringHelper.EqualsIgnoreCase(audioCodec, "wma") || StringHelper.EqualsIgnoreCase(videoCodec, "wmapro"))) + { + + if (width.HasValue && height.HasValue) + { + if ((width.Value <= 720) && (height.Value <= 576)) + { + if (string.IsNullOrEmpty(audioCodec) || StringHelper.EqualsIgnoreCase(audioCodec, "wma")) + { + return MediaFormatProfile.WMVMED_FULL; + } + return MediaFormatProfile.WMVMED_PRO; + } + } + + if (string.IsNullOrEmpty(audioCodec) || StringHelper.EqualsIgnoreCase(audioCodec, "wma")) + { + return MediaFormatProfile.WMVHIGH_FULL; + } + return MediaFormatProfile.WMVHIGH_PRO; + } + + if (StringHelper.EqualsIgnoreCase(videoCodec, "vc1")) + { + if (width.HasValue && height.HasValue) + { + if ((width.Value <= 720) && (height.Value <= 576)) + return MediaFormatProfile.VC1_ASF_AP_L1_WMA; + if ((width.Value <= 1280) && (height.Value <= 720)) + return MediaFormatProfile.VC1_ASF_AP_L2_WMA; + if ((width.Value <= 1920) && (height.Value <= 1080)) + return MediaFormatProfile.VC1_ASF_AP_L3_WMA; + } + } + else if (StringHelper.EqualsIgnoreCase(videoCodec, "mpeg2video")) + { + return MediaFormatProfile.DVR_MS; + } + + return null; + } + + public MediaFormatProfile? ResolveAudioFormat(string container, int? bitrate, int? frequency, int? channels) + { + if (StringHelper.EqualsIgnoreCase(container, "asf")) + return ResolveAudioASFFormat(bitrate); + + if (StringHelper.EqualsIgnoreCase(container, "mp3")) + return MediaFormatProfile.MP3; + + if (StringHelper.EqualsIgnoreCase(container, "lpcm")) + return ResolveAudioLPCMFormat(frequency, channels); + + if (StringHelper.EqualsIgnoreCase(container, "mp4") || + StringHelper.EqualsIgnoreCase(container, "aac")) + return ResolveAudioMP4Format(bitrate); + + if (StringHelper.EqualsIgnoreCase(container, "adts")) + return ResolveAudioADTSFormat(bitrate); + + if (StringHelper.EqualsIgnoreCase(container, "flac")) + return MediaFormatProfile.FLAC; + + if (StringHelper.EqualsIgnoreCase(container, "oga") || + StringHelper.EqualsIgnoreCase(container, "ogg")) + return MediaFormatProfile.OGG; + + return null; + } + + private MediaFormatProfile ResolveAudioASFFormat(int? bitrate) + { + if (bitrate.HasValue && bitrate.Value <= 193) + { + return MediaFormatProfile.WMA_BASE; + } + return MediaFormatProfile.WMA_FULL; + } + + private MediaFormatProfile? ResolveAudioLPCMFormat(int? frequency, int? channels) + { + if (frequency.HasValue && channels.HasValue) + { + if (frequency.Value == 44100 && channels.Value == 1) + { + return MediaFormatProfile.LPCM16_44_MONO; + } + if (frequency.Value == 44100 && channels.Value == 2) + { + return MediaFormatProfile.LPCM16_44_STEREO; + } + if (frequency.Value == 48000 && channels.Value == 1) + { + return MediaFormatProfile.LPCM16_48_MONO; + } + if (frequency.Value == 48000 && channels.Value == 2) + { + return MediaFormatProfile.LPCM16_48_STEREO; + } + + return null; + } + + return MediaFormatProfile.LPCM16_48_STEREO; + } + + private MediaFormatProfile ResolveAudioMP4Format(int? bitrate) + { + if (bitrate.HasValue && bitrate.Value <= 320) + { + return MediaFormatProfile.AAC_ISO_320; + } + return MediaFormatProfile.AAC_ISO; + } + + private MediaFormatProfile ResolveAudioADTSFormat(int? bitrate) + { + if (bitrate.HasValue && bitrate.Value <= 320) + { + return MediaFormatProfile.AAC_ADTS_320; + } + return MediaFormatProfile.AAC_ADTS; + } + + public MediaFormatProfile? ResolveImageFormat(string container, int? width, int? height) + { + if (StringHelper.EqualsIgnoreCase(container, "jpeg") || + StringHelper.EqualsIgnoreCase(container, "jpg")) + return ResolveImageJPGFormat(width, height); + + if (StringHelper.EqualsIgnoreCase(container, "png")) + return ResolveImagePNGFormat(width, height); + + if (StringHelper.EqualsIgnoreCase(container, "gif")) + return MediaFormatProfile.GIF_LRG; + + if (StringHelper.EqualsIgnoreCase(container, "raw")) + return MediaFormatProfile.RAW; + + return null; + } + + private MediaFormatProfile ResolveImageJPGFormat(int? width, int? height) + { + if (width.HasValue && height.HasValue) + { + if ((width.Value <= 160) && (height.Value <= 160)) + return MediaFormatProfile.JPEG_TN; + + if ((width.Value <= 640) && (height.Value <= 480)) + return MediaFormatProfile.JPEG_SM; + + if ((width.Value <= 1024) && (height.Value <= 768)) + { + return MediaFormatProfile.JPEG_MED; + } + + return MediaFormatProfile.JPEG_LRG; + } + + return MediaFormatProfile.JPEG_SM; + } + + private MediaFormatProfile ResolveImagePNGFormat(int? width, int? height) + { + if (width.HasValue && height.HasValue) + { + if ((width.Value <= 160) && (height.Value <= 160)) + return MediaFormatProfile.PNG_TN; + } + + return MediaFormatProfile.PNG_LRG; + } + } +} diff --git a/MediaBrowser.Model/Dlna/PlaybackErrorCode.cs b/MediaBrowser.Model/Dlna/PlaybackErrorCode.cs new file mode 100644 index 0000000000..4ed4129854 --- /dev/null +++ b/MediaBrowser.Model/Dlna/PlaybackErrorCode.cs @@ -0,0 +1,10 @@ + +namespace MediaBrowser.Model.Dlna +{ + public enum PlaybackErrorCode + { + NotAllowed = 0, + NoCompatibleStream = 1, + RateLimitExceeded = 2 + } +} diff --git a/MediaBrowser.Model/Dlna/ProfileCondition.cs b/MediaBrowser.Model/Dlna/ProfileCondition.cs new file mode 100644 index 0000000000..9234a27136 --- /dev/null +++ b/MediaBrowser.Model/Dlna/ProfileCondition.cs @@ -0,0 +1,38 @@ +using System.Xml.Serialization; + +namespace MediaBrowser.Model.Dlna +{ + 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 ProfileCondition(ProfileConditionType condition, ProfileConditionValue property, string value) + : this(condition, property, value, false) + { + + } + + public ProfileCondition(ProfileConditionType condition, ProfileConditionValue property, string value, bool isRequired) + { + Condition = condition; + Property = property; + Value = value; + IsRequired = isRequired; + } + } +}
\ No newline at end of file diff --git a/MediaBrowser.Model/Dlna/ProfileConditionType.cs b/MediaBrowser.Model/Dlna/ProfileConditionType.cs new file mode 100644 index 0000000000..b0a94c5b30 --- /dev/null +++ b/MediaBrowser.Model/Dlna/ProfileConditionType.cs @@ -0,0 +1,11 @@ +namespace MediaBrowser.Model.Dlna +{ + public enum ProfileConditionType + { + Equals = 0, + NotEquals = 1, + LessThanEqual = 2, + GreaterThanEqual = 3, + EqualsAny = 4 + } +}
\ No newline at end of file diff --git a/MediaBrowser.Model/Dlna/ProfileConditionValue.cs b/MediaBrowser.Model/Dlna/ProfileConditionValue.cs new file mode 100644 index 0000000000..a96e9ac364 --- /dev/null +++ b/MediaBrowser.Model/Dlna/ProfileConditionValue.cs @@ -0,0 +1,29 @@ +namespace MediaBrowser.Model.Dlna +{ + public enum ProfileConditionValue + { + AudioChannels = 0, + AudioBitrate = 1, + AudioProfile = 2, + Width = 3, + Height = 4, + Has64BitOffsets = 5, + PacketLength = 6, + VideoBitDepth = 7, + VideoBitrate = 8, + VideoFramerate = 9, + VideoLevel = 10, + VideoProfile = 11, + VideoTimestamp = 12, + IsAnamorphic = 13, + RefFrames = 14, + NumAudioStreams = 16, + NumVideoStreams = 17, + IsSecondaryAudio = 18, + VideoCodecTag = 19, + IsAvc = 20, + IsInterlaced = 21, + AudioSampleRate = 22, + AudioBitDepth = 23 + } +}
\ No newline at end of file diff --git a/MediaBrowser.Model/Dlna/ResolutionConfiguration.cs b/MediaBrowser.Model/Dlna/ResolutionConfiguration.cs new file mode 100644 index 0000000000..8efdb06609 --- /dev/null +++ b/MediaBrowser.Model/Dlna/ResolutionConfiguration.cs @@ -0,0 +1,14 @@ +namespace MediaBrowser.Model.Dlna +{ + public class ResolutionConfiguration + { + public int MaxWidth { get; set; } + public int MaxBitrate { get; set; } + + public ResolutionConfiguration(int maxWidth, int maxBitrate) + { + MaxWidth = maxWidth; + MaxBitrate = maxBitrate; + } + } +}
\ No newline at end of file diff --git a/MediaBrowser.Model/Dlna/ResolutionNormalizer.cs b/MediaBrowser.Model/Dlna/ResolutionNormalizer.cs new file mode 100644 index 0000000000..4fdf4972f6 --- /dev/null +++ b/MediaBrowser.Model/Dlna/ResolutionNormalizer.cs @@ -0,0 +1,99 @@ +using System; +using System.Collections.Generic; +using MediaBrowser.Model.Extensions; + +namespace MediaBrowser.Model.Dlna +{ + public class ResolutionNormalizer + { + private static readonly ResolutionConfiguration[] Configurations = + new [] + { + new ResolutionConfiguration(426, 320000), + new ResolutionConfiguration(640, 400000), + new ResolutionConfiguration(720, 950000), + new ResolutionConfiguration(1280, 2500000), + new ResolutionConfiguration(1920, 4000000), + new ResolutionConfiguration(3840, 35000000) + }; + + public static ResolutionOptions Normalize(int? inputBitrate, + int? unused1, + int? unused2, + int outputBitrate, + string inputCodec, + string outputCodec, + int? maxWidth, + int? maxHeight) + { + // If the bitrate isn't changing, then don't downlscale the resolution + if (inputBitrate.HasValue && outputBitrate >= inputBitrate.Value) + { + if (maxWidth.HasValue || maxHeight.HasValue) + { + return new ResolutionOptions + { + MaxWidth = maxWidth, + MaxHeight = maxHeight + }; + } + } + + var resolutionConfig = GetResolutionConfiguration(outputBitrate); + if (resolutionConfig != null) + { + var originvalValue = maxWidth; + + maxWidth = Math.Min(resolutionConfig.MaxWidth, maxWidth ?? resolutionConfig.MaxWidth); + if (!originvalValue.HasValue || originvalValue.Value != maxWidth.Value) + { + maxHeight = null; + } + } + + return new ResolutionOptions + { + MaxWidth = maxWidth, + MaxHeight = maxHeight + }; + } + + private static ResolutionConfiguration GetResolutionConfiguration(int outputBitrate) + { + ResolutionConfiguration previousOption = null; + + foreach (var config in Configurations) + { + if (outputBitrate <= config.MaxBitrate) + { + return previousOption ?? config; + } + + previousOption = config; + } + + return null; + } + + private static double GetVideoBitrateScaleFactor(string codec) + { + if (StringHelper.EqualsIgnoreCase(codec, "h265") || + StringHelper.EqualsIgnoreCase(codec, "hevc") || + StringHelper.EqualsIgnoreCase(codec, "vp9")) + { + return .5; + } + return 1; + } + + public static int ScaleBitrate(int bitrate, string inputVideoCodec, string outputVideoCodec) + { + var inputScaleFactor = GetVideoBitrateScaleFactor(inputVideoCodec); + var outputScaleFactor = GetVideoBitrateScaleFactor(outputVideoCodec); + var scaleFactor = outputScaleFactor/inputScaleFactor; + var newBitrate = scaleFactor*bitrate; + + return Convert.ToInt32(newBitrate); + } + } +} diff --git a/MediaBrowser.Model/Dlna/ResolutionOptions.cs b/MediaBrowser.Model/Dlna/ResolutionOptions.cs new file mode 100644 index 0000000000..6b711cfa0d --- /dev/null +++ b/MediaBrowser.Model/Dlna/ResolutionOptions.cs @@ -0,0 +1,8 @@ +namespace MediaBrowser.Model.Dlna +{ + public class ResolutionOptions + { + public int? MaxWidth { get; set; } + public int? MaxHeight { get; set; } + } +}
\ No newline at end of file diff --git a/MediaBrowser.Model/Dlna/ResponseProfile.cs b/MediaBrowser.Model/Dlna/ResponseProfile.cs new file mode 100644 index 0000000000..742253fa35 --- /dev/null +++ b/MediaBrowser.Model/Dlna/ResponseProfile.cs @@ -0,0 +1,49 @@ +using System.Collections.Generic; +using System.Xml.Serialization; +using MediaBrowser.Model.Dlna; + +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 string[] GetContainers() + { + return ContainerProfile.SplitValue(Container); + } + + public string[] GetAudioCodecs() + { + return ContainerProfile.SplitValue(AudioCodec); + } + + public string[] GetVideoCodecs() + { + return ContainerProfile.SplitValue(VideoCodec); + } + } +} diff --git a/MediaBrowser.Model/Dlna/SearchCriteria.cs b/MediaBrowser.Model/Dlna/SearchCriteria.cs new file mode 100644 index 0000000000..533605d892 --- /dev/null +++ b/MediaBrowser.Model/Dlna/SearchCriteria.cs @@ -0,0 +1,75 @@ +using MediaBrowser.Model.Extensions; +using System; +using System.Text.RegularExpressions; + +namespace MediaBrowser.Model.Dlna +{ + public class SearchCriteria + { + public SearchType SearchType { get; set; } + + /// <summary> + /// Splits the specified string. + /// </summary> + /// <param name="str">The string.</param> + /// <param name="term">The term.</param> + /// <param name="limit">The limit.</param> + /// <returns>System.String[].</returns> + private string[] RegexSplit(string str, string term, int limit) + { + return new Regex(term).Split(str, limit); + } + + /// <summary> + /// Splits the specified string. + /// </summary> + /// <param name="str">The string.</param> + /// <param name="term">The term.</param> + /// <returns>System.String[].</returns> + private string[] RegexSplit(string str, string term) + { + return Regex.Split(str, term, RegexOptions.IgnoreCase); + } + + public SearchCriteria(string search) + { + if (string.IsNullOrEmpty(search)) + { + throw new ArgumentNullException("search"); + } + + SearchType = SearchType.Unknown; + + String[] factors = RegexSplit(search, "(and|or)"); + foreach (String factor in factors) + { + String[] subFactors = RegexSplit(factor.Trim().Trim('(').Trim(')').Trim(), "\\s", 3); + + if (subFactors.Length == 3) + { + + if (StringHelper.EqualsIgnoreCase("upnp:class", subFactors[0]) && + (StringHelper.EqualsIgnoreCase("=", subFactors[1]) || StringHelper.EqualsIgnoreCase("derivedfrom", subFactors[1]))) + { + if (StringHelper.EqualsIgnoreCase("\"object.item.imageItem\"", subFactors[2]) || StringHelper.EqualsIgnoreCase("\"object.item.imageItem.photo\"", subFactors[2])) + { + SearchType = SearchType.Image; + } + else if (StringHelper.EqualsIgnoreCase("\"object.item.videoItem\"", subFactors[2])) + { + SearchType = SearchType.Video; + } + else if (StringHelper.EqualsIgnoreCase("\"object.container.playlistContainer\"", subFactors[2])) + { + SearchType = SearchType.Playlist; + } + else if (StringHelper.EqualsIgnoreCase("\"object.container.album.musicAlbum\"", subFactors[2])) + { + SearchType = SearchType.MusicAlbum; + } + } + } + } + } + } +} diff --git a/MediaBrowser.Model/Dlna/SearchType.cs b/MediaBrowser.Model/Dlna/SearchType.cs new file mode 100644 index 0000000000..27b2078792 --- /dev/null +++ b/MediaBrowser.Model/Dlna/SearchType.cs @@ -0,0 +1,12 @@ +namespace MediaBrowser.Model.Dlna +{ + public enum SearchType + { + Unknown = 0, + Audio = 1, + Image = 2, + Video = 3, + Playlist = 4, + MusicAlbum = 5 + } +}
\ No newline at end of file diff --git a/MediaBrowser.Model/Dlna/SortCriteria.cs b/MediaBrowser.Model/Dlna/SortCriteria.cs new file mode 100644 index 0000000000..600a2f58e9 --- /dev/null +++ b/MediaBrowser.Model/Dlna/SortCriteria.cs @@ -0,0 +1,17 @@ +using MediaBrowser.Model.Entities; + +namespace MediaBrowser.Model.Dlna +{ + public class SortCriteria + { + public SortOrder SortOrder + { + get { return SortOrder.Ascending; } + } + + public SortCriteria(string value) + { + + } + } +} diff --git a/MediaBrowser.Model/Dlna/StreamBuilder.cs b/MediaBrowser.Model/Dlna/StreamBuilder.cs new file mode 100644 index 0000000000..840abf618c --- /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; + } + } +} diff --git a/MediaBrowser.Model/Dlna/StreamInfo.cs b/MediaBrowser.Model/Dlna/StreamInfo.cs new file mode 100644 index 0000000000..46a1cd68b7 --- /dev/null +++ b/MediaBrowser.Model/Dlna/StreamInfo.cs @@ -0,0 +1,1092 @@ +using MediaBrowser.Model.Drawing; +using MediaBrowser.Model.Dto; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Extensions; +using MediaBrowser.Model.MediaInfo; +using MediaBrowser.Model.Session; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; + +namespace MediaBrowser.Model.Dlna +{ + /// <summary> + /// Class StreamInfo. + /// </summary> + public class StreamInfo + { + public StreamInfo() + { + AudioCodecs = new string[] { }; + VideoCodecs = new string[] { }; + SubtitleCodecs = new string[] { }; + TranscodeReasons = new TranscodeReason[] { }; + StreamOptions = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); + } + + public void SetOption(string qualifier, string name, string value) + { + if (string.IsNullOrEmpty(qualifier)) + { + SetOption(name, value); + } + else + { + SetOption(qualifier + "-" + name, value); + } + } + + public void SetOption(string name, string value) + { + StreamOptions[name] = value; + } + + public string GetOption(string qualifier, string name) + { + var value = GetOption(qualifier + "-" + name); + + if (string.IsNullOrEmpty(value)) + { + value = GetOption(name); + } + + return value; + } + + public string GetOption(string name) + { + string value; + if (StreamOptions.TryGetValue(name, out value)) + { + return value; + } + + return null; + } + + public Guid ItemId { get; set; } + + public PlayMethod PlayMethod { get; set; } + public EncodingContext Context { get; set; } + + public DlnaProfileType MediaType { get; set; } + + public string Container { get; set; } + + public string SubProtocol { get; set; } + + public long StartPositionTicks { get; set; } + + public int? SegmentLength { get; set; } + public int? MinSegments { get; set; } + public bool BreakOnNonKeyFrames { get; set; } + + public bool RequireAvc { get; set; } + public bool RequireNonAnamorphic { get; set; } + public bool CopyTimestamps { get; set; } + public bool EnableMpegtsM2TsMode { get; set; } + public bool EnableSubtitlesInManifest { get; set; } + public string[] AudioCodecs { get; set; } + public string[] VideoCodecs { get; set; } + + public int? AudioStreamIndex { get; set; } + + public int? SubtitleStreamIndex { get; set; } + + public int? TranscodingMaxAudioChannels { get; set; } + public int? GlobalMaxAudioChannels { get; set; } + + public int? AudioBitrate { get; set; } + + public int? VideoBitrate { get; set; } + + public int? MaxWidth { get; set; } + public int? MaxHeight { get; set; } + + public float? MaxFramerate { get; set; } + + public DeviceProfile DeviceProfile { get; set; } + public string DeviceProfileId { get; set; } + public string DeviceId { get; set; } + + public long? RunTimeTicks { get; set; } + + public TranscodeSeekInfo TranscodeSeekInfo { get; set; } + + public bool EstimateContentLength { get; set; } + + public MediaSourceInfo MediaSource { get; set; } + + public string[] SubtitleCodecs { get; set; } + public SubtitleDeliveryMethod SubtitleDeliveryMethod { get; set; } + public string SubtitleFormat { get; set; } + + public string PlaySessionId { get; set; } + public TranscodeReason[] TranscodeReasons { get; set; } + + public Dictionary<string, string> StreamOptions { get; private set; } + + public string MediaSourceId + { + get + { + return MediaSource == null ? null : MediaSource.Id; + } + } + + public bool IsDirectStream + { + get + { + return PlayMethod == PlayMethod.DirectStream || + PlayMethod == PlayMethod.DirectPlay; + } + } + + public string ToUrl(string baseUrl, string accessToken) + { + if (PlayMethod == PlayMethod.DirectPlay) + { + return MediaSource.Path; + } + + if (string.IsNullOrEmpty(baseUrl)) + { + throw new ArgumentNullException(baseUrl); + } + + List<string> list = new List<string>(); + foreach (NameValuePair pair in BuildParams(this, accessToken)) + { + if (string.IsNullOrEmpty(pair.Value)) + { + continue; + } + + // Try to keep the url clean by omitting defaults + if (StringHelper.EqualsIgnoreCase(pair.Name, "StartTimeTicks") && + StringHelper.EqualsIgnoreCase(pair.Value, "0")) + { + continue; + } + if (StringHelper.EqualsIgnoreCase(pair.Name, "SubtitleStreamIndex") && + StringHelper.EqualsIgnoreCase(pair.Value, "-1")) + { + continue; + } + if (StringHelper.EqualsIgnoreCase(pair.Name, "Static") && + StringHelper.EqualsIgnoreCase(pair.Value, "false")) + { + continue; + } + + var encodedValue = pair.Value.Replace(" ", "%20"); + + list.Add(string.Format("{0}={1}", pair.Name, encodedValue)); + } + + string queryString = string.Join("&", list.ToArray(list.Count)); + + return GetUrl(baseUrl, queryString); + } + + private string GetUrl(string baseUrl, string queryString) + { + if (string.IsNullOrEmpty(baseUrl)) + { + throw new ArgumentNullException(baseUrl); + } + + string extension = string.IsNullOrEmpty(Container) ? string.Empty : "." + Container; + + baseUrl = baseUrl.TrimEnd('/'); + + if (MediaType == DlnaProfileType.Audio) + { + if (StringHelper.EqualsIgnoreCase(SubProtocol, "hls")) + { + return string.Format("{0}/audio/{1}/master.m3u8?{2}", baseUrl, ItemId, queryString); + } + + return string.Format("{0}/audio/{1}/stream{2}?{3}", baseUrl, ItemId, extension, queryString); + } + + if (StringHelper.EqualsIgnoreCase(SubProtocol, "hls")) + { + return string.Format("{0}/videos/{1}/master.m3u8?{2}", baseUrl, ItemId, queryString); + } + + return string.Format("{0}/videos/{1}/stream{2}?{3}", baseUrl, ItemId, extension, queryString); + } + + private static List<NameValuePair> BuildParams(StreamInfo item, string accessToken) + { + List<NameValuePair> list = new List<NameValuePair>(); + + string audioCodecs = item.AudioCodecs.Length == 0 ? + string.Empty : + string.Join(",", item.AudioCodecs); + + string videoCodecs = item.VideoCodecs.Length == 0 ? + string.Empty : + string.Join(",", item.VideoCodecs); + + list.Add(new NameValuePair("DeviceProfileId", item.DeviceProfileId ?? string.Empty)); + list.Add(new NameValuePair("DeviceId", item.DeviceId ?? string.Empty)); + list.Add(new NameValuePair("MediaSourceId", item.MediaSourceId ?? string.Empty)); + list.Add(new NameValuePair("Static", item.IsDirectStream.ToString().ToLower())); + list.Add(new NameValuePair("VideoCodec", videoCodecs)); + list.Add(new NameValuePair("AudioCodec", audioCodecs)); + list.Add(new NameValuePair("AudioStreamIndex", item.AudioStreamIndex.HasValue ? item.AudioStreamIndex.Value.ToString(CultureInfo.InvariantCulture) : string.Empty)); + list.Add(new NameValuePair("SubtitleStreamIndex", item.SubtitleStreamIndex.HasValue && item.SubtitleDeliveryMethod != SubtitleDeliveryMethod.External ? item.SubtitleStreamIndex.Value.ToString(CultureInfo.InvariantCulture) : string.Empty)); + list.Add(new NameValuePair("VideoBitrate", item.VideoBitrate.HasValue ? item.VideoBitrate.Value.ToString(CultureInfo.InvariantCulture) : string.Empty)); + list.Add(new NameValuePair("AudioBitrate", item.AudioBitrate.HasValue ? item.AudioBitrate.Value.ToString(CultureInfo.InvariantCulture) : string.Empty)); + + list.Add(new NameValuePair("MaxFramerate", item.MaxFramerate.HasValue ? item.MaxFramerate.Value.ToString(CultureInfo.InvariantCulture) : string.Empty)); + list.Add(new NameValuePair("MaxWidth", item.MaxWidth.HasValue ? item.MaxWidth.Value.ToString(CultureInfo.InvariantCulture) : string.Empty)); + list.Add(new NameValuePair("MaxHeight", item.MaxHeight.HasValue ? item.MaxHeight.Value.ToString(CultureInfo.InvariantCulture) : string.Empty)); + + long startPositionTicks = item.StartPositionTicks; + + var isHls = StringHelper.EqualsIgnoreCase(item.SubProtocol, "hls"); + + if (isHls) + { + list.Add(new NameValuePair("StartTimeTicks", string.Empty)); + } + else + { + list.Add(new NameValuePair("StartTimeTicks", startPositionTicks.ToString(CultureInfo.InvariantCulture))); + } + + list.Add(new NameValuePair("PlaySessionId", item.PlaySessionId ?? string.Empty)); + list.Add(new NameValuePair("api_key", accessToken ?? string.Empty)); + + string liveStreamId = item.MediaSource == null ? null : item.MediaSource.LiveStreamId; + list.Add(new NameValuePair("LiveStreamId", liveStreamId ?? string.Empty)); + + list.Add(new NameValuePair("SubtitleMethod", item.SubtitleStreamIndex.HasValue && item.SubtitleDeliveryMethod != SubtitleDeliveryMethod.External ? item.SubtitleDeliveryMethod.ToString() : string.Empty)); + + + if (!item.IsDirectStream) + { + if (item.RequireNonAnamorphic) + { + list.Add(new NameValuePair("RequireNonAnamorphic", item.RequireNonAnamorphic.ToString().ToLower())); + } + + list.Add(new NameValuePair("TranscodingMaxAudioChannels", item.TranscodingMaxAudioChannels.HasValue ? item.TranscodingMaxAudioChannels.Value.ToString(CultureInfo.InvariantCulture) : string.Empty)); + + if (item.EnableSubtitlesInManifest) + { + list.Add(new NameValuePair("EnableSubtitlesInManifest", item.EnableSubtitlesInManifest.ToString().ToLower())); + } + + if (item.EnableMpegtsM2TsMode) + { + list.Add(new NameValuePair("EnableMpegtsM2TsMode", item.EnableMpegtsM2TsMode.ToString().ToLower())); + } + + if (item.EstimateContentLength) + { + list.Add(new NameValuePair("EstimateContentLength", item.EstimateContentLength.ToString().ToLower())); + } + + if (item.TranscodeSeekInfo != TranscodeSeekInfo.Auto) + { + list.Add(new NameValuePair("TranscodeSeekInfo", item.TranscodeSeekInfo.ToString().ToLower())); + } + + if (item.CopyTimestamps) + { + list.Add(new NameValuePair("CopyTimestamps", item.CopyTimestamps.ToString().ToLower())); + } + + list.Add(new NameValuePair("RequireAvc", item.RequireAvc.ToString().ToLower())); + } + + list.Add(new NameValuePair("Tag", item.MediaSource.ETag ?? string.Empty)); + + string subtitleCodecs = item.SubtitleCodecs.Length == 0 ? + string.Empty : + string.Join(",", item.SubtitleCodecs); + + list.Add(new NameValuePair("SubtitleCodec", item.SubtitleStreamIndex.HasValue && item.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Embed ? subtitleCodecs : string.Empty)); + + if (isHls) + { + list.Add(new NameValuePair("SegmentContainer", item.Container ?? string.Empty)); + + if (item.SegmentLength.HasValue) + { + list.Add(new NameValuePair("SegmentLength", item.SegmentLength.Value.ToString(CultureInfo.InvariantCulture))); + } + + if (item.MinSegments.HasValue) + { + list.Add(new NameValuePair("MinSegments", item.MinSegments.Value.ToString(CultureInfo.InvariantCulture))); + } + + list.Add(new NameValuePair("BreakOnNonKeyFrames", item.BreakOnNonKeyFrames.ToString())); + } + + foreach (var pair in item.StreamOptions) + { + if (string.IsNullOrEmpty(pair.Value)) + { + continue; + } + + // strip spaces to avoid having to encode h264 profile names + list.Add(new NameValuePair(pair.Key, pair.Value.Replace(" ", ""))); + } + + if (!item.IsDirectStream) + { + list.Add(new NameValuePair("TranscodeReasons", string.Join(",", item.TranscodeReasons.Distinct().Select(i => i.ToString()).ToArray()))); + } + + return list; + } + + public List<SubtitleStreamInfo> GetExternalSubtitles(ITranscoderSupport transcoderSupport, bool includeSelectedTrackOnly, string baseUrl, string accessToken) + { + return GetExternalSubtitles(transcoderSupport, includeSelectedTrackOnly, false, baseUrl, accessToken); + } + + public List<SubtitleStreamInfo> GetExternalSubtitles(ITranscoderSupport transcoderSupport, bool includeSelectedTrackOnly, bool enableAllProfiles, string baseUrl, string accessToken) + { + List<SubtitleStreamInfo> list = GetSubtitleProfiles(transcoderSupport, includeSelectedTrackOnly, enableAllProfiles, baseUrl, accessToken); + List<SubtitleStreamInfo> newList = new List<SubtitleStreamInfo>(); + + // First add the selected track + foreach (SubtitleStreamInfo stream in list) + { + if (stream.DeliveryMethod == SubtitleDeliveryMethod.External) + { + newList.Add(stream); + } + } + + return newList; + } + + public List<SubtitleStreamInfo> GetSubtitleProfiles(ITranscoderSupport transcoderSupport, bool includeSelectedTrackOnly, string baseUrl, string accessToken) + { + return GetSubtitleProfiles(transcoderSupport, includeSelectedTrackOnly, false, baseUrl, accessToken); + } + + public List<SubtitleStreamInfo> GetSubtitleProfiles(ITranscoderSupport transcoderSupport, bool includeSelectedTrackOnly, bool enableAllProfiles, string baseUrl, string accessToken) + { + List<SubtitleStreamInfo> list = new List<SubtitleStreamInfo>(); + + // HLS will preserve timestamps so we can just grab the full subtitle stream + long startPositionTicks = StringHelper.EqualsIgnoreCase(SubProtocol, "hls") + ? 0 + : (PlayMethod == PlayMethod.Transcode && !CopyTimestamps ? StartPositionTicks : 0); + + // First add the selected track + if (SubtitleStreamIndex.HasValue) + { + foreach (MediaStream stream in MediaSource.MediaStreams) + { + if (stream.Type == MediaStreamType.Subtitle && stream.Index == SubtitleStreamIndex.Value) + { + AddSubtitleProfiles(list, stream, transcoderSupport, enableAllProfiles, baseUrl, accessToken, startPositionTicks); + } + } + } + + if (!includeSelectedTrackOnly) + { + foreach (MediaStream stream in MediaSource.MediaStreams) + { + if (stream.Type == MediaStreamType.Subtitle && (!SubtitleStreamIndex.HasValue || stream.Index != SubtitleStreamIndex.Value)) + { + AddSubtitleProfiles(list, stream, transcoderSupport, enableAllProfiles, baseUrl, accessToken, startPositionTicks); + } + } + } + + return list; + } + + private void AddSubtitleProfiles(List<SubtitleStreamInfo> list, MediaStream stream, ITranscoderSupport transcoderSupport, bool enableAllProfiles, string baseUrl, string accessToken, long startPositionTicks) + { + if (enableAllProfiles) + { + foreach (SubtitleProfile profile in DeviceProfile.SubtitleProfiles) + { + SubtitleStreamInfo info = GetSubtitleStreamInfo(stream, baseUrl, accessToken, startPositionTicks, new[] { profile }, transcoderSupport); + + list.Add(info); + } + } + else + { + SubtitleStreamInfo info = GetSubtitleStreamInfo(stream, baseUrl, accessToken, startPositionTicks, DeviceProfile.SubtitleProfiles, transcoderSupport); + + list.Add(info); + } + } + + private SubtitleStreamInfo GetSubtitleStreamInfo(MediaStream stream, string baseUrl, string accessToken, long startPositionTicks, SubtitleProfile[] subtitleProfiles, ITranscoderSupport transcoderSupport) + { + SubtitleProfile subtitleProfile = StreamBuilder.GetSubtitleProfile(MediaSource, stream, subtitleProfiles, PlayMethod, transcoderSupport, Container, SubProtocol); + SubtitleStreamInfo info = new SubtitleStreamInfo + { + IsForced = stream.IsForced, + Language = stream.Language, + Name = stream.Language ?? "Unknown", + Format = subtitleProfile.Format, + Index = stream.Index, + DeliveryMethod = subtitleProfile.Method, + DisplayTitle = stream.DisplayTitle + }; + + if (info.DeliveryMethod == SubtitleDeliveryMethod.External) + { + if (MediaSource.Protocol == MediaProtocol.File || !StringHelper.EqualsIgnoreCase(stream.Codec, subtitleProfile.Format) || !stream.IsExternal) + { + info.Url = string.Format("{0}/Videos/{1}/{2}/Subtitles/{3}/{4}/Stream.{5}", + baseUrl, + ItemId, + MediaSourceId, + stream.Index.ToString(CultureInfo.InvariantCulture), + startPositionTicks.ToString(CultureInfo.InvariantCulture), + subtitleProfile.Format); + + if (!string.IsNullOrEmpty(accessToken)) + { + info.Url += "?api_key=" + accessToken; + } + + info.IsExternalUrl = false; + } + else + { + info.Url = stream.Path; + info.IsExternalUrl = true; + } + } + + return info; + } + + /// <summary> + /// Returns the audio stream that will be used + /// </summary> + public MediaStream TargetAudioStream + { + get + { + if (MediaSource != null) + { + return MediaSource.GetDefaultAudioStream(AudioStreamIndex); + } + + return null; + } + } + + /// <summary> + /// Returns the video stream that will be used + /// </summary> + public MediaStream TargetVideoStream + { + get + { + if (MediaSource != null) + { + return MediaSource.VideoStream; + } + + return null; + } + } + + /// <summary> + /// Predicts the audio sample rate that will be in the output stream + /// </summary> + public int? TargetAudioSampleRate + { + get + { + MediaStream stream = TargetAudioStream; + return stream == null ? null : stream.SampleRate; + } + } + + /// <summary> + /// Predicts the audio sample rate that will be in the output stream + /// </summary> + public int? TargetAudioBitDepth + { + get + { + if (IsDirectStream) + { + return TargetAudioStream == null ? (int?)null : TargetAudioStream.BitDepth; + } + + var targetAudioCodecs = TargetAudioCodec; + var audioCodec = targetAudioCodecs.Length == 0 ? null : targetAudioCodecs[0]; + if (!string.IsNullOrEmpty(audioCodec)) + { + return GetTargetAudioBitDepth(audioCodec); + } + + return TargetAudioStream == null ? (int?)null : TargetAudioStream.BitDepth; + } + } + + /// <summary> + /// Predicts the audio sample rate that will be in the output stream + /// </summary> + public int? TargetVideoBitDepth + { + get + { + if (IsDirectStream) + { + return TargetVideoStream == null ? (int?)null : TargetVideoStream.BitDepth; + } + + var targetVideoCodecs = TargetVideoCodec; + var videoCodec = targetVideoCodecs.Length == 0 ? null : targetVideoCodecs[0]; + if (!string.IsNullOrEmpty(videoCodec)) + { + return GetTargetVideoBitDepth(videoCodec); + } + + return TargetVideoStream == null ? (int?)null : TargetVideoStream.BitDepth; + } + } + + /// <summary> + /// Gets the target reference frames. + /// </summary> + /// <value>The target reference frames.</value> + public int? TargetRefFrames + { + get + { + if (IsDirectStream) + { + return TargetVideoStream == null ? (int?)null : TargetVideoStream.RefFrames; + } + + var targetVideoCodecs = TargetVideoCodec; + var videoCodec = targetVideoCodecs.Length == 0 ? null : targetVideoCodecs[0]; + if (!string.IsNullOrEmpty(videoCodec)) + { + return GetTargetRefFrames(videoCodec); + } + + return TargetVideoStream == null ? (int?)null : TargetVideoStream.RefFrames; + } + } + + /// <summary> + /// Predicts the audio sample rate that will be in the output stream + /// </summary> + public float? TargetFramerate + { + get + { + MediaStream stream = TargetVideoStream; + return MaxFramerate.HasValue && !IsDirectStream + ? MaxFramerate + : stream == null ? null : stream.AverageFrameRate ?? stream.RealFrameRate; + } + } + + /// <summary> + /// Predicts the audio sample rate that will be in the output stream + /// </summary> + public double? TargetVideoLevel + { + get + { + if (IsDirectStream) + { + return TargetVideoStream == null ? (double?)null : TargetVideoStream.Level; + } + + var targetVideoCodecs = TargetVideoCodec; + var videoCodec = targetVideoCodecs.Length == 0 ? null : targetVideoCodecs[0]; + if (!string.IsNullOrEmpty(videoCodec)) + { + return GetTargetVideoLevel(videoCodec); + } + + return TargetVideoStream == null ? (double?)null : TargetVideoStream.Level; + } + } + + public int? GetTargetVideoBitDepth(string codec) + { + var value = GetOption(codec, "videobitdepth"); + if (string.IsNullOrEmpty(value)) + { + return null; + } + + int result; + if (int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out result)) + { + return result; + } + + return null; + } + + public int? GetTargetAudioBitDepth(string codec) + { + var value = GetOption(codec, "audiobitdepth"); + if (string.IsNullOrEmpty(value)) + { + return null; + } + + int result; + if (int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out result)) + { + return result; + } + + return null; + } + + public double? GetTargetVideoLevel(string codec) + { + var value = GetOption(codec, "level"); + if (string.IsNullOrEmpty(value)) + { + return null; + } + + double result; + if (double.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out result)) + { + return result; + } + + return null; + } + + public int? GetTargetRefFrames(string codec) + { + var value = GetOption(codec, "maxrefframes"); + if (string.IsNullOrEmpty(value)) + { + return null; + } + + int result; + if (int.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out result)) + { + return result; + } + + return null; + } + + /// <summary> + /// Predicts the audio sample rate that will be in the output stream + /// </summary> + public int? TargetPacketLength + { + get + { + MediaStream stream = TargetVideoStream; + return !IsDirectStream + ? null + : stream == null ? null : stream.PacketLength; + } + } + + /// <summary> + /// Predicts the audio sample rate that will be in the output stream + /// </summary> + public string TargetVideoProfile + { + get + { + if (IsDirectStream) + { + return TargetVideoStream == null ? null : TargetVideoStream.Profile; + } + + var targetVideoCodecs = TargetVideoCodec; + var videoCodec = targetVideoCodecs.Length == 0 ? null : targetVideoCodecs[0]; + if (!string.IsNullOrEmpty(videoCodec)) + { + return GetOption(videoCodec, "profile"); + } + + return TargetVideoStream == null ? null : TargetVideoStream.Profile; + } + } + + /// <summary> + /// Gets the target video codec tag. + /// </summary> + /// <value>The target video codec tag.</value> + public string TargetVideoCodecTag + { + get + { + MediaStream stream = TargetVideoStream; + return !IsDirectStream + ? null + : stream == null ? null : stream.CodecTag; + } + } + + /// <summary> + /// Predicts the audio bitrate that will be in the output stream + /// </summary> + public int? TargetAudioBitrate + { + get + { + MediaStream stream = TargetAudioStream; + return AudioBitrate.HasValue && !IsDirectStream + ? AudioBitrate + : stream == null ? null : stream.BitRate; + } + } + + /// <summary> + /// Predicts the audio channels that will be in the output stream + /// </summary> + public int? TargetAudioChannels + { + get + { + if (IsDirectStream) + { + return TargetAudioStream == null ? (int?)null : TargetAudioStream.Channels; + } + + var targetAudioCodecs = TargetAudioCodec; + var codec = targetAudioCodecs.Length == 0 ? null : targetAudioCodecs[0]; + if (!string.IsNullOrEmpty(codec)) + { + return GetTargetRefFrames(codec); + } + + return TargetAudioStream == null ? (int?)null : TargetAudioStream.Channels; + } + } + + public int? GetTargetAudioChannels(string codec) + { + var defaultValue = GlobalMaxAudioChannels; + + var value = GetOption(codec, "audiochannels"); + if (string.IsNullOrEmpty(value)) + { + return defaultValue; + } + + int result; + if (int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out result)) + { + return Math.Min(result, defaultValue ?? result); + } + + return defaultValue; + } + + /// <summary> + /// Predicts the audio codec that will be in the output stream + /// </summary> + public string[] TargetAudioCodec + { + get + { + MediaStream stream = TargetAudioStream; + + string inputCodec = stream == null ? null : stream.Codec; + + if (IsDirectStream) + { + return string.IsNullOrEmpty(inputCodec) ? new string[] { } : new[] { inputCodec }; + } + + foreach (string codec in AudioCodecs) + { + if (StringHelper.EqualsIgnoreCase(codec, inputCodec)) + { + return string.IsNullOrEmpty(codec) ? new string[] { } : new[] { codec }; + } + } + + return AudioCodecs; + } + } + + public string[] TargetVideoCodec + { + get + { + MediaStream stream = TargetVideoStream; + + string inputCodec = stream == null ? null : stream.Codec; + + if (IsDirectStream) + { + return string.IsNullOrEmpty(inputCodec) ? new string[] { } : new[] { inputCodec }; + } + + foreach (string codec in VideoCodecs) + { + if (StringHelper.EqualsIgnoreCase(codec, inputCodec)) + { + return string.IsNullOrEmpty(codec) ? new string[] { } : new[] { codec }; + } + } + + return VideoCodecs; + } + } + + /// <summary> + /// Predicts the audio channels that will be in the output stream + /// </summary> + public long? TargetSize + { + get + { + if (IsDirectStream) + { + return MediaSource.Size; + } + + if (RunTimeTicks.HasValue) + { + int? totalBitrate = TargetTotalBitrate; + + double totalSeconds = RunTimeTicks.Value; + // Convert to ms + totalSeconds /= 10000; + // Convert to seconds + totalSeconds /= 1000; + + return totalBitrate.HasValue ? + Convert.ToInt64(totalBitrate.Value * totalSeconds) : + (long?)null; + } + + return null; + } + } + + public int? TargetVideoBitrate + { + get + { + MediaStream stream = TargetVideoStream; + + return VideoBitrate.HasValue && !IsDirectStream + ? VideoBitrate + : stream == null ? null : stream.BitRate; + } + } + + public TransportStreamTimestamp TargetTimestamp + { + get + { + TransportStreamTimestamp defaultValue = StringHelper.EqualsIgnoreCase(Container, "m2ts") + ? TransportStreamTimestamp.Valid + : TransportStreamTimestamp.None; + + return !IsDirectStream + ? defaultValue + : MediaSource == null ? defaultValue : MediaSource.Timestamp ?? TransportStreamTimestamp.None; + } + } + + public int? TargetTotalBitrate + { + get + { + return (TargetAudioBitrate ?? 0) + (TargetVideoBitrate ?? 0); + } + } + + public bool? IsTargetAnamorphic + { + get + { + if (IsDirectStream) + { + return TargetVideoStream == null ? null : TargetVideoStream.IsAnamorphic; + } + + return false; + } + } + + public bool? IsTargetInterlaced + { + get + { + if (IsDirectStream) + { + return TargetVideoStream == null ? (bool?)null : TargetVideoStream.IsInterlaced; + } + + var targetVideoCodecs = TargetVideoCodec; + var videoCodec = targetVideoCodecs.Length == 0 ? null : targetVideoCodecs[0]; + if (!string.IsNullOrEmpty(videoCodec)) + { + if (string.Equals(GetOption(videoCodec, "deinterlace"), "true", StringComparison.OrdinalIgnoreCase)) + { + return false; + } + } + + return TargetVideoStream == null ? (bool?)null : TargetVideoStream.IsInterlaced; + } + } + + public bool? IsTargetAVC + { + get + { + if (IsDirectStream) + { + return TargetVideoStream == null ? null : TargetVideoStream.IsAVC; + } + + return true; + } + } + + public int? TargetWidth + { + get + { + MediaStream videoStream = TargetVideoStream; + + if (videoStream != null && videoStream.Width.HasValue && videoStream.Height.HasValue) + { + ImageSize size = new ImageSize + { + Width = videoStream.Width.Value, + Height = videoStream.Height.Value + }; + + double? maxWidth = MaxWidth.HasValue ? (double)MaxWidth.Value : (double?)null; + double? maxHeight = MaxHeight.HasValue ? (double)MaxHeight.Value : (double?)null; + + ImageSize newSize = DrawingUtils.Resize(size, + 0, + 0, + maxWidth ?? 0, + maxHeight ?? 0); + + return Convert.ToInt32(newSize.Width); + } + + return MaxWidth; + } + } + + public int? TargetHeight + { + get + { + MediaStream videoStream = TargetVideoStream; + + if (videoStream != null && videoStream.Width.HasValue && videoStream.Height.HasValue) + { + ImageSize size = new ImageSize + { + Width = videoStream.Width.Value, + Height = videoStream.Height.Value + }; + + double? maxWidth = MaxWidth.HasValue ? (double)MaxWidth.Value : (double?)null; + double? maxHeight = MaxHeight.HasValue ? (double)MaxHeight.Value : (double?)null; + + ImageSize newSize = DrawingUtils.Resize(size, + 0, + 0, + maxWidth ?? 0, + maxHeight ?? 0); + + return Convert.ToInt32(newSize.Height); + } + + return MaxHeight; + } + } + + public int? TargetVideoStreamCount + { + get + { + if (IsDirectStream) + { + return GetMediaStreamCount(MediaStreamType.Video, int.MaxValue); + } + return GetMediaStreamCount(MediaStreamType.Video, 1); + } + } + + public int? TargetAudioStreamCount + { + get + { + if (IsDirectStream) + { + return GetMediaStreamCount(MediaStreamType.Audio, int.MaxValue); + } + return GetMediaStreamCount(MediaStreamType.Audio, 1); + } + } + + private int? GetMediaStreamCount(MediaStreamType type, int limit) + { + var count = MediaSource.GetStreamCount(type); + + if (count.HasValue) + { + count = Math.Min(count.Value, limit); + } + + return count; + } + + public List<MediaStream> GetSelectableAudioStreams() + { + return GetSelectableStreams(MediaStreamType.Audio); + } + + public List<MediaStream> GetSelectableSubtitleStreams() + { + return GetSelectableStreams(MediaStreamType.Subtitle); + } + + public List<MediaStream> GetSelectableStreams(MediaStreamType type) + { + List<MediaStream> list = new List<MediaStream>(); + + foreach (MediaStream stream in MediaSource.MediaStreams) + { + if (type == stream.Type) + { + list.Add(stream); + } + } + + return list; + } + } +} diff --git a/MediaBrowser.Model/Dlna/SubtitleDeliveryMethod.cs b/MediaBrowser.Model/Dlna/SubtitleDeliveryMethod.cs new file mode 100644 index 0000000000..b4e13c5baa --- /dev/null +++ b/MediaBrowser.Model/Dlna/SubtitleDeliveryMethod.cs @@ -0,0 +1,22 @@ +namespace MediaBrowser.Model.Dlna +{ + public enum SubtitleDeliveryMethod + { + /// <summary> + /// The encode + /// </summary> + Encode = 0, + /// <summary> + /// The embed + /// </summary> + Embed = 1, + /// <summary> + /// The external + /// </summary> + External = 2, + /// <summary> + /// The HLS + /// </summary> + Hls = 3 + } +}
\ No newline at end of file diff --git a/MediaBrowser.Model/Dlna/SubtitleProfile.cs b/MediaBrowser.Model/Dlna/SubtitleProfile.cs new file mode 100644 index 0000000000..a7e61e69a5 --- /dev/null +++ b/MediaBrowser.Model/Dlna/SubtitleProfile.cs @@ -0,0 +1,46 @@ +using MediaBrowser.Model.Extensions; +using System.Collections.Generic; +using System.Xml.Serialization; +using MediaBrowser.Model.Dlna; + +namespace MediaBrowser.Model.Dlna +{ + public class SubtitleProfile + { + [XmlAttribute("format")] + public string Format { get; set; } + + [XmlAttribute("method")] + public SubtitleDeliveryMethod Method { get; set; } + + [XmlAttribute("didlMode")] + public string DidlMode { get; set; } + + [XmlAttribute("language")] + public string Language { get; set; } + + [XmlAttribute("container")] + public string Container { get; set; } + + public string[] GetLanguages() + { + return ContainerProfile.SplitValue(Language); + } + + public bool SupportsLanguage(string subLanguage) + { + if (string.IsNullOrEmpty(Language)) + { + return true; + } + + if (string.IsNullOrEmpty(subLanguage)) + { + subLanguage = "und"; + } + + var languages = GetLanguages(); + return languages.Length == 0 || ListHelper.ContainsIgnoreCase(languages, subLanguage); + } + } +}
\ No newline at end of file diff --git a/MediaBrowser.Model/Dlna/SubtitleStreamInfo.cs b/MediaBrowser.Model/Dlna/SubtitleStreamInfo.cs new file mode 100644 index 0000000000..7a89308dcc --- /dev/null +++ b/MediaBrowser.Model/Dlna/SubtitleStreamInfo.cs @@ -0,0 +1,15 @@ +namespace MediaBrowser.Model.Dlna +{ + public class SubtitleStreamInfo + { + public string Url { get; set; } + public string Language { get; set; } + public string Name { get; set; } + public bool IsForced { get; set; } + public string Format { get; set; } + public string DisplayTitle { get; set; } + public int Index { get; set; } + public SubtitleDeliveryMethod DeliveryMethod { get; set; } + public bool IsExternalUrl { get; set; } + } +}
\ No newline at end of file diff --git a/MediaBrowser.Model/Dlna/TranscodeSeekInfo.cs b/MediaBrowser.Model/Dlna/TranscodeSeekInfo.cs new file mode 100644 index 0000000000..564ce5c605 --- /dev/null +++ b/MediaBrowser.Model/Dlna/TranscodeSeekInfo.cs @@ -0,0 +1,8 @@ +namespace MediaBrowser.Model.Dlna +{ + public enum TranscodeSeekInfo + { + Auto = 0, + Bytes = 1 + } +}
\ No newline at end of file diff --git a/MediaBrowser.Model/Dlna/TranscodingProfile.cs b/MediaBrowser.Model/Dlna/TranscodingProfile.cs new file mode 100644 index 0000000000..8453fdf6d6 --- /dev/null +++ b/MediaBrowser.Model/Dlna/TranscodingProfile.cs @@ -0,0 +1,59 @@ +using System.Collections.Generic; +using System.Xml.Serialization; +using MediaBrowser.Model.Dlna; + +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("copyTimestamps")] + public bool CopyTimestamps { get; set; } + + [XmlAttribute("context")] + public EncodingContext Context { get; set; } + + [XmlAttribute("enableSubtitlesInManifest")] + public bool EnableSubtitlesInManifest { get; set; } + + [XmlAttribute("maxAudioChannels")] + public string MaxAudioChannels { get; set; } + + [XmlAttribute("minSegments")] + public int MinSegments { get; set; } + + [XmlAttribute("segmentLength")] + public int SegmentLength { get; set; } + + [XmlAttribute("breakOnNonKeyFrames")] + public bool BreakOnNonKeyFrames { get; set; } + + public string[] GetAudioCodecs() + { + return ContainerProfile.SplitValue(AudioCodec); + } + } +} diff --git a/MediaBrowser.Model/Dlna/UpnpDeviceInfo.cs b/MediaBrowser.Model/Dlna/UpnpDeviceInfo.cs new file mode 100644 index 0000000000..f4b9d1e9bc --- /dev/null +++ b/MediaBrowser.Model/Dlna/UpnpDeviceInfo.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using MediaBrowser.Model.Net; + +namespace MediaBrowser.Model.Dlna +{ + public class UpnpDeviceInfo + { + public Uri Location { get; set; } + public Dictionary<string, string> Headers { get; set; } + public IpAddressInfo LocalIpAddress { get; set; } + public int LocalPort { get; set; } + } +} diff --git a/MediaBrowser.Model/Dlna/VideoOptions.cs b/MediaBrowser.Model/Dlna/VideoOptions.cs new file mode 100644 index 0000000000..041d2cd5d1 --- /dev/null +++ b/MediaBrowser.Model/Dlna/VideoOptions.cs @@ -0,0 +1,11 @@ +namespace MediaBrowser.Model.Dlna +{ + /// <summary> + /// Class VideoOptions. + /// </summary> + public class VideoOptions : AudioOptions + { + public int? AudioStreamIndex { get; set; } + public int? SubtitleStreamIndex { get; set; } + } +}
\ No newline at end of file diff --git a/MediaBrowser.Model/Dlna/XmlAttribute.cs b/MediaBrowser.Model/Dlna/XmlAttribute.cs new file mode 100644 index 0000000000..e8e13ba0de --- /dev/null +++ b/MediaBrowser.Model/Dlna/XmlAttribute.cs @@ -0,0 +1,13 @@ +using System.Xml.Serialization; + +namespace MediaBrowser.Model.Dlna +{ + public class XmlAttribute + { + [XmlAttribute("name")] + public string Name { get; set; } + + [XmlAttribute("value")] + public string Value { get; set; } + } +}
\ No newline at end of file |
