支持调用`shaka-packager`解密

This commit is contained in:
nilaoda 2022-07-24 20:17:52 +08:00
parent 495d8508f5
commit d6bbde3ca4
12 changed files with 173 additions and 41 deletions

View File

@ -29,13 +29,12 @@ namespace N_m3u8DL_RE.Common.Entity
StartRange == segment.StartRange &&
StopRange == segment.StopRange &&
ExpectLength == segment.ExpectLength &&
EqualityComparer<EncryptInfo>.Default.Equals(EncryptInfo, segment.EncryptInfo) &&
Url == segment.Url;
}
public override int GetHashCode()
{
return HashCode.Combine(Index, Duration, Title, StartRange, StopRange, ExpectLength, EncryptInfo, Url);
return HashCode.Combine(Index, Duration, Title, StartRange, StopRange, ExpectLength, Url);
}
}
}

View File

@ -123,6 +123,15 @@ namespace N_m3u8DL_RE.Common.Resource {
}
}
/// <summary>
/// 查找类似 Full path to the tool used for MP4 decryption, like C:\Tools\mp4decrypt.exe 的本地化字符串。
/// </summary>
public static string cmd_decryptionBinaryPath {
get {
return ResourceManager.GetString("cmd_decryptionBinaryPath", resourceCulture);
}
}
/// <summary>
/// 查找类似 Delete temporary files when done 的本地化字符串。
/// </summary>
@ -152,7 +161,7 @@ namespace N_m3u8DL_RE.Common.Resource {
}
/// <summary>
/// 查找类似 Pass decryption key(s) to mp4decrypt. format:
/// 查找类似 Pass decryption key(s) to mp4decrypt/shaka-packager. format:
///--key KID1:KEY1 --key KID2:KEY2 的本地化字符串。
/// </summary>
public static string cmd_keys {
@ -287,6 +296,15 @@ namespace N_m3u8DL_RE.Common.Resource {
}
}
/// <summary>
/// 查找类似 Use shaka-packager instead of mp4decrypt 的本地化字符串。
/// </summary>
public static string cmd_useShakaPackager {
get {
return ResourceManager.GetString("cmd_useShakaPackager", resourceCulture);
}
}
/// <summary>
/// 查找类似 Write meta json after parsed 的本地化字符串。
/// </summary>

View File

@ -231,7 +231,7 @@
<value></value>
</data>
<data name="cmd_keys" xml:space="preserve">
<value>Pass decryption key(s) to mp4decrypt. format:
<value>Pass decryption key(s) to mp4decrypt/shaka-packager. format:
--key KID1:KEY1 --key KID2:KEY2</value>
</data>
<data name="cmd_urlProcessorArgs" xml:space="preserve">
@ -240,4 +240,10 @@
<data name="cmd_MP4RealTimeDecryption" xml:space="preserve">
<value>Decrypt MP4 segments in real time</value>
</data>
<data name="cmd_useShakaPackager" xml:space="preserve">
<value>Use shaka-packager instead of mp4decrypt</value>
</data>
<data name="cmd_decryptionBinaryPath" xml:space="preserve">
<value>Full path to the tool used for MP4 decryption, like C:\Tools\mp4decrypt.exe</value>
</data>
</root>

View File

@ -251,7 +251,7 @@
<value>设置保存文件命名模板, 支持使用变量</value>
</data>
<data name="cmd_keys" xml:space="preserve">
<value>设置解密密钥, 程序调用mp4decrpyt进行解密. 格式:
<value>设置解密密钥, 程序调用mp4decrpyt/shaka-packager进行解密. 格式:
--key KID1:KEY1 --key KID2:KEY2</value>
</data>
<data name="cmd_urlProcessorArgs" xml:space="preserve">
@ -260,4 +260,10 @@
<data name="cmd_MP4RealTimeDecryption" xml:space="preserve">
<value>实时解密MP4分片</value>
</data>
<data name="cmd_useShakaPackager" xml:space="preserve">
<value>使用shaka-packager替代mp4decrypt</value>
</data>
<data name="cmd_decryptionBinaryPath" xml:space="preserve">
<value>MP4解密所用工具的全路径, 例如 C:\Tools\mp4decrypt.exe</value>
</data>
</root>

View File

