diff --git a/AaxDecrypter/AaxcDownloadConverter.cs b/AaxDecrypter/AaxcDownloadConverter.cs index d75a037a..7ba1f3a3 100644 --- a/AaxDecrypter/AaxcDownloadConverter.cs +++ b/AaxDecrypter/AaxcDownloadConverter.cs @@ -132,8 +132,7 @@ namespace AaxDecrypter public bool Step2_DownloadAndCombine() { - - var aaxcProcesser = new FFMpegAaxcProcesser(DecryptSupportLibraries.ffmpegPath); + var aaxcProcesser = new FFMpegAaxcProcesser(downloadLicense); aaxcProcesser.ProgressUpdate += AaxcProcesser_ProgressUpdate; string metadataPath = null; @@ -147,10 +146,6 @@ namespace AaxDecrypter } aaxcProcesser.ProcessBook( - downloadLicense.DownloadUrl, - downloadLicense.UserAgent, - downloadLicense.AudibleKey, - downloadLicense.AudibleIV, outputFileName, metadataPath) .GetAwaiter() @@ -172,9 +167,8 @@ namespace AaxDecrypter } /// - /// Copy all aacx metadata to m4b file. + /// Copy all aacx metadata to m4b file, including cover art. /// - /// public bool Step3_RestoreMetadata() { 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 //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) { destTags.SetData(stag.BoxType, stag.Children.Cast().ToArray()); diff --git a/AaxDecrypter/FFMpegAaxcProcesser.cs b/AaxDecrypter/FFMpegAaxcProcesser.cs index 9e937c8c..10d38b1d 100644 --- a/AaxDecrypter/FFMpegAaxcProcesser.cs +++ b/AaxDecrypter/FFMpegAaxcProcesser.cs @@ -7,96 +7,96 @@ using System.Threading.Tasks; namespace AaxDecrypter { + /// - /// Download audible aaxc, decrypt, remux,and add metadata. + /// Download audible aaxc, decrypt, and remux with chapters. /// class FFMpegAaxcProcesser { public event EventHandler ProgressUpdate; public string FFMpegPath { get; } + public DownloadLicense DownloadLicense { get; } public bool IsRunning { 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); - 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 - //m4b to the output file. - 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) - { + //aac stream to standard output 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; - downloader.ErrorDataReceived += Remuxer_ErrorDataReceived; + downloader.ErrorDataReceived += Downloader_ErrorDataReceived; downloader.Start(); 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 //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; - 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) @@ -104,7 +104,7 @@ namespace AaxDecrypter if (string.IsNullOrEmpty(e.Data)) return; - ffmpegError.AppendLine(e.Data); + remuxerError.AppendLine(e.Data); if (processedTimeRegex.IsMatch(e.Data)) { @@ -122,9 +122,92 @@ namespace AaxDecrypter 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; 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; + } } -} +} \ No newline at end of file