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