mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 23:03:09 +00:00
* feat(engine): drawElementImage capture service * feat(engine): 3D projection + compositor-effect risk gate * fix(engine): gate filter drop-shadow wherever blur gates (review) detectCssEffectRisk documented drop-shadow as a ~29dB damage case but only detected blur( in its three scan paths — a drop-shadow comp stayed on the fast path despite the gate's own correctness contract. Detect drop-shadow( in computed styles, stylesheet rules, and GSAP tween vars, pinned by a focused test that runs the real page-side closure against a DOM shim (computed / stylesheet / tween coverage + blur regression + effect-free null). Addresses miguel-heygen's blocker on #1918. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(engine): frame-capture core — fast-capture routing, worker-encode, dedup extension # Conflicts: # packages/engine/src/services/screenshotService.ts * fix(engine): document HF_FORCE_DRAWELEMENT as diagnostic-only; make armStaticDedup idempotent (review) Addresses miguel-heygen's blockers on #1919: - HF_FORCE_DRAWELEMENT promoted from a stale "SCRATCH/Uncommitted" comment to a documented diagnostic flag: it exists for upstream-Chromium repro work (gate-vs-API isolation, crbug 521861819 149-vs-151) and R&D on gated effect classes; renders under it may be damaged BY DESIGN since it bypasses gates whose thresholds encode measured damage. Never production; the safety-net blank guard also stands down under it so diagnostic frames arrive unmodified. - armStaticDedup is now idempotent: the drawElement init path arms dedup before canvas injection, then initializeSession called it again — the second run overwrote the armed state with skipReason="capture_mode" (captureMode is "drawelement" by then), producing contradictory telemetry (armed frames + a skip reason), and re-ran the verification seeks on the fallback path. It now no-ops once staticFrames or a skip decision exists. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(producer): fast-capture render stages + remote bg-image localizer * feat(lint,player): fast-capture lint rule + player media sync --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
165 lines
5.8 KiB
TypeScript
165 lines
5.8 KiB
TypeScript
import { describe, it, expect } from "vitest";
|
|
import { ParentMediaManager, type ProxyEntry } from "./parent-media";
|
|
|
|
// A fake media element whose paused state is driven by play()/pause() stubs.
|
|
function makeFakeAudio(initiallyPaused: boolean): HTMLMediaElement {
|
|
const el = new Audio();
|
|
let paused = initiallyPaused;
|
|
Object.defineProperty(el, "paused", { get: () => paused });
|
|
el.pause = () => {
|
|
paused = true;
|
|
};
|
|
el.play = () => {
|
|
paused = false;
|
|
return Promise.resolve();
|
|
};
|
|
el.src = "https://example.test/music.mp3";
|
|
return el;
|
|
}
|
|
|
|
function makeManager(overrides: Partial<{ isPaused: boolean; owner: "runtime" | "parent" }> = {}) {
|
|
const mgr = new ParentMediaManager({
|
|
dispatchEvent: () => {},
|
|
getMuted: () => false,
|
|
getVolume: () => 1,
|
|
getPlaybackRate: () => 1,
|
|
getCurrentTime: () => 0,
|
|
isPaused: () => overrides.isPaused ?? true,
|
|
});
|
|
return mgr;
|
|
}
|
|
|
|
describe("ParentMediaManager audio-src proxy lifecycle", () => {
|
|
it("replaces the audio-src proxy instead of stacking a second one", () => {
|
|
const mgr = makeManager();
|
|
mgr.setupFromUrl("https://example.test/a.mp3");
|
|
expect(mgr.entries).toHaveLength(1);
|
|
|
|
mgr.setupFromUrl("https://example.test/b.mp3");
|
|
// The old proxy must be gone, not accumulated alongside the new one.
|
|
expect(mgr.entries).toHaveLength(1);
|
|
expect(mgr.entries[0].el.src).toBe("https://example.test/b.mp3");
|
|
});
|
|
|
|
it("is a no-op when the same audio-src URL is set again", () => {
|
|
const mgr = makeManager();
|
|
mgr.setupFromUrl("https://example.test/a.mp3");
|
|
const first = mgr.entries[0];
|
|
|
|
mgr.setupFromUrl("https://example.test/a.mp3");
|
|
expect(mgr.entries).toHaveLength(1);
|
|
// Same element reference — not torn down and rebuilt.
|
|
expect(mgr.entries[0]).toBe(first);
|
|
});
|
|
|
|
it("clears the audio-src proxy on teardownUrlAudio", () => {
|
|
const mgr = makeManager();
|
|
mgr.setupFromUrl("https://example.test/a.mp3");
|
|
const el = mgr.entries[0].el;
|
|
|
|
mgr.teardownUrlAudio();
|
|
expect(mgr.entries).toHaveLength(0);
|
|
// The proxy's source is reset so it stops preloading.
|
|
expect(el.src).not.toBe("https://example.test/a.mp3");
|
|
});
|
|
|
|
it("teardownUrlAudio removes only the url proxy, leaving other entries", () => {
|
|
const mgr = makeManager();
|
|
// Simulate an iframe-adopted entry already in the pool.
|
|
const adopted: ProxyEntry = {
|
|
el: new Audio(),
|
|
start: 0,
|
|
duration: Infinity,
|
|
driftSamples: 0,
|
|
};
|
|
adopted.el.src = "https://example.test/iframe-clip.mp4";
|
|
mgr.entries.push(adopted);
|
|
|
|
mgr.setupFromUrl("https://example.test/a.mp3");
|
|
expect(mgr.entries).toHaveLength(2);
|
|
|
|
mgr.teardownUrlAudio();
|
|
expect(mgr.entries).toHaveLength(1);
|
|
expect(mgr.entries[0]).toBe(adopted);
|
|
});
|
|
|
|
it("teardownUrlAudio is safe to call with no audio-src set", () => {
|
|
const mgr = makeManager();
|
|
expect(() => mgr.teardownUrlAudio()).not.toThrow();
|
|
expect(mgr.entries).toHaveLength(0);
|
|
});
|
|
|
|
it("pauses a proxy once the playhead passes the clip end (trimmed clip)", () => {
|
|
const mgr = makeManager({ owner: "parent", isPaused: false });
|
|
const el = makeFakeAudio(false); // already playing within the clip
|
|
mgr.entries.push({ el, start: 0, duration: 5, driftSamples: 0 });
|
|
|
|
mgr.mirrorTime(3); // inside [0, 5) — stays playing
|
|
expect(el.paused).toBe(false);
|
|
|
|
mgr.mirrorTime(6); // past the trimmed end — must pause
|
|
expect(el.paused).toBe(true);
|
|
});
|
|
|
|
it("re-reads the source element's live data-duration so trims bound the proxy", () => {
|
|
const mgr = makeManager({ owner: "parent", isPaused: false });
|
|
const source = new Audio();
|
|
source.setAttribute("data-start", "0");
|
|
source.setAttribute("data-duration", "30");
|
|
// jsdom reports isConnected=false unless attached; attach it.
|
|
document.body.appendChild(source);
|
|
|
|
const el = makeFakeAudio(false);
|
|
mgr.entries.push({ el, start: 0, duration: 30, driftSamples: 0, source });
|
|
|
|
mgr.mirrorTime(20); // within 30 → playing
|
|
expect(el.paused).toBe(false);
|
|
|
|
// User trims the clip to 10s; the proxy must pick it up and pause at 20s.
|
|
source.setAttribute("data-duration", "10");
|
|
mgr.mirrorTime(20);
|
|
expect(el.paused).toBe(true);
|
|
source.remove();
|
|
});
|
|
|
|
it("scrubAll plays in-window proxies at the playhead and pauses out-of-window ones", () => {
|
|
const mgr = makeManager({ owner: "parent" });
|
|
const inWin = makeFakeAudio(true); // currently paused — scrub should start it
|
|
const outWin = makeFakeAudio(false); // currently playing, but outside its window
|
|
mgr.entries.push({ el: inWin, start: 0, duration: 5, driftSamples: 0 });
|
|
mgr.entries.push({ el: outWin, start: 10, duration: 5, driftSamples: 0 });
|
|
|
|
mgr.scrubAll(2); // playhead at 2s
|
|
|
|
// in-window proxy: positioned at rel time and AUDIBLE (the point of scrub-audio)
|
|
expect(inWin.currentTime).toBe(2);
|
|
expect(inWin.paused).toBe(false);
|
|
// out-of-window proxy: paused, not blipped
|
|
expect(outWin.paused).toBe(true);
|
|
});
|
|
|
|
it("does not duplicate or hijack a clip the composition already owns", () => {
|
|
const mgr = makeManager();
|
|
// The composition already adopted a clip with this URL.
|
|
const adopted: ProxyEntry = {
|
|
el: new Audio(),
|
|
start: 0,
|
|
duration: Infinity,
|
|
driftSamples: 0,
|
|
};
|
|
adopted.el.src = "https://example.test/shared.mp3";
|
|
mgr.entries.push(adopted);
|
|
|
|
// Pointing audio-src at the same URL must not create a second proxy...
|
|
mgr.setupFromUrl("https://example.test/shared.mp3");
|
|
expect(mgr.entries).toHaveLength(1);
|
|
expect(mgr.entries[0]).toBe(adopted);
|
|
|
|
// ...and removing audio-src must not tear down the composition's own clip
|
|
// (teardown targets the tracked proxy by reference, not by URL match).
|
|
mgr.teardownUrlAudio();
|
|
expect(mgr.entries).toHaveLength(1);
|
|
expect(mgr.entries[0]).toBe(adopted);
|
|
});
|
|
});
|