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