使用ffmpeg读取媒体信息以辅助判断媒体类型

This commit is contained in:
nilaoda 2022-08-15 00:19:13 +08:00
parent 9be32b2872
commit df354e4eb8
11 changed files with 306 additions and 38 deletions

View File

@ -69,6 +69,24 @@ namespace N_m3u8DL_RE.Common.Resource {
} }
} }
/// <summary>
/// 查找类似 Dolby Vision content is detected, binary merging is automatically enabled 的本地化字符串。
/// </summary>
public static string autoBinaryMerge2 {
get {
return ResourceManager.GetString("autoBinaryMerge2", resourceCulture);
}
}
/// <summary>
/// 查找类似 An unrecognized encryption method is detected, binary merging is automatically enabled 的本地化字符串。
/// </summary>
public static string autoBinaryMerge3 {
get {
return ResourceManager.GetString("autoBinaryMerge3", resourceCulture);
}
}
/// <summary> /// <summary>
/// 查找类似 Bad m3u8 的本地化字符串。 /// 查找类似 Bad m3u8 的本地化字符串。
/// </summary> /// </summary>
@ -368,6 +386,15 @@ namespace N_m3u8DL_RE.Common.Resource {
} }
} }
/// <summary>
/// 查找类似 ffmpeg not found, please download at: https://ffmpeg.org/download.html 的本地化字符串。
/// </summary>
public static string ffmpegNotFound {
get {
return ResourceManager.GetString("ffmpegNotFound", resourceCulture);
}
}
/// <summary> /// <summary>
/// 查找类似 Extracting TTML(raw) subtitle... 的本地化字符串。 /// 查找类似 Extracting TTML(raw) subtitle... 的本地化字符串。
/// </summary> /// </summary>
@ -503,6 +530,15 @@ namespace N_m3u8DL_RE.Common.Resource {
} }
} }
/// <summary>
/// 查找类似 Reading media info... 的本地化字符串。
/// </summary>
public static string readingInfo {
get {
return ResourceManager.GetString("readingInfo", resourceCulture);
}
}
/// <summary> /// <summary>
/// 查找类似 Trying to search for KEY from text file... 的本地化字符串。 /// 查找类似 Trying to search for KEY from text file... 的本地化字符串。
/// </summary> /// </summary>

View File

@ -264,4 +264,16 @@
<data name="searchKey" xml:space="preserve"> <data name="searchKey" xml:space="preserve">
<value>Trying to search for KEY from text file...</value> <value>Trying to search for KEY from text file...</value>
</data> </data>
<data name="ffmpegNotFound" xml:space="preserve">
<value>ffmpeg not found, please download at: https://ffmpeg.org/download.html</value>
</data>
<data name="readingInfo" xml:space="preserve">
<value>Reading media info...</value>
</data>
<data name="autoBinaryMerge2" xml:space="preserve">
<value>Dolby Vision content is detected, binary merging is automatically enabled</value>
</data>
<data name="autoBinaryMerge3" xml:space="preserve">
<value>An unrecognized encryption method is detected, binary merging is automatically enabled</value>
</data>
</root> </root>

View File

@ -284,4 +284,16 @@
<data name="searchKey" xml:space="preserve"> <data name="searchKey" xml:space="preserve">
<value>正在尝试从文本文件搜索KEY...</value> <value>正在尝试从文本文件搜索KEY...</value>
</data> </data>
<data name="ffmpegNotFound" xml:space="preserve">
<value>找不到ffmpeg请自行下载https://ffmpeg.org/download.html</value>
</data>
<data name="readingInfo" xml:space="preserve">
<value>读取媒体信息...</value>
</data>
<data name="autoBinaryMerge2" xml:space="preserve">
<value>检测到杜比视界内容,自动开启二进制合并</value>
</data>
<data name="autoBinaryMerge3" xml:space="preserve">
<value>检测到无法识别的加密方式,自动开启二进制合并</value>
</data>
</root> </root>

View File

