mirror of
https://github.com/servo/servo.git
synced 2025-08-04 05:00:08 +01:00
Auto merge of #22522 - ferjm:av.playback.improvements, r=Manishearth
HTMLMediaElement backoff protocol and other improvements to a/v playback - [X] `./mach build -d` does not report any errors - [X] `./mach test-tidy` does not report any errors This PR implements a backoff protocol to keep us from dropping frames whenever it's possible. It also add some other improvements like: - Clean up the implementation a little bit by adding all fetch request related state to `HTMLMediaElementFetchContext`. - Make sure that we ignore responses from old requests. - Set the stream to seekable iff there's support for range requests. This will likely change when we add the media cache. - Implements part of [step 8 of the seek spec](https://html.spec.whatwg.org/multipage/media.html#dom-media-seek) where we bail out if the stream is not seekable. <!-- Reviewable:start --> --- This change is [<img src="https://reviewable.io/review_button.svg" height="34" align="absmiddle" alt="Reviewable"/>](https://reviewable.io/reviews/servo/servo/22522) <!-- Reviewable:end -->
This commit is contained in:
commit
b49e7517a3
3 changed files with 240 additions and 100 deletions
10
Cargo.lock
generated
10
Cargo.lock
generated
|
@ -3507,7 +3507,7 @@ dependencies = [
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "servo-media"
|
name = "servo-media"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/servo/media#35014baeb603b56b1b8b4de75919c58f7390cc30"
|
source = "git+https://github.com/servo/media#c8cc491c850b18393586500233cc55e29800146c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"servo-media-audio 0.1.0 (git+https://github.com/servo/media)",
|
"servo-media-audio 0.1.0 (git+https://github.com/servo/media)",
|
||||||
"servo-media-gstreamer 0.1.0 (git+https://github.com/servo/media)",
|
"servo-media-gstreamer 0.1.0 (git+https://github.com/servo/media)",
|
||||||
|
@ -3517,7 +3517,7 @@ dependencies = [
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "servo-media-audio"
|
name = "servo-media-audio"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/servo/media#35014baeb603b56b1b8b4de75919c58f7390cc30"
|
source = "git+https://github.com/servo/media#c8cc491c850b18393586500233cc55e29800146c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"boxfnonce 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
"boxfnonce 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"byte-slice-cast 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
"byte-slice-cast 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
@ -3534,7 +3534,7 @@ dependencies = [
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "servo-media-gstreamer"
|
name = "servo-media-gstreamer"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/servo/media#35014baeb603b56b1b8b4de75919c58f7390cc30"
|
source = "git+https://github.com/servo/media#c8cc491c850b18393586500233cc55e29800146c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"byte-slice-cast 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
"byte-slice-cast 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"glib 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
"glib 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
@ -3553,7 +3553,7 @@ dependencies = [
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "servo-media-player"
|
name = "servo-media-player"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/servo/media#35014baeb603b56b1b8b4de75919c58f7390cc30"
|
source = "git+https://github.com/servo/media#c8cc491c850b18393586500233cc55e29800146c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ipc-channel 0.11.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
"ipc-channel 0.11.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"serde 1.0.80 (registry+https://github.com/rust-lang/crates.io-index)",
|
"serde 1.0.80 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
@ -3640,7 +3640,7 @@ dependencies = [
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "servo_media_derive"
|
name = "servo_media_derive"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/servo/media#35014baeb603b56b1b8b4de75919c58f7390cc30"
|
source = "git+https://github.com/servo/media#c8cc491c850b18393586500233cc55e29800146c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
"proc-macro2 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"quote 0.6.3 (registry+https://github.com/rust-lang/crates.io-index)",
|
"quote 0.6.3 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
|
|
@ -38,7 +38,7 @@ use crate::dom::bindings::str::{DOMString, USVString};
|
||||||
use crate::dom::bindings::utils::WindowProxyHandler;
|
use crate::dom::bindings::utils::WindowProxyHandler;
|
||||||
use crate::dom::document::PendingRestyle;
|
use crate::dom::document::PendingRestyle;
|
||||||
use crate::dom::htmlimageelement::SourceSet;
|
use crate::dom::htmlimageelement::SourceSet;
|
||||||
use crate::dom::htmlmediaelement::MediaFrameRenderer;
|
use crate::dom::htmlmediaelement::{HTMLMediaElementFetchContext, MediaFrameRenderer};
|
||||||
use crate::task::TaskBox;
|
use crate::task::TaskBox;
|
||||||
use app_units::Au;
|
use app_units::Au;
|
||||||
use canvas_traits::canvas::{
|
use canvas_traits::canvas::{
|
||||||
|
@ -103,7 +103,6 @@ use servo_media::audio::panner_node::{DistanceModel, PanningModel};
|
||||||
use servo_media::audio::param::ParamType;
|
use servo_media::audio::param::ParamType;
|
||||||
use servo_media::player::Player;
|
use servo_media::player::Player;
|
||||||
use servo_media::Backend;
|
use servo_media::Backend;
|
||||||
use servo_media::Error as ServoMediaError;
|
|
||||||
use servo_url::{ImmutableOrigin, MutableOrigin, ServoUrl};
|
use servo_url::{ImmutableOrigin, MutableOrigin, ServoUrl};
|
||||||
use smallvec::SmallVec;
|
use smallvec::SmallVec;
|
||||||
use std::cell::{Cell, RefCell, UnsafeCell};
|
use std::cell::{Cell, RefCell, UnsafeCell};
|
||||||
|
@ -484,11 +483,12 @@ unsafe_no_jsmanaged_fields!(AudioBuffer);
|
||||||
unsafe_no_jsmanaged_fields!(AudioContext<Backend>);
|
unsafe_no_jsmanaged_fields!(AudioContext<Backend>);
|
||||||
unsafe_no_jsmanaged_fields!(NodeId);
|
unsafe_no_jsmanaged_fields!(NodeId);
|
||||||
unsafe_no_jsmanaged_fields!(AnalysisEngine, DistanceModel, PanningModel, ParamType);
|
unsafe_no_jsmanaged_fields!(AnalysisEngine, DistanceModel, PanningModel, ParamType);
|
||||||
unsafe_no_jsmanaged_fields!(dyn Player<Error = ServoMediaError>);
|
unsafe_no_jsmanaged_fields!(dyn Player);
|
||||||
unsafe_no_jsmanaged_fields!(Mutex<MediaFrameRenderer>);
|
unsafe_no_jsmanaged_fields!(Mutex<MediaFrameRenderer>);
|
||||||
unsafe_no_jsmanaged_fields!(RenderApiSender);
|
unsafe_no_jsmanaged_fields!(RenderApiSender);
|
||||||
unsafe_no_jsmanaged_fields!(ResourceFetchTiming);
|
unsafe_no_jsmanaged_fields!(ResourceFetchTiming);
|
||||||
unsafe_no_jsmanaged_fields!(Timespec);
|
unsafe_no_jsmanaged_fields!(Timespec);
|
||||||
|
unsafe_no_jsmanaged_fields!(HTMLMediaElementFetchContext);
|
||||||
|
|
||||||
unsafe impl<'a> JSTraceable for &'a str {
|
unsafe impl<'a> JSTraceable for &'a str {
|
||||||
#[inline]
|
#[inline]
|
||||||
|
|
|
@ -56,8 +56,7 @@ use net_traits::{CoreResourceMsg, FetchChannels, FetchMetadata, FetchResponseLis
|
||||||
use net_traits::{NetworkError, ResourceFetchTiming, ResourceTimingType};
|
use net_traits::{NetworkError, ResourceFetchTiming, ResourceTimingType};
|
||||||
use script_layout_interface::HTMLMediaData;
|
use script_layout_interface::HTMLMediaData;
|
||||||
use servo_media::player::frame::{Frame, FrameRenderer};
|
use servo_media::player::frame::{Frame, FrameRenderer};
|
||||||
use servo_media::player::{PlaybackState, Player, PlayerEvent, StreamType};
|
use servo_media::player::{PlaybackState, Player, PlayerError, PlayerEvent, StreamType};
|
||||||
use servo_media::Error as ServoMediaError;
|
|
||||||
use servo_media::ServoMedia;
|
use servo_media::ServoMedia;
|
||||||
use servo_url::ServoUrl;
|
use servo_url::ServoUrl;
|
||||||
use std::cell::Cell;
|
use std::cell::Cell;
|
||||||
|
@ -179,10 +178,9 @@ pub struct HTMLMediaElement {
|
||||||
#[ignore_malloc_size_of = "promises are hard"]
|
#[ignore_malloc_size_of = "promises are hard"]
|
||||||
in_flight_play_promises_queue: DomRefCell<VecDeque<(Box<[Rc<Promise>]>, ErrorResult)>>,
|
in_flight_play_promises_queue: DomRefCell<VecDeque<(Box<[Rc<Promise>]>, ErrorResult)>>,
|
||||||
#[ignore_malloc_size_of = "servo_media"]
|
#[ignore_malloc_size_of = "servo_media"]
|
||||||
player: Box<Player<Error = ServoMediaError>>,
|
player: Box<Player>,
|
||||||
#[ignore_malloc_size_of = "Arc"]
|
#[ignore_malloc_size_of = "Arc"]
|
||||||
frame_renderer: Arc<Mutex<MediaFrameRenderer>>,
|
frame_renderer: Arc<Mutex<MediaFrameRenderer>>,
|
||||||
fetch_canceller: DomRefCell<FetchCanceller>,
|
|
||||||
/// https://html.spec.whatwg.org/multipage/#show-poster-flag
|
/// https://html.spec.whatwg.org/multipage/#show-poster-flag
|
||||||
show_poster: Cell<bool>,
|
show_poster: Cell<bool>,
|
||||||
/// https://html.spec.whatwg.org/multipage/#dom-media-duration
|
/// https://html.spec.whatwg.org/multipage/#dom-media-duration
|
||||||
|
@ -202,11 +200,11 @@ pub struct HTMLMediaElement {
|
||||||
played: Rc<DomRefCell<TimeRangesContainer>>,
|
played: Rc<DomRefCell<TimeRangesContainer>>,
|
||||||
/// https://html.spec.whatwg.org/multipage/#dom-media-texttracks
|
/// https://html.spec.whatwg.org/multipage/#dom-media-texttracks
|
||||||
text_tracks_list: MutNullableDom<TextTrackList>,
|
text_tracks_list: MutNullableDom<TextTrackList>,
|
||||||
/// Expected content length of the media asset being fetched or played.
|
|
||||||
content_length: Cell<Option<u64>>,
|
|
||||||
/// Time of last timeupdate notification.
|
/// Time of last timeupdate notification.
|
||||||
#[ignore_malloc_size_of = "Defined in time"]
|
#[ignore_malloc_size_of = "Defined in time"]
|
||||||
next_timeupdate_event: Cell<Timespec>,
|
next_timeupdate_event: Cell<Timespec>,
|
||||||
|
/// Latest fetch request context.
|
||||||
|
current_fetch_context: DomRefCell<Option<HTMLMediaElementFetchContext>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <https://html.spec.whatwg.org/multipage/#dom-media-networkstate>
|
/// <https://html.spec.whatwg.org/multipage/#dom-media-networkstate>
|
||||||
|
@ -253,7 +251,6 @@ impl HTMLMediaElement {
|
||||||
frame_renderer: Arc::new(Mutex::new(MediaFrameRenderer::new(
|
frame_renderer: Arc::new(Mutex::new(MediaFrameRenderer::new(
|
||||||
document.window().get_webrender_api_sender(),
|
document.window().get_webrender_api_sender(),
|
||||||
))),
|
))),
|
||||||
fetch_canceller: DomRefCell::new(Default::default()),
|
|
||||||
show_poster: Cell::new(true),
|
show_poster: Cell::new(true),
|
||||||
duration: Cell::new(f64::NAN),
|
duration: Cell::new(f64::NAN),
|
||||||
playback_position: Cell::new(0.),
|
playback_position: Cell::new(0.),
|
||||||
|
@ -263,8 +260,8 @@ impl HTMLMediaElement {
|
||||||
resource_url: DomRefCell::new(None),
|
resource_url: DomRefCell::new(None),
|
||||||
played: Rc::new(DomRefCell::new(TimeRangesContainer::new())),
|
played: Rc::new(DomRefCell::new(TimeRangesContainer::new())),
|
||||||
text_tracks_list: Default::default(),
|
text_tracks_list: Default::default(),
|
||||||
content_length: Cell::new(None),
|
|
||||||
next_timeupdate_event: Cell::new(time::get_time() + Duration::milliseconds(250)),
|
next_timeupdate_event: Cell::new(time::get_time() + Duration::milliseconds(250)),
|
||||||
|
current_fetch_context: DomRefCell::new(None),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -667,31 +664,33 @@ impl HTMLMediaElement {
|
||||||
..RequestInit::default()
|
..RequestInit::default()
|
||||||
};
|
};
|
||||||
|
|
||||||
let context = Arc::new(Mutex::new(HTMLMediaElementContext::new(
|
let mut current_fetch_context = self.current_fetch_context.borrow_mut();
|
||||||
|
if let Some(ref mut current_fetch_context) = *current_fetch_context {
|
||||||
|
current_fetch_context.cancel(CancelReason::Overridden);
|
||||||
|
}
|
||||||
|
let (fetch_context, cancel_receiver) = HTMLMediaElementFetchContext::new();
|
||||||
|
*current_fetch_context = Some(fetch_context);
|
||||||
|
let fetch_listener = Arc::new(Mutex::new(HTMLMediaElementFetchListener::new(
|
||||||
self,
|
self,
|
||||||
self.resource_url.borrow().as_ref().unwrap().clone(),
|
self.resource_url.borrow().as_ref().unwrap().clone(),
|
||||||
|
offset.unwrap_or(0),
|
||||||
)));
|
)));
|
||||||
let (action_sender, action_receiver) = ipc::channel().unwrap();
|
let (action_sender, action_receiver) = ipc::channel().unwrap();
|
||||||
let window = window_from_node(self);
|
let window = window_from_node(self);
|
||||||
let (task_source, canceller) = window
|
let (task_source, canceller) = window
|
||||||
.task_manager()
|
.task_manager()
|
||||||
.networking_task_source_with_canceller();
|
.networking_task_source_with_canceller();
|
||||||
let listener = NetworkListener {
|
let network_listener = NetworkListener {
|
||||||
context,
|
context: fetch_listener,
|
||||||
task_source,
|
task_source,
|
||||||
canceller: Some(canceller),
|
canceller: Some(canceller),
|
||||||
};
|
};
|
||||||
ROUTER.add_route(
|
ROUTER.add_route(
|
||||||
action_receiver.to_opaque(),
|
action_receiver.to_opaque(),
|
||||||
Box::new(move |message| {
|
Box::new(move |message| {
|
||||||
listener.notify_fetch(message.to().unwrap());
|
network_listener.notify_fetch(message.to().unwrap());
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
// This method may be called the first time we try to fetch the media
|
|
||||||
// resource or after a seek is requested. In the latter case, we need to
|
|
||||||
// cancel any previous on-going request. initialize() takes care of
|
|
||||||
// cancelling previous fetches if any exist.
|
|
||||||
let cancel_receiver = self.fetch_canceller.borrow_mut().initialize();
|
|
||||||
let global = self.global();
|
let global = self.global();
|
||||||
global
|
global
|
||||||
.core_resource_thread()
|
.core_resource_thread()
|
||||||
|
@ -710,13 +709,6 @@ impl HTMLMediaElement {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// XXX(ferjm) Since we only support Blob for now it is fine to always set
|
|
||||||
// the stream type to StreamType::Seekable. Once we support MediaStream,
|
|
||||||
// this should be changed to also consider StreamType::Stream.
|
|
||||||
if let Err(e) = self.player.set_stream_type(StreamType::Seekable) {
|
|
||||||
eprintln!("Could not set stream type to Seekable. {:?}", e);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Steps 1-2.
|
// Steps 1-2.
|
||||||
// Unapplicable, the `resource` variable already conveys which mode
|
// Unapplicable, the `resource` variable already conveys which mode
|
||||||
// is in use.
|
// is in use.
|
||||||
|
@ -891,7 +883,9 @@ impl HTMLMediaElement {
|
||||||
task_source.queue_simple_event(self.upcast(), atom!("emptied"), &window);
|
task_source.queue_simple_event(self.upcast(), atom!("emptied"), &window);
|
||||||
|
|
||||||
// Step 6.2.
|
// Step 6.2.
|
||||||
self.fetch_canceller.borrow_mut().cancel();
|
if let Some(ref mut current_fetch_context) = *self.current_fetch_context.borrow_mut() {
|
||||||
|
current_fetch_context.cancel(CancelReason::Error);
|
||||||
|
}
|
||||||
|
|
||||||
// Step 6.3.
|
// Step 6.3.
|
||||||
// FIXME(nox): Detach MediaSource media provider object.
|
// FIXME(nox): Detach MediaSource media provider object.
|
||||||
|
@ -1023,10 +1017,8 @@ impl HTMLMediaElement {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 3.
|
// Step 3.
|
||||||
if self.seeking.get() {
|
// The fetch request associated with this seek already takes
|
||||||
// This will cancel only the sync part of the seek algorithm.
|
// care of cancelling any previous requests.
|
||||||
self.generation_id.set(self.generation_id.get() + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Step 4.
|
// Step 4.
|
||||||
// The flag will be cleared when the media engine tells us the seek was done.
|
// The flag will be cleared when the media engine tells us the seek was done.
|
||||||
|
@ -1046,6 +1038,12 @@ impl HTMLMediaElement {
|
||||||
// XXX(ferjm) seekable attribute: we need to get the information about
|
// XXX(ferjm) seekable attribute: we need to get the information about
|
||||||
// what's been decoded and buffered so far from servo-media
|
// what's been decoded and buffered so far from servo-media
|
||||||
// and add the seekable attribute as a TimeRange.
|
// and add the seekable attribute as a TimeRange.
|
||||||
|
if let Some(ref current_fetch_context) = *self.current_fetch_context.borrow() {
|
||||||
|
if !current_fetch_context.is_seekable() {
|
||||||
|
self.seeking.set(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Step 9.
|
// Step 9.
|
||||||
// servo-media with gstreamer does not support inaccurate seeking for now.
|
// servo-media with gstreamer does not support inaccurate seeking for now.
|
||||||
|
@ -1082,12 +1080,12 @@ impl HTMLMediaElement {
|
||||||
task_source.queue_simple_event(self.upcast(), atom!("seeked"), &window);
|
task_source.queue_simple_event(self.upcast(), atom!("seeked"), &window);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn setup_media_player(&self) -> Result<(), ServoMediaError> {
|
fn setup_media_player(&self) -> Result<(), PlayerError> {
|
||||||
let (action_sender, action_receiver) = ipc::channel().unwrap();
|
let (action_sender, action_receiver) = ipc::channel().unwrap();
|
||||||
|
|
||||||
self.player.register_event_handler(action_sender)?;
|
self.player.register_event_handler(action_sender);
|
||||||
self.player
|
self.player
|
||||||
.register_frame_renderer(self.frame_renderer.clone())?;
|
.register_frame_renderer(self.frame_renderer.clone());
|
||||||
|
|
||||||
let trusted_node = Trusted::new(self);
|
let trusted_node = Trusted::new(self);
|
||||||
let window = window_from_node(self);
|
let window = window_from_node(self);
|
||||||
|
@ -1115,6 +1113,24 @@ impl HTMLMediaElement {
|
||||||
|
|
||||||
fn handle_player_event(&self, event: &PlayerEvent) {
|
fn handle_player_event(&self, event: &PlayerEvent) {
|
||||||
match *event {
|
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();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
PlayerEvent::Error => {
|
||||||
|
self.error.set(Some(&*MediaError::new(
|
||||||
|
&*window_from_node(self),
|
||||||
|
MEDIA_ERR_DECODE,
|
||||||
|
)));
|
||||||
|
self.upcast::<EventTarget>().fire_event(atom!("error"));
|
||||||
|
},
|
||||||
|
PlayerEvent::FrameUpdated => {
|
||||||
|
self.upcast::<Node>().dirty(NodeDamage::OtherNodeDamage);
|
||||||
|
},
|
||||||
PlayerEvent::MetadataUpdated(ref metadata) => {
|
PlayerEvent::MetadataUpdated(ref metadata) => {
|
||||||
// https://html.spec.whatwg.org/multipage/#media-data-processing-steps-list
|
// https://html.spec.whatwg.org/multipage/#media-data-processing-steps-list
|
||||||
// => "Once enough of the media data has been fetched to determine the duration..."
|
// => "Once enough of the media data has been fetched to determine the duration..."
|
||||||
|
@ -1179,6 +1195,39 @@ impl HTMLMediaElement {
|
||||||
|
|
||||||
// XXX Steps 12 and 13 require audio and video tracks support.
|
// XXX Steps 12 and 13 require audio and video tracks support.
|
||||||
},
|
},
|
||||||
|
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(ref 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)
|
||||||
|
},
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
PlayerEvent::EnoughData => {
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
PlayerEvent::PositionChanged(position) => {
|
PlayerEvent::PositionChanged(position) => {
|
||||||
let position = position as f64;
|
let position = position as f64;
|
||||||
let _ = self
|
let _ = self
|
||||||
|
@ -1188,25 +1237,6 @@ impl HTMLMediaElement {
|
||||||
self.playback_position.set(position);
|
self.playback_position.set(position);
|
||||||
self.time_marches_on();
|
self.time_marches_on();
|
||||||
},
|
},
|
||||||
PlayerEvent::StateChanged(ref state) => match *state {
|
|
||||||
PlaybackState::Paused => {
|
|
||||||
if self.ready_state.get() == ReadyState::HaveMetadata {
|
|
||||||
self.change_ready_state(ReadyState::HaveEnoughData);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
_ => {},
|
|
||||||
},
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
PlayerEvent::FrameUpdated => {
|
|
||||||
self.upcast::<Node>().dirty(NodeDamage::OtherNodeDamage);
|
|
||||||
},
|
|
||||||
PlayerEvent::SeekData(p) => {
|
PlayerEvent::SeekData(p) => {
|
||||||
self.fetch_request(Some(p));
|
self.fetch_request(Some(p));
|
||||||
},
|
},
|
||||||
|
@ -1221,12 +1251,13 @@ impl HTMLMediaElement {
|
||||||
};
|
};
|
||||||
ScriptThread::await_stable_state(Microtask::MediaElement(task));
|
ScriptThread::await_stable_state(Microtask::MediaElement(task));
|
||||||
},
|
},
|
||||||
PlayerEvent::Error => {
|
PlayerEvent::StateChanged(ref state) => match *state {
|
||||||
self.error.set(Some(&*MediaError::new(
|
PlaybackState::Paused => {
|
||||||
&*window_from_node(self),
|
if self.ready_state.get() == ReadyState::HaveMetadata {
|
||||||
MEDIA_ERR_DECODE,
|
self.change_ready_state(ReadyState::HaveEnoughData);
|
||||||
)));
|
}
|
||||||
self.upcast::<EventTarget>().fire_event(atom!("error"));
|
},
|
||||||
|
_ => {},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1646,7 +1677,64 @@ enum Resource {
|
||||||
Url(ServoUrl),
|
Url(ServoUrl),
|
||||||
}
|
}
|
||||||
|
|
||||||
struct HTMLMediaElementContext {
|
/// Indicates the reason why a fetch request was cancelled.
|
||||||
|
#[derive(Debug, MallocSizeOf, PartialEq)]
|
||||||
|
enum CancelReason {
|
||||||
|
/// We were asked to stop pushing data to the player.
|
||||||
|
Backoff,
|
||||||
|
/// An error ocurred while fetching the media data.
|
||||||
|
Error,
|
||||||
|
/// A new request overrode this one.
|
||||||
|
Overridden,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(MallocSizeOf)]
|
||||||
|
pub struct HTMLMediaElementFetchContext {
|
||||||
|
/// Some if the request has been cancelled.
|
||||||
|
cancel_reason: Option<CancelReason>,
|
||||||
|
/// Indicates whether the fetched stream is seekable.
|
||||||
|
is_seekable: bool,
|
||||||
|
/// Fetch canceller. Allows cancelling the current fetch request by
|
||||||
|
/// manually calling its .cancel() method or automatically on Drop.
|
||||||
|
fetch_canceller: FetchCanceller,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HTMLMediaElementFetchContext {
|
||||||
|
fn new() -> (HTMLMediaElementFetchContext, ipc::IpcReceiver<()>) {
|
||||||
|
let mut fetch_canceller = FetchCanceller::new();
|
||||||
|
let cancel_receiver = fetch_canceller.initialize();
|
||||||
|
(
|
||||||
|
HTMLMediaElementFetchContext {
|
||||||
|
cancel_reason: None,
|
||||||
|
is_seekable: false,
|
||||||
|
fetch_canceller,
|
||||||
|
},
|
||||||
|
cancel_receiver,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_seekable(&self) -> bool {
|
||||||
|
self.is_seekable
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_seekable(&mut self, seekable: bool) {
|
||||||
|
self.is_seekable = seekable;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cancel(&mut self, reason: CancelReason) {
|
||||||
|
if self.cancel_reason.is_some() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self.cancel_reason = Some(reason);
|
||||||
|
self.fetch_canceller.cancel();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cancel_reason(&self) -> &Option<CancelReason> {
|
||||||
|
&self.cancel_reason
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct HTMLMediaElementFetchListener {
|
||||||
/// The element that initiated the request.
|
/// The element that initiated the request.
|
||||||
elem: Trusted<HTMLMediaElement>,
|
elem: Trusted<HTMLMediaElement>,
|
||||||
/// The response metadata received to date.
|
/// The response metadata received to date.
|
||||||
|
@ -1655,18 +1743,22 @@ struct HTMLMediaElementContext {
|
||||||
generation_id: u32,
|
generation_id: u32,
|
||||||
/// Time of last progress notification.
|
/// Time of last progress notification.
|
||||||
next_progress_event: Timespec,
|
next_progress_event: Timespec,
|
||||||
/// True if this response is invalid and should be ignored.
|
/// Timing data for this resource.
|
||||||
ignore_response: bool,
|
|
||||||
/// timing data for this resource
|
|
||||||
resource_timing: ResourceFetchTiming,
|
resource_timing: ResourceFetchTiming,
|
||||||
/// url for the resource
|
/// Url for the resource.
|
||||||
url: ServoUrl,
|
url: ServoUrl,
|
||||||
/// Amount of data fetched.
|
/// Expected content length of the media asset being fetched or played.
|
||||||
bytes_fetched: usize,
|
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,
|
||||||
}
|
}
|
||||||
|
|
||||||
// https://html.spec.whatwg.org/multipage/#media-data-processing-steps-list
|
// https://html.spec.whatwg.org/multipage/#media-data-processing-steps-list
|
||||||
impl FetchResponseListener for HTMLMediaElementContext {
|
impl FetchResponseListener for HTMLMediaElementFetchListener {
|
||||||
fn process_request_body(&mut self) {}
|
fn process_request_body(&mut self) {}
|
||||||
|
|
||||||
fn process_request_eof(&mut self) {}
|
fn process_request_eof(&mut self) {}
|
||||||
|
@ -1674,6 +1766,11 @@ impl FetchResponseListener for HTMLMediaElementContext {
|
||||||
fn process_response(&mut self, metadata: Result<FetchMetadata, NetworkError>) {
|
fn process_response(&mut self, metadata: Result<FetchMetadata, NetworkError>) {
|
||||||
let elem = self.elem.root();
|
let elem = self.elem.root();
|
||||||
|
|
||||||
|
if elem.generation_id.get() != self.generation_id {
|
||||||
|
// A new fetch request was triggered, so we ignore this response.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
self.metadata = metadata.ok().map(|m| match m {
|
self.metadata = metadata.ok().map(|m| match m {
|
||||||
FetchMetadata::Unfiltered(m) => m,
|
FetchMetadata::Unfiltered(m) => m,
|
||||||
FetchMetadata::Filtered { unsafe_, .. } => unsafe_,
|
FetchMetadata::Filtered { unsafe_, .. } => unsafe_,
|
||||||
|
@ -1693,49 +1790,85 @@ impl FetchResponseListener for HTMLMediaElementContext {
|
||||||
};
|
};
|
||||||
|
|
||||||
// We only set the expected input size if it changes.
|
// We only set the expected input size if it changes.
|
||||||
if content_length != elem.content_length.get() {
|
if content_length != self.expected_content_length {
|
||||||
if let Some(content_length) = content_length {
|
if let Some(content_length) = content_length {
|
||||||
if let Err(e) = self.elem.root().player.set_input_size(content_length) {
|
if let Err(e) = elem.player.set_input_size(content_length) {
|
||||||
warn!("Could not set player input size {:?}", e);
|
warn!("Could not set player input size {:?}", e);
|
||||||
} else {
|
} else {
|
||||||
elem.content_length.set(Some(content_length));
|
self.expected_content_length = Some(content_length);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let status_is_ok = self
|
let (status_is_ok, is_seekable) = self
|
||||||
.metadata
|
.metadata
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.and_then(|m| m.status.as_ref())
|
.and_then(|m| m.status.as_ref())
|
||||||
.map_or(true, |s| s.0 >= 200 && s.0 < 300);
|
.map_or((true, false), |s| {
|
||||||
|
(s.0 >= 200 && s.0 < 300, s.0 == 206 || s.0 == 416)
|
||||||
|
});
|
||||||
|
|
||||||
|
if is_seekable {
|
||||||
|
// The server supports range requests,
|
||||||
|
if let Some(ref mut current_fetch_context) = *elem.current_fetch_context.borrow_mut() {
|
||||||
|
current_fetch_context.set_seekable(true);
|
||||||
|
}
|
||||||
|
// and we can safely set the type of stream to Seekable.
|
||||||
|
if let Err(e) = elem.player.set_stream_type(StreamType::Seekable) {
|
||||||
|
warn!("Could not set stream type to Seekable. {:?}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// => "If the media data cannot be fetched at all..."
|
// => "If the media data cannot be fetched at all..."
|
||||||
if !status_is_ok {
|
if !status_is_ok {
|
||||||
// Ensure that the element doesn't receive any further notifications
|
// Ensure that the element doesn't receive any further notifications
|
||||||
// of the aborted fetch.
|
// of the aborted fetch.
|
||||||
self.ignore_response = true;
|
if let Some(ref mut current_fetch_context) = *elem.current_fetch_context.borrow_mut() {
|
||||||
elem.fetch_canceller.borrow_mut().cancel();
|
current_fetch_context.cancel(CancelReason::Error);
|
||||||
|
}
|
||||||
elem.queue_dedicated_media_source_failure_steps();
|
elem.queue_dedicated_media_source_failure_steps();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn process_response_chunk(&mut self, payload: Vec<u8>) {
|
fn process_response_chunk(&mut self, payload: Vec<u8>) {
|
||||||
if self.ignore_response {
|
let elem = self.elem.root();
|
||||||
// An error was received previously, skip processing the payload.
|
// 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 {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if let Some(ref current_fetch_context) = *elem.current_fetch_context.borrow() {
|
||||||
|
if current_fetch_context.cancel_reason().is_some() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
self.bytes_fetched += payload.len();
|
let payload_len = payload.len() as u64;
|
||||||
|
|
||||||
let elem = self.elem.root();
|
|
||||||
// Push input data into the player.
|
// Push input data into the player.
|
||||||
if let Err(e) = elem.player.push_data(payload) {
|
if let Err(e) = elem.player.push_data(payload) {
|
||||||
eprintln!("Could not push input data to player {:?}", e);
|
// 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.
|
||||||
|
match e {
|
||||||
|
PlayerError::EnoughData => {
|
||||||
|
if let Some(ref mut current_fetch_context) =
|
||||||
|
*elem.current_fetch_context.borrow_mut()
|
||||||
|
{
|
||||||
|
current_fetch_context.cancel(CancelReason::Backoff);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
warn!("Could not push input data to player {:?}", e);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.latest_fetched_content += payload_len;
|
||||||
|
|
||||||
// https://html.spec.whatwg.org/multipage/#concept-media-load-resource step 4,
|
// https://html.spec.whatwg.org/multipage/#concept-media-load-resource step 4,
|
||||||
// => "If mode is remote" step 2
|
// => "If mode is remote" step 2
|
||||||
if time::get_time() > self.next_progress_event {
|
if time::get_time() > self.next_progress_event {
|
||||||
|
@ -1751,16 +1884,21 @@ impl FetchResponseListener for HTMLMediaElementContext {
|
||||||
// https://html.spec.whatwg.org/multipage/#media-data-processing-steps-list
|
// https://html.spec.whatwg.org/multipage/#media-data-processing-steps-list
|
||||||
fn process_response_eof(&mut self, status: Result<ResourceFetchTiming, NetworkError>) {
|
fn process_response_eof(&mut self, status: Result<ResourceFetchTiming, NetworkError>) {
|
||||||
let elem = self.elem.root();
|
let elem = self.elem.root();
|
||||||
if self.ignore_response {
|
// If an error was previously received and no new fetch request was triggered,
|
||||||
// An error was received previously, skip processing the payload
|
// we skip processing the payload and notify the media backend that we are done
|
||||||
// and notify the media backend that we are done pushing data.
|
// pushing data.
|
||||||
if let Err(e) = elem.player.end_of_stream() {
|
if elem.generation_id.get() == self.generation_id {
|
||||||
warn!("Could not signal EOS to player {:?}", e);
|
if let Some(ref current_fetch_context) = *elem.current_fetch_context.borrow() {
|
||||||
|
if let Some(CancelReason::Error) = current_fetch_context.cancel_reason() {
|
||||||
|
if let Err(e) = elem.player.end_of_stream() {
|
||||||
|
warn!("Could not signal EOS to player {:?}", e);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if status.is_ok() && self.bytes_fetched != 0 {
|
if status.is_ok() && self.latest_fetched_content != 0 {
|
||||||
if elem.ready_state.get() == ReadyState::HaveNothing {
|
if elem.ready_state.get() == ReadyState::HaveNothing {
|
||||||
// Make sure that we don't skip the HaveMetadata and HaveCurrentData
|
// Make sure that we don't skip the HaveMetadata and HaveCurrentData
|
||||||
// states for short streams.
|
// states for short streams.
|
||||||
|
@ -1779,7 +1917,9 @@ impl FetchResponseListener for HTMLMediaElementContext {
|
||||||
// => "If the connection is interrupted after some media data has been received..."
|
// => "If the connection is interrupted after some media data has been received..."
|
||||||
else if elem.ready_state.get() != ReadyState::HaveNothing {
|
else if elem.ready_state.get() != ReadyState::HaveNothing {
|
||||||
// Step 1
|
// Step 1
|
||||||
elem.fetch_canceller.borrow_mut().cancel();
|
if let Some(ref mut current_fetch_context) = *elem.current_fetch_context.borrow_mut() {
|
||||||
|
current_fetch_context.cancel(CancelReason::Error);
|
||||||
|
}
|
||||||
|
|
||||||
// Step 2
|
// Step 2
|
||||||
elem.error.set(Some(&*MediaError::new(
|
elem.error.set(Some(&*MediaError::new(
|
||||||
|
@ -1814,7 +1954,7 @@ impl FetchResponseListener for HTMLMediaElementContext {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ResourceTimingListener for HTMLMediaElementContext {
|
impl ResourceTimingListener for HTMLMediaElementFetchListener {
|
||||||
fn resource_timing_information(&self) -> (InitiatorType, ServoUrl) {
|
fn resource_timing_information(&self) -> (InitiatorType, ServoUrl) {
|
||||||
let initiator_type = InitiatorType::LocalName(
|
let initiator_type = InitiatorType::LocalName(
|
||||||
self.elem
|
self.elem
|
||||||
|
@ -1831,24 +1971,24 @@ impl ResourceTimingListener for HTMLMediaElementContext {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PreInvoke for HTMLMediaElementContext {
|
impl PreInvoke for HTMLMediaElementFetchListener {
|
||||||
fn should_invoke(&self) -> bool {
|
fn should_invoke(&self) -> bool {
|
||||||
//TODO: finish_load needs to run at some point if the generation changes.
|
//TODO: finish_load needs to run at some point if the generation changes.
|
||||||
self.elem.root().generation_id.get() == self.generation_id
|
self.elem.root().generation_id.get() == self.generation_id
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl HTMLMediaElementContext {
|
impl HTMLMediaElementFetchListener {
|
||||||
fn new(elem: &HTMLMediaElement, url: ServoUrl) -> HTMLMediaElementContext {
|
fn new(elem: &HTMLMediaElement, url: ServoUrl, offset: u64) -> Self {
|
||||||
HTMLMediaElementContext {
|
Self {
|
||||||
elem: Trusted::new(elem),
|
elem: Trusted::new(elem),
|
||||||
metadata: None,
|
metadata: None,
|
||||||
generation_id: elem.generation_id.get(),
|
generation_id: elem.generation_id.get(),
|
||||||
next_progress_event: time::get_time() + Duration::milliseconds(350),
|
next_progress_event: time::get_time() + Duration::milliseconds(350),
|
||||||
ignore_response: false,
|
|
||||||
resource_timing: ResourceFetchTiming::new(ResourceTimingType::Resource),
|
resource_timing: ResourceFetchTiming::new(ResourceTimingType::Resource),
|
||||||
url,
|
url,
|
||||||
bytes_fetched: 0,
|
expected_content_length: None,
|
||||||
|
latest_fetched_content: offset,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue