@onmark/cli 0.4.0 → 0.5.1

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 CHANGED
@@ -84,10 +84,16 @@ Validate or inspect the same production plan without launching Chromium:
84
84
  ```bash
85
85
  onmark check film.html
86
86
  onmark inspect film.html --json
87
+ onmark snapshot film.html --frame 42
87
88
  onmark doctor
88
89
  onmark benchmark film.html --runs 3 --json
89
90
  ```
90
91
 
92
+ `snapshot` writes a lossless production frame to
93
+ `renders/film-frame-42.png`. It uses the same render region, browser/native
94
+ path, and verified pixel contract as the complete film; it is not a preview
95
+ approximation.
96
+
91
97
  Use the default opaque H.264/AAC MP4 for delivery, or select the
92
98
  alpha-preserving ProRes 4444/PCM MOV profile by filename:
93
99
 
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
3
  "packageName": "@onmark/cli",
4
- "version": "0.4.0",
5
- "sourceRevision": "2e7e548ab5e309264477239e8c7ca1f4c15cc462",
4
+ "version": "0.5.1",
5
+ "sourceRevision": "53370ac4510c1ac0868f284b279e6fe202518924",
6
6
  "files": [
7
7
  {
8
8
  "path": "LICENSE",
@@ -11,13 +11,13 @@
11
11
  },
12
12
  {
13
13
  "path": "README.md",
14
- "bytes": 6341,
15
- "sha256": "a747c29224c33dd8431506467134f1b86e54bad92720c0bf3b751f0cba7d6d5d"
14
+ "bytes": 6595,
15
+ "sha256": "d65847637d6a2f0c2b8a39a237f3bf14fcc3598815d9befb340e5151f97fc5cb"
16
16
  },
17
17
  {
18
18
  "path": "package.json",
19
19
  "bytes": 1406,
20
- "sha256": "0b8c9e832be7f92fdf4a635aba18306806fc63a723f0f8cc320a9d33ba2c7bc8"
20
+ "sha256": "09a11a05e0f83979d3adde92cd5f658c5a530d3c95abd819c7db6e69892f6c59"
21
21
  },
22
22
  {
23
23
  "path": "packages/authoring/dist/src/frame-motion.d.ts",
@@ -66,8 +66,8 @@
66
66
  },
67
67
  {
68
68
  "path": "packages/authoring/dist/src/presentation.js",
69
- "bytes": 10584,
70
- "sha256": "4d4ec19ded892ea0372ad76c18cb6e2175b172300164d030f4c26fdc1bfde7bb"
69
+ "bytes": 14927,
70
+ "sha256": "0a47ca310dce4c37db3a241cb63301d504aa8d767063a395aa520d519e14a874"
71
71
  },
72
72
  {
73
73
  "path": "packages/authoring/dist/src/resource.d.ts",
@@ -101,13 +101,13 @@
101
101
  },
102
102
  {
103
103
  "path": "packages/bundler/dist/src/authored_html.d.ts",
104
- "bytes": 2117,
105
- "sha256": "02f8a7256bd35bbb8aaffe1d2a56fbe901aa32f1e3ae383e311696bc14b393bd"
104
+ "bytes": 2278,
105
+ "sha256": "7b40cb7d424b01b0543caf97ac9ed50ca68d94ddf969de7219b5c9c9a29f674c"
106
106
  },
107
107
  {
108
108
  "path": "packages/bundler/dist/src/authored_html.js",
109
- "bytes": 15985,
110
- "sha256": "485c622b295bc7b33dba2af2ca98800ed0e8a691baa7fef22cfd9ccb470be1ec"
109
+ "bytes": 17153,
110
+ "sha256": "841d2fc962b65d7f5cfafd4805eac28990d76f15cf298173ca1d7ac424a2480e"
111
111
  },
112
112
  {
113
113
  "path": "packages/bundler/dist/src/bundle_projection.d.ts",
@@ -126,18 +126,18 @@
126
126
  },
127
127
  {
128
128
  "path": "packages/bundler/dist/src/command.js",
129
- "bytes": 5931,
130
- "sha256": "03292a60d8989e5abbf17a34dc46736d1549689963a73e9f2cb26c531dedee1c"
129
+ "bytes": 6086,
130
+ "sha256": "39a4302c67457825502e2ad525309f3cf26ebb2bf2f957aa4b5d36bd391b53ab"
131
131
  },
132
132
  {
133
133
  "path": "packages/bundler/dist/src/generated/bundle-manifest.d.ts",
134
- "bytes": 2515,
135
- "sha256": "8aa6f33d79f9269da7d5d766d69f1d1037aee3141d76d380ea301b8b6c0d5002"
134
+ "bytes": 2500,
135
+ "sha256": "8599570ed61765e2f6e167c637f8ca7b2d83bf1c8fc44242c62aa0e2178ec290"
136
136
  },
137
137
  {
138
138
  "path": "packages/bundler/dist/src/generated/bundle-manifest.js",
139
- "bytes": 621,
140
- "sha256": "d3f7d430ebbc045db575d648e61f459848bac3c936acf9a6ec5f825260ef2458"
139
+ "bytes": 646,
140
+ "sha256": "49a7197dfca225e5ab1a296562b6dd9cbf50bd101eefd8650994085b367a50c0"
141
141
  },
142
142
  {
143
143
  "path": "packages/bundler/dist/src/generated/bundle-projection-validator.d.ts",
@@ -191,13 +191,13 @@
191
191
  },
192
192
  {
193
193
  "path": "packages/bundler/dist/src/presentation.d.ts",
194
- "bytes": 2311,
195
- "sha256": "50dea00d649ef252a7529e36f0e9f4d829022d38064ed64693dc621ff4c64fcd"
194
+ "bytes": 2312,
195
+ "sha256": "b15d3a4c69baccfd6ed56628348dfba180ea698f77993b20b4a249572d7e0cb8"
196
196
  },
197
197
  {
198
198
  "path": "packages/bundler/dist/src/presentation.js",
199
- "bytes": 20046,
200
- "sha256": "053c22902c3d758e9dbd27857be910c8f6b982001d7924833f077d237c83eb43"
199
+ "bytes": 20567,
200
+ "sha256": "62524b69029d27972163c19d3bb87832dbb7c1cb6e4b465715f4375e2ed84fca"
201
201
  },
202
202
  {
203
203
  "path": "packages/launcher/dist/desktop-release.json",
@@ -306,8 +306,8 @@
306
306
  },
307
307
  {
308
308
  "path": "packages/runtime/dist/src/generated/browser-request.d.ts",
309
- "bytes": 4883,
310
- "sha256": "15861c41b4713352e6b75bdf71c4ad165d8084d9f1031db58b563f984d618f0b"
309
+ "bytes": 5010,
310
+ "sha256": "9bd56a97a68aa2a48c4ef94c104a77e18d4b6b95e4444d76fb461468f54640ce"
311
311
  },
312
312
  {
313
313
  "path": "packages/runtime/dist/src/generated/browser-request.js",
@@ -316,8 +316,8 @@
316
316
  },
317
317
  {
318
318
  "path": "packages/runtime/dist/src/generated/browser-response.d.ts",
319
- "bytes": 1368,
320
- "sha256": "adfd72031e58b5774ebdd62578d2f3bd17009d16bf1881d48330bc42689f77fc"
319
+ "bytes": 2540,
320
+ "sha256": "f0345c35a41f7f09961fa7df181a30d991116769e9ecb28dd0627e76a0a20034"
321
321
  },
322
322
  {
323
323
  "path": "packages/runtime/dist/src/generated/browser-response.js",
@@ -326,13 +326,13 @@
326
326
  },
327
327
  {
328
328
  "path": "packages/runtime/dist/src/generated/bundle-layout.d.ts",
329
- "bytes": 639,
330
- "sha256": "1e5aee7947ce8d4c09de4e05a7e729b31d868c47640963cfe120b1ebb1af69ca"
329
+ "bytes": 660,
330
+ "sha256": "213d9166593e39cedf85489b1415834fe95b6d6b95ec00ef0a56c9f10a37c736"
331
331
  },
332
332
  {
333
333
  "path": "packages/runtime/dist/src/generated/bundle-layout.js",
334
- "bytes": 455,
335
- "sha256": "1807eab2041ee935b3bde12f12402b06d0dd3328e3fb6cb9fc0282699b311f9e"
334
+ "bytes": 480,
335
+ "sha256": "f22f9910f25b67d0953fca16488b97531dcefb273938c7ed0fcf9d04552be90e"
336
336
  },
337
337
  {
338
338
  "path": "packages/runtime/dist/src/generated/codec.d.ts",
@@ -346,13 +346,13 @@
346
346
  },
347
347
  {
348
348
  "path": "packages/runtime/dist/src/generated/runtime-contract.d.ts",
349
- "bytes": 496,
350
- "sha256": "4595ac4a096d79a3f153db88a4e19fd20be23db896b83e54527b26605e2f214c"
349
+ "bytes": 609,
350
+ "sha256": "ba1fac50be3c9494935fbc5d31c1e3037934e2d6dd1ca3adfe3d692575f68d27"
351
351
  },
352
352
  {
353
353
  "path": "packages/runtime/dist/src/generated/runtime-contract.js",
354
- "bytes": 505,
355
- "sha256": "48aff07bb36ec9f8edad25f597b985ff6a5dea9d44fbd2458e63cc4957026ece"
354
+ "bytes": 604,
355
+ "sha256": "3cc8116cc4d37c92643c672ef313bed7aa32ac466aeb8724e9563548a6d981ce"
356
356
  },
357
357
  {
358
358
  "path": "packages/runtime/dist/src/generated/validators.d.ts",
@@ -361,8 +361,8 @@
361
361
  },
362
362
  {
363
363
  "path": "packages/runtime/dist/src/generated/validators.js",
364
- "bytes": 234626,
365
- "sha256": "fdfb7d5ebac40c9845c8998dda53d4d231f8affbb1e3a2a94868415b0862351b"
364
+ "bytes": 274203,
365
+ "sha256": "8e1309412309eeda69cd27a8b3a88935cdb8a8b61d0e6776a0a4ac472039e930"
366
366
  },
367
367
  {
368
368
  "path": "packages/runtime/dist/src/host.d.ts",
@@ -376,13 +376,13 @@
376
376
  },
377
377
  {
378
378
  "path": "packages/runtime/dist/src/index.d.ts",
379
- "bytes": 1770,
380
- "sha256": "de8c076c1aea98cb88bd49f90acd38b882e0240334b88174be53e6085c2ae1e8"
379
+ "bytes": 1895,
380
+ "sha256": "5faebc8e4726ec5261e174a55811536991ae0cf42135e0f43e5fb67e9df77eba"
381
381
  },
382
382
  {
383
383
  "path": "packages/runtime/dist/src/index.js",
384
- "bytes": 1094,
385
- "sha256": "146654c7bdc89e1e1815545af22577744eeb00ec95106f81cb13a41a87690938"
384
+ "bytes": 1152,
385
+ "sha256": "10bb00b68b1aba0106334606c72a68227c2dbc9f997aaf99afd14c0502b18872"
386
386
  },
387
387
  {
388
388
  "path": "packages/runtime/dist/src/media.d.ts",
@@ -396,13 +396,13 @@
396
396
  },
397
397
  {
398
398
  "path": "packages/runtime/dist/src/presentation.d.ts",
399
- "bytes": 3560,
400
- "sha256": "8a27e87ae90d27f2fd7ab20b4048e88f344b59262ac47c5e7ba19bfda8d1dffc"
399
+ "bytes": 3943,
400
+ "sha256": "9f2abf95bc614f0a41a42c52f2563574afa24b126c1b266983cc72e1a0f83ee1"
401
401
  },
402
402
  {
403
403
  "path": "packages/runtime/dist/src/presentation.js",
404
- "bytes": 14443,
405
- "sha256": "4052fe152744acc35981ba60c68822750187a28728dd26312a9b789714e60251"
404
+ "bytes": 17755,
405
+ "sha256": "de785ba109f6cf446d211cb05387f5d42d406a61f9cb9c5e38925fbd9c4bccbe"
406
406
  },
407
407
  {
408
408
  "path": "packages/runtime/dist/src/resource.d.ts",
@@ -416,18 +416,18 @@
416
416
  },
417
417
  {
418
418
  "path": "packages/runtime/dist/src/session.d.ts",
419
- "bytes": 2057,
420
- "sha256": "628ebe74134f71f4d05642c8bf878ae89da57360e3eca34b0317e2f7d371a88c"
419
+ "bytes": 2298,
420
+ "sha256": "28b12b59ac2e042ae6e571f861b5b70dec0779a08b6ad1cf8897b262bdbc51db"
421
421
  },
422
422
  {
423
423
  "path": "packages/runtime/dist/src/session.js",
424
- "bytes": 20157,
425
- "sha256": "d2c7cc8cf073af64ff5dda45bdec3b3aca8bb8e693243ad037c84dce0165eaa6"
424
+ "bytes": 20533,
425
+ "sha256": "6383f8ff8fda51e0c51b5c20d736c45bf9d36cc76dfb41bb87588ff0b72654b6"
426
426
  },
427
427
  {
428
428
  "path": "packages/runtime/dist/src/types.d.ts",
429
- "bytes": 636,
430
- "sha256": "eb75c5eacb50cc83d5bf4b27a3ebc904c4af4a33113d0aa2772dfb54493757f8"
429
+ "bytes": 753,
430
+ "sha256": "20d1e1c41b5ed7ac43341b928a6ba331dd73fcf2fea2fe8534f0c523233358ff"
431
431
  },
432
432
  {
433
433
  "path": "packages/runtime/dist/src/types.js",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onmark/cli",
3
- "version": "0.4.0",
3
+ "version": "0.5.1",
4
4
  "description": "Screenplay-first deterministic browser video compiler",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -45,8 +45,8 @@
45
45
  "yauzl": "3.4.0"
46
46
  },
47
47
  "optionalDependencies": {
48
- "@onmark/cli-darwin-arm64": "0.4.0",
49
- "@onmark/cli-linux-x64": "0.4.0",
50
- "@onmark/cli-win32-x64": "0.4.0"
48
+ "@onmark/cli-darwin-arm64": "0.5.1",
49
+ "@onmark/cli-linux-x64": "0.5.1",
50
+ "@onmark/cli-win32-x64": "0.5.1"
51
51
  }
52
52
  }
