@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.
@@ -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 sourceFrame = sourceFrameAtMidpoint(localFrame, outputFrameRate, video.sourceFrameRate, video.source);
12
- const sourceFrameDuration = video.sourceFrameRate.denominator / video.sourceFrameRate.numerator;
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(video.sourceFrameRate) ||
30
- !exactRatioIsCanonical(source.playbackRate)) {
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 numerator = sourceDuration *
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.denominator);
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 sourceFrameAtMidpoint(localFrame, outputFrameRate, sourceFrameRate, source) {
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 affine source-time mapping. This integer projection samples
56
- // the output-frame midpoint without reconstructing a browser timeline.
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 sourceTimeNumerator = startNanoseconds * timeDenominator +
59
- midpoint * outputDenominator * speedNumerator * nanosecondsPerSecond;
60
- const frameNumerator = sourceTimeNumerator * sourceNumerator;
61
- const denominator = timeDenominator * nanosecondsPerSecond * sourceDenominator;
62
- const selectedFrame = frameNumerator / denominator;
63
- const sourceEndNumerator = endNanoseconds * sourceNumerator;
64
- const sourceFrameDenominator = nanosecondsPerSecond * sourceDenominator;
65
- const exclusiveEndFrame = (sourceEndNumerator + sourceFrameDenominator - 1n) / sourceFrameDenominator;
66
- // Ceil-rounded output may expose one midpoint beyond the trim edge. Keep
67
- // that final sample inside the last source frame intersecting the interval.
68
- const sourceFrame = Number(selectedFrame < exclusiveEndFrame ? selectedFrame : exclusiveEndFrame - 1n);
69
- if (!Number.isSafeInteger(sourceFrame)) {
70
- throw new RangeError("selected source frame exceeds JavaScript's exact integer range");
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
- return sourceFrame;
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: 2, requestId, event });
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
- sourceFrameRate: Object.freeze({ ...video.sourceFrameRate }),
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
- FRAME_TOLERANCE_SECONDS;
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 < selection.mediaTimeSeconds) {
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
  }