mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-10 22:20:14 +00:00
fix(studio): enable timeline resize for all elements, improve perf and UX
Enable trim-start and trim-end for all authored timeline elements (divs, sections, compositions) — not just video/audio/img. The deterministic-window gate was overly restrictive since all non-implicit elements have authored data-start/data-duration that define their timeline window. Replace iframe reload after resize/move with direct DOM attribute patching via patchIframeDomTiming(). This eliminates playhead-jump-to-zero, visual blinking, and race conditions from file-watcher echoes. File persistence runs in a serialized background queue (persistTimelineEdit + enqueueEdit) so rapid edits don't overwrite each other. Add mediabunny-based media probe service (mediaProbe.ts) for fast metadata extraction from file headers. Timeline elements missing sourceDuration are enriched asynchronously without waiting for DOM loadedmetadata events. Tune the runtime media preloader: lower lazy threshold from 6 to 3 clips, add 3s lookbehind window for reverse scrub, adaptive promoted-clip cap. Deduplicate getTimelineEditCapabilities — computed once in TimelineCanvas and passed as a prop to TimelineClip instead of recomputing per clip. Remove dead PlaybackAdapter re-export from useTimelinePlayer — all consumers import directly from playbackTypes.
This commit is contained in:
@@ -0,0 +1,68 @@
|
||||
import { Input, UrlSource, ALL_FORMATS } from "mediabunny";
|
||||
|
||||
export interface MediaProbeResult {
|
||||
duration: number;
|
||||
width?: number;
|
||||
height?: number;
|
||||
hasVideo: boolean;
|
||||
hasAudio: boolean;
|
||||
}
|
||||
|
||||
const cache = new Map<string, MediaProbeResult>();
|
||||
const inflight = new Map<string, Promise<MediaProbeResult | null>>();
|
||||
|
||||
function normalizeUrl(url: string): string {
|
||||
try {
|
||||
return new URL(url, window.location.href).href;
|
||||
} catch {
|
||||
return url;
|
||||
}
|
||||
}
|
||||
|
||||
async function probeOne(url: string): Promise<MediaProbeResult | null> {
|
||||
const input = new Input({
|
||||
source: new UrlSource(url),
|
||||
formats: ALL_FORMATS,
|
||||
});
|
||||
try {
|
||||
const duration = await input.getDurationFromMetadata();
|
||||
if (duration == null || !Number.isFinite(duration) || duration <= 0) return null;
|
||||
|
||||
const videoTrack = await input.getPrimaryVideoTrack();
|
||||
const audioTracks = await input.getAudioTracks();
|
||||
|
||||
const result: MediaProbeResult = {
|
||||
duration,
|
||||
width: videoTrack?.displayWidth,
|
||||
height: videoTrack?.displayHeight,
|
||||
hasVideo: videoTrack != null,
|
||||
hasAudio: audioTracks.length > 0,
|
||||
};
|
||||
return result;
|
||||
} catch {
|
||||
return null;
|
||||
} finally {
|
||||
input.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
export function getCachedProbe(url: string): MediaProbeResult | undefined {
|
||||
return cache.get(normalizeUrl(url));
|
||||
}
|
||||
|
||||
export async function probeMediaUrl(url: string): Promise<MediaProbeResult | null> {
|
||||
const key = normalizeUrl(url);
|
||||
const cached = cache.get(key);
|
||||
if (cached) return cached;
|
||||
|
||||
let pending = inflight.get(key);
|
||||
if (pending) return pending;
|
||||
|
||||
pending = probeOne(key).then((result) => {
|
||||
inflight.delete(key);
|
||||
if (result) cache.set(key, result);
|
||||
return result;
|
||||
});
|
||||
inflight.set(key, pending);
|
||||
return pending;
|
||||
}
|
||||
Reference in New Issue
Block a user