@ -228,7 +228,7 @@
<value>將輸入Url的Params添加至分片, 對某些網站很有用, 例如 kakao.com</value>
</data>
<data name="cmd_keys" xml:space="preserve">
<value>設置解密密鑰, 程序調用mp4decrpyt進行解密. 格式:
<value>設置解密密鑰, 程序調用mp4decrpyt/shaka-packager進行解密. 格式:
--key KID1:KEY1 --key KID2:KEY2</value>
</data>
<data name="cmd_urlProcessorArgs" xml:space="preserve">
@ -237,4 +237,10 @@
<data name="cmd_MP4RealTimeDecryption" xml:space="preserve">
<value>實時解密MP4分片</value>
</data>
<data name="cmd_useShakaPackager" xml:space="preserve">
<value>使用shaka-packager替代mp4decrypt</value>
</data>
<data name="cmd_decryptionBinaryPath" xml:space="preserve">
<value>MP4解密所用工具的全路徑, 例如 C:\Tools\mp4decrypt.exe</value>
</data>
</root>

View File

@ -46,5 +46,19 @@ namespace N_m3u8DL_RE.Common.Util
str = (ts.Hours.ToString("00") == "00" ? "" : ts.Hours.ToString("00") + "h") + ts.Minutes.ToString("00") + "m" + ts.Seconds.ToString("00") + "s";
return str;
}
/// <summary>
/// 寻找可执行程序
/// </summary>
/// <param name="name"></param>
/// <returns></returns>
public static string? FindExecutable(string name)
{
var fileExt = OperatingSystem.IsWindows() ? ".exe" : "";
var searchPath = new[] { Environment.CurrentDirectory, Environment.ProcessPath };
var envPath = Environment.GetEnvironmentVariable("PATH")?.Split(Path.PathSeparator) ??
Array.Empty<string>();
return searchPath.Concat(envPath).Select(p => Path.Combine(p, name + fileExt)).FirstOrDefault(File.Exists);
}
}
}

View File

@ -33,6 +33,8 @@ namespace N_m3u8DL_RE.CommandLine
private readonly static Option<bool> WriteMetaJson = new(new string[] { "--write-meta-json" }, description: ResString.cmd_writeMetaJson, getDefaultValue: () => true);
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> 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);
class MyOptionBinder : BinderBase<MyOption>
{
@ -63,6 +65,8 @@ namespace N_m3u8DL_RE.CommandLine
Keys = bindingContext.ParseResult.GetValueForOption(Keys),
UrlProcessorArgs = bindingContext.ParseResult.GetValueForOption(UrlProcessorArgs),
MP4RealTimeDecryption = bindingContext.ParseResult.GetValueForOption(MP4RealTimeDecryption),
UseShakaPackager = bindingContext.ParseResult.GetValueForOption(UseShakaPackager),
DecryptionBinaryPath = bindingContext.ParseResult.GetValueForOption(DecryptionBinaryPath),
};
//在这里设置语言
@ -87,11 +91,11 @@ namespace N_m3u8DL_RE.CommandLine
public static async Task<int> InvokeArgs(string[] args, Func<MyOption, Task> action)
{
var rootCommand = new RootCommand("N_m3u8DL-RE (Beta version) 20220723")
var rootCommand = new RootCommand("N_m3u8DL-RE (Beta version) 20220724")
{
Input, TmpDir, SaveDir, SaveName, ThreadCount, AutoSelect, SkipMerge, SkipDownload, CheckSegmentsCount,
BinaryMerge, DelAfterDone, WriteMetaJson, AppendUrlParams, Keys, Headers, /**SavePattern,**/ SubOnly, SubtitleFormat, AutoSubtitleFix,
LogLevel, UILanguage, UrlProcessorArgs, MP4RealTimeDecryption
BinaryMerge, DelAfterDone, WriteMetaJson, AppendUrlParams, Headers, /**SavePattern,**/ SubOnly, SubtitleFormat, AutoSubtitleFix,
LogLevel, UILanguage, UrlProcessorArgs, Keys, DecryptionBinaryPath, UseShakaPackager, MP4RealTimeDecryption
};
rootCommand.TreatUnmatchedTokensAsErrors = true;
rootCommand.SetHandler(async (myOption) => await action(myOption), new MyOptionBinder());

View File

@ -74,6 +74,10 @@ namespace N_m3u8DL_RE.CommandLine
/// </summary>
public bool MP4RealTimeDecryption { get; set; }
/// <summary>
/// See: <see cref="CommandInvoker.UseShakaPackager"/>.
/// </summary>
public bool UseShakaPackager { get; set; }
/// <summary>
/// See: <see cref="CommandInvoker.SubtitleFormat"/>.
/// </summary>
public SubtitleFormat SubtitleFormat { get; set; }
@ -97,5 +101,9 @@ namespace N_m3u8DL_RE.CommandLine
/// See: <see cref="CommandInvoker.UILanguage"/>.
/// </summary>
public string? UILanguage { get; set; }
/// <summary>
/// See: <see cref="CommandInvoker.DecryptionBinaryPath"/>.
/// </summary>
public string? DecryptionBinaryPath { get; set; }
}
}

View File

