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