支持下载完成后自动封装音视频

This commit is contained in:
nilaoda 2022-08-21 16:03:47 +08:00
parent 01de8a53ad
commit 8d2b1d6faa
9 changed files with 139 additions and 40 deletions

View File

@ -44,6 +44,7 @@ namespace N_m3u8DL_RE.Common.Resource
public static string cmd_uiLanguage { get => GetText("cmd_uiLanguage"); } public static string cmd_uiLanguage { get => GetText("cmd_uiLanguage"); }
public static string cmd_urlProcessorArgs { get => GetText("cmd_urlProcessorArgs"); } public static string cmd_urlProcessorArgs { get => GetText("cmd_urlProcessorArgs"); }
public static string cmd_useShakaPackager { get => GetText("cmd_useShakaPackager"); } 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 cmd_writeMetaJson { get => GetText("cmd_writeMetaJson"); }
public static string fetch { get => GetText("fetch"); } public static string fetch { get => GetText("fetch"); }
public static string ffmpegMerge { get => GetText("ffmpegMerge"); } public static string ffmpegMerge { get => GetText("ffmpegMerge"); }

View File

@ -220,6 +220,12 @@ namespace N_m3u8DL_RE.Common.Resource
zhTW: "使用shaka-packager替代mp4decrypt", zhTW: "使用shaka-packager替代mp4decrypt",
enUS: "Use shaka-packager instead of 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 ["cmd_writeMetaJson"] = new TextContainer
( (
zhCN: "解析后的信息是否输出json文件", zhCN: "解析后的信息是否输出json文件",

View File

@ -36,6 +36,7 @@ namespace N_m3u8DL_RE.CommandLine
private readonly static Option<bool> AppendUrlParams = new(new string[] { "--append-url-params" }, description: ResString.cmd_appendUrlParams, getDefaultValue: () => false); private readonly static Option<bool> AppendUrlParams = new(new string[] { "--append-url-params" }, description: ResString.cmd_appendUrlParams, 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> 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<bool> MuxAfterDone = new (new string[] { "--mux-after-done" }, description: ResString.cmd_muxAfterDone, 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); private readonly static Option<string?> 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), FFmpegBinaryPath = bindingContext.ParseResult.GetValueForOption(FFmpegBinaryPath),
KeyTextFile = bindingContext.ParseResult.GetValueForOption(KeyTextFile), KeyTextFile = bindingContext.ParseResult.GetValueForOption(KeyTextFile),
DownloadRetryCount = bindingContext.ParseResult.GetValueForOption(DownloadRetryCount), DownloadRetryCount = bindingContext.ParseResult.GetValueForOption(DownloadRetryCount),
MuxAfterDone = bindingContext.ParseResult.GetValueForOption(MuxAfterDone),
}; };
@ -91,10 +93,10 @@ 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) 20220819") var rootCommand = new RootCommand("N_m3u8DL-RE (Beta version) 20220821")
{ {
Input, TmpDir, SaveDir, SaveName, ThreadCount, DownloadRetryCount, AutoSelect, SkipMerge, SkipDownload, CheckSegmentsCount, 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, FFmpegBinaryPath,
LogLevel, UILanguage, UrlProcessorArgs, Keys, KeyTextFile, DecryptionBinaryPath, UseShakaPackager, MP4RealTimeDecryption LogLevel, UILanguage, UrlProcessorArgs, Keys, KeyTextFile, DecryptionBinaryPath, UseShakaPackager, MP4RealTimeDecryption
}; };

View File

@ -86,6 +86,10 @@ namespace N_m3u8DL_RE.CommandLine
/// </summary> /// </summary>
public bool UseShakaPackager { get; set; } public bool UseShakaPackager { get; set; }
/// <summary> /// <summary>
/// See: <see cref="CommandInvoker.MuxAfterDone"/>.
/// </summary>
public bool MuxAfterDone { get; set; }
/// <summary>
/// See: <see cref="CommandInvoker.SubtitleFormat"/>. /// See: <see cref="CommandInvoker.SubtitleFormat"/>.
/// </summary> /// </summary>
public SubtitleFormat SubtitleFormat { get; set; } public SubtitleFormat SubtitleFormat { get; set; }

View File

