From 4f6b86f9f5234096b4f8f336e297c1cf887ab2d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fernando=20Jim=C3=A9nez=20Moreno?= Date: Fri, 12 Apr 2019 11:15:26 +0200 Subject: [PATCH 01/31] Render media controls --- components/layout/construct.rs | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/components/layout/construct.rs b/components/layout/construct.rs index 21e3611f446..f56da289dda 100644 --- a/components/layout/construct.rs +++ b/components/layout/construct.rs @@ -705,7 +705,8 @@ impl<'a, ConcreteThreadSafeLayoutNode: ThreadSafeLayoutNode> // List of absolute descendants, in tree order. let mut abs_descendants = AbsoluteDescendants::new(); let mut legalizer = Legalizer::new(); - if !node.is_replaced_content() { + if !node.is_replaced_content() || + node.type_id() == Some(LayoutNodeType::Element(LayoutElementType::HTMLMediaElement)) { for kid in node.children() { if kid.get_pseudo_element_type() != PseudoElementType::Normal { self.process(&kid); @@ -1248,9 +1249,14 @@ impl<'a, ConcreteThreadSafeLayoutNode: ThreadSafeLayoutNode> // Go to a path that concatenates our kids' fragments. self.build_fragments_for_nonreplaced_inline_content(node) } else { - // Otherwise, just nuke our kids' fragments, create our fragment if any, and be done - // with it. - self.build_fragments_for_replaced_inline_content(node) + if node.type_id() == Some(LayoutNodeType::Element(LayoutElementType::HTMLMediaElement)) { + // Do not treat media elements as leafs. + self.build_flow_for_block(node, None) + } else { + // Otherwise, just nuke our kids' fragments, create our fragment if any, and be done + // with it. + self.build_fragments_for_replaced_inline_content(node) + } } } From 1c02fc94a8e40736d6dd8aca555217b7a9b8c672 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fernando=20Jim=C3=A9nez=20Moreno?= Date: Fri, 5 Apr 2019 10:50:01 +0200 Subject: [PATCH 02/31] Barebones media UI --- components/embedder_traits/resources.rs | 2 + components/script/dom/document.rs | 29 +++++++ components/script/dom/element.rs | 13 +--- components/script/dom/htmlmediaelement.rs | 78 ++++++++++++++++--- components/script/dom/htmlstyleelement.rs | 17 +++- components/script/dom/shadowroot.rs | 20 ++++- components/script/dom/webidls/Document.webidl | 6 ++ components/script/stylesheet_loader.rs | 10 ++- ports/glutin/resources.rs | 1 + ports/libsimpleservo/api/src/lib.rs | 3 + resources/media_controls.js | 23 ++++++ 11 files changed, 177 insertions(+), 25 deletions(-) create mode 100644 resources/media_controls.js diff --git a/components/embedder_traits/resources.rs b/components/embedder_traits/resources.rs index a29208fc67c..95799aef0d5 100644 --- a/components/embedder_traits/resources.rs +++ b/components/embedder_traits/resources.rs @@ -60,6 +60,7 @@ pub enum Resource { PresentationalHintsCSS, QuirksModeCSS, RippyPNG, + MediaControls, } pub trait ResourceReaderMethods { @@ -94,6 +95,7 @@ fn resources_for_tests() -> Box { Resource::PresentationalHintsCSS => "presentational-hints.css", Resource::QuirksModeCSS => "quirks-mode.css", Resource::RippyPNG => "rippy.png", + Resource::MediaControls => "media_controls.js", }; let mut path = env::current_exe().unwrap(); path = path.canonicalize().unwrap(); diff --git a/components/script/dom/document.rs b/components/script/dom/document.rs index 1122fa52552..7e1314f400a 100644 --- a/components/script/dom/document.rs +++ b/components/script/dom/document.rs @@ -163,6 +163,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 +386,12 @@ pub struct Document { shadow_roots: DomRefCell>>, /// Whether any of the shadow roots need the stylesheets flushed. shadow_roots_styles_changed: Cell, + /// 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>>, } #[derive(JSTraceable, MallocSizeOf)] @@ -2457,6 +2464,18 @@ 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) { + self.media_controls.borrow_mut().remove(id); + } } #[derive(MallocSizeOf, PartialEq)] @@ -2750,6 +2769,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 +4571,15 @@ impl DocumentMethods for Document { fn ExitFullscreen(&self) -> Rc { self.exit_fullscreen() } + + // 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> { + 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) { diff --git a/components/script/dom/element.rs b/components/script/dom/element.rs index 1145cb0fd7e..b09aaf0dce7 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 // @@ -494,7 +487,7 @@ impl Element { } // Steps 4, 5 and 6. - let shadow_root = ShadowRoot::new(self, &*self.node.owner_doc()); + let shadow_root = ShadowRoot::new(self, &*self.node.owner_doc(), is_ua_widget); self.ensure_rare_data().shadow_root = Some(Dom::from_ref(&*shadow_root)); shadow_root .upcast::() @@ -504,6 +497,8 @@ impl Element { self.node.owner_doc().register_shadow_root(&*shadow_root); } + self.upcast::().dirty(NodeDamage::OtherNodeDamage); + Ok(shadow_root) } } diff --git a/components/script/dom/htmlmediaelement.rs b/components/script/dom/htmlmediaelement.rs index 9ba07fe0c09..7be61332189 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,11 +34,12 @@ 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::htmlvideoelement::HTMLVideoElement; use crate::dom::mediaerror::MediaError; @@ -45,6 +47,7 @@ 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 +62,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 +339,11 @@ pub struct HTMLMediaElement { current_fetch_context: DomRefCell>, /// Player Id reported the player thread id: Cell, + /// 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>, } /// @@ -397,6 +406,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), } } @@ -1712,6 +1722,47 @@ impl HTMLMediaElement { .start(0) .unwrap_or_else(|_| self.playback_position.get()) } + + fn render_controls(&self) { + println!("render_controls"); + // XXX cannot render controls while parsing. + // XXX render controls as a top layer. + // XXX check that controls are not already rendered. + let element = self.htmlelement.upcast::(); + if let Ok(shadow_root) = element.attach_shadow(IsUserAgentWidget::Yes) { + let document = document_from_node(self); + let script = HTMLScriptElement::new( + local_name!("script"), + None, + &document, + ElementCreator::ScriptCreated, + ); + let mut media_controls = resources::read_string(EmbedderResource::MediaControls); + // 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 = media_controls.as_mut_str().replace("@@@id@@@", &id); + *self.media_controls_id.borrow_mut() = Some(id); + script + .upcast::() + .SetTextContent(Some(DOMString::from(media_controls))); + if let Err(e) = shadow_root + .upcast::() + .AppendChild(&*script.upcast::()) + { + warn!("Could not render media controls {:?}", e); + } + + self.upcast::().dirty(NodeDamage::OtherNodeDamage); + } + } + + fn hide_controls(&self) { + println!("hide_controls"); + } } // XXX Placeholder for [https://github.com/servo/servo/issues/22293] @@ -1754,6 +1805,9 @@ impl Drop for HTMLMediaElement { .unwrap() .shutdown_player(&client_context_id, player.clone()); } + if let Some(ref id) = *self.media_controls_id.borrow() { + document_from_node(self).unregister_media_controls(id); + } } } @@ -2177,19 +2231,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.hide_controls(); + } + }, _ => (), }; } diff --git a/components/script/dom/htmlstyleelement.rs b/components/script/dom/htmlstyleelement.rs index e483453252a..8c8864c4626 100644 --- a/components/script/dom/htmlstyleelement.rs +++ b/components/script/dom/htmlstyleelement.rs @@ -24,6 +24,7 @@ use dom_struct::dom_struct; use html5ever::{LocalName, Prefix}; use net_traits::ReferrerPolicy; use servo_arc::Arc; +use servo_url::ServoUrl; use std::cell::Cell; use style::media_queries::MediaList; use style::parser::ParserContext as CssParserContext; @@ -111,10 +112,22 @@ impl HTMLStyleElement { let mq = Arc::new(shared_lock.wrap(MediaList::parse(&context, &mut CssParser::new(&mut input)))); let loader = StylesheetLoader::for_element(self.upcast()); + let (url, origin) = if let Some(shadow_root) = self + .upcast::() + .containing_shadow_root() { + if shadow_root.is_user_agent_widget() { + (ServoUrl::parse(&format!("chrome://{:?}", window.get_url().to_string())).unwrap(), Origin::UserAgent) + } else { + (window.get_url(), Origin::Author) + } + } else { + (window.get_url(), Origin::Author) + }; + let sheet = Stylesheet::from_str( &data, - window.get_url(), - Origin::Author, + url, + origin, mq, shared_lock, Some(&loader), diff --git a/components/script/dom/shadowroot.rs b/components/script/dom/shadowroot.rs index ae108f8109c..d655b8468f5 100644 --- a/components/script/dom/shadowroot.rs +++ b/components/script/dom/shadowroot.rs @@ -28,6 +28,13 @@ 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 { @@ -39,11 +46,13 @@ pub struct ShadowRoot { author_styles: DomRefCell>, stylesheet_list: MutNullableDom, window: Dom, + /// Whether this ShadowRoot hosts a User Agent widget. + is_widget: IsUserAgentWidget, } impl ShadowRoot { #[allow(unrooted_must_root)] - fn new_inherited(host: &Element, document: &Document) -> ShadowRoot { + fn new_inherited(host: &Element, document: &Document, is_widget: IsUserAgentWidget) -> ShadowRoot { let document_fragment = DocumentFragment::new_inherited(document); let node = document_fragment.upcast::(); node.set_flag(NodeFlags::IS_IN_SHADOW_TREE, true); @@ -59,12 +68,13 @@ impl ShadowRoot { author_styles: DomRefCell::new(AuthorStyles::new()), stylesheet_list: MutNullableDom::new(None), window: Dom::from_ref(document.window()), + is_widget, } } - pub fn new(host: &Element, document: &Document) -> DomRoot { + pub fn new(host: &Element, document: &Document, is_widget: IsUserAgentWidget) -> DomRoot { reflect_dom_object( - Box::new(ShadowRoot::new_inherited(host, document)), + Box::new(ShadowRoot::new_inherited(host, document, is_widget)), document.window(), ShadowRootBinding::Wrap, ) @@ -152,6 +162,10 @@ impl ShadowRoot { root, ); } + + pub fn is_user_agent_widget(&self) -> bool { + self.is_widget == IsUserAgentWidget::Yes + } } impl ShadowRootMethods for ShadowRoot { 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/stylesheet_loader.rs b/components/script/stylesheet_loader.rs index 69975c7803b..c2716264bb7 100644 --- a/components/script/stylesheet_loader.rs +++ b/components/script/stylesheet_loader.rs @@ -141,7 +141,15 @@ impl FetchResponseListener for StylesheetContext { // TODO: Get the actual value. http://dev.w3.org/csswg/css-syntax/#environment-encoding let environment_encoding = UTF_8; let protocol_encoding_label = metadata.charset.as_ref().map(|s| &**s); - let final_url = metadata.final_url; + let final_url = if let Some(ref shadow_root) = self.shadow_root { + if shadow_root.root().is_user_agent_widget() { + ServoUrl::parse(&format!("chrome://{:?}", metadata.final_url.to_string())).unwrap() + } else { + metadata.final_url + } + } else { + metadata.final_url + }; let win = window_from_node(&*elem); diff --git a/ports/glutin/resources.rs b/ports/glutin/resources.rs index 5a48037f524..839ebdab1d4 100644 --- a/ports/glutin/resources.rs +++ b/ports/glutin/resources.rs @@ -29,6 +29,7 @@ fn filename(file: Resource) -> &'static str { Resource::PresentationalHintsCSS => "presentational-hints.css", Resource::QuirksModeCSS => "quirks-mode.css", Resource::RippyPNG => "rippy.png", + Resource::MediaControls => "media_controls.js", } } diff --git a/ports/libsimpleservo/api/src/lib.rs b/ports/libsimpleservo/api/src/lib.rs index 9dac35f1aa4..82e81f1f673 100644 --- a/ports/libsimpleservo/api/src/lib.rs +++ b/ports/libsimpleservo/api/src/lib.rs @@ -702,6 +702,9 @@ impl ResourceReaderMethods for ResourceReaderInstance { Resource::BluetoothBlocklist => { &include_bytes!("../../../../resources/gatt_blocklist.txt")[..] }, + Resource::MediaControls => { + &include_bytes!("../../../../resources/media_controls.js")[..] + }, }) } diff --git a/resources/media_controls.js b/resources/media_controls.js new file mode 100644 index 00000000000..216fad4d603 --- /dev/null +++ b/resources/media_controls.js @@ -0,0 +1,23 @@ +console.log('YO'); +let controls = document.servoGetMediaControls("@@@id@@@"); + +let style = document.createElement("style"); +style.textContent = `#controls { + -servo-top-layer: top; + display: block; + position: fixed; + left: 0px; + bottom: 0px; + height: 30px; + width: 100%; + background: blue; +}`; +controls.appendChild(style); + +let div = document.createElement("div"); +div.setAttribute("id", "controls"); +let button = document.createElement("button"); +button.textContent = "Click me"; +div.appendChild(button); +controls.appendChild(div); +console.log('INNER', div.innerHTML); From a66444968177b09a54f556d973f5cd524dcaa1b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fernando=20Jim=C3=A9nez=20Moreno?= Date: Tue, 16 Apr 2019 11:46:55 +0200 Subject: [PATCH 03/31] Media UI basic functionality --- components/embedder_traits/resources.rs | 6 +- components/layout/construct.rs | 6 +- components/script/dom/htmlmediaelement.rs | 27 +- components/script/dom/htmlstyleelement.rs | 6 +- components/script/dom/shadowroot.rs | 12 +- components/script/stylesheet_loader.rs | 3 +- ports/glutin/resources.rs | 5 +- ports/libsimpleservo/api/src/lib.rs | 5 +- resources/media_controls.css | 38 +++ resources/media_controls.js | 347 ++++++++++++++++++++-- 10 files changed, 420 insertions(+), 35 deletions(-) create mode 100644 resources/media_controls.css diff --git a/components/embedder_traits/resources.rs b/components/embedder_traits/resources.rs index 95799aef0d5..16f19e489dd 100644 --- a/components/embedder_traits/resources.rs +++ b/components/embedder_traits/resources.rs @@ -60,7 +60,8 @@ pub enum Resource { PresentationalHintsCSS, QuirksModeCSS, RippyPNG, - MediaControls, + MediaControlsCSS, + MediaControlsJS, } pub trait ResourceReaderMethods { @@ -95,7 +96,8 @@ fn resources_for_tests() -> Box { Resource::PresentationalHintsCSS => "presentational-hints.css", Resource::QuirksModeCSS => "quirks-mode.css", Resource::RippyPNG => "rippy.png", - Resource::MediaControls => "media_controls.js", + Resource::MediaControlsCSS => "media_controls.css", + Resource::MediaControlsJS => "media_controls.js", }; let mut path = env::current_exe().unwrap(); path = path.canonicalize().unwrap(); diff --git a/components/layout/construct.rs b/components/layout/construct.rs index f56da289dda..f108876d53c 100644 --- a/components/layout/construct.rs +++ b/components/layout/construct.rs @@ -706,7 +706,8 @@ impl<'a, ConcreteThreadSafeLayoutNode: ThreadSafeLayoutNode> let mut abs_descendants = AbsoluteDescendants::new(); let mut legalizer = Legalizer::new(); if !node.is_replaced_content() || - node.type_id() == Some(LayoutNodeType::Element(LayoutElementType::HTMLMediaElement)) { + node.type_id() == Some(LayoutNodeType::Element(LayoutElementType::HTMLMediaElement)) + { for kid in node.children() { if kid.get_pseudo_element_type() != PseudoElementType::Normal { self.process(&kid); @@ -1249,7 +1250,8 @@ impl<'a, ConcreteThreadSafeLayoutNode: ThreadSafeLayoutNode> // Go to a path that concatenates our kids' fragments. self.build_fragments_for_nonreplaced_inline_content(node) } else { - if node.type_id() == Some(LayoutNodeType::Element(LayoutElementType::HTMLMediaElement)) { + if node.type_id() == Some(LayoutNodeType::Element(LayoutElementType::HTMLMediaElement)) + { // Do not treat media elements as leafs. self.build_flow_for_block(node, None) } else { diff --git a/components/script/dom/htmlmediaelement.rs b/components/script/dom/htmlmediaelement.rs index 7be61332189..3e640fee358 100644 --- a/components/script/dom/htmlmediaelement.rs +++ b/components/script/dom/htmlmediaelement.rs @@ -41,6 +41,7 @@ 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; @@ -1737,21 +1738,41 @@ impl HTMLMediaElement { &document, ElementCreator::ScriptCreated, ); - let mut media_controls = resources::read_string(EmbedderResource::MediaControls); + 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 = media_controls.as_mut_str().replace("@@@id@@@", &id); + let media_controls_script = media_controls_script.as_mut_str().replace("@@@id@@@", &id); *self.media_controls_id.borrow_mut() = Some(id); script .upcast::() - .SetTextContent(Some(DOMString::from(media_controls))); + .SetTextContent(Some(DOMString::from(media_controls_script))); if let Err(e) = shadow_root .upcast::() .AppendChild(&*script.upcast::()) + { + 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::() + .SetTextContent(Some(DOMString::from(media_controls_style))); + + if let Err(e) = shadow_root + .upcast::() + .AppendChild(&*style.upcast::()) { warn!("Could not render media controls {:?}", e); } diff --git a/components/script/dom/htmlstyleelement.rs b/components/script/dom/htmlstyleelement.rs index 8c8864c4626..cf64e316218 100644 --- a/components/script/dom/htmlstyleelement.rs +++ b/components/script/dom/htmlstyleelement.rs @@ -116,7 +116,11 @@ impl HTMLStyleElement { .upcast::() .containing_shadow_root() { if shadow_root.is_user_agent_widget() { - (ServoUrl::parse(&format!("chrome://{:?}", window.get_url().to_string())).unwrap(), Origin::UserAgent) + ( + ServoUrl::parse(&format!("chrome://{:?}", window.get_url().to_string())) + .unwrap(), + Origin::UserAgent, + ) } else { (window.get_url(), Origin::Author) } diff --git a/components/script/dom/shadowroot.rs b/components/script/dom/shadowroot.rs index d655b8468f5..47deb9067e6 100644 --- a/components/script/dom/shadowroot.rs +++ b/components/script/dom/shadowroot.rs @@ -52,7 +52,11 @@ pub struct ShadowRoot { impl ShadowRoot { #[allow(unrooted_must_root)] - fn new_inherited(host: &Element, document: &Document, is_widget: IsUserAgentWidget) -> ShadowRoot { + fn new_inherited( + host: &Element, + document: &Document, + is_widget: IsUserAgentWidget, + ) -> ShadowRoot { let document_fragment = DocumentFragment::new_inherited(document); let node = document_fragment.upcast::(); node.set_flag(NodeFlags::IS_IN_SHADOW_TREE, true); @@ -72,7 +76,11 @@ impl ShadowRoot { } } - pub fn new(host: &Element, document: &Document, is_widget: IsUserAgentWidget) -> DomRoot { + pub fn new( + host: &Element, + document: &Document, + is_widget: IsUserAgentWidget, + ) -> DomRoot { reflect_dom_object( Box::new(ShadowRoot::new_inherited(host, document, is_widget)), document.window(), diff --git a/components/script/stylesheet_loader.rs b/components/script/stylesheet_loader.rs index c2716264bb7..35697fd981f 100644 --- a/components/script/stylesheet_loader.rs +++ b/components/script/stylesheet_loader.rs @@ -143,7 +143,8 @@ impl FetchResponseListener for StylesheetContext { let protocol_encoding_label = metadata.charset.as_ref().map(|s| &**s); let final_url = if let Some(ref shadow_root) = self.shadow_root { if shadow_root.root().is_user_agent_widget() { - ServoUrl::parse(&format!("chrome://{:?}", metadata.final_url.to_string())).unwrap() + ServoUrl::parse(&format!("chrome://{:?}", metadata.final_url.to_string())) + .unwrap() } else { metadata.final_url } diff --git a/ports/glutin/resources.rs b/ports/glutin/resources.rs index 839ebdab1d4..d668f0ac247 100644 --- a/ports/glutin/resources.rs +++ b/ports/glutin/resources.rs @@ -28,8 +28,9 @@ fn filename(file: Resource) -> &'static str { Resource::ServoCSS => "servo.css", Resource::PresentationalHintsCSS => "presentational-hints.css", Resource::QuirksModeCSS => "quirks-mode.css", - Resource::RippyPNG => "rippy.png", - Resource::MediaControls => "media_controls.js", + Resource::RippyPNG => "rippy.png", + Resource::MediaControlsCSS => "media_controls.css", + Resource::MediaControlsJS => "media_controls.js", } } diff --git a/ports/libsimpleservo/api/src/lib.rs b/ports/libsimpleservo/api/src/lib.rs index 82e81f1f673..2f10059cbdb 100644 --- a/ports/libsimpleservo/api/src/lib.rs +++ b/ports/libsimpleservo/api/src/lib.rs @@ -702,7 +702,10 @@ impl ResourceReaderMethods for ResourceReaderInstance { Resource::BluetoothBlocklist => { &include_bytes!("../../../../resources/gatt_blocklist.txt")[..] }, - Resource::MediaControls => { + Resource::MediaControlsCSS => { + &include_bytes!("../../../../resources/media_controls.css")[..] + }, + Resource::MediaControlsJS => { &include_bytes!("../../../../resources/media_controls.js")[..] }, }) diff --git a/resources/media_controls.css b/resources/media_controls.css new file mode 100644 index 00000000000..5d9ab8ca6bf --- /dev/null +++ b/resources/media_controls.css @@ -0,0 +1,38 @@ +button { + display: inline-block; + width: 24px; + height: 24px; + border: none; +} + +.controls { + display: block; + position: fixed; + width: 100%; + padding: 0 9px; +} + +.hidden { + display: none; +} + +.playing { + background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABIAAAASCAYAAABWzo5XAAAAKklEQVR4AWMYWUABiB2gWAG3OGHQAMT/obgBl/igNGjUoFGDKM8iIwcAAPiWPtn7PkKeAAAAAElFTkSuQmCC") no-repeat; +} + +.paused { + background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABIAAAASCAYAAABWzo5XAAAATElEQVR4AWMYmUCAWgY9AGIHahj0H4rXU+q6/0j4PRDHU24QAu8HYgXKDUK4rp4aBn0A4gZKDTpAvtcQrkigNLA3gKJ/4BMk3BWjAAA8rCr7cYjSfAAAAABJRU5ErkJggg==") no-repeat; +} + +.ended { + background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABIAAAASCAQAAAD8x0bcAAAAoUlEQVR4AbXQobIBYQCG4Sf455x08mlGEkSNwCW4H8GMoOtWVUSKgksgyuQNxpgxdn5F2gXF89U3fQp+vRUsPkniJ0nUVvZUT7wvM1X10I+lKOroSh01X2Xwb+Pg73kG1F31gVc/re0AgJaKvKELAJCayJtIAYCZvRIAKNmbAwANUa/wWkNOIjMQQDCQGSsIRqKTra2zaCR4qCaxspKo+YobdwwzP00NEq4AAAAASUVORK5CYII=") no-repeat; +} + +.volumeup { + background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABIAAAASCAQAAAD8x0bcAAAAhElEQVR4AbXRIQxBARQF0EMlGXrvIole0CudrFd6EugNSbKpeo/EH+wpiu3/J5j76glv9/p7LqbqKrMCJ+GsVUUC0Hd30ygnAaDnaVNOAmzVsFNoA8THQZhjKIwzdERXWGTo8EbLDM0wEibkj+8VOt8rWOdlDjzcNLNZjtksANdk4B/yAqWtTB5BB+CFAAAAAElFTkSuQmCC") no-repeat; +} + +.muted { + background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABIAAAASCAQAAAD8x0bcAAAAvElEQVR4Aa3OAUcDARzG4adUpSAQECYRoEAium8QBH2D1AkcAZxKRkjQ56hhCIQADOwLDIANwGzGf5zDbHcD+70AD14rruHBbBeuLPQopIAmcuHfgbkyIcONwL4XQz2H1WzND+DMwK+ZTiUSrYJtI5UgFS4B+BPKyfCkb8+Wic9qFHKb2q7R1a5DIbPhCB3fdei5YOwaeZs/3iwRHwVb9+XEQu8lUjJQzdhxa7yckQstr8Kd2s4dg3sNK20KQZNRkQgu8yAAAAAASUVORK5CYII=") no-repeat; +} + diff --git a/resources/media_controls.js b/resources/media_controls.js index 216fad4d603..25489fbec22 100644 --- a/resources/media_controls.js +++ b/resources/media_controls.js @@ -1,23 +1,328 @@ -console.log('YO'); -let controls = document.servoGetMediaControls("@@@id@@@"); +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -let style = document.createElement("style"); -style.textContent = `#controls { - -servo-top-layer: top; - display: block; - position: fixed; - left: 0px; - bottom: 0px; - height: 30px; - width: 100%; - background: blue; -}`; -controls.appendChild(style); +(() => { + "use strict"; -let div = document.createElement("div"); -div.setAttribute("id", "controls"); -let button = document.createElement("button"); -button.textContent = "Click me"; -div.appendChild(button); -controls.appendChild(div); -console.log('INNER', div.innerHTML); + const MARKUP = ` + + + + + `; + + // States. + const BUFFERING = "buffering"; + const ENDED = "ended"; + const ERRORED = "errored"; + const PAUSED = "paused"; + const PLAYING = "playing"; + + // State transitions. + const TRANSITIONS = { + buffer: { + "paused": BUFFERING, + }, + end: { + "playing": ENDED, + "paused": ENDED, + }, + error: { + "buffering": ERRORED, + "playing": ERRORED, + "paused": ERRORED, + }, + pause: { + "buffering": PAUSED, + "playing": PAUSED, + }, + play: { + "buffering": PLAYING, + "ended": PLAYING, + "paused": PLAYING, + }, + }; + + function camelCase(str) { + const rdashes = /-(.)/g; + return str.replace(rdashes, (str, p1) => { + return p1.toUpperCase(); + }); + } + + function formatTime(time, showHours = false) { + // Format the duration as "h:mm:ss" or "m:ss" + time = Math.round(time / 1000); + let hours = Math.floor(time / 3600); + let mins = Math.floor((time % 3600) / 60); + let secs = Math.floor(time % 60); + let timeString; + if (secs < 10) { + secs = "0" + secs; + } + if (hours || showHours) { + if (mins < 10) { + mins = "0" + mins; + } + timeString = hours + ":" + mins + ":" + secs; + } else { + timeString = mins + ":" + secs; + } + return timeString; + } + + class MediaControls { + constructor() { + // Get the instance of the shadow root where these controls live. + this.controls = document.servoGetMediaControls("@@@id@@@"); + // Get the instance of the host of these controls. + this.media = this.controls.host; + + // Create root element and load markup. + const root = document.createElement("div"); + root.classList.add("controls"); + root.innerHTML = MARKUP; + this.controls.appendChild(root); + + // Import elements. + this.elements = {}; + [ + "duration", + "play-pause-button", + "position-duration-box", + "volume-switch", + "volume-level" + ].forEach(id => { + this.elements[camelCase(id)] = this.controls.getElementById(id); + }); + + // Init position duration box. + const positionTextNode = Array.prototype.find.call( + this.elements.positionDurationBox.childNodes, + (n) => !!~n.textContent.search("#1") + ); + const durationSpan = this.elements.duration; + const durationFormat = durationSpan.textContent; + const positionFormat = positionTextNode.textContent; + + durationSpan.classList.add("duration"); + durationSpan.setAttribute("role", "none"); + durationSpan.id = "durationSpan"; + + Object.defineProperties(this.elements.positionDurationBox, { + durationSpan: { + value: durationSpan, + }, + position: { + get: () => { + return positionTextNode.textContent; + }, + set: (v) => { + positionTextNode.textContent = positionFormat.replace("#1", v); + }, + }, + duration: { + get: () => { + return durationSpan.textContent; + }, + set: (v) => { + durationSpan.textContent = v ? durationFormat.replace("#2", v) : ""; + }, + }, + show:{ + value: (currentTime, duration) => { + const self = this.elements.positionDurationBox; + if (self.position != currentTime) { + self.position = currentTime; + } + if (self.duration != duration) { + self.duration = duration; + } + self.classList.remove("hidden"); + }, + }, + }); + + // Add event listeners. + this.mediaEvents = [ + "play", "pause", "ended", "volumechange", "loadeddata", + "loadstart", "timeupdate", "progress", "playing", + "waiting", "canplay", "canplaythrough", "seeking", + "seeked", "emptied", "loadedmetadata", "error", "suspend"]; + this.mediaEvents.forEach(event => { + this.media.addEventListener(event, this); + }); + + this.controlEvents = [ + { el: this.elements.playPauseButton, type: "click"}, + { el: this.elements.volumeSwitch, type: "click" }, + { el: this.elements.volumeLevel, type: "change" }, + ]; + this.controlEvents.forEach(({ el, type }) => { + el.addEventListener(type, this); + }); + + // Create state transitions. + // + // It exposes one method per transition. i.e. this.pause(), this.play(), etc. + // For each transition, we check that the transition is possible and call + // the `onStateChange` handler. + for (let name in TRANSITIONS) { + if (!TRANSITIONS.hasOwnProperty(name)) { + continue; + } + this[name] = () => { + const from = this.state; + + // Checks if the transition is valid in the current state. + if (!TRANSITIONS[name][from]) { + const error = `Transition "${name}" invalid for the current state "${from}"`; + console.error(error); + throw new Error(error); + } + + const to = TRANSITIONS[name][from]; + + console.log(`transitioning from ${from} to ${to}`); + if (from == to) { + return; + } + + // Transition to the next state. + this.state = to; + this.onStateChange(from); + }; + } + + // Set initial state. + this.state = PAUSED; + this.onStateChange(null); + } + + // State change handler + onStateChange(from) { + this.render(from); + } + + render(from = this.state) { + // Error + if (this.state == ERRORED) { + //XXX render errored state + return; + } + + console.log(`render from ${from} to ${this.state}`); + if (this.state != from) { + // Play/Pause button. + const playPauseButton = this.elements.playPauseButton; + playPauseButton.classList.remove(from); + playPauseButton.classList.add(this.state); + } + + // Volume. + const volumeSwitchClass = this.media.muted || this.media.volume == 0 ? "muted" : "volumeup"; + if (!this.elements.volumeSwitch.classList.contains(volumeSwitchClass)) { + this.elements.volumeSwitch.classList = ""; + this.elements.volumeSwitch.classList.add(volumeSwitchClass); + } + const volumeLevelValue = Math.round((this.media.muted ? 0 : this.media.volume) * 100); + if (this.elements.volumeLevel.value != volumeLevelValue) { + this.elements.volumeLevel.value = volumeLevelValue; + } + + // Current time and duration. + let currentTime = "0:00"; + let duration = "0:00"; + if (!isNaN(this.media.currentTime) && !isNaN(this.media.duration)) { + currentTime = formatTime(Math.round(this.media.currentTime * 1000)); + duration = formatTime(Math.round(this.media.duration * 1000)); + } + this.elements.positionDurationBox.show(currentTime, duration); + } + + handleEvent(event) { + if (!event.isTrusted) { + console.warn(`Drop untrusted event ${event.type}`); + return; + } + + if (this.mediaEvents.includes(event.type)) { + this.onMediaEvent(event); + } else { + this.onControlEvent(event); + } + } + + onControlEvent(event) { + switch (event.type) { + case "click": + switch (event.currentTarget) { + case this.elements.playPauseButton: + this.playOrPause(); + break; + case this.elements.volumeSwitch: + this.muteUnmute(); + break; + } + break; + case "change": + switch (event.currentTarget) { + case this.elements.volumeLevel: + this.render(); + break; + } + break; + break; + throw new Error(`Unknown event ${event.type}`); + } + } + + // HTMLMediaElement event handler + onMediaEvent(event) { + console.log(event.type); + switch (event.type) { + case "ended": + this.end(); + break; + case "play": + case "pause": + // Transition to PLAYING or PAUSED state. + this[event.type](); + break; + case "volumechange": + case "timeupdate": + this.render(); + break; + case "loadedmetadata": + break; + } + } + + /* Media actions */ + + playOrPause() { + switch(this.state) { + case PLAYING: + this.media.pause(); + break; + case BUFFERING: + case ENDED: + case PAUSED: + this.media.play(); + break; + default: + throw new Error(`Invalid state ${this.state}`); + } + } + + muteUnmute() { + this.media.muted = !this.media.muted; + } + } + + new MediaControls(); +})(); From aad5b23e2ba432fd9674f0560e50b3ac5abb2fd5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fernando=20Jim=C3=A9nez=20Moreno?= Date: Thu, 18 Apr 2019 11:50:09 +0200 Subject: [PATCH 04/31] Use input instead of change for volume control --- resources/media_controls.js | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/resources/media_controls.js b/resources/media_controls.js index 25489fbec22..855e9774096 100644 --- a/resources/media_controls.js +++ b/resources/media_controls.js @@ -160,7 +160,7 @@ this.controlEvents = [ { el: this.elements.playPauseButton, type: "click"}, { el: this.elements.volumeSwitch, type: "click" }, - { el: this.elements.volumeLevel, type: "change" }, + { el: this.elements.volumeLevel, type: "input" }, ]; this.controlEvents.forEach(({ el, type }) => { el.addEventListener(type, this); @@ -269,10 +269,10 @@ break; } break; - case "change": + case "input": switch (event.currentTarget) { case this.elements.volumeLevel: - this.render(); + this.changeVolume(); break; } break; @@ -322,6 +322,13 @@ muteUnmute() { this.media.muted = !this.media.muted; } + + changeVolume() { + const volume = parseInt(this.elements.volumeLevel.value); + if (!isNaN(volume)) { + this.media.volume = volume / 100; + } + } } new MediaControls(); From 8a4d54af0bd7dc54f48ade3f4deda0aaf81eb303 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fernando=20Jim=C3=A9nez=20Moreno?= Date: Thu, 18 Apr 2019 12:02:58 +0200 Subject: [PATCH 05/31] Add progress percentage as input range --- resources/media_controls.js | 31 +++++++++++++++++++------------ 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/resources/media_controls.js b/resources/media_controls.js index 855e9774096..827c8f1cee8 100644 --- a/resources/media_controls.js +++ b/resources/media_controls.js @@ -7,6 +7,7 @@ const MARKUP = ` +