mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 04:38:33 +00:00
fix(core): apply playbackRate to all media duration resolution sites (#1288)
Extract readElementPlaybackRate() to eliminate clamping duplication across media.ts, init.ts, startResolver.ts, and timeline.ts. Apply the rate division to the two remaining sites that were missed: - startResolver.ts: visibility loop used raw source duration, hiding slowed-down videos mid-playback when no data-duration was set - timeline.ts: resolveMediaElementDurationSeconds underreported the end window sent to the renderer, affecting preview parity Also adds direct tests for readElementPlaybackRate().
This commit is contained in:
@@ -9,7 +9,7 @@ import { createThreeAdapter } from "./adapters/three";
|
||||
import { createTypegpuAdapter } from "./adapters/typegpu";
|
||||
import { patchVideoTextureCompat } from "./adapters/video-texture-compat";
|
||||
import { createWaapiAdapter } from "./adapters/waapi";
|
||||
import { refreshRuntimeMediaCache, syncRuntimeMedia } from "./media";
|
||||
import { readElementPlaybackRate, refreshRuntimeMediaCache, syncRuntimeMedia } from "./media";
|
||||
import { probeAndCacheElementVolume, type VolumeKeyframe } from "./mediaVolumeEnvelope.js";
|
||||
import { createPickerModule } from "./picker";
|
||||
import { createRuntimePlayer } from "./player";
|
||||
@@ -1356,9 +1356,7 @@ 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 playbackRate = readElementPlaybackRate(element);
|
||||
const hostRemaining =
|
||||
context.inheritedStart != null &&
|
||||
context.inheritedDuration != null &&
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, it, expect, vi, afterEach } from "vitest";
|
||||
import { refreshRuntimeMediaCache, syncRuntimeMedia } from "./media";
|
||||
import { readElementPlaybackRate, refreshRuntimeMediaCache, syncRuntimeMedia } from "./media";
|
||||
import type { RuntimeMediaClip } from "./media";
|
||||
|
||||
function createVideo(attrs: Record<string, string>): HTMLVideoElement {
|
||||
@@ -23,6 +23,37 @@ function createAudio(attrs: Record<string, string>): HTMLAudioElement {
|
||||
return el;
|
||||
}
|
||||
|
||||
describe("readElementPlaybackRate", () => {
|
||||
it("reads defaultPlaybackRate from element", () => {
|
||||
const el = document.createElement("video");
|
||||
Object.defineProperty(el, "defaultPlaybackRate", { value: 0.5, writable: true });
|
||||
expect(readElementPlaybackRate(el)).toBe(0.5);
|
||||
});
|
||||
|
||||
it("defaults to 1 when not set", () => {
|
||||
const el = document.createElement("video");
|
||||
expect(readElementPlaybackRate(el)).toBe(1);
|
||||
});
|
||||
|
||||
it("clamps to [0.1, 5]", () => {
|
||||
const el = document.createElement("video");
|
||||
Object.defineProperty(el, "defaultPlaybackRate", { value: 0.01, writable: true });
|
||||
expect(readElementPlaybackRate(el)).toBe(0.1);
|
||||
Object.defineProperty(el, "defaultPlaybackRate", { value: 10, writable: true });
|
||||
expect(readElementPlaybackRate(el)).toBe(5);
|
||||
});
|
||||
|
||||
it("defaults to 1 for NaN/negative/zero", () => {
|
||||
const el = document.createElement("video");
|
||||
Object.defineProperty(el, "defaultPlaybackRate", { value: NaN, writable: true });
|
||||
expect(readElementPlaybackRate(el)).toBe(1);
|
||||
Object.defineProperty(el, "defaultPlaybackRate", { value: -1, writable: true });
|
||||
expect(readElementPlaybackRate(el)).toBe(1);
|
||||
Object.defineProperty(el, "defaultPlaybackRate", { value: 0, writable: true });
|
||||
expect(readElementPlaybackRate(el)).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("refreshRuntimeMediaCache", () => {
|
||||
afterEach(() => {
|
||||
document.body.innerHTML = "";
|
||||
@@ -140,9 +171,7 @@ describe("refreshRuntimeMediaCache", () => {
|
||||
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 playbackRate = readElementPlaybackRate(element);
|
||||
return Number.isFinite(element.duration) && element.duration > mediaStart
|
||||
? Math.max(0, (element.duration - mediaStart) / playbackRate)
|
||||
: null;
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import { swallow } from "./diagnostics";
|
||||
import { interpolateVolumeGain, type VolumeKeyframe } from "./mediaVolumeEnvelope.js";
|
||||
|
||||
export function readElementPlaybackRate(el: HTMLMediaElement): number {
|
||||
const raw = el.defaultPlaybackRate;
|
||||
return Number.isFinite(raw) && raw > 0 ? Math.max(0.1, Math.min(5, raw)) : 1;
|
||||
}
|
||||
|
||||
export type RuntimeMediaClip = {
|
||||
el: HTMLVideoElement | HTMLAudioElement;
|
||||
start: number;
|
||||
@@ -47,11 +52,7 @@ export function refreshRuntimeMediaCache(params?: {
|
||||
if (!Number.isFinite(start)) continue;
|
||||
const mediaStart =
|
||||
Number.parseFloat(el.dataset.playbackStart ?? el.dataset.mediaStart ?? "0") || 0;
|
||||
// Read per-element rate from the native defaultPlaybackRate property.
|
||||
// LLMs set this via el.defaultPlaybackRate = 0.5 in a <script> tag.
|
||||
const rawRate = el.defaultPlaybackRate;
|
||||
const playbackRate =
|
||||
Number.isFinite(rawRate) && rawRate > 0 ? Math.max(0.1, Math.min(5, rawRate)) : 1;
|
||||
const playbackRate = readElementPlaybackRate(el);
|
||||
const loop = el.loop;
|
||||
const sourceDuration = Number.isFinite(el.duration) && el.duration > 0 ? el.duration : null;
|
||||
let duration =
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { RuntimeTimelineLike } from "./types";
|
||||
import { swallow } from "./diagnostics";
|
||||
import { readElementPlaybackRate } from "./media";
|
||||
|
||||
const AUTHORED_DURATION_ATTR = "data-hf-authored-duration";
|
||||
const AUTHORED_END_ATTR = "data-hf-authored-end";
|
||||
@@ -106,7 +107,7 @@ export function createRuntimeStartTimeResolver(params: {
|
||||
parseNumeric(element.getAttribute("data-media-start")) ??
|
||||
0;
|
||||
if (Number.isFinite(element.duration) && element.duration > playbackStart) {
|
||||
resolved = element.duration - playbackStart;
|
||||
resolved = (element.duration - playbackStart) / readElementPlaybackRate(element);
|
||||
}
|
||||
}
|
||||
if (resolved == null || resolved <= 0) {
|
||||
|
||||
@@ -5,6 +5,7 @@ import type {
|
||||
RuntimeTimelineLike,
|
||||
} from "./types";
|
||||
import { swallow } from "./diagnostics";
|
||||
import { readElementPlaybackRate } from "./media";
|
||||
import { createRuntimeStartTimeResolver } from "./startResolver";
|
||||
|
||||
const AUTHORED_DURATION_ATTR = "data-hf-authored-duration";
|
||||
@@ -208,7 +209,7 @@ export function collectRuntimeTimelinePayload(params: {
|
||||
parseNum(mediaEl.getAttribute("data-media-start")) ??
|
||||
0;
|
||||
if (Number.isFinite(mediaEl.duration) && mediaEl.duration > playbackStart) {
|
||||
return Math.max(0, mediaEl.duration - playbackStart);
|
||||
return Math.max(0, (mediaEl.duration - playbackStart) / readElementPlaybackRate(mediaEl));
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user