@hyperframes/core 0.9.21 → 0.9.23
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 +69 -69
- package/dist/index.d.ts +2 -1
- package/dist/previewProxyBox.d.ts +2 -0
- 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 +9 -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
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type { RuntimeTimelineClipIdentity } from "./runtime/types.js";
|
|
1
|
+
export type { PlaybackHeldBy, RuntimeTimelineClipIdentity } from "./runtime/types.js";
|
|
2
2
|
export type { ExecutionMode, Orientation, Asset, TimelineElement, TimelineElementBase, TimelineMediaElement, TimelineTextElement, TimelineCompositionElement, TimelineElementType, MediaElementType, CanvasResolution, Fps, FpsInput, FpsParseResult, MediaFile, CompositionAPI, PlayerAPI, AddElementData, ValidationResult, CompositionAsset, Keyframe, KeyframeProperties, ElementKeyframes, StageZoom, StageZoomKeyframe, CompositionVariableType, CompositionVariableBase, StringVariable, NumberVariable, ColorVariable, BooleanVariable, EnumVariable, CompositionVariable, CompositionSpec, WaveformData, OutputResolutionCompatibility, OutputResolutionIssueKind, } from "./core.types.js";
|
|
3
3
|
export type { SlideshowManifest, SlideRef, SlideHotspot, SlideSequence, ResolvedSlide, ResolvedSlideSequence, ResolvedSlideshow, } from "./slideshow/index.js";
|
|
4
4
|
export { parseSlideshowManifest, resolveSlideshow } from "./slideshow/index.js";
|
|
@@ -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";
|
|
@@ -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
|
+
};
|
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
import { MEDIA_ELEMENT_LEAD_SECONDS } from "./mediaClock.js";
|
|
2
|
+
import { COLOR_GRADING_SOURCE_HIDDEN_ATTR } from "../colorGrading.js";
|
|
3
|
+
import { installPausedVideoCallbacks, stagePausedVideoCallbacks, flushPausedVideoCallbacks, releasePausedVideoCallbacks, observeNativeVideoFrames, } from "./pausedVideoCallbacks.js";
|
|
4
|
+
import { PREVIEW_VIDEO_FRAME_ATTR as FRAME_ATTR, PREVIEW_VIDEO_SOURCE_ATTR, PREVIEW_VIDEO_CANVAS_ATTR, } from "../studioPreviewMark.js";
|
|
5
|
+
const SURFACE_BUDGET = 128 * 1024 * 1024;
|
|
6
|
+
const presented = new Map();
|
|
7
|
+
const authoredSeeks = new WeakSet();
|
|
8
|
+
export function getPausedVideoFrame(video) {
|
|
9
|
+
return presented.get(video)?.canvas ?? null;
|
|
10
|
+
}
|
|
11
|
+
export function ownsPausedVideoTime(video) {
|
|
12
|
+
return presented.get(video)?.driving === true;
|
|
13
|
+
}
|
|
14
|
+
export function getPausedExternalVideoFrame(video) {
|
|
15
|
+
const entry = presented.get(video);
|
|
16
|
+
if (!entry || typeof VideoFrame === "undefined")
|
|
17
|
+
return null;
|
|
18
|
+
entry.gpuFrame ??= new VideoFrame(entry.canvas, { timestamp: Math.round(entry.time * 1e6) });
|
|
19
|
+
return entry.gpuFrame;
|
|
20
|
+
}
|
|
21
|
+
export function captureNativeVideoFrame(video) {
|
|
22
|
+
const canvas = document.createElement("canvas");
|
|
23
|
+
canvas.width = video.videoWidth;
|
|
24
|
+
canvas.height = video.videoHeight;
|
|
25
|
+
const context = canvas.getContext("2d");
|
|
26
|
+
if (!context)
|
|
27
|
+
return null;
|
|
28
|
+
try {
|
|
29
|
+
context.drawImage(video, 0, 0);
|
|
30
|
+
return canvas;
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
export function nativeSnapshotStyle(video) {
|
|
37
|
+
if (getPausedVideoFrame(video))
|
|
38
|
+
return null;
|
|
39
|
+
// Its grading canvas already holds the picture; hiding the source would retire that canvas on refresh.
|
|
40
|
+
if (video.hasAttribute(COLOR_GRADING_SOURCE_HIDDEN_ATTR))
|
|
41
|
+
return null;
|
|
42
|
+
const css = getComputedStyle(video);
|
|
43
|
+
if (css.display === "none" || css.visibility === "hidden" || video.readyState < 2)
|
|
44
|
+
return null;
|
|
45
|
+
if (!video.videoWidth || !video.videoHeight)
|
|
46
|
+
return null;
|
|
47
|
+
return css;
|
|
48
|
+
}
|
|
49
|
+
function virtualTime(entry) {
|
|
50
|
+
let proto = Object.getPrototypeOf(entry.video);
|
|
51
|
+
let descriptor;
|
|
52
|
+
while (proto && !descriptor) {
|
|
53
|
+
descriptor = Object.getOwnPropertyDescriptor(proto, "currentTime");
|
|
54
|
+
proto = Object.getPrototypeOf(proto);
|
|
55
|
+
}
|
|
56
|
+
const nativeSet = descriptor?.set;
|
|
57
|
+
Object.defineProperty(entry.video, "currentTime", {
|
|
58
|
+
configurable: true,
|
|
59
|
+
get: () => entry.time,
|
|
60
|
+
set: (value) => {
|
|
61
|
+
if (Math.abs(value - entry.time) <= MEDIA_ELEMENT_LEAD_SECONDS * 2)
|
|
62
|
+
return;
|
|
63
|
+
authoredSeeks.add(entry.video);
|
|
64
|
+
remove(entry);
|
|
65
|
+
nativeSet?.call(entry.video, value);
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
entry.restoreTime = () => {
|
|
69
|
+
Reflect.deleteProperty(entry.video, "currentTime");
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
function remove(entry) {
|
|
73
|
+
entry.stopWaiting?.();
|
|
74
|
+
entry.restoreTime();
|
|
75
|
+
releasePausedVideoCallbacks(entry.video);
|
|
76
|
+
entry.gpuFrame?.close();
|
|
77
|
+
entry.video.removeAttribute(FRAME_ATTR);
|
|
78
|
+
entry.video.removeAttribute(PREVIEW_VIDEO_SOURCE_ATTR);
|
|
79
|
+
entry.canvas.remove();
|
|
80
|
+
entry.canvas.width = entry.canvas.height = 0;
|
|
81
|
+
presented.delete(entry.video);
|
|
82
|
+
}
|
|
83
|
+
export function releasePausedVideoFrame(video) {
|
|
84
|
+
const entry = presented.get(video);
|
|
85
|
+
if (entry)
|
|
86
|
+
remove(entry);
|
|
87
|
+
}
|
|
88
|
+
function refresh(entry) {
|
|
89
|
+
const { video, canvas } = entry;
|
|
90
|
+
video.removeAttribute(FRAME_ATTR);
|
|
91
|
+
if (video.hasAttribute(COLOR_GRADING_SOURCE_HIDDEN_ATTR)) {
|
|
92
|
+
canvas.remove();
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
const css = getComputedStyle(video);
|
|
96
|
+
for (const key of Array.from(css)) {
|
|
97
|
+
const value = css.getPropertyValue(key);
|
|
98
|
+
if (canvas.style.getPropertyValue(key) !== value)
|
|
99
|
+
canvas.style.setProperty(key, value);
|
|
100
|
+
}
|
|
101
|
+
if (css.position !== "absolute" && css.position !== "fixed") {
|
|
102
|
+
canvas.style.position = "absolute";
|
|
103
|
+
canvas.style.inset = "auto";
|
|
104
|
+
canvas.style.left = `${video.offsetLeft}px`;
|
|
105
|
+
canvas.style.top = `${video.offsetTop}px`;
|
|
106
|
+
canvas.style.margin = "0";
|
|
107
|
+
}
|
|
108
|
+
canvas.style.setProperty("pointer-events", "none", "important");
|
|
109
|
+
canvas.style.setProperty("animation", "none", "important");
|
|
110
|
+
canvas.style.setProperty("transition", "none", "important");
|
|
111
|
+
if (video.previousElementSibling !== canvas)
|
|
112
|
+
video.before(canvas);
|
|
113
|
+
video.setAttribute(FRAME_ATTR, "");
|
|
114
|
+
}
|
|
115
|
+
function markFrameCanvas(canvas) {
|
|
116
|
+
canvas.setAttribute(PREVIEW_VIDEO_CANVAS_ATTR, "");
|
|
117
|
+
canvas.setAttribute("data-layout-ignore", "");
|
|
118
|
+
canvas.setAttribute("data-hyperframes-ignore", "");
|
|
119
|
+
canvas.setAttribute("data-hyperframes-picker-ignore", "");
|
|
120
|
+
canvas.setAttribute("data-hf-ignore", "");
|
|
121
|
+
canvas.setAttribute("aria-hidden", "true");
|
|
122
|
+
}
|
|
123
|
+
function adoptStagedFrame(next, existing) {
|
|
124
|
+
if (existing) {
|
|
125
|
+
const { entry, context } = existing;
|
|
126
|
+
entry.stopWaiting?.();
|
|
127
|
+
entry.stopWaiting = undefined;
|
|
128
|
+
entry.gpuFrame?.close();
|
|
129
|
+
entry.gpuFrame = undefined;
|
|
130
|
+
context.clearRect(0, 0, entry.canvas.width, entry.canvas.height);
|
|
131
|
+
context.drawImage(next.canvas, 0, 0);
|
|
132
|
+
next.canvas.width = next.canvas.height = 0;
|
|
133
|
+
entry.time = next.time;
|
|
134
|
+
if (!entry.driving)
|
|
135
|
+
virtualTime(entry);
|
|
136
|
+
entry.driving = true;
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
const { video, canvas } = next;
|
|
140
|
+
releasePausedVideoFrame(video);
|
|
141
|
+
markFrameCanvas(canvas);
|
|
142
|
+
presented.set(video, next);
|
|
143
|
+
video.setAttribute(PREVIEW_VIDEO_SOURCE_ATTR, "");
|
|
144
|
+
virtualTime(next);
|
|
145
|
+
}
|
|
146
|
+
/** Keeps original nodes, URLs, intrinsic sizes and the displayed source clock intact. */
|
|
147
|
+
export function createPausedVideoFrames() {
|
|
148
|
+
installPausedVideoCallbacks();
|
|
149
|
+
let provider = null;
|
|
150
|
+
const style = document.createElement("style");
|
|
151
|
+
style.textContent = `[${FRAME_ATTR}] { opacity: 0 !important; }`;
|
|
152
|
+
const observer = new MutationObserver((records) => {
|
|
153
|
+
for (const entry of [...presented.values()]) {
|
|
154
|
+
if (!entry.video.isConnected ||
|
|
155
|
+
records.some((record) => (record.type === "childList" && record.target === entry.video) ||
|
|
156
|
+
(record.type === "attributes" &&
|
|
157
|
+
record.attributeName === "src" &&
|
|
158
|
+
(record.target === entry.video || record.target.parentElement === entry.video))))
|
|
159
|
+
remove(entry);
|
|
160
|
+
else if (records.some((record) => record.target.contains(entry.video)))
|
|
161
|
+
refresh(entry);
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
observer.observe(document.body, {
|
|
165
|
+
childList: true,
|
|
166
|
+
subtree: true,
|
|
167
|
+
attributes: true,
|
|
168
|
+
attributeFilter: ["src", "style", "class", "hidden", "data-hidden"],
|
|
169
|
+
});
|
|
170
|
+
const clear = (keep, current) => {
|
|
171
|
+
for (const entry of [...presented.values()]) {
|
|
172
|
+
if (!keep?.has(entry.video)) {
|
|
173
|
+
remove(entry);
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
entry.nativeSkipped = true;
|
|
177
|
+
const reveal = (nativeFrame) => {
|
|
178
|
+
if (presented.get(entry.video) !== entry)
|
|
179
|
+
return true;
|
|
180
|
+
if (entry.video.readyState < 2 ||
|
|
181
|
+
entry.video.seeking ||
|
|
182
|
+
(current && !current(entry.video, nativeFrame)))
|
|
183
|
+
return false;
|
|
184
|
+
remove(entry);
|
|
185
|
+
return true;
|
|
186
|
+
};
|
|
187
|
+
const ready = () => {
|
|
188
|
+
reveal(false);
|
|
189
|
+
};
|
|
190
|
+
const events = ["loadeddata", "canplay", "seeked"];
|
|
191
|
+
entry.stopWaiting?.();
|
|
192
|
+
for (const event of events)
|
|
193
|
+
entry.video.addEventListener(event, ready);
|
|
194
|
+
const stopFrames = observeNativeVideoFrames(entry.video, () => reveal(true));
|
|
195
|
+
entry.stopWaiting = () => {
|
|
196
|
+
for (const event of events)
|
|
197
|
+
entry.video.removeEventListener(event, ready);
|
|
198
|
+
stopFrames();
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
if (presented.size === 0)
|
|
202
|
+
style.remove();
|
|
203
|
+
};
|
|
204
|
+
return {
|
|
205
|
+
get active() {
|
|
206
|
+
// A skipped native clip may keep a fallback picture without owning paused playback.
|
|
207
|
+
return [...presented.values()].some((entry) => !entry.stopWaiting);
|
|
208
|
+
},
|
|
209
|
+
holdNative(videos) {
|
|
210
|
+
const held = [];
|
|
211
|
+
for (const video of videos) {
|
|
212
|
+
if (!nativeSnapshotStyle(video))
|
|
213
|
+
continue;
|
|
214
|
+
const canvas = captureNativeVideoFrame(video);
|
|
215
|
+
if (!canvas)
|
|
216
|
+
continue;
|
|
217
|
+
const entry = {
|
|
218
|
+
video,
|
|
219
|
+
canvas,
|
|
220
|
+
time: video.currentTime,
|
|
221
|
+
driving: false,
|
|
222
|
+
restoreTime: () => { },
|
|
223
|
+
};
|
|
224
|
+
held.push(entry);
|
|
225
|
+
markFrameCanvas(canvas);
|
|
226
|
+
presented.set(video, entry);
|
|
227
|
+
video.setAttribute(PREVIEW_VIDEO_SOURCE_ATTR, "");
|
|
228
|
+
if (!style.isConnected)
|
|
229
|
+
document.head.append(style);
|
|
230
|
+
refresh(entry);
|
|
231
|
+
}
|
|
232
|
+
return () => {
|
|
233
|
+
for (const entry of held)
|
|
234
|
+
if (presented.get(entry.video) === entry && !entry.driving)
|
|
235
|
+
remove(entry);
|
|
236
|
+
};
|
|
237
|
+
},
|
|
238
|
+
skipped: (el) => presented.get(el)?.nativeSkipped === true,
|
|
239
|
+
retained: (el) => presented.get(el)?.stopWaiting !== undefined,
|
|
240
|
+
available: () => provider !== null,
|
|
241
|
+
setProvider(next) {
|
|
242
|
+
if (provider === next)
|
|
243
|
+
return;
|
|
244
|
+
provider?.dispose();
|
|
245
|
+
provider = next;
|
|
246
|
+
},
|
|
247
|
+
async prepare(requests, signal) {
|
|
248
|
+
if (!provider ||
|
|
249
|
+
requests.length === 0 ||
|
|
250
|
+
requests.some(({ video }) => authoredSeeks.has(video) ||
|
|
251
|
+
(!presented.has(video) && Object.hasOwn(video, "currentTime"))))
|
|
252
|
+
return null;
|
|
253
|
+
const owner = provider;
|
|
254
|
+
const identity = (video) => JSON.stringify([
|
|
255
|
+
video.src,
|
|
256
|
+
...Array.from(video.querySelectorAll("source"), (source) => source.src),
|
|
257
|
+
]);
|
|
258
|
+
const sources = requests.map(({ video }) => identity(video));
|
|
259
|
+
const frames = await owner.decode(requests, signal);
|
|
260
|
+
let released = false;
|
|
261
|
+
const release = () => {
|
|
262
|
+
if (released)
|
|
263
|
+
return;
|
|
264
|
+
released = true;
|
|
265
|
+
for (const frame of frames)
|
|
266
|
+
frame.release();
|
|
267
|
+
};
|
|
268
|
+
const current = () => !signal.aborted &&
|
|
269
|
+
provider === owner &&
|
|
270
|
+
requests.every(({ video }, index) => video.isConnected && identity(video) === sources[index]);
|
|
271
|
+
if (!current()) {
|
|
272
|
+
release();
|
|
273
|
+
return null;
|
|
274
|
+
}
|
|
275
|
+
const valid = frames.reduce((sum, frame) => sum + frame.width * frame.height * 4, 0) <= SURFACE_BUDGET &&
|
|
276
|
+
frames.length === requests.length &&
|
|
277
|
+
frames.every((frame, index) => {
|
|
278
|
+
const time = requests[index].time;
|
|
279
|
+
return (frame.width > 0 &&
|
|
280
|
+
frame.height > 0 &&
|
|
281
|
+
frame.timestamp <= time + 1e-5 &&
|
|
282
|
+
time < frame.timestamp + frame.duration + 1e-5);
|
|
283
|
+
});
|
|
284
|
+
if (!valid) {
|
|
285
|
+
release();
|
|
286
|
+
return null;
|
|
287
|
+
}
|
|
288
|
+
return {
|
|
289
|
+
valid: current,
|
|
290
|
+
present() {
|
|
291
|
+
if (!current()) {
|
|
292
|
+
release();
|
|
293
|
+
return false;
|
|
294
|
+
}
|
|
295
|
+
const staged = [];
|
|
296
|
+
const reusable = new Map();
|
|
297
|
+
try {
|
|
298
|
+
frames.forEach((frame, index) => {
|
|
299
|
+
const { video, time } = requests[index];
|
|
300
|
+
const canvas = document.createElement("canvas");
|
|
301
|
+
canvas.width = frame.width;
|
|
302
|
+
canvas.height = frame.height;
|
|
303
|
+
staged.push({ video, canvas, time, driving: true, restoreTime: () => { } });
|
|
304
|
+
const context = canvas.getContext("2d");
|
|
305
|
+
if (!context)
|
|
306
|
+
throw new Error("Preview canvas unavailable");
|
|
307
|
+
context.drawImage(frame.source, 0, 0, canvas.width, canvas.height);
|
|
308
|
+
const prior = presented.get(video);
|
|
309
|
+
if (prior &&
|
|
310
|
+
prior.canvas.width === frame.width &&
|
|
311
|
+
prior.canvas.height === frame.height) {
|
|
312
|
+
const destination = prior.canvas.getContext("2d");
|
|
313
|
+
if (!destination)
|
|
314
|
+
throw new Error("Preview canvas unavailable");
|
|
315
|
+
reusable.set(staged[staged.length - 1], { entry: prior, context: destination });
|
|
316
|
+
}
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
catch {
|
|
320
|
+
for (const { canvas } of staged)
|
|
321
|
+
canvas.width = canvas.height = 0;
|
|
322
|
+
return false;
|
|
323
|
+
}
|
|
324
|
+
finally {
|
|
325
|
+
release();
|
|
326
|
+
}
|
|
327
|
+
const wanted = new Set(requests.map(({ video }) => video));
|
|
328
|
+
for (const entry of [...presented.values()])
|
|
329
|
+
if (!wanted.has(entry.video))
|
|
330
|
+
remove(entry);
|
|
331
|
+
if (!style.isConnected)
|
|
332
|
+
document.head.append(style);
|
|
333
|
+
for (const [index, next] of staged.entries()) {
|
|
334
|
+
adoptStagedFrame(next, reusable.get(next));
|
|
335
|
+
const { timestamp, width, height } = frames[index];
|
|
336
|
+
stagePausedVideoCallbacks(next.video, { mediaTime: timestamp, width, height });
|
|
337
|
+
}
|
|
338
|
+
return true;
|
|
339
|
+
},
|
|
340
|
+
refresh() {
|
|
341
|
+
for (const entry of presented.values())
|
|
342
|
+
refresh(entry);
|
|
343
|
+
},
|
|
344
|
+
release,
|
|
345
|
+
};
|
|
346
|
+
},
|
|
347
|
+
suspendProvider: () => provider?.suspend?.(),
|
|
348
|
+
prepareNative() {
|
|
349
|
+
for (const entry of presented.values()) {
|
|
350
|
+
entry.driving = false;
|
|
351
|
+
entry.restoreTime();
|
|
352
|
+
}
|
|
353
|
+
},
|
|
354
|
+
notify() {
|
|
355
|
+
for (const { video } of presented.values())
|
|
356
|
+
flushPausedVideoCallbacks(video);
|
|
357
|
+
},
|
|
358
|
+
clear,
|
|
359
|
+
destroy() {
|
|
360
|
+
observer.disconnect();
|
|
361
|
+
clear();
|
|
362
|
+
provider?.dispose();
|
|
363
|
+
provider = null;
|
|
364
|
+
},
|
|
365
|
+
};
|
|
366
|
+
}
|
package/dist/runtime/types.d.ts
CHANGED
|
@@ -229,6 +229,13 @@ export type RuntimePreviewCopiesMessage = {
|
|
|
229
229
|
error: string | null;
|
|
230
230
|
} | null;
|
|
231
231
|
};
|
|
232
|
+
export type FetchNotSeenToStart = null;
|
|
233
|
+
export type PlaybackHeldBy = {
|
|
234
|
+
kind: "audio" | "video";
|
|
235
|
+
readyState: number;
|
|
236
|
+
networkState: number;
|
|
237
|
+
fetchMs: number | FetchNotSeenToStart;
|
|
238
|
+
};
|
|
232
239
|
export type RuntimePreviewHealthMessage = {
|
|
233
240
|
source: "hf-preview";
|
|
234
241
|
type: "preview-health";
|
|
@@ -245,6 +252,7 @@ export type RuntimePreviewHealthMessage = {
|
|
|
245
252
|
gapP50Ms: number;
|
|
246
253
|
gapP95Ms: number;
|
|
247
254
|
clockSource: "audio" | "webaudio" | "monotonic";
|
|
255
|
+
heldBy: PlaybackHeldBy | null;
|
|
248
256
|
mode: "play";
|
|
249
257
|
} | null;
|
|
250
258
|
};
|
|
@@ -275,6 +283,7 @@ export type RuntimePlayer = {
|
|
|
275
283
|
pause: () => void;
|
|
276
284
|
seek: (timeSeconds: number, options?: {
|
|
277
285
|
keepPlaying?: boolean;
|
|
286
|
+
settled?: boolean;
|
|
278
287
|
}) => HeldSeek;
|
|
279
288
|
renderSeek: (timeSeconds: number, options?: RuntimeSeekOptions) => void;
|
|
280
289
|
getTime: () => number;
|
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
export declare const STUDIO_PREVIEW_MARK_META = "hyperframes-studio-preview";
|
|
2
|
+
export declare const PREVIEW_SEEK_HOLD_ATTR = "data-hf-preview-seek-hold";
|
|
3
|
+
export declare const PREVIEW_VIDEO_FRAME_ATTR = "data-hf-preview-video-frame";
|
|
4
|
+
export declare const PREVIEW_VIDEO_SOURCE_ATTR = "data-hf-preview-video-source";
|
|
5
|
+
export declare const PREVIEW_VIDEO_CANVAS_ATTR = "data-hf-preview-video-canvas";
|
|
2
6
|
export declare const STUDIO_PREVIEW_CAPTURE_MODE = "capture";
|
|
3
7
|
export declare const PLAYER_FRAME_ATTR = "data-hf-player-frame";
|
|
4
8
|
export declare function inPlayerFrame(doc: Document): boolean;
|
|
@@ -14,6 +18,6 @@ export declare const RUNTIME_STAMPED_ATTR = "data-hf-autostamped";
|
|
|
14
18
|
export declare const CLIP_HIDDEN_ATTR = "data-hf-clip-hidden";
|
|
15
19
|
export declare const STUDIO_PREVIEW_FAR_SCENE_ATTR = "data-hf-far-scene";
|
|
16
20
|
export declare const STUDIO_PREVIEW_PARKED_SCENE_ATTR = "data-hf-parked-scene";
|
|
17
|
-
export declare const PREVIEW_ONLY_ATTRS: readonly ["data-hf-upcoming", "data-hf-far-scene", "data-hf-parked-scene", "data-hf-preview-lazy", "data-hf-preview-raster", "data-hf-autostamped"];
|
|
21
|
+
export declare const PREVIEW_ONLY_ATTRS: readonly ["data-hf-preview-video-frame", "data-hf-preview-video-source", "data-hf-preview-video-canvas", "data-hf-upcoming", "data-hf-far-scene", "data-hf-parked-scene", "data-hf-preview-lazy", "data-hf-preview-raster", "data-hf-autostamped"];
|
|
18
22
|
export declare const isClipHiddenByRuntime: (el: HTMLElement) => boolean;
|
|
19
23
|
export declare function applyAuthoredTiming(el: Element, attrs: ReadonlyArray<readonly [name: string, value: string]>): void;
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { isWebKit } from "./runtime/mediaClock.js";
|
|
2
2
|
export const STUDIO_PREVIEW_MARK_META = "hyperframes-studio-preview";
|
|
3
|
+
export const PREVIEW_SEEK_HOLD_ATTR = "data-hf-preview-seek-hold";
|
|
4
|
+
export const PREVIEW_VIDEO_FRAME_ATTR = "data-hf-preview-video-frame";
|
|
5
|
+
export const PREVIEW_VIDEO_SOURCE_ATTR = "data-hf-preview-video-source";
|
|
6
|
+
export const PREVIEW_VIDEO_CANVAS_ATTR = "data-hf-preview-video-canvas";
|
|
3
7
|
export const STUDIO_PREVIEW_CAPTURE_MODE = "capture";
|
|
4
8
|
export const PLAYER_FRAME_ATTR = "data-hf-player-frame";
|
|
5
9
|
export function inPlayerFrame(doc) {
|
|
@@ -23,6 +27,9 @@ export const CLIP_HIDDEN_ATTR = "data-hf-clip-hidden";
|
|
|
23
27
|
export const STUDIO_PREVIEW_FAR_SCENE_ATTR = "data-hf-far-scene";
|
|
24
28
|
export const STUDIO_PREVIEW_PARKED_SCENE_ATTR = "data-hf-parked-scene";
|
|
25
29
|
export const PREVIEW_ONLY_ATTRS = [
|
|
30
|
+
PREVIEW_VIDEO_FRAME_ATTR,
|
|
31
|
+
PREVIEW_VIDEO_SOURCE_ATTR,
|
|
32
|
+
PREVIEW_VIDEO_CANVAS_ATTR,
|
|
26
33
|
STUDIO_PREVIEW_UPCOMING_ATTR,
|
|
27
34
|
STUDIO_PREVIEW_FAR_SCENE_ATTR,
|
|
28
35
|
STUDIO_PREVIEW_PARKED_SCENE_ATTR,
|