123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623 |
- #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 "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<QString> 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<QString> 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<QString> g_systemAudioEncoderWhitelist = {
- };
- 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
- }
- ///////////////////////////////////////////////////////////////////////////////////////////////////
- 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<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 = 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<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());
- }
- ///////////////////////////////////////////////////////////////////////////////////////////////////
- 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<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 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<CodecDriver> 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<CodecDriver> Codecs::determineRequiredCodecs(const PlaybackInfo& info)
- {
- QList<CodecDriver> 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<CodecDriver> 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;
- }
|