Ver Fonte

Codecs on demand

PMP will now download codec binaries from nightlies on playback start if
built with -DENABLE_CODECS=on. With "off" (the default), most codecs on
demand code will be built, but the list of downloadable codecs will be
empty.

This uses a hardcoded CODECS_BUILD_NUMBER. This is sort of ok, as long
as the list of codecs/names on nightlies doesn't change. If there's a
change, it should be updated manually. (Same if new platforms are added
etc.) Note that even if this is not changed, the code will download the
correct codecs by looking at the FFmpeg version number libmpv is linked
against.
Vincent Lang há 8 anos atrás
pai
commit
f1487a06f3

+ 3 - 3
CMakeModules/DependencyConfiguration.cmake

@@ -3,10 +3,10 @@ option(DISABLE_BUNDLED_DEPS "Disable the bundled deps on certain platforms" OFF)
 
 include(FetchDependencies)
 
-if(APPLE AND NOT DISABLE_BUNDLED_DEPS)  
+if((APPLE OR WIN32) AND NOT DISABLE_BUNDLED_DEPS)
   download_deps(
-    "plexmediaplayer-dependencies"
-    ARTIFACTNAME konvergo-depends
+    "plexmediaplayer-dependencies-codecs"
+    ARTIFACTNAME konvergo-codecs-depends
     DIRECTORY dir
     DEPHASH_VAR DEPS_HASH
     DYLIB_SCRIPT_PATH ${PROJECT_SOURCE_DIR}/scripts/fix-install-names.py

+ 12 - 0
CMakeModules/FetchDependencies.cmake

@@ -12,6 +12,18 @@ elseif(UNIX)
   set(ARCHSTR ${PLEX_BUILD_TARGET})
 endif(APPLE)
 
+option(ENABLE_CODECS "Enable CodecManifest downloading for Codecs on Demand" OFF)
+if(ENABLE_CODECS)
+  add_definitions(-DHAVE_CODEC_MANIFEST)
+
+  set(CODECS_BUILD_NUMBER 98)
+  message(STATUS "Downloading https://nightlies.plex.tv/codecs/${CODECS_BUILD_NUMBER}/CodecManifest-${ARCHSTR}.h")
+  file(
+    DOWNLOAD https://nightlies.plex.tv/codecs/${CODECS_BUILD_NUMBER}/CodecManifest-${ARCHSTR}.h  ${CMAKE_CURRENT_BINARY_DIR}/src/CodecManifest.h
+    STATUS DL_STATUS
+  )
+  message(STATUS "Result: ${DL_STATUS}")
+endif()
 
 function(get_content_of_url)
   set(ARGS URL CONTENT_VAR FILENAME)

+ 1 - 1
scripts/build-windows.bat

@@ -1,6 +1,6 @@
 cd %BUILD_DIR%  || exit /b
 
-%CMAKE_DIR%\cmake -DCRASHDUMP_SECRET=%CD_SECRET% -DCMAKE_BUILD_TYPE=RelWithDebInfo -DCMAKE_INSTALL_PREFIX=output -DDEPENDENCY_UNTAR_DIR=c:\jenkins\pmp-deps .. -G Ninja -DCODE_SIGN=ON || exit /b
+%CMAKE_DIR%\cmake -DCRASHDUMP_SECRET=%CD_SECRET% -DCMAKE_BUILD_TYPE=RelWithDebInfo -DCMAKE_INSTALL_PREFIX=output -DDEPENDENCY_UNTAR_DIR=c:\jenkins\pmp-deps .. -G Ninja -DCODE_SIGN=ON -DENABLE_CODECS=on || exit /b
 
 ninja || exit /b
 ninja windows_package || exit /b

+ 1 - 1
scripts/fetch-binaries.py

@@ -17,7 +17,7 @@ import glob
 # Edit these to set a new default dependencies build
 default_tag = "auto"
 default_release_build_number = "109"
-default_release_dir = "plexmediaplayer-dependencies"
+default_release_dir = "plexmediaplayer-dependencies-codecs"
 default_branch = "master"
 
 def sha1_for_file(path):

+ 3 - 0
src/main.cpp

@@ -13,6 +13,7 @@
 #include "system/UpdateManager.h"
 #include "QsLog.h"
 #include "Paths.h"
+#include "player/CodecsComponent.h"
 #include "player/PlayerComponent.h"
 #include "breakpad/CrashDumps.h"
 #include "Version.h"
@@ -158,6 +159,8 @@ int main(int argc, char *argv[])
     setlocale(LC_NUMERIC, "C");
 #endif
 
+    Codecs::preinitCodecs();
+
     // Initialize all the components. This needs to be done
     // early since most everything else relies on it
     //

+ 1 - 0
src/player/CMakeLists.txt

@@ -1,2 +1,3 @@
 add_sources(PlayerComponent.cpp PlayerComponent.h)
 add_sources(PlayerQuickItem.cpp PlayerQuickItem.h)
+add_sources(CodecsComponent.cpp CodecsComponent.h)

+ 515 - 0
src/player/CodecsComponent.cpp

@@ -0,0 +1,515 @@
+#include "CodecsComponent.h"
+#include <QString>
+#include <Qt>
+#include <QDir>
+#include <QDomAttr>
+#include <QDomDocument>
+#include <QDomNode>
+#include <QCoreApplication>
+#include <QUuid>
+#include <QUrl>
+#include <QUrlQuery>
+#include "system/SystemComponent.h"
+#include "utils/Utils.h"
+#include "shared/Paths.h"
+#include "PlayerComponent.h"
+
+#include "QsLog.h"
+
+#define countof(x) (sizeof(x) / sizeof((x)[0]))
+
+// For QVariant. Mysteriously makes Qt happy.
+Q_DECLARE_METATYPE(CodecDriver);
+
+#ifdef HAVE_CODEC_MANIFEST
+#include "CodecManifest.h"
+#else
+#define CODEC_VERSION   "dummy"
+#define SHLIB_PREFIX    ""
+#define SHLIB_EXTENSION "dummy"
+// Codec.name is the name of the codec implementation, Codec.codecName the name of the codec
+struct Codec {const char* name; const char* codecName; const char* profiles; int external;};
+static const Codec Decoders[] = {
+    {"dummy", "dummy", nullptr, 1},
+};
+static const Codec Encoders[] = {
+    {"dummy", "dummy", nullptr, 1},
+};
+#endif
+
+static QString g_codecVersion;
+static QList<CodecDriver> g_cachedCodecList;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+static QString getBuildType()
+{
+#ifdef Q_OS_MAC
+  return "darwin-x86_64";
+#else
+  return SystemComponent::Get().getPlatformTypeString() + "-" +
+         SystemComponent::Get().getPlatformArchString();
+#endif
+}
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+static QString plexNameToFF(QString plex)
+{
+  if (plex == "dca")
+    return "dts";
+  return plex;
+}
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+static QString codecsRootPath()
+{
+  return Paths::dataDir("codecs") + QDir::separator();
+}
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+static QString codecsPath()
+{
+  return codecsRootPath() + getBuildType() + "-" + g_codecVersion + QDir::separator();
+}
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+static int indexOfCodecInList(const QList<CodecDriver>& list, const CodecDriver& codec)
+{
+  for (int n = 0; n < list.size(); n++)
+  {
+    if (Codecs::sameCodec(list[n], codec))
+      return n;
+  }
+  return -1;
+}
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+void Codecs::updateCachedCodecList()
+{
+  g_cachedCodecList.clear();
+
+  for (CodecType type : {CodecType::Decoder, CodecType::Encoder})
+  {
+    const Codec* list = (type == CodecType::Decoder) ? Decoders : Encoders;
+    size_t count = (type == CodecType::Decoder) ? countof(Decoders) : countof(Encoders);
+
+    for (size_t i = 0; i < count; i++)
+    {
+      CodecDriver codec = {};
+      codec.type = type;
+      codec.format = plexNameToFF(list[i].codecName);
+      codec.driver = list[i].name;
+      codec.external = list[i].external;
+      g_cachedCodecList.append(codec);
+    }
+  }
+
+  // Set present flag for the installed codecs. Also, there could be codecs not
+  // on the CodecManifest.h list (system codecs, or when compiled  without
+  // codec loading).
+
+  QList<CodecDriver> installed = PlayerComponent::Get().installedCodecs();
+
+  // Surely O(n^2) won't be causing trouble, right?
+  for (const CodecDriver& installedCodec : installed)
+  {
+    int index = indexOfCodecInList(g_cachedCodecList, installedCodec);
+    if (index >= 0)
+      g_cachedCodecList[index].present = true;
+    else
+      g_cachedCodecList.append(installedCodec);
+  }
+}
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+const QList<CodecDriver>& Codecs::getCachecCodecList()
+{
+  return g_cachedCodecList;
+}
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+QList<CodecDriver> Codecs::findCodecsByFormat(const QList<CodecDriver>& list, CodecType type, const QString& format)
+{
+  QList<CodecDriver> result;
+  for (const CodecDriver& codec : list)
+  {
+    if (codec.type == type && codec.format == format)
+      result.append(codec);
+  }
+  return result;
+}
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+QString CodecDriver::getMangledName() const
+{
+  return driver + (type == CodecType::Decoder ? "_decoder" : "_encoder");
+}
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+QString CodecDriver::getFileName() const
+{
+  return SHLIB_PREFIX + getMangledName() + "-" + g_codecVersion + "-" + getBuildType() + "." + SHLIB_EXTENSION;
+}
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+QString CodecDriver::getPath() const
+{
+  return QDir(codecsPath()).absoluteFilePath(getFileName());
+}
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// Returns "" on error.
+static QString getDeviceID()
+{
+  QFile path(QDir(codecsRootPath()).absoluteFilePath(".device-id"));
+  if (path.exists())
+  {
+    // TODO: Would fail consistently if the file is not readable. Should a new ID be generated?
+    //       What should we do if the file contains binary crap, not a text UUID?
+    path.open(QFile::ReadOnly);
+    return QString::fromLatin1(path.readAll());
+  }
+
+  QString newUuid = QUuid::createUuid().toString();
+  // The UUID should be e.g. "8f6ad954-0cb9-4dbb-a5e5-e0b085f07cf8"
+  if (newUuid.startsWith("{"))
+    newUuid = newUuid.mid(1);
+  if (newUuid.endsWith("}"))
+    newUuid = newUuid.mid(0, newUuid.size() - 1);
+
+  if (!Utils::safelyWriteFile(path.fileName(), newUuid.toLatin1()))
+    return "";
+
+  return newUuid;
+}
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+static QString getFFmpegVersion()
+{
+  auto mpv = mpv::qt::Handle::FromRawHandle(mpv_create());
+  if (!mpv)
+    return "";
+  if (mpv_initialize(mpv) < 0)
+    return "";
+  return mpv::qt::get_property_variant(mpv, "ffmpeg-version").toString();
+}
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+void Codecs::preinitCodecs()
+{
+  // Extract the CI codecs version we set with --extra-version when compiling FFmpeg.
+  QString ffmpegVersion = getFFmpegVersion();
+  int sep = ffmpegVersion.indexOf(',');
+  if (sep >= 0)
+    g_codecVersion = ffmpegVersion.mid(sep + 1);
+  else
+    g_codecVersion = CODEC_VERSION;
+
+  QString path = codecsPath();
+
+  QDir("").mkpath(path);
+
+  // Follows the convention used by av_get_token().
+  QString escapedPath = path.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
+
+  getDeviceID();
+}
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+bool CodecsFetcher::codecNeedsDownload(const CodecDriver& codec)
+{
+  if (codec.present)
+    return false;
+  if (!codec.external)
+  {
+    QLOG_ERROR() << "Codec" << codec.driver << "does not exist and is not downloadable.";
+    return false;
+  }
+  for (int n = 0; n < m_Codecs.size(); n++)
+  {
+    if (Codecs::sameCodec(codec, m_Codecs[n]))
+      return false;
+  }
+  if (QFile(codec.getPath()).exists())
+  {
+    QLOG_ERROR() << "Codec" << codec.driver << "exists on disk as" << codec.getPath()
+                 << "but is not known as installed - broken codec? Skipping download.";
+    return false;
+  }
+  return true;
+}
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+void CodecsFetcher::installCodecs(const QList<CodecDriver>& codecs)
+{
+  foreach (CodecDriver codec, codecs)
+  {
+    if (codecNeedsDownload(codec))
+      m_Codecs.enqueue(codec);
+  }
+  startNext();
+}
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+static Downloader::HeaderList getPlexHeaders()
+{
+  Downloader::HeaderList headers;
+  QString auth = SystemComponent::Get().authenticationToken();
+  if (auth.size())
+    headers.append({"X-Plex-Token", auth});
+  headers.append({"X-Plex-Product", "Plex Media Player"});
+  headers.append({"X-Plex-Platform", "Konvergo"});
+  return headers;
+}
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+void CodecsFetcher::startNext()
+{
+  if (m_Codecs.isEmpty())
+  {
+    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", getDeviceID());
+  query.addQueryItem("version", g_codecVersion);
+  query.addQueryItem("build", getBuildType());
+  url.setQuery(query);
+
+  QLOG_INFO() << "Codec info request:" << url.toString();
+
+  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)
+{
+  QLOG_INFO() << "Got reply:" << QString::fromUtf8(data);
+
+  QDomDocument dom;
+  if (!dom.setContent(data))
+  {
+    QLOG_ERROR() << "XML parsing error.";
+    return false;
+  }
+
+  QDomNodeList list = dom.elementsByTagName("MediaContainer");
+  if (list.count() != 1)
+  {
+    QLOG_ERROR() << "MediaContainer XML element not found.";
+    return false;
+  }
+  list = dom.elementsByTagName("Codec");
+  if (list.count() != 1)
+  {
+    QLOG_ERROR() << "Codec XML element not found.";
+    return false;
+  }
+
+  QDomNamedNodeMap attrs = list.at(0).attributes();
+  QString url = attrs.namedItem("url").toAttr().value();
+  if (!url.size())
+  {
+    QLOG_ERROR() << "No URL found.";
+    return false;
+  }
+
+  QString hash = attrs.namedItem("fileSha").toAttr().value();
+  m_currentHash = QByteArray::fromHex(hash.toUtf8());
+  // it's hardcoded to SHA-1
+  if (!m_currentHash.size()) {
+    QLOG_ERROR() << "Hash value in unexpected format or missing:" << hash;
+    return false;
+  }
+
+  QLOG_INFO() << "Downloading codec:" << url;
+
+  Downloader *downloader = new Downloader(QVariant::fromValue(codec), url, getPlexHeaders(), this);
+  connect(downloader, &Downloader::done, this, &CodecsFetcher::codecDownloadDone);
+
+  return true;
+}
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+void CodecsFetcher::codecInfoDownloadDone(QVariant userData, bool success, const QByteArray& data)
+{
+  CodecDriver codec = userData.value<CodecDriver>();
+  if (!success || !processCodecInfoReply(data, codec))
+  {
+    QLOG_ERROR() << "Codec download failed.";
+    startNext();
+  }
+}
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+void CodecsFetcher::processCodecDownloadDone(const QByteArray& data, const CodecDriver& codec)
+{
+  QByteArray hash = QCryptographicHash::hash(data, QCryptographicHash::Sha1);
+
+  if (hash != m_currentHash)
+  {
+    QLOG_ERROR() << "Checksum mismatch: got" << hash.toHex() << "expected" << m_currentHash.toHex();
+    return;
+  }
+
+  if (!Utils::safelyWriteFile(codec.getPath(), data))
+  {
+    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::getCachecCodecList())
+  {
+    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.";
+}
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+void CodecsFetcher::codecDownloadDone(QVariant userData, bool success, const QByteArray& data)
+{
+  CodecDriver codec = userData.value<CodecDriver>();
+  QLOG_INFO() << "Codec" << codec.driver << "request finished.";
+  if (success)
+  {
+    processCodecDownloadDone(data, codec);
+  }
+  else
+  {
+    QLOG_ERROR() << "Codec download HTTP request failed.";
+  }
+  startNext();
+}
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+Downloader::Downloader(QVariant userData, const QUrl& url, const HeaderList& headers, QObject* parent)
+  : QObject(parent), m_userData(userData)
+{
+  connect(&m_WebCtrl, &QNetworkAccessManager::finished, this, &Downloader::networkFinished);
+
+  QNetworkRequest request(url);
+  for (int n = 0; n < headers.size(); n++)
+    request.setRawHeader(headers[n].first.toUtf8(), headers[n].second.toUtf8());
+  m_WebCtrl.get(request);
+}
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+void Downloader::networkFinished(QNetworkReply* pReply)
+{
+  if (pReply->error() == QNetworkReply::NoError)
+  {
+    emit done(m_userData, true, pReply->readAll());
+  }
+  else
+  {
+    emit done(m_userData, false, QByteArray());
+  }
+  pReply->deleteLater();
+  m_WebCtrl.clearAccessCache(); // make sure the TCP connection is closed
+}
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+static CodecDriver selectBestCodec(const StreamInfo& stream)
+{
+  QList<CodecDriver> codecs = Codecs::findCodecsByFormat(Codecs::getCachecCodecList(), CodecType::Decoder, stream.codec);
+  CodecDriver best = {}, secondBest = {};
+  // For now, pick the first working one, or the first installable one.
+  // In future, it might need to be more clever.
+  for (auto codec : codecs)
+  {
+    if (codec.present)
+    {
+      best = codec;
+      break;
+    }
+    if (!secondBest.valid())
+      secondBest = codec;
+  }
+  return best.valid() ? best : secondBest;
+}
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+QList<CodecDriver> Codecs::determineRequiredCodecs(const PlaybackInfo& info)
+{
+  QList<CodecDriver> result;
+
+  bool needAC3Encoder = false;
+
+  for (auto stream : info.streams)
+  {
+    if (!stream.isVideo && !stream.isAudio)
+      continue;
+    if (!stream.codec.size())
+    {
+      QLOG_ERROR() << "unidentified codec";
+      continue;
+    }
+
+    // We could do this if we'd find a nice way to enable passthrough by default:
+#if 0
+    // Can passthrough be used? If so, don't request a codec.
+    if (info.audioPassthroughCodecs.contains(stream.codec))
+      continue;
+#endif
+
+    // (Would be nice to check audioChannels here to not request the encoder
+    // when playing stereo - but unfortunately, the ac3 encoder is loaded first,
+    // and only removed when detecting stereo input)
+    if (info.enableAC3Transcoding)
+      needAC3Encoder = true;
+
+    CodecDriver best = selectBestCodec(stream);
+    if (best.valid())
+    {
+      result.append(best);
+    }
+    else
+    {
+      QLOG_ERROR() << "no decoder for" << stream.codec;
+    }
+  }
+
+  if (needAC3Encoder)
+  {
+    QList<CodecDriver> codecs = Codecs::findCodecsByFormat(Codecs::getCachecCodecList(), CodecType::Encoder, "ac3");
+    CodecDriver encoder = {};
+    for (auto codec : codecs)
+    {
+      if (codec.present)
+      {
+        encoder = codec;
+        break;
+      }
+      if (codec.external)
+        encoder = codec; // fallback
+    }
+    if (encoder.valid())
+    {
+      result.append(encoder);
+    }
+    else
+    {
+      QLOG_ERROR() << "no AC3 encoder available";
+    }
+  }
+
+  return result;
+}

+ 124 - 0
src/player/CodecsComponent.h

@@ -0,0 +1,124 @@
+#ifndef CODECS_H
+#define CODECS_H
+
+#include <QObject>
+#include <QtCore/qglobal.h>
+#include <QList>
+#include <QSize>
+#include <QNetworkAccessManager>
+#include <QNetworkReply>
+#include <QQueue>
+#include <QUrl>
+#include <QVariant>
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+enum class CodecType {
+  Decoder,
+  Encoder,
+};
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+struct CodecDriver {
+  CodecType type; // encoder/decoder
+  QString format; // e.g. "h264", the canonical FFmpeg name of the codec
+  QString driver; // specific implementation, e.g. "h264" (native) or "h264_mf" (MediaFoundation)
+  bool present;   // if false, it's a not-installed installable codec
+  bool external;  // marked as external in CodecManifest.h
+
+  // Driver name decorated with additional attributes, e.g. "h264_mf_decoder".
+  QString getMangledName() const;
+
+  // Filename of the DLL/SO including build version/type, without path.
+  // Only applies to external drivers.
+  QString getFileName() const;
+
+  // Like getFileName(), but includes full path.
+  // Only applies to external drivers.
+  QString getPath() const;
+
+  bool valid() { return format.size() > 0; }
+};
+
+struct StreamInfo {
+  bool isVideo, isAudio;
+  QString codec;
+  QString profile;
+  int audioChannels;
+  QSize videoResolution;
+};
+
+struct PlaybackInfo {
+  QList<StreamInfo> streams;            // information for _all_ streams the file has
+                                        // (even if not selected)
+  QSet<QString> audioPassthroughCodecs; // list of audio formats to pass through
+  bool enableAC3Transcoding;            // encode non-stereo to AC3
+};
+
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+class Downloader : public QObject
+{
+  Q_OBJECT
+public:
+  typedef QList<QPair<QString, QString>> HeaderList;
+  explicit Downloader(QVariant userData, const QUrl& url, const HeaderList& headers, QObject* parent);
+Q_SIGNALS:
+  void done(QVariant userData, bool success, const QByteArray& data);
+
+private Q_SLOTS:
+  void networkFinished(QNetworkReply* pReply);
+
+private:
+  QNetworkAccessManager m_WebCtrl;
+  QByteArray m_DownloadedData;
+  QVariant m_userData;
+};
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+class CodecsFetcher : public QObject
+{
+  Q_OBJECT
+public:
+  // 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);
+
+  // For free use by the user of this object.
+  QVariant userData;
+
+Q_SIGNALS:
+  void done(CodecsFetcher* sender);
+
+private Q_SLOTS:
+  void codecInfoDownloadDone(QVariant userData, bool success, const QByteArray& data);
+  void codecDownloadDone(QVariant userData, bool success, const QByteArray& data);
+
+private:
+  bool codecNeedsDownload(const CodecDriver& codec);
+  bool processCodecInfoReply(const QByteArray& data, const CodecDriver& codec);
+  void processCodecDownloadDone(const QByteArray& data, const CodecDriver& codec);
+  void startNext();
+
+  QQueue<CodecDriver> m_Codecs;
+  QByteArray m_currentHash;
+};
+
+class Codecs
+{
+public:
+  static void preinitCodecs();
+
+  static inline bool sameCodec(const CodecDriver& a, const CodecDriver& b)
+  {
+    return a.type == b.type && a.format == b.format && a.driver == b.driver;
+  }
+
+  static void updateCachedCodecList();
+
+  static const QList<CodecDriver>& getCachecCodecList();
+
+  static QList<CodecDriver> findCodecsByFormat(const QList<CodecDriver>& list, CodecType type, const QString& format);
+  static QList<CodecDriver> determineRequiredCodecs(const PlaybackInfo& info);
+};
+
+#endif // CODECS_H

+ 136 - 16
src/player/PlayerComponent.cpp

@@ -36,7 +36,7 @@ static void wakeup_cb(void *context)
 PlayerComponent::PlayerComponent(QObject* parent)
   : ComponentBase(parent), m_lastPositionUpdate(0.0), m_playbackAudioDelay(0), m_playbackStartSent(false), m_window(nullptr), m_mediaFrameRate(0),
   m_restoreDisplayTimer(this), m_reloadAudioTimer(this),
-  m_streamSwitchImminent(false)
+  m_streamSwitchImminent(false), m_doAc3Transcoding(false)
 {
   qmlRegisterType<PlayerQuickItem>("Konvergo", 1, 0, "MpvVideo"); // deprecated name
   qmlRegisterType<PlayerQuickItem>("Konvergo", 1, 0, "KonvergoVideo");
@@ -142,6 +142,10 @@ bool PlayerComponent::componentInitialize()
   // (See handler in handleMpvEvent() for details.)
   mpv::qt::command_variant(m_mpv, QStringList() << "hook-add" << "on_load" << "1" << "0");
 
+  // Setup a hook with the ID 1, which is run at a certain stage during loading.
+  // We use it to probe the codecs.
+  mpv::qt::command_variant(m_mpv, QStringList() << "hook-add" << "on_preloaded" << "2" << "0");
+
   updateAudioDeviceList();
   setAudioConfiguration();
   updateSubtitleSettings();
@@ -157,6 +161,7 @@ bool PlayerComponent::componentInitialize()
           this, &PlayerComponent::setAudioConfiguration);
 
   initializeCodecSupport();
+  Codecs::updateCachedCodecList();
 
   return true;
 }
@@ -214,6 +219,7 @@ bool PlayerComponent::load(const QString& url, const QVariantMap& options, const
 void PlayerComponent::queueMedia(const QString& url, const QVariantMap& options, const QVariantMap &metadata, const QString& audioStream, const QString& subtitleStream)
 {
   m_mediaFrameRate = metadata["frameRate"].toFloat(); // returns 0 on failure
+  m_serverMediaInfo = metadata["media"].toMap();
 
   updateVideoSettings();
 
@@ -446,11 +452,14 @@ void PlayerComponent::handleMpvEvent(mpv_event *event)
     case MPV_EVENT_CLIENT_MESSAGE:
     {
       mpv_event_client_message *msg = (mpv_event_client_message *)event->data;
+      if (msg->num_args < 3 || strcmp(msg->args[0], "hook_run") != 0)
+        break;
+      QString resumeId = QString::fromUtf8(msg->args[2]);
+      // Start "on_load" hook.
       // This happens when the player is about to load the file, but no actual loading has taken part yet.
       // We use this to block loading until we explicitly tell it to continue.
-      if (msg->num_args >= 3 && !strcmp(msg->args[0], "hook_run") && !strcmp(msg->args[1], "1"))
+      if (!strcmp(msg->args[1], "1"))
       {
-        QString resumeId = QString::fromUtf8(msg->args[2]);
         // Calling this lambda will instruct mpv to continue loading the file.
         auto resume = [=] {
           QLOG_INFO() << "resuming loading";
@@ -472,6 +481,16 @@ void PlayerComponent::handleMpvEvent(mpv_event *event)
         }
         break;
       }
+      // Start "on_preload" hook.
+      // Used to probe codecs.
+      if (!strcmp(msg->args[1], "2"))
+      {
+        startCodecsLoading([=] {
+          mpv::qt::command_variant(m_mpv, QStringList() << "hook-ack" << resumeId);
+        });
+        break;
+      }
+      break;
     }
     default:; /* ignore */
   }
@@ -716,11 +735,11 @@ void PlayerComponent::setAudioConfiguration()
 
   mpv::qt::set_option_variant(m_mpv, "af-defaults", "lavrresample" + resampleOpts);
 
-  QString passthroughCodecs;
+  m_passthroughCodecs.clear();
+
   // passthrough doesn't make sense with basic type
   if (deviceType != AUDIO_DEVICE_TYPE_BASIC)
   {
-    QStringList enabledCodecs;
     SettingsSection* audioSection = SettingsComponent::Get().getSection(SETTINGS_SECTION_AUDIO);
 
     QStringList codecs;
@@ -732,16 +751,15 @@ void PlayerComponent::setAudioConfiguration()
     for(const QString& key : codecs)
     {
       if (audioSection->value("passthrough." + key).toBool())
-        enabledCodecs << key;
+        m_passthroughCodecs << key;
     }
 
     // dts-hd includes dts, but listing dts before dts-hd may disable dts-hd.
-    if (enabledCodecs.indexOf("dts-hd") != -1)
-      enabledCodecs.removeAll("dts");
-
-    passthroughCodecs = enabledCodecs.join(",");
+    if (m_passthroughCodecs.indexOf("dts-hd") != -1)
+      m_passthroughCodecs.removeAll("dts");
   }
 
+  QString passthroughCodecs = m_passthroughCodecs.join(",");
   mpv::qt::set_option_variant(m_mpv, "audio-spdif", passthroughCodecs);
 
   // set the channel layout
@@ -759,12 +777,12 @@ void PlayerComponent::setAudioConfiguration()
   // here for now. We might need to add support for DTS transcoding
   // if we see user requests for it.
   //
-  bool doAc3Transcoding = false;
+  m_doAc3Transcoding = false;
   if (layout == "2.0" &&
       SettingsComponent::Get().value(SETTINGS_SECTION_AUDIO, "passthrough.ac3").toBool())
   {
     mpv::qt::command_variant(m_mpv, QStringList() << "af" << "add" << "@ac3:lavcac3enc");
-    doAc3Transcoding = true;
+    m_doAc3Transcoding = true;
   }
   else
   {
@@ -778,7 +796,7 @@ void PlayerComponent::setAudioConfiguration()
                                         "ac3 transcoding: %4").arg(device.toString(),
                                                                    layout.toString(),
                                                                    passthroughCodecs.isEmpty() ? "none" : passthroughCodecs,
-                                                                   doAc3Transcoding ? "yes" : "no");
+                                                                   m_doAc3Transcoding ? "yes" : "no");
   QLOG_INFO() << qPrintable(audioConfig);
 }
 
@@ -826,6 +844,13 @@ void PlayerComponent::updateVideoSettings()
   mpv::qt::set_option_variant(m_mpv, "cache", cache.toInt() * 1024);
 }
 
