Browse Source

Initial support for Bluetooth and Touch Bar controls on macOS

This uses a private API to keep the Touch Bar controls active in the foreground
Both iTunes and Safari do the same; Safari source here:
https://github.com/WebKit/webkit/blob/bde8ac5bddc49527396f3adb576d9a101a8e4828/Source/WebCore/platform/audio/mac/MediaSessionManagerMac.mm#L202
Rodger Combs 8 years ago
parent
commit
c02b6869b2

+ 2 - 1
CMakeModules/AppleConfiguration.cmake

@@ -4,8 +4,9 @@ find_library(IOKIT IOKit)
 find_library(COCOA Cocoa)
 find_Library(CARBON Carbon)
 find_library(SECURITY Security)
+find_library(MEDIAPLAYER MediaPlayer)
 
-set(OS_LIBS ${FOUNDATION} ${APPKIT} ${IOKIT} ${COCOA} ${SECURITY} ${CARBON} spmediakeytap hidremote plistparser letsmove)
+set(OS_LIBS ${FOUNDATION} ${APPKIT} ${IOKIT} ${COCOA} ${SECURITY} ${CARBON} ${MEDIAPLAYER} spmediakeytap hidremote plistparser letsmove)
 set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS}")
 set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -mmacosx-version-min=10.9 -fno-omit-frame-pointer")
 set(WARNINGS "-Wall")

+ 3 - 1
resources/inputmaps/apple-media-keys.json

