@vestra-player/sdk 1.5.47 → 1.5.49

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,31 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.5.49
4
+
5
+ **Who must change: nobody.** No changes to the plugin API (`@vestra-player/sdk/runtime` gains the additive `RuntimeOptions` and an optional sixth `createPlugin` argument), `apiVersion` stays `vestra/1`, no new capability, no player version needed, and the player's runner needs no change. **Behaviour change, approved by the maintainer (2026-11-01):** in the player, a command declaration it refuses during activation no longer fails the whole plugin. What `commands.add` checks itself (an unknown `@` menu id, more than 8 path segments, an `order` out of range, a malformed or repeated binding, a bad `button`, a reason text of the wrong shape) still throws from `add`. A plugin whose `activate` relied on that failure (none is known) now runs without the refused command.
6
+
7
+ - **A refused command declaration is skipped, not fatal.** When the player answers a `commands.add` made during activation with `invalid`, `conflict` or `limit` (a text over its limit, a path another command has, too many commands), the SDK logs it with the command id and the player's message (`[<plugin>] command <id> skipped, the player refused it (conflict): …`), skips that command and lets the rest of the plugin activate: its other commands, panels, settings pages and resolvers register and the player gets `runtime.ready`. Before, the first refusal made activation fail (`runtime.failed`) and the plugin's menus and panels were withdrawn. The command's `ready` still rejects, and the error reaches the scope's `onError` once activation completes, so a listener added anywhere in `activate` hears it. This is how a slot, sequence or resolver another plugin holds was already treated, and how VS Code treats a bad contribution (logged, the extension activates). Other failures during activation (`capability`, a timeout, an error of the player) still fail it, and a refusal after activation goes to `onError` as before.
8
+ - **A refused command sends no updates.** After a definite refusal of its registration, later state changes (`set`, a binding) send nothing, instead of updates the player answers with `command missing`; dispose it to declare the id again.
9
+ - **`TestHost` refuses a binding listed twice as player 0.6.1 does.** The player now checks a command's bindings with its binding table (`Table::canonical`) inside the registration: each binding parsed and each input once, compared as parsed inputs, before the reason, the id and path conflicts and the limits, with the `invalid command: ` prefix (`invalid command: binding F3 is listed twice`). `TestHost` answered it without the prefix and only after the conflict and limit checks; it now gives the same answer in the same order.
10
+ - **`TestHost` stays strict.** `startPlugin` still rejects when a command declaration is refused during activation, so a plugin's tests show the bad declaration. Fix the declaration, not the test. (`createPlugin` in `@vestra-player/sdk/runtime` takes `{ strictDeclarations }`; `startPlugin` sets it, the player's runner does not.)
11
+ - _Docs_: [Example: Panel menu](docs/examples/panel-menu.md) draws its list in a `ListView` instead of a `for` in a `VerticalLayout`; the [interface guidelines](docs/guidelines/interface.md#panels) gain **Long lists use `ListView`**. `Command.ready`, `Commands`, `Plugin`, `startPlugin`, [The plugin model](docs/explanation/plugins.md), [Host and Node](docs/explanation/host-and-node.md) and [Limits](docs/reference/limits.md) describe the new activation rule.
12
+
13
+ ## 1.5.48
14
+
15
+ **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.
16
+
17
+ - **`TestHost` checks a command declaration as the player does** (`commands.register`, in the player's order, with its codes and messages):
18
+ - the local id: 1–128 bytes of ASCII letters, digits, `.`, `_` and `-` (`invalid`);
19
+ - 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`;
20
+ - at most 8 path segments, an `order` within ±1,000,000, bindings in the player's syntax, each once (`invalid`);
21
+ - 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);
22
+ - 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;
23
+ - 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;
24
+ - 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.
25
+ - **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.
26
+ - **`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.
27
+ - _Commands_: `CommandOptions` and `Commands.add` say which checks the player makes (`ready` rejects with `invalid`) and which `add` makes at once.
28
+
3
29
  ## 1.5.47
4
30
 
5
31
  **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
@@ -8,7 +8,7 @@
8
8
  * @packageDocumentation
9
9
  */
10
10
  export { createPlugin } from "./plugin/index.ts";
11
- export type { PluginInstance, PluginManifest } from "./plugin/index.ts";
11
+ export type { PluginInstance, PluginManifest, RuntimeOptions, } from "./plugin/index.ts";
12
12
  export { HostError } from "./wire.ts";
13
13
  export type { Json, Transport } from "./wire.ts";
14
14
  export { API_VERSION, CAPABILITIES, CAPABILITY_INFO, MESSAGE_LIMIT, } from "./capabilities.ts";
@@ -1,5 +1,5 @@
1
1
  /** Menu commands: declaration, state sync (revisions hidden) and dispatch. */
2
- import { failure, Latest, quiet } from "./runtime.js";
2
+ import { failure, Latest, quiet, uncertain } from "./runtime.js";
3
3
  import { checkBindings } from "./bindings.js";
4
4
  import { BAR_BUTTON_ICONS, BAR_BUTTON_LIMITS, MENU_IDS } from "../commands.js";
5
5
  import { ImageHandle } from "./ui.js";
@@ -100,6 +100,10 @@ class CommandHandle {
100
100
  #last = "";
101
101
  #disposed = false;
102
102
  #registered = false;
103
+ /** Its entry in `runtime.commands` (a later handle may own the id). */
104
+ #run = () => { };
105
+ /** The player refused the declaration: no state updates follow. */
106
+ #refused = false;
103
107
  #pump;
104
108
  #label;
105
109
  /** Where the plugin added it, for the player's log. */
@@ -124,10 +128,20 @@ class CommandHandle {
124
128
  // registration itself is revision 1, so every later state follows it.
125
129
  const previous = removing(runtime).get(this.id);
126
130
  this.#pump = new Latest(async ({ state, when }) => {
131
+ if (this.#refused)
132
+ return;
127
133
  const revision = ++this.#revision;
128
134
  if (revision === 1) {
129
135
  await previous;
130
- await runtime.host.commands.register(this.id, 1, declaration, state, when);
136
+ try {
137
+ await runtime.host.commands.register(this.id, 1, declaration, state, when);
138
+ }
139
+ catch (error) {
140
+ // A definite refusal: the player has no such command to update.
141
+ if (!uncertain(error))
142
+ this.#refused = true;
143
+ throw error;
144
+ }
131
145
  this.#registered = true;
132
146
  }
133
147
  else
@@ -136,12 +150,13 @@ class CommandHandle {
136
150
  const initial = this.#state();
137
151
  this.#last = JSON.stringify(initial);
138
152
  this.ready = quiet(runtime
139
- .starting(this.#pump.push(initial), (error) => scope.report(error))
153
+ .declaring(this.#pump.push(initial), `command ${this.id}`, (error) => scope.report(error))
140
154
  .then(() => { }));
141
- runtime.commands.set(this.id, () => {
155
+ this.#run = () => {
142
156
  if (!this.#disposed)
143
157
  scope.run(options.run);
144
- });
158
+ };
159
+ runtime.commands.set(this.id, this.#run);
145
160
  this.#binding = runtime.bind(() => this.#sync());
146
161
  scope.add(this);
147
162
  }
@@ -177,7 +192,7 @@ class CommandHandle {
177
192
  return Object.keys(when).length ? { state, when } : { state };
178
193
  }
179
194
  #sync() {
180
- if (this.#disposed)
195
+ if (this.#disposed || this.#refused)
181
196
  return;
182
197
  const state = this.#state();
183
198
  const encoded = JSON.stringify(state);
@@ -196,7 +211,8 @@ class CommandHandle {
196
211
  this.#disposed = true;
197
212
  this.#scope.forget(this);
198
213
  const runtime = this.#scope.runtime;
199
- runtime.commands.delete(this.id);
214
+ if (runtime.commands.get(this.id) === this.#run)
215
+ runtime.commands.delete(this.id);
200
216
  this.#pump.stop();
201
217
  // Removed from the menu once registered. Recorded before any await, so an
202
218
  // immediate re-add of the same id registers after this removal.
@@ -6,16 +6,23 @@
6
6
  * @internal
7
7
  */
8
8
  import type { Transport } from "../wire.ts";
9
- import type { PluginManifest } from "./runtime.ts";
9
+ import type { PluginManifest, RuntimeOptions } from "./runtime.ts";
10
10
  import type { Plugin } from "./types.ts";
11
11
  export type * from "./types.ts";
12
- export type { PluginManifest };
12
+ export type { PluginManifest, RuntimeOptions };
13
13
  export interface PluginInstance {
14
14
  plugin: Plugin;
15
- /** Wait for registrations started during activation; rejects if one failed. */
15
+ /**
16
+ * Wait for registrations started during activation; rejects if one failed.
17
+ * A command declaration the player refused is not a failure (it is
18
+ * logged, skipped and reported to `onError`) unless
19
+ * `RuntimeOptions.strictDeclarations` is set.
20
+ */
16
21
  activated(): Promise<void>;
17
22
  dispose(): Promise<void>;
18
23
  }
19
24
  export declare function createPlugin(transport: Transport, manifest: PluginManifest, directory: string, dataDirectory?: string,
20
25
  /** `plugin.app` (the runner passes the player's `VESTRA_APP_INFO`). */
21
- app?: unknown): Promise<PluginInstance>;
26
+ app?: unknown,
27
+ /** `TestHost` sets `strictDeclarations`; the player's runner does not. */
28
+ options?: RuntimeOptions): Promise<PluginInstance>;
@@ -5,8 +5,10 @@ import { windowOf } from "./window.js";
5
5
  import { timelineOf } from "./timeline.js";
6
6
  export async function createPlugin(transport, manifest, directory, dataDirectory,
7
7
  /** `plugin.app` (the runner passes the player's `VESTRA_APP_INFO`). */
8
- app) {
9
- const runtime = new Runtime(transport, manifest, directory, dataDirectory, app);
8
+ app,
9
+ /** `TestHost` sets `strictDeclarations`; the player's runner does not. */
10
+ options) {
11
+ const runtime = new Runtime(transport, manifest, directory, dataDirectory, app, options);
10
12
  // Player, window and timeline state are synchronous for plugins: read them first.
11
13
  await Promise.all([
12
14
  playerOf(runtime).init(),
@@ -37,6 +37,15 @@ export type Change = ReadonlyMap<string, ReadonlySet<string>>;
37
37
  * without `update()`) still shows within about this long.
38
38
  */
39
39
  export declare const SAFETY_PASS_MS = 1000;
40
+ /** How the plugin is run: the player's runner, or `TestHost`. */
41
+ export interface RuntimeOptions {
42
+ /**
43
+ * A declaration the player refuses during activation fails activation
44
+ * (`startPlugin`, so a plugin's tests show it) instead of being logged,
45
+ * reported to `onError` and skipped (the player's runner).
46
+ */
47
+ strictDeclarations?: boolean;
48
+ }
40
49
  export declare const same: (a: Json | undefined, b: Json | undefined) => boolean;
41
50
  /** One value in flight, only the newest waiting. Each push learns its fate. */
42
51
  export declare class Latest<T> {
@@ -74,7 +83,7 @@ export declare class Runtime {
74
83
  * property writes). Not part of the plugin API; empty for older players.
75
84
  */
76
85
  readonly features: ReadonlySet<string>;
77
- constructor(transport: Transport, manifest: PluginManifest, directory: string, dataDirectory?: string, app?: unknown);
86
+ constructor(transport: Transport, manifest: PluginManifest, directory: string, dataDirectory?: string, app?: unknown, options?: RuntimeOptions);
78
87
  can(capability: string): boolean;
79
88
  /** Lazily created singletons shared by every scope of this plugin. */
80
89
  service<T>(name: string, create: () => T): T;
@@ -83,6 +92,16 @@ export declare class Runtime {
83
92
  * plugin reports ready; later ones report failures to their scope.
84
93
  */
85
94
  starting<T>(work: Promise<T>, report: (error: unknown) => void): Promise<T>;
95
+ /**
96
+ * A declaration the player checks on its own (`what`: "command x"),
97
+ * started like `starting`. In the player's runner a refusal of it during
98
+ * activation (`invalid`, `conflict`, `limit`) does not fail the plugin:
99
+ * it is logged, skipped and reported to the scope once activation
100
+ * completes, so an `onError` added anywhere in `activate` hears it (SDK
101
+ * 1.5.49; VS Code logs a bad contribution and activates the extension).
102
+ * Under `strictDeclarations` (TestHost) it fails activation as before.
103
+ */
104
+ declaring<T>(work: Promise<T>, what: string, report: (error: unknown) => void): Promise<T>;
86
105
  activated(): Promise<void>;
87
106
  bind(evaluate: () => void): Disposable;
88
107
  /**
@@ -86,6 +86,16 @@ function merge(into, from) {
86
86
  * without `update()`) still shows within about this long.
87
87
  */
88
88
  export const SAFETY_PASS_MS = 1000;
89
+ /**
90
+ * The player's answers to a declaration that refuse that one declaration
91
+ * (a command's path, texts or count): during activation they skip it
92
+ * instead of failing the plugin (`Runtime.declaring`, SDK 1.5.49).
93
+ */
94
+ const DECLARATION_REFUSALS = new Set([
95
+ "invalid",
96
+ "conflict",
97
+ "limit",
98
+ ]);
89
99
  export const same = (a, b) => JSON.stringify(a) === JSON.stringify(b);
90
100
  /** One value in flight, only the newest waiting. Each push learns its fate. */
91
101
  export class Latest {
@@ -149,6 +159,10 @@ export class Runtime {
149
159
  /** Registrations that activation must wait for before reporting ready. */
150
160
  startup = new Set();
151
161
  #activating = true;
162
+ /** `RuntimeOptions.strictDeclarations`. */
163
+ #strict;
164
+ /** Declarations skipped during activation: reported once it completes. */
165
+ #refusals = [];
152
166
  #transport;
153
167
  /** Each binding with the events whose state it read last time (held). */
154
168
  #bindings = new Map();
@@ -187,8 +201,9 @@ export class Runtime {
187
201
  * property writes). Not part of the plugin API; empty for older players.
188
202
  */
189
203
  features;
190
- constructor(transport, manifest, directory, dataDirectory, app) {
204
+ constructor(transport, manifest, directory, dataDirectory, app, options = {}) {
191
205
  this.#transport = transport;
206
+ this.#strict = options.strictDeclarations ?? false;
192
207
  this.dataDirectory = dataDirectory;
193
208
  this.local = transport.local;
194
209
  this.host = createWire(transport);
@@ -233,6 +248,32 @@ export class Runtime {
233
248
  work.catch(() => { });
234
249
  return work;
235
250
  }
251
+ /**
252
+ * A declaration the player checks on its own (`what`: "command x"),
253
+ * started like `starting`. In the player's runner a refusal of it during
254
+ * activation (`invalid`, `conflict`, `limit`) does not fail the plugin:
255
+ * it is logged, skipped and reported to the scope once activation
256
+ * completes, so an `onError` added anywhere in `activate` hears it (SDK
257
+ * 1.5.49; VS Code logs a bad contribution and activates the extension).
258
+ * Under `strictDeclarations` (TestHost) it fails activation as before.
259
+ */
260
+ declaring(work, what, report) {
261
+ if (!this.#activating || this.#strict)
262
+ return this.starting(work, report);
263
+ const settled = work.then(() => { }, (error) => {
264
+ if (!(error instanceof HostError) ||
265
+ !DECLARATION_REFUSALS.has(error.code))
266
+ throw error;
267
+ console.error(`[${this.manifest.id}] ${what} skipped, the player refused it (${error.code}): ${error.message}`);
268
+ this.#refusals.push(() => report(error));
269
+ });
270
+ // Observed now: `activated()` may come after `activate` awaits, and
271
+ // its `allSettled` still sees the rejection.
272
+ settled.catch(() => { });
273
+ this.startup.add(settled);
274
+ work.catch(() => { });
275
+ return work;
276
+ }
236
277
  async activated() {
237
278
  while (this.startup.size) {
238
279
  const pending = [...this.startup];
@@ -242,6 +283,8 @@ export class Runtime {
242
283
  throw result.reason;
243
284
  }
244
285
  this.#activating = false;
286
+ for (const report of this.#refusals.splice(0))
287
+ report();
245
288
  }
246
289
  bind(evaluate) {
247
290
  const held = new Map();
@@ -2728,8 +2728,11 @@ export declare class TestHost implements Transport {
2728
2728
  *
2729
2729
  * Rejects when `manifest.json` cannot be read or parsed, when `activate`
2730
2730
  * throws, or when a registration made during activation fails (for example
2731
- * `limit`, `conflict`, `invalid`), which is when the player would fail the
2732
- * activation.
2731
+ * `limit`, `conflict`, `invalid`). Stricter than the player's runner for
2732
+ * commands (SDK 1.5.49): there the SDK logs a command declaration the
2733
+ * player refuses during activation, skips it and activates the rest of the
2734
+ * plugin (`onError` hears it); `startPlugin` fails, so a test shows the
2735
+ * bad declaration.
2733
2736
  *
2734
2737
  * @param activate - The plugin's default-exported `activate` function.
2735
2738
  * @param options - Where the plugin lives and how to start it.
@@ -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,97 @@ 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
+ // The binding table's `Table::canonical` (player 0.6.1): each binding
3960
+ // parsed, and each input once, compared parsed, before the reason,
3961
+ // conflicts and limits.
3962
+ const bindings = (declaration.bindings ?? []);
3963
+ const seen = new Set();
3964
+ for (const binding of bindings.map(String)) {
3965
+ let input;
3966
+ try {
3967
+ input = canonical(binding);
3968
+ }
3969
+ catch (error) {
3970
+ throw commandInvalid(error.message);
3971
+ }
3972
+ if (seen.has(input))
3973
+ throw commandInvalid(`binding ${binding} is listed twice`);
3974
+ seen.add(input);
3975
+ }
3976
+ if (reason !== null)
3977
+ commandText(reason, "reason", COMMAND_LIMITS.reasonBytes);
3844
3978
  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.
3979
+ throw commandConflict("owner/localId already registered");
3980
+ // The player's own commands beside the core actions (host/actions.rs
3981
+ // `host_declarations`: Toggle Fullscreen, Quit and three outside the
3982
+ // menu), which take part in conflicts and the total like any other.
3983
+ const hostOnly = BUILTIN_COMMANDS.filter((row) => row.owner === "host" && !this.coreCommands.has(`host/${row.id}`));
3984
+ // A path may not repeat another command's (the player's own
3985
+ // included) nor be a submenu of one, or one of its submenus.
3986
+ if (path.length > 0)
3987
+ for (const existing of [
3988
+ ...hostOnly.map((row) => row.path),
3989
+ ...[...this.coreCommands.values()].map((command) => command.declaration.path),
3990
+ ...[...this.commands.values()].map((command) => menuKeys((command.declaration.path ?? []).map(menuTextKey))),
3991
+ ]) {
3992
+ const shared = Math.min(existing.length, path.length);
3993
+ if (existing.length > 0 &&
3994
+ existing.slice(0, shared).every((s, i) => s === path[i]))
3995
+ throw commandConflict("menu path duplicates a command or collides with a leaf/group");
3996
+ }
3997
+ // The player's caps (COMMAND_LIMITS): the total counts all of the
3998
+ // player's own commands too.
3847
3999
  if (this.commands.size >= COMMAND_LIMITS.perPlugin ||
3848
- this.commands.size + this.coreCommands.size >= COMMAND_LIMITS.total)
4000
+ this.commands.size + this.coreCommands.size + hostOnly.length >=
4001
+ COMMAND_LIMITS.total)
3849
4002
  throw new HostError("limit", "command registry limit reached");
3850
4003
  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");
3868
- }
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);
3874
4004
  let sent = p.state;
3875
4005
  this.commands.set(id, {
3876
4006
  declaration,
@@ -3899,10 +4029,25 @@ export class TestHost {
3899
4029
  throw new HostError("missing", `command ${p.id} is not registered`);
3900
4030
  },
3901
4031
  "commands.update": (p) => {
3902
- const command = this.command(String(p.id));
4032
+ // The request itself, as the player reads it (capabilities/commands.rs
4033
+ // `parsed`): `invalid`.
4034
+ if (!Number.isInteger(p.revision) || Number(p.revision) <= 0)
4035
+ throw new HostError("invalid", "positive revision required");
4036
+ const reason = this.#checkReason(p.state);
4037
+ // The registry's refusals of an update (commands.rs `Registry::update`)
4038
+ // all come back as `command`; the test host's one plugin is owner 1.
4039
+ const command = this.commands.get(String(p.id));
4040
+ if (!command)
4041
+ throw new HostError("command", `command missing: owner 1 / ${p.id}`);
3903
4042
  if (Number(p.revision) <= command.revision)
3904
- throw new HostError("command", `stale command revision ${p.revision}`);
3905
- this.#checkReason(p.state);
4043
+ throw new HostError("command", `stale command revision ${p.revision}; current is ${command.revision}`);
4044
+ if (reason !== null)
4045
+ try {
4046
+ commandText(reason, "reason", COMMAND_LIMITS.reasonBytes);
4047
+ }
4048
+ catch (error) {
4049
+ throw new HostError("command", error.message);
4050
+ }
3906
4051
  command.revision = Number(p.revision);
3907
4052
  command.state = p.state;
3908
4053
  if (p.when)
@@ -5283,8 +5428,11 @@ function tellResidency(host) {
5283
5428
  *
5284
5429
  * Rejects when `manifest.json` cannot be read or parsed, when `activate`
5285
5430
  * throws, or when a registration made during activation fails (for example
5286
- * `limit`, `conflict`, `invalid`), which is when the player would fail the
5287
- * activation.
5431
+ * `limit`, `conflict`, `invalid`). Stricter than the player's runner for
5432
+ * commands (SDK 1.5.49): there the SDK logs a command declaration the
5433
+ * player refuses during activation, skips it and activates the rest of the
5434
+ * plugin (`onError` hears it); `startPlugin` fails, so a test shows the
5435
+ * bad declaration.
5288
5436
  *
5289
5437
  * @param activate - The plugin's default-exported `activate` function.
5290
5438
  * @param options - Where the plugin lives and how to start it.
@@ -5369,7 +5517,10 @@ export async function startPlugin(activate, options = {}) {
5369
5517
  const instance = await createPlugin(host, manifest, directory, data,
5370
5518
  // Like the player: protocol additions it takes (`app.features` set on
5371
5519
  // the host replaces them, e.g. `[]` for an older player's behaviour).
5372
- { features: [PATCH_FEATURE], ...host.app });
5520
+ { features: [PATCH_FEATURE], ...host.app },
5521
+ // Unlike the player's runner, which logs a refused command declaration
5522
+ // and activates the rest: a plugin's tests show the refusal.
5523
+ { strictDeclarations: true });
5373
5524
  await activate(instance.plugin);
5374
5525
  // Like the player: a position or resolver another plugin holds is not an
5375
5526
  // activation failure.
@@ -487,7 +487,11 @@ export interface Scope extends Disposable {
487
487
  * Registrations started during activation (commands, panels, resolvers,
488
488
  * subscriptions) do not need to be awaited: the plugin is reported ready
489
489
  * once they all succeed, and activation fails if `activate` throws or any
490
- * of them fails. The entry module must **default-export** `activate`.
490
+ * of them fails. A command declaration the player refuses is the exception
491
+ * (SDK 1.5.49): the SDK logs and skips it, its `ready` rejects and
492
+ * {@link Scope.onError} hears it, the rest of the plugin activates
493
+ * (`startPlugin` still fails on it). The entry module must
494
+ * **default-export** `activate`.
491
495
  *
492
496
  * Stopping: the player first revokes everything the plugin owns on its side
493
497
  * (menus, windows, panels, filters, subtitle layers, snapshots, jobs,
@@ -917,7 +921,9 @@ export interface CommandButton {
917
921
  * give it an order in the menu's range (Video: 400–499).
918
922
  *
919
923
  * {@link Commands.add} throws `invalid` synchronously for a bad path,
920
- * order, group or binding. The same path registered twice, or a path that
924
+ * order or binding; the player refuses an empty or overlong text, a control
925
+ * character or whitespace at either end (`COMMAND_LIMITS`) with
926
+ * `invalid` (through `ready`). The same path registered twice, or a path that
921
927
  * is both an item and a submenu, fails with `conflict` (through `ready`).
922
928
  *
923
929
  * @see {@link Commands.add}, [Commands, menus and key bindings](../../docs/explanation/commands.md)
@@ -1136,8 +1142,21 @@ export interface Command extends Disposable {
1136
1142
  * Resolves when the host has registered the command. Rejects with
1137
1143
  * `conflict` (path already taken), `invalid` or `limit` (more than 160
1138
1144
  * commands in this plugin, or 1024 in the whole player). You rarely need
1139
- * to await it: during activation a failure fails activation, later it
1140
- * goes to {@link Scope.onError}.
1145
+ * to await it: the refusal goes to {@link Scope.onError}. During
1146
+ * activation the SDK, in the plugin's process, logs the refusal with the
1147
+ * command id and the player's message, skips the command and lets the
1148
+ * rest of the plugin activate (SDK 1.5.49; before, the refusal failed
1149
+ * activation); the error reaches `onError` once activation completes,
1150
+ * so a listener added anywhere in `activate` hears it. `startPlugin`
1151
+ * stays strict: there a refusal during activation fails it, so tests
1152
+ * show the bad declaration. Any other failure during activation
1153
+ * (`capability`, no answer) fails activation.
1154
+ *
1155
+ * Only the player's answer is skipped. What {@link Commands.add} checks
1156
+ * itself still throws from `add` (an unknown `@` menu id, more than 8
1157
+ * path segments, an `order` out of range, a malformed binding or one
1158
+ * listed twice, a bad `button`, a reason text of the wrong shape, an id
1159
+ * already declared), and fails activation unless the plugin catches it.
1141
1160
  *
1142
1161
  * @throws `conflict` (rejects) Another command already has this menu path,
1143
1162
  * or the path would be both a menu item and a submenu.
@@ -1159,8 +1178,9 @@ export interface Command extends Disposable {
1159
1178
  * when the user picks the item, presses a key or clicks the button.
1160
1179
  * Commands of all plugins merge into the player's menus by their English
1161
1180
  * path keys. Registration is asynchronous: `add` returns at once, and
1162
- * {@link Command.ready} reports the player's answer (during activation a
1163
- * refusal fails activation).
1181
+ * {@link Command.ready} reports the player's answer (during activation
1182
+ * the SDK logs a refusal and skips that command, SDK 1.5.49;
1183
+ * `startPlugin` fails on it). What `add` checks itself throws from `add`.
1164
1184
  *
1165
1185
  * @capability commands
1166
1186
  *
@@ -1190,8 +1210,15 @@ export interface Commands {
1190
1210
  * lists, presets) in a child {@link Scope} and rebuild it.
1191
1211
  *
1192
1212
  * @throws `invalid` synchronously for a malformed path (including an
1193
- * unknown `@` menu id), order, group or binding; `conflict` for an id this
1213
+ * unknown `@` menu id), order or binding; `conflict` for an id this
1194
1214
  * plugin has already declared and not disposed.
1215
+ * @throws `invalid` (`ready` rejects; during activation, activation
1216
+ * fails) for a text the player refuses: an id, title, path segment,
1217
+ * group or reason that is empty, longer than its `COMMAND_LIMITS`
1218
+ * bytes (a text value counted by its English rendering), has a control
1219
+ * character or whitespace at either end, or an id with other characters
1220
+ * than ASCII letters, digits, `.`, `_` and `-`. `TestHost`
1221
+ * refuses the same (SDK 1.5.48).
1195
1222
  *
1196
1223
  * @example Pause playback from a menu item
1197
1224
  * 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.49: 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 and `onError` hears it; during activation the SDK logs and skips that command and the rest of the plugin activates, SDK 1.5.49, while `startPlugin` 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
 
@@ -3834,6 +3841,10 @@ When the player starts the plugin:
3834
3841
 
3835
3842
  You do not need to `await` those registrations. `commands.add`, `panels.mount` and `resolvers.add` return their handle at once and register in the background; their `ready` promise is there for the rare case that you want to wait. **Activation fails** when `activate` throws or rejects, or when one of those registrations fails. A registration made later, after activation, cannot fail activation any more: its failure goes to `onError`.
3836
3843
 
3844
+ A command declaration the player refuses (`invalid`, `conflict` or `limit`: a bad text, a path another command has, too many commands) does not fail activation either (SDK 1.5.49). The SDK, in the plugin's process, logs it with the command id and the player's message, skips that command and lets the rest of the plugin activate, like VS Code with a bad contribution; the command's `ready` rejects and `onError` hears the refusal once activation completes. `startPlugin` stays strict: there the refusal fails the start, so your tests show the bad declaration.
3845
+
3846
+ This covers only the player's answer. What `commands.add` checks itself still throws from `add`: an unknown `@` menu id, more than 8 path segments, an `order` out of range, a malformed binding or one listed twice, a bad `button`, a reason text of the wrong shape, an id already declared. Uncaught, such a throw fails activation like any other exception in `activate`.
3847
+
3837
3848
  Some refusals are deliberately not failures, because they depend on which other plugins are installed: a bottom bar or backdrop that another plugin's panel already holds, the next/previous sequence that another plugin provides, a file type or web host that another plugin already resolves. The plugin keeps running without that part, the player tells the user, and the plugin manager shows it as a warning (PluginInfo.warnings). The player's built-in plugins register first, so they win these contests at start-up.
3838
3849
 
3839
3850
  #### Handles and scopes
@@ -4572,6 +4583,7 @@ A tool window (Window), a panel (Panel) and a settings page (SettingsPage) all s
4572
4583
  - **A panel or settings page file exports one component and one global.** The player composes your component into a window it owns (the main window, the floating sidebar window, the Options window), so the component instance is not a stable address: a new instance is made whenever the main window is rebuilt or the sidebar moves to or from its floating window. The **global** is the bridge: properties and callbacks are declared on it, the component reads and calls them (`ListBridge.rows`, `ListBridge.select(index)`), and the plugin talks to the global. The SDK finds the two names from the file's `export component` and `export global` lines.
4573
4584
 
4574
4585
  ```slint
4586
+ import { ListView } from "std-widgets.slint";
4575
4587
  import { Theme } from "@vestra/theme.slint";
4576
4588
 
4577
4589
  export global ListBridge {
@@ -4584,7 +4596,9 @@ export global ListBridge {
4584
4596
  export component ListPanel inherits Rectangle {
4585
4597
  background: Theme.bg-list;
4586
4598
  min-width: 200px;
4587
- VerticalLayout {
4599
+ // A ListView builds only the rows in view and scrolls the rest, so the
4600
+ // list costs the same with three rows or thirty thousand.
4601
+ ListView {
4588
4602
  for row[index] in ListBridge.rows: Rectangle {
4589
4603
  height: 20px;
4590
4604
  background: index == ListBridge.selected ? Theme.selected-bg : transparent;
@@ -4607,9 +4621,6 @@ export component ListPanel inherits Rectangle {
4607
4621
  }
4608
4622
  }
4609
4623
  }
4610
- // Takes the space below the rows. Without it the layout's maximum
4611
- // height (three 20px rows) would cap the player's main window.
4612
- Rectangle { vertical-stretch: 1; }
4613
4624
  }
4614
4625
  }
4615
4626
  ```
@@ -5176,7 +5187,7 @@ self.activations.begin(owner, self.now_ms() + 15_000)?;
5176
5187
 
5177
5188
  Source: src/host/src/packages/package_manager.rs
5178
5189
 
5179
- `boot.ts` sends `runtime.ready` only after `activate` returned and every registration started during activation succeeded (a slot, sequence or resolver another plugin holds does not count as a failure). Otherwise it sends `runtime.failed` with a `code`. A failure or a missed deadline stops the plugin, and the plugin manager lists it as failed with the message. At start-up the installed plugins are started from a queue, about 25 ms of process spawning per UI tick, so the window stays responsive while they come up.
5190
+ `boot.ts` sends `runtime.ready` only after `activate` returned and every registration started during activation succeeded (a slot, sequence or resolver another plugin holds does not count as a failure, and the SDK skips a command declaration the player refuses with `invalid`, `conflict` or `limit`: it logs it and reports it to `onError`, SDK 1.5.49). Otherwise it sends `runtime.failed` with a `code`. A failure or a missed deadline stops the plugin, and the plugin manager lists it as failed with the message. At start-up the installed plugins are started from a queue, about 25 ms of process spawning per UI tick, so the window stays responsive while they come up.
5180
5191
 
5181
5192
  #### The wire protocol
5182
5193
 
@@ -5484,6 +5495,7 @@ Use the layout of Theme: small adjustment windows: the same width, row height, l
5484
5495
  - **No status or path lines in a panel.** Messages go to the OSD; a full path is a tooltip on its row.
5485
5496
  - **Show state with colour, not words.** As in the player's playlist: what is playing in `Theme.playing` (with a thin bar at the left, Theme: layouts), what was played in `Theme.text-played`, the selection on `Theme.selected-bg`. A failed item gets the `failed` icon at the end of its row and an OSD message.
5486
5497
  - **Lists are plain**: single-line rows, background `Theme.bg-list`, no cards, no stripes, no rounded rows; scroll bars come from `std-widgets.slint` so they follow the palette.
5498
+ - **Long lists use `ListView`.** A list whose length the user decides (a playlist, search results, files, messages) goes in a `ListView` from `std-widgets.slint`, with the `for` inside it: it creates only the rows in view, so a list of thousands of rows scrolls as smoothly as one of ten. A `for` in a layout builds every row up front. Keep a bare `for` for a short list of a fixed size, such as a row of buttons. Example: Panel menu is a panel list drawn this way.
5487
5499
  - **Commands of a panel go into its right-click menu**, made of the plugin's commands (panel.showMenu); see Menus.
5488
5500
  - **The bottom bar is icons only**: no text buttons, no source path, no status text (the source name is in the window title, status is an OSD message).
5489
5501
 
@@ -5820,6 +5832,8 @@ Every SDK release's CHANGELOG entry opens with who must change and what happens
5820
5832
 
5821
5833
  | Release | Who must change | Guide |
5822
5834
  | --- | --- | --- |
5835
+ | [1.5.49](https://github.com/vestrasys/vestra-player-sdk/blob/main/CHANGELOG.md#1549) | **Nobody.** No changes to the plugin API (`@vestra-player/sdk/runtime` gains the additive `RuntimeOptions` and an optional sixth `createPlugin` argument), `apiVersion` stays `vestra/1`, no new capability, no player version needed, and the player's runner needs no change. | — |
5836
+ | [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
5837
  | [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
5838
  | [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
5839
  | [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. | — |
@@ -6304,7 +6318,11 @@ initial player, window and timeline state, then calls `activate(plugin)`.
6304
6318
  Registrations started during activation (commands, panels, resolvers,
6305
6319
  subscriptions) do not need to be awaited: the plugin is reported ready
6306
6320
  once they all succeed, and activation fails if `activate` throws or any
6307
- of them fails. The entry module must **default-export** `activate`.
6321
+ of them fails. A command declaration the player refuses is the exception
6322
+ (SDK 1.5.49): the SDK logs and skips it, its `ready` rejects and
6323
+ Scope.onError hears it, the rest of the plugin activates
6324
+ (`startPlugin` still fails on it). The entry module must
6325
+ **default-export** `activate`.
6308
6326
 
6309
6327
  Stopping: the player first revokes everything the plugin owns on its side
6310
6328
  (menus, windows, panels, filters, subtitle layers, snapshots, jobs,
@@ -6701,7 +6719,9 @@ an item without `order` moves the whole menu to the top; to avoid that,
6701
6719
  give it an order in the menu's range (Video: 400–499).
6702
6720
 
6703
6721
  Commands.add throws `invalid` synchronously for a bad path,
6704
- order, group or binding. The same path registered twice, or a path that
6722
+ order or binding; the player refuses an empty or overlong text, a control
6723
+ character or whitespace at either end (`COMMAND_LIMITS`) with
6724
+ `invalid` (through `ready`). The same path registered twice, or a path that
6705
6725
  is both an item and a submenu, fails with `conflict` (through `ready`).
6706
6726
 
6707
6727
  ```ts
@@ -6861,7 +6881,24 @@ interface Command extends Disposable {
6861
6881
  checked: Bindable<boolean | null> | Condition;
6862
6882
  }>): void;
6863
6883
  /**
6864
- * Resolves when the host has registered the command. Rejects with `conflict` (path already taken), `invalid` or `limit` (more than 160 commands in this plugin, or 1024 in the whole player). You rarely need to await it: during activation a failure fails activation, later it goes to Scope.onError.
6884
+ * Resolves when the host has registered the command. Rejects with
6885
+ * `conflict` (path already taken), `invalid` or `limit` (more than 160
6886
+ * commands in this plugin, or 1024 in the whole player). You rarely need
6887
+ * to await it: the refusal goes to Scope.onError. During
6888
+ * activation the SDK, in the plugin's process, logs the refusal with the
6889
+ * command id and the player's message, skips the command and lets the
6890
+ * rest of the plugin activate (SDK 1.5.49; before, the refusal failed
6891
+ * activation); the error reaches `onError` once activation completes,
6892
+ * so a listener added anywhere in `activate` hears it. `startPlugin`
6893
+ * stays strict: there a refusal during activation fails it, so tests
6894
+ * show the bad declaration. Any other failure during activation
6895
+ * (`capability`, no answer) fails activation.
6896
+ *
6897
+ * Only the player's answer is skipped. What Commands.add checks
6898
+ * itself still throws from `add` (an unknown `@` menu id, more than 8
6899
+ * path segments, an `order` out of range, a malformed binding or one
6900
+ * listed twice, a bad `button`, a reason text of the wrong shape, an id
6901
+ * already declared), and fails activation unless the plugin catches it.
6865
6902
  * @throws conflict (rejects) Another command already has this menu path, or the path would be both a menu item and a submenu.
6866
6903
  * @throws capability (rejects) The manifest does not declare `commands`.
6867
6904
  * @throws invalid (rejects) The player refused the declaration.
@@ -6898,8 +6935,9 @@ the menu, the binding table and the user's key overrides: it calls `run`
6898
6935
  when the user picks the item, presses a key or clicks the button.
6899
6936
  Commands of all plugins merge into the player's menus by their English
6900
6937
  path keys. Registration is asynchronous: `add` returns at once, and
6901
- Command.ready reports the player's answer (during activation a
6902
- refusal fails activation).
6938
+ Command.ready reports the player's answer (during activation
6939
+ the SDK logs a refusal and skips that command, SDK 1.5.49;
6940
+ `startPlugin` fails on it). What `add` checks itself throws from `add`.
6903
6941
 
6904
6942
  - @capability commands
6905
6943
 
@@ -6911,7 +6949,8 @@ interface Commands {
6911
6949
  *
6912
6950
  * Titles and paths cannot change in place: put changing items (track
6913
6951
  * 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.
6952
+ * @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.
6953
+ * @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
6954
  * @capability commands
6916
6955
  * @example Pause playback from a menu item
6917
6956
  * Playback ▸ Pause (and Ctrl+P) pauses and resumes, shows a check mark
@@ -8453,6 +8492,7 @@ panel.on("contextMenu", (x: number, y: number) =>
8453
8492
  `ui/panel.slint`:
8454
8493
 
8455
8494
  ```slint
8495
+ import { ListView } from "std-widgets.slint";
8456
8496
  import { Theme } from "@vestra/theme.slint";
8457
8497
 
8458
8498
  export global ListBridge {
@@ -8465,7 +8505,9 @@ export global ListBridge {
8465
8505
  export component ListPanel inherits Rectangle {
8466
8506
  background: Theme.bg-list;
8467
8507
  min-width: 200px;
8468
- VerticalLayout {
8508
+ // A ListView builds only the rows in view and scrolls the rest, so the
8509
+ // list costs the same with three rows or thirty thousand.
8510
+ ListView {
8469
8511
  for row[index] in ListBridge.rows: Rectangle {
8470
8512
  height: 20px;
8471
8513
  background: index == ListBridge.selected ? Theme.selected-bg : transparent;
@@ -8488,9 +8530,6 @@ export component ListPanel inherits Rectangle {
8488
8530
  }
8489
8531
  }
8490
8532
  }
8491
- // Takes the space below the rows. Without it the layout's maximum
8492
- // height (three 20px rows) would cap the player's main window.
8493
- Rectangle { vertical-stretch: 1; }
8494
8533
  }
8495
8534
  }
8496
8535
  ```
@@ -9392,6 +9431,7 @@ panel.bind(() => ({ rows: items, selected }));
9392
9431
  `ui/panel.slint`:
9393
9432
 
9394
9433
  ```slint
9434
+ import { ListView } from "std-widgets.slint";
9395
9435
  import { Theme } from "@vestra/theme.slint";
9396
9436
 
9397
9437
  export global ListBridge {
@@ -9404,7 +9444,9 @@ export global ListBridge {
9404
9444
  export component ListPanel inherits Rectangle {
9405
9445
  background: Theme.bg-list;
9406
9446
  min-width: 200px;
9407
- VerticalLayout {
9447
+ // A ListView builds only the rows in view and scrolls the rest, so the
9448
+ // list costs the same with three rows or thirty thousand.
9449
+ ListView {
9408
9450
  for row[index] in ListBridge.rows: Rectangle {
9409
9451
  height: 20px;
9410
9452
  background: index == ListBridge.selected ? Theme.selected-bg : transparent;
@@ -9427,9 +9469,6 @@ export component ListPanel inherits Rectangle {
9427
9469
  }
9428
9470
  }
9429
9471
  }
9430
- // Takes the space below the rows. Without it the layout's maximum
9431
- // height (three 20px rows) would cap the player's main window.
9432
- Rectangle { vertical-stretch: 1; }
9433
9472
  }
9434
9473
  }
9435
9474
  ```
@@ -17140,8 +17179,11 @@ undeclared capability fail with `capability`, as in the player.
17140
17179
 
17141
17180
  Rejects when `manifest.json` cannot be read or parsed, when `activate`
17142
17181
  throws, or when a registration made during activation fails (for example
17143
- `limit`, `conflict`, `invalid`), which is when the player would fail the
17144
- activation.
17182
+ `limit`, `conflict`, `invalid`). Stricter than the player's runner for
17183
+ commands (SDK 1.5.49): there the SDK logs a command declaration the
17184
+ player refuses during activation, skips it and activates the rest of the
17185
+ plugin (`onError` hears it); `startPlugin` fails, so a test shows the
17186
+ bad declaration.
17145
17187
 
17146
17188
  The way into a plugin's tests: one call starts the plugin the way the
17147
17189
  player's runner does and returns the host to drive, the `plugin` object
@@ -17314,6 +17356,9 @@ fails; later the failure goes to `Scope.onError`), commands
17314
17356
  already registered are unaffected, and disposing a command frees its slot
17315
17357
  at once. TestHost enforces the same caps.
17316
17358
 
17359
+ The other fields are the player's rules for one declaration (since
17360
+ 1.5.48): a command past them is refused with `invalid`, the same way.
17361
+
17317
17362
  For menus built from media content (tracks, chapters, presets), set your
17318
17363
  own sensible display cap well below `perPlugin`.
17319
17364
 
@@ -17323,6 +17368,22 @@ const COMMAND_LIMITS: {
17323
17368
  readonly perPlugin: 160;
17324
17369
  /** Commands of every plugin plus the player's own, together (1024). */
17325
17370
  readonly total: 1024;
17371
+ /** Segments of a menu path, the leaf included. */
17372
+ readonly depth: 8;
17373
+ /** UTF-8 bytes of a command's local id: ASCII letters, digits, `.`, `_` and `-`. */
17374
+ readonly idBytes: 128;
17375
+ /**
17376
+ * 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.
17377
+ */
17378
+ readonly titleBytes: 128;
17379
+ /** UTF-8 bytes of a menu path segment, a text value counted by its English rendering. */
17380
+ readonly segmentBytes: 80;
17381
+ /** UTF-8 bytes of a group. */
17382
+ readonly groupBytes: 80;
17383
+ /** UTF-8 bytes of a state's reason, as shown in the player's language. */
17384
+ readonly reasonBytes: 512;
17385
+ /** The largest `order` either way: an order is within ±1,000,000. */
17386
+ readonly order: 1000000;
17326
17387
  };
17327
17388
  ```
17328
17389
 
@@ -19389,7 +19450,7 @@ Add a window teaches the view surface panels share with tool windows; the rules
19389
19450
  - **The global `ListBridge`** holds the properties and callbacks. A panel is part of the player's main window, and the player makes a new instance of the component whenever it rebuilds that window. The global stays put, so the plugin talks to it, not to the component.
19390
19451
  - `rows` is the list (an array in TypeScript) and `selected` the selected index.
19391
19452
  - `select(int)` reports a click; `context-menu(float, float)` asks for the menu at a point.
19392
- - **The component** inherits `Rectangle`, not `Window`: the player places it. `for row[index] in ListBridge.rows` draws one 20-pixel row per item; the selected row gets `Theme.selected-bg`.
19453
+ - **The component** inherits `Rectangle`, not `Window`: the player places it. A `ListView` from `std-widgets.slint` fills it; `for row[index] in ListBridge.rows` inside it gives one 20-pixel row per item, and the selected row gets `Theme.selected-bg`. The `ListView` creates only the rows in view and scrolls the rest, so the panel stays cheap however long the list grows; a bare `for` in a layout would build every row up front (and `vestra-plugin check` warns about one over a bound array).
19393
19454
  - **The `TouchArea`** of each row: any button going down selects the row. The right button coming up asks for the menu, at the pointer's position in the window (`absolute-position` is the row's position, `mouse-x`/`mouse-y` the pointer's position in it, and `/ 1px` makes a plain number).
19394
19455
 
19395
19456
  #### The code
@@ -19491,6 +19552,7 @@ export default async function activate(plugin: Plugin) {
19491
19552
  #### examples/panel-menu/ui/panel.slint
19492
19553
 
19493
19554
  ```slint
19555
+ import { ListView } from "std-widgets.slint";
19494
19556
  import { Theme } from "@vestra/theme.slint";
19495
19557
 
19496
19558
  export global ListBridge {
@@ -19503,7 +19565,9 @@ export global ListBridge {
19503
19565
  export component ListPanel inherits Rectangle {
19504
19566
  background: Theme.bg-list;
19505
19567
  min-width: 200px;
19506
- VerticalLayout {
19568
+ // A ListView builds only the rows in view and scrolls the rest, so the
19569
+ // list costs the same with three rows or thirty thousand.
19570
+ ListView {
19507
19571
  for row[index] in ListBridge.rows: Rectangle {
19508
19572
  height: 20px;
19509
19573
  background: index == ListBridge.selected ? Theme.selected-bg : transparent;
@@ -19526,9 +19590,6 @@ export component ListPanel inherits Rectangle {
19526
19590
  }
19527
19591
  }
19528
19592
  }
19529
- // Takes the space below the rows. Without it the layout's maximum
19530
- // height (three 20px rows) would cap the player's main window.
19531
- Rectangle { vertical-stretch: 1; }
19532
19593
  }
19533
19594
  }
19534
19595
  ```
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.49: 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: L15530.
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 L5972.
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–4007; Commands, menus and key bindings L4008–4162; Playback state, controls and sources L4163–4322; Video and audio processing L4323–4443; Windows, panels, settings pages, dialogs and the OSD L4444–4708; GPU and software rendering L4709–4725; The player's Rust core L4726–5067; How the player runs and talks to plugins L5068–5407
120
+ - Guidelines: Plugin rules L5408–5419; Interface guidelines L5420–5537; Menu guidelines L5538–5596
121
+ - Upgrading: Migrate a plugin to English source text L5597–5746; Migrating off package rollback (1.5.16, removed in 1.5.17) L5747–5824; Who must change, release by release L5825–5894
122
+ - [API: @vestra-player/sdk](https://vestrasys.github.io/vestra-player-sdk/modules/_vestra-player_sdk.html): L5895–15203
123
+ - [API: @vestra-player/sdk/testing](https://vestrasys.github.io/vestra-player-sdk/modules/_vestra-player_sdk_testing.html): L15204–18090
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 L6311, Scope L6047, PluginError L5972, Disposable L5910
130
+ - Commands L6927; When L6502; Windows L7875; Panels L9321; Dialogs L9991; Player L10732; video: VideoFilter L11612, VideoView L11712, RenderOptions L11799, VideoShaders L11877, VideoUpscaling L12235; audio: AudioFilter L11657; Subtitles L12471; media: MediaCapture L12875; Resolvers L13105; Packages L13507; Osd L11477; Host L11319; Clipboard L13836; Power L13923; Tools L13982; Storage L14134; System L14333; Secrets L14413; window: MainWindow L9638; App L9818; Rendering L14676; Bindings L13735; Ui L7221; Timeline L9550; Drops L8568; Appearance L8649; I18n L8882; Settings L9165.
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. L18525–18620
137
+ - blur: A tool window with one slider that blurs the playing video. L18621–18888
138
+ - signal-generator: Generates its own video and audio: colour bars and a 1 kHz tone. L18889–19227
139
+ - bookmark: A tool window bound to the playback state: remember the position and jump back to it. L19228–19420
140
+ - panel-menu: A sidebar panel with its own right-click menu. L19421–19635
141
+ - menus: Radio menu items, separator groups and a menu item whose title changes. L19636–19801
142
+ - theme-widgets: A tool window drawn with the player's theme and widgets. L19802–19971
143
+ - markdown: A window that shows a Markdown document. L19972–20174
144
+ - i18n: A plugin in the player's language: English source text, a Chinese translation. L20175–20348
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.49",
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": {