@ -261,4 +261,16 @@
<data name="searchKey" xml:space="preserve"> <data name="searchKey" xml:space="preserve">
<value>正在嘗試從文本文件搜尋KEY...</value> <value>正在嘗試從文本文件搜尋KEY...</value>
</data> </data>
<data name="ffmpegNotFound" xml:space="preserve">
<value>找不到ffmpeg請自行下載https://ffmpeg.org/download.html</value>
</data>
<data name="readingInfo" xml:space="preserve">
<value>讀取媒體訊息...</value>
</data>
<data name="autoBinaryMerge2" xml:space="preserve">
<value>檢測到杜比視界內容,自動開啟二進位制合併</value>
</data>
<data name="autoBinaryMerge3" xml:space="preserve">
<value>檢測到無法識別的加密方式,自動開啟二進位制合併</value>
</data>
</root> </root>

View File

@ -57,7 +57,7 @@ namespace N_m3u8DL_RE.Parser.Processor.HLS
} }
catch (Exception ex) catch (Exception ex)
{ {
Logger.Error(ResString.cmd_loadKeyFailed + ": " + ex.ToString()); Logger.Error(ResString.cmd_loadKeyFailed + ": " + ex.Message);
encryptInfo.Method = EncryptMethod.UNKNOWN; encryptInfo.Method = EncryptMethod.UNKNOWN;
} }

View File

