From 8d2b1d6faad338ca92ab11de2fa5e81da3a27c96 Mon Sep 17 00:00:00 2001 From: nilaoda Date: Sun, 21 Aug 2022 16:03:47 +0800 Subject: [PATCH] =?UTF-8?q?=E6=94=AF=E6=8C=81=E4=B8=8B=E8=BD=BD=E5=AE=8C?= =?UTF-8?q?=E6=88=90=E5=90=8E=E8=87=AA=E5=8A=A8=E5=B0=81=E8=A3=85=E9=9F=B3?= =?UTF-8?q?=E8=A7=86=E9=A2=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/N_m3u8DL-RE.Common/Resource/ResString.cs | 1 + src/N_m3u8DL-RE.Common/Resource/StaticText.cs | 6 ++ src/N_m3u8DL-RE/CommandLine/CommandInvoker.cs | 6 +- src/N_m3u8DL-RE/CommandLine/MyOption.cs | 4 + src/N_m3u8DL-RE/Config/DownloaderConfig.cs | 5 + .../DownloadManager/SimpleDownloadManager.cs | 48 +++++++--- src/N_m3u8DL-RE/Entity/OutputFile.cs | 15 +++ src/N_m3u8DL-RE/Program.cs | 1 + src/N_m3u8DL-RE/Util/MergeUtil.cs | 93 +++++++++++++------ 9 files changed, 139 insertions(+), 40 deletions(-) create mode 100644 src/N_m3u8DL-RE/Entity/OutputFile.cs diff --git a/src/N_m3u8DL-RE.Common/Resource/ResString.cs b/src/N_m3u8DL-RE.Common/Resource/ResString.cs index 3f66120..667b5db 100644 --- a/src/N_m3u8DL-RE.Common/Resource/ResString.cs +++ b/src/N_m3u8DL-RE.Common/Resource/ResString.cs @@ -44,6 +44,7 @@ namespace N_m3u8DL_RE.Common.Resource public static string cmd_uiLanguage { get => GetText("cmd_uiLanguage"); } public static string cmd_urlProcessorArgs { get => GetText("cmd_urlProcessorArgs"); } public static string cmd_useShakaPackager { get => GetText("cmd_useShakaPackager"); } + public static string cmd_muxAfterDone { get => GetText("cmd_muxAfterDone"); } public static string cmd_writeMetaJson { get => GetText("cmd_writeMetaJson"); } public static string fetch { get => GetText("fetch"); } public static string ffmpegMerge { get => GetText("ffmpegMerge"); } diff --git a/src/N_m3u8DL-RE.Common/Resource/StaticText.cs b/src/N_m3u8DL-RE.Common/Resource/StaticText.cs index 1dc45ac..8ea4a33 100644 --- a/src/N_m3u8DL-RE.Common/Resource/StaticText.cs +++ b/src/N_m3u8DL-RE.Common/Resource/StaticText.cs @@ -220,6 +220,12 @@ namespace N_m3u8DL_RE.Common.Resource zhTW: "使用shaka-packager替代mp4decrypt", enUS: "Use shaka-packager instead of mp4decrypt" ), + ["cmd_muxAfterDone"] = new TextContainer + ( + zhCN: "所有工作完成时尝试使用ffmpeg混流分离的音视频(mkv)", + zhTW: "所有工作完成時嘗試使用ffmpeg混流分離的影音(mkv)", + enUS: "When all works is done, try to use ffmpeg to mux the separated audio(s) and video.(mkv)" + ), ["cmd_writeMetaJson"] = new TextContainer ( zhCN: "解析后的信息是否输出json文件", diff --git a/src/N_m3u8DL-RE/CommandLine/CommandInvoker.cs b/src/N_m3u8DL-RE/CommandLine/CommandInvoker.cs index 8f6e1e5..b5a39a4 100644 --- a/src/N_m3u8DL-RE/CommandLine/CommandInvoker.cs +++ b/src/N_m3u8DL-RE/CommandLine/CommandInvoker.cs @@ -36,6 +36,7 @@ namespace N_m3u8DL_RE.CommandLine 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); private readonly static Option UseShakaPackager = new (new string[] { "--use-shaka-packager" }, description: ResString.cmd_useShakaPackager, getDefaultValue: () => false); + private readonly static Option MuxAfterDone = new (new string[] { "--mux-after-done" }, description: ResString.cmd_muxAfterDone, 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); @@ -73,6 +74,7 @@ namespace N_m3u8DL_RE.CommandLine FFmpegBinaryPath = bindingContext.ParseResult.GetValueForOption(FFmpegBinaryPath), KeyTextFile = bindingContext.ParseResult.GetValueForOption(KeyTextFile), DownloadRetryCount = bindingContext.ParseResult.GetValueForOption(DownloadRetryCount), + MuxAfterDone = bindingContext.ParseResult.GetValueForOption(MuxAfterDone), }; @@ -91,10 +93,10 @@ namespace N_m3u8DL_RE.CommandLine public static async Task InvokeArgs(string[] args, Func action) { - var rootCommand = new RootCommand("N_m3u8DL-RE (Beta version) 20220819") + var rootCommand = new RootCommand("N_m3u8DL-RE (Beta version) 20220821") { Input, TmpDir, SaveDir, SaveName, ThreadCount, DownloadRetryCount, AutoSelect, SkipMerge, SkipDownload, CheckSegmentsCount, - BinaryMerge, DelAfterDone, WriteMetaJson, AppendUrlParams, Headers, /**SavePattern,**/ SubOnly, SubtitleFormat, AutoSubtitleFix, + BinaryMerge, DelAfterDone, WriteMetaJson, MuxAfterDone, AppendUrlParams, Headers, /**SavePattern,**/ SubOnly, SubtitleFormat, AutoSubtitleFix, FFmpegBinaryPath, LogLevel, UILanguage, UrlProcessorArgs, Keys, KeyTextFile, DecryptionBinaryPath, UseShakaPackager, MP4RealTimeDecryption }; diff --git a/src/N_m3u8DL-RE/CommandLine/MyOption.cs b/src/N_m3u8DL-RE/CommandLine/MyOption.cs index 9b4f438..54d00b4 100644 --- a/src/N_m3u8DL-RE/CommandLine/MyOption.cs +++ b/src/N_m3u8DL-RE/CommandLine/MyOption.cs @@ -86,6 +86,10 @@ namespace N_m3u8DL_RE.CommandLine /// public bool UseShakaPackager { get; set; } /// + /// See: . + /// + public bool MuxAfterDone { 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 29f6f07..0e65156 100644 --- a/src/N_m3u8DL-RE/Config/DownloaderConfig.cs +++ b/src/N_m3u8DL-RE/Config/DownloaderConfig.cs @@ -33,6 +33,7 @@ namespace N_m3u8DL_RE.Config FFmpegBinaryPath = option.FFmpegBinaryPath; KeyTextFile = option.KeyTextFile; DownloadRetryCount = option.DownloadRetryCount; + MuxAfterDone = option.MuxAfterDone; } /// @@ -92,6 +93,10 @@ namespace N_m3u8DL_RE.Config /// public bool UseShakaPackager { get; set; } /// + /// 自动混流音视频 + /// + public bool MuxAfterDone { get; set; } + /// /// MP4解密所用工具的全路径 /// public string? DecryptionBinaryPath { get; set; } diff --git a/src/N_m3u8DL-RE/DownloadManager/SimpleDownloadManager.cs b/src/N_m3u8DL-RE/DownloadManager/SimpleDownloadManager.cs index f65cf52..03ba634 100644 --- a/src/N_m3u8DL-RE/DownloadManager/SimpleDownloadManager.cs +++ b/src/N_m3u8DL-RE/DownloadManager/SimpleDownloadManager.cs @@ -20,6 +20,7 @@ namespace N_m3u8DL_RE.DownloadManager IDownloader Downloader; DownloaderConfig DownloaderConfig; DateTime NowDateTime; + List OutputFiles = new(); public SimpleDownloadManager(DownloaderConfig downloaderConfig) { @@ -94,7 +95,7 @@ namespace N_m3u8DL_RE.DownloadManager if (segments == null) return false; var type = streamSpec.MediaType ?? Common.Enum.MediaType.VIDEO; - var dirName = $"{DownloaderConfig.SaveName ?? NowDateTime.ToString("yyyy-MM-dd_HH-mm-ss")}_{streamSpec.GroupId}_{streamSpec.Codecs}_{streamSpec.Language}"; + var dirName = $"{DownloaderConfig.SaveName ?? NowDateTime.ToString("yyyy-MM-dd_HH-mm-ss")}_{streamSpec.GroupId}_{streamSpec.Codecs}_{streamSpec.Bandwidth}_{streamSpec.Language}"; //去除非法字符 dirName = ConvertUtil.GetValidFileName(dirName, filterSlash: true); var tmpDir = Path.Combine(DownloaderConfig.TmpDir ?? Environment.CurrentDirectory, dirName); @@ -250,19 +251,20 @@ namespace N_m3u8DL_RE.DownloadManager } }); - var output = Path.Combine(saveDir, saveName + $".{streamSpec.Extension ?? "ts"}"); + //修改输出后缀 + var outputExt = "." + streamSpec.Extension; + if (streamSpec.Extension == null) outputExt = ".ts"; + else if (streamSpec.MediaType == MediaType.AUDIO) outputExt = ".m4a"; + else if (streamSpec.MediaType != MediaType.SUBTITLES) outputExt = ".mp4"; + + var output = Path.Combine(saveDir, saveName + outputExt); + //检测目标文件是否存在 while (File.Exists(output)) { Logger.WarnMarkUp($"{output} => {output = Path.ChangeExtension(output, $"copy" + Path.GetExtension(output))}"); } - //修改输出后缀 - if (streamSpec.MediaType == Common.Enum.MediaType.AUDIO) - output = Path.ChangeExtension(output, ".m4a"); - else if (streamSpec.MediaType != Common.Enum.MediaType.SUBTITLES) - output = Path.ChangeExtension(output, ".mp4"); - if (DownloaderConfig.MP4RealTimeDecryption && mp4InitFile != "") { File.Delete(mp4InitFile); @@ -464,10 +466,18 @@ namespace N_m3u8DL_RE.DownloadManager } else { + //ffmpeg合并 var files = FileDic.Values.Select(v => v!.ActualFilePath).OrderBy(s => s).ToArray(); Logger.InfoMarkUp(ResString.ffmpegMerge); var ext = streamSpec.MediaType == MediaType.AUDIO ? "m4a" : "mp4"; - mergeSuccess = MergeUtil.MergeByFFmpeg(DownloaderConfig.FFmpegBinaryPath!, files, Path.ChangeExtension(output, null), ext, useAACFilter); + var ffOut = Path.Combine(Path.GetDirectoryName(output)!, Path.GetFileNameWithoutExtension(output) + $".{ext}"); + //检测目标文件是否存在 + while (File.Exists(ffOut)) + { + Logger.WarnMarkUp($"{ffOut} => {ffOut = Path.ChangeExtension(ffOut, $"copy" + Path.GetExtension(ffOut))}"); + } + mergeSuccess = MergeUtil.MergeByFFmpeg(DownloaderConfig.FFmpegBinaryPath!, files, Path.ChangeExtension(ffOut, null), ext, useAACFilter); + if (mergeSuccess) output = ffOut; } } @@ -504,10 +514,13 @@ namespace N_m3u8DL_RE.DownloadManager { File.Delete(enc); File.Move(dec, enc); - output = dec; } } + //记录所有文件信息 + if (File.Exists(output)) + OutputFiles.Add(new OutputFile() { FilePath = output, LangCode = streamSpec.Language, Description = streamSpec.Name }); + return true; } @@ -546,7 +559,20 @@ namespace N_m3u8DL_RE.DownloadManager } }); - return Results.Values.All(v => v == true); + var success = Results.Values.All(v => v == true); + + //混流 + if (success && OutputFiles.Count > 0) + { + var outName = $"{DownloaderConfig.SaveName ?? NowDateTime.ToString("yyyy-MM-dd_HH-mm-ss")}"; + Logger.WarnMarkUp($"Muxing to [grey]{outName.EscapeMarkup()}.mkv[/]"); + var result = MergeUtil.MuxInputsByFFmpeg(DownloaderConfig.FFmpegBinaryPath!, OutputFiles.ToArray(), outName); + //完成后删除各轨道文件 + if (result) OutputFiles.ForEach(f => File.Delete(f.FilePath)); + else Logger.ErrorMarkUp($"Mux failed"); + } + + return success; } } } diff --git a/src/N_m3u8DL-RE/Entity/OutputFile.cs b/src/N_m3u8DL-RE/Entity/OutputFile.cs new file mode 100644 index 0000000..10569c3 --- /dev/null +++ b/src/N_m3u8DL-RE/Entity/OutputFile.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace N_m3u8DL_RE.Entity +{ + internal class OutputFile + { + public required string FilePath { get; set; } + public string? LangCode { get; set; } + public string? Description { get; set; } + } +} diff --git a/src/N_m3u8DL-RE/Program.cs b/src/N_m3u8DL-RE/Program.cs index 2134729..c6d7e10 100644 --- a/src/N_m3u8DL-RE/Program.cs +++ b/src/N_m3u8DL-RE/Program.cs @@ -103,6 +103,7 @@ namespace N_m3u8DL_RE parserConfig.UrlProcessors.Insert(0, new NowehoryzontyUrlProcessor()); var url = string.Empty; + //url = "https://media.axprod.net/TestVectors/v7-Clear/Manifest_1080p.mpd"; //多音轨多字幕 //url = "https://cmafref.akamaized.net/cmaf/live-ull/2006350/akambr/out.mpd"; //直播 //url = "http://playertest.longtailvideo.com/adaptive/oceans_aes/oceans_aes.m3u8"; //url = "https://vod.sdn.wavve.com/hls/S01/S01_E461382925.1/1/5000/chunklist.m3u8"; diff --git a/src/N_m3u8DL-RE/Util/MergeUtil.cs b/src/N_m3u8DL-RE/Util/MergeUtil.cs index 8f8bc67..e357dc4 100644 --- a/src/N_m3u8DL-RE/Util/MergeUtil.cs +++ b/src/N_m3u8DL-RE/Util/MergeUtil.cs @@ -1,7 +1,9 @@ using N_m3u8DL_RE.Common.Log; +using N_m3u8DL_RE.Entity; using Spectre.Console; using System; using System.Collections.Generic; +using System.CommandLine; using System.Diagnostics; using System.Linq; using System.Text; @@ -46,6 +48,30 @@ namespace N_m3u8DL_RE.Util } } + private static void InvokeFFmpeg(string binary, string command, string workingDirectory) + { + using var p = new Process(); + p.StartInfo = new ProcessStartInfo() + { + WorkingDirectory = workingDirectory, + FileName = binary, + Arguments = command, + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false + }; + p.ErrorDataReceived += (sendProcess, output) => + { + if (!string.IsNullOrEmpty(output.Data)) + { + Logger.WarnMarkUp($"[grey]{output.Data.EscapeMarkup()}[/]"); + } + }; + p.Start(); + p.BeginErrorReadLine(); + p.WaitForExit(); + } + public static bool MergeByFFmpeg(string binary, string[] files, string outputPath, string muxFormat, bool useAACFilter, bool fastStart = false, bool writeDate = true, string poster = "", string audioName = "", string title = "", @@ -53,13 +79,6 @@ namespace N_m3u8DL_RE.Util { string dateString = string.IsNullOrEmpty(recTime) ? DateTime.Now.ToString("o") : recTime; - //同名文件已存在的共存策略 - if (File.Exists($"{outputPath}.{muxFormat.ToLower()}")) - { - outputPath = Path.Combine(Path.GetDirectoryName(outputPath)!, - Path.GetFileName(outputPath) + "_" + DateTime.Now.ToString("yyyy-MM-dd_HH-mm-ss")); - } - 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"; @@ -112,31 +131,51 @@ namespace N_m3u8DL_RE.Util Logger.DebugMarkUp($"{binary}: {command}"); - using var p = new Process(); - p.StartInfo = new ProcessStartInfo() - { - WorkingDirectory = Path.GetDirectoryName(files[0]), - FileName = binary, - Arguments = command.ToString(), - RedirectStandardOutput = true, - RedirectStandardError = true, - UseShellExecute = false - }; - p.ErrorDataReceived += (sendProcess, output) => - { - if (!string.IsNullOrEmpty(output.Data)) - { - Logger.WarnMarkUp($"[grey]{output.Data.EscapeMarkup()}[/]"); - } - }; - p.Start(); - p.BeginErrorReadLine(); - p.WaitForExit(); + InvokeFFmpeg(binary, command.ToString(), Path.GetDirectoryName(files[0])!); if (File.Exists($"{outputPath}.{muxFormat}") && new FileInfo($"{outputPath}.{muxFormat}").Length > 0) return true; return false; } + + public static bool MuxInputsByFFmpeg(string binary, OutputFile[] files, string outputPath) + { + string dateString = DateTime.Now.ToString("o"); + StringBuilder command = new StringBuilder("-loglevel warning -y "); + + //INPUT + foreach (var item in files) + { + command.Append($" -i \"{item.FilePath}\" "); + } + + //MAP + for (int i = 0; i < files.Length; i++) + { + command.Append($" -map {i} "); + } + + //CLEAN + command.Append(" -map_metadata -1 "); + + //LANG and NAME + for (int i = 0; i < files.Length; i++) + { + if (!string.IsNullOrEmpty(files[i].LangCode)) + command.Append($" -metadata:s:{i} language={files[i].LangCode} "); + if (!string.IsNullOrEmpty(files[i].Description)) + command.Append($" -metadata:s:{i} title={files[i].Description} "); + } + + command.Append($" -metadata date=\"{dateString}\" -ignore_unknown -copy_unknown -c copy \"{outputPath}.mkv\""); + + InvokeFFmpeg(binary, command.ToString(), Path.GetDirectoryName(files[0].FilePath)!); + + if (File.Exists($"{outputPath}.mkv") && new FileInfo($"{outputPath}.mkv").Length > 1024) + return true; + + return false; + } } }