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++) {
|
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
@@ -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())));
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user