mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-10 22:20:14 +00:00
fix(ci): scope LOC check to studio, split useTimelinePlayer + hyperframes-player under 500 LOC (#750)
* refactor: split useTimelinePlayer.ts and hyperframes-player.ts into focused modules (<500 LOC each) * fix(ci): scope 500 LOC check to packages/studio, add allowlist for grandfathered files * feat(cli): Linux ARM64 support — auto-install Chromium on DGX Spark / GB10 / Jetson Chrome Headless Shell has no Linux ARM64 binary. On arm64 Linux: - Detects the platform automatically - Tries to auto-install system Chromium via apt-get (works on Ubuntu/Debian ARM) - Falls back to clear manual instructions with exact commands - 'hyperframes browser ensure' guides through the setup interactively - After setup, all render commands work without any flags * fix(ci): disable Windows Defender real-time monitoring to prevent EPERM builds Path exclusions are insufficient — Defender re-scans new files created during bun install before the exclusion takes effect. Disable real-time monitoring for the entire job duration instead (standard CI practice). * refactor(studio): split all files >500 LOC + extract useToast, delete allowlist All 11 large files split into focused modules under 500 LOC. App.tsx extracted toast logic into useToast hook (493 LOC now). .filesize-allowlist deleted — no longer needed. * fix: remove unused imports from split files, extract useToast from App.tsx App.tsx: 504 → 493 lines (toast logic extracted to useToast hook) timelineDOM.ts: remove unused imports from re-export pattern MotionPanel.tsx: remove unused clampStudioCustomEasePoints import studioMotionOps.ts: remove unused StudioGsapMotionDirection import * fix(ci): use Set-MpPreference to fully disable Windows Defender (both jobs) * fix(producer): use node --experimental-strip-types instead of tsx for build:fonts Eliminates the tsx binary dependency that Windows Defender locks during bun install, causing EPERM errors. Node 22.6+ strips TypeScript types natively with no external binary. * chore: remove .filesize-allowlist — App.tsx is now 493 lines (<500) * fix(ci): disable Windows Defender before checkout to prevent all EPERM races * fix(producer): skip build:fonts if fontData.generated.ts already exists The generated file is tracked in git, so CI doesn't need to regenerate it. This avoids @fontsource/inter node_modules access on Windows which triggers EPERM from Defender scanning during bun install.
This commit is contained in:
@@ -0,0 +1,145 @@
|
||||
/**
|
||||
* Playback adapter utilities: factory for the static-seek adapter used when a
|
||||
* composition exposes only a `renderSeek` / `seek` API (no native play/pause
|
||||
* support), plus a thin wrapper that normalises GSAP-style `TimelineLike`
|
||||
* objects to the `PlaybackAdapter` interface.
|
||||
*/
|
||||
|
||||
import type {
|
||||
PlaybackAdapter,
|
||||
RuntimePlaybackAdapter,
|
||||
StaticSeekPlaybackClock,
|
||||
TimelineLike,
|
||||
} from "./playbackTypes";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Pure numeric helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export function isFinitePositive(value: number): boolean {
|
||||
return Number.isFinite(value) && value > 0;
|
||||
}
|
||||
|
||||
export function clampTime(time: number, duration: number): number {
|
||||
const safeDuration = Math.max(0, Number.isFinite(duration) ? duration : 0);
|
||||
const safeTime = Math.max(0, Number.isFinite(time) ? time : 0);
|
||||
return safeDuration > 0 ? Math.min(safeTime, safeDuration) : safeTime;
|
||||
}
|
||||
|
||||
export function getAdapterDuration(adapter: PlaybackAdapter | null | undefined): number {
|
||||
if (!adapter) return 0;
|
||||
try {
|
||||
const duration = Number(adapter.getDuration());
|
||||
return isFinitePositive(duration) ? duration : 0;
|
||||
} catch {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Clock factory
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export function getDefaultStaticSeekPlaybackClock(win: Window): StaticSeekPlaybackClock {
|
||||
return {
|
||||
now: () => win.performance.now(),
|
||||
requestAnimationFrame: (callback) => win.requestAnimationFrame(callback),
|
||||
cancelAnimationFrame: (handle) => win.cancelAnimationFrame(handle),
|
||||
};
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Static-seek adapter
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Wraps a render-only player (exposes `renderSeek`/`seek` but no native
|
||||
* play/pause) and drives playback via `requestAnimationFrame`.
|
||||
*/
|
||||
export function createStaticSeekPlaybackAdapter(
|
||||
player: Pick<RuntimePlaybackAdapter, "getTime"> &
|
||||
Partial<Pick<RuntimePlaybackAdapter, "renderSeek" | "seek">>,
|
||||
duration: number,
|
||||
clock: StaticSeekPlaybackClock,
|
||||
getPlaybackRate: () => number = () => 1,
|
||||
): PlaybackAdapter {
|
||||
const safeDuration = Math.max(0, Number.isFinite(duration) ? duration : 0);
|
||||
let currentTime = clampTime(Number(player.getTime?.() ?? 0), safeDuration);
|
||||
let playing = false;
|
||||
let rafId = 0;
|
||||
let playStartTime = currentTime;
|
||||
let playStartNow = clock.now();
|
||||
|
||||
const renderSeek = (time: number) => {
|
||||
currentTime = clampTime(time, safeDuration);
|
||||
if (typeof player.renderSeek === "function") {
|
||||
player.renderSeek(currentTime);
|
||||
return;
|
||||
}
|
||||
player.seek?.(currentTime);
|
||||
};
|
||||
|
||||
const stopTicker = () => {
|
||||
if (rafId) {
|
||||
clock.cancelAnimationFrame(rafId);
|
||||
rafId = 0;
|
||||
}
|
||||
};
|
||||
|
||||
const tick: FrameRequestCallback = (now) => {
|
||||
if (!playing) return;
|
||||
const playbackRate = Math.max(0.1, Number(getPlaybackRate()) || 1);
|
||||
const elapsed = ((now - playStartNow) / 1000) * playbackRate;
|
||||
renderSeek(playStartTime + elapsed);
|
||||
if (currentTime >= safeDuration) {
|
||||
playing = false;
|
||||
rafId = 0;
|
||||
return;
|
||||
}
|
||||
rafId = clock.requestAnimationFrame(tick);
|
||||
};
|
||||
|
||||
return {
|
||||
play: () => {
|
||||
if (playing || safeDuration <= 0) return;
|
||||
if (currentTime >= safeDuration) renderSeek(0);
|
||||
playing = true;
|
||||
playStartTime = currentTime;
|
||||
playStartNow = clock.now();
|
||||
stopTicker();
|
||||
rafId = clock.requestAnimationFrame(tick);
|
||||
},
|
||||
pause: () => {
|
||||
playing = false;
|
||||
stopTicker();
|
||||
},
|
||||
seek: (time) => {
|
||||
renderSeek(time);
|
||||
if (playing) {
|
||||
playStartTime = currentTime;
|
||||
playStartNow = clock.now();
|
||||
}
|
||||
},
|
||||
getTime: () => currentTime,
|
||||
getDuration: () => safeDuration,
|
||||
isPlaying: () => playing,
|
||||
};
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// GSAP timeline wrapper
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export function wrapTimeline(tl: TimelineLike): PlaybackAdapter {
|
||||
return {
|
||||
play: () => tl.play(),
|
||||
pause: () => tl.pause(),
|
||||
seek: (t) => {
|
||||
tl.pause();
|
||||
tl.seek(t);
|
||||
},
|
||||
getTime: () => tl.time(),
|
||||
getDuration: () => tl.duration(),
|
||||
isPlaying: () => tl.isActive(),
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user