mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 12:54:29 +00:00
- Delay URL.revokeObjectURL() from 0ms to 1000ms in useFrameCapture so the browser has time to initiate the download before the blob is freed. A 0ms timeout fires synchronously after the current microtask queue, before the browser's download machinery reads the URL. - Add ignoreDeprecations: '5.0' to cli and studio tsconfigs to silence TypeScript baseUrl/paths deprecation warnings without changing behavior. Co-authored-by: Jefsky Wong <jefsky@qq.com>
103 lines
3.4 KiB
TypeScript
103 lines
3.4 KiB
TypeScript
import { useState, useCallback, type MouseEvent } from "react";
|
|
import { useMountEffect } from "./useMountEffect";
|
|
import { liveTime, usePlayerStore } from "../player";
|
|
import { buildFrameCaptureFilename, buildFrameCaptureUrl } from "../utils/frameCapture";
|
|
|
|
interface UseFrameCaptureParams {
|
|
projectId: string | null;
|
|
activeCompPath: string | null;
|
|
showToast: (message: string, tone?: "error" | "info") => void;
|
|
waitForPendingDomEditSaves: () => Promise<void>;
|
|
}
|
|
|
|
export function useFrameCapture({
|
|
projectId,
|
|
activeCompPath,
|
|
showToast,
|
|
waitForPendingDomEditSaves,
|
|
}: UseFrameCaptureParams) {
|
|
const [captureFrameTime, setCaptureFrameTime] = useState(0);
|
|
|
|
useMountEffect(() => {
|
|
setCaptureFrameTime(usePlayerStore.getState().currentTime);
|
|
return liveTime.subscribe(setCaptureFrameTime);
|
|
});
|
|
|
|
const refreshCaptureFrameTime = useCallback(() => {
|
|
setCaptureFrameTime(usePlayerStore.getState().currentTime);
|
|
}, []);
|
|
|
|
const handleCaptureFrameClick = useCallback(
|
|
async (event: MouseEvent<HTMLAnchorElement>) => {
|
|
if (!projectId) return;
|
|
event.preventDefault();
|
|
try {
|
|
const time = usePlayerStore.getState().currentTime;
|
|
setCaptureFrameTime(time);
|
|
await Promise.race([
|
|
waitForPendingDomEditSaves(),
|
|
new Promise<void>((_, reject) =>
|
|
setTimeout(() => reject(new Error("Save queue timed out")), 5000),
|
|
),
|
|
]);
|
|
const href = buildFrameCaptureUrl({
|
|
projectId,
|
|
compositionPath: activeCompPath,
|
|
currentTime: time,
|
|
});
|
|
const filename = buildFrameCaptureFilename(activeCompPath, time);
|
|
const controller = new AbortController();
|
|
const timeout = setTimeout(() => controller.abort(), 30000);
|
|
try {
|
|
const response = await fetch(href, { cache: "no-store", signal: controller.signal });
|
|
clearTimeout(timeout);
|
|
if (!response.ok) {
|
|
let msg = `Capture failed (${response.status})`;
|
|
try {
|
|
const json = await response.json();
|
|
if (json?.error) msg = json.error;
|
|
} catch {
|
|
/* non-JSON response — use default message */
|
|
}
|
|
throw new Error(msg);
|
|
}
|
|
const blob = await response.blob();
|
|
const blobUrl = URL.createObjectURL(blob);
|
|
const link = document.createElement("a");
|
|
link.href = blobUrl;
|
|
link.download = filename;
|
|
document.body.appendChild(link);
|
|
link.click();
|
|
link.remove();
|
|
setTimeout(() => URL.revokeObjectURL(blobUrl), 1000);
|
|
} catch (fetchErr) {
|
|
clearTimeout(timeout);
|
|
if (fetchErr instanceof DOMException && fetchErr.name === "AbortError") {
|
|
throw new Error("Capture timed out — the server took too long to respond");
|
|
}
|
|
throw fetchErr;
|
|
}
|
|
} catch (err) {
|
|
showToast(err instanceof Error ? err.message : "Capture failed", "error");
|
|
}
|
|
},
|
|
[activeCompPath, projectId, showToast, waitForPendingDomEditSaves],
|
|
);
|
|
|
|
const captureFrameHref = projectId
|
|
? buildFrameCaptureUrl({
|
|
projectId,
|
|
compositionPath: activeCompPath,
|
|
currentTime: captureFrameTime,
|
|
})
|
|
: "#";
|
|
const captureFrameFilename = buildFrameCaptureFilename(activeCompPath, captureFrameTime);
|
|
|
|
return {
|
|
captureFrameHref,
|
|
captureFrameFilename,
|
|
handleCaptureFrameClick,
|
|
refreshCaptureFrameTime,
|
|
};
|
|
}
|