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/hls.min.js
scripts/video.min.js
scripts/luxon.min.js scripts/luxon.min.js
dashboard.html dashboard.html

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

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

@ -6,11 +6,21 @@ var globalStreamName = "";
var globalStartTimeString = ""; var globalStartTimeString = "";
var globalEndTimeString = ""; var globalEndTimeString = "";
var globalPlayer = null;
Hls.DefaultConfig.maxBufferHole = 600;
const VIDEO_FRAMES_PER_SECOND = 30; 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() { 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"); const helpLink = document.getElementById("editor-help-link");
helpLink.addEventListener("click", toggleHelpDisplay); helpLink.addEventListener("click", toggleHelpDisplay);
@ -32,10 +42,6 @@ function toggleHelpDisplay() {
} }
} }
function getVideoJS() {
return videojs.getPlayer("video");
}
function addError(errorText) { function addError(errorText) {
const errorElement = document.createElement("div"); const errorElement = document.createElement("div");
errorElement.innerText = errorText; errorElement.innerText = errorText;
@ -55,25 +61,8 @@ function addError(errorText) {
async function loadVideoPlayer(playlistURL) { async function loadVideoPlayer(playlistURL) {
let rangedPlaylistURL = assembleVideoPlaylistURL(playlistURL); let rangedPlaylistURL = assembleVideoPlaylistURL(playlistURL);
const videoElement = document.getElementById("video");
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);
return new Promise((resolve, _reject) => {
player.ready(() => {
const volume = +(localStorage.getItem("volume") ?? 0.5); const volume = +(localStorage.getItem("volume") ?? 0.5);
if (isNaN(volume)) { if (isNaN(volume)) {
volume = 0.5; volume = 0.5;
@ -82,13 +71,17 @@ async function loadVideoPlayer(playlistURL) {
} else if (volume > 1) { } else if (volume > 1) {
volume = 1; volume = 1;
} }
player.volume(volume); videoElement.volume = volume;
videoElement.addEventListener("volumechange", (_event) => {
player.on("volumechange", () => { const newVolume = videoElement.volume;
const player = getVideoJS(); localStorage.setItem("volume", newVolume);
const volume = player.volume();
localStorage.setItem("volume", volume);
}); });
globalPlayer = new Hls();
globalPlayer.attachMedia(video);
return new Promise((resolve, _reject) => {
globalPlayer.on(Hls.Events.MEDIA_ATTACHED, () => {
globalPlayer.loadSource(rangedPlaylistURL);
resolve(); resolve();
}); });
}); });
@ -100,14 +93,8 @@ async function loadVideoPlayerFromDefaultPlaylist() {
} }
function updateSegmentPlaylist() { function updateSegmentPlaylist() {
const playlistURL = `/playlist/${globalStreamName}.m3u8`; globalPlayer.destroy();
updateVideoPlayer(playlistURL); loadVideoPlayerFromDefaultPlaylist();
}
function updateVideoPlayer(newPlaylistURL) {
let rangedPlaylistURL = assembleVideoPlaylistURL(newPlaylistURL);
const player = getVideoJS();
player.src({ src: rangedPlaylistURL });
} }
function parseHumanTimeStringAsDateTime(inputTime) { function parseHumanTimeStringAsDateTime(inputTime) {

@ -1,7 +1,6 @@
var googleUser = null; var googleUser = null;
var videoInfo; var videoInfo;
var currentRange = 1; var currentRange = 1;
var globalLoadedRangeData = false;
window.addEventListener("DOMContentLoaded", async (event) => { window.addEventListener("DOMContentLoaded", async (event) => {
commonPageSetup(); commonPageSetup();
@ -35,12 +34,12 @@ window.addEventListener("DOMContentLoaded", async (event) => {
} }
const oldStart = getStartTime(); const oldStart = getStartTime();
const startAdjustment = newStart.diff(oldStart, "seconds").seconds; const startAdjustment = newStart.diff(oldStart).as("seconds");
let newDuration = newEnd === null ? Infinity : newEnd.diff(newStart, "seconds").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 // The video duration isn't precisely the video times, but can be padded by up to the
// segment length on either side. // segment length on either side.
const segmentList = getPlaylistData().segments; const segmentList = getSegmentList();
newDuration += segmentList[0].duration; newDuration += segmentList[0].duration;
newDuration += segmentList[segmentList.length - 1].duration; newDuration += segmentList[segmentList.length - 1].duration;
@ -376,9 +375,8 @@ async function initializeVideoInfo() {
await loadVideoPlayerFromDefaultPlaylist(); await loadVideoPlayerFromDefaultPlaylist();
const player = getVideoJS(); const videoElement = document.getElementById("video");
player.on("loadedmetadata", () => { const handleInitialSetupForDuration = (_event) => {
if (!globalLoadedRangeData) {
const rangeDefinitionsContainer = document.getElementById("range-definitions"); const rangeDefinitionsContainer = document.getElementById("range-definitions");
if (videoInfo.video_ranges && videoInfo.video_ranges.length > 0) { if (videoInfo.video_ranges && videoInfo.video_ranges.length > 0) {
for (let rangeIndex = 0; rangeIndex < videoInfo.video_ranges.length; rangeIndex++) { for (let rangeIndex = 0; rangeIndex < videoInfo.video_ranges.length; rangeIndex++) {
@ -412,18 +410,16 @@ async function initializeVideoInfo() {
rangeEndField.value = videoHumanTimeFromWubloaderTime(globalEndTimeString); rangeEndField.value = videoHumanTimeFromWubloaderTime(globalEndTimeString);
} }
} }
globalLoadedRangeData = true; videoElement.removeEventListener("durationchange", handleInitialSetupForDuration);
} };
// Although we may or may not have updated the range data here, this is where we know the new video duration. videoElement.addEventListener("durationchange", handleInitialSetupForDuration);
// Because of this, we need to run this to properly update range-dependent things like the clip bar UI, videoElement.addEventListener("durationchange", (_event) => {
// which require a location. // Every time this is updated, we need to update based on the new video duration
rangeDataUpdated(); rangeDataUpdated();
}); });
player.on("timeupdate", () => {
const player = getVideoJS(); videoElement.addEventListener("timeupdate", (_event) => {
const currentTime = player.currentTime(); const timePercent = (videoElement.currentTime / videoElement.duration) * 100;
const duration = player.duration();
const timePercent = (currentTime / duration) * 100;
document.getElementById("waveform-marker").style.left = `${timePercent}%`; document.getElementById("waveform-marker").style.left = `${timePercent}%`;
}); });
} }
@ -880,8 +876,8 @@ function getRangeSetClickHandler(startOrEnd) {
`range-definition-${startOrEnd}` `range-definition-${startOrEnd}`
)[0]; )[0];
const player = getVideoJS(); const videoElement = document.getElementById("video");
const videoPlayerTime = player.currentTime(); const videoPlayerTime = videoElement.currentTime;
setField.value = videoHumanTimeFromVideoPlayerTime(videoPlayerTime); setField.value = videoHumanTimeFromVideoPlayerTime(videoPlayerTime);
rangeDataUpdated(); rangeDataUpdated();
@ -923,8 +919,8 @@ function rangePlayFromStartHandler(event) {
return; return;
} }
const player = getVideoJS(); const videoElement = document.getElementById("video");
player.currentTime(startTime); videoElement.currentTime = startTime;
} }
function rangePlayFromEndHandler(event) { function rangePlayFromEndHandler(event) {
@ -936,16 +932,16 @@ function rangePlayFromEndHandler(event) {
return; return;
} }
const player = getVideoJS(); const videoElement = document.getElementById("video");
player.currentTime(endTime); videoElement.currentTime = endTime;
} }
function rangeDataUpdated() { function rangeDataUpdated() {
const clipBar = document.getElementById("clip-bar"); const clipBar = document.getElementById("clip-bar");
clipBar.innerHTML = ""; clipBar.innerHTML = "";
const player = getVideoJS(); const videoElement = document.getElementById("video");
const videoDuration = player.duration(); const videoDuration = videoElement.duration;
for (let rangeDefinition of document.getElementById("range-definitions").children) { for (let rangeDefinition of document.getElementById("range-definitions").children) {
const rangeStartField = rangeDefinition.getElementsByClassName("range-definition-start")[0]; const rangeStartField = rangeDefinition.getElementsByClassName("range-definition-start")[0];
@ -973,8 +969,8 @@ function setCurrentRangeStartToVideoTime() {
const rangeStartField = document.querySelector( const rangeStartField = document.querySelector(
`#range-definitions > div:nth-child(${currentRange}) .range-definition-start` `#range-definitions > div:nth-child(${currentRange}) .range-definition-start`
); );
const player = getVideoJS(); const videoElement = document.getElementById("video");
rangeStartField.value = videoHumanTimeFromVideoPlayerTime(player.currentTime()); rangeStartField.value = videoHumanTimeFromVideoPlayerTime(videoElement.currentTime);
rangeDataUpdated(); rangeDataUpdated();
} }
@ -982,65 +978,40 @@ function setCurrentRangeEndToVideoTime() {
const rangeEndField = document.querySelector( const rangeEndField = document.querySelector(
`#range-definitions > div:nth-child(${currentRange}) .range-definition-end` `#range-definitions > div:nth-child(${currentRange}) .range-definition-end`
); );
const player = getVideoJS(); const videoElement = document.getElementById("video");
rangeEndField.value = videoHumanTimeFromVideoPlayerTime(player.currentTime()); rangeEndField.value = videoHumanTimeFromVideoPlayerTime(videoElement.currentTime);
rangeDataUpdated(); rangeDataUpdated();
} }
function videoPlayerTimeFromWubloaderTime(wubloaderTime) { function videoPlayerTimeFromWubloaderTime(wubloaderTime) {
const videoPlaylist = getPlaylistData();
const wubloaderDateTime = dateTimeFromWubloaderTime(wubloaderTime); const wubloaderDateTime = dateTimeFromWubloaderTime(wubloaderTime);
let highestDiscontinuitySegmentBefore = 0; const segmentList = getSegmentList();
for (start of videoPlaylist.discontinuityStarts) { for (const segment of segmentList) {
const discontinuityStartSegment = videoPlaylist.segments[start]; const segmentStart = DateTime.fromISO(segment.rawProgramDateTime, { zone: "utc" });
const discontinuityStartDateTime = DateTime.fromJSDate( const segmentEnd = segmentStart.plus({ seconds: segment.duration });
discontinuityStartSegment.dateTimeObject, if (segmentStart <= wubloaderDateTime && segmentEnd > wubloaderDateTime) {
{ zone: "utc" } return segment.start + wubloaderDateTime.diff(segmentStart).as("seconds");
);
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;
} }
} }
return null;
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
);
} }
function dateTimeFromVideoPlayerTime(videoPlayerTime) { function dateTimeFromVideoPlayerTime(videoPlayerTime) {
const videoPlaylist = getPlaylistData(); const segmentList = getSegmentList();
let segmentStartTime = 0; let segmentStartTime;
let segmentDateObj; let segmentStartISOTime;
// Segments have start and end video player times on them, but only if the segments are already loaded. for (const segment of segmentList) {
// 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. const segmentEndTime = segment.start + segment.duration;
for (segment of videoPlaylist.segments) { if (videoPlayerTime >= segment.start && videoPlayerTime < segmentEndTime) {
const segmentEndTime = segmentStartTime + segment.duration; segmentStartTime = segment.start;
if (segmentStartTime <= videoPlayerTime && segmentEndTime >= videoPlayerTime) { segmentStartISOTime = segment.rawProgramDateTime;
segmentDateObj = segment.dateTimeObject;
break; break;
} }
segmentStartTime = segmentEndTime;
} }
if (segmentDateObj === undefined) { if (segmentStartISOTime === undefined) {
return null; return null;
} }
let wubloaderDateTime = DateTime.fromJSDate(segmentDateObj, { zone: "utc" }); const wubloaderDateTime = DateTime.fromISO(segmentStartISOTime, { zone: "utc" });
const offset = videoPlayerTime - segmentStartTime; const offset = videoPlayerTime - segmentStartTime;
return wubloaderDateTime.plus({ seconds: offset }); return wubloaderDateTime.plus({ seconds: offset });
} }
@ -1097,10 +1068,6 @@ function wubloaderTimeFromVideoHumanTime(videoHumanTime) {
return wubloaderTimeFromVideoPlayerTime(videoPlayerTime); return wubloaderTimeFromVideoPlayerTime(videoPlayerTime);
} }
function getPlaylistData() { function getSegmentList() {
const player = getVideoJS(); return globalPlayer.latencyController.levelDetails.fragments;
// 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];
} }

File diff suppressed because one or more lines are too long

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

@ -93,7 +93,7 @@ function updateTimeSettings() {
const startTime = getStartTime(); const startTime = getStartTime();
const endTime = getEndTime(); const endTime = getEndTime();
if (endTime && endTime.diff(startTime) < 0) { if (endTime && endTime.diff(startTime).milliseconds < 0) {
addError( addError(
"End time is before the start time. This will prevent video loading and cause other problems." "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; 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 { #clip-bar {
width: 100%; width: 100%;
height: 7px; height: 7px;

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