@@ -274,23 +276,41 @@
id="video-info-thumbnail-template-video-source-image"
class="hidden"
alt="Thumbnail preview image"
- height="360" width="640"
+ height="360"
+ width="640"
/>
-
+
Crop:
-
-
+
+
to
-
-
-
+
+
+
-
{
}
}
- document.getElementById("range-definition-chapter-marker-first-description").addEventListener("input", (event) => {
- validateChapterDescription(event.target);
- });
+ document
+ .getElementById("range-definition-chapter-marker-first-description")
+ .addEventListener("input", (event) => {
+ validateChapterDescription(event.target);
+ });
document.getElementById("video-info-title").addEventListener("input", (event) => {
validateVideoTitle();
document.getElementById("video-info-title-abbreviated").innerText =
@@ -269,7 +271,9 @@ window.addEventListener("DOMContentLoaded", async (event) => {
document
.getElementById("video-info-thumbnail-template-source-image-update")
.addEventListener("click", async (_event) => {
- const videoFrameImageElement = document.getElementById("video-info-thumbnail-template-video-source-image");
+ const videoFrameImageElement = document.getElementById(
+ "video-info-thumbnail-template-video-source-image",
+ );
const timeEntryElement = document.getElementById("video-info-thumbnail-time");
const imageTime = wubloaderTimeFromVideoHumanTime(timeEntryElement.value);
@@ -285,55 +289,84 @@ window.addEventListener("DOMContentLoaded", async (event) => {
videoFrameImageElement.src = `/frame/${globalStreamName}/source.png?${videoFrameQuery}`;
videoFrameImageElement.classList.remove("hidden");
- const templateImageElement = document.getElementById("video-info-thumbnail-template-overlay-image");
+ const templateImageElement = document.getElementById(
+ "video-info-thumbnail-template-overlay-image",
+ );
templateImageElement.src = `/thrimshim/template/${imageTemplate}.png`;
templateImageElement.classList.remove("hidden");
- const aspectRatioControls = document.getElementById("video-info-thumbnail-aspect-ratio-controls");
+ const aspectRatioControls = document.getElementById(
+ "video-info-thumbnail-aspect-ratio-controls",
+ );
aspectRatioControls.classList.remove("hidden");
createTemplateCropWidgets();
});
- document.getElementById("video-info-thumbnail-crop-0").addEventListener("input", updateTemplateCropWidgets);
- document.getElementById("video-info-thumbnail-crop-1").addEventListener("input", updateTemplateCropWidgets);
- document.getElementById("video-info-thumbnail-crop-2").addEventListener("input", updateTemplateCropWidgets);
- document.getElementById("video-info-thumbnail-crop-3").addEventListener("input", updateTemplateCropWidgets);
- document.getElementById("video-info-thumbnail-location-0").addEventListener("input", updateTemplateCropWidgets);
- document.getElementById("video-info-thumbnail-location-1").addEventListener("input", updateTemplateCropWidgets);
- document.getElementById("video-info-thumbnail-location-2").addEventListener("input", updateTemplateCropWidgets);
- document.getElementById("video-info-thumbnail-location-3").addEventListener("input", updateTemplateCropWidgets);
-
- document.getElementById("video-info-thumbnail-lock-aspect-ratio").addEventListener("change", updateTemplateCropAspectRatio);
-
- document.getElementById("video-info-thumbnail-aspect-ratio-match-right").addEventListener("click", function(){
- // Calculate and copy the aspect ratio from the video field to the template
- const videoFieldX1 = document.getElementById("video-info-thumbnail-crop-0");
- const videoFieldY1 = document.getElementById("video-info-thumbnail-crop-1");
- const videoFieldX2 = document.getElementById("video-info-thumbnail-crop-2");
- const videoFieldY2 = document.getElementById("video-info-thumbnail-crop-3");
- const videoFieldAspectRatio = (videoFieldX2.value-videoFieldX1.value)/(videoFieldY2.value-videoFieldY1.value);
-
- templateStage.setOptions({aspectRatio: videoFieldAspectRatio});
-
- // Re-apply the locked/unlocked status
- updateTemplateCropAspectRatio();
- });
+ document
+ .getElementById("video-info-thumbnail-crop-0")
+ .addEventListener("input", updateTemplateCropWidgets);
+ document
+ .getElementById("video-info-thumbnail-crop-1")
+ .addEventListener("input", updateTemplateCropWidgets);
+ document
+ .getElementById("video-info-thumbnail-crop-2")
+ .addEventListener("input", updateTemplateCropWidgets);
+ document
+ .getElementById("video-info-thumbnail-crop-3")
+ .addEventListener("input", updateTemplateCropWidgets);
+ document
+ .getElementById("video-info-thumbnail-location-0")
+ .addEventListener("input", updateTemplateCropWidgets);
+ document
+ .getElementById("video-info-thumbnail-location-1")
+ .addEventListener("input", updateTemplateCropWidgets);
+ document
+ .getElementById("video-info-thumbnail-location-2")
+ .addEventListener("input", updateTemplateCropWidgets);
+ document
+ .getElementById("video-info-thumbnail-location-3")
+ .addEventListener("input", updateTemplateCropWidgets);
- document.getElementById("video-info-thumbnail-aspect-ratio-match-left").addEventListener("click", function(){
- // Calculate and copy the aspect ratio from the template to the video field
- const templateFieldX1 = document.getElementById("video-info-thumbnail-location-0");
- const templateFieldY1 = document.getElementById("video-info-thumbnail-location-1");
- const templateFieldX2 = document.getElementById("video-info-thumbnail-location-2");
- const templateFieldY2 = document.getElementById("video-info-thumbnail-location-3");
- const templateFieldAspectRatio = (templateFieldX2.value-templateFieldX1.value)/(templateFieldY2.value-templateFieldY1.value);
+ document
+ .getElementById("video-info-thumbnail-lock-aspect-ratio")
+ .addEventListener("change", updateTemplateCropAspectRatio);
- videoFrameStage.setOptions({aspectRatio: templateFieldAspectRatio});
+ document
+ .getElementById("video-info-thumbnail-aspect-ratio-match-right")
+ .addEventListener("click", function () {
+ // Calculate and copy the aspect ratio from the video field to the template
+ const videoFieldX1 = document.getElementById("video-info-thumbnail-crop-0");
+ const videoFieldY1 = document.getElementById("video-info-thumbnail-crop-1");
+ const videoFieldX2 = document.getElementById("video-info-thumbnail-crop-2");
+ const videoFieldY2 = document.getElementById("video-info-thumbnail-crop-3");
+ const videoFieldAspectRatio =
+ (videoFieldX2.value - videoFieldX1.value) / (videoFieldY2.value - videoFieldY1.value);
+
+ templateStage.setOptions({ aspectRatio: videoFieldAspectRatio });
+
+ // Re-apply the locked/unlocked status
+ updateTemplateCropAspectRatio();
+ });
- // Re-apply the locked/unlocked status
- updateTemplateCropAspectRatio();
- });
+ document
+ .getElementById("video-info-thumbnail-aspect-ratio-match-left")
+ .addEventListener("click", function () {
+ // Calculate and copy the aspect ratio from the template to the video field
+ const templateFieldX1 = document.getElementById("video-info-thumbnail-location-0");
+ const templateFieldY1 = document.getElementById("video-info-thumbnail-location-1");
+ const templateFieldX2 = document.getElementById("video-info-thumbnail-location-2");
+ const templateFieldY2 = document.getElementById("video-info-thumbnail-location-3");
+ const templateFieldAspectRatio =
+ (templateFieldX2.value - templateFieldX1.value) /
+ (templateFieldY2.value - templateFieldY1.value);
+
+ videoFrameStage.setOptions({ aspectRatio: templateFieldAspectRatio });
+
+ // Re-apply the locked/unlocked status
+ updateTemplateCropAspectRatio();
+ });
document
.getElementById("video-info-thumbnail-template-preview-generate")
@@ -375,7 +408,7 @@ window.addEventListener("DOMContentLoaded", async (event) => {
const thumbnailTemplatesListResponse = await fetch("/thrimshim/templates");
if (thumbnailTemplatesListResponse.ok) {
const thumbnailTemplatesList = await thumbnailTemplatesListResponse.json();
- const templateNames = thumbnailTemplatesList.map(t => t.name);
+ const templateNames = thumbnailTemplatesList.map((t) => t.name);
templateNames.sort();
for (const template of thumbnailTemplatesList) {
thumbnailTemplates[template.name] = template;
@@ -401,7 +434,8 @@ window.addEventListener("DOMContentLoaded", async (event) => {
}
if (videoInfo.thumbnail_location !== null) {
for (let i = 0; i < 4; i++) {
- document.getElementById(`video-info-thumbnail-location-${i}`).value = videoInfo.thumbnail_location[i];
+ document.getElementById(`video-info-thumbnail-location-${i}`).value =
+ videoInfo.thumbnail_location[i];
}
}
document.getElementById("video-info-thumbnail-mode").value = videoInfo.thumbnail_mode;
@@ -479,11 +513,12 @@ window.addEventListener("DOMContentLoaded", async (event) => {
});
});
-
async function loadTransitions() {
const response = await fetch("/thrimshim/transitions");
if (!response.ok) {
- addError("Failed to fetch possible transition types. This probably means the wubloader host is down.");
+ addError(
+ "Failed to fetch possible transition types. This probably means the wubloader host is down.",
+ );
return;
}
knownTransitions = await response.json();
@@ -776,8 +811,12 @@ async function initializeVideoInfo() {
if (rangeIndex > 0) {
const transition = videoInfo.video_transitions[rangeIndex - 1];
const transitionType = rangeContainer.getElementsByClassName("range-transition-type")[0];
- const transitionDuration = rangeContainer.getElementsByClassName("range-transition-duration")[0];
- const transitionDurationSection = rangeContainer.getElementsByClassName("range-transition-duration-section")[0];
+ const transitionDuration = rangeContainer.getElementsByClassName(
+ "range-transition-duration",
+ )[0];
+ const transitionDurationSection = rangeContainer.getElementsByClassName(
+ "range-transition-duration-section",
+ )[0];
if (transition === null) {
transitionType.value = "";
transitionDuration.value = "";
@@ -796,7 +835,7 @@ async function initializeVideoInfo() {
const option = document.createElement("option");
option.value = type;
option.textContent = type;
- transitionType.append(option)
+ transitionType.append(option);
}
// Set type and duration.
transitionType.value = type;
@@ -1062,11 +1101,13 @@ function validateChapterDescription(chapterDescField) {
if (chapterDesc.indexOf("<") !== -1 || chapterDesc.indexOf(">") !== -1) {
chapterDescField.classList.add("input-error");
chapterDescField.title = "Chapter description may not contain angle brackets (< or >)";
- } else if (Array.from(chapterDesc).some(c => c.charCodeAt(0) > 127)) { // any char is non-ascii
+ } else if (Array.from(chapterDesc).some((c) => c.charCodeAt(0) > 127)) {
+ // any char is non-ascii
// We don't know what chars are safe outside the ascii range, so we just warn on any of them.
// We know emoji are not safe.
chapterDescField.classList.add("input-error");
- chapterDescField.title = "Chapter descriptions with non-ascii characters may cause issues; proceed with caution";
+ chapterDescField.title =
+ "Chapter descriptions with non-ascii characters may cause issues; proceed with caution";
} else {
chapterDescField.classList.remove("input-error");
chapterDescField.title = "";
@@ -1115,7 +1156,9 @@ async function sendVideoData(newState, overrideChanges) {
const transitionTypeElements = rangeContainer.getElementsByClassName("range-transition-type");
if (transitionTypeElements.length > 0) {
const transitionType = transitionTypeElements[0].value;
- const transitionDurationStr = rangeContainer.getElementsByClassName("range-transition-duration")[0].value;
+ const transitionDurationStr = rangeContainer.getElementsByClassName(
+ "range-transition-duration",
+ )[0].value;
if (transitionType === "") {
transitions.push(null);
} else {
@@ -1123,11 +1166,13 @@ async function sendVideoData(newState, overrideChanges) {
// but 0 is an error here anyway.
// Note that !(x > 0) is not equivalent to (x <= 0) due to NaN.
const transitionDuration = Number(transitionDurationStr);
- if ( !(transitionDuration > 0) ) {
- submissionError(`Couldn't submit edits: Invalid transition duration: "${transitionDurationStr}"`);
+ if (!(transitionDuration > 0)) {
+ submissionError(
+ `Couldn't submit edits: Invalid transition duration: "${transitionDurationStr}"`,
+ );
return;
}
- transitions.push([transitionType, transitionDuration])
+ transitions.push([transitionType, transitionDuration]);
// Since we're overlapping with the previous range, this range's start time is
// actually earlier. This matters for chapter markers.
rangeStartInFinalVideo -= transitionDuration;
@@ -1186,7 +1231,9 @@ async function sendVideoData(newState, overrideChanges) {
continue;
}
if (startFieldTime < rangeStartPlayer || startFieldTime > rangeEndPlayer) {
- submissionError(`The chapter at "${startField.value}" is outside its containing time range.`);
+ submissionError(
+ `The chapter at "${startField.value}" is outside its containing time range.`,
+ );
return;
}
const chapterStartTime = rangeStartInFinalVideo + startFieldTime - rangeStartPlayer;
@@ -1482,24 +1529,24 @@ async function uploadedImageToBase64() {
const fileHandle = fileInput.files[0];
const fileReader = new FileReader();
let loadPromiseResolve;
- const loadPromise = new Promise((resolve, _reject) => {
+ const loadPromise = new Promise((resolve, _reject) => {
loadPromiseResolve = resolve;
});
- fileReader.addEventListener("loadend", (event) => {
+ fileReader.addEventListener("loadend", (event) => {
loadPromiseResolve();
- });
+ });
fileReader.readAsDataURL(fileHandle);
await loadPromise;
const fileLoadData = fileReader.result;
if (fileLoadData.error) {
throw new Error(
- `An error (${fileLoadData.error.name}) occurred loading the thumbnail: ${fileLoadData.error.message}`
+ `An error (${fileLoadData.error.name}) occurred loading the thumbnail: ${fileLoadData.error.message}`,
);
- }
+ }
if (fileLoadData.substring(0, 22) !== "data:image/png;base64,") {
throw new Error("An error occurred converting the uploaded image to base64.");
- }
+ }
return fileLoadData.substring(22);
}
@@ -1536,14 +1583,14 @@ async function renderThumbnail() {
}
// Converting the result into base64 is similarly painful.
const blob = await res.blob();
- const data = await new Promise(resolve => {
+ const data = await new Promise((resolve) => {
const reader = new FileReader();
reader.onload = () => resolve(reader.result);
reader.readAsDataURL(blob);
});
if (data.substring(0, 22) !== "data:image/png;base64,") {
throw new Error("An error occurred converting the uploaded image to base64.");
- }
+ }
return data.substring(22);
}
@@ -1558,13 +1605,15 @@ function updateDownloadLink() {
const transitionTypeElements = rangeContainer.getElementsByClassName("range-transition-type");
if (transitionTypeElements.length > 0) {
const transitionType = transitionTypeElements[0].value;
- const transitionDurationStr = rangeContainer.getElementsByClassName("range-transition-duration")[0].value;
+ const transitionDurationStr = rangeContainer.getElementsByClassName(
+ "range-transition-duration",
+ )[0].value;
if (transitionType === "") {
transitions.push("");
} else {
let transitionDuration = Number(transitionDurationStr);
// We don't have a sensible way to error out here, so default invalid durations to 1s
- if ( !(transitionDuration > 0) ) {
+ if (!(transitionDuration > 0)) {
transitionDuration = 1;
}
transitions.push(`${transitionType},${transitionDuration}`);
@@ -1711,9 +1760,12 @@ function makeElement(tag, classes = [], values = {}) {
function rangeDefinitionDOM() {
// Shortcut builder for image-based buttons
- const button = (cls, src, alt) => makeElement("img", [cls, "click"], {
- src, alt, title: alt,
- });
+ const button = (cls, src, alt) =>
+ makeElement("img", [cls, "click"], {
+ src,
+ alt,
+ title: alt,
+ });
const rangeContainer = makeElement("div", ["range-definition-removable"]);
@@ -1731,7 +1783,10 @@ function rangeDefinitionDOM() {
updateTransitionTypes([transitionType]);
// Duration always starts hidden because type always starts as cut.
- const transitionDurationSection = makeElement("div", ["range-transition-duration-section", "hidden"]);
+ const transitionDurationSection = makeElement("div", [
+ "range-transition-duration-section",
+ "hidden",
+ ]);
// Add/remove hidden when type changes
transitionType.addEventListener("change", (event) => {
if (transitionType.value === "") {
@@ -1754,7 +1809,7 @@ function rangeDefinitionDOM() {
transitionContainer.append("Transition: ", transitionType, transitionDurationSection);
const rangeTimesContainer = makeElement("div", ["range-definition-times"]);
- const rangeStart = makeElement("input", ["range-definition-start"], {type: "text"});
+ const rangeStart = makeElement("input", ["range-definition-start"], { type: "text" });
const rangeStartSet = button(
"range-definition-set-start",
"images/pencil.png",
@@ -1766,7 +1821,7 @@ function rangeDefinitionDOM() {
"Play from start point",
);
const rangeTimeGap = makeElement("div", ["range-definition-between-time-gap"]);
- const rangeEnd = makeElement("input", ["range-definition-end"], {type: "text"});
+ const rangeEnd = makeElement("input", ["range-definition-end"], { type: "text" });
const rangeEndSet = button(
"range-definition-set-end",
"images/pencil.png",
@@ -1777,11 +1832,7 @@ function rangeDefinitionDOM() {
"images/play_to.png",
"Play from end point",
);
- const removeRange = button(
- "range-definition-remove",
- "images/minus.png",
- "Remove range",
- );
+ const removeRange = button("range-definition-remove", "images/minus.png", "Remove range");
if (canEditVideo()) {
rangeStartSet.addEventListener("click", getRangeSetClickHandler("start"));
@@ -2233,26 +2284,26 @@ function isNonVideoInput(element) {
return element.id.startsWith("data-correction-force-reset");
}
-/**
+/**
* Helper function to create the Jcrop widgets the first time the user chooses
- * to load the advanced template cropping tool images in a given session.
+ * to load the advanced template cropping tool images in a given session.
*/
function createTemplateCropWidgets() {
if (videoFrameStage == null) {
- videoFrameStage = Jcrop.attach('video-info-thumbnail-template-video-source-image');
- videoFrameStage.listen('crop.update',function(widget,e){
+ videoFrameStage = Jcrop.attach("video-info-thumbnail-template-video-source-image");
+ videoFrameStage.listen("crop.update", function (widget, e) {
const pos = widget.pos;
const fieldX1 = document.getElementById("video-info-thumbnail-crop-0");
const fieldY1 = document.getElementById("video-info-thumbnail-crop-1");
const fieldX2 = document.getElementById("video-info-thumbnail-crop-2");
const fieldY2 = document.getElementById("video-info-thumbnail-crop-3");
// 640x320 -> 1920x1080
- fieldX1.value = Math.round(pos.x*3);
- fieldY1.value = Math.round(pos.y*3);
- fieldX2.value = Math.round((pos.x+pos.w)*3);
- fieldY2.value = Math.round((pos.y+pos.h)*3);
+ fieldX1.value = Math.round(pos.x * 3);
+ fieldY1.value = Math.round(pos.y * 3);
+ fieldX2.value = Math.round((pos.x + pos.w) * 3);
+ fieldY2.value = Math.round((pos.y + pos.h) * 3);
});
- videoFrameStage.listen('crop.change',function(widget,e){
+ videoFrameStage.listen("crop.change", function (widget, e) {
// This only fires when the user is finished dragging, not every time the size
// of the cropped area updates. This avoids the template area updating every
// instant due to minute changes in the aspect ratio, which causes it to shrink
@@ -2261,18 +2312,18 @@ function createTemplateCropWidgets() {
});
}
if (templateStage == null) {
- templateStage = Jcrop.attach('video-info-thumbnail-template-overlay-image');
- templateStage.listen('crop.update',function(widget,e){
+ templateStage = Jcrop.attach("video-info-thumbnail-template-overlay-image");
+ templateStage.listen("crop.update", function (widget, e) {
const pos = widget.pos;
const fieldX1 = document.getElementById("video-info-thumbnail-location-0");
const fieldY1 = document.getElementById("video-info-thumbnail-location-1");
const fieldX2 = document.getElementById("video-info-thumbnail-location-2");
const fieldY2 = document.getElementById("video-info-thumbnail-location-3");
// 640x320 -> 1280x720
- fieldX1.value = Math.round(pos.x*2);
- fieldY1.value = Math.round(pos.y*2);
- fieldX2.value = Math.round((pos.x+pos.w)*2);
- fieldY2.value = Math.round((pos.y+pos.h)*2);
+ fieldX1.value = Math.round(pos.x * 2);
+ fieldY1.value = Math.round(pos.y * 2);
+ fieldX2.value = Math.round((pos.x + pos.w) * 2);
+ fieldY2.value = Math.round((pos.y + pos.h) * 2);
});
}
@@ -2290,7 +2341,12 @@ function updateTemplateCropWidgets() {
const videoFieldX2 = document.getElementById("video-info-thumbnail-crop-2");
const videoFieldY2 = document.getElementById("video-info-thumbnail-crop-3");
// Video frame: 640x360 -> 1920x1080
- const videoFrameRect = Jcrop.Rect.create(videoFieldX1.value/3, videoFieldY1.value/3, (videoFieldX2.value-videoFieldX1.value)/3, (videoFieldY2.value-videoFieldY1.value)/3);
+ const videoFrameRect = Jcrop.Rect.create(
+ videoFieldX1.value / 3,
+ videoFieldY1.value / 3,
+ (videoFieldX2.value - videoFieldX1.value) / 3,
+ (videoFieldY2.value - videoFieldY1.value) / 3,
+ );
if (videoFrameStage.active == null) {
videoFrameStage.newWidget(videoFrameRect);
} else {
@@ -2303,7 +2359,12 @@ function updateTemplateCropWidgets() {
const templateFieldX2 = document.getElementById("video-info-thumbnail-location-2");
const templateFieldY2 = document.getElementById("video-info-thumbnail-location-3");
// Template: 640x360 -> 1280x720
- const templateRect = Jcrop.Rect.create(templateFieldX1.value/2, templateFieldY1.value/2, (templateFieldX2.value-templateFieldX1.value)/2, (templateFieldY2.value-templateFieldY1.value)/2);
+ const templateRect = Jcrop.Rect.create(
+ templateFieldX1.value / 2,
+ templateFieldY1.value / 2,
+ (templateFieldX2.value - templateFieldX1.value) / 2,
+ (templateFieldY2.value - templateFieldY1.value) / 2,
+ );
if (templateStage.active == null) {
templateStage.newWidget(templateRect);
} else {
@@ -2321,11 +2382,12 @@ function updateTemplateCropAspectRatio() {
const videoFieldY1 = document.getElementById("video-info-thumbnail-crop-1");
const videoFieldX2 = document.getElementById("video-info-thumbnail-crop-2");
const videoFieldY2 = document.getElementById("video-info-thumbnail-crop-3");
- const videoFieldAspectRatio = (videoFieldX2.value-videoFieldX1.value)/(videoFieldY2.value-videoFieldY1.value);
- videoFrameStage.setOptions({aspectRatio: videoFieldAspectRatio});
- templateStage.setOptions({aspectRatio: videoFieldAspectRatio});
+ const videoFieldAspectRatio =
+ (videoFieldX2.value - videoFieldX1.value) / (videoFieldY2.value - videoFieldY1.value);
+ videoFrameStage.setOptions({ aspectRatio: videoFieldAspectRatio });
+ templateStage.setOptions({ aspectRatio: videoFieldAspectRatio });
} else {
- videoFrameStage.setOptions({aspectRatio: null});
- templateStage.setOptions({aspectRatio: null});
+ videoFrameStage.setOptions({ aspectRatio: null });
+ templateStage.setOptions({ aspectRatio: null });
}
}
diff --git a/thrimbletrimmer/scripts/stream.js b/thrimbletrimmer/scripts/stream.js
index e71e502..91c269b 100644
--- a/thrimbletrimmer/scripts/stream.js
+++ b/thrimbletrimmer/scripts/stream.js
@@ -137,7 +137,7 @@ function generateDownloadURL(startTime, endTime, downloadType, allowHoles, quali
const query = new URLSearchParams({
type: downloadType,
- allow_holes: allowHoles
+ allow_holes: allowHoles,
});
if (startURLTime) {
query.append("start", startURLTime);