From 7e65d52847bb138f101e3731e6f363c3a3b058fc Mon Sep 17 00:00:00 2001 From: Siddharth Date: Tue, 17 Mar 2026 20:07:15 -0700 Subject: [PATCH] fix --- src/lib/exporter/streamingDecoder.test.ts | 23 +++++++----- src/lib/exporter/streamingDecoder.ts | 44 +++++++++++++++-------- tests/e2e/gif-export.spec.ts | 4 +-- 3 files changed, 47 insertions(+), 24 deletions(-) diff --git a/src/lib/exporter/streamingDecoder.test.ts b/src/lib/exporter/streamingDecoder.test.ts index 18b0672..1969c84 100644 --- a/src/lib/exporter/streamingDecoder.test.ts +++ b/src/lib/exporter/streamingDecoder.test.ts @@ -6,22 +6,20 @@ describe("shouldFailDecodeEndedEarly", () => { expect( shouldFailDecodeEndedEarly({ cancelled: false, - segmentsLength: 1, - completedSegments: 1, lastDecodedFrameSec: 5.33, requiredEndSec: 6.498, + streamDurationSec: 5.33, }), ).toBe(false); }); - it("fails when decode stops before the remaining segments can be covered", () => { + it("fails when decode stops far before the required end", () => { expect( shouldFailDecodeEndedEarly({ cancelled: false, - segmentsLength: 2, - completedSegments: 1, lastDecodedFrameSec: 5.33, - requiredEndSec: 6.498, + requiredEndSec: 10, + streamDurationSec: 5.33, }), ).toBe(true); }); @@ -30,11 +28,20 @@ describe("shouldFailDecodeEndedEarly", () => { expect( shouldFailDecodeEndedEarly({ cancelled: false, - segmentsLength: 1, - completedSegments: 0, lastDecodedFrameSec: null, requiredEndSec: 1, }), ).toBe(true); }); + + it("fails when the decoder has not reached the reported stream end", () => { + expect( + shouldFailDecodeEndedEarly({ + cancelled: false, + lastDecodedFrameSec: 4.9, + requiredEndSec: 6.498, + streamDurationSec: 5.33, + }), + ).toBe(true); + }); }); diff --git a/src/lib/exporter/streamingDecoder.ts b/src/lib/exporter/streamingDecoder.ts index b4d7549..d994b66 100644 --- a/src/lib/exporter/streamingDecoder.ts +++ b/src/lib/exporter/streamingDecoder.ts @@ -14,26 +14,22 @@ export interface DecodedVideoInfo { type EarlyDecodeEndCheck = { cancelled: boolean; - segmentsLength: number; - completedSegments: number; lastDecodedFrameSec: number | null; requiredEndSec: number; + streamDurationSec?: number; }; +const EARLY_DECODE_END_THRESHOLD_SEC = 1; +const METADATA_TAIL_TOLERANCE_SEC = 1.5; +const STREAM_DURATION_MATCH_TOLERANCE_SEC = 0.25; + export function shouldFailDecodeEndedEarly({ cancelled, - segmentsLength, - completedSegments, lastDecodedFrameSec, requiredEndSec, + streamDurationSec, }: EarlyDecodeEndCheck): boolean { - if (cancelled || segmentsLength === 0) { - return false; - } - - // If we already satisfied every segment, the exporter has successfully - // filled any short metadata tail using the last decoded frame. - if (completedSegments >= segmentsLength) { + if (cancelled || requiredEndSec <= 0) { return false; } @@ -41,7 +37,28 @@ export function shouldFailDecodeEndedEarly({ return true; } - return requiredEndSec - lastDecodedFrameSec > 1; + const decodeGapSec = requiredEndSec - lastDecodedFrameSec; + if (decodeGapSec <= EARLY_DECODE_END_THRESHOLD_SEC) { + return false; + } + + if (typeof streamDurationSec !== "number" || !Number.isFinite(streamDurationSec)) { + return true; + } + + const metadataTailSec = requiredEndSec - streamDurationSec; + const decodedNearStreamEnd = + Math.abs(lastDecodedFrameSec - streamDurationSec) <= STREAM_DURATION_MATCH_TOLERANCE_SEC; + + if ( + decodedNearStreamEnd && + metadataTailSec > 0 && + metadataTailSec <= METADATA_TAIL_TOLERANCE_SEC + ) { + return false; + } + + return true; } /** Caller must close the VideoFrame after use. */ @@ -400,10 +417,9 @@ export class StreamingVideoDecoder { if ( shouldFailDecodeEndedEarly({ cancelled: this.cancelled, - segmentsLength: segments.length, - completedSegments: segmentIdx, lastDecodedFrameSec, requiredEndSec, + streamDurationSec: this.metadata.streamDuration, }) ) { const decodedAtLabel = diff --git a/tests/e2e/gif-export.spec.ts b/tests/e2e/gif-export.spec.ts index b18fd44..a851546 100644 --- a/tests/e2e/gif-export.spec.ts +++ b/tests/e2e/gif-export.spec.ts @@ -87,8 +87,8 @@ test("exports a GIF from a loaded video", async () => { await editorWindow.getByTestId("testId-gif-format-button").click(); await editorWindow.getByTestId("testId-export-button").click(); - // ── 6. Wait for the toast to say exported successfully - await expect(editorWindow.getByText(`GIF exported successfully to pending`)).toBeVisible({ + // ── 6. Wait for the success toast. + await expect(editorWindow.getByText("GIF exported successfully")).toBeVisible({ timeout: 90_000, });