ContentStrings { get; set; } = new();
public override bool Equals(object? obj)
{
@@ -43,7 +43,7 @@ namespace Mp4SubtitleParser
{
bool sawSTPP = false;
- //parse init
+ // parse init
new MP4Parser()
.Box("moov", MP4Parser.Children)
.Box("trak", MP4Parser.Children)
@@ -85,12 +85,12 @@ namespace Mp4SubtitleParser
return xmlSrc;
var _div = bodyNode.SelectSingleNode("ns:div", nsMgr);
- //Parse label
+ // Parse
label
foreach (XmlElement _p in _div!.SelectNodes("ns:p", nsMgr)!)
{
var _begin = _p.GetAttribute("begin");
var _end = _p.GetAttribute("end");
- //Handle namespace
+ // Handle namespace
foreach (XmlAttribute attr in _p.Attributes)
{
if (attr.LocalName == "begin") _begin = attr.Value;
@@ -98,8 +98,8 @@ namespace Mp4SubtitleParser
}
_p.SetAttribute("begin", Add(_begin));
_p.SetAttribute("end", Add(_end));
- //Console.WriteLine($"{_begin} {_p.GetAttribute("begin")}");
- //Console.WriteLine($"{_end} {_p.GetAttribute("begin")}");
+ // Console.WriteLine($"{_begin} {_p.GetAttribute("begin")}");
+ // Console.WriteLine($"{_end} {_p.GetAttribute("begin")}");
}
return xmlDoc.OuterXml;
@@ -135,7 +135,7 @@ namespace Mp4SubtitleParser
public static WebVttSub ExtractFromMp4s(IEnumerable items, long segTimeMs, long baseTimestamp = 0L)
{
- //read ttmls
+ // read ttmls
List xmls = new List();
int segIndex = 0;
foreach (var item in items)
@@ -143,7 +143,7 @@ namespace Mp4SubtitleParser
var dataSeg = File.ReadAllBytes(item);
var sawMDAT = false;
- //parse media
+ // parse media
new MP4Parser()
.Box("mdat", MP4Parser.AllData((data) =>
{
@@ -161,10 +161,7 @@ namespace Mp4SubtitleParser
else
{
var datas = SplitMultipleRootElements(Encoding.UTF8.GetString(data));
- foreach (var item in datas)
- {
- xmls.Add(item);
- }
+ xmls.AddRange(datas);
}
}))
.Parse(dataSeg,/* partialOkay= */ false);
@@ -181,7 +178,7 @@ namespace Mp4SubtitleParser
public static WebVttSub ExtractFromTTMLs(IEnumerable items, long segTimeMs, long baseTimestamp = 0L)
{
- //read ttmls
+ // read ttmls
List xmls = new List();
int segIndex = 0;
foreach (var item in items)
@@ -203,7 +200,7 @@ namespace Mp4SubtitleParser
private static WebVttSub ExtractSub(List xmls, long baseTimestamp)
{
- //parsing
+ // parsing
var xmlDoc = new XmlDocument();
var finalSubs = new List();
XmlNode? headNode = null;
@@ -215,7 +212,7 @@ namespace Mp4SubtitleParser
var xmlContent = item;
if (!xmlContent.Contains("(); //id, Base64
+ // PNG Subs
+ var imageDic = new Dictionary(); // id, Base64
if (ImageRegex().IsMatch(xmlDoc.InnerXml))
{
foreach (Match img in ImageRegex().Matches(xmlDoc.InnerXml))
@@ -266,7 +263,7 @@ namespace Mp4SubtitleParser
}
}
- //convert to
+ // convert
to
if (_div!.SelectNodes("ns:p", nsMgr) == null || _div!.SelectNodes("ns:p", nsMgr)!.Count == 0)
{
foreach (XmlElement _tDiv in bodyNode.SelectNodes("ns:div", nsMgr)!)
@@ -277,14 +274,14 @@ namespace Mp4SubtitleParser
}
}
- //Parse
label
+ // Parse
label
foreach (XmlElement _p in _div!.SelectNodes("ns:p", nsMgr)!)
{
var _begin = _p.GetAttribute("begin");
var _end = _p.GetAttribute("end");
var _region = _p.GetAttribute("region");
var _bgImg = _p.GetAttribute("smpte:backgroundImage");
- //Handle namespace
+ // Handle namespace
foreach (XmlAttribute attr in _p.Attributes)
{
if (attr.LocalName == "begin") _begin = attr.Value;
@@ -301,7 +298,7 @@ namespace Mp4SubtitleParser
if (string.IsNullOrEmpty(_bgImg))
{
var _spans = _p.ChildNodes;
- //Collect
+ // Collect
foreach (XmlNode _node in _spans)
{
if (_node.NodeType == XmlNodeType.Element)
@@ -333,12 +330,12 @@ namespace Mp4SubtitleParser
}
}
- //Check if one has been splitted
+ // Check if one
has been splitted
var index = finalSubs.FindLastIndex(s => s.End == _begin && s.Region == _region && s.ContentStrings.SequenceEqual(sub.ContentStrings));
- //Skip empty lines
+ // Skip empty lines
if (sub.ContentStrings.Count > 0)
{
- //Extend
duration
+ // Extend
duration
if (index != -1)
finalSubs[index].End = sub.End;
else if (!finalSubs.Contains(sub))
@@ -372,7 +369,7 @@ namespace Mp4SubtitleParser
}
- StringBuilder vtt = new StringBuilder();
+ var vtt = new StringBuilder();
vtt.AppendLine("WEBVTT");
foreach (var item in dic)
{
diff --git a/src/N_m3u8DL-RE.Parser/Mp4/MP4VttUtil.cs b/src/N_m3u8DL-RE.Parser/Mp4/MP4VttUtil.cs
index fdb62e3..d51842f 100644
--- a/src/N_m3u8DL-RE.Parser/Mp4/MP4VttUtil.cs
+++ b/src/N_m3u8DL-RE.Parser/Mp4/MP4VttUtil.cs
@@ -1,216 +1,215 @@
using N_m3u8DL_RE.Common.Entity;
using System.Text;
-namespace Mp4SubtitleParser
+namespace Mp4SubtitleParser;
+
+public class MP4VttUtil
{
- public class MP4VttUtil
+ public static (bool, uint) CheckInit(byte[] data)
{
- public static (bool, uint) CheckInit(byte[] data)
- {
- uint timescale = 0;
- bool sawWVTT = false;
+ uint timescale = 0;
+ bool sawWVTT = false;
- //parse init
- new MP4Parser()
- .Box("moov", MP4Parser.Children)
- .Box("trak", MP4Parser.Children)
- .Box("mdia", MP4Parser.Children)
- .FullBox("mdhd", (box) =>
- {
- if (!(box.Version == 0 || box.Version == 1))
- throw new Exception("MDHD version can only be 0 or 1");
- timescale = MP4Parser.ParseMDHD(box.Reader, box.Version);
- })
- .Box("minf", MP4Parser.Children)
- .Box("stbl", MP4Parser.Children)
- .FullBox("stsd", MP4Parser.SampleDescription)
- .Box("wvtt", (box) => {
- // A valid vtt init segment, though we have no actual subtitles yet.
- sawWVTT = true;
- })
- .Parse(data);
-
- return (sawWVTT, timescale);
- }
-
- public static WebVttSub ExtractSub(IEnumerable files, uint timescale)
- {
- if (timescale == 0)
- throw new Exception("Missing timescale for VTT content!");
-
- List cues = new();
-
- foreach (var item in files)
+ // parse init
+ new MP4Parser()
+ .Box("moov", MP4Parser.Children)
+ .Box("trak", MP4Parser.Children)
+ .Box("mdia", MP4Parser.Children)
+ .FullBox("mdhd", (box) =>
{
- var dataSeg = File.ReadAllBytes(item);
+ if (!(box.Version == 0 || box.Version == 1))
+ throw new Exception("MDHD version can only be 0 or 1");
+ timescale = MP4Parser.ParseMDHD(box.Reader, box.Version);
+ })
+ .Box("minf", MP4Parser.Children)
+ .Box("stbl", MP4Parser.Children)
+ .FullBox("stsd", MP4Parser.SampleDescription)
+ .Box("wvtt", (box) => {
+ // A valid vtt init segment, though we have no actual subtitles yet.
+ sawWVTT = true;
+ })
+ .Parse(data);
- bool sawTFDT = false;
- bool sawTRUN = false;
- bool sawMDAT = false;
- byte[]? rawPayload = null;
- ulong baseTime = 0;
- ulong defaultDuration = 0;
- List presentations = new();
+ return (sawWVTT, timescale);
+ }
+
+ public static WebVttSub ExtractSub(IEnumerable files, uint timescale)
+ {
+ if (timescale == 0)
+ throw new Exception("Missing timescale for VTT content!");
+
+ List cues = new();
+
+ foreach (var item in files)
+ {
+ var dataSeg = File.ReadAllBytes(item);
+
+ bool sawTFDT = false;
+ bool sawTRUN = false;
+ bool sawMDAT = false;
+ byte[]? rawPayload = null;
+ ulong baseTime = 0;
+ ulong defaultDuration = 0;
+ List presentations = new();
- //parse media
- new MP4Parser()
- .Box("moof", MP4Parser.Children)
- .Box("traf", MP4Parser.Children)
- .FullBox("tfdt", (box) =>
- {
- sawTFDT = true;
- if (!(box.Version == 0 || box.Version == 1))
- throw new Exception("TFDT version can only be 0 or 1");
- baseTime = MP4Parser.ParseTFDT(box.Reader, box.Version);
- })
- .FullBox("tfhd", (box) =>
- {
- if (box.Flags == 1000)
- throw new Exception("A TFHD box should have a valid flags value");
- defaultDuration = MP4Parser.ParseTFHD(box.Reader, box.Flags).DefaultSampleDuration;
- })
- .FullBox("trun", (box) =>
- {
- sawTRUN = true;
- if (box.Version == 1000)
- throw new Exception("A TRUN box should have a valid version value");
- if (box.Flags == 1000)
- throw new Exception("A TRUN box should have a valid flags value");
- presentations = MP4Parser.ParseTRUN(box.Reader, box.Version, box.Flags).SampleData;
- })
- .Box("mdat", MP4Parser.AllData((data) =>
- {
- if (sawMDAT)
- throw new Exception("VTT cues in mp4 with multiple MDAT are not currently supported");
- sawMDAT = true;
- rawPayload = data;
- }))
- .Parse(dataSeg,/* partialOkay= */ false);
-
- if (!sawMDAT && !sawTFDT && !sawTRUN)
+ // parse media
+ new MP4Parser()
+ .Box("moof", MP4Parser.Children)
+ .Box("traf", MP4Parser.Children)
+ .FullBox("tfdt", (box) =>
{
- throw new Exception("A required box is missing");
- }
-
- var currentTime = baseTime;
- var reader = new BinaryReader2(new MemoryStream(rawPayload!));
-
- foreach (var presentation in presentations)
+ sawTFDT = true;
+ if (!(box.Version == 0 || box.Version == 1))
+ throw new Exception("TFDT version can only be 0 or 1");
+ baseTime = MP4Parser.ParseTFDT(box.Reader, box.Version);
+ })
+ .FullBox("tfhd", (box) =>
{
- var duration = presentation.SampleDuration == 0 ? defaultDuration : presentation.SampleDuration;
- var startTime = presentation.SampleCompositionTimeOffset != 0 ?
- baseTime + presentation.SampleCompositionTimeOffset :
- currentTime;
- currentTime = startTime + duration;
- var totalSize = 0;
- do
+ if (box.Flags == 1000)
+ throw new Exception("A TFHD box should have a valid flags value");
+ defaultDuration = MP4Parser.ParseTFHD(box.Reader, box.Flags).DefaultSampleDuration;
+ })
+ .FullBox("trun", (box) =>
+ {
+ sawTRUN = true;
+ if (box.Version == 1000)
+ throw new Exception("A TRUN box should have a valid version value");
+ if (box.Flags == 1000)
+ throw new Exception("A TRUN box should have a valid flags value");
+ presentations = MP4Parser.ParseTRUN(box.Reader, box.Version, box.Flags).SampleData;
+ })
+ .Box("mdat", MP4Parser.AllData((data) =>
+ {
+ if (sawMDAT)
+ throw new Exception("VTT cues in mp4 with multiple MDAT are not currently supported");
+ sawMDAT = true;
+ rawPayload = data;
+ }))
+ .Parse(dataSeg,/* partialOkay= */ false);
+
+ if (!sawMDAT && !sawTFDT && !sawTRUN)
+ {
+ throw new Exception("A required box is missing");
+ }
+
+ var currentTime = baseTime;
+ var reader = new BinaryReader2(new MemoryStream(rawPayload!));
+
+ foreach (var presentation in presentations)
+ {
+ var duration = presentation.SampleDuration == 0 ? defaultDuration : presentation.SampleDuration;
+ var startTime = presentation.SampleCompositionTimeOffset != 0 ?
+ baseTime + presentation.SampleCompositionTimeOffset :
+ currentTime;
+ currentTime = startTime + duration;
+ var totalSize = 0;
+ do
+ {
+ // Read the payload size.
+ var payloadSize = (int)reader.ReadUInt32();
+ totalSize += payloadSize;
+
+ // Skip the type.
+ var payloadType = reader.ReadUInt32();
+ var payloadName = MP4Parser.TypeToString(payloadType);
+
+ // Read the data payload.
+ byte[]? payload = null;
+ if (payloadName == "vttc")
{
- // Read the payload size.
- var payloadSize = (int)reader.ReadUInt32();
- totalSize += payloadSize;
-
- // Skip the type.
- var payloadType = reader.ReadUInt32();
- var payloadName = MP4Parser.TypeToString(payloadType);
-
- // Read the data payload.
- byte[]? payload = null;
- if (payloadName == "vttc")
+ if (payloadSize > 8)
{
- if (payloadSize > 8)
+ payload = reader.ReadBytes(payloadSize - 8);
+ }
+ }
+ else if (payloadName == "vtte")
+ {
+ // It's a vtte, which is a vtt cue that is empty. Ignore any data that
+ // does exist.
+ reader.ReadBytes(payloadSize - 8);
+ }
+ else
+ {
+ Console.WriteLine($"Unknown box {payloadName}! Skipping!");
+ reader.ReadBytes(payloadSize - 8);
+ }
+
+ if (duration != 0)
+ {
+ if (payload != null)
+ {
+ if (timescale == 0)
+ throw new Exception("Timescale should not be zero!");
+ var cue = ParseVTTC(
+ payload,
+ 0 + (double)startTime / timescale,
+ 0 + (double)currentTime / timescale);
+ // Check if same subtitle has been splitted
+ if (cue != null)
{
- payload = reader.ReadBytes(payloadSize - 8);
- }
- }
- else if (payloadName == "vtte")
- {
- // It's a vtte, which is a vtt cue that is empty. Ignore any data that
- // does exist.
- reader.ReadBytes(payloadSize - 8);
- }
- else
- {
- Console.WriteLine($"Unknown box {payloadName}! Skipping!");
- reader.ReadBytes(payloadSize - 8);
- }
-
- if (duration != 0)
- {
- if (payload != null)
- {
- if (timescale == 0)
- throw new Exception("Timescale should not be zero!");
- var cue = ParseVTTC(
- payload,
- 0 + (double)startTime / timescale,
- 0 + (double)currentTime / timescale);
- //Check if same subtitle has been splitted
- if (cue != null)
+ var index = cues.FindLastIndex(s => s.EndTime == cue.StartTime && s.Settings == cue.Settings && s.Payload == cue.Payload);
+ if (index != -1)
{
- var index = cues.FindLastIndex(s => s.EndTime == cue.StartTime && s.Settings == cue.Settings && s.Payload == cue.Payload);
- if (index != -1)
- {
- cues[index].EndTime = cue.EndTime;
- }
- else
- {
- cues.Add(cue);
- }
+ cues[index].EndTime = cue.EndTime;
+ }
+ else
+ {
+ cues.Add(cue);
}
}
}
- else
- {
- throw new Exception("WVTT sample duration unknown, and no default found!");
- }
-
- if (!(presentation.SampleSize == 0 || totalSize <= presentation.SampleSize))
- {
- throw new Exception("The samples do not fit evenly into the sample sizes given in the TRUN box!");
- }
-
- } while (presentation.SampleSize != 0 && (totalSize < presentation.SampleSize));
-
- if (reader.HasMoreData())
- {
- //throw new Exception("MDAT which contain VTT cues and non-VTT data are not currently supported!");
}
+ else
+ {
+ throw new Exception("WVTT sample duration unknown, and no default found!");
+ }
+
+ if (!(presentation.SampleSize == 0 || totalSize <= presentation.SampleSize))
+ {
+ throw new Exception("The samples do not fit evenly into the sample sizes given in the TRUN box!");
+ }
+
+ } while (presentation.SampleSize != 0 && (totalSize < presentation.SampleSize));
+
+ if (reader.HasMoreData())
+ {
+ // throw new Exception("MDAT which contain VTT cues and non-VTT data are not currently supported!");
}
}
-
- if (cues.Count > 0)
- {
- return new WebVttSub() { Cues = cues };
- }
- return new WebVttSub();
}
- private static SubCue? ParseVTTC(byte[] data, double startTime, double endTime)
+ if (cues.Count > 0)
{
- string payload = string.Empty;
- string id = string.Empty;
- string settings = string.Empty;
- new MP4Parser()
- .Box("payl", MP4Parser.AllData((data) =>
- {
- payload = Encoding.UTF8.GetString(data);
- }))
- .Box("iden", MP4Parser.AllData((data) =>
- {
- id = Encoding.UTF8.GetString(data);
- }))
- .Box("sttg", MP4Parser.AllData((data) =>
- {
- settings = Encoding.UTF8.GetString(data);
- }))
- .Parse(data);
-
- if (!string.IsNullOrEmpty(payload))
- {
- return new SubCue() { StartTime = TimeSpan.FromSeconds(startTime), EndTime = TimeSpan.FromSeconds(endTime), Payload = payload, Settings = settings };
- }
- return null;
+ return new WebVttSub() { Cues = cues };
}
+ return new WebVttSub();
}
-}
+
+ private static SubCue? ParseVTTC(byte[] data, double startTime, double endTime)
+ {
+ string payload = string.Empty;
+ string id = string.Empty;
+ string settings = string.Empty;
+ new MP4Parser()
+ .Box("payl", MP4Parser.AllData((data) =>
+ {
+ payload = Encoding.UTF8.GetString(data);
+ }))
+ .Box("iden", MP4Parser.AllData((data) =>
+ {
+ id = Encoding.UTF8.GetString(data);
+ }))
+ .Box("sttg", MP4Parser.AllData((data) =>
+ {
+ settings = Encoding.UTF8.GetString(data);
+ }))
+ .Parse(data);
+
+ if (!string.IsNullOrEmpty(payload))
+ {
+ return new SubCue() { StartTime = TimeSpan.FromSeconds(startTime), EndTime = TimeSpan.FromSeconds(endTime), Payload = payload, Settings = settings };
+ }
+ return null;
+ }
+}
\ No newline at end of file
diff --git a/src/N_m3u8DL-RE.Parser/Mp4/MSSMoovProcessor.cs b/src/N_m3u8DL-RE.Parser/Mp4/MSSMoovProcessor.cs
index 82f6428..e8b6571 100644
--- a/src/N_m3u8DL-RE.Parser/Mp4/MSSMoovProcessor.cs
+++ b/src/N_m3u8DL-RE.Parser/Mp4/MSSMoovProcessor.cs
@@ -4,888 +4,887 @@ 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
-//https://github.com/google/ExoPlayer/blob/a9444c880230d2c2c79097e89259ce0b9f80b87d/library/extractor/src/main/java/com/google/android/exoplayer2/video/HevcConfig.java#L38
-//https://github.com/sannies/mp4parser/blob/master/isoparser/src/main/java/org/mp4parser/boxes/iso14496/part15/HevcDecoderConfigurationRecord.java
-namespace N_m3u8DL_RE.Parser.Mp4
+// 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
+// https://github.com/google/ExoPlayer/blob/a9444c880230d2c2c79097e89259ce0b9f80b87d/library/extractor/src/main/java/com/google/android/exoplayer2/video/HevcConfig.java#L38
+// https://github.com/sannies/mp4parser/blob/master/isoparser/src/main/java/org/mp4parser/boxes/iso14496/part15/HevcDecoderConfigurationRecord.java
+namespace N_m3u8DL_RE.Parser.Mp4;
+
+public partial class MSSMoovProcessor
{
- 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 => int.Parse((StreamSpec.Resolution ?? "0x0").Split('x').First());
+ private int Height => 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 string ProtecitonKID_PR;
+ private byte[] UnityMatrix
{
- [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 string ProtecitonKID_PR;
- 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","DVHE","DVH1"
- };
-
- 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 static string[] HEVC_GENERAL_PROFILE_SPACE_STRINGS = new string[] { "", "A", "B", "C" };
- 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);
- //save kid for playready
- this.ProtecitonKID_PR = HexUtil.BytesToHex(kidBytes);
- //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)
+ get
{
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_IsProtected
- tencPayload.Add(0x8); //default_Per_Sample_IV_size
- tencPayload.AddRange(HexUtil.HexToBytes(ProtecitonKID)); //default_KID
- //tencPayload.Add(0x8);//default_constant_IV_size
- //tencPayload.AddRange(new byte[8]);//default_constant_IV
- 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("isml"); //major brand
- writer.WriteUInt(1); //minor version
- writer.Write("iso5"); //compatible brand
- writer.Write("iso6"); //compatible brand
- writer.Write("piff"); //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.WriteUShort(65535); //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.Where(x => (HexUtil.HexToBytes(x[0..2])[0] & 0x1F) == 7).First());
- var pps = HexUtil.HexToBytes(arr.Where(x => (HexUtil.HexToBytes(x[0..2])[0] & 0x1F) == 8).First());
- //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 vps = HexUtil.HexToBytes(arr.Where(x => (HexUtil.HexToBytes(x[0..2])[0] >> 1) == 0x20).First());
- var sps = HexUtil.HexToBytes(arr.Where(x => (HexUtil.HexToBytes(x[0..2])[0] >> 1) == 0x21).First());
- var pps = HexUtil.HexToBytes(arr.Where(x => (HexUtil.HexToBytes(x[0..2])[0] >> 1) == 0x22).First());
- //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
- }
- }
- // 杜比视界也按照hevc处理
- else if (FourCC == "DVHE" || FourCC == "DVH1")
- {
- var arr = CodecPrivateData.Split(new[] { StartCode }, StringSplitOptions.RemoveEmptyEntries);
- var vps = HexUtil.HexToBytes(arr.Where(x => (HexUtil.HexToBytes(x[0..2])[0] >> 1) == 0x20).First());
- var sps = HexUtil.HexToBytes(arr.Where(x => (HexUtil.HexToBytes(x[0..2])[0] >> 1) == 0x21).First());
- var pps = HexUtil.HexToBytes(arr.Where(x => (HexUtil.HexToBytes(x[0..2])[0] >> 1) == 0x22).First());
- //make hvcC
- var hvcC = GetHvcC(sps, pps, vps, "dvh1");
- writer.Write(hvcC);
- if (IsProtection)
- {
- var sinfBox = GenSinf("dvh1");
- writer.Write(sinfBox);
- return Box("encv", stream.ToArray()); //Encrypted Video
- }
- else
- {
- return Box("dvh1", 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, string code = "hvc1")
- {
- var oriSps = new List(sps);
- //https://www.itu.int/rec/dologin.asp?lang=f&id=T-REC-H.265-201504-S!!PDF-E&type=items
- //Read generalProfileSpace, generalTierFlag, generalProfileIdc,
- //generalProfileCompatibilityFlags, constraintBytes, generalLevelIdc
- //from sps
- var encList = new List();
- /**
- * 处理payload, 有00 00 03 0,1,2,3的情况 统一换成00 00 XX 即丢弃03
- * 注意:此处采用的逻辑是直接简单粗暴地判断列表末尾3字节,如果是0x000003就删掉最后的0x03,可能会导致以下情况
- * 00 00 03 03 03 03 03 01 会被直接处理成 => 00 00 01
- * 此处经过测试只有直接跳过才正常,如果处理成 00 00 03 03 03 03 01 是有问题的
- *
- * 测试的数据如下:
- * 原始:42 01 01 01 60 00 00 03 00 90 00 00 03 00 00 03 00 96 a0 01 e0 20 06 61 65 95 9a 49 30 bf fc 0c 7c 0c 81 a8 08 08 08 20 00 00 03 00 20 00 00 03 03 01
- * 处理后:42 01 01 01 60 00 00 00 90 00 00 00 00 00 96 A0 01 E0 20 06 61 65 95 9A 49 30 BF FC 0C 7C 0C 81 A8 08 08 08 20 00 00 00 20 00 00 01
- */
- using (var _reader = new BinaryReader(new MemoryStream(sps)))
- {
- while (_reader.BaseStream.Position < _reader.BaseStream.Length)
- {
- encList.Add(_reader.ReadByte());
- if (encList.Count >= 3 && encList[encList.Count - 3] == 0x00 && encList[encList.Count - 2] == 0x00 && encList[encList.Count - 1] == 0x03)
- {
- encList.RemoveAt(encList.Count - 1);
- }
- }
- }
- sps = encList.ToArray();
-
- using var reader = new BinaryReader2(new MemoryStream(sps));
- reader.ReadBytes(2); //Skip 2 bytes unit header
- var firstByte = reader.ReadByte();
- var maxSubLayersMinus1 = (firstByte & 0xe) >> 1;
- var nextByte = reader.ReadByte();
- var generalProfileSpace = (nextByte & 0xc0) >> 6;
- var generalTierFlag = (nextByte & 0x20) >> 5;
- var generalProfileIdc = nextByte & 0x1f;
- var generalProfileCompatibilityFlags = reader.ReadUInt32();
- var constraintBytes = reader.ReadBytes(6);
- var generalLevelIdc = reader.ReadByte();
-
- /*var skipBit = 0;
- for (int i = 0; i < maxSubLayersMinus1; i++)
- {
- skipBit += 2; //sub_layer_profile_present_flag sub_layer_level_present_flag
- }
- if (maxSubLayersMinus1 > 0)
- {
- for (int i = maxSubLayersMinus1; i < 8; i++)
- {
- skipBit += 2; //reserved_zero_2bits
- }
- }
- for (int i = 0; i < maxSubLayersMinus1; i++)
- {
- skipBit += 2; //sub_layer_profile_present_flag sub_layer_level_present_flag
- }*/
-
- //生成编码信息
- var codecs = code +
- $".{HEVC_GENERAL_PROFILE_SPACE_STRINGS[generalProfileSpace]}{generalProfileIdc}" +
- $".{Convert.ToString(generalProfileCompatibilityFlags, 16)}" +
- $".{(generalTierFlag == 1 ? 'H' : 'L')}{generalLevelIdc}" +
- $".{HexUtil.BytesToHex(constraintBytes.Where(b => b != 0).ToArray())}";
- StreamSpec.Codecs = codecs;
-
-
- ///////////////////////
-
-
- using var stream = new MemoryStream();
- using var writer = new BinaryWriter2(stream);
-
- //var reserved1 = 0xF;
-
- writer.WriteByte(1); //configuration version
- writer.WriteByte((byte)((generalProfileSpace << 6) + (generalTierFlag == 1 ? 0x20 : 0) | generalProfileIdc)); //general_profile_space + general_tier_flag + general_profile_idc
- writer.WriteUInt(generalProfileCompatibilityFlags); //general_profile_compatibility_flags
- writer.Write(constraintBytes); //general_constraint_indicator_flags
- writer.WriteByte((byte)generalProfileIdc); //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(0x03); //numOfArrays (vps sps pps)
-
- sps = oriSps.ToArray();
- writer.WriteByte(0x20); //array_completeness + reserved + NAL_unit_type
- writer.WriteUShort(1); //numNalus
- writer.WriteUShort(vps.Length);
- writer.Write(vps);
- writer.WriteByte(0x21);
- writer.WriteUShort(1); //numNalus
- writer.WriteUShort(sps.Length);
- writer.Write(sps);
- writer.WriteByte(0x22);
- writer.WriteUShort(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
- }
-
- private byte[] GenPsshBoxForPlayReady()
- {
- using var _stream = new MemoryStream();
- using var _writer = new BinaryWriter2(_stream);
- var sysIdData = HexUtil.HexToBytes(ProtectionSystemId.Replace("-", ""));
- var psshData = HexUtil.HexToBytes(ProtectionData);
-
- _writer.Write(sysIdData); // SystemID 16 bytes
- _writer.WriteUInt(psshData.Length); //Size of Data 4 bytes
- _writer.Write(psshData); //Data
- var psshBox = FullBox("pssh", 0, 0, _stream.ToArray());
- return psshBox;
- }
-
- private byte[] GenPsshBoxForWideVine()
- {
- using var _stream = new MemoryStream();
- using var _writer = new BinaryWriter2(_stream);
- var sysIdData = HexUtil.HexToBytes("edef8ba9-79d6-4ace-a3c8-27dcd51d21ed".Replace("-", ""));
- //var kid = HexUtil.HexToBytes(ProtecitonKID);
-
- _writer.Write(sysIdData); // SystemID 16 bytes
- var psshData = HexUtil.HexToBytes($"08011210{ProtecitonKID}1A046E647265220400000000");
- _writer.WriteUInt(psshData.Length); //Size of Data 4 bytes
- _writer.Write(psshData); //Data
- var psshBox = FullBox("pssh", 0, 0, _stream.ToArray());
- return psshBox;
- }
-
- private byte[] GenMoof()
- {
- using var stream = new MemoryStream();
- using var writer = new BinaryWriter2(stream);
-
- //make senc
- writer.WriteUInt(1); //sample_count
- writer.Write(new byte[8]); //8 bytes IV
-
- var sencBox = FullBox("senc", 1, 0, stream.ToArray());
-
- var moofBox = Box("moof", sencBox); //Movie Extends Box
-
- return moofBox;
- }
-
- 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();
-
- if (IsProtection)
- {
- var psshBox1 = GenPsshBoxForPlayReady();
- var psshBox2 = GenPsshBoxForWideVine();
- moovPayload = moovPayload.Concat(psshBox1).Concat(psshBox2).ToArray();
- }
-
- var moovBox = Box("moov", moovPayload); //Movie Box
-
- stream.Write(moovBox);
-
- //var moofBox = GenMoof(); //Movie Extends Box
- //stream.Write(moofBox);
-
+ 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()
+ {
+ "HVC1","HEV1","AACL","AACH","EC-3","H264","AVC1","DAVC","AVC1","TTML","DVHE","DVH1"
+ };
+
+ 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 static string[] HEVC_GENERAL_PROFILE_SPACE_STRINGS = new string[] { "", "A", "B", "C" };
+ 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);
+ // save kid for playready
+ this.ProtecitonKID_PR = HexUtil.BytesToHex(kidBytes);
+ // 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_IsProtected
+ tencPayload.Add(0x8); // default_Per_Sample_IV_size
+ tencPayload.AddRange(HexUtil.HexToBytes(ProtecitonKID)); // default_KID
+ // tencPayload.Add(0x8);// default_constant_IV_size
+ // tencPayload.AddRange(new byte[8]);// default_constant_IV
+ 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("isml"); // major brand
+ writer.WriteUInt(1); // minor version
+ writer.Write("iso5"); // compatible brand
+ writer.Write("iso6"); // compatible brand
+ writer.Write("piff"); // 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.WriteUShort(65535); // 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.Where(x => (HexUtil.HexToBytes(x[0..2])[0] & 0x1F) == 7).First());
+ var pps = HexUtil.HexToBytes(arr.Where(x => (HexUtil.HexToBytes(x[0..2])[0] & 0x1F) == 8).First());
+ // 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 vps = HexUtil.HexToBytes(arr.Where(x => (HexUtil.HexToBytes(x[0..2])[0] >> 1) == 0x20).First());
+ var sps = HexUtil.HexToBytes(arr.Where(x => (HexUtil.HexToBytes(x[0..2])[0] >> 1) == 0x21).First());
+ var pps = HexUtil.HexToBytes(arr.Where(x => (HexUtil.HexToBytes(x[0..2])[0] >> 1) == 0x22).First());
+ // 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
+ }
+ }
+ // 杜比视界也按照hevc处理
+ else if (FourCC == "DVHE" || FourCC == "DVH1")
+ {
+ var arr = CodecPrivateData.Split(new[] { StartCode }, StringSplitOptions.RemoveEmptyEntries);
+ var vps = HexUtil.HexToBytes(arr.Where(x => (HexUtil.HexToBytes(x[0..2])[0] >> 1) == 0x20).First());
+ var sps = HexUtil.HexToBytes(arr.Where(x => (HexUtil.HexToBytes(x[0..2])[0] >> 1) == 0x21).First());
+ var pps = HexUtil.HexToBytes(arr.Where(x => (HexUtil.HexToBytes(x[0..2])[0] >> 1) == 0x22).First());
+ // make hvcC
+ var hvcC = GetHvcC(sps, pps, vps, "dvh1");
+ writer.Write(hvcC);
+ if (IsProtection)
+ {
+ var sinfBox = GenSinf("dvh1");
+ writer.Write(sinfBox);
+ return Box("encv", stream.ToArray()); // Encrypted Video
+ }
+ else
+ {
+ return Box("dvh1", 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, string code = "hvc1")
+ {
+ var oriSps = new List(sps);
+ // https://www.itu.int/rec/dologin.asp?lang=f&id=T-REC-H.265-201504-S!!PDF-E&type=items
+ // Read generalProfileSpace, generalTierFlag, generalProfileIdc,
+ // generalProfileCompatibilityFlags, constraintBytes, generalLevelIdc
+ // from sps
+ var encList = new List();
+ /**
+ * 处理payload, 有00 00 03 0,1,2,3的情况 统一换成00 00 XX 即丢弃03
+ * 注意:此处采用的逻辑是直接简单粗暴地判断列表末尾3字节,如果是0x000003就删掉最后的0x03,可能会导致以下情况
+ * 00 00 03 03 03 03 03 01 会被直接处理成 => 00 00 01
+ * 此处经过测试只有直接跳过才正常,如果处理成 00 00 03 03 03 03 01 是有问题的
+ *
+ * 测试的数据如下:
+ * 原始:42 01 01 01 60 00 00 03 00 90 00 00 03 00 00 03 00 96 a0 01 e0 20 06 61 65 95 9a 49 30 bf fc 0c 7c 0c 81 a8 08 08 08 20 00 00 03 00 20 00 00 03 03 01
+ * 处理后:42 01 01 01 60 00 00 00 90 00 00 00 00 00 96 A0 01 E0 20 06 61 65 95 9A 49 30 BF FC 0C 7C 0C 81 A8 08 08 08 20 00 00 00 20 00 00 01
+ */
+ using (var _reader = new BinaryReader(new MemoryStream(sps)))
+ {
+ while (_reader.BaseStream.Position < _reader.BaseStream.Length)
+ {
+ encList.Add(_reader.ReadByte());
+ if (encList.Count >= 3 && encList[encList.Count - 3] == 0x00 && encList[encList.Count - 2] == 0x00 && encList[encList.Count - 1] == 0x03)
+ {
+ encList.RemoveAt(encList.Count - 1);
+ }
+ }
+ }
+ sps = encList.ToArray();
+
+ using var reader = new BinaryReader2(new MemoryStream(sps));
+ reader.ReadBytes(2); // Skip 2 bytes unit header
+ var firstByte = reader.ReadByte();
+ var maxSubLayersMinus1 = (firstByte & 0xe) >> 1;
+ var nextByte = reader.ReadByte();
+ var generalProfileSpace = (nextByte & 0xc0) >> 6;
+ var generalTierFlag = (nextByte & 0x20) >> 5;
+ var generalProfileIdc = nextByte & 0x1f;
+ var generalProfileCompatibilityFlags = reader.ReadUInt32();
+ var constraintBytes = reader.ReadBytes(6);
+ var generalLevelIdc = reader.ReadByte();
+
+ /*var skipBit = 0;
+ for (int i = 0; i < maxSubLayersMinus1; i++)
+ {
+ skipBit += 2; // sub_layer_profile_present_flag sub_layer_level_present_flag
+ }
+ if (maxSubLayersMinus1 > 0)
+ {
+ for (int i = maxSubLayersMinus1; i < 8; i++)
+ {
+ skipBit += 2; // reserved_zero_2bits
+ }
+ }
+ for (int i = 0; i < maxSubLayersMinus1; i++)
+ {
+ skipBit += 2; // sub_layer_profile_present_flag sub_layer_level_present_flag
+ }*/
+
+ // 生成编码信息
+ var codecs = code +
+ $".{HEVC_GENERAL_PROFILE_SPACE_STRINGS[generalProfileSpace]}{generalProfileIdc}" +
+ $".{Convert.ToString(generalProfileCompatibilityFlags, 16)}" +
+ $".{(generalTierFlag == 1 ? 'H' : 'L')}{generalLevelIdc}" +
+ $".{HexUtil.BytesToHex(constraintBytes.Where(b => b != 0).ToArray())}";
+ StreamSpec.Codecs = codecs;
+
+
+ ///////////////////////
+
+
+ using var stream = new MemoryStream();
+ using var writer = new BinaryWriter2(stream);
+
+ // var reserved1 = 0xF;
+
+ writer.WriteByte(1); // configuration version
+ writer.WriteByte((byte)((generalProfileSpace << 6) + (generalTierFlag == 1 ? 0x20 : 0) | generalProfileIdc)); // general_profile_space + general_tier_flag + general_profile_idc
+ writer.WriteUInt(generalProfileCompatibilityFlags); // general_profile_compatibility_flags
+ writer.Write(constraintBytes); // general_constraint_indicator_flags
+ writer.WriteByte((byte)generalProfileIdc); // 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(0x03); // numOfArrays (vps sps pps)
+
+ sps = oriSps.ToArray();
+ writer.WriteByte(0x20); // array_completeness + reserved + NAL_unit_type
+ writer.WriteUShort(1); // numNalus
+ writer.WriteUShort(vps.Length);
+ writer.Write(vps);
+ writer.WriteByte(0x21);
+ writer.WriteUShort(1); // numNalus
+ writer.WriteUShort(sps.Length);
+ writer.Write(sps);
+ writer.WriteByte(0x22);
+ writer.WriteUShort(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
+ }
+
+ private byte[] GenPsshBoxForPlayReady()
+ {
+ using var _stream = new MemoryStream();
+ using var _writer = new BinaryWriter2(_stream);
+ var sysIdData = HexUtil.HexToBytes(ProtectionSystemId.Replace("-", ""));
+ var psshData = HexUtil.HexToBytes(ProtectionData);
+
+ _writer.Write(sysIdData); // SystemID 16 bytes
+ _writer.WriteUInt(psshData.Length); // Size of Data 4 bytes
+ _writer.Write(psshData); // Data
+ var psshBox = FullBox("pssh", 0, 0, _stream.ToArray());
+ return psshBox;
+ }
+
+ private byte[] GenPsshBoxForWideVine()
+ {
+ using var _stream = new MemoryStream();
+ using var _writer = new BinaryWriter2(_stream);
+ var sysIdData = HexUtil.HexToBytes("edef8ba9-79d6-4ace-a3c8-27dcd51d21ed".Replace("-", ""));
+ // var kid = HexUtil.HexToBytes(ProtecitonKID);
+
+ _writer.Write(sysIdData); // SystemID 16 bytes
+ var psshData = HexUtil.HexToBytes($"08011210{ProtecitonKID}1A046E647265220400000000");
+ _writer.WriteUInt(psshData.Length); // Size of Data 4 bytes
+ _writer.Write(psshData); // Data
+ var psshBox = FullBox("pssh", 0, 0, _stream.ToArray());
+ return psshBox;
+ }
+
+ private byte[] GenMoof()
+ {
+ using var stream = new MemoryStream();
+ using var writer = new BinaryWriter2(stream);
+
+ // make senc
+ writer.WriteUInt(1); // sample_count
+ writer.Write(new byte[8]); // 8 bytes IV
+
+ var sencBox = FullBox("senc", 1, 0, stream.ToArray());
+
+ var moofBox = Box("moof", sencBox); // Movie Extends Box
+
+ return moofBox;
+ }
+
+ 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();
+
+ if (IsProtection)
+ {
+ var psshBox1 = GenPsshBoxForPlayReady();
+ var psshBox2 = GenPsshBoxForWideVine();
+ moovPayload = moovPayload.Concat(psshBox1).Concat(psshBox2).ToArray();
+ }
+
+ var moovBox = Box("moov", moovPayload); // Movie Box
+
+ stream.Write(moovBox);
+
+ // var moofBox = GenMoof(); // Movie Extends Box
+ // stream.Write(moofBox);
+
+ return stream.ToArray();
+ }
+}
\ No newline at end of file
diff --git a/src/N_m3u8DL-RE.Parser/Processor/ContentProcessor.cs b/src/N_m3u8DL-RE.Parser/Processor/ContentProcessor.cs
index 60b0a87..ba1a6ea 100644
--- a/src/N_m3u8DL-RE.Parser/Processor/ContentProcessor.cs
+++ b/src/N_m3u8DL-RE.Parser/Processor/ContentProcessor.cs
@@ -6,11 +6,10 @@ using System.Linq;
using System.Text;
using System.Threading.Tasks;
-namespace N_m3u8DL_RE.Parser.Processor
+namespace N_m3u8DL_RE.Parser.Processor;
+
+public abstract class ContentProcessor
{
- public abstract class ContentProcessor
- {
- public abstract bool CanProcess(ExtractorType extractorType, string rawText, ParserConfig parserConfig);
- public abstract string Process(string rawText, ParserConfig parserConfig);
- }
-}
+ public abstract bool CanProcess(ExtractorType extractorType, string rawText, ParserConfig parserConfig);
+ public abstract string Process(string rawText, ParserConfig parserConfig);
+}
\ No newline at end of file
diff --git a/src/N_m3u8DL-RE.Parser/Processor/DASH/DefaultDASHContentProcessor.cs b/src/N_m3u8DL-RE.Parser/Processor/DASH/DefaultDASHContentProcessor.cs
index aa69a94..97ede43 100644
--- a/src/N_m3u8DL-RE.Parser/Processor/DASH/DefaultDASHContentProcessor.cs
+++ b/src/N_m3u8DL-RE.Parser/Processor/DASH/DefaultDASHContentProcessor.cs
@@ -7,30 +7,29 @@ using System.Linq;
using System.Text;
using System.Threading.Tasks;
-namespace N_m3u8DL_RE.Parser.Processor.DASH
+namespace N_m3u8DL_RE.Parser.Processor.DASH;
+
+///
+/// XG视频处理
+///
+public class DefaultDASHContentProcessor : ContentProcessor
{
- ///
- /// 西瓜视频处理
- ///
- public class DefaultDASHContentProcessor : ContentProcessor
+ public override bool CanProcess(ExtractorType extractorType, string mpdContent, ParserConfig parserConfig)
{
- public override bool CanProcess(ExtractorType extractorType, string mpdContent, ParserConfig parserConfig)
+ if (extractorType != ExtractorType.MPEG_DASH) return false;
+
+ if (mpdContent.Contains(" paserConfig.AppendUrlParams;
+
+ public override string Process(string oriUrl, ParserConfig paserConfig)
{
- public override bool CanProcess(ExtractorType extractorType, string oriUrl, ParserConfig paserConfig) => paserConfig.AppendUrlParams;
-
- public override string Process(string oriUrl, ParserConfig paserConfig)
+ if (oriUrl.StartsWith("http"))
{
- if (oriUrl.StartsWith("http"))
+ var uriFromConfig = new Uri(paserConfig.Url);
+ var uriFromConfigQuery = HttpUtility.ParseQueryString(uriFromConfig.Query);
+
+ var oldUri = new Uri(oriUrl);
+ var newQuery = HttpUtility.ParseQueryString(oldUri.Query);
+ foreach (var item in uriFromConfigQuery.AllKeys)
{
- var uriFromConfig = new Uri(paserConfig.Url);
- var uriFromConfigQuery = HttpUtility.ParseQueryString(uriFromConfig.Query);
-
- var oldUri = new Uri(oriUrl);
- var newQuery = HttpUtility.ParseQueryString(oldUri.Query);
- foreach (var item in uriFromConfigQuery.AllKeys)
- {
- if (newQuery.AllKeys.Contains(item))
- newQuery.Set(item, uriFromConfigQuery.Get(item));
- else
- newQuery.Add(item, uriFromConfigQuery.Get(item));
- }
-
- if (!string.IsNullOrEmpty(newQuery.ToString()))
- {
- Logger.Debug("Before: " + oriUrl);
- oriUrl = (oldUri.GetLeftPart(UriPartial.Path) + "?" + newQuery.ToString()).TrimEnd('?');
- Logger.Debug("After: " + oriUrl);
- }
+ if (newQuery.AllKeys.Contains(item))
+ newQuery.Set(item, uriFromConfigQuery.Get(item));
+ else
+ newQuery.Add(item, uriFromConfigQuery.Get(item));
}
- return oriUrl;
+ if (!string.IsNullOrEmpty(newQuery.ToString()))
+ {
+ Logger.Debug("Before: " + oriUrl);
+ oriUrl = (oldUri.GetLeftPart(UriPartial.Path) + "?" + newQuery.ToString()).TrimEnd('?');
+ Logger.Debug("After: " + oriUrl);
+ }
}
+
+ return oriUrl;
}
-}
+}
\ No newline at end of file
diff --git a/src/N_m3u8DL-RE.Parser/Processor/HLS/DefaultHLSContentProcessor.cs b/src/N_m3u8DL-RE.Parser/Processor/HLS/DefaultHLSContentProcessor.cs
index 3e69ee0..2e99e61 100644
--- a/src/N_m3u8DL-RE.Parser/Processor/HLS/DefaultHLSContentProcessor.cs
+++ b/src/N_m3u8DL-RE.Parser/Processor/HLS/DefaultHLSContentProcessor.cs
@@ -8,101 +8,94 @@ using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
-namespace N_m3u8DL_RE.Parser.Processor.HLS
+namespace N_m3u8DL_RE.Parser.Processor.HLS;
+
+public partial class DefaultHLSContentProcessor : ContentProcessor
{
- public partial class DefaultHLSContentProcessor : ContentProcessor
+ [GeneratedRegex("#EXT-X-DISCONTINUITY\\s+#EXT-X-MAP:URI=\\\"(.*?)\\\",BYTERANGE=\\\"(.*?)\\\"")]
+ private static partial Regex YkDVRegex();
+ [GeneratedRegex("#EXT-X-MAP:URI=\\\".*?BUMPER/[\\s\\S]+?#EXT-X-DISCONTINUITY")]
+ private static partial Regex DNSPRegex();
+ [GeneratedRegex("#EXTINF:.*?,\\s+.*BUMPER.*\\s+?#EXT-X-DISCONTINUITY")]
+ private static partial Regex DNSPSubRegex();
+ [GeneratedRegex("(#EXTINF.*)(\\s+)(#EXT-X-KEY.*)")]
+ private static partial Regex OrderFixRegex();
+ [GeneratedRegex("#EXT-X-MAP.*\\.apple\\.com/")]
+ private static partial Regex ATVRegex();
+ [GeneratedRegex("(#EXT-X-KEY:[\\s\\S]*?)(#EXT-X-DISCONTINUITY|#EXT-X-ENDLIST)")]
+ private static partial Regex ATVRegex2();
+
+ public override bool CanProcess(ExtractorType extractorType, string rawText, ParserConfig parserConfig) => extractorType == ExtractorType.HLS;
+
+ public override string Process(string m3u8Content, ParserConfig parserConfig)
{
- [GeneratedRegex("#EXT-X-DISCONTINUITY\\s+#EXT-X-MAP:URI=\\\"(.*?)\\\",BYTERANGE=\\\"(.*?)\\\"")]
- private static partial Regex YkDVRegex();
- [GeneratedRegex("#EXT-X-MAP:URI=\\\".*?BUMPER/[\\s\\S]+?#EXT-X-DISCONTINUITY")]
- private static partial Regex DNSPRegex();
- [GeneratedRegex("#EXTINF:.*?,\\s+.*BUMPER.*\\s+?#EXT-X-DISCONTINUITY")]
- private static partial Regex DNSPSubRegex();
- [GeneratedRegex("(#EXTINF.*)(\\s+)(#EXT-X-KEY.*)")]
- private static partial Regex OrderFixRegex();
- [GeneratedRegex("#EXT-X-MAP.*\\.apple\\.com/")]
- private static partial Regex ATVRegex();
- [GeneratedRegex("(#EXT-X-KEY:[\\s\\S]*?)(#EXT-X-DISCONTINUITY|#EXT-X-ENDLIST)")]
- private static partial Regex ATVRegex2();
-
- public override bool CanProcess(ExtractorType extractorType, string rawText, ParserConfig parserConfig) => extractorType == ExtractorType.HLS;
-
- public override string Process(string m3u8Content, ParserConfig parserConfig)
+ // 处理content以\r作为换行符的情况
+ if (m3u8Content.Contains("\r") && !m3u8Content.Contains("\n"))
{
- //处理content以\r作为换行符的情况
- if (m3u8Content.Contains("\r") && !m3u8Content.Contains("\n"))
- {
- m3u8Content = m3u8Content.Replace("\r", Environment.NewLine);
- }
-
- var m3u8Url = parserConfig.Url;
- //央视频回放
- if (m3u8Url.Contains("tlivecloud-playback-cdn.ysp.cctv.cn") && m3u8Url.Contains("endtime="))
- {
- m3u8Content += Environment.NewLine + HLSTags.ext_x_endlist;
- }
-
- //IMOOC
- if (m3u8Url.Contains("imooc.com/"))
- {
- //M3u8Content = DecodeImooc.DecodeM3u8(M3u8Content);
- }
-
- //iqy
- if (m3u8Content.StartsWith("{\"payload\""))
- {
- //
- }
-
- //针对优酷#EXT-X-VERSION:7杜比视界片源修正
- if (m3u8Content.Contains("#EXT-X-DISCONTINUITY") && m3u8Content.Contains("#EXT-X-MAP") && m3u8Content.Contains("ott.cibntv.net") && m3u8Content.Contains("ccode="))
- {
- Regex ykmap = YkDVRegex();
- foreach (Match m in ykmap.Matches(m3u8Content))
- {
- m3u8Content = m3u8Content.Replace(m.Value, $"#EXTINF:0.000000,\n#EXT-X-BYTERANGE:{m.Groups[2].Value}\n{m.Groups[1].Value}");
- }
- }
-
- //针对Disney+修正
- if (m3u8Content.Contains("#EXT-X-DISCONTINUITY") && m3u8Content.Contains("#EXT-X-MAP") && m3u8Url.Contains("media.dssott.com/"))
- {
- Regex ykmap = DNSPRegex();
- if (ykmap.IsMatch(m3u8Content))
- {
- m3u8Content = m3u8Content.Replace(ykmap.Match(m3u8Content).Value, "#XXX");
- }
- }
-
- //针对Disney+字幕修正
- if (m3u8Content.Contains("#EXT-X-DISCONTINUITY") && m3u8Content.Contains("seg_00000.vtt") && m3u8Url.Contains("media.dssott.com/"))
- {
- Regex ykmap = DNSPSubRegex();
- if (ykmap.IsMatch(m3u8Content))
- {
- m3u8Content = m3u8Content.Replace(ykmap.Match(m3u8Content).Value, "#XXX");
- }
- }
-
- //针对AppleTv修正
- if (m3u8Content.Contains("#EXT-X-DISCONTINUITY") && m3u8Content.Contains("#EXT-X-MAP") && (m3u8Url.Contains(".apple.com/") || ATVRegex().IsMatch(m3u8Content)))
- {
- //只取加密部分即可
- Regex ykmap = ATVRegex2();
- if (ykmap.IsMatch(m3u8Content))
- {
- m3u8Content = "#EXTM3U\r\n" + ykmap.Match(m3u8Content).Groups[1].Value + "\r\n#EXT-X-ENDLIST";
- }
- }
-
- //修复#EXT-X-KEY与#EXTINF出现次序异常问题
- var regex = OrderFixRegex();
- if (regex.IsMatch(m3u8Content))
- {
- m3u8Content = regex.Replace(m3u8Content, "$3$2$1");
- }
-
- return m3u8Content;
+ m3u8Content = m3u8Content.Replace("\r", Environment.NewLine);
}
+
+ var m3u8Url = parserConfig.Url;
+ // YSP回放
+ if (m3u8Url.Contains("tlivecloud-playback-cdn.ysp.cctv.cn") && m3u8Url.Contains("endtime="))
+ {
+ m3u8Content += Environment.NewLine + HLSTags.ext_x_endlist;
+ }
+
+ // IMOOC
+ if (m3u8Url.Contains("imooc.com/"))
+ {
+ // M3u8Content = DecodeImooc.DecodeM3u8(M3u8Content);
+ }
+
+ // 针对YK #EXT-X-VERSION:7杜比视界片源修正
+ if (m3u8Content.Contains("#EXT-X-DISCONTINUITY") && m3u8Content.Contains("#EXT-X-MAP") && m3u8Content.Contains("ott.cibntv.net") && m3u8Content.Contains("ccode="))
+ {
+ Regex ykmap = YkDVRegex();
+ foreach (Match m in ykmap.Matches(m3u8Content))
+ {
+ m3u8Content = m3u8Content.Replace(m.Value, $"#EXTINF:0.000000,\n#EXT-X-BYTERANGE:{m.Groups[2].Value}\n{m.Groups[1].Value}");
+ }
+ }
+
+ // 针对Disney+修正
+ if (m3u8Content.Contains("#EXT-X-DISCONTINUITY") && m3u8Content.Contains("#EXT-X-MAP") && m3u8Url.Contains("media.dssott.com/"))
+ {
+ Regex ykmap = DNSPRegex();
+ if (ykmap.IsMatch(m3u8Content))
+ {
+ m3u8Content = m3u8Content.Replace(ykmap.Match(m3u8Content).Value, "#XXX");
+ }
+ }
+
+ // 针对Disney+字幕修正
+ if (m3u8Content.Contains("#EXT-X-DISCONTINUITY") && m3u8Content.Contains("seg_00000.vtt") && m3u8Url.Contains("media.dssott.com/"))
+ {
+ Regex ykmap = DNSPSubRegex();
+ if (ykmap.IsMatch(m3u8Content))
+ {
+ m3u8Content = m3u8Content.Replace(ykmap.Match(m3u8Content).Value, "#XXX");
+ }
+ }
+
+ // 针对AppleTv修正
+ if (m3u8Content.Contains("#EXT-X-DISCONTINUITY") && m3u8Content.Contains("#EXT-X-MAP") && (m3u8Url.Contains(".apple.com/") || ATVRegex().IsMatch(m3u8Content)))
+ {
+ // 只取加密部分即可
+ Regex ykmap = ATVRegex2();
+ if (ykmap.IsMatch(m3u8Content))
+ {
+ m3u8Content = "#EXTM3U\r\n" + ykmap.Match(m3u8Content).Groups[1].Value + "\r\n#EXT-X-ENDLIST";
+ }
+ }
+
+ // 修复#EXT-X-KEY与#EXTINF出现次序异常问题
+ var regex = OrderFixRegex();
+ if (regex.IsMatch(m3u8Content))
+ {
+ m3u8Content = regex.Replace(m3u8Content, "$3$2$1");
+ }
+
+ return m3u8Content;
}
-}
+}
\ No newline at end of file
diff --git a/src/N_m3u8DL-RE.Parser/Processor/HLS/DefaultHLSKeyProcessor.cs b/src/N_m3u8DL-RE.Parser/Processor/HLS/DefaultHLSKeyProcessor.cs
index 556bdcc..9f8826c 100644
--- a/src/N_m3u8DL-RE.Parser/Processor/HLS/DefaultHLSKeyProcessor.cs
+++ b/src/N_m3u8DL-RE.Parser/Processor/HLS/DefaultHLSKeyProcessor.cs
@@ -6,112 +6,106 @@ using N_m3u8DL_RE.Common.Util;
using N_m3u8DL_RE.Parser.Config;
using N_m3u8DL_RE.Parser.Util;
using Spectre.Console;
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
-namespace N_m3u8DL_RE.Parser.Processor.HLS
+namespace N_m3u8DL_RE.Parser.Processor.HLS;
+
+public class DefaultHLSKeyProcessor : KeyProcessor
{
- public class DefaultHLSKeyProcessor : KeyProcessor
+ public override bool CanProcess(ExtractorType extractorType, string m3u8Url, string keyLine, string m3u8Content, ParserConfig paserConfig) => extractorType == ExtractorType.HLS;
+
+
+ public override EncryptInfo Process(string keyLine, string m3u8Url, string m3u8Content, ParserConfig parserConfig)
{
- public override bool CanProcess(ExtractorType extractorType, string m3u8Url, string keyLine, string m3u8Content, ParserConfig paserConfig) => extractorType == ExtractorType.HLS;
+ var iv = ParserUtil.GetAttribute(keyLine, "IV");
+ var method = ParserUtil.GetAttribute(keyLine, "METHOD");
+ var uri = ParserUtil.GetAttribute(keyLine, "URI");
+ Logger.Debug("METHOD:{},URI:{},IV:{}", method, uri, iv);
- public override EncryptInfo Process(string keyLine, string m3u8Url, string m3u8Content, ParserConfig parserConfig)
+ var encryptInfo = new EncryptInfo(method);
+
+ // IV
+ if (!string.IsNullOrEmpty(iv))
{
- var iv = ParserUtil.GetAttribute(keyLine, "IV");
- var method = ParserUtil.GetAttribute(keyLine, "METHOD");
- var uri = ParserUtil.GetAttribute(keyLine, "URI");
+ encryptInfo.IV = HexUtil.HexToBytes(iv);
+ }
+ // 自定义IV
+ if (parserConfig.CustomeIV is { Length: > 0 })
+ {
+ encryptInfo.IV = parserConfig.CustomeIV;
+ }
- Logger.Debug("METHOD:{},URI:{},IV:{}", method, uri, iv);
-
- var encryptInfo = new EncryptInfo(method);
-
- //IV
- if (!string.IsNullOrEmpty(iv))
+ // KEY
+ try
+ {
+ if (parserConfig.CustomeKey is { Length: > 0 })
{
- encryptInfo.IV = HexUtil.HexToBytes(iv);
+ encryptInfo.Key = parserConfig.CustomeKey;
}
- //自定义IV
- if (parserConfig.CustomeIV != null && parserConfig.CustomeIV.Length > 0)
+ else if (uri.ToLower().StartsWith("base64:"))
{
- encryptInfo.IV = parserConfig.CustomeIV;
+ encryptInfo.Key = Convert.FromBase64String(uri[7..]);
}
-
- //KEY
- try
+ else if (uri.ToLower().StartsWith("data:;base64,"))
{
- if (parserConfig.CustomeKey != null && parserConfig.CustomeKey.Length > 0)
- {
- encryptInfo.Key = parserConfig.CustomeKey;
- }
- else if (uri.ToLower().StartsWith("base64:"))
- {
- encryptInfo.Key = Convert.FromBase64String(uri[7..]);
- }
- else if (uri.ToLower().StartsWith("data:;base64,"))
- {
- encryptInfo.Key = Convert.FromBase64String(uri[13..]);
- }
- else if (uri.ToLower().StartsWith("data:text/plain;base64,"))
- {
- encryptInfo.Key = Convert.FromBase64String(uri[23..]);
- }
- else if (File.Exists(uri))
- {
- encryptInfo.Key = File.ReadAllBytes(uri);
- }
- else if (!string.IsNullOrEmpty(uri))
- {
- var retryCount = parserConfig.KeyRetryCount;
- var segUrl = PreProcessUrl(ParserUtil.CombineURL(m3u8Url, uri), parserConfig);
+ encryptInfo.Key = Convert.FromBase64String(uri[13..]);
+ }
+ else if (uri.ToLower().StartsWith("data:text/plain;base64,"))
+ {
+ encryptInfo.Key = Convert.FromBase64String(uri[23..]);
+ }
+ else if (File.Exists(uri))
+ {
+ encryptInfo.Key = File.ReadAllBytes(uri);
+ }
+ else if (!string.IsNullOrEmpty(uri))
+ {
+ var retryCount = parserConfig.KeyRetryCount;
+ var segUrl = PreProcessUrl(ParserUtil.CombineURL(m3u8Url, uri), parserConfig);
getHttpKey:
- try
- {
- var bytes = HTTPUtil.GetBytesAsync(segUrl, parserConfig.Headers).Result;
- encryptInfo.Key = bytes;
- }
- catch (Exception _ex) when (!_ex.Message.Contains("scheme is not supported."))
- {
- Logger.WarnMarkUp($"[grey]{_ex.Message.EscapeMarkup()} retryCount: {retryCount}[/]");
- Thread.Sleep(1000);
- if (retryCount-- > 0) goto getHttpKey;
- else throw;
- }
- }
- }
- catch (Exception ex)
- {
- Logger.Error(ResString.cmd_loadKeyFailed + ": " + ex.Message);
- encryptInfo.Method = EncryptMethod.UNKNOWN;
- }
-
- //处理自定义加密方式
- if (parserConfig.CustomMethod != null)
- {
- encryptInfo.Method = parserConfig.CustomMethod.Value;
- Logger.Warn("METHOD changed from {} to {}", method, encryptInfo.Method);
- }
-
- return encryptInfo;
- }
-
- ///
- /// 预处理URL
- ///
- private string PreProcessUrl(string url, ParserConfig parserConfig)
- {
- foreach (var p in parserConfig.UrlProcessors)
- {
- if (p.CanProcess(ExtractorType.HLS, url, parserConfig))
+ try
{
- url = p.Process(url, parserConfig);
+ var bytes = HTTPUtil.GetBytesAsync(segUrl, parserConfig.Headers).Result;
+ encryptInfo.Key = bytes;
+ }
+ catch (Exception _ex) when (!_ex.Message.Contains("scheme is not supported."))
+ {
+ Logger.WarnMarkUp($"[grey]{_ex.Message.EscapeMarkup()} retryCount: {retryCount}[/]");
+ Thread.Sleep(1000);
+ if (retryCount-- > 0) goto getHttpKey;
+ throw;
}
}
-
- return url;
}
+ catch (Exception ex)
+ {
+ Logger.Error(ResString.cmd_loadKeyFailed + ": " + ex.Message);
+ encryptInfo.Method = EncryptMethod.UNKNOWN;
+ }
+
+ // 处理自定义加密方式
+ if (parserConfig.CustomMethod != null)
+ {
+ encryptInfo.Method = parserConfig.CustomMethod.Value;
+ Logger.Warn("METHOD changed from {} to {}", method, encryptInfo.Method);
+ }
+
+ return encryptInfo;
}
-}
+
+ ///
+ /// 预处理URL
+ ///
+ private string PreProcessUrl(string url, ParserConfig parserConfig)
+ {
+ foreach (var p in parserConfig.UrlProcessors)
+ {
+ if (p.CanProcess(ExtractorType.HLS, url, parserConfig))
+ {
+ url = p.Process(url, parserConfig);
+ }
+ }
+
+ return url;
+ }
+}
\ No newline at end of file
diff --git a/src/N_m3u8DL-RE.Parser/Processor/KeyProcessor.cs b/src/N_m3u8DL-RE.Parser/Processor/KeyProcessor.cs
index ec1438d..66dd612 100644
--- a/src/N_m3u8DL-RE.Parser/Processor/KeyProcessor.cs
+++ b/src/N_m3u8DL-RE.Parser/Processor/KeyProcessor.cs
@@ -7,11 +7,10 @@ using System.Linq;
using System.Text;
using System.Threading.Tasks;
-namespace N_m3u8DL_RE.Parser.Processor
+namespace N_m3u8DL_RE.Parser.Processor;
+
+public abstract class KeyProcessor
{
- public abstract class KeyProcessor
- {
- public abstract bool CanProcess(ExtractorType extractorType, string keyLine, string m3u8Url, string m3u8Content, ParserConfig parserConfig);
- public abstract EncryptInfo Process(string keyLine, string m3u8Url, string m3u8Content, ParserConfig parserConfig);
- }
-}
+ public abstract bool CanProcess(ExtractorType extractorType, string keyLine, string m3u8Url, string m3u8Content, ParserConfig parserConfig);
+ public abstract EncryptInfo Process(string keyLine, string m3u8Url, string m3u8Content, ParserConfig parserConfig);
+}
\ No newline at end of file
diff --git a/src/N_m3u8DL-RE.Parser/Processor/UrlProcessor.cs b/src/N_m3u8DL-RE.Parser/Processor/UrlProcessor.cs
index 6f3818e..1fa2f62 100644
--- a/src/N_m3u8DL-RE.Parser/Processor/UrlProcessor.cs
+++ b/src/N_m3u8DL-RE.Parser/Processor/UrlProcessor.cs
@@ -1,16 +1,10 @@
using N_m3u8DL_RE.Common.Enum;
using N_m3u8DL_RE.Parser.Config;
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
-namespace N_m3u8DL_RE.Parser.Processor
+namespace N_m3u8DL_RE.Parser.Processor;
+
+public abstract class UrlProcessor
{
- public abstract class UrlProcessor
- {
- public abstract bool CanProcess(ExtractorType extractorType, string oriUrl, ParserConfig parserConfig);
- public abstract string Process(string oriUrl, ParserConfig parserConfig);
- }
-}
+ public abstract bool CanProcess(ExtractorType extractorType, string oriUrl, ParserConfig parserConfig);
+ public abstract string Process(string oriUrl, ParserConfig parserConfig);
+}
\ No newline at end of file
diff --git a/src/N_m3u8DL-RE.Parser/StreamExtractor.cs b/src/N_m3u8DL-RE.Parser/StreamExtractor.cs
index 81215ca..a7ef235 100644
--- a/src/N_m3u8DL-RE.Parser/StreamExtractor.cs
+++ b/src/N_m3u8DL-RE.Parser/StreamExtractor.cs
@@ -6,144 +6,142 @@ using N_m3u8DL_RE.Parser.Constants;
using N_m3u8DL_RE.Parser.Extractor;
using N_m3u8DL_RE.Common.Util;
using N_m3u8DL_RE.Common.Enum;
-using Spectre.Console;
-namespace N_m3u8DL_RE.Parser
+namespace N_m3u8DL_RE.Parser;
+
+public class StreamExtractor
{
- public class StreamExtractor
+ public ExtractorType ExtractorType => extractor.ExtractorType;
+ private IExtractor extractor;
+ private ParserConfig parserConfig = new();
+ private string rawText;
+ private static SemaphoreSlim semaphore = new(1, 1);
+
+ public Dictionary RawFiles { get; set; } = new(); // 存储(文件名,文件内容)
+
+ public StreamExtractor()
{
- public ExtractorType ExtractorType { get => extractor.ExtractorType; }
- private IExtractor extractor;
- private ParserConfig parserConfig = new ParserConfig();
- private string rawText;
- private static SemaphoreSlim semaphore = new SemaphoreSlim(1, 1);
- public Dictionary RawFiles { get; set; } = new(); //存储(文件名,文件内容)
+ }
- public StreamExtractor()
+ public StreamExtractor(ParserConfig parserConfig)
+ {
+ this.parserConfig = parserConfig;
+ }
+
+ public async Task LoadSourceFromUrlAsync(string url)
+ {
+ Logger.Info(ResString.loadingUrl + url);
+ if (url.StartsWith("file:"))
{
+ var uri = new Uri(url);
+ this.rawText = await File.ReadAllTextAsync(uri.LocalPath);
+ parserConfig.OriginalUrl = parserConfig.Url = url;
+ }
+ else if (url.StartsWith("http"))
+ {
+ parserConfig.OriginalUrl = url;
+ (this.rawText, url) = await HTTPUtil.GetWebSourceAndNewUrlAsync(url, parserConfig.Headers);
+ parserConfig.Url = url;
+ }
+ else if (File.Exists(url))
+ {
+ url = Path.GetFullPath(url);
+ this.rawText = await File.ReadAllTextAsync(url);
+ parserConfig.OriginalUrl = parserConfig.Url = new Uri(url).AbsoluteUri;
+ }
+ this.rawText = rawText.Trim();
+ LoadSourceFromText(this.rawText);
+ }
+ public void LoadSourceFromText(string rawText)
+ {
+ var rawType = "txt";
+ rawText = rawText.Trim();
+ this.rawText = rawText;
+ if (rawText.StartsWith(HLSTags.ext_m3u))
+ {
+ Logger.InfoMarkUp(ResString.matchHLS);
+ extractor = new HLSExtractor(parserConfig);
+ rawType = "m3u8";
+ }
+ else if (rawText.Contains("") && rawText.Contains("") && rawText.Contains("
+ /// 开始解析流媒体信息
+ ///
+ ///
+ public async Task> ExtractStreamsAsync()
+ {
+ try
{
- this.parserConfig = parserConfig;
+ await semaphore.WaitAsync();
+ Logger.Info(ResString.parsingStream);
+ return await extractor.ExtractStreamsAsync(rawText);
}
-
- public async Task LoadSourceFromUrlAsync(string url)
+ finally
{
- Logger.Info(ResString.loadingUrl + url);
- if (url.StartsWith("file:"))
- {
- var uri = new Uri(url);
- this.rawText = await File.ReadAllTextAsync(uri.LocalPath);
- parserConfig.OriginalUrl = parserConfig.Url = url;
- }
- else if (url.StartsWith("http"))
- {
- parserConfig.OriginalUrl = url;
- (this.rawText, url) = await HTTPUtil.GetWebSourceAndNewUrlAsync(url, parserConfig.Headers);
- parserConfig.Url = url;
- }
- else if (File.Exists(url))
- {
- url = Path.GetFullPath(url);
- this.rawText = await File.ReadAllTextAsync(url);
- parserConfig.OriginalUrl = parserConfig.Url = new Uri(url).AbsoluteUri;
- }
- this.rawText = rawText.Trim();
- LoadSourceFromText(this.rawText);
- }
-
- public void LoadSourceFromText(string rawText)
- {
- var rawType = "txt";
- rawText = rawText.Trim();
- this.rawText = rawText;
- if (rawText.StartsWith(HLSTags.ext_m3u))
- {
- Logger.InfoMarkUp(ResString.matchHLS);
- extractor = new HLSExtractor(parserConfig);
- rawType = "m3u8";
- }
- else if (rawText.Contains("
") && rawText.Contains("") && rawText.Contains("
- /// 开始解析流媒体信息
- ///
- ///
- public async Task> ExtractStreamsAsync()
- {
- try
- {
- await semaphore.WaitAsync();
- Logger.Info(ResString.parsingStream);
- return await extractor.ExtractStreamsAsync(rawText);
- }
- finally
- {
- semaphore.Release();
- }
- }
-
- ///
- /// 根据规格说明填充媒体播放列表信息
- ///
- ///
- public async Task FetchPlayListAsync(List streamSpecs)
- {
- try
- {
- await semaphore.WaitAsync();
- Logger.Info(ResString.parsingStream);
- await extractor.FetchPlayListAsync(streamSpecs);
- }
- finally
- {
- semaphore.Release();
- }
- }
-
- public async Task RefreshPlayListAsync(List streamSpecs)
- {
- try
- {
- await semaphore.WaitAsync();
- await RetryUtil.WebRequestRetryAsync(async () =>
- {
- await extractor.RefreshPlayListAsync(streamSpecs);
- return true;
- }, retryDelayMilliseconds: 1000, maxRetries: 5);
- }
- finally
- {
- semaphore.Release();
- }
+ semaphore.Release();
}
}
-}
+
+ ///
+ /// 根据规格说明填充媒体播放列表信息
+ ///
+ ///
+ public async Task FetchPlayListAsync(List streamSpecs)
+ {
+ try
+ {
+ await semaphore.WaitAsync();
+ Logger.Info(ResString.parsingStream);
+ await extractor.FetchPlayListAsync(streamSpecs);
+ }
+ finally
+ {
+ semaphore.Release();
+ }
+ }
+
+ public async Task RefreshPlayListAsync(List streamSpecs)
+ {
+ try
+ {
+ await semaphore.WaitAsync();
+ await RetryUtil.WebRequestRetryAsync(async () =>
+ {
+ await extractor.RefreshPlayListAsync(streamSpecs);
+ return true;
+ }, retryDelayMilliseconds: 1000, maxRetries: 5);
+ }
+ finally
+ {
+ semaphore.Release();
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/N_m3u8DL-RE.Parser/Util/ParserUtil.cs b/src/N_m3u8DL-RE.Parser/Util/ParserUtil.cs
index 0215c77..4186e9b 100644
--- a/src/N_m3u8DL-RE.Parser/Util/ParserUtil.cs
+++ b/src/N_m3u8DL-RE.Parser/Util/ParserUtil.cs
@@ -1,126 +1,120 @@
using N_m3u8DL_RE.Parser.Constants;
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
using System.Text.RegularExpressions;
-using System.Threading.Tasks;
-namespace N_m3u8DL_RE.Parser.Util
+namespace N_m3u8DL_RE.Parser.Util;
+
+public partial class ParserUtil
{
- public partial class ParserUtil
+ [GeneratedRegex("\\$Number%([^$]+)d\\$")]
+ private static partial Regex VarsNumberRegex();
+
+ ///
+ /// 从以下文本中获取参数
+ /// #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=2149280,CODECS="mp4a.40.2,avc1.64001f",RESOLUTION=1280x720,NAME="720"
+ ///
+ /// 等待被解析的一行文本
+ /// 留空则获取第一个英文冒号后的全部字符
+ ///
+ public static string GetAttribute(string line, string key = "")
{
- [GeneratedRegex("\\$Number%([^$]+)d\\$")]
- private static partial Regex VarsNumberRegex();
+ line = line.Trim();
+ if (key == "")
+ return line[(line.IndexOf(':') + 1)..];
- ///
- /// 从以下文本中获取参数
- /// #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=2149280,CODECS="mp4a.40.2,avc1.64001f",RESOLUTION=1280x720,NAME="720"
- ///
- /// 等待被解析的一行文本
- /// 留空则获取第一个英文冒号后的全部字符
- ///
- public static string GetAttribute(string line, string key = "")
+ var index = -1;
+ var result = string.Empty;
+ if ((index = line.IndexOf(key + "=\"", StringComparison.Ordinal)) > -1)
{
- line = line.Trim();
- if (key == "")
- return line[(line.IndexOf(':') + 1)..];
-
- var index = -1;
- var result = string.Empty;
- if ((index = line.IndexOf(key + "=\"")) > -1)
- {
- var startIndex = index + (key + "=\"").Length;
- var endIndex = startIndex + line[startIndex..].IndexOf('\"');
- result = line[startIndex..endIndex];
- }
- else if ((index = line.IndexOf(key + "=")) > -1)
- {
- var startIndex = index + (key + "=").Length;
- var endIndex = startIndex + line[startIndex..].IndexOf(',');
- if (endIndex >= startIndex) result = line[startIndex..endIndex];
- else result = line[startIndex..];
- }
-
- return result;
+ var startIndex = index + (key + "=\"").Length;
+ var endIndex = startIndex + line[startIndex..].IndexOf('\"');
+ result = line[startIndex..endIndex];
+ }
+ else if ((index = line.IndexOf(key + "=", StringComparison.Ordinal)) > -1)
+ {
+ var startIndex = index + (key + "=").Length;
+ var endIndex = startIndex + line[startIndex..].IndexOf(',');
+ if (endIndex >= startIndex) result = line[startIndex..endIndex];
+ else result = line[startIndex..];
}
- ///
- /// 从如下文本中提取
- /// [@]
- ///
- ///
- /// n(length) o(start)
- public static (long, long?) GetRange(string input)
- {
- var t = input.Split('@');
- if (t.Length > 0)
- {
- if (t.Length == 1)
- {
- return (Convert.ToInt64(t[0]), null);
- }
- if (t.Length == 2)
- {
- return (Convert.ToInt64(t[0]), Convert.ToInt64(t[1]));
- }
- }
- return (0, null);
- }
-
- ///
- /// 从100-300这种字符串中获取StartRange, ExpectLength信息
- ///
- ///
- /// StartRange, ExpectLength
- public static (long, long) ParseRange(string range)
- {
- var start = Convert.ToInt64(range.Split('-')[0]);
- var end = Convert.ToInt64(range.Split('-')[1]);
- return (start, end - start + 1);
- }
-
- ///
- /// MPD SegmentTemplate替换
- ///
- ///
- ///
- ///
- public static string ReplaceVars(string text, Dictionary keyValuePairs)
- {
- foreach (var item in keyValuePairs)
- if (text.Contains(item.Key))
- text = text.Replace(item.Key, item.Value!.ToString());
-
- //处理特殊形式数字 如 $Number%05d$
- var regex = VarsNumberRegex();
- if (regex.IsMatch(text) && keyValuePairs.ContainsKey(DASHTags.TemplateNumber))
- {
- foreach (Match m in regex.Matches(text))
- {
- text = text.Replace(m.Value, keyValuePairs[DASHTags.TemplateNumber]?.ToString()?.PadLeft(Convert.ToInt32(m.Groups[1].Value), '0'));
- }
- }
-
- return text;
- }
-
- ///
- /// 拼接Baseurl和RelativeUrl
- ///
- /// Baseurl
- /// RelativeUrl
- ///
- public static string CombineURL(string baseurl, string url)
- {
- if (string.IsNullOrEmpty(baseurl))
- return url;
-
- Uri uri1 = new Uri(baseurl); //这里直接传完整的URL即可
- Uri uri2 = new Uri(uri1, url);
- url = uri2.ToString();
-
- return url;
- }
+ return result;
}
-}
+
+ ///
+ /// 从如下文本中提取
+ /// [@]
+ ///
+ ///
+ /// n(length) o(start)
+ public static (long, long?) GetRange(string input)
+ {
+ var t = input.Split('@');
+ if (t.Length > 0)
+ {
+ if (t.Length == 1)
+ {
+ return (Convert.ToInt64(t[0]), null);
+ }
+ if (t.Length == 2)
+ {
+ return (Convert.ToInt64(t[0]), Convert.ToInt64(t[1]));
+ }
+ }
+ return (0, null);
+ }
+
+ ///
+ /// 从100-300这种字符串中获取StartRange, ExpectLength信息
+ ///
+ ///
+ /// StartRange, ExpectLength
+ public static (long, long) ParseRange(string range)
+ {
+ var start = Convert.ToInt64(range.Split('-')[0]);
+ var end = Convert.ToInt64(range.Split('-')[1]);
+ return (start, end - start + 1);
+ }
+
+ ///
+ /// MPD SegmentTemplate替换
+ ///
+ ///
+ ///
+ ///
+ public static string ReplaceVars(string text, Dictionary keyValuePairs)
+ {
+ foreach (var item in keyValuePairs)
+ if (text.Contains(item.Key))
+ text = text.Replace(item.Key, item.Value!.ToString());
+
+ // 处理特殊形式数字 如 $Number%05d$
+ var regex = VarsNumberRegex();
+ if (regex.IsMatch(text) && keyValuePairs.TryGetValue(DASHTags.TemplateNumber, out var keyValuePair))
+ {
+ foreach (Match m in regex.Matches(text))
+ {
+ text = text.Replace(m.Value, keyValuePair?.ToString()?.PadLeft(Convert.ToInt32(m.Groups[1].Value), '0'));
+ }
+ }
+
+ return text;
+ }
+
+ ///
+ /// 拼接Baseurl和RelativeUrl
+ ///
+ /// Baseurl
+ /// RelativeUrl
+ ///
+ public static string CombineURL(string baseurl, string url)
+ {
+ if (string.IsNullOrEmpty(baseurl))
+ return url;
+
+ Uri uri1 = new Uri(baseurl); // 这里直接传完整的URL即可
+ Uri uri2 = new Uri(uri1, url);
+ url = uri2.ToString();
+
+ return url;
+ }
+}
\ No newline at end of file
diff --git a/src/N_m3u8DL-RE/Column/DownloadSpeedColumn.cs b/src/N_m3u8DL-RE/Column/DownloadSpeedColumn.cs
index cc0f522..565eef6 100644
--- a/src/N_m3u8DL-RE/Column/DownloadSpeedColumn.cs
+++ b/src/N_m3u8DL-RE/Column/DownloadSpeedColumn.cs
@@ -1,66 +1,48 @@
-using N_m3u8DL_RE.Common.Log;
-using N_m3u8DL_RE.Entity;
+using N_m3u8DL_RE.Entity;
using Spectre.Console;
using Spectre.Console.Rendering;
-using System;
using System.Collections.Concurrent;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
+using N_m3u8DL_RE.Common.Util;
-namespace N_m3u8DL_RE.Column
+namespace N_m3u8DL_RE.Column;
+
+internal sealed class DownloadSpeedColumn : ProgressColumn
{
- internal sealed class DownloadSpeedColumn : ProgressColumn
+ private long _stopSpeed = 0;
+ private ConcurrentDictionary DateTimeStringDic = new();
+ protected override bool NoWrap => true;
+ private ConcurrentDictionary SpeedContainerDic { get; set; }
+
+ public DownloadSpeedColumn(ConcurrentDictionary SpeedContainerDic)
{
- private long _stopSpeed = 0;
- private ConcurrentDictionary DateTimeStringDic = new();
- protected override bool NoWrap => true;
- private ConcurrentDictionary SpeedContainerDic { get; set; }
-
- public DownloadSpeedColumn(ConcurrentDictionary SpeedContainerDic)
- {
- this.SpeedContainerDic = SpeedContainerDic;
- }
-
- public Style MyStyle { get; set; } = new Style(foreground: Color.Green);
-
- public override IRenderable Render(RenderOptions options, ProgressTask task, TimeSpan deltaTime)
- {
- var taskId = task.Id;
- var speedContainer = SpeedContainerDic[taskId];
- var now = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
- var flag = task.IsFinished || !task.IsStarted;
- //单文件下载汇报进度
- if (!flag && speedContainer.SingleSegment && speedContainer.ResponseLength != null)
- {
- task.MaxValue = (double)speedContainer.ResponseLength;
- task.Value = speedContainer.RDownloaded;
- }
- //一秒汇报一次即可
- if (DateTimeStringDic.TryGetValue(taskId, out var oldTime) && oldTime != now && !flag)
- {
- speedContainer.NowSpeed = speedContainer.Downloaded;
- //速度为0,计数增加
- if (speedContainer.Downloaded <= _stopSpeed) { speedContainer.AddLowSpeedCount(); }
- else speedContainer.ResetLowSpeedCount();
- speedContainer.Reset();
- }
- DateTimeStringDic[taskId] = now;
- var style = flag ? Style.Plain : MyStyle;
- return flag ? new Text("-", style).Centered() : new Text(FormatFileSize(speedContainer.NowSpeed) + (speedContainer.LowSpeedCount > 0 ? $"({speedContainer.LowSpeedCount})" : ""), style).Centered();
- }
-
- private static string FormatFileSize(double fileSize)
- {
- return fileSize switch
- {
- < 0 => throw new ArgumentOutOfRangeException(nameof(fileSize)),
- >= 1024 * 1024 * 1024 => string.Format("{0:########0.00}GBps", (double)fileSize / (1024 * 1024 * 1024)),
- >= 1024 * 1024 => string.Format("{0:####0.00}MBps", (double)fileSize / (1024 * 1024)),
- >= 1024 => string.Format("{0:####0.00}KBps", (double)fileSize / 1024),
- _ => string.Format("{0:####0.00}Bps", fileSize)
- };
- }
+ this.SpeedContainerDic = SpeedContainerDic;
}
-}
+
+ public Style MyStyle { get; set; } = new Style(foreground: Color.Green);
+
+ public override IRenderable Render(RenderOptions options, ProgressTask task, TimeSpan deltaTime)
+ {
+ var taskId = task.Id;
+ var speedContainer = SpeedContainerDic[taskId];
+ var now = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
+ var flag = task.IsFinished || !task.IsStarted;
+ // 单文件下载汇报进度
+ if (!flag && speedContainer.SingleSegment && speedContainer.ResponseLength != null)
+ {
+ task.MaxValue = (double)speedContainer.ResponseLength;
+ task.Value = speedContainer.RDownloaded;
+ }
+ // 一秒汇报一次即可
+ if (DateTimeStringDic.TryGetValue(taskId, out var oldTime) && oldTime != now && !flag)
+ {
+ speedContainer.NowSpeed = speedContainer.Downloaded;
+ // 速度为0,计数增加
+ if (speedContainer.Downloaded <= _stopSpeed) { speedContainer.AddLowSpeedCount(); }
+ else speedContainer.ResetLowSpeedCount();
+ speedContainer.Reset();
+ }
+ DateTimeStringDic[taskId] = now;
+ var style = flag ? Style.Plain : MyStyle;
+ return flag ? new Text("-", style).Centered() : new Text(GlobalUtil.FormatFileSize(speedContainer.NowSpeed) + (speedContainer.LowSpeedCount > 0 ? $"({speedContainer.LowSpeedCount})" : ""), style).Centered();
+ }
+}
\ No newline at end of file
diff --git a/src/N_m3u8DL-RE/Column/DownloadStatusColumn.cs b/src/N_m3u8DL-RE/Column/DownloadStatusColumn.cs
index 60edcc4..cbedb9d 100644
--- a/src/N_m3u8DL-RE/Column/DownloadStatusColumn.cs
+++ b/src/N_m3u8DL-RE/Column/DownloadStatusColumn.cs
@@ -2,48 +2,42 @@
using N_m3u8DL_RE.Entity;
using Spectre.Console;
using Spectre.Console.Rendering;
-using System;
using System.Collections.Concurrent;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
-namespace N_m3u8DL_RE.Column
+namespace N_m3u8DL_RE.Column;
+
+internal class DownloadStatusColumn : ProgressColumn
{
- internal class DownloadStatusColumn : ProgressColumn
+ private ConcurrentDictionary SpeedContainerDic { get; set; }
+ private ConcurrentDictionary DateTimeStringDic = new();
+ private ConcurrentDictionary SizeDic = new();
+ public Style MyStyle { get; set; } = new Style(foreground: Color.DarkCyan);
+ public Style FinishedStyle { get; set; } = new Style(foreground: Color.Green);
+
+ public DownloadStatusColumn(ConcurrentDictionary speedContainerDic)
{
- private ConcurrentDictionary SpeedContainerDic { get; set; }
- private ConcurrentDictionary DateTimeStringDic = new();
- private ConcurrentDictionary SizeDic = new();
- public Style MyStyle { get; set; } = new Style(foreground: Color.DarkCyan);
- public Style FinishedStyle { get; set; } = new Style(foreground: Color.Green);
-
- public DownloadStatusColumn(ConcurrentDictionary speedContainerDic)
- {
- this.SpeedContainerDic = speedContainerDic;
- }
-
- public override IRenderable Render(RenderOptions options, ProgressTask task, TimeSpan deltaTime)
- {
- if (task.Value == 0) return new Text("-", MyStyle).RightJustified();
- var now = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
-
- var speedContainer = SpeedContainerDic[task.Id];
- var size = speedContainer.RDownloaded;
-
- //一秒汇报一次即可
- if (DateTimeStringDic.TryGetValue(task.Id, out var oldTime) && oldTime != now)
- {
- var totalSize = speedContainer.SingleSegment ? (speedContainer.ResponseLength ?? 0) : (long)(size / (task.Value / task.MaxValue));
- SizeDic[task.Id] = $"{GlobalUtil.FormatFileSize(size)}/{GlobalUtil.FormatFileSize(totalSize)}";
- }
- DateTimeStringDic[task.Id] = now;
- SizeDic.TryGetValue(task.Id, out var sizeStr);
-
- if (task.IsFinished) sizeStr = GlobalUtil.FormatFileSize(size);
-
- return new Text(sizeStr ?? "-", MyStyle).RightJustified();
- }
+ this.SpeedContainerDic = speedContainerDic;
}
-}
+
+ public override IRenderable Render(RenderOptions options, ProgressTask task, TimeSpan deltaTime)
+ {
+ if (task.Value == 0) return new Text("-", MyStyle).RightJustified();
+ var now = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
+
+ var speedContainer = SpeedContainerDic[task.Id];
+ var size = speedContainer.RDownloaded;
+
+ // 一秒汇报一次即可
+ if (DateTimeStringDic.TryGetValue(task.Id, out var oldTime) && oldTime != now)
+ {
+ var totalSize = speedContainer.SingleSegment ? (speedContainer.ResponseLength ?? 0) : (long)(size / (task.Value / task.MaxValue));
+ SizeDic[task.Id] = $"{GlobalUtil.FormatFileSize(size)}/{GlobalUtil.FormatFileSize(totalSize)}";
+ }
+ DateTimeStringDic[task.Id] = now;
+ SizeDic.TryGetValue(task.Id, out var sizeStr);
+
+ if (task.IsFinished) sizeStr = GlobalUtil.FormatFileSize(size);
+
+ return new Text(sizeStr ?? "-", MyStyle).RightJustified();
+ }
+}
\ No newline at end of file
diff --git a/src/N_m3u8DL-RE/Column/MyPercentageColumn.cs b/src/N_m3u8DL-RE/Column/MyPercentageColumn.cs
index 849821b..2c74aae 100644
--- a/src/N_m3u8DL-RE/Column/MyPercentageColumn.cs
+++ b/src/N_m3u8DL-RE/Column/MyPercentageColumn.cs
@@ -1,31 +1,25 @@
using Spectre.Console.Rendering;
using Spectre.Console;
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
-namespace N_m3u8DL_RE.Column
+namespace N_m3u8DL_RE.Column;
+
+internal class MyPercentageColumn : ProgressColumn
{
- internal class MyPercentageColumn : ProgressColumn
+ ///
+ /// Gets or sets the style for a non-complete task.
+ ///
+ public Style Style { get; set; } = Style.Plain;
+
+ ///
+ /// Gets or sets the style for a completed task.
+ ///
+ public Style CompletedStyle { get; set; } = new Style(foreground: Color.Green);
+
+ ///
+ public override IRenderable Render(RenderOptions options, ProgressTask task, TimeSpan deltaTime)
{
- ///
- /// Gets or sets the style for a non-complete task.
- ///
- public Style Style { get; set; } = Style.Plain;
-
- ///
- /// Gets or sets the style for a completed task.
- ///
- public Style CompletedStyle { get; set; } = new Style(foreground: Color.Green);
-
- ///
- public override IRenderable Render(RenderOptions options, ProgressTask task, TimeSpan deltaTime)
- {
- var percentage = task.Percentage;
- var style = percentage == 100 ? CompletedStyle : Style ?? Style.Plain;
- return new Text($"{task.Value}/{task.MaxValue} {percentage:F2}%", style).RightJustified();
- }
+ var percentage = task.Percentage;
+ var style = percentage == 100 ? CompletedStyle : Style ?? Style.Plain;
+ return new Text($"{task.Value}/{task.MaxValue} {percentage:F2}%", style).RightJustified();
}
-}
+}
\ No newline at end of file
diff --git a/src/N_m3u8DL-RE/Column/RecordingDurationColumn.cs b/src/N_m3u8DL-RE/Column/RecordingDurationColumn.cs
index 461e834..4bb3190 100644
--- a/src/N_m3u8DL-RE/Column/RecordingDurationColumn.cs
+++ b/src/N_m3u8DL-RE/Column/RecordingDurationColumn.cs
@@ -1,39 +1,33 @@
using N_m3u8DL_RE.Common.Util;
using Spectre.Console;
using Spectre.Console.Rendering;
-using System;
using System.Collections.Concurrent;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
-namespace N_m3u8DL_RE.Column
+namespace N_m3u8DL_RE.Column;
+
+internal class RecordingDurationColumn : ProgressColumn
{
- internal class RecordingDurationColumn : ProgressColumn
+ protected override bool NoWrap => true;
+ private ConcurrentDictionary _recodingDurDic;
+ private ConcurrentDictionary? _refreshedDurDic;
+ public Style GreyStyle { get; set; } = new Style(foreground: Color.Grey);
+ public Style MyStyle { get; set; } = new Style(foreground: Color.DarkGreen);
+ public RecordingDurationColumn(ConcurrentDictionary recodingDurDic)
{
- protected override bool NoWrap => true;
- private ConcurrentDictionary _recodingDurDic;
- private ConcurrentDictionary? _refreshedDurDic;
- public Style GreyStyle { get; set; } = new Style(foreground: Color.Grey);
- public Style MyStyle { get; set; } = new Style(foreground: Color.DarkGreen);
- public RecordingDurationColumn(ConcurrentDictionary recodingDurDic)
- {
- _recodingDurDic = recodingDurDic;
- }
- public RecordingDurationColumn(ConcurrentDictionary recodingDurDic, ConcurrentDictionary refreshedDurDic)
- {
- _recodingDurDic = recodingDurDic;
- _refreshedDurDic = refreshedDurDic;
- }
- public override IRenderable Render(RenderOptions options, ProgressTask task, TimeSpan deltaTime)
- {
- if (_refreshedDurDic == null)
- return new Text($"{GlobalUtil.FormatTime(_recodingDurDic[task.Id])}", MyStyle).LeftJustified();
- else
- {
- return new Text($"{GlobalUtil.FormatTime(_recodingDurDic[task.Id])}/{GlobalUtil.FormatTime(_refreshedDurDic[task.Id])}", GreyStyle);
- }
+ _recodingDurDic = recodingDurDic;
+ }
+ public RecordingDurationColumn(ConcurrentDictionary recodingDurDic, ConcurrentDictionary refreshedDurDic)
+ {
+ _recodingDurDic = recodingDurDic;
+ _refreshedDurDic = refreshedDurDic;
+ }
+ public override IRenderable Render(RenderOptions options, ProgressTask task, TimeSpan deltaTime)
+ {
+ if (_refreshedDurDic == null)
+ return new Text($"{GlobalUtil.FormatTime(_recodingDurDic[task.Id])}", MyStyle).LeftJustified();
+ else
+ {
+ return new Text($"{GlobalUtil.FormatTime(_recodingDurDic[task.Id])}/{GlobalUtil.FormatTime(_refreshedDurDic[task.Id])}", GreyStyle);
}
}
-}
+}
\ No newline at end of file
diff --git a/src/N_m3u8DL-RE/Column/RecordingSizeColumn.cs b/src/N_m3u8DL-RE/Column/RecordingSizeColumn.cs
index 21b76f4..9d56934 100644
--- a/src/N_m3u8DL-RE/Column/RecordingSizeColumn.cs
+++ b/src/N_m3u8DL-RE/Column/RecordingSizeColumn.cs
@@ -1,39 +1,32 @@
using N_m3u8DL_RE.Common.Util;
-using N_m3u8DL_RE.Entity;
using Spectre.Console;
using Spectre.Console.Rendering;
-using System;
using System.Collections.Concurrent;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
-namespace N_m3u8DL_RE.Column
+namespace N_m3u8DL_RE.Column;
+
+internal class RecordingSizeColumn : ProgressColumn
{
- internal class RecordingSizeColumn : ProgressColumn
+ protected override bool NoWrap => true;
+ private ConcurrentDictionary RecodingSizeDic = new(); // 临时的大小 每秒刷新用
+ private ConcurrentDictionary _recodingSizeDic;
+ private ConcurrentDictionary DateTimeStringDic = new();
+ public Style MyStyle { get; set; } = new Style(foreground: Color.DarkCyan);
+ public RecordingSizeColumn(ConcurrentDictionary recodingSizeDic)
{
- protected override bool NoWrap => true;
- private ConcurrentDictionary RecodingSizeDic = new(); //临时的大小 每秒刷新用
- private ConcurrentDictionary _recodingSizeDic;
- private ConcurrentDictionary DateTimeStringDic = new();
- public Style MyStyle { get; set; } = new Style(foreground: Color.DarkCyan);
- public RecordingSizeColumn(ConcurrentDictionary recodingSizeDic)
- {
- _recodingSizeDic = recodingSizeDic;
- }
- public override IRenderable Render(RenderOptions options, ProgressTask task, TimeSpan deltaTime)
- {
- var now = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
- var taskId = task.Id;
- //一秒汇报一次即可
- if (DateTimeStringDic.TryGetValue(taskId, out var oldTime) && oldTime != now)
- {
- RecodingSizeDic[task.Id] = _recodingSizeDic[task.Id];
- }
- DateTimeStringDic[taskId] = now;
- var flag = RecodingSizeDic.TryGetValue(taskId, out var size);
- return new Text(GlobalUtil.FormatFileSize(flag ? size : 0), MyStyle).LeftJustified();
- }
+ _recodingSizeDic = recodingSizeDic;
}
-}
+ public override IRenderable Render(RenderOptions options, ProgressTask task, TimeSpan deltaTime)
+ {
+ var now = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
+ var taskId = task.Id;
+ // 一秒汇报一次即可
+ if (DateTimeStringDic.TryGetValue(taskId, out var oldTime) && oldTime != now)
+ {
+ RecodingSizeDic[task.Id] = _recodingSizeDic[task.Id];
+ }
+ DateTimeStringDic[taskId] = now;
+ var flag = RecodingSizeDic.TryGetValue(taskId, out var size);
+ return new Text(GlobalUtil.FormatFileSize(flag ? size : 0), MyStyle).LeftJustified();
+ }
+}
\ No newline at end of file
diff --git a/src/N_m3u8DL-RE/Column/RecordingStatusColumn.cs b/src/N_m3u8DL-RE/Column/RecordingStatusColumn.cs
index aa0a057..2925d22 100644
--- a/src/N_m3u8DL-RE/Column/RecordingStatusColumn.cs
+++ b/src/N_m3u8DL-RE/Column/RecordingStatusColumn.cs
@@ -1,23 +1,17 @@
using Spectre.Console;
using Spectre.Console.Rendering;
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
-namespace N_m3u8DL_RE.Column
+namespace N_m3u8DL_RE.Column;
+
+internal class RecordingStatusColumn : ProgressColumn
{
- internal class RecordingStatusColumn : ProgressColumn
+ protected override bool NoWrap => true;
+ public Style MyStyle { get; set; } = new Style(foreground: Color.Default);
+ public Style FinishedStyle { get; set; } = new Style(foreground: Color.Yellow);
+ public override IRenderable Render(RenderOptions options, ProgressTask task, TimeSpan deltaTime)
{
- protected override bool NoWrap => true;
- public Style MyStyle { get; set; } = new Style(foreground: Color.Default);
- public Style FinishedStyle { get; set; } = new Style(foreground: Color.Yellow);
- public override IRenderable Render(RenderOptions options, ProgressTask task, TimeSpan deltaTime)
- {
- if (task.IsFinished)
- return new Text($"{task.Value}/{task.MaxValue} Waiting ", FinishedStyle).LeftJustified();
- return new Text($"{task.Value}/{task.MaxValue} Recording", MyStyle).LeftJustified();
- }
+ if (task.IsFinished)
+ return new Text($"{task.Value}/{task.MaxValue} Waiting ", FinishedStyle).LeftJustified();
+ return new Text($"{task.Value}/{task.MaxValue} Recording", MyStyle).LeftJustified();
}
-}
+}
\ No newline at end of file
diff --git a/src/N_m3u8DL-RE/CommandLine/CommandInvoker.cs b/src/N_m3u8DL-RE/CommandLine/CommandInvoker.cs
index fbb78b8..23915ef 100644
--- a/src/N_m3u8DL-RE/CommandLine/CommandInvoker.cs
+++ b/src/N_m3u8DL-RE/CommandLine/CommandInvoker.cs
@@ -5,7 +5,6 @@ using N_m3u8DL_RE.Common.Util;
using N_m3u8DL_RE.Entity;
using N_m3u8DL_RE.Enum;
using N_m3u8DL_RE.Util;
-using NiL.JS.Expressions;
using System.CommandLine;
using System.CommandLine.Binding;
using System.CommandLine.Builder;
@@ -14,630 +13,629 @@ using System.Globalization;
using System.Net;
using System.Text.RegularExpressions;
-namespace N_m3u8DL_RE.CommandLine
+namespace N_m3u8DL_RE.CommandLine;
+
+internal partial class CommandInvoker
{
- internal partial class CommandInvoker
+ public const string VERSION_INFO = "N_m3u8DL-RE (Beta version) 20241110";
+
+ [GeneratedRegex("((best|worst)\\d*|all)")]
+ private static partial Regex ForStrRegex();
+ [GeneratedRegex("(\\d*)-(\\d*)")]
+ private static partial Regex RangeRegex();
+
+ private static readonly Argument Input = new(name: "input", description: ResString.cmd_Input);
+ private static readonly Option TmpDir = new(["--tmp-dir"], description: ResString.cmd_tmpDir);
+ private static readonly Option SaveDir = new(["--save-dir"], description: ResString.cmd_saveDir);
+ private static readonly Option SaveName = new(["--save-name"], description: ResString.cmd_saveName, parseArgument: ParseSaveName);
+ private static readonly Option SavePattern = new(["--save-pattern"], description: ResString.cmd_savePattern, getDefaultValue: () => "____");
+ private static readonly Option UILanguage = new Option(["--ui-language"], description: ResString.cmd_uiLanguage).FromAmong("en-US", "zh-CN", "zh-TW");
+ private static readonly Option UrlProcessorArgs = new(["--urlprocessor-args"], description: ResString.cmd_urlProcessorArgs);
+ private static readonly Option Keys = new(["--key"], description: ResString.cmd_keys) { Arity = ArgumentArity.OneOrMore, AllowMultipleArgumentsPerToken = false };
+ private static readonly Option KeyTextFile = new(["--key-text-file"], description: ResString.cmd_keyText);
+ private static readonly Option> Headers = new(["-H", "--header"], description: ResString.cmd_header, parseArgument: ParseHeaders) { Arity = ArgumentArity.OneOrMore, AllowMultipleArgumentsPerToken = false };
+ private static readonly Option LogLevel = new(name: "--log-level", description: ResString.cmd_logLevel, getDefaultValue: () => Common.Log.LogLevel.INFO);
+ private static readonly Option SubtitleFormat = new(name: "--sub-format", description: ResString.cmd_subFormat, getDefaultValue: () => Enum.SubtitleFormat.SRT);
+ private static readonly Option AutoSelect = new(["--auto-select"], description: ResString.cmd_autoSelect, getDefaultValue: () => false);
+ private static readonly Option SubOnly = new(["--sub-only"], description: ResString.cmd_subOnly, getDefaultValue: () => false);
+ private static readonly Option ThreadCount = new(["--thread-count"], description: ResString.cmd_threadCount, getDefaultValue: () => Environment.ProcessorCount) { ArgumentHelpName = "number" };
+ private static readonly Option DownloadRetryCount = new(["--download-retry-count"], description: ResString.cmd_downloadRetryCount, getDefaultValue: () => 3) { ArgumentHelpName = "number" };
+ private static readonly Option SkipMerge = new(["--skip-merge"], description: ResString.cmd_skipMerge, getDefaultValue: () => false);
+ private static readonly Option SkipDownload = new(["--skip-download"], description: ResString.cmd_skipDownload, getDefaultValue: () => false);
+ private static readonly Option NoDateInfo = new(["--no-date-info"], description: ResString.cmd_noDateInfo, getDefaultValue: () => false);
+ private static readonly Option BinaryMerge = new(["--binary-merge"], description: ResString.cmd_binaryMerge, getDefaultValue: () => false);
+ private static readonly Option UseFFmpegConcatDemuxer = new(["--use-ffmpeg-concat-demuxer"], description: ResString.cmd_useFFmpegConcatDemuxer, getDefaultValue: () => false);
+ private static readonly Option