mirror of
https://github.com/jellyfin/jellyfin-desktop.git
synced 2026-10-27 00:56:07 +00:00
It is really late so I'm calling it here. I wanted to put this out for relevant people to get eyes on, in order to aid in getting the Windows build of v2.0.0 out. Apologies in advance for not following any contrib guidelines or there being messes. I'm leaving in the build scripts I used for now. The build scripts still contain a few hardcoded visual studio related values for now. At the moment when I run the app on a different machine to the dev VM I'm using the qt web view doesn't seem to be rendering. summary of where this is at: the whole thing builds works on my Windows 11 virtual machine short summary of what I did: A couple of qt6 updates specific to windows were addressed. Some build system issues were addressed did some minor refactoring to sieve out binary build dependencies from generated build files since its a lot easier when you can just delete the build folder to run a clean build
1316 lines
36 KiB
C++
1316 lines
36 KiB
C++
#include "CodecsComponent.h"
|
|
#include <clocale>
|
|
#ifdef Q_OS_WIN
|
|
#include <windows.h>
|
|
#endif
|
|
#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";
|
|
#elif defined(Q_OS_FREEBSD)
|
|
return sizeof(void *) > 4 ? "FreeBSD-amd64" : "FreeBSD-i386";
|
|
#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()
|
|
{
|
|
std::setlocale(LC_NUMERIC, "C");
|
|
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)
|
|
{
|
|
(void)version; (void)name; (void)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 cbUserData, bool success, const QByteArray& data)
|
|
{
|
|
if (!success || !processCodecInfoReply(cbUserData, data))
|
|
{
|
|
qCritical() << "Codec download failed.";
|
|
startNext();
|
|
}
|
|
}
|
|
|
|
#ifdef HAVE_MINIZIP
|
|
|
|
static voidpf unz_open_file(voidpf opaque, const char* filename, int mode)
|
|
{
|
|
(void)opaque; (void)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)
|
|
{
|
|
(void)opaque;
|
|
return fread(buf, 1, size, static_cast<FILE*>(stream));
|
|
}
|
|
|
|
static uLong unz_write_file(voidpf opaque, voidpf stream, const void* buf, uLong size)
|
|
{
|
|
(void)opaque; (void)stream; (void)buf; (void)size;
|
|
return 0;
|
|
}
|
|
|
|
static int unz_close_file(voidpf opaque, voidpf stream)
|
|
{
|
|
(void)opaque;
|
|
return fclose(static_cast<FILE*>(stream));
|
|
}
|
|
|
|
static int unz_error_file(voidpf opaque, voidpf stream)
|
|
{
|
|
(void)opaque;
|
|
return ferror(static_cast<FILE*>(stream));
|
|
}
|
|
|
|
static long unz_tell_file(voidpf opaque, voidpf stream)
|
|
{
|
|
(void)opaque;
|
|
return ftell(static_cast<FILE*>(stream));
|
|
}
|
|
|
|
long unz_seek_file(voidpf opaque, voidpf stream, uLong offset, int origin)
|
|
{
|
|
(void)opaque;
|
|
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(static_cast<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)
|
|
{
|
|
(void)zip; (void)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 cbUserData, bool success, const QByteArray& data)
|
|
{
|
|
qInfo() << "Codec request finished.";
|
|
if (success)
|
|
{
|
|
processCodecDownloadDone(cbUserData, 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 = static_cast<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();
|
|
}
|
|
}
|