@ -28,6 +28,8 @@ namespace N_m3u8DL_RE.Config
SavePattern = option.SavePattern;
Keys = option.Keys;
MP4RealTimeDecryption = option.MP4RealTimeDecryption;
UseShakaPackager = option.UseShakaPackager;
DecryptionBinaryPath = option.DecryptionBinaryPath;
}
/// <summary>
@ -79,6 +81,14 @@ namespace N_m3u8DL_RE.Config
/// </summary>
public bool MP4RealTimeDecryption { get; set; } = true;
/// <summary>
/// 使用shaka-packager替代mp4decrypt
/// </summary>
public bool UseShakaPackager { get; set; }
/// <summary>
/// MP4解密所用工具的全路径
/// </summary>
public string? DecryptionBinaryPath { get; set; }
/// <summary>
/// 字幕格式
/// </summary>
public SubtitleFormat SubtitleFormat { get; set; } = SubtitleFormat.VTT;

View File

@ -52,13 +52,10 @@ namespace N_m3u8DL_RE.DownloadManager
var output = Path.Combine(saveDir, saveName + $".{streamSpec.Extension ?? "ts"}");
//mp4decrypt
var APP_DIR = Path.GetDirectoryName(Environment.ProcessPath)!;
var fileName = "mp4decrypt";
if (Environment.OSVersion.Platform == PlatformID.Win32NT)
fileName += ".exe";
var mp4decrypt = Path.Combine(APP_DIR, fileName);
if (!File.Exists(mp4decrypt)) mp4decrypt = fileName;
var mp4decrypt = DownloaderConfig.DecryptionBinaryPath!;
var mp4InitFile = "";
var mp4InitFileDec = "";
var currentKID = "";
Logger.Debug($"dirName: {dirName}; tmpDir: {tmpDir}; saveDir: {saveDir}; saveName: {saveName}; output: {output}");
@ -106,15 +103,18 @@ namespace N_m3u8DL_RE.DownloadManager
if (info.Scheme != null) Logger.WarnMarkUp($"[grey]Type: {info.Scheme}[/]");
if (info.PSSH != null) Logger.WarnMarkUp($"[grey]PSSH(WV): {info.PSSH}[/]");
if (info.KID != null) Logger.WarnMarkUp($"[grey]KID: {info.KID}[/]");
currentKID = info.KID;
//实时解密
if (DownloaderConfig.MP4RealTimeDecryption && streamSpec.Playlist.MediaInit.EncryptInfo.Method != Common.Enum.EncryptMethod.NONE)
{
var enc = result.ActualFilePath;
var dec = Path.Combine(Path.GetDirectoryName(enc)!, Path.GetFileNameWithoutExtension(enc) + "_dec" + Path.GetExtension(enc));
var dResult = await MP4DecryptUtil.DecryptAsync(mp4decrypt, DownloaderConfig.Keys, enc, dec);
var dResult = await MP4DecryptUtil.DecryptAsync(DownloaderConfig.UseShakaPackager, mp4decrypt, DownloaderConfig.Keys, enc, dec, currentKID);
if (dResult)
{
result.ActualFilePath = dec;
//实时解密不需要init文件用于合并
FileDic!.Remove(streamSpec.Playlist.MediaInit, out _);
mp4InitFileDec = dec;
}
}
}
@ -138,7 +138,7 @@ namespace N_m3u8DL_RE.DownloadManager
{
var enc = result.ActualFilePath;
var dec = Path.Combine(Path.GetDirectoryName(enc)!, Path.GetFileNameWithoutExtension(enc) + "_dec" + Path.GetExtension(enc));
var dResult = await MP4DecryptUtil.DecryptAsync(mp4decrypt, DownloaderConfig.Keys, enc, dec, mp4InitFile);
var dResult = await MP4DecryptUtil.DecryptAsync(DownloaderConfig.UseShakaPackager, mp4decrypt, DownloaderConfig.Keys, enc, dec, currentKID, mp4InitFile);
if (dResult)
{
File.Delete(enc);
@ -150,6 +150,8 @@ namespace N_m3u8DL_RE.DownloadManager
if (DownloaderConfig.MP4RealTimeDecryption && mp4InitFile != "")
{
File.Delete(mp4InitFile);
if (mp4InitFileDec != "")
File.Delete(mp4InitFileDec);
}
//校验分片数量
@ -345,7 +347,7 @@ namespace N_m3u8DL_RE.DownloadManager
}
//删除临时文件夹
if (DownloaderConfig.DelAfterDone)
if (!DownloaderConfig.SkipMerge && DownloaderConfig.DelAfterDone)
{
var files = FileDic.Values.Select(v => v!.ActualFilePath);
foreach (var file in files)
@ -359,14 +361,14 @@ namespace N_m3u8DL_RE.DownloadManager
}
//调用mp4decrypt解密
if (!DownloaderConfig.MP4RealTimeDecryption && DownloaderConfig.Keys != null && DownloaderConfig.Keys.Length > 0)
if (File.Exists(output) && !DownloaderConfig.MP4RealTimeDecryption && DownloaderConfig.Keys != null && DownloaderConfig.Keys.Length > 0)
{
if (totalCount > 1 && streamSpec.Playlist!.MediaParts.First().MediaSegments.First().EncryptInfo.Method != Common.Enum.EncryptMethod.NONE)
{
var enc = output;
var dec = Path.Combine(Path.GetDirectoryName(enc)!, Path.GetFileNameWithoutExtension(enc) + "_dec" + Path.GetExtension(enc));
Logger.InfoMarkUp($"[grey]Decrypting...[/]");
var result = await MP4DecryptUtil.DecryptAsync(mp4decrypt, DownloaderConfig.Keys, enc, dec);
var result = await MP4DecryptUtil.DecryptAsync(DownloaderConfig.UseShakaPackager, mp4decrypt, DownloaderConfig.Keys, enc, dec, currentKID);
if (result)
{
File.Delete(enc);

View File

@ -38,6 +38,33 @@ namespace N_m3u8DL_RE
try
{
//预先检查
if (option.Keys != null && option.Keys.Length > 0)
{
if (string.IsNullOrEmpty(option.DecryptionBinaryPath))
{
if (option.UseShakaPackager)
{
var file = GlobalUtil.FindExecutable("shaka-packager");
var file2 = GlobalUtil.FindExecutable("packager-linux-x64");
var file3 = GlobalUtil.FindExecutable("packager-osx-x64");
var file4 = GlobalUtil.FindExecutable("packager-win-x64");
if (file == null && file2 == null && file3 == null && file4 == null) throw new FileNotFoundException("shaka-packager not found!");
option.DecryptionBinaryPath = file ?? file2 ?? file3 ?? file4;
}
else
{
var file = GlobalUtil.FindExecutable("mp4decrypt");
if (file == null) throw new FileNotFoundException("mp4decrypt not found!");
option.DecryptionBinaryPath = file;
}
}
else if (!File.Exists(option.DecryptionBinaryPath))
{
throw new FileNotFoundException(option.DecryptionBinaryPath);
}
}
var parserConfig = new ParserConfig()
{
AppendUrlParams = option.AppendUrlParams,

View File

@ -1,41 +1,73 @@
using N_m3u8DL_RE.Config;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Diagnostics;
namespace N_m3u8DL_RE.Util
{
internal class MP4DecryptUtil
{
public static async Task<bool> DecryptAsync(string bin, string[]? keys, string source, string dest, string init = "")
public static async Task<bool> DecryptAsync(bool shakaPackager, string bin, string[]? keys, string source, string dest, string? kid, string init = "")
{
if (keys == null || keys.Length == 0) return false;
var cmd = string.Join(" ", keys.Select(k => $"--key {k}"));
var keyPair = keys.First();
if (!string.IsNullOrEmpty(kid))
{
var test = keys.Where(k => k.StartsWith(kid));
if (test.Any()) keyPair = test.First();
}
if (keyPair == null) return false;
//shakaPackager 无法单独解密init文件
if (source.EndsWith("_init.mp4") && shakaPackager) return true;
var cmd = "";
var tmpFile = "";
if (shakaPackager)
{
var enc = source;
//shakaPackager 手动构造文件
if (init != "")
{
tmpFile = Path.ChangeExtension(source, ".itmp");
DownloadUtil.CombineMultipleFilesIntoSingleFile(new string[] { init, source }, tmpFile);
enc = tmpFile;
}
cmd = $"--enable_raw_key_decryption input=\"{enc}\",stream=0,output=\"{dest}\" " +
$"--keys key_id={keyPair.Split(':')[0]}:key={keyPair.Split(':')[1]}";
}
else
{
cmd = string.Join(" ", keys.Select(k => $"--key {k}"));
if (init != "")
{
cmd += $" --fragments-info \"{init}\" ";
}
cmd += $" \"{source}\" \"{dest}\"";
}
await Process.Start(new ProcessStartInfo()
{
FileName = bin,
Arguments = cmd,
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false
})!.WaitForExitAsync();
await RunCommandAsync(bin, cmd);
if (File.Exists(dest) && new FileInfo(dest).Length > 0)
{
if (tmpFile != "" && File.Exists(tmpFile)) File.Delete(tmpFile);
return true;
}
return false;
}
private static async Task RunCommandAsync(string name, string arg)
{
await Process.Start(new ProcessStartInfo()
{
FileName = name,
Arguments = arg,
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false
})!.WaitForExitAsync();
}
}
}