@@ -90,15 +90,10 @@ class AuthoredDocument {
90
90
  }
91
91
  bindVideo(placement) {
92
92
  const element = requiredNode(this.#nodeIndex(), placement.node, "video", ELEMENTS.video);
93
- const bound = bindElement(element, placement.node);
94
93
  element.muted = true;
95
94
  element.playsInline = true;
96
95
  this.#record("video", element, placement.node, placement.interval);
97
- return {
98
- ...bound,
99
- element,
100
- source: this.#videoSource(placement),
101
- };
96
+ return bindVideoElement(element, placement.node, this.#videoSource(placement));
102
97
  }
103
98
  bindTransition(placement) {
104
99
  const nodes = this.#nodeIndex();
@@ -238,6 +233,132 @@ function bindElement(element, node, release) {
238
233
  },
239
234
  };
240
235
  }
236
+ function bindVideoElement(element, node, source) {
237
+ const bound = bindElement(element, node);
238
+ const authoredVisibility = element.style.visibility;
239
+ let layoutVisible = false;
240
+ return {
241
+ element,
242
+ source,
243
+ setVisible: bound.setVisible,
244
+ setLayoutVisible(visible) {
245
+ if (visible) {
246
+ element.style.visibility = "hidden";
247
+ bound.setVisible(true);
248
+ }
249
+ else {
250
+ bound.setVisible(false);
251
+ element.style.visibility = authoredVisibility;
252
+ }
253
+ layoutVisible = visible;
254
+ },
255
+ measureLayout() {
256
+ if (!layoutVisible) {
257
+ throw new Error("video layout requires a visible layout-only element");
258
+ }
259
+ return measureVideoLayout(element, node.nodeId);
260
+ },
261
+ dispose() {
262
+ bound.setVisible(false);
263
+ element.style.visibility = authoredVisibility;
264
+ bound.dispose();
265
+ },
266
+ };
267
+ }
268
+ function measureVideoLayout(element, nodeId) {
269
+ requireStaticVideoStyle(element);
270
+ const rectangle = pixelRectangle(element.getBoundingClientRect());
271
+ const style = getComputedStyle(element);
272
+ return Object.freeze({
273
+ nodeId,
274
+ objectFit: objectFit(style.objectFit),
275
+ objectPosition: objectPosition(style.objectPosition),
276
+ rectangle,
277
+ });
278
+ }
279
+ function requireStaticVideoStyle(element) {
280
+ const style = getComputedStyle(element);
281
+ const unsupported = [
282
+ ["border-radius", style.borderRadius, "0px"],
283
+ ["filter", style.filter, "none"],
284
+ ["mix-blend-mode", style.mixBlendMode, "normal"],
285
+ ["opacity", style.opacity, "1"],
286
+ ["transform", style.transform, "none"],
287
+ ];
288
+ for (const [name, actual, expected] of unsupported) {
289
+ if (actual !== expected) {
290
+ throw new Error(`layout-only video requires ${name}: ${expected}, found ${actual}`);
291
+ }
292
+ }
293
+ for (const name of [
294
+ "borderBottomWidth",
295
+ "borderLeftWidth",
296
+ "borderRightWidth",
297
+ "borderTopWidth",
298
+ "paddingBottom",
299
+ "paddingLeft",
300
+ "paddingRight",
301
+ "paddingTop",
302
+ ]) {
303
+ if (style[name] !== "0px") {
304
+ throw new Error(`layout-only video requires ${cssName(name)}: 0`);
305
+ }
306
+ }
307
+ if (style.clipPath !== "none") {
308
+ throw new Error("layout-only video requires clip-path: none");
309
+ }
310
+ }
311
+ function pixelRectangle(rectangle) {
312
+ const values = [rectangle.x, rectangle.y, rectangle.width, rectangle.height];
313
+ if (devicePixelRatio !== 1 ||
314
+ !values.every(Number.isSafeInteger) ||
315
+ rectangle.x < 0 ||
316
+ rectangle.y < 0 ||
317
+ rectangle.width <= 0 ||
318
+ rectangle.height <= 0 ||
319
+ rectangle.right > document.documentElement.clientWidth ||
320
+ rectangle.bottom > document.documentElement.clientHeight) {
321
+ throw new Error("layout-only video requires a positive, pixel-aligned viewport rectangle");
322
+ }
323
+ return Object.freeze({
324
+ height: rectangle.height,
325
+ width: rectangle.width,
326
+ x: rectangle.x,
327
+ y: rectangle.y,
328
+ });
329
+ }
330
+ function objectFit(value) {
331
+ if (value === "fill" || value === "contain" || value === "cover") {
332
+ return value;
333
+ }
334
+ throw new Error(`layout-only video does not support object-fit: ${value}`);
335
+ }
336
+ function objectPosition(value) {
337
+ const [x, y, ...rest] = value.trim().split(/\s+/u);
338
+ if (x === undefined || y === undefined || rest.length > 0) {
339
+ throw new Error("layout-only video requires two percentage positions");
340
+ }
341
+ return Object.freeze({
342
+ x: percentageMillionths(x),
343
+ y: percentageMillionths(y),
344
+ });
345
+ }
346
+ function percentageMillionths(value) {
347
+ const match = /^(?<whole>[0-9]{1,3})(?:\.(?<fraction>[0-9]{1,4}))?%$/u.exec(value);
348
+ if (match?.groups === undefined) {
349
+ throw new Error("layout-only video object-position must use exact percentages");
350
+ }
351
+ const whole = Number(match.groups["whole"]);
352
+ const fraction = Number((match.groups["fraction"] ?? "").padEnd(4, "0"));
353
+ const millionths = whole * 10_000 + fraction;
354
+ if (millionths > 1_000_000) {
355
+ throw new Error("layout-only video object-position must lie between 0% and 100%");
356
+ }
357
+ return millionths;
358
+ }
359
+ function cssName(value) {
360
+ return value.replace(/[A-Z]/gu, (character) => `-${character.toLowerCase()}`);
361
+ }
241
362
  function requireAuthoredId(element, node) {
242
363
  const expected = node.authoredId ?? "";
243
364
  if (element.id !== expected) {
@@ -1,8 +1,10 @@
1
+ import type { PresentationVisualCapability } from "../../../runtime/dist/src/index.js";
1
2
  import { type HtmlImageResource } from "./html_image.js";
2
3
  /** Authored document bytes and optional inline module prepared for esbuild. */
3
4
  export interface AuthoredHtml {
4
5
  readonly document: string;
5
6
  readonly motion: string | undefined;
7
+ readonly visualCapability: PresentationVisualCapability | undefined;
6
8
  readonly resources: readonly HtmlImageResource[];
7
9
  readonly shots: readonly AuthoredHtmlShot[];
8
10
  readonly regionStructure: AuthoredHtmlRegionStructure;
@@ -6,6 +6,7 @@ import { parse } from "parse5";
6
6
  import { freezeHtmlImages } from "./html_image.js";
7
7
  const MAX_HTML_BYTES = 8 * 1024 * 1024;
8
8
  const MOTION_ATTRIBUTE = "data-om-motion";
9
+ const VISUAL_CAPABILITY_META = "onmark:visual-capability";
9
10
  const RUNTIME_SCRIPT = '<script type="module" src="./presentation.js"></script>';
10
11
  /** Invalid or unreadable authored HTML at the Node bundling boundary. */
11
12
  export class AuthoredHtmlError extends Error {
@@ -18,6 +19,7 @@ export class AuthoredHtmlError extends Error {
18
19
  export async function readAuthoredHtml(path, maxOutputBytes, resolveDirectory) {
19
20
  const absolute = resolve(path);
20
21
  const source = await readBoundedSource(absolute);
22
+ const visualCapability = declaredVisualCapability(parseDocument(source));
21
23
  const directory = resolveDirectory === undefined
22
24
  ? dirname(absolute)
23
25
  : resolve(resolveDirectory);
@@ -28,6 +30,7 @@ export async function readAuthoredHtml(path, maxOutputBytes, resolveDirectory) {
28
30
  ...extracted,
29
31
  resources: frozen.resources,
30
32
  resolveDirectory: directory,
33
+ visualCapability,
31
34
  });
32
35
  }
33
36
  // ── Bounded input
@@ -267,6 +270,33 @@ function collectScripts(node) {
267
270
  });
268
271
  return scripts;
269
272
  }
273
+ function declaredVisualCapability(document) {
274
+ const declarations = [];
275
+ visit(document, (element) => {
276
+ if (element.tagName !== "meta") {
277
+ return;
278
+ }
279
+ const attributes = new Map(element.attrs.map(({ name, value }) => [name, value]));
280
+ if (attributes.get("name") === VISUAL_CAPABILITY_META) {
281
+ declarations.push(attributes.get("content") ?? "");
282
+ }
283
+ });
284
+ if (declarations.length > 1) {
285
+ throw new AuthoredHtmlError(`authored HTML may contain at most one ${VISUAL_CAPABILITY_META} declaration`);
286
+ }
287
+ const [declaration] = declarations;
288
+ if (declaration === undefined) {
289
+ return undefined;
290
+ }
291
+ switch (declaration) {
292
+ case "browserComposite":
293
+ case "separableBackdrop":
294
+ case "separableOverlay":
295
+ return declaration;
296
+ default:
297
+ throw new AuthoredHtmlError(`${VISUAL_CAPABILITY_META} must be browserComposite, separableBackdrop, or separableOverlay`);
298
+ }
299
+ }
270
300
  function findElement(node, name) {
271
301
  const pending = [node];
272
302
  while (pending.length > 0) {
@@ -14,7 +14,7 @@ const USAGE = [
14
14
  " --max-output-bytes <bytes>",
15
15
  " --frame-behavior <perFrame|placementBounded>",
16
16
  " --temporal-capability <sequential|randomAccess>",
17
- " --visual-capability <browserComposite|separableOverlay>",
17
+ " [--visual-capability <browserComposite|separableBackdrop|separableOverlay>]",
18
18
  "",
19
19
  ].join("\n");
20
20
  // ── Execution ──
@@ -45,12 +45,12 @@ async function parseArguments(arguments_) {
45
45
  const maxOutputBytes = parseByteLimit(oneValue(values["max-output-bytes"], "--max-output-bytes"));
46
46
  const frameBehavior = parseFrameBehavior(oneValue(values["frame-behavior"], "--frame-behavior"));
47
47
  const temporalCapability = parseTemporalCapability(oneValue(values["temporal-capability"], "--temporal-capability"));
48
- const visualCapability = parseVisualCapability(oneValue(values["visual-capability"], "--visual-capability"));
48
+ const visualCapability = optionalVisualCapability(values["visual-capability"]);
49
49
  const controls = {
50
50
  frameBehavior,
51
51
  maxOutputBytes,
52
52
  outputDirectory,
53
- visualCapability,
53
+ ...visualCapability,
54
54
  };
55
55
  const common = {
56
56
  ...controls,
@@ -107,12 +107,16 @@ function parseTemporalCapability(value) {
107
107
  }
108
108
  throw configuration("--temporal-capability must be sequential or randomAccess");
109
109
  }
110
- function parseVisualCapability(value) {
110
+ function optionalVisualCapability(values) {
111
+ if (values === undefined) {
112
+ return {};
113
+ }
114
+ const value = oneValue(values, "--visual-capability");
111
115
  const capability = BUNDLE_VISUAL_CAPABILITIES.find((candidate) => candidate === value);
112
116
  if (capability !== undefined) {
113
- return capability;
117
+ return { visualCapability: capability };
114
118
  }
115
- throw configuration("--visual-capability must be browserComposite or separableOverlay");
119
+ throw configuration("--visual-capability must be browserComposite, separableBackdrop, or separableOverlay");
116
120
  }
117
121
  function parseFrameBehavior(value) {
118
122
  const behavior = BUNDLE_FRAME_BEHAVIORS.find((candidate) => candidate === value);
@@ -25,11 +25,10 @@ export type BundleVersion = 1;
25
25
  /**
26
26
  * Proven relationship between browser presentation pixels and primary media.
27
27
  *
28
- * Unknown presentation code requires `BrowserComposite`. A separable overlay
29
- * is an explicit claim that the browser output is a transparent foreground
30
- * independent of the primary video beneath it.
28
+ * Unknown presentation code requires `BrowserComposite`. A separable layer
29
+ * explicitly limits browser ownership to one side of native media.
31
30
  */
32
- export type PresentationVisualCapability = "browserComposite" | "separableOverlay";
31
+ export type PresentationVisualCapability = "browserComposite" | "separableBackdrop" | "separableOverlay";
33
32
  /**
34
33
  * Stable description of one immutable presentation artifact.
35
34
  */
@@ -57,7 +56,7 @@ export interface BundleFile {
57
56
  }
58
57
  export declare const BUNDLE_VERSION: 1;
59
58
  export declare const BUNDLE_TEMPORAL_CAPABILITIES: readonly ["sequential", "randomAccess"];
60
- export declare const BUNDLE_VISUAL_CAPABILITIES: readonly ["browserComposite", "separableOverlay"];
59
+ export declare const BUNDLE_VISUAL_CAPABILITIES: readonly ["browserComposite", "separableBackdrop", "separableOverlay"];
61
60
  export declare const BUNDLE_FRAME_BEHAVIORS: readonly ["perFrame", "placementBounded"];
62
61
  export declare const BUNDLE_ENTRY_POINT: "index.html";
63
62
  export declare const BUNDLE_MANIFEST_FILE: "manifest.json";
@@ -6,6 +6,7 @@ export const BUNDLE_TEMPORAL_CAPABILITIES = Object.freeze([
6
6
  ]);
7
7
  export const BUNDLE_VISUAL_CAPABILITIES = Object.freeze([
8
8
  "browserComposite",
9
+ "separableBackdrop",
9
10
  "separableOverlay",
10
11
  ]);
11
12
  export const BUNDLE_FRAME_BEHAVIORS = Object.freeze([
@@ -15,7 +15,7 @@ interface BundleControls {
15
15
  readonly resolveDirectory?: string;
16
16
  readonly outputDirectory: string;
17
17
  readonly maxOutputBytes: number;
18
- readonly visualCapability: PresentationVisualCapability;
18
+ readonly visualCapability?: PresentationVisualCapability;
19
19
  readonly frameBehavior: PresentationFrameBehavior;
20
20
  }
21
21
  /** Explicit inputs for one authored HTML document. */
@@ -64,6 +64,7 @@ export async function bundlePresentation(options) {
64
64
  return bundle(Object.freeze({
65
65
  ...controls,
66
66
  html: Object.freeze({ ...html }),
67
+ visualCapability: resolveVisualCapability(controls.visualCapability, html.visualCapability),
67
68
  }));
68
69
  }
69
70
  async function bundle(input) {
@@ -110,7 +111,9 @@ function snapshotControls(options) {
110
111
  throw new BundleError("configuration", "maximum output bytes must be a positive safe integer");
111
112
  }
112
113
  const temporalCapability = validateTemporalCapability(options.temporalCapability);
113
- const visualCapability = validateVisualCapability(options.visualCapability);
114
+ const visualCapability = options.visualCapability === undefined
115
+ ? undefined
116
+ : validateVisualCapability(options.visualCapability);
114
117
  const frameBehavior = validateFrameBehavior(options.frameBehavior);
115
118
  if (frameBehavior === "placementBounded" &&
116
119
  temporalCapability !== "randomAccess") {
@@ -177,7 +180,15 @@ function validateVisualCapability(capability) {
177
180
  if (admitted !== undefined) {
178
181
  return admitted;
179
182
  }
180
- throw new BundleError("configuration", "visual capability must be browserComposite or separableOverlay");
183
+ throw new BundleError("configuration", "visual capability must be browserComposite, separableBackdrop, or separableOverlay");
184
+ }
185
+ function resolveVisualCapability(configured, authored) {
186
+ if (configured !== undefined &&
187
+ authored !== undefined &&
188
+ configured !== authored) {
189
+ throw new BundleError("configuration", "configured and authored visual capabilities disagree");
190
+ }
191
+ return configured ?? authored ?? "browserComposite";
181
192
  }
182
193
  function validateFrameBehavior(behavior) {
183
194
  const admitted = BUNDLE_FRAME_BEHAVIORS.find((candidate) => candidate === behavior);
@@ -2,6 +2,10 @@
2
2
  * Closed commands understood by the browser runtime.
3
3
  */
4
4
  export type BrowserCommand = {
5
+ /**
6
+ * Browser responsibility for primary-media elements.
7
+ */
8
+ mediaMode: "decoded" | "omitted" | "layoutOnly";
5
9
  plan: BrowserPlan;
6
10
  type: "load";
7
11
  } | {
@@ -66,7 +70,7 @@ export type RequestId = number;
66
70
  /**
67
71
  * Version of the native-to-browser message contract.
68
72
  */
69
- export type ProtocolVersion = 4;
73
+ export type ProtocolVersion = 5;
70
74
  /**
71
75
  * One versioned command sent from the native executor to the browser.
72
76
  */