mirror of
https://github.com/servo/servo.git
synced 2025-07-23 07:13:52 +01:00
Embed user agent stylesheets and media control resouces in libservo (#36803)
Embed user agent stylesheets and media control resouces in libservo as decided in https://github.com/servo/servo/pull/36788#issuecomment-2845332210 Signed-off-by: webbeef <me@webbeef.org>
This commit is contained in:
parent
7e2d2ed0ce
commit
3db0194e5a
11 changed files with 26 additions and 73 deletions
|
@ -12,7 +12,6 @@ use std::{f64, mem};
|
|||
use compositing_traits::{CrossProcessCompositorApi, ImageUpdate, SerializableImageData};
|
||||
use content_security_policy as csp;
|
||||
use dom_struct::dom_struct;
|
||||
use embedder_traits::resources::{self, Resource as EmbedderResource};
|
||||
use embedder_traits::{MediaPositionState, MediaSessionEvent, MediaSessionPlaybackState};
|
||||
use euclid::default::Size2D;
|
||||
use headers::{ContentLength, ContentRange, HeaderMapExt};
|
||||
|
@ -110,6 +109,12 @@ use crate::realms::{InRealm, enter_realm};
|
|||
use crate::script_runtime::CanGc;
|
||||
use crate::script_thread::ScriptThread;
|
||||
|
||||
/// A CSS file to style the media controls.
|
||||
static MEDIA_CONTROL_CSS: &str = include_str!("../resources/media-controls.css");
|
||||
|
||||
/// A JS file to control the media controls.
|
||||
static MEDIA_CONTROL_JS: &str = include_str!("../resources/media-controls.js");
|
||||
|
||||
#[derive(PartialEq)]
|
||||
enum FrameStatus {
|
||||
Locked,
|
||||
|
@ -1949,14 +1954,13 @@ impl HTMLMediaElement {
|
|||
ElementCreator::ScriptCreated,
|
||||
can_gc,
|
||||
);
|
||||
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);
|
||||
let media_controls_script = MEDIA_CONTROL_JS.replace("@@@id@@@", &id);
|
||||
*self.media_controls_id.borrow_mut() = Some(id);
|
||||
script
|
||||
.upcast::<Node>()
|
||||
|
@ -1969,7 +1973,6 @@ impl HTMLMediaElement {
|
|||
return;
|
||||
}
|
||||
|
||||
let media_controls_style = resources::read_string(EmbedderResource::MediaControlsCSS);
|
||||
let style = HTMLStyleElement::new(
|
||||
local_name!("script"),
|
||||
None,
|
||||
|
@ -1980,7 +1983,7 @@ impl HTMLMediaElement {
|
|||
);
|
||||
style
|
||||
.upcast::<Node>()
|
||||
.SetTextContent(Some(DOMString::from(media_controls_style)), can_gc);
|
||||
.SetTextContent(Some(DOMString::from(MEDIA_CONTROL_CSS)), can_gc);
|
||||
|
||||
if let Err(e) = shadow_root
|
||||
.upcast::<Node>()
|
||||
|
|
61
components/script/resources/media-controls.css
Normal file
61
components/script/resources/media-controls.css
Normal file
|
@ -0,0 +1,61 @@
|
|||
button {
|
||||
display: inline-block;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
min-width: var(--button-size);
|
||||
min-height: var(--button-size);
|
||||
padding: 6px;
|
||||
border: 0;
|
||||
margin: 0;
|
||||
background-color: transparent;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
}
|
||||
|
||||
.root {
|
||||
display: block;
|
||||
position: relative;
|
||||
min-height: 40px;
|
||||
min-width: 230px;
|
||||
}
|
||||
|
||||
.controls {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
background-color: rgba(26,26,26,.8);
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.playing {
|
||||
background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABIAAAASCAYAAABWzo5XAAAALElEQVR4AWMYQeD///8KQOwAxQq4xQkb1PAfARpwiQ9Kg0YNGjWI8iwycgAA8BVdyMCA4SwAAAAASUVORK5CYII=") no-repeat;
|
||||
}
|
||||
|
||||
.paused {
|
||||
background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABIAAAASCAYAAABWzo5XAAAATklEQVR4AWMYgeD///8C1DLoARA7UG4QAqynyHX/UcF7II6n3CAE2A/ECpQbhHBdPTUM+gDEDZQadIB8ryFckUBpYG8ARf/AJ0i4K0YBAOu3zFnxHnpDAAAAAElFTkSuQmCC") no-repeat;
|
||||
}
|
||||
|
||||
.ended {
|
||||
background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABIAAAASCAQAAAD8x0bcAAAArElEQVR4AbWRIQ7CQBBFR7QBhcYRFKISVwQcgfsgmiDq8S22BgkGAxwBJBp0BSEkhOaxYs2wW6jhjZn8eeqPfEJbfkHItoECDRRgQq9eSrBQsWLgl1rsAJgyo+TG6IsmBrocudKp1ew25MVcNG5PHDjr05i+oy946qCkcKSCUgdrLgQqCUyy0VIMJE5rsWjIqUgJbfcpFUvfUzLgzsnMA8gIxQcROXszOZH8hTfry4rnukG7bgAAAABJRU5ErkJggg==") no-repeat;
|
||||
}
|
||||
|
||||
.volumeup {
|
||||
background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABIAAAASCAQAAAD8x0bcAAAAi0lEQVR4AWOgNfh/4n/Yfybc0g1gcs////8P/RfCoeT/fyjL9v+H/1f+c2NTAgRwnun/v/+nYCoBAzB7yX9GILni/8//IjBpFAAVyQKSLkA6BJ+i3UBSDEiX4lO0C6qoDJ+iTCDpCqRDCTl8JdDhooSDYDL+wLT7/xEYmDz4omU3ZrRgKj0NjmCqAwANy7W0uf6FTQAAAABJRU5ErkJggg==") no-repeat;
|
||||
}
|
||||
|
||||
.muted {
|
||||
background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABIAAAASCAQAAAD8x0bcAAAAy0lEQVR4AayRtUICUBRA7a7Jye5RF7u+wM4vsF2s0e7O37BzkZqZ+AF2WGk4XLqZeO/dPq/TUtuoZSki7mIgFloFlv3+icgOoKE8GlsH1sVOgOgy9jGjpzIuRjqv/rgNI3/hQCuD0t+8WK7Eywx6NfSGIAX+5t90CwOF5ODgNj4EO2TzxbDkdXwlgmCdLOolr+UlEbTn37QAK0fRBz8BAImvEYwMnmiJfdRzHxTAYoAQJjqPWWxJMf+3vHEAzCf+8A4avXYR9xSpm5QAaOvUDhZuhCsAAAAASUVORK5CYII=") no-repeat;
|
||||
}
|
||||
|
||||
.fullscreen {
|
||||
background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAABGdBTUEAALGPC/xhBQAAAa1JREFUSA3Vlj1Ow0AQhW1OAFUaKmoqOEAklCIlJQ3nyBFyBahc0IAEEnUUunQWVLiloqEKNzDfmJ2V7Z1JlpKRRrt+82bezq7/yrZtH4qiOMUt2wJelWX5aQXJnYDf4UdWHOyjgFTjlm0Bz53ECAsHF65lzUFkDiffXM5Y+esQTq8CZ0ZEchLzBF5gviVsBwgiz2aYvrwtuiFWmkkjEN7S2h+wpn8Gso/ViLhXBP64+COYnkkUiAdqJLgiHhdcD74TWAnQ79pL/AsniGxki+ReTswQmSuJ2BTvm9klhGPNMUcIur+LMYHYIiiYxcd895oiUy9IbI5n3WlejX+OS/uyDV4bxNzt83IiHorfMortOuRlTLImJCe3EpisXIsz7Syulis53L6ZIhAm8hxs8PigMbeKDwpkcuRpXolAg3evipxE3YVdXGL6qqhVAKwTeZJJzwYr1+I6wrO6rcD1ZTcQ6NXtpjuL7xHRWq5ApQVyRqpJJ+PuRaT2vmiXBOPBZ4icwbmweJ7AIeR1jkjgrOFLTmLSmvy2nCSRX0B+W6755n5ZcXLlGbrHvd+W9x/rQiZUwmH0RgAAAABJRU5ErkJggg==') no-repeat;
|
||||
}
|
||||
|
||||
.fullscreen.fullscreen-active {
|
||||
background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAABGdBTUEAALGPC/xhBQAAAbtJREFUSA3tlj0vBFEUhnd8hFhrE4mPbSREovIfdFQ6hcJ/EJVColP6GRoqhZbEFqKmET2xDSpWYjxncu915u6ZzWS20DjJ6577no/3zL1mqNWwNE1HwKb4g5rrdcjayHo54oxVbG8QAepl0FNphLVBQ6bflZ2ySiLU6+a+3bY/Hq/qA0EEog4m4ieDmwOzwrNazUOPwgQKpfkVWDAEVuHvQAsUDhjqSLKmeIQXKxKQ2Jv8UPY7eejuHJIsEantJ6B6l/glIXsKvOgq/DICT+Rld6IHH9IbEursz8GM5kv68+RdxiKJLibYYr+sOeffJknyqXlyZbhJzTn/g9yuwf9Tf3QC8SUPM8eoMUuXi/vWPJcsuSuac36H3I7BZ98UeQduQGwbcQEJ03ES+3vQ8y7kakkQkQegrYzAKwXyLuQs96K5yDrrYi6r3KZJ2jEicnS2EdwCX8Dbu3P6PcGzT3brCWuvCGTc/BpOPsVyJ0UC2ZkT3wfa8iJErObZZ4BYEyzFzww3DsKF4tsiBNaAPhaZ3PrGxBo9e0PkQP6ajYELIFa5uVejh3+SNn74z0JEjkClyX1zv9Jnxzf/AaCJ561FockLAAAAAElFTkSuQmCC') no-repeat;
|
||||
}
|
416
components/script/resources/media-controls.js
Normal file
416
components/script/resources/media-controls.js
Normal file
|
@ -0,0 +1,416 @@
|
|||
/* 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/. */
|
||||
|
||||
(() => {
|
||||
"use strict";
|
||||
|
||||
// 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 generateMarkup(isAudioOnly) {
|
||||
return `
|
||||
<div class="controls">
|
||||
<button id="play-pause-button"></button>
|
||||
<input id="progress" type="range" value="0" min="0" max="100" step="1"></input>
|
||||
<span id="position-duration-box" class="hidden">
|
||||
<span id="position-text">#1</span>
|
||||
<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>
|
||||
${isAudioOnly ? "" : '<button id="fullscreen-switch" class="fullscreen"></button>'}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
const hours = Math.floor(time / 3600);
|
||||
const mins = Math.floor((time % 3600) / 60);
|
||||
const secs = Math.floor(time % 60);
|
||||
|
||||
const formattedHours =
|
||||
hours || showHours ? `${hours.toString().padStart(2, "0")}:` : "";
|
||||
|
||||
return `${formattedHours}${mins
|
||||
.toString()
|
||||
.padStart(2, "0")}:${secs.toString().padStart(2, "0")}`;
|
||||
}
|
||||
|
||||
class MediaControls {
|
||||
constructor() {
|
||||
this.nonce = Date.now();
|
||||
// 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;
|
||||
|
||||
this.mutationObserver = new MutationObserver(() => {
|
||||
// We can only get here if the `controls` attribute is removed.
|
||||
this.cleanup();
|
||||
});
|
||||
this.mutationObserver.observe(this.media, {
|
||||
attributeFilter: ["controls"]
|
||||
});
|
||||
|
||||
this.isAudioOnly = this.media.localName == "audio";
|
||||
|
||||
// Create root element and load markup.
|
||||
this.root = document.createElement("div");
|
||||
this.root.classList.add("root");
|
||||
this.root.innerHTML = generateMarkup(this.isAudioOnly);
|
||||
this.controls.appendChild(this.root);
|
||||
|
||||
|
||||
const elementNames = [
|
||||
"duration",
|
||||
"play-pause-button",
|
||||
"position-duration-box",
|
||||
"position-text",
|
||||
"progress",
|
||||
"volume-switch",
|
||||
"volume-level"
|
||||
];
|
||||
|
||||
if (!this.isAudioOnly) {
|
||||
elementNames.push("fullscreen-switch");
|
||||
}
|
||||
|
||||
// Import elements.
|
||||
this.elements = {};
|
||||
elementNames.forEach(id => {
|
||||
this.elements[camelCase(id)] = this.controls.getElementById(id);
|
||||
});
|
||||
|
||||
// Init position duration box.
|
||||
const positionTextNode = this.elements.positionText;
|
||||
const durationSpan = this.elements.duration;
|
||||
const durationFormat = durationSpan.textContent;
|
||||
const positionFormat = positionTextNode.textContent;
|
||||
|
||||
durationSpan.classList.add("duration");
|
||||
durationSpan.setAttribute("role", "none");
|
||||
|
||||
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: "input" }
|
||||
];
|
||||
|
||||
if (!this.isAudioOnly) {
|
||||
this.controlEvents.push({ el: this.elements.fullscreenSwitch, type: "click" });
|
||||
}
|
||||
|
||||
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];
|
||||
|
||||
if (from == to) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Transition to the next state.
|
||||
this.state = to;
|
||||
this.onStateChange(from);
|
||||
};
|
||||
}
|
||||
|
||||
// Set initial state.
|
||||
this.state = this.media.paused ? PAUSED : PLAYING;
|
||||
this.onStateChange(null);
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
this.mutationObserver.disconnect();
|
||||
this.mediaEvents.forEach(event => {
|
||||
this.media.removeEventListener(event, this);
|
||||
});
|
||||
this.controlEvents.forEach(({ el, type }) => {
|
||||
el.removeEventListener(type, this);
|
||||
});
|
||||
}
|
||||
|
||||
// State change handler
|
||||
onStateChange(from) {
|
||||
this.render(from);
|
||||
}
|
||||
|
||||
render(from = this.state) {
|
||||
if (!this.isAudioOnly) {
|
||||
// XXX This should ideally use clientHeight/clientWidth,
|
||||
// but for some reason I couldn't figure out yet,
|
||||
// using it breaks layout.
|
||||
this.root.style.height = this.media.videoHeight;
|
||||
this.root.style.width = this.media.videoWidth;
|
||||
}
|
||||
|
||||
// Error
|
||||
if (this.state == ERRORED) {
|
||||
//XXX render errored state
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.state != from) {
|
||||
// Play/Pause button.
|
||||
const playPauseButton = this.elements.playPauseButton;
|
||||
playPauseButton.classList.remove(from);
|
||||
playPauseButton.classList.add(this.state);
|
||||
}
|
||||
|
||||
// Progress.
|
||||
const positionPercent =
|
||||
(this.media.currentTime / this.media.duration) * 100;
|
||||
if (Number.isFinite(positionPercent)) {
|
||||
this.elements.progress.value = positionPercent;
|
||||
} else {
|
||||
this.elements.progress.value = 0;
|
||||
}
|
||||
|
||||
// Current time and duration.
|
||||
let currentTime = formatTime(0);
|
||||
let duration = formatTime(0);
|
||||
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);
|
||||
|
||||
// Volume.
|
||||
this.elements.volumeSwitch.className =
|
||||
this.media.muted || !this.media.volume ? "muted" : "volumeup";
|
||||
const volumeLevelValue = this.media.muted
|
||||
? 0
|
||||
: Math.round(this.media.volume * 100);
|
||||
if (this.elements.volumeLevel.value != volumeLevelValue) {
|
||||
this.elements.volumeLevel.value = volumeLevelValue;
|
||||
}
|
||||
}
|
||||
|
||||
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.toggleMuted();
|
||||
break;
|
||||
case this.elements.fullscreenSwitch:
|
||||
this.toggleFullscreen();
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case "input":
|
||||
switch (event.currentTarget) {
|
||||
case this.elements.volumeLevel:
|
||||
this.changeVolume();
|
||||
break;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unknown event ${event.type}`);
|
||||
}
|
||||
}
|
||||
|
||||
// HTMLMediaElement event handler
|
||||
onMediaEvent(event) {
|
||||
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":
|
||||
case "resize":
|
||||
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}`);
|
||||
}
|
||||
}
|
||||
|
||||
toggleMuted() {
|
||||
this.media.muted = !this.media.muted;
|
||||
}
|
||||
|
||||
toggleFullscreen() {
|
||||
const { fullscreenEnabled, fullscreenElement } = document;
|
||||
|
||||
const isElementFullscreen = fullscreenElement && fullscreenElement === this.media;
|
||||
|
||||
if (fullscreenEnabled && isElementFullscreen) {
|
||||
document.exitFullscreen().then(() => {
|
||||
this.elements.fullscreenSwitch.classList.remove("fullscreen-active");
|
||||
});
|
||||
} else {
|
||||
this.media.requestFullscreen().then(() => {
|
||||
this.elements.fullscreenSwitch.classList.add("fullscreen-active");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
changeVolume() {
|
||||
const volume = parseInt(this.elements.volumeLevel.value);
|
||||
if (!isNaN(volume)) {
|
||||
this.media.volume = volume / 100;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
new MediaControls();
|
||||
})();
|
||||
|
Loading…
Add table
Add a link
Reference in a new issue