重写MPD解析(点播)

This commit is contained in:
nilaoda 2022-07-10 02:22:50 +08:00
parent a8bf2f5320
commit 37e3e6217a
11 changed files with 507 additions and 19 deletions

View File

@ -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();

View File

@ -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; }
//分片信息

View File

@ -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;

View File

@ -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$";
}
}

View File

@ -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开头定义的<BaseURL>并替换本身的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<MediaSegment> segments = new();
//Initial URL
if (item.ContainsKey("InitializationUrl"))
@ -725,7 +746,7 @@ namespace N_m3u8DL_RE.Parser.Extractor
/// <summary>
/// 预处理URL
/// </summary>
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)
{

View File

@ -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<List<StreamSpec>> ExtractStreamsAsync(string rawText)
{
var streamList = new List<StreamSpec>();
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开头定义的<BaseURL>并替换本身的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<string, object?>();
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<StreamSpec> 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);
}
}
}
}
}

View File

@ -44,7 +44,7 @@ namespace N_m3u8DL_RE.Parser.Extractor
/// <summary>
/// 预处理m3u8内容
/// </summary>
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
/// <summary>
/// 预处理URL
/// </summary>
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;
}

View File

@ -18,5 +18,9 @@ namespace N_m3u8DL_RE.Parser.Extractor
Task<List<StreamSpec>> ExtractStreamsAsync(string rawText);
Task FetchPlayListAsync(List<StreamSpec> streamSpecs);
string PreProcessUrl(string url);
void PreProcessContent();
}
}

View File

@ -66,7 +66,8 @@ namespace N_m3u8DL_RE.Parser
else if (rawText.Contains("<MPD "))
{
Logger.InfoMarkUp(ResString.matchDASH);
extractor = new DASHExtractor(parserConfig);
//extractor = new DASHExtractor(parserConfig);
extractor = new DASHExtractor2(parserConfig);
}
else
{

View File

@ -1,4 +1,5 @@
using System;
using N_m3u8DL_RE.Parser.Constants;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
@ -57,6 +58,43 @@ namespace N_m3u8DL_RE.Parser.Util
return (0, null);
}
/// <summary>
/// 从100-300这种字符串中获取StartRange, ExpectLength信息
/// </summary>
/// <param name="range"></param>
/// <returns>StartRange, ExpectLength</returns>
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);
}
/// <summary>
/// MPD SegmentTemplate替换
/// </summary>
/// <param name="text"></param>
/// <param name="keyValuePairs"></param>
/// <returns></returns>
public static string ReplaceVars(string text, Dictionary<string, object?> 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;
}
/// <summary>
/// 拼接Baseurl和RelativeUrl
/// </summary>

View File

@ -16,4 +16,8 @@
<ProjectReference Include="..\N_m3u8DL-RE.Parser\N_m3u8DL-RE.Parser.csproj" />
</ItemGroup>
<ItemGroup>
<Folder Include="Downloader\" />
</ItemGroup>
</Project>