Skip to main content

script/dom/media/
mediasession.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5use dom_struct::dom_struct;
6use embedder_traits::{
7    MediaMetadata as EmbedderMediaMetadata, MediaPositionState as EmbedderMediaPositionState,
8    MediaSessionActionType, MediaSessionEvent,
9};
10use js::context::JSContext;
11use rustc_hash::FxBuildHasher;
12use script_bindings::cell::DomRefCell;
13use script_bindings::reflector::{Reflector, reflect_dom_object};
14use servo_constellation_traits::ScriptToConstellationMessage;
15
16use crate::conversions::Convert;
17use crate::dom::bindings::callback::{ExceptionHandling, RootedCallback, TracedCallback};
18use crate::dom::bindings::codegen::Bindings::HTMLMediaElementBinding::HTMLMediaElementMethods;
19use crate::dom::bindings::codegen::Bindings::MediaMetadataBinding::{
20    MediaMetadataInit, MediaMetadataMethods,
21};
22use crate::dom::bindings::codegen::Bindings::MediaSessionBinding::{
23    MediaPositionState, MediaSessionAction, MediaSessionActionHandler, MediaSessionMethods,
24    MediaSessionPlaybackState,
25};
26use crate::dom::bindings::error::{Error, Fallible};
27use crate::dom::bindings::reflector::DomGlobal;
28use crate::dom::bindings::root::DomRoot;
29use crate::dom::bindings::trace::HashMapTracedValues;
30use crate::dom::html::htmlmediaelement::{HTMLMediaElement, MediaElementWeakRef};
31use crate::dom::media::mediametadata::MediaMetadata;
32use crate::dom::window::Window;
33use crate::realms::enter_auto_realm;
34
35#[dom_struct]
36pub(crate) struct MediaSession {
37    reflector_: Reflector,
38    /// <https://w3c.github.io/mediasession/#dom-mediasession-metadata>
39    #[no_trace]
40    metadata: DomRefCell<Option<EmbedderMediaMetadata>>,
41    /// <https://w3c.github.io/mediasession/#dom-mediasession-playbackstate>
42    playback_state: DomRefCell<MediaSessionPlaybackState>,
43    /// <https://w3c.github.io/mediasession/#supported-media-session-actions>
44    action_handlers: DomRefCell<
45        HashMapTracedValues<
46            MediaSessionActionType,
47            TracedCallback<MediaSessionActionHandler>,
48            FxBuildHasher,
49        >,
50    >,
51    /// The media instance controlled by this media session.
52    /// For now only HTMLMediaElements are controlled by media sessions.
53    media_instance: DomRefCell<Option<MediaElementWeakRef>>,
54}
55
56impl MediaSession {
57    fn new_inherited() -> MediaSession {
58        MediaSession {
59            reflector_: Reflector::new(),
60            metadata: DomRefCell::new(None),
61            playback_state: DomRefCell::new(MediaSessionPlaybackState::None),
62            action_handlers: DomRefCell::new(HashMapTracedValues::new_fx()),
63            media_instance: DomRefCell::new(None),
64        }
65    }
66
67    pub(crate) fn new(cx: &mut JSContext, window: &Window) -> DomRoot<MediaSession> {
68        reflect_dom_object(cx, Box::new(MediaSession::new_inherited()), window)
69    }
70
71    pub(crate) fn register_media_instance(&self, media_instance: &HTMLMediaElement) {
72        *self.media_instance.borrow_mut() = Some(MediaElementWeakRef::new(media_instance));
73    }
74
75    pub(crate) fn handle_action(
76        &self,
77        cx: &mut js::context::JSContext,
78        action: MediaSessionActionType,
79    ) {
80        debug!("Handle media session action {:?}", action);
81
82        if let Some(handler) = self.action_handlers.borrow().get(&action) {
83            if handler.Call__(cx, ExceptionHandling::Report).is_err() {
84                warn!("Error calling MediaSessionActionHandler callback");
85            }
86            return;
87        }
88
89        // Default action. The borrow of the media instance is released before the
90        // element is used, since calling into it can run script.
91        let media_instance = {
92            let media_instance = self.media_instance.borrow();
93            media_instance.as_ref().and_then(|media| media.root())
94        };
95        if let Some(media) = media_instance {
96            match action {
97                MediaSessionActionType::Play => {
98                    let mut realm = enter_auto_realm(cx, self);
99                    let mut realm = realm.current_realm();
100                    media.Play(&mut realm);
101                },
102                MediaSessionActionType::Pause => {
103                    media.Pause(cx);
104                },
105                MediaSessionActionType::SeekBackward => {},
106                MediaSessionActionType::SeekForward => {},
107                MediaSessionActionType::PreviousTrack => {},
108                MediaSessionActionType::NextTrack => {},
109                MediaSessionActionType::SkipAd => {},
110                MediaSessionActionType::Stop => {},
111                MediaSessionActionType::SeekTo => {},
112            }
113        }
114    }
115
116    pub(crate) fn send_event(&self, event: MediaSessionEvent) {
117        let global = self.global();
118        let window = global.as_window();
119        let pipeline_id = window.pipeline_id();
120        window.send_to_constellation(ScriptToConstellationMessage::MediaSessionEvent(
121            pipeline_id,
122            event,
123        ));
124    }
125
126    pub(crate) fn update_title(&self, title: String) {
127        let mut metadata = self.metadata.borrow_mut();
128        if let Some(ref mut metadata) = *metadata {
129            // We only update the title with the data provided by the media
130            // player and iff the user did not provide a title.
131            if !metadata.title.is_empty() {
132                return;
133            }
134            metadata.title = title;
135        } else {
136            *metadata = Some(EmbedderMediaMetadata::new(title));
137        }
138        self.send_event(MediaSessionEvent::SetMetadata(
139            metadata.as_ref().unwrap().clone(),
140        ));
141    }
142}
143
144impl MediaSessionMethods<crate::DomTypeHolder> for MediaSession {
145    /// <https://w3c.github.io/mediasession/#dom-mediasession-metadata>
146    fn GetMetadata(&self, cx: &mut JSContext) -> Option<DomRoot<MediaMetadata>> {
147        if let Some(ref metadata) = *self.metadata.borrow() {
148            let mut init = MediaMetadataInit::empty();
149            init.title = metadata.title.clone().into();
150            init.artist = metadata.artist.clone().into();
151            init.album = metadata.album.clone().into();
152            let global = self.global();
153            Some(MediaMetadata::new(cx, global.as_window(), &init))
154        } else {
155            None
156        }
157    }
158
159    /// <https://w3c.github.io/mediasession/#dom-mediasession-metadata>
160    fn SetMetadata(&self, metadata: Option<&MediaMetadata>) {
161        if let Some(metadata) = metadata {
162            metadata.set_session(self);
163        }
164
165        let global = self.global();
166        let window = global.as_window();
167        let _metadata = match metadata {
168            Some(m) => {
169                let title = if m.Title().is_empty() {
170                    window.get_url().into_string()
171                } else {
172                    m.Title().into()
173                };
174                EmbedderMediaMetadata {
175                    title,
176                    artist: m.Artist().into(),
177                    album: m.Album().into(),
178                }
179            },
180            None => EmbedderMediaMetadata::new(window.get_url().into_string()),
181        };
182
183        *self.metadata.borrow_mut() = Some(_metadata.clone());
184
185        self.send_event(MediaSessionEvent::SetMetadata(_metadata));
186    }
187
188    /// <https://w3c.github.io/mediasession/#dom-mediasession-playbackstate>
189    fn PlaybackState(&self) -> MediaSessionPlaybackState {
190        *self.playback_state.borrow()
191    }
192
193    /// <https://w3c.github.io/mediasession/#dom-mediasession-playbackstate>
194    fn SetPlaybackState(&self, state: MediaSessionPlaybackState) {
195        *self.playback_state.borrow_mut() = state;
196    }
197
198    /// <https://w3c.github.io/mediasession/#update-action-handler-algorithm>
199    fn SetActionHandler(
200        &self,
201        action: MediaSessionAction,
202        handler: Option<RootedCallback<MediaSessionActionHandler>>,
203    ) {
204        match handler {
205            Some(handler) => self
206                .action_handlers
207                .borrow_mut()
208                .insert(action.convert(), handler.to_traced()),
209            None => self.action_handlers.borrow_mut().remove(&action.convert()),
210        };
211    }
212
213    /// <https://w3c.github.io/mediasession/#dom-mediasession-setpositionstate>
214    fn SetPositionState(&self, state: &MediaPositionState) -> Fallible<()> {
215        // If state is an empty dictionary, clear the position state and abort these steps.
216        if state.duration.is_none() && state.position.is_none() && state.playbackRate.is_none() {
217            let position_state = EmbedderMediaPositionState::new(0.0, 0.0, 0.0);
218            self.send_event(MediaSessionEvent::SetPositionState(position_state));
219
220            return Ok(());
221        }
222
223        let duration = if let Some(state_duration) = state.duration {
224            // If state’s duration is negative or NaN, throw a TypeError.
225            if state_duration < 0.0 || state_duration.is_nan() {
226                return Err(Error::Type(c"Duration is negative or NaN".to_owned()));
227            }
228            state_duration
229        } else {
230            // If state’s duration is not present, throw a TypeError.
231            return Err(Error::Type(c"Duration is not present".to_owned()));
232        };
233
234        let position = if let Some(state_position) = state.position {
235            // If state’s position is negative or greater than duration, throw a TypeError.
236            if *state_position < 0.0 || *state_position > duration {
237                return Err(Error::Type(
238                    c"Position is negative or greater than duration".to_owned(),
239                ));
240            }
241            *state_position
242        } else {
243            // If state’s position is not present, set it to zero.
244            0.0
245        };
246
247        let playback_rate = if let Some(state_playback_rate) = state.playbackRate {
248            // If state’s playbackRate is zero, throw a TypeError.
249            if *state_playback_rate == 0.0 {
250                return Err(Error::Type(c"Playback rate is zero".to_owned()));
251            }
252            *state_playback_rate
253        } else {
254            // If state’s playbackRate is not present, set it to 1.0.
255            1.0
256        };
257
258        // Update the position state and last position updated time.
259        let position_state = EmbedderMediaPositionState::new(duration, playback_rate, position);
260        self.send_event(MediaSessionEvent::SetPositionState(position_state));
261
262        Ok(())
263    }
264}
265
266impl Convert<MediaSessionActionType> for MediaSessionAction {
267    fn convert(self) -> MediaSessionActionType {
268        match self {
269            MediaSessionAction::Play => MediaSessionActionType::Play,
270            MediaSessionAction::Pause => MediaSessionActionType::Pause,
271            MediaSessionAction::Seekbackward => MediaSessionActionType::SeekBackward,
272            MediaSessionAction::Seekforward => MediaSessionActionType::SeekForward,
273            MediaSessionAction::Previoustrack => MediaSessionActionType::PreviousTrack,
274            MediaSessionAction::Nexttrack => MediaSessionActionType::NextTrack,
275            MediaSessionAction::Skipad => MediaSessionActionType::SkipAd,
276            MediaSessionAction::Stop => MediaSessionActionType::Stop,
277            MediaSessionAction::Seekto => MediaSessionActionType::SeekTo,
278        }
279    }
280}