kdelibs/kutils/kmediaplayer/kmediaplayer.cpp
Ivailo Monev 817b7a4aab kdecore: rework KMimeType
more accurate probing then before, notably by probing the file content
from the file slave (only the name was checked before because the
filepath was not a full path in some cases and the file could not be
open)

Signed-off-by: Ivailo Monev <xakepa10@gmail.com>
2024-05-04 20:14:53 +03:00

696 lines
21 KiB
C++

/* This file is part of the KDE libraries
Copyright (C) 2016 Ivailo Monev <xakepa10@gmail.com>
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Library General Public
License version 2, as published by the Free Software Foundation.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Library General Public License for more details.
You should have received a copy of the GNU Library General Public License
along with this library; see the file COPYING.LIB. If not, write to
the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
Boston, MA 02110-1301, USA.
*/
#include "config.h"
#include "kdebug.h"
#include "klocale.h"
#include "ksettings.h"
#include "kmediaplayer.h"
#include <QApplication>
#if defined(HAVE_MPV)
#include <locale.h>
#include <mpv/client.h>
#endif // HAVE_MPV
/*
Since sigals/slots cannot be virtual nor multiple QObject inheritance works (KAbstractPlayer
cannot inherit from QObject if it is to be used in a class that inherits QWidget), these
pre-processor definitions are used to share the code as much as possible making modification
easier.
*/
#define COMMON_STATE_LOAD \
d->m_settings->sync(); \
const QString globalaudio = d->m_settings->string("global/audiooutput", "auto"); \
const int globalvolume = d->m_settings->integer("global/volume", 90); \
const bool globalmute = d->m_settings->boolean("global/mute", false); \
setAudioOutput(d->m_settings->string(d->m_playerid + "/audiooutput", globalaudio)); \
setVolume(int(d->m_settings->integer(d->m_playerid + "/volume", globalvolume))); \
setMute(d->m_settings->boolean(d->m_playerid + "/mute", globalmute)); \
setOption("audio-client-name", d->m_playerid);
#define COMMON_STATE_SAVE \
if (d->m_handle && d->m_settings && d->m_settings->isWritable()) { \
d->m_settings->setString(d->m_playerid + "/audiooutput", audiooutput()); \
d->m_settings->setInteger(d->m_playerid + "/volume", int(volume())); \
d->m_settings->setBoolean(d->m_playerid + "/mute", mute()); \
d->m_settings->sync(); \
} else { \
kWarning() << "Could not save state"; \
}
#define COMMON_COMMAND_SENDER \
kDebug() << "Sending command" << command; \
if (d->m_handle) { \
const char* commanddata[command.size() + 1]; \
::memset(commanddata, 0, command.size() * sizeof(const char*)); \
for (int i = 0; i < command.size(); i++) { \
commanddata[i] = command.at(i).constData(); \
} \
commanddata[command.size()] = NULL; \
const int mpvresult = mpv_command(d->m_handle, commanddata); \
if (mpvresult < 0) { \
kWarning() << "Could not send command" << command << mpv_error_string(mpvresult); \
} \
}
#if defined(HAVE_MPV)
static QVariant mpvNodeToVariant(const mpv_node *mpvnode)
{
QVariant result;
switch (mpvnode->format) {
case MPV_FORMAT_NONE: {
break;
}
case MPV_FORMAT_FLAG: {
result = QVariant(bool(mpvnode->u.flag));
break;
}
case MPV_FORMAT_INT64: {
result = QVariant(qlonglong(mpvnode->u.int64));
break;
}
case MPV_FORMAT_DOUBLE: {
result = QVariant(double(mpvnode->u.double_));
break;
}
case MPV_FORMAT_STRING: {
result = QVariant(QString::fromUtf8(mpvnode->u.string));
break;
}
case MPV_FORMAT_NODE_ARRAY: {
QVariantList resultlist;
for (int i = 0; i < mpvnode->u.list->num; i++) {
resultlist.append(mpvNodeToVariant(&mpvnode->u.list->values[i]));
}
result = resultlist;
break;
}
case MPV_FORMAT_NODE_MAP: {
QVariantMap resultmap;
for (int i = 0; i < mpvnode->u.list->num; i++) {
resultmap.insert(
QString::fromUtf8(mpvnode->u.list->keys[i]),
mpvNodeToVariant(&mpvnode->u.list->values[i])
);
}
result = resultmap;
break;
}
default: {
kWarning() << "Unknown node format" << mpvnode->format;
break;
}
}
return result;
}
#endif // HAVE_MPV
// certain properties are not available when not playing for an example thus do not issue warning
// in case of error
#define COMMON_OPTION_GETTER \
kDebug() << "Getting option" << name; \
if (d->m_handle) { \
mpv_node mpvnode; \
const int mpvresult = mpv_get_property(d->m_handle, name.constData(), MPV_FORMAT_NODE, &mpvnode); \
if (mpvresult < 0) { \
kDebug() << "Could not get option" << name << mpv_error_string(mpvresult); \
return QVariant(); \
} \
QVariant result = mpvNodeToVariant(&mpvnode); \
mpv_free_node_contents(&mpvnode); \
return result; \
}
// NOTE: flags are integers
#define COMMON_OPTION_SETTER \
kDebug() << "Setting option" << name << value; \
if (d->m_handle) { \
int mpvresult = 0; \
switch (value.type()) { \
case QVariant::Bool: { \
int boolvalue = value.toBool(); \
mpvresult = mpv_set_property(d->m_handle, name.constData(), MPV_FORMAT_FLAG, &boolvalue); \
break; \
} \
case QVariant::Int: \
case QVariant::UInt: \
case QVariant::LongLong: \
case QVariant::ULongLong: { \
qint64 int64value = value.toLongLong(); \
mpvresult = mpv_set_property(d->m_handle, name.constData(), MPV_FORMAT_INT64, &int64value); \
break; \
} \
case QVariant::Float: \
case QVariant::Double: { \
double doublevalue = value.toDouble(); \
mpvresult = mpv_set_property(d->m_handle, name.constData(), MPV_FORMAT_DOUBLE, &doublevalue); \
break; \
} \
case QVariant::ByteArray: \
case QVariant::String: { \
QByteArray bytevalue = value.toByteArray(); \
char* bytevaluedata = bytevalue.data(); \
mpvresult = mpv_set_property(d->m_handle, name.constData(), MPV_FORMAT_STRING, &bytevaluedata); \
break; \
} \
default: { \
kWarning() << "Invalid option type" << value.type(); \
break; \
} \
} \
if (mpvresult < 0) { \
kWarning() << "Could not set option" << name << mpv_error_string(mpvresult); \
} \
}
#define COMMON_EVENT_HANDLER \
while (!d->m_stopprocessing) { \
mpv_event *event = mpv_wait_event(d->m_handle, 0); \
switch (event->event_id) { \
case MPV_EVENT_NONE: { \
return; \
} \
case MPV_EVENT_SHUTDOWN: { \
d->m_stopprocessing = true; \
break; \
} \
case MPV_EVENT_FILE_LOADED: { \
kDebug() << "Playback loaded"; \
emit loaded(); \
break; \
} \
case MPV_EVENT_END_FILE: { \
mpv_event_end_file *prop = static_cast<mpv_event_end_file *>(event->data); \
if (prop->reason == MPV_END_FILE_REASON_ERROR) { \
const QString mpverror = QString::fromLatin1(mpv_error_string(prop->error)); \
kWarning() << "Playback finished with error" << mpverror; \
emit finished(); \
emit error(mpverror); \
} else if (prop->reason == MPV_END_FILE_REASON_EOF \
|| prop->reason == MPV_END_FILE_REASON_STOP \
|| prop->reason == MPV_END_FILE_REASON_QUIT) { \
if (option("path").isNull()) { \
kDebug() << "Playback finished"; \
emit finished(); \
} \
} \
break; \
} \
case MPV_EVENT_PROPERTY_CHANGE: { \
mpv_event_property *prop = static_cast<mpv_event_property *>(event->data); \
kDebug() << "Property changed" << QString::fromLatin1(prop->name); \
if (strcmp(prop->name, "=time-pos") == 0 || strcmp(prop->name, "time-pos") == 0) { \
if (prop->format == MPV_FORMAT_NONE) { \
kDebug() << "The time-pos property is not valid"; \
} else if (prop->format == MPV_FORMAT_DOUBLE) { \
const double value = *(double *)prop->data; \
emit position(value); \
} else { \
kWarning() << "The time-pos format has changed" << prop->format; \
} \
} else if (strcmp(prop->name, "seekable") == 0) { \
if (prop->format == MPV_FORMAT_NONE) { \
kDebug() << "The seekable property is not valid"; \
} else if (prop->format == MPV_FORMAT_FLAG) { \
const bool value = *(bool *)prop->data; \
emit seekable(value); \
} else { \
kWarning() << "The seekable format has changed" << prop->format; \
} \
} else if (strcmp(prop->name, "partially-seekable") == 0) { \
if (option("seekable").toBool() == false) { \
if (prop->format == MPV_FORMAT_NONE) { \
kDebug() << "The partially-seekable property is not valid"; \
} else if (prop->format == MPV_FORMAT_FLAG) { \
const bool value = *(bool *)prop->data; \
emit seekable(value); \
} else { \
kWarning() << "The partially-seekable format has changed" << prop->format; \
} \
} \
} else if (strcmp(prop->name, "paused-for-cache") == 0) { \
if (prop->format == MPV_FORMAT_NONE) { \
kDebug() << "The paused-for-cache property is not valid"; \
} else if (prop->format == MPV_FORMAT_FLAG) { \
const bool value = *(bool *)prop->data; \
emit buffering(value); \
} else { \
kWarning() << "The paused-for-cache format has changed" << prop->format; \
} \
} else if (strcmp(prop->name, "core-idle") == 0) { \
if (prop->format == MPV_FORMAT_NONE) { \
kDebug() << "The core-idle property is not valid"; \
} else if (prop->format == MPV_FORMAT_FLAG) { \
const bool value = *(bool *)prop->data; \
if (!option("path").isNull()) { \
kDebug() << "Playback paused" << value; \
emit paused(value); \
} \
} else { \
kWarning() << "The core-idle format has changed" << prop->format; \
} \
} \
break; \
} \
case MPV_EVENT_LOG_MESSAGE: { \
mpv_event_log_message *msg = static_cast<mpv_event_log_message *>(event->data); \
kDebug() << msg->prefix << msg->text; \
break; \
} \
case MPV_EVENT_QUEUE_OVERFLOW: { \
kWarning() << "Event queue overflow"; \
break; \
} \
} \
}
// the video decoder may run into its own thread, make sure that does not cause trouble
#if defined(HAVE_MPV) && defined(Q_WS_X11)
static int kmp_x11_init_threads() {
QApplication::setAttribute(Qt::AA_X11InitThreads, true);
return 1;
};
Q_CONSTRUCTOR_FUNCTION(kmp_x11_init_threads)
#endif
class KAbstractPlayerPrivate
{
public:
KAbstractPlayerPrivate();
~KAbstractPlayerPrivate();
#if defined(HAVE_MPV)
mpv_handle *m_handle;
#endif
QString m_playerid;
KSettings *m_settings;
// the handle pointer is not NULL-ed once mpv_terminate_destroy() has been called, doing it
// manually is a race because _processHandleEvents() is called asynchronous
bool m_stopprocessing;
};
KAbstractPlayerPrivate::KAbstractPlayerPrivate()
: m_playerid(QApplication::applicationName()),
m_settings(new KSettings("kmediaplayer", KSettings::FullConfig)),
m_stopprocessing(false)
{
kDebug() << "Initializing player";
#if defined(HAVE_MPV)
setlocale(LC_NUMERIC, "C");
m_handle = mpv_create();
if (m_handle) {
const int rc = mpv_initialize(m_handle);
if (rc < 0) {
kWarning() << mpv_error_string(rc);
} else {
mpv_observe_property(m_handle, 0, "time-pos", MPV_FORMAT_DOUBLE);
mpv_observe_property(m_handle, 0, "paused-for-cache", MPV_FORMAT_FLAG);
mpv_observe_property(m_handle, 0, "core-idle", MPV_FORMAT_FLAG);
mpv_observe_property(m_handle, 0, "seekable", MPV_FORMAT_FLAG);
mpv_observe_property(m_handle, 0, "partially-seekable", MPV_FORMAT_FLAG);
mpv_request_log_messages(m_handle, "info");
}
} else {
kWarning() << "Context creation failed";
}
#endif
}
KAbstractPlayerPrivate::~KAbstractPlayerPrivate()
{
kDebug() << "Destroying player";
m_stopprocessing = true;
#if defined(HAVE_MPV)
mpv_terminate_destroy(m_handle);
#endif
if (m_settings) {
delete m_settings;
}
}
void KAbstractPlayer::load(const QString &path)
{
command(QList<QByteArray>() << "loadfile" << path.toLocal8Bit());
}
void KAbstractPlayer::load(const QByteArray &data)
{
// SECURITY: this is dangerous but some applications and libraries (like Okular) require it
QByteArray memorydata("memory://");
memorydata.append(data);
command(QList<QByteArray>() << "loadfile" << memorydata);
}
void KAbstractPlayer::play()
{
setOption("pause", false);
}
void KAbstractPlayer::pause()
{
setOption("pause", true);
}
void KAbstractPlayer::seek(const float position)
{
command(QList<QByteArray>() << "seek" << QByteArray::number(position) << "absolute");
}
void KAbstractPlayer::seek(const int position)
{
seek(float(position));
}
void KAbstractPlayer::stop()
{
command(QList<QByteArray>() << "stop");
}
QString KAbstractPlayer::path() const
{
return option("path").toString();
}
QString KAbstractPlayer::title() const
{
return option("media-title").toString();
}
float KAbstractPlayer::currentTime() const
{
return option("time-pos").toFloat();
}
float KAbstractPlayer::remainingTime() const
{
return option("time-remaining").toFloat();
}
float KAbstractPlayer::totalTime() const
{
return option("duration").toFloat();
}
float KAbstractPlayer::volume() const
{
return option("volume").toFloat();
}
bool KAbstractPlayer::mute() const
{
return option("mute").toBool();
}
QStringList KAbstractPlayer::protocols() const
{
static QStringList s_protocols;
if (s_protocols.isEmpty()) {
s_protocols = option("protocol-list").toStringList();
s_protocols.removeDuplicates();
qSort(s_protocols);
}
return s_protocols;
}
QString KAbstractPlayer::audiooutput() const
{
return option("audio-device").toString();
}
QStringList KAbstractPlayer::audiooutputs() const
{
const QVariantList value = option("audio-device-list").toList();
QStringList stringlist;
foreach (const QVariant &variant, value) {
QMapIterator<QString,QVariant> iter(variant.toMap());
while (iter.hasNext()) {
iter.next();
if (iter.key() == "name") {
stringlist.append(iter.value().toString());
}
}
}
return stringlist;
}
bool KAbstractPlayer::isPlaying() const
{
return !option("pause").toBool() && !option("path").isNull();
}
bool KAbstractPlayer::isBuffering() const
{
return option("paused-for-cache").toBool();
}
bool KAbstractPlayer::isSeekable() const
{
return option("seekable").toBool() || option("partially-seekable").toBool();
}
bool KAbstractPlayer::isProtocolSupported(const QString &protocol) const
{
foreach(const QString &proto, protocols()) {
if (protocol.startsWith(proto)) {
return true;
}
}
return false;
}
bool KAbstractPlayer::isPathSupported(const QString &path) const
{
const KMimeType::Ptr mime = KMimeType::findByUrl(KUrl(path));
if (mime && isMimeSupported(mime->name())) {
return true;
}
return isProtocolSupported(path);
}
void KAbstractPlayer::setVolume(const float volume)
{
setOption("volume", volume);
}
void KAbstractPlayer::setVolume(const int volume)
{
setOption("volume", volume);
}
void KAbstractPlayer::setMute(const bool mute)
{
setOption("mute", mute);
}
void KAbstractPlayer::setAudioOutput(const QString &output)
{
setOption("audio-device", output);
}
static void wakeup_audio(void *ctx)
{
KAudioPlayer *pctx = static_cast<KAudioPlayer*>(ctx);
QMetaObject::invokeMethod(pctx, "_processHandleEvents", Qt::QueuedConnection);
}
KAudioPlayer::KAudioPlayer(QObject *parent)
: QObject(parent),
d(new KAbstractPlayerPrivate())
{
#if defined(HAVE_MPV)
if (d->m_handle) {
mpv_set_wakeup_callback(d->m_handle, wakeup_audio, this);
// newer releases use vid, video is compat! the change is pre-2014 but yeah..
setOption("vid", "no");
setOption("video", "no");
COMMON_STATE_LOAD
}
#else
kWarning() << "KAudioPlayer is a stub";
#endif
}
KAudioPlayer::~KAudioPlayer()
{
#if defined(HAVE_MPV)
COMMON_STATE_SAVE
#endif
delete d;
}
void KAudioPlayer::command(const QList<QByteArray> &command) const
{
#if defined(HAVE_MPV)
COMMON_COMMAND_SENDER
#else
Q_UNUSED(command);
#endif
}
QVariant KAudioPlayer::option(const QByteArray &name) const
{
#if defined(HAVE_MPV)
COMMON_OPTION_GETTER
#else
Q_UNUSED(name);
#endif
return QVariant();
}
void KAudioPlayer::setOption(const QByteArray &name, const QVariant &value) const
{
#if defined(HAVE_MPV)
COMMON_OPTION_SETTER
#else
Q_UNUSED(name);
Q_UNUSED(value);
#endif
}
void KAudioPlayer::_processHandleEvents()
{
#if defined(HAVE_MPV)
COMMON_EVENT_HANDLER
#endif
}
void KAudioPlayer::setPlayerID(const QString &id)
{
d->m_playerid = id;
#if defined(HAVE_MPV)
COMMON_STATE_LOAD
#endif
}
bool KAudioPlayer::isMimeSupported(const QString &mime) const
{
#if defined(HAVE_MPV)
return mime.startsWith("audio/") || mime == QLatin1String("application/octet-stream");
#else
Q_UNUSED(mime);
return false;
#endif
}
/////
static void wakeup_media(void *ctx)
{
KMediaPlayer *pctx = static_cast<KMediaPlayer*>(ctx);
QMetaObject::invokeMethod(pctx, "_processHandleEvents", Qt::QueuedConnection);
}
KMediaPlayer::KMediaPlayer(QWidget *parent)
: QWidget(parent),
d(new KAbstractPlayerPrivate())
{
#if defined(HAVE_MPV)
if (d->m_handle) {
mpv_set_wakeup_callback(d->m_handle, wakeup_media, this);
// QVariant cannot be constructed from WId type and the actual WId type is ulong
QVariant wid;
if (parent) {
wid = QVariant::fromValue(qlonglong(quintptr(parent->winId())));
} else {
wid = QVariant::fromValue(qlonglong(quintptr(winId())));
}
if (wid.isValid()) {
setOption("wid", wid);
} else {
kWarning() << "Could not get widget ID";
}
// NOTE: the option for this changes like the wind
setOption("gpu-context", "x11");
COMMON_STATE_LOAD
}
#else
kWarning() << "KMediaPlayer is a stub";
#endif
}
KMediaPlayer::~KMediaPlayer()
{
#if defined(HAVE_MPV)
COMMON_STATE_SAVE
#endif
delete d;
}
void KMediaPlayer::command(const QList<QByteArray> &command) const
{
#if defined(HAVE_MPV)
COMMON_COMMAND_SENDER
#else
Q_UNUSED(command);
#endif
}
QVariant KMediaPlayer::option(const QByteArray &name) const
{
#if defined(HAVE_MPV)
COMMON_OPTION_GETTER
#else
Q_UNUSED(name);
#endif
return QVariant();
}
void KMediaPlayer::setOption(const QByteArray &name, const QVariant &value) const
{
#if defined(HAVE_MPV)
COMMON_OPTION_SETTER
#else
Q_UNUSED(name);
Q_UNUSED(value);
#endif
}
void KMediaPlayer::_processHandleEvents()
{
#if defined(HAVE_MPV)
COMMON_EVENT_HANDLER
#endif
}
void KMediaPlayer::setPlayerID(const QString &id)
{
d->m_playerid = id;
#if defined(HAVE_MPV)
COMMON_STATE_LOAD
#endif
}
bool KMediaPlayer::isMimeSupported(const QString &mime) const
{
#if defined(HAVE_MPV)
return mime.startsWith("audio/") || mime.startsWith("video/")
|| mime == QLatin1String("application/octet-stream");
#else
Q_UNUSED(mime);
return false;
#endif
}
#include "moc_kmediaplayer.cpp"