重写MPD解析(点播)
This commit is contained in:
parent
a8bf2f5320
commit
37e3e6217a
|
@ -9,14 +9,12 @@ namespace N_m3u8DL_RE.Common.Entity
|
||||||
public class MediaSegment
|
public class MediaSegment
|
||||||
{
|
{
|
||||||
public int Index { get; set; }
|
public int Index { get; set; }
|
||||||
|
|
||||||
public int TargetDuration { get; set; }
|
|
||||||
public double Duration { get; set; }
|
public double Duration { get; set; }
|
||||||
public string? Title { get; set; }
|
public string? Title { get; set; }
|
||||||
|
|
||||||
public long StartRange { get; set; } = 0L;
|
public long? StartRange { get; set; }
|
||||||
public long StopRange { get => StartRange + ExpectLength - 1; }
|
public long? StopRange { get => (StartRange != null && ExpectLength != null) ? StartRange + ExpectLength - 1 : null; }
|
||||||
public long ExpectLength { get; set; } = -1L;
|
public long? ExpectLength { get; set; }
|
||||||
|
|
||||||
public EncryptInfo EncryptInfo { get; set; } = new EncryptInfo();
|
public EncryptInfo EncryptInfo { get; set; } = new EncryptInfo();
|
||||||
|
|
||||||
|
|
|
@ -13,6 +13,12 @@ namespace N_m3u8DL_RE.Common.Entity
|
||||||
public string Url { get; set; }
|
public string Url { get; set; }
|
||||||
//是否直播
|
//是否直播
|
||||||
public bool IsLive { get; set; } = false;
|
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信息
|
//INIT信息
|
||||||
public MediaSegment? MediaInit { get; set; }
|
public MediaSegment? MediaInit { get; set; }
|
||||||
//分片信息
|
//分片信息
|
||||||
|
|
|
@ -32,7 +32,7 @@ namespace N_m3u8DL_RE.Common.Entity
|
||||||
|
|
||||||
public string Url { get; set; }
|
public string Url { get; set; }
|
||||||
|
|
||||||
public Playlist Playlist { get; set; }
|
public Playlist? Playlist { get; set; }
|
||||||
|
|
||||||
public override string ToString()
|
public override string ToString()
|
||||||
{
|
{
|
||||||
|
@ -75,8 +75,8 @@ namespace N_m3u8DL_RE.Common.Entity
|
||||||
//计算时长
|
//计算时长
|
||||||
if (Playlist != null)
|
if (Playlist != null)
|
||||||
{
|
{
|
||||||
var total = Playlist.MediaParts.Sum(x => x.MediaSegments.Sum(m => m.Duration));
|
var total = Playlist.TotalDuration;
|
||||||
returnStr += " | " + GlobalUtil.FormatTime((int)total);
|
returnStr += " | ~" + GlobalUtil.FormatTime((int)total);
|
||||||
}
|
}
|
||||||
|
|
||||||
return returnStr;
|
return returnStr;
|
||||||
|
|
|
@ -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$";
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 type = ((XmlElement)xn).GetAttribute("type"); //static dynamic
|
||||||
|
|
||||||
|
if (type == "static")
|
||||||
|
{
|
||||||
var mediaPresentationDuration = ((XmlElement)xn).GetAttribute("mediaPresentationDuration");
|
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");
|
var ns = ((XmlElement)xn).GetAttribute("xmlns");
|
||||||
|
|
||||||
XmlNamespaceManager nsMgr = new XmlNamespaceManager(mpdDoc.NameTable);
|
XmlNamespaceManager nsMgr = new XmlNamespaceManager(mpdDoc.NameTable);
|
||||||
nsMgr.AddNamespace("ns", ns);
|
nsMgr.AddNamespace("ns", ns);
|
||||||
|
|
||||||
TimeSpan ts = XmlConvert.ToTimeSpan(mediaPresentationDuration); //时长
|
|
||||||
|
|
||||||
//读取在MPD开头定义的<BaseURL>,并替换本身的URL
|
//读取在MPD开头定义的<BaseURL>,并替换本身的URL
|
||||||
var baseNode = xn.SelectSingleNode("ns:BaseURL", nsMgr);
|
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))
|
foreach (XmlElement period in xn.SelectNodes("ns:Period", nsMgr))
|
||||||
{
|
{
|
||||||
periodIndex++;
|
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()
|
var periodMsInfo = ExtractMultisegmentInfo(period, nsMgr, new JsonObject()
|
||||||
{
|
{
|
||||||
["StartNumber"] = 1,
|
["StartNumber"] = 1,
|
||||||
|
@ -410,7 +423,7 @@ namespace N_m3u8DL_RE.Parser.Extractor
|
||||||
new JsonObject()
|
new JsonObject()
|
||||||
{
|
{
|
||||||
["url"] = baseUrl,
|
["url"] = baseUrl,
|
||||||
["duration"] = ts.TotalSeconds
|
["duration"] = totalTs.TotalSeconds
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -463,6 +476,14 @@ namespace N_m3u8DL_RE.Parser.Extractor
|
||||||
|
|
||||||
//组装分片
|
//组装分片
|
||||||
Playlist playlist = new();
|
Playlist playlist = new();
|
||||||
|
playlist.IsLive = type == "static" ? false : true;
|
||||||
|
|
||||||
|
//直播刷新间隔
|
||||||
|
if (playlist.IsLive)
|
||||||
|
{
|
||||||
|
playlist.RefreshIntervalMs = updateTs.TotalMilliseconds;
|
||||||
|
}
|
||||||
|
|
||||||
List<MediaSegment> segments = new();
|
List<MediaSegment> segments = new();
|
||||||
//Initial URL
|
//Initial URL
|
||||||
if (item.ContainsKey("InitializationUrl"))
|
if (item.ContainsKey("InitializationUrl"))
|
||||||
|
@ -725,7 +746,7 @@ namespace N_m3u8DL_RE.Parser.Extractor
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 预处理URL
|
/// 预处理URL
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private string PreProcessUrl(string url)
|
public string PreProcessUrl(string url)
|
||||||
{
|
{
|
||||||
foreach (var p in ParserConfig.UrlProcessors)
|
foreach (var p in ParserConfig.UrlProcessors)
|
||||||
{
|
{
|
||||||
|
@ -738,7 +759,7 @@ namespace N_m3u8DL_RE.Parser.Extractor
|
||||||
return url;
|
return url;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void PreProcessContent()
|
public void PreProcessContent()
|
||||||
{
|
{
|
||||||
foreach (var p in ParserConfig.ContentProcessors)
|
foreach (var p in ParserConfig.ContentProcessors)
|
||||||
{
|
{
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -44,7 +44,7 @@ namespace N_m3u8DL_RE.Parser.Extractor
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 预处理m3u8内容
|
/// 预处理m3u8内容
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private void PreProcessContent()
|
public void PreProcessContent()
|
||||||
{
|
{
|
||||||
M3u8Content = M3u8Content.Trim();
|
M3u8Content = M3u8Content.Trim();
|
||||||
if (!M3u8Content.StartsWith(HLSTags.ext_m3u))
|
if (!M3u8Content.StartsWith(HLSTags.ext_m3u))
|
||||||
|
@ -64,7 +64,7 @@ namespace N_m3u8DL_RE.Parser.Extractor
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 预处理URL
|
/// 预处理URL
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private string PreProcessUrl(string url)
|
public string PreProcessUrl(string url)
|
||||||
{
|
{
|
||||||
foreach (var p in ParserConfig.UrlProcessors)
|
foreach (var p in ParserConfig.UrlProcessors)
|
||||||
{
|
{
|
||||||
|
@ -262,7 +262,7 @@ namespace N_m3u8DL_RE.Parser.Extractor
|
||||||
//解析定义的分段长度
|
//解析定义的分段长度
|
||||||
else if (line.StartsWith(HLSTags.ext_x_targetduration))
|
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))
|
else if (line.StartsWith(HLSTags.ext_x_media_sequence))
|
||||||
|
@ -439,6 +439,13 @@ namespace N_m3u8DL_RE.Parser.Extractor
|
||||||
playlist.MediaParts = mediaParts;
|
playlist.MediaParts = mediaParts;
|
||||||
playlist.IsLive = !isEndlist;
|
playlist.IsLive = !isEndlist;
|
||||||
|
|
||||||
|
//直播刷新间隔
|
||||||
|
if (playlist.IsLive)
|
||||||
|
{
|
||||||
|
//由于播放器默认从最后3个分片开始播放 此处设置刷新间隔为TargetDuration的2倍
|
||||||
|
playlist.RefreshIntervalMs = (int)((playlist.TargetDuration ?? 5) * 2 * 1000);
|
||||||
|
}
|
||||||
|
|
||||||
return playlist;
|
return playlist;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -18,5 +18,9 @@ namespace N_m3u8DL_RE.Parser.Extractor
|
||||||
Task<List<StreamSpec>> ExtractStreamsAsync(string rawText);
|
Task<List<StreamSpec>> ExtractStreamsAsync(string rawText);
|
||||||
|
|
||||||
Task FetchPlayListAsync(List<StreamSpec> streamSpecs);
|
Task FetchPlayListAsync(List<StreamSpec> streamSpecs);
|
||||||
|
|
||||||
|
string PreProcessUrl(string url);
|
||||||
|
|
||||||
|
void PreProcessContent();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -66,7 +66,8 @@ namespace N_m3u8DL_RE.Parser
|
||||||
else if (rawText.Contains("<MPD "))
|
else if (rawText.Contains("<MPD "))
|
||||||
{
|
{
|
||||||
Logger.InfoMarkUp(ResString.matchDASH);
|
Logger.InfoMarkUp(ResString.matchDASH);
|
||||||
extractor = new DASHExtractor(parserConfig);
|
//extractor = new DASHExtractor(parserConfig);
|
||||||
|
extractor = new DASHExtractor2(parserConfig);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
using System;
|
using N_m3u8DL_RE.Parser.Constants;
|
||||||
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
@ -57,6 +58,43 @@ namespace N_m3u8DL_RE.Parser.Util
|
||||||
return (0, null);
|
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>
|
/// <summary>
|
||||||
/// 拼接Baseurl和RelativeUrl
|
/// 拼接Baseurl和RelativeUrl
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
|
@ -16,4 +16,8 @@
|
||||||
<ProjectReference Include="..\N_m3u8DL-RE.Parser\N_m3u8DL-RE.Parser.csproj" />
|
<ProjectReference Include="..\N_m3u8DL-RE.Parser\N_m3u8DL-RE.Parser.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Folder Include="Downloader\" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|
Loading…
Reference in New Issue