增加ffmpeg合并且默认启用

This commit is contained in:
nilaoda 2022-08-08 22:15:49 +08:00
parent 9864b2560e
commit 7ef1f21c51
13 changed files with 207 additions and 44 deletions

View File

@ -60,6 +60,15 @@ namespace N_m3u8DL_RE.Common.Resource {
} }
} }
/// <summary>
/// 查找类似 fMP4 is detected, binary merging is automatically enabled 的本地化字符串。
/// </summary>
public static string autoBinaryMerge {
get {
return ResourceManager.GetString("autoBinaryMerge", resourceCulture);
}
}
/// <summary> /// <summary>
/// 查找类似 Bad m3u8 的本地化字符串。 /// 查找类似 Bad m3u8 的本地化字符串。
/// </summary> /// </summary>
@ -141,6 +150,15 @@ namespace N_m3u8DL_RE.Common.Resource {
} }
} }
/// <summary>
/// 查找类似 Full path to the ffmpeg binary, like C:\Tools\ffmpeg.exe 的本地化字符串。
/// </summary>
public static string cmd_ffmpegBinaryPath {
get {
return ResourceManager.GetString("cmd_ffmpegBinaryPath", resourceCulture);
}
}
/// <summary> /// <summary>
/// 查找类似 Pass custom header(s) to server, Example: /// 查找类似 Pass custom header(s) to server, Example:
///-H &quot;Cookie: mycookie&quot; -H &quot;User-Agent: iOS&quot; 的本地化字符串。 ///-H &quot;Cookie: mycookie&quot; -H &quot;User-Agent: iOS&quot; 的本地化字符串。
@ -332,6 +350,15 @@ namespace N_m3u8DL_RE.Common.Resource {
} }
} }
/// <summary>
/// 查找类似 ffmpeg merging... 的本地化字符串。
/// </summary>
public static string ffmpegMerge {
get {
return ResourceManager.GetString("ffmpegMerge", resourceCulture);
}
}
/// <summary> /// <summary>
/// 查找类似 Extracting TTML(raw) subtitle... 的本地化字符串。 /// 查找类似 Extracting TTML(raw) subtitle... 的本地化字符串。
/// </summary> /// </summary>

View File

@ -249,4 +249,13 @@
<data name="cmd_loadKeyFailed" xml:space="preserve"> <data name="cmd_loadKeyFailed" xml:space="preserve">
<value>Failed to get KEY, ignore.</value> <value>Failed to get KEY, ignore.</value>
</data> </data>
<data name="autoBinaryMerge" xml:space="preserve">
<value>fMP4 is detected, binary merging is automatically enabled</value>
</data>
<data name="cmd_ffmpegBinaryPath" xml:space="preserve">
<value>Full path to the ffmpeg binary, like C:\Tools\ffmpeg.exe</value>
</data>
<data name="ffmpegMerge" xml:space="preserve">
<value>ffmpeg merging...</value>
</data>
</root> </root>

View File

@ -269,4 +269,13 @@
<data name="cmd_loadKeyFailed" xml:space="preserve"> <data name="cmd_loadKeyFailed" xml:space="preserve">
<value>获取KEY失败忽略读取.</value> <value>获取KEY失败忽略读取.</value>
</data> </data>
<data name="autoBinaryMerge" xml:space="preserve">
<value>检测到fMP4自动开启二进制合并</value>
</data>
<data name="cmd_ffmpegBinaryPath" xml:space="preserve">
<value>ffmpeg可执行程序全路径, 例如 C:\Tools\ffmpeg.exe</value>
</data>
<data name="ffmpegMerge" xml:space="preserve">
<value>调用ffmpeg合并中...</value>
</data>
</root> </root>

View File

