@hyperframes/core 0.8.206 → 0.8.208
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/compiler/compositionScoping.js +7 -1
- package/dist/compiler/selectorIdTokens.d.ts +1 -1
- package/dist/core.types.d.ts +2 -66
- package/dist/core.types.js +1 -102
- package/dist/fps.d.ts +17 -0
- package/dist/fps.js +58 -0
- package/dist/generated/runtime-inline.js +1 -1
- package/dist/hyperframe.runtime.iife.js +76 -71
- package/dist/removeTree.d.ts +24 -0
- package/dist/removeTree.js +35 -0
- package/dist/runtime/clock.js +1 -1
- package/dist/runtime/mediaSteer.d.ts +11 -0
- package/dist/runtime/mediaSteer.js +41 -0
- package/dist/runtime/protocol.js +2 -1
- package/dist/runtime/state.d.ts +1 -2
- package/dist/runtime/timeline.js +4 -4
- package/dist/runtime/types.d.ts +21 -0
- package/dist/studioPreviewMark.d.ts +2 -0
- package/dist/studioPreviewMark.js +5 -0
- package/package.json +12 -4
- package/dist/runtime/adapters/seek-dispatch.d.ts +0 -37
- package/dist/runtime/adapters/seek-dispatch.js +0 -143
- package/dist/runtime/media.d.ts +0 -128
- package/dist/runtime/media.js +0 -585
- package/dist/runtime/preloadMedia.d.ts +0 -12
- package/dist/runtime/preloadMedia.js +0 -108
- package/dist/runtime/timedClipHide.d.ts +0 -12
- package/dist/runtime/timedClipHide.js +0 -93
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
declare const RETRY: {
|
|
2
|
+
readonly recursive: true;
|
|
3
|
+
readonly force: true;
|
|
4
|
+
readonly maxRetries: 10;
|
|
5
|
+
readonly retryDelay: 100;
|
|
6
|
+
};
|
|
7
|
+
type RemoveSync = (path: string, options: typeof RETRY) => void;
|
|
8
|
+
type RemoveAsync = (path: string, options: typeof RETRY) => Promise<void>;
|
|
9
|
+
type Warn = (message: string) => void;
|
|
10
|
+
/** Removes a tree the next step needs gone, riding out transient locks; throws if it still fails. */
|
|
11
|
+
export declare function removeTreeWithRetry(path: string, remove?: RemoveSync): void;
|
|
12
|
+
/** Async twin of {@link removeTreeWithRetry}. */
|
|
13
|
+
export declare function removeTreeWithRetryAsync(path: string, remove?: RemoveAsync): Promise<void>;
|
|
14
|
+
/** Cleans up after finished work: a refusal that outlasts the retries is warned about, never thrown. */
|
|
15
|
+
export declare function removeTreeBestEffort(path: string, options?: {
|
|
16
|
+
remove?: RemoveSync;
|
|
17
|
+
warn?: Warn;
|
|
18
|
+
}): void;
|
|
19
|
+
/** Async twin of {@link removeTreeBestEffort}. */
|
|
20
|
+
export declare function removeTreeBestEffortAsync(path: string, options?: {
|
|
21
|
+
remove?: RemoveAsync;
|
|
22
|
+
warn?: Warn;
|
|
23
|
+
}): Promise<void>;
|
|
24
|
+
export {};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { rmSync } from "node:fs";
|
|
2
|
+
import { rm } from "node:fs/promises";
|
|
3
|
+
// Windows holds files for a moment after a process exits or while antivirus scans them (EBUSY,
|
|
4
|
+
// EPERM, ENOTEMPTY). Node retries those itself when asked: about 5.5 s in all.
|
|
5
|
+
const RETRY = { recursive: true, force: true, maxRetries: 10, retryDelay: 100 };
|
|
6
|
+
function refusal(path, error) {
|
|
7
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
8
|
+
return `[cleanup] could not remove ${path}: ${reason}`;
|
|
9
|
+
}
|
|
10
|
+
/** Removes a tree the next step needs gone, riding out transient locks; throws if it still fails. */
|
|
11
|
+
export function removeTreeWithRetry(path, remove = rmSync) {
|
|
12
|
+
remove(path, RETRY);
|
|
13
|
+
}
|
|
14
|
+
/** Async twin of {@link removeTreeWithRetry}. */
|
|
15
|
+
export async function removeTreeWithRetryAsync(path, remove = rm) {
|
|
16
|
+
await remove(path, RETRY);
|
|
17
|
+
}
|
|
18
|
+
/** Cleans up after finished work: a refusal that outlasts the retries is warned about, never thrown. */
|
|
19
|
+
export function removeTreeBestEffort(path, options = {}) {
|
|
20
|
+
try {
|
|
21
|
+
removeTreeWithRetry(path, options.remove);
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
(options.warn ?? console.warn)(refusal(path, error));
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
/** Async twin of {@link removeTreeBestEffort}. */
|
|
28
|
+
export async function removeTreeBestEffortAsync(path, options = {}) {
|
|
29
|
+
try {
|
|
30
|
+
await removeTreeWithRetryAsync(path, options.remove);
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
(options.warn ?? console.warn)(refusal(path, error));
|
|
34
|
+
}
|
|
35
|
+
}
|
package/dist/runtime/clock.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { timeAtSourceTime } from "../speedRamp.js";
|
|
2
|
-
import { MEDIA_HARD_SYNC_SECONDS } from "./
|
|
2
|
+
import { MEDIA_HARD_SYNC_SECONDS } from "./mediaSteer.js";
|
|
3
3
|
import { playRangeHoldTime } from "./protocol.js";
|
|
4
4
|
/** GSAP's own `lagSmoothing` default — see the PR body for why this clock needs its own copy. */
|
|
5
5
|
const STALL_THRESHOLD_MS = 500;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** Drift a playing element may carry before sync pulls it back onto the playhead. */
|
|
2
|
+
export declare const MEDIA_SYNC_TOLERANCE_SECONDS = 0.04;
|
|
3
|
+
export declare const MEDIA_HARD_SYNC_SECONDS = 0.5;
|
|
4
|
+
/** Rate or seek for media `offset` s behind (+) or ahead (-); a lag still growing (`settled` false) never seeks. */
|
|
5
|
+
export declare function steerMedia(el: HTMLMediaElement, offset: number, baseRate: number, settled: boolean, audioOnly?: boolean): {
|
|
6
|
+
rate: number;
|
|
7
|
+
seek: boolean;
|
|
8
|
+
};
|
|
9
|
+
/** Drop `el`'s steering: it was seeked, paused or replaced, so its old offset says nothing. */
|
|
10
|
+
export declare function forgetSteering(el: HTMLMediaElement): void;
|
|
11
|
+
export declare function isSteering(el: HTMLMediaElement): boolean;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/** Drift a playing element may carry before sync pulls it back onto the playhead. */
|
|
2
|
+
export const MEDIA_SYNC_TOLERANCE_SECONDS = 0.04;
|
|
3
|
+
export const MEDIA_HARD_SYNC_SECONDS = 0.5;
|
|
4
|
+
// Steered by rate, not seeked (a seek resets a video's decoder and clicks in audio); steps keep rate writes rare.
|
|
5
|
+
const MEDIA_STEER = 0.03;
|
|
6
|
+
const MEDIA_STEER_STEPS = [
|
|
7
|
+
[0.3, 0.25],
|
|
8
|
+
[0.1, 0.1],
|
|
9
|
+
];
|
|
10
|
+
const MEDIA_STEER_RELEASE_SECONDS = 0.01;
|
|
11
|
+
// Audio never steers past 10%, where speech changes tempo; a lag held past 100 ms is seeked instead.
|
|
12
|
+
const AUDIO_MAX_STEER = 0.1;
|
|
13
|
+
const AUDIO_STEER_LIMIT_SECONDS = 0.1;
|
|
14
|
+
/** Direction (+1 fast, -1 slow) an element is being steered in; absent when it plays at its authored rate. */
|
|
15
|
+
const steering = new WeakMap();
|
|
16
|
+
/** Rate or seek for media `offset` s behind (+) or ahead (-); a lag still growing (`settled` false) never seeks. */
|
|
17
|
+
export function steerMedia(el, offset, baseRate, settled, audioOnly = el.tagName === "AUDIO") {
|
|
18
|
+
const lag = Math.abs(offset);
|
|
19
|
+
if (audioOnly && settled && lag > AUDIO_STEER_LIMIT_SECONDS) {
|
|
20
|
+
steering.delete(el);
|
|
21
|
+
return { rate: baseRate, seek: true };
|
|
22
|
+
}
|
|
23
|
+
const direction = Math.sign(offset);
|
|
24
|
+
const current = steering.get(el);
|
|
25
|
+
if (lag > MEDIA_SYNC_TOLERANCE_SECONDS)
|
|
26
|
+
steering.set(el, direction);
|
|
27
|
+
else if (current !== direction || lag <= MEDIA_STEER_RELEASE_SECONDS) {
|
|
28
|
+
steering.delete(el);
|
|
29
|
+
return { rate: baseRate, seek: false };
|
|
30
|
+
}
|
|
31
|
+
const step = MEDIA_STEER_STEPS.find(([from]) => lag > from)?.[1] ?? MEDIA_STEER;
|
|
32
|
+
const steer = audioOnly ? Math.min(step, AUDIO_MAX_STEER) : step;
|
|
33
|
+
return { rate: baseRate * (1 + direction * steer), seek: false };
|
|
34
|
+
}
|
|
35
|
+
/** Drop `el`'s steering: it was seeked, paused or replaced, so its old offset says nothing. */
|
|
36
|
+
export function forgetSteering(el) {
|
|
37
|
+
steering.delete(el);
|
|
38
|
+
}
|
|
39
|
+
export function isSteering(el) {
|
|
40
|
+
return steering.has(el);
|
|
41
|
+
}
|
package/dist/runtime/protocol.js
CHANGED
|
@@ -20,7 +20,8 @@ function greatestCommonDivisor(a, b) {
|
|
|
20
20
|
}
|
|
21
21
|
export function runtimeProtocolFpsFromNumber(value) {
|
|
22
22
|
const safe = Number.isFinite(value) && value > 0 ? value : 30;
|
|
23
|
-
const
|
|
23
|
+
const isNtscRate = Math.abs(safe * 1001 - Math.round(safe * 1001)) < 1e-6;
|
|
24
|
+
const denominator = Number.isInteger(safe) ? 1 : isNtscRate ? 1001 : 1_000_000;
|
|
24
25
|
const numerator = Math.round(safe * denominator);
|
|
25
26
|
const divisor = greatestCommonDivisor(numerator, denominator);
|
|
26
27
|
return { numerator: numerator / divisor, denominator: denominator / divisor };
|
package/dist/runtime/state.d.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import type { RuntimeDeterministicAdapter, RuntimeTimelineLike } from "./types.js";
|
|
2
|
-
import type { RuntimeMediaClip } from "./media.js";
|
|
1
|
+
import type { RuntimeDeterministicAdapter, RuntimeMediaClip, RuntimeTimelineLike } from "./types.js";
|
|
3
2
|
import type { TransportClock } from "./clock.js";
|
|
4
3
|
export type RuntimeState = {
|
|
5
4
|
capturedTimeline: RuntimeTimelineLike | null;
|
package/dist/runtime/timeline.js
CHANGED
|
@@ -3,7 +3,7 @@ import { findRootCompositionElement, parseCompositionDimension } from "./composi
|
|
|
3
3
|
import { AUTHORED_DURATION_ATTR, AUTHORED_END_ATTR, resolveAuthoredTimingWindow, } from "./authoredTiming.js";
|
|
4
4
|
import { swallow } from "./diagnostics.js";
|
|
5
5
|
import { readTimelineDurationSeconds } from "./timelineDuration.js";
|
|
6
|
-
import { readElementPlaybackRate,
|
|
6
|
+
import { readElementPlaybackRate, readMediaStart } from "./playbackRate.js";
|
|
7
7
|
import { parseStrictFiniteTimingNumber, resolveMediaElementDurationSeconds, resolveNaturalMediaTimelineDuration, resolveTimedImageDurationSeconds, } from "./playbackRate.js";
|
|
8
8
|
import { resolveCssStackingContextId } from "./stackingContext.js";
|
|
9
9
|
import { createRuntimeStartTimeResolver } from "./startResolver.js";
|
|
@@ -426,7 +426,7 @@ export function collectRuntimeTimelinePayload(params) {
|
|
|
426
426
|
parentCompositionId: compositionContext.parentCompositionId,
|
|
427
427
|
nodePath: null,
|
|
428
428
|
compositionSrc: toAbsoluteAssetUrl(node.getAttribute("data-composition-src")),
|
|
429
|
-
playbackStart:
|
|
429
|
+
playbackStart: readMediaStart(node),
|
|
430
430
|
playbackRate: readElementPlaybackRate(node),
|
|
431
431
|
assetUrl: resolveNodeAssetUrl(node),
|
|
432
432
|
timelineRole: node.getAttribute("data-timeline-role"),
|
|
@@ -560,7 +560,7 @@ export function collectRuntimeTimelinePayload(params) {
|
|
|
560
560
|
parentCompositionId: rootCompositionIdForGsap,
|
|
561
561
|
nodePath: null,
|
|
562
562
|
compositionSrc: null,
|
|
563
|
-
playbackStart:
|
|
563
|
+
playbackStart: readMediaStart(el),
|
|
564
564
|
playbackRate: readElementPlaybackRate(el),
|
|
565
565
|
assetUrl: null,
|
|
566
566
|
timelineRole: el.getAttribute("data-timeline-role"),
|
|
@@ -622,7 +622,7 @@ export function collectRuntimeTimelinePayload(params) {
|
|
|
622
622
|
parentCompositionId: rootCompositionIdForGsap,
|
|
623
623
|
nodePath: null,
|
|
624
624
|
compositionSrc: null,
|
|
625
|
-
playbackStart:
|
|
625
|
+
playbackStart: readMediaStart(el),
|
|
626
626
|
playbackRate: readElementPlaybackRate(el),
|
|
627
627
|
assetUrl: null,
|
|
628
628
|
timelineRole,
|
package/dist/runtime/types.d.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import type { HfColorGradingTarget } from "../colorGrading.js";
|
|
2
2
|
import type { RuntimeAnalyticsEvent } from "./analytics.js";
|
|
3
|
+
import type { AudioFades } from "../audioFade.js";
|
|
4
|
+
import type { RateSpec } from "../speedRamp.js";
|
|
5
|
+
import type { VolumeKeyframe } from "./mediaVolumeEnvelope.js";
|
|
3
6
|
export type RuntimeJson = string | number | boolean | null | RuntimeJson[] | {
|
|
4
7
|
[key: string]: RuntimeJson;
|
|
5
8
|
};
|
|
@@ -378,4 +381,22 @@ type RuntimeDataControlFields = {
|
|
|
378
381
|
};
|
|
379
382
|
type RuntimeBridgeControlAction = RuntimeBridgeControlActionBase | "set-runtime-data" | "clear-runtime-data";
|
|
380
383
|
export type RuntimeBridgeControlMessage = RuntimeBridgeControlMessageBase & RuntimeDataControlFields;
|
|
384
|
+
export type RuntimeMediaClip = {
|
|
385
|
+
el: HTMLVideoElement | HTMLAudioElement;
|
|
386
|
+
start: number;
|
|
387
|
+
/** The clip's own t=0 when a host's in-point cut its head; source time, lanes and fades run from it. */
|
|
388
|
+
origin?: number;
|
|
389
|
+
mediaStart: number;
|
|
390
|
+
duration: number;
|
|
391
|
+
end: number;
|
|
392
|
+
volume: number | null;
|
|
393
|
+
playbackRate: number;
|
|
394
|
+
/** The rate lane when the clip has one; otherwise `playbackRate`. */
|
|
395
|
+
rate?: RateSpec;
|
|
396
|
+
loop: boolean;
|
|
397
|
+
sourceDuration: number | null;
|
|
398
|
+
/** Probed from the GSAP timeline as the renderer does; when set they drive volume, not `data-volume`. */
|
|
399
|
+
volumeKeyframes?: VolumeKeyframe[];
|
|
400
|
+
fades?: AudioFades;
|
|
401
|
+
};
|
|
381
402
|
export {};
|
|
@@ -3,6 +3,8 @@ export declare const STUDIO_PREVIEW_CAPTURE_MODE = "capture";
|
|
|
3
3
|
export declare const STUDIO_PREVIEW_UPCOMING_ATTR = "data-hf-upcoming";
|
|
4
4
|
export declare const STUDIO_PREVIEW_LAZY_ATTR = "data-hf-preview-lazy";
|
|
5
5
|
export declare const STUDIO_PREVIEW_ERRORS = "__hfPreviewErrors";
|
|
6
|
+
export declare const refusedPreviewMessage: (status: number) => string;
|
|
7
|
+
export declare function refusedPreviewStatus(message: string): number | null;
|
|
6
8
|
export declare const PREVIEW_RASTER_ATTR = "data-hf-preview-raster";
|
|
7
9
|
export declare const RUNTIME_STAMPED_ATTR = "data-hf-autostamped";
|
|
8
10
|
export declare const CLIP_HIDDEN_ATTR = "data-hf-clip-hidden";
|
|
@@ -3,6 +3,11 @@ export const STUDIO_PREVIEW_CAPTURE_MODE = "capture";
|
|
|
3
3
|
export const STUDIO_PREVIEW_UPCOMING_ATTR = "data-hf-upcoming";
|
|
4
4
|
export const STUDIO_PREVIEW_LAZY_ATTR = "data-hf-preview-lazy";
|
|
5
5
|
export const STUDIO_PREVIEW_ERRORS = "__hfPreviewErrors";
|
|
6
|
+
export const refusedPreviewMessage = (status) => `The preview page could not be loaded (HTTP ${status}).`;
|
|
7
|
+
export function refusedPreviewStatus(message) {
|
|
8
|
+
const status = /^The preview page could not be loaded \(HTTP (\d{3})\)\.$/.exec(message)?.[1];
|
|
9
|
+
return status ? Number(status) : null;
|
|
10
|
+
}
|
|
6
11
|
export const PREVIEW_RASTER_ATTR = "data-hf-preview-raster";
|
|
7
12
|
export const RUNTIME_STAMPED_ATTR = "data-hf-autostamped";
|
|
8
13
|
export const CLIP_HIDDEN_ATTR = "data-hf-clip-hidden";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hyperframes/core",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.208",
|
|
4
4
|
"description": "",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -60,6 +60,10 @@
|
|
|
60
60
|
"import": "./dist/atomicFile.js",
|
|
61
61
|
"types": "./dist/atomicFile.d.ts"
|
|
62
62
|
},
|
|
63
|
+
"./remove-tree": {
|
|
64
|
+
"import": "./dist/removeTree.js",
|
|
65
|
+
"types": "./dist/removeTree.d.ts"
|
|
66
|
+
},
|
|
63
67
|
"./project-folders": {
|
|
64
68
|
"import": "./dist/projectFolders.js",
|
|
65
69
|
"types": "./dist/projectFolders.d.ts"
|
|
@@ -273,6 +277,10 @@
|
|
|
273
277
|
"import": "./dist/runtime/mediaClock.js",
|
|
274
278
|
"types": "./dist/runtime/mediaClock.d.ts"
|
|
275
279
|
},
|
|
280
|
+
"./runtime/media-steer": {
|
|
281
|
+
"import": "./dist/runtime/mediaSteer.js",
|
|
282
|
+
"types": "./dist/runtime/mediaSteer.d.ts"
|
|
283
|
+
},
|
|
276
284
|
"./runtime/clip-reveal": {
|
|
277
285
|
"import": "./dist/runtime/clipReveal.js",
|
|
278
286
|
"types": "./dist/runtime/clipReveal.d.ts"
|
|
@@ -409,9 +417,9 @@
|
|
|
409
417
|
"linkedom": "^0.18.12",
|
|
410
418
|
"postcss": "^8.5.8",
|
|
411
419
|
"wawoff2": "^2.0.1",
|
|
412
|
-
"@hyperframes/lint": "0.8.
|
|
413
|
-
"@hyperframes/studio-server": "0.8.
|
|
414
|
-
"@hyperframes/parsers": "0.8.
|
|
420
|
+
"@hyperframes/lint": "0.8.208",
|
|
421
|
+
"@hyperframes/studio-server": "0.8.208",
|
|
422
|
+
"@hyperframes/parsers": "0.8.208"
|
|
415
423
|
},
|
|
416
424
|
"devDependencies": {
|
|
417
425
|
"@fontsource/archivo-black": "^5.2.8",
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
export interface HfSeekEventDetail {
|
|
2
|
-
time: number;
|
|
3
|
-
waitUntil: (promise: PromiseLike<unknown>) => void;
|
|
4
|
-
}
|
|
5
|
-
/**
|
|
6
|
-
* Join `promise` to the seek-completion barrier: `waitForSeekCompletion` will
|
|
7
|
-
* not resolve until it settles, and its rejection (the first one) becomes the
|
|
8
|
-
* barrier's.
|
|
9
|
-
*
|
|
10
|
-
* Exported because the `"hf-seek"` event's synchronous `waitUntil()` is not
|
|
11
|
-
* the only asynchronous seek work a frame can owe. `runtime/vfx.ts` registers
|
|
12
|
-
* its preview-side capture here, so the CLI seek paths that already await this
|
|
13
|
-
* barrier wait for a `self` chain's paint instead of racing it.
|
|
14
|
-
*/
|
|
15
|
-
export declare function registerSeekCompletion(promise: Promise<unknown>, kind?: "video-frame"): void;
|
|
16
|
-
export declare function dispatchSeekEvent(time: number): void;
|
|
17
|
-
/**
|
|
18
|
-
* Force-dispatch a `"hf-seek"` event even if `time` equals the last dispatched
|
|
19
|
-
* time, bypassing the dedup guard.
|
|
20
|
-
*
|
|
21
|
-
* Needed for the post-video-injection GPU re-render: the engine seeks to time
|
|
22
|
-
* T (GPU adapters render once, before video frames are injected), then injects
|
|
23
|
-
* the decoded `__render_frame__` images, then must re-render GPU compositions
|
|
24
|
-
* at the *same* T so they re-upload textures from the now-present frames. The
|
|
25
|
-
* normal dedup would swallow that second dispatch.
|
|
26
|
-
*/
|
|
27
|
-
export declare function forceDispatchSeekEvent(time: number): void;
|
|
28
|
-
export declare function isSeekCompletionBarrierActive(): boolean;
|
|
29
|
-
/** Settles with every pending completion, without consuming a failure the barrier reports. */
|
|
30
|
-
export declare function seekCompletionsSettled(): Promise<void>;
|
|
31
|
-
export declare function hasPendingSeekCompletion(): boolean;
|
|
32
|
-
/** `videoFrames: false` skips only the video-frame holds: a geometry read needs every other completion. */
|
|
33
|
-
export declare function waitForSeekCompletion(options?: {
|
|
34
|
-
videoFrames?: boolean;
|
|
35
|
-
}): Promise<void>;
|
|
36
|
-
/** Reset internal state — used in tests to prevent cross-test contamination. */
|
|
37
|
-
export declare function resetSeekDispatchState(): void;
|
|
@@ -1,143 +0,0 @@
|
|
|
1
|
-
import { swallow } from "../diagnostics.js";
|
|
2
|
-
/**
|
|
3
|
-
* Shared, deduplicated `"hf-seek"` CustomEvent dispatcher for GPU adapters.
|
|
4
|
-
*
|
|
5
|
-
* Both the Three.js and TypeGPU adapters dispatch the same `"hf-seek"` event
|
|
6
|
-
* so that compositions need not know which GPU library they're paired with.
|
|
7
|
-
* Without deduplication, a seek to time T would fire two events (one from each
|
|
8
|
-
* adapter), doubling per-scrub work in any composition that has both present.
|
|
9
|
-
*
|
|
10
|
-
* This module deduplicates by tracking the last dispatched time. If the same
|
|
11
|
-
* time value is dispatched twice in the same synchronous call stack (e.g. two
|
|
12
|
-
* adapters both calling `dispatchSeekEvent(5.0)` without yielding), only the
|
|
13
|
-
* first call fires the event.
|
|
14
|
-
*
|
|
15
|
-
* The deduplication is intentionally coarse (exact float equality). Adapter
|
|
16
|
-
* seek paths clamp and normalise time before calling this function, so the
|
|
17
|
-
* values that arrive here are already stable.
|
|
18
|
-
*/
|
|
19
|
-
let _lastDispatchedTime = -1;
|
|
20
|
-
let _pendingCompletions = new Set();
|
|
21
|
-
let _videoFrameCompletions = new WeakSet();
|
|
22
|
-
let _pendingFailure;
|
|
23
|
-
let _activeCompletionBarriers = 0;
|
|
24
|
-
function dispatch(time) {
|
|
25
|
-
const pending = [];
|
|
26
|
-
let accepting = true;
|
|
27
|
-
const detail = {
|
|
28
|
-
time,
|
|
29
|
-
waitUntil: (promise) => {
|
|
30
|
-
if (!accepting) {
|
|
31
|
-
throw new Error("hf-seek waitUntil() must be called synchronously from the event listener");
|
|
32
|
-
}
|
|
33
|
-
pending.push(promise);
|
|
34
|
-
},
|
|
35
|
-
};
|
|
36
|
-
try {
|
|
37
|
-
window.dispatchEvent(new CustomEvent("hf-seek", { detail }));
|
|
38
|
-
}
|
|
39
|
-
catch (err) {
|
|
40
|
-
swallow("runtime.adapters.seek-dispatch.site1", err);
|
|
41
|
-
}
|
|
42
|
-
finally {
|
|
43
|
-
accepting = false;
|
|
44
|
-
}
|
|
45
|
-
if (pending.length === 0)
|
|
46
|
-
return;
|
|
47
|
-
registerSeekCompletion(Promise.all(pending));
|
|
48
|
-
}
|
|
49
|
-
/**
|
|
50
|
-
* Join `promise` to the seek-completion barrier: `waitForSeekCompletion` will
|
|
51
|
-
* not resolve until it settles, and its rejection (the first one) becomes the
|
|
52
|
-
* barrier's.
|
|
53
|
-
*
|
|
54
|
-
* Exported because the `"hf-seek"` event's synchronous `waitUntil()` is not
|
|
55
|
-
* the only asynchronous seek work a frame can owe. `runtime/vfx.ts` registers
|
|
56
|
-
* its preview-side capture here, so the CLI seek paths that already await this
|
|
57
|
-
* barrier wait for a `self` chain's paint instead of racing it.
|
|
58
|
-
*/
|
|
59
|
-
export function registerSeekCompletion(promise, kind) {
|
|
60
|
-
const completion = promise
|
|
61
|
-
.then(() => ({ status: "fulfilled" }))
|
|
62
|
-
.catch((reason) => ({ status: "rejected", reason }));
|
|
63
|
-
_pendingCompletions.add(completion);
|
|
64
|
-
if (kind === "video-frame")
|
|
65
|
-
_videoFrameCompletions.add(completion);
|
|
66
|
-
void completion.then((result) => {
|
|
67
|
-
if (!_pendingCompletions.delete(completion))
|
|
68
|
-
return;
|
|
69
|
-
if (result.status === "rejected" && _pendingFailure === undefined) {
|
|
70
|
-
_pendingFailure = { reason: result.reason };
|
|
71
|
-
}
|
|
72
|
-
});
|
|
73
|
-
}
|
|
74
|
-
export function dispatchSeekEvent(time) {
|
|
75
|
-
if (time === _lastDispatchedTime)
|
|
76
|
-
return;
|
|
77
|
-
_lastDispatchedTime = time;
|
|
78
|
-
dispatch(time);
|
|
79
|
-
}
|
|
80
|
-
/**
|
|
81
|
-
* Force-dispatch a `"hf-seek"` event even if `time` equals the last dispatched
|
|
82
|
-
* time, bypassing the dedup guard.
|
|
83
|
-
*
|
|
84
|
-
* Needed for the post-video-injection GPU re-render: the engine seeks to time
|
|
85
|
-
* T (GPU adapters render once, before video frames are injected), then injects
|
|
86
|
-
* the decoded `__render_frame__` images, then must re-render GPU compositions
|
|
87
|
-
* at the *same* T so they re-upload textures from the now-present frames. The
|
|
88
|
-
* normal dedup would swallow that second dispatch.
|
|
89
|
-
*/
|
|
90
|
-
export function forceDispatchSeekEvent(time) {
|
|
91
|
-
_lastDispatchedTime = time;
|
|
92
|
-
dispatch(time);
|
|
93
|
-
}
|
|
94
|
-
export function isSeekCompletionBarrierActive() {
|
|
95
|
-
return _activeCompletionBarriers > 0;
|
|
96
|
-
}
|
|
97
|
-
/** Settles with every pending completion, without consuming a failure the barrier reports. */
|
|
98
|
-
export async function seekCompletionsSettled() {
|
|
99
|
-
for (let pending = [..._pendingCompletions]; pending.length > 0; pending = [..._pendingCompletions]) {
|
|
100
|
-
await Promise.all(pending);
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
export function hasPendingSeekCompletion() {
|
|
104
|
-
return _pendingCompletions.size > 0;
|
|
105
|
-
}
|
|
106
|
-
/** `videoFrames: false` skips only the video-frame holds: a geometry read needs every other completion. */
|
|
107
|
-
export async function waitForSeekCompletion(options) {
|
|
108
|
-
const awaited = () => [..._pendingCompletions].filter((completion) => options?.videoFrames !== false || !_videoFrameCompletions.has(completion));
|
|
109
|
-
_activeCompletionBarriers += 1;
|
|
110
|
-
let failed = _pendingFailure;
|
|
111
|
-
try {
|
|
112
|
-
// Let concurrently started barriers snapshot the same retained failure
|
|
113
|
-
// before either one consumes it.
|
|
114
|
-
await Promise.resolve();
|
|
115
|
-
for (let pending = awaited(); pending.length > 0; pending = awaited()) {
|
|
116
|
-
const results = await Promise.all(pending);
|
|
117
|
-
const rejected = results.find((result) => result.status === "rejected");
|
|
118
|
-
if (failed === undefined && rejected?.status === "rejected") {
|
|
119
|
-
failed = { reason: rejected.reason };
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
const retainedFailure = _pendingFailure;
|
|
123
|
-
if (failed === undefined) {
|
|
124
|
-
failed = retainedFailure;
|
|
125
|
-
}
|
|
126
|
-
if (_pendingFailure === retainedFailure) {
|
|
127
|
-
_pendingFailure = undefined;
|
|
128
|
-
}
|
|
129
|
-
if (failed)
|
|
130
|
-
throw failed.reason;
|
|
131
|
-
}
|
|
132
|
-
finally {
|
|
133
|
-
_activeCompletionBarriers -= 1;
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
/** Reset internal state — used in tests to prevent cross-test contamination. */
|
|
137
|
-
export function resetSeekDispatchState() {
|
|
138
|
-
_lastDispatchedTime = -1;
|
|
139
|
-
_pendingCompletions = new Set();
|
|
140
|
-
_videoFrameCompletions = new WeakSet();
|
|
141
|
-
_pendingFailure = undefined;
|
|
142
|
-
_activeCompletionBarriers = 0;
|
|
143
|
-
}
|
package/dist/runtime/media.d.ts
DELETED
|
@@ -1,128 +0,0 @@
|
|
|
1
|
-
import { type VolumeKeyframe } from "./mediaVolumeEnvelope.js";
|
|
2
|
-
import { type AudioFades } from "../audioFade.js";
|
|
3
|
-
import { type RateSpec } from "../speedRamp.js";
|
|
4
|
-
import { type HostSlot } from "../mediaTiming.js";
|
|
5
|
-
export { readElementPlaybackRate, readElementRateSpec, resolveNaturalMediaTimelineDuration, } from "./playbackRate.js";
|
|
6
|
-
export declare function readElementPlaybackStart(el: Element): number;
|
|
7
|
-
export declare const HOLD_CAP_MS = 5000;
|
|
8
|
-
/**
|
|
9
|
-
* Resolve a media element's timeline window without conflating a video's
|
|
10
|
-
* authored display slot with the amount of source left to decode.
|
|
11
|
-
*
|
|
12
|
-
* An explicit video slot may outlive its source and holds the final frame.
|
|
13
|
-
* Audio remains source-bounded because it has no visual hold state.
|
|
14
|
-
*/
|
|
15
|
-
export declare function resolveRuntimeMediaClipDuration(params: {
|
|
16
|
-
isVideo: boolean;
|
|
17
|
-
sourceDuration: number | null;
|
|
18
|
-
hostRemaining: number | null;
|
|
19
|
-
explicitDuration: number | null;
|
|
20
|
-
}): number | null;
|
|
21
|
-
export type RuntimeMediaClip = {
|
|
22
|
-
el: HTMLVideoElement | HTMLAudioElement;
|
|
23
|
-
start: number;
|
|
24
|
-
/** The clip's own t=0 when a host's in-point cut its head; source time, lanes and fades run from it. */
|
|
25
|
-
origin?: number;
|
|
26
|
-
mediaStart: number;
|
|
27
|
-
duration: number;
|
|
28
|
-
end: number;
|
|
29
|
-
volume: number | null;
|
|
30
|
-
playbackRate: number;
|
|
31
|
-
/** The rate lane when the clip has one; otherwise `playbackRate`. */
|
|
32
|
-
rate?: RateSpec;
|
|
33
|
-
loop: boolean;
|
|
34
|
-
/** Source media duration in seconds (from el.duration). Used for loop wrapping. */
|
|
35
|
-
sourceDuration: number | null;
|
|
36
|
-
/**
|
|
37
|
-
* Probed volume keyframes from the GSAP timeline (same probe the renderer
|
|
38
|
-
* uses). When present, `syncRuntimeMedia` drives volume from the envelope
|
|
39
|
-
* rather than from `data-volume` + GSAP-change tracking, eliminating the
|
|
40
|
-
* race between the 60 Hz transport tick and GSAP's own seek.
|
|
41
|
-
*/
|
|
42
|
-
volumeKeyframes?: VolumeKeyframe[];
|
|
43
|
-
/** Clip-edge fades from `data-fade-in` / `data-fade-out`; see audioFade.ts. */
|
|
44
|
-
fades?: AudioFades;
|
|
45
|
-
};
|
|
46
|
-
export declare function refreshRuntimeMediaCache(params?: {
|
|
47
|
-
resolveStartSeconds?: (element: Element) => number;
|
|
48
|
-
resolveDurationSeconds?: (element: HTMLVideoElement | HTMLAudioElement) => number | null;
|
|
49
|
-
resolveHostSlots?: (element: HTMLVideoElement | HTMLAudioElement) => HostSlot[];
|
|
50
|
-
shouldIncludeElement?: (element: HTMLVideoElement | HTMLAudioElement) => boolean;
|
|
51
|
-
/**
|
|
52
|
-
* Build clips for exactly these elements instead of scanning the document for
|
|
53
|
-
* them. For a caller that already knows which media it needs this pass — the
|
|
54
|
-
* per-seek sync only touches the clips whose active state can change — and so
|
|
55
|
-
* should not pay to re-derive the window of every clip in the composition.
|
|
56
|
-
*
|
|
57
|
-
* Every field is still read FRESH from the element. This narrows which
|
|
58
|
-
* elements are visited, never how current the answer is: `el.duration` can be
|
|
59
|
-
* reset under the caller by an `el.load()` it did not make.
|
|
60
|
-
*/
|
|
61
|
-
elements?: Array<HTMLVideoElement | HTMLAudioElement>;
|
|
62
|
-
}): {
|
|
63
|
-
timedMediaEls: Array<HTMLVideoElement | HTMLAudioElement>;
|
|
64
|
-
mediaClips: RuntimeMediaClip[];
|
|
65
|
-
videoClips: RuntimeMediaClip[];
|
|
66
|
-
maxMediaEnd: number;
|
|
67
|
-
};
|
|
68
|
-
export declare function isUnplayable(el: HTMLMediaElement): boolean;
|
|
69
|
-
/**
|
|
70
|
-
* Drop every per-source sync baseline tracked for `el` — offset drift
|
|
71
|
-
* samples, the seek-past-buffered-range retry latch, and the last
|
|
72
|
-
* runtime-applied volume — so the next `syncRuntimeMedia` tick treats it as
|
|
73
|
-
* a first tick (hard resync, fresh drift baseline) instead of comparing
|
|
74
|
-
* against state computed for a different file. Used both when a clip leaves
|
|
75
|
-
* its active window (below) and by the runtime's proxy-swap helper
|
|
76
|
-
* (mediaProxy.ts) right after an in-place `src` swap, which points the same
|
|
77
|
-
* element at a different file without ever leaving its active window.
|
|
78
|
-
*/
|
|
79
|
-
export declare function evictMediaSyncState(el: HTMLMediaElement): void;
|
|
80
|
-
/** Test-only seam: whether any per-source sync state is still tracked for `el`. */
|
|
81
|
-
export declare function hasMediaSyncStateForTest(el: HTMLMediaElement): boolean;
|
|
82
|
-
export declare const MEDIA_HARD_SYNC_SECONDS = 0.5;
|
|
83
|
-
export declare const VIDEO_PRESEEK_SECONDS = 1.5;
|
|
84
|
-
export declare function mediaSourceTimeAt(clip: RuntimeMediaClip, timeSeconds: number): number;
|
|
85
|
-
/** Drift a playing audio element may carry before sync pulls it back onto the playhead. */
|
|
86
|
-
export declare const MEDIA_SYNC_TOLERANCE_SECONDS = 0.04;
|
|
87
|
-
export declare function syncRuntimeMedia(params: {
|
|
88
|
-
clips: RuntimeMediaClip[];
|
|
89
|
-
timeSeconds: number;
|
|
90
|
-
playing: boolean;
|
|
91
|
-
playbackRate: number;
|
|
92
|
-
/** Force-mute every element (parent-frame proxy owns all audio). Asserted per
|
|
93
|
-
* tick so sub-composition media added mid-playback inherits the silence. */
|
|
94
|
-
outputMuted?: boolean;
|
|
95
|
-
/**
|
|
96
|
-
* User's explicit mute preference (set via `onSetMuted`). Symmetric to
|
|
97
|
-
* `outputMuted` — also asserted per tick — so a sub-composition that
|
|
98
|
-
* activates after the user mutes doesn't briefly play at author volume
|
|
99
|
-
* before the next bridge message lands.
|
|
100
|
-
*/
|
|
101
|
-
userMuted?: boolean;
|
|
102
|
-
/**
|
|
103
|
-
* User's volume preference (0–1, set via `onSetVolume`). Multiplied with the
|
|
104
|
-
* per-clip author volume so `data-volume="0.5"` at user volume 0.8 yields 0.4.
|
|
105
|
-
*/
|
|
106
|
-
userVolume?: number;
|
|
107
|
-
/**
|
|
108
|
-
* Invoked at most once when a media element's `play()` promise rejects with
|
|
109
|
-
* `NotAllowedError`. The caller is expected to latch and post a single
|
|
110
|
-
* outbound message; further invocations are suppressed by the caller.
|
|
111
|
-
*/
|
|
112
|
-
onAutoplayBlocked?: () => void;
|
|
113
|
-
/** `clipGain` is `data-volume` for a routed clip, played ahead of its FX; 1 otherwise. */
|
|
114
|
-
onElementVolume?: (el: HTMLMediaElement, effectiveVolume: number, authorVolume: number, clipGain: number, unfadedVolume?: number) => void;
|
|
115
|
-
/** Is THIS element owned by the Web Audio transport? Owned → mute it (transport
|
|
116
|
-
* plays it); not owned → leave audible (HTMLMedia fallback). Per-element, not a
|
|
117
|
-
* global flag, so a not-yet-claimed track isn't muted by other tracks. */
|
|
118
|
-
isWebAudioOwned?: (el: HTMLMediaElement) => boolean;
|
|
119
|
-
/** Native media routed through WebAudio keeps its upstream element volume at
|
|
120
|
-
* unity; do not mistake that transport write for an authored volume edit. */
|
|
121
|
-
isWebAudioRouted?: (el: HTMLMediaElement) => boolean;
|
|
122
|
-
forceSync?: boolean;
|
|
123
|
-
/** How far the next tick will move the playhead: an audio clip due within it starts now. */
|
|
124
|
-
cueAheadSeconds?: number;
|
|
125
|
-
/** Lets a video clip that runs to the composition end hold its last frame at the terminal time.
|
|
126
|
-
* A thunk, because deriving the duration is only worth it for a clip past its own end. */
|
|
127
|
-
getCompositionDuration: () => number;
|
|
128
|
-
}): void;
|