Initial transition from VideoJS to HLS.js as the video player

pull/248/head
ElementalAlchemist 3 years ago committed by Mike Lang
parent 11bf89305a
commit 0340f06170

@ -1,4 +1,3 @@
styles/video-js.min.css
scripts/video.min.js
scripts/hls.min.js
scripts/luxon.min.js
dashboard.html

@ -5,9 +5,8 @@
<title>VST Video Editor</title>
<link rel="stylesheet" href="styles/thrimbletrimmer.css" />
<link rel="stylesheet" href="styles/video-js.min.css" />
<script src="scripts/video.min.js"></script>
<script src="scripts/hls.min.js"></script>
<script src="scripts/luxon.min.js"></script>
<script src="scripts/common.js"></script>
<script src="scripts/edit.js"></script>
@ -41,7 +40,7 @@
</div>
</form>
<video id="video" class="video-js" controls preload="auto"></video>
<video id="video" controls preload="auto"></video>
<div id="clip-bar"></div>
<div id="waveform-container">
<img id="waveform" alt="Waveform for the video" />

@ -5,9 +5,8 @@
<title>VST Restreamer</title>
<link rel="stylesheet" href="styles/thrimbletrimmer.css" />
<link rel="stylesheet" href="styles/video-js.min.css" />
<script src="scripts/video.min.js"></script>
<script src="scripts/hls.min.js"></script>
<script src="scripts/luxon.min.js"></script>
<script src="scripts/common.js"></script>
<script src="scripts/stream.js"></script>
@ -59,7 +58,7 @@
</div>
</form>
<video id="video" class="video-js" controls preload="auto"></video>
<video id="video" controls preload="auto"></video>
<div id="editor-help">
<a href="#" id="editor-help-link">Help</a>

