From 999f01e0a0cc191b7916fdbaf70a0e422779feb1 Mon Sep 17 00:00:00 2001 From: nilaoda Date: Sun, 27 Nov 2022 04:02:53 +0800 Subject: [PATCH] =?UTF-8?q?=E5=88=9D=E6=AD=A5=E6=94=AF=E6=8C=81`MSS`?= =?UTF-8?q?=E6=B5=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- src/N_m3u8DL-RE.Common/Entity/MSSData.cs | 25 + src/N_m3u8DL-RE.Common/Entity/StreamSpec.cs | 3 + src/N_m3u8DL-RE.Common/Enum/EncryptMethod.cs | 1 + src/N_m3u8DL-RE.Common/Resource/ResString.cs | 1 + src/N_m3u8DL-RE.Common/Resource/StaticText.cs | 6 + src/N_m3u8DL-RE.Parser/Constants/MSSTags.cs | 16 + .../Extractor/MSSExtractor.cs | 361 +++++++++ src/N_m3u8DL-RE.Parser/Mp4/BinaryWriter2.cs | 89 +++ src/N_m3u8DL-RE.Parser/Mp4/MP4TtmlUtil.cs | 2 +- .../Mp4/MSSMoovProcessor.cs | 739 ++++++++++++++++++ .../Processor/DefaultUrlProcessor.cs | 2 +- src/N_m3u8DL-RE.Parser/StreamExtractor.cs | 6 + src/N_m3u8DL-RE/CommandLine/CommandInvoker.cs | 2 +- .../DownloadManager/SimpleDownloadManager.cs | 39 +- src/N_m3u8DL-RE/Program.cs | 48 +- src/N_m3u8DL-RE/Util/DownloadUtil.cs | 21 + 17 files changed, 1305 insertions(+), 58 deletions(-) create mode 100644 src/N_m3u8DL-RE.Common/Entity/MSSData.cs create mode 100644 src/N_m3u8DL-RE.Parser/Constants/MSSTags.cs create mode 100644 src/N_m3u8DL-RE.Parser/Extractor/MSSExtractor.cs create mode 100644 src/N_m3u8DL-RE.Parser/Mp4/BinaryWriter2.cs create mode 100644 src/N_m3u8DL-RE.Parser/Mp4/MSSMoovProcessor.cs diff --git a/README.md b/README.md index 3e51cc2..3613239 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # N_m3u8DL-RE -跨平台的DASH/HLS下载工具。支持点播、直播。 +跨平台的DASH/HLS/MSS下载工具。支持点播、直播(DASH/HLS)。 --- diff --git a/src/N_m3u8DL-RE.Common/Entity/MSSData.cs b/src/N_m3u8DL-RE.Common/Entity/MSSData.cs new file mode 100644 index 0000000..96a0ae2 --- /dev/null +++ b/src/N_m3u8DL-RE.Common/Entity/MSSData.cs @@ -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; } = ""; + } +} diff --git a/src/N_m3u8DL-RE.Common/Entity/StreamSpec.cs b/src/N_m3u8DL-RE.Common/Entity/StreamSpec.cs index 4c03f2c..2842403 100644 --- a/src/N_m3u8DL-RE.Common/Entity/StreamSpec.cs +++ b/src/N_m3u8DL-RE.Common/Entity/StreamSpec.cs @@ -17,6 +17,9 @@ namespace N_m3u8DL_RE.Common.Entity public string? Name { get; set; } public Choise? Default { get; set; } + //MSS信息 + public MSSData? MSSData { get; set; } + //基本信息 public int? Bandwidth { get; set; } public string? Codecs { get; set; } diff --git a/src/N_m3u8DL-RE.Common/Enum/EncryptMethod.cs b/src/N_m3u8DL-RE.Common/Enum/EncryptMethod.cs index 319a466..5316c1e 100644 --- a/src/N_m3u8DL-RE.Common/Enum/EncryptMethod.cs +++ b/src/N_m3u8DL-RE.Common/Enum/EncryptMethod.cs @@ -15,6 +15,7 @@ namespace N_m3u8DL_RE.Common.Enum SAMPLE_AES_CTR, CENC, CHACHA20, + PLAYREADY, UNKNOWN } } diff --git a/src/N_m3u8DL-RE.Common/Resource/ResString.cs b/src/N_m3u8DL-RE.Common/Resource/ResString.cs index 4ee53fc..f53a07b 100644 --- a/src/N_m3u8DL-RE.Common/Resource/ResString.cs +++ b/src/N_m3u8DL-RE.Common/Resource/ResString.cs @@ -89,6 +89,7 @@ namespace N_m3u8DL_RE.Common.Resource public static string loadingUrl { get => GetText("loadingUrl"); } public static string masterM3u8Found { get => GetText("masterM3u8Found"); } public static string matchDASH { get => GetText("matchDASH"); } + public static string matchMSS { get => GetText("matchMSS"); } public static string matchTS { get => GetText("matchTS"); } public static string matchHLS { get => GetText("matchHLS"); } public static string notSupported { get => GetText("notSupported"); } diff --git a/src/N_m3u8DL-RE.Common/Resource/StaticText.cs b/src/N_m3u8DL-RE.Common/Resource/StaticText.cs index 89ccafc..d6cc83f 100644 --- a/src/N_m3u8DL-RE.Common/Resource/StaticText.cs +++ b/src/N_m3u8DL-RE.Common/Resource/StaticText.cs @@ -613,6 +613,12 @@ namespace N_m3u8DL_RE.Common.Resource zhTW: "內容匹配: [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 ( zhCN: "内容匹配: [white on deepskyblue1]HTTP Live Streaming[/]", diff --git a/src/N_m3u8DL-RE.Parser/Constants/MSSTags.cs b/src/N_m3u8DL-RE.Parser/Constants/MSSTags.cs new file mode 100644 index 0000000..bf7e32c --- /dev/null +++ b/src/N_m3u8DL-RE.Parser/Constants/MSSTags.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace N_m3u8DL_RE.Parser.Constants +{ + internal class 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}"; + } +} diff --git a/src/N_m3u8DL-RE.Parser/Extractor/MSSExtractor.cs b/src/N_m3u8DL-RE.Parser/Extractor/MSSExtractor.cs new file mode 100644 index 0000000..fb157dc --- /dev/null +++ b/src/N_m3u8DL-RE.Parser/Extractor/MSSExtractor.cs @@ -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> ExtractStreamsAsync(string rawText) + { + var streamList = new List(); + 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(); + 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; + } + + /// + /// 解析编码 + /// + /// + /// + 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 streamSpecs) + { + //这里才调用URL预处理器,节省开销 + await ProcessUrlAsync(streamSpecs); + } + + private async Task ProcessUrlAsync(List 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 streamSpecs) + { + throw new NotImplementedException(); + } + } +} diff --git a/src/N_m3u8DL-RE.Parser/Mp4/BinaryWriter2.cs b/src/N_m3u8DL-RE.Parser/Mp4/BinaryWriter2.cs new file mode 100644 index 0000000..3ddd3ea --- /dev/null +++ b/src/N_m3u8DL-RE.Parser/Mp4/BinaryWriter2.cs @@ -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); + } + } +} diff --git a/src/N_m3u8DL-RE.Parser/Mp4/MP4TtmlUtil.cs b/src/N_m3u8DL-RE.Parser/Mp4/MP4TtmlUtil.cs index c2da58b..7c1757f 100644 --- a/src/N_m3u8DL-RE.Parser/Mp4/MP4TtmlUtil.cs +++ b/src/N_m3u8DL-RE.Parser/Mp4/MP4TtmlUtil.cs @@ -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); } - if (!xmlSrc.Contains("")) return xmlSrc; + if (!xmlSrc.Contains("")) return xmlSrc; var xmlDoc = new XmlDocument(); XmlNamespaceManager? nsMgr = null; xmlDoc.LoadXml(xmlSrc); diff --git a/src/N_m3u8DL-RE.Parser/Mp4/MSSMoovProcessor.cs b/src/N_m3u8DL-RE.Parser/Mp4/MSSMoovProcessor.cs new file mode 100644 index 0000000..231febf --- /dev/null +++ b/src/N_m3u8DL-RE.Parser/Mp4/MSSMoovProcessor.cs @@ -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("\\(.*?)\\<")] + 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 SupportedFourCC = new List() + { + "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(); + sinfPayload.AddRange(frmaBox); + + var schmPayload = new List(); + 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(); + 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(); + if (StreamType == "audio") + { + var smhd = new List(); + 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(); + 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(); + 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(); + } + } +} diff --git a/src/N_m3u8DL-RE.Parser/Processor/DefaultUrlProcessor.cs b/src/N_m3u8DL-RE.Parser/Processor/DefaultUrlProcessor.cs index 8407334..dc3dd37 100644 --- a/src/N_m3u8DL-RE.Parser/Processor/DefaultUrlProcessor.cs +++ b/src/N_m3u8DL-RE.Parser/Processor/DefaultUrlProcessor.cs @@ -16,7 +16,7 @@ namespace N_m3u8DL_RE.Parser.Processor public override string Process(string oriUrl, ParserConfig paserConfig) { - if (paserConfig.AppendUrlParams) + if (paserConfig.AppendUrlParams && oriUrl.StartsWith("http")) { var uriFromConfig = new Uri(paserConfig.Url); var oldUri = new Uri(oriUrl); diff --git a/src/N_m3u8DL-RE.Parser/StreamExtractor.cs b/src/N_m3u8DL-RE.Parser/StreamExtractor.cs index 0617dd6..9195991 100644 --- a/src/N_m3u8DL-RE.Parser/StreamExtractor.cs +++ b/src/N_m3u8DL-RE.Parser/StreamExtractor.cs @@ -68,6 +68,12 @@ namespace N_m3u8DL_RE.Parser //extractor = new DASHExtractor(parserConfig); extractor = new DASHExtractor2(parserConfig); } + else if (rawText.Contains("") && rawText.Contains(" SelectedSteams; DateTime NowDateTime; List OutputFiles = new(); - public SimpleDownloadManager(DownloaderConfig downloaderConfig) + public SimpleDownloadManager(DownloaderConfig downloaderConfig, List selectedSteams, StreamExtractor streamExtractor) { this.DownloaderConfig = downloaderConfig; + this.SelectedSteams = selectedSteams; + this.StreamExtractor = streamExtractor; Downloader = new SimpleDownloader(DownloaderConfig); NowDateTime = DateTime.Now; } @@ -197,7 +203,7 @@ namespace N_m3u8DL_RE.DownloadManager var pad = "0".PadLeft(segments.Count().ToString().Length, '0'); //下载第一个分片 - if (!readInfo) + if (!readInfo || StreamExtractor.ExtractorType == ExtractorType.MSS) { var seg = segments.First(); segments = segments.Skip(1); @@ -213,6 +219,13 @@ namespace N_m3u8DL_RE.DownloadManager task.Increment(1); 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信息 if (string.IsNullOrEmpty(currentKID)) { @@ -232,12 +245,15 @@ namespace N_m3u8DL_RE.DownloadManager result.ActualFilePath = dec; } } - //ffmpeg读取信息 - Logger.WarnMarkUp(ResString.readingInfo); - mediaInfos = await MediainfoUtil.ReadInfoAsync(DownloaderConfig.MyOptions.FFmpegBinaryPath!, result!.ActualFilePath); - mediaInfos.ForEach(info => Logger.InfoMarkUp(info.ToStringMarkUp())); - ChangeSpecInfo(streamSpec, mediaInfos, ref useAACFilter); - readInfo = true; + if (!readInfo) + { + //ffmpeg读取信息 + Logger.WarnMarkUp(ResString.readingInfo); + mediaInfos = await MediainfoUtil.ReadInfoAsync(DownloaderConfig.MyOptions.FFmpegBinaryPath!, result!.ActualFilePath); + mediaInfos.ForEach(info => Logger.InfoMarkUp(info.ToStringMarkUp())); + ChangeSpecInfo(streamSpec, mediaInfos, ref useAACFilter); + readInfo = true; + } } } @@ -456,7 +472,8 @@ namespace N_m3u8DL_RE.DownloadManager //var iniFileBytes = File.ReadAllBytes(initFile!.ActualFilePath); //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 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 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; } - public async Task StartDownloadAsync(IEnumerable streamSpecs) + public async Task StartDownloadAsync() { ConcurrentDictionary SpeedContainerDic = new(); //速度计算 ConcurrentDictionary Results = new(); @@ -610,7 +627,7 @@ namespace N_m3u8DL_RE.DownloadManager await progress.StartAsync(async ctx => { //创建任务 - var dic = streamSpecs.Select(item => + var dic = SelectedSteams.Select(item => { var task = ctx.AddTask(item.ToShortString(), autoStart: false); SpeedContainerDic[task.Id] = new SpeedContainer(); //速度计算 diff --git a/src/N_m3u8DL-RE/Program.cs b/src/N_m3u8DL-RE/Program.cs index 535421f..372f3a5 100644 --- a/src/N_m3u8DL-RE/Program.cs +++ b/src/N_m3u8DL-RE/Program.cs @@ -165,46 +165,8 @@ namespace N_m3u8DL_RE //for www.nowehoryzonty.pl 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)) - { - url = option.Input; - } - - if (string.IsNullOrEmpty(url)) - { - url = AnsiConsole.Ask("Input [green]URL[/]: "); - } + var url = option.Input; //流提取器配置 var extractor = new StreamExtractor(parserConfig); @@ -268,8 +230,8 @@ namespace N_m3u8DL_RE throw new Exception(ResString.noStreamsToDownload); //HLS: 选中流中若有没加载出playlist的,加载playlist - //DASH: 加载playlist (调用url预处理器) - if (selectedStreams.Any(s => s.Playlist == null) || extractor.ExtractorType == ExtractorType.MPEG_DASH) + //DASH/MSS: 加载playlist (调用url预处理器) + if (selectedStreams.Any(s => s.Playlist == null) || extractor.ExtractorType == ExtractorType.MPEG_DASH || extractor.ExtractorType == ExtractorType.MSS) await extractor.FetchPlayListAsync(selectedStreams); //直播检测 @@ -332,8 +294,8 @@ namespace N_m3u8DL_RE else if(!livingFlag) { //开始下载 - var sdm = new SimpleDownloadManager(downloadConfig); - result = await sdm.StartDownloadAsync(selectedStreams); + var sdm = new SimpleDownloadManager(downloadConfig, selectedStreams, extractor); + result = await sdm.StartDownloadAsync(); } else { diff --git a/src/N_m3u8DL-RE/Util/DownloadUtil.cs b/src/N_m3u8DL-RE/Util/DownloadUtil.cs index 46749e9..2369a56 100644 --- a/src/N_m3u8DL-RE/Util/DownloadUtil.cs +++ b/src/N_m3u8DL-RE/Util/DownloadUtil.cs @@ -2,6 +2,7 @@ using N_m3u8DL_RE.Common.Resource; using N_m3u8DL_RE.Common.Util; using N_m3u8DL_RE.Entity; +using System.IO; using System.Net; using System.Net.Http.Headers; @@ -44,6 +45,26 @@ namespace N_m3u8DL_RE.Util var file = new Uri(url).LocalPath; 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)); if (fromPosition != null || toPosition != null) request.Headers.Range = new(fromPosition, toPosition);