基本完成MPD解析

This commit is contained in:
nilaoda 2022-06-19 23:22:34 +08:00
parent 6828d11952
commit eb6421ce72
14 changed files with 898 additions and 51 deletions

View File

@ -31,5 +31,14 @@ namespace N_m3u8DL_RE.Common.Config
/// </summary> /// </summary>
public byte[]? CustomeIV { get; set; } public byte[]? CustomeIV { get; set; }
/// <summary>
/// 组装视频分段的URL时是否要把原本URL后的参数也加上去
/// 如 Base URL = "http://xxx.com/playlist.m3u8?hmac=xxx&token=xxx"
/// 相对路径 = clip_01.ts
/// 如果 AppendUrlParams=false得 http://xxx.com/clip_01.ts
/// 如果 AppendUrlParams=true得 http://xxx.com/clip_01.ts?hmac=xxx&token=xxx
/// </summary>
public bool AppendUrlParams { get; set; } = false;
} }
} }

View File

@ -12,7 +12,7 @@ namespace N_m3u8DL_RE.Common.Entity
//对应Url信息 //对应Url信息
public string Url { get; set; } public string Url { get; set; }
//是否直播 //是否直播
public bool IsLive { get; set; } public bool IsLive { get; set; } = false;
//INIT信息 //INIT信息
public MediaSegment? MediaInit { get; set; } public MediaSegment? MediaInit { get; set; }
//分片信息 //分片信息

View File

@ -35,6 +35,8 @@ namespace N_m3u8DL_RE.Common.Entity
public override string ToString() public override string ToString()
{ {
var prefixStr = "";
var returnStr = "";
var encStr = string.Empty; var encStr = string.Empty;
//增加加密标志 //增加加密标志
@ -45,19 +47,30 @@ namespace N_m3u8DL_RE.Common.Entity
if (MediaType == Enum.MediaType.AUDIO) if (MediaType == Enum.MediaType.AUDIO)
{ {
var d = $"{GroupId} | {Name} | {Language} | {(Channels != null ? Channels + "CH" : "")} | {(Playlist != null ? Playlist.MediaParts.Sum(x => x.MediaSegments.Count) + " Segments" : "")}".Replace("| |", "|"); prefixStr = $"[deepskyblue3]Aud[/] {encStr}";
return $"[deepskyblue3]Aud[/] {encStr}" + d.EscapeMarkup().Trim().Trim('|').Trim(); var d = $"{GroupId} | {(Bandwidth != null ? (Bandwidth / 1000) + " Kbps" : "")} | {Name} | {Language} | {(Channels != null ? Channels + "CH" : "")} | {(Playlist != null ? Playlist.MediaParts.Sum(x => x.MediaSegments.Count) + " Segments" : "")}";
returnStr = d.EscapeMarkup();
} }
else if (MediaType == Enum.MediaType.SUBTITLES) else if (MediaType == Enum.MediaType.SUBTITLES)
{ {
var d = $"{GroupId} | {Language} | {Name} | {(Playlist != null ? Playlist.MediaParts.Sum(x => x.MediaSegments.Count) + " Segments" : "")}".Replace("| |", "|"); prefixStr = $"[deepskyblue3_1]Sub[/] {encStr}";
return $"[deepskyblue3_1]Sub[/] {encStr}" + d.EscapeMarkup().Trim().Trim('|').Trim(); var d = $"{GroupId} | {Language} | {Name} | {(Playlist != null ? Playlist.MediaParts.Sum(x => x.MediaSegments.Count) + " Segments" : "")}";
returnStr = d.EscapeMarkup();
} }
else else
{ {
var d = $"{Resolution} | {Bandwidth / 1000} Kbps | {FrameRate} | {Codecs} | {(Playlist != null ? Playlist.MediaParts.Sum(x => x.MediaSegments.Count) + " Segments" : "")}".Replace("| |", "|"); prefixStr = $"[aqua]Vid[/] {encStr}";
return $"[aqua]Vid[/] {encStr}" + d.EscapeMarkup().Trim().Trim('|').Trim(); var d = $"{Resolution} | {Bandwidth / 1000} Kbps | {Name} | {FrameRate} | {Codecs} | {(Playlist != null ? Playlist.MediaParts.Sum(x => x.MediaSegments.Count) + " Segments" : "")}";
returnStr = d.EscapeMarkup();
} }
returnStr = prefixStr + returnStr.Trim().Trim('|').Trim();
while (returnStr.Contains("| |"))
{
returnStr = returnStr.Replace("| |", "|");
}
return returnStr;
} }
} }
} }

View File

