@vestra-player/sdk 1.5.46 → 1.5.48
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +24 -0
- package/dist/commands.d.ts +50 -0
- package/dist/commands.js +55 -4
- package/dist/plugin/bindings.d.ts +5 -2
- package/dist/plugin/bindings.js +5 -2
- package/dist/plugin/testing.js +194 -51
- package/dist/plugin/types.d.ts +18 -6
- package/dist/plugin/ui.js +117 -11
- package/llms-full.txt +60 -21
- package/llms.txt +20 -20
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,29 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.5.48
|
|
4
|
+
|
|
5
|
+
**Who must change: nobody, but plugin tests may now fail.** No changes to the TypeScript API beyond new fields of `COMMAND_LIMITS`, `apiVersion` stays `vestra/1`, no new capability, no player version needed. `TestHost` now refuses the command declarations the player has always refused: a plugin test that starts failing shows a menu item, key or reason the real player already rejected (the command never appeared, or the plugin failed to activate). Fix the declaration, not the test.
|
|
6
|
+
|
|
7
|
+
- **`TestHost` checks a command declaration as the player does** (`commands.register`, in the player's order, with its codes and messages):
|
|
8
|
+
- the local id: 1–128 bytes of ASCII letters, digits, `.`, `_` and `-` (`invalid`);
|
|
9
|
+
- the title (128 bytes), each path segment (80 bytes) and the group (80 bytes, default `"domain"`): not empty, no control character (C0, DEL and C1), no whitespace at either end (Unicode White_Space, as the player's `trim`). A text value is checked on its English rendering, which is also the key menus merge by. The message names the field: `invalid command: path segment requires 1..=80 bytes, no controls or surrounding whitespace`;
|
|
10
|
+
- at most 8 path segments, an `order` within ±1,000,000, bindings in the player's syntax, each once (`invalid`);
|
|
11
|
+
- the state's reason: at most 512 bytes as shown, the same text rules (`invalid` at registration; `command` on an update, as the player answers every refused update);
|
|
12
|
+
- a path that repeats another command's or is a submenu of one, or one of whose submenus is a command, the player's own commands included (`Audio ▸ Mute ▸ …`, `Playback ▸ Speed` as an item, `Quit`, `Toggle Fullscreen ▸ …`): `conflict` (`command conflict: menu path duplicates a command or collides with a leaf/group`). The total (`COMMAND_LIMITS.total`) counts all 19 of the player's own commands, Toggle Fullscreen, Enter and Leave Fullscreen, Quit and Toggle Sidebar included;
|
|
13
|
+
- an id registered twice: `conflict` (was `command`); an unknown `@` menu id, a bar button without a path or with an unknown icon, give the player's full messages;
|
|
14
|
+
- an update of a command that is not registered rejects with `command` (`command missing: owner 1 / <id>`; was a plain `Error`), a revision that is not a positive integer with `invalid` (`positive revision required`), and a stale one names the current revision.
|
|
15
|
+
- **The seek items follow `setStepSeek`.** `host.coreAction("seek-backward")` and `seek-forward` carry the step in their path and title (`Playback ▸ Seek ▸ Back 2.5 s`), as the player registers them again when the step changes; they were fixed at 5 s.
|
|
16
|
+
- **`COMMAND_LIMITS`** gains the player's declaration limits: `depth` (8), `idBytes` (128), `titleBytes` (128), `segmentBytes` (80), `groupBytes` (80), `reasonBytes` (512) and `order` (1,000,000), so the player's tests can check that they agree.
|
|
17
|
+
- _Commands_: `CommandOptions` and `Commands.add` say which checks the player makes (`ready` rejects with `invalid`) and which `add` makes at once.
|
|
18
|
+
|
|
19
|
+
## 1.5.47
|
|
20
|
+
|
|
21
|
+
**Who must change: nobody.** No changes to the TypeScript API, `apiVersion` stays `vestra/1`, no new capability, no player version needed. The built-in command table follows player 0.6.0's new default keys: a plugin whose documentation or tests list the player's default keys (from `BUILTIN_COMMANDS`, or Escape, Enter, F11 and the side mouse buttons by hand) should update them.
|
|
22
|
+
|
|
23
|
+
- **Player 0.6.0 default keys** (`BUILTIN_COMMANDS`, the generated key tables, `HostAction`): **Toggle Fullscreen** (`host/fullscreen`) is `Escape` only (up to 0.5.x `Enter`, `Alt+Enter`, `F11`); **Leave Fullscreen** (`host/exit-fullscreen`) has no default key (up to 0.5.x `Escape`); the new **Enter Fullscreen** (`host/enter-fullscreen`) has no default key and no menu row and is enabled only when windowed (Leave Fullscreen only in fullscreen); **Previous** is `PageUp`, `Mouse:X2` and **Next** `PageDown`, `Mouse:X1` (the side buttons swapped: back is next, forward is previous). The side buttons now fire anywhere in the main window or the floating sidebar window, except while a menu is open, not only over the video. `host.run` takes the same actions as before (`fullscreen` toggles). `TestHost`'s core actions use the new Previous / Next bindings.
|
|
24
|
+
- **Rows that cannot change are not encoded again.** An update pass encodes every row of a bound array to find the changed ones; in a 20,000-row list that was about 40% of a click. A deep-frozen row (`Object.freeze` on it and on everything inside it: plain objects and arrays, no getters, no own `toJSON`; image and document handles allowed: they are immutable, their wire form is fixed when made, so they need no freezing) that the bind function returns again is now encoded once more and then taken from a cache kept by the row object (weakly: a row the plugin drops goes with its entry), with its limit counts. Rows that are not frozen, or hold a mutable part, are encoded on every pass as before, so changes made in place are still sent. To benefit, keep an unchanged row as the same frozen object and make a new one when it changes ([Arrays: only the changed rows are sent](docs/explanation/ui.md#arrays-only-the-changed-rows-are-sent)). Measured in the test host with the playlist at 20,000 rows memoized that way: a click from about 24 ms to about 14 ms to the rows write.
|
|
25
|
+
- _Commands, menus and key bindings_: the example of a disabled command letting its key go no longer uses Escape on Leave Fullscreen.
|
|
26
|
+
|
|
3
27
|
## 1.5.46
|
|
4
28
|
|
|
5
29
|
**Who must change: nobody.** No changes to the TypeScript API, `apiVersion` stays `vestra/1`, no new capability. The new limits and the split writes take effect with player 0.6.0 (large lists P3); with older players arrays are sent whole as before and the old limits apply there. **A plugin that binds a whole long list depends on them: set `"minPlayerVersion": "0.6.0"`.**
|
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
|
|
@@ -265,8 +315,8 @@ vestra.playback | copy-name | Copy to Clipboard ▸ File Name
|
|
|
265
315
|
vestra.playback | copy-folder | Copy to Clipboard ▸ Folder Path
|
|
266
316
|
vestra.playback | copy-path | Copy to Clipboard ▸ Full Path
|
|
267
317
|
host | play-pause | Playback ▸ Play/Pause | Space, Mouse:LeftClick
|
|
268
|
-
host | previous | Playback ▸ Previous | PageUp, Mouse:
|
|
269
|
-
host | next | Playback ▸ Next | PageDown, Mouse:
|
|
318
|
+
host | previous | Playback ▸ Previous | PageUp, Mouse:X2
|
|
319
|
+
host | next | Playback ▸ Next | PageDown, Mouse:X1
|
|
270
320
|
host | frame-back | Playback ▸ Seek ▸ Previous Frame | D
|
|
271
321
|
host | frame-forward | Playback ▸ Seek ▸ Next Frame | F
|
|
272
322
|
vestra.playback | keyframe-back | Playback ▸ Seek ▸ Previous Keyframe | Ctrl+Shift+ArrowLeft
|
|
@@ -492,7 +542,7 @@ vestra.audio | audio.normalize | Audio ▸ Processing ▸ Normalize | Shift+N
|
|
|
492
542
|
vestra.audio | audio.clarity | Audio ▸ Processing ▸ Crystalizer | Shift+C
|
|
493
543
|
vestra.audio | audio.reset | Audio ▸ Reset
|
|
494
544
|
vestra.visualizer | audio-cover | Audio ▸ Show Album Cover for Audio Only
|
|
495
|
-
host | fullscreen | Toggle Fullscreen |
|
|
545
|
+
host | fullscreen | Toggle Fullscreen | Escape
|
|
496
546
|
vestra.window | pip | Screen ▸ Picture-in-Picture
|
|
497
547
|
vestra.window | mini | Screen ▸ Mini Size | \`
|
|
498
548
|
vestra.window | size.0.5 | Screen ▸ 0.5× | 1
|
|
@@ -536,7 +586,8 @@ vestra.preferences | language.fr | Settings ▸ Language ▸ Français
|
|
|
536
586
|
vestra.preferences | language.es | Settings ▸ Language ▸ Español
|
|
537
587
|
vestra.info | info.about | About Vestra Player… | F1
|
|
538
588
|
host | quit | Quit | Cmd+Q
|
|
539
|
-
host |
|
|
589
|
+
host | enter-fullscreen | (Enter Fullscreen)
|
|
590
|
+
host | exit-fullscreen | (Leave Fullscreen)
|
|
540
591
|
host | sidebar | (Toggle Sidebar)
|
|
541
592
|
vestra.info | info.brief | (Brief Info) | Shift+Tab
|
|
542
593
|
vestra.playlist | playlist.play | (Play) | Enter, P | panel
|
|
@@ -66,8 +66,11 @@ export declare const BINDING_INPUTS: Readonly<{
|
|
|
66
66
|
*/
|
|
67
67
|
readonly named: readonly ["Space", "Enter", "Tab", "Escape", "Backspace", "Delete", "Insert", "Home", "End", "PageUp", "PageDown", "ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown", "Plus", ...string[], "NumpadPlus", "NumpadMinus", "NumpadMultiply", "NumpadDivide", "NumpadEnter", "NumpadDecimal"];
|
|
68
68
|
/**
|
|
69
|
-
* Mouse inputs on the video area
|
|
70
|
-
*
|
|
69
|
+
* Mouse inputs on the video area; the side buttons `Mouse:X1` (back) and
|
|
70
|
+
* `Mouse:X2` (forward) anywhere in the main window or the floating sidebar
|
|
71
|
+
* window, except while a menu is open (player 0.6.0).
|
|
72
|
+
* `:LeftHalf` and `:RightHalf` tell the halves of the video apart for a
|
|
73
|
+
* double click.
|
|
71
74
|
*/
|
|
72
75
|
readonly mouse: readonly ["Mouse:LeftClick", "Mouse:LeftDoubleClick", "Mouse:LeftDoubleClick:LeftHalf", "Mouse:LeftDoubleClick:RightHalf", "Mouse:MiddleClick", "Mouse:X1", "Mouse:X2", "Mouse:WheelUp", "Mouse:WheelDown", "Mouse:WheelLeft", "Mouse:WheelRight"];
|
|
73
76
|
}>;
|
package/dist/plugin/bindings.js
CHANGED
|
@@ -97,8 +97,11 @@ export const BINDING_INPUTS = Object.freeze({
|
|
|
97
97
|
"NumpadDecimal",
|
|
98
98
|
],
|
|
99
99
|
/**
|
|
100
|
-
* Mouse inputs on the video area
|
|
101
|
-
*
|
|
100
|
+
* Mouse inputs on the video area; the side buttons `Mouse:X1` (back) and
|
|
101
|
+
* `Mouse:X2` (forward) anywhere in the main window or the floating sidebar
|
|
102
|
+
* window, except while a menu is open (player 0.6.0).
|
|
103
|
+
* `:LeftHalf` and `:RightHalf` tell the halves of the video apart for a
|
|
104
|
+
* double click.
|
|
102
105
|
*/
|
|
103
106
|
mouse: [
|
|
104
107
|
"Mouse:LeftClick",
|
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
|
|
@@ -2254,7 +2313,7 @@ export class TestHost {
|
|
|
2254
2313
|
["Playback", "Previous"],
|
|
2255
2314
|
202,
|
|
2256
2315
|
"domain",
|
|
2257
|
-
["PageUp", "Mouse:
|
|
2316
|
+
["PageUp", "Mouse:X2"],
|
|
2258
2317
|
false,
|
|
2259
2318
|
],
|
|
2260
2319
|
[
|
|
@@ -2262,7 +2321,7 @@ export class TestHost {
|
|
|
2262
2321
|
["Playback", "Next"],
|
|
2263
2322
|
203,
|
|
2264
2323
|
"domain",
|
|
2265
|
-
["PageDown", "Mouse:
|
|
2324
|
+
["PageDown", "Mouse:X1"],
|
|
2266
2325
|
false,
|
|
2267
2326
|
],
|
|
2268
2327
|
[
|
|
@@ -2341,19 +2400,36 @@ export class TestHost {
|
|
|
2341
2400
|
["mute", ["Audio", "Mute"], 503, "domain", ["M"], false],
|
|
2342
2401
|
];
|
|
2343
2402
|
const host = this;
|
|
2344
|
-
for (const [id,
|
|
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)
|
|
@@ -4248,7 +4391,7 @@ export class TestHost {
|
|
|
4248
4391
|
throw new HostError("invalid", "seconds must be 0.1..3600 in tenths");
|
|
4249
4392
|
seconds = p.seconds;
|
|
4250
4393
|
}
|
|
4251
|
-
// SDK 1.5.21: the main window's Toggle Fullscreen, like
|
|
4394
|
+
// SDK 1.5.21: the main window's Toggle Fullscreen, like Escape: in when
|
|
4252
4395
|
// windowed, out when fullscreen, then `window.state`.
|
|
4253
4396
|
if (p.action === "fullscreen") {
|
|
4254
4397
|
this.coreRuns.push("fullscreen");
|
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)
|
|
@@ -999,9 +1001,12 @@ export interface CommandOptions {
|
|
|
999
1001
|
* `Home`, `End`, `PageUp`, `PageDown`, `ArrowLeft`/`Right`/`Up`/`Down`,
|
|
1000
1002
|
* `Plus`, any other single symbol as typed (`">"`, not `"Shift+."`),
|
|
1001
1003
|
* `Numpad0`–`Numpad9`, `NumpadPlus`, `NumpadMinus`, `NumpadMultiply`,
|
|
1002
|
-
* `NumpadDivide`, `NumpadEnter`, `NumpadDecimal`,
|
|
1004
|
+
* `NumpadDivide`, `NumpadEnter`, `NumpadDecimal`, the video-area mouse
|
|
1003
1005
|
* inputs `Mouse:LeftClick`, `Mouse:LeftDoubleClick[:LeftHalf|:RightHalf]`,
|
|
1004
|
-
* `Mouse:MiddleClick`, `Mouse:
|
|
1006
|
+
* `Mouse:MiddleClick`, `Mouse:WheelUp`/`Down`/`Left`/`Right`, and the side
|
|
1007
|
+
* buttons `Mouse:X1` (back), `Mouse:X2` (forward), from 0.6.0 on anywhere
|
|
1008
|
+
* in the main window or the floating sidebar window, except while a menu
|
|
1009
|
+
* is open.
|
|
1005
1010
|
*
|
|
1006
1011
|
* Platforms: on macOS `Ctrl+X` fires for both ⌃X and ⌘X while `Cmd+X`
|
|
1007
1012
|
* means ⌘X only; on Windows `Cmd` is the Windows logo key, so write
|
|
@@ -1187,8 +1192,15 @@ export interface Commands {
|
|
|
1187
1192
|
* lists, presets) in a child {@link Scope} and rebuild it.
|
|
1188
1193
|
*
|
|
1189
1194
|
* @throws `invalid` synchronously for a malformed path (including an
|
|
1190
|
-
* unknown `@` menu id), order
|
|
1195
|
+
* unknown `@` menu id), order or binding; `conflict` for an id this
|
|
1191
1196
|
* plugin has already declared and not disposed.
|
|
1197
|
+
* @throws `invalid` (`ready` rejects; during activation, activation
|
|
1198
|
+
* fails) for a text the player refuses: an id, title, path segment,
|
|
1199
|
+
* group or reason that is empty, longer than its `COMMAND_LIMITS`
|
|
1200
|
+
* bytes (a text value counted by its English rendering), has a control
|
|
1201
|
+
* character or whitespace at either end, or an id with other characters
|
|
1202
|
+
* than ASCII letters, digits, `.`, `_` and `-`. `TestHost`
|
|
1203
|
+
* refuses the same (SDK 1.5.48).
|
|
1192
1204
|
*
|
|
1193
1205
|
* @example Pause playback from a menu item
|
|
1194
1206
|
* Playback ▸ Pause (and Ctrl+P) pauses and resumes, shows a check mark
|
|
@@ -6061,13 +6073,13 @@ export interface Player {
|
|
|
6061
6073
|
* | --- | --- | --- |
|
|
6062
6074
|
* | `stop` | F4 | close the source |
|
|
6063
6075
|
* | `play-pause` | Space, a left click on the video (player 0.5.2) | pause / resume, replay after the end |
|
|
6064
|
-
* | `previous` / `next` | PageUp, Mouse:
|
|
6076
|
+
* | `previous` / `next` | PageUp, Mouse:X2 / PageDown, Mouse:X1 (from 0.6.0; up to 0.5.x X1 was previous and X2 next) | ask the sequence provider |
|
|
6065
6077
|
* | `frame-back` / `frame-forward` | D / F | `player.step("frame", …)` |
|
|
6066
6078
|
* | `seek-backward` / `seek-forward` | ← / → | relative seek by `setStepSeek` seconds (or `host.run`'s `seconds`), exact or to keyframes as `setStepSeek` says |
|
|
6067
6079
|
* | `rate-normal`, `rate-down`, `rate-up` | Z, X, C | 1×, ±0.1 within 0.25–4 |
|
|
6068
6080
|
* | `volume-up` / `volume-down` | ↑ / ↓, wheel | ±5 (`volume-up` stops at 100) |
|
|
6069
6081
|
* | `mute` | M | toggle mute |
|
|
6070
|
-
* | `fullscreen` | Enter, Alt+Enter, F11 | toggle the main window's fullscreen: enter it when windowed, leave it when fullscreen, as the Toggle Fullscreen command does (SDK 1.5.21, player 0.5.2; `unavailable` when the player has no window) |
|
|
6082
|
+
* | `fullscreen` | Escape (from 0.6.0; up to 0.5.x Enter, Alt+Enter, F11) | toggle the main window's fullscreen: enter it when windowed, leave it when fullscreen, as the Toggle Fullscreen command does (SDK 1.5.21, player 0.5.2; `unavailable` when the player has no window) |
|
|
6071
6083
|
*
|
|
6072
6084
|
* In the binding overlay their keys are `host/<action>`. `fullscreen`
|
|
6073
6085
|
* changes no playback state: read the result from `window.state.fullscreen`
|
package/dist/plugin/ui.js
CHANGED
|
@@ -83,8 +83,113 @@ const isPrimitive = (value) => value === null ||
|
|
|
83
83
|
(typeof value === "number" && Number.isFinite(value));
|
|
84
84
|
/** A row as `JSON.stringify` writes it inside an array. */
|
|
85
85
|
const encodeRow = (row) => JSON.stringify(row) ?? "null";
|
|
86
|
+
/** What a row's encoding holds: a Markdown document, an image. */
|
|
87
|
+
const HAS_MARKDOWN = 1;
|
|
88
|
+
const HAS_IMAGE = 2;
|
|
89
|
+
const marksOf = (enc) => (enc.includes('"$vestraMarkdown"') ? HAS_MARKDOWN : 0) |
|
|
90
|
+
(enc.includes('"$vestraImage"') ? HAS_IMAGE : 0);
|
|
91
|
+
/**
|
|
92
|
+
* Row encodings by row object, shared by every property (a row's JSON does
|
|
93
|
+
* not depend on where it is bound). Only rows that cannot change are kept
|
|
94
|
+
* ({@link settled}): a plugin that mutates its rows in place sees no
|
|
95
|
+
* difference, and one that keeps unchanged rows as the same deep-frozen
|
|
96
|
+
* objects skips encoding them on every pass. Weak: a row the plugin drops
|
|
97
|
+
* goes with its entry.
|
|
98
|
+
*/
|
|
99
|
+
const settledRows = new WeakMap();
|
|
100
|
+
/**
|
|
101
|
+
* Frozen rows seen once. A frozen row is verified and kept on its second
|
|
102
|
+
* pass, so rows frozen afresh on every pass cost one set entry, not a
|
|
103
|
+
* verification each.
|
|
104
|
+
*/
|
|
105
|
+
const seenRows = new WeakSet();
|
|
106
|
+
/** Frozen rows that can still change (a mutable part, a getter): never kept. */
|
|
107
|
+
const unsettledRows = new WeakSet();
|
|
108
|
+
/**
|
|
109
|
+
* `value` can never change what `JSON.stringify` writes for it: a primitive,
|
|
110
|
+
* an image or document handle (their wire form is fixed when made), or a
|
|
111
|
+
* frozen plain object or array whose own enumerable properties are data
|
|
112
|
+
* properties (no getter, no own `toJSON`) holding such values. Freezing
|
|
113
|
+
* fixes the properties and the prototype for good, so the answer holds as
|
|
114
|
+
* long as the object lives.
|
|
115
|
+
*/
|
|
116
|
+
function settled(value, depth) {
|
|
117
|
+
if (value === null || typeof value !== "object")
|
|
118
|
+
return typeof value !== "function";
|
|
119
|
+
if (value instanceof ImageHandle || value instanceof MarkdownHandle)
|
|
120
|
+
return true;
|
|
121
|
+
if (depth > UI_VALUE_LIMITS.depth || !Object.isFrozen(value))
|
|
122
|
+
return false;
|
|
123
|
+
const proto = Object.getPrototypeOf(value);
|
|
124
|
+
if (Array.isArray(value)
|
|
125
|
+
? proto !== Array.prototype
|
|
126
|
+
: proto !== Object.prototype && proto !== null)
|
|
127
|
+
return false;
|
|
128
|
+
// An own `toJSON`, enumerable or not, is called by `JSON.stringify`.
|
|
129
|
+
if (Object.prototype.hasOwnProperty.call(value, "toJSON"))
|
|
130
|
+
return false;
|
|
131
|
+
for (const key of Object.keys(value)) {
|
|
132
|
+
const property = Object.getOwnPropertyDescriptor(value, key);
|
|
133
|
+
if (!property || !("value" in property))
|
|
134
|
+
return false;
|
|
135
|
+
if (!settled(property.value, depth + 1))
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
return true;
|
|
139
|
+
}
|
|
140
|
+
/** A row's encoding and marks, from {@link settledRows} when it is kept. */
|
|
141
|
+
function rowEncoding(row, cache) {
|
|
142
|
+
if (cache && row !== null && typeof row === "object") {
|
|
143
|
+
// A kept row is frozen: looked up first, `isFrozen` only on a miss.
|
|
144
|
+
const kept = settledRows.get(row);
|
|
145
|
+
if (kept)
|
|
146
|
+
return kept;
|
|
147
|
+
}
|
|
148
|
+
if (cache &&
|
|
149
|
+
row !== null &&
|
|
150
|
+
typeof row === "object" &&
|
|
151
|
+
Object.isFrozen(row)) {
|
|
152
|
+
const enc = encodeRow(row);
|
|
153
|
+
const entry = { enc, marks: marksOf(enc) };
|
|
154
|
+
if (!seenRows.has(row))
|
|
155
|
+
seenRows.add(row);
|
|
156
|
+
else if (!unsettledRows.has(row)) {
|
|
157
|
+
if (settled(row, 1))
|
|
158
|
+
settledRows.set(row, entry);
|
|
159
|
+
else
|
|
160
|
+
unsettledRows.add(row);
|
|
161
|
+
}
|
|
162
|
+
return entry;
|
|
163
|
+
}
|
|
164
|
+
const enc = encodeRow(row);
|
|
165
|
+
return { enc, marks: marksOf(enc) };
|
|
166
|
+
}
|
|
86
167
|
/** Each row of an array as `JSON.stringify` writes it; a hole is `null`. */
|
|
87
|
-
|
|
168
|
+
function encodeRows(rows) {
|
|
169
|
+
// A `toJSON` added to the shared prototypes would change any row's JSON:
|
|
170
|
+
// then nothing is taken from (or kept in) the cache.
|
|
171
|
+
const cache = !("toJSON" in Object.prototype) && !("toJSON" in Array.prototype);
|
|
172
|
+
const enc = [];
|
|
173
|
+
const marks = new Uint8Array(rows.length);
|
|
174
|
+
let markdown = false;
|
|
175
|
+
for (let i = 0; i < rows.length; i++) {
|
|
176
|
+
const row = rowEncoding(rows[i], cache);
|
|
177
|
+
enc[i] = row.enc;
|
|
178
|
+
marks[i] = row.marks;
|
|
179
|
+
if (row.marks & HAS_MARKDOWN)
|
|
180
|
+
markdown = true;
|
|
181
|
+
}
|
|
182
|
+
return { enc, marks, markdown, cache };
|
|
183
|
+
}
|
|
184
|
+
/** A row's limit counts, kept with its encoding when the row cannot change. */
|
|
185
|
+
function rowCount(row, cache) {
|
|
186
|
+
const kept = cache && row !== null && typeof row === "object"
|
|
187
|
+
? settledRows.get(row)
|
|
188
|
+
: undefined;
|
|
189
|
+
if (!kept)
|
|
190
|
+
return countRow(row);
|
|
191
|
+
return (kept.count ??= countRow(row));
|
|
192
|
+
}
|
|
88
193
|
function sameRows(a, b) {
|
|
89
194
|
if (a.length !== b.length)
|
|
90
195
|
return false;
|
|
@@ -102,7 +207,7 @@ function sentOf(value, patches) {
|
|
|
102
207
|
return {
|
|
103
208
|
kind: "rows",
|
|
104
209
|
rows: {
|
|
105
|
-
enc: encodeRows(value),
|
|
210
|
+
enc: encodeRows(value).enc,
|
|
106
211
|
field: keyed?.field ?? null,
|
|
107
212
|
keys: keyed?.keys ?? null,
|
|
108
213
|
counts: [],
|
|
@@ -142,7 +247,7 @@ function tooLarge(key, ops) {
|
|
|
142
247
|
function rowPatch(sent, next, rows) {
|
|
143
248
|
if (sent.enc.length === 0)
|
|
144
249
|
return null;
|
|
145
|
-
if (next.
|
|
250
|
+
if (next.markdown)
|
|
146
251
|
return null;
|
|
147
252
|
const nextKeyed = sent.field === null ? null : next.keyed();
|
|
148
253
|
const keyed = nextKeyed !== null && nextKeyed.field === sent.field;
|
|
@@ -152,11 +257,12 @@ function rowPatch(sent, next, rows) {
|
|
|
152
257
|
// A released image is refused wherever it is, as in a whole write: the
|
|
153
258
|
// rows the patch does not carry are checked too (those holding images).
|
|
154
259
|
for (let j = 0; j < rows.length; j++)
|
|
155
|
-
if (plan.from[j] >= 0 && next.
|
|
260
|
+
if (plan.from[j] >= 0 && next.marks[j] & HAS_IMAGE)
|
|
156
261
|
wireValue(rows[j] ?? null);
|
|
157
262
|
const counts = next.enc.map((_, j) => {
|
|
158
263
|
const from = plan.from[j];
|
|
159
|
-
return (from >= 0 ? sent.counts[from] : undefined) ??
|
|
264
|
+
return ((from >= 0 ? sent.counts[from] : undefined) ??
|
|
265
|
+
rowCount(rows[j], next.cache));
|
|
160
266
|
});
|
|
161
267
|
if (!withinLimits(counts))
|
|
162
268
|
return null;
|
|
@@ -372,8 +478,10 @@ export class ViewBase {
|
|
|
372
478
|
}
|
|
373
479
|
if (Array.isArray(value) && this.#patches) {
|
|
374
480
|
// Row by row: the same work as one `JSON.stringify`, and the rows to
|
|
375
|
-
// diff against those last sent.
|
|
376
|
-
|
|
481
|
+
// diff against those last sent. Rows that cannot change (deep-frozen
|
|
482
|
+
// objects bound again, SDK 1.5.47) are taken from `settledRows`.
|
|
483
|
+
const encoded = encodeRows(value);
|
|
484
|
+
const { enc } = encoded;
|
|
377
485
|
const signature = () => `[${enc.join(",")}]`;
|
|
378
486
|
const changed = sent?.kind === "rows"
|
|
379
487
|
? !sameRows(sent.rows.enc, enc)
|
|
@@ -386,7 +494,7 @@ export class ViewBase {
|
|
|
386
494
|
value,
|
|
387
495
|
changed,
|
|
388
496
|
signature,
|
|
389
|
-
rows: {
|
|
497
|
+
rows: { ...encoded, keyed },
|
|
390
498
|
sent: (counts = []) => ({
|
|
391
499
|
kind: "rows",
|
|
392
500
|
rows: {
|
|
@@ -470,9 +578,7 @@ export class ViewBase {
|
|
|
470
578
|
const seq = ++this.#seq;
|
|
471
579
|
// Rows holding Markdown documents go whole, as before: the player
|
|
472
580
|
// merges documents in place and takes no parts of them.
|
|
473
|
-
if (Array.isArray(value) &&
|
|
474
|
-
pending.rows &&
|
|
475
|
-
!pending.rows.enc.some((row) => row.includes(MARKDOWN_KEY))) {
|
|
581
|
+
if (Array.isArray(value) && pending.rows && !pending.rows.markdown) {
|
|
476
582
|
const sizes = pending.rows.enc.map(jsonBytes);
|
|
477
583
|
const count = prefixWithin(sizes, CHUNK_BYTES);
|
|
478
584
|
if (count < value.length) {
|
package/llms-full.txt
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# @vestra-player/sdk 1.5.
|
|
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
|
|
|
@@ -2630,8 +2637,8 @@ Every command the player (`host`) and its built-in plugins register when the pla
|
|
|
2630
2637
|
| Copy to Clipboard ▸ Folder Path | | `vestra.playback/copy-folder` |
|
|
2631
2638
|
| Copy to Clipboard ▸ Full Path | | `vestra.playback/copy-path` |
|
|
2632
2639
|
| Playback ▸ Play/Pause | `Space`, `Mouse:LeftClick` | `host/play-pause` |
|
|
2633
|
-
| Playback ▸ Previous | `PageUp`, `Mouse:
|
|
2634
|
-
| Playback ▸ Next | `PageDown`, `Mouse:
|
|
2640
|
+
| Playback ▸ Previous | `PageUp`, `Mouse:X2` | `host/previous` |
|
|
2641
|
+
| Playback ▸ Next | `PageDown`, `Mouse:X1` | `host/next` |
|
|
2635
2642
|
| Playback ▸ Seek ▸ Previous Frame | `D` | `host/frame-back` |
|
|
2636
2643
|
| Playback ▸ Seek ▸ Next Frame | `F` | `host/frame-forward` |
|
|
2637
2644
|
| Playback ▸ Seek ▸ Previous Keyframe | `Ctrl+Shift+ArrowLeft` | `vestra.playback/keyframe-back` |
|
|
@@ -2857,7 +2864,7 @@ Every command the player (`host`) and its built-in plugins register when the pla
|
|
|
2857
2864
|
| Audio ▸ Processing ▸ Crystalizer | `Shift+C` | `vestra.audio/audio.clarity` |
|
|
2858
2865
|
| Audio ▸ Reset | | `vestra.audio/audio.reset` |
|
|
2859
2866
|
| Audio ▸ Show Album Cover for Audio Only | | `vestra.visualizer/audio-cover` |
|
|
2860
|
-
| Toggle Fullscreen | `
|
|
2867
|
+
| Toggle Fullscreen | `Escape` | `host/fullscreen` |
|
|
2861
2868
|
| Screen ▸ Picture-in-Picture | | `vestra.window/pip` |
|
|
2862
2869
|
| Screen ▸ Mini Size | ``` | `vestra.window/mini` |
|
|
2863
2870
|
| Screen ▸ 0.5× | `1` | `vestra.window/size.0.5` |
|
|
@@ -2901,7 +2908,8 @@ Every command the player (`host`) and its built-in plugins register when the pla
|
|
|
2901
2908
|
| Settings ▸ Language ▸ Español | | `vestra.preferences/language.es` |
|
|
2902
2909
|
| About Vestra Player… | `F1` | `vestra.info/info.about` |
|
|
2903
2910
|
| Quit | `Cmd+Q` | `host/quit` |
|
|
2904
|
-
|
|
|
2911
|
+
| Enter Fullscreen (not in the menu) | | `host/enter-fullscreen` |
|
|
2912
|
+
| Leave Fullscreen (not in the menu) | | `host/exit-fullscreen` |
|
|
2905
2913
|
| Toggle Sidebar (not in the menu) | | `host/sidebar` |
|
|
2906
2914
|
| Brief Info (not in the menu) | `Shift+Tab` | `vestra.info/info.brief` |
|
|
2907
2915
|
| Play (not in the menu) | `Enter`, `P` (panel focus) | `vestra.playlist/playlist.play` |
|
|
@@ -2979,7 +2987,7 @@ A menu item shows the first of its keyboard bindings that is in effect.
|
|
|
2979
2987
|
| `digits` | `0`–`9` | Digit keys of the main keyboard: `0` to `9`. |
|
|
2980
2988
|
| `functionKeys` | `F1`–`F24` | Function keys: `F1` to `F24`. |
|
|
2981
2989
|
| `named` | `Space`, `Enter`, `Tab`, `Escape`, `Backspace`, `Delete`, `Insert`, `Home`, `End`, `PageUp`, `PageDown`, `ArrowLeft`, `ArrowRight`, `ArrowUp`, `ArrowDown`, `Plus`, `Numpad0`, `Numpad1`, `Numpad2`, `Numpad3`, `Numpad4`, `Numpad5`, `Numpad6`, `Numpad7`, `Numpad8`, `Numpad9`, `NumpadPlus`, `NumpadMinus`, `NumpadMultiply`, `NumpadDivide`, `NumpadEnter`, `NumpadDecimal` | Other named keys, matched by the physical key. `Plus` is the `+` key however it is typed; the `Numpad…` names are the numeric keypad only, never the main keyboard. |
|
|
2982
|
-
| `mouse` | `Mouse:LeftClick`, `Mouse:LeftDoubleClick`, `Mouse:LeftDoubleClick:LeftHalf`, `Mouse:LeftDoubleClick:RightHalf`, `Mouse:MiddleClick`, `Mouse:X1`, `Mouse:X2`, `Mouse:WheelUp`, `Mouse:WheelDown`, `Mouse:WheelLeft`, `Mouse:WheelRight` | Mouse inputs on the video area. `:LeftHalf` and `:RightHalf` tell the halves of the video apart for a double click. |
|
|
2990
|
+
| `mouse` | `Mouse:LeftClick`, `Mouse:LeftDoubleClick`, `Mouse:LeftDoubleClick:LeftHalf`, `Mouse:LeftDoubleClick:RightHalf`, `Mouse:MiddleClick`, `Mouse:X1`, `Mouse:X2`, `Mouse:WheelUp`, `Mouse:WheelDown`, `Mouse:WheelLeft`, `Mouse:WheelRight` | Mouse inputs on the video area; the side buttons `Mouse:X1` (back) and `Mouse:X2` (forward) anywhere in the main window or the floating sidebar window, except while a menu is open (player 0.6.0). `:LeftHalf` and `:RightHalf` tell the halves of the video apart for a double click. |
|
|
2983
2991
|
|
|
2984
2992
|
#### Core actions
|
|
2985
2993
|
|
|
@@ -2989,8 +2997,8 @@ The player's own playback commands, which plugin.host.run runs by id (capability
|
|
|
2989
2997
|
| --- | --- | --- |
|
|
2990
2998
|
| `stop` | Close | `F4` |
|
|
2991
2999
|
| `play-pause` | Playback ▸ Play/Pause | `Space`, `Mouse:LeftClick` |
|
|
2992
|
-
| `previous` | Playback ▸ Previous | `PageUp`, `Mouse:
|
|
2993
|
-
| `next` | Playback ▸ Next | `PageDown`, `Mouse:
|
|
3000
|
+
| `previous` | Playback ▸ Previous | `PageUp`, `Mouse:X2` |
|
|
3001
|
+
| `next` | Playback ▸ Next | `PageDown`, `Mouse:X1` |
|
|
2994
3002
|
| `frame-back` | Playback ▸ Seek ▸ Previous Frame | `D` |
|
|
2995
3003
|
| `frame-forward` | Playback ▸ Seek ▸ Next Frame | `F` |
|
|
2996
3004
|
| `seek-backward` | Playback ▸ Seek ▸ Back 5 s | `ArrowLeft` |
|
|
@@ -3001,9 +3009,9 @@ The player's own playback commands, which plugin.host.run runs by id (capability
|
|
|
3001
3009
|
| `volume-up` | Audio ▸ Volume + | `ArrowUp`, `Mouse:WheelUp` |
|
|
3002
3010
|
| `volume-down` | Audio ▸ Volume − | `ArrowDown`, `Mouse:WheelDown` |
|
|
3003
3011
|
| `mute` | Audio ▸ Mute | `M` |
|
|
3004
|
-
| `fullscreen` | Toggle Fullscreen | `
|
|
3012
|
+
| `fullscreen` | Toggle Fullscreen | `Escape` |
|
|
3005
3013
|
|
|
3006
|
-
The player also owns **Toggle Fullscreen** (`host/fullscreen`), **Leave Fullscreen** (`host/exit-fullscreen
|
|
3014
|
+
The player also owns **Toggle Fullscreen** (`host/fullscreen`, `Escape` from player 0.6.0; up to 0.5.x `Enter`, `Alt+Enter` and `F11`), **Enter Fullscreen** (`host/enter-fullscreen`, player 0.6.0: no default key, outside the menu, enabled only when windowed), **Leave Fullscreen** (`host/exit-fullscreen`: no default key from player 0.6.0, up to 0.5.x `Escape`; outside the menu, enabled only in fullscreen) and **Quit** (`host/quit`), and **Toggle Sidebar** (`host/sidebar`, player 0.5.10: the bottom bar's sidebar button, outside the menu, no default key); their keys are in Built-in commands.
|
|
3007
3015
|
|
|
3008
3016
|
#### Keys the system takes first
|
|
3009
3017
|
|
|
@@ -4106,7 +4114,7 @@ flowchart TB
|
|
|
4106
4114
|
|
|
4107
4115
|
- **Where bindings apply.** Bindings belong to the main window: they fire while it or the floating sidebar window has focus. They do **not** fire in tool windows or the Options window; keys there are the Slint file's own business (a `FocusScope`, a default button). While a menu or a system dialog is open, bindings pause.
|
|
4108
4116
|
- **Conflicts are not errors.** A binding that loses is written to the player's log and takes effect by itself when the winner goes away (its plugin stops, the user rebinds it). Tests can check `host.conflicts` (TestHost.conflicts).
|
|
4109
|
-
- **A disabled command lets its key go.** This is what makes context-dependent keys work:
|
|
4117
|
+
- **A disabled command lets its key go.** This is what makes context-dependent keys work: a command bound to `Delete` that is disabled while nothing is selected does nothing then, so `Delete` reaches the focused control (a text field deletes a character). Give `reason` to a command whose key should still be taken and explained while it is disabled.
|
|
4110
4118
|
- **`repeat: true`** fires again on the keyboard's auto-repeat while the key is held; without it a binding fires once per press.
|
|
4111
4119
|
- **Some keys never reach a binding.** The system takes some keys first, and the standard close keys close a tool window, the floating sidebar or the Options window before any binding sees them (in the main window they are ordinary keys). Keys the system takes first lists them; default bindings avoid them.
|
|
4112
4120
|
|
|
@@ -4119,7 +4127,7 @@ Key names describe keys, not characters, with two deliberate exceptions:
|
|
|
4119
4127
|
- **Option combinations match the physical key**, so `Alt+O` fires for ⌥O although that key types `ø`, and dead keys (⌥E, ⌥N…) fire too. On layouts that need Option to type a symbol (German ⌥5 = ``, ⌥L = `@`), a binding for the typed symbol (`"["`, `"@"`) wins over one for the physical key (`Alt+5`).
|
|
4120
4128
|
- **`Ctrl` is portable, `Cmd` is not.** On macOS `Ctrl+X` fires for both ⌃X and ⌘X, `Cmd+X` only for ⌘X. On Windows `Cmd` is the Windows logo key, which the system mostly keeps, so write `Ctrl+…` for a binding that works on both systems.
|
|
4121
4129
|
|
|
4122
|
-
Mouse bindings apply only over the video area. `Mouse:LeftDoubleClick:LeftHalf` and `:RightHalf` win over the plain double click; wheel bindings count accumulated scroll steps. The right button is reserved for the context menu, and a left drag in window mode moves the window instead of clicking. A `Mouse:LeftClick` fires on release, except when the same modifiers also have a double-click binding and the click's command is not checkable: then the player waits the system's double-click interval to tell a click from a double click. (A checkable command such as play/pause runs at once; a double click runs it twice, which restores its state, and the double-click binding still fires.) The player's defaults (player 0.5.2): a left click on the video is Play/Pause and nothing is bound to a double click (up to 0.5.1 a double click was Toggle Fullscreen); a click whose command is disabled does nothing.
|
|
4130
|
+
Mouse bindings apply only over the video area, except the side buttons `Mouse:X1` (back) and `Mouse:X2` (forward), which fire anywhere in the main window or the floating sidebar window, except while a menu is open, as in browsers (player 0.6.0; before, only over the video area). `Mouse:LeftDoubleClick:LeftHalf` and `:RightHalf` win over the plain double click; wheel bindings count accumulated scroll steps. The right button is reserved for the context menu, and a left drag in window mode moves the window instead of clicking. A `Mouse:LeftClick` fires on release, except when the same modifiers also have a double-click binding and the click's command is not checkable: then the player waits the system's double-click interval to tell a click from a double click. (A checkable command such as play/pause runs at once; a double click runs it twice, which restores its state, and the double-click binding still fires.) The player's defaults (player 0.5.2): a left click on the video is Play/Pause and nothing is bound to a double click (up to 0.5.1 a double click was Toggle Fullscreen); a click whose command is disabled does nothing.
|
|
4123
4131
|
|
|
4124
4132
|
##### The user's bindings
|
|
4125
4133
|
|
|
@@ -4144,7 +4152,7 @@ plugin.commands.add({
|
|
|
4144
4152
|
|
|
4145
4153
|
#### The player's own commands
|
|
4146
4154
|
|
|
4147
|
-
The player registers some commands itself, with the owner `host`: **Toggle Fullscreen
|
|
4155
|
+
The player registers some commands itself, with the owner `host`: **Toggle Fullscreen** (`Escape` from player 0.6.0), **Enter Fullscreen** and **Leave Fullscreen** (player 0.6.0: no default key, each enabled only where it acts), **Quit**, **Toggle Sidebar** (the bottom bar's sidebar button, player 0.5.10) and the core playback actions (play/pause, stop, previous/next, frame step, relative seek, speed, volume, mute). They exist whether or not any plugin runs, and the user rebinds them like any other command.
|
|
4148
4156
|
|
|
4149
4157
|
A plugin button that does what a core action does should run that action with plugin.host.run (capability `commands`): it applies the same checks and shows the same on-screen text as the menu item and the key, so a bottom bar's play button and the Space key behave identically. The action ids, their menu places and default keys are in Reference: commands; what they do to playback is in Playback.
|
|
4150
4158
|
|
|
@@ -4650,6 +4658,8 @@ From player 0.6.0 and SDK 1.5.44, a changed array (a list's rows) is sent as the
|
|
|
4650
4658
|
- **Your Slint code must not write into the rows of an array property** (`rows[i].selected = true` in a handler). A whole write used to overwrite such a change on the next update; a row patch touches only the rows whose value changed in your plugin, so the edited row keeps the Slint-side value until your value for that row changes. Send the event to the plugin with a callback and change the row there.
|
|
4651
4659
|
- **`set()` with an unchanged array sends the whole array**, as `set()` always sends: use it to pull a list back to your rows. A bound array whose rows did not change sends nothing.
|
|
4652
4660
|
|
|
4661
|
+
To find the changed rows the SDK encodes every row of a bound array on each update pass, which in a list of tens of thousands of rows is most of a pass. From SDK 1.5.47 it skips that for rows it can prove unchanged: a **deep-frozen** row object (`Object.freeze` on the row and on every object and array inside it, plain objects and arrays only, no getters or own `toJSON`; image and document handles may sit inside: they are immutable, so they need no freezing) that the bind function returns again is encoded once and then taken from a cache kept by the row object. To use it in a long list, keep each row as the same frozen object while it does not change and make a new one when it does (memoize by the row's id and the fields it shows). Nothing else changes: rows that are not frozen, or that hold a mutable part, are encoded on every pass as before, so a plugin that changes its rows in place keeps working.
|
|
4662
|
+
|
|
4653
4663
|
##### Two-way controls
|
|
4654
4664
|
|
|
4655
4665
|
A control whose value both the user and the plugin change (a slider, a switch, a text field, a combo box) needs a **two-way binding to an `in-out` property**:
|
|
@@ -5817,6 +5827,8 @@ Every SDK release's CHANGELOG entry opens with who must change and what happens
|
|
|
5817
5827
|
|
|
5818
5828
|
| Release | Who must change | Guide |
|
|
5819
5829
|
| --- | --- | --- |
|
|
5830
|
+
| [1.5.48](https://github.com/vestrasys/vestra-player-sdk/blob/main/CHANGELOG.md#1548) | **Nobody, but plugin tests may now fail.** No changes to the TypeScript API beyond new fields of `COMMAND_LIMITS`, `apiVersion` stays `vestra/1`, no new capability, no player version needed. | — |
|
|
5831
|
+
| [1.5.47](https://github.com/vestrasys/vestra-player-sdk/blob/main/CHANGELOG.md#1547) | **Nobody.** No changes to the TypeScript API, `apiVersion` stays `vestra/1`, no new capability, no player version needed. | — |
|
|
5820
5832
|
| [1.5.46](https://github.com/vestrasys/vestra-player-sdk/blob/main/CHANGELOG.md#1546) | **Nobody.** No changes to the TypeScript API, `apiVersion` stays `vestra/1`, no new capability. | — |
|
|
5821
5833
|
| [1.5.45](https://github.com/vestrasys/vestra-player-sdk/blob/main/CHANGELOG.md#1545) | **Nobody.** No changes to the TypeScript API, `apiVersion` stays `vestra/1`, no new capability, no player version needed. | — |
|
|
5822
5834
|
| [1.5.44](https://github.com/vestrasys/vestra-player-sdk/blob/main/CHANGELOG.md#1544) | **Nobody.** No changes to the TypeScript API, `apiVersion` stays `vestra/1`, no new capability. | — |
|
|
@@ -6697,7 +6709,9 @@ an item without `order` moves the whole menu to the top; to avoid that,
|
|
|
6697
6709
|
give it an order in the menu's range (Video: 400–499).
|
|
6698
6710
|
|
|
6699
6711
|
Commands.add throws `invalid` synchronously for a bad path,
|
|
6700
|
-
order
|
|
6712
|
+
order or binding; the player refuses an empty or overlong text, a control
|
|
6713
|
+
character or whitespace at either end (`COMMAND_LIMITS`) with
|
|
6714
|
+
`invalid` (through `ready`). The same path registered twice, or a path that
|
|
6701
6715
|
is both an item and a submenu, fails with `conflict` (through `ready`).
|
|
6702
6716
|
|
|
6703
6717
|
```ts
|
|
@@ -6765,9 +6779,12 @@ interface CommandOptions {
|
|
|
6765
6779
|
* `Home`, `End`, `PageUp`, `PageDown`, `ArrowLeft`/`Right`/`Up`/`Down`,
|
|
6766
6780
|
* `Plus`, any other single symbol as typed (`">"`, not `"Shift+."`),
|
|
6767
6781
|
* `Numpad0`–`Numpad9`, `NumpadPlus`, `NumpadMinus`, `NumpadMultiply`,
|
|
6768
|
-
* `NumpadDivide`, `NumpadEnter`, `NumpadDecimal`,
|
|
6782
|
+
* `NumpadDivide`, `NumpadEnter`, `NumpadDecimal`, the video-area mouse
|
|
6769
6783
|
* inputs `Mouse:LeftClick`, `Mouse:LeftDoubleClick[:LeftHalf|:RightHalf]`,
|
|
6770
|
-
* `Mouse:MiddleClick`, `Mouse:
|
|
6784
|
+
* `Mouse:MiddleClick`, `Mouse:WheelUp`/`Down`/`Left`/`Right`, and the side
|
|
6785
|
+
* buttons `Mouse:X1` (back), `Mouse:X2` (forward), from 0.6.0 on anywhere
|
|
6786
|
+
* in the main window or the floating sidebar window, except while a menu
|
|
6787
|
+
* is open.
|
|
6771
6788
|
*
|
|
6772
6789
|
* Platforms: on macOS `Ctrl+X` fires for both ⌃X and ⌘X while `Cmd+X`
|
|
6773
6790
|
* means ⌘X only; on Windows `Cmd` is the Windows logo key, so write
|
|
@@ -6904,7 +6921,8 @@ interface Commands {
|
|
|
6904
6921
|
*
|
|
6905
6922
|
* Titles and paths cannot change in place: put changing items (track
|
|
6906
6923
|
* lists, presets) in a child Scope and rebuild it.
|
|
6907
|
-
* @throws invalid synchronously for a malformed path (including an unknown `@` menu id), order
|
|
6924
|
+
* @throws invalid synchronously for a malformed path (including an unknown `@` menu id), order or binding; `conflict` for an id this plugin has already declared and not disposed.
|
|
6925
|
+
* @throws invalid (`ready` rejects; during activation, activation fails) for a text the player refuses: an id, title, path segment, group or reason that is empty, longer than its `COMMAND_LIMITS` bytes (a text value counted by its English rendering), has a control character or whitespace at either end, or an id with other characters than ASCII letters, digits, `.`, `_` and `-`. `TestHost` refuses the same (SDK 1.5.48).
|
|
6908
6926
|
* @capability commands
|
|
6909
6927
|
* @example Pause playback from a menu item
|
|
6910
6928
|
* Playback ▸ Pause (and Ctrl+P) pauses and resumes, shows a check mark
|
|
@@ -11252,13 +11270,13 @@ and the main window's fullscreen toggle.
|
|
|
11252
11270
|
| --- | --- | --- |
|
|
11253
11271
|
| `stop` | F4 | close the source |
|
|
11254
11272
|
| `play-pause` | Space, a left click on the video (player 0.5.2) | pause / resume, replay after the end |
|
|
11255
|
-
| `previous` / `next` | PageUp, Mouse:
|
|
11273
|
+
| `previous` / `next` | PageUp, Mouse:X2 / PageDown, Mouse:X1 (from 0.6.0; up to 0.5.x X1 was previous and X2 next) | ask the sequence provider |
|
|
11256
11274
|
| `frame-back` / `frame-forward` | D / F | `player.step("frame", …)` |
|
|
11257
11275
|
| `seek-backward` / `seek-forward` | ← / → | relative seek by `setStepSeek` seconds (or `host.run`'s `seconds`), exact or to keyframes as `setStepSeek` says |
|
|
11258
11276
|
| `rate-normal`, `rate-down`, `rate-up` | Z, X, C | 1×, ±0.1 within 0.25–4 |
|
|
11259
11277
|
| `volume-up` / `volume-down` | ↑ / ↓, wheel | ±5 (`volume-up` stops at 100) |
|
|
11260
11278
|
| `mute` | M | toggle mute |
|
|
11261
|
-
| `fullscreen` | Enter, Alt+Enter, F11 | toggle the main window's fullscreen: enter it when windowed, leave it when fullscreen, as the Toggle Fullscreen command does (SDK 1.5.21, player 0.5.2; `unavailable` when the player has no window) |
|
|
11279
|
+
| `fullscreen` | Escape (from 0.6.0; up to 0.5.x Enter, Alt+Enter, F11) | toggle the main window's fullscreen: enter it when windowed, leave it when fullscreen, as the Toggle Fullscreen command does (SDK 1.5.21, player 0.5.2; `unavailable` when the player has no window) |
|
|
11262
11280
|
|
|
11263
11281
|
In the binding overlay their keys are `host/<action>`. `fullscreen`
|
|
11264
11282
|
changes no playback state: read the result from `window.state.fullscreen`
|
|
@@ -17307,6 +17325,9 @@ fails; later the failure goes to `Scope.onError`), commands
|
|
|
17307
17325
|
already registered are unaffected, and disposing a command frees its slot
|
|
17308
17326
|
at once. TestHost enforces the same caps.
|
|
17309
17327
|
|
|
17328
|
+
The other fields are the player's rules for one declaration (since
|
|
17329
|
+
1.5.48): a command past them is refused with `invalid`, the same way.
|
|
17330
|
+
|
|
17310
17331
|
For menus built from media content (tracks, chapters, presets), set your
|
|
17311
17332
|
own sensible display cap well below `perPlugin`.
|
|
17312
17333
|
|
|
@@ -17316,6 +17337,22 @@ const COMMAND_LIMITS: {
|
|
|
17316
17337
|
readonly perPlugin: 160;
|
|
17317
17338
|
/** Commands of every plugin plus the player's own, together (1024). */
|
|
17318
17339
|
readonly total: 1024;
|
|
17340
|
+
/** Segments of a menu path, the leaf included. */
|
|
17341
|
+
readonly depth: 8;
|
|
17342
|
+
/** UTF-8 bytes of a command's local id: ASCII letters, digits, `.`, `_` and `-`. */
|
|
17343
|
+
readonly idBytes: 128;
|
|
17344
|
+
/**
|
|
17345
|
+
* UTF-8 bytes of a title, a text value counted by its English rendering. Like every command text: not empty, no control characters, no whitespace at either end.
|
|
17346
|
+
*/
|
|
17347
|
+
readonly titleBytes: 128;
|
|
17348
|
+
/** UTF-8 bytes of a menu path segment, a text value counted by its English rendering. */
|
|
17349
|
+
readonly segmentBytes: 80;
|
|
17350
|
+
/** UTF-8 bytes of a group. */
|
|
17351
|
+
readonly groupBytes: 80;
|
|
17352
|
+
/** UTF-8 bytes of a state's reason, as shown in the player's language. */
|
|
17353
|
+
readonly reasonBytes: 512;
|
|
17354
|
+
/** The largest `order` either way: an order is within ±1,000,000. */
|
|
17355
|
+
readonly order: 1000000;
|
|
17319
17356
|
};
|
|
17320
17357
|
```
|
|
17321
17358
|
|
|
@@ -17625,7 +17662,9 @@ const BINDING_INPUTS: Readonly<{
|
|
|
17625
17662
|
* Other named keys, matched by the physical key. `Plus` is the `+` key however it is typed; the `Numpad…` names are the numeric keypad only, never the main keyboard.
|
|
17626
17663
|
*/
|
|
17627
17664
|
readonly named: readonly ["Space", "Enter", "Tab", "Escape", "Backspace", "Delete", "Insert", "Home", "End", "PageUp", "PageDown", "ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown", "Plus", ...string[], "NumpadPlus", "NumpadMinus", "NumpadMultiply", "NumpadDivide", "NumpadEnter", "NumpadDecimal"];
|
|
17628
|
-
/**
|
|
17665
|
+
/**
|
|
17666
|
+
* Mouse inputs on the video area; the side buttons `Mouse:X1` (back) and `Mouse:X2` (forward) anywhere in the main window or the floating sidebar window, except while a menu is open (player 0.6.0). `:LeftHalf` and `:RightHalf` tell the halves of the video apart for a double click.
|
|
17667
|
+
*/
|
|
17629
17668
|
readonly mouse: readonly ["Mouse:LeftClick", "Mouse:LeftDoubleClick", "Mouse:LeftDoubleClick:LeftHalf", "Mouse:LeftDoubleClick:RightHalf", "Mouse:MiddleClick", "Mouse:X1", "Mouse:X2", "Mouse:WheelUp", "Mouse:WheelDown", "Mouse:WheelLeft", "Mouse:WheelRight"];
|
|
17630
17669
|
}>;
|
|
17631
17670
|
```
|
package/llms.txt
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Vestra Player SDK
|
|
2
2
|
|
|
3
|
-
> @vestra-player/sdk 1.5.
|
|
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
|
|