@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.
- package/README.md +25 -2
- package/onmark-release.json +19 -19
- 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 +19 -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 +451 -102
- package/packages/runtime/dist/src/media.d.ts +7 -0
- package/packages/runtime/dist/src/media.js +70 -8
- package/packages/runtime/dist/src/session.js +12 -1
|
@@ -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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
const
|
|
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(
|
|
23
|
-
const
|
|
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:
|
|
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) {
|