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