diff --git a/main.cpp b/main.cpp index d571c4eb..385c1688 100644 --- a/main.cpp +++ b/main.cpp @@ -24,6 +24,7 @@ typedef QApplication Application; #include "mpv.h" #include "screensaver.h" #include "qclipboardproxy.h" +#include "mediacontrols.h" #else #include @@ -101,6 +102,7 @@ int main(int argc, char **argv) qmlRegisterType("com.stremio.screensaver", 1, 0, "ScreenSaver"); qmlRegisterType("com.stremio.libmpv", 1, 0, "MpvObject"); qmlRegisterType("com.stremio.clipboard", 1, 0, "Clipboard"); + qmlRegisterType("com.stremio.mediacontrols", 1, 0, "MediaControls"); InitializeParameters(engine, app); diff --git a/main.qml b/main.qml index 6199cd19..adcb425b 100644 --- a/main.qml +++ b/main.qml @@ -8,6 +8,7 @@ import com.stremio.process 1.0 import com.stremio.screensaver 1.0 import com.stremio.libmpv 1.0 import com.stremio.clipboard 1.0 +import com.stremio.mediacontrols 1.0 import QtQml 2.2 import "autoupdater.js" as Autoupdater @@ -76,6 +77,7 @@ ApplicationWindow { mpv.setProperty(args[0], args[1]); if (args[0] === "pause") { shouldDisableScreensaver(!args[1]); + syncMediaControls(); } } if (ev === "mpv-observe-prop") mpv.observeProperty(args) @@ -145,7 +147,36 @@ ApplicationWindow { } function isPlayerPlaying() { - return root.visible && typeof(mpv.getProperty("path"))==="string" && !mpv.getProperty("pause") + return root.visible && isMediaPlaying() + } + + function hasLoadedMedia() { + var path = mpv.getProperty("path") + return typeof(path) === "string" && path.length > 0 && !mpv.getProperty("idle-active") + } + + function isMediaPlaying() { + return hasLoadedMedia() && !mpv.getProperty("pause") + } + + function syncMediaControls() { + mediaControls.active = hasLoadedMedia() + mediaControls.playing = isMediaPlaying() + } + + function setMediaPaused(paused) { + if (!hasLoadedMedia()) { + syncMediaControls() + return + } + + mpv.setProperty("pause", paused) + wakeupEvent() + syncMediaControls() + } + + function toggleMediaPlayPause() { + setMediaPaused(!mpv.getProperty("pause")) } // Received external message @@ -230,6 +261,26 @@ ApplicationWindow { id: clipboard } + MediaControls { + id: mediaControls + } + + Connections { + target: mediaControls + + function onPlayRequested() { + setMediaPaused(false) + } + + function onPauseRequested() { + setMediaPaused(true) + } + + function onTogglePlayPauseRequested() { + toggleMediaPlayPause() + } + } + // // Streaming server // @@ -299,7 +350,13 @@ ApplicationWindow { MpvObject { id: mpv anchors.fill: parent - onMpvEvent: function(ev, args) { transport.event(ev, args) } + onShellPropertyChanged: function(name, value) { + syncMediaControls(); + } + onMpvEvent: function(ev, args) { + transport.event(ev, args) + syncMediaControls(); + } } // @@ -689,5 +746,7 @@ ApplicationWindow { // Check for updates console.info(" **** Completed. Loading Autoupdater ***") Autoupdater.initAutoUpdater(autoUpdater, root.autoUpdaterErr, autoUpdaterShortTimer, autoUpdaterLongTimer, autoUpdaterRestartTimer, webView.profile.httpUserAgent); + + syncMediaControls(); } } diff --git a/mediacontrols.cpp b/mediacontrols.cpp new file mode 100644 index 00000000..9e6aff04 --- /dev/null +++ b/mediacontrols.cpp @@ -0,0 +1,63 @@ +#include "mediacontrols.h" + +#ifndef Q_OS_MACOS + +MediaControls::MediaControls(QObject *parent) + : QObject(parent), m_active(false), m_playing(false), d(nullptr) +{ +} + +MediaControls::~MediaControls() = default; + +bool MediaControls::isActive() const +{ + return m_active; +} + +bool MediaControls::isPlaying() const +{ + return m_playing; +} + +void MediaControls::setActive(bool active) +{ + if (m_active == active) { + return; + } + + m_active = active; + Q_EMIT activeChanged(); +} + +void MediaControls::setPlaying(bool playing) +{ + if (m_playing == playing) { + return; + } + + m_playing = playing; + Q_EMIT playingChanged(); +} + +void MediaControls::requestPlay() +{ + if (m_active) { + Q_EMIT playRequested(); + } +} + +void MediaControls::requestPause() +{ + if (m_active) { + Q_EMIT pauseRequested(); + } +} + +void MediaControls::requestTogglePlayPause() +{ + if (m_active) { + Q_EMIT togglePlayPauseRequested(); + } +} + +#endif // Q_OS_MACOS diff --git a/mediacontrols.h b/mediacontrols.h new file mode 100644 index 00000000..573bf3c1 --- /dev/null +++ b/mediacontrols.h @@ -0,0 +1,41 @@ +#ifndef MEDIACONTROLS_H +#define MEDIACONTROLS_H + +#include + +class MediaControlsPrivate; + +class MediaControls : public QObject +{ + Q_OBJECT + Q_PROPERTY(bool active READ isActive WRITE setActive NOTIFY activeChanged) + Q_PROPERTY(bool playing READ isPlaying WRITE setPlaying NOTIFY playingChanged) + +public: + explicit MediaControls(QObject *parent = nullptr); + ~MediaControls() override; + + bool isActive() const; + bool isPlaying() const; + +public slots: + void setActive(bool active); + void setPlaying(bool playing); + void requestPlay(); + void requestPause(); + void requestTogglePlayPause(); + +signals: + void activeChanged(); + void playingChanged(); + void playRequested(); + void pauseRequested(); + void togglePlayPauseRequested(); + +private: + bool m_active; + bool m_playing; + MediaControlsPrivate *d; +}; + +#endif // MEDIACONTROLS_H diff --git a/mediacontrols_macos.mm b/mediacontrols_macos.mm new file mode 100644 index 00000000..85e3a79d --- /dev/null +++ b/mediacontrols_macos.mm @@ -0,0 +1,195 @@ +#include "mediacontrols.h" + +#ifdef Q_OS_MACOS + +#import + +#include + +@interface StremioMediaCommandTarget : NSObject +- (instancetype)initWithControls:(MediaControls *)controls; +- (MPRemoteCommandHandlerStatus)handlePlayCommand:(MPRemoteCommandEvent *)event; +- (MPRemoteCommandHandlerStatus)handlePauseCommand:(MPRemoteCommandEvent *)event; +- (MPRemoteCommandHandlerStatus)handleTogglePlayPauseCommand:(MPRemoteCommandEvent *)event; +@end + +@implementation StremioMediaCommandTarget { + MediaControls *m_controls; +} + +- (instancetype)initWithControls:(MediaControls *)controls +{ + self = [super init]; + if (self) { + m_controls = controls; + } + return self; +} + +- (MPRemoteCommandHandlerStatus)invokeControlSlot:(const char *)slot +{ + if (!m_controls || !m_controls->isActive()) { + return MPRemoteCommandHandlerStatusCommandFailed; + } + + QMetaObject::invokeMethod(m_controls, slot, Qt::QueuedConnection); + return MPRemoteCommandHandlerStatusSuccess; +} + +- (MPRemoteCommandHandlerStatus)handlePlayCommand:(MPRemoteCommandEvent *)event +{ + Q_UNUSED(event); + return [self invokeControlSlot:"requestPlay"]; +} + +- (MPRemoteCommandHandlerStatus)handlePauseCommand:(MPRemoteCommandEvent *)event +{ + Q_UNUSED(event); + return [self invokeControlSlot:"requestPause"]; +} + +- (MPRemoteCommandHandlerStatus)handleTogglePlayPauseCommand:(MPRemoteCommandEvent *)event +{ + Q_UNUSED(event); + return [self invokeControlSlot:"requestTogglePlayPause"]; +} + +@end + +class MediaControlsPrivate +{ +public: + explicit MediaControlsPrivate(MediaControls *controls) + : commandTarget(nil) + { + if (@available(macOS 10.12.2, *)) { + commandTarget = [[StremioMediaCommandTarget alloc] initWithControls:controls]; + + MPRemoteCommandCenter *commandCenter = [MPRemoteCommandCenter sharedCommandCenter]; + [commandCenter.playCommand addTarget:commandTarget action:@selector(handlePlayCommand:)]; + [commandCenter.pauseCommand addTarget:commandTarget action:@selector(handlePauseCommand:)]; + [commandCenter.togglePlayPauseCommand addTarget:commandTarget action:@selector(handleTogglePlayPauseCommand:)]; + + updateCommandState(false, false); + updateNowPlaying(false, false); + } + } + + ~MediaControlsPrivate() + { + if (@available(macOS 10.12.2, *)) { + MPRemoteCommandCenter *commandCenter = [MPRemoteCommandCenter sharedCommandCenter]; + [commandCenter.playCommand removeTarget:commandTarget action:@selector(handlePlayCommand:)]; + [commandCenter.pauseCommand removeTarget:commandTarget action:@selector(handlePauseCommand:)]; + [commandCenter.togglePlayPauseCommand removeTarget:commandTarget action:@selector(handleTogglePlayPauseCommand:)]; + updateNowPlaying(false, false); + } + + [commandTarget release]; + } + + void updateCommandState(bool active, bool playing) + { + if (@available(macOS 10.12.2, *)) { + MPRemoteCommandCenter *commandCenter = [MPRemoteCommandCenter sharedCommandCenter]; + commandCenter.playCommand.enabled = active && !playing; + commandCenter.pauseCommand.enabled = active && playing; + commandCenter.togglePlayPauseCommand.enabled = active; + } + } + + void updateNowPlaying(bool active, bool playing) + { + if (@available(macOS 10.12.2, *)) { + MPNowPlayingInfoCenter *nowPlayingCenter = [MPNowPlayingInfoCenter defaultCenter]; + + if (!active) { + nowPlayingCenter.playbackState = MPNowPlayingPlaybackStateStopped; + nowPlayingCenter.nowPlayingInfo = nil; + return; + } + + nowPlayingCenter.playbackState = playing + ? MPNowPlayingPlaybackStatePlaying + : MPNowPlayingPlaybackStatePaused; + nowPlayingCenter.nowPlayingInfo = @{ + MPMediaItemPropertyTitle: @"Stremio", + MPNowPlayingInfoPropertyPlaybackRate: @(playing ? 1.0 : 0.0) + }; + } + } + +private: + StremioMediaCommandTarget *commandTarget; +}; + +MediaControls::MediaControls(QObject *parent) + : QObject(parent), m_active(false), m_playing(false), d(new MediaControlsPrivate(this)) +{ +} + +MediaControls::~MediaControls() +{ + delete d; +} + +bool MediaControls::isActive() const +{ + return m_active; +} + +bool MediaControls::isPlaying() const +{ + return m_playing; +} + +void MediaControls::setActive(bool active) +{ + if (m_active == active) { + return; + } + + m_active = active; + if (d) { + d->updateCommandState(m_active, m_playing); + d->updateNowPlaying(m_active, m_playing); + } + Q_EMIT activeChanged(); +} + +void MediaControls::setPlaying(bool playing) +{ + if (m_playing == playing) { + return; + } + + m_playing = playing; + if (d) { + d->updateCommandState(m_active, m_playing); + d->updateNowPlaying(m_active, m_playing); + } + Q_EMIT playingChanged(); +} + +void MediaControls::requestPlay() +{ + if (m_active) { + Q_EMIT playRequested(); + } +} + +void MediaControls::requestPause() +{ + if (m_active) { + Q_EMIT pauseRequested(); + } +} + +void MediaControls::requestTogglePlayPause() +{ + if (m_active) { + Q_EMIT togglePlayPauseRequested(); + } +} + +#endif // Q_OS_MACOS diff --git a/mpv.cpp b/mpv.cpp index 06fc4c4f..553277f8 100644 --- a/mpv.cpp +++ b/mpv.cpp @@ -2,9 +2,11 @@ #include #include +#include #include #include +#include #include #include @@ -22,6 +24,8 @@ namespace { +const uint64_t ShellObserverId = 1; + void on_mpv_redraw(void *ctx) { MpvObject::on_update(ctx); @@ -175,6 +179,8 @@ void MpvObject::initialize_mpv() { // // Setup handling events from MPV mpv_set_wakeup_callback(mpv, wakeup, this); + observeShellProperties(); + foreach (const QString &name, observed_properties) { mpv_observe_property(mpv, 0, name.toStdString().c_str(), MPV_FORMAT_NODE); } @@ -210,6 +216,13 @@ void MpvObject::observeProperty(const QString& name) mpv_observe_property(mpv, 0, name.toStdString().c_str(), MPV_FORMAT_NODE); } +void MpvObject::observeShellProperties() +{ + mpv_observe_property(mpv, ShellObserverId, "path", MPV_FORMAT_NODE); + mpv_observe_property(mpv, ShellObserverId, "pause", MPV_FORMAT_NODE); + mpv_observe_property(mpv, ShellObserverId, "idle-active", MPV_FORMAT_NODE); +} + void MpvObject::wakeup(void *ctx) { QMetaObject::invokeMethod((MpvObject*)ctx, "on_mpv_events", Qt::QueuedConnection); @@ -232,8 +245,11 @@ void MpvObject::handle_mpv_event(mpv_event *event) { eventJson["id"] = qint64(event->reply_userdata); - if (event->error < 0) + if (event->error < 0) { + if (event->reply_userdata == ShellObserverId) + return; eventJson["error"] = QString(mpv_error_string(event->error)); + } switch (event->event_id) { // WARNING: we are not handling the following event types, it does not seem we need them: @@ -241,30 +257,40 @@ void MpvObject::handle_mpv_event(mpv_event *event) { // case MPV_EVENT_CLIENT_MESSAGE: case MPV_EVENT_PROPERTY_CHANGE: { mpv_event_property *prop = (mpv_event_property *) event->data; - eventJson["name"] = QString(prop->name); + const QString propName(prop->name); + QVariant propValue; + eventJson["name"] = propName; // NOTE: because we always observe as node, we can handle only that case; we are handling the others, to be safe :) switch (prop->format) { case MPV_FORMAT_NODE: + propValue = mpv::qt::node_to_variant((mpv_node *) prop->data); // Show the player only if there is a video stream - if(((mpv_node *)prop->data)->format == MPV_FORMAT_INT64 && eventJson["name"] == "vid") + if(((mpv_node *)prop->data)->format == MPV_FORMAT_INT64 && propName == "vid") this->setVisible(true); - eventJson["data"] = QJsonValue::fromVariant(mpv::qt::node_to_variant((mpv_node *) prop->data)); + eventJson["data"] = QJsonValue::fromVariant(propValue); break; case MPV_FORMAT_DOUBLE: - eventJson["data"] = *(double *)prop->data; + propValue = *(double *)prop->data; + eventJson["data"] = propValue.toDouble(); break; case MPV_FORMAT_FLAG: - eventJson["data"] = *(int *)prop->data; + propValue = *(int *)prop->data; + eventJson["data"] = propValue.toInt(); break; case MPV_FORMAT_STRING: - eventJson["data"] = QString(*(char **)prop->data); + propValue = QString(*(char **)prop->data); + eventJson["data"] = propValue.toString(); break; default: break; } - Q_EMIT mpvEvent("mpv-prop-change", eventJson); + if (event->reply_userdata == ShellObserverId) { + Q_EMIT shellPropertyChanged(propName, propValue); + } else { + Q_EMIT mpvEvent("mpv-prop-change", eventJson); + } break; } case MPV_EVENT_END_FILE: { diff --git a/mpv.h b/mpv.h index 05a5ee8e..efb72db6 100644 --- a/mpv.h +++ b/mpv.h @@ -35,7 +35,7 @@ public slots: signals: void onUpdate(); void mpvEvent(const QString& ev, const QVariant& value); - + void shellPropertyChanged(const QString& name, const QVariant& value = QVariant()); private slots: void doUpdate(); void on_mpv_events(); @@ -44,6 +44,7 @@ private slots: static void wakeup(void *ctx); void handle_mpv_event(mpv_event *event); void initialize_mpv(); + void observeShellProperties(); QSet observed_properties; }; diff --git a/stremio.pro b/stremio.pro index 5a2f36a6..e2f76df8 100644 --- a/stremio.pro +++ b/stremio.pro @@ -19,10 +19,12 @@ DEFINES += QAPPLICATION_CLASS=QApplication mac { QMAKE_LFLAGS_SONAME = -Wl,-install_name,@executable_path/../Frameworks/ LIBS += -framework CoreFoundation + LIBS += -weak_framework MediaPlayer QMAKE_RPATHDIR += @executable_path/../Frameworks QMAKE_RPATHDIR += @executable_path/lib #LIBS += -L $$PWD/deps/libmpv/mac/lib -lmpv LIBS += -L${MPV_BIN_PATH}/lib -lmpv -lc + OBJECTIVE_SOURCES += mediacontrols_macos.mm } # pkg-config way of linking with mpv works perfectly on the mac distribution process, because macdeployqt will also ship all libraries @@ -67,6 +69,7 @@ SOURCES += main.cpp \ screensaver.cpp \ autoupdater.cpp \ systemtray.cpp \ + mediacontrols.cpp \ qclipboardproxy.cpp \ verifysig.c @@ -85,6 +88,7 @@ HEADERS += \ mainapplication.h \ autoupdater.h \ systemtray.h \ + mediacontrols.h \ qclipboardproxy.h \ verifysig.h \ publickey.h