@ -6,11 +6,21 @@ var globalStreamName = "";
var globalStartTimeString = "";
var globalEndTimeString = "";
var globalPlayer = null;
Hls.DefaultConfig.maxBufferHole = 600;
const VIDEO_FRAMES_PER_SECOND = 30;
const PLAYBACK_RATES = [0.5, 1, 1.25, 1.5, 2];
const PLAYBACK_RATES = [0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2];
function commonPageSetup() {
if (!Hls.isSupported()) {
addError(
"Your browser doesn't support MediaSource extensions. Video playback and editing won't work."
);
}
const helpLink = document.getElementById("editor-help-link");
helpLink.addEventListener("click", toggleHelpDisplay);
@ -32,10 +42,6 @@ function toggleHelpDisplay() {
}
}
function getVideoJS() {
return videojs.getPlayer("video");
}
function addError(errorText) {
const errorElement = document.createElement("div");
errorElement.innerText = errorText;
@ -55,40 +61,27 @@ function addError(errorText) {
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);
});
let defaultOptions = {
sources: [{ src: rangedPlaylistURL }],
liveui: true,
controls: true,
autoplay: false,
playbackRates: PLAYBACK_RATES,
inactivityTimeout: 0,
controlBar: {
fullscreenToggle: true,
volumePanel: {
inline: false,
},
},
};
const player = videojs("video", defaultOptions);
globalPlayer = new Hls();
globalPlayer.attachMedia(video);
return new Promise((resolve, _reject) => {
player.ready(() => {
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;
}
player.volume(volume);
player.on("volumechange", () => {
const player = getVideoJS();
const volume = player.volume();
localStorage.setItem("volume", volume);
});
globalPlayer.on(Hls.Events.MEDIA_ATTACHED, () => {
globalPlayer.loadSource(rangedPlaylistURL);
resolve();
});
});
@ -100,14 +93,8 @@ async function loadVideoPlayerFromDefaultPlaylist() {
}
function updateSegmentPlaylist() {
const playlistURL = `/playlist/${globalStreamName}.m3u8`;
updateVideoPlayer(playlistURL);
}
function updateVideoPlayer(newPlaylistURL) {
let rangedPlaylistURL = assembleVideoPlaylistURL(newPlaylistURL);
const player = getVideoJS();
player.src({ src: rangedPlaylistURL });
globalPlayer.destroy();
loadVideoPlayerFromDefaultPlaylist();
}
function parseHumanTimeStringAsDateTime(inputTime) {

@ -1,7 +1,6 @@
var googleUser = null;
var videoInfo;
var currentRange = 1;
var globalLoadedRangeData = false;
window.addEventListener("DOMContentLoaded", async (event) => {
commonPageSetup();
@ -35,12 +34,12 @@ window.addEventListener("DOMContentLoaded", async (event) => {
}
const oldStart = getStartTime();
const startAdjustment = newStart.diff(oldStart, "seconds").seconds;
let newDuration = newEnd === null ? Infinity : newEnd.diff(newStart, "seconds").seconds;
const startAdjustment = newStart.diff(oldStart).as("seconds");
let newDuration = newEnd === null ? Infinity : newEnd.diff(newStart).as("seconds");
// The video duration isn't precisely the video times, but can be padded by up to the
// segment length on either side.
const segmentList = getPlaylistData().segments;
const segmentList = getSegmentList();
newDuration += segmentList[0].duration;
newDuration += segmentList[segmentList.length - 1].duration;
@ -376,54 +375,51 @@ async function initializeVideoInfo() {
await loadVideoPlayerFromDefaultPlaylist();
const player = getVideoJS();
player.on("loadedmetadata", () => {
if (!globalLoadedRangeData) {
const rangeDefinitionsContainer = document.getElementById("range-definitions");
if (videoInfo.video_ranges && videoInfo.video_ranges.length > 0) {
for (let rangeIndex = 0; rangeIndex < videoInfo.video_ranges.length; rangeIndex++) {
if (rangeIndex >= rangeDefinitionsContainer.children.length) {
addRangeDefinition();
}
const startWubloaderTime = videoInfo.video_ranges[rangeIndex][0];
const endWubloaderTime = videoInfo.video_ranges[rangeIndex][1];
if (startWubloaderTime) {
const startField =
rangeDefinitionsContainer.children[rangeIndex].getElementsByClassName(
"range-definition-start"
)[0];
startField.value = videoHumanTimeFromWubloaderTime(startWubloaderTime);
}
if (endWubloaderTime) {
const endField =
rangeDefinitionsContainer.children[rangeIndex].getElementsByClassName(
"range-definition-end"
)[0];
endField.value = videoHumanTimeFromWubloaderTime(endWubloaderTime);
}
const videoElement = document.getElementById("video");
const handleInitialSetupForDuration = (_event) => {
const rangeDefinitionsContainer = document.getElementById("range-definitions");
if (videoInfo.video_ranges && videoInfo.video_ranges.length > 0) {
for (let rangeIndex = 0; rangeIndex < videoInfo.video_ranges.length; rangeIndex++) {
if (rangeIndex >= rangeDefinitionsContainer.children.length) {
addRangeDefinition();
}
} else {
const rangeStartField =
rangeDefinitionsContainer.getElementsByClassName("range-definition-start")[0];
rangeStartField.value = videoHumanTimeFromWubloaderTime(globalStartTimeString);
if (globalEndTimeString) {
const rangeEndField =
rangeDefinitionsContainer.getElementsByClassName("range-definition-end")[0];
rangeEndField.value = videoHumanTimeFromWubloaderTime(globalEndTimeString);
const startWubloaderTime = videoInfo.video_ranges[rangeIndex][0];
const endWubloaderTime = videoInfo.video_ranges[rangeIndex][1];
if (startWubloaderTime) {
const startField =
rangeDefinitionsContainer.children[rangeIndex].getElementsByClassName(
"range-definition-start"
)[0];
startField.value = videoHumanTimeFromWubloaderTime(startWubloaderTime);
}
if (endWubloaderTime) {
const endField =
rangeDefinitionsContainer.children[rangeIndex].getElementsByClassName(
"range-definition-end"
)[0];
endField.value = videoHumanTimeFromWubloaderTime(endWubloaderTime);
}
}
} else {
const rangeStartField =
rangeDefinitionsContainer.getElementsByClassName("range-definition-start")[0];
rangeStartField.value = videoHumanTimeFromWubloaderTime(globalStartTimeString);
if (globalEndTimeString) {
const rangeEndField =
rangeDefinitionsContainer.getElementsByClassName("range-definition-end")[0];
rangeEndField.value = videoHumanTimeFromWubloaderTime(globalEndTimeString);
}
globalLoadedRangeData = true;
}
// Although we may or may not have updated the range data here, this is where we know the new video duration.
// Because of this, we need to run this to properly update range-dependent things like the clip bar UI,
// which require a location.
videoElement.removeEventListener("durationchange", handleInitialSetupForDuration);
};
videoElement.addEventListener("durationchange", handleInitialSetupForDuration);
videoElement.addEventListener("durationchange", (_event) => {
// Every time this is updated, we need to update based on the new video duration
rangeDataUpdated();
});
player.on("timeupdate", () => {
const player = getVideoJS();
const currentTime = player.currentTime();
const duration = player.duration();
const timePercent = (currentTime / duration) * 100;
videoElement.addEventListener("timeupdate", (_event) => {
const timePercent = (videoElement.currentTime / videoElement.duration) * 100;
document.getElementById("waveform-marker").style.left = `${timePercent}%`;
});
}
@ -880,8 +876,8 @@ function getRangeSetClickHandler(startOrEnd) {
`range-definition-${startOrEnd}`
)[0];
const player = getVideoJS();
const videoPlayerTime = player.currentTime();
const videoElement = document.getElementById("video");
const videoPlayerTime = videoElement.currentTime;
setField.value = videoHumanTimeFromVideoPlayerTime(videoPlayerTime);
rangeDataUpdated();
@ -923,8 +919,8 @@ function rangePlayFromStartHandler(event) {
return;
}
const player = getVideoJS();
player.currentTime(startTime);
const videoElement = document.getElementById("video");
videoElement.currentTime = startTime;
}
function rangePlayFromEndHandler(event) {
@ -936,16 +932,16 @@ function rangePlayFromEndHandler(event) {
return;
}
const player = getVideoJS();
player.currentTime(endTime);
const videoElement = document.getElementById("video");
videoElement.currentTime = endTime;
}
function rangeDataUpdated() {
const clipBar = document.getElementById("clip-bar");
clipBar.innerHTML = "";
const player = getVideoJS();
const videoDuration = player.duration();
const videoElement = document.getElementById("video");
const videoDuration = videoElement.duration;
for (let rangeDefinition of document.getElementById("range-definitions").children) {
const rangeStartField = rangeDefinition.getElementsByClassName("range-definition-start")[0];
@ -973,8 +969,8 @@ function setCurrentRangeStartToVideoTime() {
const rangeStartField = document.querySelector(
`#range-definitions > div:nth-child(${currentRange}) .range-definition-start`
);
const player = getVideoJS();
rangeStartField.value = videoHumanTimeFromVideoPlayerTime(player.currentTime());
const videoElement = document.getElementById("video");
rangeStartField.value = videoHumanTimeFromVideoPlayerTime(videoElement.currentTime);
rangeDataUpdated();
}
@ -982,65 +978,40 @@ function setCurrentRangeEndToVideoTime() {
const rangeEndField = document.querySelector(
`#range-definitions > div:nth-child(${currentRange}) .range-definition-end`
);
const player = getVideoJS();
rangeEndField.value = videoHumanTimeFromVideoPlayerTime(player.currentTime());
const videoElement = document.getElementById("video");
rangeEndField.value = videoHumanTimeFromVideoPlayerTime(videoElement.currentTime);
rangeDataUpdated();
}
function videoPlayerTimeFromWubloaderTime(wubloaderTime) {
const videoPlaylist = getPlaylistData();
const wubloaderDateTime = dateTimeFromWubloaderTime(wubloaderTime);
let highestDiscontinuitySegmentBefore = 0;
for (start of videoPlaylist.discontinuityStarts) {
const discontinuityStartSegment = videoPlaylist.segments[start];
const discontinuityStartDateTime = DateTime.fromJSDate(
discontinuityStartSegment.dateTimeObject,
{ zone: "utc" }
);
const highestDiscontinuitySegmentDateTime = DateTime.fromJSDate(
videoPlaylist.segments[highestDiscontinuitySegmentBefore].dateTimeObject,
{ zone: "utc" }
);
if (
discontinuityStartDateTime.diff(wubloaderDateTime).milliseconds < 0 && // Discontinuity starts before the provided time
discontinuityStartDateTime.diff(highestDiscontinuitySegmentDateTime).milliseconds > 0 // Discontinuity starts after the latest found discontinuity
) {
highestDiscontinuitySegmentBefore = start;
const segmentList = getSegmentList();
for (const segment of segmentList) {
const segmentStart = DateTime.fromISO(segment.rawProgramDateTime, { zone: "utc" });
const segmentEnd = segmentStart.plus({ seconds: segment.duration });
if (segmentStart <= wubloaderDateTime && segmentEnd > wubloaderDateTime) {
return segment.start + wubloaderDateTime.diff(segmentStart).as("seconds");
}
}
let highestDiscontinuitySegmentStart = 0;
for (let segment = 0; segment < highestDiscontinuitySegmentBefore; segment++) {
highestDiscontinuitySegmentStart += videoPlaylist.segments[segment].duration;
}
const highestDiscontinuitySegmentDateTime = DateTime.fromJSDate(
videoPlaylist.segments[highestDiscontinuitySegmentBefore].dateTimeObject,
{ zone: "utc" }
);
return (
highestDiscontinuitySegmentStart +
wubloaderDateTime.diff(highestDiscontinuitySegmentDateTime, "seconds").seconds
);
return null;
}
function dateTimeFromVideoPlayerTime(videoPlayerTime) {
const videoPlaylist = getPlaylistData();
let segmentStartTime = 0;
let segmentDateObj;
// Segments have start and end video player times on them, but only if the segments are already loaded.
// This is not the case before the video is loaded for the first time, or outside the video's buffer if it hasn't played that far/part.
for (segment of videoPlaylist.segments) {
const segmentEndTime = segmentStartTime + segment.duration;
if (segmentStartTime <= videoPlayerTime && segmentEndTime >= videoPlayerTime) {
segmentDateObj = segment.dateTimeObject;
const segmentList = getSegmentList();
let segmentStartTime;
let segmentStartISOTime;
for (const segment of segmentList) {
const segmentEndTime = segment.start + segment.duration;
if (videoPlayerTime >= segment.start && videoPlayerTime < segmentEndTime) {
segmentStartTime = segment.start;
segmentStartISOTime = segment.rawProgramDateTime;
break;
}
segmentStartTime = segmentEndTime;
}
if (segmentDateObj === undefined) {
if (segmentStartISOTime === undefined) {
return null;
}
let wubloaderDateTime = DateTime.fromJSDate(segmentDateObj, { zone: "utc" });
const wubloaderDateTime = DateTime.fromISO(segmentStartISOTime, { zone: "utc" });
const offset = videoPlayerTime - segmentStartTime;
return wubloaderDateTime.plus({ seconds: offset });
}
@ -1097,10 +1068,6 @@ function wubloaderTimeFromVideoHumanTime(videoHumanTime) {
return wubloaderTimeFromVideoPlayerTime(videoPlayerTime);
}
function getPlaylistData() {
const player = getVideoJS();
// Currently, this only supports a single playlist. We only give one playlist (or master playlist file) to VideoJS,
// so this should be fine for now. If we need to support multiple playlists in the future (varying quality levels,
// etc.), this and all callers will need to be updated.
return player.tech("OK").vhs.playlists.master.playlists[0];
function getSegmentList() {
return globalPlayer.latencyController.levelDetails.fragments;
}

File diff suppressed because one or more lines are too long

@ -1,5 +1,6 @@
function moveSpeed(player, amount) {
let currentIndex = PLAYBACK_RATES.indexOf(player.playbackRate());
function moveSpeed(amount) {
const videoElement = document.getElementById("video");
let currentIndex = PLAYBACK_RATES.indexOf(videoElement.playbackRate);
if (currentIndex === -1) {
addError("The playback rate has somehow gone very wrong.");
return;
@ -8,15 +9,15 @@ function moveSpeed(player, amount) {
if (currentIndex < 0 || currentIndex >= PLAYBACK_RATES.length) {
return; // We've reached/exceeded the edge
}
player.playbackRate(PLAYBACK_RATES[currentIndex]);
videoElement.playbackRate = PLAYBACK_RATES[currentIndex];
}
function increaseSpeed(player) {
moveSpeed(player, 1);
function increaseSpeed() {
moveSpeed(1);
}
function decreaseSpeed(player) {
moveSpeed(player, -1);
function decreaseSpeed() {
moveSpeed(-1);
}
document.addEventListener("keypress", (event) => {
@ -24,69 +25,69 @@ document.addEventListener("keypress", (event) => {
return;
}
const player = getVideoJS();
const videoElement = document.getElementById("video");
switch (event.key) {
case "0":
player.currentTime(0);
videoElement.currentTime = 0;
break;
case "1":
player.currentTime(player.duration() * 0.1);
videoElement.currentTime = videoElement.duration * 0.1;
break;
case "2":
player.currentTime(player.duration() * 0.2);
videoElement.currentTime = videoElement.duration * 0.2;
break;
case "3":
player.currentTime(player.duration() * 0.3);
videoElement.currentTime = videoElement.duration * 0.3;
break;
case "4":
player.currentTime(player.duration() * 0.4);
videoElement.currentTime = videoElement.duration * 0.4;
break;
case "5":
player.currentTime(player.duration() * 0.5);
videoElement.currentTime = videoElement.duration * 0.5;
break;
case "6":
player.currentTime(player.duration() * 0.6);
videoElement.currentTime = videoElement.duration * 0.6;
break;
case "7":
player.currentTime(player.duration() * 0.7);
videoElement.currentTime = videoElement.duration * 0.7;
break;
case "8":
player.currentTime(player.duration() * 0.8);
videoElement.currentTime = videoElement.duration * 0.8;
break;
case "9":
player.currentTime(player.duration() * 0.9);
videoElement.currentTime = videoElement.duration * 0.9;
break;
case "j":
player.currentTime(player.currentTime() - 10);
videoElement.currentTime -= 10;
break;
case "k":
case " ":
if (player.paused()) {
player.play();
if (videoElement.paused) {
videoElement.play();
} else {
player.pause();
videoElement.pause();
}
break;
case "l":
player.currentTime(player.currentTime() + 10);
videoElement.currentTime += 10;
break;
case "ArrowLeft":
player.currentTime(player.currentTime() - 5);
videoElement.currentTime -= 5;
break;
case "ArrowRight":
player.currentTime(player.currentTime() + 5);
videoElement.currentTime += 5;
break;
case ",":
player.currentTime(player.currentTime() - 1 / VIDEO_FRAMES_PER_SECOND);
videoElement.currentTime -= 1 / VIDEO_FRAMES_PER_SECOND;
break;
case ".":
player.currentTime(player.currentTime() + 1 / VIDEO_FRAMES_PER_SECOND);
videoElement.currentTime += 1 / VIDEO_FRAMES_PER_SECOND;
break;
case "=":
increaseSpeed(player);
increaseSpeed();
break;
case "-":
decreaseSpeed(player);
decreaseSpeed();
break;
case "[":
if (typeof setCurrentRangeStartToVideoTime === "function") {
@ -119,13 +120,13 @@ document.addEventListener("keydown", (event) => {
return;
}
const player = getVideoJS();
const videoElement = document.getElementById("video");
switch (event.key) {
case "ArrowLeft":
player.currentTime(player.currentTime() - 5);
videoElement.currentTime -= 5;
break;
case "ArrowRight":
player.currentTime(player.currentTime() + 5);
videoElement.currentTime += 5;
break;
default:
break;

@ -93,7 +93,7 @@ function updateTimeSettings() {
const startTime = getStartTime();
const endTime = getEndTime();
if (endTime && endTime.diff(startTime) < 0) {
if (endTime && endTime.diff(startTime).milliseconds < 0) {
addError(
"End time is before the start time. This will prevent video loading and cause other problems."
);

File diff suppressed because one or more lines are too long

@ -84,44 +84,6 @@ a,
max-height: 50vh;
}
/* START BLOCK
* We want to style the VideoJS player controls to have a full-screen-width progress bar.
* Since we're taking the progress bar out, we also need to do a couple other restylings.
*/
#video .vjs-control-bar .vjs-time-control {
display: block; /* We want to display these */
}
#video .vjs-control-bar .vjs-progress-control {
position: absolute;
bottom: 26px; /* Aligns the bar to the top of the control bar */
left: 0;
right: 0;
width: 100%;
height: 10px;
}
#video .vjs-control-bar .vjs-progress-control .vjs-progress-holder {
margin-left: 0px;
margin-right: 0px;
}
#video .vjs-control-bar .vjs-remaining-time {
/* Right-align the controls we want to be right-aligned by using this to shove
* the rest of the controls to the right
*/
flex-grow: 1;
text-align: left;
}
/* END BLOCK */
/* Separately from that, it'd also be nice for the video controls not to cover the video,
* so the size of the video is reduced by the progress bar height here.
*/
#video .vjs-tech {
height: calc(100% - 33px);
}
#clip-bar {
width: 100%;
height: 7px;

File diff suppressed because one or more lines are too long
Loading…
Cancel
Save