mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 23:03:09 +00:00
fix: add progress logging during silent render pipeline stages (#1220)
* fix: add progress logging during silent render pipeline stages The render pipeline only updates progress at stage boundaries (5%, 10%, 25%), leaving multi-minute gaps with zero log output on low-memory hardware. This adds log.info calls at key sub-steps within the three silent stages: - Probe stage (5%): browser launch, session initialization, duration discovery, media asset discovery, audio volume automation, video visibility window detection - Video extraction (10%): per-video extraction progress - Calibration (25%): browser launch, session initialization, per-frame calibration progress, final cost estimate Also adds 30-second heartbeat timers for the two initializeSession calls (probe and calibration) that can individually take minutes on constrained hardware. Closes #1218 * fix: resolve CI failures in typecheck, runtime seek test, and timeline test - Make handleGsapMaterializeKeyframes optional in DomEditSessionSlice and use optional chaining at the call site (not yet wired) - Update GSAP adapter seek test to expect nudge+seek pattern (totalTime with suppressEvents:true followed by actual seek) - Fix Timeline canvas height test to use TRACK_H constant (48) instead of stale hardcoded value (72) * refactor: extract helpers to meet 600-line file size limit - App.tsx (603→594): extract StudioToast component - useDomEditSession.ts (688→600): extract useGsapSelectionHandlers hook - Timeline.tsx (614→557): extract useTimelineAssetDrop hook - PropertyPanel.tsx (647→584): extract TimingSection to propertyPanelTimingSection * style: fix formatting in TimelineToolbar
This commit is contained in:
@@ -12,6 +12,7 @@ import {
|
||||
shouldHandleTimelineDeleteKey,
|
||||
shouldAutoScrollTimeline,
|
||||
} from "./Timeline";
|
||||
import { RULER_H, TRACK_H } from "./timelineLayout";
|
||||
import { formatTime } from "../lib/time";
|
||||
|
||||
describe("generateTicks", () => {
|
||||
@@ -230,8 +231,7 @@ describe("getTimelinePlayheadLeft", () => {
|
||||
|
||||
describe("getTimelineCanvasHeight", () => {
|
||||
it("includes bottom scroll buffer below the last track", () => {
|
||||
// RULER_H (24) + trackCount * TRACK_H (48) + scroll buffer
|
||||
expect(getTimelineCanvasHeight(3)).toBeGreaterThan(24 + 3 * 48);
|
||||
expect(getTimelineCanvasHeight(3)).toBeGreaterThan(RULER_H + 3 * TRACK_H);
|
||||
});
|
||||
|
||||
it("still keeps ruler space when there are no tracks", () => {
|
||||
|
||||
@@ -8,7 +8,7 @@ import { useTimelineRangeSelection } from "./useTimelineRangeSelection";
|
||||
import { useTimelinePlayhead } from "./useTimelinePlayhead";
|
||||
import { type TrackVisualStyle, getTrackStyle } from "./timelineIcons";
|
||||
import { getTimelinePixelsPerSecond } from "./timelineZoom";
|
||||
import { TIMELINE_ASSET_MIME, TIMELINE_BLOCK_MIME } from "../../utils/timelineAssetDrop";
|
||||
import { useTimelineAssetDrop } from "./timelineDragDrop";
|
||||
import { TimelineEmptyState } from "./TimelineEmptyState";
|
||||
import { TimelineCanvas } from "./TimelineCanvas";
|
||||
import {
|
||||
@@ -19,11 +19,9 @@ import { useTimelineClipDrag } from "./useTimelineClipDrag";
|
||||
import { ClipContextMenu } from "./ClipContextMenu";
|
||||
import {
|
||||
GUTTER,
|
||||
TRACK_H,
|
||||
generateTicks,
|
||||
getTimelineCanvasHeight,
|
||||
shouldShowTimelineShortcutHint,
|
||||
resolveTimelineAssetDrop,
|
||||
} from "./timelineLayout";
|
||||
|
||||
// Re-export pure utilities so existing imports from "./Timeline" still resolve.
|
||||
@@ -364,71 +362,15 @@ export const Timeline = memo(function Timeline({
|
||||
[resizingClip],
|
||||
);
|
||||
|
||||
const [isDragOver, setIsDragOver] = useState(false);
|
||||
const handleAssetDragOver = useCallback((e: React.DragEvent) => {
|
||||
const hasFiles = e.dataTransfer.files.length > 0;
|
||||
const types = Array.from(e.dataTransfer.types);
|
||||
const hasAsset = types.includes(TIMELINE_ASSET_MIME);
|
||||
const hasBlock = types.includes(TIMELINE_BLOCK_MIME);
|
||||
if (!hasFiles && !hasAsset && !hasBlock) return;
|
||||
e.preventDefault();
|
||||
if (hasAsset || hasBlock) e.dataTransfer.dropEffect = "copy";
|
||||
setIsDragOver(true);
|
||||
}, []);
|
||||
|
||||
const handleAssetDrop = useCallback(
|
||||
(e: React.DragEvent) => {
|
||||
e.preventDefault();
|
||||
setIsDragOver(false);
|
||||
const scroll = scrollRef.current;
|
||||
const rect = scroll?.getBoundingClientRect();
|
||||
const dropInput = {
|
||||
rectLeft: rect?.left ?? 0,
|
||||
rectTop: rect?.top ?? 0,
|
||||
scrollLeft: scroll?.scrollLeft ?? 0,
|
||||
scrollTop: scroll?.scrollTop ?? 0,
|
||||
pixelsPerSecond: ppsRef.current,
|
||||
duration: durationRef.current,
|
||||
trackHeight: TRACK_H,
|
||||
trackOrder: trackOrderRef.current,
|
||||
};
|
||||
if (onFileDrop && e.dataTransfer.files.length > 0) {
|
||||
void onFileDrop(
|
||||
Array.from(e.dataTransfer.files),
|
||||
scroll && rect ? resolveTimelineAssetDrop(dropInput, e.clientX, e.clientY) : undefined,
|
||||
);
|
||||
return;
|
||||
}
|
||||
const assetPayload = e.dataTransfer.getData(TIMELINE_ASSET_MIME);
|
||||
if (assetPayload && onAssetDrop && scroll && rect) {
|
||||
try {
|
||||
const parsed = JSON.parse(assetPayload) as { path?: string };
|
||||
if (parsed.path)
|
||||
void onAssetDrop(
|
||||
parsed.path,
|
||||
resolveTimelineAssetDrop(dropInput, e.clientX, e.clientY),
|
||||
);
|
||||
} catch {
|
||||
/* ignore malformed drag payloads */
|
||||
}
|
||||
return;
|
||||
}
|
||||
const blockPayload = e.dataTransfer.getData(TIMELINE_BLOCK_MIME);
|
||||
if (blockPayload && onBlockDrop && scroll && rect) {
|
||||
try {
|
||||
const parsed = JSON.parse(blockPayload) as { name?: string };
|
||||
if (parsed.name)
|
||||
void onBlockDrop(
|
||||
parsed.name,
|
||||
resolveTimelineAssetDrop(dropInput, e.clientX, e.clientY),
|
||||
);
|
||||
} catch {
|
||||
/* ignore malformed drag payloads */
|
||||
}
|
||||
}
|
||||
},
|
||||
[onAssetDrop, onBlockDrop, onFileDrop],
|
||||
);
|
||||
const { isDragOver, setIsDragOver, handleAssetDragOver, handleAssetDrop } = useTimelineAssetDrop({
|
||||
scrollRef,
|
||||
ppsRef,
|
||||
durationRef,
|
||||
trackOrderRef,
|
||||
onFileDrop,
|
||||
onAssetDrop,
|
||||
onBlockDrop,
|
||||
});
|
||||
|
||||
if (!timelineReady || elements.length === 0) {
|
||||
return (
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
// fallow-ignore-file clone-families
|
||||
import { useCallback, useState, type RefObject } from "react";
|
||||
import { TIMELINE_ASSET_MIME, TIMELINE_BLOCK_MIME } from "../../utils/timelineAssetDrop";
|
||||
import { TRACK_H, resolveTimelineAssetDrop } from "./timelineLayout";
|
||||
|
||||
interface UseTimelineAssetDropOptions {
|
||||
scrollRef: RefObject<HTMLDivElement | null>;
|
||||
ppsRef: RefObject<number>;
|
||||
durationRef: RefObject<number>;
|
||||
trackOrderRef: RefObject<number[]>;
|
||||
onFileDrop?: (
|
||||
files: File[],
|
||||
placement?: { start: number; track: number },
|
||||
) => Promise<void> | void;
|
||||
onAssetDrop?: (
|
||||
assetPath: string,
|
||||
placement: { start: number; track: number },
|
||||
) => Promise<void> | void;
|
||||
onBlockDrop?: (
|
||||
blockName: string,
|
||||
placement: { start: number; track: number },
|
||||
) => Promise<void> | void;
|
||||
}
|
||||
|
||||
export function useTimelineAssetDrop({
|
||||
scrollRef,
|
||||
ppsRef,
|
||||
durationRef,
|
||||
trackOrderRef,
|
||||
onFileDrop,
|
||||
onAssetDrop,
|
||||
onBlockDrop,
|
||||
}: UseTimelineAssetDropOptions) {
|
||||
const [isDragOver, setIsDragOver] = useState(false);
|
||||
|
||||
const handleAssetDragOver = useCallback((e: React.DragEvent) => {
|
||||
const hasFiles = e.dataTransfer.files.length > 0;
|
||||
const types = Array.from(e.dataTransfer.types);
|
||||
const hasAsset = types.includes(TIMELINE_ASSET_MIME);
|
||||
const hasBlock = types.includes(TIMELINE_BLOCK_MIME);
|
||||
if (!hasFiles && !hasAsset && !hasBlock) return;
|
||||
e.preventDefault();
|
||||
if (hasAsset || hasBlock) e.dataTransfer.dropEffect = "copy";
|
||||
setIsDragOver(true);
|
||||
}, []);
|
||||
|
||||
const handleAssetDrop = useCallback(
|
||||
// fallow-ignore-next-line complexity
|
||||
(e: React.DragEvent) => {
|
||||
e.preventDefault();
|
||||
setIsDragOver(false);
|
||||
const scroll = scrollRef.current;
|
||||
const rect = scroll?.getBoundingClientRect();
|
||||
const dropInput = {
|
||||
rectLeft: rect?.left ?? 0,
|
||||
rectTop: rect?.top ?? 0,
|
||||
scrollLeft: scroll?.scrollLeft ?? 0,
|
||||
scrollTop: scroll?.scrollTop ?? 0,
|
||||
pixelsPerSecond: ppsRef.current,
|
||||
duration: durationRef.current,
|
||||
trackHeight: TRACK_H,
|
||||
trackOrder: trackOrderRef.current,
|
||||
};
|
||||
if (onFileDrop && e.dataTransfer.files.length > 0) {
|
||||
void onFileDrop(
|
||||
Array.from(e.dataTransfer.files),
|
||||
scroll && rect ? resolveTimelineAssetDrop(dropInput, e.clientX, e.clientY) : undefined,
|
||||
);
|
||||
return;
|
||||
}
|
||||
const assetPayload = e.dataTransfer.getData(TIMELINE_ASSET_MIME);
|
||||
if (assetPayload && onAssetDrop && scroll && rect) {
|
||||
try {
|
||||
const parsed = JSON.parse(assetPayload) as { path?: string };
|
||||
if (parsed.path)
|
||||
void onAssetDrop(
|
||||
parsed.path,
|
||||
resolveTimelineAssetDrop(dropInput, e.clientX, e.clientY),
|
||||
);
|
||||
} catch {
|
||||
/* ignore malformed drag payloads */
|
||||
}
|
||||
return;
|
||||
}
|
||||
const blockPayload = e.dataTransfer.getData(TIMELINE_BLOCK_MIME);
|
||||
if (blockPayload && onBlockDrop && scroll && rect) {
|
||||
try {
|
||||
const parsed = JSON.parse(blockPayload) as { name?: string };
|
||||
if (parsed.name)
|
||||
void onBlockDrop(
|
||||
parsed.name,
|
||||
resolveTimelineAssetDrop(dropInput, e.clientX, e.clientY),
|
||||
);
|
||||
} catch {
|
||||
/* ignore malformed drag payloads */
|
||||
}
|
||||
}
|
||||
},
|
||||
[onAssetDrop, onBlockDrop, onFileDrop, scrollRef, ppsRef, durationRef, trackOrderRef],
|
||||
);
|
||||
|
||||
return { isDragOver, setIsDragOver, handleAssetDragOver, handleAssetDrop };
|
||||
}
|
||||
Reference in New Issue
Block a user