Files
ArtPlayer/docs/upscaler/index.html
T

1068 lines
48 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, viewport-fit=cover"
/>
<meta name="google-adsense-account" content="ca-pub-8579453810848662" />
<title>
Free AI Video Upscaler - Enhance Video Quality Online | ArtPlayer
</title>
<meta
name="description"
content="Free browser-based AI video upscaler. Enhance and upscale MP4/WebM videos using WebGPU/WebGL super resolution. No upload required - runs 100% locally in your browser."
/>
<meta
name="keywords"
content="video upscaler, AI video enhancement, super resolution, WebGPU, WebGL, free video upscaler, online video enhancer, video quality improvement, 4K upscaling"
/>
<meta name="robots" content="index,follow" />
<meta name="author" content="ArtPlayer" />
<link rel="canonical" href="https://artplayer.org/upscaler/" />
<meta property="og:type" content="website" />
<meta property="og:url" content="https://artplayer.org/upscaler/" />
<meta
property="og:title"
content="Free AI Video Upscaler - Enhance Video Quality Online"
/>
<meta
property="og:description"
content="Upscale videos with AI super resolution directly in your browser. Free, private, no upload needed."
/>
<meta property="og:site_name" content="ArtPlayer" />
<meta name="twitter:card" content="summary_large_image" />
<meta
name="twitter:title"
content="Free AI Video Upscaler - Enhance Video Quality Online"
/>
<meta
name="twitter:description"
content="Upscale videos with AI super resolution directly in your browser. Free, private, no upload needed."
/>
<link
rel="shortcut icon"
href="/assets/img/favicon.ico"
type="image/x-icon"
/>
<script
async
src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-8579453810848662"
crossorigin="anonymous"
></script>
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = {
theme: {
extend: {
fontFamily: {
sans: [
"-apple-system",
"BlinkMacSystemFont",
"Segoe UI",
"Roboto",
"sans-serif",
],
},
},
},
};
</script>
</head>
<body
class="min-h-screen flex flex-col bg-slate-50 text-slate-900 antialiased font-sans"
>
<main
class="flex-1 max-w-3xl w-full mx-auto px-4 sm:px-6 py-8 sm:py-12"
>
<!-- Title Section -->
<div class="text-center mb-8">
<h1
class="text-2xl sm:text-3xl font-bold text-slate-900 mb-3 tracking-tight"
>
Video Upscaler
</h1>
<p
class="text-slate-500 text-sm sm:text-base max-w-md mx-auto leading-relaxed"
>
Enhance video quality with AI super resolution. Runs locally
in your browser — no upload needed.
</p>
</div>
<!-- Upload Card -->
<div
class="bg-white rounded-2xl shadow-sm border border-slate-200 overflow-hidden"
>
<div class="p-4 sm:p-6">
<!-- Upload Area -->
<div class="mb-6">
<input
type="file"
id="universalInput"
accept="video/*"
class="sr-only"
/>
<label
for="universalInput"
class="group relative flex flex-col w-full aspect-video cursor-pointer items-center justify-center rounded-xl border-2 border-dashed border-slate-300 overflow-hidden bg-gradient-to-br from-slate-50 to-gray-100 transition-all duration-200 hover:from-blue-50 hover:to-blue-100 hover:border-blue-300"
id="uploadLabel"
>
<!-- Upload Placeholder -->
<div
id="uploadPlaceholder"
class="flex flex-col items-center px-4"
>
<div
class="w-12 h-12 sm:w-14 sm:h-14 rounded-full bg-slate-100 flex items-center justify-center mb-3 sm:mb-4"
>
<svg
class="w-6 h-6 sm:w-7 sm:h-7 text-slate-400"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="1.5"
d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"
/>
</svg>
</div>
<span
class="text-slate-700 font-medium text-sm sm:text-base mb-1"
>Choose a video file</span
>
<span
class="text-slate-400 text-xs sm:text-sm mb-3 sm:mb-4"
>or drag and drop here</span
>
<button
type="button"
onclick="loadSampleVideo(event)"
class="inline-flex items-center gap-1.5 sm:gap-2 px-3 sm:px-4 py-1.5 sm:py-2 text-xs sm:text-sm font-semibold text-white bg-blue-500 hover:bg-blue-600 rounded-lg transition-all"
>
<svg
class="w-3.5 h-3.5 sm:w-4 sm:h-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z"
/>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
Try sample
</button>
</div>
<!-- Video Preview (shown after file selection) -->
<div
id="videoPreviewArea"
class="absolute inset-0 hidden bg-slate-900"
>
<video
id="videoPreviewPlayer"
class="w-full h-full object-contain pointer-events-none"
muted
playsinline
></video>
<!-- Video Info Overlay -->
<div
class="absolute top-3 left-3 flex items-center gap-2"
>
<span
class="inline-flex items-center gap-1.5 bg-black/50 backdrop-blur-sm text-white text-xs font-medium px-2.5 py-1 rounded-md"
>
<svg
class="w-3.5 h-3.5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z"
/>
</svg>
<span id="videoMetaInfo"></span>
</span>
</div>
<!-- File Name Overlay -->
<div
class="absolute bottom-3 left-3 right-3 flex items-center justify-between gap-2"
>
<span
id="videoFileName"
class="text-white text-sm font-medium truncate min-w-0 drop-shadow-md"
></span>
<span
class="text-white/80 text-xs flex-shrink-0"
>Click to change</span
>
</div>
</div>
</label>
</div>
<!-- Network Selection -->
<div
class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3 sm:gap-4 mb-6"
>
<div>
<label
class="block text-sm font-medium text-slate-700 mb-0.5 sm:mb-1"
>
Quality Model
</label>
<p class="text-xs text-slate-400">
Higher = better quality, slower
</p>
</div>
<div
class="inline-flex items-center gap-0.5 sm:gap-1 rounded-lg bg-slate-100 p-1 self-start sm:self-auto"
id="networkToggle"
>
<button
type="button"
data-network="small"
class="rounded-md px-2.5 sm:px-3 py-1.5 text-xs sm:text-sm font-medium text-slate-600 transition-all duration-150"
>
Fast
</button>
<button
type="button"
data-network="medium"
class="rounded-md px-2.5 sm:px-3 py-1.5 text-xs sm:text-sm font-medium bg-slate-800 text-white shadow-sm transition-all duration-150"
>
Balanced
</button>
<button
type="button"
data-network="large"
class="rounded-md px-2.5 sm:px-3 py-1.5 text-xs sm:text-sm font-medium text-slate-600 transition-all duration-150"
>
Quality
</button>
</div>
</div>
<!-- Process Button -->
<button
id="processBtn"
onclick="handleProcess()"
disabled
class="relative w-full rounded-xl px-4 py-3.5 text-sm font-semibold text-white overflow-hidden bg-gradient-to-b from-slate-800 to-slate-900 transition-all duration-200 hover:from-slate-700 hover:to-slate-800 hover:shadow-lg disabled:bg-slate-200 disabled:from-slate-200 disabled:to-slate-200 disabled:text-slate-400 disabled:shadow-none"
>
<span
class="progress-fill absolute inset-y-0 left-0 w-0 bg-emerald-600 transition-all duration-200 ease-out z-0"
id="progressFill"
></span>
<span id="progressText" class="relative z-10">
Select a video to start
</span>
</button>
</div>
<!-- Result Section -->
<div
class="border-t border-slate-100 bg-slate-50 p-5 sm:p-8 hidden"
id="resultSection"
>
<div class="flex items-center gap-2 mb-4">
<svg
class="w-5 h-5 text-emerald-500"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
<span class="text-sm font-medium text-slate-700"
>Upscaling Complete</span
>
</div>
<div id="videoResultSection" class="hidden">
<div id="videoComparisonViewer" class="mb-5">
<div
class="comparison-container relative w-full max-w-full rounded-xl overflow-hidden bg-slate-900 shadow-md select-none"
id="videoComparisonContainer"
>
<video
id="resultVideoEnhanced"
class="comparison-img enhanced block w-full h-auto bg-slate-900"
controls
loop
muted
playsinline
preload="auto"
>
<source
id="resultVideoSourceEnhanced"
type="video/mp4"
/>
Your browser does not support the video tag.
</video>
<video
id="resultVideoOriginal"
class="comparison-img original absolute inset-0 w-full h-full bg-slate-900 object-contain"
loop
playsinline
preload="auto"
>
<source
id="resultVideoSourceOriginal"
type="video/mp4"
/>
Your browser does not support the video tag.
</video>
<div
class="comparison-handle absolute left-1/2 top-0 h-full w-10 -translate-x-1/2 cursor-ew-resize z-10 flex items-center justify-center"
id="videoComparisonHandle"
>
<div
class="absolute inset-y-0 left-1/2 w-[2px] -translate-x-1/2 bg-white/90 shadow-[0_0_8px_rgba(0,0,0,0.4)]"
></div>
<div
class="relative h-9 w-9 rounded-full bg-white shadow-lg border border-slate-200 flex items-center justify-center cursor-ew-resize"
id="videoComparisonKnob"
>
<svg
class="w-4 h-4 text-slate-400"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M10 19l-7-7 7-7m4 14l7-7-7-7"
/>
</svg>
</div>
</div>
<div
class="comparison-labels absolute top-3 w-full flex justify-between px-3 pointer-events-none z-10"
>
<div
class="bg-slate-900/70 px-2.5 py-1 rounded-md text-xs font-medium text-white backdrop-blur-sm"
>
Original
</div>
<div
class="bg-slate-900/70 px-2.5 py-1 rounded-md text-xs font-medium text-white backdrop-blur-sm"
>
Upscaled
</div>
</div>
</div>
</div>
<!-- File Info -->
<div
class="grid grid-cols-3 gap-2 sm:gap-3 mb-5 text-center"
>
<div
class="bg-white rounded-lg px-2 sm:px-3 py-2.5 sm:py-3 border border-slate-200"
>
<div
class="text-[10px] sm:text-xs text-slate-400 mb-0.5 sm:mb-1"
>
Original
</div>
<div
class="text-xs sm:text-sm font-medium text-slate-700"
id="originalVideoSize"
>
-
</div>
</div>
<div
class="bg-white rounded-lg px-2 sm:px-3 py-2.5 sm:py-3 border border-slate-200"
>
<div
class="text-[10px] sm:text-xs text-slate-400 mb-0.5 sm:mb-1"
>
Upscaled
</div>
<div
class="text-xs sm:text-sm font-medium text-slate-700"
id="resultVideoSize"
>
-
</div>
</div>
<div
class="bg-white rounded-lg px-2 sm:px-3 py-2.5 sm:py-3 border border-slate-200"
>
<div
class="text-[10px] sm:text-xs text-slate-400 mb-0.5 sm:mb-1"
>
Backend
</div>
<div
class="text-xs sm:text-sm font-medium text-slate-700"
id="backendVideoInfo"
>
-
</div>
</div>
</div>
<!-- Action Buttons -->
<div class="flex gap-3">
<button
class="flex-1 inline-flex items-center justify-center gap-2 rounded-xl bg-slate-900 px-4 py-3 text-sm font-semibold text-white hover:bg-slate-800 transition-colors"
onclick="downloadResult()"
>
<svg
class="w-4 h-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"
/>
</svg>
Download
</button>
<button
class="flex-1 inline-flex items-center justify-center gap-2 rounded-xl border border-slate-200 bg-white px-4 py-3 text-sm font-medium text-slate-700 hover:bg-slate-50 transition-colors"
onclick="resetUpscalerUI()"
>
<svg
class="w-4 h-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"
/>
</svg>
Start Over
</button>
</div>
</div>
</div>
</div>
</main>
<!-- Footer -->
<footer class="w-full border-t border-slate-200 bg-white">
<div
class="max-w-4xl mx-auto px-4 sm:px-6 py-3 sm:py-4 flex flex-col sm:flex-row items-center justify-center sm:justify-between gap-1 sm:gap-0 text-xs sm:text-sm text-slate-500"
>
<span>Powered by WebGPU / WebGL</span>
<span
>Open source on
<a
href="https://github.com/zhw2590582/ArtPlayer"
class="text-slate-700 hover:underline"
>GitHub</a
></span
>
</div>
</footer>
<script src="upscaler.js"></script>
<script>
var _hmt = _hmt || [];
var hm = document.createElement("script");
hm.src =
"https://hm.baidu.com/hm.js?879b2890d8d025c751785170763ca82b";
var s = document.getElementsByTagName("script")[0];
s.parentNode.insertBefore(hm, s);
</script>
<script>
let currentResultBlob = null;
let currentOriginalUrl = null;
let isComparisonDragging = false;
let comparisonCleanup = null;
let currentVideoFile = null;
// Create a configurable Upscaler instance
const upscaler = new Upscaler({
networkSize: "medium",
weightsBaseUrl: "/assets/websr/weights",
workerUrl: "/assets/websr/worker/main.js",
});
function updateComparisonPosition(element, rect, x) {
x = Math.max(0, Math.min(x, rect.width));
const percentage = (x / rect.width) * 100;
return { x, percentage };
}
function applyMaskToElement(element, percentage) {
const maskGradient = `linear-gradient(to right, black 0%, black ${percentage}%, transparent ${percentage}%, transparent 100%)`;
element.style.maskImage = maskGradient;
element.style.webkitMaskImage = maskGradient;
}
function setupComparisonDrag(container, handle, originalElement) {
// Return cleanup function to remove listeners
const abortController = new AbortController();
const { signal } = abortController;
function updatePosition(e) {
if (!isComparisonDragging) return;
const rect = container.getBoundingClientRect();
let x = e.clientX - rect.left;
if (e.touches) {
x = e.touches[0].clientX - rect.left;
}
const { percentage } = updateComparisonPosition(
originalElement,
rect,
x,
);
handle.style.left = x + "px";
applyMaskToElement(originalElement, percentage);
}
function startDrag(e) {
e.preventDefault();
isComparisonDragging = true;
container.style.cursor = "ew-resize";
}
function stopDrag() {
isComparisonDragging = false;
container.style.cursor = "default";
}
handle.addEventListener("mousedown", startDrag, { signal });
handle.addEventListener("touchstart", startDrag, {
passive: false,
signal,
});
document.addEventListener("mousemove", updatePosition, {
signal,
});
document.addEventListener("touchmove", updatePosition, {
passive: false,
signal,
});
document.addEventListener("mouseup", stopDrag, { signal });
document.addEventListener("touchend", stopDrag, { signal });
container.addEventListener(
"click",
(e) => {
const rect = container.getBoundingClientRect();
const x = e.clientX - rect.left;
const { percentage } = updateComparisonPosition(
originalElement,
rect,
x,
);
handle.style.left = x + "px";
applyMaskToElement(originalElement, percentage);
},
{ signal },
);
applyMaskToElement(originalElement, 50);
handle.style.left = container.offsetWidth / 2 + "px";
return () => abortController.abort();
}
function initVideoComparisonDrag() {
// Cleanup previous listeners
if (comparisonCleanup) {
comparisonCleanup();
comparisonCleanup = null;
}
const container = document.getElementById(
"videoComparisonContainer",
);
const handle = document.getElementById("videoComparisonHandle");
const originalVideo = document.getElementById(
"resultVideoOriginal",
);
const enhancedVideo = document.getElementById(
"resultVideoEnhanced",
);
enhancedVideo.addEventListener("play", () =>
originalVideo.play(),
);
enhancedVideo.addEventListener("pause", () =>
originalVideo.pause(),
);
enhancedVideo.addEventListener("timeupdate", () => {
if (
Math.abs(
originalVideo.currentTime -
enhancedVideo.currentTime,
) > 0.1
) {
originalVideo.currentTime = enhancedVideo.currentTime;
}
});
enhancedVideo.addEventListener("seeking", () => {
originalVideo.currentTime = enhancedVideo.currentTime;
});
comparisonCleanup = setupComparisonDrag(
container,
handle,
originalVideo,
);
enhancedVideo.autoplay = true;
enhancedVideo.play().catch(() => {});
}
function formatDuration(seconds) {
if (seconds < 60) return `${Math.floor(seconds)}s`;
const m = Math.floor(seconds / 60);
const s = Math.floor(seconds % 60);
return s > 0 ? `${m}m ${s}s` : `${m}m`;
}
async function loadSampleVideo(e) {
e.preventDefault();
e.stopPropagation();
const sampleUrl =
"https://artplayer.org/assets/sample/test1.mp4";
const btn = e.currentTarget;
const originalText = btn.innerHTML;
btn.disabled = true;
btn.innerHTML = `
<svg class="w-4 h-4 animate-spin" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
Loading...
`;
try {
const response = await fetch(sampleUrl);
const blob = await response.blob();
const file = new File([blob], "sample_video.mp4", {
type: "video/mp4",
});
// Create a DataTransfer to set files on input
const dataTransfer = new DataTransfer();
dataTransfer.items.add(file);
document.getElementById("universalInput").files =
dataTransfer.files;
document
.getElementById("universalInput")
.dispatchEvent(new Event("change"));
} catch (error) {
console.error("Failed to load sample video:", error);
alert("Failed to load sample video. Please try again.");
btn.disabled = false;
btn.innerHTML = originalText;
}
}
let previewVideoUrl = null;
function showVideoPreview(file) {
const placeholder =
document.getElementById("uploadPlaceholder");
const previewArea = document.getElementById("videoPreviewArea");
const previewVideo =
document.getElementById("videoPreviewPlayer");
const fileName = document.getElementById("videoFileName");
const metaInfo = document.getElementById("videoMetaInfo");
const uploadLabel = document.getElementById("uploadLabel");
if (!placeholder || !previewArea || !previewVideo) return;
// Revoke previous URL if exists
if (previewVideoUrl) {
URL.revokeObjectURL(previewVideoUrl);
}
// Create new URL for the video
previewVideoUrl = URL.createObjectURL(file);
previewVideo.src = previewVideoUrl;
// Show preview area, hide placeholder
placeholder.classList.add("hidden");
previewArea.classList.remove("hidden");
// Hide border completely
uploadLabel.classList.remove(
"border-dashed",
"border-2",
"border-slate-300",
);
uploadLabel.classList.add("border-0");
fileName.textContent = file.name;
metaInfo.textContent = "Loading...";
previewVideo.onloadedmetadata = () => {
const width = previewVideo.videoWidth;
const height = previewVideo.videoHeight;
const duration = previewVideo.duration;
metaInfo.textContent = `${width}×${height} · ${formatDuration(duration)}`;
};
}
function resetVideoPreview() {
const placeholder =
document.getElementById("uploadPlaceholder");
const previewArea = document.getElementById("videoPreviewArea");
const previewVideo =
document.getElementById("videoPreviewPlayer");
const uploadLabel = document.getElementById("uploadLabel");
if (placeholder) placeholder.classList.remove("hidden");
if (previewArea) previewArea.classList.add("hidden");
if (previewVideo) {
previewVideo.pause();
previewVideo.src = "";
}
// Restore dashed border
if (uploadLabel) {
uploadLabel.classList.remove("border-0");
uploadLabel.classList.add(
"border-dashed",
"border-2",
"border-slate-300",
);
}
if (previewVideoUrl) {
URL.revokeObjectURL(previewVideoUrl);
previewVideoUrl = null;
}
}
// Watch file input, only allow video and control button state
(function initFileInputWatcher() {
const fileInput = document.getElementById("universalInput");
const processBtn = document.getElementById("processBtn");
if (!fileInput || !processBtn) return;
processBtn.disabled = true;
fileInput.addEventListener("change", async () => {
const file = fileInput.files[0];
// If user cancelled selection, keep previous state
if (!file) return;
if (!file.type.startsWith("video/")) {
alert(
"Only video files are supported, please choose again.",
);
fileInput.value = "";
return;
}
processBtn.disabled = false;
const progressText =
document.getElementById("progressText");
if (progressText)
progressText.textContent = "Start upscaling";
// Save file reference
currentVideoFile = file;
// Show video preview
showVideoPreview(file);
});
})();
// Network size toggle buttons (small / medium / large)
(function initNetworkToggle() {
const toggle = document.getElementById("networkToggle");
if (!toggle) return;
const activeClasses = [
"bg-slate-800",
"text-white",
"shadow-sm",
];
const inactiveClasses = ["text-slate-600"];
toggle.addEventListener("click", (event) => {
const button = event.target.closest("button[data-network]");
if (!button) return;
const buttons = toggle.querySelectorAll(
"button[data-network]",
);
buttons.forEach((btn) => {
btn.classList.remove(...activeClasses);
btn.classList.add(...inactiveClasses);
});
button.classList.remove(...inactiveClasses);
button.classList.add(...activeClasses);
});
})();
// Drag and drop support
(function initDragDrop() {
const uploadLabel = document.getElementById("uploadLabel");
const fileInput = document.getElementById("universalInput");
if (!uploadLabel || !fileInput) return;
["dragenter", "dragover", "dragleave", "drop"].forEach(
(eventName) => {
uploadLabel.addEventListener(eventName, (e) => {
e.preventDefault();
e.stopPropagation();
});
},
);
["dragenter", "dragover"].forEach((eventName) => {
uploadLabel.addEventListener(eventName, () => {
uploadLabel.classList.remove(
"from-slate-50",
"to-gray-100",
"border-slate-300",
);
uploadLabel.classList.add(
"from-blue-50",
"to-blue-100",
"border-blue-500",
);
});
});
["dragleave", "drop"].forEach((eventName) => {
uploadLabel.addEventListener(eventName, () => {
uploadLabel.classList.remove(
"from-blue-50",
"to-blue-100",
"border-blue-500",
);
uploadLabel.classList.add(
"from-slate-50",
"to-gray-100",
"border-slate-300",
);
});
});
uploadLabel.addEventListener("drop", (e) => {
const dt = e.dataTransfer;
const files = dt.files;
if (files.length > 0) {
fileInput.files = files;
fileInput.dispatchEvent(new Event("change"));
}
});
})();
async function handleProcess() {
const file = currentVideoFile;
if (!file || !file.type.startsWith("video/")) {
alert("Please choose a valid video file.");
return;
}
const networkToggle = document.getElementById("networkToggle");
const activeBtn = networkToggle?.querySelector(
"button.bg-slate-800[data-network]",
);
const networkSize = activeBtn?.dataset.network || "medium";
const processBtn = document.getElementById("processBtn");
const resultSection = document.getElementById("resultSection");
const videoResultSection =
document.getElementById("videoResultSection");
processBtn.disabled = true;
resultSection.classList.add("hidden");
videoResultSection?.classList.add("hidden");
document.getElementById("progressText").textContent =
"Initializing...";
document.getElementById("progressFill").style.width = "0%";
try {
let lastSeekTime = 0;
const previewVideo =
document.getElementById("videoPreviewPlayer");
const videoDuration = previewVideo?.duration || 0;
upscaler.onProgress((progress) => {
document.getElementById("progressFill").style.width =
progress + "%";
document.getElementById("progressText").textContent =
`Processing... ${Math.round(progress)}%`;
// Sync preview video seek, once per second
const now = Date.now();
if (
previewVideo &&
videoDuration &&
now - lastSeekTime >= 1000
) {
lastSeekTime = now;
const targetTime = (progress / 100) * videoDuration;
previewVideo.currentTime = targetTime;
}
});
const resultBlob = await upscaler.upscaleVideo(
file,
networkSize,
);
currentResultBlob = resultBlob;
const videoUrl = URL.createObjectURL(resultBlob);
const originalVideoUrl =
currentOriginalUrl || URL.createObjectURL(file);
currentOriginalUrl = originalVideoUrl;
const enhancedSource = document.getElementById(
"resultVideoSourceEnhanced",
);
const originalSource = document.getElementById(
"resultVideoSourceOriginal",
);
enhancedSource.src = videoUrl;
enhancedSource.type = "video/mp4";
document.getElementById("resultVideoEnhanced").load();
originalSource.src = originalVideoUrl;
originalSource.type = "video/mp4";
document.getElementById("resultVideoOriginal").load();
document.getElementById("originalVideoSize").textContent =
formatBytes(file.size);
document.getElementById("resultVideoSize").textContent =
formatBytes(resultBlob.size);
document.getElementById("backendVideoInfo").textContent =
Upscaler.getBackendType();
resultSection.classList.remove("hidden");
videoResultSection?.classList.remove("hidden");
// Scroll to result section
resultSection.scrollIntoView({
behavior: "smooth",
block: "start",
});
// Initialize comparison drag after video loads
const enhancedVideo = document.getElementById(
"resultVideoEnhanced",
);
if (enhancedVideo.readyState >= 2) {
initVideoComparisonDrag();
} else {
enhancedVideo.addEventListener(
"loadeddata",
initVideoComparisonDrag,
{ once: true },
);
}
} catch (error) {
console.error("Upscaling error:", error);
alert("Upscaling failed:\n" + error.message);
} finally {
processBtn.disabled = false;
document.getElementById("progressFill").style.width = "0%";
document.getElementById("progressText").textContent =
"Start upscaling";
}
}
function downloadResult() {
if (!currentResultBlob) return alert("No result available.");
const a = document.createElement("a");
a.href = URL.createObjectURL(currentResultBlob);
a.download = `upscaled_${Date.now()}.mp4`;
a.click();
URL.revokeObjectURL(a.href);
}
function formatBytes(bytes) {
if (!bytes) return "0 B";
const k = 1024,
sizes = ["B", "KB", "MB", "GB"];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return (bytes / Math.pow(k, i)).toFixed(1) + " " + sizes[i];
}
function resetUpscalerUI() {
// Cleanup comparison listeners
if (comparisonCleanup) {
comparisonCleanup();
comparisonCleanup = null;
}
document.getElementById("universalInput").value = "";
currentResultBlob = null;
currentVideoFile = null;
if (currentOriginalUrl) URL.revokeObjectURL(currentOriginalUrl);
currentOriginalUrl = null;
document.getElementById("processBtn").disabled = true;
document.getElementById("progressFill").style.width = "0%";
document.getElementById("progressText").textContent =
"Select a video to start";
document
.getElementById("resultSection")
.classList.add("hidden");
document
.getElementById("videoResultSection")
?.classList.add("hidden");
["resultVideoEnhanced", "resultVideoOriginal"].forEach((id) => {
const v = document.getElementById(id);
if (v) {
v.pause();
v.removeAttribute("src");
v.load();
}
});
resetVideoPreview();
}
console.log("Upscaler ready:", Upscaler.getBackendType());
</script>
</body>
</html>