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