@ -69,6 +69,15 @@ namespace N_m3u8DL_RE.Common.Resource {
} }
} }
/// <summary>
/// 查找类似 验证最后一个分片有效性 的本地化字符串。
/// </summary>
public static string checkingLast {
get {
return ResourceManager.GetString("checkingLast", resourceCulture);
}
}
/// <summary> /// <summary>
/// 查找类似 获取: 的本地化字符串。 /// 查找类似 获取: 的本地化字符串。
/// </summary> /// </summary>
@ -105,6 +114,15 @@ namespace N_m3u8DL_RE.Common.Resource {
} }
} }
/// <summary>
/// 查找类似 内容匹配: [white on mediumorchid1]Dynamic Adaptive Streaming over HTTP[/] 的本地化字符串。
/// </summary>
public static string matchDASH {
get {
return ResourceManager.GetString("matchDASH", resourceCulture);
}
}
/// <summary> /// <summary>
/// 查找类似 内容匹配: [white on deepskyblue1]HTTP Live Streaming[/] 的本地化字符串。 /// 查找类似 内容匹配: [white on deepskyblue1]HTTP Live Streaming[/] 的本地化字符串。
/// </summary> /// </summary>
@ -123,6 +141,15 @@ namespace N_m3u8DL_RE.Common.Resource {
} }
} }
/// <summary>
/// 查找类似 正在解析媒体信息... 的本地化字符串。
/// </summary>
public static string parsingStream {
get {
return ResourceManager.GetString("parsingStream", resourceCulture);
}
}
/// <summary> /// <summary>
/// 查找类似 [grey](按键盘上下键以浏览更多内容)[/] 的本地化字符串。 /// 查找类似 [grey](按键盘上下键以浏览更多内容)[/] 的本地化字符串。
/// </summary> /// </summary>

View File

@ -136,4 +136,13 @@
<data name="writeJson" xml:space="preserve"> <data name="writeJson" xml:space="preserve">
<value>Writing meta.json</value> <value>Writing meta.json</value>
</data> </data>
<data name="matchDASH" xml:space="preserve">
<value>Content Matched: [white on mediumorchid1]Dynamic Adaptive Streaming over HTTP[/]</value>
</data>
<data name="checkingLast" xml:space="preserve">
<value>Verifying the validity of the last segment</value>
</data>
<data name="parsingStream" xml:space="preserve">
<value>Parsing streams...</value>
</data>
</root> </root>

View File

@ -136,4 +136,13 @@
<data name="writeJson" xml:space="preserve"> <data name="writeJson" xml:space="preserve">
<value>写出meta.json</value> <value>写出meta.json</value>
</data> </data>
<data name="matchDASH" xml:space="preserve">
<value>内容匹配: [white on mediumorchid1]Dynamic Adaptive Streaming over HTTP[/]</value>
</data>
<data name="checkingLast" xml:space="preserve">
<value>验证最后一个分片有效性</value>
</data>
<data name="parsingStream" xml:space="preserve">
<value>正在解析媒体信息...</value>
</data>
</root> </root>

View File

@ -136,4 +136,13 @@
<data name="writeJson" xml:space="preserve"> <data name="writeJson" xml:space="preserve">
<value>寫出meta.json</value> <value>寫出meta.json</value>
</data> </data>
<data name="matchDASH" xml:space="preserve">
<value>內容匹配: [white on mediumorchid1]Dynamic Adaptive Streaming over HTTP[/]</value>
</data>
<data name="checkingLast" xml:space="preserve">
<value>驗證最後一個分片有效性</value>
</data>
<data name="parsingStream" xml:space="preserve">
<value>正在解析媒體信息...</value>
</data>
</root> </root>

View File

