diff --git a/crates/compositor/src/regions.rs b/crates/compositor/src/regions.rs index a79ae67a0..8910070c3 100644 --- a/crates/compositor/src/regions.rs +++ b/crates/compositor/src/regions.rs @@ -49,18 +49,22 @@ pub fn speed_segments_for_window( for region in overlapping { let start = region.start_sec.max(source_start_sec).max(cursor); let end = region.end_sec.min(source_end_sec); + // Projection can leave a zero-width region at a trim boundary. It may still pass + // the overlap filter when rounding puts its start just inside the clip window. Skip + // it before emitting the 1x gap, or the unchanged cursor makes that gap render twice. + if end <= start { + continue; + } if start > cursor { push_speed_segment(&mut spans, cursor, start, 1.0, fps); } - if end > start { - let speed = if region.speed.is_finite() && region.speed > 0.0 { - region.speed - } else { - 1.0 - }; - push_speed_segment(&mut spans, start, end, speed, fps); - cursor = end; - } + let speed = if region.speed.is_finite() && region.speed > 0.0 { + region.speed + } else { + 1.0 + }; + push_speed_segment(&mut spans, start, end, speed, fps); + cursor = end; } if cursor < source_end_sec { push_speed_segment(&mut spans, cursor, source_end_sec, 1.0, fps); @@ -3133,6 +3137,16 @@ mod exporter_frame_totals { 60, "région débordant la fenêtre gardée" ); + let issue_876_fps = 60.0; + let first_clip = frames( + 0.0, + 44.81830642526596, + &[region(44.818, 44.818, 1.5)], + issue_876_fps, + ); + let second_clip = frames(0.0, 47.833333, &[region(0.0, 1.87, 1.5)], issue_876_fps); + assert_eq!(first_clip, 2690, "une région vide ne double pas le premier clip"); + assert_eq!(first_clip + second_clip, 5523, "total corrigé du repro #876"); assert_eq!( frames( 0.0, diff --git a/src/lib/exporter/outputFrameCount.test.ts b/src/lib/exporter/outputFrameCount.test.ts index 55a969ac6..038eb4b42 100644 --- a/src/lib/exporter/outputFrameCount.test.ts +++ b/src/lib/exporter/outputFrameCount.test.ts @@ -60,6 +60,21 @@ describe("outputFrameCount", () => { ).toBe(60); }); + it("does not double-count a clip for an empty speed region at its trimmed edge (#876)", () => { + const clips = [ + { sourceStartSec: 0, sourceEndSec: 44.81830642526596 }, + { sourceStartSec: 0, sourceEndSec: 47.833333 }, + ]; + const regions = [ + { startSec: 44.818, endSec: 44.818, speed: 1.5, clipIndex: 0 }, + { startSec: 0, endSec: 1.87, speed: 1.5, clipIndex: 1 }, + ]; + + // The first region is empty after trim projection. Before the fix it caused the first + // clip's normal-speed prefix to be counted twice, yielding the reported 8,213 frames. + expect(outputFrameCount(clips, regions, 60)).toBe(5_523); + }); + it("never renders the same source time twice when two regions overlap", () => { // A stale payload can overlap; the first region keeps the covered portion, matching // `speed_segments_for_window`'s cursor. diff --git a/src/lib/exporter/outputFrameCount.ts b/src/lib/exporter/outputFrameCount.ts index 71424ba08..54216f012 100644 --- a/src/lib/exporter/outputFrameCount.ts +++ b/src/lib/exporter/outputFrameCount.ts @@ -75,14 +75,18 @@ export function clipOutputFrameCount( for (const region of overlapping) { const start = Math.max(region.startSec, sourceStartSec, cursor); const end = Math.min(region.endSec, sourceEndSec); + // Projection can leave a zero-width region at a trim boundary. It may still pass + // the overlap filter when rounding puts its start just inside the clip window. Skip + // it before emitting the 1x gap, or the unchanged cursor makes that gap render twice. + if (end <= start) { + continue; + } if (start > cursor) { frames += segmentFrames(cursor, start, 1, fps); } - if (end > start) { - const speed = Number.isFinite(region.speed) && region.speed > 0 ? region.speed : 1; - frames += segmentFrames(start, end, speed, fps); - cursor = end; - } + const speed = Number.isFinite(region.speed) && region.speed > 0 ? region.speed : 1; + frames += segmentFrames(start, end, speed, fps); + cursor = end; } if (cursor < sourceEndSec) { frames += segmentFrames(cursor, sourceEndSec, 1, fps);