mirror of
https://github.com/jellyfin/jellyfin-desktop.git
synced 2026-10-26 16:45:24 +00:00
1302 lines
36 KiB
C++
1302 lines
36 KiB
C++
#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";
|
|
#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()
|
|
{
|
|
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();
|
|
}
|
|
}
|