How the Now Playing dylib Works in vorssaint‑utils: Reverse Engineering macOS MediaRemote
The Now Playing dylib is a separately signed dynamic library that uses dlopen and dlsym to asynchronously invoke private MediaRemote APIs, aggregating playback data into JSON to circumvent macOS 15.4's signature restrictions on private framework access.
The vorssaint‑utils repository solves a critical macOS 15.4 compatibility issue by implementing a standalone Now Playing dylib that bridges the gap between a Perl helper script and Apple's private MediaRemote framework. Because recent macOS versions restrict MediaRemote data to cryptographically signed binaries, the main application cannot link against the framework directly. Instead, the Now Playing dylib operates as an isolated, signed component that the host application dynamically loads at runtime to retrieve current media playback information.
Why macOS 15.4 Requires a Separate Dylib Approach
In macOS 15.4 and later, the private MediaRemote framework enforces strict code‑signing requirements that prevent standard applications from accessing now‑playing metadata. The framework validates that the calling binary is properly signed by Apple, which blocks direct linkage from the main vorssaint‑utils executable.
To bypass this restriction, the project compiles the Now Playing functionality as a discrete dynamic library (NowPlayingAdapter.dylib) that is signed independently during the build process. This separation allows the dylib to satisfy MediaRemote's security requirements while the main application remains responsible solely for consuming the resulting data.
Core Architecture of the Now Playing dylib
The implementation centers on the [NowPlayingAdapter.swift](https://github.com/vorssaint/vorssaint-utils/blob/main/Sources/NowPlayingAdapter/NowPlayingAdapter.swift) source file, which provides the low‑level interface to MediaRemote through dynamic symbol resolution and concurrent callback handling.
Dynamic Loading and Symbol Resolution
Rather than linking against MediaRemote at compile time, the dylib loads the private framework at runtime using standard POSIX dynamic loading APIs:
dlopen: Opens/System/Library/PrivateFrameworks/MediaRemote.framework/MediaRemotewithRTLD_LAZY(line 47).dlsym: Resolves four critical function pointers via a helper that wrapsdlsym:
Asynchronous Data Collection
The dylib employs Grand Central Dispatch to handle multiple concurrent callbacks from MediaRemote without blocking the calling thread:
- A dedicated
DispatchQueuelabeledcom.vorssaint.now‑playing‑adaptermanages execution context. - A
DispatchGroupcoordinates four parallel asynchronous requests for metadata, PID, bundle ID, and playback state. - Thread‑safe mutation of the results dictionary occurs through an
NSLockwrapped helper methodset(_:_:)(lines 56–61).
Each callback populates specific keys in the shared reply dictionary:
kMRMediaRemoteNowPlayingInfoTitle,Artist, andAlbum(lines 66–70)- Playback rate and track duration (lines 71–72)
- Base64‑encoded artwork data, capped at 12 MiB (lines 73–76)
- Process ID, display bundle ID, and playing Boolean (lines 80–100)
JSON Serialization and Output
Once all callbacks complete via group.wait() (lines 103–107), the dylib serializes the aggregated dictionary:
- Success: Encodes the metadata dictionary as compact JSON and writes it to
stdoutvia theemithelper (lines 38–42). - Failure: Emits a minimal error object containing
erroranddetailskeys.
This single‑line JSON protocol ensures atomic communication with the parent process, eliminating partial read scenarios in the consuming Perl script.
Inter‑Process Communication: Perl Bridge to Swift
The host application does not load the dylib directly; instead, it delegates to Resources/now‑playing.pl, a Perl script that acts as a thin wrapper:
use DynaLoader;
my $lib = DynaLoader::dl_load_file('NowPlayingAdapter.dylib');
my $sym = DynaLoader::dl_find_symbol($lib, 'vorssaint_now_playing_get');
my $func = DynaLoader::dl_install_xsub('vorssaint_now_playing_get', $sym);
my $json_line = $func->(); # Returns one JSON line
print $json_line;
The Perl interpreter loads the signed dylib via DynaLoader, invokes the C‑exported entry point vorssaint_now_playing_get, and forwards the JSON output to stdout. The main Swift application (RadialNowPlayingSupport.swift) captures this output, deserializes the JSON, and updates the user interface accordingly.
Build and Code Signing Pipeline
The separation requires careful build orchestration via build.sh:
- Compilation: The Swift source compiles into a dynamic library with static linking where possible to minimize external dependencies.
- Signing: The resulting
NowPlayingAdapter.dylibreceives its own code signature, distinct from the main application bundle. - Deployment: The signed dylib ships alongside the Perl script in the application resources, ensuring MediaRemote recognizes the binary as authorized.
This architecture ensures that MediaRemote's security checks pass while keeping the main application free from private API entanglements that might trigger App Store rejection or runtime crashes on newer macOS versions.
Practical Implementation Examples
Loading and invoking the dylib via Perl:
# Resources/now-playing.pl
use DynaLoader;
my $lib_path = './NowPlayingAdapter.dylib';
my $lib = DynaLoader::dl_load_file($lib_path)
or die "Failed to load dylib: $!";
my $sym = DynaLoader::dl_find_symbol($lib, 'vorssaint_now_playing_get')
or die "Symbol not found: $!";
my $get_now_playing = DynaLoader::dl_install_xsub(
'vorssaint_now_playing_get',
$sym
);
my $json_output = $get_now_playing->();
print $json_output; # Single line of JSON
Parsing the JSON response in Swift:
// RadialNowPlayingSupport.swift
import Foundation
func fetchNowPlayingData() -> [String: Any]? {
let task = Process()
task.launchPath = "/usr/bin/perl"
task.arguments = ["Resources/now-playing.pl"]
let pipe = Pipe()
task.standardOutput = pipe
task.launch()
task.waitUntilExit()
guard let data = pipe.fileHandleForReading.readDataToEndOfFile(),
let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
return nil
}
return json
}
Manual testing of the standalone dylib:
# Build the release version
swift build -c release -Xswiftc -static
# Execute via Perl wrapper
/usr/bin/perl -I. -M'./Resources/now-playing.pl' -e 'vorssaint_now_playing_get()'
Summary
- Separate Signing: The Now Playing dylib compiles and signs independently to satisfy macOS 15.4's MediaRemote code‑signing requirements.
- Dynamic Loading: Runtime
dlopen/dlsymcalls avoid direct linking against private frameworks in the main executable. - Concurrent Collection: Dispatch queues and groups gather metadata, artwork, and playback state asynchronously from four MediaRemote APIs.
- Atomic JSON Protocol: Thread‑safe aggregation produces a single‑line JSON payload written to stdout for reliable inter‑process communication.
- Perl Bridge: A lightweight DynaLoader wrapper isolates the signed component from the main application logic, maintaining architectural boundaries.
Frequently Asked Questions
Why does the Now Playing dylib require separate code signing?
macOS 15.4 and later enforce strict cryptographic checks within the MediaRemote framework, refusing to return metadata to binaries that lack specific Apple signatures. By compiling and signing the Now Playing dylib as a discrete component, vorssaint‑utils satisfies these security requirements while keeping the main application free from private API dependencies that could trigger rejection or instability.
How does the dylib avoid static linking to MediaRemote?
The implementation uses POSIX dynamic loading APIs (dlopen with RTLD_LAZY) to open the private framework at runtime, followed by dlsym to resolve function pointers for MRMediaRemoteGetNowPlayingInfo and related symbols. This runtime resolution prevents the linker from recording MediaRemote dependencies in the dylib's load commands, ensuring the binary loads successfully even when the framework's internal structure changes between macOS versions.
What specific media metadata does the dylib extract?
The Now Playing dylib retrieves comprehensive playback information including track title, artist, album name, playback rate, and duration via MRMediaRemoteGetNowPlayingInfo. It additionally queries the active application's process ID, bundle display identifier, and Boolean playing state through separate MediaRemote calls, aggregating all values into a unified dictionary for JSON serialization.
How does the main application receive data from the dylib?
Communication occurs through a Unix pipe connected to a Perl helper script (now-playing.pl). The script uses DynaLoader to load the signed dylib and invoke the vorssaint_now_playing_get entry point, which writes a single JSON line to stdout. The main Swift application executes this Perl script as a subprocess, captures the standard output, and deserializes the JSON into a dictionary for UI updates.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →