@onmark/cli 0.2.3 → 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 +21 -21
- package/package.json +4 -4
- package/packages/runtime/dist/src/generated/browser-request.d.ts +35 -2
- 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 +779 -311
- package/packages/runtime/dist/src/index.d.ts +1 -1
- package/packages/runtime/dist/src/media.d.ts +2 -0
- package/packages/runtime/dist/src/media.js +141 -29
- package/packages/runtime/dist/src/session.js +20 -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,6 +7,8 @@ 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;
|
|
@@ -8,12 +8,8 @@ export function videoFrameSelection(frame, video, outputFrameRate) {
|
|
|
8
8
|
return undefined;
|
|
9
9
|
}
|
|
10
10
|
const localFrame = frame.index - video.interval.start;
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
return Object.freeze({
|
|
14
|
-
mediaTimeSeconds: sourceFrame * sourceFrameDuration,
|
|
15
|
-
seekTimeSeconds: (sourceFrame + 0.5) * sourceFrameDuration,
|
|
16
|
-
});
|
|
11
|
+
const sourceTime = sourceTimeAtMidpoint(localFrame, outputFrameRate, video.source);
|
|
12
|
+
return selectSourceFrame(sourceTime, video.sourceTiming, video.source);
|
|
17
13
|
}
|
|
18
14
|
/** Checks protocol source facts without deriving any authored timeline. */
|
|
19
15
|
export function videoSourceMappingIsValid(video, outputFrameRate) {
|
|
@@ -26,14 +22,23 @@ export function videoSourceMappingIsValid(video, outputFrameRate) {
|
|
|
26
22
|
naturalEnd > MAX_DURATION_NANOSECONDS) {
|
|
27
23
|
return false;
|
|
28
24
|
}
|
|
29
|
-
if (!exactRatioIsCanonical(
|
|
30
|
-
!
|
|
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
31
|
return false;
|
|
32
32
|
}
|
|
33
33
|
const sourceDuration = end - start;
|
|
34
|
-
const
|
|
34
|
+
const playbackNumerator = sourceDuration *
|
|
35
|
+
BigInt(outputFrameRate.numerator) *
|
|
36
|
+
BigInt(source.playbackRate.denominator) *
|
|
37
|
+
BigInt(source.plays);
|
|
38
|
+
const holdNumerator = BigInt(source.holdLastNanoseconds) *
|
|
35
39
|
BigInt(outputFrameRate.numerator) *
|
|
36
|
-
BigInt(source.playbackRate.
|
|
40
|
+
BigInt(source.playbackRate.numerator);
|
|
41
|
+
const numerator = playbackNumerator + holdNumerator;
|
|
37
42
|
const denominator = 1000000000n *
|
|
38
43
|
BigInt(outputFrameRate.denominator) *
|
|
39
44
|
BigInt(source.playbackRate.numerator);
|
|
@@ -41,35 +46,142 @@ export function videoSourceMappingIsValid(video, outputFrameRate) {
|
|
|
41
46
|
const actualFrames = BigInt(video.interval.end - video.interval.start);
|
|
42
47
|
return actualFrames === expectedFrames;
|
|
43
48
|
}
|
|
44
|
-
function
|
|
49
|
+
function sourceTimeAtMidpoint(localFrame, outputFrameRate, source) {
|
|
45
50
|
const midpoint = 2n * BigInt(localFrame) + 1n;
|
|
46
51
|
const outputNumerator = BigInt(outputFrameRate.numerator);
|
|
47
52
|
const outputDenominator = BigInt(outputFrameRate.denominator);
|
|
48
|
-
const sourceNumerator = BigInt(sourceFrameRate.numerator);
|
|
49
|
-
const sourceDenominator = BigInt(sourceFrameRate.denominator);
|
|
50
53
|
const speedNumerator = BigInt(source.playbackRate.numerator);
|
|
51
54
|
const speedDenominator = BigInt(source.playbackRate.denominator);
|
|
52
55
|
const startNanoseconds = BigInt(source.startNanoseconds);
|
|
53
56
|
const endNanoseconds = BigInt(source.endNanoseconds);
|
|
57
|
+
const plays = BigInt(source.plays);
|
|
54
58
|
const nanosecondsPerSecond = 1000000000n;
|
|
55
|
-
// Rust owns the
|
|
56
|
-
//
|
|
59
|
+
// Rust owns the source treatment. This integer projection samples one exact
|
|
60
|
+
// pass without reconstructing a browser timeline.
|
|
57
61
|
const timeDenominator = 2n * outputNumerator * speedDenominator;
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
const
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
+
}
|
|
123
|
+
return Object.freeze({
|
|
124
|
+
mediaTimeSeconds,
|
|
125
|
+
seekTimeSeconds,
|
|
126
|
+
readinessToleranceSeconds,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
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;
|
|
71
175
|
}
|
|
72
|
-
|
|
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;
|
|
73
185
|
}
|
|
74
186
|
/** Checks the canonical positive-rational spelling used by Rust wire values. */
|
|
75
187
|
export function exactRatioIsCanonical(rate) {
|
|
@@ -157,7 +157,7 @@ export class RuntimeSession {
|
|
|
157
157
|
}
|
|
158
158
|
}
|
|
159
159
|
function response(requestId, event) {
|
|
160
|
-
return decodeBrowserResponse({ version:
|
|
160
|
+
return decodeBrowserResponse({ version: 3, requestId, event });
|
|
161
161
|
}
|
|
162
162
|
function invalidRequest(requestId, message) {
|
|
163
163
|
return response(requestId, {
|
|
@@ -406,13 +406,31 @@ function snapshotVideo(video) {
|
|
|
406
406
|
shotId: video.shotId,
|
|
407
407
|
assetId: video.assetId,
|
|
408
408
|
interval: Object.freeze({ ...video.interval }),
|
|
409
|
-
|
|
409
|
+
sourceTiming: snapshotSourceTiming(video.sourceTiming),
|
|
410
410
|
source: Object.freeze({
|
|
411
411
|
...video.source,
|
|
412
412
|
playbackRate: Object.freeze({ ...video.source.playbackRate }),
|
|
413
413
|
}),
|
|
414
414
|
});
|
|
415
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
|
+
}
|
|
416
434
|
function snapshotOverlay(overlay) {
|
|
417
435
|
return Object.freeze({
|
|
418
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
|
}
|