Implement custom video controls for the new player (so we can better control styling)

pull/248/head
ElementalAlchemist 3 years ago committed by Mike Lang
parent 56699d5737
commit 497c975e3e

@ -40,7 +40,37 @@
</div>
</form>
<video id="video" controls preload="auto"></video>
<video id="video" preload="auto"></video>
<div id="video-controls">
<div id="video-controls-bar">
<div>
<img id="video-controls-play-pause" src="images/video-controls/play.png" class="click" />
</div>
<div id="video-controls-time">
<span id="video-controls-current-time"></span>
/
<span id="video-controls-duration"></span>
</div>
<div id="video-controls-spacer"></div>
<div id="video-controls-volume">
<img
id="video-controls-volume-mute"
src="images/video-controls/volume.png"
class="click"
/>
<progress id="video-controls-volume-level" value="0.5" class="click"></progress>
</div>
<div>
<select id="video-controls-playback-speed"></select>
</div>
<div>
<select id="video-controls-quality"></select>
</div>
</div>
<progress id="video-controls-playback-position" value="0" class="click"></progress>
</div>
<div id="clip-bar"></div>
<div id="waveform-container">
<img id="waveform" alt="Waveform for the video" />

Binary file not shown.

After

Width:  |  Height:  |  Size: 612 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 862 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 805 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 803 B

@ -58,7 +58,36 @@
</div>
</form>
<video id="video" controls preload="auto"></video>
<video id="video" preload="auto"></video>
<div id="video-controls">
<div id="video-controls-bar">
<div>
<img id="video-controls-play-pause" src="images/video-controls/play.png" class="click" />
</div>
<div id="video-controls-time">
<span id="video-controls-current-time"></span>
/
<span id="video-controls-duration"></span>
</div>
<div id="video-controls-spacer"></div>
<div id="video-controls-volume">
<img
id="video-controls-volume-mute"
src="images/video-controls/volume.png"
class="click"
/>
<progress id="video-controls-volume-level" value="0.5" class="click"></progress>
</div>
<div>
<select id="video-controls-playback-speed"></select>
</div>
<div>
<select id="video-controls-quality"></select>
</div>
</div>
<progress id="video-controls-playback-position" value="0" class="click"></progress>
</div>
<div id="editor-help">
<a href="#" id="editor-help-link">Help</a>

