aboutsummaryrefslogtreecommitdiffstats
path: root/components/script/dom/htmlmediaelement.rs
diff options
context:
space:
mode:
Diffstat (limited to 'components/script/dom/htmlmediaelement.rs')
-rw-r--r--components/script/dom/htmlmediaelement.rs1111
1 files changed, 646 insertions, 465 deletions
diff --git a/components/script/dom/htmlmediaelement.rs b/components/script/dom/htmlmediaelement.rs
index e2b31e918de..0d54d188e59 100644
--- a/components/script/dom/htmlmediaelement.rs
+++ b/components/script/dom/htmlmediaelement.rs
@@ -330,6 +330,34 @@ impl From<MediaStreamOrBlob> for SrcObject {
}
}
+#[derive(JSTraceable, MallocSizeOf)]
+struct DroppableHtmlMediaElement {
+ /// Player Id reported the player thread
+ player_id: Cell<u64>,
+ #[ignore_malloc_size_of = "Defined in other crates"]
+ #[no_trace]
+ player_context: WindowGLContext,
+}
+
+impl DroppableHtmlMediaElement {
+ fn new(player_id: Cell<u64>, player_context: WindowGLContext) -> Self {
+ Self {
+ player_id,
+ player_context,
+ }
+ }
+
+ pub(crate) fn set_player_id(&self, id: u64) {
+ self.player_id.set(id);
+ }
+}
+
+impl Drop for DroppableHtmlMediaElement {
+ fn drop(&mut self) {
+ self.player_context
+ .send(GLPlayerMsg::UnregisterPlayer(self.player_id.get()));
+ }
+}
#[dom_struct]
#[allow(non_snake_case)]
pub(crate) struct HTMLMediaElement {
@@ -411,16 +439,12 @@ pub(crate) struct HTMLMediaElement {
next_timeupdate_event: Cell<Instant>,
/// Latest fetch request context.
current_fetch_context: DomRefCell<Option<HTMLMediaElementFetchContext>>,
- /// Player Id reported the player thread
- id: Cell<u64>,
/// Media controls id.
/// In order to workaround the lack of privileged JS context, we secure the
/// the access to the "privileged" document.servoGetMediaControls(id) API by
/// keeping a whitelist of media controls identifiers.
media_controls_id: DomRefCell<Option<String>>,
- #[ignore_malloc_size_of = "Defined in other crates"]
- #[no_trace]
- player_context: WindowGLContext,
+ droppable: DroppableHtmlMediaElement,
}
/// <https://html.spec.whatwg.org/multipage/#dom-media-networkstate>
@@ -488,9 +512,11 @@ impl HTMLMediaElement {
text_tracks_list: Default::default(),
next_timeupdate_event: Cell::new(Instant::now() + Duration::from_millis(250)),
current_fetch_context: DomRefCell::new(None),
- id: Cell::new(0),
media_controls_id: DomRefCell::new(None),
- player_context: document.window().get_player_context(),
+ droppable: DroppableHtmlMediaElement::new(
+ Cell::new(0),
+ document.window().get_player_context(),
+ ),
}
}
@@ -878,6 +904,9 @@ impl HTMLMediaElement {
fn fetch_request(&self, offset: Option<u64>, seek_lock: Option<SeekLock>) {
if self.resource_url.borrow().is_none() && self.blob_url.borrow().is_none() {
eprintln!("Missing request url");
+ if let Some(seek_lock) = seek_lock {
+ seek_lock.unlock(/* successful seek */ false);
+ }
self.queue_dedicated_media_source_failure_steps();
return;
}
@@ -923,9 +952,17 @@ impl HTMLMediaElement {
*current_fetch_context = Some(HTMLMediaElementFetchContext::new(request.id));
let listener =
- HTMLMediaElementFetchListener::new(self, url.clone(), offset.unwrap_or(0), seek_lock);
+ HTMLMediaElementFetchListener::new(self, request.id, url.clone(), offset.unwrap_or(0));
self.owner_document().fetch_background(request, listener);
+
+ // Since we cancelled the previous fetch, from now on the media element
+ // will only receive response data from the new fetch that's been
+ // initiated. This means the player can resume operation, since all subsequent data
+ // pushes will originate from the new seek offset.
+ if let Some(seek_lock) = seek_lock {
+ seek_lock.unlock(/* successful seek */ true);
+ }
}
// https://html.spec.whatwg.org/multipage/#concept-media-load-resource
@@ -1357,6 +1394,10 @@ impl HTMLMediaElement {
task_source.queue_simple_event(self.upcast(), atom!("seeked"));
}
+ fn set_player_id(&self, player_id: u64) {
+ self.droppable.set_player_id(player_id);
+ }
+
/// <https://html.spec.whatwg.org/multipage/#poster-frame>
pub(crate) fn process_poster_image_loaded(&self, image: Arc<RasterImage>) {
if !self.show_poster.get() {
@@ -1414,6 +1455,7 @@ impl HTMLMediaElement {
audio_renderer,
Box::new(window.get_player_context()),
);
+ let player_id = player.lock().unwrap().get_id();
*self.player.borrow_mut() = Some(player);
@@ -1430,7 +1472,7 @@ impl HTMLMediaElement {
trace!("Player event {:?}", event);
let this = trusted_node.clone();
task_source.queue(task!(handle_player_event: move || {
- this.root().handle_player_event(&event, CanGc::note());
+ this.root().handle_player_event(player_id, &event, CanGc::note());
}));
}),
);
@@ -1451,7 +1493,7 @@ impl HTMLMediaElement {
})
.unwrap_or((0, None));
- self.id.set(player_id);
+ self.set_player_id(player_id);
self.video_renderer.lock().unwrap().player_id = Some(player_id);
if let Some(image_receiver) = image_receiver {
@@ -1514,406 +1556,458 @@ impl HTMLMediaElement {
}
}
- fn handle_player_event(&self, event: &PlayerEvent, can_gc: CanGc) {
- match *event {
- PlayerEvent::EndOfStream => {
- // https://html.spec.whatwg.org/multipage/#media-data-processing-steps-list
- // => "If the media data can be fetched but is found by inspection to be in
- // an unsupported format, or can otherwise not be rendered at all"
- if self.ready_state.get() < ReadyState::HaveMetadata {
- self.queue_dedicated_media_source_failure_steps();
- } else {
- // https://html.spec.whatwg.org/multipage/#reaches-the-end
- match self.direction_of_playback() {
- PlaybackDirection::Forwards => {
- // Step 1.
- if self.Loop() {
- self.seek(
- self.earliest_possible_position(),
- /* approximate_for_speed*/ false,
- );
- } else {
- // Step 2.
- // The **ended playback** condition is implemented inside of
- // the HTMLMediaElementMethods::Ended method
-
- // Step 3.
- let this = Trusted::new(self);
-
- self.owner_global().task_manager().media_element_task_source().queue(
- task!(reaches_the_end_steps: move || {
- let this = this.root();
- // Step 3.1.
- this.upcast::<EventTarget>().fire_event(atom!("timeupdate"), CanGc::note());
-
- // Step 3.2.
- if this.Ended() && !this.Paused() {
- // Step 3.2.1.
- this.paused.set(true);
-
- // Step 3.2.2.
- this.upcast::<EventTarget>().fire_event(atom!("pause"), CanGc::note());
-
- // Step 3.2.3.
- this.take_pending_play_promises(Err(Error::Abort));
- this.fulfill_in_flight_play_promises(|| ());
- }
-
- // Step 3.3.
- this.upcast::<EventTarget>().fire_event(atom!("ended"), CanGc::note());
- })
- );
-
- // https://html.spec.whatwg.org/multipage/#dom-media-have_current_data
- self.change_ready_state(ReadyState::HaveCurrentData);
- }
- },
+ fn end_of_playback_in_forwards_direction(&self) {
+ // Step 1. If the media element has a loop attribute specified, then seek to the earliest
+ // posible position of the media resource and return.
+ if self.Loop() {
+ self.seek(
+ self.earliest_possible_position(),
+ /* approximate_for_speed*/ false,
+ );
+ return;
+ }
+ // Step 2. The ended IDL attribute starts returning true once the event loop returns to
+ // step 1.
+ // The **ended playback** condition is implemented inside of
+ // the HTMLMediaElementMethods::Ended method
- PlaybackDirection::Backwards => {
- if self.playback_position.get() <= self.earliest_possible_position() {
- self.owner_global()
- .task_manager()
- .media_element_task_source()
- .queue_simple_event(self.upcast(), atom!("ended"));
- }
- },
- }
- }
- },
- PlayerEvent::Error(ref error) => {
- error!("Player error: {:?}", error);
+ // Step 3. Queue a media element task given the media element and the following steps:
+ let this = Trusted::new(self);
- // If we have already flagged an error condition while processing
- // the network response, we should silently skip any observable
- // errors originating while decoding the erroneous response.
- if self.in_error_state() {
- return;
+ self.owner_global()
+ .task_manager()
+ .media_element_task_source()
+ .queue(task!(reaches_the_end_steps: move || {
+ let this = this.root();
+ // Step 3.1. Fire an event named timeupdate at the media element
+ this.upcast::<EventTarget>().fire_event(atom!("timeupdate"), CanGc::note());
+
+ // Step 3.2. If the media element has ended playback, the direction of playback is
+ // forwards, and paused is false, then:
+ if this.Ended() && !this.Paused() {
+ // Step 3.2.1. Set the paused attribute to true
+ this.paused.set(true);
+
+ // Step 3.2.2. Fire an event named pause at the media element
+ this.upcast::<EventTarget>().fire_event(atom!("pause"), CanGc::note());
+
+ // Step 3.2.3. Take pending play promises and reject pending play promises with
+ // the result and an "AbortError" DOMException
+ this.take_pending_play_promises(Err(Error::Abort));
+ this.fulfill_in_flight_play_promises(|| ());
}
- // https://html.spec.whatwg.org/multipage/#loading-the-media-resource:media-data-13
- // 1. The user agent should cancel the fetching process.
- if let Some(ref mut current_fetch_context) =
- *self.current_fetch_context.borrow_mut()
- {
- current_fetch_context.cancel(CancelReason::Error);
- }
- // 2. Set the error attribute to the result of creating a MediaError with MEDIA_ERR_DECODE.
- self.error.set(Some(&*MediaError::new(
- &self.owner_window(),
- MEDIA_ERR_DECODE,
- can_gc,
- )));
+ // Step 3.3. Fire an event named ended at the media element.
+ this.upcast::<EventTarget>().fire_event(atom!("ended"), CanGc::note());
+ }));
- // 3. Set the element's networkState attribute to the NETWORK_IDLE value.
- self.network_state.set(NetworkState::Idle);
+ // https://html.spec.whatwg.org/multipage/#dom-media-have_current_data
+ self.change_ready_state(ReadyState::HaveCurrentData);
+ }
- // 4. Set the element's delaying-the-load-event flag to false. This stops delaying the load event.
- self.delay_load_event(false, can_gc);
+ fn playback_end(&self) {
+ // https://html.spec.whatwg.org/multipage/#media-data-processing-steps-list
+ // => "If the media data can be fetched but is found by inspection to be in
+ // an unsupported format, or can otherwise not be rendered at all"
+ if self.ready_state.get() < ReadyState::HaveMetadata {
+ self.queue_dedicated_media_source_failure_steps();
+ return;
+ }
- // 5. Fire an event named error at the media element.
- self.upcast::<EventTarget>()
- .fire_event(atom!("error"), can_gc);
+ // https://html.spec.whatwg.org/multipage/#reaches-the-end
+ match self.direction_of_playback() {
+ PlaybackDirection::Forwards => self.end_of_playback_in_forwards_direction(),
- // TODO: 6. Abort the overall resource selection algorithm.
- },
- PlayerEvent::VideoFrameUpdated => {
- // Check if the frame was resized
- if let Some(frame) = self.video_renderer.lock().unwrap().current_frame {
- self.handle_resize(Some(frame.width as u32), Some(frame.height as u32));
+ PlaybackDirection::Backwards => {
+ if self.playback_position.get() <= self.earliest_possible_position() {
+ self.owner_global()
+ .task_manager()
+ .media_element_task_source()
+ .queue_simple_event(self.upcast(), atom!("ended"));
}
},
- PlayerEvent::MetadataUpdated(ref metadata) => {
- // https://html.spec.whatwg.org/multipage/#media-data-processing-steps-list
- // => If the media resource is found to have an audio track
- if !metadata.audio_tracks.is_empty() {
- for (i, _track) in metadata.audio_tracks.iter().enumerate() {
- // Step 1.
- let kind = match i {
- 0 => DOMString::from("main"),
- _ => DOMString::new(),
- };
- let window = self.owner_window();
- let audio_track = AudioTrack::new(
- &window,
- DOMString::new(),
- kind,
- DOMString::new(),
- DOMString::new(),
- Some(&*self.AudioTracks()),
- can_gc,
- );
-
- // Steps 2. & 3.
- self.AudioTracks().add(&audio_track);
-
- // Step 4
- if let Some(servo_url) = self.resource_url.borrow().as_ref() {
- let fragment = MediaFragmentParser::from(servo_url);
- if let Some(id) = fragment.id() {
- if audio_track.id() == DOMString::from(id) {
- self.AudioTracks()
- .set_enabled(self.AudioTracks().len() - 1, true);
- }
- }
+ }
+ }
- if fragment.tracks().contains(&audio_track.kind().into()) {
- self.AudioTracks()
- .set_enabled(self.AudioTracks().len() - 1, true);
- }
- }
+ fn playback_error(&self, error: &str, can_gc: CanGc) {
+ error!("Player error: {:?}", error);
+
+ // If we have already flagged an error condition while processing
+ // the network response, we should silently skip any observable
+ // errors originating while decoding the erroneous response.
+ if self.in_error_state() {
+ return;
+ }
+
+ // https://html.spec.whatwg.org/multipage/#loading-the-media-resource:media-data-13
+ // 1. The user agent should cancel the fetching process.
+ if let Some(ref mut current_fetch_context) = *self.current_fetch_context.borrow_mut() {
+ current_fetch_context.cancel(CancelReason::Error);
+ }
+ // 2. Set the error attribute to the result of creating a MediaError with MEDIA_ERR_DECODE.
+ self.error.set(Some(&*MediaError::new(
+ &self.owner_window(),
+ MEDIA_ERR_DECODE,
+ can_gc,
+ )));
+
+ // 3. Set the element's networkState attribute to the NETWORK_IDLE value.
+ self.network_state.set(NetworkState::Idle);
+
+ // 4. Set the element's delaying-the-load-event flag to false. This stops delaying the load event.
+ self.delay_load_event(false, can_gc);
+
+ // 5. Fire an event named error at the media element.
+ self.upcast::<EventTarget>()
+ .fire_event(atom!("error"), can_gc);
+
+ // TODO: 6. Abort the overall resource selection algorithm.
+ }
+
+ fn playback_metadata_updated(
+ &self,
+ metadata: &servo_media::player::metadata::Metadata,
+ can_gc: CanGc,
+ ) {
+ // https://html.spec.whatwg.org/multipage/#media-data-processing-steps-list
+ // => If the media resource is found to have an audio track
+ if !metadata.audio_tracks.is_empty() {
+ for (i, _track) in metadata.audio_tracks.iter().enumerate() {
+ // Step 1.
+ let kind = match i {
+ 0 => DOMString::from("main"),
+ _ => DOMString::new(),
+ };
+ let window = self.owner_window();
+ let audio_track = AudioTrack::new(
+ &window,
+ DOMString::new(),
+ kind,
+ DOMString::new(),
+ DOMString::new(),
+ Some(&*self.AudioTracks()),
+ can_gc,
+ );
+
+ // Steps 2. & 3.
+ self.AudioTracks().add(&audio_track);
- // Step 5. & 6,
- if self.AudioTracks().enabled_index().is_none() {
+ // Step 4
+ if let Some(servo_url) = self.resource_url.borrow().as_ref() {
+ let fragment = MediaFragmentParser::from(servo_url);
+ if let Some(id) = fragment.id() {
+ if audio_track.id() == DOMString::from(id) {
self.AudioTracks()
.set_enabled(self.AudioTracks().len() - 1, true);
}
+ }
- // Steps 7.
- let event = TrackEvent::new(
- self.global().as_window(),
- atom!("addtrack"),
- false,
- false,
- &Some(VideoTrackOrAudioTrackOrTextTrack::AudioTrack(audio_track)),
- can_gc,
- );
-
- event
- .upcast::<Event>()
- .fire(self.upcast::<EventTarget>(), can_gc);
+ if fragment.tracks().contains(&audio_track.kind().into()) {
+ self.AudioTracks()
+ .set_enabled(self.AudioTracks().len() - 1, true);
}
}
- // => If the media resource is found to have a video track
- if !metadata.video_tracks.is_empty() {
- for (i, _track) in metadata.video_tracks.iter().enumerate() {
- // Step 1.
- let kind = match i {
- 0 => DOMString::from("main"),
- _ => DOMString::new(),
- };
- let window = self.owner_window();
- let video_track = VideoTrack::new(
- &window,
- DOMString::new(),
- kind,
- DOMString::new(),
- DOMString::new(),
- Some(&*self.VideoTracks()),
- can_gc,
- );
-
- // Steps 2. & 3.
- self.VideoTracks().add(&video_track);
-
- // Step 4.
- if let Some(track) = self.VideoTracks().item(0) {
- if let Some(servo_url) = self.resource_url.borrow().as_ref() {
- let fragment = MediaFragmentParser::from(servo_url);
- if let Some(id) = fragment.id() {
- if track.id() == DOMString::from(id) {
- self.VideoTracks().set_selected(0, true);
- }
- } else if fragment.tracks().contains(&track.kind().into()) {
- self.VideoTracks().set_selected(0, true);
- }
- }
- }
+ // Step 5. & 6,
+ if self.AudioTracks().enabled_index().is_none() {
+ self.AudioTracks()
+ .set_enabled(self.AudioTracks().len() - 1, true);
+ }
- // Step 5. & 6.
- if self.VideoTracks().selected_index().is_none() {
- self.VideoTracks()
- .set_selected(self.VideoTracks().len() - 1, true);
- }
+ // Steps 7.
+ let event = TrackEvent::new(
+ self.global().as_window(),
+ atom!("addtrack"),
+ false,
+ false,
+ &Some(VideoTrackOrAudioTrackOrTextTrack::AudioTrack(audio_track)),
+ can_gc,
+ );
- // Steps 7.
- let event = TrackEvent::new(
- self.global().as_window(),
- atom!("addtrack"),
- false,
- false,
- &Some(VideoTrackOrAudioTrackOrTextTrack::VideoTrack(video_track)),
- can_gc,
- );
-
- event
- .upcast::<Event>()
- .fire(self.upcast::<EventTarget>(), can_gc);
- }
- }
+ event
+ .upcast::<Event>()
+ .fire(self.upcast::<EventTarget>(), can_gc);
+ }
+ }
- // => "Once enough of the media data has been fetched to determine the duration..."
+ // => If the media resource is found to have a video track
+ if !metadata.video_tracks.is_empty() {
+ for (i, _track) in metadata.video_tracks.iter().enumerate() {
// Step 1.
- // servo-media owns the media timeline.
-
- // Step 2.
- // XXX(ferjm) Update the timeline offset.
+ let kind = match i {
+ 0 => DOMString::from("main"),
+ _ => DOMString::new(),
+ };
+ let window = self.owner_window();
+ let video_track = VideoTrack::new(
+ &window,
+ DOMString::new(),
+ kind,
+ DOMString::new(),
+ DOMString::new(),
+ Some(&*self.VideoTracks()),
+ can_gc,
+ );
- // Step 3.
- self.playback_position.set(0.);
+ // Steps 2. & 3.
+ self.VideoTracks().add(&video_track);
// Step 4.
- let previous_duration = self.duration.get();
- if let Some(duration) = metadata.duration {
- self.duration.set(duration.as_secs() as f64);
- } else {
- self.duration.set(f64::INFINITY);
+ if let Some(track) = self.VideoTracks().item(0) {
+ if let Some(servo_url) = self.resource_url.borrow().as_ref() {
+ let fragment = MediaFragmentParser::from(servo_url);
+ if let Some(id) = fragment.id() {
+ if track.id() == DOMString::from(id) {
+ self.VideoTracks().set_selected(0, true);
+ }
+ } else if fragment.tracks().contains(&track.kind().into()) {
+ self.VideoTracks().set_selected(0, true);
+ }
+ }
}
- if previous_duration != self.duration.get() {
- self.owner_global()
- .task_manager()
- .media_element_task_source()
- .queue_simple_event(self.upcast(), atom!("durationchange"));
+
+ // Step 5. & 6.
+ if self.VideoTracks().selected_index().is_none() {
+ self.VideoTracks()
+ .set_selected(self.VideoTracks().len() - 1, true);
}
- // Step 5.
- self.handle_resize(Some(metadata.width), Some(metadata.height));
+ // Steps 7.
+ let event = TrackEvent::new(
+ self.global().as_window(),
+ atom!("addtrack"),
+ false,
+ false,
+ &Some(VideoTrackOrAudioTrackOrTextTrack::VideoTrack(video_track)),
+ can_gc,
+ );
- // Step 6.
- self.change_ready_state(ReadyState::HaveMetadata);
+ event
+ .upcast::<Event>()
+ .fire(self.upcast::<EventTarget>(), can_gc);
+ }
+ }
- // Step 7.
- let mut jumped = false;
-
- // Step 8.
- if self.default_playback_start_position.get() > 0. {
- self.seek(
- self.default_playback_start_position.get(),
- /* approximate_for_speed*/ false,
- );
- jumped = true;
- }
+ // => "Once enough of the media data has been fetched to determine the duration..."
+ // Step 1.
+ // servo-media owns the media timeline.
- // Step 9.
- self.default_playback_start_position.set(0.);
+ // Step 2.
+ // XXX(ferjm) Update the timeline offset.
- // Steps 10 and 11.
- if let Some(servo_url) = self.resource_url.borrow().as_ref() {
- let fragment = MediaFragmentParser::from(servo_url);
- if let Some(start) = fragment.start() {
- if start > 0. && start < self.duration.get() {
- self.playback_position.set(start);
- if !jumped {
- self.seek(self.playback_position.get(), false)
- }
- }
+ // Step 3.
+ self.playback_position.set(0.);
+
+ // Step 4.
+ let previous_duration = self.duration.get();
+ if let Some(duration) = metadata.duration {
+ self.duration.set(duration.as_secs() as f64);
+ } else {
+ self.duration.set(f64::INFINITY);
+ }
+ if previous_duration != self.duration.get() {
+ self.owner_global()
+ .task_manager()
+ .media_element_task_source()
+ .queue_simple_event(self.upcast(), atom!("durationchange"));
+ }
+
+ // Step 5.
+ self.handle_resize(Some(metadata.width), Some(metadata.height));
+
+ // Step 6.
+ self.change_ready_state(ReadyState::HaveMetadata);
+
+ // Step 7.
+ let mut jumped = false;
+
+ // Step 8.
+ if self.default_playback_start_position.get() > 0. {
+ self.seek(
+ self.default_playback_start_position.get(),
+ /* approximate_for_speed*/ false,
+ );
+ jumped = true;
+ }
+
+ // Step 9.
+ self.default_playback_start_position.set(0.);
+
+ // Steps 10 and 11.
+ if let Some(servo_url) = self.resource_url.borrow().as_ref() {
+ let fragment = MediaFragmentParser::from(servo_url);
+ if let Some(start) = fragment.start() {
+ if start > 0. && start < self.duration.get() {
+ self.playback_position.set(start);
+ if !jumped {
+ self.seek(self.playback_position.get(), false)
}
}
+ }
+ }
- // Step 12 & 13 are already handled by the earlier media track processing.
+ // Step 12 & 13 are already handled by the earlier media track processing.
- // We wait until we have metadata to render the controls, so we render them
- // with the appropriate size.
- if self.Controls() {
- self.render_controls(can_gc);
- }
+ // We wait until we have metadata to render the controls, so we render them
+ // with the appropriate size.
+ if self.Controls() {
+ self.render_controls(can_gc);
+ }
- let global = self.global();
- let window = global.as_window();
+ let global = self.global();
+ let window = global.as_window();
+
+ // Update the media session metadata title with the obtained metadata.
+ window.Navigator().MediaSession().update_title(
+ metadata
+ .title
+ .clone()
+ .unwrap_or(window.get_url().into_string()),
+ );
+ }
- // Update the media session metadata title with the obtained metadata.
- window.Navigator().MediaSession().update_title(
- metadata
- .title
- .clone()
- .unwrap_or(window.get_url().into_string()),
- );
- },
- PlayerEvent::NeedData => {
- // The player needs more data.
- // If we already have a valid fetch request, we do nothing.
- // Otherwise, if we have no request and the previous request was
- // cancelled because we got an EnoughData event, we restart
- // fetching where we left.
- if let Some(ref current_fetch_context) = *self.current_fetch_context.borrow() {
- match current_fetch_context.cancel_reason() {
- Some(reason) if *reason == CancelReason::Backoff => {
- // XXX(ferjm) Ideally we should just create a fetch request from
- // where we left. But keeping track of the exact next byte that the
- // media backend expects is not the easiest task, so I'm simply
- // seeking to the current playback position for now which will create
- // a new fetch request for the last rendered frame.
- self.seek(self.playback_position.get(), false)
- },
- _ => (),
- }
+ fn playback_video_frame_updated(&self) {
+ // Check if the frame was resized
+ if let Some(frame) = self.video_renderer.lock().unwrap().current_frame {
+ self.handle_resize(Some(frame.width as u32), Some(frame.height as u32));
+ }
+ }
+
+ fn playback_need_data(&self) {
+ // The player needs more data.
+ // If we already have a valid fetch request, we do nothing.
+ // Otherwise, if we have no request and the previous request was
+ // cancelled because we got an EnoughData event, we restart
+ // fetching where we left.
+ if let Some(ref current_fetch_context) = *self.current_fetch_context.borrow() {
+ if let Some(reason) = current_fetch_context.cancel_reason() {
+ // XXX(ferjm) Ideally we should just create a fetch request from
+ // where we left. But keeping track of the exact next byte that the
+ // media backend expects is not the easiest task, so I'm simply
+ // seeking to the current playback position for now which will create
+ // a new fetch request for the last rendered frame.
+ if *reason == CancelReason::Backoff {
+ self.seek(self.playback_position.get(), false);
}
- },
- PlayerEvent::EnoughData => {
- self.change_ready_state(ReadyState::HaveEnoughData);
-
- // The player has enough data and it is asking us to stop pushing
- // bytes, so we cancel the ongoing fetch request iff we are able
- // to restart it from where we left. Otherwise, we continue the
- // current fetch request, assuming that some frames will be dropped.
- if let Some(ref mut current_fetch_context) =
- *self.current_fetch_context.borrow_mut()
- {
- if current_fetch_context.is_seekable() {
- current_fetch_context.cancel(CancelReason::Backoff);
- }
+ return;
+ }
+ }
+
+ if let Some(ref mut current_fetch_context) = *self.current_fetch_context.borrow_mut() {
+ if let Err(e) = {
+ let mut data_source = current_fetch_context.data_source().borrow_mut();
+ data_source.set_locked(false);
+ data_source.process_into_player_from_queue(self.player.borrow().as_ref().unwrap())
+ } {
+ // If we are pushing too much data and we know that we can
+ // restart the download later from where we left, we cancel
+ // the current request. Otherwise, we continue the request
+ // assuming that we may drop some frames.
+ if e == PlayerError::EnoughData {
+ current_fetch_context.cancel(CancelReason::Backoff);
+ }
+ }
+ }
+ }
+
+ fn playback_enough_data(&self) {
+ self.change_ready_state(ReadyState::HaveEnoughData);
+
+ // The player has enough data and it is asking us to stop pushing
+ // bytes, so we cancel the ongoing fetch request iff we are able
+ // to restart it from where we left. Otherwise, we continue the
+ // current fetch request, assuming that some frames will be dropped.
+ if let Some(ref mut current_fetch_context) = *self.current_fetch_context.borrow_mut() {
+ if current_fetch_context.is_seekable() {
+ current_fetch_context.cancel(CancelReason::Backoff);
+ }
+ }
+ }
+
+ fn playback_position_changed(&self, position: u64) {
+ let position = position as f64;
+ let _ = self
+ .played
+ .borrow_mut()
+ .add(self.playback_position.get(), position);
+ self.playback_position.set(position);
+ self.time_marches_on();
+ let media_position_state =
+ MediaPositionState::new(self.duration.get(), self.playbackRate.get(), position);
+ debug!(
+ "Sending media session event set position state {:?}",
+ media_position_state
+ );
+ self.send_media_session_event(MediaSessionEvent::SetPositionState(media_position_state));
+ }
+
+ fn playback_seek_done(&self) {
+ // Continuation of
+ // https://html.spec.whatwg.org/multipage/#dom-media-seek
+
+ // Step 13.
+ let task = MediaElementMicrotask::Seeked {
+ elem: DomRoot::from_ref(self),
+ generation_id: self.generation_id.get(),
+ };
+ ScriptThread::await_stable_state(Microtask::MediaElement(task));
+ }
+
+ fn playback_state_changed(&self, state: &PlaybackState) {
+ let mut media_session_playback_state = MediaSessionPlaybackState::None_;
+ match *state {
+ PlaybackState::Paused => {
+ media_session_playback_state = MediaSessionPlaybackState::Paused;
+ if self.ready_state.get() == ReadyState::HaveMetadata {
+ self.change_ready_state(ReadyState::HaveEnoughData);
}
},
- PlayerEvent::PositionChanged(position) => {
- let position = position as f64;
- let _ = self
- .played
- .borrow_mut()
- .add(self.playback_position.get(), position);
- self.playback_position.set(position);
- self.time_marches_on();
- let media_position_state =
- MediaPositionState::new(self.duration.get(), self.playbackRate.get(), position);
- debug!(
- "Sending media session event set position state {:?}",
- media_position_state
- );
- self.send_media_session_event(MediaSessionEvent::SetPositionState(
- media_position_state,
- ));
+ PlaybackState::Playing => {
+ media_session_playback_state = MediaSessionPlaybackState::Playing;
},
- PlayerEvent::SeekData(p, ref seek_lock) => {
- self.fetch_request(Some(p), Some(seek_lock.clone()));
+ PlaybackState::Buffering => {
+ // Do not send the media session playback state change event
+ // in this case as a None_ state is expected to clean up the
+ // session.
+ return;
},
- PlayerEvent::SeekDone(_) => {
- // Continuation of
- // https://html.spec.whatwg.org/multipage/#dom-media-seek
-
- // Step 13.
- let task = MediaElementMicrotask::Seeked {
- elem: DomRoot::from_ref(self),
- generation_id: self.generation_id.get(),
- };
- ScriptThread::await_stable_state(Microtask::MediaElement(task));
+ _ => {},
+ };
+ debug!(
+ "Sending media session event playback state changed to {:?}",
+ media_session_playback_state
+ );
+ self.send_media_session_event(MediaSessionEvent::PlaybackStateChange(
+ media_session_playback_state,
+ ));
+ }
+
+ fn handle_player_event(&self, player_id: usize, event: &PlayerEvent, can_gc: CanGc) {
+ // Ignore the asynchronous event from previous player.
+ if self
+ .player
+ .borrow()
+ .as_ref()
+ .is_none_or(|player| player.lock().unwrap().get_id() != player_id)
+ {
+ return;
+ }
+
+ match *event {
+ PlayerEvent::EndOfStream => self.playback_end(),
+ PlayerEvent::Error(ref error) => self.playback_error(error, can_gc),
+ PlayerEvent::VideoFrameUpdated => self.playback_video_frame_updated(),
+ PlayerEvent::MetadataUpdated(ref metadata) => {
+ self.playback_metadata_updated(metadata, can_gc)
},
- PlayerEvent::StateChanged(ref state) => {
- let mut media_session_playback_state = MediaSessionPlaybackState::None_;
- match *state {
- PlaybackState::Paused => {
- media_session_playback_state = MediaSessionPlaybackState::Paused;
- if self.ready_state.get() == ReadyState::HaveMetadata {
- self.change_ready_state(ReadyState::HaveEnoughData);
- }
- },
- PlaybackState::Playing => {
- media_session_playback_state = MediaSessionPlaybackState::Playing;
- },
- PlaybackState::Buffering => {
- // Do not send the media session playback state change event
- // in this case as a None_ state is expected to clean up the
- // session.
- return;
- },
- _ => {},
- };
- debug!(
- "Sending media session event playback state changed to {:?}",
- media_session_playback_state
- );
- self.send_media_session_event(MediaSessionEvent::PlaybackStateChange(
- media_session_playback_state,
- ));
+ PlayerEvent::NeedData => self.playback_need_data(),
+ PlayerEvent::EnoughData => self.playback_enough_data(),
+ PlayerEvent::PositionChanged(position) => self.playback_position_changed(position),
+ PlayerEvent::SeekData(p, ref seek_lock) => {
+ self.fetch_request(Some(p), Some(seek_lock.clone()))
},
+ PlayerEvent::SeekDone(_) => self.playback_seek_done(),
+ PlayerEvent::StateChanged(ref state) => self.playback_state_changed(state),
}
}
@@ -2111,13 +2205,6 @@ impl HTMLMediaElement {
}
}
-impl Drop for HTMLMediaElement {
- fn drop(&mut self) {
- self.player_context
- .send(GLPlayerMsg::UnregisterPlayer(self.id.get()));
- }
-}
-
impl HTMLMediaElementMethods<crate::DomTypeHolder> for HTMLMediaElement {
// https://html.spec.whatwg.org/multipage/#dom-media-networkstate
fn NetworkState(&self) -> u16 {
@@ -2659,6 +2746,80 @@ enum Resource {
Url(ServoUrl),
}
+#[derive(Debug, MallocSizeOf, PartialEq)]
+enum DataBuffer {
+ Payload(Vec<u8>),
+ EndOfStream,
+}
+
+#[derive(MallocSizeOf)]
+struct BufferedDataSource {
+ /// During initial setup and seeking (including clearing the buffer queue
+ /// and resetting the end-of-stream state), the data source should be locked and
+ /// any request for processing should be ignored until the media player informs us
+ /// via the NeedData event that it is ready to accept incoming data.
+ locked: Cell<bool>,
+ /// Temporary storage for incoming data.
+ buffers: VecDeque<DataBuffer>,
+}
+
+impl BufferedDataSource {
+ fn new() -> BufferedDataSource {
+ BufferedDataSource {
+ locked: Cell::new(true),
+ buffers: VecDeque::default(),
+ }
+ }
+
+ fn set_locked(&self, locked: bool) {
+ self.locked.set(locked)
+ }
+
+ fn add_buffer_to_queue(&mut self, buffer: DataBuffer) {
+ debug_assert_ne!(
+ self.buffers.back(),
+ Some(&DataBuffer::EndOfStream),
+ "The media backend not expects any further data after end of stream"
+ );
+
+ self.buffers.push_back(buffer);
+ }
+
+ fn process_into_player_from_queue(
+ &mut self,
+ player: &Arc<Mutex<dyn Player>>,
+ ) -> Result<(), PlayerError> {
+ // Early out if any request for processing should be ignored.
+ if self.locked.get() {
+ return Ok(());
+ }
+
+ while let Some(buffer) = self.buffers.pop_front() {
+ match buffer {
+ DataBuffer::Payload(payload) => {
+ if let Err(e) = player.lock().unwrap().push_data(payload) {
+ warn!("Could not push input data to player {:?}", e);
+ return Err(e);
+ }
+ },
+ DataBuffer::EndOfStream => {
+ if let Err(e) = player.lock().unwrap().end_of_stream() {
+ warn!("Could not signal EOS to player {:?}", e);
+ return Err(e);
+ }
+ },
+ }
+ }
+
+ Ok(())
+ }
+
+ fn reset(&mut self) {
+ self.locked.set(true);
+ self.buffers.clear();
+ }
+}
+
/// Indicates the reason why a fetch request was cancelled.
#[derive(Debug, MallocSizeOf, PartialEq)]
enum CancelReason {
@@ -2672,12 +2833,16 @@ enum CancelReason {
#[derive(MallocSizeOf)]
pub(crate) struct HTMLMediaElementFetchContext {
+ /// The fetch request id.
+ request_id: RequestId,
/// Some if the request has been cancelled.
cancel_reason: Option<CancelReason>,
/// Indicates whether the fetched stream is seekable.
is_seekable: bool,
/// Indicates whether the fetched stream is origin clean.
origin_clean: bool,
+ /// The buffered data source which to be processed by media backend.
+ data_source: DomRefCell<BufferedDataSource>,
/// Fetch canceller. Allows cancelling the current fetch request by
/// manually calling its .cancel() method or automatically on Drop.
fetch_canceller: FetchCanceller,
@@ -2686,13 +2851,19 @@ pub(crate) struct HTMLMediaElementFetchContext {
impl HTMLMediaElementFetchContext {
fn new(request_id: RequestId) -> HTMLMediaElementFetchContext {
HTMLMediaElementFetchContext {
+ request_id,
cancel_reason: None,
is_seekable: false,
origin_clean: true,
+ data_source: DomRefCell::new(BufferedDataSource::new()),
fetch_canceller: FetchCanceller::new(request_id),
}
}
+ fn request_id(&self) -> RequestId {
+ self.request_id
+ }
+
fn is_seekable(&self) -> bool {
self.is_seekable
}
@@ -2709,11 +2880,16 @@ impl HTMLMediaElementFetchContext {
self.origin_clean = false;
}
+ fn data_source(&self) -> &DomRefCell<BufferedDataSource> {
+ &self.data_source
+ }
+
fn cancel(&mut self, reason: CancelReason) {
if self.cancel_reason.is_some() {
return;
}
self.cancel_reason = Some(reason);
+ self.data_source.borrow_mut().reset();
self.fetch_canceller.cancel();
}
@@ -2729,6 +2905,8 @@ struct HTMLMediaElementFetchListener {
metadata: Option<Metadata>,
/// The generation of the media element when this fetch started.
generation_id: u32,
+ /// The fetch request id.
+ request_id: RequestId,
/// Time of last progress notification.
next_progress_event: Instant,
/// Timing data for this resource.
@@ -2737,16 +2915,12 @@ struct HTMLMediaElementFetchListener {
url: ServoUrl,
/// Expected content length of the media asset being fetched or played.
expected_content_length: Option<u64>,
- /// Number of the last byte fetched from the network for the ongoing
- /// request. It is only reset to 0 if we reach EOS. Seek requests
- /// set it to the requested position. Requests triggered after an
- /// EnoughData event uses this value to restart the download from
- /// the last fetched position.
- latest_fetched_content: u64,
- /// The media player discards all data pushes until the seek block
- /// is released right before pushing the data from the offset requested
- /// by a seek request.
- seek_lock: Option<SeekLock>,
+ /// Actual content length of the media asset was fetched.
+ fetched_content_length: u64,
+ /// Discarded content length from the network for the ongoing
+ /// request if range requests are not supported. Seek requests set it
+ /// to the required position (in bytes).
+ content_length_to_discard: u64,
}
// https://html.spec.whatwg.org/multipage/#media-data-processing-steps-list
@@ -2758,11 +2932,6 @@ impl FetchResponseListener for HTMLMediaElementFetchListener {
fn process_response(&mut self, _: RequestId, metadata: Result<FetchMetadata, NetworkError>) {
let elem = self.elem.root();
- if elem.generation_id.get() != self.generation_id || elem.player.borrow().is_none() {
- // A new fetch request was triggered, so we ignore this response.
- return;
- }
-
if let Ok(FetchMetadata::Filtered {
filtered: FilteredMetadata::Opaque | FilteredMetadata::OpaqueRedirect(_),
..
@@ -2794,24 +2963,25 @@ impl FetchResponseListener for HTMLMediaElementFetchListener {
// We only set the expected input size if it changes.
if content_length != self.expected_content_length {
if let Some(content_length) = content_length {
- if let Err(e) = elem
- .player
- .borrow()
- .as_ref()
- .unwrap()
- .lock()
- .unwrap()
- .set_input_size(content_length)
- {
- warn!("Could not set player input size {:?}", e);
- } else {
- self.expected_content_length = Some(content_length);
- }
+ self.expected_content_length = Some(content_length);
}
}
}
}
+ // Explicit media player initialization with live/seekable source.
+ if let Err(e) = elem
+ .player
+ .borrow()
+ .as_ref()
+ .unwrap()
+ .lock()
+ .unwrap()
+ .set_input_size(self.expected_content_length.unwrap_or_default())
+ {
+ warn!("Could not set player input size {:?}", e);
+ }
+
let (status_is_ok, is_seekable) = self.metadata.as_ref().map_or((true, false), |s| {
let status = &s.status;
(
@@ -2839,52 +3009,51 @@ impl FetchResponseListener for HTMLMediaElementFetchListener {
}
}
- fn process_response_chunk(&mut self, _: RequestId, payload: Vec<u8>) {
+ fn process_response_chunk(&mut self, _: RequestId, chunk: Vec<u8>) {
let elem = self.elem.root();
- // If an error was received previously or if we triggered a new fetch request,
- // we skip processing the payload.
- if elem.generation_id.get() != self.generation_id || elem.player.borrow().is_none() {
- return;
- }
- if let Some(ref current_fetch_context) = *elem.current_fetch_context.borrow() {
+
+ self.fetched_content_length += chunk.len() as u64;
+
+ // If an error was received previously, we skip processing the payload.
+ if let Some(ref mut current_fetch_context) = *elem.current_fetch_context.borrow_mut() {
if current_fetch_context.cancel_reason().is_some() {
return;
}
- }
-
- let payload_len = payload.len() as u64;
- if let Some(seek_lock) = self.seek_lock.take() {
- seek_lock.unlock(/* successful seek */ true);
- }
+ // Discard chunk of the response body if fetch context doesn't
+ // support range requests.
+ let payload = if !current_fetch_context.is_seekable() &&
+ self.content_length_to_discard != 0
+ {
+ if chunk.len() as u64 > self.content_length_to_discard {
+ let shrink_chunk = chunk[self.content_length_to_discard as usize..].to_vec();
+ self.content_length_to_discard = 0;
+ shrink_chunk
+ } else {
+ // Completely discard this response chunk.
+ self.content_length_to_discard -= chunk.len() as u64;
+ return;
+ }
+ } else {
+ chunk
+ };
- // Push input data into the player.
- if let Err(e) = elem
- .player
- .borrow()
- .as_ref()
- .unwrap()
- .lock()
- .unwrap()
- .push_data(payload)
- {
- // If we are pushing too much data and we know that we can
- // restart the download later from where we left, we cancel
- // the current request. Otherwise, we continue the request
- // assuming that we may drop some frames.
- if e == PlayerError::EnoughData {
- if let Some(ref mut current_fetch_context) =
- *elem.current_fetch_context.borrow_mut()
- {
+ if let Err(e) = {
+ let mut data_source = current_fetch_context.data_source().borrow_mut();
+ data_source.add_buffer_to_queue(DataBuffer::Payload(payload));
+ data_source.process_into_player_from_queue(elem.player.borrow().as_ref().unwrap())
+ } {
+ // If we are pushing too much data and we know that we can
+ // restart the download later from where we left, we cancel
+ // the current request. Otherwise, we continue the request
+ // assuming that we may drop some frames.
+ if e == PlayerError::EnoughData {
current_fetch_context.cancel(CancelReason::Backoff);
}
+ return;
}
- warn!("Could not push input data to player {:?}", e);
- return;
}
- self.latest_fetched_content += payload_len;
-
// https://html.spec.whatwg.org/multipage/#concept-media-load-resource step 4,
// => "If mode is remote" step 2
if Instant::now() > self.next_progress_event {
@@ -2903,38 +3072,45 @@ impl FetchResponseListener for HTMLMediaElementFetchListener {
status: Result<ResourceFetchTiming, NetworkError>,
) {
trace!("process response eof");
- if let Some(seek_lock) = self.seek_lock.take() {
- seek_lock.unlock(/* successful seek */ false);
- }
let elem = self.elem.root();
- if elem.generation_id.get() != self.generation_id || elem.player.borrow().is_none() {
- return;
- }
-
// There are no more chunks of the response body forthcoming, so we can
// go ahead and notify the media backend not to expect any further data.
- if let Err(e) = elem
- .player
- .borrow()
- .as_ref()
- .unwrap()
- .lock()
- .unwrap()
- .end_of_stream()
- {
- warn!("Could not signal EOS to player {:?}", e);
- }
+ if let Some(ref mut current_fetch_context) = *elem.current_fetch_context.borrow_mut() {
+ // On initial state change READY -> PAUSED the media player perform
+ // seek to initial position by event with seek segment (TIME format)
+ // while media stack operates in BYTES format and configuring segment
+ // start and stop positions without the total size of the stream is not
+ // possible. As fallback the media player perform seek with BYTES format
+ // and initiate seek request via "seek-data" callback with required offset.
+ if self.expected_content_length.is_none() && self.fetched_content_length != 0 {
+ if let Err(e) = elem
+ .player
+ .borrow()
+ .as_ref()
+ .unwrap()
+ .lock()
+ .unwrap()
+ .set_input_size(self.fetched_content_length)
+ {
+ warn!("Could not set player input size {:?}", e);
+ }
+ }
+
+ let mut data_source = current_fetch_context.data_source().borrow_mut();
- // If an error was previously received we skip processing the payload.
- if let Some(ref current_fetch_context) = *elem.current_fetch_context.borrow() {
+ data_source.add_buffer_to_queue(DataBuffer::EndOfStream);
+ let _ =
+ data_source.process_into_player_from_queue(elem.player.borrow().as_ref().unwrap());
+
+ // If an error was previously received we skip processing the payload.
if let Some(CancelReason::Error) = current_fetch_context.cancel_reason() {
return;
}
}
- if status.is_ok() && self.latest_fetched_content != 0 {
+ if status.is_ok() && self.fetched_content_length != 0 {
elem.upcast::<EventTarget>()
.fire_event(atom!("progress"), CanGc::note());
@@ -3015,28 +3191,33 @@ impl ResourceTimingListener for HTMLMediaElementFetchListener {
impl PreInvoke for HTMLMediaElementFetchListener {
fn should_invoke(&self) -> bool {
- //TODO: finish_load needs to run at some point if the generation changes.
- self.elem.root().generation_id.get() == self.generation_id
+ let elem = self.elem.root();
+
+ if elem.generation_id.get() != self.generation_id || elem.player.borrow().is_none() {
+ return false;
+ }
+
+ // A new fetch request was triggered, so we skip processing previous request.
+ elem.current_fetch_context
+ .borrow()
+ .as_ref()
+ .is_some_and(|context| context.request_id() == self.request_id)
}
}
impl HTMLMediaElementFetchListener {
- fn new(
- elem: &HTMLMediaElement,
- url: ServoUrl,
- offset: u64,
- seek_lock: Option<SeekLock>,
- ) -> Self {
+ fn new(elem: &HTMLMediaElement, request_id: RequestId, url: ServoUrl, offset: u64) -> Self {
Self {
elem: Trusted::new(elem),
metadata: None,
generation_id: elem.generation_id.get(),
+ request_id,
next_progress_event: Instant::now() + Duration::from_millis(350),
resource_timing: ResourceFetchTiming::new(ResourceTimingType::Resource),
url,
expected_content_length: None,
- latest_fetched_content: offset,
- seek_lock,
+ fetched_content_length: 0,
+ content_length_to_discard: offset,
}
}
}