@hyperframes/core 0.9.20 → 0.9.22
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/htmlDocument.d.ts +1 -1
- package/dist/compiler/htmlDocument.js +2 -2
- package/dist/generated/runtime-inline.js +1 -1
- package/dist/hyperframe.runtime.iife.js +74 -74
- package/dist/index.d.ts +1 -0
- package/dist/previewProxyBox.d.ts +2 -0
- 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/pausedVideoCallbacks.d.ts +10 -0
- package/dist/runtime/pausedVideoCallbacks.js +157 -0
- package/dist/runtime/pausedVideoFrames.d.ts +52 -0
- package/dist/runtime/pausedVideoFrames.js +366 -0
- package/dist/runtime/types.d.ts +1 -0
- package/dist/runtime/wallClock.d.ts +8 -0
- package/dist/runtime/wallClock.js +13 -0
- package/dist/studioPreviewMark.d.ts +5 -1
- package/dist/studioPreviewMark.js +7 -0
- package/package.json +4 -4
package/dist/index.d.ts
CHANGED
|
@@ -37,6 +37,7 @@ export { parseCubeLut, CubeLutParseError, type CubeLut3D, type ParseCubeLutOptio
|
|
|
37
37
|
export { bakeHfColorGradingCube, type BakeHfColorGradingCubeOptions } from "./colorGradingBake.js";
|
|
38
38
|
export { firstFrameColourArgs, hdrToSdrToneMapFilter, keyframeTransferArgs, parseKeyframeTransfers, readsKeyframeTransfers, toneMapReferenceNits, parseFirstFrameColour, type ToneMapFrameColour, type ToneMapSourceColour, } from "./hdrToneMap.js";
|
|
39
39
|
export { HEAVY_BITRATE, HEAVY_PIXEL_SHARE, PREVIEW_PROXY_BOX_PARAM, PREVIEW_PROXY_LIGHT_PARAM, PREVIEW_PROXY_PRIORITY_HEADER, PREVIEW_PROXY_PROGRESS_HEADER, PREVIEW_PROXY_RETRY_PARAM, PREVIEW_PROXY_STILL_PARAM, PREVIEW_QUALITY_LEVELS, formatPreviewProxyBox, isHeavyForPreview, isPreviewQuality, parsePreviewProxyBox, quantizePreviewProxyBox, scalePreviewProxyBox, type PreviewProxyBox, type PreviewQuality, type PreviewSourceFacts, } from "./previewProxyBox.js";
|
|
40
|
+
export type { PausedVideoFrameProvider, PausedVideoFrameRequest, PausedVideoFrame, } from "./runtime/pausedVideoFrames.js";
|
|
40
41
|
export { HYPERFRAME_RUNTIME_ARTIFACTS, HYPERFRAME_RUNTIME_CONTRACT, loadHyperframeRuntimeSource, type HyperframeRuntimeContract, } from "./inline-scripts/hyperframe.js";
|
|
41
42
|
export { HYPERFRAME_RUNTIME_GLOBALS, HYPERFRAME_BRIDGE_SOURCES, HYPERFRAME_CONTROL_ACTIONS, type HyperframeControlAction, } from "./inline-scripts/runtimeContract.js";
|
|
42
43
|
export { getHyperframeRuntimeScript } from "./generated/runtime-inline.js";
|
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,10 @@
|
|
|
1
|
+
type Frame = Pick<VideoFrameCallbackMetadata, "mediaTime" | "width" | "height">;
|
|
2
|
+
/** Install before authored scripts: Three's VideoTexture registers its first callback in its constructor. */
|
|
3
|
+
export declare function installPausedVideoCallbacks(): void;
|
|
4
|
+
export declare function stagePausedVideoCallbacks(video: HTMLVideoElement, frame: Frame): void;
|
|
5
|
+
/** Notify once before composition redraw; callbacks registered from a callback wait for the next frame. */
|
|
6
|
+
export declare function flushPausedVideoCallbacks(video: HTMLVideoElement): void;
|
|
7
|
+
export declare function releasePausedVideoCallbacks(video: HTMLVideoElement): void;
|
|
8
|
+
/** Recovery must observe native frames even while authored callbacks still see staged pixels. */
|
|
9
|
+
export declare function observeNativeVideoFrames(video: HTMLVideoElement, onFrame: () => boolean): () => void;
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
const registry = () => HTMLVideoElement.prototype.requestVideoFrameCallback?.__hfPausedCallbacks;
|
|
2
|
+
function stateFor(owner, video) {
|
|
3
|
+
let state = owner.states.get(video);
|
|
4
|
+
if (!state) {
|
|
5
|
+
state = {
|
|
6
|
+
pending: new Map(),
|
|
7
|
+
frame: null,
|
|
8
|
+
count: 0,
|
|
9
|
+
nativeCount: 0,
|
|
10
|
+
synthetic: false,
|
|
11
|
+
dirty: false,
|
|
12
|
+
};
|
|
13
|
+
owner.states.set(video, state);
|
|
14
|
+
}
|
|
15
|
+
return state;
|
|
16
|
+
}
|
|
17
|
+
function requestNative(owner, video, state, pending, id) {
|
|
18
|
+
return owner.request.call(video, function (now, metadata) {
|
|
19
|
+
pending.native = null;
|
|
20
|
+
const advance = Math.max(0, metadata.presentedFrames - state.nativeCount);
|
|
21
|
+
state.nativeCount = metadata.presentedFrames;
|
|
22
|
+
if (state.frame)
|
|
23
|
+
return;
|
|
24
|
+
state.pending.delete(id());
|
|
25
|
+
if (!state.synthetic) {
|
|
26
|
+
state.count = metadata.presentedFrames;
|
|
27
|
+
pending.callback.call(this, now, metadata);
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
state.count += advance;
|
|
31
|
+
pending.callback.call(this, now, { ...metadata, presentedFrames: state.count });
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
/** Install before authored scripts: Three's VideoTexture registers its first callback in its constructor. */
|
|
35
|
+
export function installPausedVideoCallbacks() {
|
|
36
|
+
const proto = HTMLVideoElement.prototype;
|
|
37
|
+
const request = proto.requestVideoFrameCallback;
|
|
38
|
+
if (typeof request !== "function" || request.__hfPausedCallbacks)
|
|
39
|
+
return;
|
|
40
|
+
const owner = {
|
|
41
|
+
states: new WeakMap(),
|
|
42
|
+
request,
|
|
43
|
+
cancel: proto.cancelVideoFrameCallback,
|
|
44
|
+
};
|
|
45
|
+
const patched = function (callback) {
|
|
46
|
+
const state = stateFor(owner, this);
|
|
47
|
+
const pending = { callback, native: null };
|
|
48
|
+
const id = requestNative(owner, this, state, pending, () => id);
|
|
49
|
+
pending.native = id;
|
|
50
|
+
state.pending.set(id, pending);
|
|
51
|
+
return id;
|
|
52
|
+
};
|
|
53
|
+
patched.__hfPausedCallbacks = owner;
|
|
54
|
+
proto.requestVideoFrameCallback = patched;
|
|
55
|
+
proto.cancelVideoFrameCallback = function (id) {
|
|
56
|
+
const pending = owner.states.get(this)?.pending.get(id);
|
|
57
|
+
if (!pending)
|
|
58
|
+
return owner.cancel.call(this, id);
|
|
59
|
+
owner.states.get(this)?.pending.delete(id);
|
|
60
|
+
if (pending.native !== null)
|
|
61
|
+
owner.cancel.call(this, pending.native);
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
export function stagePausedVideoCallbacks(video, frame) {
|
|
65
|
+
const owner = registry();
|
|
66
|
+
if (!owner)
|
|
67
|
+
return;
|
|
68
|
+
const state = stateFor(owner, video);
|
|
69
|
+
state.frame = frame;
|
|
70
|
+
state.dirty = true;
|
|
71
|
+
}
|
|
72
|
+
/** Notify once before composition redraw; callbacks registered from a callback wait for the next frame. */
|
|
73
|
+
export function flushPausedVideoCallbacks(video) {
|
|
74
|
+
const owner = registry();
|
|
75
|
+
const state = owner?.states.get(video);
|
|
76
|
+
if (!owner || !state?.frame || !state.dirty)
|
|
77
|
+
return;
|
|
78
|
+
state.dirty = false;
|
|
79
|
+
state.synthetic = true;
|
|
80
|
+
state.count++;
|
|
81
|
+
const now = performance.now();
|
|
82
|
+
const metadata = {
|
|
83
|
+
...state.frame,
|
|
84
|
+
presentedFrames: state.count,
|
|
85
|
+
presentationTime: now,
|
|
86
|
+
expectedDisplayTime: now,
|
|
87
|
+
processingDuration: 0,
|
|
88
|
+
};
|
|
89
|
+
for (const [id, pending] of [...state.pending]) {
|
|
90
|
+
if (!state.pending.delete(id))
|
|
91
|
+
continue;
|
|
92
|
+
if (pending.native !== null)
|
|
93
|
+
owner.cancel.call(video, pending.native);
|
|
94
|
+
try {
|
|
95
|
+
pending.callback.call(undefined, now, metadata);
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
reportError(error);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
export function releasePausedVideoCallbacks(video) {
|
|
103
|
+
const owner = registry();
|
|
104
|
+
const state = owner?.states.get(video);
|
|
105
|
+
if (!owner || !state)
|
|
106
|
+
return;
|
|
107
|
+
state.frame = null;
|
|
108
|
+
state.dirty = false;
|
|
109
|
+
for (const [id, pending] of state.pending) {
|
|
110
|
+
if (pending.native === null)
|
|
111
|
+
pending.native = requestNative(owner, video, state, pending, () => id);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
/** Recovery must observe native frames even while authored callbacks still see staged pixels. */
|
|
115
|
+
export function observeNativeVideoFrames(video, onFrame) {
|
|
116
|
+
const owner = registry();
|
|
117
|
+
const request = owner?.request ?? video.requestVideoFrameCallback;
|
|
118
|
+
const cancel = owner?.cancel ?? video.cancelVideoFrameCallback;
|
|
119
|
+
let handle = null;
|
|
120
|
+
let stopped = false;
|
|
121
|
+
let playingAt = video.paused ? null : video.currentTime;
|
|
122
|
+
const stop = () => {
|
|
123
|
+
if (stopped)
|
|
124
|
+
return;
|
|
125
|
+
stopped = true;
|
|
126
|
+
if (handle !== null && typeof cancel === "function")
|
|
127
|
+
cancel.call(video, handle);
|
|
128
|
+
video.removeEventListener("playing", playing);
|
|
129
|
+
video.removeEventListener("timeupdate", progress);
|
|
130
|
+
};
|
|
131
|
+
const arm = () => {
|
|
132
|
+
if (!stopped && typeof request === "function")
|
|
133
|
+
handle = request.call(video, presented);
|
|
134
|
+
};
|
|
135
|
+
const presented = () => {
|
|
136
|
+
handle = null;
|
|
137
|
+
if (stopped)
|
|
138
|
+
return;
|
|
139
|
+
if (onFrame())
|
|
140
|
+
stop();
|
|
141
|
+
else
|
|
142
|
+
arm();
|
|
143
|
+
};
|
|
144
|
+
const playing = () => {
|
|
145
|
+
playingAt = video.currentTime;
|
|
146
|
+
};
|
|
147
|
+
const progress = () => {
|
|
148
|
+
if (stopped || playingAt === null || video.paused || video.currentTime === playingAt)
|
|
149
|
+
return;
|
|
150
|
+
if (onFrame())
|
|
151
|
+
stop();
|
|
152
|
+
};
|
|
153
|
+
video.addEventListener("playing", playing);
|
|
154
|
+
video.addEventListener("timeupdate", progress);
|
|
155
|
+
arm();
|
|
156
|
+
return stop;
|
|
157
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
export interface PausedVideoFrameRequest {
|
|
2
|
+
video: HTMLVideoElement;
|
|
3
|
+
source: string;
|
|
4
|
+
time: number;
|
|
5
|
+
settled?: boolean;
|
|
6
|
+
}
|
|
7
|
+
export interface PausedVideoFrame {
|
|
8
|
+
source: CanvasImageSource;
|
|
9
|
+
timestamp: number;
|
|
10
|
+
duration: number;
|
|
11
|
+
width: number;
|
|
12
|
+
height: number;
|
|
13
|
+
release(): void;
|
|
14
|
+
}
|
|
15
|
+
/** Decoding lives in the host; core owns composition timing and presentation. */
|
|
16
|
+
export interface PausedVideoFrameProvider {
|
|
17
|
+
decode(requests: PausedVideoFrameRequest[], signal: AbortSignal): Promise<PausedVideoFrame[]>;
|
|
18
|
+
suspend?(): Promise<void>;
|
|
19
|
+
dispose(): void;
|
|
20
|
+
}
|
|
21
|
+
export interface PreparedPausedVideoFrames {
|
|
22
|
+
valid(): boolean;
|
|
23
|
+
present(): boolean;
|
|
24
|
+
refresh(): void;
|
|
25
|
+
release(): void;
|
|
26
|
+
}
|
|
27
|
+
export declare function getPausedVideoFrame(video: HTMLVideoElement): HTMLCanvasElement | null;
|
|
28
|
+
export declare function ownsPausedVideoTime(video: HTMLMediaElement): boolean;
|
|
29
|
+
export declare function getPausedExternalVideoFrame(video: HTMLVideoElement): VideoFrame | null;
|
|
30
|
+
export declare function captureNativeVideoFrame(video: HTMLVideoElement): HTMLCanvasElement | null;
|
|
31
|
+
export declare function nativeSnapshotStyle(video: HTMLVideoElement): CSSStyleDeclaration | null;
|
|
32
|
+
export declare function releasePausedVideoFrame(video: HTMLVideoElement): void;
|
|
33
|
+
/** Keeps original nodes, URLs, intrinsic sizes and the displayed source clock intact. */
|
|
34
|
+
export declare function createPausedVideoFrames(): {
|
|
35
|
+
readonly active: boolean;
|
|
36
|
+
holdNative(videos: HTMLVideoElement[]): () => void;
|
|
37
|
+
skipped: (el: HTMLMediaElement) => boolean;
|
|
38
|
+
retained: (el: HTMLMediaElement) => boolean;
|
|
39
|
+
available: () => boolean;
|
|
40
|
+
setProvider(next: PausedVideoFrameProvider | null): void;
|
|
41
|
+
prepare(requests: PausedVideoFrameRequest[], signal: AbortSignal): Promise<{
|
|
42
|
+
valid: () => boolean;
|
|
43
|
+
present(): boolean;
|
|
44
|
+
refresh(): void;
|
|
45
|
+
release: () => void;
|
|
46
|
+
} | null>;
|
|
47
|
+
suspendProvider: () => Promise<void> | undefined;
|
|
48
|
+
prepareNative(): void;
|
|
49
|
+
notify(): void;
|
|
50
|
+
clear: (keep?: Set<HTMLVideoElement>, current?: (video: HTMLVideoElement, presented: boolean) => boolean) => void;
|
|
51
|
+
destroy(): void;
|
|
52
|
+
};
|