@ -34,7 +34,7 @@ namespace N_m3u8DL_RE.Common.Util
Timeout = TimeSpan.FromMinutes(2) Timeout = TimeSpan.FromMinutes(2)
}; };
public static async Task<HttpResponseMessage> DoGetAsync(string url, Dictionary<string, string>? headers = null) private static async Task<HttpResponseMessage> DoGetAsync(string url, Dictionary<string, string>? headers = null)
{ {
Logger.Debug(ResString.fetch + url); Logger.Debug(ResString.fetch + url);
using var webRequest = new HttpRequestMessage(HttpMethod.Get, url); using var webRequest = new HttpRequestMessage(HttpMethod.Get, url);

View File

@ -0,0 +1,740 @@
using N_m3u8DL_RE.Common.Config;
using N_m3u8DL_RE.Common.Entity;
using N_m3u8DL_RE.Common.Enum;
using N_m3u8DL_RE.Common.Log;
using N_m3u8DL_RE.Common.Resource;
using N_m3u8DL_RE.Common.Util;
using N_m3u8DL_RE.Parser.Util;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Text;
using System.Text.Json.Nodes;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Xml;
namespace N_m3u8DL_RE.Parser.Extractor
{
//code from https://github.com/ytdl-org/youtube-dl/blob/master/youtube_dl/extractor/common.py#L2076
internal class DASHExtractor : IExtractor
{
private string MpdUrl = string.Empty;
private string BaseUrl = string.Empty;
private string MpdContent = string.Empty;
public ParserConfig ParserConfig { get; set; }
public DASHExtractor(ParserConfig parserConfig)
{
this.ParserConfig = parserConfig;
this.MpdUrl = parserConfig.Url ?? string.Empty;
if (!string.IsNullOrEmpty(parserConfig.BaseUrl))
this.BaseUrl = parserConfig.BaseUrl;
else
this.BaseUrl = this.MpdUrl;
}
public async Task<List<StreamSpec>> ExtractStreamsAsync(string rawText)
{
this.MpdContent = rawText;
this.PreProcessContent();
XmlDocument mpdDoc = new XmlDocument();
mpdDoc.LoadXml(MpdContent);
XmlNode xn = null;
//Select MPD node
foreach (XmlNode node in mpdDoc.ChildNodes)
{
if (node.NodeType == XmlNodeType.Element && node.Name == "MPD")
{
xn = node;
break;
}
}
var mediaPresentationDuration = ((XmlElement)xn).GetAttribute("mediaPresentationDuration");
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);
if (baseNode != null)
{
if (MpdUrl.Contains("kkbox.com.tw/"))
{
var badUrl = baseNode.InnerText;
var goodUrl = badUrl.Replace("//https:%2F%2F", "//");
BaseUrl = goodUrl;
}
else
{
BaseUrl = baseNode.InnerText;
}
}
var formatList = new List<JsonObject>(); //存放所有音视频清晰度
var periodIndex = 0; //解决同一个period且同id导致被重复添加分片
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 periodMsInfo = ExtractMultisegmentInfo(period, nsMgr, new JsonObject()
{
["StartNumber"] = 1,
["Timescale"] = 1
});
foreach (XmlElement adaptationSet in period.SelectNodes("ns:AdaptationSet", nsMgr))
{
var adaptionSetMsInfo = ExtractMultisegmentInfo(adaptationSet, nsMgr, periodMsInfo);
foreach (XmlElement representation in adaptationSet.SelectNodes("ns:Representation", nsMgr))
{
string GetAttribute(string key)
{
var v1 = representation.GetAttribute(key);
if (string.IsNullOrEmpty(v1))
return adaptationSet.GetAttribute(key);
return v1;
}
var mimeType = GetAttribute("mimeType");
var contentType = mimeType.Split('/')[0];
if (contentType == "video" || contentType == "audio" || contentType == "text")
{
var baseUrl = "";
bool CheckBaseUrl()
{
return Regex.IsMatch(baseUrl, @"^https?://");
}
var list = new List<XmlNodeList>()
{
representation.ChildNodes,
adaptationSet.ChildNodes,
period.ChildNodes,
mpdDoc.ChildNodes
};
foreach (XmlNodeList xmlNodeList in list)
{
foreach (XmlNode node in xmlNodeList)
{
if (node.Name == "BaseURL")
{
baseUrl = node.InnerText + baseUrl;
if (CheckBaseUrl()) break;
}
}
if (CheckBaseUrl()) break;
}
string GetBaseUrl(string url)
{
if (url.Contains("?"))
url = url.Remove(url.LastIndexOf('?'));
url = url.Substring(0, url.LastIndexOf('/') + 1);
return url;
}
var mpdBaseUrl = string.IsNullOrEmpty(BaseUrl) ? GetBaseUrl(MpdUrl) : BaseUrl;
if (!string.IsNullOrEmpty(mpdBaseUrl) && !CheckBaseUrl())
{
if (!mpdBaseUrl.EndsWith("/") && !baseUrl.StartsWith("/"))
{
mpdBaseUrl += "/";
}
baseUrl = ParserUtil.CombineURL(mpdBaseUrl, baseUrl);
}
var representationId = GetAttribute("id");
var lang = GetAttribute("lang");
var bandwidth = IntOrNull(GetAttribute("bandwidth"));
var frameRate = GetAttribute("frameRate");
if (frameRate.Contains("/"))
{
var d = Convert.ToDouble(frameRate.Split('/')[0]) / Convert.ToDouble(frameRate.Split('/')[1]);
frameRate = d.ToString("0.000");
}
var f = new JsonObject()
{
["PeriodIndex"] = periodIndex,
["ContentType"] = contentType,
["FormatId"] = representationId,
["ManifestUrl"] = MpdUrl,
["Width"] = IntOrNull(GetAttribute("width")),
["Height"] = IntOrNull(GetAttribute("height")),
["Tbr"] = (int)DoubleOrNull(bandwidth),
["Asr"] = IntOrNull(GetAttribute("audioSamplingRate")),
["Fps"] = DoubleOrNull(frameRate),
["Language"] = lang,
["Codecs"] = GetAttribute("codecs")
};
var representationMsInfo = ExtractMultisegmentInfo(representation, nsMgr, adaptionSetMsInfo);
string PrepareTemplate(string templateName, string[] identifiers)
{
var tmpl = representationMsInfo?[templateName]?.GetValue<string>();
var t = new StringBuilder();
var inTemplate = false;
foreach (var ch in tmpl)
{
t.Append(ch);
if (ch == '$')
{
inTemplate = !inTemplate;
}
else if (ch == '%' && !inTemplate)
{
t.Append(ch);
}
}
var str = t.ToString();
str = str.Replace("$RepresentationID$", representationId);
str = Regex.Replace(str, "\\$(" + string.Join("|", identifiers) + ")\\$", "{{$1}}");
str = Regex.Replace(str, "\\$(" + string.Join("|", identifiers) + ")%([^$]+)d\\$", "{{$1}}{0:D$2}");
str = str.Replace("$$", "$");
return str;
}
string PadNumber(string template, string key, long value)
{
string ReplaceFirst(string text, string search, string replace)
{
int pos = text.IndexOf(search);
if (pos < 0)
{
return text;
}
return text.Substring(0, pos) + replace + text.Substring(pos + search.Length);
}
template = template.Replace("{{" + key + "}}", "");
var m = Regex.Match(template, "{0:D(\\d+)}");
return ReplaceFirst(template, m.Value, value.ToString("0".PadRight(Convert.ToInt32(m.Groups[1].Value), '0')));
}
if (representationMsInfo.ContainsKey("Initialization"))
{
var initializationTemplate = PrepareTemplate("Initialization", new string[] { "Bandwidth" });
var initializationUrl = "";
if (initializationTemplate.Contains("{0:D"))
{
if (initializationTemplate.Contains("{{Bandwidth}}"))
initializationUrl = PadNumber(initializationTemplate, "Bandwidth", bandwidth);
}
else
{
initializationUrl = initializationTemplate.Replace("{{Bandwidth}}", bandwidth.ToString());
}
representationMsInfo["InitializationUrl"] = ParserUtil.CombineURL(baseUrl, initializationUrl);
}
string LocationKey(string location)
{
return Regex.IsMatch(location, "^https?://") ? "url" : "path";
}
if (!representationMsInfo.ContainsKey("SegmentUrls") && representationMsInfo.ContainsKey("Media"))
{
var mediaTemplate = PrepareTemplate("Media", new string[] { "Number", "Bandwidth", "Time" });
var mediaLocationKey = LocationKey(mediaTemplate);
if (mediaTemplate.Contains("{{Number") && !representationMsInfo.ContainsKey("S"))
{
var segmentDuration = 0.0;
if (!representationMsInfo.ContainsKey("TotalNumber") && representationMsInfo.ContainsKey("SegmentDuration"))
{
segmentDuration = DoubleOrNull(representationMsInfo["SegmentDuration"].GetValue<double>(), representationMsInfo["Timescale"].GetValue<int>());
representationMsInfo["TotalNumber"] = (int)Math.Ceiling(periodDuration.TotalSeconds / segmentDuration);
}
var fragments = new JsonArray();
for (int i = representationMsInfo["StartNumber"].GetValue<int>(); i < representationMsInfo["StartNumber"].GetValue<int>() + representationMsInfo["TotalNumber"].GetValue<int>(); i++)
{
var segUrl = "";
if (mediaTemplate.Contains("{0:D"))
{
if (mediaTemplate.Contains("{{Bandwidth}}"))
segUrl = PadNumber(mediaTemplate, "Bandwidth", bandwidth);
if (mediaTemplate.Contains("{{Number}}"))
segUrl = PadNumber(mediaTemplate, "Number", i);
}
else
{
segUrl = mediaTemplate.Replace("{{Bandwidth}}", bandwidth.ToString());
segUrl = segUrl.Replace("{{Number}}", i.ToString());
}
fragments.Add(new JsonObject()
{
[mediaLocationKey] = ParserUtil.CombineURL(baseUrl, segUrl),
["duration"] = segmentDuration
});
}
representationMsInfo["Fragments"] = fragments;
}
else
{
var fragments = new JsonArray();
var segmentTime = 0L;
var segmentD = 0L;
var segmentNumber = representationMsInfo["StartNumber"].GetValue<int>();
void addSegmentUrl()
{
var segUrl = "";
if (mediaTemplate.Contains("{0:D"))
{
if (mediaTemplate.Contains("{{Bandwidth}}"))
segUrl = PadNumber(mediaTemplate, "Bandwidth", bandwidth);
if (mediaTemplate.Contains("{{Number}}"))
segUrl = PadNumber(mediaTemplate, "Number", segmentNumber);
if (mediaTemplate.Contains("{{Time}}"))
segUrl = PadNumber(mediaTemplate, "Time", segmentTime);
}
else
{
segUrl = mediaTemplate.Replace("{{Bandwidth}}", bandwidth.ToString());
segUrl = segUrl.Replace("{{Number}}", segmentNumber.ToString());
segUrl = segUrl.Replace("{{Time}}", segmentTime.ToString());
}
fragments.Add(new JsonObject()
{
[mediaLocationKey] = ParserUtil.CombineURL(baseUrl, segUrl),
["duration"] = DoubleOrNull(segmentD, representationMsInfo["Timescale"].GetValue<int>())
});
}
if (representationMsInfo.ContainsKey("S"))
{
var array = representationMsInfo["S"].GetValue<JsonArray>();
for (int i = 0; i < array.Count; i++)
{
var s = array[i];
segmentTime = s["t"].GetValue<long>() == 0L ? segmentTime : s["t"].GetValue<long>();
segmentD = s["d"].GetValue<long>();
addSegmentUrl();
segmentNumber++;
for (int j = 0; j < s["r"].GetValue<long>(); j++)
{
segmentTime += segmentD;
addSegmentUrl();
segmentNumber++;
}
segmentTime += segmentD;
}
}
representationMsInfo["Fragments"] = fragments;
}
}
else if (representationMsInfo.ContainsKey("SegmentUrls") && representationMsInfo.ContainsKey("S"))
{
var fragments = new JsonArray();
var segmentIndex = 0;
var timescale = representationMsInfo["Timescale"].GetValue<int>();
foreach (var s in representationMsInfo["S"].GetValue<JsonArray>())
{
var duration = DoubleOrNull(s["d"], timescale);
for (int j = 0; j < s["r"].GetValue<long>() + 1; j++)
{
var segmentUri = representationMsInfo["SegmentUrls"][segmentIndex].GetValue<string>();
fragments.Add(new JsonObject()
{
[LocationKey(segmentUri)] = ParserUtil.CombineURL(baseUrl, segmentUri),
["duration"] = duration
});
segmentIndex++;
}
}
representationMsInfo["Fragments"] = fragments;
}
else if (representationMsInfo.ContainsKey("SegmentUrls"))
{
var fragments = new JsonArray();
var segmentDuration = DoubleOrNull(representationMsInfo["SegmentDuration"].GetValue<double>(), representationMsInfo.ContainsKey("SegmentDuration") ? representationMsInfo["Timescale"].GetValue<int>() : 1);
foreach (var jsonNode in representationMsInfo["SegmentUrls"].GetValue<JsonArray>())
{
var segmentUrl = jsonNode.GetValue<string>();
if (segmentDuration != -1)
{
fragments.Add(new JsonObject()
{
[LocationKey(segmentUrl)] = ParserUtil.CombineURL(baseUrl, segmentUrl),
["duration"] = segmentDuration
});
}
else
{
fragments.Add(new JsonObject()
{
[LocationKey(segmentUrl)] = ParserUtil.CombineURL(baseUrl, segmentUrl)
});
}
}
representationMsInfo["Fragments"] = fragments;
}
if (representationMsInfo.ContainsKey("Fragments"))
{
f["Url"] = string.IsNullOrEmpty(MpdUrl) ? baseUrl : MpdUrl;
f["FragmentBaseUrl"] = baseUrl;
if (representationMsInfo.ContainsKey("InitializationUrl"))
{
f["InitializationUrl"] = ParserUtil.CombineURL(baseUrl, representationMsInfo["InitializationUrl"].GetValue<string>());
if (f["InitializationUrl"].GetValue<string>().StartsWith("$$Range"))
{
f["InitializationUrl"] = ParserUtil.CombineURL(baseUrl, f["InitializationUrl"].GetValue<string>());
}
f["Fragments"] = JsonArray.Parse(representationMsInfo["Fragments"].ToJsonString());
}
}
else
{
//整段mp4
f["Fragments"] = new JsonArray() {
new JsonObject()
{
["url"] = baseUrl,
["duration"] = ts.TotalSeconds
}
};
}
//处理同一ID分散在不同Period的情况
if (formatList.Any(_f => _f["FormatId"].ToJsonString() == f["FormatId"].ToJsonString() && _f["Width"].ToJsonString() == f["Width"].ToJsonString() && _f["ContentType"].ToJsonString() == f["ContentType"].ToJsonString()))
{
for (int i = 0; i < formatList.Count; i++)
{
//参数相同但不在同一个Period才可以
if (formatList[i]["FormatId"].ToJsonString() == f["FormatId"].ToJsonString() && formatList[i]["Width"].ToJsonString() == f["Width"].ToJsonString() && formatList[i]["ContentType"].ToJsonString() == f["ContentType"].ToJsonString() && formatList[i]["PeriodIndex"].ToJsonString() != f["PeriodIndex"].ToJsonString())
{
var array = formatList[i]["Fragments"].AsArray();
foreach (var item in f["Fragments"].AsArray())
{
array.Add(item);
}
formatList[i]["Fragments"] = array;
break;
}
}
}
else
{
formatList.Add(f);
}
}
}
}
}
var streamList = new List<StreamSpec>();
foreach (var item in formatList)
{
//基本信息
StreamSpec streamSpec = new();
streamSpec.Name = item["FormatId"].GetValue<string>();
streamSpec.Bandwidth = item["Tbr"].GetValue<int>();
streamSpec.Codecs = item["Codecs"].GetValue<string>();
streamSpec.Language = item["Language"].GetValue<string>();
streamSpec.FrameRate = item["Fps"].GetValue<double>();
streamSpec.Resolution = item["Width"].GetValue<int>() != -1 ? $"{item["Width"]}x{item["Height"]}" : "";
streamSpec.Url = MpdUrl;
streamSpec.MediaType = item["ContentType"].GetValue<string>() switch
{
"text" => MediaType.SUBTITLES,
"audio" => MediaType.AUDIO,
_ => null
};
//组装分片
Playlist playlist = new();
List<MediaSegment> segments = new();
//Initial URL
if (item.ContainsKey("InitializationUrl"))
{
var initUrl = item["InitializationUrl"].GetValue<string>();
if (Regex.IsMatch(initUrl, "\\$\\$Range=(\\d+)-(\\d+)"))
{
var match = Regex.Match(initUrl, "\\$\\$Range=(\\d+)-(\\d+)");
string rangeStr = match.Value;
long start = Convert.ToInt64(match.Groups[1].Value);
long end = Convert.ToInt64(match.Groups[2].Value);
playlist.MediaInit = new MediaSegment()
{
EncryptInfo = new EncryptInfo()
{
Method = EncryptMethod.UNKNOWN
},
Url = PreProcessUrl(initUrl.Replace(rangeStr, "")),
StartRange = start,
ExpectLength = end - start + 1
};
}
else
{
playlist.MediaInit = new MediaSegment()
{
Url = PreProcessUrl(initUrl)
};
}
}
//分片地址
var fragments = item["Fragments"].AsArray();
var index = 0;
foreach (var fragment in fragments)
{
var seg = fragment.AsObject();
var dur = seg.ContainsKey("duration") ? seg["duration"].GetValue<double>() : 0.0;
var url = seg.ContainsKey("url") ? seg["url"].GetValue<string>() : seg["path"].GetValue<string>();
url = PreProcessUrl(url);
MediaSegment mediaSegment = new()
{
Index = index,
Duration = dur,
Url = url,
};
if (Regex.IsMatch(url, "\\$\\$Range=(\\d+)-(\\d+)"))
{
var match = Regex.Match(url, "\\$\\$Range=(\\d+)-(\\d+)");
string rangeStr = match.Value;
long start = Convert.ToInt64(match.Groups[1].Value);
long end = Convert.ToInt64(match.Groups[2].Value);
mediaSegment.StartRange = start;
mediaSegment.ExpectLength = end - start + 1;
}
segments.Add(mediaSegment);
index++;
}
playlist.MediaParts = new List<MediaPart>()
{
new MediaPart()
{
MediaSegments = segments
}
};
//统一添加EncryptInfo
playlist.MediaInit.EncryptInfo = new EncryptInfo()
{
Method = EncryptMethod.UNKNOWN
};
foreach (var seg in playlist.MediaParts[0].MediaSegments)
{
seg.EncryptInfo = new EncryptInfo()
{
Method = EncryptMethod.UNKNOWN
};
}
streamSpec.Playlist = playlist;
streamList.Add(streamSpec);
}
return streamList;
}
static bool CheckValid(string url)
{
try
{
HttpWebRequest request = (HttpWebRequest)WebRequest.Create(new Uri(url));
request.Timeout = 120000;
HttpWebResponse response = (HttpWebResponse)request.GetResponse();
if (((int)response.StatusCode).ToString().StartsWith("2")) return true;
else return false;
}
catch (Exception) { return false; }
}
static double DoubleOrNull(object text, int scale = 1)
{
try
{
return Convert.ToDouble(text) / scale;
}
catch (Exception)
{
return -1;
}
}
static int IntOrNull(string text, int scale = 1)
{
try
{
return Convert.ToInt32(text) / scale;
}
catch (Exception)
{
return -1;
}
}
private JsonObject ExtractMultisegmentInfo(XmlElement period, XmlNamespaceManager nsMgr, JsonObject jsonObject)
{
var MultisegmentInfo = new JsonObject();
foreach (var item in jsonObject)
{
MultisegmentInfo[item.Key] = JsonNode.Parse(item.Value.ToJsonString());
}
void ExtractCommon(XmlNode source)
{
var sourceE = (XmlElement)source;
var segmentTimeline = source.SelectSingleNode("ns:SegmentTimeline", nsMgr);
if (segmentTimeline != null)
{
var sE = segmentTimeline.SelectNodes("ns:S", nsMgr);
if (sE?.Count > 0)
{
MultisegmentInfo["TotalNumber"] = 0;
var SList = new JsonArray();
foreach (XmlElement s in sE)
{
var r = string.IsNullOrEmpty(s.GetAttribute("r")) ? 0 : Convert.ToInt64(s.GetAttribute("r"));
MultisegmentInfo["TotalNumber"] = MultisegmentInfo["TotalNumber"]?.GetValue<int>() + 1 + r;
SList.Add(new JsonObject()
{
["t"] = string.IsNullOrEmpty(s.GetAttribute("t")) ? 0 : Convert.ToInt64(s.GetAttribute("t")),
["d"] = Convert.ToInt64(s.GetAttribute("d")),
["r"] = r
});
}
MultisegmentInfo.Add("S", SList);
}
}
var startNumber = sourceE.GetAttribute("startNumber");
if (!string.IsNullOrEmpty(startNumber))
{
MultisegmentInfo["StartNumber"] = Convert.ToInt32(startNumber);
}
var timescale = sourceE.GetAttribute("timescale");
if (!string.IsNullOrEmpty(timescale))
{
MultisegmentInfo["Timescale"] = Convert.ToInt32(timescale);
}
var segmentDuration = sourceE.GetAttribute("duration");
if (!string.IsNullOrEmpty(segmentDuration))
{
MultisegmentInfo["SegmentDuration"] = Convert.ToDouble(segmentDuration);
}
}
void ExtractInitialization(XmlNode source)
{
var initialization = source.SelectSingleNode("ns:Initialization", nsMgr);
if (initialization != null)
{
MultisegmentInfo["InitializationUrl"] = ((XmlElement)initialization).GetAttribute("sourceURL");
if (((XmlElement)initialization).HasAttribute("range"))
{
MultisegmentInfo["InitializationUrl"] += "$$Range=" + ((XmlElement)initialization).GetAttribute("range");
}
}
}
var segmentList = period.SelectSingleNode("ns:SegmentList", nsMgr);
if (segmentList != null)
{
ExtractCommon(segmentList);
ExtractInitialization(segmentList);
var segmentUrlsE = segmentList.SelectNodes("ns:SegmentURL", nsMgr);
var urls = new JsonArray();
foreach (XmlElement segment in segmentUrlsE)
{
if (segment.HasAttribute("mediaRange"))
{
urls.Add("$$Range=" + segment.GetAttribute("mediaRange"));
}
else
{
urls.Add(segment.GetAttribute("media"));
}
}
MultisegmentInfo["SegmentUrls"] = urls;
}
else
{
var segmentTemplate = period.SelectSingleNode("ns:SegmentTemplate", nsMgr);
if (segmentTemplate != null)
{
ExtractCommon(segmentTemplate);
var media = ((XmlElement)segmentTemplate).GetAttribute("media");
if (!string.IsNullOrEmpty(media))
{
MultisegmentInfo["Media"] = media;
}
var initialization = ((XmlElement)segmentTemplate).GetAttribute("initialization");
if (!string.IsNullOrEmpty(initialization))
{
MultisegmentInfo["Initialization"] = initialization;
}
else
{
ExtractInitialization(segmentTemplate);
}
}
}
return MultisegmentInfo;
}
/// <summary>
/// 预处理URL
/// </summary>
private string PreProcessUrl(string url)
{
if (ParserConfig.AppendUrlParams)
{
url += new Regex("\\?.*").Match(MpdUrl).Value;
}
return url;
}
private void PreProcessContent()
{
//XiGua
if (this.MpdContent.Contains("<mas:") && !this.MpdContent.Contains("xmlns:mas"))
{
this.MpdContent = this.MpdContent.Replace("<MPD ", "<MPD xmlns:mas=\"urn:marlin:mas:1-0:services:schemas:mpd\" ");
}
}
public async Task FetchPlayListAsync(List<StreamSpec> streamSpecs)
{
//MPD不需要重新去读取内容只判断一下最后的分片是否有效即可
foreach (var item in streamSpecs)
{
//检测最后一片的有效性
if (item.Playlist.MediaParts[0].MediaSegments.Count > 1)
{
var last = item.Playlist.MediaParts[0].MediaSegments.Last();
var secondToLast = item.Playlist.MediaParts[0].MediaSegments[item.Playlist.MediaParts[0].MediaSegments.Count - 2];
var urlLast = last.Url;
var urlSecondToLast = secondToLast.Url;
//普通分段才判断
if (urlLast.StartsWith("http") && !Regex.IsMatch(urlLast, "\\$\\$Range=(\\d+)-(\\d+)"))
{
Logger.Warn(ResString.checkingLast + $"({(item.MediaType == MediaType.AUDIO ? "Audio" : (item.MediaType == MediaType.SUBTITLES ? "Sub" : "Video"))})");
//倒数第二段正常,倒数第一段不正常
if (CheckValid(urlSecondToLast) && !CheckValid(urlLast))
item.Playlist.MediaParts[0].MediaSegments.RemoveAt(item.Playlist.MediaParts[0].MediaSegments.Count - 1);
}
}
}
}
}
}

