Skip to main content

media_remote/high_level/
now_playing.rs

1use std::{
2    collections::HashMap,
3    sync::{atomic::AtomicU64, Arc, Mutex, RwLock, RwLockReadGuard},
4    time::SystemTime,
5};
6
7#[cfg(feature = "artwork")]
8use std::io::Cursor;
9
10#[cfg(feature = "artwork")]
11use image::ImageReader;
12
13use crate::{
14    add_observer, get_bundle_info, get_now_playing_application_is_playing,
15    get_now_playing_client_bundle_identifier, get_now_playing_client_parent_app_bundle_identifier,
16    get_now_playing_info, register_for_now_playing_notifications, remove_observer,
17    unregister_for_now_playing_notifications, InfoTypes, Notification, NowPlayingInfo, Number,
18    Observer, Subscription,
19};
20
21use crate::{Controller, ListenerToken};
22
23/// A struct for managing and interacting with the "Now Playing" media session.
24///
25/// The `NowPlaying` struct allows access to the currently playing media information,
26/// and provides functionality to control playback (e.g., play, pause, skip).
27///
28///
29/// # Example
30/// ```rust
31/// use media_remote::{Controller, NowPlaying};
32///
33/// let now_playing = NowPlaying::new();
34/// now_playing.play();
35/// ```
36pub struct NowPlaying {
37    info: Arc<RwLock<Option<NowPlayingInfo>>>,
38    observers: Vec<Observer>,
39    listeners: Arc<
40        Mutex<
41            HashMap<
42                ListenerToken,
43                Box<dyn Fn(RwLockReadGuard<'_, Option<NowPlayingInfo>>) + Send + Sync>,
44            >,
45        >,
46    >,
47    token_counter: Arc<AtomicU64>,
48}
49
50fn update_all(info: Arc<RwLock<Option<NowPlayingInfo>>>) {
51    let mut info_guard = info.write().unwrap();
52    *info_guard = Some(NowPlayingInfo {
53        is_playing: None,
54        title: None,
55        artist: None,
56        album: None,
57        #[cfg(feature = "artwork")]
58        album_cover: None,
59        elapsed_time: None,
60        duration: None,
61        playback_rate: None,
62        info_update_time: None,
63        bundle_id: None,
64        bundle_name: None,
65        #[cfg(feature = "artwork")]
66        bundle_icon: None,
67    });
68    drop(info_guard);
69
70    update_state(info.clone());
71    update_app(info.clone());
72    update_info(info.clone());
73}
74
75fn update_state(info: Arc<RwLock<Option<NowPlayingInfo>>>) {
76    let mut info_guard = info.write().unwrap();
77    if info_guard.as_ref().is_none() {
78        drop(info_guard);
79        return update_all(info.clone());
80    }
81
82    let is_playing = get_now_playing_application_is_playing();
83    if let Some(is_playing) = is_playing {
84        info_guard.as_mut().unwrap().is_playing = Some(is_playing);
85    }
86}
87
88fn update_info(info: Arc<RwLock<Option<NowPlayingInfo>>>) {
89    let mut info_guard = info.write().unwrap();
90    if info_guard.as_ref().is_none() {
91        drop(info_guard);
92        return update_all(info.clone());
93    }
94
95    let now_playing_info = get_now_playing_info();
96    if let Some(info) = now_playing_info {
97        macro_rules! update_string_info {
98            ($key:expr, $field:expr) => {
99                if let Some(InfoTypes::String(s)) = info.get($key) {
100                    if !s.is_empty() {
101                        $field = Some(s.clone());
102                    }
103                }
104            };
105        }
106
107        update_string_info!(
108            "kMRMediaRemoteNowPlayingInfoTitle",
109            info_guard.as_mut().unwrap().title
110        );
111        update_string_info!(
112            "kMRMediaRemoteNowPlayingInfoArtist",
113            info_guard.as_mut().unwrap().artist
114        );
115        update_string_info!(
116            "kMRMediaRemoteNowPlayingInfoAlbum",
117            info_guard.as_mut().unwrap().album
118        );
119
120        macro_rules! update_float_info {
121            ($key:expr, $field:expr) => {
122                if let Some(InfoTypes::Number(Number::Floating(f))) = info.get($key) {
123                    $field = Some(f.clone());
124                }
125            };
126        }
127
128        update_float_info!(
129            "kMRMediaRemoteNowPlayingInfoDuration",
130            info_guard.as_mut().unwrap().duration
131        );
132        update_float_info!(
133            "kMRMediaRemoteNowPlayingInfoElapsedTime",
134            info_guard.as_mut().unwrap().elapsed_time
135        );
136        update_float_info!(
137            "kMRMediaRemoteNowPlayingInfoPlaybackRate",
138            info_guard.as_mut().unwrap().playback_rate
139        );
140
141        #[cfg(feature = "artwork")]
142        if let Some(InfoTypes::Data(d)) = info.get("kMRMediaRemoteNowPlayingInfoArtworkData") {
143            info_guard.as_mut().unwrap().album_cover = ImageReader::new(Cursor::new(d))
144                .with_guessed_format()
145                .ok()
146                .and_then(|img| img.decode().ok());
147        }
148
149        info_guard.as_mut().unwrap().info_update_time = info
150            .get("kMRMediaRemoteNowPlayingInfoTimestamp")
151            .and_then(|f| match f {
152                InfoTypes::SystemTime(t) => Some(t.clone()),
153                _ => None,
154            })
155            .or(Some(SystemTime::now()));
156    }
157}
158
159fn update_app(info: Arc<RwLock<Option<NowPlayingInfo>>>) {
160    let mut info_guard = info.write().unwrap();
161    if info_guard.as_ref().is_none() {
162        drop(info_guard);
163        return update_all(info.clone());
164    }
165
166    let mut bundle_id = get_now_playing_client_parent_app_bundle_identifier();
167    if bundle_id.is_none() {
168        bundle_id = get_now_playing_client_bundle_identifier();
169    }
170
171    if let Some(id) = bundle_id {
172        let bundle_info = get_bundle_info(id.as_str());
173        if let Some(info) = bundle_info {
174            info_guard.as_mut().unwrap().bundle_id = Some(id);
175            info_guard.as_mut().unwrap().bundle_name = Some(info.name);
176            #[cfg(feature = "artwork")]
177            {
178                info_guard.as_mut().unwrap().bundle_icon = Some(info.icon);
179            }
180        }
181    }
182}
183
184impl NowPlaying {
185    fn register(&mut self) {
186        register_for_now_playing_notifications();
187
188        // initialize with current state
189        let info = Arc::clone(&self.info);
190        update_all(info.clone());
191
192        macro_rules! add_observer_macro {
193            ($notification:expr, $update_fn:expr) => {{
194                let info = Arc::clone(&self.info);
195                let listeners = Arc::clone(&self.listeners);
196
197                self.observers.push(add_observer($notification, move || {
198                    $update_fn(info.clone());
199                    for (_, listener) in listeners.clone().lock().unwrap().iter() {
200                        listener(info.read().unwrap());
201                    }
202                }));
203            }};
204        }
205
206        add_observer_macro!(Notification::NowPlayingApplicationDidChange, update_app);
207
208        add_observer_macro!(Notification::NowPlayingInfoDidChange, update_info);
209        // add_observer_macro!(
210        //     Notification::NowPlayingApplicationClientStateDidChange,
211        //     update_info
212        // );
213        // add_observer_macro!(Notification::PlaybackQueueContentItemsChanged, update_info);
214        // add_observer_macro!(Notification::NowPlayingPlaybackQueueChanged, update_info);
215
216        add_observer_macro!(
217            Notification::NowPlayingApplicationIsPlayingDidChange,
218            update_state
219        );
220    }
221
222    /// Creates a new instance of `NowPlaying` and registers for playback notifications.
223    ///
224    /// This function initializes a new `NowPlaying` object, sets up necessary observers,
225    /// and ensures that media metadata is updated upon creation.
226    ///
227    /// # Returns
228    /// - `NowPlaying`: A new instance of the `NowPlaying` struct.
229    ///
230    /// # Example
231    /// ```rust
232    /// use media_remote::NowPlaying;
233    ///
234    /// let now_playing = NowPlaying::new();
235    /// ```
236    pub fn new() -> Self {
237        let mut new_instance = Self {
238            info: Arc::new(RwLock::new(None)),
239            observers: vec![],
240            listeners: Arc::new(Mutex::new(HashMap::new())),
241            token_counter: Arc::new(AtomicU64::new(0)),
242        };
243
244        new_instance.register();
245
246        new_instance
247    }
248
249    /// Retrieves the latest now playing information.
250    ///
251    /// This function provides a read-locked view of the current playing media metadata.
252    ///
253    /// # Note
254    /// - The lock should be released as soon as possible to minimize blocking time.
255    ///
256    /// # Returns
257    /// - `RwLockReadGuard<'_, Option<NowPlayingInfo>>`: A guard to the now playing metadata.
258    ///
259    /// # Example
260    /// ```rust
261    /// use media_remote::NowPlaying;
262    ///
263    /// let now_playing = NowPlaying::new();
264    /// let guard = now_playing.get_info();
265    /// let info = guard.as_ref();
266    ///
267    /// if let Some(info) = info {
268    ///     println!("Currently playing: {:?}", info.title);
269    /// }
270    ///
271    /// drop(guard);
272    /// ```
273    pub fn get_info(&self) -> RwLockReadGuard<'_, Option<NowPlayingInfo>> {
274        let mut info_guard = self.info.write().unwrap();
275        let info = info_guard.as_mut();
276
277        if info.is_some() {
278            let info = info.unwrap();
279            if info.is_playing.is_some_and(|x| x)
280                && info.elapsed_time.is_some()
281                && info.info_update_time.is_some()
282            {
283                info.elapsed_time = Some(
284                    info.elapsed_time.unwrap()
285                        + info.info_update_time.unwrap().elapsed().unwrap().as_secs() as f64,
286                );
287                info.info_update_time = Some(SystemTime::now())
288            }
289        }
290
291        drop(info_guard);
292
293        self.info.read().unwrap()
294    }
295}
296
297impl Drop for NowPlaying {
298    fn drop(&mut self) {
299        while let Some(observer) = self.observers.pop() {
300            remove_observer(observer);
301        }
302
303        unregister_for_now_playing_notifications();
304    }
305}
306
307impl Controller for NowPlaying {
308    fn is_info_some(&self) -> bool {
309        self.info.read().unwrap().as_ref().is_some()
310    }
311}
312
313impl Subscription for NowPlaying {
314    fn get_info(&self) -> RwLockReadGuard<'_, Option<NowPlayingInfo>> {
315        self.get_info()
316    }
317
318    fn get_token_counter(&self) -> Arc<AtomicU64> {
319        self.token_counter.clone()
320    }
321
322    fn get_listeners(
323        &self,
324    ) -> Arc<
325        Mutex<
326            HashMap<
327                super::subscription::ListenerToken,
328                Box<dyn Fn(RwLockReadGuard<'_, Option<NowPlayingInfo>>) + Send + Sync>,
329            >,
330        >,
331    > {
332        self.listeners.clone()
333    }
334}