feat: initialize frame-analyzer project with video analysis capabilities
- Add package.json with dependencies for React, Vite, and TypeScript. - Create main application structure in App.tsx for video loading and frame analysis. - Implement video scanning logic to identify candidate frames based on visual changes. - Add candidate ranking and trimming functionality to optimize selected frames. - Develop media handling utilities for capturing frames and managing video playback. - Introduce clipboard functionality for copying and downloading frames. - Design user interface with responsive styles for video selection and analysis controls. - Establish TypeScript types for video loading, scan settings, and candidate frames. - Configure TypeScript and Vite for project compilation and development.
This commit is contained in:
@@ -0,0 +1,70 @@
|
||||
import { seekVideo } from "./video";
|
||||
|
||||
export function captureAnalysisFrame(
|
||||
video: HTMLVideoElement,
|
||||
canvas: HTMLCanvasElement,
|
||||
width: number,
|
||||
height: number,
|
||||
): ImageData {
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
|
||||
const context = canvas.getContext("2d", { willReadFrequently: true });
|
||||
if (!context) throw new Error("Could not create the analysis canvas context.");
|
||||
|
||||
context.drawImage(video, 0, 0, width, height);
|
||||
return context.getImageData(0, 0, width, height);
|
||||
}
|
||||
|
||||
export async function createThumbnailUrl(video: HTMLVideoElement, maxWidth = 360): Promise<string> {
|
||||
const scale = Math.min(1, maxWidth / video.videoWidth);
|
||||
const width = Math.max(1, Math.round(video.videoWidth * scale));
|
||||
const height = Math.max(1, Math.round(video.videoHeight * scale));
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
|
||||
const context = canvas.getContext("2d");
|
||||
if (!context) throw new Error("Could not create thumbnail canvas context.");
|
||||
|
||||
context.drawImage(video, 0, 0, width, height);
|
||||
|
||||
const blob = await canvasToBlob(canvas, "image/webp", 0.82);
|
||||
return URL.createObjectURL(blob);
|
||||
}
|
||||
|
||||
export async function extractFullResolutionFrame(
|
||||
video: HTMLVideoElement,
|
||||
time: number,
|
||||
type: "image/png" | "image/jpeg" | "image/webp" = "image/png",
|
||||
quality?: number,
|
||||
): Promise<Blob> {
|
||||
await seekVideo(video, time);
|
||||
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.width = video.videoWidth;
|
||||
canvas.height = video.videoHeight;
|
||||
|
||||
const context = canvas.getContext("2d");
|
||||
if (!context) throw new Error("Could not create full-resolution canvas context.");
|
||||
|
||||
context.drawImage(video, 0, 0);
|
||||
return canvasToBlob(canvas, type, quality);
|
||||
}
|
||||
|
||||
export async function canvasToBlob(
|
||||
canvas: HTMLCanvasElement,
|
||||
type: string,
|
||||
quality?: number,
|
||||
): Promise<Blob> {
|
||||
return new Promise((resolve, reject) => {
|
||||
canvas.toBlob(
|
||||
(blob) => {
|
||||
if (!blob) reject(new Error("Canvas export failed."));
|
||||
else resolve(blob);
|
||||
},
|
||||
type,
|
||||
quality,
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
export async function copyImageBlobToClipboard(blob: Blob): Promise<void> {
|
||||
if (!navigator.clipboard || typeof ClipboardItem === "undefined") {
|
||||
throw new Error("Image clipboard writing is not available in this browser.");
|
||||
}
|
||||
|
||||
await navigator.clipboard.write([
|
||||
new ClipboardItem({
|
||||
[blob.type]: blob,
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
||||
export function downloadBlob(blob: Blob, filename: string): void {
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement("a");
|
||||
link.href = url;
|
||||
link.download = filename;
|
||||
document.body.append(link);
|
||||
link.click();
|
||||
link.remove();
|
||||
window.setTimeout(() => URL.revokeObjectURL(url), 1000);
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
export async function waitForVideoMetadata(video: HTMLVideoElement): Promise<void> {
|
||||
if (Number.isFinite(video.duration) && video.videoWidth > 0) return;
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const cleanup = () => {
|
||||
video.removeEventListener("loadedmetadata", onLoaded);
|
||||
video.removeEventListener("error", onError);
|
||||
};
|
||||
const onLoaded = () => {
|
||||
cleanup();
|
||||
resolve();
|
||||
};
|
||||
const onError = () => {
|
||||
cleanup();
|
||||
reject(new Error(video.error?.message || "This browser could not load the video."));
|
||||
};
|
||||
|
||||
video.addEventListener("loadedmetadata", onLoaded, { once: true });
|
||||
video.addEventListener("error", onError, { once: true });
|
||||
});
|
||||
}
|
||||
|
||||
export async function seekVideo(video: HTMLVideoElement, time: number): Promise<void> {
|
||||
const target = Math.min(Math.max(time, 0), video.duration || time);
|
||||
|
||||
if (Math.abs(video.currentTime - target) < 0.01 && video.readyState >= 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const cleanup = () => {
|
||||
video.removeEventListener("seeked", onSeeked);
|
||||
video.removeEventListener("loadeddata", onFrameReady);
|
||||
video.removeEventListener("error", onError);
|
||||
};
|
||||
const finish = () => {
|
||||
cleanup();
|
||||
resolve();
|
||||
};
|
||||
const onSeeked = finish;
|
||||
const onFrameReady = finish;
|
||||
const onError = () => {
|
||||
cleanup();
|
||||
reject(new Error(video.error?.message || "Video seek failed."));
|
||||
};
|
||||
|
||||
video.addEventListener("seeked", onSeeked, { once: true });
|
||||
video.addEventListener("loadeddata", onFrameReady, { once: true });
|
||||
video.addEventListener("error", onError, { once: true });
|
||||
video.currentTime = target;
|
||||
|
||||
if (Math.abs(video.currentTime - target) < 0.01 && video.readyState >= 2) {
|
||||
queueMicrotask(finish);
|
||||
}
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user