+/////////////////////////////////////////////////////////////////////////////////////////
+void PlayerComponent::userCommand(QString command)
+{
+  QByteArray cmdUtf8 = command.toUtf8();
+  mpv_command_string(m_mpv, cmdUtf8.data());
+}
+
 /////////////////////////////////////////////////////////////////////////////////////////
 void PlayerComponent::initializeCodecSupport()
 {
@@ -854,10 +879,105 @@ bool PlayerComponent::checkCodecSupport(const QString& codec)
 }
 
 /////////////////////////////////////////////////////////////////////////////////////////
-void PlayerComponent::userCommand(QString command)
+QList<CodecDriver> convertCodecList(QVariant list, CodecType type)
 {
-  QByteArray cmdUtf8 = command.toUtf8();
-  mpv_command_string(m_mpv, cmdUtf8.data());
+  QList<CodecDriver> codecs;
+
+  foreach (const QVariant& e, list.toList())
+  {
+    QVariantMap map = e.toMap();
+
+    QString family = map["family"].toString();
+    QString codec = map["codec"].toString();
+    QString driver = map["driver"].toString();
+
+    // Only include FFmpeg codecs; exclude pseudo-codecs like spdif.
+    if (family != "lavc")
+      continue;
+
+    CodecDriver ncodec = {};
+    ncodec.type = type;
+    ncodec.format = codec;
+    ncodec.driver = driver;
+    ncodec.present = true;
+
+    codecs.append(ncodec);
+  }
+
+  return codecs;
+}
+
+/////////////////////////////////////////////////////////////////////////////////////////
+QList<CodecDriver> PlayerComponent::installedCodecs()
+{
+  QList<CodecDriver> codecs;
+
+  codecs.append(convertCodecList(mpv::qt::get_property_variant(m_mpv, "decoder-list"), CodecType::Decoder));
+  codecs.append(convertCodecList(mpv::qt::get_property_variant(m_mpv, "encoder-list"), CodecType::Encoder));
+
+  return codecs;
+}
+
+/////////////////////////////////////////////////////////////////////////////////////////
+PlaybackInfo PlayerComponent::getPlaybackInfo()
+{
+  PlaybackInfo info = {};
+
+  for (auto codec : m_passthroughCodecs)
+  {
+    // Normalize back to canonical codec names.
+    if (codec == "dts-hd")
+      codec = "dts";
+    info.audioPassthroughCodecs.insert(codec);
+  }
+
+  info.enableAC3Transcoding = m_doAc3Transcoding;
+
+  auto tracks = mpv::qt::get_property_variant(m_mpv, "track-list");
+  foreach (const QVariant& track, tracks.toList())
+  {
+    QVariantMap map = track.toMap();
+    QString type = map["type"].toString();
+
+    StreamInfo stream = {};
+    stream.isVideo = type == "video";
+    stream.isAudio = type == "audio";
+    stream.codec = map["codec"].toString();
+    stream.audioChannels = map["demux-channel-count"].toInt();
+    stream.videoResolution = QSize(map["demux-w"].toInt(), map["demux-h"].toInt());
+
+    if (stream.isVideo)
+    {
+      // Assume there's only 1 video stream. We get the profile from the
+      // server because mpv can't be bothered to determine it.
+      stream.profile = m_serverMediaInfo["videoProfile"].toString();
+    }
+
+    info.streams.append(stream);
+  }
+
+  return info;
+}
+
+// For QVariant.
+Q_DECLARE_METATYPE(std::function<void()>);
+
+/////////////////////////////////////////////////////////////////////////////////////////
+void PlayerComponent::startCodecsLoading(std::function<void()> resume)
+{
+  auto fetcher = new CodecsFetcher();
+  fetcher->userData = QVariant::fromValue(resume);
+  connect(fetcher, &CodecsFetcher::done, this, &PlayerComponent::onCodecsLoadingDone);
+  Codecs::updateCachedCodecList();
+  QList<CodecDriver> codecs = Codecs::determineRequiredCodecs(getPlaybackInfo());
+  fetcher->installCodecs(codecs);
+}
+
+/////////////////////////////////////////////////////////////////////////////////////////
+void PlayerComponent::onCodecsLoadingDone(CodecsFetcher* sender)
+{
+  sender->deleteLater();
+  sender->userData.value<std::function<void()>>()();
 }
 
 /////////////////////////////////////////////////////////////////////////////////////////

