aboutsummaryrefslogtreecommitdiff
path: root/MediaBrowser.Model/Dlna
diff options
context:
space:
mode:
authorAndrew Rabert <ar@nullsum.net>2018-12-27 18:27:57 -0500
committerAndrew Rabert <ar@nullsum.net>2018-12-27 18:27:57 -0500
commita86b71899ec52c44ddc6c3018e8cc5e9d7ff4d62 (patch)
treea74f6ea4a8abfa1664a605d31d48bc38245ccf58 /MediaBrowser.Model/Dlna
parent9bac3ac616b01f67db98381feb09d34ebe821f9a (diff)
Add GPL modules
Diffstat (limited to 'MediaBrowser.Model/Dlna')
-rw-r--r--MediaBrowser.Model/Dlna/AudioOptions.cs87
-rw-r--r--MediaBrowser.Model/Dlna/CodecProfile.cs68
-rw-r--r--MediaBrowser.Model/Dlna/CodecType.cs9
-rw-r--r--MediaBrowser.Model/Dlna/ConditionProcessor.cs284
-rw-r--r--MediaBrowser.Model/Dlna/ContainerProfile.cs101
-rw-r--r--MediaBrowser.Model/Dlna/ContentFeatureBuilder.cs236
-rw-r--r--MediaBrowser.Model/Dlna/DeviceIdentification.cs61
-rw-r--r--MediaBrowser.Model/Dlna/DeviceProfile.cs327
-rw-r--r--MediaBrowser.Model/Dlna/DeviceProfileInfo.cs24
-rw-r--r--MediaBrowser.Model/Dlna/DeviceProfileType.cs8
-rw-r--r--MediaBrowser.Model/Dlna/DirectPlayProfile.cs36
-rw-r--r--MediaBrowser.Model/Dlna/DlnaFlags.cs48
-rw-r--r--MediaBrowser.Model/Dlna/DlnaMaps.cs56
-rw-r--r--MediaBrowser.Model/Dlna/DlnaProfileType.cs9
-rw-r--r--MediaBrowser.Model/Dlna/EncodingContext.cs8
-rw-r--r--MediaBrowser.Model/Dlna/HeaderMatchType.cs9
-rw-r--r--MediaBrowser.Model/Dlna/HttpHeaderInfo.cs17
-rw-r--r--MediaBrowser.Model/Dlna/IDeviceDiscovery.cs11
-rw-r--r--MediaBrowser.Model/Dlna/ITranscoderSupport.cs25
-rw-r--r--MediaBrowser.Model/Dlna/MediaFormatProfile.cs113
-rw-r--r--MediaBrowser.Model/Dlna/MediaFormatProfileResolver.cs439
-rw-r--r--MediaBrowser.Model/Dlna/PlaybackErrorCode.cs10
-rw-r--r--MediaBrowser.Model/Dlna/ProfileCondition.cs38
-rw-r--r--MediaBrowser.Model/Dlna/ProfileConditionType.cs11
-rw-r--r--MediaBrowser.Model/Dlna/ProfileConditionValue.cs29
-rw-r--r--MediaBrowser.Model/Dlna/ResolutionConfiguration.cs14
-rw-r--r--MediaBrowser.Model/Dlna/ResolutionNormalizer.cs99
-rw-r--r--MediaBrowser.Model/Dlna/ResolutionOptions.cs8
-rw-r--r--MediaBrowser.Model/Dlna/ResponseProfile.cs49
-rw-r--r--MediaBrowser.Model/Dlna/SearchCriteria.cs75
-rw-r--r--MediaBrowser.Model/Dlna/SearchType.cs12
-rw-r--r--MediaBrowser.Model/Dlna/SortCriteria.cs17
-rw-r--r--MediaBrowser.Model/Dlna/StreamBuilder.cs1900
-rw-r--r--MediaBrowser.Model/Dlna/StreamInfo.cs1092
-rw-r--r--MediaBrowser.Model/Dlna/SubtitleDeliveryMethod.cs22
-rw-r--r--MediaBrowser.Model/Dlna/SubtitleProfile.cs46
-rw-r--r--MediaBrowser.Model/Dlna/SubtitleStreamInfo.cs15
-rw-r--r--MediaBrowser.Model/Dlna/TranscodeSeekInfo.cs8
-rw-r--r--MediaBrowser.Model/Dlna/TranscodingProfile.cs59
-rw-r--r--MediaBrowser.Model/Dlna/UpnpDeviceInfo.cs14
-rw-r--r--MediaBrowser.Model/Dlna/VideoOptions.cs11
-rw-r--r--MediaBrowser.Model/Dlna/XmlAttribute.cs13
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&lt;System.Int32&gt;.</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