Add MPRIS support

This commit is contained in:
Andrew Rabert committed 2025-11-23 16:19:09 -05:00
1 parent 811ca84b24
commit 00dfb70e1b
21 files changed
+2340 -36

No files matched your search

+2
View File
@@ -140,6 +140,8 @@ document.addEventListener('keydown', (e) => {
(async () => {
console.log('Auto-connect: starting');
await window.apiPromise;
const savedServer = window.jmpInfo.settings.main.userWebClient;
console.log('Auto-connect: savedServer =', savedServer);
+323 -15
View File
@@ -19,38 +19,346 @@ class jmpInputPlugin {
this.type = 'input';
this.id = 'jmpInputPlugin';
this.durationCheckInterval = null;
this.positionUpdateInterval = null;
this.attachedPlayer = null;
(async () => {
const api = await window.apiPromise;
api.input.hostInput.connect((actions) => {
actions.forEach(action => {
if (remap.hasOwnProperty(action)) {
action = remap[action];
if (action === 'shuffle') {
playbackManager.setQueueShuffleMode('Shuffle');
} else if (action === 'sorted') {
playbackManager.setQueueShuffleMode('Sorted');
} else if (action === 'previous') {
const currentPlayer = playbackManager._currentPlayer;
if (currentPlayer && playbackManager.isPlayingAudio(currentPlayer)) {
const currentTime = playbackManager.currentTime(currentPlayer);
const currentIndex = playbackManager.getCurrentPlaylistIndex(currentPlayer);
if (currentTime >= 5 * 1000 || currentIndex <= 0) {
playbackManager.seekPercent(0, currentPlayer);
} else {
playbackManager.previousTrack(currentPlayer);
}
} else if (currentPlayer) {
playbackManager.previousTrack(currentPlayer);
}
} else {
if (remap.hasOwnProperty(action)) {
action = remap[action];
}
inputManager.handleCommand(action, {});
}
inputManager.handleCommand(action, {});
});
});
// Listen for fullscreen setting changes and trigger fullscreenchange event
let lastFullscreenState = window.jmpInfo.settings.main.fullscreen;
window.jmpInfo.settingsUpdate.push(function(section) {
if (section === 'main') {
const currentFullscreenState = window.jmpInfo.settings.main.fullscreen;
if (currentFullscreenState !== lastFullscreenState) {
lastFullscreenState = currentFullscreenState;
console.log('[jmpInputPlugin] Fullscreen setting changed, triggering fullscreenchange event');
const currentPlayer = playbackManager._currentPlayer;
if (currentPlayer) {
window.Events.trigger(currentPlayer, 'fullscreenchange');
const updateQueueState = function() {
try {
if (!api || !api.player) {
return;
}
const playlist = playbackManager._playQueueManager?.getPlaylist();
if (!playlist || !Array.isArray(playlist)) {
return;
}
const currentIndex = playbackManager._playQueueManager?.getCurrentPlaylistIndex();
if (currentIndex === undefined || currentIndex === null || currentIndex < 0) {
return;
}
const canNext = currentIndex < playlist.length - 1;
const state = playbackManager.getPlayerState();
const isMusic = state?.NowPlayingItem?.MediaType === 'Audio';
const canPrevious = isMusic ? true : currentIndex > 0;
api.player.notifyQueueChange(canNext, canPrevious);
} catch (e) {
console.error('PlayerMedia: Error in updateQueueState:', e);
}
};
let globalListenersAttached = false;
let lastReportedPosition = 0;
if (!globalListenersAttached) {
let lastFullscreenState = window.jmpInfo.settings.main.fullscreen;
window.jmpInfo.settingsUpdate.push(function(section) {
if (section === 'main') {
const currentFullscreenState = window.jmpInfo.settings.main.fullscreen;
if (currentFullscreenState !== lastFullscreenState) {
lastFullscreenState = currentFullscreenState;
const currentPlayer = playbackManager._currentPlayer;
if (currentPlayer) {
window.Events.trigger(currentPlayer, 'fullscreenchange');
}
}
}
});
if (window.api && window.api.player) {
window.api.player.playbackRateChanged.connect(function(rate) {
const currentPlayer = playbackManager._currentPlayer;
if (currentPlayer && currentPlayer._playRate !== undefined) {
currentPlayer._playRate = rate;
}
if (window.Events && currentPlayer) {
window.Events.trigger(currentPlayer, 'ratechange');
}
});
} else {
console.log('SystemMedia: Cannot attach playbackRateChanged listener - api:', !!window.api, 'player:', !!window.api?.player);
}
if (window.api && window.api.input) {
window.api.input.volumeChanged.connect(function(volume) {
const currentPlayer = playbackManager._currentPlayer;
if (currentPlayer && typeof currentPlayer.setVolume === 'function') {
currentPlayer.setVolume(volume);
}
});
window.api.input.rateChanged.connect(function(rate) {
const currentPlayer = playbackManager._currentPlayer;
if (currentPlayer && typeof currentPlayer.setPlaybackRate === 'function') {
currentPlayer.setPlaybackRate(rate);
}
});
// WORKAROUND: MPRIS spec has no "Buffering" playback state
// During seek buffering, we set Rate to 0.0 to stop MPRIS clients from
// auto-incrementing position. Once buffering completes, we restore Rate to 1.0.
window.api.input.positionSeek.connect(function(positionMs) {
const currentPlayer = playbackManager._currentPlayer;
if (currentPlayer) {
const duration = playbackManager.duration();
if (duration) {
const percent = (positionMs * 10000) / duration * 100;
playbackManager.seekPercent(percent, currentPlayer);
}
}
api.player.notifyPosition(Math.floor(positionMs));
lastReportedPosition = positionMs;
api.player.notifyRateChange(0.0);
});
}
window.Events.on(playbackManager, 'playlistitemremove', updateQueueState);
window.Events.on(playbackManager, 'playlistitemadd', updateQueueState);
window.Events.on(playbackManager, 'playlistitemchange', updateQueueState);
window.Events.on(playbackManager, 'playbackstop', function(e, playbackStopInfo) {
updateQueueState();
const isNavigating = !!(playbackStopInfo && playbackStopInfo.nextMediaType);
api.player.notifyPlaybackStop(isNavigating);
});
globalListenersAttached = true;
}
window.Events.on(playbackManager, 'playbackstart', (e, player) => {
if (!player) return;
const state = playbackManager.getPlayerState();
if (state && state.NowPlayingItem) {
let serverAddress = '';
if (window.ApiClient && typeof window.ApiClient.serverAddress === 'function') {
serverAddress = window.ApiClient.serverAddress();
}
api.player.notifyMetadata(state.NowPlayingItem, serverAddress || '');
const initialPos = playbackManager.currentTime();
if (initialPos !== undefined && initialPos !== null) {
api.player.notifyPosition(Math.floor(initialPos));
lastReportedPosition = initialPos;
}
if (player && typeof player.getVolume === 'function') {
const volume = player.getVolume();
api.player.notifyVolumeChange(volume / 100.0);
}
}
let lastDuration = 0;
const checkDuration = function() {
const duration = playbackManager.duration();
if (duration && duration !== lastDuration) {
lastDuration = duration;
const durationMs = Math.floor(duration / 10000);
api.player.notifyDurationChange(durationMs);
}
};
if (player !== this.attachedPlayer) {
if (this.attachedPlayer) {
window.Events.off(this.attachedPlayer, 'shufflequeuemodechange');
window.Events.off(this.attachedPlayer, 'repeatmodechange');
window.Events.off(this.attachedPlayer, 'playing');
window.Events.off(this.attachedPlayer, 'pause');
window.Events.off(this.attachedPlayer, 'playbackstop');
window.Events.off(this.attachedPlayer, 'volumechange');
window.Events.off(this.attachedPlayer, 'ratechange');
window.Events.off(this.attachedPlayer, 'timeupdate');
}
this.attachedPlayer = player;
window.Events.on(player, 'shufflequeuemodechange', () => {
const mode = playbackManager.getQueueShuffleMode();
const enabled = (mode === 'Shuffle');
api.player.notifyShuffleChange(enabled);
});
window.Events.on(player, 'repeatmodechange', () => {
const mode = playbackManager.getRepeatMode();
api.player.notifyRepeatChange(mode);
});
window.Events.on(player, 'playing', () => {
api.player.notifyPlaybackState('Playing');
updateQueueState();
api.player.notifyRateChange(1.0);
if (this.durationCheckInterval) {
clearInterval(this.durationCheckInterval);
}
checkDuration();
this.durationCheckInterval = setInterval(checkDuration, 1000);
const initialPos = playbackManager.currentTime();
if (initialPos !== undefined && initialPos !== null) {
api.player.notifyPosition(Math.floor(initialPos));
}
if (this.positionUpdateInterval) {
clearInterval(this.positionUpdateInterval);
}
this.positionUpdateInterval = setInterval(() => {
const positionMs = playbackManager.currentTime();
if (positionMs !== undefined && positionMs !== null) {
const positionDiff = Math.abs(positionMs - lastReportedPosition);
if (lastReportedPosition > 0 && positionDiff > 2000) {
api.player.notifyRateChange(0.0);
api.player.notifySeek(Math.floor(positionMs));
} else {
api.player.notifyPosition(Math.floor(positionMs));
}
lastReportedPosition = positionMs;
}
}, 500);
});
window.Events.on(player, 'pause', () => {
api.player.notifyPlaybackState('Paused');
api.player.notifyRateChange(0.0);
if (this.durationCheckInterval) {
clearInterval(this.durationCheckInterval);
this.durationCheckInterval = null;
}
const currentPos = playbackManager.currentTime();
if (currentPos !== undefined && currentPos !== null) {
lastReportedPosition = currentPos;
}
});
window.Events.on(player, 'playbackstop', () => {
if (this.durationCheckInterval) {
clearInterval(this.durationCheckInterval);
this.durationCheckInterval = null;
}
if (this.positionUpdateInterval) {
clearInterval(this.positionUpdateInterval);
this.positionUpdateInterval = null;
}
lastDuration = 0;
lastReportedPosition = 0;
});
window.Events.on(player, 'volumechange', () => {
if (player && typeof player.getVolume === 'function') {
const volume = player.getVolume();
api.player.notifyVolumeChange(volume / 100.0);
}
});
window.Events.on(player, 'ratechange', () => {
if (player && typeof player.getPlaybackRate === 'function') {
const rate = player.getPlaybackRate();
api.player.notifyRateChange(rate);
}
});
window.Events.on(player, 'timeupdate', () => {
const positionMs = playbackManager.currentTime();
if (positionMs !== undefined && positionMs !== null) {
const positionDiff = Math.abs(positionMs - lastReportedPosition);
if (lastReportedPosition > 0 && positionDiff > 2000) {
// Player-initiated seek - set rate to 0 during buffering
api.player.notifyRateChange(0.0);
api.player.notifySeek(Math.floor(positionMs));
} else if (positionDiff > 100) {
api.player.notifyPosition(Math.floor(positionMs));
}
lastReportedPosition = positionMs;
}
});
}
});
api.system.hello("jmpInputPlugin");
})();
}
destroy() {
if (this.durationCheckInterval) {
clearInterval(this.durationCheckInterval);
this.durationCheckInterval = null;
}
if (this.positionUpdateInterval) {
clearInterval(this.positionUpdateInterval);
this.positionUpdateInterval = null;
}
if (this.attachedPlayer) {
window.Events.off(this.attachedPlayer, 'shufflequeuemodechange');
window.Events.off(this.attachedPlayer, 'repeatmodechange');
window.Events.off(this.attachedPlayer, 'playing');
window.Events.off(this.attachedPlayer, 'pause');
window.Events.off(this.attachedPlayer, 'playbackstop');
window.Events.off(this.attachedPlayer, 'volumechange');
window.Events.off(this.attachedPlayer, 'timeupdate');
this.attachedPlayer = null;
}
if (window.playbackManager) {
window.Events.off(window.playbackManager, 'playlistitemremove');
window.Events.off(window.playbackManager, 'playlistitemadd');
window.Events.off(window.playbackManager, 'playlistitemchange');
window.Events.off(window.playbackManager, 'playbackstop');
}
console.log('SystemMedia: jmpInputPlugin destroyed and cleaned up');
}
}
window._jmpInputPlugin = jmpInputPlugin;
})();
})();
+5 -1
View File
@@ -262,6 +262,10 @@ class mpvAudioPlayer {
setPlaybackRate(value) {
this._playRate = value;
window.api.player.setPlaybackRate(value * 1000);
if (window.api && window.api.player) {
window.api.player.notifyRateChange(value);
}
}
getPlaybackRate() {
@@ -348,4 +352,4 @@ function getSupportedFeatures() {
}
window._mpvAudioPlayer = mpvAudioPlayer;
})();
})();
+19 -16
View File
@@ -612,6 +612,20 @@
return this._supportedFeatures.includes(feature);
}
isFullscreen() {
// Check native window fullscreen state
if (window.jmpInfo && window.jmpInfo.settings && window.jmpInfo.settings.main) {
return window.jmpInfo.settings.main.fullscreen === true;
}
return false;
}
toggleFullscreen() {
if (window.api && window.api.input) {
window.api.input.executeActions(['host:fullscreen']);
}
}
// Save this for when playback stops, because querying the time at that point might return 0
currentTime(val) {
if (val != null) {
@@ -690,6 +704,10 @@
let playSpeed = +value; //this comes as a string from player force int for now
this._playRate = playSpeed;
window.api.player.setPlaybackRate(playSpeed * 1000);
if (window.api && window.api.player) {
window.api.player.notifyRateChange(playSpeed);
}
}
getPlaybackRate() {
@@ -879,22 +897,7 @@
setAspectRatio(value) {
window.jmpInfo.settings.video.aspect = value;
}
isFullscreen() {
// Check native window fullscreen state from settings (universal source of truth)
if (window.jmpInfo && window.jmpInfo.settings && window.jmpInfo.settings.main) {
return window.jmpInfo.settings.main.fullscreen === true;
}
return false;
}
toggleFullscreen() {
// Toggle native window fullscreen via host command
if (window.api && window.api.input) {
window.api.input.executeActions(['host:fullscreen']);
}
}
}
}
/* eslint-enable indent */
window._mpvVideoPlayer = mpvVideoPlayer;
+20
View File
@@ -554,3 +554,23 @@ async function showSettingsModal() {
});
closeContainer.appendChild(close);
}
let lastFullscreenState = window.jmpInfo.settings.main.fullscreen;
window.jmpInfo.settingsUpdate.push(function(section) {
if (section === 'main') {
const currentFullscreenState = window.jmpInfo.settings.main.fullscreen;
if (currentFullscreenState !== lastFullscreenState) {
lastFullscreenState = currentFullscreenState;
if (window.api && window.api.player) {
window.api.player.notifyFullscreenChange(currentFullscreenState);
console.log('Player fullscreen notified');
}
if (window.Events && window.playbackManager && window.playbackManager._currentPlayer) {
window.Events.trigger(window.playbackManager._currentPlayer, 'fullscreenchange');
}
}
}
});
+1
View File
@@ -22,6 +22,7 @@ add_subdirectory(system)
add_subdirectory(settings)
add_subdirectory(power)
add_subdirectory(taskbar)
add_subdirectory(mpris)
get_property(ALL_SRCS GLOBAL PROPERTY SRCS_LIST)
set(MAIN_SRCS main.cpp)
+2
View File
@@ -13,6 +13,7 @@
#include "settings/SettingsComponent.h"
#include "taskbar/TaskbarComponent.h"
#include "ui/WindowManager.h"
#include "mpris/MprisComponent.h"
#if KONVERGO_OPENELEC
#include "system/openelec/OESystemComponent.h"
@@ -60,6 +61,7 @@ void ComponentManager::initialize()
registerComponent(&PowerComponent::Get());
registerComponent(&TaskbarComponent::Get());
registerComponent(&WindowManager::Get());
registerComponent(&MprisComponent::Get());
#if KONVERGO_OPENELEC
registerComponent(&OESystemComponent::Get());
+21
View File
@@ -258,6 +258,27 @@ void InputComponent::sendAction(const QString action)
emit hostInput(actionsToSend);
}
/////////////////////////////////////////////////////////////////////////////////////////
void InputComponent::setVolume(int volume)
{
qDebug() << "InputComponent: setVolume" << volume;
emit volumeChanged(volume);
}
/////////////////////////////////////////////////////////////////////////////////////////
void InputComponent::seekTo(qint64 position)
{
qDebug() << "InputComponent: seekTo" << position << "ms";
emit positionSeek(position);
}
/////////////////////////////////////////////////////////////////////////////////////////
void InputComponent::setRate(double rate)
{
qDebug() << "InputComponent: setRate" << rate;
emit rateChanged(rate);
}
/////////////////////////////////////////////////////////////////////////////////////////
void InputComponent::registerHostCommand(const QString& command, QObject* receiver, const char* slot)
{
+14
View File
@@ -108,6 +108,11 @@ public:
void cancelAutoRepeat();
void sendAction(const QString action);
// Volume, seek, and rate control
Q_INVOKABLE void setVolume(int volume);
Q_INVOKABLE void seekTo(qint64 position);
Q_INVOKABLE void setRate(double rate);
signals:
// Always emitted when any input arrives
void receivedInput();
@@ -116,6 +121,15 @@ signals:
// in the keymap, such as "host:fullscreen".
void hostInput(const QStringList& actions);
// Emitted when volume should be changed (0-100)
void volumeChanged(int volume);
// Emitted when position should be changed (milliseconds)
void positionSeek(qint64 position);
// Emitted when playback rate should be changed (1.0 = normal speed)
void rateChanged(double rate);
private Q_SLOTS:
void remapInput(const QString& source, const QString& keycode, InputBase::InputkeyState keyState);
+7 -1
View File
@@ -92,7 +92,8 @@ void ShowLicenseInfo()
/////////////////////////////////////////////////////////////////////////////////////////
QStringList g_qtFlags = {
"--enable-gpu-rasterization"
"--enable-gpu-rasterization",
"--disable-features=MediaSessionService"
};
/////////////////////////////////////////////////////////////////////////////////////////
@@ -238,6 +239,11 @@ int main(int argc, char *argv[])
webEngineDataDir = d.absolutePath() + "/QtWebEngine";
}
#ifdef Q_OS_LINUX
// Disable QtWebEngine's automatic MPRIS registration - we handle it ourselves
qputenv("QTWEBENGINE_CHROMIUM_FLAGS", "--disable-features=MediaSessionService,HardwareMediaKeyHandling");
#endif
QtWebEngineQuick::initialize();
QApplication app(newArgc, newArgv);
+3
View File
@@ -0,0 +1,3 @@
add_sources(MprisComponent.cpp MprisComponent.h)
add_sources(MprisRootAdaptor.cpp MprisRootAdaptor.h)
add_sources(MprisPlayerAdaptor.cpp MprisPlayerAdaptor.h)
File diff suppressed because it is too large. Load diff
+164
View File
@@ -0,0 +1,164 @@
#ifndef MPRISCOMPONENT_H
#define MPRISCOMPONENT_H
#include <QObject>
#include <QDBusConnection>
#include <QDBusObjectPath>
#include <QVariantMap>
#include <QTimer>
#include <memory>
#include "ComponentManager.h"
#include "player/PlayerComponent.h"
class QNetworkAccessManager;
class QNetworkReply;
#include "MprisRootAdaptor.h"
#include "MprisPlayerAdaptor.h"
class MprisComponent : public ComponentBase
{
Q_OBJECT
DEFINE_SINGLETON(MprisComponent);
public:
explicit MprisComponent(QObject* parent = nullptr);
~MprisComponent() override;
const char* componentName() override { return "mpris"; }
bool componentExport() override { return false; }
bool componentInitialize() override;
void componentPostInitialize() override;
bool canQuit() const { return false; }
bool canRaise() const { return true; }
bool canSetFullscreen() const { return true; }
bool fullscreen() const;
void setFullscreen(bool value);
bool hasTrackList() const { return false; }
QString identity() const { return "Jellyfin Media Player"; }
QString desktopEntry() const { return "jellyfinmediaplayer"; }
QStringList supportedUriSchemes() const { return QStringList(); }
QStringList supportedMimeTypes() const { return QStringList(); }
QString playbackStatus() const;
QString loopStatus() const;
double rate() const;
bool shuffle() const;
QVariantMap metadata() const { return m_metadata; }
double volume() const;
qint64 position() const;
double minimumRate() const { return 0.25; }
double maximumRate() const { return 2.0; }
bool canGoNext() const;
bool canGoPrevious() const;
bool canPlay() const;
bool canPause() const;
bool canSeek() const;
bool canControl() const;
// Notification slots - connected to PlayerComponent signals
void notifyShuffleChange(bool enabled);
void notifyRepeatChange(const QString& mode);
void notifyFullscreenChange(bool isFullscreen);
void notifyRateChange(double rate);
void notifyQueueChange(bool canNext, bool canPrevious);
void notifyPlaybackStop(bool isNavigating);
void notifyDurationChange(qint64 durationMs);
void notifyPlaybackState(const QString& state);
void notifyPosition(qint64 positionMs);
void notifySeek(qint64 positionMs);
void notifyMetadata(const QVariantMap& metadata, const QString& baseUrl);
void notifyVolumeChange(double volume);
public Q_SLOTS:
// MPRIS D-Bus method handlers - called from D-Bus clients
void Raise();
void Quit();
void Next();
void Previous();
void Pause();
void PlayPause();
void Stop();
void Play();
void Seek(qint64 offset);
void SetPosition(const QDBusObjectPath& trackId, qint64 position);
void OpenUri(const QString& uri);
void setVolume(double volume);
void setLoopStatus(const QString& value);
void setRate(double value);
void setShuffle(bool value);
private Q_SLOTS:
void onPlayerPlaying();
void onPlayerPaused();
void onPlayerStopped();
void onPlayerFinished();
void onPlayerStateChanged(PlayerComponent::State newState, PlayerComponent::State oldState);
void onPlayerPositionUpdate(quint64 position);
void onPlayerDurationChanged(qint64 duration);
void onPlayerMetaData(const QVariantMap& metadata, const QUrl& baseUrl);
void onPlayerVolumeChanged();
void onShuffleModeChanged(bool shuffleEnabled);
void onRepeatModeChanged(const QString& repeatMode);
void onAlbumArtDownloaded();
Q_SIGNALS:
void propertiesChanged(const QString& interface,
const QVariantMap& changedProperties,
const QStringList& invalidatedProperties);
private:
void updatePlaybackStatus(const QString& status);
void updateMetadata(const QVariantMap& jellyfinMeta, const QUrl& baseUrl = QUrl());
void emitPropertyChange(const QString& interface, const QString& property, const QVariant& value);
void emitMultiplePropertyChanges(const QString& interface, const QVariantMap& properties);
void connectPlayerSignals();
void disconnectPlayerSignals();
QString generateTrackId() const;
QString handleAlbumArt(const QString& artUrl);
void cleanupAlbumArt();
QString extractArtworkUrl(const QVariantMap& metadata, const QUrl& baseUrl);
void updateNavigationCapabilities();
bool m_enabled;
PlayerComponent* m_player;
std::unique_ptr<MprisRootAdaptor> m_rootAdaptor;
std::unique_ptr<MprisPlayerAdaptor> m_playerAdaptor;
QString m_playbackStatus;
QVariantMap m_metadata;
qint64 m_position;
qint64 m_duration;
QString m_currentTrackId;
QString m_currentMediaType;
QTimer* m_positionTimer;
bool m_canGoNext;
bool m_canGoPrevious;
bool m_shuffle;
QString m_loopStatus;
double m_rate;
double m_volume;
bool m_seekPending;
qint64 m_expectedPosition;
bool m_isNavigating;
QString m_currentArtPath;
QString m_albumArtDir;
QString m_pendingArtPath;
QString m_pendingArtUrl;
PlayerComponent::State m_playerState;
QNetworkAccessManager* m_albumArtManager;
QNetworkReply* m_pendingArtReply;
};
#endif // MPRISCOMPONENT_H
+150
View File
@@ -0,0 +1,150 @@
#include "MprisPlayerAdaptor.h"
#include "MprisComponent.h"
MprisPlayerAdaptor::MprisPlayerAdaptor(MprisComponent* parent)
: QDBusAbstractAdaptor(parent)
, m_component(parent)
{
}
QString MprisPlayerAdaptor::playbackStatus() const
{
return m_component->playbackStatus();
}
QString MprisPlayerAdaptor::loopStatus() const
{
return m_component->loopStatus();
}
void MprisPlayerAdaptor::setLoopStatus(const QString& value)
{
m_component->setLoopStatus(value);
}
double MprisPlayerAdaptor::rate() const
{
return m_component->rate();
}
void MprisPlayerAdaptor::setRate(double value)
{
m_component->setRate(value);
}
bool MprisPlayerAdaptor::shuffle() const
{
return m_component->shuffle();
}
void MprisPlayerAdaptor::setShuffle(bool value)
{
m_component->setShuffle(value);
}
QVariantMap MprisPlayerAdaptor::metadata() const
{
return m_component->metadata();
}
double MprisPlayerAdaptor::volume() const
{
return m_component->volume();
}
void MprisPlayerAdaptor::setVolume(double value)
{
m_component->setVolume(value);
}
qint64 MprisPlayerAdaptor::position() const
{
return m_component->position();
}
double MprisPlayerAdaptor::minimumRate() const
{
return m_component->minimumRate();
}
double MprisPlayerAdaptor::maximumRate() const
{
return m_component->maximumRate();
}
bool MprisPlayerAdaptor::canGoNext() const
{
return m_component->canGoNext();
}
bool MprisPlayerAdaptor::canGoPrevious() const
{
return m_component->canGoPrevious();
}
bool MprisPlayerAdaptor::canPlay() const
{
return m_component->canPlay();
}
bool MprisPlayerAdaptor::canPause() const
{
return m_component->canPause();
}
bool MprisPlayerAdaptor::canSeek() const
{
return m_component->canSeek();
}
bool MprisPlayerAdaptor::canControl() const
{
return m_component->canControl();
}
void MprisPlayerAdaptor::Next()
{
m_component->Next();
}
void MprisPlayerAdaptor::Previous()
{
m_component->Previous();
}
void MprisPlayerAdaptor::Pause()
{
m_component->Pause();
}
void MprisPlayerAdaptor::PlayPause()
{
m_component->PlayPause();
}
void MprisPlayerAdaptor::Stop()
{
m_component->Stop();
}
void MprisPlayerAdaptor::Play()
{
m_component->Play();
}
void MprisPlayerAdaptor::Seek(qint64 offset)
{
m_component->Seek(offset);
Q_EMIT Seeked(m_component->position());
}
void MprisPlayerAdaptor::SetPosition(const QDBusObjectPath& trackId, qint64 position)
{
m_component->SetPosition(trackId, position);
Q_EMIT Seeked(m_component->position());
}
void MprisPlayerAdaptor::OpenUri(const QString& uri)
{
m_component->OpenUri(uri);
}
+77
View File
@@ -0,0 +1,77 @@
#ifndef MPRISPLAYERADAPTOR_H
#define MPRISPLAYERADAPTOR_H
#include <QDBusAbstractAdaptor>
#include <QDBusObjectPath>
#include <QVariantMap>
class MprisComponent;
class MprisPlayerAdaptor : public QDBusAbstractAdaptor
{
Q_OBJECT
Q_CLASSINFO("D-Bus Interface", "org.mpris.MediaPlayer2.Player")
// Properties
Q_PROPERTY(QString PlaybackStatus READ playbackStatus)
Q_PROPERTY(QString LoopStatus READ loopStatus WRITE setLoopStatus)
Q_PROPERTY(double Rate READ rate WRITE setRate)
Q_PROPERTY(bool Shuffle READ shuffle WRITE setShuffle)
Q_PROPERTY(QVariantMap Metadata READ metadata)
Q_PROPERTY(double Volume READ volume WRITE setVolume)
Q_PROPERTY(qint64 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(MprisComponent* parent);
~MprisPlayerAdaptor() override = default;
// Properties
QString playbackStatus() const;
QString loopStatus() const;
void setLoopStatus(const QString& value);
double rate() const;
void setRate(double value);
bool shuffle() const;
void setShuffle(bool value);
QVariantMap metadata() const;
double volume() const;
void setVolume(double value);
qint64 position() const;
double minimumRate() const;
double maximumRate() const;
bool canGoNext() const;
bool canGoPrevious() const;
bool canPlay() const;
bool canPause() const;
bool canSeek() const;
bool canControl() const;
public Q_SLOTS:
// Methods
void Next();
void Previous();
void Pause();
void PlayPause();
void Stop();
void Play();
void Seek(qint64 offset);
void SetPosition(const QDBusObjectPath& trackId, qint64 position);
void OpenUri(const QString& uri);
Q_SIGNALS:
// Signals
void Seeked(qint64 position);
private:
MprisComponent* m_component;
};
#endif // MPRISPLAYERADAPTOR_H
+68
View File
@@ -0,0 +1,68 @@
#include "MprisRootAdaptor.h"
#include "MprisComponent.h"
MprisRootAdaptor::MprisRootAdaptor(MprisComponent* parent)
: QDBusAbstractAdaptor(parent)
, m_component(parent)
{
}
bool MprisRootAdaptor::canQuit() const
{
return m_component->canQuit();
}
bool MprisRootAdaptor::canRaise() const
{
return m_component->canRaise();
}
bool MprisRootAdaptor::canSetFullscreen() const
{
return m_component->canSetFullscreen();
}
bool MprisRootAdaptor::fullscreen() const
{
return m_component->fullscreen();
}
void MprisRootAdaptor::setFullscreen(bool value)
{
m_component->setFullscreen(value);
}
bool MprisRootAdaptor::hasTrackList() const
{
return m_component->hasTrackList();
}
QString MprisRootAdaptor::identity() const
{
return m_component->identity();
}
QString MprisRootAdaptor::desktopEntry() const
{
return m_component->desktopEntry();
}
QStringList MprisRootAdaptor::supportedUriSchemes() const
{
return m_component->supportedUriSchemes();
}
QStringList MprisRootAdaptor::supportedMimeTypes() const
{
return m_component->supportedMimeTypes();
}
void MprisRootAdaptor::Raise()
{
m_component->Raise();
}
void MprisRootAdaptor::Quit()
{
m_component->Quit();
}
+50
View File
@@ -0,0 +1,50 @@
#ifndef MPRISROOTADAPTOR_H
#define MPRISROOTADAPTOR_H
#include <QDBusAbstractAdaptor>
#include <QStringList>
class MprisComponent;
class MprisRootAdaptor : public QDBusAbstractAdaptor
{
Q_OBJECT
Q_CLASSINFO("D-Bus Interface", "org.mpris.MediaPlayer2")
// Properties
Q_PROPERTY(bool CanQuit READ canQuit)
Q_PROPERTY(bool CanRaise READ canRaise)
Q_PROPERTY(bool CanSetFullscreen READ canSetFullscreen)
Q_PROPERTY(bool Fullscreen READ fullscreen WRITE setFullscreen)
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(MprisComponent* parent);
~MprisRootAdaptor() override = default;
// Properties
bool canQuit() const;
bool canRaise() const;
bool canSetFullscreen() const;
bool fullscreen() const;
void setFullscreen(bool value);
bool hasTrackList() const;
QString identity() const;
QString desktopEntry() const;
QStringList supportedUriSchemes() const;
QStringList supportedMimeTypes() const;
public Q_SLOTS:
// Methods
void Raise();
void Quit();
private:
MprisComponent* m_component;
};
#endif // MPRISROOTADAPTOR_H
+94 -1
View File
@@ -738,6 +738,78 @@ void PlayerComponent::play()
m_mpv->command( args);
}
///////////////////////////////////////////////////////////////////////////////////////////////////
void PlayerComponent::notifyShuffleChange(bool enabled)
{
emit shuffleChanged(enabled);
}
///////////////////////////////////////////////////////////////////////////////////////////////////
void PlayerComponent::notifyRepeatChange(const QString& mode)
{
emit repeatChanged(mode);
}
///////////////////////////////////////////////////////////////////////////////////////////////////
void PlayerComponent::notifyFullscreenChange(bool isFullscreen)
{
emit fullscreenChanged(isFullscreen);
}
///////////////////////////////////////////////////////////////////////////////////////////////////
void PlayerComponent::notifyRateChange(double rate)
{
emit rateChanged(rate);
}
///////////////////////////////////////////////////////////////////////////////////////////////////
void PlayerComponent::notifyQueueChange(bool canNext, bool canPrevious)
{
emit queueChanged(canNext, canPrevious);
}
///////////////////////////////////////////////////////////////////////////////////////////////////
void PlayerComponent::notifyPlaybackStop(bool isNavigating)
{
emit playbackStopped(isNavigating);
}
///////////////////////////////////////////////////////////////////////////////////////////////////
void PlayerComponent::notifyDurationChange(qint64 durationMs)
{
emit durationChanged(durationMs);
}
///////////////////////////////////////////////////////////////////////////////////////////////////
void PlayerComponent::notifyPlaybackState(const QString& state)
{
emit playbackStateChanged(state);
}
///////////////////////////////////////////////////////////////////////////////////////////////////
void PlayerComponent::notifyPosition(qint64 positionMs)
{
emit positionChanged(positionMs);
}
///////////////////////////////////////////////////////////////////////////////////////////////////
void PlayerComponent::notifySeek(qint64 positionMs)
{
emit seekPerformed(positionMs);
}
///////////////////////////////////////////////////////////////////////////////////////////////////
void PlayerComponent::notifyMetadata(const QVariantMap& metadata, const QString& baseUrl)
{
emit metadataChanged(metadata, baseUrl);
}
///////////////////////////////////////////////////////////////////////////////////////////////////
void PlayerComponent::notifyVolumeChange(double volume)
{
emit volumeChanged(volume);
}
///////////////////////////////////////////////////////////////////////////////////////////////////
void PlayerComponent::stop()
{
@@ -1031,7 +1103,9 @@ void PlayerComponent::setPlaybackRate(int rate)
qWarning() << "PlayerComponent::setPlaybackRate: mpv not initialized yet";
return;
}
m_mpv->setProperty( "speed", rate / 1000.0);
double speed = rate / 1000.0;
m_mpv->setProperty( "speed", speed);
emit playbackRateChanged(speed);
}
/////////////////////////////////////////////////////////////////////////////////////////
@@ -1764,3 +1838,22 @@ QString PlayerComponent::videoInformation() const
return infoStr;
}
/////////////////////////////////////////////////////////////////////////////////////////
void PlayerComponent::setWebPlaylist(const QVariantList& playlist, const QString& currentItemId)
{
m_webPlaylist = playlist;
m_currentWebPlaylistItemId = currentItemId;
emit webPlaylistChanged(playlist, currentItemId);
}
/////////////////////////////////////////////////////////////////////////////////////////
QVariantList PlayerComponent::getWebPlaylist() const
{
return m_webPlaylist;
}
/////////////////////////////////////////////////////////////////////////////////////////
QString PlayerComponent::getCurrentWebPlaylistItemId() const
{
return m_currentWebPlaylistItemId;
}
+41 -2
View File
@@ -61,7 +61,21 @@ public:
Q_INVOKABLE virtual void pause();
Q_INVOKABLE virtual void play();
// OS media integration notifications (called from JavaScript)
Q_INVOKABLE void notifyShuffleChange(bool enabled);
Q_INVOKABLE void notifyRepeatChange(const QString& mode);
Q_INVOKABLE void notifyFullscreenChange(bool isFullscreen);
Q_INVOKABLE void notifyRateChange(double rate);
Q_INVOKABLE void notifyQueueChange(bool canNext, bool canPrevious);
Q_INVOKABLE void notifyPlaybackStop(bool isNavigating);
Q_INVOKABLE void notifyDurationChange(qint64 durationMs);
Q_INVOKABLE void notifyPlaybackState(const QString& state);
Q_INVOKABLE void notifyPosition(qint64 positionMs);
Q_INVOKABLE void notifySeek(qint64 positionMs);
Q_INVOKABLE void notifyMetadata(const QVariantMap& metadata, const QString& baseUrl);
Q_INVOKABLE void notifyVolumeChange(double volume);
// 0-100 volume 0=mute and 100=normal
// Ignored if no audio output active (e.g. when no file is playing).
Q_INVOKABLE virtual void setVolume(int volume);
@@ -123,6 +137,10 @@ public:
Q_INVOKABLE qint64 getPosition();
Q_INVOKABLE qint64 getDuration();
Q_INVOKABLE QVariantList getWebPlaylist() const;
Q_INVOKABLE QString getCurrentWebPlaylistItemId() const;
Q_INVOKABLE void setWebPlaylist(const QVariantList& playlist, const QString& currentItemId);
QRect videoRectangle() { return m_videoRectangle; }
void setMpvController(MpvController* controller) {
@@ -188,6 +206,7 @@ Q_SIGNALS:
void windowVisible(bool visible);
// emitted as soon as the duration of the current file is known
void updateDuration(qint64 milliseconds);
void playbackRateChanged(double rate);
// current position in ms should be triggered 2 times a second
// when position updates
@@ -198,7 +217,22 @@ Q_SIGNALS:
void onMpvEvents();
void onMetaData(const QVariantMap &meta, QUrl baseUrl);
void webPlaylistChanged(const QVariantList& playlist, const QString& currentItemId);
// OS media integration signals (for MPRIS, SMTC, etc.)
void shuffleChanged(bool enabled);
void repeatChanged(const QString& mode);
void fullscreenChanged(bool isFullscreen);
void rateChanged(double rate);
void queueChanged(bool canNext, bool canPrevious);
void playbackStopped(bool isNavigating);
void durationChanged(qint64 durationMs);
void playbackStateChanged(const QString& state);
void positionChanged(qint64 positionMs);
void seekPerformed(qint64 positionMs);
void metadataChanged(const QVariantMap& metadata, const QString& baseUrl);
void volumeChanged(double volume);
private:
// this is the function actually implemented in the backends. the variantmap contains
// a few known keys:
@@ -255,6 +289,11 @@ private:
QVariant m_currentSubtitleStream;
QVariant m_currentAudioStream;
QRect m_videoRectangle;
QVariantList m_webPlaylist;
QString m_currentWebPlaylistItemId;
QTimer* m_playlistTimer;
QVariantList m_queuedItems;
};
#endif // PLAYERCOMPONENT_H
+2
View File
@@ -254,6 +254,8 @@ void WindowManager::onVisibilityChanged(QWindow::Visibility visibility)
{
section->setValueNoSave("fullscreen", m_fullscreen);
}
emit fullScreenSwitched();
}
// Save geometry when exiting fullscreen
+3
View File
@@ -49,6 +49,9 @@ public:
public slots:
void toggleFullscreen();
signals:
void fullScreenSwitched();
private slots:
void onVisibilityChanged(QWindow::Visibility visibility);
void onScreenAdded(QScreen* screen);