+ 15 - 0
src/player/PlayerComponent.h

@@ -9,7 +9,10 @@
 #include <QTimer>
 #include <QTextStream>
 
+#include <functional>
+
 #include "ComponentManager.h"
+#include "CodecsComponent.h"
 
 #include <mpv/client.h>
 #include <mpv/qthelper.hpp>
@@ -95,6 +98,12 @@ public:
   // including unknown codec names.
   Q_INVOKABLE virtual bool checkCodecSupport(const QString& codec);
 
+  // Return list of currently installed codecs. Includes builtin codecs.
+  // Downloadable, but not yet installed codecs are excluded.
+  // May include codecs that do not work, like vc1_mmal on RPIs with no license.
+  // (checkCodecSupport() handles this specific case to a degree.)
+  Q_INVOKABLE virtual QList<CodecDriver> installedCodecs();
+
   Q_INVOKABLE void userCommand(QString command);
 
   const mpv::qt::Handle getMpvHandle() const { return m_mpv; }
@@ -117,6 +126,7 @@ private Q_SLOTS:
   void onRestoreDisplay();
   void onRefreshRateChange();
   void onReloadAudio();
+  void onCodecsLoadingDone(CodecsFetcher* sender);
 
 Q_SIGNALS:
   void playing(const QString& url);
