@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 +16 -0
- package/dist/commands.d.ts +50 -0
- package/dist/commands.js +50 -0
- package/dist/plugin/testing.js +191 -48
- package/dist/plugin/types.d.ts +11 -2
- package/llms-full.txt +34 -4
- package/llms.txt +20 -20
- package/package.json +1 -1
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.
|
package/dist/commands.d.ts
CHANGED
|
@@ -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
|
package/dist/plugin/testing.js
CHANGED
|
@@ -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
|
-
|
|
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,
|
|
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
|
|
3838
|
-
|
|
3839
|
-
|
|
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
|
|
3842
|
-
if (menu !== null &&
|
|
3843
|
-
throw
|
|
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
|
|
3846
|
-
// The player's
|
|
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
|
|
3991
|
+
this.commands.size + this.coreCommands.size + hostOnly.length >=
|
|
3992
|
+
COMMAND_LIMITS.total)
|
|
3849
3993
|
throw new HostError("limit", "command registry limit reached");
|
|
3850
|
-
|
|
3851
|
-
const
|
|
3852
|
-
|
|
3853
|
-
|
|
3854
|
-
|
|
3855
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
package/dist/plugin/types.d.ts
CHANGED
|
@@ -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
|
|
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
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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:
|
|
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
|
|
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–
|
|
119
|
-
- Explanation: How a plugin runs
|
|
120
|
-
- Guidelines: Plugin rules
|
|
121
|
-
- Upgrading: Migrate a plugin to English source text
|
|
122
|
-
- [API: @vestra-player/sdk](https://vestrasys.github.io/vestra-player-sdk/modules/_vestra-player_sdk.html):
|
|
123
|
-
- [API: @vestra-player/sdk/testing](https://vestrasys.github.io/vestra-player-sdk/modules/_vestra-player_sdk_testing.html):
|
|
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
|
|
130
|
-
- Commands
|
|
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.
|
|
137
|
-
- blur: A tool window with one slider that blurs the playing video.
|
|
138
|
-
- signal-generator: Generates its own video and audio: colour bars and a 1 kHz tone.
|
|
139
|
-
- bookmark: A tool window bound to the playback state: remember the position and jump back to it.
|
|
140
|
-
- panel-menu: A sidebar panel with its own right-click menu.
|
|
141
|
-
- menus: Radio menu items, separator groups and a menu item whose title changes.
|
|
142
|
-
- theme-widgets: A tool window drawn with the player's theme and widgets.
|
|
143
|
-
- markdown: A window that shows a Markdown document.
|
|
144
|
-
- i18n: A plugin in the player's language: English source text, a Chinese translation.
|
|
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
|
|