初步支持`MSS`流
This commit is contained in:
parent
37bdd62c2e
commit
999f01e0a0
|
@ -1,5 +1,5 @@
|
||||||
# N_m3u8DL-RE
|
# N_m3u8DL-RE
|
||||||
跨平台的DASH/HLS下载工具。支持点播、直播。
|
跨平台的DASH/HLS/MSS下载工具。支持点播、直播(DASH/HLS)。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,25 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace N_m3u8DL_RE.Common.Entity
|
||||||
|
{
|
||||||
|
public class MSSData
|
||||||
|
{
|
||||||
|
public string FourCC { get; set; } = "";
|
||||||
|
public string CodecPrivateData { get; set; } = "";
|
||||||
|
public string Type { get; set; } = "";
|
||||||
|
public int Timesacle { get; set; }
|
||||||
|
public int SamplingRate { get; set; }
|
||||||
|
public int Channels { get; set; }
|
||||||
|
public int BitsPerSample { get; set; }
|
||||||
|
public int NalUnitLengthField { get; set; }
|
||||||
|
public long Duration { get; set; }
|
||||||
|
|
||||||
|
public bool IsProtection { get; set; } = false;
|
||||||
|
public string ProtectionSystemID { get; set; } = "";
|
||||||
|
public string ProtectionData { get; set; } = "";
|
||||||
|
}
|
||||||
|
}
|
|
@ -17,6 +17,9 @@ namespace N_m3u8DL_RE.Common.Entity
|
||||||
public string? Name { get; set; }
|
public string? Name { get; set; }
|
||||||
public Choise? Default { get; set; }
|
public Choise? Default { get; set; }
|
||||||
|
|
||||||
|
//MSS信息
|
||||||
|
public MSSData? MSSData { get; set; }
|
||||||
|
|
||||||
//基本信息
|
//基本信息
|
||||||
public int? Bandwidth { get; set; }
|
public int? Bandwidth { get; set; }
|
||||||
public string? Codecs { get; set; }
|
public string? Codecs { get; set; }
|
||||||
|
|
|
@ -15,6 +15,7 @@ namespace N_m3u8DL_RE.Common.Enum
|
||||||
SAMPLE_AES_CTR,
|
SAMPLE_AES_CTR,
|
||||||
CENC,
|
CENC,
|
||||||
CHACHA20,
|
CHACHA20,
|
||||||
|
PLAYREADY,
|
||||||
UNKNOWN
|
UNKNOWN
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -89,6 +89,7 @@ namespace N_m3u8DL_RE.Common.Resource
|
||||||
public static string loadingUrl { get => GetText("loadingUrl"); }
|
public static string loadingUrl { get => GetText("loadingUrl"); }
|
||||||
public static string masterM3u8Found { get => GetText("masterM3u8Found"); }
|
public static string masterM3u8Found { get => GetText("masterM3u8Found"); }
|
||||||
public static string matchDASH { get => GetText("matchDASH"); }
|
public static string matchDASH { get => GetText("matchDASH"); }
|
||||||
|
public static string matchMSS { get => GetText("matchMSS"); }
|
||||||
public static string matchTS { get => GetText("matchTS"); }
|
public static string matchTS { get => GetText("matchTS"); }
|
||||||
public static string matchHLS { get => GetText("matchHLS"); }
|
public static string matchHLS { get => GetText("matchHLS"); }
|
||||||
public static string notSupported { get => GetText("notSupported"); }
|
public static string notSupported { get => GetText("notSupported"); }
|
||||||
|
|
|
@ -613,6 +613,12 @@ namespace N_m3u8DL_RE.Common.Resource
|
||||||
zhTW: "內容匹配: [white on mediumorchid1]Dynamic Adaptive Streaming over HTTP[/]",
|
zhTW: "內容匹配: [white on mediumorchid1]Dynamic Adaptive Streaming over HTTP[/]",
|
||||||
enUS: "Content Matched: [white on mediumorchid1]Dynamic Adaptive Streaming over HTTP[/]"
|
enUS: "Content Matched: [white on mediumorchid1]Dynamic Adaptive Streaming over HTTP[/]"
|
||||||
),
|
),
|
||||||
|
["matchMSS"] = new TextContainer
|
||||||
|
(
|
||||||
|
zhCN: "内容匹配: [white on steelblue1]Microsoft Smooth Streaming[/]",
|
||||||
|
zhTW: "內容匹配: [white on steelblue1]Microsoft Smooth Streaming[/]",
|
||||||
|
enUS: "Content Matched: [white on steelblue1]Microsoft Smooth Streaming[/]"
|
||||||
|
),
|
||||||
["matchHLS"] = new TextContainer
|
["matchHLS"] = new TextContainer
|
||||||
(
|
(
|
||||||
zhCN: "内容匹配: [white on deepskyblue1]HTTP Live Streaming[/]",
|
zhCN: "内容匹配: [white on deepskyblue1]HTTP Live Streaming[/]",
|
||||||
|
|
|
@ -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 MSSTags
|
||||||
|
{
|
||||||
|
public static string Bitrate = "{Bitrate}";
|
||||||
|
public static string Bitrate_BK = "{bitrate}";
|
||||||
|
public static string StartTime = "{start_time}";
|
||||||
|
public static string StartTime_BK = "{start time}";
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,361 @@
|
||||||
|
using N_m3u8DL_RE.Common.Entity;
|
||||||
|
using N_m3u8DL_RE.Common.Enum;
|
||||||
|
using N_m3u8DL_RE.Common.Log;
|
||||||
|
using N_m3u8DL_RE.Common.Util;
|
||||||
|
using N_m3u8DL_RE.Parser.Config;
|
||||||
|
using N_m3u8DL_RE.Parser.Constants;
|
||||||
|
using N_m3u8DL_RE.Parser.Mp4;
|
||||||
|
using N_m3u8DL_RE.Parser.Util;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using System.Xml;
|
||||||
|
using System.Xml.Linq;
|
||||||
|
|
||||||
|
namespace N_m3u8DL_RE.Parser.Extractor
|
||||||
|
{
|
||||||
|
//Microsoft Smooth Streaming
|
||||||
|
//https://test.playready.microsoft.com/smoothstreaming/SSWSS720H264/SuperSpeedway_720.ism/manifest
|
||||||
|
//file:///C:/Users/nilaoda/Downloads/[MS-SSTR]-180316.pdf
|
||||||
|
internal partial class MSSExtractor : IExtractor
|
||||||
|
{
|
||||||
|
[GeneratedRegex("00000001\\d7([0-9a-fA-F]{6})")]
|
||||||
|
private static partial Regex VCodecsRegex();
|
||||||
|
|
||||||
|
////////////////////////////////////////
|
||||||
|
|
||||||
|
private static EncryptMethod DEFAULT_METHOD = EncryptMethod.PLAYREADY;
|
||||||
|
|
||||||
|
public ExtractorType ExtractorType => ExtractorType.MSS;
|
||||||
|
|
||||||
|
private string IsmUrl = string.Empty;
|
||||||
|
private string BaseUrl = string.Empty;
|
||||||
|
private string IsmContent = string.Empty;
|
||||||
|
public ParserConfig ParserConfig { get; set; }
|
||||||
|
|
||||||
|
public MSSExtractor(ParserConfig parserConfig)
|
||||||
|
{
|
||||||
|
this.ParserConfig = parserConfig;
|
||||||
|
SetInitUrl();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SetInitUrl()
|
||||||
|
{
|
||||||
|
this.IsmUrl = ParserConfig.Url ?? string.Empty;
|
||||||
|
if (!string.IsNullOrEmpty(ParserConfig.BaseUrl))
|
||||||
|
this.BaseUrl = ParserConfig.BaseUrl;
|
||||||
|
else
|
||||||
|
this.BaseUrl = this.IsmUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<StreamSpec>> ExtractStreamsAsync(string rawText)
|
||||||
|
{
|
||||||
|
var streamList = new List<StreamSpec>();
|
||||||
|
this.IsmContent = rawText;
|
||||||
|
this.PreProcessContent();
|
||||||
|
|
||||||
|
var xmlDocument = XDocument.Parse(IsmContent);
|
||||||
|
|
||||||
|
//选中第一个SmoothStreamingMedia节点
|
||||||
|
var ssmElement = xmlDocument.Elements().First(e => e.Name.LocalName == "SmoothStreamingMedia");
|
||||||
|
var timeScaleStr = ssmElement.Attribute("TimeScale")?.Value ?? "10000000";
|
||||||
|
var durationStr = ssmElement.Attribute("Duration")?.Value;
|
||||||
|
var isLiveStr = ssmElement.Attribute("IsLive")?.Value;
|
||||||
|
|
||||||
|
var isProtection = false;
|
||||||
|
var protectionSystemId = "";
|
||||||
|
var protectionData = "";
|
||||||
|
|
||||||
|
//加密检测
|
||||||
|
var protectElement = ssmElement.Elements().FirstOrDefault(e => e.Name.LocalName == "Protection");
|
||||||
|
if (protectElement != null)
|
||||||
|
{
|
||||||
|
var protectionHeader = protectElement.Element("ProtectionHeader");
|
||||||
|
if (protectionHeader != null)
|
||||||
|
{
|
||||||
|
isProtection = true;
|
||||||
|
protectionSystemId = protectionHeader.Attribute("SystemID")?.Value ?? "9A04F079-9840-4286-AB92-E65BE0885F95";
|
||||||
|
protectionData = HexUtil.BytesToHex(Convert.FromBase64String(protectionHeader.Value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//所有StreamIndex节点
|
||||||
|
var streamIndexElements = ssmElement.Elements().Where(e => e.Name.LocalName == "StreamIndex");
|
||||||
|
|
||||||
|
foreach (var streamIndex in streamIndexElements)
|
||||||
|
{
|
||||||
|
var type = streamIndex.Attribute("Type")?.Value; //"video" / "audio" / "text"
|
||||||
|
var name = streamIndex.Attribute("Name")?.Value;
|
||||||
|
var subType = streamIndex.Attribute("Subtype")?.Value; //text track
|
||||||
|
//如果有则不从QualityLevel读取
|
||||||
|
//Bitrate = "{bitrate}" / "{Bitrate}"
|
||||||
|
//StartTimeSubstitution = "{start time}" / "{start_time}"
|
||||||
|
var urlPattern = streamIndex.Attribute("Url")?.Value;
|
||||||
|
var language = streamIndex.Attribute("Language")?.Value;
|
||||||
|
|
||||||
|
//所有c节点
|
||||||
|
var cElements = streamIndex.Elements().Where(e => e.Name.LocalName == "c");
|
||||||
|
|
||||||
|
//所有QualityLevel节点
|
||||||
|
var qualityLevelElements = streamIndex.Elements().Where(e => e.Name.LocalName == "QualityLevel");
|
||||||
|
|
||||||
|
foreach (var qualityLevel in qualityLevelElements)
|
||||||
|
{
|
||||||
|
urlPattern = (qualityLevel.Attribute("Url")?.Value ?? urlPattern)!
|
||||||
|
.Replace(MSSTags.Bitrate_BK, MSSTags.Bitrate).Replace(MSSTags.StartTime_BK, MSSTags.StartTime);
|
||||||
|
var fourCC = qualityLevel.Attribute("FourCC")!.Value.ToUpper();
|
||||||
|
var samplingRateStr = qualityLevel.Attribute("SamplingRate")?.Value;
|
||||||
|
var bitsPerSampleStr = qualityLevel.Attribute("BitsPerSample")?.Value;
|
||||||
|
var nalUnitLengthFieldStr = qualityLevel.Attribute("NALUnitLengthField")?.Value;
|
||||||
|
var indexStr = qualityLevel.Attribute("Index")?.Value;
|
||||||
|
var codecPrivateData = qualityLevel.Attribute("CodecPrivateData")?.Value ?? "";
|
||||||
|
var audioTag = qualityLevel.Attribute("AudioTag")?.Value;
|
||||||
|
var bitrate = Convert.ToInt32(qualityLevel.Attribute("Bitrate")?.Value ?? "0");
|
||||||
|
var width = Convert.ToInt32(qualityLevel.Attribute("MaxWidth")?.Value ?? "0");
|
||||||
|
var height = Convert.ToInt32(qualityLevel.Attribute("MaxHeight")?.Value ?? "0");
|
||||||
|
var channels = qualityLevel.Attribute("Channels")?.Value;
|
||||||
|
|
||||||
|
StreamSpec streamSpec = new();
|
||||||
|
streamSpec.Extension = "m4s";
|
||||||
|
streamSpec.OriginalUrl = ParserConfig.OriginalUrl;
|
||||||
|
streamSpec.PeriodId = indexStr;
|
||||||
|
streamSpec.Playlist = new Playlist();
|
||||||
|
streamSpec.Playlist.MediaParts.Add(new MediaPart());
|
||||||
|
streamSpec.GroupId = name ?? indexStr;
|
||||||
|
streamSpec.Bandwidth = bitrate;
|
||||||
|
streamSpec.Codecs = ParseCodecs(fourCC, codecPrivateData);
|
||||||
|
streamSpec.Language = language;
|
||||||
|
streamSpec.Resolution = width == 0 ? null : $"{width}x{height}";
|
||||||
|
streamSpec.Url = IsmUrl;
|
||||||
|
streamSpec.Channels = channels;
|
||||||
|
streamSpec.MediaType = type switch
|
||||||
|
{
|
||||||
|
"text" => MediaType.SUBTITLES,
|
||||||
|
"audio" => MediaType.AUDIO,
|
||||||
|
_ => null
|
||||||
|
};
|
||||||
|
|
||||||
|
streamSpec.Playlist.MediaInit = new MediaSegment();
|
||||||
|
if (!string.IsNullOrEmpty(codecPrivateData))
|
||||||
|
{
|
||||||
|
streamSpec.Playlist.MediaInit.Url = $"hex://{codecPrivateData}";
|
||||||
|
}
|
||||||
|
|
||||||
|
var currentTime = 0L;
|
||||||
|
var segIndex = 0;
|
||||||
|
var varDic = new Dictionary<string, object?>();
|
||||||
|
varDic[MSSTags.Bitrate] = bitrate;
|
||||||
|
|
||||||
|
foreach (var c in cElements)
|
||||||
|
{
|
||||||
|
//每个C元素包含三个属性:@t(start time)\@r(repeat count)\@d(duration)
|
||||||
|
var _startTimeStr = c.Attribute("t")?.Value;
|
||||||
|
var _durationStr = c.Attribute("d")?.Value;
|
||||||
|
var _repeatCountStr = c.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[MSSTags.StartTime] = currentTime;
|
||||||
|
var oriUrl = ParserUtil.CombineURL(this.BaseUrl, urlPattern!);
|
||||||
|
var mediaUrl = ParserUtil.ReplaceVars(oriUrl, varDic);
|
||||||
|
MediaSegment mediaSegment = new();
|
||||||
|
mediaSegment.Url = mediaUrl;
|
||||||
|
if (oriUrl.Contains(MSSTags.StartTime))
|
||||||
|
mediaSegment.NameFromVar = currentTime.ToString();
|
||||||
|
mediaSegment.Duration = _duration / (double)timescale;
|
||||||
|
mediaSegment.Index = segIndex++;
|
||||||
|
streamSpec.Playlist.MediaParts[0].MediaSegments.Add(mediaSegment);
|
||||||
|
if (_repeatCount < 0)
|
||||||
|
{
|
||||||
|
//负数表示一直重复 直到period结束 注意减掉已经加入的1个片段
|
||||||
|
_repeatCount = (long)Math.Ceiling(Convert.ToInt64(durationStr) / (double)_duration) - 1;
|
||||||
|
}
|
||||||
|
for (long i = 0; i < _repeatCount; i++)
|
||||||
|
{
|
||||||
|
currentTime += _duration;
|
||||||
|
MediaSegment _mediaSegment = new();
|
||||||
|
varDic[MSSTags.StartTime] = currentTime;
|
||||||
|
var _oriUrl = ParserUtil.CombineURL(this.BaseUrl, urlPattern!);
|
||||||
|
var _mediaUrl = ParserUtil.ReplaceVars(_oriUrl, varDic);
|
||||||
|
_mediaSegment.Url = _mediaUrl;
|
||||||
|
_mediaSegment.Index = segIndex++;
|
||||||
|
_mediaSegment.Duration = _duration / (double)timescale;
|
||||||
|
if (_oriUrl.Contains(MSSTags.StartTime))
|
||||||
|
_mediaSegment.NameFromVar = currentTime.ToString();
|
||||||
|
streamSpec.Playlist.MediaParts[0].MediaSegments.Add(_mediaSegment);
|
||||||
|
}
|
||||||
|
currentTime += _duration;
|
||||||
|
}
|
||||||
|
|
||||||
|
//生成MOOV数据
|
||||||
|
if (MSSMoovProcessor.CanHandle(fourCC!))
|
||||||
|
{
|
||||||
|
streamSpec.MSSData = new MSSData()
|
||||||
|
{
|
||||||
|
FourCC = fourCC!,
|
||||||
|
CodecPrivateData = codecPrivateData,
|
||||||
|
Type = type!,
|
||||||
|
Timesacle = Convert.ToInt32(timeScaleStr),
|
||||||
|
Duration = Convert.ToInt64(durationStr),
|
||||||
|
SamplingRate = Convert.ToInt32(samplingRateStr ?? "48000"),
|
||||||
|
Channels = Convert.ToInt32(channels ?? "2"),
|
||||||
|
BitsPerSample = Convert.ToInt32(bitsPerSampleStr ?? "16"),
|
||||||
|
NalUnitLengthField = Convert.ToInt32(nalUnitLengthFieldStr ?? "4"),
|
||||||
|
IsProtection = isProtection,
|
||||||
|
ProtectionData = protectionData,
|
||||||
|
ProtectionSystemID = protectionSystemId,
|
||||||
|
};
|
||||||
|
var processor = new MSSMoovProcessor(streamSpec);
|
||||||
|
var header = processor.GenHeader(); //trackId可能不正确
|
||||||
|
streamSpec.Playlist!.MediaInit!.Url = $"base64://{Convert.ToBase64String(header)}";
|
||||||
|
//为音视频写入加密信息
|
||||||
|
if (isProtection && type != "text")
|
||||||
|
{
|
||||||
|
if (streamSpec.Playlist.MediaInit != null)
|
||||||
|
{
|
||||||
|
streamSpec.Playlist.MediaInit.EncryptInfo.Method = DEFAULT_METHOD;
|
||||||
|
}
|
||||||
|
foreach (var item in streamSpec.Playlist.MediaParts[0].MediaSegments)
|
||||||
|
{
|
||||||
|
item.EncryptInfo.Method = DEFAULT_METHOD;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
streamList.Add(streamSpec);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Logger.WarnMarkUp($"[green]{fourCC}[/] not supported! Skiped.");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//为视频设置默认轨道
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 解析编码
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="fourCC"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
private static string? ParseCodecs(string fourCC, string? privateData)
|
||||||
|
{
|
||||||
|
if (fourCC == "TTML") return "stpp";
|
||||||
|
if (string.IsNullOrEmpty(privateData)) return null;
|
||||||
|
|
||||||
|
return fourCC switch
|
||||||
|
{
|
||||||
|
//AVC视频
|
||||||
|
"H264" or "X264" or "DAVC" or "AVC1" => ParseAVCCodecs(privateData),
|
||||||
|
//AAC音频
|
||||||
|
"AAC" or "AACL" or "AACH" or "AACP" => ParseAACCodecs(fourCC, privateData),
|
||||||
|
//默认返回fourCC本身
|
||||||
|
_ => fourCC.ToLower()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ParseAVCCodecs(string privateData)
|
||||||
|
{
|
||||||
|
var result = VCodecsRegex().Match(privateData).Groups[1].Value;
|
||||||
|
return string.IsNullOrEmpty(result) ? "avc1.4D401E" : $"avc1.{result}";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ParseAACCodecs(string fourCC, string privateData)
|
||||||
|
{
|
||||||
|
var mpProfile = 2;
|
||||||
|
if (fourCC == "AACH")
|
||||||
|
{
|
||||||
|
mpProfile = 5; // High Efficiency AAC Profile
|
||||||
|
}
|
||||||
|
else if (!string.IsNullOrEmpty(privateData))
|
||||||
|
{
|
||||||
|
mpProfile = (Convert.ToByte(privateData[..2], 16) & 0xF8) >> 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $"mp4a.40.{mpProfile}";
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task FetchPlayListAsync(List<StreamSpec> streamSpecs)
|
||||||
|
{
|
||||||
|
//这里才调用URL预处理器,节省开销
|
||||||
|
await ProcessUrlAsync(streamSpecs);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ProcessUrlAsync(List<StreamSpec> streamSpecs)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < streamSpecs.Count; i++)
|
||||||
|
{
|
||||||
|
var playlist = streamSpecs[i].Playlist;
|
||||||
|
if (playlist != null)
|
||||||
|
{
|
||||||
|
if (playlist.MediaInit != null)
|
||||||
|
{
|
||||||
|
playlist.MediaInit!.Url = PreProcessUrl(playlist.MediaInit!.Url);
|
||||||
|
}
|
||||||
|
for (int ii = 0; ii < playlist!.MediaParts.Count; ii++)
|
||||||
|
{
|
||||||
|
var part = playlist.MediaParts[ii];
|
||||||
|
for (int iii = 0; iii < part.MediaSegments.Count; iii++)
|
||||||
|
{
|
||||||
|
part.MediaSegments[iii].Url = PreProcessUrl(part.MediaSegments[iii].Url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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, IsmContent, ParserConfig))
|
||||||
|
{
|
||||||
|
IsmContent = p.Process(IsmContent, ParserConfig);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task RefreshPlayListAsync(List<StreamSpec> streamSpecs)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,89 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Mp4SubtitleParser
|
||||||
|
{
|
||||||
|
//make BinaryWriter in Big Endian
|
||||||
|
class BinaryWriter2 : BinaryWriter
|
||||||
|
{
|
||||||
|
private static bool IsLittleEndian = BitConverter.IsLittleEndian;
|
||||||
|
public BinaryWriter2(System.IO.Stream stream) : base(stream) { }
|
||||||
|
|
||||||
|
|
||||||
|
public void WriteUInt(decimal n, int offset = 0)
|
||||||
|
{
|
||||||
|
var arr = BitConverter.GetBytes((uint)n);
|
||||||
|
if (IsLittleEndian)
|
||||||
|
Array.Reverse(arr);
|
||||||
|
if (offset != 0)
|
||||||
|
arr = arr[offset..];
|
||||||
|
BaseStream.Write(arr);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Write(string text)
|
||||||
|
{
|
||||||
|
BaseStream.Write(Encoding.ASCII.GetBytes(text));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void WriteInt(decimal n, int offset = 0)
|
||||||
|
{
|
||||||
|
var arr = BitConverter.GetBytes((int)n);
|
||||||
|
if (IsLittleEndian)
|
||||||
|
Array.Reverse(arr);
|
||||||
|
if (offset != 0)
|
||||||
|
arr = arr[offset..];
|
||||||
|
BaseStream.Write(arr);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void WriteULong(decimal n, int offset = 0)
|
||||||
|
{
|
||||||
|
var arr = BitConverter.GetBytes((ulong)n);
|
||||||
|
if (IsLittleEndian)
|
||||||
|
Array.Reverse(arr);
|
||||||
|
if (offset != 0)
|
||||||
|
arr = arr[offset..];
|
||||||
|
BaseStream.Write(arr);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void WriteUShort(decimal n, int padding = 0)
|
||||||
|
{
|
||||||
|
var arr = BitConverter.GetBytes((ushort)n);
|
||||||
|
if (IsLittleEndian)
|
||||||
|
Array.Reverse(arr);
|
||||||
|
while (padding > 0)
|
||||||
|
{
|
||||||
|
arr = arr.Concat(new byte[] { 0x00 }).ToArray();
|
||||||
|
padding--;
|
||||||
|
}
|
||||||
|
BaseStream.Write(arr);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void WriteShort(decimal n, int padding = 0)
|
||||||
|
{
|
||||||
|
var arr = BitConverter.GetBytes((short)n);
|
||||||
|
if (IsLittleEndian)
|
||||||
|
Array.Reverse(arr);
|
||||||
|
while (padding > 0)
|
||||||
|
{
|
||||||
|
arr = arr.Concat(new byte[] { 0x00 }).ToArray();
|
||||||
|
padding--;
|
||||||
|
}
|
||||||
|
BaseStream.Write(arr);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void WriteByte(byte n, int padding = 0)
|
||||||
|
{
|
||||||
|
var arr = new byte[] { n };
|
||||||
|
while (padding > 0)
|
||||||
|
{
|
||||||
|
arr = arr.Concat(new byte[] { 0x00 }).ToArray();
|
||||||
|
padding--;
|
||||||
|
}
|
||||||
|
BaseStream.Write(arr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -66,7 +66,7 @@ namespace Mp4SubtitleParser
|
||||||
return string.Format("{0:00}:{1:00}:{2:00}.{3:000}", ts.Hours, ts.Minutes, ts.Seconds, ts.Milliseconds);
|
return string.Format("{0:00}:{1:00}:{2:00}.{3:000}", ts.Hours, ts.Minutes, ts.Seconds, ts.Milliseconds);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!xmlSrc.Contains("<?xml") || !xmlSrc.Contains("<head>")) return xmlSrc;
|
if (!xmlSrc.Contains("<tt") || !xmlSrc.Contains("<head>")) return xmlSrc;
|
||||||
var xmlDoc = new XmlDocument();
|
var xmlDoc = new XmlDocument();
|
||||||
XmlNamespaceManager? nsMgr = null;
|
XmlNamespaceManager? nsMgr = null;
|
||||||
xmlDoc.LoadXml(xmlSrc);
|
xmlDoc.LoadXml(xmlSrc);
|
||||||
|
|
|
@ -0,0 +1,739 @@
|
||||||
|
using Mp4SubtitleParser;
|
||||||
|
using N_m3u8DL_RE.Common.Entity;
|
||||||
|
using N_m3u8DL_RE.Common.Util;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
|
//https://github.com/canalplus/rx-player/blob/48d1f845064cea5c5a3546d2c53b1855c2be149d/src/parsers/manifest/smooth/get_codecs.ts
|
||||||
|
//https://github.dev/Dash-Industry-Forum/dash.js/blob/2aad3e79079b4de0bcd961ce6b4957103d98a621/src/mss/MssFragmentMoovProcessor.js
|
||||||
|
//https://github.com/yt-dlp/yt-dlp/blob/3639df54c3298e35b5ae2a96a25bc4d3c38950d0/yt_dlp/downloader/ism.py
|
||||||
|
namespace N_m3u8DL_RE.Parser.Mp4
|
||||||
|
{
|
||||||
|
public partial class MSSMoovProcessor
|
||||||
|
{
|
||||||
|
[GeneratedRegex("\\<KID\\>(.*?)\\<")]
|
||||||
|
private static partial Regex KIDRegex();
|
||||||
|
|
||||||
|
private static string StartCode = "00000001";
|
||||||
|
private StreamSpec StreamSpec;
|
||||||
|
private int TrackId = 2;
|
||||||
|
private string FourCC;
|
||||||
|
private string CodecPrivateData;
|
||||||
|
private int Timesacle;
|
||||||
|
private long Duration;
|
||||||
|
private string Language { get => StreamSpec.Language ?? "und"; }
|
||||||
|
private int Width { get => int.Parse((StreamSpec.Resolution ?? "0x0").Split('x').First()); }
|
||||||
|
private int Height { get => int.Parse((StreamSpec.Resolution ?? "0x0").Split('x').Last()); }
|
||||||
|
private string StreamType;
|
||||||
|
private int Channels;
|
||||||
|
private int BitsPerSample;
|
||||||
|
private int SamplingRate;
|
||||||
|
private int NalUnitLengthField;
|
||||||
|
private long CreationTime = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
|
||||||
|
|
||||||
|
private bool IsProtection;
|
||||||
|
private string ProtectionSystemId;
|
||||||
|
private string ProtectionData;
|
||||||
|
private string ProtecitonKID;
|
||||||
|
private byte[] UnityMatrix
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
using var stream = new MemoryStream();
|
||||||
|
using var writer = new BinaryWriter2(stream);
|
||||||
|
writer.WriteInt(0x10000);
|
||||||
|
writer.WriteInt(0);
|
||||||
|
writer.WriteInt(0);
|
||||||
|
writer.WriteInt(0);
|
||||||
|
writer.WriteInt(0x10000);
|
||||||
|
writer.WriteInt(0);
|
||||||
|
writer.WriteInt(0);
|
||||||
|
writer.WriteInt(0);
|
||||||
|
writer.WriteInt(0x40000000);
|
||||||
|
return stream.ToArray();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private static byte TRACK_ENABLED = 0x1;
|
||||||
|
private static byte TRACK_IN_MOVIE = 0x2;
|
||||||
|
private static byte TRACK_IN_PREVIEW = 0x4;
|
||||||
|
private static byte SELF_CONTAINED = 0x1;
|
||||||
|
private static List<string> SupportedFourCC = new List<string>()
|
||||||
|
{
|
||||||
|
"HVC1","HEV1","AACL","AACH","EC-3","H264","AVC1","DAVC","AVC1","TTML"
|
||||||
|
};
|
||||||
|
|
||||||
|
public MSSMoovProcessor(StreamSpec streamSpec)
|
||||||
|
{
|
||||||
|
this.StreamSpec = streamSpec;
|
||||||
|
var data = streamSpec.MSSData!;
|
||||||
|
this.NalUnitLengthField = data.NalUnitLengthField;
|
||||||
|
this.CodecPrivateData = data.CodecPrivateData;
|
||||||
|
this.FourCC = data.FourCC;
|
||||||
|
this.Timesacle = data.Timesacle;
|
||||||
|
this.Duration = data.Duration;
|
||||||
|
this.StreamType = data.Type;
|
||||||
|
this.Channels = data.Channels;
|
||||||
|
this.SamplingRate = data.SamplingRate;
|
||||||
|
this.BitsPerSample = data.BitsPerSample;
|
||||||
|
this.IsProtection = data.IsProtection;
|
||||||
|
this.ProtectionData = data.ProtectionData;
|
||||||
|
this.ProtectionSystemId = data.ProtectionSystemID;
|
||||||
|
|
||||||
|
//需要手动生成CodecPrivateData
|
||||||
|
if (string.IsNullOrEmpty(CodecPrivateData))
|
||||||
|
{
|
||||||
|
GenCodecPrivateDataForAAC();
|
||||||
|
}
|
||||||
|
|
||||||
|
//解析KID
|
||||||
|
if (IsProtection)
|
||||||
|
{
|
||||||
|
ExtractKID();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private int SamplingFrequencyIndex(int samplingRate) => samplingRate switch
|
||||||
|
{
|
||||||
|
96000 => 0x0,
|
||||||
|
88200 => 0x1,
|
||||||
|
64000 => 0x2,
|
||||||
|
48000 => 0x3,
|
||||||
|
44100 => 0x4,
|
||||||
|
32000 => 0x5,
|
||||||
|
24000 => 0x6,
|
||||||
|
22050 => 0x7,
|
||||||
|
16000 => 0x8,
|
||||||
|
12000 => 0x9,
|
||||||
|
11025 => 0xA,
|
||||||
|
8000 => 0xB,
|
||||||
|
7350 => 0xC,
|
||||||
|
_ => 0x0
|
||||||
|
};
|
||||||
|
|
||||||
|
private void GenCodecPrivateDataForAAC()
|
||||||
|
{
|
||||||
|
var objectType = 0x02; //AAC Main Low Complexity => object Type = 2
|
||||||
|
var indexFreq = SamplingFrequencyIndex(SamplingRate);
|
||||||
|
|
||||||
|
if (FourCC == "AACH")
|
||||||
|
{
|
||||||
|
// 4 bytes : XXXXX XXXX XXXX XXXX XXXXX XXX XXXXXXX
|
||||||
|
// ' ObjectType' 'Freq Index' 'Channels value' 'Extens Sampl Freq' 'ObjectType' 'GAS' 'alignment = 0'
|
||||||
|
objectType = 0x05; // High Efficiency AAC Profile = object Type = 5 SBR
|
||||||
|
var codecPrivateData = new byte[4];
|
||||||
|
var extensionSamplingFrequencyIndex = SamplingFrequencyIndex(SamplingRate * 2); // in HE AAC Extension Sampling frequence
|
||||||
|
// equals to SamplingRate*2
|
||||||
|
//Freq Index is present for 3 bits in the first byte, last bit is in the second
|
||||||
|
codecPrivateData[0] = (byte)((objectType << 3) | (indexFreq >> 1));
|
||||||
|
codecPrivateData[1] = (byte)((indexFreq << 7) | (Channels << 3) | (extensionSamplingFrequencyIndex >> 1));
|
||||||
|
codecPrivateData[2] = (byte)((extensionSamplingFrequencyIndex << 7) | (0x02 << 2)); // origin object type equals to 2 => AAC Main Low Complexity
|
||||||
|
codecPrivateData[3] = 0x0; //alignment bits
|
||||||
|
|
||||||
|
var arr16 = new ushort[2];
|
||||||
|
arr16[0] = (ushort)((codecPrivateData[0] << 8) + codecPrivateData[1]);
|
||||||
|
arr16[1] = (ushort)((codecPrivateData[2] << 8) + codecPrivateData[3]);
|
||||||
|
|
||||||
|
//convert decimal to hex value
|
||||||
|
this.CodecPrivateData = HexUtil.BytesToHex(BitConverter.GetBytes(arr16[0])).PadLeft(16, '0');
|
||||||
|
this.CodecPrivateData += HexUtil.BytesToHex(BitConverter.GetBytes(arr16[1])).PadLeft(16, '0');
|
||||||
|
}
|
||||||
|
else if (FourCC.StartsWith("AAC"))
|
||||||
|
{
|
||||||
|
// 2 bytes : XXXXX XXXX XXXX XXX
|
||||||
|
// ' ObjectType' 'Freq Index' 'Channels value' 'GAS = 000'
|
||||||
|
var codecPrivateData = new byte[2];
|
||||||
|
//Freq Index is present for 3 bits in the first byte, last bit is in the second
|
||||||
|
codecPrivateData[0] = (byte)((objectType << 3) | (indexFreq >> 1));
|
||||||
|
codecPrivateData[1] = (byte)((indexFreq << 7) | Channels << 3);
|
||||||
|
// put the 2 bytes in an 16 bits array
|
||||||
|
var arr16 = new ushort[1];
|
||||||
|
arr16[0] = (ushort)((codecPrivateData[0] << 8) + codecPrivateData[1]);
|
||||||
|
|
||||||
|
//convert decimal to hex value
|
||||||
|
this.CodecPrivateData = HexUtil.BytesToHex(BitConverter.GetBytes(arr16[0])).PadLeft(16, '0');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ExtractKID()
|
||||||
|
{
|
||||||
|
//playready
|
||||||
|
if (ProtectionSystemId.ToUpper() == "9A04F079-9840-4286-AB92-E65BE0885F95")
|
||||||
|
{
|
||||||
|
var bytes = HexUtil.HexToBytes(ProtectionData.Replace("00", ""));
|
||||||
|
var text = Encoding.ASCII.GetString(bytes);
|
||||||
|
var kidBytes = Convert.FromBase64String(KIDRegex().Match(text).Groups[1].Value);
|
||||||
|
//fix byte order
|
||||||
|
var reverse1 = new byte[4] { kidBytes[3], kidBytes[2], kidBytes[1], kidBytes[0] };
|
||||||
|
var reverse2 = new byte[4] { kidBytes[5], kidBytes[4], kidBytes[7], kidBytes[6] };
|
||||||
|
Array.Copy(reverse1, 0, kidBytes, 0, reverse1.Length);
|
||||||
|
Array.Copy(reverse2, 0, kidBytes, 4, reverse1.Length);
|
||||||
|
this.ProtecitonKID = HexUtil.BytesToHex(kidBytes);
|
||||||
|
}
|
||||||
|
//widevine
|
||||||
|
else if (ProtectionSystemId.ToUpper() == "EDEF8BA9-79D6-4ACE-A3C8-27DCD51D21ED")
|
||||||
|
{
|
||||||
|
throw new NotSupportedException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool CanHandle(string fourCC) => SupportedFourCC.Contains(fourCC);
|
||||||
|
|
||||||
|
private byte[] Box(string boxType, byte[] payload)
|
||||||
|
{
|
||||||
|
using var stream = new MemoryStream();
|
||||||
|
using var writer = new BinaryWriter2(stream);
|
||||||
|
|
||||||
|
writer.WriteUInt(8 + (uint)payload.Length);
|
||||||
|
writer.Write(boxType);
|
||||||
|
writer.Write(payload);
|
||||||
|
|
||||||
|
return stream.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte[] FullBox(string boxType, byte version, uint flags, byte[] payload)
|
||||||
|
{
|
||||||
|
using var stream = new MemoryStream();
|
||||||
|
using var writer = new BinaryWriter2(stream);
|
||||||
|
|
||||||
|
writer.Write(version);
|
||||||
|
writer.WriteUInt(flags, offset: 1);
|
||||||
|
writer.Write(payload);
|
||||||
|
|
||||||
|
return Box(boxType, stream.ToArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte[] GenSinf(string codec)
|
||||||
|
{
|
||||||
|
var frmaBox = Box("frma", Encoding.ASCII.GetBytes(codec));
|
||||||
|
|
||||||
|
var sinfPayload = new List<byte>();
|
||||||
|
sinfPayload.AddRange(frmaBox);
|
||||||
|
|
||||||
|
var schmPayload = new List<byte>();
|
||||||
|
schmPayload.AddRange(Encoding.ASCII.GetBytes("cenc")); //scheme_type 'cenc' => common encryption
|
||||||
|
schmPayload.AddRange(new byte[] { 0, 1, 0, 0 }); //scheme_version Major version 1, Minor version 0
|
||||||
|
var schmBox = FullBox("schm", 0, 0, schmPayload.ToArray());
|
||||||
|
|
||||||
|
sinfPayload.AddRange(schmBox);
|
||||||
|
|
||||||
|
var tencPayload = new List<byte>();
|
||||||
|
tencPayload.AddRange(new byte[] { 0, 0 });
|
||||||
|
tencPayload.Add(0x1); //default_IsEncrypted
|
||||||
|
tencPayload.Add(0x8); //default_IV_size
|
||||||
|
tencPayload.AddRange(HexUtil.HexToBytes(ProtecitonKID)); //default_KID
|
||||||
|
var tencBox = FullBox("tenc", 0, 0, tencPayload.ToArray());
|
||||||
|
|
||||||
|
var schiBox = Box("schi", tencBox);
|
||||||
|
sinfPayload.AddRange(schiBox);
|
||||||
|
|
||||||
|
var sinfBox = Box("sinf", sinfPayload.ToArray());
|
||||||
|
|
||||||
|
return sinfBox;
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte[] GenFtyp()
|
||||||
|
{
|
||||||
|
using var stream = new MemoryStream();
|
||||||
|
using var writer = new BinaryWriter2(stream);
|
||||||
|
|
||||||
|
writer.Write("iso6"); //major brand
|
||||||
|
writer.WriteUInt(1); //minor version
|
||||||
|
writer.Write("isom"); //compatible brand
|
||||||
|
writer.Write("iso6"); //compatible brand
|
||||||
|
writer.Write("msdh"); //compatible brand
|
||||||
|
|
||||||
|
return Box("ftyp", stream.ToArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte[] GenMvhd()
|
||||||
|
{
|
||||||
|
using var stream = new MemoryStream();
|
||||||
|
using var writer = new BinaryWriter2(stream);
|
||||||
|
|
||||||
|
writer.WriteULong(CreationTime); //creation_time
|
||||||
|
writer.WriteULong(CreationTime); //modification_time
|
||||||
|
writer.WriteUInt(Timesacle); //timescale
|
||||||
|
writer.WriteULong(Duration); //duration
|
||||||
|
writer.WriteUShort(1, padding: 2); //rate
|
||||||
|
writer.WriteByte(1, padding: 1); //volume
|
||||||
|
writer.WriteUShort(0); //reserved
|
||||||
|
writer.WriteUInt(0);
|
||||||
|
writer.WriteUInt(0);
|
||||||
|
|
||||||
|
writer.Write(UnityMatrix);
|
||||||
|
|
||||||
|
writer.WriteUInt(0); //pre defined
|
||||||
|
writer.WriteUInt(0);
|
||||||
|
writer.WriteUInt(0);
|
||||||
|
writer.WriteUInt(0);
|
||||||
|
writer.WriteUInt(0);
|
||||||
|
writer.WriteUInt(0);
|
||||||
|
|
||||||
|
writer.WriteUInt(0xffffffff); //next track id
|
||||||
|
|
||||||
|
|
||||||
|
return FullBox("mvhd", 1, 0, stream.ToArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte[] GenTkhd()
|
||||||
|
{
|
||||||
|
using var stream = new MemoryStream();
|
||||||
|
using var writer = new BinaryWriter2(stream);
|
||||||
|
|
||||||
|
writer.WriteULong(CreationTime); //creation_time
|
||||||
|
writer.WriteULong(CreationTime); //modification_time
|
||||||
|
writer.WriteUInt(TrackId); //track id
|
||||||
|
writer.WriteUInt(0); //reserved
|
||||||
|
writer.WriteULong(Duration); //duration
|
||||||
|
writer.WriteUInt(0); //reserved
|
||||||
|
writer.WriteUInt(0);
|
||||||
|
writer.WriteShort(0); //layer
|
||||||
|
writer.WriteShort(0); //alternate group
|
||||||
|
writer.WriteByte(StreamType == "audio" ? (byte)1 : (byte)0, padding: 1); //volume
|
||||||
|
writer.WriteUShort(0); //reserved
|
||||||
|
|
||||||
|
writer.Write(UnityMatrix);
|
||||||
|
|
||||||
|
writer.WriteUShort(Width, padding: 2); //width
|
||||||
|
writer.WriteUShort(Height, padding: 2); //height
|
||||||
|
|
||||||
|
return FullBox("tkhd", 1, (uint)TRACK_ENABLED | TRACK_IN_MOVIE | TRACK_IN_PREVIEW, stream.ToArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private byte[] GenMdhd()
|
||||||
|
{
|
||||||
|
using var stream = new MemoryStream();
|
||||||
|
using var writer = new BinaryWriter2(stream);
|
||||||
|
|
||||||
|
writer.WriteULong(CreationTime); //creation_time
|
||||||
|
writer.WriteULong(CreationTime); //modification_time
|
||||||
|
writer.WriteUInt(Timesacle); //timescale
|
||||||
|
writer.WriteULong(Duration); //duration
|
||||||
|
writer.WriteUShort((Language[0] - 0x60) << 10 | (Language[1] - 0x60) << 5 | (Language[2] - 0x60)); //language
|
||||||
|
writer.WriteUShort(0); //pre defined
|
||||||
|
|
||||||
|
return FullBox("mdhd", 1, 0, stream.ToArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte[] GenHdlr()
|
||||||
|
{
|
||||||
|
using var stream = new MemoryStream();
|
||||||
|
using var writer = new BinaryWriter2(stream);
|
||||||
|
|
||||||
|
writer.WriteUInt(0); //pre defined
|
||||||
|
if (StreamType == "audio") writer.Write("soun");
|
||||||
|
else if (StreamType == "video") writer.Write("vide");
|
||||||
|
else if (StreamType == "text") writer.Write("subt");
|
||||||
|
else throw new NotSupportedException();
|
||||||
|
|
||||||
|
writer.WriteUInt(0); //reserved
|
||||||
|
writer.WriteUInt(0);
|
||||||
|
writer.WriteUInt(0);
|
||||||
|
writer.Write($"{StreamSpec.GroupId ?? "RE Handler"}\0"); //name
|
||||||
|
|
||||||
|
return FullBox("hdlr", 0, 0, stream.ToArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte[] GenMinf()
|
||||||
|
{
|
||||||
|
using var stream = new MemoryStream();
|
||||||
|
using var writer = new BinaryWriter2(stream);
|
||||||
|
|
||||||
|
var minfPayload = new List<byte>();
|
||||||
|
if (StreamType == "audio")
|
||||||
|
{
|
||||||
|
var smhd = new List<byte>();
|
||||||
|
smhd.Add(0); smhd.Add(0); //balance
|
||||||
|
smhd.Add(0); smhd.Add(0); //reserved
|
||||||
|
|
||||||
|
minfPayload.AddRange(FullBox("smhd", 0, 0, smhd.ToArray())); //Sound Media Header
|
||||||
|
}
|
||||||
|
else if (StreamType == "video")
|
||||||
|
{
|
||||||
|
var vmhd = new List<byte>();
|
||||||
|
vmhd.Add(0); vmhd.Add(0); //graphics mode
|
||||||
|
vmhd.Add(0); vmhd.Add(0); vmhd.Add(0); vmhd.Add(0); vmhd.Add(0); vmhd.Add(0);//opcolor
|
||||||
|
|
||||||
|
minfPayload.AddRange(FullBox("vmhd", 0, 1, vmhd.ToArray())); //Video Media Header
|
||||||
|
}
|
||||||
|
else if (StreamType == "text")
|
||||||
|
{
|
||||||
|
minfPayload.AddRange(FullBox("sthd", 0, 0, new byte[0])); //Subtitle Media Header
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new NotSupportedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
var drefPayload = new List<byte>();
|
||||||
|
drefPayload.Add(0); drefPayload.Add(0); drefPayload.Add(0); drefPayload.Add(1); //entry count
|
||||||
|
drefPayload.AddRange(FullBox("url ", 0, SELF_CONTAINED, new byte[0])); //Data Entry URL Box
|
||||||
|
|
||||||
|
var dinfPayload = FullBox("dref", 0, 0, drefPayload.ToArray()); //Data Reference Box
|
||||||
|
minfPayload.AddRange(Box("dinf", dinfPayload.ToArray())); //Data Information Box
|
||||||
|
|
||||||
|
return minfPayload.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte[] GenEsds(byte[] audioSpecificConfig)
|
||||||
|
{
|
||||||
|
using var stream = new MemoryStream();
|
||||||
|
using var writer = new BinaryWriter2(stream);
|
||||||
|
|
||||||
|
// ESDS length = esds box header length (= 12) +
|
||||||
|
// ES_Descriptor header length (= 5) +
|
||||||
|
// DecoderConfigDescriptor header length (= 15) +
|
||||||
|
// decoderSpecificInfo header length (= 2) +
|
||||||
|
// AudioSpecificConfig length (= codecPrivateData length)
|
||||||
|
// esdsLength = 34 + len(audioSpecificConfig)
|
||||||
|
|
||||||
|
// ES_Descriptor (see ISO/IEC 14496-1 (Systems))
|
||||||
|
writer.WriteByte(0x03); //tag = 0x03 (ES_DescrTag)
|
||||||
|
writer.WriteByte((byte)(20 + audioSpecificConfig.Length)); //size
|
||||||
|
writer.WriteByte((byte)((TrackId & 0xFF00) >> 8)); //ES_ID = track_id
|
||||||
|
writer.WriteByte((byte)(TrackId & 0x00FF));
|
||||||
|
writer.WriteByte(0); //flags and streamPriority
|
||||||
|
|
||||||
|
// DecoderConfigDescriptor (see ISO/IEC 14496-1 (Systems))
|
||||||
|
writer.WriteByte(0x04); //tag = 0x04 (DecoderConfigDescrTag)
|
||||||
|
writer.WriteByte((byte)(15 + audioSpecificConfig.Length)); //size
|
||||||
|
writer.WriteByte(0x40); //objectTypeIndication = 0x40 (MPEG-4 AAC)
|
||||||
|
writer.WriteByte((0x05 << 2) | (0 << 1) | 1); //reserved = 1
|
||||||
|
writer.WriteByte(0xFF); //buffersizeDB = undefined
|
||||||
|
writer.WriteByte(0xFF);
|
||||||
|
writer.WriteByte(0xFF);
|
||||||
|
|
||||||
|
var bandwidth = StreamSpec.Bandwidth!;
|
||||||
|
writer.WriteByte((byte)((bandwidth & 0xFF000000) >> 24)); //maxBitrate
|
||||||
|
writer.WriteByte((byte)((bandwidth & 0x00FF0000) >> 16));
|
||||||
|
writer.WriteByte((byte)((bandwidth & 0x0000FF00) >> 8));
|
||||||
|
writer.WriteByte((byte)(bandwidth & 0x000000FF));
|
||||||
|
writer.WriteByte((byte)((bandwidth & 0xFF000000) >> 24)); //avgbitrate
|
||||||
|
writer.WriteByte((byte)((bandwidth & 0x00FF0000) >> 16));
|
||||||
|
writer.WriteByte((byte)((bandwidth & 0x0000FF00) >> 8));
|
||||||
|
writer.WriteByte((byte)(bandwidth & 0x000000FF));
|
||||||
|
|
||||||
|
// DecoderSpecificInfo (see ISO/IEC 14496-1 (Systems))
|
||||||
|
writer.WriteByte(0x05); //tag = 0x05 (DecSpecificInfoTag)
|
||||||
|
writer.WriteByte((byte)audioSpecificConfig.Length); //size
|
||||||
|
writer.Write(audioSpecificConfig); //AudioSpecificConfig bytes
|
||||||
|
|
||||||
|
return FullBox("esds", 0, 0, stream.ToArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte[] GetSampleEntryBox()
|
||||||
|
{
|
||||||
|
using var stream = new MemoryStream();
|
||||||
|
using var writer = new BinaryWriter2(stream);
|
||||||
|
|
||||||
|
writer.WriteByte(0); //reserved
|
||||||
|
writer.WriteByte(0);
|
||||||
|
writer.WriteByte(0);
|
||||||
|
writer.WriteByte(0);
|
||||||
|
writer.WriteByte(0);
|
||||||
|
writer.WriteByte(0);
|
||||||
|
writer.WriteUShort(1); //data reference index
|
||||||
|
|
||||||
|
if (StreamType == "audio")
|
||||||
|
{
|
||||||
|
writer.WriteUInt(0); //reserved2
|
||||||
|
writer.WriteUInt(0);
|
||||||
|
writer.WriteUShort(Channels); //channels
|
||||||
|
writer.WriteUShort(BitsPerSample); //bits_per_sample
|
||||||
|
writer.WriteUShort(0); //pre defined
|
||||||
|
writer.WriteUShort(0); //reserved3
|
||||||
|
writer.WriteUShort(SamplingRate, padding: 2); //sampling_rate
|
||||||
|
|
||||||
|
var audioSpecificConfig = HexUtil.HexToBytes(CodecPrivateData);
|
||||||
|
var esdsBox = GenEsds(audioSpecificConfig);
|
||||||
|
writer.Write(esdsBox);
|
||||||
|
|
||||||
|
if (FourCC.StartsWith("AAC"))
|
||||||
|
{
|
||||||
|
if (IsProtection)
|
||||||
|
{
|
||||||
|
var sinfBox = GenSinf("mp4a");
|
||||||
|
writer.Write(sinfBox);
|
||||||
|
return Box("enca", stream.ToArray()); //Encrypted Audio
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return Box("mp4a", stream.ToArray());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (FourCC == "EC-3")
|
||||||
|
{
|
||||||
|
if (IsProtection)
|
||||||
|
{
|
||||||
|
var sinfBox = GenSinf("ec-3");
|
||||||
|
writer.Write(sinfBox);
|
||||||
|
return Box("enca", stream.ToArray()); //Encrypted Audio
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return Box("ec-3", stream.ToArray());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (StreamType == "video")
|
||||||
|
{
|
||||||
|
writer.WriteUShort(0); //pre defined
|
||||||
|
writer.WriteUShort(0); //reserved
|
||||||
|
writer.WriteUInt(0); //pre defined
|
||||||
|
writer.WriteUInt(0);
|
||||||
|
writer.WriteUInt(0);
|
||||||
|
writer.WriteUShort(Width); //width
|
||||||
|
writer.WriteUShort(Height); //height
|
||||||
|
writer.WriteUShort(0x48, padding: 2); //horiz resolution 72 dpi
|
||||||
|
writer.WriteUShort(0x48, padding: 2); //vert resolution 72 dpi
|
||||||
|
writer.WriteUInt(0); //reserved
|
||||||
|
writer.WriteUShort(1); //frame count
|
||||||
|
for (int i = 0; i < 32; i++) //compressor name
|
||||||
|
{
|
||||||
|
writer.WriteByte(0);
|
||||||
|
}
|
||||||
|
writer.WriteUShort(0x18); //depth
|
||||||
|
writer.WriteShort(-1); //pre defined
|
||||||
|
|
||||||
|
var codecPrivateData = HexUtil.HexToBytes(CodecPrivateData);
|
||||||
|
|
||||||
|
if (FourCC == "H264" || FourCC == "AVC1" || FourCC == "DAVC" || FourCC == "AVC1")
|
||||||
|
{
|
||||||
|
var arr = CodecPrivateData.Split(new[] { StartCode }, StringSplitOptions.RemoveEmptyEntries);
|
||||||
|
var sps = HexUtil.HexToBytes(arr[0]);
|
||||||
|
var pps = HexUtil.HexToBytes(arr[1]);
|
||||||
|
//make avcC
|
||||||
|
var avcC = GetAvcC(sps, pps);
|
||||||
|
writer.Write(avcC);
|
||||||
|
if (IsProtection)
|
||||||
|
{
|
||||||
|
var sinfBox = GenSinf("avc1");
|
||||||
|
writer.Write(sinfBox);
|
||||||
|
return Box("encv", stream.ToArray()); //Encrypted Video
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return Box("avc1", stream.ToArray()); //AVC Simple Entry
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (FourCC == "HVC1" || FourCC == "HEV1")
|
||||||
|
{
|
||||||
|
var arr = CodecPrivateData.Split(new[] { StartCode }, StringSplitOptions.RemoveEmptyEntries);
|
||||||
|
var sps = HexUtil.HexToBytes(arr[0]);
|
||||||
|
var pps = HexUtil.HexToBytes(arr[1]);
|
||||||
|
var vps = arr.Length > 2 ? HexUtil.HexToBytes(arr[2]) : null;
|
||||||
|
//make hvcC
|
||||||
|
var hvcC = GetHvcC(sps, pps, vps);
|
||||||
|
writer.Write(hvcC);
|
||||||
|
if (IsProtection)
|
||||||
|
{
|
||||||
|
var sinfBox = GenSinf("hvc1");
|
||||||
|
writer.Write(sinfBox);
|
||||||
|
return Box("encv", stream.ToArray()); //Encrypted Video
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return Box("hvc1", stream.ToArray()); //HEVC Simple Entry
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new NotSupportedException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (StreamType == "text")
|
||||||
|
{
|
||||||
|
if (FourCC == "TTML")
|
||||||
|
{
|
||||||
|
writer.Write("http://www.w3.org/ns/ttml\0"); //namespace
|
||||||
|
writer.Write("\0"); //schema location
|
||||||
|
writer.Write("\0"); //auxilary mime types(??)
|
||||||
|
return Box("stpp", stream.ToArray()); //TTML Simple Entry
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new NotSupportedException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new NotSupportedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new NotSupportedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte[] GetAvcC(byte[] sps, byte[] pps)
|
||||||
|
{
|
||||||
|
using var stream = new MemoryStream();
|
||||||
|
using var writer = new BinaryWriter2(stream);
|
||||||
|
|
||||||
|
writer.WriteByte(1); //configuration version
|
||||||
|
writer.Write(sps[1..4]); //avc profile indication + profile compatibility + avc level indication
|
||||||
|
writer.WriteByte((byte)(0xfc | (NalUnitLengthField - 1))); //complete representation (1) + reserved (11111) + length size minus one
|
||||||
|
writer.WriteByte(1); //reserved (0) + number of sps (0000001)
|
||||||
|
writer.WriteUShort(sps.Length);
|
||||||
|
writer.Write(sps);
|
||||||
|
writer.WriteByte(1); //number of pps
|
||||||
|
writer.WriteUShort(pps.Length);
|
||||||
|
writer.Write(pps);
|
||||||
|
|
||||||
|
return Box("avcC", stream.ToArray()); //AVC Decoder Configuration Record
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte[] GetHvcC(byte[] sps, byte[] pps, byte[]? vps)
|
||||||
|
{
|
||||||
|
using var stream = new MemoryStream();
|
||||||
|
using var writer = new BinaryWriter2(stream);
|
||||||
|
|
||||||
|
var generalProfileCompatibilityFlags = 0xffffffff;
|
||||||
|
var generalConstraintIndicatorFlags = 0xffffffffffff;
|
||||||
|
writer.WriteByte(1); //configuration version
|
||||||
|
writer.WriteByte(0 << 6 | 0 << 5 | 0); //general_profile_space + general_tier_flag + general_profile_idc
|
||||||
|
writer.WriteUInt(generalProfileCompatibilityFlags); //general_profile_compatibility_flags
|
||||||
|
writer.WriteUInt(generalConstraintIndicatorFlags >> 16); //general_constraint_indicator_flags
|
||||||
|
writer.WriteUShort(ushort.MaxValue);
|
||||||
|
writer.WriteByte(0); //general_level_idc
|
||||||
|
writer.WriteUShort(0xf000); //reserved + min_spatial_segmentation_idc
|
||||||
|
writer.WriteByte(0xfc); //reserved + parallelismType
|
||||||
|
writer.WriteByte(0 | 0xfc); //reserved + chromaFormat
|
||||||
|
writer.WriteByte(0 | 0xf8); //reserved + bitDepthLumaMinus8
|
||||||
|
writer.WriteByte(0 | 0xf8); //reserved + bitDepthChromaMinus8
|
||||||
|
writer.WriteUShort(0); //avgFrameRate
|
||||||
|
writer.WriteByte((byte)(0 << 6 | 0 << 3 | 0 << 2 | (NalUnitLengthField - 1))); //constantFrameRate + numTemporalLayers + temporalIdNested + lengthSizeMinusOne
|
||||||
|
writer.WriteByte((byte)(vps != null ? 0x03 : 0x02)); //numOfArrays (vps sps pps)
|
||||||
|
|
||||||
|
if (vps != null)
|
||||||
|
{
|
||||||
|
writer.WriteByte(32); //array_completeness + reserved + NAL_unit_type
|
||||||
|
writer.WriteUShort(1); //numNalus
|
||||||
|
writer.WriteUShort(vps.Length);
|
||||||
|
writer.Write(vps);
|
||||||
|
}
|
||||||
|
writer.WriteByte(33);
|
||||||
|
writer.WriteByte(1); //numNalus
|
||||||
|
writer.WriteUShort(sps.Length);
|
||||||
|
writer.Write(sps);
|
||||||
|
writer.WriteByte(34);
|
||||||
|
writer.WriteByte(1); //numNalus
|
||||||
|
writer.WriteUShort(pps.Length);
|
||||||
|
writer.Write(pps);
|
||||||
|
|
||||||
|
return Box("hvcC", stream.ToArray()); //HEVC Decoder Configuration Record
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte[] GetStsd()
|
||||||
|
{
|
||||||
|
using var stream = new MemoryStream();
|
||||||
|
using var writer = new BinaryWriter2(stream);
|
||||||
|
|
||||||
|
writer.WriteUInt(1); //entry count
|
||||||
|
var sampleEntryData = GetSampleEntryBox();
|
||||||
|
writer.Write(sampleEntryData);
|
||||||
|
|
||||||
|
return stream.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte[] GetMehd()
|
||||||
|
{
|
||||||
|
using var stream = new MemoryStream();
|
||||||
|
using var writer = new BinaryWriter2(stream);
|
||||||
|
|
||||||
|
writer.WriteULong(Duration);
|
||||||
|
|
||||||
|
return FullBox("mehd", 1, 0, stream.ToArray()); //Movie Extends Header Box
|
||||||
|
}
|
||||||
|
private byte[] GetTrex()
|
||||||
|
{
|
||||||
|
using var stream = new MemoryStream();
|
||||||
|
using var writer = new BinaryWriter2(stream);
|
||||||
|
|
||||||
|
writer.WriteUInt(TrackId); //track id
|
||||||
|
writer.WriteUInt(1); //default sample description index
|
||||||
|
writer.WriteUInt(0); //default sample duration
|
||||||
|
writer.WriteUInt(0); //default sample size
|
||||||
|
writer.WriteUInt(0); //default sample flags
|
||||||
|
|
||||||
|
return FullBox("trex", 0, 0, stream.ToArray()); //Track Extends Box
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] GenHeader(byte[] firstSegment)
|
||||||
|
{
|
||||||
|
new MP4Parser()
|
||||||
|
.Box("moof", MP4Parser.Children)
|
||||||
|
.Box("traf", MP4Parser.Children)
|
||||||
|
.FullBox("tfhd", (box) =>
|
||||||
|
{
|
||||||
|
TrackId = (int)box.Reader.ReadUInt32();
|
||||||
|
})
|
||||||
|
.Parse(firstSegment);
|
||||||
|
|
||||||
|
return GenHeader();
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] GenHeader()
|
||||||
|
{
|
||||||
|
using var stream = new MemoryStream();
|
||||||
|
|
||||||
|
var ftyp = GenFtyp(); // File Type Box
|
||||||
|
stream.Write(ftyp);
|
||||||
|
|
||||||
|
var moovPayload = GenMvhd(); // Movie Header Box
|
||||||
|
|
||||||
|
var trakPayload = GenTkhd(); // Track Header Box
|
||||||
|
|
||||||
|
var mdhdPayload = GenMdhd(); // Media Header Box
|
||||||
|
|
||||||
|
var hdlrPayload = GenHdlr(); // Handler Reference Box
|
||||||
|
|
||||||
|
var mdiaPayload = mdhdPayload.Concat(hdlrPayload).ToArray();
|
||||||
|
|
||||||
|
var minfPayload = GenMinf();
|
||||||
|
|
||||||
|
|
||||||
|
var sttsPayload = new byte[] { 0, 0, 0, 0 }; //entry count
|
||||||
|
var stblPayload = FullBox("stts", 0, 0, sttsPayload); //Decoding Time to Sample Box
|
||||||
|
|
||||||
|
var stscPayload = new byte[] { 0, 0, 0, 0 }; //entry count
|
||||||
|
var stscBox = FullBox("stsc", 0, 0, stscPayload); //Sample To Chunk Box
|
||||||
|
|
||||||
|
var stcoPayload = new byte[] { 0, 0, 0, 0 }; //entry count
|
||||||
|
var stcoBox = FullBox("stco", 0, 0, stcoPayload); //Chunk Offset Box
|
||||||
|
|
||||||
|
var stszPayload = new byte[] { 0, 0, 0, 0, 0, 0, 0, 0 }; //sample size, sample count
|
||||||
|
var stszBox = FullBox("stsz", 0, 0, stszPayload); //Sample Size Box
|
||||||
|
|
||||||
|
var stsdPayload = GetStsd();
|
||||||
|
var stsdBox = FullBox("stsd", 0, 0, stsdPayload); //Sample Description Box
|
||||||
|
|
||||||
|
stblPayload = stblPayload.Concat(stscBox).Concat(stcoBox).Concat(stszBox).Concat(stsdBox).ToArray();
|
||||||
|
|
||||||
|
|
||||||
|
var stblBox = Box("stbl", stblPayload); //Sample Table Box
|
||||||
|
minfPayload = minfPayload.Concat(stblBox).ToArray();
|
||||||
|
|
||||||
|
var minfBox = Box("minf", minfPayload); //Media Information Box
|
||||||
|
mdiaPayload = mdiaPayload.Concat(minfBox).ToArray();
|
||||||
|
|
||||||
|
var mdiaBox = Box("mdia", mdiaPayload); //Media Box
|
||||||
|
trakPayload = trakPayload.Concat(mdiaBox).ToArray();
|
||||||
|
|
||||||
|
var trakBox = Box("trak", trakPayload); //Track Box
|
||||||
|
moovPayload = moovPayload.Concat(trakBox).ToArray();
|
||||||
|
|
||||||
|
var mvexPayload = GetMehd();
|
||||||
|
var trexBox = GetTrex();
|
||||||
|
mvexPayload = mvexPayload.Concat(trexBox).ToArray();
|
||||||
|
|
||||||
|
var mvexBox = Box("mvex", mvexPayload); //Movie Extends Box
|
||||||
|
moovPayload = moovPayload.Concat(mvexBox).ToArray();
|
||||||
|
|
||||||
|
var moovBox = Box("moov", moovPayload); //Movie Box
|
||||||
|
|
||||||
|
stream.Write(moovBox);
|
||||||
|
|
||||||
|
return stream.ToArray();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -16,7 +16,7 @@ namespace N_m3u8DL_RE.Parser.Processor
|
||||||
|
|
||||||
public override string Process(string oriUrl, ParserConfig paserConfig)
|
public override string Process(string oriUrl, ParserConfig paserConfig)
|
||||||
{
|
{
|
||||||
if (paserConfig.AppendUrlParams)
|
if (paserConfig.AppendUrlParams && oriUrl.StartsWith("http"))
|
||||||
{
|
{
|
||||||
var uriFromConfig = new Uri(paserConfig.Url);
|
var uriFromConfig = new Uri(paserConfig.Url);
|
||||||
var oldUri = new Uri(oriUrl);
|
var oldUri = new Uri(oriUrl);
|
||||||
|
|
|
@ -68,6 +68,12 @@ namespace N_m3u8DL_RE.Parser
|
||||||
//extractor = new DASHExtractor(parserConfig);
|
//extractor = new DASHExtractor(parserConfig);
|
||||||
extractor = new DASHExtractor2(parserConfig);
|
extractor = new DASHExtractor2(parserConfig);
|
||||||
}
|
}
|
||||||
|
else if (rawText.Contains("</SmoothStreamingMedia>") && rawText.Contains("<SmoothStreamingMedia"))
|
||||||
|
{
|
||||||
|
Logger.InfoMarkUp(ResString.matchMSS);
|
||||||
|
//extractor = new DASHExtractor(parserConfig);
|
||||||
|
extractor = new MSSExtractor(parserConfig);
|
||||||
|
}
|
||||||
else if (rawText == ResString.ReLiveTs)
|
else if (rawText == ResString.ReLiveTs)
|
||||||
{
|
{
|
||||||
Logger.InfoMarkUp(ResString.matchTS);
|
Logger.InfoMarkUp(ResString.matchTS);
|
||||||
|
|
|
@ -18,7 +18,7 @@ namespace N_m3u8DL_RE.CommandLine
|
||||||
{
|
{
|
||||||
internal partial class CommandInvoker
|
internal partial class CommandInvoker
|
||||||
{
|
{
|
||||||
public const string VERSION_INFO = "N_m3u8DL-RE (Beta version) 20221120";
|
public const string VERSION_INFO = "N_m3u8DL-RE (Beta version) 20221127";
|
||||||
|
|
||||||
[GeneratedRegex("((best|worst)\\d*|all)")]
|
[GeneratedRegex("((best|worst)\\d*|all)")]
|
||||||
private static partial Regex ForStrRegex();
|
private static partial Regex ForStrRegex();
|
||||||
|
|
|
@ -7,6 +7,8 @@ using N_m3u8DL_RE.Common.Resource;
|
||||||
using N_m3u8DL_RE.Config;
|
using N_m3u8DL_RE.Config;
|
||||||
using N_m3u8DL_RE.Downloader;
|
using N_m3u8DL_RE.Downloader;
|
||||||
using N_m3u8DL_RE.Entity;
|
using N_m3u8DL_RE.Entity;
|
||||||
|
using N_m3u8DL_RE.Parser;
|
||||||
|
using N_m3u8DL_RE.Parser.Mp4;
|
||||||
using N_m3u8DL_RE.Util;
|
using N_m3u8DL_RE.Util;
|
||||||
using Spectre.Console;
|
using Spectre.Console;
|
||||||
using Spectre.Console.Rendering;
|
using Spectre.Console.Rendering;
|
||||||
|
@ -20,12 +22,16 @@ namespace N_m3u8DL_RE.DownloadManager
|
||||||
{
|
{
|
||||||
IDownloader Downloader;
|
IDownloader Downloader;
|
||||||
DownloaderConfig DownloaderConfig;
|
DownloaderConfig DownloaderConfig;
|
||||||
|
StreamExtractor StreamExtractor;
|
||||||
|
List<StreamSpec> SelectedSteams;
|
||||||
DateTime NowDateTime;
|
DateTime NowDateTime;
|
||||||
List<OutputFile> OutputFiles = new();
|
List<OutputFile> OutputFiles = new();
|
||||||
|
|
||||||
public SimpleDownloadManager(DownloaderConfig downloaderConfig)
|
public SimpleDownloadManager(DownloaderConfig downloaderConfig, List<StreamSpec> selectedSteams, StreamExtractor streamExtractor)
|
||||||
{
|
{
|
||||||
this.DownloaderConfig = downloaderConfig;
|
this.DownloaderConfig = downloaderConfig;
|
||||||
|
this.SelectedSteams = selectedSteams;
|
||||||
|
this.StreamExtractor = streamExtractor;
|
||||||
Downloader = new SimpleDownloader(DownloaderConfig);
|
Downloader = new SimpleDownloader(DownloaderConfig);
|
||||||
NowDateTime = DateTime.Now;
|
NowDateTime = DateTime.Now;
|
||||||
}
|
}
|
||||||
|
@ -197,7 +203,7 @@ namespace N_m3u8DL_RE.DownloadManager
|
||||||
var pad = "0".PadLeft(segments.Count().ToString().Length, '0');
|
var pad = "0".PadLeft(segments.Count().ToString().Length, '0');
|
||||||
|
|
||||||
//下载第一个分片
|
//下载第一个分片
|
||||||
if (!readInfo)
|
if (!readInfo || StreamExtractor.ExtractorType == ExtractorType.MSS)
|
||||||
{
|
{
|
||||||
var seg = segments.First();
|
var seg = segments.First();
|
||||||
segments = segments.Skip(1);
|
segments = segments.Skip(1);
|
||||||
|
@ -213,6 +219,13 @@ namespace N_m3u8DL_RE.DownloadManager
|
||||||
task.Increment(1);
|
task.Increment(1);
|
||||||
if (result != null && result.Success)
|
if (result != null && result.Success)
|
||||||
{
|
{
|
||||||
|
//修复MSS init
|
||||||
|
if (StreamExtractor.ExtractorType == ExtractorType.MSS)
|
||||||
|
{
|
||||||
|
var processor = new MSSMoovProcessor(streamSpec);
|
||||||
|
var header = processor.GenHeader(File.ReadAllBytes(result.ActualFilePath));
|
||||||
|
await File.WriteAllBytesAsync(FileDic[streamSpec.Playlist!.MediaInit!]!.ActualFilePath, header);
|
||||||
|
}
|
||||||
//读取init信息
|
//读取init信息
|
||||||
if (string.IsNullOrEmpty(currentKID))
|
if (string.IsNullOrEmpty(currentKID))
|
||||||
{
|
{
|
||||||
|
@ -232,6 +245,8 @@ namespace N_m3u8DL_RE.DownloadManager
|
||||||
result.ActualFilePath = dec;
|
result.ActualFilePath = dec;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (!readInfo)
|
||||||
|
{
|
||||||
//ffmpeg读取信息
|
//ffmpeg读取信息
|
||||||
Logger.WarnMarkUp(ResString.readingInfo);
|
Logger.WarnMarkUp(ResString.readingInfo);
|
||||||
mediaInfos = await MediainfoUtil.ReadInfoAsync(DownloaderConfig.MyOptions.FFmpegBinaryPath!, result!.ActualFilePath);
|
mediaInfos = await MediainfoUtil.ReadInfoAsync(DownloaderConfig.MyOptions.FFmpegBinaryPath!, result!.ActualFilePath);
|
||||||
|
@ -240,6 +255,7 @@ namespace N_m3u8DL_RE.DownloadManager
|
||||||
readInfo = true;
|
readInfo = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
//开始下载
|
//开始下载
|
||||||
var options = new ParallelOptions()
|
var options = new ParallelOptions()
|
||||||
|
@ -456,7 +472,8 @@ namespace N_m3u8DL_RE.DownloadManager
|
||||||
//var iniFileBytes = File.ReadAllBytes(initFile!.ActualFilePath);
|
//var iniFileBytes = File.ReadAllBytes(initFile!.ActualFilePath);
|
||||||
//var sawTtml = MP4TtmlUtil.CheckInit(iniFileBytes);
|
//var sawTtml = MP4TtmlUtil.CheckInit(iniFileBytes);
|
||||||
var mp4s = FileDic.OrderBy(s => s.Key.Index).Select(s => s.Value).Select(v => v!.ActualFilePath).Where(p => p.EndsWith(".m4s")).ToArray();
|
var mp4s = FileDic.OrderBy(s => s.Key.Index).Select(s => s.Value).Select(v => v!.ActualFilePath).Where(p => p.EndsWith(".m4s")).ToArray();
|
||||||
var finalVtt = MP4TtmlUtil.ExtractFromMp4s(mp4s, 0);
|
var segmentDurMs = FileDic.Where(s => s.Value.ActualFilePath.EndsWith(".m4s")).First().Key.Duration * 1000;
|
||||||
|
var finalVtt = MP4TtmlUtil.ExtractFromMp4s(mp4s, (long)segmentDurMs);
|
||||||
//写出字幕
|
//写出字幕
|
||||||
var firstKey = FileDic.Keys.First();
|
var firstKey = FileDic.Keys.First();
|
||||||
var files = FileDic.OrderBy(s => s.Key.Index).Select(s => s.Value).Select(v => v!.ActualFilePath).ToArray();
|
var files = FileDic.OrderBy(s => s.Key.Index).Select(s => s.Value).Select(v => v!.ActualFilePath).ToArray();
|
||||||
|
@ -585,7 +602,7 @@ namespace N_m3u8DL_RE.DownloadManager
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<bool> StartDownloadAsync(IEnumerable<StreamSpec> streamSpecs)
|
public async Task<bool> StartDownloadAsync()
|
||||||
{
|
{
|
||||||
ConcurrentDictionary<int, SpeedContainer> SpeedContainerDic = new(); //速度计算
|
ConcurrentDictionary<int, SpeedContainer> SpeedContainerDic = new(); //速度计算
|
||||||
ConcurrentDictionary<StreamSpec, bool?> Results = new();
|
ConcurrentDictionary<StreamSpec, bool?> Results = new();
|
||||||
|
@ -610,7 +627,7 @@ namespace N_m3u8DL_RE.DownloadManager
|
||||||
await progress.StartAsync(async ctx =>
|
await progress.StartAsync(async ctx =>
|
||||||
{
|
{
|
||||||
//创建任务
|
//创建任务
|
||||||
var dic = streamSpecs.Select(item =>
|
var dic = SelectedSteams.Select(item =>
|
||||||
{
|
{
|
||||||
var task = ctx.AddTask(item.ToShortString(), autoStart: false);
|
var task = ctx.AddTask(item.ToShortString(), autoStart: false);
|
||||||
SpeedContainerDic[task.Id] = new SpeedContainer(); //速度计算
|
SpeedContainerDic[task.Id] = new SpeedContainer(); //速度计算
|
||||||
|
|
|
@ -165,46 +165,8 @@ namespace N_m3u8DL_RE
|
||||||
//for www.nowehoryzonty.pl
|
//for www.nowehoryzonty.pl
|
||||||
parserConfig.UrlProcessors.Insert(0, new NowehoryzontyUrlProcessor());
|
parserConfig.UrlProcessors.Insert(0, new NowehoryzontyUrlProcessor());
|
||||||
|
|
||||||
var url = string.Empty;
|
|
||||||
//url = "https://vod-ftc-eu-west-1.media.dssott.com/ps01/disney/29a73209-b706-4f21-8384-acddccb154d2/ctr-all-6cf6fec6-94dc-4f8b-ae67-5ada60ee1e83-42bd5bca-f9a8-4299-83e3-0fb2b4ec0a62.m3u8"; //迪士尼
|
|
||||||
//url = "https://play.itunes.apple.com/WebObjects/MZPlay.woa/hls/subscription/playlist.m3u8?cc=US&svcId=tvs.vds.4105&a=1580273278&isExternal=true&brandId=tvs.sbd.4000&id=337246031&l=en-US&aec=UHD&xtrick=true&webbrowser=true"; //啥都有
|
|
||||||
//url = "https://media.axprod.net/TestVectors/v7-Clear/Manifest_1080p.mpd"; //多音轨多字幕
|
|
||||||
//url = "https://cmafref.akamaized.net/cmaf/live-ull/2006350/akambr/out.mpd"; //直播
|
|
||||||
//url = "http://playertest.longtailvideo.com/adaptive/oceans_aes/oceans_aes.m3u8";
|
|
||||||
//url = "https://vod.sdn.wavve.com/hls/S01/S01_E461382925.1/1/5000/chunklist.m3u8";
|
|
||||||
url = "https://bitmovin-a.akamaihd.net/content/art-of-motion_drm/mpds/11331.mpd";
|
|
||||||
//url = "http://tv-live.ynkmit.com/tv/anning.m3u8?txSecret=7528f35fb4b62bd24d55b891899db68f&txTime=632C8680"; //直播
|
|
||||||
//url = "https://rest-as.ott.kaltura.com/api_v3/service/assetFile/action/playManifest/partnerId/147/assetId/1304099/assetType/media/assetFileId/16136929/contextType/PLAYBACK/isAltUrl/False/ks/djJ8MTQ3fMusTFH6PCZpcrfKLQwI-pPm9ex6b6r49wioe32WH2udXeM4reyWIkSDpi7HhvhxBHAHAKiHrcnkmIJQpyAt4MuDBG0ywGQ-jOeqQFcTRQ8BGJGw6g-smSBLwSbo4CCx9M9vWNJX3GkOfhoMAY4yRU-ur3okHiVq1mUJ82XBd_iVqLuzodnc9sJEtcHH0zc5CoPiTq2xor-dq3yDURnZm3isfSN3t9uLIJEW09oE-SJ84DM5GUuFUdbnIV8bdcWUsPicUg-Top1G2D3WcWXq4EvPnwvD8jrC_vsiOpLHf5akAwtdGsJ6__cXUmT7a-QlfjdvaZ5T8UhDLnttHmsxYs2E5c0lh4uOvvJou8dD8iYxUexlPI2j4QUkBRxqOEVLSNV3Y82-5TTRqgnK_uGYXHwk7EAmDws7hbLj2-DJ1heXDcye3OJYdunJgAS-9ma5zmQQNiY_HYh6wj2N1HpCTNAtWWga6R9fC0VgBTZbidW-YwMSGzIvMQfIfWKe15X7Oc_hCs-zGfW9XeRJZrutcWKK_D_HlzpQVBF2vIF3XgaI/a.mpd";
|
|
||||||
//url = "https://dash.akamaized.net/dash264/TestCases/2c/qualcomm/1/MultiResMPEG2.mpd";
|
|
||||||
url = "https://cmaf.lln.latam.hbomaxcdn.com/videos/GYPGKMQjoDkVLBQEAAAAo/1/1b5ad5/1_single_J8sExA_1080hi.mpd";
|
|
||||||
//url = "https://livesim.dashif.org/dash/vod/testpic_2s/multi_subs.mpd"; //ttml + mp4
|
|
||||||
//url = "http://media.axprod.net/TestVectors/v6-Clear/Manifest_1080p.mpd"; //vtt + mp4
|
|
||||||
//url = "https://livesim.dashif.org/dash/vod/testpic_2s/xml_subs.mpd"; //ttml
|
|
||||||
//url = "https://storage.googleapis.com/shaka-demo-assets/angel-one-hls/hls.m3u8"; //HLS vtt
|
|
||||||
//url = "https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_adv_example_hevc/master.m3u8"; //高级HLS fMP4+VTT
|
|
||||||
//url = "https://events-delivery.apple.com/0205eyyhwbbqexozkwmgccegwnjyrktg/m3u8/vod_index-dpyfrsVksFWjneFiptbXnAMYBtGYbXeZ.m3u8"; //高级HLS fMP4+VTT
|
|
||||||
//url = "https://apionvod5.seezntv.com/ktmain1/cold/CP/55521/202207/media/MIAM61RPSGL150000100_DRM/MIAM61RPSGL150000100_H.m3u8?sid=0000000F50000040000A700000020000";
|
|
||||||
//url = "https://ewcdn12.nowe.com/session/16-5-72579e3-2103014898783810281/Content/DASH_VOS3/VOD/6908/19585/d2afa5fe-e9c8-40f0-8d18-648aaaf292b6/f677841a-9d8f-2ff5-3517-674ba49ef192/manifest.mpd?token=894db5d69931835f82dd8e393974ef9f_1658146180";
|
|
||||||
//url = "https://ols-ww100-cp.akamaized.net/manifest/master/06ee6f68-ee80-11ea-9bc5-02b68fb543c4/65794a72596d6c30496a6f7a4e6a67324e4441774d444173496e42735958526d62334a74496a6f695a47567a6133527663434973496d526c646d6c6a5a565235634755694f694a335a5749694c434a746232526c62434936496e6470626d527664334d694c434a7663315235634755694f694a6a61484a76625755694c434a7663794936496a45774d6934774c6a41694c434a68634841694f69497a4c6a416966513d3d/dash.mpd?cpatoken=exp=1658223027~acl=/manifest/master/06ee6f68-ee80-11ea-9bc5-02b68fb543c4/*~hmac=644c608aac361f688e9b24b0f345c801d0f2d335819431d1873ff7aeac46d6b2&access_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJkZXZpY2VfaWQiOm51bGwsIndhdGNoX3R5cGUiOiJQUkVNSVVNIiwicHJvZ3JhbV9pZCI6ImUwMWRmYjAyLTM1YmItMTFlOS1hNDI3LTA2YTA0MTdjMWQxZSIsImFkX3RhZyI6ZmFsc2UsInBhcmVudF9wcm9ncmFtX2lkIjoiZmJmMDc2MDYtMzNmYi0xMWU5LWE0MjctMDZhMDQxN2MxZDFlIiwiY2xpZW50X2lkIjoiNGQ3MDViZTQtYTQ5ZS0xMWVhLWJiMzctMDI0MmFjMTMwMDAyIiwidmlkZW9fdHlwZSI6InZvZCIsImdyYW50X3R5cGUiOiJwbGF5X3ZpZGVvIiwidXNlcl9pZCI6ImFhNTMxZWQ2LWM2NTMtNDliYS04NGI1LWFkZDRmNGIzNGMyNyIsImN1cnJlbnRfc2Vjb25kIjowLCJyZXBvcnRfaWQiOiJOU1RHIiwic2NvcGUiOlsicHVibGljOi4qIiwibWU6LioiXSwiZXhwIjoxNjU4Mzk1ODI2LCJkZXRlY3Rpb25faWQiOm51bGwsInZpZGVvX2lkIjoiODc0Yjk0ZDItNzZiYi00YzliLTgzODQtNzJlMTA0NWVjOGMxIiwiaXNzIjoiQXNpYXBsYXktT0F1dGgtU2VydmVyIiwiaWF0IjoxNjU4MTM2NjI2LCJ0ZXJyaXRvcnkiOiJUVyJ9.1juciYIyMNzykXKu-nGLR_cYWvPMEAE9ub-ny7RzFnM";
|
|
||||||
//url = "https://a38avoddashs3ww-a.akamaihd.net/ondemand/iad_2/8e91/f2f2/ec5a/430f-bd7a-0779f4a0189d/685cda75-609c-41c1-86bb-688f4cdb5521_corrected.mpd";
|
|
||||||
//url = "https://dcs-vod.mp.lura.live/vod/p/session/manifest.mpd?i=i177610817-nb45239a2-e962-4137-bc70-1790359619e6";
|
|
||||||
//url = "https://theater.kktv.com.tw/98/04000198010001_584b26392f7f7f11fc62299214a55fb7/16113081449d8d5e9960_sub_dash.mpd"; //MPD+VTT
|
|
||||||
//url = "https://vsl.play.kakao.com/vod/rvty90n7btua6u9oebr97i8zl/dash/vhs/cenc/adaptive.mpd?e=1658297362&p=71&h=53766bdde112d59da2b2514e8ab41e81"; //需要补params
|
|
||||||
//url = "https://a38avoddashs3ww-a.akamaihd.net/ondemand/iad_2/8e91/f2f2/ec5a/430f-bd7a-0779f4a0189d/685cda75-609c-41c1-86bb-688f4cdb5521_corrected.mpd";
|
|
||||||
//url = "http://ht.grelighting.cn/m3u8/OEtYNVNjMFF5N1g2VzNkZ2lwbWEvd1ZtTGJ0dlZXOEk=.m3u8"; //特殊的图片伪装
|
|
||||||
//url = "https://ali6.a.yximgs.com/udata/music/music_295e59bdb3084def8158873ad6f5c8250.jpg"; //PNG图片伪装
|
|
||||||
//url = "https://vod.cds.nowonline.com.br/Content/dsc/VOD/movie/df/83/4dd69316-2022-45c0-a4f7-c35cad87df83/manifest.mpd"; //TTML+PNG
|
|
||||||
//url = "";
|
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(option.Input))
|
var url = option.Input;
|
||||||
{
|
|
||||||
url = option.Input;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(url))
|
|
||||||
{
|
|
||||||
url = AnsiConsole.Ask<string>("Input [green]URL[/]: ");
|
|
||||||
}
|
|
||||||
|
|
||||||
//流提取器配置
|
//流提取器配置
|
||||||
var extractor = new StreamExtractor(parserConfig);
|
var extractor = new StreamExtractor(parserConfig);
|
||||||
|
@ -268,8 +230,8 @@ namespace N_m3u8DL_RE
|
||||||
throw new Exception(ResString.noStreamsToDownload);
|
throw new Exception(ResString.noStreamsToDownload);
|
||||||
|
|
||||||
//HLS: 选中流中若有没加载出playlist的,加载playlist
|
//HLS: 选中流中若有没加载出playlist的,加载playlist
|
||||||
//DASH: 加载playlist (调用url预处理器)
|
//DASH/MSS: 加载playlist (调用url预处理器)
|
||||||
if (selectedStreams.Any(s => s.Playlist == null) || extractor.ExtractorType == ExtractorType.MPEG_DASH)
|
if (selectedStreams.Any(s => s.Playlist == null) || extractor.ExtractorType == ExtractorType.MPEG_DASH || extractor.ExtractorType == ExtractorType.MSS)
|
||||||
await extractor.FetchPlayListAsync(selectedStreams);
|
await extractor.FetchPlayListAsync(selectedStreams);
|
||||||
|
|
||||||
//直播检测
|
//直播检测
|
||||||
|
@ -332,8 +294,8 @@ namespace N_m3u8DL_RE
|
||||||
else if(!livingFlag)
|
else if(!livingFlag)
|
||||||
{
|
{
|
||||||
//开始下载
|
//开始下载
|
||||||
var sdm = new SimpleDownloadManager(downloadConfig);
|
var sdm = new SimpleDownloadManager(downloadConfig, selectedStreams, extractor);
|
||||||
result = await sdm.StartDownloadAsync(selectedStreams);
|
result = await sdm.StartDownloadAsync();
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
using N_m3u8DL_RE.Common.Resource;
|
using N_m3u8DL_RE.Common.Resource;
|
||||||
using N_m3u8DL_RE.Common.Util;
|
using N_m3u8DL_RE.Common.Util;
|
||||||
using N_m3u8DL_RE.Entity;
|
using N_m3u8DL_RE.Entity;
|
||||||
|
using System.IO;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Net.Http.Headers;
|
using System.Net.Http.Headers;
|
||||||
|
|
||||||
|
@ -44,6 +45,26 @@ namespace N_m3u8DL_RE.Util
|
||||||
var file = new Uri(url).LocalPath;
|
var file = new Uri(url).LocalPath;
|
||||||
return await CopyFileAsync(file, path, speedContainer, fromPosition, toPosition);
|
return await CopyFileAsync(file, path, speedContainer, fromPosition, toPosition);
|
||||||
}
|
}
|
||||||
|
if (url.StartsWith("base64://"))
|
||||||
|
{
|
||||||
|
var bytes = Convert.FromBase64String(url[9..]);
|
||||||
|
await File.WriteAllBytesAsync(path, bytes);
|
||||||
|
return new DownloadResult()
|
||||||
|
{
|
||||||
|
ActualContentLength = bytes.Length,
|
||||||
|
ActualFilePath = path,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (url.StartsWith("hex://"))
|
||||||
|
{
|
||||||
|
var bytes = HexUtil.HexToBytes(url[6..]);
|
||||||
|
await File.WriteAllBytesAsync(path, bytes);
|
||||||
|
return new DownloadResult()
|
||||||
|
{
|
||||||
|
ActualContentLength = bytes.Length,
|
||||||
|
ActualFilePath = path,
|
||||||
|
};
|
||||||
|
}
|
||||||
using var request = new HttpRequestMessage(HttpMethod.Get, new Uri(url));
|
using var request = new HttpRequestMessage(HttpMethod.Get, new Uri(url));
|
||||||
if (fromPosition != null || toPosition != null)
|
if (fromPosition != null || toPosition != null)
|
||||||
request.Headers.Range = new(fromPosition, toPosition);
|
request.Headers.Range = new(fromPosition, toPosition);
|
||||||
|
|
Loading…
Reference in New Issue