@ -246,4 +246,13 @@
<data name="cmd_loadKeyFailed" xml:space="preserve"> <data name="cmd_loadKeyFailed" xml:space="preserve">
<value>獲取KEY失敗忽略讀取.</value> <value>獲取KEY失敗忽略讀取.</value>
</data> </data>
<data name="autoBinaryMerge" xml:space="preserve">
<value>檢測到fMP4自動開啟二進位制合併</value>
</data>
<data name="cmd_ffmpegBinaryPath" xml:space="preserve">
<value>ffmpeg可執行程序全路徑, 例如 C:\Tools\ffmpeg.exe</value>
</data>
<data name="ffmpegMerge" xml:space="preserve">
<value>調用ffmpeg合併中...</value>
</data>
</root> </root>

View File

@ -2,14 +2,8 @@
using N_m3u8DL_RE.Common.Entity; using N_m3u8DL_RE.Common.Entity;
using N_m3u8DL_RE.Common.Log; using N_m3u8DL_RE.Common.Log;
using N_m3u8DL_RE.Common.Resource; using N_m3u8DL_RE.Common.Resource;
using N_m3u8DL_RE.Parser.Util;
using N_m3u8DL_RE.Parser.Constants; using N_m3u8DL_RE.Parser.Constants;
using N_m3u8DL_RE.Parser.Extractor; 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; using N_m3u8DL_RE.Common.Util;
namespace N_m3u8DL_RE.Parser namespace N_m3u8DL_RE.Parser

View File