View File

@ -107,14 +107,11 @@ namespace N_m3u8DL_RE.Parser.Extractor
/// </summary> /// </summary>
private string PreProcessUrl(string url) private string PreProcessUrl(string url)
{ {
if (url.Contains("?__gda__")) if (ParserConfig.AppendUrlParams)
{
url += new Regex("\\?__gda__.*").Match(M3u8Url).Value;
}
if (M3u8Url.Contains("//dlsc.hcs.cmvideo.cn") && (url.EndsWith(".ts") || url.EndsWith(".mp4")))
{ {
url += new Regex("\\?.*").Match(M3u8Url).Value; url += new Regex("\\?.*").Match(M3u8Url).Value;
} }
return url; return url;
} }
@ -480,9 +477,16 @@ namespace N_m3u8DL_RE.Parser.Extractor
private async Task<byte[]> ParseKeyAsync(string uri) private async Task<byte[]> ParseKeyAsync(string uri)
{ {
var segUrl = PreProcessUrl(ParserUtil.CombineURL(BaseUrl, uri)); if (uri.ToLower().StartsWith("base64:"))
var bytes = await HTTPUtil.GetBytesAsync(segUrl, ParserConfig.Headers); {
return bytes; return Convert.FromBase64String(uri.Substring(7));
}
else
{
var segUrl = PreProcessUrl(ParserUtil.CombineURL(BaseUrl, uri));
var bytes = await HTTPUtil.GetBytesAsync(segUrl, ParserConfig.Headers);
return bytes;
}
} }
public async Task<List<StreamSpec>> ExtractStreamsAsync(string rawText) public async Task<List<StreamSpec>> ExtractStreamsAsync(string rawText)
@ -494,12 +498,6 @@ namespace N_m3u8DL_RE.Parser.Extractor
Logger.Warn(ResString.masterM3u8Found); Logger.Warn(ResString.masterM3u8Found);
var lists = await ParseMasterListAsync(); var lists = await ParseMasterListAsync();
lists = lists.DistinctBy(p => p.Url).ToList(); lists = lists.DistinctBy(p => p.Url).ToList();
for (int i = 0; i < lists.Count; i++)
{
//重新加载m3u8
await LoadM3u8FromUrlAsync(lists[i].Url);
lists[i].Playlist = await ParseListAsync();
}
return lists; return lists;
} }
else else
@ -530,5 +528,15 @@ namespace N_m3u8DL_RE.Parser.Extractor
this.M3u8Url = this.BaseUrl = url; this.M3u8Url = this.BaseUrl = url;
this.PreProcessContent(); this.PreProcessContent();
} }
public async Task FetchPlayListAsync(List<StreamSpec> lists)
{
for (int i = 0; i < lists.Count; i++)
{
//重新加载m3u8
await LoadM3u8FromUrlAsync(lists[i].Url);
lists[i].Playlist = await ParseListAsync();
}
}
} }
} }