@ -26,9 +26,7 @@ namespace N_m3u8DL_RE.DownloadManager
NowDateTime = DateTime.Now; NowDateTime = DateTime.Now;
} }
private async Task<bool> DownloadStreamAsync(StreamSpec streamSpec, ProgressTask task) private string? ReadInit(byte[] data)
{
string? ReadInit(byte[] data)
{ {
var info = MP4InitUtil.ReadInit(data); var info = MP4InitUtil.ReadInit(data);
if (info.Scheme != null) Logger.WarnMarkUp($"[grey]Type: {info.Scheme}[/]"); if (info.Scheme != null) Logger.WarnMarkUp($"[grey]Type: {info.Scheme}[/]");
@ -37,6 +35,28 @@ namespace N_m3u8DL_RE.DownloadManager
return info.KID; return info.KID;
} }
private void ChangeSpecInfo(StreamSpec streamSpec, List<Mediainfo> mediainfos)
{
if (!DownloaderConfig.BinaryMerge && mediainfos.Any(m => m.DolbyVison == true))
{
DownloaderConfig.BinaryMerge = true;
Logger.WarnMarkUp(ResString.autoBinaryMerge2);
}
if (mediainfos.All(m => m.Type == "Audio"))
{
streamSpec.MediaType = MediaType.AUDIO;
}
else if (mediainfos.All(m => m.Type == "Subtitle"))
{
streamSpec.MediaType = MediaType.SUBTITLES;
if (streamSpec.Extension == null || streamSpec.Extension == "ts")
streamSpec.Extension = "vtt";
}
}
private async Task<bool> DownloadStreamAsync(StreamSpec streamSpec, ProgressTask task)
{
ConcurrentDictionary<MediaSegment, DownloadResult?> FileDic = new(); ConcurrentDictionary<MediaSegment, DownloadResult?> FileDic = new();
var segments = streamSpec.Playlist?.MediaParts.SelectMany(m => m.MediaSegments); var segments = streamSpec.Playlist?.MediaParts.SelectMany(m => m.MediaSegments);
@ -50,19 +70,14 @@ namespace N_m3u8DL_RE.DownloadManager
var saveDir = DownloaderConfig.SaveDir ?? Environment.CurrentDirectory; var saveDir = DownloaderConfig.SaveDir ?? Environment.CurrentDirectory;
var saveName = DownloaderConfig.SaveName != null ? $"{DownloaderConfig.SaveName}.{type}.{streamSpec.Language}".TrimEnd('.') : dirName; var saveName = DownloaderConfig.SaveName != null ? $"{DownloaderConfig.SaveName}.{type}.{streamSpec.Language}".TrimEnd('.') : dirName;
var headers = DownloaderConfig.Headers; var headers = DownloaderConfig.Headers;
var output = Path.Combine(saveDir, saveName + $".{streamSpec.Extension ?? "ts"}");
//检测目标文件是否存在
while (File.Exists(output))
{
Logger.WarnMarkUp($"{output} => {output = Path.ChangeExtension(output, $"copy" + Path.GetExtension(output))}");
}
//mp4decrypt //mp4decrypt
var mp4decrypt = DownloaderConfig.DecryptionBinaryPath!; var mp4decrypt = DownloaderConfig.DecryptionBinaryPath!;
var mp4InitFile = ""; var mp4InitFile = "";
var currentKID = ""; var currentKID = "";
var readInfo = false; //是否读取过
Logger.Debug($"dirName: {dirName}; tmpDir: {tmpDir}; saveDir: {saveDir}; saveName: {saveName}; output: {output}"); Logger.Debug($"dirName: {dirName}; tmpDir: {tmpDir}; saveDir: {saveDir}; saveName: {saveName}");
//创建文件夹 //创建文件夹
if (!Directory.Exists(tmpDir)) Directory.CreateDirectory(tmpDir); if (!Directory.Exists(tmpDir)) Directory.CreateDirectory(tmpDir);
@ -83,7 +98,13 @@ namespace N_m3u8DL_RE.DownloadManager
//下载init //下载init
if (streamSpec.Playlist?.MediaInit != null) if (streamSpec.Playlist?.MediaInit != null)
{ {
totalCount++; //对于fMP4自动开启二进制合并
if (!DownloaderConfig.BinaryMerge && streamSpec.MediaType != MediaType.SUBTITLES)
{
DownloaderConfig.BinaryMerge = true;
Logger.WarnMarkUp($"[darkorange3_1]{ResString.autoBinaryMerge}[/]");
}
var path = Path.Combine(tmpDir, "_init.mp4.tmp"); var path = Path.Combine(tmpDir, "_init.mp4.tmp");
var result = await Downloader.DownloadSegmentAsync(streamSpec.Playlist.MediaInit, path, headers); var result = await Downloader.DownloadSegmentAsync(streamSpec.Playlist.MediaInit, path, headers);
FileDic[streamSpec.Playlist.MediaInit] = result; FileDic[streamSpec.Playlist.MediaInit] = result;
@ -94,12 +115,6 @@ namespace N_m3u8DL_RE.DownloadManager
mp4InitFile = result.ActualFilePath; mp4InitFile = result.ActualFilePath;
task.Increment(1); task.Increment(1);
//修改输出后缀
if (streamSpec.MediaType == Common.Enum.MediaType.AUDIO)
output = Path.ChangeExtension(output, ".m4a");
else
output = Path.ChangeExtension(output, ".mp4");
//读取mp4信息 //读取mp4信息
if (result != null && result.Success) if (result != null && result.Success)
{ {
@ -125,11 +140,53 @@ namespace N_m3u8DL_RE.DownloadManager
FileDic[streamSpec.Playlist.MediaInit]!.ActualFilePath = dec; FileDic[streamSpec.Playlist.MediaInit]!.ActualFilePath = dec;
} }
} }
//ffmpeg读取信息
if (!readInfo)
{
Logger.WarnMarkUp(ResString.readingInfo);
var mediainfos = await MediainfoUtil.ReadInfoAsync(DownloaderConfig.FFmpegBinaryPath!, result.ActualFilePath);
mediainfos.ForEach(info => Logger.InfoMarkUp(info.ToStringMarkUp()));
ChangeSpecInfo(streamSpec, mediainfos);
readInfo = true;
}
} }
} }
//开始下载 //计算填零个数
var pad = "0".PadLeft(segments.Count().ToString().Length, '0'); var pad = "0".PadLeft(segments.Count().ToString().Length, '0');
//下载第一个分片
if (!readInfo)
{
var seg = segments.First();
segments = segments.Skip(1);
var index = seg.Index;
var path = Path.Combine(tmpDir, index.ToString(pad) + $".{streamSpec.Extension ?? "clip"}.tmp");
var result = await Downloader.DownloadSegmentAsync(seg, path, headers);
FileDic[seg] = result;
task.Increment(1);
//实时解密
if (DownloaderConfig.MP4RealTimeDecryption && seg.EncryptInfo.Method != Common.Enum.EncryptMethod.NONE && result != null)
{
var enc = result.ActualFilePath;
var dec = Path.Combine(Path.GetDirectoryName(enc)!, Path.GetFileNameWithoutExtension(enc) + "_dec" + Path.GetExtension(enc));
var dResult = await MP4DecryptUtil.DecryptAsync(DownloaderConfig.UseShakaPackager, mp4decrypt, DownloaderConfig.Keys, enc, dec, currentKID, mp4InitFile);
if (dResult)
{
File.Delete(enc);
result.ActualFilePath = dec;
}
}
//ffmpeg读取信息
Logger.WarnMarkUp(ResString.readingInfo);
var mediainfos = await MediainfoUtil.ReadInfoAsync(DownloaderConfig.FFmpegBinaryPath!, result!.ActualFilePath);
mediainfos.ForEach(info => Logger.InfoMarkUp(info.ToStringMarkUp()));
ChangeSpecInfo(streamSpec, mediainfos);
readInfo = true;
}
//开始下载
var options = new ParallelOptions() var options = new ParallelOptions()
{ {
MaxDegreeOfParallelism = DownloaderConfig.ThreadCount MaxDegreeOfParallelism = DownloaderConfig.ThreadCount
@ -155,6 +212,19 @@ namespace N_m3u8DL_RE.DownloadManager
} }
}); });
var output = Path.Combine(saveDir, saveName + $".{streamSpec.Extension ?? "ts"}");
//检测目标文件是否存在
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 != "") if (DownloaderConfig.MP4RealTimeDecryption && mp4InitFile != "")
{ {
File.Delete(mp4InitFile); File.Delete(mp4InitFile);
@ -346,13 +416,6 @@ namespace N_m3u8DL_RE.DownloadManager
//合并 //合并
if (!DownloaderConfig.SkipMerge) if (!DownloaderConfig.SkipMerge)
{ {
//对于fMP4自动开启二进制合并
if (!DownloaderConfig.BinaryMerge && streamSpec.MediaType != MediaType.SUBTITLES && mp4InitFile != "")
{
DownloaderConfig.BinaryMerge = true;
Logger.WarnMarkUp($"[white on darkorange3_1]{ResString.autoBinaryMerge}[/]");
}
//字幕也使用二进制合并 //字幕也使用二进制合并
if (DownloaderConfig.BinaryMerge || streamSpec.MediaType == MediaType.SUBTITLES) if (DownloaderConfig.BinaryMerge || streamSpec.MediaType == MediaType.SUBTITLES)
{ {

View File

@ -47,7 +47,7 @@ namespace N_m3u8DL_RE.Downloader
} }
else if (segment.EncryptInfo.Method == EncryptMethod.SAMPLE_AES_CTR) else if (segment.EncryptInfo.Method == EncryptMethod.SAMPLE_AES_CTR)
{ {
throw new NotSupportedException("SAMPLE-AES-CTR"); //throw new NotSupportedException("SAMPLE-AES-CTR");
} }
} }
return dResult; return dResult;

View File

@ -0,0 +1,31 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Spectre.Console;
namespace N_m3u8DL_RE.Entity
{
internal class Mediainfo
{
public string Id { get; set; }
public string Text { get; set; }
public string BaseInfo { get; set; }
public string Bitrate { get; set; }
public string Resolution { get; set; }
public string Fps { get; set; }
public string Type { get; set; }
public bool DolbyVison { get; set; }
public override string? ToString()
{
return $"{(string.IsNullOrEmpty(Id) ? "NaN" : Id)}: " + string.Join(", ", new List<string> { Type, BaseInfo, Resolution, Fps, Bitrate }.Where(i => !string.IsNullOrEmpty(i)));
}
public string ToStringMarkUp()
{
return "[steelblue]" + ToString().EscapeMarkup() + (DolbyVison ? " [darkorange3_1][[DOVI]][/]" : "") + "[/]";
}
}
}

View File

@ -39,13 +39,12 @@ namespace N_m3u8DL_RE
try try
{ {
//预先检查ffmpeg //预先检查ffmpeg
if (!option.BinaryMerge) if (option.FFmpegBinaryPath == null)
{
option.FFmpegBinaryPath = GlobalUtil.FindExecutable("ffmpeg"); option.FFmpegBinaryPath = GlobalUtil.FindExecutable("ffmpeg");
if (string.IsNullOrEmpty(option.FFmpegBinaryPath)) if (string.IsNullOrEmpty(option.FFmpegBinaryPath))
{ {
throw new FileNotFoundException("ffmpeg not found!"); throw new FileNotFoundException(ResString.ffmpegNotFound);
}
} }
//预先检查 //预先检查
@ -192,6 +191,12 @@ namespace N_m3u8DL_RE
if (lists.Count() > 1) if (lists.Count() > 1)
await extractor.FetchPlayListAsync(selectedStreams); await extractor.FetchPlayListAsync(selectedStreams);
//无法识别的加密方式,自动开启二进制合并
if (selectedStreams.Any(s => s.Playlist.MediaParts.Any(p => p.MediaSegments.Any(m => m.EncryptInfo.Method == EncryptMethod.UNKNOWN))))
{
Logger.WarnMarkUp($"[darkorange3_1]{ResString.autoBinaryMerge3}[/]");
}
if (option.WriteMetaJson) if (option.WriteMetaJson)
{ {
Logger.Warn(ResString.writeJson); Logger.Warn(ResString.writeJson);
@ -228,7 +233,11 @@ namespace N_m3u8DL_RE
} }
catch (Exception ex) catch (Exception ex)
{ {
Logger.Error(ex.ToString()); string msg = ex.Message;
#if DEBUG
msg = ex.ToString();
#endif
Logger.Error(msg);
await Task.Delay(3000); await Task.Delay(3000);
} }
} }

View File

@ -0,0 +1,86 @@
using N_m3u8DL_RE.Entity;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Xml.Linq;
namespace N_m3u8DL_RE.Util
{
internal partial class MediainfoUtil
{
[RegexGenerator(" Stream #.*")]
private static partial Regex TextRegex();
[RegexGenerator("#0:\\d(\\[0x\\w+?\\])")]
private static partial Regex IdRegex();
[RegexGenerator(": (\\w+): (.*)")]
private static partial Regex TypeRegex();
[RegexGenerator("(.*?)(,|$)")]
private static partial Regex BaseInfoRegex();
[RegexGenerator(" \\/ 0x\\w+")]
private static partial Regex ReplaceRegex();
[RegexGenerator("\\d{2,}x\\d+")]
private static partial Regex ResRegex();
[RegexGenerator("\\d+ kb\\/s")]
private static partial Regex BitrateRegex();
[RegexGenerator("\\d+ fps")]
private static partial Regex FpsRegex();
public static async Task<List<Mediainfo>> ReadInfoAsync(string binary, string file)
{
var result = new List<Mediainfo>();
if (string.IsNullOrEmpty(file) || !File.Exists(file)) return result;
string cmd = "-hide_banner -i \"" + file + "\"";
var p = Process.Start(new ProcessStartInfo()
{
FileName = binary,
Arguments = cmd,
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false
})!;
var output = p.StandardError.ReadToEnd();
await p.WaitForExitAsync();
foreach (Match stream in TextRegex().Matches(output))
{
var info = new Mediainfo()
{
Text = TypeRegex().Match(stream.Value).Groups[2].Value,
Id = IdRegex().Match(stream.Value).Groups[1].Value,
Type = TypeRegex().Match(stream.Value).Groups[1].Value,
};
info.Resolution = ResRegex().Match(info.Text).Value;
info.Bitrate = BitrateRegex().Match(info.Text).Value;
info.Fps = FpsRegex().Match(info.Text).Value;
info.BaseInfo = BaseInfoRegex().Match(info.Text).Groups[1].Value;
info.BaseInfo = ReplaceRegex().Replace(info.BaseInfo, "");
if (info.BaseInfo.Contains("dvhe")
|| info.BaseInfo.Contains("dvh1")
|| info.BaseInfo.Contains("DOVI")
|| info.Type.Contains("dvvideo")
)
info.DolbyVison = true;
result.Add(info);
}
if (result.Count == 0)
{
result.Add(new Mediainfo()
{
Type = "Unknown"
});
}
return result;
}
}
}

View File

@ -51,6 +51,13 @@ 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"));
}
bool useAACFilter = true; bool useAACFilter = true;
StringBuilder command = new StringBuilder("-loglevel warning -i concat:\""); StringBuilder command = new StringBuilder("-loglevel warning -i concat:\"");
string ddpAudio = string.Empty; string ddpAudio = string.Empty;