#include "CodecsComponent.h" #include #include #include #include #include #include #include #include #include #include #include "system/SystemComponent.h" #include "settings/SettingsComponent.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 // 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 g_systemVideoDecoderWhitelist = { // definitely work "h264_mmal", "mpeg2_mmal", "mpeg4_mmal", "vc1_mmal", // still sketchy at best, partially broken "h264_mf", "hevc_mf", "vc1_mf", "wmv1_mf", "wmv2_mf", "wmv3_mf", "mpeg4_mf", "msmpeg4v1_mf", "msmpeg4v2_mf", "msmpeg4v3_mf", }; static QSet g_systemAudioDecoderWhitelist = { // should work well "aac_at", "ac3_at", "mp1_at", "mp2_at", "mp3_at", "ac3_mf", "eac3_mf", "aac_mf", "mp1_mf", "mp2_mf", "mp3_mf", }; static QSet g_systemAudioEncoderWhitelist = { }; static QString g_codecVersion; static QList g_cachedCodecList; /////////////////////////////////////////////////////////////////////////////////////////////////// static QString getBuildType() { #ifdef Q_OS_MAC return "darwin-x86_64"; #else return SystemComponent::Get().getPlatformTypeString() + "-" + SystemComponent::Get().getPlatformArchString(); #endif } /////////////////////////////////////////////////////////////////////////////////////////////////// QString Codecs::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& 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 = Codecs::plexNameToFF(list[i].codecName); codec.driver = list[i].name; codec.external = list[i].external; if (!codec.isSystemCodec()) 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 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& Codecs::getCachecCodecList() { return g_cachedCodecList; } /////////////////////////////////////////////////////////////////////////////////////////////////// QList Codecs::findCodecsByFormat(const QList& list, CodecType type, const QString& format) { QList 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()); } /////////////////////////////////////////////////////////////////////////////////////////////////// bool CodecDriver::isSystemCodec() const { // MS Windows if (driver.endsWith("_mf")) return true; // OSX if (driver.endsWith("_at")) return true; // Linux on RPI if (driver.endsWith("_mmal")) return true; return false; } /////////////////////////////////////////////////////////////////////////////////////////////////// bool CodecDriver::isWhitelistedSystemAudioCodec() const { if (type == CodecType::Decoder) return g_systemAudioDecoderWhitelist.contains(driver); else return g_systemAudioEncoderWhitelist.contains(driver); } /////////////////////////////////////////////////////////////////////////////////////////////////// bool CodecDriver::isWhitelistedSystemVideoCodec() const { if (type == CodecType::Decoder) return g_systemVideoDecoderWhitelist.contains(driver); return false; } /////////////////////////////////////////////////////////////////////////////////////////////////// // 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& 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(); 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(); 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 bool useSystemAudioDecoders() { return SettingsComponent::Get().value(SETTINGS_SECTION_MAIN, "useSystemAudioCodecs2").toBool(); } /////////////////////////////////////////////////////////////////////////////////////////////////// static bool useSystemVideoDecoders() { return SettingsComponent::Get().value(SETTINGS_SECTION_MAIN, "useSystemVideoCodecs").toBool(); } /////////////////////////////////////////////////////////////////////////////////////////////////// static CodecDriver selectBestDecoder(const StreamInfo& stream) { QList codecs = Codecs::findCodecsByFormat(Codecs::getCachecCodecList(), CodecType::Decoder, stream.codec); CodecDriver best = {}; int bestScore = -1; for (auto codec : codecs) { int score = -1; if (codec.isSystemCodec()) { // we always want to avoid using non-whitelisted system codecs // on the other hand, always prefer whitelisted system codecs if ((codec.isWhitelistedSystemAudioCodec() && useSystemAudioDecoders()) || (codec.isWhitelistedSystemVideoCodec() && useSystemVideoDecoders())) score = 15; } else { // prefer codecs which do not have to be downloaded over others if (codec.present) score = 10; else score = 5; } if (score > bestScore) { best = codec; bestScore = score; } } return best; } /////////////////////////////////////////////////////////////////////////////////////////////////// QList Codecs::determineRequiredCodecs(const PlaybackInfo& info) { QList result; bool needAC3Encoder = false; QLOG_INFO() << "Using system audio decoders:" << useSystemAudioDecoders(); QLOG_INFO() << "Using system video decoders:" << useSystemVideoDecoders(); #if !defined(HAVE_CODEC_MANIFEST) QLOG_INFO() << "Not using on-demand codecs."; #endif 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 = selectBestDecoder(stream); if (best.valid()) { result.append(best); } else { QLOG_ERROR() << "no decoder for" << stream.codec; } } if (needAC3Encoder) { QList codecs = Codecs::findCodecsByFormat(Codecs::getCachecCodecList(), CodecType::Encoder, "ac3"); CodecDriver encoder = {}; for (auto codec : codecs) { if (codec.present && (!codec.isSystemCodec() || codec.isWhitelistedSystemAudioCodec())) { encoder = codec; break; } if (codec.external) encoder = codec; // fallback } if (encoder.valid()) { result.append(encoder); } else { QLOG_ERROR() << "no AC3 encoder available"; } } return result; }