Browse Source

Bump deps, add EAE

Vincent Lang 7 years ago
parent
commit
cdb7fa5526

+ 2 - 2
CMakeModules/DependencyConfiguration.cmake

@@ -6,10 +6,10 @@ if(DEPENDENCY_TOKEN)
   set(DEPENDCY_FOLDER "")
   if(OPENELEC)
     set(DEPENDCY_FOLDER plexmediaplayer-openelec-codecs)
-    set(DEPS_BUILD_NUMBER 77)
+    set(DEPS_BUILD_NUMBER 80)
   elseif(APPLE OR WIN32)
     set(DEPENDCY_FOLDER plexmediaplayer-dependencies-codecs)
-    set(DEPS_BUILD_NUMBER 232)
+    set(DEPS_BUILD_NUMBER 235)
   endif()
   if(NOT (DEPENDCY_FOLDER STREQUAL ""))
     download_deps(

+ 1 - 0
CMakeModules/FetchDependencies.cmake

@@ -25,6 +25,7 @@ endif(APPLE)
 option(ENABLE_CODECS "Enable downloading for Codecs on Demand" OFF)
 if(ENABLE_CODECS)
   add_definitions(-DHAVE_CODEC_MANIFEST)
+  add_definitions(-DEAE_VERSION=133)
 endif()
 
 function(get_content_of_url)

+ 23 - 0
src/CMakeLists.txt

@@ -157,6 +157,28 @@ set_target_properties(${MAIN_TARGET} PROPERTIES
 copy_resources(${MAIN_TARGET})
 clang_tidy(${MAIN_TARGET})
 
+find_library(MINIZIP_LIBRARY minizip)
+if(WIN32)
+  # FindZLIB doesn't find this. (It might be possible to fix the minizip build
+  # to not require this.)
+  find_library(ZLIB_LIBRARIES zlibwapi)
+  if(ZLIB_LIBRARIES)
+    set(ZLIB_FOUND on)
+  endif()
+else()
+  include(FindZLIB)
+endif()
+
+if(ZLIB_FOUND AND MINIZIP_LIBRARY)
+  message(STATUS "Using minizip")
+  set(MINIZIP_LIBS ${MINIZIP_LIBRARY} ${ZLIB_LIBRARIES})
+  add_definitions(-DHAVE_MINIZIP)
+else()
+  if(ENABLE_CODECS)
+    message(FATAL_ERROR "minizip/zlib required in this configuration")
+  endif()
+endif()
+
 target_link_libraries(${MAIN_TARGET}
   shared
   qhttp
@@ -172,6 +194,7 @@ target_link_libraries(${MAIN_TARGET}
   ${ICU_LIBRARIES}
   ${CMAKE_THREAD_LIBS_INIT}
   ${RPI_LIBS}
+  ${MINIZIP_LIBS}
 )
 
 install(TARGETS ${MAIN_TARGET} DESTINATION ${INSTALL_BIN_DIR})

+ 2 - 0
src/main.cpp

@@ -255,6 +255,7 @@ int main(int argc, char *argv[])
     delete uniqueApp;
     Globals::EngineDestroy();
 
+    Codecs::Uninit();
     Log::Uninit();
     return ret;
   }
@@ -268,6 +269,7 @@ int main(int argc, char *argv[])
 
     errApp.exec();
 
+    Codecs::Uninit();
     Log::Uninit();
     return 1;
   }

+ 453 - 33
src/player/CodecsComponent.cpp

@@ -6,13 +6,22 @@
 #include <QDomDocument>
 #include <QDomNode>
 #include <QCoreApplication>
+#include <QProcess>
 #include <QUuid>
 #include <QUrl>
 #include <QUrlQuery>
 #include <QResource>
+#include <QSaveFile>
 #include <QStandardPaths>
 #include <QSysInfo>
 #include <QCryptographicHash>
+#include <QTemporaryDir>
+
+#ifdef HAVE_MINIZIP
+#include <minizip/unzip.h>
+#include <minizip/ioapi.h>
+#endif
+
 #include "system/SystemComponent.h"
 #include "settings/SettingsComponent.h"
 #include "utils/Utils.h"
@@ -42,6 +51,16 @@ static const Codec Encoders[] = {
 };
 #endif
 
+#define STRINGIFY_(x) #x
+#define STRINGIFY(x) STRINGIFY_(x)
+
+#ifdef EAE_VERSION
+#define HAVE_EAE 1
+#else
+#define EAE_VERSION unavailable
+#define HAVE_EAE 0
+#endif
+
 // We might want to use Codec.quality to decide this one day.
 // But for now, it's better if we can quickly change these.
 static QSet<QString> g_systemVideoDecoderWhitelist = {
@@ -91,6 +110,9 @@ static QList<CodecDriver> g_cachedCodecList;
 
 static QString g_deviceID;
 
+static QString g_eaeWatchFolder;
+static QProcess* g_eaeProcess;
+
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 static QString getBuildType()
 {
@@ -104,6 +126,22 @@ static QString getBuildType()
 #endif
 }
 
+///////////////////////////////////////////////////////////////////////////////////////////////////
+static QString getEAEBuildType()
+{
+#if defined(Q_OS_MAC)
+  return "darwin-x86_64";
+#elif defined(Q_OS_WIN)
+  return sizeof(void *) > 4 ? "windows-x86_64" : "windows-i386";
+#elif defined(TARGET_RPI)
+  return "linux-raspi2-arm7";
+#elif defined(Q_OS_LINUX)
+  return sizeof(void *) > 4 ? "linux-ubuntu-x86_64" : "linux-ubuntu-i686";
+#else
+  return "unknown";
+#endif
+}
+
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 QString Codecs::plexNameToFF(QString plex)
 {
@@ -132,6 +170,21 @@ static QString codecsPath()
   return codecsRootPath() + g_codecVersion + "-" + getBuildType() + QDir::separator();
 }
 
+///////////////////////////////////////////////////////////////////////////////////////////////////
+static QString eaePrefixPath()
+{
+  return codecsRootPath() + "EasyAudioEncoder-" + STRINGIFY(EAE_VERSION) + "-" + getBuildType();
+}
+
+static QString eaeBinaryPath()
+{
+QString exeSuffix = "";
+#ifdef Q_OS_WIN
+  exeSuffix = ".exe";
+#endif
+  return eaePrefixPath() + "/EasyAudioEncoder/EasyAudioEncoder" + exeSuffix;
+}
+
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 static int indexOfCodecInList(const QList<CodecDriver>& list, const CodecDriver& codec)
 {
@@ -230,9 +283,23 @@ bool CodecDriver::isSystemCodec() const
   // Linux on RPI
   if (driver.endsWith("_mmal"))
     return true;
+  // Not really a system codec, but treated as such for convenience
+  if (driver.endsWith("_eae"))
+    return true;
   return false;
 }
 
+///////////////////////////////////////////////////////////////////////////////////////////////////
+QString CodecDriver::getSystemCodecType() const
+{
+  if (!isSystemCodec())
+    return "";
+  int splitAt = driver.indexOf("_");
+  if (splitAt < 0)
+    return "";
+  return driver.mid(splitAt + 1);
+}
+
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 bool CodecDriver::isWhitelistedSystemAudioCodec() const
 {
@@ -339,6 +406,16 @@ static QString getFFmpegVersion()
   return mpv::qt::get_property(mpv, "ffmpeg-version").toString();
 }
 
+///////////////////////////////////////////////////////////////////////////////////////////////////
+static void setEnv(QString var, QString val)
+{
+#ifdef Q_OS_WIN
+  SetEnvironmentVariableW(var.toStdWString().c_str(), val.toStdWString().c_str());
+#else
+  qputenv(var.toUtf8().data(), val.toUtf8().data());
+#endif
+}
+
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 void Codecs::preinitCodecs()
 {
@@ -357,11 +434,13 @@ void Codecs::preinitCodecs()
   // Follows the convention used by av_get_token().
   QString escapedPath = path.replace("\\", "\\\\").replace(":", "\\:").replace("'", "\\'");
   // This must be run before any threads are started etc. (for safety).
-#ifdef Q_OS_WIN
-  SetEnvironmentVariableW(L"FFMPEG_EXTERNAL_LIBS", escapedPath.toStdWString().c_str());
-#else
-  qputenv("FFMPEG_EXTERNAL_LIBS", escapedPath.toUtf8().data());
-#endif
+  setEnv("FFMPEG_EXTERNAL_LIBS", escapedPath);
+
+  QTemporaryDir d(QDir::tempPath() + "/pmp-eae-XXXXXX");
+  d.setAutoRemove(false);
+  g_eaeWatchFolder = d.path();
+
+  setEnv("EAE_ROOT", g_eaeWatchFolder);
 
   g_deviceID = loadDeviceID();
 }
@@ -417,6 +496,9 @@ static void probeCodecs()
   if (g_deviceID.isEmpty())
     throw FatalException("Could not read device-id.");
 
+  if (g_eaeWatchFolder.isEmpty())
+    throw FatalException("Could not create EAE working directory.");
+
 #ifdef Q_OS_WIN32
   if (useSystemVideoDecoders())
   {
@@ -519,6 +601,12 @@ void CodecsFetcher::installCodecs(const QList<CodecDriver>& codecs)
   {
     if (codecNeedsDownload(codec))
       m_Codecs.enqueue(codec);
+    if (codec.getSystemCodecType() == "eae")
+    {
+      m_eaeNeeded = true;
+      if (!QFile(eaeBinaryPath()).exists())
+        m_fetchEAE = true;
+    }
   }
   startNext();
 }
@@ -535,32 +623,56 @@ static Downloader::HeaderList getPlexHeaders()
   return headers;
 }
 
+static QUrl buildCodecQuery(QString version, QString name, QString build)
+{
+  QString host = "https://plex.tv";
+
+  QUrl url = QUrl(host + "/api/codecs/" + name);
+  QUrlQuery query;
+  query.addQueryItem("deviceId", g_deviceID);
+  query.addQueryItem("version", version);
+  query.addQueryItem("build", build);
+  query.addQueryItem("oldestPreviousVersion", SettingsComponent::Get().oldestPreviousVersion());
+
+  url.setQuery(query);
+
+  return url;
+}
+
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 void CodecsFetcher::startNext()
 {
+  if (m_fetchEAE)
+  {
+    m_fetchEAE = false;
+
+    QUrl url = buildCodecQuery(STRINGIFY(EAE_VERSION), "easyaudioencoder", getEAEBuildType());
+
+    Downloader *downloader = new Downloader(QVariant("eae"), url, getPlexHeaders(), this);
+    connect(downloader, &Downloader::done, this, &CodecsFetcher::codecInfoDownloadDone);
+    return;
+  }
+
   if (m_Codecs.isEmpty())
   {
+    // Do final initializations.
+    if (m_eaeNeeded)
+      startEAE();
+
     emit done(this);
     return;
   }
 
   CodecDriver codec = m_Codecs.dequeue();
 
-  QString host = "https://plex.tv";
-  QUrl url = QUrl(host + "/api/codecs/" + codec.getMangledName());
-  QUrlQuery query;
-  query.addQueryItem("deviceId", g_deviceID);
-  query.addQueryItem("version", g_codecVersion);
-  query.addQueryItem("build", getBuildType());
-  query.addQueryItem("oldestPreviousVersion", SettingsComponent::Get().oldestPreviousVersion());
-  url.setQuery(query);
+  QUrl url = buildCodecQuery(g_codecVersion, codec.getMangledName(), getBuildType());
 
   Downloader *downloader = new Downloader(QVariant::fromValue(codec), url, getPlexHeaders(), this);
   connect(downloader, &Downloader::done, this, &CodecsFetcher::codecInfoDownloadDone);
 }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
-bool CodecsFetcher::processCodecInfoReply(const QByteArray& data, const CodecDriver& codec)
+bool CodecsFetcher::processCodecInfoReply(const QVariant& context, const QByteArray& data)
 {
   QLOG_INFO() << "Got reply:" << QString::fromUtf8(data);
 
@@ -600,7 +712,7 @@ bool CodecsFetcher::processCodecInfoReply(const QByteArray& data, const CodecDri
     return false;
   }
 
-  Downloader *downloader = new Downloader(QVariant::fromValue(codec), url, getPlexHeaders(), this);
+  Downloader *downloader = new Downloader(context, url, getPlexHeaders(), this);
   connect(downloader, &Downloader::done, this, &CodecsFetcher::codecDownloadDone);
 
   return true;
@@ -609,16 +721,213 @@ bool CodecsFetcher::processCodecInfoReply(const QByteArray& data, const CodecDri
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 void CodecsFetcher::codecInfoDownloadDone(QVariant userData, bool success, const QByteArray& data)
 {
-  CodecDriver codec = userData.value<CodecDriver>();
-  if (!success || !processCodecInfoReply(data, codec))
+  if (!success || !processCodecInfoReply(userData, data))
   {
     QLOG_ERROR() << "Codec download failed.";
     startNext();
   }
 }
 
+#ifdef HAVE_MINIZIP
+
+static voidpf unz_open_file(voidpf opaque, const char* filename, int mode)
+{
+#ifdef Q_OS_WIN32
+  return _wfopen(QString::fromUtf8(filename).toStdWString().c_str(), L"rb");
+#else
+  return fopen(filename, "rb");
+#endif
+}
+
+static uLong unz_read_file(voidpf opaque, voidpf stream, void* buf, uLong size)
+{
+  return fread(buf, 1, size, (FILE *)stream);
+}
+
+static uLong unz_write_file(voidpf opaque, voidpf stream, const void* buf, uLong size)
+{
+  return 0;
+}
+
+static int unz_close_file(voidpf opaque, voidpf stream)
+{
+  return fclose((FILE *)stream);
+}
+
+static int unz_error_file(voidpf opaque, voidpf stream)
+{
+  return ferror((FILE *)stream);
+}
+
+static long unz_tell_file(voidpf opaque, voidpf stream)
+{
+  return ftell((FILE *)stream);
+}
+
+long unz_seek_file(voidpf opaque, voidpf stream, uLong offset, int origin)
+{
+  int whence = -1;
+  switch (origin)
+  {
+    case ZLIB_FILEFUNC_SEEK_CUR:
+      whence = SEEK_CUR;
+      break;
+    case ZLIB_FILEFUNC_SEEK_END:
+      whence = SEEK_END;
+      break;
+    case ZLIB_FILEFUNC_SEEK_SET:
+      whence = SEEK_SET;
+      break;
+  }
+  return fseek((FILE *)stream, offset, whence);
+}
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+static bool extractZip(QString zip, QString dest)
+{
+  bool success = false;
+
+  zlib_filefunc_def unzfilefuncs = {};
+  unzfilefuncs.zopen_file = unz_open_file;
+  unzfilefuncs.zread_file = unz_read_file;
+  unzfilefuncs.zwrite_file = unz_write_file;
+  unzfilefuncs.ztell_file = unz_tell_file;
+  unzfilefuncs.zseek_file = unz_seek_file;
+  unzfilefuncs.zclose_file = unz_close_file;
+  unzfilefuncs.zerror_file = unz_error_file;
+
+  unzFile file = unzOpen2(zip.toUtf8().data(), &unzfilefuncs);
+  if (!file)
+  {
+    QLOG_ERROR() << "could not open .zip file.";
+    goto fail;
+  }
+
+  unz_global_info info;
+  int unzerr;
+  if ((unzerr = unzGetGlobalInfo(file, &info)) != UNZ_OK)
+  {
+    QLOG_ERROR() << "unzGlobalInfo() failed with" << unzerr;
+    goto fail;
+  }
+
+  if ((unzerr = unzGoToFirstFile(file)) != UNZ_OK)
+  {
+    QLOG_ERROR() << "unzGoToFirstFile() failed with" << unzerr;
+    goto fail;
+  }
+
+  for (ZPOS64_T n = 0; n < info.number_entry; n++)
+  {
+    if (n > 0 && (unzerr = unzGoToNextFile(file)) != UNZ_OK)
+    {
+      QLOG_ERROR() << "unzGoToNextFile() failed with" << unzerr;
+      goto fail;
+    }
+
+    char filename[256];
+    unz_file_info finfo;
+
+    if ((unzerr = unzGetCurrentFileInfo(file, &finfo, filename, sizeof(filename), 0, 0, 0, 0)) != UNZ_OK)
+    {
+      QLOG_ERROR() << "unzGetCurrentFileInfo() failed with" << unzerr;
+      goto fail;
+    }
+
+    if ((unzerr = unzOpenCurrentFile(file)) != UNZ_OK)
+    {
+      QLOG_ERROR() << "unzOpenCurrentFile() failed with" << unzerr;
+      goto fail;
+    }
+
+    char *pathpart = strrchr(filename, '/');
+    if (pathpart)
+    {
+      //  This part sucks especially: temporarily cut off the string.
+      *pathpart = '\0';
+
+      QDir dir(dest + "/" + filename);
+      if (!dir.mkpath("."))
+      {
+        QLOG_ERROR() << "could not create zip sub directory";
+        goto fail;
+      }
+
+      *pathpart = '/';
+    }
+
+    // Directory (probably)
+    if (QString(filename).endsWith("/"))
+      continue;
+
+    QString writepath = dest + "/" + filename;
+
+    QSaveFile out(writepath);
+    if (!out.open(QIODevice::WriteOnly))
+    {
+      QLOG_ERROR() << "could not open output file" << filename;
+      goto fail;
+    }
+
+    while (true)
+    {
+      char buf[4096];
+      int read = unzReadCurrentFile(file, buf, sizeof(buf));
+
+      if (read == 0)
+        break;
+
+      if (read < 0)
+      {
+        QLOG_ERROR() << "error decompressing zip entry" << filename;
+        goto fail;
+      }
+
+      if (out.write(buf, read) != read)
+      {
+        QLOG_ERROR() << "error writing output file" << filename;
+        goto fail;
+      }
+    }
+
+    if (!out.commit())
+    {
+      QLOG_ERROR() << "error closing output file" << filename;
+      goto fail;
+    }
+
+#ifndef _WIN32
+    // Set the executable bit.
+    // We could try setting the full permissions as stored in the file, but why bother.
+    if (finfo.external_fa & 0x400000)
+    {
+      if (!QFile::setPermissions(writepath, QFileDevice::Permissions(0x5145)))
+      {
+        QLOG_ERROR() << "could not set output executable bit on extracted file";
+        goto fail;
+      }
+    }
+#endif
+  }
+
+  success = true;
+fail:
+  unzClose(file);
+  return success;
+}
+
+#else /* ifdef HAVE_MINIZIP */
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+static bool extractZip(QString zip, QString dest)
+{
+  return false;
+}
+
+#endif
+
 ///////////////////////////////////////////////////////////////////////////////////////////////////
-void CodecsFetcher::processCodecDownloadDone(const QByteArray& data, const CodecDriver& codec)
+void CodecsFetcher::processCodecDownloadDone(const QVariant& context, const QByteArray& data)
 {
   QByteArray hash = QCryptographicHash::hash(data, QCryptographicHash::Sha1);
 
@@ -628,23 +937,52 @@ void CodecsFetcher::processCodecDownloadDone(const QByteArray& data, const Codec
     return;
   }
 
-  QLOG_INFO() << "Storing codec as" << codec.getPath();
-
-  if (!Utils::safelyWriteFile(codec.getPath(), data))
+  if (context == QVariant("eae"))
   {
-    QLOG_ERROR() << "Writing codec file failed.";
-    return;
-  }
+    QString dest = eaePrefixPath() + ".zip";
 
-  // This causes libmpv and eventually libavcodec to rescan and load new codecs.
-  Codecs::updateCachedCodecList();
-  for (const CodecDriver& item : Codecs::getCachedCodecList())
+    QLOG_INFO() << "Storing EAE as" << dest;
+
+    if (!Utils::safelyWriteFile(dest, data))
+    {
+      QLOG_ERROR() << "Writing codec file failed.";
+      return;
+    }
+
+    QDir dir(dest);
+    dir.removeRecursively();
+
+    if (!extractZip(dest, eaePrefixPath()))
+    {
+      QLOG_ERROR() << "Could not extract zip.";
+      dir.removeRecursively();
+      return;
+    }
+
+    QFile::remove(dest);
+  }
+  else
   {
-    if (Codecs::sameCodec(item, codec) && !item.present)
+    CodecDriver codec = context.value<CodecDriver>();
+
+    QLOG_INFO() << "Storing codec as" << codec.getPath();
+
+    if (!Utils::safelyWriteFile(codec.getPath(), data))
     {
-      QLOG_ERROR() << "Codec could not be loaded after installing it.";
+      QLOG_ERROR() << "Writing codec file failed.";
       return;
     }
+
+    // This causes libmpv and eventually libavcodec to rescan and load new codecs.
+    Codecs::updateCachedCodecList();
+    for (const CodecDriver& item : Codecs::getCachedCodecList())
+    {
+      if (Codecs::sameCodec(item, codec) && !item.present)
+      {
+        QLOG_ERROR() << "Codec could not be loaded after installing it.";
+        return;
+      }
+    }
   }
 
   QLOG_INFO() << "Codec download and installation succeeded.";
@@ -653,11 +991,10 @@ void CodecsFetcher::processCodecDownloadDone(const QByteArray& data, const Codec
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 void CodecsFetcher::codecDownloadDone(QVariant userData, bool success, const QByteArray& data)
 {
-  CodecDriver codec = userData.value<CodecDriver>();
-  QLOG_INFO() << "Codec" << codec.driver << "request finished.";
+  QLOG_INFO() << "Codec request finished.";
   if (success)
   {
-    processCodecDownloadDone(data, codec);
+    processCodecDownloadDone(userData, data);
   }
   else
   {
@@ -666,6 +1003,72 @@ void CodecsFetcher::codecDownloadDone(QVariant userData, bool success, const QBy
   startNext();
 }
 
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+void CodecsFetcher::startEAE()
+{
+  if (!g_eaeProcess)
+  {
+    g_eaeProcess = new QProcess();
+    g_eaeProcess->setProcessChannelMode(QProcess::ForwardedChannels);
+    connect(g_eaeProcess, &QProcess::stateChanged,
+      [](QProcess::ProcessState s)
+      {
+        QLOG_INFO() << "EAE process state:" << s;
+      }
+    );
+    connect(g_eaeProcess, &QProcess::errorOccurred,
+      [](QProcess::ProcessError e)
+      {
+        QLOG_INFO() << "EAE process error:" << e;
+      }
+    );
+    connect(g_eaeProcess, static_cast<void(QProcess::*)(int, QProcess::ExitStatus)>(&QProcess::finished),
+      [](int exitCode, QProcess::ExitStatus exitStatus)
+      {
+        QLOG_INFO() << "EAE process finished:" << exitCode << exitStatus;
+      }
+    );
+  }
+
+  if (g_eaeProcess->state() == QProcess::NotRunning)
+  {
+    if (g_eaeProcess->program().size())
+    {
+      int exitCode = g_eaeProcess->exitStatus() == QProcess::NormalExit ? g_eaeProcess->exitCode() : -1;
+      QLOG_ERROR() << "EAE died with exit code" << exitCode;
+    }
+
+    QLOG_INFO() << "Starting EAE.";
+
+    g_eaeProcess->setProgram(eaeBinaryPath());
+    g_eaeProcess->setWorkingDirectory(g_eaeWatchFolder);
+
+    QDir dir(g_eaeWatchFolder);
+    dir.removeRecursively();
+    dir.mkpath(".");
+
+    static const QStringList watchfolder_names =
+    {
+      "Convert to WAV (to 2ch or less)",
+      "Convert to WAV (to 8ch or less)",
+      "Convert to Dolby Digital (Low Quality - 384 kbps)",
+      "Convert to Dolby Digital (High Quality - 640 kbps)",
+      "Convert to Dolby Digital Plus (High Quality - 384 kbps)",
+      "Convert to Dolby Digital Plus (Max Quality - 1024 kbps)",
+    };
+    for (auto folder : watchfolder_names)
+    {
+      if (!dir.mkpath(folder))
+      {
+        QLOG_ERROR() << "Could not create watch folder";
+      }
+    }
+
+    g_eaeProcess->start();
+  }
+}
+
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 Downloader::Downloader(QVariant userData, const QUrl& url, const HeaderList& headers, QObject* parent)
   : QObject(parent), m_userData(userData), m_lastProgress(-1)
@@ -760,6 +1163,8 @@ static CodecDriver selectBestDecoder(const StreamInfo& stream)
         if (stream.audioSampleRate > 0 && (stream.audioSampleRate < 8000 || stream.audioSampleRate > 48000))
           score = 1;
       }
+      if (codec.getSystemCodecType() == "eae")
+        score = HAVE_EAE ? 2 : -1;
     }
     else
     {
@@ -770,7 +1175,7 @@ static CodecDriver selectBestDecoder(const StreamInfo& stream)
         score = 5;
     }
 
-    if (score > bestScore)
+    if (score > bestScore && score >= 0)
     {
       best = codec;
       bestScore = score;
@@ -853,3 +1258,18 @@ QList<CodecDriver> Codecs::determineRequiredCodecs(const PlaybackInfo& info)
 
   return result;
 }
+
+void Codecs::Uninit()
+{
+  if (g_eaeProcess)
+  {
+    delete g_eaeProcess;
+    g_eaeProcess = nullptr;
+  }
+
+  if (!g_eaeWatchFolder.isEmpty())
+  {
+    QDir dir(g_eaeWatchFolder);
+    dir.removeRecursively();
+  }
+}

+ 15 - 2
src/player/CodecsComponent.h

@@ -44,6 +44,9 @@ struct CodecDriver {
   // depending on input media and OS version.
   bool isSystemCodec() const;
 
+  // Return "mf" for Windows codecs, "at" for OSX audio, "mmal" for RPI video, "eae" for EAE, "" otherwise
+  QString getSystemCodecType() const;
+
   bool isWhitelistedSystemAudioCodec() const;
   bool isWhitelistedSystemVideoCodec() const;
 
@@ -94,6 +97,11 @@ class CodecsFetcher : public QObject
 {
   Q_OBJECT
 public:
+  CodecsFetcher()
+  : m_eaeNeeded(false), m_fetchEAE(false)
+  {
+  }
+
   // Download the given list of codecs (skip download for codecs already
   // installed). Then call done(userData), regardless of success.
   void installCodecs(const QList<CodecDriver>& codecs);
@@ -110,12 +118,15 @@ private Q_SLOTS:
 
 private:
   bool codecNeedsDownload(const CodecDriver& codec);
-  bool processCodecInfoReply(const QByteArray& data, const CodecDriver& codec);
-  void processCodecDownloadDone(const QByteArray& data, const CodecDriver& codec);
+  bool processCodecInfoReply(const QVariant& context, const QByteArray& data);
+  void processCodecDownloadDone(const QVariant& context, const QByteArray& data);
   void startNext();
+  void startEAE();
 
   QQueue<CodecDriver> m_Codecs;
   QByteArray m_currentHash;
+  bool m_eaeNeeded;
+  bool m_fetchEAE;
 };
 
 class Codecs
@@ -136,6 +147,8 @@ public:
 
   static void updateCachedCodecList();
 
+  static void Uninit();
+
   static const QList<CodecDriver>& getCachedCodecList();
 
   static QList<CodecDriver> findCodecsByFormat(const QList<CodecDriver>& list, CodecType type, const QString& format);

+ 5 - 1
src/player/PlayerComponent.cpp

@@ -963,7 +963,11 @@ void PlayerComponent::setAudioConfiguration()
   if (deviceType == AUDIO_DEVICE_TYPE_SPDIF &&
       SettingsComponent::Get().value(SETTINGS_SECTION_AUDIO, "passthrough.ac3").toBool())
   {
-    mpv::qt::command(m_mpv, QStringList() << "af" << "add" << "@ac3:lavcac3enc");
+    QString filterArgs = "";
+#ifdef EAE_VERSION
+    filterArgs += ":encoder=ac3_eae:o=[eae_batch_frames=1]";
+#endif
+    mpv::qt::command(m_mpv, QStringList() << "af" << "add" << ("@ac3:lavcac3enc" + filterArgs));
     m_doAc3Transcoding = true;
   }
   else