mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-04 07:19:52 +00:00
initial code (#2)
* feat: initial code port from hyperframes-internal Port all OSS-ready packages from the internal monorepo: - @hyperframes/core — shared types, HTML generation, GSAP utilities, runtime - @hyperframes/cli — CLI for creating, previewing, and rendering compositions - @hyperframes/engine — framework-agnostic rendering engine (BeginFrame + FFmpeg) - @hyperframes/producer — video rendering pipeline (Puppeteer + FFmpeg) - @hyperframes/ui-player — browser-based video player component - @hyperframes/studio — composition editor (React frontend + Hono backend) Includes regression test suite with Docker-based test harness. All HeyGen-internal references, deployment infrastructure, and proprietary assets have been removed. Package names migrated from @app/* to @hyperframes/*. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: scrub internal codenames and stale references from OSS port - Replace static.heygen.ai runtime URLs in test fixtures - Remove internal CDN publish script (publish-hyperframe-runtime.ts) - Replace sandbox-studio, sandbox-interceptor, __magicEditRuntime with neutral names (studio, hyperframe-runtime, __hyperframeRuntime) - Fix stale Vault API / localhost references in docs - Remove broken deprecated_studio link Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: remove remaining internal codenames and stale references - Delete stale producer README.md and PIPELINE.md (referenced nonexistent files) - Replace "Cerberus" codename with "HyperFrames" in test design reviews - Replace magic-edit postMessage identifiers with hf-preview/hf-parent - Rename debug-magic-edit-timeline.ts to debug-timeline.ts - Replace "Motion Cut" with "HyperFrames" in Timeline comments - Fix studio/CLI references to nonexistent archive package (use local data/projects/ dir, stub render proxy) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
10621e7903
commit
9f8e5ba5a1
@@ -0,0 +1,113 @@
|
||||
/**
|
||||
* Video Frame Injector
|
||||
*
|
||||
* Creates a BeforeCaptureHook that replaces native <video> elements with
|
||||
* pre-extracted frame images during rendering. This is the Hyperframes-specific
|
||||
* video handling strategy — OSS users with different video pipelines can
|
||||
* provide their own hook or skip video injection entirely.
|
||||
*/
|
||||
|
||||
import { type Page } from "puppeteer-core";
|
||||
import { promises as fs } from "fs";
|
||||
import { type FrameLookupTable } from "./videoFrameExtractor.js";
|
||||
import { injectVideoFramesBatch, syncVideoFrameVisibility } from "./screenshotService.js";
|
||||
import { type BeforeCaptureHook } from "./frameCapture.js";
|
||||
import { DEFAULT_CONFIG, type EngineConfig } from "../config.js";
|
||||
|
||||
function createFrameDataUriCache(cacheLimit: number) {
|
||||
const cache = new Map<string, string>();
|
||||
const inFlight = new Map<string, Promise<string>>();
|
||||
|
||||
function remember(framePath: string, dataUri: string): string {
|
||||
if (cache.has(framePath)) {
|
||||
cache.delete(framePath);
|
||||
}
|
||||
cache.set(framePath, dataUri);
|
||||
if (cache.size > cacheLimit) {
|
||||
const oldestKey = cache.keys().next().value;
|
||||
if (oldestKey) {
|
||||
cache.delete(oldestKey);
|
||||
}
|
||||
}
|
||||
return dataUri;
|
||||
}
|
||||
|
||||
async function get(framePath: string): Promise<string> {
|
||||
const cached = cache.get(framePath);
|
||||
if (cached) {
|
||||
remember(framePath, cached);
|
||||
return cached;
|
||||
}
|
||||
|
||||
const existing = inFlight.get(framePath);
|
||||
if (existing) {
|
||||
return existing;
|
||||
}
|
||||
|
||||
const pending = fs
|
||||
.readFile(framePath)
|
||||
.then((frameData) => {
|
||||
const mimeType = framePath.endsWith(".png") ? "image/png" : "image/jpeg";
|
||||
const dataUri = `data:${mimeType};base64,${frameData.toString("base64")}`;
|
||||
return remember(framePath, dataUri);
|
||||
})
|
||||
.finally(() => {
|
||||
inFlight.delete(framePath);
|
||||
});
|
||||
inFlight.set(framePath, pending);
|
||||
return pending;
|
||||
}
|
||||
|
||||
return { get };
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a BeforeCaptureHook that injects pre-extracted video frames
|
||||
* into the page, replacing native <video> elements with frame images.
|
||||
*/
|
||||
export function createVideoFrameInjector(
|
||||
frameLookup: FrameLookupTable | null,
|
||||
config?: Partial<Pick<EngineConfig, "frameDataUriCacheLimit">>,
|
||||
): BeforeCaptureHook | null {
|
||||
if (!frameLookup) return null;
|
||||
|
||||
const cacheLimit = Math.max(32, config?.frameDataUriCacheLimit ?? DEFAULT_CONFIG.frameDataUriCacheLimit);
|
||||
const frameCache = createFrameDataUriCache(cacheLimit);
|
||||
const lastInjectedFrameByVideo = new Map<string, number>();
|
||||
|
||||
return async (page: Page, time: number) => {
|
||||
const activePayloads = frameLookup.getActiveFramePayloads(time);
|
||||
|
||||
const updates: Array<{ videoId: string; dataUri: string; frameIndex: number }> = [];
|
||||
const activeIds = new Set<string>();
|
||||
if (activePayloads.size > 0) {
|
||||
const pendingReads: Array<Promise<{ videoId: string; dataUri: string; frameIndex: number }>> = [];
|
||||
for (const [videoId, payload] of activePayloads) {
|
||||
activeIds.add(videoId);
|
||||
const lastFrameIndex = lastInjectedFrameByVideo.get(videoId);
|
||||
if (lastFrameIndex === payload.frameIndex) continue;
|
||||
pendingReads.push(
|
||||
frameCache.get(payload.framePath).then((dataUri) => ({ videoId, dataUri, frameIndex: payload.frameIndex })),
|
||||
);
|
||||
}
|
||||
updates.push(...(await Promise.all(pendingReads)));
|
||||
}
|
||||
|
||||
for (const videoId of Array.from(lastInjectedFrameByVideo.keys())) {
|
||||
if (!activeIds.has(videoId)) {
|
||||
lastInjectedFrameByVideo.delete(videoId);
|
||||
}
|
||||
}
|
||||
|
||||
await syncVideoFrameVisibility(page, Array.from(activeIds));
|
||||
if (updates.length > 0) {
|
||||
await injectVideoFramesBatch(
|
||||
page,
|
||||
updates.map((u) => ({ videoId: u.videoId, dataUri: u.dataUri })),
|
||||
);
|
||||
for (const update of updates) {
|
||||
lastInjectedFrameByVideo.set(update.videoId, update.frameIndex);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user