gsmtc/
session.rs

1use crate::{
2    model::{Image, MediaModel, PlaybackStatus, SessionModel, TimelineModel},
3    util::{request_media_properties, ResultExt},
4};
5use std::{
6    convert::TryInto,
7    sync::{Arc, Weak},
8};
9use tokio::sync::mpsc;
10use tracing::{debug, event, warn, Level};
11use windows::{
12    core::{AgileReference, Result},
13    Foundation::{EventRegistrationToken, TypedEventHandler},
14    Media::Control::GlobalSystemMediaTransportControlsSession,
15};
16
17pub(crate) struct SessionHandle {
18    pub id: usize,
19    pub sender: Arc<mpsc::UnboundedSender<SessionCommand>>,
20}
21
22struct SessionWorker {
23    model: SessionModel,
24
25    session: GlobalSystemMediaTransportControlsSession,
26
27    loop_tx: Weak<mpsc::UnboundedSender<SessionCommand>>,
28    loop_rx: mpsc::UnboundedReceiver<SessionCommand>,
29
30    sess_tx: mpsc::UnboundedSender<SessionUpdateEvent>,
31
32    playback_token: EventRegistrationToken,
33    media_token: EventRegistrationToken,
34    timeline_token: EventRegistrationToken,
35}
36
37/// Events emitted by an internal session-worker.
38///
39/// The internal worker stops after the event-receiver is dropped and an attempt was made to send an event.
40#[derive(Debug)]
41pub enum SessionUpdateEvent {
42    /// The session was updated.
43    Model(SessionModel),
44    /// The current media of the session was updated.
45    Media(SessionModel, Option<Image>),
46}
47
48#[derive(Debug)]
49pub(crate) enum SessionCommand {
50    PlaybackInfoChanged,
51    MediaPropertiesChanged,
52    MediaPropertiesResult(Box<MediaModel>, Option<Image>), // TODO: boxing doesn't seem ideal here
53    TimelinePropertiesChanged,
54    Close,
55}
56
57impl SessionHandle {
58    pub fn create(
59        id: usize,
60        sess: GlobalSystemMediaTransportControlsSession,
61        sess_tx: mpsc::UnboundedSender<SessionUpdateEvent>,
62    ) -> Result<(Self, String)> {
63        let (loop_tx, loop_rx) = mpsc::unbounded_channel();
64        let loop_tx = Arc::new(loop_tx);
65
66        let source = sess.SourceAppUserModelId()?.to_string();
67        let session_tx = Arc::downgrade(&loop_tx);
68        let (playback_token, media_token, timeline_token) = (
69            sess.PlaybackInfoChanged(&feed_eventloop_handler(session_tx.clone(), || {
70                SessionCommand::PlaybackInfoChanged
71            }))?,
72            sess.MediaPropertiesChanged(&feed_eventloop_handler(session_tx.clone(), || {
73                SessionCommand::MediaPropertiesChanged
74            }))?,
75            sess.TimelinePropertiesChanged(&feed_eventloop_handler(session_tx.clone(), || {
76                SessionCommand::TimelinePropertiesChanged
77            }))?,
78        );
79        SessionWorker {
80            session: sess,
81            model: SessionModel {
82                playback: None,
83                timeline: None,
84                media: None,
85                source: source.clone(),
86            },
87
88            loop_tx: session_tx,
89            loop_rx,
90            sess_tx,
91
92            playback_token,
93            media_token,
94            timeline_token,
95        }
96        .spawn();
97
98        Ok((
99            Self {
100                sender: loop_tx,
101                id,
102            },
103            source,
104        ))
105    }
106}
107
108impl SessionWorker {
109    fn spawn(self) {
110        tokio::spawn(self.run());
111    }
112
113    async fn run(mut self) {
114        if let Some(loop_tx) = self.loop_tx.upgrade() {
115            loop_tx.send(SessionCommand::PlaybackInfoChanged).ok();
116            loop_tx.send(SessionCommand::TimelinePropertiesChanged).ok();
117            loop_tx.send(SessionCommand::MediaPropertiesChanged).ok();
118        }
119
120        while let Some(cmd) = self.loop_rx.recv().await {
121            match self.handle_command(cmd) {
122                Err(e) => {
123                    event!(Level::WARN, error = %e, source = %self.model.source, "Could not handle command")
124                }
125                Ok(false) => break,
126                _ => (),
127            }
128        }
129    }
130
131    /// Returns Result<running>
132    fn handle_command(&mut self, cmd: SessionCommand) -> Result<bool> {
133        event!(Level::TRACE, source = %self.model.source, command = ?cmd);
134        match cmd {
135            SessionCommand::PlaybackInfoChanged => {
136                let model = self.session.GetPlaybackInfo()?.try_into().opt()?;
137                if model != self.model.playback {
138                    self.model.playback = model;
139
140                    self.sess_tx
141                        .send(SessionUpdateEvent::Model(self.model.clone()))
142                        .ok();
143                }
144            }
145            SessionCommand::MediaPropertiesChanged => {
146                let loop_tx = self.loop_tx.clone();
147                let session = AgileReference::new(&self.session)?;
148                tokio::spawn(async move {
149                    match request_media_properties(loop_tx, session).await {
150                        Ok(None) => debug!("Empty media properties"),
151                        Err(e) => event!(Level::WARN, error = %e, "Could not get media properties"),
152                        _ => (),
153                    }
154                });
155            }
156            SessionCommand::TimelinePropertiesChanged => {
157                let model = self.session.GetTimelineProperties()?.try_into()?;
158                if !timeline_actually_the_same(&self.model.timeline, &model) {
159                    let should_skip = skip_timeline_emit(&self.model, &model);
160                    self.model.timeline = Some(model);
161
162                    if !should_skip {
163                        self.sess_tx
164                            .send(SessionUpdateEvent::Model(self.model.clone()))
165                            .ok();
166                    }
167                }
168            }
169            SessionCommand::MediaPropertiesResult(media, image) => {
170                self.model.media = Some(*media);
171                self.sess_tx
172                    .send(SessionUpdateEvent::Media(self.model.clone(), image))
173                    .ok();
174            }
175            SessionCommand::Close => return Ok(false),
176        };
177
178        Ok(true)
179    }
180}
181
182fn timeline_actually_the_same(first: &Option<TimelineModel>, second: &TimelineModel) -> bool {
183    first
184        .as_ref()
185        .map(|first| {
186            first.eq(second)
187                || (first.start == second.start
188                    && first.end == second.end
189                    && rough_eq(
190                        second.last_updated_at_ms - first.last_updated_at_ms,
191                        (second.position - first.position) / 10_000,
192                    ))
193        })
194        .unwrap_or_default()
195}
196
197#[inline]
198fn rough_eq(a: i64, b: i64) -> bool {
199    // either: a = b - 1 so b = a + 1
200    // or: a = b + 1 so b = a - 1
201    // or: a == b
202    a == b || (a < b + 2 && a > b - 2)
203}
204
205fn skip_timeline_emit(model: &SessionModel, new: &TimelineModel) -> bool {
206    model
207        .playback
208        .as_ref()
209        .map(|playback| {
210            playback.status == PlaybackStatus::Paused
211                && model
212                    .timeline
213                    .as_ref()
214                    .map(|old| {
215                        old.start == new.start && old.end == new.end && old.position == new.position
216                    })
217                    .unwrap_or_default()
218        })
219        .unwrap_or_default()
220}
221
222impl Drop for SessionWorker {
223    fn drop(&mut self) {
224        self.session
225            .RemovePlaybackInfoChanged(self.playback_token)
226            .ok();
227        self.session
228            .RemoveTimelinePropertiesChanged(self.timeline_token)
229            .ok();
230        self.session
231            .RemoveMediaPropertiesChanged(self.media_token)
232            .ok();
233    }
234}
235
236fn feed_eventloop_handler<F, LoopCmd, TSender, TResult>(
237    tx: Weak<mpsc::UnboundedSender<LoopCmd>>,
238    f: F,
239) -> TypedEventHandler<TSender, TResult>
240where
241    TSender: windows::core::RuntimeType,
242    TResult: windows::core::RuntimeType,
243    F: Fn() -> LoopCmd + Send + 'static,
244    LoopCmd: Send + 'static,
245{
246    TypedEventHandler::new(move |_, _| {
247        if let Some(tx) = tx.upgrade() {
248            if let Err(e) = tx.send(f()) {
249                warn!(error = %e, "Cannot send to event-loop from windows event handler");
250            }
251        }
252        Ok(())
253    })
254}