@@ -3,7 +3,9 @@
   "idmatcher": "AppleMediaKeys.*",
   "mapping":
   {
-    "KEY_PLAY": "play_pause",
+    "KEY_PLAY": "play",
+    "KEY_PAUSE": "pause",
+    "KEY_PLAY_PAUSE": "play_pause",
     "KEY_NEXT": "step_forward",
     "KEY_PREV": "step_backward",
     "KEY_FAST": "step_forward",

+ 1 - 0
src/input/InputComponent.h

@@ -43,6 +43,7 @@ signals:
 #define INPUT_KEY_MENU      "KEY_MENU"
 #define INPUT_KEY_PLAY      "KEY_PLAY"
 #define INPUT_KEY_PAUSE     "KEY_PAUSE"
+#define INPUT_KEY_PLAY_PAUSE "KEY_PLAY_PAUSE"
 #define INPUT_KEY_STOP      "KEY_STOP"
 #define INPUT_KEY_DOWN      "KEY_DOWN"
 #define INPUT_KEY_BACK      "KEY_BACK"

+ 14 - 0
src/input/apple/InputAppleMediaKeys.h

@@ -6,6 +6,7 @@
 #define KONVERGO_INPUTAPPLEMEDIAKEYS_H
 
 #include "input/InputComponent.h"
+#include "player/PlayerComponent.h"
 
 class InputAppleMediaKeys : public InputBase
 {
@@ -17,6 +18,19 @@ public:
 
 private:
   void* m_delegate;
+  void handleStateChanged(PlayerComponent::State newState, PlayerComponent::State oldState);
+  void handlePositionUpdate(quint64 position);
+  void handleUpdateDuration(qint64 duration);
+
+  typedef void (*SetNowPlayingVisibilityFunc)(void* origin, int visibility);
+  typedef void* (*GetLocalOriginFunc)(void);
+  typedef void (*SetCanBeNowPlayingApplicationFunc)(int);
+  SetNowPlayingVisibilityFunc SetNowPlayingVisibility;
+  GetLocalOriginFunc GetLocalOrigin;
+  SetCanBeNowPlayingApplicationFunc SetCanBeNowPlayingApplication;
+
+  bool m_pendingUpdate;
+  quint64 m_currentTime;
 };
 
 #endif //KONVERGO_INPUTAPPLEMEDIAKEYS_H

+ 147 - 6
src/input/apple/InputAppleMediaKeys.mm

@@ -6,6 +6,10 @@
 #include "SPMediaKeyTap.h"
 #include "QsLog.h"
 
+#import <dlfcn.h>
+
+#import <MediaPlayer/MediaPlayer.h>
+
 @interface MediaKeysDelegate : NSObject
 {
   SPMediaKeyTap* keyTap;
@@ -21,11 +25,28 @@
   self = [super init];
   if (self) {
     input = input_;
-    keyTap = [[SPMediaKeyTap alloc] initWithDelegate:self];
-    if ([SPMediaKeyTap usesGlobalMediaKeyTap])
-      [keyTap startWatchingMediaKeys];
-    else
-      QLOG_WARN() << "Could not grab global media keys";
+    if (NSClassFromString(@"MPRemoteCommandCenter")) {
+      MPRemoteCommandCenter* center = [MPRemoteCommandCenter sharedCommandCenter];
+#define CONFIG_CMD(name) \
+  [center.name ## Command addTarget:self action:@selector(gotCommand:)]
+      CONFIG_CMD(play);
+      CONFIG_CMD(pause);
+      CONFIG_CMD(togglePlayPause);
+      CONFIG_CMD(stop);
+      CONFIG_CMD(nextTrack);
+      CONFIG_CMD(previousTrack);
+      CONFIG_CMD(seekForward);
+      CONFIG_CMD(seekBackward);
+      CONFIG_CMD(skipForward);
+      CONFIG_CMD(skipBackward);
+      [center.changePlaybackPositionCommand addTarget:self action:@selector(gotPlaybackPosition:)];
+    } else {
+      keyTap = [[SPMediaKeyTap alloc] initWithDelegate:self];
+      if ([SPMediaKeyTap usesGlobalMediaKeyTap])
+        [keyTap startWatchingMediaKeys];
+      else
+        QLOG_WARN() << "Could not grab global media keys";
+    }
   }
   return self;
 }
@@ -35,6 +56,38 @@
   [super dealloc];
 }
 
+-(MPRemoteCommandHandlerStatus)gotCommand:(MPRemoteCommandEvent *)event
+{
+  QString keyPressed;
+  MPRemoteCommand* command = [event command];
+
+#define CMD(name) [MPRemoteCommandCenter sharedCommandCenter].name ## Command
+  if (command == CMD(play)) {
+    keyPressed = INPUT_KEY_PLAY;
+  } else if (command == CMD(pause)) {
+    keyPressed = INPUT_KEY_PAUSE;
+  } else if (command == CMD(togglePlayPause)) {
+    keyPressed = INPUT_KEY_PLAY_PAUSE;
+  } else if (command == CMD(stop)) {
+    keyPressed = INPUT_KEY_STOP;
+  } else if (command == CMD(nextTrack)) {
+    keyPressed = INPUT_KEY_NEXT;
+  } else if (command == CMD(previousTrack)) {
+    keyPressed = INPUT_KEY_PREV;
+  } else {
+    return MPRemoteCommandHandlerStatusCommandFailed;
+  }
+
+  emit input->receivedInput("AppleMediaKeys", keyPressed, InputBase::KeyPressed);
+  return MPRemoteCommandHandlerStatusSuccess;
+}
+
+-(MPRemoteCommandHandlerStatus)gotPlaybackPosition:(MPChangePlaybackPositionCommandEvent *)event
+{
+  PlayerComponent::Get().seekTo(event.positionTime * 1000);
+  return MPRemoteCommandHandlerStatusSuccess;
+}
+
 -(void)mediaKeyTap:(SPMediaKeyTap *)keyTap receivedMediaKeyEvent:(NSEvent *)event
 {
   int keyCode = (([event data1] & 0xFFFF0000) >> 16);
@@ -45,7 +98,7 @@
 
   switch (keyCode) {
     case NX_KEYTYPE_PLAY:
-      keyPressed = INPUT_KEY_PLAY;
+      keyPressed = INPUT_KEY_PLAY_PAUSE;
       break;
     case NX_KEYTYPE_FAST:
       keyPressed = "KEY_FAST";
@@ -69,9 +122,97 @@
 
 @end
 
+// macOS private enum
+enum {
+    MRNowPlayingClientVisibilityUndefined = 0,
+    MRNowPlayingClientVisibilityAlwaysVisible,
+    MRNowPlayingClientVisibilityVisibleWhenBackgrounded,
+    MRNowPlayingClientVisibilityNeverVisible
+};
+
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 bool InputAppleMediaKeys::initInput()
 {
+  m_currentTime = 0;
+  m_pendingUpdate = false;
   m_delegate = [[MediaKeysDelegate alloc] initWithInput:this];
+  if (NSClassFromString(@"MPNowPlayingInfoCenter")) {
+    connect(&PlayerComponent::Get(), &PlayerComponent::stateChanged, this, &InputAppleMediaKeys::handleStateChanged);
+    connect(&PlayerComponent::Get(), &PlayerComponent::positionUpdate, this, &InputAppleMediaKeys::handlePositionUpdate);
+    connect(&PlayerComponent::Get(), &PlayerComponent::updateDuration, this, &InputAppleMediaKeys::handleUpdateDuration);
+    void* lib = dlopen("/System/Library/PrivateFrameworks/MediaRemote.framework/MediaRemote", RTLD_NOW);
+    if (lib) {
+#define LOAD_FUNC(name) \
+  name = (name ## Func)dlsym(lib, "MRMediaRemote" #name)
+      LOAD_FUNC(SetNowPlayingVisibility);
+      LOAD_FUNC(GetLocalOrigin);
+      LOAD_FUNC(SetCanBeNowPlayingApplication);
+      if (SetCanBeNowPlayingApplication)
+        SetCanBeNowPlayingApplication(1);
+    }
+  }
   return true;
 }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+static MPNowPlayingPlaybackState convertState(PlayerComponent::State newState)
+{
+  switch (newState) {
+    case PlayerComponent::State::finished:
+      return MPNowPlayingPlaybackStateStopped;
+    case PlayerComponent::State::canceled:
+    case PlayerComponent::State::error:
+      return MPNowPlayingPlaybackStateInterrupted;
+    case PlayerComponent::State::buffering:
+    case PlayerComponent::State::paused:
+      return MPNowPlayingPlaybackStatePaused;
+    case PlayerComponent::State::playing:
+      return MPNowPlayingPlaybackStatePlaying;
+    default:
+      return MPNowPlayingPlaybackStateUnknown;
+  }
+}
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+void InputAppleMediaKeys::handleStateChanged(PlayerComponent::State newState, PlayerComponent::State oldState)
+{
+  MPNowPlayingPlaybackState newMPState = convertState(newState);
+  MPNowPlayingInfoCenter *center = [MPNowPlayingInfoCenter defaultCenter];
+  NSMutableDictionary *playingInfo = [NSMutableDictionary dictionaryWithDictionary:center.nowPlayingInfo];
+  [playingInfo setObject:[NSNumber numberWithDouble:(double)m_currentTime / 1000] forKey:MPNowPlayingInfoPropertyElapsedPlaybackTime];
+  center.nowPlayingInfo = playingInfo;
+  [MPNowPlayingInfoCenter defaultCenter].playbackState = newMPState;
+  if (SetNowPlayingVisibility && GetLocalOrigin) {
+    if (newState == PlayerComponent::State::finished || newState == PlayerComponent::State::canceled || newState == PlayerComponent::State::error)
+      SetNowPlayingVisibility(GetLocalOrigin(), MRNowPlayingClientVisibilityNeverVisible);
+    else if (newState == PlayerComponent::State::paused || newState == PlayerComponent::State::playing || newState == PlayerComponent::State::buffering)
+      SetNowPlayingVisibility(GetLocalOrigin(), MRNowPlayingClientVisibilityAlwaysVisible);
+  }
+
+  m_pendingUpdate = true;
+}
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+void InputAppleMediaKeys::handlePositionUpdate(quint64 position)
+{
+  m_currentTime = position;
+
+  if (m_pendingUpdate) {
+    MPNowPlayingInfoCenter *center = [MPNowPlayingInfoCenter defaultCenter];
+    NSMutableDictionary *playingInfo = [NSMutableDictionary dictionaryWithDictionary:center.nowPlayingInfo];
+    [playingInfo setObject:[NSNumber numberWithDouble:(double)position / 1000] forKey:MPNowPlayingInfoPropertyElapsedPlaybackTime];
+    center.nowPlayingInfo = playingInfo;
+    [MPNowPlayingInfoCenter defaultCenter].playbackState = [MPNowPlayingInfoCenter defaultCenter].playbackState;
+    m_pendingUpdate = false;
+  }
+}
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+void InputAppleMediaKeys::handleUpdateDuration(qint64 duration)
+{
+  MPNowPlayingInfoCenter *center = [MPNowPlayingInfoCenter defaultCenter];
+  NSMutableDictionary *playingInfo = [NSMutableDictionary dictionaryWithDictionary:center.nowPlayingInfo];
+  [playingInfo setObject:[NSNumber numberWithDouble:(double)duration / 1000] forKey:MPMediaItemPropertyPlaybackDuration];
+  center.nowPlayingInfo = playingInfo;
+  m_pendingUpdate = true;
+}

+ 4 - 0
src/player/PlayerComponent.cpp

@@ -463,6 +463,7 @@ void PlayerComponent::updatePlaybackState()
       emit error(m_playbackError);
       break;
     }
+    emit stateChanged(newState, m_state);
     m_state = newState;
   }
 
@@ -1139,6 +1140,9 @@ void PlayerComponent::updateVideoAspectSettings()
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 void PlayerComponent::updateVideoSettings()
 {
+  if (!m_mpv)
+    return;
+
   QVariant syncMode = SettingsComponent::Get().value(SETTINGS_SECTION_VIDEO, "sync_mode");
   mpv::qt::set_property(m_mpv, "video-sync", syncMode);
 

+ 1 - 0
src/player/PlayerComponent.h

@@ -164,6 +164,7 @@ Q_SIGNALS:
   void error(const QString& msg); // playback stopped due to external error
   // To be phased out. Raised on finished() and canceled().
   void stopped();                 // playback finished successfully, or was stopped with stop()
+  void stateChanged(State newState, State oldState); // all state changes
 
   // true if the video (or music) is actually playing
   // false if nothing is loaded, playback is paused, during seeking, or media is being loaded