@@ -172,6 +182,8 @@ private:
   void checkCurrentAudioDevice(const QSet<QString>& old_devs, const QSet<QString>& new_devs);
   void appendAudioFormat(QTextStream& info, const QString& property) const;
   void initializeCodecSupport();
+  PlaybackInfo getPlaybackInfo();
+  void startCodecsLoading(std::function<void()> resume);
 
   mpv::qt::Handle m_mpv;
 
@@ -186,6 +198,9 @@ private:
   QSet<QString> m_audioDevices;
   bool m_streamSwitchImminent;
   QMap<QString, bool> m_codecSupport;
+  bool m_doAc3Transcoding;
+  QStringList m_passthroughCodecs;
+  QVariantMap m_serverMediaInfo;
 };
 
 #endif // PLAYERCOMPONENT_H

+ 2 - 0
src/system/SystemComponent.cpp

@@ -269,6 +269,8 @@ void SystemComponent::userInformation(const QVariantMap& userModel)
   }
 
   SettingsComponent::Get().setUserRoleList(roleList);
+
+  m_authenticationToken = userModel.value("authenticationToken").toString();
 }
 
 /////////////////////////////////////////////////////////////////////////////////////////

+ 3 - 0
src/system/SystemComponent.h

@@ -69,6 +69,8 @@ public:
 
   inline bool isOpenELEC() { return m_platformType == platformTypeOpenELEC; }
 
