使用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>
/// 查找类似 Bad m3u8 的本地化字符串。
/// </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>
/// 查找类似 Extracting TTML(raw) subtitle... 的本地化字符串。
/// </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>
/// 查找类似 Trying to search for KEY from text file... 的本地化字符串。
/// </summary>

View File

@ -264,4 +264,16 @@
<data name="searchKey" xml:space="preserve">
<value>Trying to search for KEY from text file...</value>
</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>

View File

@ -284,4 +284,16 @@
<data name="searchKey" xml:space="preserve">
<value>正在尝试从文本文件搜索KEY...</value>
</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>

View File

@ -261,4 +261,16 @@
<data name="searchKey" xml:space="preserve">
<value>正在嘗試從文本文件搜尋KEY...</value>
</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>

View File

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

View File

@ -26,9 +26,7 @@ namespace N_m3u8DL_RE.DownloadManager
NowDateTime = DateTime.Now;
}
private async Task<bool> DownloadStreamAsync(StreamSpec streamSpec, ProgressTask task)
{
string? ReadInit(byte[] data)
private string? ReadInit(byte[] data)
{
var info = MP4InitUtil.ReadInit(data);
if (info.Scheme != null) Logger.WarnMarkUp($"[grey]Type: {info.Scheme}[/]");
@ -37,6 +35,28 @@ namespace N_m3u8DL_RE.DownloadManager
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();
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 saveName = DownloaderConfig.SaveName != null ? $"{DownloaderConfig.SaveName}.{type}.{streamSpec.Language}".TrimEnd('.') : dirName;
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
var mp4decrypt = DownloaderConfig.DecryptionBinaryPath!;
var mp4InitFile = "";
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);
@ -83,7 +98,13 @@ namespace N_m3u8DL_RE.DownloadManager
//下载init
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 result = await Downloader.DownloadSegmentAsync(streamSpec.Playlist.MediaInit, path, headers);
FileDic[streamSpec.Playlist.MediaInit] = result;
@ -94,12 +115,6 @@ namespace N_m3u8DL_RE.DownloadManager
mp4InitFile = result.ActualFilePath;
task.Increment(1);
//修改输出后缀
if (streamSpec.MediaType == Common.Enum.MediaType.AUDIO)
output = Path.ChangeExtension(output, ".m4a");
else
output = Path.ChangeExtension(output, ".mp4");
//读取mp4信息
if (result != null && result.Success)
{
@ -125,11 +140,53 @@ namespace N_m3u8DL_RE.DownloadManager
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');
//下载第一个分片
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()
{
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 != "")
{
File.Delete(mp4InitFile);
@ -346,13 +416,6 @@ namespace N_m3u8DL_RE.DownloadManager
//合并
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)
{

View File

@ -47,7 +47,7 @@ namespace N_m3u8DL_RE.Downloader
}
else if (segment.EncryptInfo.Method == EncryptMethod.SAMPLE_AES_CTR)
{
throw new NotSupportedException("SAMPLE-AES-CTR");
//throw new NotSupportedException("SAMPLE-AES-CTR");
}
}
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
{
//预先检查ffmpeg
if (!option.BinaryMerge)
{
if (option.FFmpegBinaryPath == null)
option.FFmpegBinaryPath = GlobalUtil.FindExecutable("ffmpeg");
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)
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)
{
Logger.Warn(ResString.writeJson);
@ -228,7 +233,11 @@ namespace N_m3u8DL_RE
}
catch (Exception ex)
{
Logger.Error(ex.ToString());
string msg = ex.Message;
#if DEBUG
msg = ex.ToString();
#endif
Logger.Error(msg);
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;
//同名文件已存在的共存策略
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;
StringBuilder command = new StringBuilder("-loglevel warning -i concat:\"");
string ddpAudio = string.Empty;