@hyperframes/core 0.8.206 → 0.8.207
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 +74 -69
- package/dist/removeTree.d.ts +24 -0
- package/dist/removeTree.js +35 -0
- package/dist/runtime/media.d.ts +6 -3
- package/dist/runtime/media.js +50 -49
- package/dist/runtime/mediaSteer.d.ts +10 -0
- package/dist/runtime/mediaSteer.js +40 -0
- package/dist/runtime/protocol.js +2 -1
- package/dist/studioPreviewMark.d.ts +2 -0
- package/dist/studioPreviewMark.js +5 -0
- package/package.json +12 -4
|
@@ -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/media.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { type VolumeKeyframe } from "./mediaVolumeEnvelope.js";
|
|
|
2
2
|
import { type AudioFades } from "../audioFade.js";
|
|
3
3
|
import { type RateSpec } from "../speedRamp.js";
|
|
4
4
|
import { type HostSlot } from "../mediaTiming.js";
|
|
5
|
+
export { MEDIA_SYNC_TOLERANCE_SECONDS } from "./mediaSteer.js";
|
|
5
6
|
export { readElementPlaybackRate, readElementRateSpec, resolveNaturalMediaTimelineDuration, } from "./playbackRate.js";
|
|
6
7
|
export declare function readElementPlaybackStart(el: Element): number;
|
|
7
8
|
export declare const HOLD_CAP_MS = 5000;
|
|
@@ -80,10 +81,8 @@ export declare function evictMediaSyncState(el: HTMLMediaElement): void;
|
|
|
80
81
|
/** Test-only seam: whether any per-source sync state is still tracked for `el`. */
|
|
81
82
|
export declare function hasMediaSyncStateForTest(el: HTMLMediaElement): boolean;
|
|
82
83
|
export declare const MEDIA_HARD_SYNC_SECONDS = 0.5;
|
|
83
|
-
export declare const
|
|
84
|
+
export declare const MEDIA_PRESEEK_SECONDS = 1.5;
|
|
84
85
|
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
86
|
export declare function syncRuntimeMedia(params: {
|
|
88
87
|
clips: RuntimeMediaClip[];
|
|
89
88
|
timeSeconds: number;
|
|
@@ -120,8 +119,12 @@ export declare function syncRuntimeMedia(params: {
|
|
|
120
119
|
* unity; do not mistake that transport write for an authored volume edit. */
|
|
121
120
|
isWebAudioRouted?: (el: HTMLMediaElement) => boolean;
|
|
122
121
|
forceSync?: boolean;
|
|
122
|
+
/** False while the playhead is not moving (audio output has not started): a paused video waits for it. */
|
|
123
|
+
clockAdvancing?: boolean;
|
|
123
124
|
/** How far the next tick will move the playhead: an audio clip due within it starts now. */
|
|
124
125
|
cueAheadSeconds?: number;
|
|
126
|
+
/** Seeks a sounding element under a gain dip; false when it has no dip stage and the sync seeks it plainly. */
|
|
127
|
+
seekUnderDip?: (el: HTMLMediaElement, target: number) => boolean;
|
|
125
128
|
/** Lets a video clip that runs to the composition end hold its last frame at the terminal time.
|
|
126
129
|
* A thunk, because deriving the duration is only worth it for a clip past its own end. */
|
|
127
130
|
getCompositionDuration: () => number;
|
package/dist/runtime/media.js
CHANGED
|
@@ -13,6 +13,8 @@ import { findInjectedRenderFrame } from "./renderFrameSibling.js";
|
|
|
13
13
|
import { isRenderFrameVideo } from "./preloadMedia.js";
|
|
14
14
|
import { registerSeekCompletion } from "./adapters/seek-dispatch.js";
|
|
15
15
|
import { cutToHostSlots } from "../mediaTiming.js";
|
|
16
|
+
import { forgetSteering, isSteering, MEDIA_SYNC_TOLERANCE_SECONDS, steerMedia, } from "./mediaSteer.js";
|
|
17
|
+
export { MEDIA_SYNC_TOLERANCE_SECONDS } from "./mediaSteer.js";
|
|
16
18
|
export { readElementPlaybackRate, readElementRateSpec, resolveNaturalMediaTimelineDuration, } from "./playbackRate.js";
|
|
17
19
|
export function readElementPlaybackStart(el) {
|
|
18
20
|
return readMediaStart(el);
|
|
@@ -181,7 +183,8 @@ export function evictMediaSyncState(el) {
|
|
|
181
183
|
strictDriftSamples.delete(el);
|
|
182
184
|
seekLoadRetried.delete(el);
|
|
183
185
|
lastRuntimeAppliedVolume.delete(el);
|
|
184
|
-
|
|
186
|
+
forgetSteering(el);
|
|
187
|
+
heldAhead.delete(el);
|
|
185
188
|
}
|
|
186
189
|
/** Test-only seam: whether any per-source sync state is still tracked for `el`. */
|
|
187
190
|
export function hasMediaSyncStateForTest(el) {
|
|
@@ -190,10 +193,10 @@ export function hasMediaSyncStateForTest(el) {
|
|
|
190
193
|
strictDriftSamples.has(el) ||
|
|
191
194
|
seekLoadRetried.has(el) ||
|
|
192
195
|
lastRuntimeAppliedVolume.has(el) ||
|
|
193
|
-
|
|
196
|
+
isSteering(el));
|
|
194
197
|
}
|
|
195
198
|
export const MEDIA_HARD_SYNC_SECONDS = 0.5;
|
|
196
|
-
export const
|
|
199
|
+
export const MEDIA_PRESEEK_SECONDS = 1.5;
|
|
197
200
|
export function mediaSourceTimeAt(clip, timeSeconds) {
|
|
198
201
|
const elapsed = Math.max(0, Math.min(timeSeconds, clip.end) - (clip.origin ?? clip.start));
|
|
199
202
|
const target = clip.mediaStart + sourceTimeAt(clip.rate ?? clip.playbackRate, elapsed);
|
|
@@ -207,13 +210,12 @@ export function mediaSourceTimeAt(clip, timeSeconds) {
|
|
|
207
210
|
return Math.min(target, clip.sourceDuration);
|
|
208
211
|
return target;
|
|
209
212
|
}
|
|
210
|
-
function
|
|
213
|
+
function prepareUpcomingMedia(clip, time, playing, rate) {
|
|
211
214
|
const { el } = clip;
|
|
212
215
|
const dueIn = clip.start - time;
|
|
213
216
|
if (!playing ||
|
|
214
|
-
el.tagName !== "VIDEO" ||
|
|
215
217
|
dueIn <= 0 ||
|
|
216
|
-
dueIn >
|
|
218
|
+
dueIn > MEDIA_PRESEEK_SECONDS * rate ||
|
|
217
219
|
!el.paused ||
|
|
218
220
|
el.seeking ||
|
|
219
221
|
el.readyState < el.HAVE_METADATA ||
|
|
@@ -224,7 +226,7 @@ function prepareUpcomingVideo(clip, time, playing, rate) {
|
|
|
224
226
|
if (!Number.isFinite(target) || target < 0 || Math.abs(el.currentTime - target) <= 0.001)
|
|
225
227
|
return;
|
|
226
228
|
try {
|
|
227
|
-
// A trimmed
|
|
229
|
+
// A trimmed clip seeked as it opens shows no frame and plays late, then is seeked again while it sounds.
|
|
228
230
|
// Hidden preparation must not hold the visible-frame seek barrier.
|
|
229
231
|
el.currentTime = mediaElementTime(target);
|
|
230
232
|
}
|
|
@@ -232,35 +234,12 @@ function prepareUpcomingVideo(clip, time, playing, rate) {
|
|
|
232
234
|
swallow("runtime.media.preseek", err);
|
|
233
235
|
}
|
|
234
236
|
}
|
|
235
|
-
/** Drift a playing audio element may carry before sync pulls it back onto the playhead. */
|
|
236
|
-
export const MEDIA_SYNC_TOLERANCE_SECONDS = 0.04;
|
|
237
237
|
// Chromium refuses a slower playbackRate (NotSupportedError).
|
|
238
238
|
const MIN_NATIVE_PLAYBACK_RATE = 1 / 16;
|
|
239
|
-
// A
|
|
240
|
-
//
|
|
241
|
-
const
|
|
242
|
-
|
|
243
|
-
const VIDEO_STEER_STEPS = [
|
|
244
|
-
[0.3, 0.25],
|
|
245
|
-
[0.1, 0.1],
|
|
246
|
-
];
|
|
247
|
-
const VIDEO_STEER_RELEASE_SECONDS = 0.01;
|
|
248
|
-
/** Direction (+1 fast, -1 slow) a video is being steered in; absent when it plays at its authored rate. */
|
|
249
|
-
const videoSteering = new WeakMap();
|
|
250
|
-
/** Rate for a playing video `offset` seconds behind (+) or ahead (-) of the playhead. */
|
|
251
|
-
function steeredVideoRate(el, offset, baseRate) {
|
|
252
|
-
const direction = Math.sign(offset);
|
|
253
|
-
const steering = videoSteering.get(el);
|
|
254
|
-
if (Math.abs(offset) > MEDIA_SYNC_TOLERANCE_SECONDS)
|
|
255
|
-
videoSteering.set(el, direction);
|
|
256
|
-
else if (steering !== direction || Math.abs(offset) <= VIDEO_STEER_RELEASE_SECONDS) {
|
|
257
|
-
videoSteering.delete(el);
|
|
258
|
-
return baseRate;
|
|
259
|
-
}
|
|
260
|
-
const lag = Math.abs(offset);
|
|
261
|
-
const steer = VIDEO_STEER_STEPS.find(([from]) => lag > from)?.[1] ?? VIDEO_STEER;
|
|
262
|
-
return baseRate * (1 + direction * steer);
|
|
263
|
-
}
|
|
239
|
+
// A video this far ahead of the playhead, or at its out-point, is held until the playhead reaches it:
|
|
240
|
+
// steering a lead of a few hundred ms takes seconds and shows frames past the cut.
|
|
241
|
+
const VIDEO_LEAD_HOLD_SECONDS = 0.1;
|
|
242
|
+
const heldAhead = new WeakSet();
|
|
264
243
|
// fallow-ignore-next-line complexity
|
|
265
244
|
export function syncRuntimeMedia(params) {
|
|
266
245
|
const forceMuteAll = !!(params.outputMuted || params.userMuted);
|
|
@@ -451,12 +430,24 @@ export function syncRuntimeMedia(params) {
|
|
|
451
430
|
(el.ended && canSeekEndedMediaBackward && drift > 0.001) ||
|
|
452
431
|
staleAudioOnFirstTick ||
|
|
453
432
|
(drift > MEDIA_HARD_SYNC_SECONDS && (firstTickOfClip || offsetJumped || catastrophicDrift));
|
|
454
|
-
//
|
|
455
|
-
//
|
|
456
|
-
//
|
|
457
|
-
//
|
|
458
|
-
|
|
459
|
-
|
|
433
|
+
// Seeking playing media resets a video's decoder (a ~150ms freeze that loops into a
|
|
434
|
+
// seek→freeze→drift→seek stutter) and jumps sounding audio (a click). So playing media skips
|
|
435
|
+
// strict and force sync and is steered by rate; hard sync (>0.5s) and audio past its steer
|
|
436
|
+
// limit seek. A paused transport pauses it below, so a seek that pauses mid-playback lands it.
|
|
437
|
+
if (el.tagName === "VIDEO" && !isHeldVideoTail) {
|
|
438
|
+
// A loop's playhead wraps before its element, so its offset jumps by a lap: steering owns loops.
|
|
439
|
+
const ahead = !clip.loop &&
|
|
440
|
+
(offset < -VIDEO_LEAD_HOLD_SECONDS ||
|
|
441
|
+
currentElTime >= mediaSourceTimeAt(clip, clip.end) - MEDIA_SYNC_TOLERANCE_SECONDS);
|
|
442
|
+
if (!playing || (!ahead && offset >= -MEDIA_SYNC_TOLERANCE_SECONDS))
|
|
443
|
+
heldAhead.delete(el);
|
|
444
|
+
else if (ahead && !el.paused) {
|
|
445
|
+
heldAhead.add(el);
|
|
446
|
+
el.pause();
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
const holdingAhead = heldAhead.has(el);
|
|
450
|
+
const isPlayingMedia = !el.paused && playing;
|
|
460
451
|
// Only apply strict sync when offset has stabilized (not growing).
|
|
461
452
|
// During initial buffering, offset grows ~16ms/tick as the timeline
|
|
462
453
|
// advances while media stays at 0. Accumulated drift from pause/play
|
|
@@ -464,7 +455,8 @@ export function syncRuntimeMedia(params) {
|
|
|
464
455
|
const heldBelowFloor = params.playing && !playing;
|
|
465
456
|
const offsetStabilized = heldBelowFloor || (prevOffset !== undefined && Math.abs(offset - prevOffset) < 0.004);
|
|
466
457
|
let strictSync = false;
|
|
467
|
-
if (!
|
|
458
|
+
if (!isPlayingMedia &&
|
|
459
|
+
!holdingAhead &&
|
|
468
460
|
!hardSync &&
|
|
469
461
|
!firstTickOfClip &&
|
|
470
462
|
offsetStabilized &&
|
|
@@ -479,12 +471,15 @@ export function syncRuntimeMedia(params) {
|
|
|
479
471
|
else if (drift <= MEDIA_SYNC_TOLERANCE_SECONDS) {
|
|
480
472
|
strictDriftSamples.set(el, 0);
|
|
481
473
|
}
|
|
482
|
-
const forceSync = !
|
|
474
|
+
const forceSync = !isPlayingMedia && !holdingAhead && params.forceSync && drift > 0.02;
|
|
475
|
+
const steered = isPlayingMedia && !hardSync ? steerMedia(el, offset, baseRate, offsetStabilized) : null;
|
|
476
|
+
const steerSeeks = steered?.seek === true;
|
|
477
|
+
const dipped = steerSeeks && params.seekUnderDip?.(el, relTime) === true;
|
|
483
478
|
try {
|
|
484
|
-
// A hard sync lands the
|
|
485
|
-
if (!
|
|
486
|
-
|
|
487
|
-
const rate = Math.max(MIN_NATIVE_PLAYBACK_RATE,
|
|
479
|
+
// A hard sync lands the element on the playhead, so its pre-seek offset says nothing.
|
|
480
|
+
if (!steered)
|
|
481
|
+
forgetSteering(el);
|
|
482
|
+
const rate = Math.max(MIN_NATIVE_PLAYBACK_RATE, steered?.rate ?? baseRate);
|
|
488
483
|
// Some engines read a rate back at lower precision; an equal-enough rate is not rewritten.
|
|
489
484
|
if (Math.abs(el.playbackRate - rate) > 1e-6)
|
|
490
485
|
el.playbackRate = rate;
|
|
@@ -493,7 +488,7 @@ export function syncRuntimeMedia(params) {
|
|
|
493
488
|
// ignore unsupported playbackRate
|
|
494
489
|
swallow("runtime.media.site1", err);
|
|
495
490
|
}
|
|
496
|
-
if (hardSync || strictSync || forceSync) {
|
|
491
|
+
if (hardSync || strictSync || forceSync || (steerSeeks && !dipped)) {
|
|
497
492
|
// Skip the per-tick seek (and the `el.load()` drift-recovery retry
|
|
498
493
|
// below) for `<video>` elements that have a sibling
|
|
499
494
|
// `<img id="__render_frame_<id>__">`. The sibling is created only
|
|
@@ -526,6 +521,7 @@ export function syncRuntimeMedia(params) {
|
|
|
526
521
|
holdSeekBarrierUntilVideoLands(el);
|
|
527
522
|
}
|
|
528
523
|
playRequested.delete(el);
|
|
524
|
+
heldAhead.delete(el);
|
|
529
525
|
}
|
|
530
526
|
else if (!playing) {
|
|
531
527
|
holdSeekBarrierUntilVideoLands(el);
|
|
@@ -534,7 +530,12 @@ export function syncRuntimeMedia(params) {
|
|
|
534
530
|
if (!el.paused)
|
|
535
531
|
el.pause();
|
|
536
532
|
}
|
|
537
|
-
else if (playing &&
|
|
533
|
+
else if (playing &&
|
|
534
|
+
!heldAhead.has(el) &&
|
|
535
|
+
params.clockAdvancing !== false &&
|
|
536
|
+
el.paused &&
|
|
537
|
+
!playRequested.has(el) &&
|
|
538
|
+
!isUnplayable(el)) {
|
|
538
539
|
// `HTMLMediaElement.play()` is spec'd to queue playback and resolve
|
|
539
540
|
// once enough data is buffered, so we can unconditionally call it —
|
|
540
541
|
// no need to gate on `readyState` or defer to a `canplay` listener.
|
|
@@ -580,6 +581,6 @@ export function syncRuntimeMedia(params) {
|
|
|
580
581
|
lastRelativeTime.set(el, relTime);
|
|
581
582
|
if (!el.paused)
|
|
582
583
|
el.pause();
|
|
583
|
-
|
|
584
|
+
prepareUpcomingMedia(clip, params.timeSeconds, params.playing, params.playbackRate);
|
|
584
585
|
}
|
|
585
586
|
}
|
|
@@ -0,0 +1,10 @@
|
|
|
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
|
+
/** Rate or seek for media `offset` s behind (+) or ahead (-); a lag still growing (`settled` false) never seeks. */
|
|
4
|
+
export declare function steerMedia(el: HTMLMediaElement, offset: number, baseRate: number, settled: boolean, audioOnly?: boolean): {
|
|
5
|
+
rate: number;
|
|
6
|
+
seek: boolean;
|
|
7
|
+
};
|
|
8
|
+
/** Drop `el`'s steering: it was seeked, paused or replaced, so its old offset says nothing. */
|
|
9
|
+
export declare function forgetSteering(el: HTMLMediaElement): void;
|
|
10
|
+
export declare function isSteering(el: HTMLMediaElement): boolean;
|
|
@@ -0,0 +1,40 @@
|
|
|
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
|
+
// Steered by rate, not seeked (a seek resets a video's decoder and clicks in audio); steps keep rate writes rare.
|
|
4
|
+
const MEDIA_STEER = 0.03;
|
|
5
|
+
const MEDIA_STEER_STEPS = [
|
|
6
|
+
[0.3, 0.25],
|
|
7
|
+
[0.1, 0.1],
|
|
8
|
+
];
|
|
9
|
+
const MEDIA_STEER_RELEASE_SECONDS = 0.01;
|
|
10
|
+
// Audio never steers past 10%, where speech changes tempo; a lag held past 100 ms is seeked instead.
|
|
11
|
+
const AUDIO_MAX_STEER = 0.1;
|
|
12
|
+
const AUDIO_STEER_LIMIT_SECONDS = 0.1;
|
|
13
|
+
/** Direction (+1 fast, -1 slow) an element is being steered in; absent when it plays at its authored rate. */
|
|
14
|
+
const steering = new WeakMap();
|
|
15
|
+
/** Rate or seek for media `offset` s behind (+) or ahead (-); a lag still growing (`settled` false) never seeks. */
|
|
16
|
+
export function steerMedia(el, offset, baseRate, settled, audioOnly = el.tagName === "AUDIO") {
|
|
17
|
+
const lag = Math.abs(offset);
|
|
18
|
+
if (audioOnly && settled && lag > AUDIO_STEER_LIMIT_SECONDS) {
|
|
19
|
+
steering.delete(el);
|
|
20
|
+
return { rate: baseRate, seek: true };
|
|
21
|
+
}
|
|
22
|
+
const direction = Math.sign(offset);
|
|
23
|
+
const current = steering.get(el);
|
|
24
|
+
if (lag > MEDIA_SYNC_TOLERANCE_SECONDS)
|
|
25
|
+
steering.set(el, direction);
|
|
26
|
+
else if (current !== direction || lag <= MEDIA_STEER_RELEASE_SECONDS) {
|
|
27
|
+
steering.delete(el);
|
|
28
|
+
return { rate: baseRate, seek: false };
|
|
29
|
+
}
|
|
30
|
+
const step = MEDIA_STEER_STEPS.find(([from]) => lag > from)?.[1] ?? MEDIA_STEER;
|
|
31
|
+
const steer = audioOnly ? Math.min(step, AUDIO_MAX_STEER) : step;
|
|
32
|
+
return { rate: baseRate * (1 + direction * steer), seek: false };
|
|
33
|
+
}
|
|
34
|
+
/** Drop `el`'s steering: it was seeked, paused or replaced, so its old offset says nothing. */
|
|
35
|
+
export function forgetSteering(el) {
|
|
36
|
+
steering.delete(el);
|
|
37
|
+
}
|
|
38
|
+
export function isSteering(el) {
|
|
39
|
+
return steering.has(el);
|
|
40
|
+
}
|
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 };
|
|
@@ -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.207",
|
|
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.207",
|
|
421
|
+
"@hyperframes/studio-server": "0.8.207",
|
|
422
|
+
"@hyperframes/parsers": "0.8.207"
|
|
415
423
|
},
|
|
416
424
|
"devDependencies": {
|
|
417
425
|
"@fontsource/archivo-black": "^5.2.8",
|