+  inline QString authenticationToken() { return m_authenticationToken; }
+
   Q_INVOKABLE void crashApp();
 
 signals:
@@ -84,6 +86,7 @@ private:
   PlatformArch m_platformArch;
   QString m_overridePlatform;
   bool m_doLogMessages;
+  QString m_authenticationToken;
 
 };
 

+ 12 - 1
src/utils/Utils.cpp

@@ -1,7 +1,6 @@
 #include "Utils.h"
 #include <QtGlobal>
 #include <QStandardPaths>
-#include <QByteArray>
 #include <QCoreApplication>
 #include <QDir>
 #include <QProcess>
@@ -11,6 +10,8 @@
 #include <QVariant>
 #include <qnetworkinterface.h>
 #include <QUuid>
+#include <QFile>
+#include <QSaveFile>
 
 #include "settings/SettingsComponent.h"
 #include "settings/SettingsSection.h"
@@ -130,3 +131,13 @@ QString Utils::ClientUUID()
   }
   return storedUUID;
 }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+bool Utils::safelyWriteFile(const QString& filename, const QByteArray& data)
+{
+  QSaveFile file(filename);
+  if (!file.open(QIODevice::WriteOnly))
+    return false;
+  file.write(data);
+  return file.commit();
+}

+ 2 - 0
src/utils/Utils.h

@@ -3,6 +3,7 @@
 
 #include <QString>
 #include <QUrl>
+#include <QByteArray>
 #include <QVariant>
 #include <QException>
 #include <QtCore/qjsondocument.h>
@@ -54,6 +55,7 @@ namespace Utils
   QString ComputerName();
   QString PrimaryIPv4Address();
   QString ClientUUID();
+  bool safelyWriteFile(const QString& filename, const QByteArray& data);
 }
 
 #endif // UTILS_H