View File

@ -13,5 +13,7 @@ namespace N_m3u8DL_RE.Parser.Extractor
ParserConfig ParserConfig { get; set; } ParserConfig ParserConfig { get; set; }
Task<List<StreamSpec>> ExtractStreamsAsync(string rawText); Task<List<StreamSpec>> ExtractStreamsAsync(string rawText);
Task FetchPlayListAsync(List<StreamSpec> streamSpecs);
} }
} }

View File

@ -38,6 +38,11 @@ namespace N_m3u8DL_RE.Parser
this.rawText = HTTPUtil.GetWebSourceAsync(url, parserConfig.Headers).Result; this.rawText = HTTPUtil.GetWebSourceAsync(url, parserConfig.Headers).Result;
parserConfig.Url = url; parserConfig.Url = url;
} }
else if (File.Exists(url))
{
this.rawText = File.ReadAllText(url);
parserConfig.Url = new Uri(url).AbsoluteUri;
}
this.rawText = rawText.Trim(); this.rawText = rawText.Trim();
LoadSourceFromText(this.rawText); LoadSourceFromText(this.rawText);
} }
@ -51,9 +56,10 @@ namespace N_m3u8DL_RE.Parser
Logger.InfoMarkUp(ResString.matchHLS); Logger.InfoMarkUp(ResString.matchHLS);
extractor = new HLSExtractor(parserConfig); extractor = new HLSExtractor(parserConfig);
} }
else if (rawText.StartsWith("..")) else if (rawText.Contains("<MPD "))
{ {
Logger.InfoMarkUp(ResString.matchDASH);
extractor = new DASHExtractor(parserConfig);
} }
else else
{ {
@ -61,9 +67,24 @@ namespace N_m3u8DL_RE.Parser
} }
} }
public Task<List<StreamSpec>> ExtractStreamsAsync() /// <summary>
/// 开始解析流媒体信息
/// </summary>
/// <returns></returns>
public async Task<List<StreamSpec>> ExtractStreamsAsync()
{ {
return extractor.ExtractStreamsAsync(rawText); Logger.Info(ResString.parsingStream);
return await extractor.ExtractStreamsAsync(rawText);
}
/// <summary>
/// 根据规格说明填充媒体播放列表信息
/// </summary>
/// <param name="streamSpecs"></param>
public async Task FetchPlayListAsync(List<StreamSpec> streamSpecs)
{
Logger.Info(ResString.parsingStream);
await extractor.FetchPlayListAsync(streamSpecs);
} }
} }
} }

