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}