@onmark/cli 0.1.4 → 0.2.3

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.
@@ -10,4 +10,11 @@ export interface VideoFrameSelection {
10
10
  }
11
11
  /** Selects the source frame visible at one output-frame midpoint. */
12
12
  export declare function videoFrameSelection(frame: RuntimeFrame, video: RuntimeVideo, outputFrameRate: FrameRate): VideoFrameSelection | undefined;
13
+ /** Checks protocol source facts without deriving any authored timeline. */
14
+ export declare function videoSourceMappingIsValid(video: RuntimeVideo, outputFrameRate: FrameRate): boolean;
15
+ /** Checks the canonical positive-rational spelling used by Rust wire values. */
16
+ export declare function exactRatioIsCanonical(rate: {
17
+ readonly numerator: number;
18
+ readonly denominator: number;
19
+ }): boolean;
13
20
  export {};
@@ -1,28 +1,90 @@
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 sourceFrame = sourceFrameAtMidpoint(localFrame, outputFrameRate, video.sourceFrameRate);
11
+ const sourceFrame = sourceFrameAtMidpoint(localFrame, outputFrameRate, video.sourceFrameRate, video.source);
10
12
  const sourceFrameDuration = video.sourceFrameRate.denominator / video.sourceFrameRate.numerator;
11
13
  return Object.freeze({
12
14
  mediaTimeSeconds: sourceFrame * sourceFrameDuration,
13
15
  seekTimeSeconds: (sourceFrame + 0.5) * sourceFrameDuration,
14
16
  });
15
17
  }
16
- function sourceFrameAtMidpoint(localFrame, outputFrameRate, sourceFrameRate) {
17
- const numerator = (2n * BigInt(localFrame) + 1n) *
18
- BigInt(outputFrameRate.denominator) *
19
- BigInt(sourceFrameRate.numerator);
20
- const denominator = 2n *
18
+ /** Checks protocol source facts without deriving any authored timeline. */
19
+ export function videoSourceMappingIsValid(video, outputFrameRate) {
20
+ const source = video.source;
21
+ const start = BigInt(source.startNanoseconds);
22
+ const end = BigInt(source.endNanoseconds);
23
+ const naturalEnd = BigInt(source.naturalEndNanoseconds);
24
+ if (start >= end ||
25
+ end > naturalEnd ||
26
+ naturalEnd > MAX_DURATION_NANOSECONDS) {
27
+ return false;
28
+ }
29
+ if (!exactRatioIsCanonical(video.sourceFrameRate) ||
30
+ !exactRatioIsCanonical(source.playbackRate)) {
31
+ return false;
32
+ }
33
+ const sourceDuration = end - start;
34
+ const numerator = sourceDuration *
21
35
  BigInt(outputFrameRate.numerator) *
22
- BigInt(sourceFrameRate.denominator);
23
- const sourceFrame = Number(numerator / denominator);
36
+ BigInt(source.playbackRate.denominator);
37
+ const denominator = 1000000000n *
38
+ BigInt(outputFrameRate.denominator) *
39
+ BigInt(source.playbackRate.numerator);
40
+ const expectedFrames = (numerator + denominator - 1n) / denominator;
41
+ const actualFrames = BigInt(video.interval.end - video.interval.start);
42
+ return actualFrames === expectedFrames;
43
+ }
44
+ function sourceFrameAtMidpoint(localFrame, outputFrameRate, sourceFrameRate, source) {
45
+ const midpoint = 2n * BigInt(localFrame) + 1n;
46
+ const outputNumerator = BigInt(outputFrameRate.numerator);
47
+ const outputDenominator = BigInt(outputFrameRate.denominator);
48
+ const sourceNumerator = BigInt(sourceFrameRate.numerator);
49
+ const sourceDenominator = BigInt(sourceFrameRate.denominator);
50
+ const speedNumerator = BigInt(source.playbackRate.numerator);
51
+ const speedDenominator = BigInt(source.playbackRate.denominator);
52
+ const startNanoseconds = BigInt(source.startNanoseconds);
53
+ const endNanoseconds = BigInt(source.endNanoseconds);
54
+ 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.
57
+ 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);
24
69
  if (!Number.isSafeInteger(sourceFrame)) {
25
70
  throw new RangeError("selected source frame exceeds JavaScript's exact integer range");
26
71
  }
27
72
  return sourceFrame;
28
73
  }
74
+ /** Checks the canonical positive-rational spelling used by Rust wire values. */
75
+ export function exactRatioIsCanonical(rate) {
76
+ if (!Number.isInteger(rate.numerator) ||
77
+ !Number.isInteger(rate.denominator) ||
78
+ rate.numerator < 1 ||
79
+ rate.denominator < 1 ||
80
+ rate.numerator > MAX_RATIO_PART ||
81
+ rate.denominator > MAX_RATIO_PART) {
82
+ return false;
83
+ }
84
+ let left = BigInt(rate.numerator);
85
+ let right = BigInt(rate.denominator);
86
+ while (right !== 0n) {
87
+ [left, right] = [right, left % right];
88
+ }
89
+ return left === 1n;
90
+ }
@@ -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: 1, requestId, event });
160
+ return decodeBrowserResponse({ version: 2, 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);
@@ -400,6 +407,10 @@ function snapshotVideo(video) {
400
407
  assetId: video.assetId,
401
408
  interval: Object.freeze({ ...video.interval }),
402
409
  sourceFrameRate: Object.freeze({ ...video.sourceFrameRate }),
410
+ source: Object.freeze({
411
+ ...video.source,
412
+ playbackRate: Object.freeze({ ...video.source.playbackRate }),
413
+ }),
403
414
  });
404
415
  }
405
416
  function snapshotOverlay(overlay) {