@vestra-player/sdk 1.5.44 → 1.5.45
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 +11 -0
- package/dist/plugin/index.js +4 -1
- package/dist/plugin/player.d.ts +11 -0
- package/dist/plugin/player.js +80 -7
- package/dist/plugin/runtime.d.ts +58 -1
- package/dist/plugin/runtime.js +216 -16
- package/dist/plugin/scope.d.ts +9 -1
- package/dist/plugin/scope.js +24 -0
- package/dist/plugin/testing.d.ts +7 -0
- package/dist/plugin/testing.js +7 -0
- package/dist/plugin/types.d.ts +24 -15
- package/dist/plugin/ui.d.ts +5 -1
- package/dist/plugin/ui.js +19 -5
- package/llms-full.txt +47 -24
- package/llms.txt +23 -23
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.5.45
|
|
4
|
+
|
|
5
|
+
**Who must change: nobody.** No changes to the TypeScript API, `apiVersion` stays `vestra/1`, no new capability, no player version needed. Only when the SDK re-evaluates bindings changes; what a plugin's views and commands show stays the same. One case can show in tests: a plugin that changes state outside a handler (a timer, a promise continuation after the handler returned, a child process) without calling `plugin.update()`, and a test that relied on `host.setSource(...)` to refresh it. Have the plugin call `plugin.update()` after such a change (as the documentation always said), or wait `await host.settle(1000)` in the test; `settle()` does not wait for the once-a-second full pass.
|
|
6
|
+
|
|
7
|
+
- **Precise update passes.** While media plays, the player sends its state about ten times a second, and each change used to re-run every `view.bind` function and every command `enabled`, `checked`, `busy` and `reason` function of the plugin. Now the SDK records which fields of `plugin.player.state` each of them read (`position`, `paused`, `phase`, `source`, …) and, after a player state change, re-runs only those that read a field that changed: a list bound in a panel no longer re-runs on every position tick. `isCurrent` and `Media.current` count as reading the source's identity.
|
|
8
|
+
- **Full passes as before** after any code of the plugin the SDK runs (a command's `run`, a window, panel or settings page callback, a listener, `onEnd`, and so on), when the promise it returned settles, after `plugin.update()`, and after every other state change (window, subtitles, panels, timeline, appearance). One exception keeps a playing list cheap: a `player.onChange` listener that returned synchronously and read only fields of the state that did not change is not followed by a full pass (a listener that read a changed field, no field at all, or returned a promise still is).
|
|
9
|
+
- **A full pass at least once a second while precise passes skip something.** State a plugin changes outside code the SDK runs (its own `setInterval`, a socket callback) without calling `plugin.update()` used to show at the next player state change (within about 0.1 s while playing); now it shows at the latest about 1 s later. Call `plugin.update()` after such a change, as the documentation always said.
|
|
10
|
+
- **`plugin.player.state`** reads as before (the same keys in the same order, frozen, JSON, spread, `structuredClone`, `util.inspect` and `assert.deepEqual` give the same values); its fields are getters now, so `Object.getOwnPropertyDescriptor` shows `get` instead of `value`, and `Reflect.ownKeys` and `Object.getOwnPropertySymbols` also list `Symbol(nodejs.util.inspect.custom)` (not enumerable).
|
|
11
|
+
- **`Scrub.active`** turning false (a new source, a failed seek) now causes a full pass, so a binding that reads it follows.
|
|
12
|
+
- **`TestHost`** runs the same code: the plugin's tests see the same passes as the player. `settle()` does not wait for the once-a-second full pass; `settle(1000)` does (`TestHost.settle`, _Test a plugin_ say so).
|
|
13
|
+
|
|
3
14
|
## 1.5.44
|
|
4
15
|
|
|
5
16
|
**Who must change: nobody.** No changes to the TypeScript API, `apiVersion` stays `vestra/1`, no new capability. How the SDK sends array properties changes, on players that take it (player 0.6.0); with older players it sends whole values as before. The read-only copy of the widget library gains components that need player 0.6.0 (`"minPlayerVersion": "0.6.0"` for a plugin whose Slint uses them; an older player fails to compile such a file).
|
package/dist/plugin/index.js
CHANGED
package/dist/plugin/player.d.ts
CHANGED
|
@@ -3,6 +3,12 @@ import { Latest } from "./runtime.ts";
|
|
|
3
3
|
import type { Runtime } from "./runtime.ts";
|
|
4
4
|
import type { ScopeImpl } from "./scope.ts";
|
|
5
5
|
import type { Conditional, Disposable, Player, PlayerState, PluginError } from "./types.ts";
|
|
6
|
+
/**
|
|
7
|
+
* The key of `plugin.player.state` in the runtime's field tracking
|
|
8
|
+
* (`Runtime.depends`). Its fields are the state's own, plus `stamp` for
|
|
9
|
+
* what `isCurrent` and `Media.current` compare.
|
|
10
|
+
*/
|
|
11
|
+
export declare const PLAYER = "player";
|
|
6
12
|
export declare const rawOf: (state: PlayerState) => SourceState | undefined;
|
|
7
13
|
/** A step-seek length the player accepts: 0.1–3600 seconds in tenths. */
|
|
8
14
|
export declare const validStep: (seconds: unknown) => seconds is number;
|
|
@@ -14,6 +20,11 @@ export declare class PlayerService {
|
|
|
14
20
|
readonly runtime: Runtime;
|
|
15
21
|
raw: SourceState;
|
|
16
22
|
state: PlayerState;
|
|
23
|
+
/**
|
|
24
|
+
* The fields the last accepted state changed (`onChange` listeners are
|
|
25
|
+
* told so a listener that read none of them needs no full pass).
|
|
26
|
+
*/
|
|
27
|
+
change: ReadonlyMap<string, ReadonlySet<string>>;
|
|
17
28
|
readonly output: Latest<{
|
|
18
29
|
volume?: number;
|
|
19
30
|
muted?: boolean;
|
package/dist/plugin/player.js
CHANGED
|
@@ -40,9 +40,63 @@ const audioTrack = (track) => Object.freeze({
|
|
|
40
40
|
title: track.title,
|
|
41
41
|
language: track.language,
|
|
42
42
|
});
|
|
43
|
-
|
|
43
|
+
/**
|
|
44
|
+
* The key of `plugin.player.state` in the runtime's field tracking
|
|
45
|
+
* (`Runtime.depends`). Its fields are the state's own, plus `stamp` for
|
|
46
|
+
* what `isCurrent` and `Media.current` compare.
|
|
47
|
+
*/
|
|
48
|
+
export const PLAYER = "player";
|
|
49
|
+
/** `util.inspect` shows a state's values, not its getters. */
|
|
50
|
+
const INSPECT = Symbol.for("nodejs.util.inspect.custom");
|
|
51
|
+
const values = new WeakMap();
|
|
52
|
+
/**
|
|
53
|
+
* The state a plugin sees: each field a getter over the same values, so the
|
|
54
|
+
* SDK learns which fields a binding or listener read (`Runtime.depends`).
|
|
55
|
+
* It behaves like the plain frozen object it was: same keys in the same
|
|
56
|
+
* order, enumerable, JSON, spread, `structuredClone` and `util.inspect`
|
|
57
|
+
* give the values.
|
|
58
|
+
*/
|
|
59
|
+
function tracked(runtime, data) {
|
|
60
|
+
const state = {};
|
|
61
|
+
for (const key of Object.keys(data)) {
|
|
62
|
+
const value = data[key];
|
|
63
|
+
Object.defineProperty(state, key, {
|
|
64
|
+
enumerable: true,
|
|
65
|
+
get: () => {
|
|
66
|
+
runtime?.depends(PLAYER, key);
|
|
67
|
+
return value;
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
Object.defineProperty(state, INSPECT, { value: () => data });
|
|
72
|
+
const frozen = Object.freeze(state);
|
|
73
|
+
values.set(frozen, data);
|
|
74
|
+
return frozen;
|
|
75
|
+
}
|
|
76
|
+
/** The fields of `next` that differ from `last` (`stamp` included). */
|
|
77
|
+
function changes(last, lastRaw, next, nextRaw) {
|
|
78
|
+
const a = values.get(last);
|
|
79
|
+
const b = values.get(next);
|
|
80
|
+
const changed = [];
|
|
81
|
+
for (const key of Object.keys(b)) {
|
|
82
|
+
const x = a[key];
|
|
83
|
+
const y = b[key];
|
|
84
|
+
if (x === y)
|
|
85
|
+
continue;
|
|
86
|
+
if (x === null ||
|
|
87
|
+
y === null ||
|
|
88
|
+
typeof x !== "object" ||
|
|
89
|
+
typeof y !== "object" ||
|
|
90
|
+
JSON.stringify(x) !== JSON.stringify(y))
|
|
91
|
+
changed.push(key);
|
|
92
|
+
}
|
|
93
|
+
if (!sameStamp(lastRaw.stamp, nextRaw.stamp))
|
|
94
|
+
changed.push("stamp");
|
|
95
|
+
return changed;
|
|
96
|
+
}
|
|
97
|
+
function view(raw, runtime) {
|
|
44
98
|
const reason = raw.terminalReason;
|
|
45
|
-
const
|
|
99
|
+
const data = Object.freeze({
|
|
46
100
|
sourceId: raw.stamp.generation,
|
|
47
101
|
kind: raw.kind === "producer" ? "stream" : "media",
|
|
48
102
|
phase: raw.phase,
|
|
@@ -78,6 +132,7 @@ function view(raw) {
|
|
|
78
132
|
active: raw.audioOutput?.active ?? null,
|
|
79
133
|
}),
|
|
80
134
|
});
|
|
135
|
+
const state = tracked(runtime, data);
|
|
81
136
|
raws.set(state, raw);
|
|
82
137
|
return state;
|
|
83
138
|
}
|
|
@@ -110,12 +165,18 @@ export function abortError() {
|
|
|
110
165
|
export class PlayerService {
|
|
111
166
|
runtime;
|
|
112
167
|
raw = empty;
|
|
113
|
-
state
|
|
168
|
+
state;
|
|
169
|
+
/**
|
|
170
|
+
* The fields the last accepted state changed (`onChange` listeners are
|
|
171
|
+
* told so a listener that read none of them needs no full pass).
|
|
172
|
+
*/
|
|
173
|
+
change = new Map();
|
|
114
174
|
#watchers = new Set();
|
|
115
175
|
#operation = 0;
|
|
116
176
|
output;
|
|
117
177
|
constructor(runtime) {
|
|
118
178
|
this.runtime = runtime;
|
|
179
|
+
this.state = view(empty, runtime);
|
|
119
180
|
// The SDK's copy of the state (a mirror): a plugin listens through
|
|
120
181
|
// `watch` and the bindings that read `state`.
|
|
121
182
|
runtime.on("source.state", (data) => this.accept(data), "events", true);
|
|
@@ -137,12 +198,16 @@ export class PlayerService {
|
|
|
137
198
|
return;
|
|
138
199
|
if (JSON.stringify(raw) === JSON.stringify(this.raw))
|
|
139
200
|
return;
|
|
201
|
+
const last = this.state;
|
|
202
|
+
const lastRaw = this.raw;
|
|
140
203
|
this.raw = raw;
|
|
141
|
-
this.state = view(raw);
|
|
204
|
+
this.state = view(raw, this.runtime);
|
|
205
|
+
const changed = changes(last, lastRaw, this.state, raw);
|
|
206
|
+
this.change = new Map([[PLAYER, new Set(changed)]]);
|
|
142
207
|
for (const watcher of [...this.#watchers])
|
|
143
208
|
watcher(raw);
|
|
144
|
-
//
|
|
145
|
-
this.runtime.
|
|
209
|
+
// A precise pass: only the bindings that read a changed field.
|
|
210
|
+
this.runtime.changed(PLAYER, changed);
|
|
146
211
|
}
|
|
147
212
|
watch(watcher) {
|
|
148
213
|
this.#watchers.add(watcher);
|
|
@@ -244,6 +309,7 @@ class MediaHandle {
|
|
|
244
309
|
this.#ticket = ticket;
|
|
245
310
|
}
|
|
246
311
|
get current() {
|
|
312
|
+
this.#service.runtime.depends(PLAYER, "stamp");
|
|
247
313
|
return this.#service.raw.stamp.generation === this.#ticket.stamp.generation;
|
|
248
314
|
}
|
|
249
315
|
ready(options = {}) {
|
|
@@ -313,7 +379,9 @@ class ScrubHandle {
|
|
|
313
379
|
#pending = null;
|
|
314
380
|
#watch;
|
|
315
381
|
#queue;
|
|
382
|
+
#service;
|
|
316
383
|
constructor(service) {
|
|
384
|
+
this.#service = service;
|
|
317
385
|
this.#allowed = { ...service.raw.stamp };
|
|
318
386
|
this.#watch = service.watch((raw) => {
|
|
319
387
|
if (!sameStamp(raw.stamp, this.#allowed) &&
|
|
@@ -377,6 +445,9 @@ class ScrubHandle {
|
|
|
377
445
|
this.#active = false;
|
|
378
446
|
this.#queue.stop();
|
|
379
447
|
void this.#watch.dispose();
|
|
448
|
+
// `active` changed, maybe outside plugin code (a new source, a failed
|
|
449
|
+
// seek): bindings that read it see it in a full pass.
|
|
450
|
+
this.#service.runtime.invalidate();
|
|
380
451
|
}
|
|
381
452
|
}
|
|
382
453
|
const superseded = new HostError("superseded", "gesture ended");
|
|
@@ -434,10 +505,11 @@ export function player(scope) {
|
|
|
434
505
|
return {
|
|
435
506
|
get state() {
|
|
436
507
|
scope.runtime.reading("source.state");
|
|
508
|
+
scope.runtime.depends(PLAYER);
|
|
437
509
|
return service().state;
|
|
438
510
|
},
|
|
439
511
|
onChange(listener) {
|
|
440
|
-
const watch = service().watch(() => scope.
|
|
512
|
+
const watch = service().watch(() => scope.runOn(service().change, listener, service().state));
|
|
441
513
|
return scope.add({
|
|
442
514
|
dispose: async () => {
|
|
443
515
|
scope.forget(watch);
|
|
@@ -448,6 +520,7 @@ export function player(scope) {
|
|
|
448
520
|
refresh: () => service().refresh(),
|
|
449
521
|
isCurrent(state) {
|
|
450
522
|
const raw = raws.get(state);
|
|
523
|
+
scope.runtime.depends(PLAYER, "stamp");
|
|
451
524
|
return !!raw && sameStamp(raw.stamp, service().raw.stamp);
|
|
452
525
|
},
|
|
453
526
|
async open(source, options = {}) {
|
package/dist/plugin/runtime.d.ts
CHANGED
|
@@ -23,6 +23,20 @@ export declare const failure: (code: string, message?: string) => PluginError;
|
|
|
23
23
|
export declare const uncertain: (error: unknown) => boolean;
|
|
24
24
|
/** Mark a promise as observed so an unused `ready` never crashes the runner. */
|
|
25
25
|
export declare function quiet<T>(promise: Promise<T>): Promise<T>;
|
|
26
|
+
/**
|
|
27
|
+
* The fields of the SDK's state copies that a binding or listener read, by
|
|
28
|
+
* state (`player`: `plugin.player.state`). An empty set: the state was
|
|
29
|
+
* taken whole (no field read was seen), so any change of it counts.
|
|
30
|
+
*/
|
|
31
|
+
export type Deps = Map<string, Set<string>>;
|
|
32
|
+
/** A change of the SDK's state copies: the fields that changed, by state. */
|
|
33
|
+
export type Change = ReadonlyMap<string, ReadonlySet<string>>;
|
|
34
|
+
/**
|
|
35
|
+
* The longest a binding waits for a full pass while precise passes skip
|
|
36
|
+
* it: state a plugin changes outside code the SDK runs (its own timer,
|
|
37
|
+
* without `update()`) still shows within about this long.
|
|
38
|
+
*/
|
|
39
|
+
export declare const SAFETY_PASS_MS = 1000;
|
|
26
40
|
export declare const same: (a: Json | undefined, b: Json | undefined) => boolean;
|
|
27
41
|
/** One value in flight, only the newest waiting. Each push learns its fate. */
|
|
28
42
|
export declare class Latest<T> {
|
|
@@ -76,6 +90,36 @@ export declare class Runtime {
|
|
|
76
90
|
* on it, so the event counts as listened to while the binding reads it.
|
|
77
91
|
*/
|
|
78
92
|
reading(event: string): void;
|
|
93
|
+
/**
|
|
94
|
+
* A field of the SDK's copy of a state (`key`) is read; without `field`,
|
|
95
|
+
* the state is taken (`plugin.player.state`). What is being evaluated
|
|
96
|
+
* then depends on it: a precise pass after a change of that field runs
|
|
97
|
+
* it again.
|
|
98
|
+
*/
|
|
99
|
+
depends(key: string, field?: string): void;
|
|
100
|
+
/**
|
|
101
|
+
* Evaluate one part of the binding being evaluated (one `view.bind`
|
|
102
|
+
* function), identified by `token`. A precise pass skips it when nothing
|
|
103
|
+
* it read last time changed, and returns `undefined`; what it read then
|
|
104
|
+
* still counts for the binding.
|
|
105
|
+
*/
|
|
106
|
+
part<T>(token: object, read: () => T): {
|
|
107
|
+
value: T;
|
|
108
|
+
} | undefined;
|
|
109
|
+
/**
|
|
110
|
+
* Run plugin code (a state listener) and return the state copy fields it
|
|
111
|
+
* read; they are not counted as a binding's.
|
|
112
|
+
*/
|
|
113
|
+
observe<T>(run: () => T): {
|
|
114
|
+
value: T;
|
|
115
|
+
deps: Deps;
|
|
116
|
+
};
|
|
117
|
+
/**
|
|
118
|
+
* Whether code that read `deps` while `change` was delivered needs a full
|
|
119
|
+
* pass after it: it read a field that changed, or no field of that state
|
|
120
|
+
* at all (so the SDK cannot tell what it depends on).
|
|
121
|
+
*/
|
|
122
|
+
needsFull(deps: Deps, change: Change): boolean;
|
|
79
123
|
/**
|
|
80
124
|
* Evaluate `read` as `label`, added at `where`: the state it reads is
|
|
81
125
|
* reported as read by it. `field`: a command's `enabled` or `checked`,
|
|
@@ -88,8 +132,21 @@ export declare class Runtime {
|
|
|
88
132
|
* line (`main.ts:353`). Captured now, formatted when asked.
|
|
89
133
|
*/
|
|
90
134
|
callSite(): () => string | undefined;
|
|
91
|
-
/**
|
|
135
|
+
/**
|
|
136
|
+
* A full pass: every binding is evaluated. Requested after plugin code
|
|
137
|
+
* the SDK ran, by `plugin.update()`, and on any change other than a state
|
|
138
|
+
* copy's fields. Coalesced: one pass per microtask turn, however many
|
|
139
|
+
* requests.
|
|
140
|
+
*/
|
|
92
141
|
invalidate(): void;
|
|
142
|
+
/**
|
|
143
|
+
* Fields of the SDK's copy of a state (`key`) changed: a precise pass
|
|
144
|
+
* evaluates only the bindings (and view bind functions) that read one of
|
|
145
|
+
* them, unless a full pass is due anyway.
|
|
146
|
+
*/
|
|
147
|
+
changed(key: string, fields: Iterable<string>): void;
|
|
148
|
+
/** The plugin was disposed: cancel the pending safety pass, arm no more. */
|
|
149
|
+
stop(): void;
|
|
93
150
|
/**
|
|
94
151
|
* Host events with one `events.subscribe` per name, however many listeners.
|
|
95
152
|
* Without the `events` capability the listener simply never fires.
|
package/dist/plugin/runtime.js
CHANGED
|
@@ -53,6 +53,39 @@ export function quiet(promise) {
|
|
|
53
53
|
promise.catch(() => { });
|
|
54
54
|
return promise;
|
|
55
55
|
}
|
|
56
|
+
/** Whether something that read `deps` may see a different result after `change`. */
|
|
57
|
+
function affected(deps, change) {
|
|
58
|
+
for (const [key, fields] of change) {
|
|
59
|
+
const read = deps.get(key);
|
|
60
|
+
if (!read)
|
|
61
|
+
continue;
|
|
62
|
+
if (read.size === 0)
|
|
63
|
+
return true;
|
|
64
|
+
for (const field of fields)
|
|
65
|
+
if (read.has(field))
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
/** Add `from` to `into` (an empty set, the whole state, wins). */
|
|
71
|
+
function merge(into, from) {
|
|
72
|
+
for (const [key, fields] of from) {
|
|
73
|
+
const set = into.get(key);
|
|
74
|
+
if (!set)
|
|
75
|
+
into.set(key, new Set(fields));
|
|
76
|
+
else if (set.size && fields.size)
|
|
77
|
+
for (const field of fields)
|
|
78
|
+
set.add(field);
|
|
79
|
+
else
|
|
80
|
+
set.clear();
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* The longest a binding waits for a full pass while precise passes skip
|
|
85
|
+
* it: state a plugin changes outside code the SDK runs (its own timer,
|
|
86
|
+
* without `update()`) still shows within about this long.
|
|
87
|
+
*/
|
|
88
|
+
export const SAFETY_PASS_MS = 1000;
|
|
56
89
|
export const same = (a, b) => JSON.stringify(a) === JSON.stringify(b);
|
|
57
90
|
/** One value in flight, only the newest waiting. Each push learns its fate. */
|
|
58
91
|
export class Latest {
|
|
@@ -124,6 +157,22 @@ export class Runtime {
|
|
|
124
157
|
/** Who reads now (a command's field and where it was added). */
|
|
125
158
|
#reader;
|
|
126
159
|
#scheduled = false;
|
|
160
|
+
/** The next pass evaluates every binding (not only a state copy changed). */
|
|
161
|
+
#full = false;
|
|
162
|
+
/** State copy changes since the last pass (a precise pass, unless `#full`). */
|
|
163
|
+
#pending = new Map();
|
|
164
|
+
/** The change the running precise pass is for; `undefined` in a full pass. */
|
|
165
|
+
#pass;
|
|
166
|
+
/** The state copy fields read by what is being evaluated now. */
|
|
167
|
+
#deps;
|
|
168
|
+
/** Each part's reads and fields last time (`part`). */
|
|
169
|
+
#parts = new WeakMap();
|
|
170
|
+
/** A precise pass skipped something: a full pass follows (`SAFETY_PASS_MS`). */
|
|
171
|
+
#safety;
|
|
172
|
+
/** The running pass skipped a binding or a part. */
|
|
173
|
+
#skipped = false;
|
|
174
|
+
/** The plugin was disposed: no safety pass is armed any more. */
|
|
175
|
+
#stopped = false;
|
|
127
176
|
#events = new Map();
|
|
128
177
|
views = new Map();
|
|
129
178
|
commands = new Map();
|
|
@@ -196,11 +245,12 @@ export class Runtime {
|
|
|
196
245
|
}
|
|
197
246
|
bind(evaluate) {
|
|
198
247
|
const held = new Map();
|
|
199
|
-
|
|
248
|
+
const binding = { held, deps: new Map() };
|
|
249
|
+
this.#bindings.set(evaluate, binding);
|
|
200
250
|
this.invalidate();
|
|
201
251
|
return {
|
|
202
252
|
dispose: async () => {
|
|
203
|
-
if (this.#bindings.get(evaluate) !==
|
|
253
|
+
if (this.#bindings.get(evaluate) !== binding)
|
|
204
254
|
return;
|
|
205
255
|
this.#bindings.delete(evaluate);
|
|
206
256
|
for (const hold of held.values())
|
|
@@ -231,6 +281,92 @@ export class Runtime {
|
|
|
231
281
|
return `${reader.label} is a function that reads ${read}${at}${hint}`;
|
|
232
282
|
});
|
|
233
283
|
}
|
|
284
|
+
/**
|
|
285
|
+
* A field of the SDK's copy of a state (`key`) is read; without `field`,
|
|
286
|
+
* the state is taken (`plugin.player.state`). What is being evaluated
|
|
287
|
+
* then depends on it: a precise pass after a change of that field runs
|
|
288
|
+
* it again.
|
|
289
|
+
*/
|
|
290
|
+
depends(key, field) {
|
|
291
|
+
const deps = this.#deps;
|
|
292
|
+
if (!deps)
|
|
293
|
+
return;
|
|
294
|
+
let fields = deps.get(key);
|
|
295
|
+
if (!fields)
|
|
296
|
+
deps.set(key, (fields = new Set()));
|
|
297
|
+
if (field !== undefined)
|
|
298
|
+
fields.add(field);
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* Evaluate one part of the binding being evaluated (one `view.bind`
|
|
302
|
+
* function), identified by `token`. A precise pass skips it when nothing
|
|
303
|
+
* it read last time changed, and returns `undefined`; what it read then
|
|
304
|
+
* still counts for the binding.
|
|
305
|
+
*/
|
|
306
|
+
part(token, read) {
|
|
307
|
+
const last = this.#parts.get(token);
|
|
308
|
+
const outerReads = this.#reads;
|
|
309
|
+
const outerDeps = this.#deps;
|
|
310
|
+
if (last && this.#pass && !affected(last.deps, this.#pass)) {
|
|
311
|
+
this.#skipped = true;
|
|
312
|
+
if (outerReads)
|
|
313
|
+
for (const [event, why] of last.reads)
|
|
314
|
+
if (!outerReads.has(event))
|
|
315
|
+
outerReads.set(event, why);
|
|
316
|
+
if (outerDeps)
|
|
317
|
+
merge(outerDeps, last.deps);
|
|
318
|
+
return undefined;
|
|
319
|
+
}
|
|
320
|
+
const reads = new Map();
|
|
321
|
+
const deps = new Map();
|
|
322
|
+
this.#reads = outerReads && reads;
|
|
323
|
+
this.#deps = deps;
|
|
324
|
+
try {
|
|
325
|
+
return { value: read() };
|
|
326
|
+
}
|
|
327
|
+
finally {
|
|
328
|
+
this.#reads = outerReads;
|
|
329
|
+
this.#deps = outerDeps;
|
|
330
|
+
this.#parts.set(token, { reads, deps });
|
|
331
|
+
if (outerReads)
|
|
332
|
+
for (const [event, why] of reads)
|
|
333
|
+
if (!outerReads.has(event))
|
|
334
|
+
outerReads.set(event, why);
|
|
335
|
+
if (outerDeps)
|
|
336
|
+
merge(outerDeps, deps);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* Run plugin code (a state listener) and return the state copy fields it
|
|
341
|
+
* read; they are not counted as a binding's.
|
|
342
|
+
*/
|
|
343
|
+
observe(run) {
|
|
344
|
+
const outerReads = this.#reads;
|
|
345
|
+
const outerDeps = this.#deps;
|
|
346
|
+
const deps = new Map();
|
|
347
|
+
this.#reads = undefined;
|
|
348
|
+
this.#deps = deps;
|
|
349
|
+
try {
|
|
350
|
+
return { value: run(), deps };
|
|
351
|
+
}
|
|
352
|
+
finally {
|
|
353
|
+
this.#reads = outerReads;
|
|
354
|
+
this.#deps = outerDeps;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
/**
|
|
358
|
+
* Whether code that read `deps` while `change` was delivered needs a full
|
|
359
|
+
* pass after it: it read a field that changed, or no field of that state
|
|
360
|
+
* at all (so the SDK cannot tell what it depends on).
|
|
361
|
+
*/
|
|
362
|
+
needsFull(deps, change) {
|
|
363
|
+
for (const key of change.keys()) {
|
|
364
|
+
const read = deps.get(key);
|
|
365
|
+
if (!read || read.size === 0)
|
|
366
|
+
return true;
|
|
367
|
+
}
|
|
368
|
+
return affected(deps, change);
|
|
369
|
+
}
|
|
234
370
|
/**
|
|
235
371
|
* Evaluate `read` as `label`, added at `where`: the state it reads is
|
|
236
372
|
* reported as read by it. `field`: a command's `enabled` or `checked`,
|
|
@@ -286,18 +422,24 @@ export class Runtime {
|
|
|
286
422
|
return undefined;
|
|
287
423
|
}
|
|
288
424
|
/** Evaluate one binding, holding the events whose state it read. */
|
|
289
|
-
#evaluate(evaluate,
|
|
425
|
+
#evaluate(evaluate, binding) {
|
|
426
|
+
const held = binding.held;
|
|
290
427
|
const reads = new Map();
|
|
428
|
+
const deps = new Map();
|
|
291
429
|
const outer = this.#reads;
|
|
430
|
+
const outerDeps = this.#deps;
|
|
292
431
|
const reader = this.#reader;
|
|
293
432
|
this.#reads = reads;
|
|
433
|
+
this.#deps = deps;
|
|
294
434
|
this.#reader = undefined;
|
|
295
435
|
try {
|
|
296
436
|
evaluate();
|
|
297
437
|
}
|
|
298
438
|
finally {
|
|
299
439
|
this.#reads = outer;
|
|
440
|
+
this.#deps = outerDeps;
|
|
300
441
|
this.#reader = reader;
|
|
442
|
+
binding.deps = deps;
|
|
301
443
|
for (const [event, hold] of held)
|
|
302
444
|
if (!reads.has(event)) {
|
|
303
445
|
held.delete(event);
|
|
@@ -308,24 +450,82 @@ export class Runtime {
|
|
|
308
450
|
held.set(event, this.hold(event, why));
|
|
309
451
|
}
|
|
310
452
|
}
|
|
311
|
-
/**
|
|
453
|
+
/**
|
|
454
|
+
* A full pass: every binding is evaluated. Requested after plugin code
|
|
455
|
+
* the SDK ran, by `plugin.update()`, and on any change other than a state
|
|
456
|
+
* copy's fields. Coalesced: one pass per microtask turn, however many
|
|
457
|
+
* requests.
|
|
458
|
+
*/
|
|
312
459
|
invalidate() {
|
|
460
|
+
this.#full = true;
|
|
461
|
+
this.#schedule();
|
|
462
|
+
}
|
|
463
|
+
/**
|
|
464
|
+
* Fields of the SDK's copy of a state (`key`) changed: a precise pass
|
|
465
|
+
* evaluates only the bindings (and view bind functions) that read one of
|
|
466
|
+
* them, unless a full pass is due anyway.
|
|
467
|
+
*/
|
|
468
|
+
changed(key, fields) {
|
|
469
|
+
let set = this.#pending.get(key);
|
|
470
|
+
if (!set)
|
|
471
|
+
this.#pending.set(key, (set = new Set()));
|
|
472
|
+
for (const field of fields)
|
|
473
|
+
set.add(field);
|
|
474
|
+
this.#schedule();
|
|
475
|
+
}
|
|
476
|
+
#schedule() {
|
|
313
477
|
if (this.#scheduled)
|
|
314
478
|
return;
|
|
315
479
|
this.#scheduled = true;
|
|
316
|
-
queueMicrotask(() =>
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
480
|
+
queueMicrotask(() => this.#run());
|
|
481
|
+
}
|
|
482
|
+
/**
|
|
483
|
+
* One pass. A precise pass that skips anything arms a full pass
|
|
484
|
+
* `SAFETY_PASS_MS` later (unless a full pass comes first), for plugin
|
|
485
|
+
* state changed outside code the SDK ran.
|
|
486
|
+
*/
|
|
487
|
+
#run() {
|
|
488
|
+
this.#scheduled = false;
|
|
489
|
+
const full = this.#full;
|
|
490
|
+
const change = this.#pending;
|
|
491
|
+
this.#full = false;
|
|
492
|
+
this.#pending = new Map();
|
|
493
|
+
if (full) {
|
|
494
|
+
clearTimeout(this.#safety);
|
|
495
|
+
this.#safety = undefined;
|
|
496
|
+
}
|
|
497
|
+
this.#skipped = false;
|
|
498
|
+
for (const [evaluate, binding] of [...this.#bindings]) {
|
|
499
|
+
if (this.#bindings.get(evaluate) !== binding)
|
|
500
|
+
continue;
|
|
501
|
+
if (!full && !affected(binding.deps, change)) {
|
|
502
|
+
this.#skipped = true;
|
|
503
|
+
continue;
|
|
327
504
|
}
|
|
328
|
-
|
|
505
|
+
this.#pass = full ? undefined : change;
|
|
506
|
+
try {
|
|
507
|
+
this.#evaluate(evaluate, binding);
|
|
508
|
+
}
|
|
509
|
+
catch (error) {
|
|
510
|
+
console.error(`[${this.manifest.id}] binding failed:`, error);
|
|
511
|
+
}
|
|
512
|
+
finally {
|
|
513
|
+
this.#pass = undefined;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
if (this.#skipped && !this.#safety && !this.#stopped) {
|
|
517
|
+
this.#safety = setTimeout(() => {
|
|
518
|
+
this.#safety = undefined;
|
|
519
|
+
this.invalidate();
|
|
520
|
+
}, SAFETY_PASS_MS);
|
|
521
|
+
this.#safety.unref?.();
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
/** The plugin was disposed: cancel the pending safety pass, arm no more. */
|
|
525
|
+
stop() {
|
|
526
|
+
this.#stopped = true;
|
|
527
|
+
clearTimeout(this.#safety);
|
|
528
|
+
this.#safety = undefined;
|
|
329
529
|
}
|
|
330
530
|
/**
|
|
331
531
|
* Host events with one `events.subscribe` per name, however many listeners.
|
package/dist/plugin/scope.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Runtime } from "./runtime.ts";
|
|
1
|
+
import type { Change, Runtime } from "./runtime.ts";
|
|
2
2
|
import type { Disposable, Plugin, PluginError, Scope } from "./types.ts";
|
|
3
3
|
export declare class ScopeImpl implements Plugin {
|
|
4
4
|
#private;
|
|
@@ -54,6 +54,14 @@ export declare class ScopeImpl implements Plugin {
|
|
|
54
54
|
report(error: unknown): void;
|
|
55
55
|
/** Invoke plugin code on its behalf: report failures, then refresh bindings. */
|
|
56
56
|
run(handler: (...args: never[]) => unknown, ...args: unknown[]): void;
|
|
57
|
+
/**
|
|
58
|
+
* Run a listener of a state change (`player.onChange`) like `run`, except
|
|
59
|
+
* that the full pass after it is left out when it returned synchronously
|
|
60
|
+
* and read only fields of the state that `change` left alone: its result
|
|
61
|
+
* then depends on nothing new. The precise pass for the change runs
|
|
62
|
+
* anyway, and a full pass follows within `SAFETY_PASS_MS`.
|
|
63
|
+
*/
|
|
64
|
+
runOn(change: Change, handler: (...args: never[]) => unknown, ...args: unknown[]): void;
|
|
57
65
|
update(): void;
|
|
58
66
|
dispose(): Promise<void>;
|
|
59
67
|
}
|
package/dist/plugin/scope.js
CHANGED
|
@@ -145,6 +145,30 @@ export class ScopeImpl {
|
|
|
145
145
|
}
|
|
146
146
|
this.update();
|
|
147
147
|
}
|
|
148
|
+
/**
|
|
149
|
+
* Run a listener of a state change (`player.onChange`) like `run`, except
|
|
150
|
+
* that the full pass after it is left out when it returned synchronously
|
|
151
|
+
* and read only fields of the state that `change` left alone: its result
|
|
152
|
+
* then depends on nothing new. The precise pass for the change runs
|
|
153
|
+
* anyway, and a full pass follows within `SAFETY_PASS_MS`.
|
|
154
|
+
*/
|
|
155
|
+
runOn(change, handler, ...args) {
|
|
156
|
+
if (this.disposed)
|
|
157
|
+
return;
|
|
158
|
+
let full = true;
|
|
159
|
+
try {
|
|
160
|
+
const { value, deps } = this.runtime.observe(() => handler(...args));
|
|
161
|
+
if (value instanceof Promise)
|
|
162
|
+
value.catch((error) => this.report(error)).finally(() => this.update());
|
|
163
|
+
else
|
|
164
|
+
full = this.runtime.needsFull(deps, change);
|
|
165
|
+
}
|
|
166
|
+
catch (error) {
|
|
167
|
+
this.report(error);
|
|
168
|
+
}
|
|
169
|
+
if (full)
|
|
170
|
+
this.update();
|
|
171
|
+
}
|
|
148
172
|
update() {
|
|
149
173
|
this.runtime.invalidate();
|
|
150
174
|
}
|
package/dist/plugin/testing.d.ts
CHANGED
|
@@ -2621,6 +2621,13 @@ export declare class TestHost implements Transport {
|
|
|
2621
2621
|
* for: on a loaded machine it can finish after `settle()` returns. Poll
|
|
2622
2622
|
* the result you assert on instead (second example).
|
|
2623
2623
|
*
|
|
2624
|
+
* `settle()` does not wait for the SDK's once-a-second full update pass
|
|
2625
|
+
* (SDK 1.5.45). After `setSource` (or another player state change) only
|
|
2626
|
+
* the bindings that read a changed field of `plugin.player.state` run
|
|
2627
|
+
* again; state the plugin changed outside a handler without
|
|
2628
|
+
* `plugin.update()` shows only in the next full pass. Have the plugin
|
|
2629
|
+
* call `plugin.update()` after such a change, or wait `settle(1000)`.
|
|
2630
|
+
*
|
|
2624
2631
|
* @param ms - Extra time to wait, for debounces and timers (default 0).
|
|
2625
2632
|
*
|
|
2626
2633
|
* @example Wait past a debounced save
|
package/dist/plugin/testing.js
CHANGED
|
@@ -3213,6 +3213,13 @@ export class TestHost {
|
|
|
3213
3213
|
* for: on a loaded machine it can finish after `settle()` returns. Poll
|
|
3214
3214
|
* the result you assert on instead (second example).
|
|
3215
3215
|
*
|
|
3216
|
+
* `settle()` does not wait for the SDK's once-a-second full update pass
|
|
3217
|
+
* (SDK 1.5.45). After `setSource` (or another player state change) only
|
|
3218
|
+
* the bindings that read a changed field of `plugin.player.state` run
|
|
3219
|
+
* again; state the plugin changed outside a handler without
|
|
3220
|
+
* `plugin.update()` shows only in the next full pass. Have the plugin
|
|
3221
|
+
* call `plugin.update()` after such a change, or wait `settle(1000)`.
|
|
3222
|
+
*
|
|
3216
3223
|
* @param ms - Extra time to wait, for debounces and timers (default 0).
|
|
3217
3224
|
*
|
|
3218
3225
|
* @example Wait past a debounced save
|
package/dist/plugin/types.d.ts
CHANGED
|
@@ -565,14 +565,16 @@ export interface CommandState {
|
|
|
565
565
|
reason: TextValue | null;
|
|
566
566
|
}
|
|
567
567
|
/**
|
|
568
|
-
* A value, or a function the SDK re-evaluates on every update pass
|
|
569
|
-
*
|
|
568
|
+
* A value, or a function the SDK re-evaluates on every full update pass,
|
|
569
|
+
* and after a player state change only if it read a field of
|
|
570
|
+
* `plugin.player.state` that changed (see {@link Scope.update}); only
|
|
571
|
+
* changes are sent to the player.
|
|
570
572
|
*
|
|
571
573
|
* @example Mix a computed state with a fixed one
|
|
572
574
|
* The Loop item carries a check mark while a loop is set and is always
|
|
573
|
-
* enabled. A function is evaluated again in every update pass (after
|
|
574
|
-
* handler and player state change
|
|
575
|
-
* value never changes.
|
|
575
|
+
* enabled. A function is evaluated again in every full update pass (after
|
|
576
|
+
* each handler), and after a player state change only if it read a changed
|
|
577
|
+
* field; only changes are sent, and a plain value never changes.
|
|
576
578
|
* ```ts
|
|
577
579
|
* plugin.commands.add({
|
|
578
580
|
* id: "loop",
|
|
@@ -1060,8 +1062,9 @@ export interface CommandOptions {
|
|
|
1060
1062
|
* @remarks
|
|
1061
1063
|
* The handle of one menu item and its keys. Its state (`enabled`, `checked`,
|
|
1062
1064
|
* `busy`, `reason`) is bound: functions given to {@link Commands.add} or
|
|
1063
|
-
* {@link Command.set} are evaluated again in every update pass
|
|
1064
|
-
*
|
|
1065
|
+
* {@link Command.set} are evaluated again in every full update pass (after
|
|
1066
|
+
* a player state change only if they read a changed field of
|
|
1067
|
+
* `plugin.player.state`) and only changes reach the player. Its title, path, bindings and button cannot
|
|
1065
1068
|
* change in place; dispose the command (or a child {@link Scope}) and add it
|
|
1066
1069
|
* again. Disposing it, its scope or stopping the plugin removes the menu
|
|
1067
1070
|
* item, its keys and its bar button.
|
|
@@ -1081,7 +1084,7 @@ export interface CommandOptions {
|
|
|
1081
1084
|
* path: ["@video", "Export…"],
|
|
1082
1085
|
* run: runExport,
|
|
1083
1086
|
* });
|
|
1084
|
-
* // A function: re-evaluated in every
|
|
1087
|
+
* // A function: re-evaluated in every full pass and when `phase` changes.
|
|
1085
1088
|
* exportCommand.set({ enabled: () => plugin.player.state.phase === "ready" });
|
|
1086
1089
|
* ```
|
|
1087
1090
|
*
|
|
@@ -1706,8 +1709,9 @@ export interface Ui {
|
|
|
1706
1709
|
* callback.
|
|
1707
1710
|
*
|
|
1708
1711
|
* `bind` is the usual way: the SDK re-evaluates bound functions in every
|
|
1709
|
-
* update pass (after each handler it calls,
|
|
1710
|
-
*
|
|
1712
|
+
* full update pass (after each handler it calls, on {@link Scope.update}),
|
|
1713
|
+
* and after a player state change those that read a changed field of
|
|
1714
|
+
* `plugin.player.state`, and sends only what changed, at most one write in
|
|
1711
1715
|
* flight per view. `set` sends now, even an unchanged value.
|
|
1712
1716
|
*
|
|
1713
1717
|
* Writes that fail (a name the Slint file does not declare, a value of the
|
|
@@ -1843,7 +1847,11 @@ export interface View {
|
|
|
1843
1847
|
*
|
|
1844
1848
|
* @remarks
|
|
1845
1849
|
* The SDK calls `props` in every update pass (and right after `bind`) and
|
|
1846
|
-
* sends the values that differ from the ones it sent last.
|
|
1850
|
+
* sends the values that differ from the ones it sent last. After a change
|
|
1851
|
+
* of the player state alone it calls `props` only if it read a field of
|
|
1852
|
+
* `plugin.player.state` that changed (SDK 1.5.45); a full pass follows
|
|
1853
|
+
* after each handler, on {@link Scope.update} and at least once a second
|
|
1854
|
+
* while the player state keeps changing. Because an
|
|
1847
1855
|
* unchanged value is not sent again, text the user typed into an `in-out`
|
|
1848
1856
|
* property stays until the plugin's value for it changes.
|
|
1849
1857
|
*
|
|
@@ -1864,8 +1872,9 @@ export interface View {
|
|
|
1864
1872
|
* @example Show what is playing and whether it can seek
|
|
1865
1873
|
* The status window always shows the current file and enables its seek
|
|
1866
1874
|
* controls only for seekable sources. The SDK calls the function right
|
|
1867
|
-
* away
|
|
1868
|
-
*
|
|
1875
|
+
* away, again in every full update pass (after each handler) and after
|
|
1876
|
+
* each change of `source` or `can`, and sends only the values that
|
|
1877
|
+
* changed.
|
|
1869
1878
|
* ```ts
|
|
1870
1879
|
* const status = plugin.windows.create("ui/status.slint");
|
|
1871
1880
|
* status.bind(() => ({
|
|
@@ -5697,8 +5706,8 @@ export interface Player {
|
|
|
5697
5706
|
* @example Drive the player's Next and Previous from a playlist
|
|
5698
5707
|
* The player's Next and Previous items and keys play the plugin's list.
|
|
5699
5708
|
* The player calls `next` and `previous` when the user asks; `canNext`
|
|
5700
|
-
* and `canPrevious` are evaluated again after every handler the SDK runs
|
|
5701
|
-
* and while they return false the player disables its items ("No next
|
|
5709
|
+
* and `canPrevious` are evaluated again after every handler the SDK runs
|
|
5710
|
+
* (and after a player state change if they read a changed field), and while they return false the player disables its items ("No next
|
|
5702
5711
|
* item").
|
|
5703
5712
|
* ```ts
|
|
5704
5713
|
* declare const items: string[];
|
package/dist/plugin/ui.d.ts
CHANGED
|
@@ -38,7 +38,11 @@ export declare abstract class ViewBase implements View {
|
|
|
38
38
|
on<A extends Json[]>(callback: string, handler: (...args: A) => unknown): Disposable;
|
|
39
39
|
set(props: Props): void;
|
|
40
40
|
bind(props: () => Props): Disposable;
|
|
41
|
-
/**
|
|
41
|
+
/**
|
|
42
|
+
* Current desired properties (bound values win over earlier `set`). A
|
|
43
|
+
* precise pass (`Runtime.part`) skips a bind function that read none of
|
|
44
|
+
* the changed state: its last values stay.
|
|
45
|
+
*/
|
|
42
46
|
protected snapshot(): Record<string, PropValue>;
|
|
43
47
|
protected evaluate(): void;
|
|
44
48
|
/** The host already shows these values (e.g. sent as initial properties). */
|
package/dist/plugin/ui.js
CHANGED
|
@@ -247,18 +247,32 @@ export class ViewBase {
|
|
|
247
247
|
this.flush();
|
|
248
248
|
}
|
|
249
249
|
bind(props) {
|
|
250
|
-
this
|
|
250
|
+
// `token` names this binding of this view in the runtime's read
|
|
251
|
+
// tracking: the same function bound to two views is two parts.
|
|
252
|
+
this.#binders.set(props, {
|
|
253
|
+
site: this.scope.runtime.callSite(),
|
|
254
|
+
token: {},
|
|
255
|
+
});
|
|
251
256
|
this.scope.update();
|
|
252
257
|
return this.scope.onDispose(() => {
|
|
253
258
|
this.#binders.delete(props);
|
|
254
259
|
});
|
|
255
260
|
}
|
|
256
|
-
/**
|
|
261
|
+
/**
|
|
262
|
+
* Current desired properties (bound values win over earlier `set`). A
|
|
263
|
+
* precise pass (`Runtime.part`) skips a bind function that read none of
|
|
264
|
+
* the changed state: its last values stay.
|
|
265
|
+
*/
|
|
257
266
|
snapshot() {
|
|
258
|
-
|
|
267
|
+
const runtime = this.scope.runtime;
|
|
268
|
+
for (const [binder, { site, token }] of this.#binders) {
|
|
259
269
|
try {
|
|
260
|
-
|
|
261
|
-
|
|
270
|
+
// Encoded inside the part: what encoding reads counts as the
|
|
271
|
+
// function's (a state object it returned).
|
|
272
|
+
const result = runtime.part(token, () => encode(runtime.reader("a view's bind", site, binder)));
|
|
273
|
+
if (!result)
|
|
274
|
+
continue;
|
|
275
|
+
for (const [key, value] of Object.entries(result.value))
|
|
262
276
|
this.#values.set(key, value);
|
|
263
277
|
}
|
|
264
278
|
catch (error) {
|
package/llms-full.txt
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# @vestra-player/sdk 1.5.
|
|
1
|
+
# @vestra-player/sdk 1.5.45: 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
|
|
|
@@ -1903,6 +1903,8 @@ host.handle("processing.replace", () => {
|
|
|
1903
1903
|
|
|
1904
1904
|
`await host.settle()` after every action that makes the plugin talk to the player; `host.settle(400)` also lets debounces and timers due by then fire. Then assert on what the host recorded: `host.window(id).props` (kebab-case names), `host.command(id).state`, `host.osd`, `host.source`.
|
|
1905
1905
|
|
|
1906
|
+
`settle` does not wait for the once-a-second full update pass (SDK 1.5.45): after `host.setSource(...)` only the bindings that read a changed field of `plugin.player.state` run again. A value your plugin changed outside a handler (a timer, a promise continuation, a child process) without `plugin.update()` shows in the next full pass, at most about a second later; call `plugin.update()` in the plugin after such a change, or wait `await host.settle(1000)` in the test.
|
|
1907
|
+
|
|
1906
1908
|
`settle` waits only for calls to the player. Work the plugin does on its own (reading or parsing a file, a child process, a network request) can finish later on a loaded machine; poll the value you assert on instead:
|
|
1907
1909
|
|
|
1908
1910
|
```ts nocheck
|
|
@@ -3866,14 +3868,18 @@ There is no cleanup function to return from `activate`. Because every resource i
|
|
|
3866
3868
|
A plugin's state is plain variables. The SDK keeps the player in step with it through **bindings** and the **update pass**:
|
|
3867
3869
|
|
|
3868
3870
|
- A binding is a function the SDK calls to get a current value: view.bind(() => props) for a window, panel or settings page, and the function form of a command's `enabled`, `checked`, `busy` and `reason` (Bindable).
|
|
3869
|
-
- In an update pass the SDK calls
|
|
3871
|
+
- In an update pass the SDK calls the bindings of the plugin, compares each result with the value it last sent, and sends only what changed.
|
|
3870
3872
|
|
|
3871
|
-
|
|
3873
|
+
A **full pass** calls every binding. It runs:
|
|
3872
3874
|
|
|
3873
3875
|
- after every handler the SDK calls returns (a command's `run`, a view callback, an `onChange`, `onStatus`, `onEnd` or `onStop` listener), and again when the promise it returned settles;
|
|
3874
|
-
- after the
|
|
3876
|
+
- after the subtitle, window, panel position, timeline, appearance, filter or export state changes;
|
|
3875
3877
|
- when the plugin calls plugin.update().
|
|
3876
3878
|
|
|
3879
|
+
A **precise pass** follows a change of the player state (SDK 1.5.45). While media plays the player sends its state about ten times a second, mostly with only `position` changed. The SDK notes which fields of `plugin.player.state` each binding read the last time it ran (each `view.bind` function on its own; a command's four functions together) and calls only the bindings that read a field that changed. A panel whose list reads nothing of the player state is not rebuilt on every position tick; a label that shows the position is. `player.isCurrent` and `Media.current` count as reading the source. A `player.onChange` listener is followed by a full pass, as above, unless it returned synchronously and read only fields that did not change.
|
|
3880
|
+
|
|
3881
|
+
A precise pass cannot see state the plugin changed by itself. So while precise passes leave bindings out, a full pass follows at least once a second: a variable your own timer changed without `plugin.update()` appears within about a second (before SDK 1.5.45 it usually appeared at the next position tick). Call `plugin.update()` after such a change to show it at once.
|
|
3882
|
+
|
|
3877
3883
|
Several requests within one microtask are merged into one pass, and every target has at most one write in flight: values that change meanwhile are merged into the next write. A binding that changes 60 times a second therefore costs one write per round trip, never a growing queue.
|
|
3878
3884
|
|
|
3879
3885
|
One click, end to end (a bookmark window; the video is at 42 s):
|
|
@@ -3893,7 +3899,7 @@ sequenceDiagram
|
|
|
3893
3899
|
S->>U: the label changes, Jump to bookmark is enabled
|
|
3894
3900
|
```
|
|
3895
3901
|
|
|
3896
|
-
The rule that follows: **state you change outside a handler, or after an `await` inside one, appears only at the next pass.** Changing a variable from your own `setInterval`, a socket callback or the middle of a long task shows nothing until something triggers a
|
|
3902
|
+
The rule that follows: **state you change outside a handler, or after an `await` inside one, appears only at the next full pass.** Changing a variable from your own `setInterval`, a socket callback or the middle of a long task shows nothing until something triggers one (at most about a second while media plays); call `plugin.update()` after such a change. Keep a slider responsive while dragging shows the pattern; How Slint and TypeScript talk explains `set` versus `bind` for views.
|
|
3897
3903
|
|
|
3898
3904
|
#### Stopping
|
|
3899
3905
|
|
|
@@ -4009,7 +4015,7 @@ const toggle = plugin.commands.add({
|
|
|
4009
4015
|
|
|
4010
4016
|
- **The id is the command's identity.** It is unique within the plugin; the player knows the command as `<manifest id>/<id>`. The user's own key bindings are stored under that pair, so changing a title or moving the command to another menu keeps them.
|
|
4011
4017
|
- **`path` puts it in the main menu.** Leave it out for a command that only has bindings or lives in a panel's context menu (Panel.showMenu); it then needs a `title`.
|
|
4012
|
-
- **State is declared, not pushed.** `enabled`, `checked`, `busy` and `reason` are values or functions (Bindable); `enabled` and `checked` may also be conditions from `plugin.when` (below). Functions are evaluated in every update pass and sent only when they change, so the menu's check mark follows the player without a listener you write. Command.set replaces them later.
|
|
4018
|
+
- **State is declared, not pushed.** `enabled`, `checked`, `busy` and `reason` are values or functions (Bindable); `enabled` and `checked` may also be conditions from `plugin.when` (below). Functions are evaluated in every full update pass, and after a player state change only if they read a field that changed; they are sent only when they change, so the menu's check mark follows the player without a listener you write. Command.set replaces them later.
|
|
4013
4019
|
- **The player calls `run` only when it can run.** While a command is disabled or busy, neither its menu item nor its binding calls `run`; `reason` tells the user why (it appears in the menu and, for a key press, on the video).
|
|
4014
4020
|
- **Titles and paths are fixed.** They cannot change in place. A menu that follows changing content (tracks, presets, a radio group) lives in a child scope that is disposed and rebuilt when the content changes; `order` keeps its place (Handles and scopes).
|
|
4015
4021
|
- Disposing the command removes it from the menu and the binding table; the same id can be added again at once. The number of commands per plugin and per player is limited (Reference: limits); the limit stops runaway menus, so cap menus built from media content (tracks, chapters) yourself.
|
|
@@ -4266,7 +4272,7 @@ player.openStream (capability `producer.av`) makes a source whose picture and so
|
|
|
4266
4272
|
|
|
4267
4273
|
#### Next and previous
|
|
4268
4274
|
|
|
4269
|
-
The player's **Previous** and **Next** (menu, keys, the bottom bar) are not tied to a playlist. One plugin at a time can **provide the sequence** with provideSequence, giving `next()`, `previous()` and two flags, `canNext` and `canPrevious`, which are bindings re-evaluated in every update pass.
|
|
4275
|
+
The player's **Previous** and **Next** (menu, keys, the bottom bar) are not tied to a playlist. One plugin at a time can **provide the sequence** with provideSequence, giving `next()`, `previous()` and two flags, `canNext` and `canPrevious`, which are bindings re-evaluated in every full update pass, and after a player state change only if they read a changed field.
|
|
4270
4276
|
|
|
4271
4277
|
- Every plugin sees the flags as `player.state.can.next` and `can.previous`, and player.next() asks the provider to move; the provider's `next` runs in its own scope.
|
|
4272
4278
|
- A second provider fails with `conflict` and does not fail activation; it is tried again when its flags change, so it takes over once the first provider goes away.
|
|
@@ -4627,12 +4633,12 @@ export component ListPanel inherits Rectangle {
|
|
|
4627
4633
|
|
|
4628
4634
|
| | `set({ x })` | `bind(() => ({ x }))` |
|
|
4629
4635
|
| ---------------- | --------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------- |
|
|
4630
|
-
| When it is sent | Immediately (in the next write). | In every update pass, if `x` differs from the last value sent.
|
|
4636
|
+
| When it is sent | Immediately (in the next write). | In every full update pass, and after a player state change if the function read a changed field; when `x` differs from the last value sent. |
|
|
4631
4637
|
| Same value again | Sent again. Use this to pull a control back to the model value after the user moved it. | Not sent. A field the user edited stays as the user left it until your value changes. |
|
|
4632
4638
|
| Write fails | Retried with back-off (0.25 s doubling up to 30 s). | Not repeated until the value changes. |
|
|
4633
4639
|
| Typical use | One-off values: a document, a reset. | Everything derived from plugin state: labels, enabled flags, lists, status lines. |
|
|
4634
4640
|
|
|
4635
|
-
Update passes run after every handler the SDK calls, after player and other state changes, and when you call plugin.update(); several requests are merged into one pass, and each view has at most one write in flight (The update pass has the details).
|
|
4641
|
+
Update passes run after every handler the SDK calls, after player and other state changes, and when you call plugin.update(); several requests are merged into one pass, and each view has at most one write in flight. After a player state change only the bind functions that read a changed field of `plugin.player.state` run again (The update pass has the details).
|
|
4636
4642
|
|
|
4637
4643
|
The rule to remember: **state you change after an `await` inside a handler appears only when the handler finishes**, unless something else triggers a pass. Call `plugin.update()` right after changing it (see Keep a slider responsive while dragging).
|
|
4638
4644
|
|
|
@@ -5810,6 +5816,7 @@ Every SDK release's CHANGELOG entry opens with who must change and what happens
|
|
|
5810
5816
|
|
|
5811
5817
|
| Release | Who must change | Guide |
|
|
5812
5818
|
| --- | --- | --- |
|
|
5819
|
+
| [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. | — |
|
|
5813
5820
|
| [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. | — |
|
|
5814
5821
|
| [1.5.43](https://github.com/vestrasys/vestra-player-sdk/blob/main/CHANGELOG.md#1543) | **Nobody.** Additions only, `apiVersion` stays `vestra/1`, no new capability. | — |
|
|
5815
5822
|
| [1.5.42](https://github.com/vestrasys/vestra-player-sdk/blob/main/CHANGELOG.md#1542) | **Nobody.** Documentation only: no API changes, `apiVersion` stays `vestra/1`, no player version needed. | — |
|
|
@@ -6367,8 +6374,10 @@ interface CommandState {
|
|
|
6367
6374
|
|
|
6368
6375
|
### Bindable
|
|
6369
6376
|
|
|
6370
|
-
A value, or a function the SDK re-evaluates on every update pass
|
|
6371
|
-
|
|
6377
|
+
A value, or a function the SDK re-evaluates on every full update pass,
|
|
6378
|
+
and after a player state change only if it read a field of
|
|
6379
|
+
`plugin.player.state` that changed (see Scope.update); only
|
|
6380
|
+
changes are sent to the player.
|
|
6372
6381
|
|
|
6373
6382
|
```ts
|
|
6374
6383
|
type Bindable<T> = T | (() => T);
|
|
@@ -6377,9 +6386,9 @@ type Bindable<T> = T | (() => T);
|
|
|
6377
6386
|
Example: Mix a computed state with a fixed one
|
|
6378
6387
|
|
|
6379
6388
|
The Loop item carries a check mark while a loop is set and is always
|
|
6380
|
-
enabled. A function is evaluated again in every update pass (after
|
|
6381
|
-
handler and player state change
|
|
6382
|
-
value never changes.
|
|
6389
|
+
enabled. A function is evaluated again in every full update pass (after
|
|
6390
|
+
each handler), and after a player state change only if it read a changed
|
|
6391
|
+
field; only changes are sent, and a plain value never changes.
|
|
6383
6392
|
```ts
|
|
6384
6393
|
plugin.commands.add({
|
|
6385
6394
|
id: "loop",
|
|
@@ -6800,8 +6809,9 @@ from the menu and the binding table; the id can be added again right away.
|
|
|
6800
6809
|
|
|
6801
6810
|
The handle of one menu item and its keys. Its state (`enabled`, `checked`,
|
|
6802
6811
|
`busy`, `reason`) is bound: functions given to Commands.add or
|
|
6803
|
-
Command.set are evaluated again in every update pass
|
|
6804
|
-
|
|
6812
|
+
Command.set are evaluated again in every full update pass (after
|
|
6813
|
+
a player state change only if they read a changed field of
|
|
6814
|
+
`plugin.player.state`) and only changes reach the player. Its title, path, bindings and button cannot
|
|
6805
6815
|
change in place; dispose the command (or a child Scope) and add it
|
|
6806
6816
|
again. Disposing it, its scope or stopping the plugin removes the menu
|
|
6807
6817
|
item, its keys and its bar button.
|
|
@@ -6864,7 +6874,7 @@ const exportCommand = plugin.commands.add({
|
|
|
6864
6874
|
path: ["@video", "Export…"],
|
|
6865
6875
|
run: runExport,
|
|
6866
6876
|
});
|
|
6867
|
-
// A function: re-evaluated in every
|
|
6877
|
+
// A function: re-evaluated in every full pass and when `phase` changes.
|
|
6868
6878
|
exportCommand.set({ enabled: () => plugin.player.state.phase === "ready" });
|
|
6869
6879
|
```
|
|
6870
6880
|
|
|
@@ -7415,8 +7425,9 @@ a property back: a value the user changes reaches it only through a
|
|
|
7415
7425
|
callback.
|
|
7416
7426
|
|
|
7417
7427
|
`bind` is the usual way: the SDK re-evaluates bound functions in every
|
|
7418
|
-
update pass (after each handler it calls,
|
|
7419
|
-
|
|
7428
|
+
full update pass (after each handler it calls, on Scope.update),
|
|
7429
|
+
and after a player state change those that read a changed field of
|
|
7430
|
+
`plugin.player.state`, and sends only what changed, at most one write in
|
|
7420
7431
|
flight per view. `set` sends now, even an unchanged value.
|
|
7421
7432
|
|
|
7422
7433
|
Writes that fail (a name the Slint file does not declare, a value of the
|
|
@@ -7519,7 +7530,11 @@ interface View {
|
|
|
7519
7530
|
* Keep properties equal to a function of the plugin's state.
|
|
7520
7531
|
*
|
|
7521
7532
|
* The SDK calls `props` in every update pass (and right after `bind`) and
|
|
7522
|
-
* sends the values that differ from the ones it sent last.
|
|
7533
|
+
* sends the values that differ from the ones it sent last. After a change
|
|
7534
|
+
* of the player state alone it calls `props` only if it read a field of
|
|
7535
|
+
* `plugin.player.state` that changed (SDK 1.5.45); a full pass follows
|
|
7536
|
+
* after each handler, on Scope.update and at least once a second
|
|
7537
|
+
* while the player state keeps changing. Because an
|
|
7523
7538
|
* unchanged value is not sent again, text the user typed into an `in-out`
|
|
7524
7539
|
* property stays until the plugin's value for it changes.
|
|
7525
7540
|
*
|
|
@@ -7537,8 +7552,9 @@ interface View {
|
|
|
7537
7552
|
* @example Show what is playing and whether it can seek
|
|
7538
7553
|
* The status window always shows the current file and enables its seek
|
|
7539
7554
|
* controls only for seekable sources. The SDK calls the function right
|
|
7540
|
-
* away
|
|
7541
|
-
*
|
|
7555
|
+
* away, again in every full update pass (after each handler) and after
|
|
7556
|
+
* each change of `source` or `can`, and sends only the values that
|
|
7557
|
+
* changed.
|
|
7542
7558
|
* ```ts
|
|
7543
7559
|
* const status = plugin.windows.create("ui/status.slint");
|
|
7544
7560
|
* status.bind(() => ({
|
|
@@ -10947,8 +10963,8 @@ interface Player {
|
|
|
10947
10963
|
* @example Drive the player's Next and Previous from a playlist
|
|
10948
10964
|
* The player's Next and Previous items and keys play the plugin's list.
|
|
10949
10965
|
* The player calls `next` and `previous` when the user asks; `canNext`
|
|
10950
|
-
* and `canPrevious` are evaluated again after every handler the SDK runs
|
|
10951
|
-
* and while they return false the player disables its items ("No next
|
|
10966
|
+
* and `canPrevious` are evaluated again after every handler the SDK runs
|
|
10967
|
+
* (and after a player state change if they read a changed field), and while they return false the player disables its items ("No next
|
|
10952
10968
|
* item").
|
|
10953
10969
|
* ```ts
|
|
10954
10970
|
* const playAt = async (at: number) => {
|
|
@@ -17001,6 +17017,13 @@ class TestHost implements Transport {
|
|
|
17001
17017
|
* or parsing a file, a child process, a network request) is not waited
|
|
17002
17018
|
* for: on a loaded machine it can finish after `settle()` returns. Poll
|
|
17003
17019
|
* the result you assert on instead (second example).
|
|
17020
|
+
*
|
|
17021
|
+
* `settle()` does not wait for the SDK's once-a-second full update pass
|
|
17022
|
+
* (SDK 1.5.45). After `setSource` (or another player state change) only
|
|
17023
|
+
* the bindings that read a changed field of `plugin.player.state` run
|
|
17024
|
+
* again; state the plugin changed outside a handler without
|
|
17025
|
+
* `plugin.update()` shows only in the next full pass. Have the plugin
|
|
17026
|
+
* call `plugin.update()` after such a change, or wait `settle(1000)`.
|
|
17004
17027
|
* @param ms - Extra time to wait, for debounces and timers (default 0).
|
|
17005
17028
|
* @capability None.
|
|
17006
17029
|
* @throws Never.
|
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.45: 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
|
|
|
@@ -33,7 +33,7 @@ my-plugin/
|
|
|
33
33
|
}
|
|
34
34
|
```
|
|
35
35
|
|
|
36
|
-
Fields: `id`, `version`, `apiVersion`, `entry`, `capabilities`, `name`, `author`, `punchline`, `description`, `tags`, `icon`, `readme`, `changelog`, `sourceUrl`, `license`, `minPlayerVersion`; unknown fields and capabilities are rejected. Rules and limits:
|
|
36
|
+
Fields: `id`, `version`, `apiVersion`, `entry`, `capabilities`, `name`, `author`, `punchline`, `description`, `tags`, `icon`, `readme`, `changelog`, `sourceUrl`, `license`, `minPlayerVersion`; unknown fields and capabilities are rejected. Rules and limits: L2218–2291.
|
|
37
37
|
|
|
38
38
|
**main.ts**
|
|
39
39
|
|
|
@@ -78,9 +78,9 @@ export default async function activate(plugin: Plugin) {
|
|
|
78
78
|
- `secrets`: Secrets.get/set/delete
|
|
79
79
|
- `rendering.manage`: Rendering.setPreference/restartPlayer
|
|
80
80
|
- `video.upscaling`: VideoUpscaling.state/set/onChange
|
|
81
|
-
- No capability: `tools`, `storage`, `system`, `app`, `ui` (images and Markdown), `i18n`, reading window.state, appearance.theme, packages.list and rendering.state. Details:
|
|
81
|
+
- No capability: `tools`, `storage`, `system`, `app`, `ui` (images and Markdown), `i18n`, reading window.state, appearance.theme, packages.list and rendering.state. Details: L2292–2352.
|
|
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–
|
|
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: L15479.
|
|
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 L5952.
|
|
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
|
-
- 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–
|
|
118
|
-
- Reference: Manifest
|
|
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):
|
|
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–2291; Capabilities L2292–2352; Limits L2353–2498; Commands, menus and bindings L2499–3024; Platforms L3025–3055; Theme L3056–3405; Command line L3406–3463; Error codes L3464–3511; Translations L3512–3604; Test host L3605–3791
|
|
119
|
+
- Explanation: How a plugin runs L3792–3994; Commands, menus and key bindings L3995–4149; Playback state, controls and sources L4150–4309; Video and audio processing L4310–4430; Windows, panels, settings pages, dialogs and the OSD L4431–4693; GPU and software rendering L4694–4710; The player's Rust core L4711–5052; How the player runs and talks to plugins L5053–5392
|
|
120
|
+
- Guidelines: Plugin rules L5393–5404; Interface guidelines L5405–5521; Menu guidelines L5522–5580
|
|
121
|
+
- Upgrading: Migrate a plugin to English source text L5581–5730; Migrating off package rollback (1.5.16, removed in 1.5.17) L5731–5808; Who must change, release by release L5809–5874
|
|
122
|
+
- [API: @vestra-player/sdk](https://vestrasys.github.io/vestra-player-sdk/modules/_vestra-player_sdk.html): L5875–15152
|
|
123
|
+
- [API: @vestra-player/sdk/testing](https://vestrasys.github.io/vestra-player-sdk/modules/_vestra-player_sdk_testing.html): L15153–18027
|
|
124
124
|
|
|
125
125
|
## API
|
|
126
126
|
|
|
127
127
|
Where each `plugin.<module>` is declared:
|
|
128
128
|
|
|
129
|
-
- Plugin
|
|
130
|
-
- Commands
|
|
129
|
+
- Plugin L6291, Scope L6027, PluginError L5952, Disposable L5890
|
|
130
|
+
- Commands L6881; When L6478; Windows L7824; Panels L9270; Dialogs L9940; Player L10681; video: VideoFilter L11561, VideoView L11661, RenderOptions L11748, VideoShaders L11826, VideoUpscaling L12184; audio: AudioFilter L11606; Subtitles L12420; media: MediaCapture L12824; Resolvers L13054; Packages L13456; Osd L11426; Host L11268; Clipboard L13785; Power L13872; Tools L13931; Storage L14083; System L14282; Secrets L14362; window: MainWindow L9587; App L9767; Rendering L14625; Bindings L13684; Ui L7170; Timeline L9499; Drops L8517; Appearance L8598; I18n L8831; Settings L9114.
|
|
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. L18462–18557
|
|
137
|
+
- blur: A tool window with one slider that blurs the playing video. L18558–18825
|
|
138
|
+
- signal-generator: Generates its own video and audio: colour bars and a 1 kHz tone. L18826–19164
|
|
139
|
+
- bookmark: A tool window bound to the playback state: remember the position and jump back to it. L19165–19357
|
|
140
|
+
- panel-menu: A sidebar panel with its own right-click menu. L19358–19572
|
|
141
|
+
- menus: Radio menu items, separator groups and a menu item whose title changes. L19573–19738
|
|
142
|
+
- theme-widgets: A tool window drawn with the player's theme and widgets. L19739–19908
|
|
143
|
+
- markdown: A window that shows a Markdown document. L19909–20111
|
|
144
|
+
- i18n: A plugin in the player's language: English source text, a Chinese translation. L20112–20285
|
|
145
145
|
|
|
146
146
|
## Optional
|
|
147
147
|
|