mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-01 19:42:03 +00:00
fix(player): clamp playbackRate to runtime [0.1, 5] range (#1120)
This commit is contained in:
@@ -1496,3 +1496,121 @@ describe("HyperframesPlayer volume and mute", () => {
|
||||
expect(zeroVolumeHtml).not.toBe(mutedHtml);
|
||||
});
|
||||
});
|
||||
|
||||
// ── Playback rate ──
|
||||
|
||||
describe("HyperframesPlayer playback rate", () => {
|
||||
let player: HTMLElement & {
|
||||
playbackRate: number;
|
||||
iframeElement: HTMLIFrameElement;
|
||||
};
|
||||
let mockAudio: {
|
||||
preload: string;
|
||||
src: string;
|
||||
muted: boolean;
|
||||
volume: number;
|
||||
playbackRate: number;
|
||||
currentTime: number;
|
||||
load: ReturnType<typeof vi.fn>;
|
||||
play: ReturnType<typeof vi.fn>;
|
||||
pause: ReturnType<typeof vi.fn>;
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
await import("./hyperframes-player.js");
|
||||
|
||||
mockAudio = {
|
||||
preload: "",
|
||||
src: "",
|
||||
muted: false,
|
||||
volume: 1,
|
||||
playbackRate: 1,
|
||||
currentTime: 0,
|
||||
load: vi.fn(),
|
||||
play: vi.fn().mockResolvedValue(undefined),
|
||||
pause: vi.fn(),
|
||||
};
|
||||
vi.spyOn(globalThis, "Audio").mockImplementation(
|
||||
() => mockAudio as unknown as HTMLAudioElement,
|
||||
);
|
||||
|
||||
player = document.createElement("hyperframes-player") as typeof player;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
document.body.innerHTML = "";
|
||||
});
|
||||
|
||||
it("defaults playbackRate to 1", () => {
|
||||
document.body.appendChild(player);
|
||||
expect(player.playbackRate).toBe(1);
|
||||
});
|
||||
|
||||
it("clamps playbackRate to [0.1, 5]", () => {
|
||||
document.body.appendChild(player);
|
||||
|
||||
player.playbackRate = 100;
|
||||
expect(player.playbackRate).toBe(5);
|
||||
// Assert the reflected attribute directly so the setter clamp is pinned
|
||||
// independently of the getter clamp.
|
||||
expect(player.getAttribute("playback-rate")).toBe("5");
|
||||
|
||||
player.playbackRate = 0.01;
|
||||
expect(player.playbackRate).toBe(0.1);
|
||||
expect(player.getAttribute("playback-rate")).toBe("0.1");
|
||||
});
|
||||
|
||||
it("falls back to 1 for a non-positive or non-finite playbackRate", () => {
|
||||
document.body.appendChild(player);
|
||||
|
||||
player.playbackRate = -2;
|
||||
expect(player.playbackRate).toBe(1);
|
||||
expect(player.getAttribute("playback-rate")).toBe("1");
|
||||
|
||||
player.playbackRate = 0;
|
||||
expect(player.playbackRate).toBe(1);
|
||||
|
||||
player.playbackRate = NaN;
|
||||
expect(player.playbackRate).toBe(1);
|
||||
});
|
||||
|
||||
it("propagates the clamped rate to parent media, never an out-of-range value", () => {
|
||||
player.setAttribute("audio-src", "https://cdn.example.com/narration.mp3");
|
||||
document.body.appendChild(player);
|
||||
|
||||
player.setAttribute("playback-rate", "50");
|
||||
expect(mockAudio.playbackRate).toBe(5);
|
||||
|
||||
player.setAttribute("playback-rate", "0.01");
|
||||
expect(mockAudio.playbackRate).toBe(0.1);
|
||||
});
|
||||
|
||||
it("sends the clamped rate as a set-playback-rate control to the iframe", () => {
|
||||
document.body.appendChild(player);
|
||||
|
||||
const postMessageSpy = vi.fn();
|
||||
Object.defineProperty(player.iframeElement, "contentWindow", {
|
||||
value: { postMessage: postMessageSpy },
|
||||
configurable: true,
|
||||
});
|
||||
|
||||
player.setAttribute("playback-rate", "50");
|
||||
expect(postMessageSpy).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
source: "hf-parent",
|
||||
type: "control",
|
||||
action: "set-playback-rate",
|
||||
playbackRate: 5,
|
||||
}),
|
||||
"*",
|
||||
);
|
||||
});
|
||||
|
||||
it("clamps an out-of-range value set directly via the attribute on read", () => {
|
||||
document.body.appendChild(player);
|
||||
|
||||
player.setAttribute("playback-rate", "999");
|
||||
expect(player.playbackRate).toBe(5);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -18,6 +18,21 @@ import { ShaderLoaderState } from "./shader-loader-state.js";
|
||||
import { PLAYER_STYLES } from "./styles.js";
|
||||
import { type DirectTimelineAdapter } from "./timeline-adapters.js";
|
||||
|
||||
// Playback-rate bounds mirror the runtime clamp in
|
||||
// packages/core/src/runtime/init.ts (applyPlaybackRate) and media.ts so the
|
||||
// player accepts the same range as the in-iframe runtime: an out-of-range rate
|
||||
// would otherwise drive the parent-proxied <audio> outside the bounds the
|
||||
// timeline itself respects. Clamping here also shields the native
|
||||
// HTMLMediaElement.playbackRate setter, which throws for extreme values in
|
||||
// production browsers.
|
||||
const MIN_PLAYBACK_RATE = 0.1;
|
||||
const MAX_PLAYBACK_RATE = 5;
|
||||
|
||||
function clampPlaybackRate(rate: number): number {
|
||||
if (!Number.isFinite(rate) || rate <= 0) return 1;
|
||||
return Math.max(MIN_PLAYBACK_RATE, Math.min(MAX_PLAYBACK_RATE, rate));
|
||||
}
|
||||
|
||||
class HyperframesPlayer extends HTMLElement {
|
||||
static get observedAttributes() {
|
||||
return [
|
||||
@@ -174,7 +189,7 @@ class HyperframesPlayer extends HTMLElement {
|
||||
this.posterEl = setupPoster(this.shadow, val, this.posterEl);
|
||||
break;
|
||||
case "playback-rate": {
|
||||
const rate = parseFloat(val || "1");
|
||||
const rate = clampPlaybackRate(parseFloat(val || "1"));
|
||||
this._media.updatePlaybackRate(rate);
|
||||
this._sendControl("set-playback-rate", { playbackRate: rate });
|
||||
this._directTimelineAdapter?.timeScale?.(rate);
|
||||
@@ -296,10 +311,10 @@ class HyperframesPlayer extends HTMLElement {
|
||||
}
|
||||
|
||||
get playbackRate() {
|
||||
return parseFloat(this.getAttribute("playback-rate") || "1");
|
||||
return clampPlaybackRate(parseFloat(this.getAttribute("playback-rate") || "1"));
|
||||
}
|
||||
set playbackRate(r: number) {
|
||||
this.setAttribute("playback-rate", String(r));
|
||||
this.setAttribute("playback-rate", String(clampPlaybackRate(r)));
|
||||
}
|
||||
|
||||
get shaderCaptureScale() {
|
||||
|
||||
Reference in New Issue
Block a user