123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299 |
- #include "CodecsComponent.h"
- #include <QDebug>
- #include <QString>
- #include <Qt>
- #include <QDir>
- #include <QDomAttr>
- #include <QDomDocument>
- #include <QDomNode>
- #include <QCoreApplication>
- #include <QProcess>
- #include <QUuid>
- #include <QUrl>
- #include <QUrlQuery>
- #include <QResource>
- #include <QSaveFile>
- #include <QStandardPaths>
- #include <QOperatingSystemVersion>
- #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"
- #include "shared/Paths.h"
- #include "PlayerComponent.h"
- #define countof(x) (sizeof(x) / sizeof((x)[0]))
- // For QVariant. Mysteriously makes Qt happy.
- Q_DECLARE_METATYPE(CodecDriver);
- #ifdef HAVE_CODEC_MANIFEST
- #define WITH_CODECS 1
- #include "CodecManifest.h"
- #else
- #define WITH_CODECS 0
- #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
- #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 = {
- // RPI
- "h264_mmal",
- "mpeg2_mmal",
- "mpeg4_mmal",
- "vc1_mmal",
- };
- static QSet<QString> g_systemAudioDecoderWhitelist = {
- // OSX
- "eac3_at",
- // Windows
- "eac3_mf",
- };
- static QSet<QString> g_systemAudioEncoderWhitelist = {
- };
- static QSize g_mediaFoundationH264MaxResolution;
- static QString g_codecVersion;
- static QList<CodecDriver> g_cachedCodecList;
- static QString g_deviceID;
- static QString g_eaeWatchFolder;
- static QProcess* g_eaeProcess;
- ///////////////////////////////////////////////////////////////////////////////////////////////////
- static QString getBuildType()
- {
- #ifdef Q_OS_MAC
- return "darwin-x86_64";
- #elif defined(TARGET_RPI)
- return "openelec-armv7";
- #else
- return SystemComponent::Get().getPlatformTypeString() + "-" +
- SystemComponent::Get().getPlatformArchString();
- #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)
- {
- if (plex == "dca")
- return "dts";
- return plex;
- }
- ///////////////////////////////////////////////////////////////////////////////////////////////////
- QString Codecs::plexNameFromFF(QString ffname)
- {
- if (ffname == "dts")
- return "dca";
- return ffname;
- }
- ///////////////////////////////////////////////////////////////////////////////////////////////////
- static QString codecsRootPath()
- {
- return Paths::dataDir("Codecs") + QDir::separator();
- }
- ///////////////////////////////////////////////////////////////////////////////////////////////////
- static QString codecsPath()
- {
- return codecsRootPath() + g_codecVersion + "-" + getBuildType() + QDir::separator();
- }
- ///////////////////////////////////////////////////////////////////////////////////////////////////
- static QString eaePrefixPath()
- {
- // (Keep in sync with PMS paths.)
- 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 bool eaeIsPresent()
- {
- return QFile(eaeBinaryPath()).exists();
- }
- ///////////////////////////////////////////////////////////////////////////////////////////////////
- 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().installedCodecDrivers();
- // 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::getCachedCodecList()
- {
- 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() + "." + 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;
- // Not really a system codec, but treated as such for convenience
- if (driver.endsWith("_eae"))
- return true;
- if (driver.endsWith("_cuvid"))
- 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
- {
- 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;
- }
- ///////////////////////////////////////////////////////////////////////////////////////////////////
- static bool useSystemAudioDecoders()
- {
- return true;
- }
- ///////////////////////////////////////////////////////////////////////////////////////////////////
- static bool useSystemVideoDecoders()
- {
- return SettingsComponent::Get().value(SETTINGS_SECTION_MAIN, "useSystemVideoCodecs").toBool();
- }
- /////////////////////////////////////////////////////////////////////////////////////////
- // Load the device ID, do some minimal verification, return "" on error.
- static QString loadDeviceID(QString filename)
- {
- QFile path(filename);
- if (!path.open(QFile::ReadOnly))
- return "";
- auto res = QString::fromLatin1(path.readAll());
- if (res.size() < 32 || res.size() > 512)
- res = ""; // mark as invalid
- return res;
- }
- /////////////////////////////////////////////////////////////////////////////////////////
- static QString findOldDeviceID()
- {
- return "";
- }
- ///////////////////////////////////////////////////////////////////////////////////////////////////
- // Returns "" on error.
- static QString loadDeviceID()
- {
- QString deviceIDFilename = QDir(codecsRootPath()).absoluteFilePath(".device-id");
- QString id = loadDeviceID(deviceIDFilename);
- if (id.isEmpty())
- {
- id = findOldDeviceID();
- if (id.isEmpty())
- {
- id = QUuid::createUuid().toString();
- // The UUID should be e.g. "8f6ad954-0cb9-4dbb-a5e5-e0b085f07cf8"
- if (id.startsWith("{"))
- id = id.mid(1);
- if (id.endsWith("}"))
- id = id.mid(0, id.size() - 1);
- }
- Utils::safelyWriteFile(deviceIDFilename, id.toLatin1());
- // We load it again to make sure writing it succeeded. If it doesn't, we'll
- // error out at a later point.
- id = loadDeviceID(deviceIDFilename);
- }
- return id;
- }
- ///////////////////////////////////////////////////////////////////////////////////////////////////
- static QString getFFmpegVersion()
- {
- auto mpv = mpv::qt::Handle::FromRawHandle(mpv_create());
- if (!mpv || mpv_initialize(mpv) < 0)
- return "";
- 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()
- {
- // 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(":", "\\:").replace("'", "\\'");
- // This must be run before any threads are started etc. (for safety).
- 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();
- }
- #if 0
- ///////////////////////////////////////////////////////////////////////////////////////////////////
- static bool probeDecoder(QString decoder, QString resourceName)
- {
- QResource resource(resourceName);
- qDebug() << "Testing decoding of" << resource.fileName();
- if (!resource.isValid())
- return false;
- auto mpv = mpv::qt::Handle::FromRawHandle(mpv_create());
- if (!mpv || mpv_initialize(mpv) < 0)
- return false;
- // Disable any output.
- mpv::qt::set_property(mpv, "vo", "null");
- // Force the decoder. The ",-" means that if the first entry fails, the next codec in the global
- // codec list will not be tried, and decoding fails.
- mpv::qt::set_property(mpv, "vd", "lavc:" + decoder + ",-");
- // Attempt decoding, and return success.
- auto data = QByteArray::fromRawData((const char *)resource.data(), resource.size());
- if (resource.isCompressed())
- data = qUncompress(data);
- auto hex = data.toHex();
- mpv::qt::command(mpv, QVariantList{"loadfile", "hex://" + QString::fromLatin1(hex)});
- bool result = false;
- while (1) {
- mpv_event *event = mpv_wait_event(mpv, 0);
- if (event->event_id == MPV_EVENT_SHUTDOWN)
- break;
- if (event->event_id == MPV_EVENT_END_FILE)
- {
- mpv_event_end_file *endFile = (mpv_event_end_file *)event->data;
- result = endFile->reason == MPV_END_FILE_REASON_EOF;
- break;
- }
- }
- qDebug() << "Result:" << result;
- return result;
- }
- #endif
- ///////////////////////////////////////////////////////////////////////////////////////////////////
- static void probeCodecs()
- {
- #if 0
- if (useSystemVideoDecoders())
- {
- if (probeDecoder("h264_mf", ":/testmedia/high_4096x2304.h264"))
- g_mediaFoundationH264MaxResolution = QSize(4096, 2304);
- else if (probeDecoder("h264_mf", ":/testmedia/high_4096x2160.h264"))
- g_mediaFoundationH264MaxResolution = QSize(4096, 2160);
- else if (probeDecoder("h264_mf", ":/testmedia/high_4096x1080.h264"))
- g_mediaFoundationH264MaxResolution = QSize(4096, 1080);
- else
- g_systemVideoDecoderWhitelist.remove("h264_mf");
- qDebug() << "h264_mf max. resolution:" << g_mediaFoundationH264MaxResolution;
- }
- #endif
- #ifdef Q_OS_MAC
- auto current = QOperatingSystemVersion::current();
- // Unsupported, but avoid picking up broken Perian decoders.
- if (current <= QOperatingSystemVersion::OSXYosemite)
- g_systemAudioDecoderWhitelist.remove("ac3_at");
- // Unknown Apple crashes
- if (current <= QOperatingSystemVersion::OSXElCapitan)
- g_systemAudioDecoderWhitelist.remove("aac_at");
- #endif
- }
- ///////////////////////////////////////////////////////////////////////////////////////////////////
- static void updateCodecs()
- {
- QStringList candidates = {
- codecsRootPath(),
- Paths::dataDir() + "/codecs/",
- };
- QSet<QString> codecFiles;
- bool needEAE = false;
- for (auto dir : candidates)
- {
- QDir qdir(dir);
- if (!qdir.exists())
- continue;
- for (auto entry : qdir.entryList(QDir::Dirs | QDir::NoDotAndDotDot))
- {
- QDir entryDir = qdir;
- if (!entryDir.cd(entry))
- continue;
- for (auto codecdirEntry : entryDir.entryList(QDir::Files))
- codecFiles.insert(codecdirEntry);
- // NOTE: PMS also uses this prefix
- if (entry.startsWith("EasyAudioEncoder-") && !eaeIsPresent())
- needEAE = true;
- }
- }
- QList<CodecDriver> install;
- for (CodecDriver& codec : g_cachedCodecList)
- {
- if ((codecFiles.contains(codec.getFileName()) && codec.external && !codec.present) ||
- (codec.getSystemCodecType() == "eae" && needEAE))
- install.append(codec);
- }
- if (!install.empty())
- {
- QStringList codecs;
- for (auto codec : install)
- codecs.append(codec.getMangledName());
- qInfo() << "Updating some codecs: " + codecs.join(", ");
- auto fetcher = new CodecsFetcher();
- QObject::connect(fetcher, &CodecsFetcher::done, [](CodecsFetcher* sender)
- {
- qInfo() << "Codec update finished.";
- sender->deleteLater();
- });
- fetcher->startCodecs = false;
- fetcher->installCodecs(install);
- }
- }
- ///////////////////////////////////////////////////////////////////////////////////////////////////
- static void deleteOldCodecs()
- {
- QStringList neededPaths = {
- codecsPath(),
- };
- for (auto entry : QDir(codecsRootPath()).entryList(QDir::Dirs | QDir::NoDotAndDotDot))
- {
- QDir entryPath = codecsRootPath();
- if (!entryPath.cd(entry))
- continue;
- bool needed = false;
- for (auto neededPath : neededPaths)
- {
- if (entryPath.absolutePath() == QDir(neededPath).absolutePath())
- {
- needed = true;
- break;
- }
- }
- if (needed)
- continue;
- // Same version, but different platform -> just keep it.
- if (entry.startsWith(g_codecVersion + "-"))
- continue;
- // EAE is "special"
- if (entry.startsWith(QString("EasyAudioEncoder-") + STRINGIFY(EAE_VERSION) + "-"))
- continue;
- qDebug() << "Deleting old directory: " << entryPath.absolutePath();
- entryPath.removeRecursively();
- }
- }
- ///////////////////////////////////////////////////////////////////////////////////////////////////
- void Codecs::initCodecs()
- {
- if (g_deviceID.isEmpty())
- throw FatalException("Could not read device-id.");
- if (g_eaeWatchFolder.isEmpty())
- throw FatalException("Could not create EAE working directory.");
- Codecs::updateCachedCodecList();
- updateCodecs();
- deleteOldCodecs();
- probeCodecs();
- }
- ///////////////////////////////////////////////////////////////////////////////////////////////////
- bool CodecsFetcher::codecNeedsDownload(const CodecDriver& codec)
- {
- if (codec.present)
- return false;
- if (!codec.external)
- {
- qCritical() << "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;
- }
- QFile codecFile(codec.getPath());
- if (codecFile.exists())
- {
- qCritical() << "Codec" << codec.driver << "exists on disk as" << codec.getPath()
- << "but is not known as installed - broken codec?";
- if (!codecFile.remove())
- return false;
- qCritical() << "Retrying download.";
- }
- return true;
- }
- ///////////////////////////////////////////////////////////////////////////////////////////////////
- void CodecsFetcher::installCodecs(const QList<CodecDriver>& codecs)
- {
- foreach (CodecDriver codec, codecs)
- {
- if (codecNeedsDownload(codec))
- m_Codecs.enqueue(codec);
- if (codec.getSystemCodecType() == "eae")
- {
- m_eaeNeeded = true;
- if (!eaeIsPresent())
- m_fetchEAE = true;
- }
- }
- startNext();
- }
- ///////////////////////////////////////////////////////////////////////////////////////////////////
- static Downloader::HeaderList getPlexHeaders()
- {
- Downloader::HeaderList headers;
- return headers;
- }
- static QUrl buildCodecQuery(QString version, QString name, QString build)
- {
- return QUrl("");
- }
- ///////////////////////////////////////////////////////////////////////////////////////////////////
- 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 && startCodecs)
- startEAE();
- emit done(this);
- return;
- }
- CodecDriver codec = m_Codecs.dequeue();
- 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 QVariant& context, const QByteArray& data)
- {
- qInfo() << "Got reply:" << QString::fromUtf8(data);
- QDomDocument dom;
- if (!dom.setContent(data))
- {
- qCritical() << "XML parsing error.";
- return false;
- }
- QDomNodeList list = dom.elementsByTagName("MediaContainer");
- if (list.count() != 1)
- {
- qCritical() << "MediaContainer XML element not found.";
- return false;
- }
- list = dom.elementsByTagName("Codec");
- if (list.count() != 1)
- {
- qCritical() << "Codec XML element not found.";
- return false;
- }
- QDomNamedNodeMap attrs = list.at(0).attributes();
- QString url = attrs.namedItem("url").toAttr().value();
- if (!url.size())
- {
- qCritical() << "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()) {
- qCritical() << "Hash value in unexpected format or missing:" << hash;
- return false;
- }
- Downloader *downloader = new Downloader(context, url, getPlexHeaders(), this);
- connect(downloader, &Downloader::done, this, &CodecsFetcher::codecDownloadDone);
- return true;
- }
- ///////////////////////////////////////////////////////////////////////////////////////////////////
- void CodecsFetcher::codecInfoDownloadDone(QVariant userData, bool success, const QByteArray& data)
- {
- if (!success || !processCodecInfoReply(userData, data))
- {
- qCritical() << "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)
- {
- qCritical() << "could not open .zip file.";
- goto fail;
- }
- unz_global_info info;
- int unzerr;
- if ((unzerr = unzGetGlobalInfo(file, &info)) != UNZ_OK)
- {
- qCritical() << "unzGlobalInfo() failed with" << unzerr;
- goto fail;
- }
- if ((unzerr = unzGoToFirstFile(file)) != UNZ_OK)
- {
- qCritical() << "unzGoToFirstFile() failed with" << unzerr;
- goto fail;
- }
- for (ZPOS64_T n = 0; n < info.number_entry; n++)
- {
- if (n > 0 && (unzerr = unzGoToNextFile(file)) != UNZ_OK)
- {
- qCritical() << "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)
- {
- qCritical() << "unzGetCurrentFileInfo() failed with" << unzerr;
- goto fail;
- }
- if ((unzerr = unzOpenCurrentFile(file)) != UNZ_OK)
- {
- qCritical() << "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("."))
- {
- qCritical() << "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))
- {
- qCritical() << "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)
- {
- qCritical() << "error decompressing zip entry" << filename;
- goto fail;
- }
- if (out.write(buf, read) != read)
- {
- qCritical() << "error writing output file" << filename;
- goto fail;
- }
- }
- if (!out.commit())
- {
- qCritical() << "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)))
- {
- qCritical() << "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 QVariant& context, const QByteArray& data)
- {
- QByteArray hash = QCryptographicHash::hash(data, QCryptographicHash::Sha1);
- if (hash != m_currentHash)
- {
- qCritical() << "Checksum mismatch: got" << hash.toHex() << "expected" << m_currentHash.toHex();
- return;
- }
- if (context == QVariant("eae"))
- {
- QString dest = eaePrefixPath() + ".zip";
- qInfo() << "Storing EAE as" << dest;
- if (!Utils::safelyWriteFile(dest, data))
- {
- qCritical() << "Writing codec file failed.";
- return;
- }
- QDir dir(dest);
- dir.removeRecursively();
- if (!extractZip(dest, eaePrefixPath()))
- {
- qCritical() << "Could not extract zip.";
- dir.removeRecursively();
- return;
- }
- QFile::remove(dest);
- }
- else
- {
- CodecDriver codec = context.value<CodecDriver>();
- qInfo() << "Storing codec as" << codec.getPath();
- if (!Utils::safelyWriteFile(codec.getPath(), data))
- {
- qCritical() << "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)
- {
- qCritical() << "Codec could not be loaded after installing it.";
- return;
- }
- }
- }
- qInfo() << "Codec download and installation succeeded.";
- }
- ///////////////////////////////////////////////////////////////////////////////////////////////////
- void CodecsFetcher::codecDownloadDone(QVariant userData, bool success, const QByteArray& data)
- {
- qInfo() << "Codec request finished.";
- if (success)
- {
- processCodecDownloadDone(userData, data);
- }
- else
- {
- qCritical() << "Codec download HTTP request failed.";
- }
- startNext();
- }
- ///////////////////////////////////////////////////////////////////////////////////////////////////
- void CodecsFetcher::startEAE()
- {
- if (!g_eaeProcess)
- {
- g_eaeProcess = new QProcess();
- g_eaeProcess->setProcessChannelMode(QProcess::ForwardedChannels);
- connect(g_eaeProcess, &QProcess::stateChanged,
- [](QProcess::ProcessState s)
- {
- qInfo() << "EAE process state:" << s;
- }
- );
- connect(g_eaeProcess, &QProcess::errorOccurred,
- [](QProcess::ProcessError e)
- {
- qInfo() << "EAE process error:" << e;
- }
- );
- connect(g_eaeProcess, static_cast<void(QProcess::*)(int, QProcess::ExitStatus)>(&QProcess::finished),
- [](int exitCode, QProcess::ExitStatus exitStatus)
- {
- qInfo() << "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;
- qCritical() << "EAE died with exit code" << exitCode;
- }
- qInfo() << "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))
- {
- qCritical() << "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)
- {
- qInfo() << "HTTP request:" << url.toDisplayString();
- m_currentStartTime.start();
- 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());
- QNetworkReply *reply = m_WebCtrl.get(request);
- if (reply)
- {
- connect(reply, &QNetworkReply::downloadProgress, this, &Downloader::downloadProgress);
- }
- }
- ///////////////////////////////////////////////////////////////////////////////////////////////////
- void Downloader::downloadProgress(qint64 bytesReceived, qint64 bytesTotal)
- {
- if (bytesTotal > 0)
- {
- int progress = (int)(bytesReceived * 100 / bytesTotal);
- if (m_lastProgress < 0 || progress > m_lastProgress + 10)
- {
- m_lastProgress = progress;
- qInfo() << "HTTP request at" << progress << "% (" << bytesReceived << "/" << bytesTotal << ")";
- }
- }
- }
- ///////////////////////////////////////////////////////////////////////////////////////////////////
- void Downloader::networkFinished(QNetworkReply* pReply)
- {
- qInfo() << "HTTP finished after" << (m_currentStartTime.elapsed() + 500) / 1000
- << "seconds for a request of" << pReply->size() << "bytes.";
- if (pReply->error() == QNetworkReply::NoError)
- {
- emit done(m_userData, true, pReply->readAll());
- }
- else
- {
- qCritical() << "HTTP download error:" << pReply->errorString();
- emit done(m_userData, false, QByteArray());
- }
- pReply->deleteLater();
- m_WebCtrl.clearAccessCache(); // make sure the TCP connection is closed
- }
- ///////////////////////////////////////////////////////////////////////////////////////////////////
- static CodecDriver selectBestDecoder(const StreamInfo& stream)
- {
- QList<CodecDriver> codecs = Codecs::findCodecsByFormat(Codecs::getCachedCodecList(), 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 = 10;
- if (codec.format == "h264")
- {
- // Avoid using system video decoders for h264 profiles usually not supported.
- if (stream.profile != "" && stream.profile != "main" && stream.profile != "baseline" && stream.profile != "high")
- score = 1;
- }
- if (codec.driver == "h264_mf")
- {
- if (!stream.videoResolution.isEmpty())
- {
- QSize res = stream.videoResolution;
- if (res.width() > g_mediaFoundationH264MaxResolution.width() ||
- res.height() > g_mediaFoundationH264MaxResolution.height())
- score = 1;
- }
- }
- if (codec.driver == "aac_mf")
- {
- // Arbitrary but documented and enforced 6 channel limit by MS.
- if (stream.audioChannels > 6)
- score = 1;
- // Another arbitrary limit.
- if (stream.audioSampleRate > 0 && (stream.audioSampleRate < 8000 || stream.audioSampleRate > 48000))
- score = 1;
- }
- if (codec.getSystemCodecType() == "eae")
- score = HAVE_EAE ? 2 : -1;
- }
- else
- {
- // prefer codecs which do not have to be downloaded over others
- if (codec.present)
- score = 15;
- else
- score = 5;
- }
- if (score > bestScore && score >= 0)
- {
- best = codec;
- bestScore = score;
- }
- }
- return best;
- }
- ///////////////////////////////////////////////////////////////////////////////////////////////////
- QList<CodecDriver> Codecs::determineRequiredCodecs(const PlaybackInfo& info)
- {
- QList<CodecDriver> result;
- bool needAC3Encoder = false;
- qInfo() << "Using system audio decoders:" << useSystemAudioDecoders();
- qInfo() << "Using system video decoders:" << useSystemVideoDecoders();
- #if !defined(HAVE_CODEC_MANIFEST)
- qInfo() << "Not using on-demand codecs.";
- #endif
- for (auto stream : info.streams)
- {
- if (!stream.isVideo && !stream.isAudio)
- continue;
- if (!stream.codec.size())
- {
- qCritical() << "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
- {
- qCritical() << "no decoder for" << stream.codec;
- }
- }
- if (needAC3Encoder)
- {
- QList<CodecDriver> codecs = Codecs::findCodecsByFormat(Codecs::getCachedCodecList(), 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
- {
- qCritical() << "no AC3 encoder available";
- }
- }
- return result;
- }
- void Codecs::Uninit()
- {
- if (g_eaeProcess)
- {
- delete g_eaeProcess;
- g_eaeProcess = nullptr;
- }
- if (!g_eaeWatchFolder.isEmpty())
- {
- QDir dir(g_eaeWatchFolder);
- dir.removeRecursively();
- }
- }
|