@vestra-player/sdk 1.5.37 → 1.5.38
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 +9 -0
- package/README.md +1 -0
- package/dist/plugin/appearance.js +11 -2
- package/dist/plugin/packages.js +3 -0
- package/dist/plugin/player.js +6 -1
- package/dist/plugin/resolvers.js +10 -2
- package/dist/plugin/runtime.d.ts +14 -1
- package/dist/plugin/runtime.js +107 -16
- package/dist/plugin/subtitles.js +14 -2
- package/dist/plugin/testing.d.ts +10 -0
- package/dist/plugin/testing.js +22 -2
- package/dist/plugin/timeline.d.ts +1 -1
- package/dist/plugin/timeline.js +10 -2
- package/dist/plugin/types.d.ts +11 -0
- package/dist/plugin/ui.js +9 -2
- package/dist/plugin/window.d.ts +1 -1
- package/dist/plugin/window.js +10 -2
- package/dist/wire.d.ts +17 -2
- package/dist/wire.js +14 -3
- package/llms-full.txt +22 -4
- package/llms.txt +22 -22
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.5.38
|
|
4
|
+
|
|
5
|
+
**Who must change: nobody.** Additions only, `apiVersion` stays `vestra/1`. The marks below are read by player 0.5.18 and later; older players ignore them, and a plugin built with an older SDK keeps working (the player builds the `plugin` object with its own SDK). What may change for a plugin is the player's lazy activation, off by default in player 0.5.18 and turned on in a later release: a plugin that holds nothing only its process can serve is stopped while idle and started on its next use, so state kept only in variables is lost between uses (keep it in `plugin.storage.session`).
|
|
6
|
+
|
|
7
|
+
- **Mirror subscriptions** (player 0.5.18): the SDK's own subscriptions that keep `plugin.player.state`, `plugin.window.state`, `plugin.timeline.marks`, `plugin.subtitles.state`, `plugin.appearance`, `plugin.resolvers.fileTypes` and panel positions current are sent as `events.subscribe { event, mirror: true }`. When the plugin adds a listener (an `onChange`, `onEnd`, `onFileTypesChange`) or a binding that reads that state (a command's `enabled`, `checked`, `busy` or `reason` function, a view's `bind`), the SDK sends `events.subscribe { event }` again; when the last one goes, `events.subscribe { event, mirror: true }`. A change made while the plugin activates is sent before it reports ready, so the player sees it when it classifies the plugin. The player keeps a plugin whose subscriptions are all mirrors parkable; the plugin sees no difference.
|
|
8
|
+
- **`PluginInfo.process`** (player 0.5.18, optional): for a running plugin, `"running"` while its process runs, `"idle"` while the player stopped it because nothing used it (its entries stay and its next use starts it). `state` stays `"running"`, and a change of `process` alone does not fire `packages.onChange`.
|
|
9
|
+
- **`TestHost.listening`**: the subscribed events the plugin really listens to (`subscriptions` without the mirrors).
|
|
10
|
+
- _Plugins_ (concepts) has a new section, _Idle plugins are stopped_: what stays while a plugin is stopped, what keeps it running (listeners, bindings that read state, windows, panels, chains, streams…) and what does not (network requests, timers and promises the player does not see: as in Chrome Manifest V3, keep what they need in session storage).
|
|
11
|
+
|
|
3
12
|
## 1.5.37
|
|
4
13
|
|
|
5
14
|
**Who must change: nobody.** Additions only, `apiVersion` stays `vestra/1`. The new API needs player 0.5.16 (`"minPlayerVersion": "0.5.16"`): older players do not load a plugin that declares the new capability `video.upscaling` (an unknown capability). A plugin that does not use it sees no difference.
|
package/README.md
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
<a href="https://github.com/vestrasys/vestra-player-sdk/blob/main/CHANGELOG.md">Changelog</a> ·
|
|
16
16
|
<a href="https://vestrasys.github.io/vestra-player-sdk/llms.txt">llms.txt</a>
|
|
17
17
|
</p>
|
|
18
|
+
|
|
18
19
|
## Features
|
|
19
20
|
|
|
20
21
|
- **One typed API** (`vestra/1`) for commands and menus, windows, panels, settings pages, on-screen messages, playback state and control, video and audio filters, and generated streams.
|
|
@@ -29,6 +29,7 @@ class AppearanceService {
|
|
|
29
29
|
if (this.#started)
|
|
30
30
|
return;
|
|
31
31
|
this.#started = true;
|
|
32
|
+
// A mirror: a plugin listens through `watch` and bindings.
|
|
32
33
|
this.#runtime.on("appearance.changed", (data) => {
|
|
33
34
|
const event = data;
|
|
34
35
|
const language = preference(event?.language);
|
|
@@ -39,7 +40,7 @@ class AppearanceService {
|
|
|
39
40
|
const theme = event?.theme;
|
|
40
41
|
if (theme && THEMES.includes(theme))
|
|
41
42
|
this.accept(theme);
|
|
42
|
-
});
|
|
43
|
+
}, "events", true);
|
|
43
44
|
}
|
|
44
45
|
accept(theme) {
|
|
45
46
|
if (theme === this.theme)
|
|
@@ -51,7 +52,13 @@ class AppearanceService {
|
|
|
51
52
|
}
|
|
52
53
|
watch(listener) {
|
|
53
54
|
this.#listeners.add(listener);
|
|
54
|
-
|
|
55
|
+
const held = this.#runtime.hold("appearance.changed");
|
|
56
|
+
return {
|
|
57
|
+
dispose: async () => {
|
|
58
|
+
this.#listeners.delete(listener);
|
|
59
|
+
await held.dispose();
|
|
60
|
+
},
|
|
61
|
+
};
|
|
55
62
|
}
|
|
56
63
|
}
|
|
57
64
|
const appearanceOf = (runtime) => runtime.service("appearance", () => new AppearanceService(runtime));
|
|
@@ -60,6 +67,7 @@ export function appearance(scope) {
|
|
|
60
67
|
return {
|
|
61
68
|
get theme() {
|
|
62
69
|
service().start();
|
|
70
|
+
scope.runtime.reading("appearance.changed");
|
|
63
71
|
return service().theme;
|
|
64
72
|
},
|
|
65
73
|
onChange(listener) {
|
|
@@ -80,6 +88,7 @@ export function appearance(scope) {
|
|
|
80
88
|
},
|
|
81
89
|
get language() {
|
|
82
90
|
service().start();
|
|
91
|
+
scope.runtime.reading("appearance.changed");
|
|
83
92
|
return service().language;
|
|
84
93
|
},
|
|
85
94
|
async setLanguage(language) {
|
package/dist/plugin/packages.js
CHANGED
|
@@ -164,6 +164,9 @@ const info = (plugin) => Object.freeze({
|
|
|
164
164
|
...(warning.slot === undefined ? {} : { slot: warning.slot }),
|
|
165
165
|
...(warning.owner === undefined ? {} : { owner: warning.owner }),
|
|
166
166
|
}))),
|
|
167
|
+
...(plugin.process === "running" || plugin.process === "idle"
|
|
168
|
+
? { process: plugin.process }
|
|
169
|
+
: {}),
|
|
167
170
|
...(plugin.display
|
|
168
171
|
? {
|
|
169
172
|
display: Object.freeze({
|
package/dist/plugin/player.js
CHANGED
|
@@ -116,7 +116,9 @@ export class PlayerService {
|
|
|
116
116
|
output;
|
|
117
117
|
constructor(runtime) {
|
|
118
118
|
this.runtime = runtime;
|
|
119
|
-
|
|
119
|
+
// The SDK's copy of the state (a mirror): a plugin listens through
|
|
120
|
+
// `watch` and the bindings that read `state`.
|
|
121
|
+
runtime.on("source.state", (data) => this.accept(data), "events", true);
|
|
120
122
|
this.output = new Latest(async ({ volume, muted }) => {
|
|
121
123
|
if (volume !== undefined)
|
|
122
124
|
await runtime.host.playback.set("volume", volume);
|
|
@@ -144,9 +146,11 @@ export class PlayerService {
|
|
|
144
146
|
}
|
|
145
147
|
watch(watcher) {
|
|
146
148
|
this.#watchers.add(watcher);
|
|
149
|
+
const held = this.runtime.hold("source.state");
|
|
147
150
|
return {
|
|
148
151
|
dispose: async () => {
|
|
149
152
|
this.#watchers.delete(watcher);
|
|
153
|
+
await held.dispose();
|
|
150
154
|
},
|
|
151
155
|
};
|
|
152
156
|
}
|
|
@@ -429,6 +433,7 @@ export function player(scope) {
|
|
|
429
433
|
const service = () => playerOf(scope.runtime);
|
|
430
434
|
return {
|
|
431
435
|
get state() {
|
|
436
|
+
scope.runtime.reading("source.state");
|
|
432
437
|
return service().state;
|
|
433
438
|
},
|
|
434
439
|
onChange(listener) {
|
package/dist/plugin/resolvers.js
CHANGED
|
@@ -26,11 +26,12 @@ class FileTypesService {
|
|
|
26
26
|
if (this.#started)
|
|
27
27
|
return;
|
|
28
28
|
this.#started = true;
|
|
29
|
+
// A mirror: a plugin listens through `watch` and bindings.
|
|
29
30
|
this.#runtime.on("resolvers.fileTypes", (data) => {
|
|
30
31
|
const types = data?.fileTypes;
|
|
31
32
|
if (Array.isArray(types) && types.every((t) => typeof t === "string"))
|
|
32
33
|
this.accept(types);
|
|
33
|
-
});
|
|
34
|
+
}, "events", true);
|
|
34
35
|
// What changed between the start-up read and the subscription.
|
|
35
36
|
const window = windowOf(this.#runtime);
|
|
36
37
|
void window.read().then(() => {
|
|
@@ -50,7 +51,13 @@ class FileTypesService {
|
|
|
50
51
|
}
|
|
51
52
|
watch(listener) {
|
|
52
53
|
this.#listeners.add(listener);
|
|
53
|
-
|
|
54
|
+
const held = this.#runtime.hold("resolvers.fileTypes");
|
|
55
|
+
return {
|
|
56
|
+
dispose: async () => {
|
|
57
|
+
this.#listeners.delete(listener);
|
|
58
|
+
await held.dispose();
|
|
59
|
+
},
|
|
60
|
+
};
|
|
54
61
|
}
|
|
55
62
|
}
|
|
56
63
|
const fileTypesOf = (runtime) => runtime.service("fileTypes", () => new FileTypesService(runtime));
|
|
@@ -58,6 +65,7 @@ export const resolvers = (scope) => ({
|
|
|
58
65
|
get fileTypes() {
|
|
59
66
|
const service = fileTypesOf(scope.runtime);
|
|
60
67
|
service.start();
|
|
68
|
+
scope.runtime.reading("resolvers.fileTypes");
|
|
61
69
|
return service.types;
|
|
62
70
|
},
|
|
63
71
|
onFileTypesChange(listener) {
|
package/dist/plugin/runtime.d.ts
CHANGED
|
@@ -53,15 +53,28 @@ export declare class Runtime {
|
|
|
53
53
|
starting<T>(work: Promise<T>, report: (error: unknown) => void): Promise<T>;
|
|
54
54
|
activated(): Promise<void>;
|
|
55
55
|
bind(evaluate: () => void): Disposable;
|
|
56
|
+
/**
|
|
57
|
+
* A service's state for `event` is read: a binding evaluated now depends
|
|
58
|
+
* on it, so the event counts as listened to while the binding reads it.
|
|
59
|
+
*/
|
|
60
|
+
reading(event: string): void;
|
|
56
61
|
/** Coalesce: one evaluation pass per microtask turn, however many changes. */
|
|
57
62
|
invalidate(): void;
|
|
58
63
|
/**
|
|
59
64
|
* Host events with one `events.subscribe` per name, however many listeners.
|
|
60
65
|
* Without the `events` capability the listener simply never fires.
|
|
66
|
+
* `mirror`: the SDK's own copy of the state (`plugin.player.state` and
|
|
67
|
+
* the like), not a listener of the plugin's (see {@link EventGroup}).
|
|
61
68
|
*/
|
|
62
69
|
on(event: string, listener: Listener,
|
|
63
70
|
/** The capability the event needs; `null`: none. */
|
|
64
|
-
needs?: string | null): Disposable;
|
|
71
|
+
needs?: string | null, mirror?: boolean): Disposable;
|
|
72
|
+
/**
|
|
73
|
+
* Count `event` as listened to while held: a watcher of a service's
|
|
74
|
+
* state, a binding that reads it. Nothing when nothing subscribed to it
|
|
75
|
+
* (no capability, or the service did not start).
|
|
76
|
+
*/
|
|
77
|
+
hold(event: string): Disposable;
|
|
65
78
|
/** Raw transport events the host pushes without a subscription. */
|
|
66
79
|
raw(event: string, listener: Listener): Disposable;
|
|
67
80
|
}
|
package/dist/plugin/runtime.js
CHANGED
|
@@ -87,7 +87,10 @@ export class Runtime {
|
|
|
87
87
|
startup = new Set();
|
|
88
88
|
#activating = true;
|
|
89
89
|
#transport;
|
|
90
|
-
|
|
90
|
+
/** Each binding with the events whose state it read last time (held). */
|
|
91
|
+
#bindings = new Map();
|
|
92
|
+
/** The events read by the binding being evaluated. */
|
|
93
|
+
#reads;
|
|
91
94
|
#scheduled = false;
|
|
92
95
|
#events = new Map();
|
|
93
96
|
views = new Map();
|
|
@@ -149,14 +152,47 @@ export class Runtime {
|
|
|
149
152
|
this.#activating = false;
|
|
150
153
|
}
|
|
151
154
|
bind(evaluate) {
|
|
152
|
-
|
|
155
|
+
const held = new Map();
|
|
156
|
+
this.#bindings.set(evaluate, held);
|
|
153
157
|
this.invalidate();
|
|
154
158
|
return {
|
|
155
159
|
dispose: async () => {
|
|
160
|
+
if (this.#bindings.get(evaluate) !== held)
|
|
161
|
+
return;
|
|
156
162
|
this.#bindings.delete(evaluate);
|
|
163
|
+
for (const hold of held.values())
|
|
164
|
+
await hold.dispose();
|
|
165
|
+
held.clear();
|
|
157
166
|
},
|
|
158
167
|
};
|
|
159
168
|
}
|
|
169
|
+
/**
|
|
170
|
+
* A service's state for `event` is read: a binding evaluated now depends
|
|
171
|
+
* on it, so the event counts as listened to while the binding reads it.
|
|
172
|
+
*/
|
|
173
|
+
reading(event) {
|
|
174
|
+
this.#reads?.add(event);
|
|
175
|
+
}
|
|
176
|
+
/** Evaluate one binding, holding the events whose state it read. */
|
|
177
|
+
#evaluate(evaluate, held) {
|
|
178
|
+
const reads = new Set();
|
|
179
|
+
const outer = this.#reads;
|
|
180
|
+
this.#reads = reads;
|
|
181
|
+
try {
|
|
182
|
+
evaluate();
|
|
183
|
+
}
|
|
184
|
+
finally {
|
|
185
|
+
this.#reads = outer;
|
|
186
|
+
for (const [event, hold] of held)
|
|
187
|
+
if (!reads.has(event)) {
|
|
188
|
+
held.delete(event);
|
|
189
|
+
void hold.dispose();
|
|
190
|
+
}
|
|
191
|
+
for (const event of reads)
|
|
192
|
+
if (!held.has(event))
|
|
193
|
+
held.set(event, this.hold(event));
|
|
194
|
+
}
|
|
195
|
+
}
|
|
160
196
|
/** Coalesce: one evaluation pass per microtask turn, however many changes. */
|
|
161
197
|
invalidate() {
|
|
162
198
|
if (this.#scheduled)
|
|
@@ -164,9 +200,11 @@ export class Runtime {
|
|
|
164
200
|
this.#scheduled = true;
|
|
165
201
|
queueMicrotask(() => {
|
|
166
202
|
this.#scheduled = false;
|
|
167
|
-
for (const evaluate of this.#bindings) {
|
|
203
|
+
for (const [evaluate, held] of [...this.#bindings]) {
|
|
204
|
+
if (this.#bindings.get(evaluate) !== held)
|
|
205
|
+
continue;
|
|
168
206
|
try {
|
|
169
|
-
evaluate();
|
|
207
|
+
this.#evaluate(evaluate, held);
|
|
170
208
|
}
|
|
171
209
|
catch (error) {
|
|
172
210
|
console.error(`[${this.manifest.id}] binding failed:`, error);
|
|
@@ -177,36 +215,89 @@ export class Runtime {
|
|
|
177
215
|
/**
|
|
178
216
|
* Host events with one `events.subscribe` per name, however many listeners.
|
|
179
217
|
* Without the `events` capability the listener simply never fires.
|
|
218
|
+
* `mirror`: the SDK's own copy of the state (`plugin.player.state` and
|
|
219
|
+
* the like), not a listener of the plugin's (see {@link EventGroup}).
|
|
180
220
|
*/
|
|
181
221
|
on(event, listener,
|
|
182
222
|
/** The capability the event needs; `null`: none. */
|
|
183
|
-
needs = "events") {
|
|
223
|
+
needs = "events", mirror = false) {
|
|
184
224
|
let entry = this.#events.get(event);
|
|
225
|
+
const created = !entry;
|
|
185
226
|
if (!entry) {
|
|
186
|
-
entry = { listeners: new Set() };
|
|
227
|
+
entry = { listeners: new Set(), active: 0 };
|
|
187
228
|
this.#events.set(event, entry);
|
|
188
|
-
if (needs === null || this.can(needs)) {
|
|
189
|
-
const group = entry;
|
|
190
|
-
entry.off = this.host.events.subscribe(event, (data) => {
|
|
191
|
-
for (const each of [...group.listeners])
|
|
192
|
-
each(data);
|
|
193
|
-
});
|
|
194
|
-
this.starting(entry.off, (error) => console.error(`[${this.manifest.id}] subscribe ${event}:`, error));
|
|
195
|
-
}
|
|
196
229
|
}
|
|
197
|
-
entry.listeners.add(listener);
|
|
198
230
|
const group = entry;
|
|
231
|
+
group.listeners.add(listener);
|
|
232
|
+
if (!mirror)
|
|
233
|
+
group.active++;
|
|
234
|
+
if (created && (needs === null || this.can(needs))) {
|
|
235
|
+
group.told = group.active > 0;
|
|
236
|
+
group.off = this.host.events.subscribe(event, (data) => {
|
|
237
|
+
for (const each of [...group.listeners])
|
|
238
|
+
each(data);
|
|
239
|
+
}, !group.told);
|
|
240
|
+
this.starting(group.off, (error) => console.error(`[${this.manifest.id}] subscribe ${event}:`, error));
|
|
241
|
+
}
|
|
242
|
+
else
|
|
243
|
+
this.#tell(event, group);
|
|
244
|
+
let listening = true;
|
|
199
245
|
return {
|
|
200
246
|
dispose: async () => {
|
|
247
|
+
if (!listening)
|
|
248
|
+
return;
|
|
249
|
+
listening = false;
|
|
201
250
|
group.listeners.delete(listener);
|
|
202
|
-
if (
|
|
251
|
+
if (!mirror)
|
|
252
|
+
group.active--;
|
|
253
|
+
if (this.#events.get(event) !== group)
|
|
203
254
|
return;
|
|
255
|
+
if (group.listeners.size)
|
|
256
|
+
return this.#tell(event, group);
|
|
204
257
|
this.#events.delete(event);
|
|
205
258
|
const off = await group.off?.catch(() => undefined);
|
|
206
259
|
await off?.().catch(() => { });
|
|
207
260
|
},
|
|
208
261
|
};
|
|
209
262
|
}
|
|
263
|
+
/**
|
|
264
|
+
* Count `event` as listened to while held: a watcher of a service's
|
|
265
|
+
* state, a binding that reads it. Nothing when nothing subscribed to it
|
|
266
|
+
* (no capability, or the service did not start).
|
|
267
|
+
*/
|
|
268
|
+
hold(event) {
|
|
269
|
+
const group = this.#events.get(event);
|
|
270
|
+
if (!group)
|
|
271
|
+
return { dispose: async () => { } };
|
|
272
|
+
group.active++;
|
|
273
|
+
this.#tell(event, group);
|
|
274
|
+
let held = true;
|
|
275
|
+
return {
|
|
276
|
+
dispose: async () => {
|
|
277
|
+
if (!held)
|
|
278
|
+
return;
|
|
279
|
+
held = false;
|
|
280
|
+
group.active--;
|
|
281
|
+
if (this.#events.get(event) === group)
|
|
282
|
+
this.#tell(event, group);
|
|
283
|
+
},
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
/** Tell the player when `group` changes between mirror and listener. */
|
|
287
|
+
#tell(event, group) {
|
|
288
|
+
const off = group.off;
|
|
289
|
+
const active = group.active > 0;
|
|
290
|
+
if (!off || group.told === active)
|
|
291
|
+
return;
|
|
292
|
+
group.told = active;
|
|
293
|
+
// Part of activation while it runs: the player classifies the plugin
|
|
294
|
+
// at its ready, which must see this mark. A failure is not one of
|
|
295
|
+
// activation (an older player never refuses it).
|
|
296
|
+
const told = off
|
|
297
|
+
.then(() => this.host.events.listen(event, active))
|
|
298
|
+
.catch(() => { });
|
|
299
|
+
this.starting(told, () => { });
|
|
300
|
+
}
|
|
210
301
|
/** Raw transport events the host pushes without a subscription. */
|
|
211
302
|
raw(event, listener) {
|
|
212
303
|
const off = this.#transport.on(event, listener);
|
package/dist/plugin/subtitles.js
CHANGED
|
@@ -62,6 +62,8 @@ export const SUBTITLE_LIMITS = Object.freeze({
|
|
|
62
62
|
/** Loads of every plugin in progress at the same time (8). */
|
|
63
63
|
uploads: 8,
|
|
64
64
|
});
|
|
65
|
+
/** The events the subtitle state follows (it has none of its own). */
|
|
66
|
+
const EVENTS = ["source.state", "playback.backend"];
|
|
65
67
|
const raws = new WeakMap();
|
|
66
68
|
const targets = new WeakMap();
|
|
67
69
|
const CHUNK = 32 * 1024;
|
|
@@ -85,6 +87,7 @@ class SubtitleService {
|
|
|
85
87
|
constructor(runtime) {
|
|
86
88
|
this.#runtime = runtime;
|
|
87
89
|
// No subtitle event exists: re-read when the source or backend changes.
|
|
90
|
+
// Mirrors: a plugin listens through `watch` and bindings.
|
|
88
91
|
runtime.on("source.state", (data) => {
|
|
89
92
|
const raw = data;
|
|
90
93
|
const source = `${raw.stamp.generation}:${raw.phase}`;
|
|
@@ -92,8 +95,8 @@ class SubtitleService {
|
|
|
92
95
|
return;
|
|
93
96
|
this.#source = source;
|
|
94
97
|
void this.refresh().catch(() => { });
|
|
95
|
-
});
|
|
96
|
-
runtime.on("playback.backend", () => void this.refresh().catch(() => { }));
|
|
98
|
+
}, "events", true);
|
|
99
|
+
runtime.on("playback.backend", () => void this.refresh().catch(() => { }), "events", true);
|
|
97
100
|
this.style = new Latest(async ({ style, options }) => {
|
|
98
101
|
const expected = await this.expected(options);
|
|
99
102
|
await this.#mutate(++this.#operation, (operationId) => runtime.host.subtitles.style({
|
|
@@ -106,12 +109,20 @@ class SubtitleService {
|
|
|
106
109
|
}
|
|
107
110
|
watch(watcher) {
|
|
108
111
|
this.#watchers.add(watcher);
|
|
112
|
+
const held = EVENTS.map((event) => this.#runtime.hold(event));
|
|
109
113
|
return {
|
|
110
114
|
dispose: async () => {
|
|
111
115
|
this.#watchers.delete(watcher);
|
|
116
|
+
for (const hold of held.splice(0))
|
|
117
|
+
await hold.dispose();
|
|
112
118
|
},
|
|
113
119
|
};
|
|
114
120
|
}
|
|
121
|
+
/** A binding that reads the state follows both events. */
|
|
122
|
+
reading() {
|
|
123
|
+
for (const event of EVENTS)
|
|
124
|
+
this.#runtime.reading(event);
|
|
125
|
+
}
|
|
115
126
|
/** Keep the newest view: older generations and older revisions are dropped. */
|
|
116
127
|
accept(raw) {
|
|
117
128
|
const old = this.raw;
|
|
@@ -258,6 +269,7 @@ export function subtitles(scope) {
|
|
|
258
269
|
const host = () => scope.runtime.host.subtitles;
|
|
259
270
|
return {
|
|
260
271
|
get state() {
|
|
272
|
+
service().reading();
|
|
261
273
|
return service().state;
|
|
262
274
|
},
|
|
263
275
|
onChange(listener) {
|
package/dist/plugin/testing.d.ts
CHANGED
|
@@ -535,6 +535,16 @@ export declare class TestHost implements Transport {
|
|
|
535
535
|
readonly calls: Call[];
|
|
536
536
|
/** The host events the plugin subscribed to (needs `events` etc.). */
|
|
537
537
|
readonly subscriptions: Set<string>;
|
|
538
|
+
/**
|
|
539
|
+
* The subscribed events the plugin really listens to: an `onChange` and
|
|
540
|
+
* the like, or a binding that reads the state. The SDK's own copies of
|
|
541
|
+
* the state (what keeps `plugin.player.state` current) are left out.
|
|
542
|
+
* Empty means the player may stop the plugin while nothing uses it
|
|
543
|
+
* (unless it holds something else: a window, a panel, a filter…).
|
|
544
|
+
*
|
|
545
|
+
* @since 1.5.38
|
|
546
|
+
*/
|
|
547
|
+
readonly listening: Set<string>;
|
|
538
548
|
/**
|
|
539
549
|
* The player's source facts (phase, position, duration, paused, rate,
|
|
540
550
|
* audio tracks, loop, ...). Wire-level shape; change it with
|
package/dist/plugin/testing.js
CHANGED
|
@@ -333,6 +333,16 @@ export class TestHost {
|
|
|
333
333
|
calls = [];
|
|
334
334
|
/** The host events the plugin subscribed to (needs `events` etc.). */
|
|
335
335
|
subscriptions = new Set();
|
|
336
|
+
/**
|
|
337
|
+
* The subscribed events the plugin really listens to: an `onChange` and
|
|
338
|
+
* the like, or a binding that reads the state. The SDK's own copies of
|
|
339
|
+
* the state (what keeps `plugin.player.state` current) are left out.
|
|
340
|
+
* Empty means the player may stop the plugin while nothing uses it
|
|
341
|
+
* (unless it holds something else: a window, a panel, a filter…).
|
|
342
|
+
*
|
|
343
|
+
* @since 1.5.38
|
|
344
|
+
*/
|
|
345
|
+
listening = new Set();
|
|
336
346
|
/**
|
|
337
347
|
* The player's source facts (phase, position, duration, paused, rate,
|
|
338
348
|
* audio tracks, loop, ...). Wire-level shape; change it with
|
|
@@ -3377,8 +3387,18 @@ export class TestHost {
|
|
|
3377
3387
|
};
|
|
3378
3388
|
}
|
|
3379
3389
|
#defaults = {
|
|
3380
|
-
"events.subscribe": (p) =>
|
|
3381
|
-
|
|
3390
|
+
"events.subscribe": (p) => {
|
|
3391
|
+
const event = String(p.event);
|
|
3392
|
+
this.subscriptions.add(event);
|
|
3393
|
+
if (p.mirror === true)
|
|
3394
|
+
this.listening.delete(event);
|
|
3395
|
+
else
|
|
3396
|
+
this.listening.add(event);
|
|
3397
|
+
},
|
|
3398
|
+
"events.unsubscribe": (p) => {
|
|
3399
|
+
this.subscriptions.delete(String(p.event));
|
|
3400
|
+
this.listening.delete(String(p.event));
|
|
3401
|
+
},
|
|
3382
3402
|
"commands.register": (p) => {
|
|
3383
3403
|
const id = String(p.id);
|
|
3384
3404
|
// The player's built-in menu ids (MENU_IDS) in the first path segment.
|
|
@@ -16,7 +16,7 @@ export declare class TimelineService {
|
|
|
16
16
|
accept(marks: WireMark[]): void;
|
|
17
17
|
read(): Promise<void>;
|
|
18
18
|
watch(listener: (marks: readonly TimelineMark[]) => void): {
|
|
19
|
-
dispose: () => Promise<
|
|
19
|
+
dispose: () => Promise<void>;
|
|
20
20
|
};
|
|
21
21
|
}
|
|
22
22
|
export declare const timelineOf: (runtime: Runtime) => TimelineService;
|
package/dist/plugin/timeline.js
CHANGED
|
@@ -27,13 +27,14 @@ export class TimelineService {
|
|
|
27
27
|
if (this.#started)
|
|
28
28
|
return;
|
|
29
29
|
this.#started = true;
|
|
30
|
+
// A mirror: a plugin listens through `watch` and bindings.
|
|
30
31
|
this.#runtime.on("timeline.changed", (data) => {
|
|
31
32
|
const marks = data?.marks;
|
|
32
33
|
if (Array.isArray(marks))
|
|
33
34
|
this.accept(marks);
|
|
34
35
|
else
|
|
35
36
|
void this.read();
|
|
36
|
-
});
|
|
37
|
+
}, "events", true);
|
|
37
38
|
}
|
|
38
39
|
accept(marks) {
|
|
39
40
|
const next = view(marks);
|
|
@@ -51,7 +52,13 @@ export class TimelineService {
|
|
|
51
52
|
}
|
|
52
53
|
watch(listener) {
|
|
53
54
|
this.#listeners.add(listener);
|
|
54
|
-
|
|
55
|
+
const held = this.#runtime.hold("timeline.changed");
|
|
56
|
+
return {
|
|
57
|
+
dispose: async () => {
|
|
58
|
+
this.#listeners.delete(listener);
|
|
59
|
+
await held.dispose();
|
|
60
|
+
},
|
|
61
|
+
};
|
|
55
62
|
}
|
|
56
63
|
}
|
|
57
64
|
export const timelineOf = (runtime) => runtime.service("timeline", () => new TimelineService(runtime));
|
|
@@ -60,6 +67,7 @@ export function timeline(scope) {
|
|
|
60
67
|
return {
|
|
61
68
|
get marks() {
|
|
62
69
|
service().start();
|
|
70
|
+
scope.runtime.reading("timeline.changed");
|
|
63
71
|
return service().marks;
|
|
64
72
|
},
|
|
65
73
|
onChange(listener) {
|
package/dist/plugin/types.d.ts
CHANGED
|
@@ -7971,6 +7971,17 @@ export interface PluginInfo {
|
|
|
7971
7971
|
* @since 1.5.9
|
|
7972
7972
|
*/
|
|
7973
7973
|
display?: PluginDisplay;
|
|
7974
|
+
/**
|
|
7975
|
+
* For `state: "running"`: `"running"` while its process runs, `"idle"`
|
|
7976
|
+
* while the player stopped it because nothing used it (lazy activation:
|
|
7977
|
+
* it starts again on its next use, its menu items, keys, settings pages,
|
|
7978
|
+
* resolvers and drop targets stay). Absent in other states and from
|
|
7979
|
+
* players before 0.5.18. A change between the two alone does not fire
|
|
7980
|
+
* {@link Packages.onChange}.
|
|
7981
|
+
*
|
|
7982
|
+
* @since 1.5.38
|
|
7983
|
+
*/
|
|
7984
|
+
process?: "running" | "idle";
|
|
7974
7985
|
}
|
|
7975
7986
|
/**
|
|
7976
7987
|
* {@link PluginInfo.display}: a package's `name`, `punchline`,
|
package/dist/plugin/ui.js
CHANGED
|
@@ -534,13 +534,20 @@ class LayoutService {
|
|
|
534
534
|
if (this.#started || !this.#runtime.can("ui.mount"))
|
|
535
535
|
return;
|
|
536
536
|
this.#started = true;
|
|
537
|
-
|
|
537
|
+
// A mirror: panels listen through `watch`.
|
|
538
|
+
this.#runtime.on("window.state", () => void this.read(), "events", true);
|
|
538
539
|
void this.read();
|
|
539
540
|
}
|
|
540
541
|
watch(listener) {
|
|
541
542
|
this.start();
|
|
542
543
|
this.#listeners.add(listener);
|
|
543
|
-
|
|
544
|
+
const held = this.#runtime.hold("window.state");
|
|
545
|
+
return {
|
|
546
|
+
dispose: async () => {
|
|
547
|
+
this.#listeners.delete(listener);
|
|
548
|
+
await held.dispose();
|
|
549
|
+
},
|
|
550
|
+
};
|
|
544
551
|
}
|
|
545
552
|
async read() {
|
|
546
553
|
if (this.#reading) {
|
package/dist/plugin/window.d.ts
CHANGED
|
@@ -26,7 +26,7 @@ export declare class WindowService {
|
|
|
26
26
|
accept(facts: WindowFacts): void;
|
|
27
27
|
read(): Promise<void>;
|
|
28
28
|
watch(listener: (state: WindowState) => void): {
|
|
29
|
-
dispose: () => Promise<
|
|
29
|
+
dispose: () => Promise<void>;
|
|
30
30
|
};
|
|
31
31
|
}
|
|
32
32
|
export declare const windowOf: (runtime: Runtime) => WindowService;
|
package/dist/plugin/window.js
CHANGED
|
@@ -40,6 +40,7 @@ export class WindowService {
|
|
|
40
40
|
if (this.#started)
|
|
41
41
|
return;
|
|
42
42
|
this.#started = true;
|
|
43
|
+
// A mirror: a plugin listens through `watch` and bindings.
|
|
43
44
|
this.#runtime.on("window.state", (data) => {
|
|
44
45
|
const facts = data;
|
|
45
46
|
// The event carries the facts; anything else is read again.
|
|
@@ -47,7 +48,7 @@ export class WindowService {
|
|
|
47
48
|
this.accept(facts);
|
|
48
49
|
else
|
|
49
50
|
void this.read();
|
|
50
|
-
});
|
|
51
|
+
}, "events", true);
|
|
51
52
|
void this.read();
|
|
52
53
|
}
|
|
53
54
|
/** The palette, as the `window.state` reply carried it (`plugin.appearance`). */
|
|
@@ -91,7 +92,13 @@ export class WindowService {
|
|
|
91
92
|
}
|
|
92
93
|
watch(listener) {
|
|
93
94
|
this.#listeners.add(listener);
|
|
94
|
-
|
|
95
|
+
const held = this.#runtime.hold("window.state");
|
|
96
|
+
return {
|
|
97
|
+
dispose: async () => {
|
|
98
|
+
this.#listeners.delete(listener);
|
|
99
|
+
await held.dispose();
|
|
100
|
+
},
|
|
101
|
+
};
|
|
95
102
|
}
|
|
96
103
|
}
|
|
97
104
|
export const windowOf = (runtime) => runtime.service("window", () => new WindowService(runtime));
|
|
@@ -101,6 +108,7 @@ export function mainWindow(scope) {
|
|
|
101
108
|
return {
|
|
102
109
|
get state() {
|
|
103
110
|
service().start();
|
|
111
|
+
scope.runtime.reading("window.state");
|
|
104
112
|
return service().state;
|
|
105
113
|
},
|
|
106
114
|
onChange(listener) {
|
package/dist/wire.d.ts
CHANGED
|
@@ -141,6 +141,8 @@ export interface PackageInfo {
|
|
|
141
141
|
description: string | null;
|
|
142
142
|
tags: string[];
|
|
143
143
|
};
|
|
144
|
+
/** Whether it has a process now (player 0.5.18, `state: "running"`). */
|
|
145
|
+
process?: "running" | "idle";
|
|
144
146
|
}
|
|
145
147
|
/** Where an installed package came from (`packages.install` `source`). */
|
|
146
148
|
export type WireSource = {
|
|
@@ -365,6 +367,8 @@ export interface Transport {
|
|
|
365
367
|
*/
|
|
366
368
|
locale?: string;
|
|
367
369
|
}
|
|
370
|
+
/** The events a plugin subscribes to (`events.subscribe`). */
|
|
371
|
+
type HostEvent = "source.state" | "playback.backend" | "window.state" | "packages.changed" | "bindings.changed" | "timeline.changed" | "appearance.changed" | "resolvers.fileTypes";
|
|
368
372
|
export declare function createWire(transport: Transport): {
|
|
369
373
|
processing: {
|
|
370
374
|
state: () => Promise<import("./processing.ts").ProcessingState>;
|
|
@@ -599,8 +603,19 @@ export declare function createWire(transport: Transport): {
|
|
|
599
603
|
}) => Promise<void>;
|
|
600
604
|
};
|
|
601
605
|
events: {
|
|
602
|
-
/**
|
|
603
|
-
|
|
606
|
+
/**
|
|
607
|
+
* One `events.subscribe` per name however many listeners. `mirror`:
|
|
608
|
+
* only the SDK keeps a copy of the state (player 0.5.18; older
|
|
609
|
+
* players ignore it); `listen` changes it.
|
|
610
|
+
*/
|
|
611
|
+
subscribe(event: HostEvent, listener: (data: unknown) => void, mirror?: boolean): Promise<() => Promise<void>>;
|
|
612
|
+
/**
|
|
613
|
+
* Tell the player whether a subscribed event has a real listener
|
|
614
|
+
* (`active`) or only the SDK's copy of the state: `events.subscribe`
|
|
615
|
+
* again, which replaces the mark (player 0.5.18; older players keep
|
|
616
|
+
* one subscription per name and ignore the mark).
|
|
617
|
+
*/
|
|
618
|
+
listen: (event: HostEvent, active: boolean) => Promise<void>;
|
|
604
619
|
};
|
|
605
620
|
ui: {
|
|
606
621
|
/** Player 0.2.0: the remote images a Markdown document shows. */
|
package/dist/wire.js
CHANGED
|
@@ -255,14 +255,18 @@ export function createWire(transport) {
|
|
|
255
255
|
set: (options) => transport.request("layout.set", options),
|
|
256
256
|
},
|
|
257
257
|
events: {
|
|
258
|
-
/**
|
|
259
|
-
|
|
258
|
+
/**
|
|
259
|
+
* One `events.subscribe` per name however many listeners. `mirror`:
|
|
260
|
+
* only the SDK keeps a copy of the state (player 0.5.18; older
|
|
261
|
+
* players ignore it); `listen` changes it.
|
|
262
|
+
*/
|
|
263
|
+
async subscribe(event, listener, mirror = false) {
|
|
260
264
|
const off = transport.on(event, listener);
|
|
261
265
|
let subscription = subscriptions.get(event);
|
|
262
266
|
if (!subscription) {
|
|
263
267
|
subscription = {
|
|
264
268
|
count: 0,
|
|
265
|
-
ready: transport.request("events.subscribe", { event }),
|
|
269
|
+
ready: transport.request("events.subscribe", mirror ? { event, mirror: true } : { event }),
|
|
266
270
|
};
|
|
267
271
|
subscriptions.set(event, subscription);
|
|
268
272
|
}
|
|
@@ -287,6 +291,13 @@ export function createWire(transport) {
|
|
|
287
291
|
}
|
|
288
292
|
};
|
|
289
293
|
},
|
|
294
|
+
/**
|
|
295
|
+
* Tell the player whether a subscribed event has a real listener
|
|
296
|
+
* (`active`) or only the SDK's copy of the state: `events.subscribe`
|
|
297
|
+
* again, which replaces the mark (player 0.5.18; older players keep
|
|
298
|
+
* one subscription per name and ignore the mark).
|
|
299
|
+
*/
|
|
300
|
+
listen: (event, active) => transport.request("events.subscribe", active ? { event } : { event, mirror: true }),
|
|
290
301
|
},
|
|
291
302
|
ui: {
|
|
292
303
|
/** Player 0.2.0: the remote images a Markdown document shows. */
|
package/llms-full.txt
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# @vestra-player/sdk 1.5.
|
|
1
|
+
# @vestra-player/sdk 1.5.38: 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
|
|
|
@@ -329,11 +329,11 @@ The rule that follows: **state you change outside a handler, or after an `await`
|
|
|
329
329
|
|
|
330
330
|
#### Stopping
|
|
331
331
|
|
|
332
|
-
The player stops a plugin when the user disables or uninstalls it, when it is reloaded or upgraded,
|
|
332
|
+
The player stops a plugin when the user disables or uninstalls it, when it is reloaded or upgraded, when the player quits, and, with lazy activation, when nothing has used it for a while (Idle plugins).
|
|
333
333
|
|
|
334
334
|
1. The player first removes everything the plugin owns on its side: menu items, windows, panels, settings pages, filter chains, subtitle layers, snapshots, export jobs, resolvers, generated streams.
|
|
335
335
|
2. The SDK then refuses new requests, fails pending ones with `code: "disabled"` and disposes the root scope. That disposal only cleans up locally (timers, abort signals, sockets): the player-side resources are already gone.
|
|
336
|
-
3. A process that has not exited 300 ms later is killed.
|
|
336
|
+
3. A process that has not exited 300 ms later is killed (2 s later when it stops because it is idle).
|
|
337
337
|
|
|
338
338
|
So `onDispose` cleanup must be quick and must not rely on the player. Media the plugin opened with player.open **keeps playing** after it stops.
|
|
339
339
|
|
|
@@ -344,7 +344,7 @@ A plugin's process does not live as long as the player. Within one player sessio
|
|
|
344
344
|
- the plugin is **reloaded** (from the plugin manager, or a development load with `--plugin`) or **upgraded**;
|
|
345
345
|
- it **crashed**, or failed to start, and is started again.
|
|
346
346
|
|
|
347
|
-
A later player may restart a plugin in more cases: restarting plugins that share one process together when one of them faults or hangs, or stopping a plugin nobody uses and starting it on its next use. Write the plugin for those too.
|
|
347
|
+
A later player may restart a plugin in more cases: restarting plugins that share one process together when one of them faults or hangs, or stopping a plugin nobody uses and starting it on its next use (Idle plugins). Write the plugin for those too.
|
|
348
348
|
|
|
349
349
|
Everything in the process is gone after such a restart: variables, timers, caches, open sockets. The menus, windows and panels come back because `activate` creates them again. **Keep state that must survive a restart in session storage (or persistent storage):**
|
|
350
350
|
|
|
@@ -358,6 +358,16 @@ Session storage is for state that belongs to this run of the player: a queue the
|
|
|
358
358
|
|
|
359
359
|
Write `activate` so that running it twice in one session is normal: read what you kept, then build the interface from it.
|
|
360
360
|
|
|
361
|
+
#### Idle plugins are stopped
|
|
362
|
+
|
|
363
|
+
With lazy activation (player 0.5.18 and later, while the player turns it on), the player works like a browser's extension service workers (Chrome Manifest V3): a plugin that holds nothing only its process can serve is stopped after about a minute without use, and started again by its next use.
|
|
364
|
+
|
|
365
|
+
- **What stays while it is stopped**: its menu items and keys (with the last `enabled`, `checked` and `reason` it set), its settings pages, its file and link resolvers and its drop targets. Using one of them, or showing one of its settings pages, starts the plugin; it runs `activate`, registers them again, and then receives the click, the open or the drop. The player also remembers these between runs, so a plugin nobody uses is not started at launch at all.
|
|
366
|
+
- **What keeps it running**: anything the player must deliver to it or that is its running work: a listener (`player.onChange`, `window.onChange`, `bindings.onChange`, `packages.onChange` and the other `onChange`, `onEnd`), a command state, panel or window binding that reads `plugin.player.state` or another state, an open window, a mounted panel, an OSD panel, a timeline layer, a filter chain, a stream or subtitle track it added, a next/previous provider, a sleep lease, a window setting, a job or snapshot in progress. Reading `plugin.player.state` inside a handler does not: the SDK reads the state again when the plugin starts.
|
|
367
|
+
- **What does not keep it running**: work the player does not see. A `fetch`, a socket, a `setTimeout` or `setInterval`, or a promise the plugin awaits is lost when an idle plugin is stopped, as in Chrome. Set the command's `busy` while its work runs (a busy command keeps the plugin running), or keep what it needs in session storage and pick it up in `activate`.
|
|
368
|
+
|
|
369
|
+
`TestHost.listening` lists the events a plugin really listens to; empty means listeners do not keep it running.
|
|
370
|
+
|
|
361
371
|
#### The data directory
|
|
362
372
|
|
|
363
373
|
plugin.dataDirectory is a writable directory for the plugin's own files (settings, caches, remembered folders). It needs no capability; the player creates it before it starts the plugin.
|
|
@@ -10735,6 +10745,10 @@ interface PluginInfo {
|
|
|
10735
10745
|
* The manifest's display texts in the interface language, translated with that package's catalog; absent from players before 0.4.3 (read `manifest.json` there).
|
|
10736
10746
|
*/
|
|
10737
10747
|
display?: PluginDisplay;
|
|
10748
|
+
/**
|
|
10749
|
+
* For `state: "running"`: `"running"` while its process runs, `"idle"` while the player stopped it because nothing used it (lazy activation: it starts again on its next use, its menu items, keys, settings pages, resolvers and drop targets stay). Absent in other states and from players before 0.5.18. A change between the two alone does not fire Packages.onChange.
|
|
10750
|
+
*/
|
|
10751
|
+
process?: "running" | "idle";
|
|
10738
10752
|
}
|
|
10739
10753
|
```
|
|
10740
10754
|
|
|
@@ -12796,6 +12810,10 @@ class TestHost implements Transport {
|
|
|
12796
12810
|
readonly calls: Call[];
|
|
12797
12811
|
/** The host events the plugin subscribed to (needs `events` etc.). */
|
|
12798
12812
|
readonly subscriptions: Set<string>;
|
|
12813
|
+
/**
|
|
12814
|
+
* The subscribed events the plugin really listens to: an `onChange` and the like, or a binding that reads the state. The SDK's own copies of the state (what keeps `plugin.player.state` current) are left out. Empty means the player may stop the plugin while nothing uses it (unless it holds something else: a window, a panel, a filter…).
|
|
12815
|
+
*/
|
|
12816
|
+
readonly listening: Set<string>;
|
|
12799
12817
|
/**
|
|
12800
12818
|
* The player's source facts (phase, position, duration, paused, rate, audio tracks, loop, ...). Wire-level shape; change it with TestHost.setSource, which also broadcasts it.
|
|
12801
12819
|
*/
|
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.38: 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. 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: L2646–2716.
|
|
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
|
|
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: L2717–2777.
|
|
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:
|
|
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: L2087–2275; TestHost: L12758.
|
|
84
84
|
|
|
85
85
|
**Command line**
|
|
86
86
|
|
|
@@ -108,39 +108,39 @@ 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 L3780.
|
|
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–237
|
|
117
|
-
- Concepts: How a plugin runs L238–
|
|
118
|
-
- How-to: Control playback
|
|
119
|
-
- Guidelines: Interface guidelines
|
|
120
|
-
- Reference: Manifest
|
|
121
|
-
- [API: @vestra-player/sdk](https://vestrasys.github.io/vestra-player-sdk/modules/_vestra-player_sdk.html):
|
|
122
|
-
- [API: @vestra-player/sdk/testing](https://vestrasys.github.io/vestra-player-sdk/modules/_vestra-player_sdk_testing.html):
|
|
117
|
+
- Concepts: How a plugin runs L238–418; Commands, menus and key bindings L419–530; Playback state, controls and sources L531–676; Video and audio processing L677–763; Windows, panels, settings pages, dialogs and the OSD L764–1052
|
|
118
|
+
- How-to: Control playback L1053–1181; Store a plugin's data L1182–1249; Load subtitles from a plugin L1250–1328; Keep a slider responsive while dragging L1329–1503; Remember a window's state between runs L1504–1633; Offer a GPU rendering switch L1634–1754; Translate a plugin L1755–1938; Migrate a plugin to English source text L1939–2086; Test a plugin L2087–2275; Publish a plugin L2276–2364; Migrating off package rollback (1.5.16, removed in 1.5.17) L2365–2440
|
|
119
|
+
- Guidelines: Interface guidelines L2441–2548; Menu guidelines L2549–2645
|
|
120
|
+
- Reference: Manifest L2646–2716; Capabilities L2717–2777; Limits L2778–2907; Commands, menus and bindings L2908–3391; Platforms L3392–3422; Theme L3423–3644; Command line L3645–3702
|
|
121
|
+
- [API: @vestra-player/sdk](https://vestrasys.github.io/vestra-player-sdk/modules/_vestra-player_sdk.html): L3703–12442
|
|
122
|
+
- [API: @vestra-player/sdk/testing](https://vestrasys.github.io/vestra-player-sdk/modules/_vestra-player_sdk_testing.html): L12443–15162
|
|
123
123
|
|
|
124
124
|
## API
|
|
125
125
|
|
|
126
126
|
Where each `plugin.<module>` is declared:
|
|
127
127
|
|
|
128
|
-
- Plugin
|
|
129
|
-
- Commands
|
|
128
|
+
- Plugin L4106, Scope L3855, PluginError L3780, Disposable L3718
|
|
129
|
+
- Commands L4503; Windows L5440; Panels L6886; Dialogs L7554; Player L8295; video: VideoFilter L9175, VideoView L9275, RenderOptions L9362, VideoUpscaling L9558; audio: AudioFilter L9220; Subtitles L9754; media: MediaCapture L10158; Resolvers L10388; Packages L10790; Osd L9040; Host L8882; Clipboard L11118; Power L11205; Tools L11264; Storage L11416; System L11615; Secrets L11695; window: MainWindow L7203; App L7383; Rendering L11958; Bindings L11018; Ui L4792; Timeline L7115; Drops L6133; Appearance L6214; I18n L6447; Settings L6730.
|
|
130
130
|
|
|
131
131
|
## Examples
|
|
132
132
|
|
|
133
133
|
Tested plugins in [examples/](https://github.com/vestrasys/vestra-player-sdk/tree/main/examples):
|
|
134
134
|
|
|
135
|
-
- hello: The README Quick start: one menu command and an on-screen message.
|
|
136
|
-
- blur: A tool window with one slider that blurs the playing video.
|
|
137
|
-
- signal-generator: Generates its own video and audio: colour bars and a 1 kHz tone.
|
|
138
|
-
- bookmark: A tool window bound to the playback state: remember the position and jump back to it.
|
|
139
|
-
- panel-menu: A sidebar panel with its own right-click menu.
|
|
140
|
-
- menus: Radio menu items, separator groups and a menu item whose title changes.
|
|
141
|
-
- theme-widgets: A tool window drawn with the player's theme and widgets.
|
|
142
|
-
- markdown: A window that shows a Markdown document.
|
|
143
|
-
- i18n: A plugin in the player's language: English source text, a Chinese translation.
|
|
135
|
+
- hello: The README Quick start: one menu command and an on-screen message. L15597–15686
|
|
136
|
+
- blur: A tool window with one slider that blurs the playing video. L15687–15948
|
|
137
|
+
- signal-generator: Generates its own video and audio: colour bars and a 1 kHz tone. L15949–16281
|
|
138
|
+
- bookmark: A tool window bound to the playback state: remember the position and jump back to it. L16282–16468
|
|
139
|
+
- panel-menu: A sidebar panel with its own right-click menu. L16469–16677
|
|
140
|
+
- menus: Radio menu items, separator groups and a menu item whose title changes. L16678–16837
|
|
141
|
+
- theme-widgets: A tool window drawn with the player's theme and widgets. L16838–17001
|
|
142
|
+
- markdown: A window that shows a Markdown document. L17002–17198
|
|
143
|
+
- i18n: A plugin in the player's language: English source text, a Chinese translation. L17199–17366
|
|
144
144
|
|
|
145
145
|
## Optional
|
|
146
146
|
|