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}