You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
wubloader/thrimbletrimmer/scripts/edit.js

1069 lines
35 KiB
JavaScript

var googleUser = null;
var videoInfo;
var currentRange = 1;
window.addEventListener("DOMContentLoaded", async (event) => {
commonPageSetup();
const timeUpdateForm = document.getElementById("stream-time-settings");
timeUpdateForm.addEventListener("submit", (event) => {
event.preventDefault();
if (!videoInfo) {
addError(
"Time updates are ignored before the video metadata has been retrieved from Wubloader."
);
return;
}
const newStartField = document.getElementById("stream-time-setting-start");
const newStart = dateObjFromInputTime(newStartField.value);
if (!newStart) {
addError("Failed to parse start time");
return;
}
const newEndField = document.getElementById("stream-time-setting-end");
let newEnd = null;
if (newEndField.value !== "") {
newEnd = dateObjFromInputTime(newEndField.value);
if (!newEnd) {
addError("Failed to parse end time");
return;
}
}
const oldStart = getStartTime();
const startAdjustment = newStart - oldStart;
const newDuration = newEnd === null ? Infinity : newEnd - newStart;
// Abort for ranges that exceed new times
for (const rangeContainer of document.getElementById("range-definitions").children) {
const rangeStartField = rangeContainer.getElementsByClassName("range-definition-start")[0];
const rangeEndField = rangeContainer.getElementsByClassName("range-definition-end")[0];
const rangeStart = videoPlayerTimeFromVideoHumanTime(rangeStartField.value);
const rangeEnd = videoPlayerTimeFromVideoHumanTime(rangeEndField.value);
if (rangeStart !== null && rangeStart < startAdjustment) {
addError("The specified video load time excludes part of an edited clip range.");
return;
}
if (rangeEnd !== null && rangeEnd + startAdjustment > newDuration) {
addError("The specified video load time excludes part of an edited clip range.");
return;
}
}
globalStartTimeString = getWubloaderTimeFromDateWithMilliseconds(newStart);
globalEndTimeString = getWubloaderTimeFromDateWithMilliseconds(newEnd);
updateSegmentPlaylist();
for (const rangeContainer of document.getElementById("range-definitions").children) {
const rangeStartField = rangeContainer.getElementsByClassName("range-definition-start")[0];
const rangeEndField = rangeContainer.getElementsByClassName("range-definition-end")[0];
const rangeStart = videoPlayerTimeFromVideoHumanTime(rangeStartField.value);
if (rangeStart !== null) {
rangeStartField.value = videoHumanTimeFromVideoPlayerTime(startAdjustment + rangeStart);
}
const rangeEnd = videoPlayerTimeFromVideoHumanTime(rangeEndField.value);
if (rangeEnd !== null) {
rangeEndField.value = videoHumanTimeFromVideoPlayerTime(startAdjustment + rangeEnd);
}
}
const waveformImage = document.getElementById("waveform");
if (newEnd === null) {
waveformImage.classList.add("hidden");
} else {
updateWaveform();
waveformImage.classList.remove("hidden");
}
});
await loadVideoInfo();
updateDownloadLink();
document.getElementById("stream-time-setting-start-pad").addEventListener("click", (_event) => {
const startTimeField = document.getElementById("stream-time-setting-start");
let startTime = startTimeField.value;
startTime = parseInputTimeAsNumberOfSeconds(startTime);
if (isNaN(startTime)) {
addError("Couldn't parse entered start time for padding");
return;
}
startTime -= 60;
const startTimeDate = new Date(globalBusStartTime);
startTimeDate.setSeconds(startTimeDate.getSeconds() + startTime);
startTime = getBusTimeFromDateObj(startTimeDate);
startTimeField.value = startTime;
});
document.getElementById("stream-time-setting-end-pad").addEventListener("click", (_event) => {
const endTimeField = document.getElementById("stream-time-setting-end");
let endTime = endTimeField.value;
endTime = parseInputTimeAsNumberOfSeconds(endTime);
if (isNaN(endTime)) {
addError("Couldn't parse entered end time for padding");
return;
}
endTime += 60;
const endTimeDate = new Date(globalBusStartTime);
endTimeDate.setSeconds(endTimeDate.getSeconds() + endTime);
endTime = getBusTimeFromDateObj(endTimeDate);
endTimeField.value = endTime;
});
const addRangeIcon = document.getElementById("add-range-definition");
addRangeIcon.addEventListener("click", (_event) => {
addRangeDefinition();
});
addRangeIcon.addEventListener("keypress", (event) => {
if (event.key === "Enter") {
addRangeDefinition();
}
});
for (const rangeStartSet of document.getElementsByClassName("range-definition-set-start")) {
rangeStartSet.addEventListener("click", getRangeSetClickHandler("start"));
}
for (const rangeEndSet of document.getElementsByClassName("range-definition-set-end")) {
rangeEndSet.addEventListener("click", getRangeSetClickHandler("end"));
}
for (const rangeStart of document.getElementsByClassName("range-definition-start")) {
rangeStart.addEventListener("change", (_event) => {
rangeDataUpdated();
});
}
for (const rangeEnd of document.getElementsByClassName("range-definition-end")) {
rangeEnd.addEventListener("change", (_event) => {
rangeDataUpdated();
});
}
document.getElementById("submit-button").addEventListener("click", (_event) => {
submitVideo();
});
document.getElementById("save-button").addEventListener("click", (_event) => {
saveVideoDraft();
});
document.getElementById("advanced-submission").addEventListener("click", (_event) => {
const advancedOptionsContainer = document.getElementById("advanced-submission-options");
advancedOptionsContainer.classList.toggle("hidden");
});
document
.getElementById("advanced-submission-option-allow-holes")
.addEventListener("change", () => {
updateDownloadLink();
});
document.getElementById("download-type-select").addEventListener("change", () => {
updateDownloadLink();
});
document.getElementById("manual-link-update").addEventListener("click", (_event) => {
const manualLinkDataContainer = document.getElementById("data-correction-manual-link");
manualLinkDataContainer.classList.toggle("hidden");
});
document
.getElementById("data-correction-manual-link-submit")
.addEventListener("click", (_event) => {
setManualVideoLink();
});
document.getElementById("cancel-video-upload").addEventListener("click", (_event) => {
cancelVideoUpload();
});
document.getElementById("reset-entire-video").addEventListener("click", (_event) => {
const forceResetConfirmationContainer = document.getElementById(
"data-correction-force-reset-confirm"
);
forceResetConfirmationContainer.classList.remove("hidden");
});
document.getElementById("data-correction-force-reset-yes").addEventListener("click", (_event) => {
resetVideoRow();
});
document.getElementById("data-correction-force-reset-no").addEventListener("click", (_event) => {
const forceResetConfirmationContainer = document.getElementById(
"data-correction-force-reset-confirm"
);
forceResetConfirmationContainer.classList.add("hidden");
});
document.getElementById("google-auth-sign-out").addEventListener("click", (_event) => {
googleSignOut();
});
});
async function loadVideoInfo() {
const queryParams = new URLSearchParams(window.location.search);
if (!queryParams.has("id")) {
addError("No video ID specified. Failed to load video data.");
return;
}
const videoID = queryParams.get("id");
const dataResponse = await fetch("/thrimshim/" + videoID);
if (!dataResponse.ok) {
addError(
"Failed to load video data. This probably means that the URL is out of date (video ID changed) or that everything is broken (or that the Wubloader host is down)."
);
return;
}
videoInfo = await dataResponse.json();
initializeVideoInfo();
}
async function initializeVideoInfo() {
globalStreamName = videoInfo.video_channel;
globalBusStartTime = new Date(videoInfo.bustime_start);
const eventStartTime = dateObjFromWubloaderTime(videoInfo.event_start);
const eventEndTime = dateObjFromWubloaderTime(videoInfo.event_end);
// To account for various things (stream delay, just slightly off logging, etc.), we pad the start time by one minute
eventStartTime.setMinutes(eventStartTime.getMinutes() - 1);
// To account for various things (stream delay, just slightly off logging, etc.), we pad the end time by one minute.
// To account for the fact that we don't record seconds, but the event could've ended any time in the recorded minute, we pad by an additional minute.
eventEndTime.setMinutes(eventEndTime.getMinutes() + 2);
globalStartTimeString = getWubloaderTimeFromDateWithMilliseconds(eventStartTime);
globalEndTimeString = getWubloaderTimeFromDateWithMilliseconds(eventEndTime);
// If a video was previously edited to points outside the video range, we should expand the loaded video to include the edited range
if (videoInfo.video_start) {
const videoStartTime = dateObjFromWubloaderTime(videoInfo.video_start);
if (videoStartTime < eventStartTime) {
videoStartTime.setMinutes(videoStartTime.getMinutes() - 1);
globalStartTimeString = getWubloaderTimeFromDateWithMilliseconds(videoStartTime);
}
}
if (videoInfo.video_end) {
const videoEndTime = dateObjFromWubloaderTime(videoInfo.video_end);
if (videoEndTime > eventEndTime) {
// If we're getting the time from a previous draft edit, we don't need to pad as hard on the end
videoEndTime.setMinutes(videoEndTime.getMinutes() + 1);
globalEndTimeString = getWubloaderTimeFromDateWithMilliseconds(videoEndTime);
}
}
document.getElementById("stream-time-setting-stream").innerText = globalStreamName;
document.getElementById("stream-time-setting-start").value =
getBusTimeFromTimeString(globalStartTimeString);
document.getElementById("stream-time-setting-end").value =
getBusTimeFromTimeString(globalEndTimeString);
updateWaveform();
const titlePrefixElem = document.getElementById("video-info-title-prefix");
titlePrefixElem.innerText = videoInfo.title_prefix;
const titleElem = document.getElementById("video-info-title");
if (videoInfo.video_title) {
titleElem.value = videoInfo.video_title;
} else {
titleElem.value = videoInfo.description;
}
const descriptionElem = document.getElementById("video-info-description");
if (videoInfo.video_description) {
descriptionElem.value = videoInfo.video_description;
} else {
descriptionElem.value = videoInfo.description;
}
const tagsElem = document.getElementById("video-info-tags");
if (videoInfo.video_tags) {
tagsElem.value = videoInfo.video_tags.join(",");
} else {
tagsElem.value = videoInfo.tags.join(",");
}
if (videoInfo.notes) {
const notesTextElem = document.getElementById("video-info-editor-notes");
notesTextElem.innerText = videoInfo.notes;
const notesContainer = document.getElementById("video-info-editor-notes-container");
notesContainer.classList.remove("hidden");
}
let modifiedAdvancedOptions = false;
if (videoInfo.allow_holes) {
const allowHolesCheckbox = document.getElementById("advanced-submission-option-allow-holes");
allowHolesCheckbox.checked = true;
modifiedAdvancedOptions = true;
}
const uploadLocationSelection = document.getElementById(
"advanced-submission-option-upload-location"
);
for (locationName of videoInfo.upload_locations) {
const option = document.createElement("option");
option.value = locationName;
option.innerText = locationName;
if (videoInfo.upload_location === locationName) {
option.selected = true;
}
uploadLocationSelection.appendChild(option);
}
if (uploadLocationSelection.options.selectedIndex > 0) {
modifiedAdvancedOptions = true;
}
if (videoInfo.uploader_whitelist) {
modifiedAdvancedOptions = true;
const uploaderAllowlistBox = document.getElementById(
"advanced-submission-option-uploader-allow"
);
uploaderAllowlistBox.value = videoInfo.uploader_whitelist.join(",");
}
if (modifiedAdvancedOptions) {
const advancedSubmissionContainer = document.getElementById("advanced-submission-options");
advancedSubmissionContainer.classList.remove("hidden");
}
await loadVideoPlayerFromDefaultPlaylist();
const player = getVideoJS();
player.on("loadedmetadata", () => {
// For now, there's only one range in the data we receive from thrimshim, so we'll populate that as-is here.
// This will need to be updated when thrimshim supports multiple video ranges.
const rangeDefinitionsContainer = document.getElementById("range-definitions");
const rangeDefinitionStart =
rangeDefinitionsContainer.getElementsByClassName("range-definition-start")[0];
const rangeDefinitionEnd =
rangeDefinitionsContainer.getElementsByClassName("range-definition-end")[0];
rangeDefinitionStart.addEventListener("change", (_event) => {
rangeDataUpdated();
});
rangeDefinitionEnd.addEventListener("change", (_event) => {
rangeDataUpdated();
});
if (videoInfo.video_start) {
rangeDefinitionStart.value = videoHumanTimeFromWubloaderTime(videoInfo.video_start);
} else {
rangeDefinitionStart.value = videoHumanTimeFromVideoPlayerTime(0);
}
if (videoInfo.video_end) {
rangeDefinitionEnd.value = videoHumanTimeFromWubloaderTime(videoInfo.video_end);
} else {
const player = getVideoJS();
rangeDefinitionEnd.value = videoHumanTimeFromVideoPlayerTime(player.duration());
}
if (videoInfo.video_start && videoInfo.video_end) {
rangeDataUpdated();
}
});
}
function updateWaveform() {
let waveformURL = "/waveform/" + globalStreamName + "/" + videoInfo.video_quality + ".png?";
const queryStringParts = startAndEndTimeQueryStringParts();
waveformURL += queryStringParts.join("&");
const waveformElem = document.getElementById("waveform");
waveformElem.src = waveformURL;
}
function googleOnSignIn(googleUserData) {
googleUser = googleUserData;
const signInElem = document.getElementById("google-auth-sign-in");
const signOutElem = document.getElementById("google-auth-sign-out");
signInElem.classList.add("hidden");
signOutElem.classList.remove("hidden");
}
async function googleSignOut() {
if (googleUser) {
googleUser = null;
await gapi.auth2.getAuthInstance().signOut();
const signInElem = document.getElementById("google-auth-sign-in");
const signOutElem = document.getElementById("google-auth-sign-out");
signInElem.classList.remove("hidden");
signOutElem.classList.add("hidden");
}
}
function getStartTime() {
if (!globalStartTimeString) {
return null;
}
return dateObjFromWubloaderTime(globalStartTimeString);
}
function getEndTime() {
if (!globalEndTimeString) {
return null;
}
return dateObjFromWubloaderTime(globalEndTimeString);
}
function getBusTimeFromTimeString(timeString) {
const time = dateObjFromWubloaderTime(timeString);
return getBusTimeFromDateObj(time);
}
function getBusTimeFromDateObj(time) {
const busTimeMilliseconds = time - globalBusStartTime;
let remainingBusTimeSeconds = busTimeMilliseconds / 1000;
const hours = Math.floor(remainingBusTimeSeconds / 3600);
remainingBusTimeSeconds %= 3600;
let minutes = Math.floor(remainingBusTimeSeconds / 60);
let seconds = remainingBusTimeSeconds % 60;
let milliseconds = Math.round((seconds % 1) * 1000);
seconds = Math.trunc(seconds);
while (minutes.toString().length < 2) {
minutes = `0${minutes}`;
}
while (seconds.toString().length < 2) {
seconds = `0${seconds}`;
}
if (milliseconds > 0) {
while (milliseconds.toString().length < 3) {
milliseconds = `0${milliseconds}`;
}
return `${hours}:${minutes}:${seconds}.${milliseconds}`;
}
return `${hours}:${minutes}:${seconds}`;
}
async function submitVideo() {
return sendVideoData(true, false);
}
async function saveVideoDraft() {
return sendVideoData(false, false);
}
async function sendVideoData(edited, overrideChanges) {
const submissionResponseElem = document.getElementById("submission-response");
submissionResponseElem.classList.value = ["submission-response-pending"];
submissionResponseElem.innerText = "Submitting video...";
const rangesData = [];
for (const rangeContainer of document.getElementById("range-definitions").children) {
const rangeStart = rangeContainer.getElementsByClassName("range-definition-start")[0].value;
const rangeEnd = rangeContainer.getElementsByClassName("range-definition-end")[0].value;
const rangeStartSubmit = wubloaderTimeFromVideoHumanTime(rangeStart);
const rangeEndSubmit = wubloaderTimeFromVideoHumanTime(rangeEnd);
if (edited && (!rangeStartSubmit || !rangeEndSubmit)) {
submissionResponseElem.classList.value = ["submission-response-error"];
let errorMessage;
if (!rangeStartSubmit && !rangeEndSubmit) {
errorMessage = `The range endpoints "${rangeStartSubmit}" and "${rangeEndSubmit}" are not valid.`;
} else if (!rangeStartSubmit) {
errorMessage = `The range endpoint "${rangeStartSubmit} is not valid.`;
} else {
errorMessage = `The range endpoint "${rangeEndSubmit}" is not valid.`;
}
submissionResponseElem.innerText = errorMessage;
return;
}
let transitionType = rangeContainer.getElementsByClassName("range-definition-transition-type");
let transitionDuration = rangeContainer.getElementsByClassName(
"range-definition-transition-duration"
);
if (transitionType.length > 0 && transitionDuration.length > 0) {
transitionType = transitionType[0].value;
transitionDuration = transitionDuration[0].value;
if (edited && transitionType !== "" && transitionDuration === "") {
submissionResponseElem.classList.value = ["submission-response-error"];
submissionResponseElem.innerText = "A non-cut transition was specified with no duration";
return;
}
} else {
transitionType = null;
transitionDuration = null;
}
rangesData.push({
start: rangeStartSubmit,
end: rangeEndSubmit,
transition: transitionType,
duration: transitionDuration,
});
}
// Currently this only supports one range. When multiple ranges are supported, expand this.
const videoStart = rangesData[0].start;
const videoEnd = rangesData[0].end;
const videoTitle = document.getElementById("video-info-title").value;
const videoDescription = document.getElementById("video-info-description").value;
const videoTags = document.getElementById("video-info-tags").value.split(",");
const allowHoles = document.getElementById("advanced-submission-option-allow-holes").checked;
const uploadLocation = document.getElementById(
"advanced-submission-option-upload-location"
).value;
const uploaderAllowlistValue = document.getElementById(
"advanced-submission-option-uploader-allow"
).value;
const uploaderAllowlist = uploaderAllowlistValue ? uploaderAllowlistValue.split(",") : null;
const state = edited ? "EDITED" : "UNEDITED";
const editData = {
video_start: videoStart,
video_end: videoEnd,
video_title: videoTitle,
video_description: videoDescription,
video_tags: videoTags,
allow_holes: allowHoles,
upload_location: uploadLocation,
video_channel: globalStreamName,
video_quality: videoInfo.video_quality,
uploader_whitelist: uploaderAllowlist,
state: state,
// We also provide some sheet column values to verify data hasn't changed.
sheet_name: videoInfo.sheet_name,
event_start: videoInfo.event_start,
event_end: videoInfo.event_end,
category: videoInfo.category,
description: videoInfo.description,
notes: videoInfo.notes,
tags: videoInfo.tags,
};
if (googleUser) {
editData.token = googleUser.getAuthResponse().id_token;
}
if (overrideChanges) {
editData.overrideChanges = true;
}
const submitResponse = await fetch(`/thrimshim/${videoInfo.id}`, {
method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
},
body: JSON.stringify(editData),
});
if (submitResponse.ok) {
submissionResponseElem.classList.value = ["submission-response-success"];
if (edited) {
submissionResponseElem.innerText = `Submitted edit from ${videoStart} to ${videoEnd}`;
} else {
submissionResponseElem.innerText = "Saved draft";
}
} else {
submissionResponseElem.classList.value = ["submission-response-error"];
if (submitResponse.status === 409) {
const serverErrorNode = document.createTextNode(await submitResponse.text());
const submitButton = document.createElement("button");
submitButton.text = "Submit Anyway";
submitButton.addEventListener("click", (_event) => {
sendVideoData(edited, true);
});
submissionResponseElem.innerHTML = "";
submissionResponseElem.appendChild(serverErrorNode);
submissionResponseElem.appendChild(submitButton);
} else if (submitResponse.status === 401) {
submissionResponseElem.innerText = "Unauthorized. Did you remember to sign in?";
} else {
submissionResponseElem.innerText = `${
submitResponse.statusText
}: ${await submitResponse.text()}`;
}
}
}
function updateDownloadLink() {
// Currently this only supports one range. When download links can download multiple ranges, this should be updated.
const firstRangeStartField = document.getElementsByClassName("range-definition-start")[0];
const firstRangeEndField = document.getElementsByClassName("range-definition-end")[0];
const startTime = firstRangeStartField.value
? wubloaderTimeFromVideoHumanTime(firstRangeStartField.value)
: getStartTime();
const endTime = firstRangeEndField.value
? wubloaderTimeFromVideoHumanTime(firstRangeEndField.value)
: getEndTime();
const downloadType = document.getElementById("download-type-select").value;
const allowHoles = document.getElementById("advanced-submission-option-allow-holes").checked;
const downloadURL = generateDownloadURL(
startTime,
endTime,
downloadType,
allowHoles,
videoInfo.video_quality
);
document.getElementById("download-link").href = downloadURL;
}
async function setManualVideoLink() {
let uploadLocation;
if (document.getElementById("data-correction-manual-link-youtube").checked) {
uploadLocation = "youtube-manual";
} else {
uploadLocation = "manual";
}
const link = document.getElementById("data-correction-manual-link-entry").value;
const request = {
link: link,
upload_location: uploadLocation,
};
if (googleUser) {
request.token = googleUser.getAuthResponse().id_token;
}
const responseElem = document.getElementById("data-correction-manual-link-response");
responseElem.innerText = "Submitting link...";
const response = await fetch(`/thrimshim/manual-link/${videoInfo.id}`, {
method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
},
body: JSON.stringify(request),
});
if (response.ok) {
responseElem.innerText = `Manual link set to ${link}`;
} else {
responseElem.innerText = `${response.statusText}: ${await response.text()}`;
}
}
async function cancelVideoUpload() {
const request = {};
if (googleUser) {
request.token = googleUser.getAuthResponse().id_token;
}
const responseElem = document.getElementById("data-correction-cancel-response");
responseElem.innerText = "Submitting cancel request...";
const response = await fetch(`/thrimshim/reset/${videoInfo.id}?force=false`, {
method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
},
body: JSON.stringify(request),
});
if (response.ok) {
responseElem.innerText = "Row has been cancelled. Reloading...";
setTimeout(() => {
window.location.reload();
}, 1000);
} else {
responseElem.innerText = `${response.statusText}: ${await response.text()}`;
}
}
async function resetVideoRow() {
const request = {};
if (googleUser) {
request.token = googleUser.getAuthResponse().id_token;
}
const responseElem = document.getElementById("data-correction-cancel-response");
responseElem.innerText = "Submitting reset request...";
const response = await fetch(`/thrimshim/reset/${videoInfo.id}?force=true`, {
method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
},
body: JSON.stringify(request),
});
if (response.ok) {
responseElem.innerText = "Row has been reset. Reloading...";
setTimeout(() => {
window.location.reload();
}, 1000);
} else {
responseElem.innerText = `${response.statusText}: ${await response.text()}`;
}
}
function addRangeDefinition() {
const newRangeDOM = rangeDefinitionDOM();
const rangeContainer = document.getElementById("range-definitions");
rangeContainer.appendChild(newRangeDOM);
}
const RANGE_TRANSITION_TYPES = [
"fade",
"wipeleft",
"wiperight",
"wipeup",
"wipedown",
"slideleft",
"slideright",
"slideup",
"slidedown",
"circlecrop",
"rectcrop",
"distance",
"fadeblack",
"fadewhite",
"radial",
"smoothleft",
"smoothright",
"smoothup",
"smoothdown",
"circleopen",
"circleclose",
"vertopen",
"vertclose",
"horzopen",
"horzclose",
"dissolve",
"pixelize",
"diagtl",
"diagtr",
"diagbl",
"diagbr",
"hlslice",
"hrslice",
"vuslice",
"vdslice",
"hblur",
"fadegrays",
"wipetl",
"wipetr",
"wipebl",
"wipebr",
"squeezeh",
"squeezev",
];
function rangeDefinitionDOM() {
const container = document.createElement("div");
container.classList.add("range-definition-removable");
const transitionContainer = document.createElement("div");
transitionContainer.classList.add("range-definition-transition");
const transitionSelection = document.createElement("select");
transitionSelection.classList.add("range-definition-transition-type");
const noTransitionOption = document.createElement("option");
noTransitionOption.value = "";
noTransitionOption.innerText = "No transition (hard cut)";
transitionSelection.appendChild(noTransitionOption);
for (transitionType of RANGE_TRANSITION_TYPES) {
const transitionOption = document.createElement("option");
transitionOption.value = transitionType;
transitionOption.innerText = transitionType;
transitionSelection.appendChild(transitionOption);
}
transitionSelection.addEventListener("change", (_event) => {
rangeDataUpdated();
});
transitionContainer.appendChild(transitionSelection);
const transitionDurationInput = document.createElement("input");
transitionDurationInput.type = "number";
transitionDurationInput.min = 0;
transitionDurationInput.step = "any";
transitionDurationInput.placeholder = "Duration (seconds)";
transitionDurationInput.classList.add("range-definition-transition-duration");
transitionContainer.appendChild(transitionDurationInput);
container.appendChild(transitionContainer);
const rangeContainer = document.createElement("div");
rangeContainer.classList.add("range-definition-times");
const rangeStart = document.createElement("input");
rangeStart.type = "text";
rangeStart.classList.add("range-definition-start");
const rangeStartSet = document.createElement("img");
rangeStartSet.src = "images/pencil.png";
rangeStartSet.alt = "Set range start point to current video time";
rangeStartSet.classList.add("range-definition-set-start");
rangeStartSet.classList.add("click");
const rangeTimeGap = document.createElement("div");
rangeTimeGap.classList.add("range-definition-between-time-gap");
const rangeEnd = document.createElement("input");
rangeEnd.type = "text";
rangeEnd.classList.add("range-definition-end");
const rangeEndSet = document.createElement("img");
rangeEndSet.src = "images/pencil.png";
rangeEndSet.alt = "Set range end point to current video time";
rangeEndSet.classList.add("range-definition-set-end");
rangeEndSet.classList.add("click");
const removeRange = document.createElement("img");
removeRange.alt = "Remove range";
removeRange.src = "images/minus.png";
removeRange.classList.add("range-definition-remove");
removeRange.classList.add("click");
rangeStartSet.addEventListener("click", getRangeSetClickHandler("start"));
rangeEndSet.addEventListener("click", getRangeSetClickHandler("end"));
removeRange.addEventListener("click", (event) => {
let rangeContainer = event.currentTarget;
while (rangeContainer && !rangeContainer.classList.contains("range-definition-removable")) {
rangeContainer = rangeContainer.parentElement;
}
if (rangeContainer) {
const rangeParent = rangeContainer.parentNode;
for (let rangeNum = 0; rangeNum < rangeParent.children.length; rangeNum++) {
if (rangeContainer === rangeParent.children[rangeNum]) {
if (rangeNum + 1 <= currentRange) {
// currentRange is 1-indexed to index into DOM with querySelector
currentRange--;
break;
}
}
}
rangeParent.removeChild(rangeContainer);
updateCurrentRangeIndicator();
rangeDataUpdated();
}
});
const currentRangeMarker = document.createElement("img");
currentRangeMarker.alt = "Range affected by keyboard shortcuts";
currentRangeMarker.title = "Range affected by keyboard shortcuts";
currentRangeMarker.src = "images/arrow.png";
currentRangeMarker.classList.add("range-definition-current");
currentRangeMarker.classList.add("hidden");
rangeContainer.appendChild(rangeStart);
rangeContainer.appendChild(rangeStartSet);
rangeContainer.appendChild(rangeTimeGap);
rangeContainer.appendChild(rangeEnd);
rangeContainer.appendChild(rangeEndSet);
rangeContainer.appendChild(removeRange);
rangeContainer.appendChild(currentRangeMarker);
container.appendChild(rangeContainer);
return container;
}
function getRangeSetClickHandler(startOrEnd) {
return function (event) {
const setButton = event.currentTarget;
const setField = setButton.parentElement.getElementsByClassName(
`range-definition-${startOrEnd}`
)[0];
const player = getVideoJS();
const videoPlayerTime = player.currentTime();
setField.value = videoHumanTimeFromVideoPlayerTime(videoPlayerTime);
};
}
function moveToNextRange() {
currentRange++;
if (currentRange > document.getElementById("range-definitions").children.length) {
addRangeDefinition();
}
updateCurrentRangeIndicator();
}
function moveToPreviousRange() {
if (currentRange <= 1) {
return;
}
currentRange--;
updateCurrentRangeIndicator();
}
function updateCurrentRangeIndicator() {
for (let arrowElem of document.getElementsByClassName("range-definition-current")) {
arrowElem.classList.add("hidden");
}
document
.querySelector(`#range-definitions > div:nth-child(${currentRange}) .range-definition-current`)
.classList.remove("hidden");
}
function rangeDataUpdated() {
const clipBar = document.getElementById("clip-bar");
clipBar.innerHTML = "";
const player = getVideoJS();
const videoDuration = player.duration();
for (let rangeDefinition of document.getElementById("range-definitions").children) {
const rangeStartField = rangeDefinition.getElementsByClassName("range-definition-start")[0];
const rangeEndField = rangeDefinition.getElementsByClassName("range-definition-end")[0];
const rangeStart = videoPlayerTimeFromVideoHumanTime(rangeStartField.value);
const rangeEnd = videoPlayerTimeFromVideoHumanTime(rangeEndField.value);
if (rangeStart === null || rangeEnd === null) {
continue;
}
const rangeStartPercentage = (rangeStart / videoDuration) * 100;
const rangeEndPercentage = (rangeEnd / videoDuration) * 100;
const widthPercentage = rangeEndPercentage - rangeStartPercentage;
const marker = document.createElement("div");
marker.style.width = `${widthPercentage}%`;
marker.style.left = `${rangeStartPercentage}%`;
clipBar.appendChild(marker);
}
updateDownloadLink();
}
function setCurrentRangeStartToVideoTime() {
const rangeStartField = document.querySelector(
`#range-definitions > div:nth-child(${currentRange}) .range-definition-start`
);
const player = getVideoJS();
rangeStartField.value = videoHumanTimeFromVideoPlayerTime(player.currentTime());
rangeDataUpdated();
}
function setCurrentRangeEndToVideoTime() {
const rangeEndField = document.querySelector(
`#range-definitions > div:nth-child(${currentRange}) .range-definition-end`
);
const player = getVideoJS();
rangeEndField.value = videoHumanTimeFromVideoPlayerTime(player.currentTime());
rangeDataUpdated();
}
function videoPlayerTimeFromWubloaderTime(wubloaderTime) {
const videoPlaylist = getPlaylistData();
const wubloaderDateObj = dateObjFromWubloaderTime(wubloaderTime);
let highestDiscontinuitySegmentBefore = 0;
for (start of videoPlaylist.discontinuityStarts) {
const discontinuityStartSegment = videoPlaylist.segments[start];
if (
discontinuityStartSegment.dateTimeObject < wubloaderDateObj &&
discontinuityStartSegment.dateTimeObject >
videoPlaylist.segments[highestDiscontinuitySegmentBefore].dateTimeObject
) {
highestDiscontinuitySegmentBefore = start;
}
}
let highestDiscontinuitySegmentStart = 0;
for (let segment = 0; segment < highestDiscontinuitySegmentBefore; segment++) {
highestDiscontinuitySegmentStart += videoPlaylist.segments[segment].duration;
}
return (
highestDiscontinuitySegmentStart +
secondsDifference(
videoPlaylist.segments[highestDiscontinuitySegmentBefore].dateTimeObject,
wubloaderDateObj
)
);
}
function wubloaderTimeFromVideoPlayerTime(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;
break;
}
segmentStartTime = segmentEndTime;
}
if (segmentDateObj === undefined) {
return null;
}
let wubloaderDateObj = new Date(segmentDateObj);
const offset = videoPlayerTime - segmentStartTime;
const offsetMilliseconds = offset * 1000;
wubloaderDateObj.setMilliseconds(wubloaderDateObj.getMilliseconds() + offsetMilliseconds);
return wubloaderDateObj;
}
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);
}
function wubloaderTimeFromVideoHumanTime(videoHumanTime) {
const videoPlayerTime = videoPlayerTimeFromVideoHumanTime(videoHumanTime);
if (videoPlayerTime === null) {
return null;
}
return wubloaderTimeFromVideoPlayerTime(videoPlayerTime);
}
function dateObjFromInputTime(inputTime) {
const inputSeconds = parseInputTimeAsNumberOfSeconds(inputTime);
if (isNaN(inputSeconds)) {
return null;
}
return new Date(globalBusStartTime.getTime() + 1000 * inputSeconds);
}
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 secondsDifference(date1, date2) {
if (date2 > date1) {
return (date2 - date1) / 1000;
}
return (date1 - date2) / 1000;
}