@ -8,9 +8,9 @@ var globalStartTimeString = "";
var globalEndTimeString = "";
var globalPlayer = null;
var globalSetUpControls = false;
Hls.DefaultConfig.maxBufferHole = 600;
Hls.DefaultConfig.debug = true;
const VIDEO_FRAMES_PER_SECOND = 30;
@ -65,18 +65,8 @@ async function loadVideoPlayer(playlistURL) {
let rangedPlaylistURL = assembleVideoPlaylistURL(playlistURL);
const videoElement = document.getElementById("video");
const volume = +(localStorage.getItem("volume") ?? 0.5);
if (isNaN(volume)) {
volume = 0.5;
} else if (volume < 0) {
volume = 0;
} else if (volume > 1) {
volume = 1;
}
videoElement.volume = volume;
videoElement.addEventListener("volumechange", (_event) => {
const newVolume = videoElement.volume;
localStorage.setItem("volume", newVolume);
videoElement.addEventListener("loadedmetadata", (_event) => {
setUpVideoControls();
});
globalPlayer = new Hls();
@ -84,6 +74,34 @@ async function loadVideoPlayer(playlistURL) {
return new Promise((resolve, _reject) => {
globalPlayer.on(Hls.Events.MEDIA_ATTACHED, () => {
globalPlayer.loadSource(rangedPlaylistURL);
globalPlayer.on(Hls.Events.ERROR, (_event, data) => {
if (data.fatal) {
switch (data.type) {
case Hls.ErrorTypes.NETWORK_ERROR:
console.log("A fatal network error occurred; retrying");
console.log(data);
globalPlayer.startLoad();
break;
case Hls.ErrorTypes.MEDIA_ERROR:
console.log("A fatal media error occurred; retrying");
console.log(data);
globalPlayer.recoverMediaError();
break;
default:
console.log("A fatal error occurred; resetting video player");
console.log(data);
addError(
"Some sort of video player error occurred. Thrimbletrimmer is resetting the video player."
);
resetVideoPlayer();
}
} else {
console.log("A non-fatal video player error occurred; HLS.js will retry");
console.log(data);
}
});
resolve();
});
});
@ -94,21 +112,149 @@ async function loadVideoPlayerFromDefaultPlaylist() {
await loadVideoPlayer(playlistURL);
}
function resetVideoPlayer() {
updateSegmentPlaylist();
}
function updateSegmentPlaylist() {
globalPlayer.destroy();
loadVideoPlayerFromDefaultPlaylist();
}
function parseHumanTimeStringAsDateTime(inputTime) {
function setUpVideoControls() {
// Setting this up so it's removed from the event doesn't work; loadedmetadata fires twice anyway.
// We still need to prevent double-setup, so here we are.
if (globalSetUpControls) {
return;
}
globalSetUpControls = true;
const videoElement = document.getElementById("video");
const playPauseButton = document.getElementById("video-controls-play-pause");
if (videoElement.paused) {
playPauseButton.src = "images/video-controls/play.png";
} else {
playPauseButton.src = "images/video-controls/pause.png";
}
const togglePlayState = (_event) => {
if (videoElement.paused) {
videoElement.play();
} else {
videoElement.pause();
}
};
playPauseButton.addEventListener("click", togglePlayState);
videoElement.addEventListener("click", togglePlayState);
videoElement.addEventListener("play", (_event) => {
playPauseButton.src = "images/video-controls/pause.png";
});
videoElement.addEventListener("pause", (_event) => {
playPauseButton.src = "images/video-controls/play.png";
});
const currentTime = document.getElementById("video-controls-current-time");
currentTime.innerText = videoHumanTimeFromVideoPlayerTime(videoElement.currentTime);
videoElement.addEventListener("timeupdate", (_event) => {
currentTime.innerText = videoHumanTimeFromVideoPlayerTime(videoElement.currentTime);
});
const duration = document.getElementById("video-controls-duration");
duration.innerText = videoHumanTimeFromVideoPlayerTime(videoElement.duration);
videoElement.addEventListener("durationchange", (_event) => {
duration.innerText = videoHumanTimeFromVideoPlayerTime(videoElement.duration);
});
const volumeMuted = document.getElementById("video-controls-volume-mute");
if (videoElement.muted) {
volumeMuted.src = "images/video-controls/volume-mute.png";
} else {
volumeMuted.src = "images/video-controls/volume.png";
}
const volumeLevel = document.getElementById("video-controls-volume-level");
const defaultVolume = +(localStorage.getItem("volume") ?? 0.5);
if (isNaN(defaultVolume)) {
defaultVolume = 0.5;
} else if (defaultVolume < 0) {
defaultVolume = 0;
} else if (defaultVolume > 1) {
defaultVolume = 1;
}
videoElement.volume = defaultVolume;
volumeLevel.value = videoElement.volume;
volumeLevel.addEventListener("click", (event) => {
videoElement.volume = event.offsetX / event.target.offsetWidth;
videoElement.muted = false;
});
videoElement.addEventListener("volumechange", (_event) => {
if (videoElement.muted) {
volumeMuted.src = "images/video-controls/volume-mute.png";
} else {
volumeMuted.src = "images/video-controls/volume.png";
}
volumeLevel.value = videoElement.volume;
localStorage.setItem("volume", videoElement.volume);
});
const playbackSpeed = document.getElementById("video-controls-playback-speed");
for (const speed of PLAYBACK_RATES) {
const speedOption = document.createElement("option");
speedOption.value = speed;
speedOption.innerText = `${speed}x`;
if (speed === 1) {
speedOption.selected = true;
}
playbackSpeed.appendChild(speedOption);
}
playbackSpeed.addEventListener("change", (_event) => {
const speed = +playbackSpeed.value;
videoElement.playbackRate = speed;
});
const quality = document.getElementById("video-controls-quality");
const defaultQuality = localStorage.getItem("quality");
for (const [qualityIndex, qualityLevel] of globalPlayer.levels.entries()) {
const qualityOption = document.createElement("option");
qualityOption.value = qualityIndex;
qualityOption.innerText = qualityLevel.name;
if (qualityLevel.name === defaultQuality) {
qualityOption.selected = true;
}
quality.appendChild(qualityOption);
}
localStorage.setItem("quality", quality.options[quality.options.selectedIndex].innerText);
quality.addEventListener("change", (_event) => {
globalPlayer.currentLevel = +quality.value;
});
const playbackPosition = document.getElementById("video-controls-playback-position");
playbackPosition.max = videoElement.duration;
playbackPosition.value = videoElement.currentTime;
videoElement.addEventListener("durationchange", (_event) => {
playbackPosition.max = videoElement.duration;
});
videoElement.addEventListener("timeupdate", (_event) => {
playbackPosition.value = videoElement.currentTime;
});
playbackPosition.addEventListener("click", (event) => {
const newPosition = (event.offsetX / event.target.offsetWidth) * videoElement.duration;
videoElement.currentTime = newPosition;
playbackPosition.value = newPosition;
});
}
function dateTimeMathObjectFromBusTime(busTime) {
// We need to handle inputs like "-0:10:15" in a way that consistently makes the time negative.
// Since we can't assign the negative sign to any particular part, we'll check for the whole thing here.
let direction = 1;
if (inputTime.startsWith("-")) {
inputTime = inputTime.slice(1);
if (busTime.startsWith("-")) {
busTime = busTime.slice(1);
direction = -1;
}
const parts = inputTime.split(":", 3);
const parts = busTime.split(":", 3);
const hours = parseInt(parts[0]) * direction;
const minutes = (parts[1] || 0) * direction;
const seconds = (parts[2] || 0) * direction;
@ -116,7 +262,7 @@ function parseHumanTimeStringAsDateTime(inputTime) {
}
function dateTimeFromBusTime(busTime) {
return globalBusStartTime.plus(parseHumanTimeStringAsDateTime(busTime));
return globalBusStartTime.plus(dateTimeMathObjectFromBusTime(busTime));
}
function busTimeFromDateTime(dateTime) {
@ -172,3 +318,51 @@ function startAndEndTimeQueryStringParts() {
}
return queryStringParts;
}
function videoHumanTimeFromVideoPlayerTime(videoPlayerTime) {
const hours = Math.floor(videoPlayerTime / 3600);
let minutes = Math.floor((videoPlayerTime % 3600) / 60);
let seconds = Math.floor(videoPlayerTime % 60);
let milliseconds = Math.floor((videoPlayerTime * 1000) % 1000);
while (minutes.toString().length < 2) {
minutes = `0${minutes}`;
}
while (seconds.toString().length < 2) {
seconds = `0${seconds}`;
}
while (milliseconds.toString().length < 3) {
milliseconds = `0${milliseconds}`;
}
if (hours > 0) {
return `${hours}:${minutes}:${seconds}.${milliseconds}`;
}
return `${minutes}:${seconds}.${milliseconds}`;
}
function videoPlayerTimeFromVideoHumanTime(videoHumanTime) {
let timeParts = videoHumanTime.split(":", 2);
let hours;
let minutes;
let seconds;
if (timeParts.length < 2) {
hours = 0;
minutes = 0;
seconds = +timeParts[0];
} else if (timeParts.length < 3) {
hours = 0;
minutes = parseInt(timeParts[0]);
seconds = +timeParts[1];
} else {
hours = parseInt(timeParts[0]);
minutes = parseInt(timeParts[1]);
seconds = +timeParts[2];
}
if (isNaN(hours) || isNaN(minutes) || isNaN(seconds)) {
return null;
}
return hours * 3600 + minutes * 60 + seconds;
}

@ -1036,40 +1036,6 @@ function wubloaderTimeFromVideoPlayerTime(videoPlayerTime) {
return wubloaderTimeFromDateTime(dt);
}
function videoHumanTimeFromVideoPlayerTime(videoPlayerTime) {
const minutes = Math.floor(videoPlayerTime / 60);
let seconds = Math.floor(videoPlayerTime % 60);
let milliseconds = Math.floor((videoPlayerTime * 1000) % 1000);
while (seconds.toString().length < 2) {
seconds = `0${seconds}`;
}
while (milliseconds.toString().length < 3) {
milliseconds = `0${milliseconds}`;
}
return `${minutes}:${seconds}.${milliseconds}`;
}
function videoPlayerTimeFromVideoHumanTime(videoHumanTime) {
let timeParts = videoHumanTime.split(":", 2);
let minutes;
let seconds;
if (timeParts.length < 2) {
minutes = 0;
seconds = +timeParts[0];
} else {
minutes = parseInt(timeParts[0]);
seconds = +timeParts[1];
}
if (isNaN(minutes) || isNaN(seconds)) {
return null;
}
return minutes * 60 + seconds;
}
function videoHumanTimeFromWubloaderTime(wubloaderTime) {
const videoPlayerTime = videoPlayerTimeFromWubloaderTime(wubloaderTime);
return videoHumanTimeFromVideoPlayerTime(videoPlayerTime);

@ -76,7 +76,7 @@ function dateTimeFromTimeString(timeString, timeStringFormat) {
case 2:
return dateTimeFromBusTime(timeString);
case 3:
return DateTime.now().setZone("utc").minus(parseHumanTimeStringAsDateTime(timeString));
return DateTime.now().setZone("utc").minus(dateTimeMathObjectFromBusTime(timeString));
}
}

@ -84,6 +84,97 @@ a,
max-height: 50vh;
}
#video-controls {
font-size: 125%;
}
#video-controls select {
appearance: none;
font-size: inherit;
background: inherit;
border: none;
text-align: center;
}
#video-controls option {
background: #222;
padding: 2px;
}
#video-controls-bar {
display: flex;
align-items: center;
gap: 8px;
}
#video-controls-spacer {
flex-grow: 1;
}
#video-controls-volume {
display: flex;
align-items: center;
}
#video-controls-volume-level {
width: 100px;
height: 8px;
border-radius: 0;
border: 0;
}
#video-controls-playback-position {
height: 10px;
border-radius: 0;
border: 0;
}
/* For some reason, there's not a cross-browser way to style <progress> elements.
* This should be replaced with a cross-browser way of doing this when possible.
* I only implemented WebKit/Blink and Firefox here because if you still use IE,
* I quite frankly don't care about you.
*/
/* WEBKIT/BLINK SECTION */
#video-controls-volume-level::-webkit-progress-bar {
background: #ffffff30;
}
#video-controls-volume-level::-webkit-progress-value {
background: #ffffffc0;
}
#video-controls-playback-position::-webkit-progress-bar {
background: #ffffff30;
}
#video-controls-playback-position::-webkit-progress-value {
background: #ffffffc0;
}
/* END WEBKIT/BLINK */
/* FIREFOX SECTION */
#video-controls-volume-level {
background: #ffffff30;
}
#video-controls-volume-level::-moz-progress-bar {
background: #ffffffc0;
}
#video-controls-playback-position {
background: #ffffff30;
}
#video-controls-playback-position::-moz-progress-bar {
background: #ffffffc0;
}
/* END FIREFOX */
#video-controls-playback-position {
width: 100%;
}
#clip-bar {
width: 100%;
height: 7px;

Loading…
Cancel
Save