@vestra-player/sdk 1.5.47 → 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,21 @@
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
+
3
19
  ## 1.5.47
4
20
 
5
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.
@@ -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
@@ -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
@@ -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)
@@ -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)
@@ -1190,8 +1192,15 @@ export interface Commands {
1190
1192
  * lists, presets) in a child {@link Scope} and rebuild it.
1191
1193
  *
1192
1194
  * @throws `invalid` synchronously for a malformed path (including an
1193
- * unknown `@` menu id), order, group or binding; `conflict` for an id this
1195
+ * unknown `@` menu id), order or binding; `conflict` for an id this
1194
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).
1195
1204
  *
1196
1205
  * @example Pause playback from a menu item
1197
1206
  * Playback ▸ Pause (and Ctrl+P) pauses and resumes, shows a check mark
package/llms-full.txt CHANGED
@@ -1,4 +1,4 @@
1
- # @vestra-player/sdk 1.5.47: 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
 
@@ -5820,6 +5827,7 @@ Every SDK release's CHANGELOG entry opens with who must change and what happens
5820
5827
 
5821
5828
  | Release | Who must change | Guide |
5822
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. | — |
5823
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. | — |
5824
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. | — |
5825
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. | — |
@@ -6701,7 +6709,9 @@ an item without `order` moves the whole menu to the top; to avoid that,
6701
6709
  give it an order in the menu's range (Video: 400–499).
6702
6710
 
6703
6711
  Commands.add throws `invalid` synchronously for a bad path,
6704
- 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
6705
6715
  is both an item and a submenu, fails with `conflict` (through `ready`).
6706
6716
 
6707
6717
  ```ts
@@ -6911,7 +6921,8 @@ interface Commands {
6911
6921
  *
6912
6922
  * Titles and paths cannot change in place: put changing items (track
6913
6923
  * lists, presets) in a child Scope and rebuild it.
6914
- * @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).
6915
6926
  * @capability commands
6916
6927
  * @example Pause playback from a menu item
6917
6928
  * Playback ▸ Pause (and Ctrl+P) pauses and resumes, shows a check mark
@@ -17314,6 +17325,9 @@ fails; later the failure goes to `Scope.onError`), commands
17314
17325
  already registered are unaffected, and disposing a command frees its slot
17315
17326
  at once. TestHost enforces the same caps.
17316
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
+
17317
17331
  For menus built from media content (tracks, chapters, presets), set your
17318
17332
  own sensible display cap well below `perPlugin`.
17319
17333
 
@@ -17323,6 +17337,22 @@ const COMMAND_LIMITS: {
17323
17337
  readonly perPlugin: 160;
17324
17338
  /** Commands of every plugin plus the player's own, together (1024). */
17325
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;
17326
17356
  };
17327
17357
  ```
17328
17358
 
package/llms.txt CHANGED
@@ -1,6 +1,6 @@
1
1
  # Vestra Player SDK
2
2
 
3
- > @vestra-player/sdk 1.5.47: 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: L15491.
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 L5958.
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–3026; Platforms L3027–3057; Theme L3058–3407; Command line L3408–3465; Error codes L3466–3513; Translations L3514–3606; Test host L3607–3793
119
- - Explanation: How a plugin runs L3794–3996; Commands, menus and key bindings L3997–4151; Playback state, controls and sources L4152–4311; Video and audio processing L4312–4432; Windows, panels, settings pages, dialogs and the OSD L4433–4697; GPU and software rendering L4698–4714; The player's Rust core L4715–5056; How the player runs and talks to plugins L5057–5396
120
- - Guidelines: Plugin rules L5397–5408; Interface guidelines L5409–5525; Menu guidelines L5526–5584
121
- - Upgrading: Migrate a plugin to English source text L5585–5734; Migrating off package rollback (1.5.16, removed in 1.5.17) L5735–5812; Who must change, release by release L5813–5880
122
- - [API: @vestra-player/sdk](https://vestrasys.github.io/vestra-player-sdk/modules/_vestra-player_sdk.html): L5881–15164
123
- - [API: @vestra-player/sdk/testing](https://vestrasys.github.io/vestra-player-sdk/modules/_vestra-player_sdk_testing.html): L15165–18029
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 L6297, Scope L6033, PluginError L5958, Disposable L5896
130
- - Commands L6890; When L6484; Windows L7836; Panels L9282; Dialogs L9952; Player L10693; video: VideoFilter L11573, VideoView L11673, RenderOptions L11760, VideoShaders L11838, VideoUpscaling L12196; audio: AudioFilter L11618; Subtitles L12432; media: MediaCapture L12836; Resolvers L13066; Packages L13468; Osd L11438; Host L11280; Clipboard L13797; Power L13884; Tools L13943; Storage L14095; System L14294; Secrets L14374; window: MainWindow L9599; App L9779; Rendering L14637; Bindings L13696; Ui L7182; Timeline L9511; Drops L8529; Appearance L8610; I18n L8843; Settings L9126.
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. L18464–18559
137
- - blur: A tool window with one slider that blurs the playing video. L18560–18827
138
- - signal-generator: Generates its own video and audio: colour bars and a 1 kHz tone. L18828–19166
139
- - bookmark: A tool window bound to the playback state: remember the position and jump back to it. L19167–19359
140
- - panel-menu: A sidebar panel with its own right-click menu. L19360–19574
141
- - menus: Radio menu items, separator groups and a menu item whose title changes. L19575–19740
142
- - theme-widgets: A tool window drawn with the player's theme and widgets. L19741–19910
143
- - markdown: A window that shows a Markdown document. L19911–20113
144
- - i18n: A plugin in the player's language: English source text, a Chinese translation. L20114–20287
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.47",
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": {