@ -26,7 +26,7 @@ namespace N_m3u8DL_RE.CommandLine
private readonly static Option<int> ThreadCount = new(new string[] { "--thread-count" }, description: ResString.cmd_threadCount, getDefaultValue: () => 8); private readonly static Option<int> ThreadCount = new(new string[] { "--thread-count" }, description: ResString.cmd_threadCount, getDefaultValue: () => 8);
private readonly static Option<bool> SkipMerge = new(new string[] { "--skip-merge" }, description: ResString.cmd_skipMerge, getDefaultValue: () => false); private readonly static Option<bool> SkipMerge = new(new string[] { "--skip-merge" }, description: ResString.cmd_skipMerge, getDefaultValue: () => false);
private readonly static Option<bool> SkipDownload = new(new string[] { "--skip-download" }, description: ResString.cmd_skipDownload, getDefaultValue: () => false); private readonly static Option<bool> SkipDownload = new(new string[] { "--skip-download" }, description: ResString.cmd_skipDownload, getDefaultValue: () => false);
private readonly static Option<bool> BinaryMerge = new(new string[] { "--binary-merge" }, description: ResString.cmd_binaryMerge, getDefaultValue: () => true); private readonly static Option<bool> BinaryMerge = new(new string[] { "--binary-merge" }, description: ResString.cmd_binaryMerge, getDefaultValue: () => false);
private readonly static Option<bool> DelAfterDone = new(new string[] { "--del-after-done" }, description: ResString.cmd_delAfterDone, getDefaultValue: () => true); private readonly static Option<bool> DelAfterDone = new(new string[] { "--del-after-done" }, description: ResString.cmd_delAfterDone, getDefaultValue: () => true);
private readonly static Option<bool> AutoSubtitleFix = new(new string[] { "--auto-subtitle-fix" }, description: ResString.cmd_subtitleFix, getDefaultValue: () => true); private readonly static Option<bool> AutoSubtitleFix = new(new string[] { "--auto-subtitle-fix" }, description: ResString.cmd_subtitleFix, getDefaultValue: () => true);
private readonly static Option<bool> CheckSegmentsCount = new(new string[] { "--check-segments-count" }, description: ResString.cmd_checkSegmentsCount, getDefaultValue: () => true); private readonly static Option<bool> 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<bool> MP4RealTimeDecryption = new (new string[] { "--mp4-real-time-decryption" }, description: ResString.cmd_MP4RealTimeDecryption, getDefaultValue: () => false); private readonly static Option<bool> MP4RealTimeDecryption = new (new string[] { "--mp4-real-time-decryption" }, description: ResString.cmd_MP4RealTimeDecryption, getDefaultValue: () => false);
private readonly static Option<bool> UseShakaPackager = new (new string[] { "--use-shaka-packager" }, description: ResString.cmd_useShakaPackager, getDefaultValue: () => false); private readonly static Option<bool> UseShakaPackager = new (new string[] { "--use-shaka-packager" }, description: ResString.cmd_useShakaPackager, getDefaultValue: () => false);
private readonly static Option<string?> DecryptionBinaryPath = new(new string[] { "--decryption-binary-path" }, description: ResString.cmd_decryptionBinaryPath); private readonly static Option<string?> DecryptionBinaryPath = new(new string[] { "--decryption-binary-path" }, description: ResString.cmd_decryptionBinaryPath);
private readonly static Option<string?> FFmpegBinaryPath = new(new string[] { "--ffmpeg-binary-path" }, description: ResString.cmd_ffmpegBinaryPath);
class MyOptionBinder : BinderBase<MyOption> class MyOptionBinder : BinderBase<MyOption>
{ {
@ -67,6 +68,7 @@ namespace N_m3u8DL_RE.CommandLine
MP4RealTimeDecryption = bindingContext.ParseResult.GetValueForOption(MP4RealTimeDecryption), MP4RealTimeDecryption = bindingContext.ParseResult.GetValueForOption(MP4RealTimeDecryption),
UseShakaPackager = bindingContext.ParseResult.GetValueForOption(UseShakaPackager), UseShakaPackager = bindingContext.ParseResult.GetValueForOption(UseShakaPackager),
DecryptionBinaryPath = bindingContext.ParseResult.GetValueForOption(DecryptionBinaryPath), DecryptionBinaryPath = bindingContext.ParseResult.GetValueForOption(DecryptionBinaryPath),
FFmpegBinaryPath = bindingContext.ParseResult.GetValueForOption(FFmpegBinaryPath),
}; };
//在这里设置语言 //在这里设置语言
@ -91,10 +93,11 @@ namespace N_m3u8DL_RE.CommandLine
public static async Task<int> InvokeArgs(string[] args, Func<MyOption, Task> action) public static async Task<int> InvokeArgs(string[] args, Func<MyOption, Task> 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, Input, TmpDir, SaveDir, SaveName, ThreadCount, AutoSelect, SkipMerge, SkipDownload, CheckSegmentsCount,
BinaryMerge, DelAfterDone, WriteMetaJson, AppendUrlParams, Headers, /**SavePattern,**/ SubOnly, SubtitleFormat, AutoSubtitleFix, BinaryMerge, DelAfterDone, WriteMetaJson, AppendUrlParams, Headers, /**SavePattern,**/ SubOnly, SubtitleFormat, AutoSubtitleFix,
FFmpegBinaryPath,
LogLevel, UILanguage, UrlProcessorArgs, Keys, DecryptionBinaryPath, UseShakaPackager, MP4RealTimeDecryption LogLevel, UILanguage, UrlProcessorArgs, Keys, DecryptionBinaryPath, UseShakaPackager, MP4RealTimeDecryption
}; };
rootCommand.TreatUnmatchedTokensAsErrors = true; rootCommand.TreatUnmatchedTokensAsErrors = true;

View File

@ -105,5 +105,9 @@ namespace N_m3u8DL_RE.CommandLine
/// See: <see cref="CommandInvoker.DecryptionBinaryPath"/>. /// See: <see cref="CommandInvoker.DecryptionBinaryPath"/>.
/// </summary> /// </summary>
public string? DecryptionBinaryPath { get; set; } public string? DecryptionBinaryPath { get; set; }
/// <summary>
/// See: <see cref="CommandInvoker.FFmpegBinaryPath"/>.
/// </summary>
public string? FFmpegBinaryPath { get; set; }
} }
} }

View File

