Adapter für alles inklusive Browser, AppleScript für Spotify und Musik, Medientasten als letzte Reserve. Die Reihenfolge ist keine Vorliebe, sondern Absicherung: Apple hat die Now-Playing-Schnittstelle in macOS 15.4 schon einmal zugemacht. Fällt der Adapter aus, verliert das Widget seine Anzeige — aber nicht seine Funktion. Deshalb bleiben die Transporttasten auch dann bedienbar, wenn gar keine Metadaten da sind. Der Fortschritt zählt zwischen den Ereignissen selbst hoch. Spike B hatte gezeigt, dass elapsedTime nicht laufend nachgeliefert wird: ein Scrubber, der stur den letzten Wert anzeigt, steht still, während der Titel läuft. Gerechnet wird aus asOf und playbackRate, begrenzt auf die Titellänge — sonst zeigt die Anzeige nach dem Ende weiter steigende Zahlen. Teilmeldungen (diff = true) ergänzen den Stand, statt ihn zu ersetzen. Eine Meldung mit nur der Position würde sonst Titel, Interpret und Cover löschen. In AnyDecodable wird Bool vor Double geprüft. Andernfalls wird "playing": true zu 1 und die Wiedergabe gilt für immer als angehalten. Play/Pause schaltet sofort um, statt auf das nächste Ereignis zu warten. Ein Knopf, der eine halbe Sekunde später reagiert, fühlt sich kaputt an; das echte Ereignis korrigiert den Stand ohnehin. Der Adapter liegt als Quellcode unter Vendor/ (BSD-3, Stand in ONYX-VERSION.txt) und wird beim Bauen kompiliert, statt als fertige Binary eingecheckt zu werden. Er wird bewusst nicht gelinkt: geladen wird er von /usr/bin/perl, im eigenen Prozess greift Apples Entitlement-Prüfung. 124 Tests grün.
232 lines
6.6 KiB
Objective-C
232 lines
6.6 KiB
Objective-C
// Copyright (c) 2025 Alexander5015
|
|
// This file is licensed under the BSD 3-Clause License.
|
|
|
|
#include <MediaPlayer/MediaPlayer.h>
|
|
|
|
#import "NowPlayingTest.h"
|
|
|
|
NS_ASSUME_NONNULL_BEGIN
|
|
|
|
// Constants
|
|
static const NSTimeInterval kDefaultTrackDuration = 10.0 * 60.0; // 10 minutes
|
|
static const float kPlayingRate = 1.0f;
|
|
static const float kPausedRate = 0.0f;
|
|
|
|
@implementation NowPlayingInfoDelegate {
|
|
MPNowPlayingInfoCenter *_center;
|
|
}
|
|
|
|
- (instancetype)init {
|
|
if (self = [super init]) {
|
|
_center = [MPNowPlayingInfoCenter defaultCenter];
|
|
}
|
|
return self;
|
|
}
|
|
|
|
- (MPNowPlayingInfoCenter *)center {
|
|
return _center;
|
|
}
|
|
|
|
- (void)updateMetadataWithTitle:(NSString *)title
|
|
artist:(NSString *)artist
|
|
duration:(NSTimeInterval)duration {
|
|
NSMutableDictionary *nowPlayingInfo = [@{
|
|
MPMediaItemPropertyTitle : title ?: @"Unknown Title",
|
|
MPMediaItemPropertyAlbumTitle : @"Unknown Album",
|
|
MPMediaItemPropertyArtist : artist ?: @"Unknown Artist",
|
|
MPMediaItemPropertyPlaybackDuration : @(duration),
|
|
MPNowPlayingInfoPropertyElapsedPlaybackTime : @0,
|
|
MPNowPlayingInfoPropertyCurrentPlaybackDate : [NSDate date],
|
|
MPNowPlayingInfoPropertyPlaybackRate : @(kPlayingRate),
|
|
MPNowPlayingInfoPropertyMediaType : @(MPNowPlayingInfoMediaTypeAudio),
|
|
MPNowPlayingInfoPropertyServiceIdentifier :
|
|
@"com.vandenbe.MediaRemoteAdapter.TestClient",
|
|
} mutableCopy];
|
|
|
|
#if defined(__MAC_15_0) && __MAC_OS_X_VERSION_MAX_ALLOWED >= __MAC_15_0
|
|
if (@available(macOS 15, *)) {
|
|
nowPlayingInfo[MPNowPlayingInfoPropertyExcludeFromSuggestions] = @YES;
|
|
}
|
|
#endif
|
|
|
|
self.center.playbackState = MPNowPlayingPlaybackStatePlaying;
|
|
self.center.nowPlayingInfo = [nowPlayingInfo copy];
|
|
}
|
|
|
|
- (void)setPlaybackRate:(float)rate elapsedTime:(NSTimeInterval)time {
|
|
NSMutableDictionary *currentInfo = [self.center.nowPlayingInfo mutableCopy];
|
|
if (!currentInfo) {
|
|
return;
|
|
}
|
|
|
|
currentInfo[MPNowPlayingInfoPropertyPlaybackRate] = @(rate);
|
|
currentInfo[MPNowPlayingInfoPropertyElapsedPlaybackTime] = @(time);
|
|
self.center.nowPlayingInfo = [currentInfo copy];
|
|
}
|
|
|
|
@end
|
|
|
|
@implementation RemoteCommandCenterDelegate
|
|
|
|
- (instancetype)initWithListener:
|
|
(id<RemoteCommandCenterDelegateListener>)listener {
|
|
if (self = [super init]) {
|
|
_listener = listener;
|
|
[self setupRemoteCommandHandlers];
|
|
}
|
|
return self;
|
|
}
|
|
|
|
- (void)setupRemoteCommandHandlers {
|
|
MPRemoteCommandCenter *commandCenter =
|
|
[MPRemoteCommandCenter sharedCommandCenter];
|
|
|
|
[commandCenter.playCommand addTarget:self
|
|
action:@selector(handlePlayCommand:)];
|
|
[commandCenter.pauseCommand addTarget:self
|
|
action:@selector(handlePauseCommand:)];
|
|
}
|
|
|
|
- (MPRemoteCommandHandlerStatus)handlePlayCommand:
|
|
(MPRemoteCommandEvent *)event {
|
|
[self.listener didReceivePlayCommand];
|
|
return MPRemoteCommandHandlerStatusSuccess;
|
|
}
|
|
|
|
- (MPRemoteCommandHandlerStatus)handlePauseCommand:
|
|
(MPRemoteCommandEvent *)event {
|
|
[self.listener didReceivePauseCommand];
|
|
return MPRemoteCommandHandlerStatusSuccess;
|
|
}
|
|
|
|
@end
|
|
|
|
@interface NowPlayingPublishTest ()
|
|
@property(nonatomic, strong, readwrite)
|
|
NowPlayingInfoDelegate *nowPlayingDelegate;
|
|
@property(nonatomic, strong, readwrite)
|
|
RemoteCommandCenterDelegate *commandDelegate;
|
|
@property(nonatomic, assign, readwrite) BOOL isPlaying;
|
|
@property(nonatomic, assign, readwrite) NSTimeInterval elapsedTime;
|
|
@property(nonatomic, strong, nullable, readwrite) NSDate *playbackStartDate;
|
|
@property(nonatomic, assign, readwrite) NSTimeInterval totalDuration;
|
|
@end
|
|
|
|
@implementation NowPlayingPublishTest
|
|
|
|
- (instancetype)init {
|
|
if (self = [super init]) {
|
|
[self setupDelegates];
|
|
[self initializePlaybackState];
|
|
[self setupInitialTrack];
|
|
}
|
|
return self;
|
|
}
|
|
|
|
- (void)setupDelegates {
|
|
self.nowPlayingDelegate = [[NowPlayingInfoDelegate alloc] init];
|
|
self.commandDelegate =
|
|
[[RemoteCommandCenterDelegate alloc] initWithListener:self];
|
|
}
|
|
|
|
- (void)initializePlaybackState {
|
|
self.totalDuration = kDefaultTrackDuration;
|
|
self.elapsedTime = 0.0;
|
|
self.playbackStartDate = [NSDate date];
|
|
self.isPlaying = YES;
|
|
}
|
|
|
|
- (void)setupInitialTrack {
|
|
[self.nowPlayingDelegate updateMetadataWithTitle:@"Is It Broken Yet?"
|
|
artist:@"Alexander5015, ungive"
|
|
duration:self.totalDuration];
|
|
[self updateNowPlayingInfo];
|
|
}
|
|
|
|
- (void)didReceivePlayCommand {
|
|
if (self.isPlaying) {
|
|
return; // Already playing
|
|
}
|
|
|
|
[self startPlayback];
|
|
}
|
|
|
|
- (void)didReceivePauseCommand {
|
|
if (!self.isPlaying) {
|
|
return; // Already paused
|
|
}
|
|
|
|
[self pausePlayback];
|
|
}
|
|
|
|
- (void)startPlayback {
|
|
self.isPlaying = YES;
|
|
self.playbackStartDate = [NSDate date];
|
|
[self updateNowPlayingInfo];
|
|
}
|
|
|
|
- (void)pausePlayback {
|
|
self.isPlaying = NO;
|
|
[self updateElapsedTimeFromPlaybackStart];
|
|
self.playbackStartDate = nil;
|
|
[self updateNowPlayingInfo];
|
|
}
|
|
|
|
- (void)updateElapsedTimeFromPlaybackStart {
|
|
if (!self.playbackStartDate) {
|
|
return;
|
|
}
|
|
|
|
NSTimeInterval playedInterval =
|
|
[[NSDate date] timeIntervalSinceDate:self.playbackStartDate];
|
|
self.elapsedTime += playedInterval;
|
|
|
|
// Ensure elapsed time doesn't exceed total duration
|
|
if (self.elapsedTime > self.totalDuration) {
|
|
self.elapsedTime = self.totalDuration;
|
|
}
|
|
}
|
|
|
|
- (void)updateNowPlayingInfo {
|
|
NSTimeInterval currentElapsedTime = [self calculateCurrentElapsedTime];
|
|
float playbackRate = [self calculatePlaybackRate:currentElapsedTime];
|
|
|
|
[self.nowPlayingDelegate setPlaybackRate:playbackRate
|
|
elapsedTime:currentElapsedTime];
|
|
}
|
|
|
|
- (NSTimeInterval)calculateCurrentElapsedTime {
|
|
NSTimeInterval currentElapsed = self.elapsedTime;
|
|
|
|
if (self.isPlaying && self.playbackStartDate) {
|
|
NSTimeInterval intervalSinceStart =
|
|
[[NSDate date] timeIntervalSinceDate:self.playbackStartDate];
|
|
currentElapsed += intervalSinceStart;
|
|
|
|
// Cap at total duration
|
|
if (currentElapsed > self.totalDuration) {
|
|
currentElapsed = self.totalDuration;
|
|
}
|
|
}
|
|
|
|
return currentElapsed;
|
|
}
|
|
|
|
- (float)calculatePlaybackRate:(NSTimeInterval)currentElapsedTime {
|
|
if (!self.isPlaying) {
|
|
return kPausedRate;
|
|
}
|
|
|
|
// Check if track has ended
|
|
if (currentElapsedTime >= self.totalDuration) {
|
|
self.isPlaying = NO; // Auto-pause when track ends
|
|
return kPausedRate;
|
|
}
|
|
|
|
return kPlayingRate;
|
|
}
|
|
|
|
@end
|
|
|
|
NS_ASSUME_NONNULL_END
|