feat: enhance candidate ranking logic and improve video seeking functionality
This commit is contained in:
@@ -39,16 +39,16 @@ export function rankAndTrimCandidates(
|
||||
for (let bucket = 0; bucket < bucketCount; bucket++) {
|
||||
const start = bucket * bucketSize;
|
||||
const end = bucket === bucketCount - 1 ? duration + 0.001 : start + bucketSize;
|
||||
const best = pool
|
||||
.filter((candidate) => candidate.time >= start && candidate.time < end)
|
||||
.sort((a, b) => b.score - a.score)[0];
|
||||
const best = bestCandidateInWindow(pool, start, end);
|
||||
|
||||
if (best && respectsMinGap([...selected.values()], best, minGapSeconds)) {
|
||||
if (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 (respectsMinGap([...selected.values()], candidate, minGapSeconds)) {
|
||||
selected.set(frameKey(candidate.time), candidate);
|
||||
@@ -89,3 +89,34 @@ function frameKey(time: number) {
|
||||
function respectsMinGap(selected: ScoredFrame[], candidate: ScoredFrame, minGapSeconds: number) {
|
||||
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
@@ -21,9 +21,12 @@ export async function waitForVideoMetadata(video: HTMLVideoElement): Promise<voi
|
||||
}
|
||||
|
||||
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) {
|
||||
await waitForRenderableFrame(video);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -53,4 +56,25 @@ export async function seekVideo(video: HTMLVideoElement, time: number): Promise<
|
||||
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())));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user