Phase 4: Medien-Widget mit dreistufiger Kaskade

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.
This commit is contained in:
Guido Schmit
2026-08-10 21:02:13 +02:00
parent 03dd7b945e
commit 433cdcc3dd
53 changed files with 4888 additions and 2 deletions

View File

@@ -0,0 +1,126 @@
// Copyright (c) 2025 Jonas van den Berg
// This file is licensed under the BSD 3-Clause License.
#ifndef MEDIAREMOTEADAPTER_ADAPTER_H
#define MEDIAREMOTEADAPTER_ADAPTER_H
#import <Foundation/Foundation.h>
// Methods suffixed with "_env" read its parameters from the environment.
// Parameters must have the format:
// MEDIAREMOTEADAPTER_<FUNC_NAME>_<PARAM_INDEX>_<PARAM_NAME>
// Example: MEDIAREMOTEADAPTER_adapter_send_0_command
extern NSString *kMRAProcessIdentifier;
extern NSString *kMRABundleIdentifier;
extern NSString *kMRAParentApplicationBundleIdentifier;
extern NSString *kMRAPlaying;
extern NSString *kMRADurationMicros;
extern NSString *kMRAElapsedTimeMicros;
extern NSString *kMRATimestampEpochMicros;
extern NSString *kMRAElapsedTimeNow;
extern NSString *kMRAElapsedTimeNowMicros;
extern NSString *kMRATitle;
extern NSString *kMRAArtist;
extern NSString *kMRAAlbum;
extern NSString *kMRADuration;
extern NSString *kMRAElapsedTime;
extern NSString *kMRATimestamp;
extern NSString *kMRAArtworkMimeType;
extern NSString *kMRAArtworkData;
extern NSString *kMRAChapterNumber;
extern NSString *kMRAComposer;
extern NSString *kMRAGenre;
extern NSString *kMRAIsAdvertisement;
extern NSString *kMRAIsBanned;
extern NSString *kMRAIsInWishList;
extern NSString *kMRAIsLiked;
extern NSString *kMRAIsMusicApp;
extern NSString *kMRAPlaybackRate;
extern NSString *kMRAProhibitsSkip;
extern NSString *kMRAQueueIndex;
extern NSString *kMRARadioStationIdentifier;
extern NSString *kMRARepeatMode;
extern NSString *kMRAShuffleMode;
extern NSString *kMRAStartTime;
extern NSString *kMRASupportsFastForward15Seconds;
extern NSString *kMRASupportsIsBanned;
extern NSString *kMRASupportsIsLiked;
extern NSString *kMRASupportsRewind15Seconds;
extern NSString *kMRATotalChapterCount;
extern NSString *kMRATotalDiscCount;
extern NSString *kMRATotalQueueCount;
extern NSString *kMRATotalTrackCount;
extern NSString *kMRATrackNumber;
extern NSString *kMRAUniqueIdentifier;
extern NSString *kMRAContentItemIdentifier;
extern NSString *kMRARadioStationHash;
extern NSString *kMRAMediaType;
// Prints the current MediaRemote now playing information to stdout.
// Data is encoded as a JSON dictionary or "null" when there is no information.
extern void adapter_get();
extern void adapter_get_env();
// Streams MediaRemote now playing updates to stdout.
// Each update is printed on a separate lined, encoded as a JSON dictionary.
// Exits when the process receives a SIGTERM signal.
extern void adapter_stream();
extern void adapter_stream_env();
typedef enum {
kMRAPlay = 0,
kMRAPause = 1,
kMRATogglePlayPause = 2,
kMRAStop = 3,
kMRANextTrack = 4,
kMRAPreviousTrack = 5,
kMRAToggleShuffle = 6,
kMRAToggleRepeat = 7,
kMRAStartForwardSeek = 8,
kMRAEndForwardSeek = 9,
kMRAStartBackwardSeek = 10,
kMRAEndBackwardSeek = 11,
kMRAGoBackFifteenSeconds = 12,
kMRASkipFifteenSeconds = 13,
} MRACommand;
// Sends the given MediaRemote command to the current now playing application.
extern void adapter_send(MRACommand command);
extern void adapter_send_env();
// Seeks the timeline of the nowplaying application to the given position.
// The position must be given in microseconds.
extern void adapter_seek(long position);
extern void adapter_seek_env();
typedef enum {
kMRAShuffleDisabled = 1,
kMRAShuffleAlbums = 2,
kMRAShuffleTracks = 3,
} MRAShuffleMode;
extern void adapter_shuffle(MRAShuffleMode mode);
extern void adapter_shuffle_env();
typedef enum {
kMRARepeatDisabled = 1,
kMRARepeatTrack = 2,
kMRARepeatPlaylist = 3,
} MRARepeatMode;
extern void adapter_repeat(MRARepeatMode mode);
extern void adapter_repeat_env();
extern void adapter_speed(int speed);
extern void adapter_speed_env();
// Tests whether the process is entitled to use the MediaRemote framework.
// Exits with exit code 0, if it is. Any other exit code means it is not.
extern void adapter_test();
#endif // MEDIAREMOTEADAPTER_ADAPTER_H