media_remote/low_level/
low_level.rs

1use block2::RcBlock;
2use core::ffi::c_int;
3use dispatch2::ffi::{dispatch_queue_create, DISPATCH_QUEUE_SERIAL};
4use objc2::{runtime::AnyObject, Encoding};
5use objc2_core_foundation::{
6    CFData, CFDate, CFDictionary, CFDictionaryGetCount, CFDictionaryGetKeysAndValues,
7};
8use objc2_foundation::{NSNumber, NSString};
9use std::{
10    collections::HashMap,
11    ffi::c_void,
12    ptr::{self, NonNull},
13    rc::Rc,
14    sync::{Arc, Condvar, Mutex},
15    time::Duration,
16};
17
18use crate::{low_level::c_functions::*, Command, Id, InfoTypes, Number};
19
20/// Timeout duration for waiting on the media remote response.
21const TIMEOUT_DURATION: Duration = Duration::from_secs(5);
22
23macro_rules! safely_dispatch_and_wait {
24    ($closure:expr, $type:ty, $func:ident) => {{
25        let result = Arc::new((Mutex::new(None), Condvar::new()));
26
27        let result_clone = Arc::clone(&result);
28        let block = RcBlock::new(move |arg: $type| {
29            let (lock, cvar) = &*result_clone;
30            let mut result_guard = lock.lock().unwrap();
31
32            *result_guard = $closure(arg);
33
34            cvar.notify_one();
35        });
36
37        unsafe {
38            let queue = dispatch_queue_create(ptr::null(), DISPATCH_QUEUE_SERIAL);
39            if queue.is_null() {
40                return None;
41            }
42
43            $func(queue, &block);
44        }
45
46        let (lock, cvar) = &*result;
47        let result_guard = match lock.lock() {
48            Ok(guard) => guard,
49            Err(_) => return None,
50        };
51
52        let (result_guard, timeout_result) = match cvar.wait_timeout(result_guard, TIMEOUT_DURATION)
53        {
54            Ok(res) => res,
55            Err(_) => return None,
56        };
57
58        if timeout_result.timed_out() {
59            None
60        } else {
61            result_guard.clone()
62        }
63    }};
64}
65
66/// Checks whether the currently playing media application is actively playing.
67///
68/// The check is performed asynchronously using a callback mechanism,
69/// but the function blocks the calling thread until a result is available or a timeout occurs.
70///
71/// # Returns
72/// - `Some(true)`: If a media application is playing.
73/// - `Some(false)`: If no media is currently playing.
74/// - `None`: If the function times out (e.g., due to an API failure or missing response).
75///
76///
77/// # Example
78/// ```rust
79/// use media_remote::get_now_playing_application_is_playing;
80///
81/// if let Some(is_playing) = get_now_playing_application_is_playing() {
82///     println!("Is playing: {}", is_playing);
83/// } else {
84///     println!("Failed to retrieve playing state.");
85/// }
86/// ```
87pub fn get_now_playing_application_is_playing() -> Option<bool> {
88    safely_dispatch_and_wait!(
89        |is_playing: c_int| Some(is_playing != 0),
90        c_int,
91        MRMediaRemoteGetNowPlayingApplicationIsPlaying
92    )
93}
94
95/// Retrieves the current "now playing" client ID (which is a reference).
96///
97/// This function **should not be used** because the ID is a short-lived reference,
98/// likely only valid within the block where it is obtained.
99/// Using it outside the block could lead to undefined behavior or dangling references.
100///
101/// If client identification is needed, consider an alternative approach
102/// that ensures the ID remains valid for the required duration.
103///
104/// # Example (Do not use)
105/// ```rust
106/// use media_remote::get_now_playing_client;
107///
108/// let client_id = get_now_playing_client();
109/// match client_id {
110///     Some(client) => println!("Now playing client: {:?}", client),
111///     None => println!("No client found or timed out."),
112/// }
113/// ```
114pub fn get_now_playing_client() -> Option<Id> {
115    safely_dispatch_and_wait!(
116        |id: Id| {
117            if !id.is_null() {
118                Some(id)
119            } else {
120                None
121            }
122        },
123        Id,
124        MRMediaRemoteGetNowPlayingClient
125    )
126}
127
128/// Retrieves the current "now playing" application PID.
129///
130/// The check is performed asynchronously using a callback mechanism,
131/// but the function blocks the calling thread until a result is available or a timeout occurs.
132/// If a application PID ID is received, it will be returned as `Some(PID)`, otherwise, it returns `None`.
133///
134/// # Returns
135/// - `Option<PID>`:
136///     - `Some(PID)` if a valid application PID is found.
137///     - `None` if the client PID retrieval failed (due to timeout or invalid result).
138///
139/// # Example
140/// ```rust
141/// use media_remote::get_now_playing_application_pid;
142///
143/// let pid = get_now_playing_application_pid();
144/// match pid {
145///     Some(pid) => println!("Now playing application PID: {:?}", pid),
146///     None => println!("No application found or timed out."),
147/// }
148/// ```
149pub fn get_now_playing_application_pid() -> Option<i32> {
150    safely_dispatch_and_wait!(
151        |pid: c_int| {
152            if pid != 0 {
153                Some(pid)
154            } else {
155                None
156            }
157        },
158        c_int,
159        MRMediaRemoteGetNowPlayingApplicationPID
160    )
161}
162
163/// Retrieves the currently playing media information as a `HashMap<String, InfoTypes>`.
164///
165/// The function interacts with Apple's CoreFoundation API to extract metadata
166/// related to the currently playing media. It blocks execution until the data is retrieved.
167///
168/// The keys used in the returned `HashMap` can be found in the [EnergyBar repository](https://github.com/billziss-gh/EnergyBar/blob/master/src/System/NowPlaying.m),
169/// but note that the types may not be correct.
170///
171/// # Returns
172/// - `Some(HashMap<String, InfoTypes>)`: If metadata is successfully retrieved.
173/// - `None`: If no metadata is available or retrieval fails.
174///
175///
176/// # Example
177/// ```rust
178/// use media_remote::get_now_playing_info;
179///
180/// if let Some(info) = get_now_playing_info() {
181///     for (key, value) in info.iter() {
182///         println!("{}: {:?}", key, value);
183///     }
184/// } else {
185///     println!("No now playing info available.");
186/// }
187/// ```
188pub fn get_now_playing_info() -> Option<HashMap<String, InfoTypes>> {
189    #![allow(useless_ptr_null_checks)]
190    let info = safely_dispatch_and_wait!(
191        |dict: NonNull<CFDictionary>| {
192            if dict.as_ptr().is_null() {
193                return None;
194            }
195
196            unsafe {
197                let count = CFDictionaryGetCount(dict.as_ref());
198
199                let mut keys: Vec<*const c_void> = vec![ptr::null(); count.try_into().unwrap()];
200                let mut values: Vec<*const c_void> = vec![ptr::null(); count.try_into().unwrap()];
201
202                CFDictionaryGetKeysAndValues(dict.as_ref(), keys.as_mut_ptr(), values.as_mut_ptr());
203
204                let mut info = HashMap::<String, InfoTypes>::new();
205                for i in 0..count.try_into().unwrap() {
206                    let key_ptr = keys[i];
207                    let val_ptr = values[i];
208
209                    let key_ref = &*(key_ptr as *const NSString);
210                    let val_ref = &*(val_ptr as *const AnyObject);
211
212                    let class_name = val_ref.class().name().to_str().unwrap_or_default();
213
214                    let value = match class_name {
215                        "__NSCFNumber" => {
216                            let num_ref = &*(val_ptr as *const NSNumber);
217                            let number = match num_ref.encoding() {
218                                Encoding::Char
219                                | Encoding::Short
220                                | Encoding::Int
221                                | Encoding::Long
222                                | Encoding::LongLong => Number::Signed(num_ref.as_i64()),
223                                Encoding::UChar
224                                | Encoding::UShort
225                                | Encoding::UInt
226                                | Encoding::ULong
227                                | Encoding::ULongLong => Number::Unsigned(num_ref.as_u64()),
228                                Encoding::Float | Encoding::Double => {
229                                    Number::Floating(num_ref.as_f64())
230                                }
231                                _ => unreachable!(),
232                            };
233
234                            InfoTypes::Number(number)
235                        }
236                        "__NSCFString" | "__NSCFConstantString" | "NSTaggedPointerString" => {
237                            let str_ref = &*(val_ptr as *const NSString);
238                            InfoTypes::String(str_ref.to_string())
239                        }
240                        "__NSTaggedDate" => {
241                            let date_ref = &*(val_ptr as *const CFDate);
242                            InfoTypes::SystemTime(date_ref.to_system_time().unwrap())
243                        }
244                        "NSSubrangeData" | "_NSInlineData" => {
245                            let data_ref = &*(val_ptr as *const CFData);
246                            InfoTypes::Data(data_ref.to_vec())
247                        }
248                        _ => InfoTypes::Unsupported,
249                    };
250
251                    info.insert(key_ref.to_string(), value);
252                }
253
254                Some(Rc::new(info))
255            }
256        },
257        NonNull<CFDictionary>,
258        MRMediaRemoteGetNowPlayingInfo
259    );
260
261    info.and_then(|info| Rc::try_unwrap(info).ok())
262}
263
264macro_rules! get_bundle_identifier {
265    ($getter:ident) => {
266        safely_dispatch_and_wait!(
267            |id: Id| {
268                if !id.is_null() {
269                    unsafe {
270                        let property = $getter(id);
271                        if !property.is_null() {
272                            return Some((*property).to_string());
273                        }
274                    }
275                }
276                None
277            },
278            Id,
279            MRMediaRemoteGetNowPlayingClient
280        )
281    };
282}
283
284/// Retrieves the bundle identifier of the parent app for the current "now playing" client.
285///
286/// This function attempts to get the parent application's bundle identifier
287/// for the currently active media client. The operation is performed asynchronously
288/// but blocks the calling thread until a result is available or a timeout occurs.
289///
290/// Because the original `NSString` reference is short-lived, it is converted into a `String`
291/// to ensure safe usage beyond the scope of the function.
292///
293/// # Returns
294/// - `Option<String>`:
295///     - `Some(String)` containing the bundle identifier if retrieval is successful.
296///     - `None` if the client ID is invalid, the bundle identifier is null, or retrieval fails.
297///
298/// # Example
299/// ```rust
300/// use media_remote::get_now_playing_client_parent_app_bundle_identifier;
301///
302/// if let Some(bundle_id) = get_now_playing_client_parent_app_bundle_identifier() {
303///     println!("Now playing client parent app: {}", bundle_id);
304/// } else {
305///     println!("No parent app found or retrieval failed.");
306/// }
307/// ```
308pub fn get_now_playing_client_parent_app_bundle_identifier() -> Option<String> {
309    get_bundle_identifier!(MRNowPlayingClientGetParentAppBundleIdentifier)
310}
311
312/// Retrieves the bundle identifier of the current "now playing" client.
313///
314/// This function attempts to get the application's bundle identifier
315/// for the currently active media client. The operation is performed asynchronously
316/// but blocks the calling thread until a result is available or a timeout occurs.
317///
318/// Because the original `NSString` reference is short-lived, it is converted into a `String`
319/// to ensure safe usage beyond the scope of the function.
320///
321/// # Returns
322/// - `Option<String>`:
323///     - `Some(String)` containing the bundle identifier if retrieval is successful.
324///     - `None` if the client ID is invalid, the bundle identifier is null, or retrieval fails.
325///
326/// # Example
327/// ```rust
328/// use media_remote::get_now_playing_client_bundle_identifier;
329///
330/// if let Some(bundle_id) = get_now_playing_client_bundle_identifier() {
331///     println!("Now playing client app: {}", bundle_id);
332/// } else {
333///     println!("No app found or retrieval failed.");
334/// }
335/// ```
336pub fn get_now_playing_client_bundle_identifier() -> Option<String> {
337    get_bundle_identifier!(MRNowPlayingClientGetBundleIdentifier)
338}
339
340/// Sends a media command to the currently active media client.
341///
342/// This function sends a command to the media remote system, instructing the currently
343/// active media client to perform a specific action.
344///
345/// # Note
346/// - The `useInfo` argument is not supported by this function and is not used in
347/// the current implementation.
348/// - If no media is currently playing, this function may open iTunes (or the default media player)
349/// to handle the command.
350///
351/// # Arguments
352/// - `command`: The `Command` to be sent to the media client. This can represent various
353///   actions such as play, pause, skip, or volume control.
354///
355/// # Returns
356/// - `bool`:
357///     - `true` if the command was successfully sent and processed.
358///     - `false` if the operation failed or the command was not recognized.
359///
360/// # Example
361/// ```rust
362/// use media_remote::{send_command, Command};
363///
364/// if send_command(Command::Play) {
365///     println!("Command sent successfully.");
366/// } else {
367///     println!("Failed to send command.");
368/// }
369/// ```
370pub fn send_command(command: Command) -> bool {
371    unsafe { MRMediaRemoteSendCommand(command.into(), ptr::null()) }
372}
373
374/// Sets the playback speed of the currently active media client.
375///
376/// # Arguments
377/// - `speed`: The playback speed multiplier.
378///
379/// # Note
380/// - Playback speed changes typically do not work most of the time.
381///   Depending on the media client or content, setting the playback speed may not have the desired effect.
382///
383/// # Example
384/// ```rust
385/// use media_remote::set_playback_speed;
386///
387/// let speed = 2;
388/// set_playback_speed(speed);
389/// println!("Playback speed set to: {}", speed);
390/// ```
391pub fn set_playback_speed(speed: i32) {
392    unsafe { MRMediaRemoteSetPlaybackSpeed(speed) }
393}
394
395/// Sets the elapsed time of the currently playing media.
396///
397/// # Arguments
398/// - `elapsed_time`: The elapsed time in seconds to set the current position of the media.
399///
400/// # Note
401/// - **Limitations**: Setting the elapsed time can often cause the media to pause. Be cautious
402///   when using this function, as the playback might be interrupted and require manual resumption.
403///
404/// # Example
405/// ```rust
406/// use media_remote::set_elapsed_time;
407///
408/// let elapsed = 1.0;  
409/// set_elapsed_time(elapsed);
410/// println!("Elapsed time set to: {} seconds", elapsed);
411pub fn set_elapsed_time(elapsed_time: f64) {
412    unsafe { MRMediaRemoteSetElapsedTime(elapsed_time) }
413}
414
415/// Registers for "Now Playing" notifications.
416///
417/// This function **must** be called before adding any observers using [`add_observer`],
418/// or notifications may not be received.
419///
420/// # Example
421/// ```rust
422/// use media_remote::register_for_now_playing_notifications;
423///
424/// register_for_now_playing_notifications();
425/// ```
426pub fn register_for_now_playing_notifications() {
427    unsafe {
428        let queue = dispatch_queue_create(ptr::null(), DISPATCH_QUEUE_SERIAL);
429        MRMediaRemoteRegisterForNowPlayingNotifications(queue);
430    }
431}
432
433/// Unregisters from "Now Playing" notifications.
434///
435///
436/// # Example
437/// ```rust
438/// use media_remote::unregister_for_now_playing_notifications;
439///
440/// unregister_for_now_playing_notifications();
441/// ```
442pub fn unregister_for_now_playing_notifications() {
443    unsafe {
444        MRMediaRemoteUnregisterForNowPlayingNotifications();
445    }
446}