feat: enhance candidate ranking logic and improve video seeking functionality

This commit is contained in:
Ben
2026-05-14 11:11:27 -07:00
parent 65ef5420e7
commit 3a7b98ec75
2 changed files with 61 additions and 6 deletions
+36 -5
View File
@@ -39,16 +39,16 @@ export function rankAndTrimCandidates(
for (let bucket = 0; bucket < bucketCount; bucket++) { for (let bucket = 0; bucket < bucketCount; bucket++) {
const start = bucket * bucketSize; const start = bucket * bucketSize;
const end = bucket === bucketCount - 1 ? duration + 0.001 : start + bucketSize; const end = bucket === bucketCount - 1 ? duration + 0.001 : start + bucketSize;
const best = pool const best = bestCandidateInWindow(pool, start, end);
.filter((candidate) => candidate.time >= start && candidate.time < end)
.sort((a, b) => b.score - a.score)[0];
if (best && respectsMinGap([...selected.values()], best, minGapSeconds)) { if (best) {
selected.set(frameKey(best.time), best); selected.set(frameKey(best.time), best);
} }
} }
for (const candidate of [...pool].sort((a, b) => b.score - a.score)) { for (const candidate of [...pool].sort(
(a, b) => temporalCoverageScore(selected, b, duration) - temporalCoverageScore(selected, a, duration),
)) {
if (selected.size >= targetCount) break; if (selected.size >= targetCount) break;
if (respectsMinGap([...selected.values()], candidate, minGapSeconds)) { if (respectsMinGap([...selected.values()], candidate, minGapSeconds)) {
selected.set(frameKey(candidate.time), candidate); selected.set(frameKey(candidate.time), candidate);
@@ -89,3 +89,34 @@ function frameKey(time: number) {
function respectsMinGap(selected: ScoredFrame[], candidate: ScoredFrame, minGapSeconds: number) { function respectsMinGap(selected: ScoredFrame[], candidate: ScoredFrame, minGapSeconds: number) {
return selected.every((item) => Math.abs(item.time - candidate.time) >= minGapSeconds); return selected.every((item) => Math.abs(item.time - candidate.time) >= minGapSeconds);
} }
function bestCandidateInWindow(candidates: ScoredFrame[], start: number, end: number) {
const windowCandidates = candidates.filter((candidate) => candidate.time >= start && candidate.time < end);
if (windowCandidates.length === 0) {
return undefined;
}
const floor = percentile(windowCandidates, 0.5) * 0.8;
return windowCandidates
.filter((candidate) => candidate.score >= floor)
.sort((a, b) => b.score - a.score)[0];
}
function temporalCoverageScore(
selected: Map<string, ScoredFrame>,
candidate: ScoredFrame,
duration: number,
) {
const selectedValues = [...selected.values()];
if (selectedValues.length === 0) {
return candidate.score;
}
const nearestDistance = Math.min(
...selectedValues.map((item) => Math.abs(item.time - candidate.time)),
candidate.time,
Math.max(0, duration - candidate.time),
);
return nearestDistance * 2 + candidate.score;
}
+25 -1
View File
@@ -21,9 +21,12 @@ export async function waitForVideoMetadata(video: HTMLVideoElement): Promise<voi
} }
export async function seekVideo(video: HTMLVideoElement, time: number): Promise<void> { export async function seekVideo(video: HTMLVideoElement, time: number): Promise<void> {
const target = Math.min(Math.max(time, 0), video.duration || time); const duration = Number.isFinite(video.duration) ? video.duration : time;
const frameSafety = 1 / 120;
const target = Math.min(Math.max(time, 0), Math.max(0, duration - frameSafety));
if (Math.abs(video.currentTime - target) < 0.01 && video.readyState >= 2) { if (Math.abs(video.currentTime - target) < 0.01 && video.readyState >= 2) {
await waitForRenderableFrame(video);
return; return;
} }
@@ -53,4 +56,25 @@ export async function seekVideo(video: HTMLVideoElement, time: number): Promise<
queueMicrotask(finish); queueMicrotask(finish);
} }
}); });
await waitForRenderableFrame(video);
}
async function waitForRenderableFrame(video: HTMLVideoElement): Promise<void> {
if ("requestVideoFrameCallback" in video) {
await new Promise<void>((resolve) => {
const handle = (
video as HTMLVideoElement & {
requestVideoFrameCallback: (callback: () => void) => number;
}
).requestVideoFrameCallback(() => resolve());
if (!handle && video.readyState >= 2) {
resolve();
}
});
return;
}
await new Promise<void>((resolve) => requestAnimationFrame(() => requestAnimationFrame(() => resolve())));
} }