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    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(&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.root() {
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 state is an empty dictionary, clear the position state and abort these steps.
207        if state.duration.is_none() && state.position.is_none() && state.playbackRate.is_none() {
208            let position_state = EmbedderMediaPositionState::new(0.0, 0.0, 0.0);
209            self.send_event(MediaSessionEvent::SetPositionState(position_state));
210
211            return Ok(());
212        }
213
214        let duration = if let Some(state_duration) = state.duration {
215            // If state’s duration is negative or NaN, throw a TypeError.
216            if state_duration < 0.0 || state_duration.is_nan() {
217                return Err(Error::Type("Duration is negative or NaN".to_owned()));
218            }
219            state_duration
220        } else {
221            // If state’s duration is not present, throw a TypeError.
222            return Err(Error::Type("Duration is not present".to_owned()));
223        };
224
225        let position = if let Some(state_position) = state.position {
226            // If state’s position is negative or greater than duration, throw a TypeError.
227            if *state_position < 0.0 || *state_position > duration {
228                return Err(Error::Type(
229                    "Position is negative or greater than duration".to_owned(),
230                ));
231            }
232            *state_position
233        } else {
234            // If state’s position is not present, set it to zero.
235            0.0
236        };
237
238        let playback_rate = if let Some(state_playback_rate) = state.playbackRate {
239            // If state’s playbackRate is zero, throw a TypeError.
240            if *state_playback_rate == 0.0 {
241                return Err(Error::Type("Playback rate is zero".to_owned()));
242            }
243            *state_playback_rate
244        } else {
245            // If state’s playbackRate is not present, set it to 1.0.
246            1.0
247        };
248
249        // Update the position state and last position updated time.
250        let position_state = EmbedderMediaPositionState::new(duration, playback_rate, position);
251        self.send_event(MediaSessionEvent::SetPositionState(position_state));
252
253        Ok(())
254    }
255}
256
257impl Convert<MediaSessionActionType> for MediaSessionAction {
258    fn convert(self) -> MediaSessionActionType {
259        match self {
260            MediaSessionAction::Play => MediaSessionActionType::Play,
261            MediaSessionAction::Pause => MediaSessionActionType::Pause,
262            MediaSessionAction::Seekbackward => MediaSessionActionType::SeekBackward,
263            MediaSessionAction::Seekforward => MediaSessionActionType::SeekForward,
264            MediaSessionAction::Previoustrack => MediaSessionActionType::PreviousTrack,
265            MediaSessionAction::Nexttrack => MediaSessionActionType::NextTrack,
266            MediaSessionAction::Skipad => MediaSessionActionType::SkipAd,
267            MediaSessionAction::Stop => MediaSessionActionType::Stop,
268            MediaSessionAction::Seekto => MediaSessionActionType::SeekTo,
269        }
270    }
271}