#include "CodecsComponent.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef HAVE_MINIZIP #include #include #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 g_systemVideoDecoderWhitelist = { // RPI "h264_mmal", "mpeg2_mmal", "mpeg4_mmal", "vc1_mmal", }; static QSet g_systemAudioDecoderWhitelist = { // OSX "eac3_at", // Windows "eac3_mf", }; static QSet g_systemAudioEncoderWhitelist = { }; static QSize g_mediaFoundationH264MaxResolution; static QString g_codecVersion; static QList 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& 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().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& Codecs::getCachedCodecList() { 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() + "." + 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 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 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& 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(); 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(&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 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 Codecs::determineRequiredCodecs(const PlaybackInfo& info) { QList 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 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(); } }