From 1027c367d197bbd94997d10ab92c737ba6a94adb Mon Sep 17 00:00:00 2001 From: EvAlex Date: Thu, 18 Jan 2018 18:20:32 +0300 Subject: [PATCH] implement segment timeline (#3) --- DashTools/DashTools.csproj | 5 ++ DashTools/Mpd/AssetIdentifier.cs | 16 ++++ DashTools/Mpd/BaseUrl.cs | 21 +++++ DashTools/Mpd/DescriptorType.cs | 55 +++++++++++++ DashTools/MpdAdaptationSet.cs | 7 ++ DashTools/MpdDownloader.cs | 57 +++++++++---- DashTools/MpdElement.cs | 5 +- DashTools/MpdPeriod.cs | 115 ++++++++++++++++++++++++++- DashTools/MpdSegmentTemplate.cs | 29 ++++++- DashTools/SegmentTimeline.cs | 36 +++++++++ DashTools/SegmentTimelineItem.cs | 68 ++++++++++++++++ DashTools/TrackRepresentation.cs | 61 +++++++++++--- DashTools/XmlAttributeParseHelper.cs | 32 +++++++- 13 files changed, 475 insertions(+), 32 deletions(-) create mode 100644 DashTools/Mpd/AssetIdentifier.cs create mode 100644 DashTools/Mpd/BaseUrl.cs create mode 100644 DashTools/Mpd/DescriptorType.cs create mode 100644 DashTools/SegmentTimeline.cs create mode 100644 DashTools/SegmentTimelineItem.cs diff --git a/DashTools/DashTools.csproj b/DashTools/DashTools.csproj index e77fcdb..440af9a 100644 --- a/DashTools/DashTools.csproj +++ b/DashTools/DashTools.csproj @@ -40,6 +40,7 @@ + @@ -54,10 +55,14 @@ + + + + diff --git a/DashTools/Mpd/AssetIdentifier.cs b/DashTools/Mpd/AssetIdentifier.cs new file mode 100644 index 0000000..e9b0375 --- /dev/null +++ b/DashTools/Mpd/AssetIdentifier.cs @@ -0,0 +1,16 @@ +using System.Xml.Linq; + +namespace Qoollo.MpegDash.Mpd +{ + /// + /// The AssetIdentifier is used to identify the asset on Period level. If two different Periods contain + /// equivalent Asset Identifiers then the content in the two Periods belong to the same asset. + /// + public class AssetIdentifier : DescriptorType + { + public AssetIdentifier(XElement node) + : base(node) + { + } + } +} \ No newline at end of file diff --git a/DashTools/Mpd/BaseUrl.cs b/DashTools/Mpd/BaseUrl.cs new file mode 100644 index 0000000..858f2c4 --- /dev/null +++ b/DashTools/Mpd/BaseUrl.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Xml.Linq; + +namespace Qoollo.MpegDash.Mpd +{ + public class BaseUrl : MpdElement + { + public BaseUrl(XElement node) + : base(node) + { + } + + public string Value + { + get { return node.Value; } + } + } +} diff --git a/DashTools/Mpd/DescriptorType.cs b/DashTools/Mpd/DescriptorType.cs new file mode 100644 index 0000000..5f1b3d8 --- /dev/null +++ b/DashTools/Mpd/DescriptorType.cs @@ -0,0 +1,55 @@ +using System; +using System.Xml.Linq; + +namespace Qoollo.MpegDash.Mpd +{ + public class DescriptorType : MpdElement + { + public DescriptorType(XElement node) + : base(node) + { + } + + /// + /// Mandatory. + /// + /// Specifies a URI to identify the scheme. The semantics of this + /// element are specific to the scheme specified by this attribute. + /// The @schemeIdUri may be a URN or URL.When a URL is + /// used, it should also contain a month-date in the form + /// mmyyyy; the assignment of the URL must have been + /// authorized by the owner of the domain name in that URL on + /// or very close to that date, to avoid problems when domain + /// names change ownership. + /// + public string SchemeIdUri + { + get { return helper.ParseMandatoryString("schemeIdUri"); } + } + + /// + /// Optional + /// + /// Specifies the value for the descriptor element. The value + /// space and semantics must be defined by the owners of the + /// scheme identified in the @schemeIdUri attribute. + /// + public string Value + { + get { return helper.ParseOptionalString("value"); } + } + + /// + /// Optional + /// + /// specifies an identifier for the descriptor. Descriptors with + /// identical values for this attribute shall be synonymous, i.e. + /// the processing of one of the descriptors with an identical + /// value is sufficient. + /// + public string Id + { + get { return helper.ParseOptionalString("id"); } + } + } +} \ No newline at end of file diff --git a/DashTools/MpdAdaptationSet.cs b/DashTools/MpdAdaptationSet.cs index 0a530f0..cf7368d 100644 --- a/DashTools/MpdAdaptationSet.cs +++ b/DashTools/MpdAdaptationSet.cs @@ -107,6 +107,13 @@ public uint SubsegmentStartsWithSAP } } + /// + /// Specifies default Segment Template information. + /// + /// Information in this element is overridden by information in + /// AdapationSet.SegmentTemplate and + /// Representation.SegmentTemplate, if present. + /// public MpdSegmentTemplate SegmentTemplate { get { return segmentTemplate.Value; } diff --git a/DashTools/MpdDownloader.cs b/DashTools/MpdDownloader.cs index ed4b3cf..7bdc2d9 100644 --- a/DashTools/MpdDownloader.cs +++ b/DashTools/MpdDownloader.cs @@ -9,7 +9,7 @@ namespace Qoollo.MpegDash { - public class MpdDownloader + public class MpdDownloader : IDisposable { private readonly Uri mpdUrl; @@ -369,23 +369,19 @@ private Task DownloadFragment(string fragmentUrl) ? new Uri(fragmentUrl) : new Uri(mpdUrl, fragmentUrl); - string destPath = Path.Combine(destinationDir, GetLastPartOfPath(fragmentUrl)); - if (string.IsNullOrWhiteSpace(Path.GetExtension(destPath))) - destPath = Path.ChangeExtension(destPath, "mp4"); - if (File.Exists(destPath)) - destPath = Path.Combine(Path.GetDirectoryName(destPath), Path.ChangeExtension((Path.GetFileNameWithoutExtension(destPath) + "_1"), Path.GetExtension(destPath))); + string destPath = Path.Combine(destinationDir, GetFileNameForFragmentUrl(fragmentUrl)); + + int i = 0; + while (File.Exists(destPath)) + { + i++; + destPath = Path.Combine(Path.GetDirectoryName(destPath), Path.ChangeExtension((Path.GetFileNameWithoutExtension(destPath) + "_" + i), Path.GetExtension(destPath))); + } return Task.Factory.StartNew(() => { - try - { - client.DownloadFile(url, destPath); - return destPath; - } - catch - { - return null; - } + client.DownloadFile(url, destPath); + return destPath; }); } } @@ -420,7 +416,7 @@ private MpdWalker CreateMpdWalker() return new MpdWalker(mpd.Value); } - private string GetLastPartOfPath(string url) + private string GetFileNameForFragmentUrl(string url) { string fileName = url; if (IsAbsoluteUrl(url)) @@ -429,13 +425,40 @@ private string GetLastPartOfPath(string url) if (fileName.Contains("/")) fileName = fileName.Substring(fileName.LastIndexOf("/") + 1); } + + int queryStartIndex = fileName.IndexOf("?"); + if (queryStartIndex >= 0) + fileName = fileName.Substring(0, queryStartIndex); + + string extension = Path.GetExtension(fileName); + if (string.IsNullOrWhiteSpace(extension)) + fileName = Path.ChangeExtension(fileName, "mp4"); + + fileName = ReplaceIllegalCharsInFileName(fileName); + + return fileName; + } + + private string ReplaceIllegalCharsInFileName(string fileName) + { + var illegalChars = new[] { '/', '\\', ':', '*', '?', '"', '<', '>', '|' }; + foreach (var ch in illegalChars) + { + fileName = fileName.Replace(ch, '_'); + } return fileName; } - bool IsAbsoluteUrl(string url) + private bool IsAbsoluteUrl(string url) { Uri result; return Uri.TryCreate(url, UriKind.Absolute, out result); } + + public void Dispose() + { + if (mpd.IsValueCreated) + mpd.Value.Dispose(); + } } } diff --git a/DashTools/MpdElement.cs b/DashTools/MpdElement.cs index 4f931ff..ae9da30 100644 --- a/DashTools/MpdElement.cs +++ b/DashTools/MpdElement.cs @@ -1,4 +1,7 @@ -using System.Xml.Linq; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Xml.Linq; namespace Qoollo.MpegDash { diff --git a/DashTools/MpdPeriod.cs b/DashTools/MpdPeriod.cs index a846812..af6485f 100644 --- a/DashTools/MpdPeriod.cs +++ b/DashTools/MpdPeriod.cs @@ -1,4 +1,5 @@ -using System; +using Qoollo.MpegDash.Mpd; +using System; using System.Collections; using System.Collections.Generic; using System.Linq; @@ -11,7 +12,13 @@ public class MpdPeriod : MpdElement internal MpdPeriod(XElement node) : base(node) { - this.adaptationSets = new Lazy>(ParseAdaptationSets); + baseUrls = new Lazy>(ParseBaseUrls); + segmentBase = new Lazy(ParseSegmentBase); + segmentList = new Lazy(ParseSegmentList); + segmentTemplate = new Lazy(ParseSegmentTemplate); + assetIdentifier = new Lazy(ParseAssetIdentifier); + adaptationSets = new Lazy>(ParseAdaptationSets); + } public string Id @@ -34,6 +41,110 @@ public bool BitstreamSwitching get { return helper.ParseOptionalBool("bitstreamSwitching", false); } } + /// + /// 0...N + /// + /// Specifies a base URL that can be used for reference resolution + /// and alternative URL selection + /// + public IEnumerable BaseUrls + { + get { return baseUrls.Value; } + } + private readonly Lazy> baseUrls; + + private IEnumerable ParseBaseUrls() + { + return node.Elements() + .Where(n => n.Name.LocalName == "BaseUrl") + .Select(n => new BaseUrl(n)); + } + + /// + /// 0...1 + /// + /// Specifies default Segment Base information. + /// + /// Information in this element is overridden by information in + /// AdapationSet.SegmentBase and Representation.SegmentBase, if present. + /// + public SegmentBase SegmentBase + { + get { return segmentBase.Value; } + } + private readonly Lazy segmentBase; + + private SegmentBase ParseSegmentBase() + { + return node.Elements() + .Where(n => n.Name.LocalName == "SegmentBase") + .Select(n => new SegmentBase(n)) + .FirstOrDefault(); + } + + /// + /// 0...1 + /// + /// Specifies default Segment List information. + /// + /// Information in this element is overridden by information in + /// AdapationSet.SegmentList and Representation.SegmentList, if present. + /// + public MpdSegmentList SegmentList + { + get { return segmentList.Value; } + } + private readonly Lazy segmentList; + + private MpdSegmentList ParseSegmentList() + { + return node.Elements() + .Where(n => n.Name.LocalName == "SegmentList") + .Select(n => new MpdSegmentList(n)) + .FirstOrDefault(); + } + + /// + /// 0...1 + /// + /// Specifies default Segment Template information. + /// + /// Information in this element is overridden by information in + /// AdapationSet.SegmentTemplate and Representation.SegmentTemplate, if present. + /// + public MpdSegmentTemplate SegmentTemplate + { + get { return segmentTemplate.Value; } + } + private readonly Lazy segmentTemplate; + + private MpdSegmentTemplate ParseSegmentTemplate() + { + return node.Elements() + .Where(n => n.Name.LocalName == "SegmentTemplate") + .Select(n => new MpdSegmentTemplate(n)) + .FirstOrDefault(); + } + + /// + /// 0...1 + /// + /// Specifies that this Period belongs to a certain asset. + /// + public AssetIdentifier AssetIdentifier + { + get { return assetIdentifier.Value; } + } + private readonly Lazy assetIdentifier; + + private AssetIdentifier ParseAssetIdentifier() + { + return node.Elements() + .Where(n => n.Name.LocalName == "AssetIdentifier") + .Select(n => new AssetIdentifier(n)) + .FirstOrDefault(); + } + public IEnumerable AdaptationSets { get { return adaptationSets.Value; } diff --git a/DashTools/MpdSegmentTemplate.cs b/DashTools/MpdSegmentTemplate.cs index 84b517d..a5ac000 100644 --- a/DashTools/MpdSegmentTemplate.cs +++ b/DashTools/MpdSegmentTemplate.cs @@ -1,18 +1,23 @@ -using System.Xml.Linq; +using System; +using System.Linq; +using System.Xml.Linq; namespace Qoollo.MpegDash { /// - /// Specifies Segment template information. + /// Specifies Segment Template information. /// public class MpdSegmentTemplate : MultipleSegmentBase { internal MpdSegmentTemplate(XElement node) : base(node) { + segmentTimeline = ParseSegmentTimeline(); } /// + /// Optional + /// /// Specifies the template to create the Media Segment List. /// public string Media @@ -21,6 +26,8 @@ public string Media } /// + /// Optional + /// /// Specifies the template to create the Index Segment List. /// If neither the $Number$ nor the $Time$ identifier is included, /// this provides the URL to a Representation Index. @@ -31,6 +38,8 @@ public string Index } /// + /// Optional + /// /// Specifies the template to create the Initialization Segment. /// Neither $Number$ nor the $Time$ identifier shall be included. /// @@ -40,6 +49,8 @@ public string Initialization } /// + /// Optional + /// /// Specifies the template to create the Bitstream Switching Segment. /// Neither $Number$ nor the $Time$ identifier shall be included. /// @@ -47,5 +58,19 @@ public bool BitstreamSwitching { get { return helper.ParseOptionalBool("bitstreamSwitching", false); } } + + public SegmentTimeline SegmentTimeline + { + get { return segmentTimeline; } + } + private readonly SegmentTimeline segmentTimeline; + + private SegmentTimeline ParseSegmentTimeline() + { + return node.Elements() + .Where(n => n.Name.LocalName == "SegmentTimeline") + .Select(n => new SegmentTimeline(n)) + .FirstOrDefault(); + } } } \ No newline at end of file diff --git a/DashTools/SegmentTimeline.cs b/DashTools/SegmentTimeline.cs new file mode 100644 index 0000000..3034dba --- /dev/null +++ b/DashTools/SegmentTimeline.cs @@ -0,0 +1,36 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Xml.Linq; + +namespace Qoollo.MpegDash +{ + public class SegmentTimeline : MpdElement, IEnumerable + { + private readonly IEnumerable items; + + public SegmentTimeline(XElement node) + : base(node) + { + items = ParseItems(); + } + + private IEnumerable ParseItems() + { + return node.Elements() + .Where(n => n.Name.LocalName == "S") + .Select(n => new SegmentTimelineItem(n)); + } + + public IEnumerator GetEnumerator() + { + return items.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return items.GetEnumerator(); + } + } +} \ No newline at end of file diff --git a/DashTools/SegmentTimelineItem.cs b/DashTools/SegmentTimelineItem.cs new file mode 100644 index 0000000..e4ed131 --- /dev/null +++ b/DashTools/SegmentTimelineItem.cs @@ -0,0 +1,68 @@ +using System.Xml.Linq; + +namespace Qoollo.MpegDash +{ + public class SegmentTimelineItem : MpdElement + { + public SegmentTimelineItem(XElement node) + : base(node) + { + } + + /// + /// Optional + /// + /// Specifies the MPD start time, in @timescale units, the + /// first Segment in the series starts relative to the beginning + /// of the Period. + /// + /// The value of this attribute must be equal to or greater + /// than the sum of the previous S element earliest + /// presentation time and the sum of the contiguous + /// Segment durations. + /// + /// If the value of the attribute is greater than what is + /// expressed by the previous S element, it expresses + /// discontinuities in the timeline. + /// + /// If not present then the value shall be assumed to + /// be zero for the first S element and for the subsequent S + /// elements, the value shall be assumed to be the sum of + /// the previous S element's earliest presentation time and + /// contiguous duration (i.e.previous S@t + @d* (@r + 1)). + /// + public ulong? Time + { + get { return helper.ParseOptionalUlong("t"); } + } + + /// + /// Mandatory + /// + /// Specifies the Segment duration, in units of the value of + /// the @timescale. + /// + public ulong Duration + { + get { return helper.ParseMandatoryUlong("d"); } + } + + /// + /// Optional. Default: 0 + /// + /// Specifies the repeat count of the number of following + /// contiguous Segments with the same duration expressed + /// by the value of @d.This value is zero-based (e.g.a value + /// of three means four Segments in the contiguous series). + /// A negative value of the @r attribute of the S + /// element indicates that the duration indicated in @d + /// attribute repeats until the start of the next S + /// element, the end of the Period or until the next + /// MPD update. + /// + public int RepeatCount + { + get { return helper.ParseOptionalInt("r", 0).Value; } + } + } +} \ No newline at end of file diff --git a/DashTools/TrackRepresentation.cs b/DashTools/TrackRepresentation.cs index d077812..e4e11f6 100644 --- a/DashTools/TrackRepresentation.cs +++ b/DashTools/TrackRepresentation.cs @@ -81,18 +81,25 @@ private IEnumerable GetSegments() var segmentTemplate = adaptationSet.SegmentTemplate ?? representation.SegmentTemplate; if (segmentTemplate != null) { - int i = 1; - while (true) + var segments = GetSegmentsFromTimeline(segmentTemplate); + + bool hasTimelineItems = false; + foreach (var segment in segments) { - yield return new TrackRepresentationSegment + hasTimelineItems = true; + + yield return segment; + } + + if (!hasTimelineItems) + { + segments = GetSegmentsFromRepresentation(representation); + foreach (var segment in segments) { - Path = segmentTemplate.Media - .Replace("$RepresentationID$", representation.Id) - .Replace("$Number$", i.ToString()), - Duration = TimeSpan.FromMilliseconds(segmentTemplate.Duration.Value) - }; - i++; + yield return segment; + } } + } else if (representation.SegmentList != null) { @@ -109,6 +116,42 @@ private IEnumerable GetSegments() throw new Exception("Failed to determine Segments"); } + private IEnumerable GetSegmentsFromRepresentation(MpdRepresentation representation) + { + int i = 1; + while (true) + { + yield return new TrackRepresentationSegment + { + Path = representation.SegmentTemplate.Media + .Replace("$RepresentationID$", representation.Id) + .Replace("$Number$", i.ToString()), + Duration = TimeSpan.FromMilliseconds(representation.SegmentTemplate.Duration.Value) + }; + i++; + } + } + + private IEnumerable GetSegmentsFromTimeline(MpdSegmentTemplate segmentTemplate) + { + int i = 1; + foreach (var item in segmentTemplate.SegmentTimeline) + { + int count = Math.Max(1, item.RepeatCount); + for (int j = 0; j < count; j++) + { + yield return new TrackRepresentationSegment + { + Path = segmentTemplate.Media + .Replace("$RepresentationID$", representation.Id) + .Replace("$Number$", i.ToString()), + Duration = TimeSpan.FromMilliseconds(item.Duration) + }; + i++; + } + } + } + internal IEnumerable GetFragmentsPaths(TimeSpan from, TimeSpan to) { var span = TimeSpan.Zero; diff --git a/DashTools/XmlAttributeParseHelper.cs b/DashTools/XmlAttributeParseHelper.cs index cb0d9f6..19d7a6a 100644 --- a/DashTools/XmlAttributeParseHelper.cs +++ b/DashTools/XmlAttributeParseHelper.cs @@ -16,6 +16,20 @@ public XmlAttributeParseHelper(XElement node) this.node = node; } + public string ParseMandatoryString(string attributeName) + { + var attr = node.Attribute(attributeName); + if (attr == null) + throw new Exception("Attribute \"" + attributeName + "\" not found on element " + node.ToString()); + return attr.Value; + } + + public string ParseOptionalString(string attributeName) + { + var attr = node.Attribute(attributeName); + return attr == null ? null : attr.Value; + } + public DateTimeOffset? ParseDateTimeOffset(string attributeName, bool mandatoryCondition) { if (!mandatoryCondition && node.Attribute(attributeName) == null) @@ -25,7 +39,7 @@ public XmlAttributeParseHelper(XElement node) public uint ParseUint(string attributeName) { - return uint.Parse(node.Attribute("bandwidth").Value); + return uint.Parse(node.Attribute(attributeName).Value); } public DateTimeOffset? ParseOptionalDateTimeOffset(string attributeName, DateTimeOffset? defaultValue = null) @@ -60,6 +74,22 @@ public bool ParseOptionalBool(string attributeName, bool defaultValue) : uint.Parse(attr.Value); } + public int? ParseOptionalInt(string attributeName, int? defaultValue = null) + { + var attr = node.Attribute(attributeName); + return attr == null + ? defaultValue + : int.Parse(attr.Value); + } + + public ulong ParseMandatoryUlong(string attributeName) + { + var attr = node.Attribute(attributeName); + if (attr == null) + throw new Exception("Attribute \"" + attributeName + "\" not found on element " + node.ToString()); + return ulong.Parse(attr.Value); + } + public ulong? ParseOptionalUlong(string attributeName, ulong? defaultValue = null) { var attr = node.Attribute(attributeName);