script/dom/
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 std::rc::Rc;
6
7use constellation_traits::ScriptToConstellationMessage;
8use dom_struct::dom_struct;
9use embedder_traits::{
10    MediaMetadata as EmbedderMediaMetadata, MediaSessionActionType, MediaSessionEvent,
11};
12use rustc_hash::FxBuildHasher;
13
14use super::bindings::trace::HashMapTracedValues;
15use crate::conversions::Convert;
16use crate::dom::bindings::callback::ExceptionHandling;
17use crate::dom::bindings::cell::DomRefCell;
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::num::Finite;
28use crate::dom::bindings::reflector::{DomGlobal, Reflector, reflect_dom_object};
29use crate::dom::bindings::root::{DomRoot, MutNullableDom};
30use crate::dom::bindings::str::DOMString;
31use crate::dom::html::htmlmediaelement::HTMLMediaElement;
32use crate::dom::mediametadata::MediaMetadata;
33use crate::dom::window::Window;
34use crate::realms::{InRealm, enter_realm};
35use crate::script_runtime::CanGc;
36
37#[dom_struct]
38pub(crate) struct MediaSession {
39    reflector_: Reflector,
40    /// <https://w3c.github.io/mediasession/#dom-mediasession-metadata>
41    #[ignore_malloc_size_of = "defined in embedder_traits"]
42    #[no_trace]
43    metadata: DomRefCell<Option<EmbedderMediaMetadata>>,
44    /// <https://w3c.github.io/mediasession/#dom-mediasession-playbackstate>
45    playback_state: DomRefCell<MediaSessionPlaybackState>,
46    /// <https://w3c.github.io/mediasession/#supported-media-session-actions>
47    #[ignore_malloc_size_of = "Rc"]
48    action_handlers: DomRefCell<
49        HashMapTracedValues<MediaSessionActionType, Rc<MediaSessionActionHandler>, FxBuildHasher>,
50    >,
51    /// The media instance controlled by this media session.
52    /// For now only HTMLMediaElements are controlled by media sessions.
53    media_instance: MutNullableDom<HTMLMediaElement>,
54}
55
56impl MediaSession {
57    #[cfg_attr(crown, allow(crown::unrooted_must_root))]
58    fn new_inherited() -> MediaSession {
59        MediaSession {
60            reflector_: Reflector::new(),
61            metadata: DomRefCell::new(None),
62            playback_state: DomRefCell::new(MediaSessionPlaybackState::None),
63            action_handlers: DomRefCell::new(HashMapTracedValues::new_fx()),
64            media_instance: Default::default(),
65        }
66    }
67
68    pub(crate) fn new(window: &Window, can_gc: CanGc) -> DomRoot<MediaSession> {
69        reflect_dom_object(Box::new(MediaSession::new_inherited()), window, can_gc)
70    }
71
72    pub(crate) fn register_media_instance(&self, media_instance: &HTMLMediaElement) {
73        self.media_instance.set(Some(media_instance));
74    }
75
76    pub(crate) fn handle_action(&self, action: MediaSessionActionType, can_gc: CanGc) {
77        debug!("Handle media session action {:?}", action);
78
79        if let Some(handler) = self.action_handlers.borrow().get(&action) {
80            if handler.Call__(ExceptionHandling::Report, can_gc).is_err() {
81                warn!("Error calling MediaSessionActionHandler callback");
82            }
83            return;
84        }
85
86        // Default action.
87        if let Some(media) = self.media_instance.get() {
88            match action {
89                MediaSessionActionType::Play => {
90                    let realm = enter_realm(self);
91                    media.Play(InRealm::Entered(&realm), can_gc);
92                },
93                MediaSessionActionType::Pause => {
94                    media.Pause(can_gc);
95                },
96                MediaSessionActionType::SeekBackward => {},
97                MediaSessionActionType::SeekForward => {},
98                MediaSessionActionType::PreviousTrack => {},
99                MediaSessionActionType::NextTrack => {},
100                MediaSessionActionType::SkipAd => {},
101                MediaSessionActionType::Stop => {},
102                MediaSessionActionType::SeekTo => {},
103            }
104        }
105    }
106
107    pub(crate) fn send_event(&self, event: MediaSessionEvent) {
108        let global = self.global();
109        let window = global.as_window();
110        let pipeline_id = window.pipeline_id();
111        window.send_to_constellation(ScriptToConstellationMessage::MediaSessionEvent(
112            pipeline_id,
113            event,
114        ));
115    }
116
117    pub(crate) fn update_title(&self, title: String) {
118        let mut metadata = self.metadata.borrow_mut();
119        if let Some(ref mut metadata) = *metadata {
120            // We only update the title with the data provided by the media
121            // player and iff the user did not provide a title.
122            if !metadata.title.is_empty() {
123                return;
124            }
125            metadata.title = title;
126        } else {
127            *metadata = Some(EmbedderMediaMetadata::new(title));
128        }
129        self.send_event(MediaSessionEvent::SetMetadata(
130            metadata.as_ref().unwrap().clone(),
131        ));
132    }
133}
134
135impl MediaSessionMethods<crate::DomTypeHolder> for MediaSession {
136    /// <https://w3c.github.io/mediasession/#dom-mediasession-metadata>
137    fn GetMetadata(&self, can_gc: CanGc) -> Option<DomRoot<MediaMetadata>> {
138        if let Some(ref metadata) = *self.metadata.borrow() {
139            let mut init = MediaMetadataInit::empty();
140            init.title = DOMString::from_string(metadata.title.clone());
141            init.artist = DOMString::from_string(metadata.artist.clone());
142            init.album = DOMString::from_string(metadata.album.clone());
143            let global = self.global();
144            Some(MediaMetadata::new(global.as_window(), &init, can_gc))
145        } else {
146            None
147        }
148    }
149
150    /// <https://w3c.github.io/mediasession/#dom-mediasession-metadata>
151    fn SetMetadata(&self, metadata: Option<&MediaMetadata>) {
152        if let Some(metadata) = metadata {
153            metadata.set_session(self);
154        }
155
156        let global = self.global();
157        let window = global.as_window();
158        let _metadata = match metadata {
159            Some(m) => {
160                let title = if m.Title().is_empty() {
161                    window.get_url().into_string()
162                } else {
163                    m.Title().into()
164                };
165                EmbedderMediaMetadata {
166                    title,
167                    artist: m.Artist().into(),
168                    album: m.Album().into(),
169                }
170            },
171            None => EmbedderMediaMetadata::new(window.get_url().into_string()),
172        };
173
174        *self.metadata.borrow_mut() = Some(_metadata.clone());
175
176        self.send_event(MediaSessionEvent::SetMetadata(_metadata));
177    }
178
179    /// <https://w3c.github.io/mediasession/#dom-mediasession-playbackstate>
180    fn PlaybackState(&self) -> MediaSessionPlaybackState {
181        *self.playback_state.borrow()
182    }
183
184    /// <https://w3c.github.io/mediasession/#dom-mediasession-playbackstate>
185    fn SetPlaybackState(&self, state: MediaSessionPlaybackState) {
186        *self.playback_state.borrow_mut() = state;
187    }
188
189    /// <https://w3c.github.io/mediasession/#update-action-handler-algorithm>
190    fn SetActionHandler(
191        &self,
192        action: MediaSessionAction,
193        handler: Option<Rc<MediaSessionActionHandler>>,
194    ) {
195        match handler {
196            Some(handler) => self
197                .action_handlers
198                .borrow_mut()
199                .insert(action.convert(), handler.clone()),
200            None => self.action_handlers.borrow_mut().remove(&action.convert()),
201        };
202    }
203
204    /// <https://w3c.github.io/mediasession/#dom-mediasession-setpositionstate>
205    fn SetPositionState(&self, state: &MediaPositionState) -> Fallible<()> {
206        // If the state is an empty dictionary then clear the position state.
207        if state.duration.is_none() && state.position.is_none() && state.playbackRate.is_none() {
208            if let Some(media_instance) = self.media_instance.get() {
209                media_instance.reset();
210            }
211            return Ok(());
212        }
213
214        // If the duration is not present or its value is null, throw a TypeError.
215        if state.duration.is_none() {
216            return Err(Error::Type(
217                "duration is not present or its value is null".to_owned(),
218            ));
219        }
220
221        // If the duration is negative, throw a TypeError.
222        if let Some(state_duration) = state.duration {
223            if *state_duration < 0.0 {
224                return Err(Error::Type("duration is negative".to_owned()));
225            }
226        }
227
228        // If the position is negative or greater than duration, throw a TypeError.
229        if let Some(state_position) = state.position {
230            if *state_position < 0.0 {
231                return Err(Error::Type("position is negative".to_owned()));
232            }
233            if let Some(state_duration) = state.duration {
234                if *state_position > *state_duration {
235                    return Err(Error::Type("position is greater than duration".to_owned()));
236                }
237            }
238        }
239
240        // If the playbackRate is zero throw a TypeError.
241        if let Some(state_playback_rate) = state.playbackRate {
242            if *state_playback_rate <= 0.0 {
243                return Err(Error::Type("playbackRate is zero".to_owned()));
244            }
245        }
246
247        // Update the position state and last position updated time.
248        if let Some(media_instance) = self.media_instance.get() {
249            media_instance.set_duration(state.duration.map(|v| *v).unwrap());
250            // If the playbackRate is not present or its value is null, set it to 1.0.
251            media_instance.SetPlaybackRate(state.playbackRate.unwrap_or(Finite::wrap(1.0)))?;
252            // If the position is not present or its value is null, set it to zero.
253            media_instance.SetCurrentTime(state.position.unwrap_or(Finite::wrap(0.0)));
254        }
255
256        Ok(())
257    }
258}
259
260impl Convert<MediaSessionActionType> for MediaSessionAction {
261    fn convert(self) -> MediaSessionActionType {
262        match self {
263            MediaSessionAction::Play => MediaSessionActionType::Play,
264            MediaSessionAction::Pause => MediaSessionActionType::Pause,
265            MediaSessionAction::Seekbackward => MediaSessionActionType::SeekBackward,
266            MediaSessionAction::Seekforward => MediaSessionActionType::SeekForward,
267            MediaSessionAction::Previoustrack => MediaSessionActionType::PreviousTrack,
268            MediaSessionAction::Nexttrack => MediaSessionActionType::NextTrack,
269            MediaSessionAction::Skipad => MediaSessionActionType::SkipAd,
270            MediaSessionAction::Stop => MediaSessionActionType::Stop,
271            MediaSessionAction::Seekto => MediaSessionActionType::SeekTo,
272        }
273    }
274}