mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-10 22:20:14 +00:00
feat(studio): add a preview volume control (#3282)
This commit is contained in:
@@ -12,5 +12,5 @@ export function scrubMusicAtSeek(iframe: HTMLIFrameElement | null, nextTime: num
|
||||
if (!music || s.audioMuted) return;
|
||||
const rel = nextTime - music.start;
|
||||
const audioFileTime = rel >= 0 && rel <= music.duration ? (music.playbackStart ?? 0) + rel : null;
|
||||
scrubPreviewAudio(iframe, audioFileTime, music.domId ?? music.id);
|
||||
scrubPreviewAudio(iframe, audioFileTime, music.domId ?? music.id, s.audioVolume);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
// @vitest-environment jsdom
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { buildMissingCompositionElements } from "./timelineIframeHelpers";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
buildMissingCompositionElements,
|
||||
scrubPreviewAudio,
|
||||
setPreviewMediaVolume,
|
||||
stopScrubPreviewAudio,
|
||||
} from "./timelineIframeHelpers";
|
||||
import type { IframeWindow } from "./playbackTypes";
|
||||
|
||||
function makeDoc(html: string): Document {
|
||||
@@ -49,3 +54,36 @@ describe("buildMissingCompositionElements — hfId (R7)", () => {
|
||||
expect(entry?.hfId).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("setPreviewMediaVolume", () => {
|
||||
it("sends a clamped runtime volume to a direct preview iframe", () => {
|
||||
const iframe = document.createElement("iframe");
|
||||
document.body.append(iframe);
|
||||
const postMessage = vi.spyOn(iframe.contentWindow!, "postMessage");
|
||||
|
||||
setPreviewMediaVolume(iframe, 1.5);
|
||||
|
||||
expect(postMessage).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ action: "set-volume", volume: 1 }),
|
||||
"*",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("scrubPreviewAudio", () => {
|
||||
it("scales scrub feedback by the Studio preview volume", () => {
|
||||
const iframe = document.createElement("iframe");
|
||||
document.body.append(iframe);
|
||||
const audio = iframe.contentDocument?.createElement("audio");
|
||||
if (!audio || !iframe.contentDocument?.body) throw new Error("expected iframe audio document");
|
||||
audio.id = "music";
|
||||
audio.play = vi.fn(async () => {});
|
||||
audio.pause = vi.fn();
|
||||
iframe.contentDocument.body.append(audio);
|
||||
|
||||
scrubPreviewAudio(iframe, 0.5, "music", 0.4);
|
||||
|
||||
expect(audio.volume).toBeCloseTo(0.1);
|
||||
stopScrubPreviewAudio();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -84,9 +84,15 @@ export function autoHealMissingCompositionIds(doc: Document): void {
|
||||
|
||||
type PreviewPlayerHost = HTMLElement & {
|
||||
muted?: boolean;
|
||||
volume?: number;
|
||||
playbackRate?: number;
|
||||
};
|
||||
|
||||
function normalizePreviewVolume(volume: number): number {
|
||||
if (!Number.isFinite(volume)) return 1;
|
||||
return Math.max(0, Math.min(1, volume));
|
||||
}
|
||||
|
||||
function isPreviewPlayerHost(value: unknown): value is PreviewPlayerHost {
|
||||
return value instanceof HTMLElement;
|
||||
}
|
||||
@@ -123,6 +129,19 @@ export function setPreviewMediaMuted(iframe: HTMLIFrameElement | null, muted: bo
|
||||
} catch {}
|
||||
}
|
||||
|
||||
export function setPreviewMediaVolume(iframe: HTMLIFrameElement | null, volume: number): void {
|
||||
if (!iframe) return;
|
||||
const nextVolume = normalizePreviewVolume(volume);
|
||||
try {
|
||||
const host = resolvePreviewPlayerHost(iframe);
|
||||
if (host && typeof host.volume === "number") {
|
||||
host.volume = nextVolume;
|
||||
return;
|
||||
}
|
||||
postPreviewControl(iframe, "set-volume", { volume: nextVolume });
|
||||
} catch {}
|
||||
}
|
||||
|
||||
export function setPreviewPlaybackRate(
|
||||
iframe: HTMLIFrameElement | null,
|
||||
playbackRate: number,
|
||||
@@ -193,14 +212,14 @@ function resolveScrubAudioEl(doc: Document, musicId?: string | null): HTMLAudioE
|
||||
);
|
||||
}
|
||||
|
||||
function applyScrub(el: HTMLAudioElement, audioFileTime: number): void {
|
||||
function applyScrub(el: HTMLAudioElement, audioFileTime: number, previewVolume: number): void {
|
||||
if (scrubAudioEl && scrubAudioEl !== el) stopScrubPreviewAudio();
|
||||
if (scrubPrevMuted === null) scrubPrevMuted = el.muted;
|
||||
if (scrubPrevVolume === null) scrubPrevVolume = el.volume;
|
||||
scrubAudioEl = el;
|
||||
try {
|
||||
el.muted = false;
|
||||
el.volume = SCRUB_VOLUME;
|
||||
el.volume = SCRUB_VOLUME * normalizePreviewVolume(previewVolume);
|
||||
if (Math.abs(el.currentTime - audioFileTime) > 0.04) el.currentTime = audioFileTime;
|
||||
if (el.paused) void el.play().catch(() => {});
|
||||
} catch {
|
||||
@@ -218,6 +237,7 @@ export function scrubPreviewAudio(
|
||||
iframe: HTMLIFrameElement | null,
|
||||
audioFileTime: number | null,
|
||||
musicId?: string | null,
|
||||
previewVolume = 1,
|
||||
): void {
|
||||
if (!iframe) return;
|
||||
if (audioFileTime === null) {
|
||||
@@ -232,7 +252,7 @@ export function scrubPreviewAudio(
|
||||
}
|
||||
if (!doc) return;
|
||||
const el = resolveScrubAudioEl(doc, musicId);
|
||||
if (el) applyScrub(el, audioFileTime);
|
||||
if (el) applyScrub(el, audioFileTime, previewVolume);
|
||||
}
|
||||
|
||||
export function stopScrubPreviewAudio(): void {
|
||||
|
||||
Reference in New Issue
Block a user