@vestra-player/sdk 1.5.46 → 1.5.48

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/CHANGELOG.md CHANGED
@@ -1,5 +1,29 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.5.48
4
+
5
+ **Who must change: nobody, but plugin tests may now fail.** No changes to the TypeScript API beyond new fields of `COMMAND_LIMITS`, `apiVersion` stays `vestra/1`, no new capability, no player version needed. `TestHost` now refuses the command declarations the player has always refused: a plugin test that starts failing shows a menu item, key or reason the real player already rejected (the command never appeared, or the plugin failed to activate). Fix the declaration, not the test.
6
+
7
+ - **`TestHost` checks a command declaration as the player does** (`commands.register`, in the player's order, with its codes and messages):
8
+ - the local id: 1–128 bytes of ASCII letters, digits, `.`, `_` and `-` (`invalid`);
9
+ - the title (128 bytes), each path segment (80 bytes) and the group (80 bytes, default `"domain"`): not empty, no control character (C0, DEL and C1), no whitespace at either end (Unicode White_Space, as the player's `trim`). A text value is checked on its English rendering, which is also the key menus merge by. The message names the field: `invalid command: path segment requires 1..=80 bytes, no controls or surrounding whitespace`;
10
+ - at most 8 path segments, an `order` within ±1,000,000, bindings in the player's syntax, each once (`invalid`);
11
+ - the state's reason: at most 512 bytes as shown, the same text rules (`invalid` at registration; `command` on an update, as the player answers every refused update);
12
+ - a path that repeats another command's or is a submenu of one, or one of whose submenus is a command, the player's own commands included (`Audio ▸ Mute ▸ …`, `Playback ▸ Speed` as an item, `Quit`, `Toggle Fullscreen ▸ …`): `conflict` (`command conflict: menu path duplicates a command or collides with a leaf/group`). The total (`COMMAND_LIMITS.total`) counts all 19 of the player's own commands, Toggle Fullscreen, Enter and Leave Fullscreen, Quit and Toggle Sidebar included;
13
+ - an id registered twice: `conflict` (was `command`); an unknown `@` menu id, a bar button without a path or with an unknown icon, give the player's full messages;
14
+ - an update of a command that is not registered rejects with `command` (`command missing: owner 1 / <id>`; was a plain `Error`), a revision that is not a positive integer with `invalid` (`positive revision required`), and a stale one names the current revision.
15
+ - **The seek items follow `setStepSeek`.** `host.coreAction("seek-backward")` and `seek-forward` carry the step in their path and title (`Playback ▸ Seek ▸ Back 2.5 s`), as the player registers them again when the step changes; they were fixed at 5 s.
16
+ - **`COMMAND_LIMITS`** gains the player's declaration limits: `depth` (8), `idBytes` (128), `titleBytes` (128), `segmentBytes` (80), `groupBytes` (80), `reasonBytes` (512) and `order` (1,000,000), so the player's tests can check that they agree.
17
+ - _Commands_: `CommandOptions` and `Commands.add` say which checks the player makes (`ready` rejects with `invalid`) and which `add` makes at once.
18
+
19
+ ## 1.5.47
20
+
21
+ **Who must change: nobody.** No changes to the TypeScript API, `apiVersion` stays `vestra/1`, no new capability, no player version needed. The built-in command table follows player 0.6.0's new default keys: a plugin whose documentation or tests list the player's default keys (from `BUILTIN_COMMANDS`, or Escape, Enter, F11 and the side mouse buttons by hand) should update them.
22
+
23
+ - **Player 0.6.0 default keys** (`BUILTIN_COMMANDS`, the generated key tables, `HostAction`): **Toggle Fullscreen** (`host/fullscreen`) is `Escape` only (up to 0.5.x `Enter`, `Alt+Enter`, `F11`); **Leave Fullscreen** (`host/exit-fullscreen`) has no default key (up to 0.5.x `Escape`); the new **Enter Fullscreen** (`host/enter-fullscreen`) has no default key and no menu row and is enabled only when windowed (Leave Fullscreen only in fullscreen); **Previous** is `PageUp`, `Mouse:X2` and **Next** `PageDown`, `Mouse:X1` (the side buttons swapped: back is next, forward is previous). The side buttons now fire anywhere in the main window or the floating sidebar window, except while a menu is open, not only over the video. `host.run` takes the same actions as before (`fullscreen` toggles). `TestHost`'s core actions use the new Previous / Next bindings.
24
+ - **Rows that cannot change are not encoded again.** An update pass encodes every row of a bound array to find the changed ones; in a 20,000-row list that was about 40% of a click. A deep-frozen row (`Object.freeze` on it and on everything inside it: plain objects and arrays, no getters, no own `toJSON`; image and document handles allowed: they are immutable, their wire form is fixed when made, so they need no freezing) that the bind function returns again is now encoded once more and then taken from a cache kept by the row object (weakly: a row the plugin drops goes with its entry), with its limit counts. Rows that are not frozen, or hold a mutable part, are encoded on every pass as before, so changes made in place are still sent. To benefit, keep an unchanged row as the same frozen object and make a new one when it changes ([Arrays: only the changed rows are sent](docs/explanation/ui.md#arrays-only-the-changed-rows-are-sent)). Measured in the test host with the playlist at 20,000 rows memoized that way: a click from about 24 ms to about 14 ms to the rows write.
25
+ - _Commands, menus and key bindings_: the example of a disabled command letting its key go no longer uses Escape on Leave Fullscreen.
26
+
3
27
  ## 1.5.46
4
28
 
5
29
  **Who must change: nobody.** No changes to the TypeScript API, `apiVersion` stays `vestra/1`, no new capability. The new limits and the split writes take effect with player 0.6.0 (large lists P3); with older players arrays are sent whole as before and the old limits apply there. **A plugin that binds a whole long list depends on them: set `"minPlayerVersion": "0.6.0"`.**
@@ -11,6 +11,9 @@ import type { ConditionJson } from "./plugin/when.ts";
11
11
  * already registered are unaffected, and disposing a command frees its slot
12
12
  * at once. {@link TestHost} enforces the same caps.
13
13
  *
14
+ * The other fields are the player's rules for one declaration (since
15
+ * 1.5.48): a command past them is refused with `invalid`, the same way.
16
+ *
14
17
  * For menus built from media content (tracks, chapters, presets), set your
15
18
  * own sensible display cap well below `perPlugin`.
16
19
  *
@@ -41,6 +44,53 @@ export declare const COMMAND_LIMITS: {
41
44
  readonly perPlugin: 160;
42
45
  /** Commands of every plugin plus the player's own, together (1024). */
43
46
  readonly total: 1024;
47
+ /**
48
+ * Segments of a menu path, the leaf included.
49
+ *
50
+ * @since 1.5.48
51
+ */
52
+ readonly depth: 8;
53
+ /**
54
+ * UTF-8 bytes of a command's local id: ASCII letters, digits, `.`,
55
+ * `_` and `-`.
56
+ *
57
+ * @since 1.5.48
58
+ */
59
+ readonly idBytes: 128;
60
+ /**
61
+ * UTF-8 bytes of a title, a text value counted by its English
62
+ * rendering. Like every command text: not empty, no control characters,
63
+ * no whitespace at either end.
64
+ *
65
+ * @since 1.5.48
66
+ */
67
+ readonly titleBytes: 128;
68
+ /**
69
+ * UTF-8 bytes of a menu path segment, a text value counted by its
70
+ * English rendering.
71
+ *
72
+ * @since 1.5.48
73
+ */
74
+ readonly segmentBytes: 80;
75
+ /**
76
+ * UTF-8 bytes of a group.
77
+ *
78
+ * @since 1.5.48
79
+ */
80
+ readonly groupBytes: 80;
81
+ /**
82
+ * UTF-8 bytes of a state's reason, as shown in the player's
83
+ * language.
84
+ *
85
+ * @since 1.5.48
86
+ */
87
+ readonly reasonBytes: 512;
88
+ /**
89
+ * The largest `order` either way: an order is within ±1,000,000.
90
+ *
91
+ * @since 1.5.48
92
+ */
93
+ readonly order: 1000000;
44
94
  };
45
95
  /**
46
96
  * The player's built-in top-level menus, by id. Write `@<id>` as the first
package/dist/commands.js CHANGED
@@ -7,6 +7,9 @@
7
7
  * already registered are unaffected, and disposing a command frees its slot
8
8
  * at once. {@link TestHost} enforces the same caps.
9
9
  *
10
+ * The other fields are the player's rules for one declaration (since
11
+ * 1.5.48): a command past them is refused with `invalid`, the same way.
12
+ *
10
13
  * For menus built from media content (tracks, chapters, presets), set your
11
14
  * own sensible display cap well below `perPlugin`.
12
15
  *
@@ -37,6 +40,53 @@ export const COMMAND_LIMITS = {
37
40
  perPlugin: 160,
38
41
  /** Commands of every plugin plus the player's own, together (1024). */
39
42
  total: 1024,
43
+ /**
44
+ * Segments of a menu path, the leaf included.
45
+ *
46
+ * @since 1.5.48
47
+ */
48
+ depth: 8,
49
+ /**
50
+ * UTF-8 bytes of a command's local id: ASCII letters, digits, `.`,
51
+ * `_` and `-`.
52
+ *
53
+ * @since 1.5.48
54
+ */
55
+ idBytes: 128,
56
+ /**
57
+ * UTF-8 bytes of a title, a text value counted by its English
58
+ * rendering. Like every command text: not empty, no control characters,
59
+ * no whitespace at either end.
60
+ *
61
+ * @since 1.5.48
62
+ */
63
+ titleBytes: 128,
64
+ /**
65
+ * UTF-8 bytes of a menu path segment, a text value counted by its
66
+ * English rendering.
67
+ *
68
+ * @since 1.5.48
69
+ */
70
+ segmentBytes: 80,
71
+ /**
72
+ * UTF-8 bytes of a group.
73
+ *
74
+ * @since 1.5.48
75
+ */
76
+ groupBytes: 80,
77
+ /**
78
+ * UTF-8 bytes of a state's reason, as shown in the player's
79
+ * language.
80
+ *
81
+ * @since 1.5.48
82
+ */
83
+ reasonBytes: 512,
84
+ /**
85
+ * The largest `order` either way: an order is within ±1,000,000.
86
+ *
87
+ * @since 1.5.48
88
+ */
89
+ order: 1_000_000,
40
90
  };
41
91
  /**
42
92
  * The player's built-in top-level menus, by id. Write `@<id>` as the first
@@ -265,8 +315,8 @@ vestra.playback | copy-name | Copy to Clipboard ▸ File Name
265
315
  vestra.playback | copy-folder | Copy to Clipboard ▸ Folder Path
266
316
  vestra.playback | copy-path | Copy to Clipboard ▸ Full Path
267
317
  host | play-pause | Playback ▸ Play/Pause | Space, Mouse:LeftClick
268
- host | previous | Playback ▸ Previous | PageUp, Mouse:X1
269
- host | next | Playback ▸ Next | PageDown, Mouse:X2
318
+ host | previous | Playback ▸ Previous | PageUp, Mouse:X2
319
+ host | next | Playback ▸ Next | PageDown, Mouse:X1
270
320
  host | frame-back | Playback ▸ Seek ▸ Previous Frame | D
271
321
  host | frame-forward | Playback ▸ Seek ▸ Next Frame | F
272
322
  vestra.playback | keyframe-back | Playback ▸ Seek ▸ Previous Keyframe | Ctrl+Shift+ArrowLeft
@@ -492,7 +542,7 @@ vestra.audio | audio.normalize | Audio ▸ Processing ▸ Normalize | Shift+N
492
542
  vestra.audio | audio.clarity | Audio ▸ Processing ▸ Crystalizer | Shift+C
493
543
  vestra.audio | audio.reset | Audio ▸ Reset
494
544
  vestra.visualizer | audio-cover | Audio ▸ Show Album Cover for Audio Only
495
- host | fullscreen | Toggle Fullscreen | Enter, Alt+Enter, F11
545
+ host | fullscreen | Toggle Fullscreen | Escape
496
546
  vestra.window | pip | Screen ▸ Picture-in-Picture
497
547
  vestra.window | mini | Screen ▸ Mini Size | \`
498
548
  vestra.window | size.0.5 | Screen ▸ 0.5× | 1
@@ -536,7 +586,8 @@ vestra.preferences | language.fr | Settings ▸ Language ▸ Français
536
586
  vestra.preferences | language.es | Settings ▸ Language ▸ Español
537
587
  vestra.info | info.about | About Vestra Player… | F1
538
588
  host | quit | Quit | Cmd+Q
539
- host | exit-fullscreen | (Leave Fullscreen) | Escape
589
+ host | enter-fullscreen | (Enter Fullscreen)
590
+ host | exit-fullscreen | (Leave Fullscreen)
540
591
  host | sidebar | (Toggle Sidebar)
541
592
  vestra.info | info.brief | (Brief Info) | Shift+Tab
542
593
  vestra.playlist | playlist.play | (Play) | Enter, P | panel
@@ -66,8 +66,11 @@ export declare const BINDING_INPUTS: Readonly<{
66
66
  */
67
67
  readonly named: readonly ["Space", "Enter", "Tab", "Escape", "Backspace", "Delete", "Insert", "Home", "End", "PageUp", "PageDown", "ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown", "Plus", ...string[], "NumpadPlus", "NumpadMinus", "NumpadMultiply", "NumpadDivide", "NumpadEnter", "NumpadDecimal"];
68
68
  /**
69
- * Mouse inputs on the video area. `:LeftHalf` and `:RightHalf` tell the
70
- * halves of the video apart for a double click.
69
+ * Mouse inputs on the video area; the side buttons `Mouse:X1` (back) and
70
+ * `Mouse:X2` (forward) anywhere in the main window or the floating sidebar
71
+ * window, except while a menu is open (player 0.6.0).
72
+ * `:LeftHalf` and `:RightHalf` tell the halves of the video apart for a
73
+ * double click.
71
74
  */
72
75
  readonly mouse: readonly ["Mouse:LeftClick", "Mouse:LeftDoubleClick", "Mouse:LeftDoubleClick:LeftHalf", "Mouse:LeftDoubleClick:RightHalf", "Mouse:MiddleClick", "Mouse:X1", "Mouse:X2", "Mouse:WheelUp", "Mouse:WheelDown", "Mouse:WheelLeft", "Mouse:WheelRight"];
73
76
  }>;
@@ -97,8 +97,11 @@ export const BINDING_INPUTS = Object.freeze({
97
97
  "NumpadDecimal",
98
98
  ],
99
99
  /**
100
- * Mouse inputs on the video area. `:LeftHalf` and `:RightHalf` tell the
101
- * halves of the video apart for a double click.
100
+ * Mouse inputs on the video area; the side buttons `Mouse:X1` (back) and
101
+ * `Mouse:X2` (forward) anywhere in the main window or the floating sidebar
102
+ * window, except while a menu is open (player 0.6.0).
103
+ * `:LeftHalf` and `:RightHalf` tell the halves of the video apart for a
104
+ * double click.
102
105
  */
103
106
  mouse: [
104
107
  "Mouse:LeftClick",
@@ -78,7 +78,7 @@ import { CAPABILITY_INFO, MESSAGE_LIMIT } from "../capabilities.js";
78
78
  import { OSD_LIMITS } from "./osd.js";
79
79
  import { SECRET_LIMITS, SESSION_STORAGE_LIMITS } from "./services.js";
80
80
  import { SUBTITLE_LIMITS } from "./subtitles.js";
81
- import { BINDING_INPUTS } from "./bindings.js";
81
+ import { BINDING_INPUTS, canonical } from "./bindings.js";
82
82
  import { THEME_PALETTES } from "../palettes.js";
83
83
  import { IMAGE_LIMITS, UI_VALUE_LIMITS, valueLimitProblem } from "./values.js";
84
84
  import { validStep } from "./player.js";
@@ -316,6 +316,64 @@ const TEST_ZH_HANS = {
316
316
  };
317
317
  /** The player's limit of sidebar tabs. */
318
318
  const MAX_SIDEBAR_TABS = 8;
319
+ /**
320
+ * The player's refusal of a declaration (commands.rs `CommandError`): code
321
+ * `invalid`, `conflict` or `limit`, its message prefixed as the player's.
322
+ */
323
+ const commandInvalid = (message) => new HostError("invalid", `invalid command: ${message}`);
324
+ const commandConflict = (message) => new HostError("conflict", `command conflict: ${message}`);
325
+ /**
326
+ * The player's check of a command text (commands.rs `text`): 1 to `limit`
327
+ * UTF-8 bytes, no control character (`char::is_control`: C0, DEL and C1)
328
+ * and no whitespace at either end (`str::trim`: Unicode White_Space).
329
+ */
330
+ function commandText(value, label, limit) {
331
+ const bytes = Buffer.byteLength(value, "utf8");
332
+ if (bytes === 0 ||
333
+ bytes > limit ||
334
+ /^\p{White_Space}|\p{White_Space}$/u.test(value) ||
335
+ /\p{Cc}/u.test(value))
336
+ throw commandInvalid(`${label} requires 1..=${limit} bytes, no controls or surrounding whitespace`);
337
+ }
338
+ /**
339
+ * A menu text (title or path segment) as the player keys it: a string as
340
+ * declared, a text value by its English rendering (capabilities/commands.rs
341
+ * `menu_text`).
342
+ */
343
+ function menuTextKey(value) {
344
+ if (typeof value === "string")
345
+ return value;
346
+ if (!isTextValue(value))
347
+ throw new HostError("invalid", "title and path segments are strings or texts");
348
+ if ("$vestraList" in value)
349
+ throw new HostError("invalid", "a menu text is a string or a text, not a list");
350
+ const problem = textProblem(value);
351
+ if (problem)
352
+ throw new HostError("invalid", problem);
353
+ return textKey(value);
354
+ }
355
+ /**
356
+ * The declaration checks of the player's `Registry::register`, in its
357
+ * order, on the keyed title and path (`@<id>` already replaced).
358
+ */
359
+ function checkDeclaration(id, title, path, declaration) {
360
+ commandText(id, "localId", COMMAND_LIMITS.idBytes);
361
+ if (!/^[A-Za-z0-9._-]+$/.test(id))
362
+ throw commandInvalid("localId permits ASCII letters, digits, '.', '_' and '-'");
363
+ commandText(title, "title", COMMAND_LIMITS.titleBytes);
364
+ if (path.length > COMMAND_LIMITS.depth)
365
+ throw commandInvalid(`menu path allows at most ${COMMAND_LIMITS.depth} segments`);
366
+ for (const segment of path)
367
+ commandText(segment, "path segment", COMMAND_LIMITS.segmentBytes);
368
+ const group = declaration.group ?? "domain";
369
+ if (typeof group !== "string")
370
+ throw new HostError("invalid", "group is a string");
371
+ commandText(group, "group", COMMAND_LIMITS.groupBytes);
372
+ const order = declaration.order ?? 0;
373
+ if (!Number.isInteger(order) ||
374
+ Math.abs(order) > COMMAND_LIMITS.order)
375
+ throw commandInvalid("order must be within ±1000000");
376
+ }
319
377
  /**
320
378
  * An in-memory fake Vestra Player for plugin tests.
321
379
  *
@@ -927,8 +985,9 @@ export class TestHost {
927
985
  /** A command state's reason, refused as the player refuses it. */
928
986
  #checkReason(state) {
929
987
  const reason = state?.reason;
930
- if (reason !== null && reason !== undefined)
931
- this.#declared(reason, "reason");
988
+ return reason !== null && reason !== undefined
989
+ ? this.#declared(reason, "reason")
990
+ : null;
932
991
  }
933
992
  /**
934
993
  * Row patches (player 0.6.0): the `seq` of each property's last write, by
@@ -2254,7 +2313,7 @@ export class TestHost {
2254
2313
  ["Playback", "Previous"],
2255
2314
  202,
2256
2315
  "domain",
2257
- ["PageUp", "Mouse:X1"],
2316
+ ["PageUp", "Mouse:X2"],
2258
2317
  false,
2259
2318
  ],
2260
2319
  [
@@ -2262,7 +2321,7 @@ export class TestHost {
2262
2321
  ["Playback", "Next"],
2263
2322
  203,
2264
2323
  "domain",
2265
- ["PageDown", "Mouse:X2"],
2324
+ ["PageDown", "Mouse:X1"],
2266
2325
  false,
2267
2326
  ],
2268
2327
  [
@@ -2341,19 +2400,36 @@ export class TestHost {
2341
2400
  ["mute", ["Audio", "Mute"], 503, "domain", ["M"], false],
2342
2401
  ];
2343
2402
  const host = this;
2344
- for (const [id, path, order, group, bindings, repeat, radio] of table) {
2403
+ for (const [id, fixed, order, group, bindings, repeat, radio] of table) {
2404
+ // The seek items carry the step (`setStepSeek`): the player
2405
+ // registers them again when it changes (core_actions.rs `seek_title`).
2406
+ const seek = id === "seek-forward"
2407
+ ? "Forward"
2408
+ : id === "seek-backward"
2409
+ ? "Back"
2410
+ : null;
2411
+ const pathOf = () => seek === null
2412
+ ? fixed
2413
+ : [
2414
+ ...fixed.slice(0, -1),
2415
+ `${seek} ${String(Math.round(host.stepSeek * 1000) / 1000)} s`,
2416
+ ];
2417
+ const declaration = {
2418
+ order,
2419
+ group,
2420
+ radio: radio ?? false,
2421
+ bindings,
2422
+ repeat,
2423
+ };
2424
+ Object.defineProperties(declaration, {
2425
+ title: { get: () => pathOf().at(-1), enumerable: true },
2426
+ path: { get: pathOf, enumerable: true },
2427
+ });
2345
2428
  const command = {
2346
- declaration: {
2347
- title: path.at(-1),
2348
- path,
2349
- order,
2350
- group,
2351
- radio: radio ?? false,
2352
- bindings,
2353
- repeat,
2354
- },
2429
+ declaration,
2355
2430
  // The player's own words in the player's language.
2356
2431
  get display() {
2432
+ const path = pathOf();
2357
2433
  const leaf = path.at(-1);
2358
2434
  const step = /^(Back|Forward) (\S+) s$/.exec(leaf);
2359
2435
  const title = step
@@ -3834,43 +3910,95 @@ export class TestHost {
3834
3910
  },
3835
3911
  "commands.register": (p) => {
3836
3912
  const id = String(p.id);
3837
- // The player's built-in menu ids (MENU_IDS) in the first path segment.
3838
- const path = (p.declaration?.path ?? []).map(String);
3839
- const menu = path[0]?.startsWith("@") ? path[0].slice(1) : null;
3913
+ // The player's checks, in its order (capabilities/commands.rs
3914
+ // `register`, then commands.rs `Registry::register`), with its codes
3915
+ // and messages.
3916
+ if (!Number.isInteger(p.revision) || Number(p.revision) <= 0)
3917
+ throw new HostError("invalid", "positive revision required");
3918
+ // The reason is checked (and shown) when it arrives.
3919
+ const reason = this.#checkReason(p.state);
3920
+ const declaration = p.declaration;
3921
+ const button = declaration.button;
3922
+ const maxOrder = BAR_BUTTON_LIMITS.maxOrder;
3923
+ // Player 0.5.10: a bar button is { icon, order }, its order 0–999,
3924
+ // its icon a name or an image of the plugin.
3925
+ if (button !== undefined && button !== null) {
3926
+ if (typeof button !== "object" ||
3927
+ Array.isArray(button) ||
3928
+ !Object.keys(button).every((key) => key === "icon" || key === "order"))
3929
+ throw new HostError("invalid", "button is { icon, order }");
3930
+ const { icon, order } = button;
3931
+ if (!Number.isInteger(order) || +order < 0 || +order > maxOrder)
3932
+ throw new HostError("invalid", `button order must be an integer 0–${maxOrder}`);
3933
+ if (typeof icon !== "string" && this.imagePath(icon) === null)
3934
+ throw new HostError("invalid", "button icon is an icon name or an image from plugin.ui.image");
3935
+ }
3936
+ // Text values in the title and path are keyed by their English
3937
+ // rendering; the built-in menu ids (MENU_IDS) and the player's old
3938
+ // Chinese menu titles by the menus' keys.
3939
+ const title = menuTextKey(declaration.title);
3940
+ const declared = (declaration.path ?? []);
3941
+ if (!Array.isArray(declared))
3942
+ throw new HostError("invalid", "path is a list of menu texts");
3943
+ const keys = declared.map(menuTextKey);
3944
+ const menu = keys[0]?.startsWith("@") ? keys[0].slice(1) : null;
3840
3945
  if (menu !== null && !MENU_IDS.includes(menu))
3841
- throw new HostError("invalid", `unknown menu id @${menu}`);
3842
- if (menu !== null && path.length < 2)
3843
- throw new HostError("invalid", `menu id @${menu} needs an item after it`);
3946
+ throw commandInvalid(`unknown menu id @${menu}; the built-in menus are ${MENU_IDS.map((known) => `@${known}`).join(", ")}`);
3947
+ if (menu !== null && keys.length < 2)
3948
+ throw commandInvalid(`menu id @${menu} needs an item after it`);
3949
+ const path = menuKeys(keys);
3950
+ checkDeclaration(id, title, path, declaration);
3951
+ if (button !== undefined && button !== null) {
3952
+ if (path.length === 0)
3953
+ throw commandInvalid("button needs a menu path: a bar button is also a menu item");
3954
+ const { icon } = button;
3955
+ if (typeof icon === "string" &&
3956
+ !BAR_BUTTON_ICONS.includes(icon))
3957
+ throw commandInvalid(`unknown button icon ${JSON.stringify(icon)}: a name of @vestra/icons.slint (${BAR_BUTTON_ICONS.join(", ")}) or an image from plugin.ui.image`);
3958
+ }
3959
+ const bindings = (declaration.bindings ?? []);
3960
+ for (const binding of bindings)
3961
+ try {
3962
+ canonical(String(binding));
3963
+ }
3964
+ catch (error) {
3965
+ throw commandInvalid(error.message);
3966
+ }
3967
+ if (reason !== null)
3968
+ commandText(reason, "reason", COMMAND_LIMITS.reasonBytes);
3844
3969
  if (this.commands.has(id))
3845
- throw new HostError("command", "owner/localId already registered");
3846
- // The player's caps (COMMAND_LIMITS): the total counts the core commands too.
3970
+ throw commandConflict("owner/localId already registered");
3971
+ // The player's own commands beside the core actions (host/actions.rs
3972
+ // `host_declarations`: Toggle Fullscreen, Quit and three outside the
3973
+ // menu), which take part in conflicts and the total like any other.
3974
+ const hostOnly = BUILTIN_COMMANDS.filter((row) => row.owner === "host" && !this.coreCommands.has(`host/${row.id}`));
3975
+ // A path may not repeat another command's (the player's own
3976
+ // included) nor be a submenu of one, or one of its submenus.
3977
+ if (path.length > 0)
3978
+ for (const existing of [
3979
+ ...hostOnly.map((row) => row.path),
3980
+ ...[...this.coreCommands.values()].map((command) => command.declaration.path),
3981
+ ...[...this.commands.values()].map((command) => menuKeys((command.declaration.path ?? []).map(menuTextKey))),
3982
+ ]) {
3983
+ const shared = Math.min(existing.length, path.length);
3984
+ if (existing.length > 0 &&
3985
+ existing.slice(0, shared).every((s, i) => s === path[i]))
3986
+ throw commandConflict("menu path duplicates a command or collides with a leaf/group");
3987
+ }
3988
+ // The player's caps (COMMAND_LIMITS): the total counts all of the
3989
+ // player's own commands too.
3847
3990
  if (this.commands.size >= COMMAND_LIMITS.perPlugin ||
3848
- this.commands.size + this.coreCommands.size >= COMMAND_LIMITS.total)
3991
+ this.commands.size + this.coreCommands.size + hostOnly.length >=
3992
+ COMMAND_LIMITS.total)
3849
3993
  throw new HostError("limit", "command registry limit reached");
3850
- const host = this;
3851
- const declaration = p.declaration;
3852
- // Player 0.5.10: a bar button needs a path, an order 0–999 and a
3853
- // known icon or an image of the plugin.
3854
- if (declaration.button !== undefined) {
3855
- const { icon, order } = declaration.button;
3856
- if (path.length === 0)
3857
- throw new HostError("invalid", "button needs a menu path");
3858
- if (!Number.isInteger(order) ||
3859
- order < 0 ||
3860
- order > BAR_BUTTON_LIMITS.maxOrder)
3861
- throw new HostError("invalid", `button order must be an integer 0–${BAR_BUTTON_LIMITS.maxOrder}`);
3862
- if (typeof icon === "string") {
3863
- if (!BAR_BUTTON_ICONS.includes(icon))
3864
- throw new HostError("invalid", `unknown button icon "${icon}"`);
3865
- }
3866
- else if (this.imagePath(icon) === null)
3867
- throw new HostError("invalid", "button icon is an icon name or an image from plugin.ui.image");
3994
+ // The binding table: each input once per command.
3995
+ const seen = new Set();
3996
+ for (const binding of bindings.map(String)) {
3997
+ if (seen.has(binding))
3998
+ throw new HostError("invalid", `binding ${binding} is listed twice`);
3999
+ seen.add(binding);
3868
4000
  }
3869
- // Like the player: the menu texts and the reason are checked (and
3870
- // shown) when they arrive.
3871
- for (const segment of [declaration.title, ...(declaration.path ?? [])])
3872
- this.#declared(segment, "a menu text");
3873
- this.#checkReason(p.state);
4001
+ const host = this;
3874
4002
  let sent = p.state;
3875
4003
  this.commands.set(id, {
3876
4004
  declaration,
@@ -3899,10 +4027,25 @@ export class TestHost {
3899
4027
  throw new HostError("missing", `command ${p.id} is not registered`);
3900
4028
  },
3901
4029
  "commands.update": (p) => {
3902
- const command = this.command(String(p.id));
4030
+ // The request itself, as the player reads it (capabilities/commands.rs
4031
+ // `parsed`): `invalid`.
4032
+ if (!Number.isInteger(p.revision) || Number(p.revision) <= 0)
4033
+ throw new HostError("invalid", "positive revision required");
4034
+ const reason = this.#checkReason(p.state);
4035
+ // The registry's refusals of an update (commands.rs `Registry::update`)
4036
+ // all come back as `command`; the test host's one plugin is owner 1.
4037
+ const command = this.commands.get(String(p.id));
4038
+ if (!command)
4039
+ throw new HostError("command", `command missing: owner 1 / ${p.id}`);
3903
4040
  if (Number(p.revision) <= command.revision)
3904
- throw new HostError("command", `stale command revision ${p.revision}`);
3905
- this.#checkReason(p.state);
4041
+ throw new HostError("command", `stale command revision ${p.revision}; current is ${command.revision}`);
4042
+ if (reason !== null)
4043
+ try {
4044
+ commandText(reason, "reason", COMMAND_LIMITS.reasonBytes);
4045
+ }
4046
+ catch (error) {
4047
+ throw new HostError("command", error.message);
4048
+ }
3906
4049
  command.revision = Number(p.revision);
3907
4050
  command.state = p.state;
3908
4051
  if (p.when)
@@ -4248,7 +4391,7 @@ export class TestHost {
4248
4391
  throw new HostError("invalid", "seconds must be 0.1..3600 in tenths");
4249
4392
  seconds = p.seconds;
4250
4393
  }
4251
- // SDK 1.5.21: the main window's Toggle Fullscreen, like Enter: in when
4394
+ // SDK 1.5.21: the main window's Toggle Fullscreen, like Escape: in when
4252
4395
  // windowed, out when fullscreen, then `window.state`.
4253
4396
  if (p.action === "fullscreen") {
4254
4397
  this.coreRuns.push("fullscreen");
@@ -917,7 +917,9 @@ export interface CommandButton {
917
917
  * give it an order in the menu's range (Video: 400–499).
918
918
  *
919
919
  * {@link Commands.add} throws `invalid` synchronously for a bad path,
920
- * order, group or binding. The same path registered twice, or a path that
920
+ * order or binding; the player refuses an empty or overlong text, a control
921
+ * character or whitespace at either end (`COMMAND_LIMITS`) with
922
+ * `invalid` (through `ready`). The same path registered twice, or a path that
921
923
  * is both an item and a submenu, fails with `conflict` (through `ready`).
922
924
  *
923
925
  * @see {@link Commands.add}, [Commands, menus and key bindings](../../docs/explanation/commands.md)
@@ -999,9 +1001,12 @@ export interface CommandOptions {
999
1001
  * `Home`, `End`, `PageUp`, `PageDown`, `ArrowLeft`/`Right`/`Up`/`Down`,
1000
1002
  * `Plus`, any other single symbol as typed (`">"`, not `"Shift+."`),
1001
1003
  * `Numpad0`–`Numpad9`, `NumpadPlus`, `NumpadMinus`, `NumpadMultiply`,
1002
- * `NumpadDivide`, `NumpadEnter`, `NumpadDecimal`, and the video-area mouse
1004
+ * `NumpadDivide`, `NumpadEnter`, `NumpadDecimal`, the video-area mouse
1003
1005
  * inputs `Mouse:LeftClick`, `Mouse:LeftDoubleClick[:LeftHalf|:RightHalf]`,
1004
- * `Mouse:MiddleClick`, `Mouse:X1`, `Mouse:X2`, `Mouse:WheelUp`/`Down`/`Left`/`Right`.
1006
+ * `Mouse:MiddleClick`, `Mouse:WheelUp`/`Down`/`Left`/`Right`, and the side
1007
+ * buttons `Mouse:X1` (back), `Mouse:X2` (forward), from 0.6.0 on anywhere
1008
+ * in the main window or the floating sidebar window, except while a menu
1009
+ * is open.
1005
1010
  *
1006
1011
  * Platforms: on macOS `Ctrl+X` fires for both ⌃X and ⌘X while `Cmd+X`
1007
1012
  * means ⌘X only; on Windows `Cmd` is the Windows logo key, so write
@@ -1187,8 +1192,15 @@ export interface Commands {
1187
1192
  * lists, presets) in a child {@link Scope} and rebuild it.
1188
1193
  *
1189
1194
  * @throws `invalid` synchronously for a malformed path (including an
1190
- * unknown `@` menu id), order, group or binding; `conflict` for an id this
1195
+ * unknown `@` menu id), order or binding; `conflict` for an id this
1191
1196
  * plugin has already declared and not disposed.
1197
+ * @throws `invalid` (`ready` rejects; during activation, activation
1198
+ * fails) for a text the player refuses: an id, title, path segment,
1199
+ * group or reason that is empty, longer than its `COMMAND_LIMITS`
1200
+ * bytes (a text value counted by its English rendering), has a control
1201
+ * character or whitespace at either end, or an id with other characters
1202
+ * than ASCII letters, digits, `.`, `_` and `-`. `TestHost`
1203
+ * refuses the same (SDK 1.5.48).
1192
1204
  *
1193
1205
  * @example Pause playback from a menu item
1194
1206
  * Playback ▸ Pause (and Ctrl+P) pauses and resumes, shows a check mark
@@ -6061,13 +6073,13 @@ export interface Player {
6061
6073
  * | --- | --- | --- |
6062
6074
  * | `stop` | F4 | close the source |
6063
6075
  * | `play-pause` | Space, a left click on the video (player 0.5.2) | pause / resume, replay after the end |
6064
- * | `previous` / `next` | PageUp, Mouse:X1 / PageDown, Mouse:X2 | ask the sequence provider |
6076
+ * | `previous` / `next` | PageUp, Mouse:X2 / PageDown, Mouse:X1 (from 0.6.0; up to 0.5.x X1 was previous and X2 next) | ask the sequence provider |
6065
6077
  * | `frame-back` / `frame-forward` | D / F | `player.step("frame", …)` |
6066
6078
  * | `seek-backward` / `seek-forward` | ← / → | relative seek by `setStepSeek` seconds (or `host.run`'s `seconds`), exact or to keyframes as `setStepSeek` says |
6067
6079
  * | `rate-normal`, `rate-down`, `rate-up` | Z, X, C | 1×, ±0.1 within 0.25–4 |
6068
6080
  * | `volume-up` / `volume-down` | ↑ / ↓, wheel | ±5 (`volume-up` stops at 100) |
6069
6081
  * | `mute` | M | toggle mute |
6070
- * | `fullscreen` | Enter, Alt+Enter, F11 | toggle the main window's fullscreen: enter it when windowed, leave it when fullscreen, as the Toggle Fullscreen command does (SDK 1.5.21, player 0.5.2; `unavailable` when the player has no window) |
6082
+ * | `fullscreen` | Escape (from 0.6.0; up to 0.5.x Enter, Alt+Enter, F11) | toggle the main window's fullscreen: enter it when windowed, leave it when fullscreen, as the Toggle Fullscreen command does (SDK 1.5.21, player 0.5.2; `unavailable` when the player has no window) |
6071
6083
  *
6072
6084
  * In the binding overlay their keys are `host/<action>`. `fullscreen`
6073
6085
  * changes no playback state: read the result from `window.state.fullscreen`
package/dist/plugin/ui.js CHANGED
@@ -83,8 +83,113 @@ const isPrimitive = (value) => value === null ||
83
83
  (typeof value === "number" && Number.isFinite(value));
84
84
  /** A row as `JSON.stringify` writes it inside an array. */
85
85
  const encodeRow = (row) => JSON.stringify(row) ?? "null";
86
+ /** What a row's encoding holds: a Markdown document, an image. */
87
+ const HAS_MARKDOWN = 1;
88
+ const HAS_IMAGE = 2;
89
+ const marksOf = (enc) => (enc.includes('"$vestraMarkdown"') ? HAS_MARKDOWN : 0) |
90
+ (enc.includes('"$vestraImage"') ? HAS_IMAGE : 0);
91
+ /**
92
+ * Row encodings by row object, shared by every property (a row's JSON does
93
+ * not depend on where it is bound). Only rows that cannot change are kept
94
+ * ({@link settled}): a plugin that mutates its rows in place sees no
95
+ * difference, and one that keeps unchanged rows as the same deep-frozen
96
+ * objects skips encoding them on every pass. Weak: a row the plugin drops
97
+ * goes with its entry.
98
+ */
99
+ const settledRows = new WeakMap();
100
+ /**
101
+ * Frozen rows seen once. A frozen row is verified and kept on its second
102
+ * pass, so rows frozen afresh on every pass cost one set entry, not a
103
+ * verification each.
104
+ */
105
+ const seenRows = new WeakSet();
106
+ /** Frozen rows that can still change (a mutable part, a getter): never kept. */
107
+ const unsettledRows = new WeakSet();
108
+ /**
109
+ * `value` can never change what `JSON.stringify` writes for it: a primitive,
110
+ * an image or document handle (their wire form is fixed when made), or a
111
+ * frozen plain object or array whose own enumerable properties are data
112
+ * properties (no getter, no own `toJSON`) holding such values. Freezing
113
+ * fixes the properties and the prototype for good, so the answer holds as
114
+ * long as the object lives.
115
+ */
116
+ function settled(value, depth) {
117
+ if (value === null || typeof value !== "object")
118
+ return typeof value !== "function";
119
+ if (value instanceof ImageHandle || value instanceof MarkdownHandle)
120
+ return true;
121
+ if (depth > UI_VALUE_LIMITS.depth || !Object.isFrozen(value))
122
+ return false;
123
+ const proto = Object.getPrototypeOf(value);
124
+ if (Array.isArray(value)
125
+ ? proto !== Array.prototype
126
+ : proto !== Object.prototype && proto !== null)
127
+ return false;
128
+ // An own `toJSON`, enumerable or not, is called by `JSON.stringify`.
129
+ if (Object.prototype.hasOwnProperty.call(value, "toJSON"))
130
+ return false;
131
+ for (const key of Object.keys(value)) {
132
+ const property = Object.getOwnPropertyDescriptor(value, key);
133
+ if (!property || !("value" in property))
134
+ return false;
135
+ if (!settled(property.value, depth + 1))
136
+ return false;
137
+ }
138
+ return true;
139
+ }
140
+ /** A row's encoding and marks, from {@link settledRows} when it is kept. */
141
+ function rowEncoding(row, cache) {
142
+ if (cache && row !== null && typeof row === "object") {
143
+ // A kept row is frozen: looked up first, `isFrozen` only on a miss.
144
+ const kept = settledRows.get(row);
145
+ if (kept)
146
+ return kept;
147
+ }
148
+ if (cache &&
149
+ row !== null &&
150
+ typeof row === "object" &&
151
+ Object.isFrozen(row)) {
152
+ const enc = encodeRow(row);
153
+ const entry = { enc, marks: marksOf(enc) };
154
+ if (!seenRows.has(row))
155
+ seenRows.add(row);
156
+ else if (!unsettledRows.has(row)) {
157
+ if (settled(row, 1))
158
+ settledRows.set(row, entry);
159
+ else
160
+ unsettledRows.add(row);
161
+ }
162
+ return entry;
163
+ }
164
+ const enc = encodeRow(row);
165
+ return { enc, marks: marksOf(enc) };
166
+ }
86
167
  /** Each row of an array as `JSON.stringify` writes it; a hole is `null`. */
87
- const encodeRows = (rows) => Array.from(rows, encodeRow);
168
+ function encodeRows(rows) {
169
+ // A `toJSON` added to the shared prototypes would change any row's JSON:
170
+ // then nothing is taken from (or kept in) the cache.
171
+ const cache = !("toJSON" in Object.prototype) && !("toJSON" in Array.prototype);
172
+ const enc = [];
173
+ const marks = new Uint8Array(rows.length);
174
+ let markdown = false;
175
+ for (let i = 0; i < rows.length; i++) {
176
+ const row = rowEncoding(rows[i], cache);
177
+ enc[i] = row.enc;
178
+ marks[i] = row.marks;
179
+ if (row.marks & HAS_MARKDOWN)
180
+ markdown = true;
181
+ }
182
+ return { enc, marks, markdown, cache };
183
+ }
184
+ /** A row's limit counts, kept with its encoding when the row cannot change. */
185
+ function rowCount(row, cache) {
186
+ const kept = cache && row !== null && typeof row === "object"
187
+ ? settledRows.get(row)
188
+ : undefined;
189
+ if (!kept)
190
+ return countRow(row);
191
+ return (kept.count ??= countRow(row));
192
+ }
88
193
  function sameRows(a, b) {
89
194
  if (a.length !== b.length)
90
195
  return false;
@@ -102,7 +207,7 @@ function sentOf(value, patches) {
102
207
  return {
103
208
  kind: "rows",
104
209
  rows: {
105
- enc: encodeRows(value),
210
+ enc: encodeRows(value).enc,
106
211
  field: keyed?.field ?? null,
107
212
  keys: keyed?.keys ?? null,
108
213
  counts: [],
@@ -142,7 +247,7 @@ function tooLarge(key, ops) {
142
247
  function rowPatch(sent, next, rows) {
143
248
  if (sent.enc.length === 0)
144
249
  return null;
145
- if (next.enc.some((row) => row.includes('"$vestraMarkdown"')))
250
+ if (next.markdown)
146
251
  return null;
147
252
  const nextKeyed = sent.field === null ? null : next.keyed();
148
253
  const keyed = nextKeyed !== null && nextKeyed.field === sent.field;
@@ -152,11 +257,12 @@ function rowPatch(sent, next, rows) {
152
257
  // A released image is refused wherever it is, as in a whole write: the
153
258
  // rows the patch does not carry are checked too (those holding images).
154
259
  for (let j = 0; j < rows.length; j++)
155
- if (plan.from[j] >= 0 && next.enc[j].includes('"$vestraImage"'))
260
+ if (plan.from[j] >= 0 && next.marks[j] & HAS_IMAGE)
156
261
  wireValue(rows[j] ?? null);
157
262
  const counts = next.enc.map((_, j) => {
158
263
  const from = plan.from[j];
159
- return (from >= 0 ? sent.counts[from] : undefined) ?? countRow(rows[j]);
264
+ return ((from >= 0 ? sent.counts[from] : undefined) ??
265
+ rowCount(rows[j], next.cache));
160
266
  });
161
267
  if (!withinLimits(counts))
162
268
  return null;
@@ -372,8 +478,10 @@ export class ViewBase {
372
478
  }
373
479
  if (Array.isArray(value) && this.#patches) {
374
480
  // Row by row: the same work as one `JSON.stringify`, and the rows to
375
- // diff against those last sent.
376
- const enc = encodeRows(value);
481
+ // diff against those last sent. Rows that cannot change (deep-frozen
482
+ // objects bound again, SDK 1.5.47) are taken from `settledRows`.
483
+ const encoded = encodeRows(value);
484
+ const { enc } = encoded;
377
485
  const signature = () => `[${enc.join(",")}]`;
378
486
  const changed = sent?.kind === "rows"
379
487
  ? !sameRows(sent.rows.enc, enc)
@@ -386,7 +494,7 @@ export class ViewBase {
386
494
  value,
387
495
  changed,
388
496
  signature,
389
- rows: { enc, keyed },
497
+ rows: { ...encoded, keyed },
390
498
  sent: (counts = []) => ({
391
499
  kind: "rows",
392
500
  rows: {
@@ -470,9 +578,7 @@ export class ViewBase {
470
578
  const seq = ++this.#seq;
471
579
  // Rows holding Markdown documents go whole, as before: the player
472
580
  // merges documents in place and takes no parts of them.
473
- if (Array.isArray(value) &&
474
- pending.rows &&
475
- !pending.rows.enc.some((row) => row.includes(MARKDOWN_KEY))) {
581
+ if (Array.isArray(value) && pending.rows && !pending.rows.markdown) {
476
582
  const sizes = pending.rows.enc.map(jsonBytes);
477
583
  const count = prefixWithin(sizes, CHUNK_BYTES);
478
584
  if (count < value.length) {
package/llms-full.txt CHANGED
@@ -1,4 +1,4 @@
1
- # @vestra-player/sdk 1.5.46: full reference
1
+ # @vestra-player/sdk 1.5.48: full reference
2
2
 
3
3
  > Everything needed to write a Vestra Player plugin (plugin API `vestra/1`): the guides, every public declaration of `@vestra-player/sdk` and `@vestra-player/sdk/testing` with its documentation, and the complete source of the example plugins. Generated from the SDK's sources by scripts/llms.mjs; the short index is llms.txt.
4
4
 
@@ -2357,12 +2357,19 @@ The player bounds what one request may cost it. The limits are guards against ru
2357
2357
 
2358
2358
  #### Commands
2359
2359
 
2360
- A command added past a limit fails with code `limit` (its `ready` rejects; during activation, activation fails). Commands already registered are unaffected, and disposing one frees its slot at once.
2360
+ A command added past `perPlugin` or `total` fails with code `limit` (its `ready` rejects; during activation, activation fails). Commands already registered are unaffected, and disposing one frees its slot at once. A declaration past the other limits fails with `invalid` the same way (SDK 1.5.48: `TestHost` refuses it too).
2361
2361
 
2362
2362
  | Limit | Value | Meaning |
2363
2363
  | --- | --- | --- |
2364
2364
  | `COMMAND_LIMITS.perPlugin` | 160 | Commands one plugin instance may have at the same time. |
2365
2365
  | `COMMAND_LIMITS.total` | 1024 | Commands of every plugin plus the player's own, together. |
2366
+ | `COMMAND_LIMITS.depth` | 8 | Segments of a menu path, the leaf included. |
2367
+ | `COMMAND_LIMITS.idBytes` | 128 | UTF-8 bytes of a command's local id: ASCII letters, digits, `.`, `_` and `-`. |
2368
+ | `COMMAND_LIMITS.titleBytes` | 128 | UTF-8 bytes of a title, a text value counted by its English rendering. Like every command text: not empty, no control characters, no whitespace at either end. |
2369
+ | `COMMAND_LIMITS.segmentBytes` | 80 | UTF-8 bytes of a menu path segment, a text value counted by its English rendering. |
2370
+ | `COMMAND_LIMITS.groupBytes` | 80 | UTF-8 bytes of a group. |
2371
+ | `COMMAND_LIMITS.reasonBytes` | 512 | UTF-8 bytes of a state's reason, as shown in the player's language. |
2372
+ | `COMMAND_LIMITS.order` | 1000000 | The largest `order` either way: an order is within ±1,000,000. |
2366
2373
 
2367
2374
  #### Bottom-bar buttons
2368
2375
 
@@ -2630,8 +2637,8 @@ Every command the player (`host`) and its built-in plugins register when the pla
2630
2637
  | Copy to Clipboard ▸ Folder Path | | `vestra.playback/copy-folder` |
2631
2638
  | Copy to Clipboard ▸ Full Path | | `vestra.playback/copy-path` |
2632
2639
  | Playback ▸ Play/Pause | `Space`, `Mouse:LeftClick` | `host/play-pause` |
2633
- | Playback ▸ Previous | `PageUp`, `Mouse:X1` | `host/previous` |
2634
- | Playback ▸ Next | `PageDown`, `Mouse:X2` | `host/next` |
2640
+ | Playback ▸ Previous | `PageUp`, `Mouse:X2` | `host/previous` |
2641
+ | Playback ▸ Next | `PageDown`, `Mouse:X1` | `host/next` |
2635
2642
  | Playback ▸ Seek ▸ Previous Frame | `D` | `host/frame-back` |
2636
2643
  | Playback ▸ Seek ▸ Next Frame | `F` | `host/frame-forward` |
2637
2644
  | Playback ▸ Seek ▸ Previous Keyframe | `Ctrl+Shift+ArrowLeft` | `vestra.playback/keyframe-back` |
@@ -2857,7 +2864,7 @@ Every command the player (`host`) and its built-in plugins register when the pla
2857
2864
  | Audio ▸ Processing ▸ Crystalizer | `Shift+C` | `vestra.audio/audio.clarity` |
2858
2865
  | Audio ▸ Reset | | `vestra.audio/audio.reset` |
2859
2866
  | Audio ▸ Show Album Cover for Audio Only | | `vestra.visualizer/audio-cover` |
2860
- | Toggle Fullscreen | `Enter`, `Alt+Enter`, `F11` | `host/fullscreen` |
2867
+ | Toggle Fullscreen | `Escape` | `host/fullscreen` |
2861
2868
  | Screen ▸ Picture-in-Picture | | `vestra.window/pip` |
2862
2869
  | Screen ▸ Mini Size | ``` | `vestra.window/mini` |
2863
2870
  | Screen ▸ 0.5× | `1` | `vestra.window/size.0.5` |
@@ -2901,7 +2908,8 @@ Every command the player (`host`) and its built-in plugins register when the pla
2901
2908
  | Settings ▸ Language ▸ Español | | `vestra.preferences/language.es` |
2902
2909
  | About Vestra Player… | `F1` | `vestra.info/info.about` |
2903
2910
  | Quit | `Cmd+Q` | `host/quit` |
2904
- | Leave Fullscreen (not in the menu) | `Escape` | `host/exit-fullscreen` |
2911
+ | Enter Fullscreen (not in the menu) | | `host/enter-fullscreen` |
2912
+ | Leave Fullscreen (not in the menu) | | `host/exit-fullscreen` |
2905
2913
  | Toggle Sidebar (not in the menu) | | `host/sidebar` |
2906
2914
  | Brief Info (not in the menu) | `Shift+Tab` | `vestra.info/info.brief` |
2907
2915
  | Play (not in the menu) | `Enter`, `P` (panel focus) | `vestra.playlist/playlist.play` |
@@ -2979,7 +2987,7 @@ A menu item shows the first of its keyboard bindings that is in effect.
2979
2987
  | `digits` | `0`–`9` | Digit keys of the main keyboard: `0` to `9`. |
2980
2988
  | `functionKeys` | `F1`–`F24` | Function keys: `F1` to `F24`. |
2981
2989
  | `named` | `Space`, `Enter`, `Tab`, `Escape`, `Backspace`, `Delete`, `Insert`, `Home`, `End`, `PageUp`, `PageDown`, `ArrowLeft`, `ArrowRight`, `ArrowUp`, `ArrowDown`, `Plus`, `Numpad0`, `Numpad1`, `Numpad2`, `Numpad3`, `Numpad4`, `Numpad5`, `Numpad6`, `Numpad7`, `Numpad8`, `Numpad9`, `NumpadPlus`, `NumpadMinus`, `NumpadMultiply`, `NumpadDivide`, `NumpadEnter`, `NumpadDecimal` | Other named keys, matched by the physical key. `Plus` is the `+` key however it is typed; the `Numpad…` names are the numeric keypad only, never the main keyboard. |
2982
- | `mouse` | `Mouse:LeftClick`, `Mouse:LeftDoubleClick`, `Mouse:LeftDoubleClick:LeftHalf`, `Mouse:LeftDoubleClick:RightHalf`, `Mouse:MiddleClick`, `Mouse:X1`, `Mouse:X2`, `Mouse:WheelUp`, `Mouse:WheelDown`, `Mouse:WheelLeft`, `Mouse:WheelRight` | Mouse inputs on the video area. `:LeftHalf` and `:RightHalf` tell the halves of the video apart for a double click. |
2990
+ | `mouse` | `Mouse:LeftClick`, `Mouse:LeftDoubleClick`, `Mouse:LeftDoubleClick:LeftHalf`, `Mouse:LeftDoubleClick:RightHalf`, `Mouse:MiddleClick`, `Mouse:X1`, `Mouse:X2`, `Mouse:WheelUp`, `Mouse:WheelDown`, `Mouse:WheelLeft`, `Mouse:WheelRight` | Mouse inputs on the video area; the side buttons `Mouse:X1` (back) and `Mouse:X2` (forward) anywhere in the main window or the floating sidebar window, except while a menu is open (player 0.6.0). `:LeftHalf` and `:RightHalf` tell the halves of the video apart for a double click. |
2983
2991
 
2984
2992
  #### Core actions
2985
2993
 
@@ -2989,8 +2997,8 @@ The player's own playback commands, which plugin.host.run runs by id (capability
2989
2997
  | --- | --- | --- |
2990
2998
  | `stop` | Close | `F4` |
2991
2999
  | `play-pause` | Playback ▸ Play/Pause | `Space`, `Mouse:LeftClick` |
2992
- | `previous` | Playback ▸ Previous | `PageUp`, `Mouse:X1` |
2993
- | `next` | Playback ▸ Next | `PageDown`, `Mouse:X2` |
3000
+ | `previous` | Playback ▸ Previous | `PageUp`, `Mouse:X2` |
3001
+ | `next` | Playback ▸ Next | `PageDown`, `Mouse:X1` |
2994
3002
  | `frame-back` | Playback ▸ Seek ▸ Previous Frame | `D` |
2995
3003
  | `frame-forward` | Playback ▸ Seek ▸ Next Frame | `F` |
2996
3004
  | `seek-backward` | Playback ▸ Seek ▸ Back 5 s | `ArrowLeft` |
@@ -3001,9 +3009,9 @@ The player's own playback commands, which plugin.host.run runs by id (capability
3001
3009
  | `volume-up` | Audio ▸ Volume + | `ArrowUp`, `Mouse:WheelUp` |
3002
3010
  | `volume-down` | Audio ▸ Volume − | `ArrowDown`, `Mouse:WheelDown` |
3003
3011
  | `mute` | Audio ▸ Mute | `M` |
3004
- | `fullscreen` | Toggle Fullscreen | `Enter`, `Alt+Enter`, `F11` |
3012
+ | `fullscreen` | Toggle Fullscreen | `Escape` |
3005
3013
 
3006
- The player also owns **Toggle Fullscreen** (`host/fullscreen`), **Leave Fullscreen** (`host/exit-fullscreen`, `Escape`, only in fullscreen) and **Quit** (`host/quit`), and **Toggle Sidebar** (`host/sidebar`, player 0.5.10: the bottom bar's sidebar button, outside the menu, no default key); their keys are in Built-in commands.
3014
+ The player also owns **Toggle Fullscreen** (`host/fullscreen`, `Escape` from player 0.6.0; up to 0.5.x `Enter`, `Alt+Enter` and `F11`), **Enter Fullscreen** (`host/enter-fullscreen`, player 0.6.0: no default key, outside the menu, enabled only when windowed), **Leave Fullscreen** (`host/exit-fullscreen`: no default key from player 0.6.0, up to 0.5.x `Escape`; outside the menu, enabled only in fullscreen) and **Quit** (`host/quit`), and **Toggle Sidebar** (`host/sidebar`, player 0.5.10: the bottom bar's sidebar button, outside the menu, no default key); their keys are in Built-in commands.
3007
3015
 
3008
3016
  #### Keys the system takes first
3009
3017
 
@@ -4106,7 +4114,7 @@ flowchart TB
4106
4114
 
4107
4115
  - **Where bindings apply.** Bindings belong to the main window: they fire while it or the floating sidebar window has focus. They do **not** fire in tool windows or the Options window; keys there are the Slint file's own business (a `FocusScope`, a default button). While a menu or a system dialog is open, bindings pause.
4108
4116
  - **Conflicts are not errors.** A binding that loses is written to the player's log and takes effect by itself when the winner goes away (its plugin stops, the user rebinds it). Tests can check `host.conflicts` (TestHost.conflicts).
4109
- - **A disabled command lets its key go.** This is what makes context-dependent keys work: `Escape` bound to "Leave Fullscreen" does nothing outside fullscreen, so `Escape` reaches the focused control. Give `reason` to a command whose key should still be taken and explained while it is disabled.
4117
+ - **A disabled command lets its key go.** This is what makes context-dependent keys work: a command bound to `Delete` that is disabled while nothing is selected does nothing then, so `Delete` reaches the focused control (a text field deletes a character). Give `reason` to a command whose key should still be taken and explained while it is disabled.
4110
4118
  - **`repeat: true`** fires again on the keyboard's auto-repeat while the key is held; without it a binding fires once per press.
4111
4119
  - **Some keys never reach a binding.** The system takes some keys first, and the standard close keys close a tool window, the floating sidebar or the Options window before any binding sees them (in the main window they are ordinary keys). Keys the system takes first lists them; default bindings avoid them.
4112
4120
 
@@ -4119,7 +4127,7 @@ Key names describe keys, not characters, with two deliberate exceptions:
4119
4127
  - **Option combinations match the physical key**, so `Alt+O` fires for ⌥O although that key types `ø`, and dead keys (⌥E, ⌥N…) fire too. On layouts that need Option to type a symbol (German ⌥5 = ``, ⌥L = `@`), a binding for the typed symbol (`"["`, `"@"`) wins over one for the physical key (`Alt+5`).
4120
4128
  - **`Ctrl` is portable, `Cmd` is not.** On macOS `Ctrl+X` fires for both ⌃X and ⌘X, `Cmd+X` only for ⌘X. On Windows `Cmd` is the Windows logo key, which the system mostly keeps, so write `Ctrl+…` for a binding that works on both systems.
4121
4129
 
4122
- Mouse bindings apply only over the video area. `Mouse:LeftDoubleClick:LeftHalf` and `:RightHalf` win over the plain double click; wheel bindings count accumulated scroll steps. The right button is reserved for the context menu, and a left drag in window mode moves the window instead of clicking. A `Mouse:LeftClick` fires on release, except when the same modifiers also have a double-click binding and the click's command is not checkable: then the player waits the system's double-click interval to tell a click from a double click. (A checkable command such as play/pause runs at once; a double click runs it twice, which restores its state, and the double-click binding still fires.) The player's defaults (player 0.5.2): a left click on the video is Play/Pause and nothing is bound to a double click (up to 0.5.1 a double click was Toggle Fullscreen); a click whose command is disabled does nothing.
4130
+ Mouse bindings apply only over the video area, except the side buttons `Mouse:X1` (back) and `Mouse:X2` (forward), which fire anywhere in the main window or the floating sidebar window, except while a menu is open, as in browsers (player 0.6.0; before, only over the video area). `Mouse:LeftDoubleClick:LeftHalf` and `:RightHalf` win over the plain double click; wheel bindings count accumulated scroll steps. The right button is reserved for the context menu, and a left drag in window mode moves the window instead of clicking. A `Mouse:LeftClick` fires on release, except when the same modifiers also have a double-click binding and the click's command is not checkable: then the player waits the system's double-click interval to tell a click from a double click. (A checkable command such as play/pause runs at once; a double click runs it twice, which restores its state, and the double-click binding still fires.) The player's defaults (player 0.5.2): a left click on the video is Play/Pause and nothing is bound to a double click (up to 0.5.1 a double click was Toggle Fullscreen); a click whose command is disabled does nothing.
4123
4131
 
4124
4132
  ##### The user's bindings
4125
4133
 
@@ -4144,7 +4152,7 @@ plugin.commands.add({
4144
4152
 
4145
4153
  #### The player's own commands
4146
4154
 
4147
- The player registers some commands itself, with the owner `host`: **Toggle Fullscreen**, **Leave Fullscreen**, **Quit**, **Toggle Sidebar** (the bottom bar's sidebar button, player 0.5.10) and the core playback actions (play/pause, stop, previous/next, frame step, relative seek, speed, volume, mute). They exist whether or not any plugin runs, and the user rebinds them like any other command.
4155
+ The player registers some commands itself, with the owner `host`: **Toggle Fullscreen** (`Escape` from player 0.6.0), **Enter Fullscreen** and **Leave Fullscreen** (player 0.6.0: no default key, each enabled only where it acts), **Quit**, **Toggle Sidebar** (the bottom bar's sidebar button, player 0.5.10) and the core playback actions (play/pause, stop, previous/next, frame step, relative seek, speed, volume, mute). They exist whether or not any plugin runs, and the user rebinds them like any other command.
4148
4156
 
4149
4157
  A plugin button that does what a core action does should run that action with plugin.host.run (capability `commands`): it applies the same checks and shows the same on-screen text as the menu item and the key, so a bottom bar's play button and the Space key behave identically. The action ids, their menu places and default keys are in Reference: commands; what they do to playback is in Playback.
4150
4158
 
@@ -4650,6 +4658,8 @@ From player 0.6.0 and SDK 1.5.44, a changed array (a list's rows) is sent as the
4650
4658
  - **Your Slint code must not write into the rows of an array property** (`rows[i].selected = true` in a handler). A whole write used to overwrite such a change on the next update; a row patch touches only the rows whose value changed in your plugin, so the edited row keeps the Slint-side value until your value for that row changes. Send the event to the plugin with a callback and change the row there.
4651
4659
  - **`set()` with an unchanged array sends the whole array**, as `set()` always sends: use it to pull a list back to your rows. A bound array whose rows did not change sends nothing.
4652
4660
 
4661
+ To find the changed rows the SDK encodes every row of a bound array on each update pass, which in a list of tens of thousands of rows is most of a pass. From SDK 1.5.47 it skips that for rows it can prove unchanged: a **deep-frozen** row object (`Object.freeze` on the row and on every object and array inside it, plain objects and arrays only, no getters or own `toJSON`; image and document handles may sit inside: they are immutable, so they need no freezing) that the bind function returns again is encoded once and then taken from a cache kept by the row object. To use it in a long list, keep each row as the same frozen object while it does not change and make a new one when it does (memoize by the row's id and the fields it shows). Nothing else changes: rows that are not frozen, or that hold a mutable part, are encoded on every pass as before, so a plugin that changes its rows in place keeps working.
4662
+
4653
4663
  ##### Two-way controls
4654
4664
 
4655
4665
  A control whose value both the user and the plugin change (a slider, a switch, a text field, a combo box) needs a **two-way binding to an `in-out` property**:
@@ -5817,6 +5827,8 @@ Every SDK release's CHANGELOG entry opens with who must change and what happens
5817
5827
 
5818
5828
  | Release | Who must change | Guide |
5819
5829
  | --- | --- | --- |
5830
+ | [1.5.48](https://github.com/vestrasys/vestra-player-sdk/blob/main/CHANGELOG.md#1548) | **Nobody, but plugin tests may now fail.** No changes to the TypeScript API beyond new fields of `COMMAND_LIMITS`, `apiVersion` stays `vestra/1`, no new capability, no player version needed. | — |
5831
+ | [1.5.47](https://github.com/vestrasys/vestra-player-sdk/blob/main/CHANGELOG.md#1547) | **Nobody.** No changes to the TypeScript API, `apiVersion` stays `vestra/1`, no new capability, no player version needed. | — |
5820
5832
  | [1.5.46](https://github.com/vestrasys/vestra-player-sdk/blob/main/CHANGELOG.md#1546) | **Nobody.** No changes to the TypeScript API, `apiVersion` stays `vestra/1`, no new capability. | — |
5821
5833
  | [1.5.45](https://github.com/vestrasys/vestra-player-sdk/blob/main/CHANGELOG.md#1545) | **Nobody.** No changes to the TypeScript API, `apiVersion` stays `vestra/1`, no new capability, no player version needed. | — |
5822
5834
  | [1.5.44](https://github.com/vestrasys/vestra-player-sdk/blob/main/CHANGELOG.md#1544) | **Nobody.** No changes to the TypeScript API, `apiVersion` stays `vestra/1`, no new capability. | — |
@@ -6697,7 +6709,9 @@ an item without `order` moves the whole menu to the top; to avoid that,
6697
6709
  give it an order in the menu's range (Video: 400–499).
6698
6710
 
6699
6711
  Commands.add throws `invalid` synchronously for a bad path,
6700
- order, group or binding. The same path registered twice, or a path that
6712
+ order or binding; the player refuses an empty or overlong text, a control
6713
+ character or whitespace at either end (`COMMAND_LIMITS`) with
6714
+ `invalid` (through `ready`). The same path registered twice, or a path that
6701
6715
  is both an item and a submenu, fails with `conflict` (through `ready`).
6702
6716
 
6703
6717
  ```ts
@@ -6765,9 +6779,12 @@ interface CommandOptions {
6765
6779
  * `Home`, `End`, `PageUp`, `PageDown`, `ArrowLeft`/`Right`/`Up`/`Down`,
6766
6780
  * `Plus`, any other single symbol as typed (`">"`, not `"Shift+."`),
6767
6781
  * `Numpad0`–`Numpad9`, `NumpadPlus`, `NumpadMinus`, `NumpadMultiply`,
6768
- * `NumpadDivide`, `NumpadEnter`, `NumpadDecimal`, and the video-area mouse
6782
+ * `NumpadDivide`, `NumpadEnter`, `NumpadDecimal`, the video-area mouse
6769
6783
  * inputs `Mouse:LeftClick`, `Mouse:LeftDoubleClick[:LeftHalf|:RightHalf]`,
6770
- * `Mouse:MiddleClick`, `Mouse:X1`, `Mouse:X2`, `Mouse:WheelUp`/`Down`/`Left`/`Right`.
6784
+ * `Mouse:MiddleClick`, `Mouse:WheelUp`/`Down`/`Left`/`Right`, and the side
6785
+ * buttons `Mouse:X1` (back), `Mouse:X2` (forward), from 0.6.0 on anywhere
6786
+ * in the main window or the floating sidebar window, except while a menu
6787
+ * is open.
6771
6788
  *
6772
6789
  * Platforms: on macOS `Ctrl+X` fires for both ⌃X and ⌘X while `Cmd+X`
6773
6790
  * means ⌘X only; on Windows `Cmd` is the Windows logo key, so write
@@ -6904,7 +6921,8 @@ interface Commands {
6904
6921
  *
6905
6922
  * Titles and paths cannot change in place: put changing items (track
6906
6923
  * lists, presets) in a child Scope and rebuild it.
6907
- * @throws invalid synchronously for a malformed path (including an unknown `@` menu id), order, group or binding; `conflict` for an id this plugin has already declared and not disposed.
6924
+ * @throws invalid synchronously for a malformed path (including an unknown `@` menu id), order or binding; `conflict` for an id this plugin has already declared and not disposed.
6925
+ * @throws invalid (`ready` rejects; during activation, activation fails) for a text the player refuses: an id, title, path segment, group or reason that is empty, longer than its `COMMAND_LIMITS` bytes (a text value counted by its English rendering), has a control character or whitespace at either end, or an id with other characters than ASCII letters, digits, `.`, `_` and `-`. `TestHost` refuses the same (SDK 1.5.48).
6908
6926
  * @capability commands
6909
6927
  * @example Pause playback from a menu item
6910
6928
  * Playback ▸ Pause (and Ctrl+P) pauses and resumes, shows a check mark
@@ -11252,13 +11270,13 @@ and the main window's fullscreen toggle.
11252
11270
  | --- | --- | --- |
11253
11271
  | `stop` | F4 | close the source |
11254
11272
  | `play-pause` | Space, a left click on the video (player 0.5.2) | pause / resume, replay after the end |
11255
- | `previous` / `next` | PageUp, Mouse:X1 / PageDown, Mouse:X2 | ask the sequence provider |
11273
+ | `previous` / `next` | PageUp, Mouse:X2 / PageDown, Mouse:X1 (from 0.6.0; up to 0.5.x X1 was previous and X2 next) | ask the sequence provider |
11256
11274
  | `frame-back` / `frame-forward` | D / F | `player.step("frame", …)` |
11257
11275
  | `seek-backward` / `seek-forward` | ← / → | relative seek by `setStepSeek` seconds (or `host.run`'s `seconds`), exact or to keyframes as `setStepSeek` says |
11258
11276
  | `rate-normal`, `rate-down`, `rate-up` | Z, X, C | 1×, ±0.1 within 0.25–4 |
11259
11277
  | `volume-up` / `volume-down` | ↑ / ↓, wheel | ±5 (`volume-up` stops at 100) |
11260
11278
  | `mute` | M | toggle mute |
11261
- | `fullscreen` | Enter, Alt+Enter, F11 | toggle the main window's fullscreen: enter it when windowed, leave it when fullscreen, as the Toggle Fullscreen command does (SDK 1.5.21, player 0.5.2; `unavailable` when the player has no window) |
11279
+ | `fullscreen` | Escape (from 0.6.0; up to 0.5.x Enter, Alt+Enter, F11) | toggle the main window's fullscreen: enter it when windowed, leave it when fullscreen, as the Toggle Fullscreen command does (SDK 1.5.21, player 0.5.2; `unavailable` when the player has no window) |
11262
11280
 
11263
11281
  In the binding overlay their keys are `host/<action>`. `fullscreen`
11264
11282
  changes no playback state: read the result from `window.state.fullscreen`
@@ -17307,6 +17325,9 @@ fails; later the failure goes to `Scope.onError`), commands
17307
17325
  already registered are unaffected, and disposing a command frees its slot
17308
17326
  at once. TestHost enforces the same caps.
17309
17327
 
17328
+ The other fields are the player's rules for one declaration (since
17329
+ 1.5.48): a command past them is refused with `invalid`, the same way.
17330
+
17310
17331
  For menus built from media content (tracks, chapters, presets), set your
17311
17332
  own sensible display cap well below `perPlugin`.
17312
17333
 
@@ -17316,6 +17337,22 @@ const COMMAND_LIMITS: {
17316
17337
  readonly perPlugin: 160;
17317
17338
  /** Commands of every plugin plus the player's own, together (1024). */
17318
17339
  readonly total: 1024;
17340
+ /** Segments of a menu path, the leaf included. */
17341
+ readonly depth: 8;
17342
+ /** UTF-8 bytes of a command's local id: ASCII letters, digits, `.`, `_` and `-`. */
17343
+ readonly idBytes: 128;
17344
+ /**
17345
+ * UTF-8 bytes of a title, a text value counted by its English rendering. Like every command text: not empty, no control characters, no whitespace at either end.
17346
+ */
17347
+ readonly titleBytes: 128;
17348
+ /** UTF-8 bytes of a menu path segment, a text value counted by its English rendering. */
17349
+ readonly segmentBytes: 80;
17350
+ /** UTF-8 bytes of a group. */
17351
+ readonly groupBytes: 80;
17352
+ /** UTF-8 bytes of a state's reason, as shown in the player's language. */
17353
+ readonly reasonBytes: 512;
17354
+ /** The largest `order` either way: an order is within ±1,000,000. */
17355
+ readonly order: 1000000;
17319
17356
  };
17320
17357
  ```
17321
17358
 
@@ -17625,7 +17662,9 @@ const BINDING_INPUTS: Readonly<{
17625
17662
  * Other named keys, matched by the physical key. `Plus` is the `+` key however it is typed; the `Numpad…` names are the numeric keypad only, never the main keyboard.
17626
17663
  */
17627
17664
  readonly named: readonly ["Space", "Enter", "Tab", "Escape", "Backspace", "Delete", "Insert", "Home", "End", "PageUp", "PageDown", "ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown", "Plus", ...string[], "NumpadPlus", "NumpadMinus", "NumpadMultiply", "NumpadDivide", "NumpadEnter", "NumpadDecimal"];
17628
- /** Mouse inputs on the video area. `:LeftHalf` and `:RightHalf` tell the halves of the video apart for a double click. */
17665
+ /**
17666
+ * Mouse inputs on the video area; the side buttons `Mouse:X1` (back) and `Mouse:X2` (forward) anywhere in the main window or the floating sidebar window, except while a menu is open (player 0.6.0). `:LeftHalf` and `:RightHalf` tell the halves of the video apart for a double click.
17667
+ */
17629
17668
  readonly mouse: readonly ["Mouse:LeftClick", "Mouse:LeftDoubleClick", "Mouse:LeftDoubleClick:LeftHalf", "Mouse:LeftDoubleClick:RightHalf", "Mouse:MiddleClick", "Mouse:X1", "Mouse:X2", "Mouse:WheelUp", "Mouse:WheelDown", "Mouse:WheelLeft", "Mouse:WheelRight"];
17630
17669
  }>;
17631
17670
  ```
package/llms.txt CHANGED
@@ -1,6 +1,6 @@
1
1
  # Vestra Player SDK
2
2
 
3
- > @vestra-player/sdk 1.5.46: write plugins for Vestra Player in TypeScript (plugin API `vestra/1`). A plugin is a folder with `manifest.json` and an entry module whose default export `activate(plugin)` the player calls; it runs in its own Node.js process (the player's built-in plugins share one). Everything a plugin creates (commands, windows, panels, filters, listeners…) is a handle that is released automatically when the plugin stops. L<n>: a line of llms-full.txt (next to this file), which has every page, declaration and example.
3
+ > @vestra-player/sdk 1.5.48: write plugins for Vestra Player in TypeScript (plugin API `vestra/1`). A plugin is a folder with `manifest.json` and an entry module whose default export `activate(plugin)` the player calls; it runs in its own Node.js process (the player's built-in plugins share one). Everything a plugin creates (commands, windows, panels, filters, listeners…) is a handle that is released automatically when the plugin stops. L<n>: a line of llms-full.txt (next to this file), which has every page, declaration and example.
4
4
 
5
5
  **Rules**
6
6
 
@@ -80,7 +80,7 @@ export default async function activate(plugin: Plugin) {
80
80
  - `video.upscaling`: VideoUpscaling.state/set/onChange
81
81
  - No capability: `tools`, `storage`, `system`, `app`, `ui` (images and Markdown), `i18n`, reading window.state, appearance.theme, packages.list and rendering.state. Details: L2293–2353.
82
82
 
83
- **Test**: `tests/*.test.ts` with `node:test`, run by `vestra-plugin test`. `startPlugin(activate, { directory: new URL("..", import.meta.url) })` from `@vestra-player/sdk/testing` returns `{ host, stop }`, `host` an in-memory player (TestHost): act (`host.command(id).run()`, `host.press(key)`), `await host.settle()`, assert on `host.osd`, `host.window(id).props`… Guide: L1861–2021; TestHost: L15484.
83
+ **Test**: `tests/*.test.ts` with `node:test`, run by `vestra-plugin test`. `startPlugin(activate, { directory: new URL("..", import.meta.url) })` from `@vestra-player/sdk/testing` returns `{ host, stop }`, `host` an in-memory player (TestHost): act (`host.command(id).run()`, `host.press(key)`), `await host.settle()`, assert on `host.osd`, `host.window(id).props`… Guide: L1861–2021; TestHost: L15502.
84
84
 
85
85
  **Command line**
86
86
 
@@ -108,40 +108,40 @@ export default async function activate(plugin: Plugin) {
108
108
  - `limit`: A player-wide limit was reached, a property value is over the limits of PropValue
109
109
  - `reserved`: The id is `vestra` or starts with `vestra.`
110
110
  - `versionMismatch`: The package needs a newer player
111
- - Others: PluginError, llms-full.txt L5954.
111
+ - Others: PluginError, llms-full.txt L5966.
112
112
 
113
113
  ## Reference
114
114
 
115
115
  - [llms-full.txt](https://vestrasys.github.io/vestra-player-sdk/llms-full.txt), also node_modules/@vestra-player/sdk/llms-full.txt; pages: [site](https://vestrasys.github.io/vestra-player-sdk/).
116
116
  - Tutorials: Getting started L9–134; Add a window L135–213; Add settings and translations L214–367; Test, check, pack and publish L368–424; Write your own blur kernel L425–685
117
117
  - How-to: Control playback L686–854; Store a plugin's data L855–961; Load subtitles from a plugin L962–1074; Keep a slider responsive while dragging L1075–1262; Remember a window's state between runs L1263–1414; Offer a GPU rendering switch L1415–1540; Keep a command's state without keeping the plugin running L1541–1697; Translate a plugin L1698–1860; Test a plugin L1861–2021; Publish a plugin L2022–2123; Troubleshooting L2124–2217
118
- - Reference: Manifest L2218–2292; Capabilities L2293–2353; Limits L2354–2499; Commands, menus and bindings L2500–3025; Platforms L3026–3056; Theme L3057–3406; Command line L3407–3464; Error codes L3465–3512; Translations L3513–3605; Test host L3606–3792
119
- - Explanation: How a plugin runs L3793–3995; Commands, menus and key bindings L3996–4150; Playback state, controls and sources L4151–4310; Video and audio processing L4311–4431; Windows, panels, settings pages, dialogs and the OSD L4432–4694; GPU and software rendering L4695–4711; The player's Rust core L4712–5053; How the player runs and talks to plugins L5054–5393
120
- - Guidelines: Plugin rules L5394–5405; Interface guidelines L5406–5522; Menu guidelines L5523–5581
121
- - Upgrading: Migrate a plugin to English source text L5582–5731; Migrating off package rollback (1.5.16, removed in 1.5.17) L5732–5809; Who must change, release by release L5810–5876
122
- - [API: @vestra-player/sdk](https://vestrasys.github.io/vestra-player-sdk/modules/_vestra-player_sdk.html): L5877–15157
123
- - [API: @vestra-player/sdk/testing](https://vestrasys.github.io/vestra-player-sdk/modules/_vestra-player_sdk_testing.html): L15158–18020
118
+ - Reference: Manifest L2218–2292; Capabilities L2293–2353; Limits L2354–2506; Commands, menus and bindings L2507–3033; Platforms L3034–3064; Theme L3065–3414; Command line L3415–3472; Error codes L3473–3520; Translations L3521–3613; Test host L3614–3800
119
+ - Explanation: How a plugin runs L3801–4003; Commands, menus and key bindings L4004–4158; Playback state, controls and sources L4159–4318; Video and audio processing L4319–4439; Windows, panels, settings pages, dialogs and the OSD L4440–4704; GPU and software rendering L4705–4721; The player's Rust core L4722–5063; How the player runs and talks to plugins L5064–5403
120
+ - Guidelines: Plugin rules L5404–5415; Interface guidelines L5416–5532; Menu guidelines L5533–5591
121
+ - Upgrading: Migrate a plugin to English source text L5592–5741; Migrating off package rollback (1.5.16, removed in 1.5.17) L5742–5819; Who must change, release by release L5820–5888
122
+ - [API: @vestra-player/sdk](https://vestrasys.github.io/vestra-player-sdk/modules/_vestra-player_sdk.html): L5889–15175
123
+ - [API: @vestra-player/sdk/testing](https://vestrasys.github.io/vestra-player-sdk/modules/_vestra-player_sdk_testing.html): L15176–18059
124
124
 
125
125
  ## API
126
126
 
127
127
  Where each `plugin.<module>` is declared:
128
128
 
129
- - Plugin L6293, Scope L6029, PluginError L5954, Disposable L5892
130
- - Commands L6883; When L6480; Windows L7829; Panels L9275; Dialogs L9945; Player L10686; video: VideoFilter L11566, VideoView L11666, RenderOptions L11753, VideoShaders L11831, VideoUpscaling L12189; audio: AudioFilter L11611; Subtitles L12425; media: MediaCapture L12829; Resolvers L13059; Packages L13461; Osd L11431; Host L11273; Clipboard L13790; Power L13877; Tools L13936; Storage L14088; System L14287; Secrets L14367; window: MainWindow L9592; App L9772; Rendering L14630; Bindings L13689; Ui L7175; Timeline L9504; Drops L8522; Appearance L8603; I18n L8836; Settings L9119.
129
+ - Plugin L6305, Scope L6041, PluginError L5966, Disposable L5904
130
+ - Commands L6900; When L6492; Windows L7847; Panels L9293; Dialogs L9963; Player L10704; video: VideoFilter L11584, VideoView L11684, RenderOptions L11771, VideoShaders L11849, VideoUpscaling L12207; audio: AudioFilter L11629; Subtitles L12443; media: MediaCapture L12847; Resolvers L13077; Packages L13479; Osd L11449; Host L11291; Clipboard L13808; Power L13895; Tools L13954; Storage L14106; System L14305; Secrets L14385; window: MainWindow L9610; App L9790; Rendering L14648; Bindings L13707; Ui L7193; Timeline L9522; Drops L8540; Appearance L8621; I18n L8854; Settings L9137.
131
131
 
132
132
  ## Examples
133
133
 
134
134
  Tested plugins in [examples/](https://github.com/vestrasys/vestra-player-sdk/tree/main/examples):
135
135
 
136
- - hello: The README Quick start: one menu command and an on-screen message. L18455–18550
137
- - blur: A tool window with one slider that blurs the playing video. L18551–18818
138
- - signal-generator: Generates its own video and audio: colour bars and a 1 kHz tone. L18819–19157
139
- - bookmark: A tool window bound to the playback state: remember the position and jump back to it. L19158–19350
140
- - panel-menu: A sidebar panel with its own right-click menu. L19351–19565
141
- - menus: Radio menu items, separator groups and a menu item whose title changes. L19566–19731
142
- - theme-widgets: A tool window drawn with the player's theme and widgets. L19732–19901
143
- - markdown: A window that shows a Markdown document. L19902–20104
144
- - i18n: A plugin in the player's language: English source text, a Chinese translation. L20105–20278
136
+ - hello: The README Quick start: one menu command and an on-screen message. L18494–18589
137
+ - blur: A tool window with one slider that blurs the playing video. L18590–18857
138
+ - signal-generator: Generates its own video and audio: colour bars and a 1 kHz tone. L18858–19196
139
+ - bookmark: A tool window bound to the playback state: remember the position and jump back to it. L19197–19389
140
+ - panel-menu: A sidebar panel with its own right-click menu. L19390–19604
141
+ - menus: Radio menu items, separator groups and a menu item whose title changes. L19605–19770
142
+ - theme-widgets: A tool window drawn with the player's theme and widgets. L19771–19940
143
+ - markdown: A window that shows a Markdown document. L19941–20143
144
+ - i18n: A plugin in the player's language: English source text, a Chinese translation. L20144–20317
145
145
 
146
146
  ## Optional
147
147
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vestra-player/sdk",
3
- "version": "1.5.46",
3
+ "version": "1.5.48",
4
4
  "description": "Plugin API types, test host and plugin CLI for Vestra Player (apiVersion vestra/1)",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {