diff options
author | bors-servo <lbergstrom+bors@mozilla.com> | 2019-07-22 21:33:47 -0400 |
---|---|---|
committer | GitHub <noreply@github.com> | 2019-07-22 21:33:47 -0400 |
commit | 42b6b18f7631cc111577417574fc6ff5249a23f9 (patch) | |
tree | 451d228c0f2853b6ed20966096c6fc5edae6e149 /components/script | |
parent | 28f7a87186178d24c449f110155b5e3d87c4de0c (diff) | |
parent | 388bb453b7d6042ad775c75ac256e68543713d3c (diff) | |
download | servo-42b6b18f7631cc111577417574fc6ff5249a23f9.tar.gz servo-42b6b18f7631cc111577417574fc6ff5249a23f9.zip |
Auto merge of #23208 - ferjm:media.ui, r=emilio,jdm
Media controls
<strike>This is still highly WIP. It depends on #22743 and so it is based on top of it.
The basic controls functionality is there, but the layout is highly broken. There is a hack to at least make the controls render on top of the video, but it is not correctly positioned. The controls' div container ends up as sibbling of the media element in the flow tree while IIUC it should end up as a child.</strike>
- [X] `./mach build -d` does not report any errors
- [x] `./mach test-tidy` does not report any errors
- [X] These changes fix #22721 and fix #22720
There is at least an extra dependency to improve the functionality and visual aspect: #22728.
<!-- 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/23208)
<!-- Reviewable:end -->
Diffstat (limited to 'components/script')
-rw-r--r-- | components/script/dom/document.rs | 36 | ||||
-rw-r--r-- | components/script/dom/element.rs | 22 | ||||
-rw-r--r-- | components/script/dom/htmlmediaelement.rs | 109 | ||||
-rw-r--r-- | components/script/dom/node.rs | 65 | ||||
-rw-r--r-- | components/script/dom/shadowroot.rs | 35 | ||||
-rw-r--r-- | components/script/dom/webidls/Document.webidl | 6 | ||||
-rw-r--r-- | components/script/dom/webidls/HTMLMediaElement.webidl | 2 |
7 files changed, 217 insertions, 58 deletions
diff --git a/components/script/dom/document.rs b/components/script/dom/document.rs index 1122fa52552..5b17c8ea004 100644 --- a/components/script/dom/document.rs +++ b/components/script/dom/document.rs @@ -20,6 +20,7 @@ use crate::dom::bindings::codegen::Bindings::HTMLIFrameElementBinding::HTMLIFram use crate::dom::bindings::codegen::Bindings::NodeBinding::NodeMethods; use crate::dom::bindings::codegen::Bindings::NodeFilterBinding::NodeFilter; use crate::dom::bindings::codegen::Bindings::PerformanceBinding::PerformanceMethods; +use crate::dom::bindings::codegen::Bindings::ShadowRootBinding::ShadowRootMethods; use crate::dom::bindings::codegen::Bindings::TouchBinding::TouchMethods; use crate::dom::bindings::codegen::Bindings::WindowBinding::{ FrameRequestCallback, ScrollBehavior, WindowMethods, @@ -163,6 +164,7 @@ use style::stylesheet_set::DocumentStylesheetSet; use style::stylesheets::{Origin, OriginSet, Stylesheet}; use url::percent_encoding::percent_decode; use url::Host; +use uuid::Uuid; /// The number of times we are allowed to see spurious `requestAnimationFrame()` calls before /// falling back to fake ones. @@ -385,6 +387,12 @@ pub struct Document { shadow_roots: DomRefCell<HashSet<Dom<ShadowRoot>>>, /// Whether any of the shadow roots need the stylesheets flushed. shadow_roots_styles_changed: Cell<bool>, + /// List of registered media controls. + /// We need to keep this list to allow the media controls to + /// access the "privileged" document.servoGetMediaControls(id) API, + /// where `id` needs to match any of the registered ShadowRoots + /// hosting the media controls UI. + media_controls: DomRefCell<HashMap<String, Dom<ShadowRoot>>>, } #[derive(JSTraceable, MallocSizeOf)] @@ -2457,6 +2465,23 @@ impl Document { self.responsive_images.borrow_mut().remove(i); } } + + pub fn register_media_controls(&self, controls: &ShadowRoot) -> String { + let id = Uuid::new_v4().to_string(); + self.media_controls + .borrow_mut() + .insert(id.clone(), Dom::from_ref(controls)); + id + } + + pub fn unregister_media_controls(&self, id: &str) { + if let Some(ref media_controls) = self.media_controls.borrow_mut().remove(id) { + let media_controls = DomRoot::from_ref(&**media_controls); + media_controls.Host().detach_shadow(); + } else { + debug_assert!(false, "Trying to unregister unknown media controls"); + } + } } #[derive(MallocSizeOf, PartialEq)] @@ -2750,6 +2775,7 @@ impl Document { delayed_tasks: Default::default(), shadow_roots: DomRefCell::new(HashSet::new()), shadow_roots_styles_changed: Cell::new(false), + media_controls: DomRefCell::new(HashMap::new()), } } @@ -4551,6 +4577,16 @@ impl DocumentMethods for Document { fn ExitFullscreen(&self) -> Rc<Promise> { self.exit_fullscreen() } + + // check-tidy: no specs after this line + // Servo only API to get an instance of the controls of a specific + // media element matching the given id. + fn ServoGetMediaControls(&self, id: DOMString) -> Fallible<DomRoot<ShadowRoot>> { + match self.media_controls.borrow().get(&*id) { + Some(m) => Ok(DomRoot::from_ref(&*m)), + None => Err(Error::InvalidAccess), + } + } } fn update_with_current_time_ms(marker: &Cell<u64>) { diff --git a/components/script/dom/element.rs b/components/script/dom/element.rs index 1145cb0fd7e..91d7a8aa450 100644 --- a/components/script/dom/element.rs +++ b/components/script/dom/element.rs @@ -78,7 +78,7 @@ use crate::dom::nodelist::NodeList; use crate::dom::promise::Promise; use crate::dom::raredata::ElementRareData; use crate::dom::servoparser::ServoParser; -use crate::dom::shadowroot::ShadowRoot; +use crate::dom::shadowroot::{IsUserAgentWidget, ShadowRoot}; use crate::dom::text::Text; use crate::dom::validation::Validatable; use crate::dom::virtualmethods::{vtable_for, VirtualMethods}; @@ -231,13 +231,6 @@ impl FromStr for AdjacentPosition { } } -/// Whether a shadow root hosts an User Agent widget. -#[derive(PartialEq)] -pub enum IsUserAgentWidget { - No, - Yes, -} - // // Element methods // @@ -498,14 +491,25 @@ impl Element { self.ensure_rare_data().shadow_root = Some(Dom::from_ref(&*shadow_root)); shadow_root .upcast::<Node>() - .set_containing_shadow_root(&shadow_root); + .set_containing_shadow_root(Some(&shadow_root)); if self.is_connected() { self.node.owner_doc().register_shadow_root(&*shadow_root); } + self.upcast::<Node>().dirty(NodeDamage::OtherNodeDamage); + Ok(shadow_root) } + + pub fn detach_shadow(&self) { + if let Some(ref shadow_root) = self.shadow_root() { + shadow_root.detach(); + self.ensure_rare_data().shadow_root = None; + } else { + debug_assert!(false, "Trying to detach a non-attached shadow root"); + } + } } #[allow(unsafe_code)] diff --git a/components/script/dom/htmlmediaelement.rs b/components/script/dom/htmlmediaelement.rs index 9ba07fe0c09..1b6f8ab1e3c 100644 --- a/components/script/dom/htmlmediaelement.rs +++ b/components/script/dom/htmlmediaelement.rs @@ -15,6 +15,7 @@ use crate::dom::bindings::codegen::Bindings::HTMLMediaElementBinding::HTMLMediaE use crate::dom::bindings::codegen::Bindings::HTMLSourceElementBinding::HTMLSourceElementMethods; use crate::dom::bindings::codegen::Bindings::MediaErrorBinding::MediaErrorConstants::*; use crate::dom::bindings::codegen::Bindings::MediaErrorBinding::MediaErrorMethods; +use crate::dom::bindings::codegen::Bindings::NodeBinding::NodeBinding::NodeMethods; use crate::dom::bindings::codegen::Bindings::TextTrackBinding::{TextTrackKind, TextTrackMode}; use crate::dom::bindings::codegen::InheritTypes::{ElementTypeId, HTMLElementTypeId}; use crate::dom::bindings::codegen::InheritTypes::{HTMLMediaElementTypeId, NodeTypeId}; @@ -33,18 +34,21 @@ use crate::dom::document::Document; use crate::dom::element::{ cors_setting_for_element, reflect_cross_origin_attribute, set_cross_origin_attribute, }; -use crate::dom::element::{AttributeMutation, Element}; +use crate::dom::element::{AttributeMutation, Element, ElementCreator}; use crate::dom::event::Event; use crate::dom::eventtarget::EventTarget; use crate::dom::globalscope::GlobalScope; use crate::dom::htmlelement::HTMLElement; +use crate::dom::htmlscriptelement::HTMLScriptElement; use crate::dom::htmlsourceelement::HTMLSourceElement; +use crate::dom::htmlstyleelement::HTMLStyleElement; use crate::dom::htmlvideoelement::HTMLVideoElement; use crate::dom::mediaerror::MediaError; use crate::dom::mediastream::MediaStream; use crate::dom::node::{document_from_node, window_from_node, Node, NodeDamage, UnbindContext}; use crate::dom::performanceresourcetiming::InitiatorType; use crate::dom::promise::Promise; +use crate::dom::shadowroot::IsUserAgentWidget; use crate::dom::texttrack::TextTrack; use crate::dom::texttracklist::TextTrackList; use crate::dom::timeranges::{TimeRanges, TimeRangesContainer}; @@ -59,6 +63,7 @@ use crate::network_listener::{self, NetworkListener, PreInvoke, ResourceTimingLi use crate::script_thread::ScriptThread; use crate::task_source::TaskSource; use dom_struct::dom_struct; +use embedder_traits::resources::{self, Resource as EmbedderResource}; use euclid::Size2D; use headers::{ContentLength, ContentRange, HeaderMapExt}; use html5ever::{LocalName, Prefix}; @@ -335,6 +340,11 @@ pub struct HTMLMediaElement { 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>>, } /// <https://html.spec.whatwg.org/multipage/#dom-media-networkstate> @@ -397,6 +407,7 @@ impl HTMLMediaElement { next_timeupdate_event: Cell::new(time::get_time() + Duration::milliseconds(250)), current_fetch_context: DomRefCell::new(None), id: Cell::new(0), + media_controls_id: DomRefCell::new(None), } } @@ -1637,6 +1648,12 @@ impl HTMLMediaElement { // https://github.com/servo/media/issues/156 // 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(); + } }, PlayerEvent::NeedData => { // The player needs more data. @@ -1712,6 +1729,68 @@ impl HTMLMediaElement { .start(0) .unwrap_or_else(|_| self.playback_position.get()) } + + fn render_controls(&self) { + let element = self.htmlelement.upcast::<Element>(); + if self.ready_state.get() < ReadyState::HaveMetadata || element.is_shadow_host() { + // Bail out if we have no metadata yet or + // if we are already showing the controls. + return; + } + let shadow_root = element.attach_shadow(IsUserAgentWidget::Yes).unwrap(); + let document = document_from_node(self); + let script = HTMLScriptElement::new( + local_name!("script"), + None, + &document, + ElementCreator::ScriptCreated, + ); + let mut media_controls_script = resources::read_string(EmbedderResource::MediaControlsJS); + // This is our hacky way to temporarily workaround the lack of a privileged + // JS context. + // The media controls UI accesses the document.servoGetMediaControls(id) API + // to get an instance to the media controls ShadowRoot. + // `id` needs to match the internally generated UUID assigned to a media element. + let id = document.register_media_controls(&shadow_root); + let media_controls_script = media_controls_script.as_mut_str().replace("@@@id@@@", &id); + *self.media_controls_id.borrow_mut() = Some(id); + script + .upcast::<Node>() + .SetTextContent(Some(DOMString::from(media_controls_script))); + if let Err(e) = shadow_root + .upcast::<Node>() + .AppendChild(&*script.upcast::<Node>()) + { + warn!("Could not render media controls {:?}", e); + return; + } + + let media_controls_style = resources::read_string(EmbedderResource::MediaControlsCSS); + let style = HTMLStyleElement::new( + local_name!("script"), + None, + &document, + ElementCreator::ScriptCreated, + ); + style + .upcast::<Node>() + .SetTextContent(Some(DOMString::from(media_controls_style))); + + if let Err(e) = shadow_root + .upcast::<Node>() + .AppendChild(&*style.upcast::<Node>()) + { + warn!("Could not render media controls {:?}", e); + } + + self.upcast::<Node>().dirty(NodeDamage::OtherNodeDamage); + } + + fn remove_controls(&self) { + if let Some(id) = self.media_controls_id.borrow_mut().take() { + document_from_node(self).unregister_media_controls(&id); + } + } } // XXX Placeholder for [https://github.com/servo/servo/issues/22293] @@ -1754,6 +1833,7 @@ impl Drop for HTMLMediaElement { .unwrap() .shutdown_player(&client_context_id, player.clone()); } + self.remove_controls(); } } @@ -1783,6 +1863,11 @@ impl HTMLMediaElementMethods for HTMLMediaElement { // https://html.spec.whatwg.org/multipage/#dom-media-defaultmuted make_bool_setter!(SetDefaultMuted, "muted"); + // https://html.spec.whatwg.org/multipage/#dom-media-controls + make_bool_getter!(Controls, "controls"); + // https://html.spec.whatwg.org/multipage/#dom-media-controls + make_bool_setter!(SetControls, "controls"); + // https://html.spec.whatwg.org/multipage/#dom-media-src make_url_getter!(Src, "src"); @@ -2177,19 +2262,23 @@ impl VirtualMethods for HTMLMediaElement { fn attribute_mutated(&self, attr: &Attr, mutation: AttributeMutation) { self.super_type().unwrap().attribute_mutated(attr, mutation); - if &local_name!("muted") == attr.local_name() { - self.SetMuted(mutation.new_value(attr).is_some()); - return; - } - - if mutation.new_value(attr).is_none() { - return; - } - match attr.local_name() { + &local_name!("muted") => { + self.SetMuted(mutation.new_value(attr).is_some()); + }, &local_name!("src") => { + if mutation.new_value(attr).is_none() { + return; + } self.media_element_load_algorithm(); }, + &local_name!("controls") => { + if mutation.new_value(attr).is_some() { + self.render_controls(); + } else { + self.remove_controls(); + } + }, _ => (), }; } diff --git a/components/script/dom/node.rs b/components/script/dom/node.rs index fe7f9ec29c2..8165d0c4eef 100644 --- a/components/script/dom/node.rs +++ b/components/script/dom/node.rs @@ -283,7 +283,7 @@ impl Node { for node in new_child.traverse_preorder(ShadowIncluding::No) { if parent_in_shadow_tree { if let Some(shadow_root) = self.containing_shadow_root() { - node.set_containing_shadow_root(&*shadow_root); + node.set_containing_shadow_root(Some(&*shadow_root)); } debug_assert!(node.containing_shadow_root().is_some()); } @@ -299,6 +299,36 @@ impl Node { } } + /// Clean up flags and unbind from tree. + pub fn complete_remove_subtree(root: &Node, context: &UnbindContext) { + for node in root.traverse_preorder(ShadowIncluding::Yes) { + // Out-of-document elements never have the descendants flag set. + node.set_flag( + NodeFlags::IS_IN_DOC | + NodeFlags::IS_CONNECTED | + NodeFlags::HAS_DIRTY_DESCENDANTS | + NodeFlags::HAS_SNAPSHOT | + NodeFlags::HANDLED_SNAPSHOT, + false, + ); + } + for node in root.traverse_preorder(ShadowIncluding::Yes) { + // This needs to be in its own loop, because unbind_from_tree may + // rely on the state of IS_IN_DOC of the context node's descendants, + // e.g. when removing a <form>. + vtable_for(&&*node).unbind_from_tree(&context); + node.style_and_layout_data.get().map(|d| node.dispose(d)); + // https://dom.spec.whatwg.org/#concept-node-remove step 14 + if let Some(element) = node.as_custom_element() { + ScriptThread::enqueue_callback_reaction( + &*element, + CallbackReaction::Disconnected, + None, + ); + } + } + } + /// Removes the given child from this node's list of children. /// /// Fails unless `child` is a child of this node. @@ -339,32 +369,7 @@ impl Node { child.parent_node.set(None); self.children_count.set(self.children_count.get() - 1); - for node in child.traverse_preorder(ShadowIncluding::Yes) { - // Out-of-document elements never have the descendants flag set. - node.set_flag( - NodeFlags::IS_IN_DOC | - NodeFlags::IS_CONNECTED | - NodeFlags::HAS_DIRTY_DESCENDANTS | - NodeFlags::HAS_SNAPSHOT | - NodeFlags::HANDLED_SNAPSHOT, - false, - ); - } - for node in child.traverse_preorder(ShadowIncluding::Yes) { - // This needs to be in its own loop, because unbind_from_tree may - // rely on the state of IS_IN_DOC of the context node's descendants, - // e.g. when removing a <form>. - vtable_for(&&*node).unbind_from_tree(&context); - node.style_and_layout_data.get().map(|d| node.dispose(d)); - // https://dom.spec.whatwg.org/#concept-node-remove step 14 - if let Some(element) = node.as_custom_element() { - ScriptThread::enqueue_callback_reaction( - &*element, - CallbackReaction::Disconnected, - None, - ); - } - } + Self::complete_remove_subtree(child, &context); } pub fn to_untrusted_node_address(&self) -> UntrustedNodeAddress { @@ -961,8 +966,8 @@ impl Node { .map(|sr| DomRoot::from_ref(&**sr)) } - pub fn set_containing_shadow_root(&self, shadow_root: &ShadowRoot) { - self.ensure_rare_data().containing_shadow_root = Some(Dom::from_ref(shadow_root)); + pub fn set_containing_shadow_root(&self, shadow_root: Option<&ShadowRoot>) { + self.ensure_rare_data().containing_shadow_root = shadow_root.map(Dom::from_ref); } pub fn is_in_html_doc(&self) -> bool { @@ -3082,7 +3087,7 @@ pub struct UnbindContext<'a> { impl<'a> UnbindContext<'a> { /// Create a new `UnbindContext` value. - fn new( + pub fn new( parent: &'a Node, prev_sibling: Option<&'a Node>, next_sibling: Option<&'a Node>, diff --git a/components/script/dom/shadowroot.rs b/components/script/dom/shadowroot.rs index ae108f8109c..d6df87bded8 100644 --- a/components/script/dom/shadowroot.rs +++ b/components/script/dom/shadowroot.rs @@ -14,7 +14,7 @@ use crate::dom::document::Document; use crate::dom::documentfragment::DocumentFragment; use crate::dom::documentorshadowroot::{DocumentOrShadowRoot, StyleSheetInDocument}; use crate::dom::element::Element; -use crate::dom::node::{Node, NodeDamage, NodeFlags, ShadowIncluding}; +use crate::dom::node::{Node, NodeDamage, NodeFlags, ShadowIncluding, UnbindContext}; use crate::dom::stylesheetlist::{StyleSheetList, StyleSheetListOwner}; use crate::dom::window::Window; use crate::stylesheet_set::StylesheetSetRef; @@ -28,13 +28,20 @@ use style::media_queries::Device; use style::shared_lock::SharedRwLockReadGuard; use style::stylesheets::Stylesheet; +/// Whether a shadow root hosts an User Agent widget. +#[derive(JSTraceable, MallocSizeOf, PartialEq)] +pub enum IsUserAgentWidget { + No, + Yes, +} + // https://dom.spec.whatwg.org/#interface-shadowroot #[dom_struct] pub struct ShadowRoot { document_fragment: DocumentFragment, document_or_shadow_root: DocumentOrShadowRoot, document: Dom<Document>, - host: Dom<Element>, + host: MutNullableDom<Element>, /// List of author styles associated with nodes in this shadow tree. author_styles: DomRefCell<AuthorStyles<StyleSheetInDocument>>, stylesheet_list: MutNullableDom<StyleSheetList>, @@ -55,7 +62,7 @@ impl ShadowRoot { document_fragment, document_or_shadow_root: DocumentOrShadowRoot::new(document.window()), document: Dom::from_ref(document), - host: Dom::from_ref(host), + host: MutNullableDom::new(Some(host)), author_styles: DomRefCell::new(AuthorStyles::new()), stylesheet_list: MutNullableDom::new(None), window: Dom::from_ref(document.window()), @@ -70,6 +77,14 @@ impl ShadowRoot { ) } + pub fn detach(&self) { + self.document.unregister_shadow_root(&self); + let node = self.upcast::<Node>(); + node.set_containing_shadow_root(None); + Node::complete_remove_subtree(&node, &UnbindContext::new(node, None, None, None)); + self.host.set(None); + } + pub fn get_focused_element(&self) -> Option<DomRoot<Element>> { //XXX get retargeted focused element None @@ -123,9 +138,9 @@ impl ShadowRoot { self.document.invalidate_shadow_roots_stylesheets(); self.author_styles.borrow_mut().stylesheets.force_dirty(); // Mark the host element dirty so a reflow will be performed. - self.host - .upcast::<Node>() - .dirty(NodeDamage::NodeStyleDamaged); + if let Some(host) = self.host.get() { + host.upcast::<Node>().dirty(NodeDamage::NodeStyleDamaged); + } } /// Remove any existing association between the provided id and any elements @@ -209,7 +224,8 @@ impl ShadowRootMethods for ShadowRoot { /// https://dom.spec.whatwg.org/#dom-shadowroot-host fn Host(&self) -> DomRoot<Element> { - DomRoot::from_ref(&self.host) + let host = self.host.get(); + host.expect("Trying to get host from a detached shadow root") } // https://drafts.csswg.org/cssom/#dom-document-stylesheets @@ -241,7 +257,10 @@ impl LayoutShadowRootHelpers for LayoutDom<ShadowRoot> { #[inline] #[allow(unsafe_code)] unsafe fn get_host_for_layout(&self) -> LayoutDom<Element> { - (*self.unsafe_get()).host.to_layout() + (*self.unsafe_get()) + .host + .get_inner_as_layout() + .expect("We should never do layout on a detached shadow root") } #[inline] diff --git a/components/script/dom/webidls/Document.webidl b/components/script/dom/webidls/Document.webidl index 0127f8ecf98..ebcdea32166 100644 --- a/components/script/dom/webidls/Document.webidl +++ b/components/script/dom/webidls/Document.webidl @@ -211,3 +211,9 @@ partial interface Document { }; Document implements DocumentOrShadowRoot; + +// Servo internal API. +partial interface Document { + [Throws] + ShadowRoot servoGetMediaControls(DOMString id); +}; diff --git a/components/script/dom/webidls/HTMLMediaElement.webidl b/components/script/dom/webidls/HTMLMediaElement.webidl index 21ac720fd06..cdf32c98312 100644 --- a/components/script/dom/webidls/HTMLMediaElement.webidl +++ b/components/script/dom/webidls/HTMLMediaElement.webidl @@ -53,7 +53,7 @@ interface HTMLMediaElement : HTMLElement { void pause(); // controls - // [CEReactions] attribute boolean controls; + [CEReactions] attribute boolean controls; [Throws] attribute double volume; attribute boolean muted; [CEReactions] attribute boolean defaultMuted; |