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