Skip to content

Commit e2c4f3f

Browse files
Merge pull request #414 from theopfr/fix/correct-frame-count
fix: export frame counter exceeding total frames
2 parents 8aa8541 + 14bbe8f commit e2c4f3f

3 files changed

Lines changed: 31 additions & 17 deletions

File tree

src/lib/exporter/gifExporter.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -174,11 +174,11 @@ export class GifExporter {
174174
});
175175

176176
// Calculate effective duration and frame count (excluding trim regions)
177-
const effectiveDuration = this.streamingDecoder.getEffectiveDuration(
177+
const { effectiveDuration, totalFrames } = this.streamingDecoder.getExportMetrics(
178+
this.config.frameRate,
178179
this.config.trimRegions,
179180
this.config.speedRegions,
180181
);
181-
const totalFrames = Math.ceil(effectiveDuration * this.config.frameRate);
182182

183183
// Calculate frame delay in milliseconds (gif.js uses ms)
184184
const frameDelay = Math.round(1000 / this.config.frameRate);

src/lib/exporter/streamingDecoder.ts

Lines changed: 27 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { WebDemuxer } from "web-demuxer";
22
import type { SpeedRegion, TrimRegion } from "@/components/video-editor/types";
33

44
const SOURCE_LOAD_TIMEOUT_MS = 60_000;
5-
5+
const EPSILON_SEC = 0.001;
66
/**
77
* Build a full WebCodecs-compatible AV1 codec string from the AV1CodecConfigurationRecord.
88
* web-demuxer may return a bare "av01" when the WASM-side parser fails to read
@@ -246,10 +246,11 @@ export class StreamingVideoDecoder {
246246
speedRegions,
247247
);
248248
const segmentOutputFrameCounts = segments.map((segment) =>
249-
Math.ceil(((segment.endSec - segment.startSec) / segment.speed) * targetFrameRate),
249+
Math.ceil(
250+
((segment.endSec - segment.startSec - EPSILON_SEC) / segment.speed) * targetFrameRate,
251+
),
250252
);
251253
const frameDurationUs = 1_000_000 / targetFrameRate;
252-
const epsilonSec = 0.001;
253254

254255
// Async frame queue — decoder pushes, consumer pulls
255256
const pendingFrames: VideoFrame[] = [];
@@ -360,7 +361,7 @@ export class StreamingVideoDecoder {
360361

361362
const sourceTimeSec =
362363
segment.startSec + (segmentFrameIndex / targetFrameRate) * segment.speed;
363-
if (sourceTimeSec >= segment.endSec - epsilonSec) return false;
364+
if (sourceTimeSec >= segment.endSec - EPSILON_SEC) return false;
364365

365366
const clone = new VideoFrame(heldFrame, { timestamp: heldFrame.timestamp });
366367
await onFrame(clone, exportFrameIndex * frameDurationUs, sourceTimeSec * 1000);
@@ -379,7 +380,7 @@ export class StreamingVideoDecoder {
379380
// Finalize completed segments before handling this frame.
380381
while (
381382
segmentIdx < segments.length &&
382-
frameTimeSec >= segments[segmentIdx].endSec - epsilonSec
383+
frameTimeSec >= segments[segmentIdx].endSec - EPSILON_SEC
383384
) {
384385
const segment = segments[segmentIdx];
385386
while (!this.cancelled && (await emitHeldFrameForTarget(segment))) {
@@ -391,7 +392,7 @@ export class StreamingVideoDecoder {
391392
if (
392393
heldFrame &&
393394
segmentIdx < segments.length &&
394-
heldFrameSec < segments[segmentIdx].startSec - epsilonSec
395+
heldFrameSec < segments[segmentIdx].startSec - EPSILON_SEC
395396
) {
396397
heldFrame.close();
397398
heldFrame = null;
@@ -406,7 +407,7 @@ export class StreamingVideoDecoder {
406407
const currentSegment = segments[segmentIdx];
407408

408409
// Before current segment (trimmed region or pre-roll).
409-
if (frameTimeSec < currentSegment.startSec - epsilonSec) {
410+
if (frameTimeSec < currentSegment.startSec - EPSILON_SEC) {
410411
frame.close();
411412
continue;
412413
}
@@ -427,7 +428,7 @@ export class StreamingVideoDecoder {
427428

428429
const sourceTimeSec =
429430
currentSegment.startSec + (segmentFrameIndex / targetFrameRate) * currentSegment.speed;
430-
if (sourceTimeSec >= currentSegment.endSec - epsilonSec) {
431+
if (sourceTimeSec >= currentSegment.endSec - EPSILON_SEC) {
431432
break;
432433
}
433434
if (sourceTimeSec > handoffBoundarySec) {
@@ -449,7 +450,7 @@ export class StreamingVideoDecoder {
449450
if (heldFrame && segmentIdx < segments.length) {
450451
while (!this.cancelled && segmentIdx < segments.length) {
451452
const segment = segments[segmentIdx];
452-
if (heldFrameSec < segment.startSec - epsilonSec) {
453+
if (heldFrameSec < segment.startSec - EPSILON_SEC) {
453454
break;
454455
}
455456

@@ -461,7 +462,7 @@ export class StreamingVideoDecoder {
461462
segmentFrameIndex = 0;
462463
if (
463464
segmentIdx < segments.length &&
464-
heldFrameSec < segments[segmentIdx].startSec - epsilonSec
465+
heldFrameSec < segments[segmentIdx].startSec - EPSILON_SEC
465466
) {
466467
break;
467468
}
@@ -536,11 +537,24 @@ export class StreamingVideoDecoder {
536537
return segments;
537538
}
538539

539-
getEffectiveDuration(trimRegions?: TrimRegion[], speedRegions?: SpeedRegion[]): number {
540+
getExportMetrics(
541+
targetFrameRate: number,
542+
trimRegions?: TrimRegion[],
543+
speedRegions?: SpeedRegion[],
544+
): { effectiveDuration: number; totalFrames: number } {
540545
if (!this.metadata) throw new Error("Must call loadMetadata() first");
541546
const trimSegments = this.computeSegments(this.metadata.duration, trimRegions);
542-
const speedSegments = this.splitBySpeed(trimSegments, speedRegions);
543-
return speedSegments.reduce((sum, seg) => sum + (seg.endSec - seg.startSec) / seg.speed, 0);
547+
const segments = this.splitBySpeed(trimSegments, speedRegions);
548+
return {
549+
effectiveDuration: segments.reduce(
550+
(sum, seg) => sum + (seg.endSec - seg.startSec) / seg.speed,
551+
0,
552+
),
553+
totalFrames: segments.reduce((sum, seg) => {
554+
const segDur = seg.endSec - seg.startSec - EPSILON_SEC;
555+
return sum + Math.max(0, Math.ceil((segDur / seg.speed) * targetFrameRate));
556+
}, 0),
557+
};
544558
}
545559

546560
private splitBySpeed(

src/lib/exporter/videoExporter.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -157,11 +157,11 @@ export class VideoExporter {
157157
this.muxer = muxer;
158158
await muxer.initialize();
159159

160-
const effectiveDuration = streamingDecoder.getEffectiveDuration(
160+
const { effectiveDuration, totalFrames } = streamingDecoder.getExportMetrics(
161+
this.config.frameRate,
161162
this.config.trimRegions,
162163
this.config.speedRegions,
163164
);
164-
const totalFrames = Math.ceil(effectiveDuration * this.config.frameRate);
165165
const readEndSec = Math.max(videoInfo.duration, videoInfo.streamDuration ?? 0) + 0.5;
166166

167167
console.log("[VideoExporter] Original duration:", videoInfo.duration, "s");

0 commit comments

Comments
 (0)