Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions src/lib/exporter/timelineSegments.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,29 @@ describe("splitBySpeed", () => {
]);
});

it("keeps output disjoint when speed regions overlap", () => {
expect(splitBySpeed(full, [speed(1000, 5000, 2), speed(4000, 7000, 3)])).toEqual([
{ startSec: 0, endSec: 1, speed: 1 },
{ startSec: 1, endSec: 5, speed: 2 },
{ startSec: 5, endSec: 7, speed: 3 },
{ startSec: 7, endSec: 10, speed: 1 },
]);
});

// Not a variant of the test above: there the two regions merely overlap, here the
// second is fully swallowed by the first. main emitted
// `[0-1 x1, 1-8 x2, 3-5 x3, 5-10 x1]` — the third entry starts BEFORE the second
// ends, so the list stops being ascending and 5-8 ships twice. `decodeAll` walks
// these with a forward-only frame cursor, so that is a backwards seek, not just a
// duplicated stretch. Keep both cases.
it("ignores a later speed region fully covered by the earliest region", () => {
Comment thread
EtienneLescot marked this conversation as resolved.
expect(splitBySpeed(full, [speed(1000, 8000, 2), speed(3000, 5000, 3)])).toEqual([
{ startSec: 0, endSec: 1, speed: 1 },
{ startSec: 1, endSec: 8, speed: 2 },
{ startSec: 8, endSec: 10, speed: 1 },
]);
});

it("drops sub-segments narrower than the minimum width", () => {
// A speed region ending a sliver before the segment end must not emit a 1x crumb.
const result = splitBySpeed(full, [speed(0, 9_999.95, 2)]);
Expand Down
17 changes: 13 additions & 4 deletions src/lib/exporter/timelineSegments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,14 @@ export function computeKeepSegments(

/**
* Splits keep-segments by overlapping speed regions, annotating each sub-segment
* with its playback speed multiplier (defaults to 1×). Regions are assumed
* non-overlapping; when they do overlap the earliest-starting one wins.
* with its playback speed multiplier (defaults to 1×).
*
* Overlapping regions are handled rather than assumed away: the earliest-starting
* one keeps the stretch it already covers, and a later region contributes only the
* part past the cursor (nothing at all when it is fully covered). Output is always
* disjoint and ascending — `decodeAll` walks it with a forward-only frame cursor,
* so an overlap would otherwise duplicate source and seek backwards. Same rule as
* the native path's `speed_segments_for_window` in `crates/compositor/src/regions.rs`.
*/
export function splitBySpeed(
segments: TimelineSegment[],
Expand All @@ -80,8 +86,11 @@ export function splitBySpeed(
for (const sr of overlapping) {
const srStart = Math.max(sr.startMs / 1000, segment.startSec);
const srEnd = Math.min(sr.endMs / 1000, segment.endSec);
if (cursor < srStart) result.push({ startSec: cursor, endSec: srStart, speed: 1 });
result.push({ startSec: srStart, endSec: srEnd, speed: sr.speed });
if (srEnd <= cursor) continue;
Comment thread
EtienneLescot marked this conversation as resolved.
const effectiveStart = Math.max(srStart, cursor);
if (cursor < effectiveStart)
result.push({ startSec: cursor, endSec: effectiveStart, speed: 1 });
result.push({ startSec: effectiveStart, endSec: srEnd, speed: sr.speed });
cursor = srEnd;
}
if (cursor < segment.endSec)
Expand Down
Loading