@onmark/cli 0.2.1 → 0.3.0
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/README.md +2 -2
- package/onmark-release.json +23 -23
- package/package.json +4 -4
- package/packages/bundler/dist/src/authored_html.js +4 -1
- package/packages/runtime/dist/src/generated/browser-request.d.ts +53 -3
- package/packages/runtime/dist/src/generated/browser-response.d.ts +1 -1
- package/packages/runtime/dist/src/generated/validators.d.ts +2 -2
- package/packages/runtime/dist/src/generated/validators.js +952 -135
- package/packages/runtime/dist/src/index.d.ts +1 -1
- package/packages/runtime/dist/src/media.d.ts +9 -0
- package/packages/runtime/dist/src/media.js +189 -15
- package/packages/runtime/dist/src/session.js +31 -2
- package/packages/runtime/dist/src/video.js +9 -4
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export { ProtocolDecodeError, decodeBrowserRequest, decodeBrowserResponse, } from "./generated/codec.js";
|
|
2
2
|
export type { BrowserRequest } from "./generated/browser-request.js";
|
|
3
3
|
export type { BrowserResponse } from "./generated/browser-response.js";
|
|
4
|
-
export type { BrowserPlan } from "./generated/browser-request.js";
|
|
4
|
+
export type { BrowserPlan, BrowserVideo } from "./generated/browser-request.js";
|
|
5
5
|
export { runtimeFrameAt, type RuntimeFrame } from "./clock.js";
|
|
6
6
|
export { videoFrameSelection, type RuntimeVideo, type VideoFrameSelection, } from "./media.js";
|
|
7
7
|
export { DecodedVideo, materializedVideoSource, type BrowserVideoElement, type DecodedVideoOptions, } from "./video.js";
|
|
@@ -7,7 +7,16 @@ type FrameRate = RuntimePlan["frameRate"];
|
|
|
7
7
|
export interface VideoFrameSelection {
|
|
8
8
|
readonly mediaTimeSeconds: number;
|
|
9
9
|
readonly seekTimeSeconds: number;
|
|
10
|
+
/** Largest callback error that cannot identify an adjacent source frame. */
|
|
11
|
+
readonly readinessToleranceSeconds: number;
|
|
10
12
|
}
|
|
11
13
|
/** Selects the source frame visible at one output-frame midpoint. */
|
|
12
14
|
export declare function videoFrameSelection(frame: RuntimeFrame, video: RuntimeVideo, outputFrameRate: FrameRate): VideoFrameSelection | undefined;
|
|
15
|
+
/** Checks protocol source facts without deriving any authored timeline. */
|
|
16
|
+
export declare function videoSourceMappingIsValid(video: RuntimeVideo, outputFrameRate: FrameRate): boolean;
|
|
17
|
+
/** Checks the canonical positive-rational spelling used by Rust wire values. */
|
|
18
|
+
export declare function exactRatioIsCanonical(rate: {
|
|
19
|
+
readonly numerator: number;
|
|
20
|
+
readonly denominator: number;
|
|
21
|
+
}): boolean;
|
|
13
22
|
export {};
|
|
@@ -1,28 +1,202 @@
|
|
|
1
1
|
// Browser-media projection from exact Rust-owned frame facts.
|
|
2
2
|
// It selects source frames without becoming a second timeline solver.
|
|
3
|
+
const MAX_DURATION_NANOSECONDS = 18446744073709551615n;
|
|
4
|
+
const MAX_RATIO_PART = 4_294_967_295;
|
|
3
5
|
/** Selects the source frame visible at one output-frame midpoint. */
|
|
4
6
|
export function videoFrameSelection(frame, video, outputFrameRate) {
|
|
5
7
|
if (frame.index < video.interval.start || frame.index >= video.interval.end) {
|
|
6
8
|
return undefined;
|
|
7
9
|
}
|
|
8
10
|
const localFrame = frame.index - video.interval.start;
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
+
const sourceTime = sourceTimeAtMidpoint(localFrame, outputFrameRate, video.source);
|
|
12
|
+
return selectSourceFrame(sourceTime, video.sourceTiming, video.source);
|
|
13
|
+
}
|
|
14
|
+
/** Checks protocol source facts without deriving any authored timeline. */
|
|
15
|
+
export function videoSourceMappingIsValid(video, outputFrameRate) {
|
|
16
|
+
const source = video.source;
|
|
17
|
+
const start = BigInt(source.startNanoseconds);
|
|
18
|
+
const end = BigInt(source.endNanoseconds);
|
|
19
|
+
const naturalEnd = BigInt(source.naturalEndNanoseconds);
|
|
20
|
+
if (start >= end ||
|
|
21
|
+
end > naturalEnd ||
|
|
22
|
+
naturalEnd > MAX_DURATION_NANOSECONDS) {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
if (!exactRatioIsCanonical(source.playbackRate) ||
|
|
26
|
+
!Number.isInteger(source.plays) ||
|
|
27
|
+
source.plays <= 0 ||
|
|
28
|
+
source.plays > 4_294_967_295 ||
|
|
29
|
+
!canonicalUnsignedInteger(source.holdLastNanoseconds) ||
|
|
30
|
+
!sourceTimingIsValid(video.sourceTiming, naturalEnd)) {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
const sourceDuration = end - start;
|
|
34
|
+
const playbackNumerator = sourceDuration *
|
|
35
|
+
BigInt(outputFrameRate.numerator) *
|
|
36
|
+
BigInt(source.playbackRate.denominator) *
|
|
37
|
+
BigInt(source.plays);
|
|
38
|
+
const holdNumerator = BigInt(source.holdLastNanoseconds) *
|
|
39
|
+
BigInt(outputFrameRate.numerator) *
|
|
40
|
+
BigInt(source.playbackRate.numerator);
|
|
41
|
+
const numerator = playbackNumerator + holdNumerator;
|
|
42
|
+
const denominator = 1000000000n *
|
|
43
|
+
BigInt(outputFrameRate.denominator) *
|
|
44
|
+
BigInt(source.playbackRate.numerator);
|
|
45
|
+
const expectedFrames = (numerator + denominator - 1n) / denominator;
|
|
46
|
+
const actualFrames = BigInt(video.interval.end - video.interval.start);
|
|
47
|
+
return actualFrames === expectedFrames;
|
|
48
|
+
}
|
|
49
|
+
function sourceTimeAtMidpoint(localFrame, outputFrameRate, source) {
|
|
50
|
+
const midpoint = 2n * BigInt(localFrame) + 1n;
|
|
51
|
+
const outputNumerator = BigInt(outputFrameRate.numerator);
|
|
52
|
+
const outputDenominator = BigInt(outputFrameRate.denominator);
|
|
53
|
+
const speedNumerator = BigInt(source.playbackRate.numerator);
|
|
54
|
+
const speedDenominator = BigInt(source.playbackRate.denominator);
|
|
55
|
+
const startNanoseconds = BigInt(source.startNanoseconds);
|
|
56
|
+
const endNanoseconds = BigInt(source.endNanoseconds);
|
|
57
|
+
const plays = BigInt(source.plays);
|
|
58
|
+
const nanosecondsPerSecond = 1000000000n;
|
|
59
|
+
// Rust owns the source treatment. This integer projection samples one exact
|
|
60
|
+
// pass without reconstructing a browser timeline.
|
|
61
|
+
const timeDenominator = 2n * outputNumerator * speedDenominator;
|
|
62
|
+
const elapsedNumerator = midpoint * outputDenominator * speedNumerator * nanosecondsPerSecond;
|
|
63
|
+
const passNumerator = (endNanoseconds - startNanoseconds) * timeDenominator;
|
|
64
|
+
const playbackNumerator = passNumerator * plays;
|
|
65
|
+
const sourceTimeNumerator = elapsedNumerator < playbackNumerator
|
|
66
|
+
? startNanoseconds * timeDenominator + (elapsedNumerator % passNumerator)
|
|
67
|
+
: endNanoseconds * timeDenominator - 1n;
|
|
68
|
+
return {
|
|
69
|
+
numerator: sourceTimeNumerator,
|
|
70
|
+
denominator: timeDenominator,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
// ── CFR and VFR projection ──
|
|
74
|
+
function selectSourceFrame(time, timing, source) {
|
|
75
|
+
switch (timing.kind) {
|
|
76
|
+
case "constant":
|
|
77
|
+
return selectConstantFrame(time, timing.frameRate, source);
|
|
78
|
+
case "variable":
|
|
79
|
+
return selectVariableFrame(time, timing);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
function selectConstantFrame(time, rate, source) {
|
|
83
|
+
const rateNumerator = BigInt(rate.numerator);
|
|
84
|
+
const frameDenominator = time.denominator * 1000000000n * BigInt(rate.denominator);
|
|
85
|
+
const selected = (time.numerator * rateNumerator) / frameDenominator;
|
|
86
|
+
const sourceEnd = BigInt(source.endNanoseconds) * rateNumerator;
|
|
87
|
+
const exclusiveEnd = divideCeil(sourceEnd, 1000000000n * BigInt(rate.denominator));
|
|
88
|
+
const frame = selected < exclusiveEnd ? selected : exclusiveEnd - 1n;
|
|
89
|
+
const rateDenominator = BigInt(rate.denominator);
|
|
90
|
+
return projectFrameInterval(frame * rateDenominator, (frame + 1n) * rateDenominator, rateNumerator, rateDenominator);
|
|
91
|
+
}
|
|
92
|
+
function selectVariableFrame(time, timing) {
|
|
93
|
+
const selected = sourceFrameIndex(time, timing.timebase, timing.boundaries);
|
|
94
|
+
const start = timing.boundaries[selected];
|
|
95
|
+
const end = timing.boundaries[selected + 1];
|
|
96
|
+
if (start === undefined || end === undefined) {
|
|
97
|
+
throw new RangeError("selected source frame lies outside its timestamp map");
|
|
98
|
+
}
|
|
99
|
+
const startTick = BigInt(start);
|
|
100
|
+
const endTick = BigInt(end);
|
|
101
|
+
const timebaseNumerator = BigInt(timing.timebase.numerator);
|
|
102
|
+
const previous = timing.boundaries[selected - 1];
|
|
103
|
+
const previousDistance = previous === undefined ? endTick - startTick : startTick - BigInt(previous);
|
|
104
|
+
const nearestDistance = previousDistance < endTick - startTick
|
|
105
|
+
? previousDistance
|
|
106
|
+
: endTick - startTick;
|
|
107
|
+
return projectFrameInterval(startTick * timebaseNumerator, endTick * timebaseNumerator, BigInt(timing.timebase.denominator), nearestDistance * timebaseNumerator);
|
|
108
|
+
}
|
|
109
|
+
function projectFrameInterval(startNumerator, endNumerator, denominator, neighborDistanceNumerator) {
|
|
110
|
+
// Frame identity remains exact until this single projection into the
|
|
111
|
+
// browser's floating-point media API. Reject intervals for which no
|
|
112
|
+
// representable interior second can prove the selected frame.
|
|
113
|
+
const mediaTimeSeconds = rationalSeconds(startNumerator, denominator);
|
|
114
|
+
const seekTimeSeconds = rationalSeconds(startNumerator + endNumerator, 2n * denominator);
|
|
115
|
+
const endTimeSeconds = rationalSeconds(endNumerator, denominator);
|
|
116
|
+
const readinessToleranceSeconds = Math.min(0.000_001, rationalSeconds(neighborDistanceNumerator, denominator) / 4);
|
|
117
|
+
if (!Number.isFinite(mediaTimeSeconds) ||
|
|
118
|
+
!(mediaTimeSeconds < seekTimeSeconds) ||
|
|
119
|
+
!(seekTimeSeconds < endTimeSeconds) ||
|
|
120
|
+
!(readinessToleranceSeconds > 0)) {
|
|
121
|
+
throw new RangeError("selected source frame has no representable interior seek time");
|
|
122
|
+
}
|
|
11
123
|
return Object.freeze({
|
|
12
|
-
mediaTimeSeconds
|
|
13
|
-
seekTimeSeconds
|
|
124
|
+
mediaTimeSeconds,
|
|
125
|
+
seekTimeSeconds,
|
|
126
|
+
readinessToleranceSeconds,
|
|
14
127
|
});
|
|
15
128
|
}
|
|
16
|
-
function
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
129
|
+
function rationalSeconds(numerator, denominator) {
|
|
130
|
+
const whole = numerator / denominator;
|
|
131
|
+
const remainder = numerator % denominator;
|
|
132
|
+
return Number(whole) + Number(remainder) / Number(denominator);
|
|
133
|
+
}
|
|
134
|
+
function sourceFrameIndex(time, timebase, boundaries) {
|
|
135
|
+
const targetNumerator = time.numerator * BigInt(timebase.denominator);
|
|
136
|
+
const targetDenominator = time.denominator * BigInt(timebase.numerator) * 1000000000n;
|
|
137
|
+
let low = 0;
|
|
138
|
+
let high = boundaries.length;
|
|
139
|
+
while (low < high) {
|
|
140
|
+
const middle = low + Math.floor((high - low) / 2);
|
|
141
|
+
const boundary = boundaries[middle];
|
|
142
|
+
if (boundary !== undefined &&
|
|
143
|
+
BigInt(boundary) * targetDenominator <= targetNumerator) {
|
|
144
|
+
low = middle + 1;
|
|
145
|
+
}
|
|
146
|
+
else {
|
|
147
|
+
high = middle;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return Math.min(low - 1, boundaries.length - 2);
|
|
151
|
+
}
|
|
152
|
+
// ── Wire-fact validation ──
|
|
153
|
+
function sourceTimingIsValid(timing, naturalEndNanoseconds) {
|
|
154
|
+
if (timing.kind === "constant") {
|
|
155
|
+
return exactRatioIsCanonical(timing.frameRate);
|
|
156
|
+
}
|
|
157
|
+
if (!exactRatioIsCanonical(timing.timebase) ||
|
|
158
|
+
timing.boundaries.length < 3 ||
|
|
159
|
+
timing.boundaries.length > 100_000) {
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
let previous = -1n;
|
|
163
|
+
for (const spelling of timing.boundaries) {
|
|
164
|
+
if (!canonicalUnsignedInteger(spelling)) {
|
|
165
|
+
return false;
|
|
166
|
+
}
|
|
167
|
+
const boundary = BigInt(spelling);
|
|
168
|
+
if (boundary <= previous) {
|
|
169
|
+
return false;
|
|
170
|
+
}
|
|
171
|
+
previous = boundary;
|
|
172
|
+
}
|
|
173
|
+
if (timing.boundaries[0] !== "0") {
|
|
174
|
+
return false;
|
|
175
|
+
}
|
|
176
|
+
const duration = divideCeil(previous * BigInt(timing.timebase.numerator) * 1000000000n, BigInt(timing.timebase.denominator));
|
|
177
|
+
return duration === naturalEndNanoseconds;
|
|
178
|
+
}
|
|
179
|
+
function canonicalUnsignedInteger(value) {
|
|
180
|
+
return (/^(0|[1-9][0-9]*)$/u.test(value) &&
|
|
181
|
+
BigInt(value) <= MAX_DURATION_NANOSECONDS);
|
|
182
|
+
}
|
|
183
|
+
function divideCeil(numerator, denominator) {
|
|
184
|
+
return (numerator + denominator - 1n) / denominator;
|
|
185
|
+
}
|
|
186
|
+
/** Checks the canonical positive-rational spelling used by Rust wire values. */
|
|
187
|
+
export function exactRatioIsCanonical(rate) {
|
|
188
|
+
if (!Number.isInteger(rate.numerator) ||
|
|
189
|
+
!Number.isInteger(rate.denominator) ||
|
|
190
|
+
rate.numerator < 1 ||
|
|
191
|
+
rate.denominator < 1 ||
|
|
192
|
+
rate.numerator > MAX_RATIO_PART ||
|
|
193
|
+
rate.denominator > MAX_RATIO_PART) {
|
|
194
|
+
return false;
|
|
195
|
+
}
|
|
196
|
+
let left = BigInt(rate.numerator);
|
|
197
|
+
let right = BigInt(rate.denominator);
|
|
198
|
+
while (right !== 0n) {
|
|
199
|
+
[left, right] = [right, left % right];
|
|
26
200
|
}
|
|
27
|
-
return
|
|
201
|
+
return left === 1n;
|
|
28
202
|
}
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import { decodeBrowserResponse } from "./generated/codec.js";
|
|
5
5
|
import { MAX_FAILURE_MESSAGE_CHARACTERS, MAX_PENDING_RESOURCE_CHARACTERS, MAX_PENDING_RESOURCES, } from "./generated/runtime-contract.js";
|
|
6
6
|
import { runtimeFrameAt } from "./clock.js";
|
|
7
|
+
import { exactRatioIsCanonical, videoSourceMappingIsValid } from "./media.js";
|
|
7
8
|
/** Expected failure reported by a browser adapter. */
|
|
8
9
|
export class RuntimeAdapterError extends Error {
|
|
9
10
|
kind;
|
|
@@ -156,7 +157,7 @@ export class RuntimeSession {
|
|
|
156
157
|
}
|
|
157
158
|
}
|
|
158
159
|
function response(requestId, event) {
|
|
159
|
-
return decodeBrowserResponse({ version:
|
|
160
|
+
return decodeBrowserResponse({ version: 3, requestId, event });
|
|
160
161
|
}
|
|
161
162
|
function invalidRequest(requestId, message) {
|
|
162
163
|
return response(requestId, {
|
|
@@ -195,6 +196,9 @@ function readinessFailure(requestId, operationCode, error) {
|
|
|
195
196
|
return operationFailure(requestId, operationCode, error);
|
|
196
197
|
}
|
|
197
198
|
function planViolation(plan) {
|
|
199
|
+
if (!exactRatioIsCanonical(plan.frameRate)) {
|
|
200
|
+
return "plan frame rate is not canonical";
|
|
201
|
+
}
|
|
198
202
|
if (plan.timeline.start >= plan.timeline.end) {
|
|
199
203
|
return "plan timeline interval is empty or reversed";
|
|
200
204
|
}
|
|
@@ -267,6 +271,9 @@ function planViolation(plan) {
|
|
|
267
271
|
if (!insideInterval(video.interval, shotInterval)) {
|
|
268
272
|
return "plan video interval falls outside its shot";
|
|
269
273
|
}
|
|
274
|
+
if (!videoSourceMappingIsValid(video, plan.frameRate)) {
|
|
275
|
+
return "plan video source mapping disagrees with its interval";
|
|
276
|
+
}
|
|
270
277
|
}
|
|
271
278
|
for (const overlay of plan.overlays) {
|
|
272
279
|
const nodeViolation = claimNode(overlay.node, nodeIds, authoredIds);
|
|
@@ -399,9 +406,31 @@ function snapshotVideo(video) {
|
|
|
399
406
|
shotId: video.shotId,
|
|
400
407
|
assetId: video.assetId,
|
|
401
408
|
interval: Object.freeze({ ...video.interval }),
|
|
402
|
-
|
|
409
|
+
sourceTiming: snapshotSourceTiming(video.sourceTiming),
|
|
410
|
+
source: Object.freeze({
|
|
411
|
+
...video.source,
|
|
412
|
+
playbackRate: Object.freeze({ ...video.source.playbackRate }),
|
|
413
|
+
}),
|
|
403
414
|
});
|
|
404
415
|
}
|
|
416
|
+
function snapshotSourceTiming(timing) {
|
|
417
|
+
switch (timing.kind) {
|
|
418
|
+
case "constant":
|
|
419
|
+
return Object.freeze({
|
|
420
|
+
kind: timing.kind,
|
|
421
|
+
frameRate: Object.freeze({ ...timing.frameRate }),
|
|
422
|
+
});
|
|
423
|
+
case "variable":
|
|
424
|
+
return Object.freeze({
|
|
425
|
+
kind: timing.kind,
|
|
426
|
+
timebase: Object.freeze({ ...timing.timebase }),
|
|
427
|
+
boundaries: snapshotFrameBoundaries(timing.boundaries),
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
function snapshotFrameBoundaries([first, second, third, ...rest]) {
|
|
432
|
+
return Object.freeze([first, second, third, ...rest]);
|
|
433
|
+
}
|
|
405
434
|
function snapshotOverlay(overlay) {
|
|
406
435
|
return Object.freeze({
|
|
407
436
|
node: snapshotNode(overlay.node),
|
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
import { BUNDLE_ASSET_DIRECTORY } from "./generated/bundle-layout.js";
|
|
4
4
|
import { requireReadinessTimeout } from "./resource.js";
|
|
5
5
|
import { RuntimeAdapterError } from "./session.js";
|
|
6
|
-
const FRAME_TOLERANCE_SECONDS = 0.000_001;
|
|
7
6
|
const LOAD_READINESS = {
|
|
8
7
|
event: "loadeddata",
|
|
9
8
|
failureMessage: "video data failed to load",
|
|
@@ -247,7 +246,9 @@ class StagedFrame {
|
|
|
247
246
|
}
|
|
248
247
|
matches(selection) {
|
|
249
248
|
return (selection.mediaTimeSeconds === this.#selection.mediaTimeSeconds &&
|
|
250
|
-
selection.seekTimeSeconds === this.#selection.seekTimeSeconds
|
|
249
|
+
selection.seekTimeSeconds === this.#selection.seekTimeSeconds &&
|
|
250
|
+
selection.readinessToleranceSeconds ===
|
|
251
|
+
this.#selection.readinessToleranceSeconds);
|
|
251
252
|
}
|
|
252
253
|
async confirm(timeoutMilliseconds) {
|
|
253
254
|
const observation = await observedBeforeDeadline(this.#observation, timeoutMilliseconds, this.#pendingResource);
|
|
@@ -267,7 +268,7 @@ class StagedFrame {
|
|
|
267
268
|
#inspectFrame = (_now, metadata) => {
|
|
268
269
|
this.#frameCallback = undefined;
|
|
269
270
|
const exactFrame = Math.abs(metadata.mediaTime - this.#selection.mediaTimeSeconds) <=
|
|
270
|
-
|
|
271
|
+
this.#selection.readinessToleranceSeconds;
|
|
271
272
|
if (exactFrame) {
|
|
272
273
|
this.#finish({ kind: "presented" });
|
|
273
274
|
return;
|
|
@@ -337,7 +338,11 @@ function requireSelection(selection) {
|
|
|
337
338
|
if (!Number.isFinite(selection.mediaTimeSeconds) ||
|
|
338
339
|
selection.mediaTimeSeconds < 0 ||
|
|
339
340
|
!Number.isFinite(selection.seekTimeSeconds) ||
|
|
340
|
-
selection.seekTimeSeconds
|
|
341
|
+
selection.seekTimeSeconds <= selection.mediaTimeSeconds ||
|
|
342
|
+
!Number.isFinite(selection.readinessToleranceSeconds) ||
|
|
343
|
+
selection.readinessToleranceSeconds <= 0 ||
|
|
344
|
+
selection.readinessToleranceSeconds >=
|
|
345
|
+
selection.seekTimeSeconds - selection.mediaTimeSeconds) {
|
|
341
346
|
throw new RuntimeAdapterError("operation", "video frame selection is invalid");
|
|
342
347
|
}
|
|
343
348
|
}
|