diff --git a/src/N_m3u8DL-RE.Common/Resource/ResString.Designer.cs b/src/N_m3u8DL-RE.Common/Resource/ResString.Designer.cs index cda11d3..aed0669 100644 --- a/src/N_m3u8DL-RE.Common/Resource/ResString.Designer.cs +++ b/src/N_m3u8DL-RE.Common/Resource/ResString.Designer.cs @@ -60,6 +60,15 @@ namespace N_m3u8DL_RE.Common.Resource { } } + /// + /// 查找类似 fMP4 is detected, binary merging is automatically enabled 的本地化字符串。 + /// + public static string autoBinaryMerge { + get { + return ResourceManager.GetString("autoBinaryMerge", resourceCulture); + } + } + /// /// 查找类似 Bad m3u8 的本地化字符串。 /// @@ -141,6 +150,15 @@ namespace N_m3u8DL_RE.Common.Resource { } } + /// + /// 查找类似 Full path to the ffmpeg binary, like C:\Tools\ffmpeg.exe 的本地化字符串。 + /// + public static string cmd_ffmpegBinaryPath { + get { + return ResourceManager.GetString("cmd_ffmpegBinaryPath", resourceCulture); + } + } + /// /// 查找类似 Pass custom header(s) to server, Example: ///-H "Cookie: mycookie" -H "User-Agent: iOS" 的本地化字符串。 @@ -332,6 +350,15 @@ namespace N_m3u8DL_RE.Common.Resource { } } + /// + /// 查找类似 ffmpeg merging... 的本地化字符串。 + /// + public static string ffmpegMerge { + get { + return ResourceManager.GetString("ffmpegMerge", resourceCulture); + } + } + /// /// 查找类似 Extracting TTML(raw) subtitle... 的本地化字符串。 /// diff --git a/src/N_m3u8DL-RE.Common/Resource/ResString.resx b/src/N_m3u8DL-RE.Common/Resource/ResString.resx index 849c5ee..2e06829 100644 --- a/src/N_m3u8DL-RE.Common/Resource/ResString.resx +++ b/src/N_m3u8DL-RE.Common/Resource/ResString.resx @@ -249,4 +249,13 @@ Failed to get KEY, ignore. + + fMP4 is detected, binary merging is automatically enabled + + + Full path to the ffmpeg binary, like C:\Tools\ffmpeg.exe + + + ffmpeg merging... + \ 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 79e413c..f86d071 100644 --- a/src/N_m3u8DL-RE.Common/Resource/ResString.zh-Hans.resx +++ b/src/N_m3u8DL-RE.Common/Resource/ResString.zh-Hans.resx @@ -269,4 +269,13 @@ 获取KEY失败,忽略读取. + + 检测到fMP4,自动开启二进制合并 + + + ffmpeg可执行程序全路径, 例如 C:\Tools\ffmpeg.exe + + + 调用ffmpeg合并中... + \ 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 60fe92d..adbd381 100644 --- a/src/N_m3u8DL-RE.Common/Resource/ResString.zh-Hant.resx +++ b/src/N_m3u8DL-RE.Common/Resource/ResString.zh-Hant.resx @@ -246,4 +246,13 @@ 獲取KEY失敗,忽略讀取. + + 檢測到fMP4,自動開啟二進位制合併 + + + ffmpeg可執行程序全路徑, 例如 C:\Tools\ffmpeg.exe + + + 調用ffmpeg合併中... + \ 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 86c995f..dfc333d 100644 --- a/src/N_m3u8DL-RE.Parser/StreamExtractor.cs +++ b/src/N_m3u8DL-RE.Parser/StreamExtractor.cs @@ -2,14 +2,8 @@ using N_m3u8DL_RE.Common.Entity; using N_m3u8DL_RE.Common.Log; using N_m3u8DL_RE.Common.Resource; -using N_m3u8DL_RE.Parser.Util; using N_m3u8DL_RE.Parser.Constants; using N_m3u8DL_RE.Parser.Extractor; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; using N_m3u8DL_RE.Common.Util; namespace N_m3u8DL_RE.Parser diff --git a/src/N_m3u8DL-RE/CommandLine/CommandInvoker.cs b/src/N_m3u8DL-RE/CommandLine/CommandInvoker.cs index 85b0c6d..2510f47 100644 --- a/src/N_m3u8DL-RE/CommandLine/CommandInvoker.cs +++ b/src/N_m3u8DL-RE/CommandLine/CommandInvoker.cs @@ -26,7 +26,7 @@ namespace N_m3u8DL_RE.CommandLine private readonly static Option ThreadCount = new(new string[] { "--thread-count" }, description: ResString.cmd_threadCount, getDefaultValue: () => 8); private readonly static Option SkipMerge = new(new string[] { "--skip-merge" }, description: ResString.cmd_skipMerge, getDefaultValue: () => false); private readonly static Option SkipDownload = new(new string[] { "--skip-download" }, description: ResString.cmd_skipDownload, getDefaultValue: () => false); - private readonly static Option BinaryMerge = new(new string[] { "--binary-merge" }, description: ResString.cmd_binaryMerge, getDefaultValue: () => true); + private readonly static Option BinaryMerge = new(new string[] { "--binary-merge" }, description: ResString.cmd_binaryMerge, getDefaultValue: () => false); private readonly static Option DelAfterDone = new(new string[] { "--del-after-done" }, description: ResString.cmd_delAfterDone, getDefaultValue: () => true); private readonly static Option AutoSubtitleFix = new(new string[] { "--auto-subtitle-fix" }, description: ResString.cmd_subtitleFix, getDefaultValue: () => true); private readonly static Option CheckSegmentsCount = new(new string[] { "--check-segments-count" }, description: ResString.cmd_checkSegmentsCount, getDefaultValue: () => true); @@ -35,6 +35,7 @@ namespace N_m3u8DL_RE.CommandLine private readonly static Option MP4RealTimeDecryption = new (new string[] { "--mp4-real-time-decryption" }, description: ResString.cmd_MP4RealTimeDecryption, getDefaultValue: () => false); private readonly static Option UseShakaPackager = new (new string[] { "--use-shaka-packager" }, description: ResString.cmd_useShakaPackager, getDefaultValue: () => false); private readonly static Option DecryptionBinaryPath = new(new string[] { "--decryption-binary-path" }, description: ResString.cmd_decryptionBinaryPath); + private readonly static Option FFmpegBinaryPath = new(new string[] { "--ffmpeg-binary-path" }, description: ResString.cmd_ffmpegBinaryPath); class MyOptionBinder : BinderBase { @@ -67,6 +68,7 @@ namespace N_m3u8DL_RE.CommandLine MP4RealTimeDecryption = bindingContext.ParseResult.GetValueForOption(MP4RealTimeDecryption), UseShakaPackager = bindingContext.ParseResult.GetValueForOption(UseShakaPackager), DecryptionBinaryPath = bindingContext.ParseResult.GetValueForOption(DecryptionBinaryPath), + FFmpegBinaryPath = bindingContext.ParseResult.GetValueForOption(FFmpegBinaryPath), }; //在这里设置语言 @@ -91,10 +93,11 @@ namespace N_m3u8DL_RE.CommandLine public static async Task InvokeArgs(string[] args, Func action) { - var rootCommand = new RootCommand("N_m3u8DL-RE (Beta version) 20220726") + var rootCommand = new RootCommand("N_m3u8DL-RE (Beta version) 20220808") { Input, TmpDir, SaveDir, SaveName, ThreadCount, AutoSelect, SkipMerge, SkipDownload, CheckSegmentsCount, BinaryMerge, DelAfterDone, WriteMetaJson, AppendUrlParams, Headers, /**SavePattern,**/ SubOnly, SubtitleFormat, AutoSubtitleFix, + FFmpegBinaryPath, LogLevel, UILanguage, UrlProcessorArgs, Keys, DecryptionBinaryPath, UseShakaPackager, MP4RealTimeDecryption }; rootCommand.TreatUnmatchedTokensAsErrors = true; diff --git a/src/N_m3u8DL-RE/CommandLine/MyOption.cs b/src/N_m3u8DL-RE/CommandLine/MyOption.cs index 71103ef..9e37bae 100644 --- a/src/N_m3u8DL-RE/CommandLine/MyOption.cs +++ b/src/N_m3u8DL-RE/CommandLine/MyOption.cs @@ -105,5 +105,9 @@ namespace N_m3u8DL_RE.CommandLine /// See: . /// public string? DecryptionBinaryPath { get; set; } + /// + /// See: . + /// + public string? FFmpegBinaryPath { get; set; } } } \ No newline at end of file diff --git a/src/N_m3u8DL-RE/Config/DownloaderConfig.cs b/src/N_m3u8DL-RE/Config/DownloaderConfig.cs index 844397a..290f9de 100644 --- a/src/N_m3u8DL-RE/Config/DownloaderConfig.cs +++ b/src/N_m3u8DL-RE/Config/DownloaderConfig.cs @@ -30,6 +30,7 @@ namespace N_m3u8DL_RE.Config MP4RealTimeDecryption = option.MP4RealTimeDecryption; UseShakaPackager = option.UseShakaPackager; DecryptionBinaryPath = option.DecryptionBinaryPath; + FFmpegBinaryPath = option.FFmpegBinaryPath; } /// @@ -103,5 +104,9 @@ namespace N_m3u8DL_RE.Config /// 解密KEYs /// public string[]? Keys { get; set; } + /// + /// ffmpeg路径 + /// + public string? FFmpegBinaryPath { get; set; } } } diff --git a/src/N_m3u8DL-RE/DownloadManager/SimpleDownloadManager.cs b/src/N_m3u8DL-RE/DownloadManager/SimpleDownloadManager.cs index ecb4826..7375dc4 100644 --- a/src/N_m3u8DL-RE/DownloadManager/SimpleDownloadManager.cs +++ b/src/N_m3u8DL-RE/DownloadManager/SimpleDownloadManager.cs @@ -1,5 +1,6 @@ using Mp4SubtitleParser; using N_m3u8DL_RE.Common.Entity; +using N_m3u8DL_RE.Common.Enum; using N_m3u8DL_RE.Common.Log; using N_m3u8DL_RE.Common.Resource; using N_m3u8DL_RE.Config; @@ -335,15 +336,24 @@ namespace N_m3u8DL_RE.DownloadManager //合并 if (!DownloaderConfig.SkipMerge) { + //对于fMP4,自动开启二进制合并 + if (mp4InitFile != "") + { + DownloaderConfig.BinaryMerge = true; + Logger.WarnMarkUp($"[white on darkorange3_1]{ResString.autoBinaryMerge}[/]"); + } + if (DownloaderConfig.BinaryMerge) { Logger.InfoMarkUp(ResString.binaryMerge); var files = FileDic.Values.Select(v => v!.ActualFilePath).OrderBy(s => s).ToArray(); - DownloadUtil.CombineMultipleFilesIntoSingleFile(files, output); + MergeUtil.CombineMultipleFilesIntoSingleFile(files, output); } else { - throw new NotImplementedException(); + var files = FileDic.Values.Select(v => v!.ActualFilePath).OrderBy(s => s).ToArray(); + Logger.InfoMarkUp(ResString.ffmpegMerge); + MergeUtil.MergeByFFmpeg(DownloaderConfig.FFmpegBinaryPath!, files, Path.ChangeExtension(output, null), "mp4"); } } diff --git a/src/N_m3u8DL-RE/Program.cs b/src/N_m3u8DL-RE/Program.cs index ef20495..de8ff25 100644 --- a/src/N_m3u8DL-RE/Program.cs +++ b/src/N_m3u8DL-RE/Program.cs @@ -38,6 +38,16 @@ namespace N_m3u8DL_RE try { + //预先检查ffmpeg + if (!option.BinaryMerge) + { + option.FFmpegBinaryPath = GlobalUtil.FindExecutable("ffmpeg"); + if (string.IsNullOrEmpty(option.FFmpegBinaryPath)) + { + throw new FileNotFoundException(option.FFmpegBinaryPath); + } + } + //预先检查 if (option.Keys != null && option.Keys.Length > 0) { diff --git a/src/N_m3u8DL-RE/Util/DownloadUtil.cs b/src/N_m3u8DL-RE/Util/DownloadUtil.cs index 5d4661a..faedb0c 100644 --- a/src/N_m3u8DL-RE/Util/DownloadUtil.cs +++ b/src/N_m3u8DL-RE/Util/DownloadUtil.cs @@ -67,38 +67,5 @@ namespace N_m3u8DL_RE.Util ActualFilePath = path }; } - - /// - /// 输入一堆已存在的文件,合并到新文件 - /// - /// - /// - public static void CombineMultipleFilesIntoSingleFile(string[] files, string outputFilePath) - { - if (files.Length == 0) return; - if (files.Length == 1) - { - FileInfo fi = new FileInfo(files[0]); - fi.CopyTo(outputFilePath, true); - return; - } - - if (!Directory.Exists(Path.GetDirectoryName(outputFilePath))) - Directory.CreateDirectory(Path.GetDirectoryName(outputFilePath)!); - - string[] inputFilePaths = files; - using (var outputStream = File.Create(outputFilePath)) - { - foreach (var inputFilePath in inputFilePaths) - { - if (inputFilePath == "") - continue; - using (var inputStream = File.OpenRead(inputFilePath)) - { - inputStream.CopyTo(outputStream); - } - } - } - } } } diff --git a/src/N_m3u8DL-RE/Util/MP4DecryptUtil.cs b/src/N_m3u8DL-RE/Util/MP4DecryptUtil.cs index 6844860..1676acc 100644 --- a/src/N_m3u8DL-RE/Util/MP4DecryptUtil.cs +++ b/src/N_m3u8DL-RE/Util/MP4DecryptUtil.cs @@ -30,7 +30,7 @@ namespace N_m3u8DL_RE.Util if (init != "") { tmpFile = Path.ChangeExtension(source, ".itmp"); - DownloadUtil.CombineMultipleFilesIntoSingleFile(new string[] { init, source }, tmpFile); + MergeUtil.CombineMultipleFilesIntoSingleFile(new string[] { init, source }, tmpFile); enc = tmpFile; } diff --git a/src/N_m3u8DL-RE/Util/MergeUtil.cs b/src/N_m3u8DL-RE/Util/MergeUtil.cs new file mode 100644 index 0000000..4a6e7a2 --- /dev/null +++ b/src/N_m3u8DL-RE/Util/MergeUtil.cs @@ -0,0 +1,116 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Xml; +using System.Xml.Linq; + +namespace N_m3u8DL_RE.Util +{ + internal class MergeUtil + { + /// + /// 输入一堆已存在的文件,合并到新文件 + /// + /// + /// + public static void CombineMultipleFilesIntoSingleFile(string[] files, string outputFilePath) + { + if (files.Length == 0) return; + if (files.Length == 1) + { + FileInfo fi = new FileInfo(files[0]); + fi.CopyTo(outputFilePath, true); + return; + } + + if (!Directory.Exists(Path.GetDirectoryName(outputFilePath))) + Directory.CreateDirectory(Path.GetDirectoryName(outputFilePath)!); + + string[] inputFilePaths = files; + using (var outputStream = File.Create(outputFilePath)) + { + foreach (var inputFilePath in inputFilePaths) + { + if (inputFilePath == "") + continue; + using (var inputStream = File.OpenRead(inputFilePath)) + { + inputStream.CopyTo(outputStream); + } + } + } + } + + public static void MergeByFFmpeg(string binary, string[] files, string outputPath, string muxFormat, + bool fastStart = false, + bool writeDate = true, string poster = "", string audioName = "", string title = "", + string copyright = "", string comment = "", string encodingTool = "", string recTime = "") + { + string dateString = string.IsNullOrEmpty(recTime) ? DateTime.Now.ToString("o") : recTime; + + bool useAACFilter = true; + StringBuilder command = new StringBuilder("-loglevel warning -i concat:\""); + string ddpAudio = string.Empty; + string addPoster = "-map 1 -c:v:1 copy -disposition:v:1 attached_pic"; + ddpAudio = (File.Exists($"{Path.GetFileNameWithoutExtension(outputPath + ".mp4")}.txt") ? File.ReadAllText($"{Path.GetFileNameWithoutExtension(outputPath + ".mp4")}.txt") : ""); + if (!string.IsNullOrEmpty(ddpAudio)) useAACFilter = false; + + foreach (string t in files) + { + command.Append(Path.GetFileName(t) + "|"); + } + + switch (muxFormat.ToUpper()) + { + case ("MP4"): + command.Append("\" " + (string.IsNullOrEmpty(poster) ? "" : "-i \"" + poster + "\"")); + command.Append(" " + (string.IsNullOrEmpty(ddpAudio) ? "" : "-i \"" + ddpAudio + "\"")); + command.Append( + $" -map 0:v? {(string.IsNullOrEmpty(ddpAudio) ? "-map 0:a?" : $"-map {(string.IsNullOrEmpty(poster) ? "1" : "2")}:a -map 0:a?")} -map 0:s? " + (string.IsNullOrEmpty(poster) ? "" : addPoster) + + (writeDate ? " -metadata date=\"" + dateString + "\"" : "") + + " -metadata encoding_tool=\"" + encodingTool + "\" -metadata title=\"" + title + + "\" -metadata copyright=\"" + copyright + "\" -metadata comment=\"" + comment + + $"\" -metadata:s:a:{(string.IsNullOrEmpty(ddpAudio) ? "0" : "1")} handler_name=\"" + audioName + $"\" -metadata:s:a:{(string.IsNullOrEmpty(ddpAudio) ? "0" : "1")} handler=\"" + audioName + "\" "); + command.Append(string.IsNullOrEmpty(ddpAudio) ? "" : " -metadata:s:a:0 handler_name=\"DD+\" -metadata:s:a:0 handler=\"DD+\" "); + if (fastStart) + command.Append("-movflags +faststart"); + command.Append(" -c copy -y " + (useAACFilter ? "-bsf:a aac_adtstoasc" : "") + " \"" + outputPath + ".mp4\""); + break; + case ("MKV"): + command.Append("\" -map 0 -c copy -y " + (useAACFilter ? "-bsf:a aac_adtstoasc" : "") + " \"" + outputPath + ".mkv\""); + break; + case ("FLV"): + command.Append("\" -map 0 -c copy -y " + (useAACFilter ? "-bsf:a aac_adtstoasc" : "") + " \"" + outputPath + ".flv\""); + break; + case ("TS"): + command.Append("\" -map 0 -c copy -y -f mpegts -bsf:v h264_mp4toannexb \"" + outputPath + ".ts\""); + break; + case ("VTT"): + command.Append("\" -map 0 -y \"" + outputPath + ".srt\""); //Convert To Srt + break; + case ("EAC3"): + command.Append("\" -map 0:a -c copy -y \"" + outputPath + ".eac3\""); + break; + case ("AAC"): + command.Append("\" -map 0:a -c copy -y \"" + outputPath + ".m4a\""); + break; + case ("AC3"): + command.Append("\" -map 0:a -c copy -y \"" + outputPath + ".ac3\""); + break; + } + + Process.Start(new ProcessStartInfo() + { + WorkingDirectory = Path.GetDirectoryName(files[0]), + FileName = binary, + Arguments = command.ToString(), + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false + })!.WaitForExit(); + } + } +}