diff --git a/src/N_m3u8DL-RE.Common/Resource/ResString.Designer.cs b/src/N_m3u8DL-RE.Common/Resource/ResString.Designer.cs index 87aa6af..b031a27 100644 --- a/src/N_m3u8DL-RE.Common/Resource/ResString.Designer.cs +++ b/src/N_m3u8DL-RE.Common/Resource/ResString.Designer.cs @@ -170,6 +170,15 @@ namespace N_m3u8DL_RE.Common.Resource { } } + /// + /// 查找类似 Decrypt MP4 segments in real time 的本地化字符串。 + /// + public static string cmd_MP4RealTimeDecryption { + get { + return ResourceManager.GetString("cmd_MP4RealTimeDecryption", resourceCulture); + } + } + /// /// 查找类似 Set output directory 的本地化字符串。 /// diff --git a/src/N_m3u8DL-RE.Common/Resource/ResString.resx b/src/N_m3u8DL-RE.Common/Resource/ResString.resx index 757f282..9fd5184 100644 --- a/src/N_m3u8DL-RE.Common/Resource/ResString.resx +++ b/src/N_m3u8DL-RE.Common/Resource/ResString.resx @@ -237,4 +237,7 @@ Give these arguments to the URL Processors. + + Decrypt MP4 segments in real time + \ No newline at end of file diff --git a/src/N_m3u8DL-RE.Common/Resource/ResString.zh-Hans.resx b/src/N_m3u8DL-RE.Common/Resource/ResString.zh-Hans.resx index 67bac30..2f4b653 100644 --- a/src/N_m3u8DL-RE.Common/Resource/ResString.zh-Hans.resx +++ b/src/N_m3u8DL-RE.Common/Resource/ResString.zh-Hans.resx @@ -257,4 +257,7 @@ 此字符串将直接传递给URL Processor + + 实时解密MP4分片 + \ No newline at end of file diff --git a/src/N_m3u8DL-RE.Common/Resource/ResString.zh-Hant.resx b/src/N_m3u8DL-RE.Common/Resource/ResString.zh-Hant.resx index c1a51ce..1ab99c0 100644 --- a/src/N_m3u8DL-RE.Common/Resource/ResString.zh-Hant.resx +++ b/src/N_m3u8DL-RE.Common/Resource/ResString.zh-Hant.resx @@ -234,4 +234,7 @@ 此字符串將直接傳遞給URL Processor + + 實時解密MP4分片 + \ 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 03e33d8..07b1b75 100644 --- a/src/N_m3u8DL-RE/CommandLine/CommandInvoker.cs +++ b/src/N_m3u8DL-RE/CommandLine/CommandInvoker.cs @@ -12,8 +12,8 @@ namespace N_m3u8DL_RE.CommandLine { private readonly static Argument Input = new(name: "input", description: ResString.cmd_Input); private readonly static Option TmpDir = new(new string[] { "--tmp-dir" }, description: ResString.cmd_tmpDir); - private readonly static Option SaveDir = new(new string[] { "--save-dir", "-o" }, description: ResString.cmd_saveDir); - private readonly static Option SaveName = new(new string[] { "--save-name", "-O" }, description: ResString.cmd_saveName); + private readonly static Option SaveDir = new(new string[] { "--save-dir" }, description: ResString.cmd_saveDir); + private readonly static Option SaveName = new(new string[] { "--save-name" }, description: ResString.cmd_saveName); private readonly static Option SavePattern = new(new string[] { "--save-pattern" }, description: ResString.cmd_savePattern, getDefaultValue: () => "____"); private readonly static Option UILanguage = new(new string[] { "--ui-language" }, description: ResString.cmd_uiLanguage); private readonly static Option UrlProcessorArgs = new(new string[] { "--urlprocessor-args" }, description: ResString.cmd_urlProcessorArgs); @@ -32,6 +32,7 @@ namespace N_m3u8DL_RE.CommandLine private readonly static Option CheckSegmentsCount = new(new string[] { "--check-segments-count" }, description: ResString.cmd_checkSegmentsCount, getDefaultValue: () => true); private readonly static Option WriteMetaJson = new(new string[] { "--write-meta-json" }, description: ResString.cmd_writeMetaJson, getDefaultValue: () => true); private readonly static Option AppendUrlParams = new(new string[] { "--append-url-params" }, description: ResString.cmd_appendUrlParams, getDefaultValue: () => false); + private readonly static Option MP4RealTimeDecryption = new (new string[] { "--mp4-real-time-decryption" }, description: ResString.cmd_MP4RealTimeDecryption, getDefaultValue: () => false); class MyOptionBinder : BinderBase { @@ -61,6 +62,7 @@ namespace N_m3u8DL_RE.CommandLine SavePattern = bindingContext.ParseResult.GetValueForOption(SavePattern), Keys = bindingContext.ParseResult.GetValueForOption(Keys), UrlProcessorArgs = bindingContext.ParseResult.GetValueForOption(UrlProcessorArgs), + MP4RealTimeDecryption = bindingContext.ParseResult.GetValueForOption(MP4RealTimeDecryption), }; //在这里设置语言 @@ -85,11 +87,11 @@ namespace N_m3u8DL_RE.CommandLine public static async Task InvokeArgs(string[] args, Func action) { - var rootCommand = new RootCommand("N_m3u8DL-RE (Beta version) 20220721") + var rootCommand = new RootCommand("N_m3u8DL-RE (Beta version) 20220723") { Input, TmpDir, SaveDir, SaveName, ThreadCount, AutoSelect, SkipMerge, SkipDownload, CheckSegmentsCount, BinaryMerge, DelAfterDone, WriteMetaJson, AppendUrlParams, Keys, Headers, /**SavePattern,**/ SubOnly, SubtitleFormat, AutoSubtitleFix, - LogLevel, UILanguage, UrlProcessorArgs + LogLevel, UILanguage, UrlProcessorArgs, MP4RealTimeDecryption }; rootCommand.TreatUnmatchedTokensAsErrors = true; rootCommand.SetHandler(async (myOption) => await action(myOption), new MyOptionBinder()); diff --git a/src/N_m3u8DL-RE/CommandLine/MyOption.cs b/src/N_m3u8DL-RE/CommandLine/MyOption.cs index 194624f..18da283 100644 --- a/src/N_m3u8DL-RE/CommandLine/MyOption.cs +++ b/src/N_m3u8DL-RE/CommandLine/MyOption.cs @@ -70,6 +70,10 @@ namespace N_m3u8DL_RE.CommandLine /// public bool AppendUrlParams { get; set; } /// + /// See: . + /// + public bool MP4RealTimeDecryption { get; set; } + /// /// See: . /// public SubtitleFormat SubtitleFormat { get; set; } diff --git a/src/N_m3u8DL-RE/Config/DownloaderConfig.cs b/src/N_m3u8DL-RE/Config/DownloaderConfig.cs index 6541d79..f820ad4 100644 --- a/src/N_m3u8DL-RE/Config/DownloaderConfig.cs +++ b/src/N_m3u8DL-RE/Config/DownloaderConfig.cs @@ -27,6 +27,7 @@ namespace N_m3u8DL_RE.Config ThreadCount = option.ThreadCount; SavePattern = option.SavePattern; Keys = option.Keys; + MP4RealTimeDecryption = option.MP4RealTimeDecryption; } /// @@ -74,6 +75,10 @@ namespace N_m3u8DL_RE.Config /// public bool AutoSubtitleFix { get; set; } = true; /// + /// MP4实时解密 + /// + public bool MP4RealTimeDecryption { get; set; } = true; + /// /// 字幕格式 /// public SubtitleFormat SubtitleFormat { get; set; } = SubtitleFormat.VTT; diff --git a/src/N_m3u8DL-RE/DownloadManager/SimpleDownloadManager.cs b/src/N_m3u8DL-RE/DownloadManager/SimpleDownloadManager.cs index 7ce30b2..3e9cce5 100644 --- a/src/N_m3u8DL-RE/DownloadManager/SimpleDownloadManager.cs +++ b/src/N_m3u8DL-RE/DownloadManager/SimpleDownloadManager.cs @@ -51,6 +51,15 @@ namespace N_m3u8DL_RE.DownloadManager var headers = DownloaderConfig.Headers; var output = Path.Combine(saveDir, saveName + $".{streamSpec.Extension ?? "ts"}"); + //mp4decrypt + var APP_DIR = Path.GetDirectoryName(Environment.ProcessPath)!; + var fileName = "mp4decrypt"; + if (Environment.OSVersion.Platform == PlatformID.Win32NT) + fileName += ".exe"; + var mp4decrypt = Path.Combine(APP_DIR, fileName); + if (!File.Exists(mp4decrypt)) mp4decrypt = fileName; + var mp4InitFile = ""; + Logger.Debug($"dirName: {dirName}; tmpDir: {tmpDir}; saveDir: {saveDir}; saveName: {saveName}; output: {output}"); //创建文件夹 @@ -76,12 +85,20 @@ namespace N_m3u8DL_RE.DownloadManager var path = Path.Combine(tmpDir, "_init.mp4.tmp"); var result = await Downloader.DownloadSegmentAsync(streamSpec.Playlist.MediaInit, path, headers); FileDic[streamSpec.Playlist.MediaInit] = result; + if (result == null) + { + throw new Exception("Download init file failed!"); + } + mp4InitFile = result.ActualFilePath; task.Increment(1); + //修改输出后缀 if (streamSpec.MediaType == Common.Enum.MediaType.AUDIO) output = Path.ChangeExtension(output, ".m4a"); else output = Path.ChangeExtension(output, ".mp4"); + + //读取mp4信息 if (result != null && result.Success) { var data = File.ReadAllBytes(result.ActualFilePath); @@ -89,6 +106,17 @@ namespace N_m3u8DL_RE.DownloadManager if (info.Scheme != null) Logger.WarnMarkUp($"[grey]Type: {info.Scheme}[/]"); if (info.PSSH != null) Logger.WarnMarkUp($"[grey]PSSH(WV): {info.PSSH}[/]"); if (info.KID != null) Logger.WarnMarkUp($"[grey]KID: {info.KID}[/]"); + //实时解密 + if (DownloaderConfig.MP4RealTimeDecryption && streamSpec.Playlist.MediaInit.EncryptInfo.Method != Common.Enum.EncryptMethod.NONE) + { + var enc = result.ActualFilePath; + var dec = Path.Combine(Path.GetDirectoryName(enc)!, Path.GetFileNameWithoutExtension(enc) + "_dec" + Path.GetExtension(enc)); + var dResult = await MP4DecryptUtil.DecryptAsync(mp4decrypt, DownloaderConfig.Keys, enc, dec); + if (dResult) + { + result.ActualFilePath = dec; + } + } } } @@ -105,8 +133,25 @@ namespace N_m3u8DL_RE.DownloadManager var result = await Downloader.DownloadSegmentAsync(seg, path, headers); FileDic[seg] = result; task.Increment(1); + //实时解密 + if (DownloaderConfig.MP4RealTimeDecryption && seg.EncryptInfo.Method != Common.Enum.EncryptMethod.NONE && result != null) + { + var enc = result.ActualFilePath; + var dec = Path.Combine(Path.GetDirectoryName(enc)!, Path.GetFileNameWithoutExtension(enc) + "_dec" + Path.GetExtension(enc)); + var dResult = await MP4DecryptUtil.DecryptAsync(mp4decrypt, DownloaderConfig.Keys, enc, dec, mp4InitFile); + if (dResult) + { + File.Delete(enc); + result.ActualFilePath = dec; + } + } }); + if (DownloaderConfig.MP4RealTimeDecryption && mp4InitFile != "") + { + File.Delete(mp4InitFile); + } + //校验分片数量 if (DownloaderConfig.CheckSegmentsCount && FileDic.Values.Any(s => s == null)) { @@ -313,30 +358,16 @@ namespace N_m3u8DL_RE.DownloadManager } } - if (DownloaderConfig.Keys != null && DownloaderConfig.Keys.Length > 0) + //调用mp4decrypt解密 + if (!DownloaderConfig.MP4RealTimeDecryption && DownloaderConfig.Keys != null && DownloaderConfig.Keys.Length > 0) { - var APP_DIR = Path.GetDirectoryName(Environment.ProcessPath)!; - var fileName = "mp4decrypt"; - if (Environment.OSVersion.Platform == PlatformID.Win32NT) - fileName += ".exe"; - var mp4decrypt = Path.Combine(APP_DIR, fileName); - if (!File.Exists(mp4decrypt)) mp4decrypt = fileName; - if (streamSpec.Playlist!.MediaParts.First().MediaSegments.First().EncryptInfo != null - && streamSpec.Playlist!.MediaParts.First().MediaSegments.First().EncryptInfo.Method != Common.Enum.EncryptMethod.NONE) + if (totalCount > 1 && streamSpec.Playlist!.MediaParts.First().MediaSegments.First().EncryptInfo.Method != Common.Enum.EncryptMethod.NONE) { var enc = output; var dec = Path.Combine(Path.GetDirectoryName(enc)!, Path.GetFileNameWithoutExtension(enc) + "_dec" + Path.GetExtension(enc)); - var cmd = string.Join(" ", DownloaderConfig.Keys.Select(k => $"--key {k}")) + $" \"{enc}\" \"{dec}\""; Logger.InfoMarkUp($"[grey]Decrypting...[/]"); - await Process.Start(new ProcessStartInfo() - { - FileName = mp4decrypt, - Arguments = cmd, - RedirectStandardOutput = true, - RedirectStandardError = true, - UseShellExecute = false - })!.WaitForExitAsync(); - if (File.Exists(dec) && new FileInfo(dec).Length > 0) + var result = await MP4DecryptUtil.DecryptAsync(mp4decrypt, DownloaderConfig.Keys, enc, dec); + if (result) { File.Delete(enc); output = dec; diff --git a/src/N_m3u8DL-RE/Util/MP4DecryptUtil.cs b/src/N_m3u8DL-RE/Util/MP4DecryptUtil.cs new file mode 100644 index 0000000..79729ad --- /dev/null +++ b/src/N_m3u8DL-RE/Util/MP4DecryptUtil.cs @@ -0,0 +1,41 @@ +using N_m3u8DL_RE.Config; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace N_m3u8DL_RE.Util +{ + internal class MP4DecryptUtil + { + public static async Task DecryptAsync(string bin, string[]? keys, string source, string dest, string init = "") + { + if (keys == null || keys.Length == 0) return false; + + var cmd = string.Join(" ", keys.Select(k => $"--key {k}")); + if (init != "") + { + cmd += $" --fragments-info \"{init}\" "; + } + cmd += $" \"{source}\" \"{dest}\""; + + await Process.Start(new ProcessStartInfo() + { + FileName = bin, + Arguments = cmd, + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false + })!.WaitForExitAsync(); + + if (File.Exists(dest) && new FileInfo(dest).Length > 0) + { + return true; + } + + return false; + } + } +}