mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-10-30 09:16:55 +00:00
fix(player): fade the controls when idle and copy crossorigin to the audio it plays (#6509)
* fix(player): fade the controls when idle and copy crossorigin to the audio it plays * fix(player): only keyboard focus keeps the controls on screen
This commit is contained in:
@@ -103,3 +103,65 @@ describe("createControls playback speed name", () => {
|
||||
api.destroy();
|
||||
});
|
||||
});
|
||||
|
||||
describe("createControls idle hide", () => {
|
||||
function setup() {
|
||||
vi.useFakeTimers();
|
||||
const host = document.createElement("div");
|
||||
document.body.appendChild(host);
|
||||
const api = createControls(host, noopCallbacks());
|
||||
const controls = host.querySelector<HTMLElement>(".hfp-controls")!;
|
||||
return { host, api, controls };
|
||||
}
|
||||
|
||||
it("hides after 3 s of no input while playing, and returns on movement", () => {
|
||||
const { host, api, controls } = setup();
|
||||
api.updatePlaying(true);
|
||||
host.dispatchEvent(new Event("mousemove"));
|
||||
vi.advanceTimersByTime(3000);
|
||||
expect(controls.classList.contains("hfp-hidden")).toBe(true);
|
||||
host.dispatchEvent(new Event("mousemove"));
|
||||
expect(controls.classList.contains("hfp-hidden")).toBe(false);
|
||||
api.destroy();
|
||||
host.remove();
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it("stays visible while paused or while a control has focus", () => {
|
||||
const { host, api, controls } = setup();
|
||||
vi.advanceTimersByTime(5000);
|
||||
expect(controls.classList.contains("hfp-hidden")).toBe(false);
|
||||
api.updatePlaying(true);
|
||||
controls.querySelector<HTMLElement>("button")!.focus();
|
||||
vi.advanceTimersByTime(5000);
|
||||
expect(controls.classList.contains("hfp-hidden")).toBe(false);
|
||||
api.destroy();
|
||||
host.remove();
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it("comes back on a key press and stays visible until idle again", () => {
|
||||
const { host, api, controls } = setup();
|
||||
api.updatePlaying(true);
|
||||
vi.advanceTimersByTime(3000);
|
||||
host.dispatchEvent(new KeyboardEvent("keydown", { key: "ArrowRight" }));
|
||||
expect(controls.classList.contains("hfp-hidden")).toBe(false);
|
||||
vi.advanceTimersByTime(2999);
|
||||
expect(controls.classList.contains("hfp-hidden")).toBe(false);
|
||||
api.destroy();
|
||||
host.remove();
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it("restarting playback state does not push the hide back", () => {
|
||||
const { host, api, controls } = setup();
|
||||
api.updatePlaying(true);
|
||||
vi.advanceTimersByTime(2000);
|
||||
api.updatePlaying(true);
|
||||
vi.advanceTimersByTime(1000);
|
||||
expect(controls.classList.contains("hfp-hidden")).toBe(true);
|
||||
api.destroy();
|
||||
host.remove();
|
||||
vi.useRealTimers();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -338,22 +338,25 @@ export function createControls(
|
||||
document.addEventListener("touchmove", onTouchMove, { passive: true });
|
||||
document.addEventListener("touchend", onTouchEnd);
|
||||
|
||||
const startHideTimer = () => {
|
||||
const canHide = () =>
|
||||
isPlaying && !scrubbing && !volumeScrubbing && !controls.querySelector(":focus-visible");
|
||||
|
||||
const reveal = () => {
|
||||
controls.classList.remove("hfp-hidden");
|
||||
if (hideTimeout) clearTimeout(hideTimeout);
|
||||
hideTimeout = setTimeout(() => {
|
||||
if (isPlaying) controls.classList.add("hfp-hidden");
|
||||
if (canHide()) controls.classList.add("hfp-hidden");
|
||||
}, 3000);
|
||||
};
|
||||
|
||||
const host = parent instanceof ShadowRoot ? (parent.host as HTMLElement) : parent;
|
||||
const onHostMouseMove = () => {
|
||||
controls.classList.remove("hfp-hidden");
|
||||
startHideTimer();
|
||||
};
|
||||
const onHostActivity = () => reveal();
|
||||
const onHostMouseLeave = () => {
|
||||
if (isPlaying) controls.classList.add("hfp-hidden");
|
||||
if (canHide()) controls.classList.add("hfp-hidden");
|
||||
};
|
||||
host.addEventListener("mousemove", onHostMouseMove);
|
||||
host.addEventListener("mousemove", onHostActivity);
|
||||
host.addEventListener("keydown", onHostActivity);
|
||||
host.addEventListener("focusin", onHostActivity);
|
||||
host.addEventListener("mouseleave", onHostMouseLeave);
|
||||
|
||||
return {
|
||||
@@ -365,11 +368,14 @@ export function createControls(
|
||||
time.textContent = `${formatTime(clampedCurrent)} / ${formatTime(duration)}`;
|
||||
},
|
||||
updatePlaying(playing: boolean) {
|
||||
const changed = playing !== isPlaying;
|
||||
isPlaying = playing;
|
||||
playBtn.classList.toggle("hfp-playing", playing);
|
||||
playBtn.setAttribute("aria-label", playing ? "Pause" : "Play");
|
||||
if (playing) startHideTimer();
|
||||
else controls.classList.remove("hfp-hidden");
|
||||
if (!playing) {
|
||||
if (hideTimeout) clearTimeout(hideTimeout);
|
||||
controls.classList.remove("hfp-hidden");
|
||||
} else if (changed) reveal();
|
||||
},
|
||||
updateSpeed(speed: number) {
|
||||
const idx = presets.indexOf(speed);
|
||||
@@ -408,7 +414,9 @@ export function createControls(
|
||||
document.removeEventListener("touchmove", onVolumeTouchMove);
|
||||
document.removeEventListener("touchend", onVolumeTouchEnd);
|
||||
document.removeEventListener("click", onDocClick);
|
||||
host.removeEventListener("mousemove", onHostMouseMove);
|
||||
host.removeEventListener("mousemove", onHostActivity);
|
||||
host.removeEventListener("keydown", onHostActivity);
|
||||
host.removeEventListener("focusin", onHostActivity);
|
||||
host.removeEventListener("mouseleave", onHostMouseLeave);
|
||||
if (hideTimeout) clearTimeout(hideTimeout);
|
||||
controls.remove();
|
||||
|
||||
@@ -597,3 +597,36 @@ describe("ParentMediaManager drift policy", () => {
|
||||
expect(writes[1]).toBeCloseTo(mediaElementTime(5.43 + 0.23), 9);
|
||||
});
|
||||
});
|
||||
|
||||
describe("proxy crossorigin", () => {
|
||||
it.each(["audio", "video"] as const)("a %s proxy carries its source's crossorigin", (tag) => {
|
||||
const created: HTMLMediaElement[] = [];
|
||||
const real = document.createElement.bind(document);
|
||||
vi.spyOn(document, "createElement").mockImplementation((t: string) => {
|
||||
const el = real(t);
|
||||
if (t === "video") created.push(el as HTMLMediaElement);
|
||||
return el;
|
||||
});
|
||||
const OrigAudio = globalThis.Audio;
|
||||
globalThis.Audio = function () {
|
||||
const el = real("audio");
|
||||
created.push(el);
|
||||
return el;
|
||||
} as unknown as typeof Audio;
|
||||
const source = real(tag);
|
||||
source.setAttribute("crossorigin", "anonymous");
|
||||
const parent = Object.create(ParentMediaManager.prototype) as ParentMediaManager;
|
||||
Object.assign(parent, {
|
||||
_entries: [],
|
||||
_urlTaken: () => false,
|
||||
_getMuted: () => false,
|
||||
_getVolume: () => 1,
|
||||
_getPlaybackRate: () => 1,
|
||||
});
|
||||
(parent as any)._createEntry("https://x/a.mp3", tag, 0, 5, source);
|
||||
const proxy = created.find((e) => e.tagName.toLowerCase() === tag)!;
|
||||
globalThis.Audio = OrigAudio;
|
||||
vi.restoreAllMocks();
|
||||
expect(proxy.getAttribute("crossorigin")).toBe("anonymous");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -413,6 +413,8 @@ export class ParentMediaManager {
|
||||
|
||||
const el = tag === "video" ? document.createElement("video") : new Audio();
|
||||
el.preload = "auto";
|
||||
const crossOrigin = source?.getAttribute("crossorigin");
|
||||
if (crossOrigin != null) el.setAttribute("crossorigin", crossOrigin);
|
||||
el.src = src;
|
||||
el.load();
|
||||
el.muted = this._getMuted();
|
||||
|
||||
Reference in New Issue
Block a user