Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ if(WIN32)
set(MPV_LIBRARY_mpv ${CMAKE_CURRENT_SOURCE_DIR}/deps/libmpv/win32/mpv.lib)
endif()

if(UNIX AND NOT APPLE)
list(APPEND SOURCES mpris.cpp)
endif()

if(APPLE)
list(APPEND SOURCES images/stremio.icns)
set_source_files_properties(images/stremio.icns PROPERTIES MACOSX_PACKAGE_LOCATION "Resources")
Expand Down
18 changes: 18 additions & 0 deletions main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ typedef QApplication Application;
#include "screensaver.h"
#include "qclipboardproxy.h"

#ifdef Q_OS_LINUX
#include "mpris.h"
#endif

#else
#include <QGuiApplication>
#endif
Expand Down Expand Up @@ -106,6 +110,20 @@ int main(int argc, char **argv)

engine->load(QUrl(QStringLiteral("qrc:/main.qml")));

#ifdef Q_OS_LINUX
{
QObject *root = engine->rootObjects().value(0);
if (root) {
MpvObject *mpv = root->findChild<MpvObject*>();
if (mpv) {
mprisSetup(mpv);
} else {
qWarning("MPRIS: MpvObject not found in QML object tree");
}
}
}
#endif

#ifndef Q_OS_MACOS
QObject::connect( &app, &SingleApplication::receivedMessage, &app, &MainApp::processMessage );
#endif
Expand Down
191 changes: 191 additions & 0 deletions mpris.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
#include "mpris.h"

#include <QtQuick/QQuickItem>
#include <QtQuick/QQuickWindow>
#include <QJsonObject>

static const char *MPRIS_OBJECT_PATH = "/org/mpris/MediaPlayer2";
static const char *PLAYER_IFACE = "org.mpris.MediaPlayer2.Player";
static const char *SERVICE_NAME = "org.mpris.MediaPlayer2.stremio";

// ─── MprisRootAdaptor ─────────────────────────────────────────────────────────

MprisRootAdaptor::MprisRootAdaptor(QObject *parent)
: QDBusAbstractAdaptor(parent)
{}

void MprisRootAdaptor::Raise()
{
QQuickItem *item = qobject_cast<QQuickItem *>(parent());
if (!item) return;
QQuickWindow *win = item->window();
if (!win) return;
win->show();
win->raise();
win->requestActivate();
}

// ─── MprisPlayerAdaptor ───────────────────────────────────────────────────────

MprisPlayerAdaptor::MprisPlayerAdaptor(MpvObject *player)
: QDBusAbstractAdaptor(player), m_player(player)
{
player->observeProperty("idle-active");
player->observeProperty("pause");
player->observeProperty("media-title");
player->observeProperty("duration");
player->observeProperty("volume");

connect(player, &MpvObject::mpvEvent,
this, &MprisPlayerAdaptor::onMpvEvent);
}

QString MprisPlayerAdaptor::playbackStatus() const
{
if (m_idle) return "Stopped";
if (m_paused) return "Paused";
return "Playing";
}

QVariantMap MprisPlayerAdaptor::metadata() const
{
QVariantMap map;
map["mpris:trackid"] = QVariant::fromValue(
QDBusObjectPath(QString("/org/stremio/track/%1").arg(m_trackSeq))
);
if (!m_title.isEmpty())
map["xesam:title"] = m_title;
if (m_duration > 0.0)
map["mpris:length"] = qlonglong(m_duration * 1e6);
return map;
}

double MprisPlayerAdaptor::volume() const
{
return m_player->getProperty("volume").toDouble() / 100.0;
}

void MprisPlayerAdaptor::setVolume(double v)
{
m_player->setProperty("volume", v * 100.0);
emitPropertiesChanged({{"Volume", v}});
}

qlonglong MprisPlayerAdaptor::position() const
{
return qlonglong(m_player->getProperty("time-pos").toDouble() * 1e6);
}

void MprisPlayerAdaptor::PlayPause()
{
m_player->command(QVariantList() << "cycle" << "pause");
}

