From 37e3e6217a8365da76c4e69534ba67e8c0786959 Mon Sep 17 00:00:00 2001 From: nilaoda Date: Sun, 10 Jul 2022 02:22:50 +0800 Subject: [PATCH] =?UTF-8?q?=E9=87=8D=E5=86=99MPD=E8=A7=A3=E6=9E=90(?= =?UTF-8?q?=E7=82=B9=E6=92=AD)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/N_m3u8DL-RE.Common/Entity/MediaSegment.cs | 8 +- src/N_m3u8DL-RE.Common/Entity/Playlist.cs | 6 + src/N_m3u8DL-RE.Common/Entity/StreamSpec.cs | 6 +- src/N_m3u8DL-RE.Parser/Constants/DASHTags.cs | 16 + .../Extractor/DASHExtractor.cs | 33 +- .../Extractor/DASHExtractor2.cs | 393 ++++++++++++++++++ .../Extractor/HLSExtractor.cs | 13 +- .../Extractor/IExtractor.cs | 4 + src/N_m3u8DL-RE.Parser/StreamExtractor.cs | 3 +- src/N_m3u8DL-RE.Parser/Util/ParserUtil.cs | 40 +- src/N_m3u8DL-RE/N_m3u8DL-RE.csproj | 4 + 11 files changed, 507 insertions(+), 19 deletions(-) create mode 100644 src/N_m3u8DL-RE.Parser/Constants/DASHTags.cs create mode 100644 src/N_m3u8DL-RE.Parser/Extractor/DASHExtractor2.cs diff --git a/src/N_m3u8DL-RE.Common/Entity/MediaSegment.cs b/src/N_m3u8DL-RE.Common/Entity/MediaSegment.cs index aa5fcd3..9727a9c 100644 --- a/src/N_m3u8DL-RE.Common/Entity/MediaSegment.cs +++ b/src/N_m3u8DL-RE.Common/Entity/MediaSegment.cs @@ -9,14 +9,12 @@ namespace N_m3u8DL_RE.Common.Entity public class MediaSegment { public int Index { get; set; } - - public int TargetDuration { get; set; } public double Duration { get; set; } public string? Title { get; set; } - public long StartRange { get; set; } = 0L; - public long StopRange { get => StartRange + ExpectLength - 1; } - public long ExpectLength { get; set; } = -1L; + public long? StartRange { get; set; } + public long? StopRange { get => (StartRange != null && ExpectLength != null) ? StartRange + ExpectLength - 1 : null; } + public long? ExpectLength { get; set; } public EncryptInfo EncryptInfo { get; set; } = new EncryptInfo(); diff --git a/src/N_m3u8DL-RE.Common/Entity/Playlist.cs b/src/N_m3u8DL-RE.Common/Entity/Playlist.cs index 0265166..65a8bb6 100644 --- a/src/N_m3u8DL-RE.Common/Entity/Playlist.cs +++ b/src/N_m3u8DL-RE.Common/Entity/Playlist.cs @@ -13,6 +13,12 @@ namespace N_m3u8DL_RE.Common.Entity public string Url { get; set; } //是否直播 public bool IsLive { get; set; } = false; + //直播刷新间隔毫秒(默认15秒) + public double RefreshIntervalMs { get; set; } = 15000; + //所有分片时长总和 + public double TotalDuration { get => MediaParts.Sum(x => x.MediaSegments.Sum(m => m.Duration)); } + //所有分片中最长时长 + public double? TargetDuration { get; set; } //INIT信息 public MediaSegment? MediaInit { get; set; } //分片信息 diff --git a/src/N_m3u8DL-RE.Common/Entity/StreamSpec.cs b/src/N_m3u8DL-RE.Common/Entity/StreamSpec.cs index cc70a74..e1a116b 100644 --- a/src/N_m3u8DL-RE.Common/Entity/StreamSpec.cs +++ b/src/N_m3u8DL-RE.Common/Entity/StreamSpec.cs @@ -32,7 +32,7 @@ namespace N_m3u8DL_RE.Common.Entity public string Url { get; set; } - public Playlist Playlist { get; set; } + public Playlist? Playlist { get; set; } public override string ToString() { @@ -75,8 +75,8 @@ namespace N_m3u8DL_RE.Common.Entity //计算时长 if (Playlist != null) { - var total = Playlist.MediaParts.Sum(x => x.MediaSegments.Sum(m => m.Duration)); - returnStr += " | " + GlobalUtil.FormatTime((int)total); + var total = Playlist.TotalDuration; + returnStr += " | ~" + GlobalUtil.FormatTime((int)total); } return returnStr; diff --git a/src/N_m3u8DL-RE.Parser/Constants/DASHTags.cs b/src/N_m3u8DL-RE.Parser/Constants/DASHTags.cs new file mode 100644 index 0000000..4a1ebbe --- /dev/null +++ b/src/N_m3u8DL-RE.Parser/Constants/DASHTags.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace N_m3u8DL_RE.Parser.Constants +{ + internal class DASHTags + { + public static string TemplateRepresentationID = "$RepresentationID$"; + public static string TemplateBandwidth = "$Bandwidth$"; + public static string TemplateNumber = "$Number$"; + public static string TemplateTime = "$Time$"; + } +} diff --git a/src/N_m3u8DL-RE.Parser/Extractor/DASHExtractor.cs b/src/N_m3u8DL-RE.Parser/Extractor/DASHExtractor.cs index 2b08d52..23b9b2b 100644 --- a/src/N_m3u8DL-RE.Parser/Extractor/DASHExtractor.cs +++ b/src/N_m3u8DL-RE.Parser/Extractor/DASHExtractor.cs @@ -57,15 +57,28 @@ namespace N_m3u8DL_RE.Parser.Extractor } } + TimeSpan updateTs = TimeSpan.FromSeconds(0); //更新时长 + TimeSpan totalTs = TimeSpan.FromSeconds(0); //总时长 + var type = ((XmlElement)xn).GetAttribute("type"); //static dynamic - var mediaPresentationDuration = ((XmlElement)xn).GetAttribute("mediaPresentationDuration"); + + if (type == "static") + { + var mediaPresentationDuration = ((XmlElement)xn).GetAttribute("mediaPresentationDuration"); + totalTs = XmlConvert.ToTimeSpan(mediaPresentationDuration); + } + else if (type == "dynamic") + { + var minimumUpdatePeriod = ((XmlElement)xn).GetAttribute("minimumUpdatePeriod"); + updateTs = XmlConvert.ToTimeSpan(minimumUpdatePeriod); + } + var ns = ((XmlElement)xn).GetAttribute("xmlns"); XmlNamespaceManager nsMgr = new XmlNamespaceManager(mpdDoc.NameTable); nsMgr.AddNamespace("ns", ns); - TimeSpan ts = XmlConvert.ToTimeSpan(mediaPresentationDuration); //时长 //读取在MPD开头定义的,并替换本身的URL var baseNode = xn.SelectSingleNode("ns:BaseURL", nsMgr); @@ -90,7 +103,7 @@ namespace N_m3u8DL_RE.Parser.Extractor foreach (XmlElement period in xn.SelectNodes("ns:Period", nsMgr)) { periodIndex++; - var periodDuration = string.IsNullOrEmpty(period.GetAttribute("duration")) ? XmlConvert.ToTimeSpan(mediaPresentationDuration) : XmlConvert.ToTimeSpan(period.GetAttribute("duration")); + var periodDuration = string.IsNullOrEmpty(period.GetAttribute("duration")) ? totalTs : XmlConvert.ToTimeSpan(period.GetAttribute("duration")); var periodMsInfo = ExtractMultisegmentInfo(period, nsMgr, new JsonObject() { ["StartNumber"] = 1, @@ -410,7 +423,7 @@ namespace N_m3u8DL_RE.Parser.Extractor new JsonObject() { ["url"] = baseUrl, - ["duration"] = ts.TotalSeconds + ["duration"] = totalTs.TotalSeconds } }; } @@ -463,6 +476,14 @@ namespace N_m3u8DL_RE.Parser.Extractor //组装分片 Playlist playlist = new(); + playlist.IsLive = type == "static" ? false : true; + + //直播刷新间隔 + if (playlist.IsLive) + { + playlist.RefreshIntervalMs = updateTs.TotalMilliseconds; + } + List segments = new(); //Initial URL if (item.ContainsKey("InitializationUrl")) @@ -725,7 +746,7 @@ namespace N_m3u8DL_RE.Parser.Extractor /// /// 预处理URL /// - private string PreProcessUrl(string url) + public string PreProcessUrl(string url) { foreach (var p in ParserConfig.UrlProcessors) { @@ -738,7 +759,7 @@ namespace N_m3u8DL_RE.Parser.Extractor return url; } - private void PreProcessContent() + public void PreProcessContent() { foreach (var p in ParserConfig.ContentProcessors) { diff --git a/src/N_m3u8DL-RE.Parser/Extractor/DASHExtractor2.cs b/src/N_m3u8DL-RE.Parser/Extractor/DASHExtractor2.cs new file mode 100644 index 0000000..9905b0e --- /dev/null +++ b/src/N_m3u8DL-RE.Parser/Extractor/DASHExtractor2.cs @@ -0,0 +1,393 @@ +using N_m3u8DL_RE.Common.Entity; +using N_m3u8DL_RE.Common.Enum; +using N_m3u8DL_RE.Parser.Config; +using N_m3u8DL_RE.Parser.Constants; +using N_m3u8DL_RE.Parser.Util; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Xml; +using System.Xml.Linq; + +namespace N_m3u8DL_RE.Parser.Extractor +{ + //https://blog.csdn.net/leek5533/article/details/117750191 + internal class DASHExtractor2 : IExtractor + { + public ExtractorType ExtractorType => ExtractorType.MPEG_DASH; + + private string MpdUrl = string.Empty; + private string BaseUrl = string.Empty; + private string MpdContent = string.Empty; + public ParserConfig ParserConfig { get; set; } + + public DASHExtractor2(ParserConfig parserConfig) + { + this.ParserConfig = parserConfig; + this.MpdUrl = parserConfig.Url ?? string.Empty; + if (!string.IsNullOrEmpty(parserConfig.BaseUrl)) + this.BaseUrl = parserConfig.BaseUrl; + } + + private string ExtendBaseUrl(XElement element, string oriBaseUrl) + { + var target = element.Elements().Where(e => e.Name.LocalName == "BaseURL"); + if (target.Any()) + { + oriBaseUrl = ParserUtil.CombineURL(oriBaseUrl, target.First().Value); + } + + return oriBaseUrl; + } + + private double? GetFrameRate(XElement element) + { + var frameRate = element.Attribute("frameRate")?.Value; + if (frameRate != null && frameRate.Contains("/")) + { + var d = Convert.ToDouble(frameRate.Split('/')[0]) / Convert.ToDouble(frameRate.Split('/')[1]); + frameRate = d.ToString("0.000"); + return Convert.ToDouble(frameRate); + } + return null; + } + + public async Task> ExtractStreamsAsync(string rawText) + { + var streamList = new List(); + + this.MpdContent = rawText; + this.PreProcessContent(); + + + var xmlDocument = XDocument.Parse(MpdContent); + + //选中第一个MPD节点 + var mpdElement = xmlDocument.Elements().First(e => e.Name.LocalName == "MPD"); + + //类型 static点播, dynamic直播 + var type = mpdElement.Attribute("type")?.Value; + bool isLive = type == "dynamic"; + + //分片最大时长 + var maxSegmentDuration = mpdElement.Attribute("maxSegmentDuration")?.Value; + //MPD更新间隔 + var minimumUpdatePeriod = mpdElement.Attribute("minimumUpdatePeriod")?.Value; + //分片从该时间起可用 + var availabilityStartTime = mpdElement.Attribute("availabilityStartTime")?.Value; + //MPD发布时间 + var publishTime = mpdElement.Attribute("publishTime")?.Value; + //MPD总时长 + var mediaPresentationDuration = mpdElement.Attribute("mediaPresentationDuration")?.Value; + + //读取在MPD开头定义的,并替换本身的URL + var baseUrlElements = mpdElement.Elements().Where(e => e.Name.LocalName == "BaseURL"); + if (baseUrlElements.Any()) + { + var baseUrl = baseUrlElements.First().Value; + if (baseUrl.Contains("kkbox.com.tw/")) baseUrl = baseUrl.Replace("//https:%2F%2F", "//"); + this.BaseUrl = ParserUtil.CombineURL(this.MpdUrl, baseUrl); + } + else + { + this.BaseUrl = this.MpdUrl; + } + + //全部Period + var periods = mpdElement.Elements().Where(e => e.Name.LocalName == "Period"); + foreach (var period in periods) + { + //本Period时长 + var periodDuration = period.Attribute("duration")?.Value; + + //最终分片会使用的baseurl + var segBaseUrl = this.BaseUrl; + + //处理baseurl嵌套 + segBaseUrl = ExtendBaseUrl(period, segBaseUrl); + + //本Period中的全部AdaptationSet + var adaptationSets = period.Elements().Where(e => e.Name.LocalName == "AdaptationSet"); + foreach (var adaptationSet in adaptationSets) + { + //处理baseurl嵌套 + segBaseUrl = ExtendBaseUrl(adaptationSet, segBaseUrl); + + var mimeType = adaptationSet.Attribute("mimeType")?.Value; + var frameRate = GetFrameRate(adaptationSet); + //本AdaptationSet中的全部Representation + var representations = adaptationSet.Elements().Where(e => e.Name.LocalName == "Representation"); + foreach (var representation in representations) + { + //处理baseurl嵌套 + segBaseUrl = ExtendBaseUrl(representation, segBaseUrl); + + if (mimeType == null) + { + mimeType = representation.Attribute("mimeType")?.Value ?? ""; + } + var bandwidth = representation.Attribute("bandwidth"); + StreamSpec streamSpec = new(); + streamSpec.Playlist = new Playlist(); + streamSpec.Playlist.MediaParts.Add(new MediaPart()); + streamSpec.GroupId = representation.Attribute("id")?.Value; + streamSpec.Bandwidth = Convert.ToInt32(bandwidth?.Value ?? "0"); + streamSpec.Codecs = representation.Attribute("codecs")?.Value; + streamSpec.Language = representation.Attribute("lang")?.Value; + streamSpec.FrameRate = frameRate ?? GetFrameRate(representation); + streamSpec.Resolution = representation.Attribute("width")?.Value != null ? $"{representation.Attribute("width")?.Value}x{representation.Attribute("height")?.Value}" : null; + streamSpec.Url = MpdUrl; + streamSpec.MediaType = mimeType.Split("/")[0] switch + { + "text" => MediaType.SUBTITLES, + "audio" => MediaType.AUDIO, + _ => null + }; + streamSpec.Playlist.IsLive = isLive; + //设置刷新间隔 + if (minimumUpdatePeriod != null) + { + streamSpec.Playlist.RefreshIntervalMs = XmlConvert.ToTimeSpan(minimumUpdatePeriod).TotalMilliseconds; + } + + //读取声道数量 + var audioChannelConfiguration = adaptationSet.Elements().Where(e => e.Name.LocalName == "AudioChannelConfiguration"); + if (audioChannelConfiguration.Any()) + { + streamSpec.Channels = audioChannelConfiguration.First().Attribute("value")?.Value; + } + + + //第一种形式 SegmentBase + var segmentBaseElements = representation.Elements().Where(e => e.Name.LocalName == "SegmentBase"); + if (segmentBaseElements.Any()) + { + streamSpec.Playlist.MediaParts[0].MediaSegments.Add + ( + new MediaSegment() + { + Index = 0, + Url = PreProcessUrl(segBaseUrl), + Duration = XmlConvert.ToTimeSpan(periodDuration ?? mediaPresentationDuration ?? "PT0S").TotalSeconds + } + ); + } + + //第二种形式 SegmentList.SegmentList + var segmentListElements = representation.Elements().Where(e => e.Name.LocalName == "SegmentList"); + if (segmentListElements.Any()) + { + var segmentList = segmentListElements.First(); + //处理init url + var initialization = segmentList.Elements().First(e => e.Name.LocalName == "Initialization"); + var initUrl = ParserUtil.CombineURL(segBaseUrl, initialization.Attribute("sourceURL")?.Value); + var initRange = initialization.Attribute("range")?.Value; + streamSpec.Playlist.MediaInit = new MediaSegment(); + streamSpec.Playlist.MediaInit.Url = PreProcessUrl(initUrl); + if (initRange != null) + { + var (start, expect) = ParserUtil.ParseRange(initRange); + streamSpec.Playlist.MediaInit.StartRange = start; + streamSpec.Playlist.MediaInit.ExpectLength = expect; + } + //处理分片 + var segmentURLs = segmentList.Elements().Where(e => e.Name.LocalName == "SegmentURL"); + for (int segmentIndex = 0; segmentIndex < segmentURLs.Count(); segmentIndex++) + { + var segmentURL = segmentURLs.ElementAt(segmentIndex); + var mediaUrl = ParserUtil.CombineURL(segBaseUrl, segmentURL.Attribute("media")?.Value); + var mediaRange = segmentURL.Attribute("range")?.Value; + MediaSegment mediaSegment = new(); + mediaSegment.Url = PreProcessUrl(mediaUrl); + if (mediaRange != null) + { + var (start, expect) = ParserUtil.ParseRange(initRange); + mediaSegment.StartRange = start; + mediaSegment.ExpectLength = expect; + } + streamSpec.Playlist.MediaParts[0].MediaSegments.Add(mediaSegment); + } + } + + //第三种形式 SegmentTemplate+SegmentTimeline + //通配符有$RepresentationID$ $Bandwidth$ $Number$ $Time$ + + //adaptationSets中的segmentTemplate + var segmentTemplateElementsOuter = adaptationSet.Elements().Where(e => e.Name.LocalName == "SegmentTemplate"); + //representation中的segmentTemplate + var segmentTemplateElements = representation.Elements().Where(e => e.Name.LocalName == "SegmentTemplate"); + if (segmentTemplateElements.Any() || segmentTemplateElementsOuter.Any()) + { + var segmentTemplate = segmentTemplateElements.FirstOrDefault() ?? segmentTemplateElementsOuter.FirstOrDefault(); + var segmentTemplateOuter = segmentTemplateElementsOuter.FirstOrDefault() ?? segmentTemplateElements.FirstOrDefault(); + var varDic = new Dictionary(); + varDic[DASHTags.TemplateRepresentationID] = streamSpec.GroupId; + varDic[DASHTags.TemplateBandwidth] = bandwidth?.Value; + //timesacle + var timescaleStr = segmentTemplate.Attribute("timescale")?.Value ?? segmentTemplateOuter.Attribute("timescale")?.Value ?? "1000"; + var durationStr = segmentTemplate.Attribute("duration")?.Value ?? segmentTemplateOuter.Attribute("duration")?.Value; + var startNumberStr = segmentTemplate.Attribute("startNumber")?.Value ?? segmentTemplateOuter.Attribute("startNumber")?.Value ?? "0"; + //处理init url + var initialization = segmentTemplate.Attribute("initialization")?.Value ?? segmentTemplateOuter.Attribute("initialization")?.Value; + var initUrl = ParserUtil.ReplaceVars(ParserUtil.CombineURL(segBaseUrl, initialization), varDic); + streamSpec.Playlist.MediaInit = new MediaSegment(); + streamSpec.Playlist.MediaInit.Url = PreProcessUrl(initUrl); + //处理分片 + var media = segmentTemplate.Attribute("media")?.Value ?? segmentTemplateOuter.Attribute("media")?.Value; + var segmentTimelineElements = segmentTemplate.Elements().Where(e => e.Name.LocalName == "SegmentTimeline"); + if (segmentTimelineElements.Any()) + { + //使用了SegmentTimeline 结果精确 + var segmentTimeline = segmentTimelineElements.First(); + var segNumber = Convert.ToInt32(startNumberStr); + var Ss = segmentTimeline.Elements().Where(e => e.Name.LocalName == "S"); + var currentTime = 0L; + var segIndex = 0; + foreach (var S in Ss) + { + //每个S元素包含三个属性:@t(start time)\@r(repeat count)\@d(duration) + var _startTimeStr = S.Attribute("t")?.Value; + var _durationStr = S.Attribute("d")?.Value; + var _repeatCountStr = S.Attribute("r")?.Value; + + if (_startTimeStr != null) currentTime = Convert.ToInt64(_startTimeStr); + var _duration = Convert.ToInt64(_durationStr); + var timescale = Convert.ToInt32(timescaleStr); + var _repeatCount = Convert.ToInt64(_repeatCountStr); + varDic[DASHTags.TemplateTime] = currentTime; + varDic[DASHTags.TemplateNumber] = segNumber++; + var mediaUrl = ParserUtil.ReplaceVars(ParserUtil.CombineURL(segBaseUrl, media), varDic); + MediaSegment mediaSegment = new(); + mediaSegment.Url = PreProcessUrl(mediaUrl); + mediaSegment.Duration = _duration / (double)timescale; + mediaSegment.Index = segIndex++; + streamSpec.Playlist.MediaParts[0].MediaSegments.Add(mediaSegment); + if (_repeatCount < 0) + { + //负数表示一直重复 直到period结束 注意减掉已经加入的1个片段 + _repeatCount = (long)Math.Ceiling(XmlConvert.ToTimeSpan(periodDuration ?? mediaPresentationDuration ?? "PT0S").TotalSeconds * timescale / _duration) - 1; + } + for (long i = 0; i < _repeatCount; i++) + { + currentTime += _duration; + MediaSegment _mediaSegment = new(); + varDic[DASHTags.TemplateTime] = currentTime; + varDic[DASHTags.TemplateNumber] = segNumber++; + var _mediaUrl = ParserUtil.ReplaceVars(ParserUtil.CombineURL(segBaseUrl, media), varDic); + _mediaSegment.Url = PreProcessUrl(_mediaUrl); + _mediaSegment.Index = segIndex++; + _mediaSegment.Duration = _duration / (double)timescale; + streamSpec.Playlist.MediaParts[0].MediaSegments.Add(_mediaSegment); + } + currentTime += _duration; + } + } + else + { + //没用SegmentTimeline 需要计算总分片数量 不精确 + var timescale = Convert.ToInt32(timescaleStr); + var startNumber = Convert.ToInt32(startNumberStr); + var duration = Convert.ToInt32(durationStr); + var totalNumber = (long)Math.Ceiling(XmlConvert.ToTimeSpan(periodDuration ?? mediaPresentationDuration ?? "PT0S").TotalSeconds * timescale / Convert.ToInt32(durationStr)); + for (int index = startNumber, segIndex = 0; index < startNumber + totalNumber; index++, segIndex++) + { + varDic[DASHTags.TemplateNumber] = index; + var mediaUrl = ParserUtil.ReplaceVars(ParserUtil.CombineURL(segBaseUrl, media), varDic); + MediaSegment mediaSegment = new(); + mediaSegment.Url = PreProcessUrl(mediaUrl); + mediaSegment.Index = segIndex; + mediaSegment.Duration = duration / (double)timescale; + streamSpec.Playlist.MediaParts[0].MediaSegments.Add(mediaSegment); + } + } + } + + //判断加密情况 + if (adaptationSet.Elements().Any(e => e.Name.LocalName == "ContentProtection")) + { + if (streamSpec.Playlist.MediaInit != null) + { + streamSpec.Playlist.MediaInit.EncryptInfo.Method = EncryptMethod.UNKNOWN; + } + foreach (var item in streamSpec.Playlist.MediaParts[0].MediaSegments) + { + item.EncryptInfo.Method = EncryptMethod.UNKNOWN; + } + } + + //处理同一ID分散在不同Period的情况 这种情况作为新的part出现 + var _index = streamList.FindIndex(_f => _f.GroupId == streamSpec.GroupId && _f.Resolution == streamSpec.Resolution && _f.MediaType == streamSpec.MediaType); + if (_index > -1) + { + var startIndex = streamList[_index].Playlist?.MediaParts.Last().MediaSegments.Last().Index + 1; + foreach (var item in streamSpec.Playlist.MediaParts[0].MediaSegments) + { + item.Index = item.Index + startIndex.Value; + } + streamList[_index].Playlist?.MediaParts.Add(new MediaPart() + { + MediaSegments = streamSpec.Playlist.MediaParts[0].MediaSegments + }); + } + else + { + streamList.Add(streamSpec); + } + } + } + } + + //为视频设置默认轨道 + var aL = streamList.Where(s => s.MediaType == MediaType.AUDIO); + var sL = streamList.Where(s => s.MediaType == MediaType.SUBTITLES); + foreach (var item in streamList) + { + if (!string.IsNullOrEmpty(item.Resolution)) + { + if (aL.Any()) + { + item.AudioId = aL.First().GroupId; + } + if (sL.Any()) + { + item.SubtitleId = sL.First().GroupId; + } + } + } + + return streamList; + } + + + public async Task FetchPlayListAsync(List streamSpecs) + { + return; + } + + public string PreProcessUrl(string url) + { + foreach (var p in ParserConfig.UrlProcessors) + { + if (p.CanProcess(ExtractorType, url, ParserConfig)) + { + url = p.Process(url, ParserConfig); + } + } + + return url; + } + + public void PreProcessContent() + { + foreach (var p in ParserConfig.ContentProcessors) + { + if (p.CanProcess(ExtractorType, MpdContent, ParserConfig)) + { + MpdContent = p.Process(MpdContent, ParserConfig); + } + } + } + } +} diff --git a/src/N_m3u8DL-RE.Parser/Extractor/HLSExtractor.cs b/src/N_m3u8DL-RE.Parser/Extractor/HLSExtractor.cs index e58a69f..ab913b2 100644 --- a/src/N_m3u8DL-RE.Parser/Extractor/HLSExtractor.cs +++ b/src/N_m3u8DL-RE.Parser/Extractor/HLSExtractor.cs @@ -44,7 +44,7 @@ namespace N_m3u8DL_RE.Parser.Extractor /// /// 预处理m3u8内容 /// - private void PreProcessContent() + public void PreProcessContent() { M3u8Content = M3u8Content.Trim(); if (!M3u8Content.StartsWith(HLSTags.ext_m3u)) @@ -64,7 +64,7 @@ namespace N_m3u8DL_RE.Parser.Extractor /// /// 预处理URL /// - private string PreProcessUrl(string url) + public string PreProcessUrl(string url) { foreach (var p in ParserConfig.UrlProcessors) { @@ -262,7 +262,7 @@ namespace N_m3u8DL_RE.Parser.Extractor //解析定义的分段长度 else if (line.StartsWith(HLSTags.ext_x_targetduration)) { - segment.Duration = Convert.ToDouble(ParserUtil.GetAttribute(line)); + playlist.TargetDuration = Convert.ToDouble(ParserUtil.GetAttribute(line)); } //解析起始编号 else if (line.StartsWith(HLSTags.ext_x_media_sequence)) @@ -439,6 +439,13 @@ namespace N_m3u8DL_RE.Parser.Extractor playlist.MediaParts = mediaParts; playlist.IsLive = !isEndlist; + //直播刷新间隔 + if (playlist.IsLive) + { + //由于播放器默认从最后3个分片开始播放 此处设置刷新间隔为TargetDuration的2倍 + playlist.RefreshIntervalMs = (int)((playlist.TargetDuration ?? 5) * 2 * 1000); + } + return playlist; } diff --git a/src/N_m3u8DL-RE.Parser/Extractor/IExtractor.cs b/src/N_m3u8DL-RE.Parser/Extractor/IExtractor.cs index 8f1f68a..25c0c5f 100644 --- a/src/N_m3u8DL-RE.Parser/Extractor/IExtractor.cs +++ b/src/N_m3u8DL-RE.Parser/Extractor/IExtractor.cs @@ -18,5 +18,9 @@ namespace N_m3u8DL_RE.Parser.Extractor Task> ExtractStreamsAsync(string rawText); Task FetchPlayListAsync(List streamSpecs); + + string PreProcessUrl(string url); + + void PreProcessContent(); } } diff --git a/src/N_m3u8DL-RE.Parser/StreamExtractor.cs b/src/N_m3u8DL-RE.Parser/StreamExtractor.cs index 6c46f39..461f5d1 100644 --- a/src/N_m3u8DL-RE.Parser/StreamExtractor.cs +++ b/src/N_m3u8DL-RE.Parser/StreamExtractor.cs @@ -66,7 +66,8 @@ namespace N_m3u8DL_RE.Parser else if (rawText.Contains(" + /// 从100-300这种字符串中获取StartRange, ExpectLength信息 + /// + /// + /// StartRange, ExpectLength + public static (long, long) ParseRange(string range) + { + var start = Convert.ToInt64(range.Split('-')[0]); + var end = Convert.ToInt64(range.Split('-')[1]); + return (start, end - start + 1); + } + + /// + /// MPD SegmentTemplate替换 + /// + /// + /// + /// + public static string ReplaceVars(string text, Dictionary keyValuePairs) + { + foreach (var item in keyValuePairs) + if (text.Contains(item.Key)) + text = text.Replace(item.Key, item.Value.ToString()); + + //处理特殊形式数字 如 $Number%05d$ + var regex = new Regex("\\$Number%([^$]+)d\\$"); + if (regex.IsMatch(text) && keyValuePairs.ContainsKey(DASHTags.TemplateNumber)) + { + foreach (Match m in regex.Matches(text)) + { + text = text.Replace(m.Value, keyValuePairs[DASHTags.TemplateNumber]?.ToString()?.PadLeft(Convert.ToInt32(m.Groups[1].Value), '0')); + } + } + + return text; + } + /// /// 拼接Baseurl和RelativeUrl /// diff --git a/src/N_m3u8DL-RE/N_m3u8DL-RE.csproj b/src/N_m3u8DL-RE/N_m3u8DL-RE.csproj index 19ccf75..1b0354f 100644 --- a/src/N_m3u8DL-RE/N_m3u8DL-RE.csproj +++ b/src/N_m3u8DL-RE/N_m3u8DL-RE.csproj @@ -16,4 +16,8 @@ + + + +