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();
+ }
+ }
+}