mirror of
https://github.com/servo/servo.git
synced 2025-07-22 14:53:49 +01:00
Media UI basic functionality
This commit is contained in:
parent
1c02fc94a8
commit
a664449681
10 changed files with 420 additions and 35 deletions
|
@ -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<dyn ResourceReaderMethods + Sync + Send> {
|
|||
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();
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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::<Node>()
|
||||
.SetTextContent(Some(DOMString::from(media_controls)));
|
||||
.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);
|
||||
}
|
||||
|
|
|
@ -116,7 +116,11 @@ impl HTMLStyleElement {
|
|||
.upcast::<Node>()
|
||||
.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)
|
||||
}
|
||||
|
|
|
@ -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>();
|
||||
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<ShadowRoot> {
|
||||
pub fn new(
|
||||
host: &Element,
|
||||
document: &Document,
|
||||
is_widget: IsUserAgentWidget,
|
||||
) -> DomRoot<ShadowRoot> {
|
||||
reflect_dom_object(
|
||||
Box::new(ShadowRoot::new_inherited(host, document, is_widget)),
|
||||
document.window(),
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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")[..]
|
||||
},
|
||||
})
|
||||
|
|
38
resources/media_controls.css
Normal file
38
resources/media_controls.css
Normal file
|
@ -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;
|
||||
}
|
||||
|
|
@ -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 = `
|
||||
<button id="play-pause-button"></button>
|
||||
<span id="position-duration-box" class="hidden">
|
||||
#1
|
||||
<span id="duration"> / #2</span>
|
||||
</span>
|
||||
<button id="volume-switch"></button>
|
||||
<input id="volume-level" type="range" value="100" min="0" max="100" step="1"></input>
|
||||
`;
|
||||
|
||||
// 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();
|
||||
})();
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue