fix(core): account for playbackRate in media duration resolution (#1287)

* fix(core): account for playbackRate in media duration resolution

resolveDurationSeconds computed sourceDuration as (element.duration - mediaStart)
without dividing by playbackRate. A 5s source at 0.5x should span 10s on the
timeline, but was capped at 5s — causing the video to go black once the raw
source was exhausted.

Read defaultPlaybackRate from the element (same clamping as refreshRuntimeMediaCache)
and divide sourceDuration by it so the effective timeline window matches the
actual playback speed.

* test(core): add regression test for playbackRate in resolveDurationSeconds

Pins the fix: a 5s source at 0.5x playbackRate must resolve to 10s effective
duration when resolveDurationSeconds is provided (mirroring the init.ts callback
pattern). Without the rate division, this would return 5s and clip early.
This commit is contained in:
Miguel Ángel
2026-06-08 21:17:41 -04:00
committed by GitHub
parent c12987e301
commit 7b6c87fa88
2 changed files with 26 additions and 1 deletions
+4 -1
View File
@@ -1356,6 +1356,9 @@ export function initSandboxRuntimeModular(): void {
const mediaStart =
Number.parseFloat(element.dataset.playbackStart ?? element.dataset.mediaStart ?? "0") ||
0;
const rawRate = element.defaultPlaybackRate;
const playbackRate =
Number.isFinite(rawRate) && rawRate > 0 ? Math.max(0.1, Math.min(5, rawRate)) : 1;
const hostRemaining =
context.inheritedStart != null &&
context.inheritedDuration != null &&
@@ -1364,7 +1367,7 @@ export function initSandboxRuntimeModular(): void {
: null;
const sourceDuration =
Number.isFinite(element.duration) && element.duration > mediaStart
? Math.max(0, element.duration - mediaStart)
? Math.max(0, (element.duration - mediaStart) / playbackRate)
: null;
if (sourceDuration != null && hostRemaining != null) {
return Math.min(sourceDuration, hostRemaining);
+22
View File
@@ -131,6 +131,28 @@ describe("refreshRuntimeMediaCache", () => {
expect(result.mediaClips[0].duration).toBe(20);
});
it("resolveDurationSeconds must account for playbackRate (regression: clip clipped early)", () => {
const el = createVideo({ "data-start": "0", "data-duration": "10" });
Object.defineProperty(el, "defaultPlaybackRate", { value: 0.5, writable: true });
Object.defineProperty(el, "duration", { value: 5, writable: true });
const result = refreshRuntimeMediaCache({
resolveDurationSeconds: (element) => {
const mediaStart =
Number.parseFloat(element.dataset.playbackStart ?? element.dataset.mediaStart ?? "0") ||
0;
const rawRate = element.defaultPlaybackRate;
const playbackRate =
Number.isFinite(rawRate) && rawRate > 0 ? Math.max(0.1, Math.min(5, rawRate)) : 1;
return Number.isFinite(element.duration) && element.duration > mediaStart
? Math.max(0, (element.duration - mediaStart) / playbackRate)
: null;
},
});
// 5s source at 0.5x = 10s effective; should NOT be capped to 5s
expect(result.mediaClips[0].duration).toBe(10);
expect(result.mediaClips[0].end).toBe(10);
});
it("reads native loop attribute", () => {
createVideo({ "data-start": "0", "data-duration": "15", loop: "" });
const result = refreshRuntimeMediaCache();