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,101 @@
|
||||
import { rankAndTrimCandidates } from "./candidateRanking";
|
||||
import { frameDifferenceRatio } from "./frameDifference";
|
||||
import { captureAnalysisFrame, createThumbnailUrl } from "../media/canvas";
|
||||
import { seekVideo } from "../media/video";
|
||||
import type { CandidateFrame, ScanSettings } from "../types/scan";
|
||||
|
||||
type ScanCallbacks = {
|
||||
onProgress: (progress: number) => void;
|
||||
signal: AbortSignal;
|
||||
};
|
||||
|
||||
export async function scanVideoForCandidates(
|
||||
video: HTMLVideoElement,
|
||||
settings: ScanSettings,
|
||||
callbacks: ScanCallbacks,
|
||||
): Promise<CandidateFrame[]> {
|
||||
const analysisCanvas = document.createElement("canvas");
|
||||
const candidates: CandidateFrame[] = [];
|
||||
let lastAcceptedFrame: ImageData | null = null;
|
||||
let lastAcceptedTime: number | null = null;
|
||||
|
||||
try {
|
||||
for (
|
||||
let time = 0;
|
||||
time < video.duration;
|
||||
time += settings.sampleIntervalSeconds
|
||||
) {
|
||||
if (callbacks.signal.aborted) {
|
||||
throw new DOMException("Scan cancelled.", "AbortError");
|
||||
}
|
||||
|
||||
await seekVideo(video, time);
|
||||
|
||||
const imageData = captureAnalysisFrame(
|
||||
video,
|
||||
analysisCanvas,
|
||||
settings.analysisWidth,
|
||||
settings.analysisHeight,
|
||||
);
|
||||
|
||||
if (!lastAcceptedFrame) {
|
||||
lastAcceptedFrame = imageData;
|
||||
|
||||
if (settings.includeFirstFrame) {
|
||||
const thumbnailUrl = await createThumbnailUrl(video);
|
||||
candidates.push({
|
||||
id: crypto.randomUUID(),
|
||||
time,
|
||||
score: 1,
|
||||
reason: "initial-frame",
|
||||
thumbnailUrl,
|
||||
});
|
||||
lastAcceptedTime = time;
|
||||
}
|
||||
} else {
|
||||
const diffRatio = frameDifferenceRatio(
|
||||
lastAcceptedFrame,
|
||||
imageData,
|
||||
settings.pixelDeltaThreshold,
|
||||
);
|
||||
|
||||
const hasEnoughChange = diffRatio >= settings.changedPixelRatioThreshold;
|
||||
const hasEnoughTimeGap =
|
||||
lastAcceptedTime === null ||
|
||||
time - lastAcceptedTime >= settings.minSecondsBetweenCaptures;
|
||||
|
||||
if (hasEnoughChange && hasEnoughTimeGap) {
|
||||
const thumbnailUrl = await createThumbnailUrl(video);
|
||||
candidates.push({
|
||||
id: crypto.randomUUID(),
|
||||
time,
|
||||
score: diffRatio,
|
||||
reason: "visual-change",
|
||||
thumbnailUrl,
|
||||
});
|
||||
lastAcceptedFrame = imageData;
|
||||
lastAcceptedTime = time;
|
||||
}
|
||||
}
|
||||
|
||||
callbacks.onProgress(Math.min(time / video.duration, 1));
|
||||
|
||||
if (candidates.length >= settings.maxCandidates) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
callbacks.onProgress(1);
|
||||
|
||||
const ranked = rankAndTrimCandidates(candidates, settings.finalTargetCount, video.duration);
|
||||
const rankedIds = new Set(ranked.map((candidate) => candidate.id));
|
||||
candidates
|
||||
.filter((candidate) => !rankedIds.has(candidate.id))
|
||||
.forEach((candidate) => URL.revokeObjectURL(candidate.thumbnailUrl));
|
||||
|
||||
return ranked;
|
||||
} catch (error) {
|
||||
candidates.forEach((candidate) => URL.revokeObjectURL(candidate.thumbnailUrl));
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user