View File

@ -14,6 +14,9 @@ namespace N_m3u8DL_RE.Parser.Util
{ {
public static List<StreamSpec> SelectStreams(IEnumerable<StreamSpec> lists) public static List<StreamSpec> SelectStreams(IEnumerable<StreamSpec> lists)
{ {
if (lists.Count() == 1)
return new List<StreamSpec>(lists);
//基本流 //基本流
var basicStreams = lists.Where(x => x.MediaType == null); var basicStreams = lists.Where(x => x.MediaType == null);
//可选音频轨道 //可选音频轨道

View File

@ -32,8 +32,8 @@ namespace N_m3u8DL_RE
//Logger.LogLevel = LogLevel.DEBUG; //Logger.LogLevel = LogLevel.DEBUG;
var config = new ParserConfig(); var config = new ParserConfig();
var url = string.Empty; var url = string.Empty;
//url = "http://playertest.longtailvideo.com/adaptive/oceans_aes/oceans_aes.m3u8"; url = "http://playertest.longtailvideo.com/adaptive/oceans_aes/oceans_aes.m3u8";
url = "https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_16x9/bipbop_16x9_variant.m3u8"; //url = "https://bitmovin-a.akamaihd.net/content/MI201109210084_1/mpds/f08e80da-bf1d-4e3d-8899-f0f6155f6efa.mpd";
if (string.IsNullOrEmpty(url)) if (string.IsNullOrEmpty(url))
{ {
@ -44,7 +44,14 @@ namespace N_m3u8DL_RE
var extractor = new StreamExtractor(config); var extractor = new StreamExtractor(config);
extractor.LoadSourceFromUrl(url); extractor.LoadSourceFromUrl(url);
//解析流信息
var streams = await extractor.ExtractStreamsAsync(); var streams = await extractor.ExtractStreamsAsync();
if (streams.Count == 0)
{
throw new Exception("解析失败");
}
//全部媒体 //全部媒体
var lists = streams.OrderByDescending(p => p.Bandwidth); var lists = streams.OrderByDescending(p => p.Bandwidth);
//基本流 //基本流
@ -59,33 +66,23 @@ namespace N_m3u8DL_RE
Logger.Info(ResString.streamsInfo, lists.Count(), basicStreams.Count(), audios.Count(), subs.Count()); Logger.Info(ResString.streamsInfo, lists.Count(), basicStreams.Count(), audios.Count(), subs.Count());
if (streams.Count > 1) foreach (var item in lists)
{ {
Logger.InfoMarkUp(item.ToString());
foreach (var item in lists) Logger.InfoMarkUp(item.ToString());
var selectedStreams = PromptUtil.SelectStreams(lists);
Logger.Info(ResString.selectedStream);
await File.WriteAllTextAsync("meta_selected.json", GlobalUtil.ConvertToJson(selectedStreams), Encoding.UTF8);
foreach (var item in selectedStreams)
{
Logger.InfoMarkUp(item.ToString());
}
} }
else if (streams.Count == 1)
//展示交互式选择框
var selectedStreams = PromptUtil.SelectStreams(lists);
//加载playlist
Logger.Info(ResString.selectedStream);
await extractor.FetchPlayListAsync(selectedStreams);
Logger.Warn(ResString.writeJson);
await File.WriteAllTextAsync("meta_selected.json", GlobalUtil.ConvertToJson(selectedStreams), Encoding.UTF8);
foreach (var item in selectedStreams)
{ {
var playlist = streams.First().Playlist; Logger.InfoMarkUp(item.ToString());
if (playlist.IsLive)
{
Logger.Warn(ResString.liveFound);
}
//Print(playlist);
}
else
{
throw new Exception("解析失败");
} }
} }
catch (Exception ex) catch (Exception ex)
{ {