mirror of
https://github.com/jellyfin/jellyfin-desktop.git
synced 2026-10-26 16:45:24 +00:00
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
This commit is contained in:
1 parent
13dc9c10de
commit
c02b6869b2
7 files changed
+172
-8
No files matched your search
@@ -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,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",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user