This release is a pre-release and may not be stable for production use.
macos-mediaremote-python
An unofficial Python wrapper for ungive/mediaremote-adapter. It provides asyncio APIs to read macOS Now Playing metadata, subscribe to updates, and control the current player.
Import it as macos_mediaremote. The distribution name is
macos-mediaremote-python, and the current alpha version is 0.1.0a1.
Installation
Requires macOS and Python 3.11+. Install the alpha release in a virtual environment:
python3 -m venv /tmp/mediaremote-demo
/tmp/mediaremote-demo/bin/python -m pip install macos-mediaremote-python==0.1.0a1
Compatible macOS installations use the bundled universal2 wheel. For local development, see Building from source.
The wheel includes the framework, upstream Perl script, and licenses. Installing, importing, and running an installed wheel never downloads native code. Wheel users do not need Git, CMake, Xcode, or Homebrew.
At runtime, the wrapper invokes macOS's /usr/bin/perl, preserving the upstream
DynaLoader mechanism for loading the framework. Python does not load the private
framework directly.
Read the current state
import asyncio
from macos_mediaremote import MediaRemote
async def main():
remote = MediaRemote()
state = await remote.get()
if state is not None:
print(state.title, state.artist, state.playing)
print(state.bundle_identifier)
print(state.elapsed_time, state.duration) # Seconds
print(state.raw.get("parentApplicationBundleIdentifier"))
asyncio.run(main())
Inside an existing asyncio application, use await remote.get() directly.
Each get() call starts a short-lived helper process. Importing the package and
constructing MediaRemote() have no process or network side effects.
Subscribe to updates
import asyncio
from macos_mediaremote import MediaRemote
async def main():
async with MediaRemote().stream() as events:
async for state in events:
if state is None:
print("No current media session")
else:
print(state.title, state.playing)
asyncio.run(main())
Each stream() owns one persistent helper process and must be used with
async with. Entering the context waits up to initialization_timeout seconds
for the first valid snapshot, which is retained for iteration. Subsequent silence
has no idle timeout. Every event is a complete snapshot (--no-diff upstream).
Upstream may emit an empty snapshot before the current media state arrives.
The first None does not mean initialization has settled.
- Leaving the context after
break, a consumer exception, or cancellation closes the helper. Cleanup sends SIGTERM, escalates to SIGKILL after one second if necessary, and reaps the process. - Cancellation preserves
asyncio.CancelledError.await events.aclose()is idempotent. Do not read the same iterator concurrently or callaclose()while another task is still awaitinganext(). Cancel and await that task first. - Clean EOF before the first snapshot is a protocol error. Clean EOF after a
snapshot ends iteration. A nonzero exit raises
HelperError; it is not retried. - stdout uses upstream's newline-delimited JSON protocol. UTF-8 characters may
span reads, and one read may contain multiple events. The stream does not use
--human-readableor accept a single pretty-printed JSON event spanning lines. - The default limit is 4 MiB per event or complete command response. Exceeding it raises an error and closes the helper. Slow consumers apply backpressure through bounded pipes instead of accumulating an unbounded Python event queue.
- stderr is continuously drained, retaining only its last 64 KiB. Nonfatal stderr
does not stop successful operations. Inspect
events.stderrduring a stream, orHelperError.stderr/HelperTimeoutError.stderron failure. Successful short-lived commands do not expose stderr.
Control the current player
These methods change real playback state. Call them only when intended:
await remote.play()
await remote.pause()
await remote.toggle_play_pause()
await remote.previous_track()
await remote.next_track()
await remote.seek(42.5) # Seconds; sends 42500000 microseconds upstream
You can also import Command from the package and call
await remote.send(Command.PLAY).
Arguments are validated and passed as a subprocess argument list, never through
a shell. seek() accepts finite, nonnegative seconds, rounded to the nearest
integer microsecond using Python's round(), within the upstream signed 64-bit
range. Seeking to zero is supported.
Commands target the player selected by the system at dispatch time. The API
does not enumerate or lock arbitrary sessions. If the current player changes
from A to B after get(), the next control may go to B. This version does not
provide target-player checks; even a future check would leave a race between
checking and dispatching.
Success means the upstream command exited successfully, not that the player acknowledged or applied it. In particular, seeking has no player acknowledgement. Upstream's implicit application-launch behavior is preserved, so some commands may launch a player.
Types, units, and errors
get() and stream events return NowPlaying | None. A null or empty snapshot
means no session is reported. Nonempty snapshots preserve missing and unknown
fields. --allow-missing-title is enabled by default, so a session can have no
title. Missing fields are not replaced with empty strings or zero.
| Attribute | Type and unit |
|---|---|
bundle_identifier, title, artist, album |
str or None |
playing |
bool or None |
duration, elapsed_time |
float or None, in seconds; elapsed time is the position at timestamp, not a live clock |
timestamp |
float or None, Unix epoch seconds |
playback_rate |
float or None, playback multiplier |
raw |
Top-level read-only mapping preserving upstream keys, nulls, and original units; unknown nested objects are not recursively frozen |
Public time attributes, seek(), and all timeouts use seconds. raw deliberately
preserves upstream microsecond fields such as durationMicros, elapsedTimeMicros,
and timestampEpochMicros. This version does not extrapolate playback progress.
Artwork is omitted by default. With MediaRemote(include_artwork=True), raw may
include base64-encoded artworkData, depending on the player.
The complete configuration is:
remote = MediaRemote(
timeout=5.0,
initialization_timeout=5.0,
include_artwork=False,
max_output_bytes=4 * 1024 * 1024,
)
The output limit can be set between 1 KiB and 64 MiB.
All public exceptions are available from the package root:
MediaRemoteError: common runtime error base class.HelperError: helper startup or exit failure, withreturncodeandstderr.HelperTimeoutError: command or initialization timeout; also aTimeoutError.ProtocolError: invalid or oversized protocol output.UnsupportedPlatformError: attempted use on a non-macOS platform.
Invalid API arguments raise ValueError.
Upstream and packaging
Package version 0.1.0a1 pins upstream
v0.7.7, commit
e3ff5021eb0875858bd05f48d2e9ba2e962d1cf6.
upstream.lock.json records the archive SHA-256, deployment
target, and package-to-upstream version mapping. Each build extracts verified
archive bytes into a fresh directory. It does not build from an edited checkout
or fetch a moving latest version at runtime.
The build uses standard setuptools.build_meta with small hooks to build the
native resources and set the wheel tag. See the
setuptools customization documentation
and platform tag specification.
Upstream CMake builds both arm64 and x86_64. This package produces a universal2
wheel tagged py3-none-macosx_11_0_universal2, with Root-Is-Purelib: false.
none means the helper is independent of the CPython ABI; the platform tag must
not be changed to any.
The upstream native and Perl sources are unmodified. Packaging flattens the
framework's Versions/A directory to avoid relying on wheel installers to
preserve symlinks, changes its dylib install name to
@rpath/MediaRemoteAdapter.framework/MediaRemoteAdapter, and applies and verifies
an ad-hoc signature. The upstream test client is not bundled, and the test
command, which may create a synthetic Now Playing entry, is not exposed or run.
Building from source
Requires macOS, Xcode Command Line Tools, CMake, and Python 3.11.8+:
python3 -m venv .venv
.venv/bin/python -m pip install 'setuptools>=77,<85' build pytest pytest-asyncio
.venv/bin/python -m pytest -q
.venv/bin/python -m build
Install the resulting wheel into a separate environment to verify it:
python3 -m venv /tmp/mediaremote-local
/tmp/mediaremote-local/bin/python -m pip install --no-index --no-deps dist/*.whl
/tmp/mediaremote-local/bin/python examples/read_once.py
python -m build creates an sdist, then builds a wheel from it. The sdist includes
this project's build scripts, lock file, licenses, tests, and examples, but not
the upstream source archive. Source installation requires native build tools and
network access to the pinned GitHub archive. Build isolation also installs the
build dependencies declared in pyproject.toml.
In an existing source directory, .build/upstream/source.tar.gz can be reused as
a SHA-256-verified cache. Building from an sdist downloads it again. These steps
only happen at build time.
Support and limitations
- The native deployment target is macOS 11.0. This is a binary build target, not a runtime guarantee for every macOS 11+ release. Python's own deployment requirements also apply.
- Live media metadata and streaming have been verified on the development Mac, running macOS 26.5.1 on Apple Silicon. Hosted CI separately checks installation and read-only helper execution on Intel and Apple Silicon; it does not exercise real desktop players.
- This package depends on private MediaRemote APIs and the system Perl access mechanism. Future macOS updates may break it.
- Browsers and other players may report different fields, omit titles or artwork, or ignore some commands.
- The pinned upstream
stream.mstill callsrequestAll()before registering notifications. This leaves a potential initialization window for missed updates. The first event is not an atomic subscription-ready barrier. This package preserves upstream initialization behavior. get()returningNonealone cannot prove private API access is working: no active session and an access failure may be hard to distinguish. Read-only verification does not create synthetic media to resolve that uncertainty.
Validation status
Validated on 2026-09-11 with macOS 26.5.1, Apple Silicon, and Python 3.14.5:
- 88 automated tests passed, covering protocol parsing, time units, control arguments, timeouts, cancellation, stderr, backpressure, and process cleanup. Playback controls were tested using simulated helpers.
- A universal2 wheel was built from the sdist and installed offline in a new virtual environment outside the repository. The read example and a read-only subscription ran from that installation.
- Installed resources were checked for both architectures, the macOS 11.0 deployment target, framework signature, system library dependencies, resource lookup, and wheel RECORD integrity.
- Live reads returned a nonempty Now Playing snapshot, the stream received
updates, and cancellation reaped the helper. No real playback controls or
upstream
testcommand were executed. - Interactive Intel desktops, real playback controls, a player compatibility matrix, Developer ID signing, and notarized distribution remain unverified. See the CI workflow below for automated Python and hosted macOS coverage.
examples/verify_installed.py performs the installed-wheel read-only audit. It
does not send playback controls or print actual track metadata values.
License and release status
The wrapper uses BSD-3-Clause; see LICENSE. Upstream copyright belongs to Jonas van den Berg and contributors. Its original license is preserved in licenses/mediaremote-adapter.txt and bundled with the wheel. The wrapper is independently implemented against the public CLI/JSON protocol.
Framework layout changes, install-name changes, and re-signing are packaging steps performed by this project, not official upstream artifacts.
Version 0.1.0a1 is an alpha release. The public API may change before a stable
release. See the support and validation sections above for the tested platforms
and remaining limitations.
CI and release workflow
.github/workflows/release.yml runs on pushes to main, pull requests, and
manual dispatch. Ordinary pushes and pull requests never upload to PyPI.
The workflow tests Python 3.11 through 3.14, builds a universal2 wheel from the
sdist, runs twine check --strict, and installs that same wheel in fresh
Apple Silicon and Intel macOS 15 environments on Python 3.11 and 3.14. Runtime
tests run outside the source checkout. The installed-wheel audit verifies
resources, signatures, deployment targets, library dependencies, and a read-only
get() call. A successful empty read on a hosted runner is not evidence of live
player compatibility; desktop playback and streaming still need separate tests.
release_checks.py rejects inconsistent package versions, upstream pins, and
architecture settings. The wheel tag is derived from the locked deployment
target. You can run the metadata checks locally without invoking the helper:
python release_checks.py
Publishing uses PyPI Trusted Publishing with these identifiers:
| Setting | Value |
|---|---|
| Project | macos-mediaremote-python |
| GitHub owner | Mai-icy |
| Repository | macos-mediaremote-python |
| Workflow filename | release.yml |
| GitHub environment | pypi |
For an authorized release, create a vVERSION tag matching the package version,
then manually dispatch this workflow at that tag with publish=true. The input
defaults to false. Publishing requires all checks to pass and approval in the
pypi environment. Only the publishing job receives OIDC permissions; it uploads
the already-tested artifacts without rebuilding. No persistent PyPI token or
TestPyPI workflow is used. Creating or pushing a tag alone does not upload a
release.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file macos_mediaremote_python-0.1.0a1.tar.gz.
File metadata
- Download URL: macos_mediaremote_python-0.1.0a1.tar.gz
- Upload date:
- Size: 28.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b74d605bc70c86f5abe6f37c930393e46f254ca52c8761b3dd38abd1313f0672
|
|
| MD5 |
0a62f8b9191dbcd11bcf59c51c145b7a
|
|
| BLAKE2b-256 |
d00573bbf2271b67988d90285589bb43ef7c025b44ec96d10958abff949802b2
|
Provenance
The following attestation bundles were made for macos_mediaremote_python-0.1.0a1.tar.gz:
Publisher:
release.yml on Mai-icy/macos-mediaremote-python
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
macos_mediaremote_python-0.1.0a1.tar.gz -
Subject digest:
b74d605bc70c86f5abe6f37c930393e46f254ca52c8761b3dd38abd1313f0672 - Sigstore transparency entry: 2789848111
- Sigstore integration time:
-
Permalink:
Mai-icy/macos-mediaremote-python@7243b4209098924e782d2b411bc62a17822ec405 -
Branch / Tag:
refs/tags/v0.1.0a1 - Owner: https://github.com/Mai-icy
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@7243b4209098924e782d2b411bc62a17822ec405 -
Trigger Event:
workflow_dispatch
-
Statement type:
File details
Details for the file macos_mediaremote_python-0.1.0a1-py3-none-macosx_11_0_universal2.whl.
File metadata
- Download URL: macos_mediaremote_python-0.1.0a1-py3-none-macosx_11_0_universal2.whl
- Upload date:
- Size: 89.4 kB
- Tags: Python 3, macOS 11.0+ universal2 (ARM64, x86-64)
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c8f93b15e2e5bc26ee6f90d3d480381c45e4e156738f597a19b0d13f014765c2
|
|
| MD5 |
e6aa366f1be2bba9640a122b367c77fa
|
|
| BLAKE2b-256 |
aff8bc44e3c70be8f332adb0488bcde10b9a0270ed21b5a4d27600e628ff67c8
|
Provenance
The following attestation bundles were made for macos_mediaremote_python-0.1.0a1-py3-none-macosx_11_0_universal2.whl:
Publisher:
release.yml on Mai-icy/macos-mediaremote-python
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
macos_mediaremote_python-0.1.0a1-py3-none-macosx_11_0_universal2.whl -
Subject digest:
c8f93b15e2e5bc26ee6f90d3d480381c45e4e156738f597a19b0d13f014765c2 - Sigstore transparency entry: 2789848163
- Sigstore integration time:
-
Permalink:
Mai-icy/macos-mediaremote-python@7243b4209098924e782d2b411bc62a17822ec405 -
Branch / Tag:
refs/tags/v0.1.0a1 - Owner: https://github.com/Mai-icy
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@7243b4209098924e782d2b411bc62a17822ec405 -
Trigger Event:
workflow_dispatch
-
Statement type: