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