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#[derive(Debug)]
41pub enum SessionUpdateEvent {
42 Model(SessionModel),
44 Media(SessionModel, Option<Image>),
46}
47
48#[derive(Debug)]
49pub(crate) enum SessionCommand {
50 PlaybackInfoChanged,
51 MediaPropertiesChanged,
52 MediaPropertiesResult(Box<MediaModel>, Option<Image>), 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 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 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}