@ -33,6 +33,7 @@ namespace N_m3u8DL_RE.Config
FFmpegBinaryPath = option.FFmpegBinaryPath; FFmpegBinaryPath = option.FFmpegBinaryPath;
KeyTextFile = option.KeyTextFile; KeyTextFile = option.KeyTextFile;
DownloadRetryCount = option.DownloadRetryCount; DownloadRetryCount = option.DownloadRetryCount;
MuxAfterDone = option.MuxAfterDone;
} }
/// <summary> /// <summary>
@ -92,6 +93,10 @@ namespace N_m3u8DL_RE.Config
/// </summary> /// </summary>
public bool UseShakaPackager { get; set; } public bool UseShakaPackager { get; set; }
/// <summary> /// <summary>
/// 自动混流音视频
/// </summary>
public bool MuxAfterDone { get; set; }
/// <summary>
/// MP4解密所用工具的全路径 /// MP4解密所用工具的全路径
/// </summary> /// </summary>
public string? DecryptionBinaryPath { get; set; } public string? DecryptionBinaryPath { get; set; }

View File

@ -20,6 +20,7 @@ namespace N_m3u8DL_RE.DownloadManager
IDownloader Downloader; IDownloader Downloader;
DownloaderConfig DownloaderConfig; DownloaderConfig DownloaderConfig;
DateTime NowDateTime; DateTime NowDateTime;
List<OutputFile> OutputFiles = new();
public SimpleDownloadManager(DownloaderConfig downloaderConfig) public SimpleDownloadManager(DownloaderConfig downloaderConfig)
{ {
@ -94,7 +95,7 @@ namespace N_m3u8DL_RE.DownloadManager
if (segments == null) return false; if (segments == null) return false;
var type = streamSpec.MediaType ?? Common.Enum.MediaType.VIDEO; 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); dirName = ConvertUtil.GetValidFileName(dirName, filterSlash: true);
var tmpDir = Path.Combine(DownloaderConfig.TmpDir ?? Environment.CurrentDirectory, dirName); 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)) while (File.Exists(output))
{ {
Logger.WarnMarkUp($"{output} => {output = Path.ChangeExtension(output, $"copy" + Path.GetExtension(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 != "") if (DownloaderConfig.MP4RealTimeDecryption && mp4InitFile != "")
{ {
File.Delete(mp4InitFile); File.Delete(mp4InitFile);
@ -464,10 +466,18 @@ namespace N_m3u8DL_RE.DownloadManager
} }
else else
{ {
//ffmpeg合并
var files = FileDic.Values.Select(v => v!.ActualFilePath).OrderBy(s => s).ToArray(); var files = FileDic.Values.Select(v => v!.ActualFilePath).OrderBy(s => s).ToArray();
Logger.InfoMarkUp(ResString.ffmpegMerge); Logger.InfoMarkUp(ResString.ffmpegMerge);
var ext = streamSpec.MediaType == MediaType.AUDIO ? "m4a" : "mp4"; 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.Delete(enc);
File.Move(dec, 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; 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;
} }
} }
} }

View File

@ -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; }
}
}

View File

@ -103,6 +103,7 @@ namespace N_m3u8DL_RE
parserConfig.UrlProcessors.Insert(0, new NowehoryzontyUrlProcessor()); parserConfig.UrlProcessors.Insert(0, new NowehoryzontyUrlProcessor());
var url = string.Empty; 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 = "https://cmafref.akamaized.net/cmaf/live-ull/2006350/akambr/out.mpd"; //直播
//url = "http://playertest.longtailvideo.com/adaptive/oceans_aes/oceans_aes.m3u8"; //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"; //url = "https://vod.sdn.wavve.com/hls/S01/S01_E461382925.1/1/5000/chunklist.m3u8";

View File

@ -1,7 +1,9 @@
using N_m3u8DL_RE.Common.Log; using N_m3u8DL_RE.Common.Log;
using N_m3u8DL_RE.Entity;
using Spectre.Console; using Spectre.Console;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.CommandLine;
using System.Diagnostics; using System.Diagnostics;
using System.Linq; using System.Linq;
using System.Text; 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, public static bool MergeByFFmpeg(string binary, string[] files, string outputPath, string muxFormat, bool useAACFilter,
bool fastStart = false, bool fastStart = false,
bool writeDate = true, string poster = "", string audioName = "", string title = "", 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; 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:\""); StringBuilder command = new StringBuilder("-loglevel warning -i concat:\"");
string ddpAudio = string.Empty; string ddpAudio = string.Empty;
string addPoster = "-map 1 -c:v:1 copy -disposition:v:1 attached_pic"; 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}"); Logger.DebugMarkUp($"{binary}: {command}");
using var p = new Process(); InvokeFFmpeg(binary, command.ToString(), Path.GetDirectoryName(files[0])!);
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();
if (File.Exists($"{outputPath}.{muxFormat}") && new FileInfo($"{outputPath}.{muxFormat}").Length > 0) if (File.Exists($"{outputPath}.{muxFormat}") && new FileInfo($"{outputPath}.{muxFormat}").Length > 0)
return true; return true;
return false; 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;
}
} }
} }