@ -30,6 +30,7 @@ namespace N_m3u8DL_RE.Config
MP4RealTimeDecryption = option.MP4RealTimeDecryption; MP4RealTimeDecryption = option.MP4RealTimeDecryption;
UseShakaPackager = option.UseShakaPackager; UseShakaPackager = option.UseShakaPackager;
DecryptionBinaryPath = option.DecryptionBinaryPath; DecryptionBinaryPath = option.DecryptionBinaryPath;
FFmpegBinaryPath = option.FFmpegBinaryPath;
} }
/// <summary> /// <summary>
@ -103,5 +104,9 @@ namespace N_m3u8DL_RE.Config
/// 解密KEYs /// 解密KEYs
/// </summary> /// </summary>
public string[]? Keys { get; set; } public string[]? Keys { get; set; }
/// <summary>
/// ffmpeg路径
/// </summary>
public string? FFmpegBinaryPath { get; set; }
} }
} }

View File

@ -1,5 +1,6 @@
using Mp4SubtitleParser; using Mp4SubtitleParser;
using N_m3u8DL_RE.Common.Entity; using N_m3u8DL_RE.Common.Entity;
using N_m3u8DL_RE.Common.Enum;
using N_m3u8DL_RE.Common.Log; using N_m3u8DL_RE.Common.Log;
using N_m3u8DL_RE.Common.Resource; using N_m3u8DL_RE.Common.Resource;
using N_m3u8DL_RE.Config; using N_m3u8DL_RE.Config;
@ -335,15 +336,24 @@ namespace N_m3u8DL_RE.DownloadManager
//合并 //合并
if (!DownloaderConfig.SkipMerge) if (!DownloaderConfig.SkipMerge)
{ {
//对于fMP4自动开启二进制合并
if (mp4InitFile != "")
{
DownloaderConfig.BinaryMerge = true;
Logger.WarnMarkUp($"[white on darkorange3_1]{ResString.autoBinaryMerge}[/]");
}
if (DownloaderConfig.BinaryMerge) if (DownloaderConfig.BinaryMerge)
{ {
Logger.InfoMarkUp(ResString.binaryMerge); Logger.InfoMarkUp(ResString.binaryMerge);
var files = FileDic.Values.Select(v => v!.ActualFilePath).OrderBy(s => s).ToArray(); var files = FileDic.Values.Select(v => v!.ActualFilePath).OrderBy(s => s).ToArray();
DownloadUtil.CombineMultipleFilesIntoSingleFile(files, output); MergeUtil.CombineMultipleFilesIntoSingleFile(files, output);
} }
else 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");
} }
} }

View File

@ -38,6 +38,16 @@ namespace N_m3u8DL_RE
try 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) if (option.Keys != null && option.Keys.Length > 0)
{ {

View File

@ -67,38 +67,5 @@ namespace N_m3u8DL_RE.Util
ActualFilePath = path ActualFilePath = path
}; };
} }
/// <summary>
/// 输入一堆已存在的文件,合并到新文件
/// </summary>
/// <param name="files"></param>
/// <param name="outputFilePath"></param>
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);
}
}
}
}
} }
} }

View File

@ -30,7 +30,7 @@ namespace N_m3u8DL_RE.Util
if (init != "") if (init != "")
{ {
tmpFile = Path.ChangeExtension(source, ".itmp"); tmpFile = Path.ChangeExtension(source, ".itmp");
DownloadUtil.CombineMultipleFilesIntoSingleFile(new string[] { init, source }, tmpFile); MergeUtil.CombineMultipleFilesIntoSingleFile(new string[] { init, source }, tmpFile);
enc = tmpFile; enc = tmpFile;
} }

View File

@ -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
{
/// <summary>
/// 输入一堆已存在的文件,合并到新文件
/// </summary>
/// <param name="files"></param>
/// <param name="outputFilePath"></param>
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();
}
}
}