void MprisPlayerAdaptor::Play()
{
m_player->setProperty("pause", false);
}

void MprisPlayerAdaptor::Pause()
{
m_player->setProperty("pause", true);
}

void MprisPlayerAdaptor::Stop()
{
m_player->command(QVariantList() << "stop");
}

void MprisPlayerAdaptor::Seek(qlonglong offsetUs)
{
m_player->command(QVariantList() << "seek" << double(offsetUs) / 1e6 << "relative");
emit Seeked(position());
}

void MprisPlayerAdaptor::SetPosition(const QDBusObjectPath &trackId, qlonglong positionUs)
{
// Ignore stale requests targeting a previous track
QString expected = QString("/org/stremio/track/%1").arg(m_trackSeq);
if (trackId.path() != expected) return;
m_player->setProperty("time-pos", double(positionUs) / 1e6);
emit Seeked(positionUs);
}

void MprisPlayerAdaptor::onMpvEvent(const QString &event, const QVariant &value)
{
if (event == "mpv-prop-change") {
QJsonObject obj = value.toJsonObject();
QString name = obj["name"].toString();
QVariant data = obj["data"].toVariant();

if (name == "idle-active") {
bool idle = data.toBool();
if (idle != m_idle) {
m_idle = idle;
if (m_idle) {
m_title.clear();
m_duration = 0.0;
emitPropertiesChanged({
{"PlaybackStatus", playbackStatus()},
{"Metadata", QVariant::fromValue(metadata())}
});
} else {
emitPropertiesChanged({{"PlaybackStatus", playbackStatus()}});
}
}
Comment on lines +125 to +135
} else if (name == "pause") {
bool paused = data.toBool();
if (paused != m_paused) {
m_paused = paused;
emitPropertiesChanged({{"PlaybackStatus", playbackStatus()}});
}
} else if (name == "media-title") {
m_title = data.toString();
m_trackSeq++;
emitPropertiesChanged({{"Metadata", QVariant::fromValue(metadata())}});
} else if (name == "duration") {
m_duration = data.toDouble();
emitPropertiesChanged({{"Metadata", QVariant::fromValue(metadata())}});
} else if (name == "volume") {
emitPropertiesChanged({{"Volume", data.toDouble() / 100.0}});
}
} else if (event == "mpv-event-ended") {
m_idle = true;
m_title.clear();
m_duration = 0.0;
emitPropertiesChanged({
{"PlaybackStatus", QString("Stopped")},
{"Metadata", QVariant::fromValue(metadata())}
});
}
}

void MprisPlayerAdaptor::emitPropertiesChanged(const QVariantMap &changed)
{
auto msg = QDBusMessage::createSignal(
MPRIS_OBJECT_PATH,
"org.freedesktop.DBus.Properties",
"PropertiesChanged"
);
msg << QString(PLAYER_IFACE) << changed << QStringList();
QDBusConnection::sessionBus().send(msg);
}

// ─── Setup ────────────────────────────────────────────────────────────────────

void mprisSetup(MpvObject *player)
{
new MprisRootAdaptor(player);
new MprisPlayerAdaptor(player);

QDBusConnection bus = QDBusConnection::sessionBus();
if (!bus.registerObject(MPRIS_OBJECT_PATH, player)) {
qWarning("MPRIS: failed to register D-Bus object at %s", MPRIS_OBJECT_PATH);
return;
}
if (!bus.registerService(SERVICE_NAME)) {
qWarning("MPRIS: failed to register service %s", SERVICE_NAME);
return;
}
qDebug("MPRIS: registered successfully — media controllers can now control Stremio");
}
110 changes: 110 additions & 0 deletions mpris.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
#ifndef MPRIS_H
#define MPRIS_H

#include <QtDBus/QDBusAbstractAdaptor>
#include <QtDBus/QDBusConnection>
#include <QtDBus/QDBusMessage>
#include <QtDBus/QDBusObjectPath>
#include <QVariantMap>

#include "mpv.h"

