media: Massive UX improvements

- Keyboard controls are now available, though they might not be good enough. Thanks firefox.
  - f: Fullscreen
  - m: Mute
  - Space: Play/Pause
  - Home: Jump to Start
  - End: Jump to End
  - Comma: Go back 0.1s
  - Period: Go forward 0.1s
  - Left Arrow: Go back 5s
  - Right Arrow: Go forward 5s
  - Up Arrow: Increase logarithmic volume by 5%
  - Down Arrow: Decrease logarithmic volume by 5%
  - P: Toggle pitch correction
  - Page Up: Speed up video
  - Page Down: Slow down video
- Mouse controls are improved too.
- Accessibility drastically improved.
- Now using the new symbols as well.
This commit is contained in:
Michael Fabian 'Xaymar' Dirks
2022-11-27 06:45:20 +01:00
parent 368d1695f8
commit 6313815f57
+360 -244
View File
@@ -1,3 +1,7 @@
'use strict';
import("./HTMLMediaElement.mjs");
async function xmr_media_lazyload_perform(element) { async function xmr_media_lazyload_perform(element) {
let content = element.querySelector("noscript"); let content = element.querySelector("noscript");
if (content) { if (content) {
@@ -22,262 +26,374 @@ async function xmr_initialize_player(el) {
if (!media) if (!media)
el.querySelector("audio"); el.querySelector("audio");
// Play/Pause
let play_pause = el.querySelector(".play"); let play_pause = el.querySelector(".play");
play_pause.update = function() { { // Play/Pause
if (media.paused) { play_pause.update = function() {
play_pause.classList.remove("playing"); if (media.paused) {
play_pause.innerText = "⏵"; play_pause.classList.remove("playing");
} else if (media.error || media.ended) { play_pause.dataset.symbol = "⏵";
play_pause.classList.remove("playing"); play_pause.ariaLabel = "Play";
play_pause.innerText = "⏹"; } else if (media.error || media.ended) {
} else { play_pause.classList.remove("playing");
play_pause.classList.add("playing"); play_pause.dataset.symbol = "⏹";
play_pause.innerText = ""; play_pause.ariaLabel = "Stopped";
}
}
play_pause.addEventListener("click", () => {
if (media.paused) {
media.play();
} else {
media.pause();
}
});
media.addEventListener("play", () => { play_pause.update(); });
media.addEventListener("pause", () => { play_pause.update(); });
media.addEventListener("ended", () => { play_pause.update(); });
media.addEventListener("error", () => { play_pause.update(); });
play_pause.update();
// Mute
let audio_mute = el.querySelector(".mute");
audio_mute.update = function() {
if (media.muted) {
audio_mute.classList.add("muted");
audio_mute.innerText = "🔇";
} else {
audio_mute.classList.remove("muted");
if (media.volume > 0.5) {
audio_mute.innerText = "🔊";
} else if (media.volume > 0.125) {
audio_mute.innerText = "🔉";
} else { } else {
audio_mute.innerText = "🔈"; play_pause.classList.add("playing");
play_pause.dataset.symbol = "⏸";
play_pause.ariaLabel = "Pause";
} }
} }
} play_pause.addEventListener("click", () => {
audio_mute.addEventListener("click", () => { if (media.paused) {
media.muted = !media.muted;
});
media.addEventListener("volumechange", () => {
audio_mute.update();
});
audio_mute.update();
// Volume
let audio_volume = el.querySelector(".volume");
audio_volume.min = 0;
audio_volume.max = 2147483647;
audio_volume.update = function() {
let min = parseInt(audio_volume.min, 10);
let max = parseInt(audio_volume.max, 10);
let dlt = max - min;
audio_volume.value = (media.volume * dlt) + min;
if (media.muted) {
audio_volume.classList.add("muted");
} else {
audio_volume.classList.remove("muted");
}
}
audio_volume.addEventListener("input", () => {
let min = parseInt(audio_volume.min, 10);
let max = parseInt(audio_volume.max, 10);
let val = parseInt(audio_volume.value, 10);
let dlt = max - min;
let vol = (val + min) / dlt;
media.volume = vol;
media.muted = (media.volume < 0.01);
});
media.addEventListener("volumechange", () => { audio_volume.update(); });
audio_volume.update();
// Time
let playback_time = el.querySelector(".time");
playback_time.update = function() {
function formatSeconds(seconds, with_ms, with_minutes, with_hour) {
let result = [];
if (isFinite(seconds) && !isNaN(seconds)) {
let tainted_seconds = seconds % 60;
let pure_seconds = Math.floor(seconds % 60);
let tainted_minutes = ((seconds - pure_seconds) / 60);
let pure_minutes = Math.floor(tainted_minutes % 60);
let pure_hours = Math.floor((tainted_minutes - pure_minutes) / 60);
result.unshift(`${pure_seconds.toString(10).padStart(2, '0')}`);
if (with_ms) {
result[0] = `${result[0]}.${Math.floor((tainted_seconds % 1) * 100).toString(10).padStart(2, '0')}`
}
if ((with_minutes === true) || ((with_minutes === undefined) && (pure_minutes > 0))) {
result[0] = result[0].padStart(2, '0');
result.unshift(`${pure_minutes.toString(10)}`);
}
if ((with_hour === true) || ((with_hour === undefined) && (pure_hours > 0))) {
result[0] = result[0].padStart(2, '0');
result.unshift(`${pure_hours.toString(10)}`);
}
}
return result;
}
let duration = formatSeconds(media.duration, true);
let current = formatSeconds(media.currentTime, true, duration.length >= 2, duration.length >= 3);
for (let idx in duration) {
current[idx].padStart(duration[idx].length, '0');
}
if (duration.length > 0) {
playback_time.innerText = `${current.join(':')} / ${duration.join(':')}`;
} else {
playback_time.innerText = `${current.join(':')}`;
}
}
media.addEventListener("timeupdate", () => { playback_time.update(); });
media.addEventListener("durationupdate", () => { playback_progress.update(); });
media.addEventListener("loadedmetadata", () => { playback_time.update(); });
playback_time.update();
// Progress
let playback_progress = el.querySelector(".progress");
playback_progress.update = function() {
playback_progress.disabled = !media.seekable;
playback_progress.min = 0;
playback_progress.max = media.duration * 100;
playback_progress.value = media.currentTime * 100;
}
playback_progress.addEventListener("input", () => {
if (isFinite(media.duration)) {
media.currentTime = parseInt(playback_progress.value, 10) / 100;
}
});
media.addEventListener("timeupdate", () => { playback_progress.update(); });
media.addEventListener("durationupdate", () => { playback_progress.update(); });
media.addEventListener("loadeddata", () => { playback_progress.update(); });
media.addEventListener("loadedmetadata", () => { playback_progress.update(); });
playback_progress.update();
// Fullscreen
let fullscreen = el.querySelector(".fullscreen");
fullscreen.update = function() {
if (document.fullscreenElement) {
fullscreen.innerText = "⬚";
} else {
fullscreen.innerText = "⛶";
}
};
fullscreen.addEventListener("click", () => {
if (document.fullscreenElement) {
document.exitFullscreen();
} else {
el.requestFullscreen();
}
});
el.addEventListener("fullscreenchange", (ev) => {
fullscreen.update();
});
fullscreen.update();
// Media Play/Pause by click
media.addEventListener("click", () => {
play_pause.click();
})
// Media Fullscreen by dblclick
media.addEventListener("dblclick", () => {
fullscreen.click();
})
// Hide/Show controls
el.showOverlay = function() {
el.cancelHideOverlay();
el.classList.remove("hide");
}
el.hideOverlay = function() {
if (!media.paused && !media.ended && !media.error) {
el.classList.add("hide");
}
}
el.delayHideOverlay = function() {
el.cancelHideOverlay();
el.timer = setTimeout(() => { el.hideOverlay(); }, 2500);
}
el.cancelHideOverlay = function() {
if (el.timer) {
clearTimeout(el.timer);
}
}
el.addEventListener("mousemove", () => {
el.showOverlay();
if (!media.paused && !media.ended && !media.error) {
el.delayHideOverlay();
}
});
el.hideOverlay();
// Show/Hide Audio controls without audio.
function checkAudioPresence() {
let hasAudio = Boolean(media.mozHasAudio)
|| Boolean(media.webkitAudioDecodedByteCount)
|| Boolean(media.audioTracks && media.audioTracks.length);
audio_mute.style.display = hasAudio ? "" : "none";
audio_volume.style.display = hasAudio ? "" : "none";
}
media.addEventListener("loadedmetadata", () => { checkAudioPresence() });
media.addEventListener("loadeddata", () => { checkAudioPresence() });
checkAudioPresence();
// Variants
let variant = el.querySelector(".variant");
variant.update = function() {
// Store current state and pause.
let paused = media.paused;
let muted = media.muted;
let volume = media.volume;
let time = media.currentTime;
media.pause();
play_pause.update();
// Tell the media source to begin loading.
media.src = variant.value;
media.load();
media.currentTime = time;
media.addEventListener("canplay", () => {
media.currentTime = time;
media.volume = volume;
media.muted = muted;
if (paused) {
media.pause();
} else {
media.play(); media.play();
} else {
media.pause();
}
});
play_pause.addEventListener("keydown", (ev) => {
if (ev.isComposing || ev.keyCode === 229) {
// Ignore IME compositing.
return;
}
if (["Space", "Enter"].includes(ev.code)) {
play_pause.click();
ev.preventDefault();
ev.stopPropagation();
}
});
media.addEventListener("play", () => { play_pause.update(); });
media.addEventListener("pause", () => { play_pause.update(); });
media.addEventListener("ended", () => { play_pause.update(); });
media.addEventListener("error", () => { play_pause.update(); });
play_pause.update();
}
let playback_time = el.querySelector(".time");
{ // Time
playback_time.update = function() {
function formatSeconds(seconds, with_ms, with_minutes, with_hour) {
let result = [];
if (isFinite(seconds) && !isNaN(seconds)) {
let tainted_seconds = seconds % 60;
let pure_seconds = Math.floor(seconds % 60);
let tainted_minutes = ((seconds - pure_seconds) / 60);
let pure_minutes = Math.floor(tainted_minutes % 60);
let pure_hours = Math.floor((tainted_minutes - pure_minutes) / 60);
result.unshift(`${pure_seconds.toString(10).padStart(2, '0')}`);
if (with_ms) {
result[0] = `${result[0]}.${Math.floor((tainted_seconds % 1) * 100).toString(10).padStart(2, '0')}`
}
if ((with_minutes === true) || ((with_minutes === undefined) && (pure_minutes > 0))) {
result[0] = result[0].padStart(2, '0');
result.unshift(`${pure_minutes.toString(10)}`);
}
if ((with_hour === true) || ((with_hour === undefined) && (pure_hours > 0))) {
result[0] = result[0].padStart(2, '0');
result.unshift(`${pure_hours.toString(10)}`);
}
}
return result;
} }
let duration = formatSeconds(media.duration, true);
let current = formatSeconds(media.currentTime, true, duration.length >= 2, duration.length >= 3);
for (let idx in duration) {
current[idx].padStart(duration[idx].length, '0');
}
if (duration.length > 0) {
playback_time.innerText = `${current.join(':')} / ${duration.join(':')}`;
} else {
playback_time.innerText = `${current.join(':')}`;
}
}
media.addEventListener("timeupdate", () => { playback_time.update(); });
media.addEventListener("durationupdate", () => { playback_progress.update(); });
media.addEventListener("loadedmetadata", () => { playback_time.update(); });
playback_time.update();
}
let playback_progress = el.querySelector(".progress");
{ // Progress
playback_progress.update = function() {
playback_progress.disabled = !media.seekable;
playback_progress.min = 0;
playback_progress.max = media.duration * 100;
playback_progress.value = media.currentTime * 100;
}
playback_progress.addEventListener("input", () => {
if (isFinite(media.duration)) {
media.currentTime = parseInt(playback_progress.value, 10) / 100;
}
});
media.addEventListener("timeupdate", () => { playback_progress.update(); });
media.addEventListener("durationupdate", () => { playback_progress.update(); });
media.addEventListener("loadeddata", () => { playback_progress.update(); });
media.addEventListener("loadedmetadata", () => { playback_progress.update(); });
playback_progress.update();
}
let audio_mute = el.querySelector(".mute");
{ // Mute
audio_mute.update = function() {
if (media.muted) {
audio_mute.classList.add("muted");
audio_mute.dataset.symbol = "🔇";
audio_mute.ariaLabel = "Unmute";
} else {
audio_mute.classList.remove("muted");
if (media.volume > 0.5) {
audio_mute.dataset.symbol = "🔊";
} else if (media.volume > 0.25) {
audio_mute.dataset.symbol = "🔉";
} else {
audio_mute.dataset.symbol = "🔈";
}
audio_mute.ariaLabel = `Volume: ${(media.volume * 100).toString(10)}%`;
}
}
audio_mute.addEventListener("click", () => {
media.muted = !media.muted;
});
audio_mute.addEventListener("keydown", (ev) => {
if (ev.isComposing || ev.keyCode === 229) {
// Ignore IME compositing.
return;
}
if (["Space", "Enter"].includes(ev.code)) {
audio_mute.click();
ev.preventDefault();
ev.stopPropagation();
}
});
media.addEventListener("volumechange", () => {
audio_mute.update();
});
audio_mute.update();
}
let audio_volume = el.querySelector(".volume");
{ // Volume
audio_volume.min = 0;
audio_volume.max = 2147483647;
audio_volume.update = function() {
let min = parseInt(audio_volume.min, 10);
let max = parseInt(audio_volume.max, 10);
let dlt = max - min;
audio_volume.value = (media.logVolume * dlt) + min;
audio_volume.ariaLabel = `Volume: ${(media.volume * 100).toString(10)}%`;
if (media.muted) {
audio_volume.classList.add("muted");
} else {
audio_volume.classList.remove("muted");
}
}
audio_volume.addEventListener("input", () => {
let min = parseInt(audio_volume.min, 10);
let max = parseInt(audio_volume.max, 10);
let val = parseInt(audio_volume.value, 10);
let dlt = max - min;
let vol = (val + min) / dlt;
media.logVolume = vol;
media.muted = (media.volume < 0.01);
});
media.addEventListener("volumechange", () => { audio_volume.update(); });
audio_volume.update();
}
let fullscreen = el.querySelector(".fullscreen");
{ // Fullscreen
fullscreen.update = function() {
if (document.fullscreenElement) {
fullscreen.dataset.symbol = "⬚";
} else {
fullscreen.dataset.symbol = "⛶";
}
};
fullscreen.addEventListener("click", () => {
if (document.fullscreenElement) {
document.exitFullscreen();
} else {
el.requestFullscreen();
}
});
fullscreen.addEventListener("keydown", (ev) => {
if (ev.isComposing || ev.keyCode === 229) {
// Ignore IME compositing.
return;
}
if (["Space", "Enter"].includes(ev.code)) {
fullscreen.click();
ev.preventDefault();
}
})
el.addEventListener("fullscreenchange", () => {
fullscreen.update();
});
fullscreen.update();
}
let variant = el.querySelector(".variant");
{ // Variants
variant.update = function() {
// Store current state and pause.
let paused = media.paused;
let muted = media.muted;
let volume = media.volume;
let time = media.currentTime;
media.pause();
play_pause.update(); play_pause.update();
}, {
"once": true // Tell the media source to begin loading.
media.src = variant.value;
media.load();
media.currentTime = time;
media.addEventListener("canplay", () => {
media.currentTime = time;
media.volume = volume;
media.muted = muted;
if (paused) {
media.pause();
} else {
media.play();
}
play_pause.update();
}, {
"once": true
})
}
variant.addEventListener("input", () => { variant.update(); });
variant.addEventListener("value", () => { variant.update(); });
// Variants - Add options from available sources.
let sources = el.querySelectorAll("source");
for (let k of sources) {
let option = document.createElement("option");
option.value = k.src;
option.textContent = k.title;
variant.appendChild(option);
}
variant.value = media.currentSrc;
}
{ // Keyboard/Mouse Controls
el.addEventListener("keydown", (ev) => {
if (ev.isComposing || ev.keyCode === 229) {
// Ignore IME compositing.
return;
}
if (["f", "F"].includes(ev.key)) {
fullscreen.click();
ev.preventDefault();
ev.stopPropagation();
} else if (["m", "M"].includes(ev.key)) {
audio_mute.click();
ev.preventDefault();
ev.stopPropagation();
} else if (ev.code === "Space") {
play_pause.click();
ev.preventDefault();
ev.stopPropagation();
} else if (ev.code === "Home") {
media.currentTime = 0;
ev.preventDefault();
ev.stopPropagation();
} else if (ev.code === "ArrowLeft") {
media.currentTime -= 5;
ev.preventDefault();
ev.stopPropagation();
} else if (ev.code === "Comma") {
media.currentTime -= .1;
ev.preventDefault();
ev.stopPropagation();
} else if (ev.code === "Period") {
media.currentTime += .1;
ev.preventDefault();
ev.stopPropagation();
} else if (ev.code === "ArrowRight") {
media.currentTime += 5;
ev.preventDefault();
ev.stopPropagation();
} else if (ev.code === "End") {
media.currentTime = media.duration;
ev.preventDefault();
ev.stopPropagation();
} else if (ev.code === "ArrowUp") {
media.volume = Math.max(0, Math.min(media.logVolume + 0.05, 1.0));
ev.preventDefault();
ev.stopPropagation();
} else if (ev.code === "ArrowDown") {
media.volume = Math.max(0, Math.min(media.logVolume - 0.05, 1.0));
ev.preventDefault();
ev.stopPropagation();
} else if (["p", "P"].includes(ev.key)) {
media.preservesPitch = !media.preservesPitch;
ev.preventDefault();
ev.stopPropagation();
} else if (ev.code === "PageDown") {
media.playbackRate = Math.max(0.25, Math.min(media.playbackRate - 0.05, 4.0));
ev.preventDefault();
ev.stopPropagation();
} else if (ev.code === "PageUp") {
media.playbackRate = Math.max(0.25, Math.min(media.playbackRate + 0.05, 4.0));
ev.preventDefault();
ev.stopPropagation();
}
});
media.addEventListener("click", () => {
play_pause.click();
});
media.addEventListener("dblclick", () => {
fullscreen.click();
}) })
} }
variant.addEventListener("input", () => { variant.update(); });
variant.addEventListener("value", () => { variant.update(); });
// Variants - Add options from available sources. { // Hide/Show controls
let sources = el.querySelectorAll("source"); el.showOverlay = function() {
for (let k of sources) { el.cancelHideOverlay();
let option = document.createElement("option"); el.classList.remove("hide");
option.value = k.src; }
option.textContent = k.title; el.hideOverlay = function() {
variant.appendChild(option); if (!media.paused && !media.ended && !media.error) {
el.classList.add("hide");
}
}
el.delayHideOverlay = function() {
el.cancelHideOverlay();
el.timer = setTimeout(() => { el.hideOverlay(); }, 2500);
}
el.cancelHideOverlay = function() {
if (el.timer) {
clearTimeout(el.timer);
}
}
el.addEventListener("mousemove", () => {
el.showOverlay();
if (!media.paused && !media.ended && !media.error) {
el.delayHideOverlay();
}
});
el.hideOverlay();
}
{ // Show/Hide Audio controls without audio.
function checkAudioPresence() {
let hasAudio = Boolean(media.mozHasAudio)
|| Boolean(media.webkitAudioDecodedByteCount)
|| Boolean(media.audioTracks && media.audioTracks.length);
audio_mute.style.display = hasAudio ? "" : "none";
audio_volume.style.display = hasAudio ? "" : "none";
}
media.addEventListener("loadedmetadata", () => { checkAudioPresence() });
media.addEventListener("loadeddata", () => { checkAudioPresence() });
checkAudioPresence();
} }
variant.value = media.currentSrc;
// Signal the browser to try and load some information. // Signal the browser to try and load some information.
media.load(); media.load();