media-remote 0.5.1

Bindings for MediaRemote.framework
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
<div align="center">

# MediaRemote in Rust

[![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/nohackjustnoobb/media-remote/main.yml?style=for-the-badge&label=test)](https://github.com/nohackjustnoobb/media-remote/actions/workflows/main.yml)
[![GitHub License](https://img.shields.io/github/license/nohackjustnoobb/media-remote?style=for-the-badge)](https://github.com/nohackjustnoobb/media-remote/blob/master/LICENSE)
[![Crates.io Version](https://img.shields.io/crates/v/media-remote?style=for-the-badge)](https://crates.io/crates/media-remote)

</div>

> [!IMPORTANT]  
> After macOS 15.4, Apple introduced entitlement verification in the mediaremoted daemon. Clients without the required entitlement are denied access to NowPlaying information. To bypass this limitation, there are three solutions:
>
> 1. Perl Adapter (Recommended, *Does not* require SIP to be disabled)
>
>    Use a bundled Perl script to interface with the system's `mediaremote` via [mediaremote-adapter]https://github.com/ungive/mediaremote-adapter/tree/master. See [macOS 15.4+]#macos-154 for more information.
>
> 2. AppleScript / JavaScript for Automation (*Does not* require SIP to be disabled)
>
>    Use system automation to retrieve info. See [macOS 15.4+]#macos-154 for more information.
>
> 3. Code Injection (*Requires* SIP to be disabled)
>
>    Use [MediaRemoteWizard]https://github.com/Mx-Iris/MediaRemoteWizard to inject code into mediaremoted, overriding core methods to return YES, thereby allowing any client to connect.

> [!WARNING]
> Since MediaRemote is a private Apple framework, using it may introduce compatibility or stability issues, and your app may not be approved for distribution on the App Store. Use this library at your own risk.

This library provides bindings for Apple's private framework, **MediaRemote**. It is primarily designed to access information about media that is currently playing. Therefore, not all methods from the MediaRemote framework are included in these bindings.

This library **should** be safe to use. However, it is the first attempt at building these bindings, so there is a high chance of unexpected errors. If you encounter any issues, please report them in the issue tracker or submit a pull request to help improve the library.

## Quick Start

To get started, first ensure that the library is installed.

```toml
[dependencies]
media-remote = "*"
```

> [!NOTE]
> ### Cargo Features
>
> The `artwork` feature is **enabled by default**. It enables album cover and app icon decoding via the `image` crate.
>
> To disable artwork (reduce dependencies and memory usage):
>
> ```toml
> [dependencies]
> media-remote = { version = "*", default-features = false }
> ```
>
> When disabled:
> - `NowPlayingInfo.album_cover` and `NowPlayingInfo.bundle_icon` are removed.
> - The `image` and `base64` crates are not compiled.

Minimal example:

```rust
use media_remote::prelude::*;

fn main() {
    // Create an instance of NowPlaying to interact with the media remote.
    let now_playing = NowPlaying::new();

    // Use a guard lock to safely access media information within this block.
    // The guard should be released as soon as possible to avoid blocking.
    {
        let guard = now_playing.get_info();
        let info = guard.as_ref();

        // If information is available, print the title of the currently playing media.
        if let Some(info) = info {
            println!("Currently playing: {:?}", info.title);
        }
    }

    // Toggle the play/pause state of the media.
    now_playing.toggle();
}
```

## API Documentation

_This is a brief documentation. More detailed documentation, including examples, is written inside the code documentation. Hover over the function to check the documentation._

<details>
  <summary>High Level API</summary>

### `NowPlaying::new() -> NowPlaying`

Creates a new instance of `NowPlaying` and registers for playback notifications.

- **Returns**:
  - `NowPlaying`: A new instance of the `NowPlaying` struct.

### `NowPlaying::get_info(&self) -> RwLockReadGuard<'_, Option<NowPlayingInfo>>`

Retrieves the latest now playing information.

- **Returns**:
  - `RwLockReadGuard<'_, Option<NowPlayingInfo>>`: A guard to the now playing metadata.

- **Note**:
  - The lock should be released as soon as possible to minimize blocking time.

### `NowPlaying::subscribe<F: Fn(RwLockReadGuard<'_, Option<NowPlayingInfo>>) + Send + Sync + 'static>(&self, listener: F) -> ListenerToken`

Subscribes a listener to receive updates when the "Now Playing" information changes.

- **Arguments**:
  - `listener`: A function or closure that accepts a `RwLockReadGuard<'_, Option<NowPlayingInfo>>`.

- **Returns**:
  - `ListenerToken`: A token representing the listener, which can later be used to unsubscribe.

### `NowPlaying::unsubscribe(&self, token: ListenerToken)`

Unsubscribes a previously registered listener using the provided `ListenerToken`.

- **Arguments**:
  - `token`: The `ListenerToken` returned when the listener was subscribed.

### `NowPlayingInfo`

```rust
pub struct NowPlayingInfo {
    pub is_playing: Option<bool>,
    pub title: Option<String>,
    pub artist: Option<String>,
    pub album: Option<String>,
    #[cfg(feature = "artwork")]
    pub album_cover: Option<DynamicImage>,
    pub elapsed_time: Option<f64>,
    pub duration: Option<f64>,
    pub playback_rate: Option<f64>,
    pub info_update_time: Option<SystemTime>,
    pub bundle_id: Option<String>,
    pub bundle_name: Option<String>,
    #[cfg(feature = "artwork")]
    pub bundle_icon: Option<DynamicImage>,
}
```

> [!NOTE]
> The `album_cover` and `bundle_icon` fields are only available when the `artwork` feature is enabled (default). Disable default features to remove them.

### Media Control Functions

These functions allow you to control the currently playing media. To use these functions, import `media_remote::Controller`.

- `NowPlaying::toggle(&self) -> bool`

  Toggles between play and pause states.

- `NowPlaying::play(&self) -> bool`

  Starts playing the media.

- `NowPlaying::pause(&self) -> bool`

  Pauses the media.

- `NowPlaying::next(&self) -> bool`

  Skips to the next track.

- `NowPlaying::previous(&self) -> bool`

  Goes back to the previous track.

- `NowPlaying::toggle_shuffle(&self) -> bool`

  Toggles the shuffle state of the playback queue.

- `NowPlaying::toggle_repeat(&self) -> bool`

  Toggles the repeat state of the playback queue.

- `NowPlaying::start_forward_seek(&self) -> bool`

  Starts a forward seek operation.

- `NowPlaying::end_forward_seek(&self) -> bool`

  Ends a forward seek operation.

- `NowPlaying::start_backward_seek(&self) -> bool`

  Starts a backward seek operation.

- `NowPlaying::end_backward_seek(&self) -> bool`

  Ends a backward seek operation.

- `NowPlaying::go_back_fifteen_seconds(&self) -> bool`

  Seeks backward by fifteen seconds.

- `NowPlaying::skip_fifteen_seconds(&self) -> bool`

  Skips forward by fifteen seconds.

- `NowPlaying::set_playback_speed(&self, speed: i32)`

  Sets the playback speed of the currently active media client.

  - **Arguments**:
    - `speed`: The playback speed multiplier.

  - **Note**:
    - Playback speed changes typically do not work most of the time. Depending on the media client or content, setting the playback speed may not have the desired effect.

- `NowPlaying::set_elapsed_time(&self, elapsed_time: f64)`

  Sets the elapsed time of the currently playing media.

  - **Arguments**:
    - `elapsed_time`: The elapsed time in seconds to set the current position of the media.

  - **Note**:
    - Setting the elapsed time can often cause the media to pause. Be cautious when using this function, as the playback might be interrupted and require manual resumption.
  </details>

<details>
  <summary>Low Level API</summary>

### `get_now_playing_application_is_playing() -> Option<bool>`

Checks whether the currently playing media application is actively playing.

- **Returns**:
  - `Some(true)`: If a media application is playing.
  - `Some(false)`: If no media is currently playing.
  - `None`: If the function times out (e.g., due to an API failure or missing response).

### `get_now_playing_client() -> Option<Id>`

Retrieves the current "now playing" client ID (which is a reference).

- **Returns**:
  - `Some(Id)`: If a valid client ID is found.
  - `None`: If no client ID is found or the request times out.

- **Note**:
  - This function should not be used as the returned ID is short-lived and may cause undefined behavior when used outside of the block.

### `get_now_playing_application_pid() -> Option<i32>`

Retrieves the current "now playing" application PID.

- **Returns**:
  - `Some(PID)`: If a valid application PID is found.
  - `None`: If no application PID is found or the request times out.

### `get_now_playing_info() -> Option<HashMap<String, InfoTypes>>`

Retrieves the currently playing media information as a `HashMap<String, InfoTypes>`. The function interacts with Apple's CoreFoundation API to extract metadata related to the currently playing media.

- **Returns**:
  - `Some(HashMap<String, InfoTypes>)`: If metadata is successfully retrieved.
  - `None`: If no metadata is available or retrieval fails.

### `get_now_playing_client_parent_app_bundle_identifier() -> Option<String>`

Retrieves the bundle identifier of the parent app for the current "now playing" client.

- **Returns**:
  - `Some(String)`: The bundle identifier of the parent app if successfully retrieved.
  - `None`: If the client ID is invalid, the bundle identifier is null, or retrieval fails.

### `get_now_playing_client_bundle_identifier() -> Option<String>`

Retrieves the bundle identifier of the current "now playing" client.

- **Returns**:
  - `Some(String)`: The bundle identifier of the client app if successfully retrieved.
  - `None`: If the client ID is invalid, the bundle identifier is null, or retrieval fails.

### `send_command(command: Command) -> bool`

Sends a media command to the currently active media client.

- **Arguments**:
  - `command`: The Command to be sent, representing an action like play, pause, skip, etc.

- **Returns**:
  - `true`: If the command was successfully sent and processed.
  - `false`: If the operation failed or the command was not recognized.

- **Notes**:
  - The `useInfo` argument is not supported by this function and is not used in the current implementation.
  - If no media is currently playing, this function may open iTunes (or the default media player) to handle the command.

### `set_playback_speed(speed: i32)`

Sets the playback speed of the currently active media client.

- **Arguments**:
  - `speed`: The playback speed multiplier.

- **Note**:
  - Playback speed changes typically do not work most of the time. Depending on the media client or content, setting the playback speed may not have the desired effect.

### `set_elapsed_time(elapsed_time: f64)`

Sets the elapsed time of the currently playing media.

- **Arguments**:
  - `elapsed_time`: The elapsed time in seconds to set the current position of the media.

- **Note**:
  - Setting the elapsed time can often cause the media to pause. Be cautious when using this function, as the playback might be interrupted and require manual resumption.

### `register_for_now_playing_notifications()`

Registers the caller for "Now Playing" notifications.

- **Note**:
  - Must be called before adding observers to ensure notifications are received.

### `unregister_for_now_playing_notifications()`

Unregisters the caller for "Now Playing" notifications.

- **Note**:
  - Should be called when notifications are no longer needed to free resources.

  </details>

  <details>
  <summary>Helper Functions</summary>

### `add_observer(notification: Notification, closure: F) -> Observer`

Adds an observer for a specific media notification.

- **Arguments**:
  - `notification`: The Notification type representing the event to observe.
  - `closure`: A closure to execute when the notification is received.

- **Returns**:
  - An Observer handle that can be used to remove the observer later.

- **Note**:
  - `register_for_now_playing_notifications()` **must** be called before using this function, or notifications may not be received.

### `remove_observer(observer: Observer)`

Removes a previously added observer.

- **Arguments**:
  - `observer`: The Observer handle returned from add_observer().

### `get_bundle_info(id: &str) -> Option<BundleInfo>`

Retrieves information about an application based on its bundle identifier, including the application's name and icon.

- **Arguments**:
  - `id`: A string slice representing the bundle identifier of the application.

- **Returns**:
  - `Some(BundleInfo)`: If the application is found, containing the application's name and icon.
  - `None`: If the application cannot be found, or if there is an error retrieving the information.

</details>

## macOS 15.4+

For macOS 15.4+, you have two options:

### 1. Perl Adapter (Recommended)

Use `NowPlayingPerl`. This method uses an embedded Perl script to interface with a custom adapter, allowing it to bypass the entitlement check. This is based on [mediaremote-adapter](https://github.com/ungive/mediaremote-adapter/tree/master) by [ungive](https://github.com/ungive).

**Pros:**

- Supports real-time updates.
- **Supports retrieval of artwork** (disable with `--no-artwork` via the `artwork` Cargo feature).
- API is nearly identical to `NowPlaying`.

**Cons:**

- Spawns a background process (`perl`).

```rust
use media_remote::NowPlayingPerl;

let now_playing = NowPlayingPerl::new();
// Use it just like NowPlaying
```

### 2. AppleScript / JXA (Alternative)

Use `NowPlayingJXA`. This method uses JavaScript for Automation.

**Pros:**

- No external binary assets (uses system `osascript`).

**Cons:**

- **Cannot retrieval artwork.**
- Updates may have a delay (polling/interval based).

```rust
use media_remote::NowPlayingJXA;
let now_playing = NowPlayingJXA::new(Duration::from_secs(1));
```

If you want to use JAX directly, use the `get_raw_info` function for unprocessed data, or the formatted version, `get_info`. Note that both functions may return None if an error occurs.

## Development

### Update MediaRemote Adapter

To update the `mediaremote-adapter` submodule and rebuild the assets:

1. Update the submodule:

   ```bash
   git submodule update --remote
   ```

2. Run the build script:
   ```bash
   ./build.sh
   ```

### Testing

There are some tests that run indefinitely to test subscriptions. These are valid for `NowPlaying`, `NowPlayingPerl`, and `NowPlayingJXA`. Since they run forever, they are ignored by default.

To run these tests, use:

```bash
cargo test --test <test_name> -- --nocapture --exact --ignored
```

Where `<test_name>` can be:

- `test_now_playing`
- `test_now_playing_perl`
- `test_now_playing_jxa`