// org.mpris.MediaPlayer2 — interface raiz (identidade do player)
class MprisRootAdaptor : public QDBusAbstractAdaptor {
Q_OBJECT
Q_CLASSINFO("D-Bus Interface", "org.mpris.MediaPlayer2")
Q_PROPERTY(bool CanQuit READ canQuit)
Q_PROPERTY(bool CanRaise READ canRaise)
Q_PROPERTY(bool HasTrackList READ hasTrackList)
Q_PROPERTY(QString Identity READ identity)
Q_PROPERTY(QString DesktopEntry READ desktopEntry)
Q_PROPERTY(QStringList SupportedUriSchemes READ supportedUriSchemes)
Q_PROPERTY(QStringList SupportedMimeTypes READ supportedMimeTypes)

public:
explicit MprisRootAdaptor(QObject *parent);

bool canQuit() const { return false; }
bool canRaise() const { return true; }
bool hasTrackList() const { return false; }
QString identity() const { return "Stremio"; }
QString desktopEntry() const { return "smartcode-stremio"; }
QStringList supportedUriSchemes() const { return {}; }
QStringList supportedMimeTypes() const { return {}; }

public slots:
void Raise();
void Quit() {}
};

// org.mpris.MediaPlayer2.Player — controle de playback
class MprisPlayerAdaptor : public QDBusAbstractAdaptor {
Q_OBJECT
Q_CLASSINFO("D-Bus Interface", "org.mpris.MediaPlayer2.Player")
Q_PROPERTY(QString PlaybackStatus READ playbackStatus)
Q_PROPERTY(QString LoopStatus READ loopStatus)
Q_PROPERTY(double Rate READ rate)
Q_PROPERTY(bool Shuffle READ shuffle)
Q_PROPERTY(QVariantMap Metadata READ metadata)
Q_PROPERTY(double Volume READ volume WRITE setVolume)
Q_PROPERTY(qlonglong Position READ position)
Q_PROPERTY(double MinimumRate READ minimumRate)
Q_PROPERTY(double MaximumRate READ maximumRate)
Q_PROPERTY(bool CanGoNext READ canGoNext)
Q_PROPERTY(bool CanGoPrevious READ canGoPrevious)
Q_PROPERTY(bool CanPlay READ canPlay)
Q_PROPERTY(bool CanPause READ canPause)
Q_PROPERTY(bool CanSeek READ canSeek)
Q_PROPERTY(bool CanControl READ canControl)

public:
explicit MprisPlayerAdaptor(MpvObject *player);

QString playbackStatus() const;
QString loopStatus() const { return "None"; }
double rate() const { return 1.0; }
bool shuffle() const { return false; }
QVariantMap metadata() const;
double volume() const;
void setVolume(double v);
qlonglong position() const;
double minimumRate() const { return 1.0; }
double maximumRate() const { return 1.0; }
bool canGoNext() const { return false; }
bool canGoPrevious() const { return false; }
bool canPlay() const { return true; }
bool canPause() const { return true; }
bool canSeek() const { return true; }
bool canControl() const { return true; }

public slots:
void PlayPause();
void Play();
void Pause();
void Stop();
void Next() {}
void Previous() {}
void Seek(qlonglong offsetUs);
void SetPosition(const QDBusObjectPath &trackId, qlonglong positionUs);
void OpenUri(const QString &uri) { Q_UNUSED(uri) }

signals:
void Seeked(qlonglong positionUs);

private slots:
void onMpvEvent(const QString &event, const QVariant &value);

private:
void emitPropertiesChanged(const QVariantMap &changed);

MpvObject *m_player;
bool m_idle = true;
bool m_paused = false;
QString m_title;
double m_duration = 0.0;
int m_trackSeq = 0;
};

void mprisSetup(MpvObject *player);

#endif // MPRIS_H
5 changes: 5 additions & 0 deletions stremio.pro
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,11 @@ SOURCES += main.cpp \
qclipboardproxy.cpp \
verifysig.c

unix:!mac {
SOURCES += mpris.cpp
HEADERS += mpris.h
}

RESOURCES += qml.qrc

# Additional import path used to resolve QML modules in Qt Creator's code model
Expand Down