@hyperframes/core 0.9.20 → 0.9.21
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/generated/runtime-inline.js +1 -1
- package/dist/hyperframe.runtime.iife.js +73 -73
- package/dist/runtime/bridge.js +2 -1
- package/dist/runtime/clock.d.ts +5 -0
- package/dist/runtime/clock.js +49 -3
- package/dist/runtime/mediaSteer.d.ts +1 -1
- package/dist/runtime/mediaSteer.js +1 -10
- package/dist/runtime/wallClock.d.ts +8 -0
- package/dist/runtime/wallClock.js +13 -0
- package/package.json +4 -4
package/dist/runtime/bridge.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { wallSetTimeout } from "./wallClock.js";
|
|
1
2
|
import { swallow } from "./diagnostics.js";
|
|
2
3
|
import { inspectRuntimeProtocol, runtimeProtocolFpsToNumber, runtimeProtocolMetadata, } from "./protocol.js";
|
|
3
4
|
let runtimeProtocolFps = 30;
|
|
@@ -134,7 +135,7 @@ function flashElements(selectors, duration) {
|
|
|
134
135
|
const els = document.querySelectorAll(selector);
|
|
135
136
|
els.forEach((el) => {
|
|
136
137
|
el.classList.add("__hf-flash");
|
|
137
|
-
|
|
138
|
+
wallSetTimeout(() => el.classList.remove("__hf-flash"), duration);
|
|
138
139
|
});
|
|
139
140
|
}
|
|
140
141
|
catch (err) {
|
package/dist/runtime/clock.d.ts
CHANGED
|
@@ -30,6 +30,8 @@ export declare class TransportClock {
|
|
|
30
30
|
private _lastReadMs;
|
|
31
31
|
private _lastNow;
|
|
32
32
|
private _audioStart;
|
|
33
|
+
private _reading;
|
|
34
|
+
private _ahead;
|
|
33
35
|
constructor(opts?: {
|
|
34
36
|
initialTime?: number;
|
|
35
37
|
rate?: number;
|
|
@@ -38,6 +40,9 @@ export declare class TransportClock {
|
|
|
38
40
|
});
|
|
39
41
|
now(): number;
|
|
40
42
|
private _elementTimeNeverBehind;
|
|
43
|
+
private _betweenUpdates;
|
|
44
|
+
private _runAhead;
|
|
45
|
+
aheadOf(el: HTMLMediaElement): number;
|
|
41
46
|
private _heldUntilAudioOutputStarts;
|
|
42
47
|
private _pendingAudioStart;
|
|
43
48
|
/** Folds a >500ms gap since the last read into `_playStartMs` so it's never reported as played time. */
|
package/dist/runtime/clock.js
CHANGED
|
@@ -6,6 +6,15 @@ const STALL_ADJUSTED_LAG_MS = 33;
|
|
|
6
6
|
const HAVE_FUTURE_DATA = 3;
|
|
7
7
|
const AUDIO_START_WAIT_MS = 1500;
|
|
8
8
|
const MEDIA_SYNC_STEER_TOLERANCE_SECONDS = 0.04;
|
|
9
|
+
const MAX_MEDIA_STEP_SECONDS = 0.25;
|
|
10
|
+
const MEDIA_STEPS_KEPT = 8;
|
|
11
|
+
const furthestAhead = (reading) => reading ? Math.max(0, ...reading.steps) : 0;
|
|
12
|
+
function noteStep(reading, time) {
|
|
13
|
+
const step = time - reading.time;
|
|
14
|
+
if (step > 0 && step <= MAX_MEDIA_STEP_SECONDS)
|
|
15
|
+
reading.steps = [...reading.steps.slice(1 - MEDIA_STEPS_KEPT), step];
|
|
16
|
+
reading.time = time;
|
|
17
|
+
}
|
|
9
18
|
export class TransportClock {
|
|
10
19
|
_baseTime = 0;
|
|
11
20
|
_playStartMs = null;
|
|
@@ -20,6 +29,8 @@ export class TransportClock {
|
|
|
20
29
|
_lastReadMs = null;
|
|
21
30
|
_lastNow = 0;
|
|
22
31
|
_audioStart = null;
|
|
32
|
+
_reading = null;
|
|
33
|
+
_ahead = null;
|
|
23
34
|
constructor(opts) {
|
|
24
35
|
this._baseTime = opts?.initialTime ?? 0;
|
|
25
36
|
this._rate = opts?.rate ?? 1;
|
|
@@ -27,6 +38,7 @@ export class TransportClock {
|
|
|
27
38
|
this._nowMs = opts?.nowMs ?? (() => performance.now());
|
|
28
39
|
}
|
|
29
40
|
now() {
|
|
41
|
+
this._ahead = null;
|
|
30
42
|
if (this._playStartMs === null)
|
|
31
43
|
return this._baseTime;
|
|
32
44
|
// Audio-master: drift is impossible because audio IS the clock. Clearing
|
|
@@ -63,13 +75,42 @@ export class TransportClock {
|
|
|
63
75
|
const spec = rate ?? 1;
|
|
64
76
|
const time = timeAtSourceTime(spec, el.currentTime - mediaStart) + compositionStart;
|
|
65
77
|
const held = this._heldUntilAudioOutputStarts(el.seeking, time);
|
|
66
|
-
if (held !== undefined)
|
|
78
|
+
if (held !== undefined) {
|
|
79
|
+
this._reading = null;
|
|
67
80
|
return held;
|
|
68
|
-
|
|
69
|
-
|
|
81
|
+
}
|
|
82
|
+
const between = this._betweenUpdates(el, time);
|
|
83
|
+
if (between !== null)
|
|
84
|
+
return between;
|
|
70
85
|
const wrapped = el.loop && this._lastNow - time > timeAtSourceTime(spec, el.duration) / 2;
|
|
71
86
|
return !wrapped && el.readyState < HAVE_FUTURE_DATA ? this._lastNow : null;
|
|
72
87
|
}
|
|
88
|
+
// Chromium moves currentTime in 10-50 ms steps, so reading it as is holds a frame, then jumps. Run on at the play
|
|
89
|
+
// rate, kept within [element time, element time + its largest recent step], never back; null when time is behind
|
|
90
|
+
// the playhead by more than that step (a late clip, not a step).
|
|
91
|
+
_betweenUpdates(el, time) {
|
|
92
|
+
const nowMs = this._nowMs();
|
|
93
|
+
const last = this._reading?.el === el ? this._reading : null;
|
|
94
|
+
if (time < this._lastNow - furthestAhead(last))
|
|
95
|
+
return null;
|
|
96
|
+
if (!last) {
|
|
97
|
+
this._reading = { el, time, steps: [], from: time, fromMs: nowMs };
|
|
98
|
+
return this._runAhead(el, time, Math.max(time, this._lastNow));
|
|
99
|
+
}
|
|
100
|
+
noteStep(last, time);
|
|
101
|
+
const run = last.from + ((nowMs - last.fromMs) / 1000) * this._rate;
|
|
102
|
+
const held = Math.min(Math.max(run, time), time + furthestAhead(last));
|
|
103
|
+
if (held !== run)
|
|
104
|
+
Object.assign(last, { from: held, fromMs: nowMs });
|
|
105
|
+
return this._runAhead(el, time, Math.max(held, this._lastNow));
|
|
106
|
+
}
|
|
107
|
+
_runAhead(el, time, now) {
|
|
108
|
+
this._ahead = { el, seconds: now - time };
|
|
109
|
+
return now;
|
|
110
|
+
}
|
|
111
|
+
aheadOf(el) {
|
|
112
|
+
return this._ahead?.el === el ? this._ahead.seconds : 0;
|
|
113
|
+
}
|
|
73
114
|
_heldUntilAudioOutputStarts(seeking, time) {
|
|
74
115
|
const pending = this._pendingAudioStart();
|
|
75
116
|
if (seeking)
|
|
@@ -110,6 +151,7 @@ export class TransportClock {
|
|
|
110
151
|
this._playStartMs = this._nowMs();
|
|
111
152
|
this._audioStart = { sinceMs: this._playStartMs, at: this._baseTime, from: null };
|
|
112
153
|
this._lastNow = this._baseTime;
|
|
154
|
+
this._reading = null;
|
|
113
155
|
// Not a stall: the gap since the clock was last read (possibly a long
|
|
114
156
|
// paused idle) says nothing about lost playback time, since none was
|
|
115
157
|
// playing. `_applyStallCorrection` treats null as "nothing to compare
|
|
@@ -122,6 +164,7 @@ export class TransportClock {
|
|
|
122
164
|
return false;
|
|
123
165
|
this._baseTime = this.now();
|
|
124
166
|
this._playStartMs = null;
|
|
167
|
+
this._reading = null;
|
|
125
168
|
return true;
|
|
126
169
|
}
|
|
127
170
|
seek(timeSeconds) {
|
|
@@ -130,6 +173,7 @@ export class TransportClock {
|
|
|
130
173
|
: Math.max(0, timeSeconds);
|
|
131
174
|
this._baseTime = clamped;
|
|
132
175
|
this._lastNow = clamped;
|
|
176
|
+
this._reading = null;
|
|
133
177
|
if (this._playStartMs !== null) {
|
|
134
178
|
this._playStartMs = this._nowMs();
|
|
135
179
|
this._audioStart = { sinceMs: this._playStartMs, at: clamped, from: null };
|
|
@@ -143,6 +187,7 @@ export class TransportClock {
|
|
|
143
187
|
}
|
|
144
188
|
setRate(rate) {
|
|
145
189
|
const safe = Number.isFinite(rate) && rate > 0 ? Math.max(0.1, Math.min(5, rate)) : 1;
|
|
190
|
+
this._reading = null;
|
|
146
191
|
if (this._playStartMs !== null) {
|
|
147
192
|
this._baseTime = this.now();
|
|
148
193
|
this._playStartMs = this._nowMs();
|
|
@@ -189,6 +234,7 @@ export class TransportClock {
|
|
|
189
234
|
this._lastReadMs = null;
|
|
190
235
|
}
|
|
191
236
|
this._audioSource = null;
|
|
237
|
+
this._reading = null;
|
|
192
238
|
}
|
|
193
239
|
/** The element the playhead currently follows, if the source is one. */
|
|
194
240
|
// fallow-ignore-next-line unused-class-member
|
|
@@ -3,7 +3,7 @@ export declare const MEDIA_SYNC_TOLERANCE_SECONDS = 0.04;
|
|
|
3
3
|
export declare const MEDIA_HARD_SYNC_SECONDS = 0.5;
|
|
4
4
|
export declare function markMediaRestarted(el: HTMLMediaElement): void;
|
|
5
5
|
/** Rate or seek for media `offset` s behind (+) or ahead (-); a lag still growing (`settled` false) never seeks. */
|
|
6
|
-
export declare function steerMedia(el: HTMLMediaElement, offset: number, baseRate: number, settled: boolean, audioOnly?: boolean
|
|
6
|
+
export declare function steerMedia(el: HTMLMediaElement, offset: number, baseRate: number, settled: boolean, audioOnly?: boolean): {
|
|
7
7
|
rate: number;
|
|
8
8
|
seek: boolean;
|
|
9
9
|
};
|
|
@@ -13,8 +13,6 @@ const MEDIA_STEER_RELEASE_SECONDS = 0.01;
|
|
|
13
13
|
const AUDIO_MAX_STEER = 0.1;
|
|
14
14
|
const AUDIO_STEER_LIMIT_SECONDS = 0.1;
|
|
15
15
|
const WEBKIT_AUDIO_START_LAG_SECONDS = 0.65;
|
|
16
|
-
// A video nobody sees runs up to twice as fast (half as slow) to reach the playhead: the lag in seconds times this.
|
|
17
|
-
const HIDDEN_GAIN = 5;
|
|
18
16
|
/** Direction (+1 fast, -1 slow) an element is being steered in; absent when it plays at its authored rate. */
|
|
19
17
|
const steering = new WeakMap();
|
|
20
18
|
/** Started or seeked, not yet moving in tolerance: its lag is its own output latency, which a seek only renews. */
|
|
@@ -23,7 +21,7 @@ export function markMediaRestarted(el) {
|
|
|
23
21
|
catchingUp.add(el);
|
|
24
22
|
}
|
|
25
23
|
/** Rate or seek for media `offset` s behind (+) or ahead (-); a lag still growing (`settled` false) never seeks. */
|
|
26
|
-
export function steerMedia(el, offset, baseRate, settled, audioOnly = el.tagName === "AUDIO"
|
|
24
|
+
export function steerMedia(el, offset, baseRate, settled, audioOnly = el.tagName === "AUDIO") {
|
|
27
25
|
const lag = Math.abs(offset);
|
|
28
26
|
if (audioOnly && isWebKit()) {
|
|
29
27
|
steering.delete(el);
|
|
@@ -38,8 +36,6 @@ export function steerMedia(el, offset, baseRate, settled, audioOnly = el.tagName
|
|
|
38
36
|
return { rate: baseRate, seek: true };
|
|
39
37
|
}
|
|
40
38
|
const direction = Math.sign(offset);
|
|
41
|
-
if (hidden && lag > MEDIA_SYNC_TOLERANCE_SECONDS)
|
|
42
|
-
return steerHidden(el, direction, lag, baseRate);
|
|
43
39
|
const current = steering.get(el);
|
|
44
40
|
if (lag > MEDIA_SYNC_TOLERANCE_SECONDS)
|
|
45
41
|
steering.set(el, direction);
|
|
@@ -51,11 +47,6 @@ export function steerMedia(el, offset, baseRate, settled, audioOnly = el.tagName
|
|
|
51
47
|
const steer = audioOnly ? Math.min(step, AUDIO_MAX_STEER) : step;
|
|
52
48
|
return { rate: baseRate * (1 + direction * steer), seek: false };
|
|
53
49
|
}
|
|
54
|
-
function steerHidden(el, direction, lag, baseRate) {
|
|
55
|
-
steering.set(el, direction);
|
|
56
|
-
const steer = Math.min(direction > 0 ? 1 : 0.5, lag * HIDDEN_GAIN);
|
|
57
|
-
return { rate: baseRate * (1 + direction * steer), seek: false };
|
|
58
|
-
}
|
|
59
50
|
/** Drop `el`'s steering: it was seeked, paused or replaced, so its old offset says nothing. */
|
|
60
51
|
export function forgetSteering(el) {
|
|
61
52
|
steering.delete(el);
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
type TimeoutId = ReturnType<typeof setTimeout>;
|
|
2
|
+
type IntervalId = ReturnType<typeof setInterval>;
|
|
3
|
+
type Handler = (...args: never[]) => void;
|
|
4
|
+
export declare function wallSetTimeout(handler: Handler, timeout?: number, ...args: unknown[]): TimeoutId;
|
|
5
|
+
export declare function wallClearTimeout(id: TimeoutId | null | undefined): void;
|
|
6
|
+
export declare function wallSetInterval(handler: Handler, timeout?: number, ...args: unknown[]): IntervalId;
|
|
7
|
+
export declare function wallClearInterval(id: IntervalId | null | undefined): void;
|
|
8
|
+
export {};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
const shim = () => globalThis.__HF_VIRTUAL_TIME__;
|
|
2
|
+
export function wallSetTimeout(handler, timeout, ...args) {
|
|
3
|
+
return (shim()?.originalSetTimeout ?? setTimeout)(handler, timeout, ...args);
|
|
4
|
+
}
|
|
5
|
+
export function wallClearTimeout(id) {
|
|
6
|
+
(shim()?.originalClearTimeout ?? clearTimeout)(id ?? undefined);
|
|
7
|
+
}
|
|
8
|
+
export function wallSetInterval(handler, timeout, ...args) {
|
|
9
|
+
return (shim()?.originalSetInterval ?? setInterval)(handler, timeout, ...args);
|
|
10
|
+
}
|
|
11
|
+
export function wallClearInterval(id) {
|
|
12
|
+
(shim()?.originalClearInterval ?? clearInterval)(id ?? undefined);
|
|
13
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hyperframes/core",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.21",
|
|
4
4
|
"description": "",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -437,9 +437,9 @@
|
|
|
437
437
|
"linkedom": "^0.18.12",
|
|
438
438
|
"postcss": "^8.5.8",
|
|
439
439
|
"wawoff2": "^2.0.1",
|
|
440
|
-
"@hyperframes/lint": "0.9.
|
|
441
|
-
"@hyperframes/
|
|
442
|
-
"@hyperframes/
|
|
440
|
+
"@hyperframes/lint": "0.9.21",
|
|
441
|
+
"@hyperframes/parsers": "0.9.21",
|
|
442
|
+
"@hyperframes/studio-server": "0.9.21"
|
|
443
443
|
},
|
|
444
444
|
"devDependencies": {
|
|
445
445
|
"@fontsource/archivo-black": "^5.2.8",
|