Fixed ffmpeg cover art not showing on android as discussed.

This commit is contained in:
Michael Bucari-Tovo 2021-06-28 11:19:03 -06:00
parent b53aabe0e3
commit f148650e57
2 changed files with 150 additions and 71 deletions

View File

@ -132,8 +132,7 @@ namespace AaxDecrypter
public bool Step2_DownloadAndCombine() public bool Step2_DownloadAndCombine()
{ {
var aaxcProcesser = new FFMpegAaxcProcesser(downloadLicense);
var aaxcProcesser = new FFMpegAaxcProcesser(DecryptSupportLibraries.ffmpegPath);
aaxcProcesser.ProgressUpdate += AaxcProcesser_ProgressUpdate; aaxcProcesser.ProgressUpdate += AaxcProcesser_ProgressUpdate;
string metadataPath = null; string metadataPath = null;
@ -147,10 +146,6 @@ namespace AaxDecrypter
} }
aaxcProcesser.ProcessBook( aaxcProcesser.ProcessBook(
downloadLicense.DownloadUrl,
downloadLicense.UserAgent,
downloadLicense.AudibleKey,
downloadLicense.AudibleIV,
outputFileName, outputFileName,
metadataPath) metadataPath)
.GetAwaiter() .GetAwaiter()
@ -172,9 +167,8 @@ namespace AaxDecrypter
} }
/// <summary> /// <summary>
/// Copy all aacx metadata to m4b file. /// Copy all aacx metadata to m4b file, including cover art.
/// </summary> /// </summary>
/// <returns></returns>
public bool Step3_RestoreMetadata() public bool Step3_RestoreMetadata()
{ {
var outFile = new TagLib.Mpeg4.File(outputFileName, TagLib.ReadStyle.Average); var outFile = new TagLib.Mpeg4.File(outputFileName, TagLib.ReadStyle.Average);
@ -186,6 +180,8 @@ namespace AaxDecrypter
//copy all metadata fields in the source file, even those that TagLib doesn't //copy all metadata fields in the source file, even those that TagLib doesn't
//recognize, to the output file. //recognize, to the output file.
//NOTE: Chapters aren't stored in MPEG-4 metadata. They are encoded as a Timed
//Text Stream (MPEG-4 Part 17), so taglib doesn't read or write them.
foreach (var stag in sourceTag) foreach (var stag in sourceTag)
{ {
destTags.SetData(stag.BoxType, stag.Children.Cast<TagLib.Mpeg4.AppleDataBox>().ToArray()); destTags.SetData(stag.BoxType, stag.Children.Cast<TagLib.Mpeg4.AppleDataBox>().ToArray());

View File

@ -7,96 +7,96 @@ using System.Threading.Tasks;
namespace AaxDecrypter namespace AaxDecrypter
{ {
/// <summary> /// <summary>
/// Download audible aaxc, decrypt, remux,and add metadata. /// Download audible aaxc, decrypt, and remux with chapters.
/// </summary> /// </summary>
class FFMpegAaxcProcesser class FFMpegAaxcProcesser
{ {
public event EventHandler<TimeSpan> ProgressUpdate; public event EventHandler<TimeSpan> ProgressUpdate;
public string FFMpegPath { get; } public string FFMpegPath { get; }
public DownloadLicense DownloadLicense { get; }
public bool IsRunning { get; private set; } public bool IsRunning { get; private set; }
public bool Succeeded { get; private set; } public bool Succeeded { get; private set; }
public string FFMpegStandardError => ffmpegError.ToString(); public string FFMpegRemuxerStandardError => remuxerError.ToString();
public string FFMpegDownloaderStandardError => downloaderError.ToString();
private StringBuilder ffmpegError = new StringBuilder(); private StringBuilder remuxerError = new StringBuilder();
private StringBuilder downloaderError = new StringBuilder();
private static Regex processedTimeRegex = new Regex("time=(\\d{2}):(\\d{2}):(\\d{2}).\\d{2}", RegexOptions.IgnoreCase | RegexOptions.Compiled); private static Regex processedTimeRegex = new Regex("time=(\\d{2}):(\\d{2}):(\\d{2}).\\d{2}", RegexOptions.IgnoreCase | RegexOptions.Compiled);
public FFMpegAaxcProcesser(string ffmpegPath) public FFMpegAaxcProcesser( DownloadLicense downloadLicense)
{ {
FFMpegPath = ffmpegPath; FFMpegPath = DecryptSupportLibraries.ffmpegPath;
DownloadLicense = downloadLicense;
} }
public async Task ProcessBook(string aaxcUrl, string userAgent, string audibleKey, string audibleIV, string outputFile, string metadataPath = null)
public async Task ProcessBook(string outputFile, string ffmetaChaptersPath = null)
{ {
//This process gets the aaxc from the url and streams the decrypted //This process gets the aaxc from the url and streams the decrypted
//m4b to the output file. //aac stream to standard output
var StartInfo = new ProcessStartInfo
{
FileName = FFMpegPath,
RedirectStandardError = true,
CreateNoWindow = true,
WindowStyle = ProcessWindowStyle.Hidden,
UseShellExecute = false,
WorkingDirectory = Path.GetDirectoryName(FFMpegPath),
};
if (metadataPath != null)
{
StartInfo.ArgumentList.Add("-ignore_chapters"); //prevents ffmpeg from copying chapter info from aaxc to output file
StartInfo.ArgumentList.Add("true");
}
StartInfo.ArgumentList.Add("-audible_key");
StartInfo.ArgumentList.Add(audibleKey);
StartInfo.ArgumentList.Add("-audible_iv");
StartInfo.ArgumentList.Add(audibleIV);
StartInfo.ArgumentList.Add("-user_agent");
StartInfo.ArgumentList.Add(userAgent);
StartInfo.ArgumentList.Add("-i");
StartInfo.ArgumentList.Add(aaxcUrl);
if (metadataPath != null)
{
StartInfo.ArgumentList.Add("-f");
StartInfo.ArgumentList.Add("ffmetadata");
StartInfo.ArgumentList.Add("-i");
StartInfo.ArgumentList.Add(metadataPath);
StartInfo.ArgumentList.Add("-map_metadata");
StartInfo.ArgumentList.Add("1");
}
StartInfo.ArgumentList.Add("-c"); //copy all codecs to output
StartInfo.ArgumentList.Add("copy");
StartInfo.ArgumentList.Add("-f"); //force output format: mp4
StartInfo.ArgumentList.Add("mp4");
StartInfo.ArgumentList.Add("-movflags"); //don't add nero format chapter flags
StartInfo.ArgumentList.Add("disable_chpl+faststart");
StartInfo.ArgumentList.Add(outputFile);
StartInfo.ArgumentList.Add("-y"); //overwrite existing file
await ProcessBook(StartInfo);
}
private async Task ProcessBook(ProcessStartInfo startInfo)
{
var downloader = new Process var downloader = new Process
{ {
StartInfo = startInfo StartInfo = getDownloaderStartInfo()
};
//This process retreves an aac stream from standard input and muxes
// it into an m4b along with the cover art and metadata.
var remuxer = new Process
{
StartInfo = getRemuxerStartInfo(outputFile, ffmetaChaptersPath)
}; };
IsRunning = true; IsRunning = true;
downloader.ErrorDataReceived += Remuxer_ErrorDataReceived; downloader.ErrorDataReceived += Downloader_ErrorDataReceived;
downloader.Start(); downloader.Start();
downloader.BeginErrorReadLine(); downloader.BeginErrorReadLine();
remuxer.ErrorDataReceived += Remuxer_ErrorDataReceived;
remuxer.Start();
remuxer.BeginErrorReadLine();
var pipedOutput = downloader.StandardOutput.BaseStream;
var pipedInput = remuxer.StandardInput.BaseStream;
//All the work done here. Copy download standard output into //All the work done here. Copy download standard output into
//remuxer standard input //remuxer standard input
await downloader.WaitForExitAsync(); await Task.Run(() =>
{
int lastRead = 0;
byte[] buffer = new byte[32 * 1024];
do
{
lastRead = pipedOutput.Read(buffer, 0, buffer.Length);
pipedInput.Write(buffer, 0, lastRead);
} while (lastRead > 0 && !remuxer.HasExited);
});
//Closing input stream terminates remuxer
pipedInput.Close();
//If the remuxer exited due to failure, downloader will still have
//data in the pipe. Force kill downloader to continue.
if (remuxer.HasExited && !downloader.HasExited)
downloader.Kill();
remuxer.WaitForExit();
downloader.WaitForExit();
IsRunning = false; IsRunning = false;
Succeeded = downloader.ExitCode == 0; Succeeded = downloader.ExitCode == 0 && remuxer.ExitCode == 0;
}
private void Downloader_ErrorDataReceived(object sender, DataReceivedEventArgs e)
{
if (string.IsNullOrEmpty(e.Data))
return;
downloaderError.AppendLine(e.Data);
} }
private void Remuxer_ErrorDataReceived(object sender, DataReceivedEventArgs e) private void Remuxer_ErrorDataReceived(object sender, DataReceivedEventArgs e)
@ -104,7 +104,7 @@ namespace AaxDecrypter
if (string.IsNullOrEmpty(e.Data)) if (string.IsNullOrEmpty(e.Data))
return; return;
ffmpegError.AppendLine(e.Data); remuxerError.AppendLine(e.Data);
if (processedTimeRegex.IsMatch(e.Data)) if (processedTimeRegex.IsMatch(e.Data))
{ {
@ -122,9 +122,92 @@ namespace AaxDecrypter
if (e.Data.Contains("aac bitstream error")) if (e.Data.Contains("aac bitstream error"))
{ {
//This happens if input is corrupt (should never happen) or if caller
//supplied wrong key/iv
var process = sender as Process; var process = sender as Process;
process.Kill(); process.Kill();
} }
} }
private ProcessStartInfo getDownloaderStartInfo() =>
new ProcessStartInfo
{
FileName = FFMpegPath,
RedirectStandardError = true,
RedirectStandardOutput = true,
CreateNoWindow = true,
WindowStyle = ProcessWindowStyle.Hidden,
UseShellExecute = false,
WorkingDirectory = Path.GetDirectoryName(FFMpegPath),
ArgumentList ={
"-nostdin",
"-audible_key",
DownloadLicense.AudibleKey,
"-audible_iv",
DownloadLicense.AudibleIV,
"-user_agent",
DownloadLicense.UserAgent, //user-agent is requied for CDN to serve the file
"-i",
DownloadLicense.DownloadUrl,
"-c:a", //audio codec
"copy", //copy stream
"-f", //force output format: adts
"adts",
"pipe:" //pipe output to stdout
}
};
private ProcessStartInfo getRemuxerStartInfo(string outputFile, string ffmetaChaptersPath = null)
{
var startInfo = new ProcessStartInfo
{
FileName = FFMpegPath,
RedirectStandardError = true,
RedirectStandardInput = true,
CreateNoWindow = true,
WindowStyle = ProcessWindowStyle.Hidden,
UseShellExecute = false,
WorkingDirectory = Path.GetDirectoryName(FFMpegPath),
};
startInfo.ArgumentList.Add("-thread_queue_size");
startInfo.ArgumentList.Add("1024");
startInfo.ArgumentList.Add("-f"); //force input format: aac
startInfo.ArgumentList.Add("aac");
startInfo.ArgumentList.Add("-i"); //read input from stdin
startInfo.ArgumentList.Add("pipe:");
if (ffmetaChaptersPath is null)
{
//copy metadata from aaxc file.
startInfo.ArgumentList.Add("-user_agent");
startInfo.ArgumentList.Add(DownloadLicense.UserAgent);
startInfo.ArgumentList.Add("-i");
startInfo.ArgumentList.Add(DownloadLicense.DownloadUrl);
}
else
{
//copy metadata from supplied metadata file
startInfo.ArgumentList.Add("-f");
startInfo.ArgumentList.Add("ffmetadata");
startInfo.ArgumentList.Add("-i");
startInfo.ArgumentList.Add(ffmetaChaptersPath);
}
startInfo.ArgumentList.Add("-map"); //map file 0 (aac audio stream)
startInfo.ArgumentList.Add("0");
startInfo.ArgumentList.Add("-map_chapters"); //copy chapter data from file 1 (either metadata file or aaxc file)
startInfo.ArgumentList.Add("1");
startInfo.ArgumentList.Add("-c"); //copy all mapped streams
startInfo.ArgumentList.Add("copy");
startInfo.ArgumentList.Add("-f"); //force output format: mp4
startInfo.ArgumentList.Add("mp4");
startInfo.ArgumentList.Add("-movflags");
startInfo.ArgumentList.Add("disable_chpl"); //Disable Nero chapters format
startInfo.ArgumentList.Add(outputFile);
startInfo.ArgumentList.Add("-y"); //overwrite existing
return startInfo;
}
} }
} }