@vestra-player/sdk 1.5.36 → 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 +18 -0
- package/README.md +1 -0
- package/dist/api-requests.js +2 -0
- package/dist/host-capabilities.js +11 -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/scope.d.ts +1 -0
- package/dist/plugin/scope.js +2 -1
- package/dist/plugin/services.d.ts +7 -1
- package/dist/plugin/services.js +26 -2
- package/dist/plugin/subtitles.js +14 -2
- package/dist/plugin/testing.d.ts +72 -2
- package/dist/plugin/testing.js +119 -3
- package/dist/plugin/timeline.d.ts +1 -1
- package/dist/plugin/timeline.js +10 -2
- package/dist/plugin/types.d.ts +286 -4
- 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 +29 -2
- package/dist/wire.js +19 -3
- package/llms-full.txt +341 -6
- package/llms.txt +23 -22
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
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
|
+
|
|
12
|
+
## 1.5.37
|
|
13
|
+
|
|
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.
|
|
15
|
+
|
|
16
|
+
- **`plugin.video.upscaling`** (player 0.5.16): the GPU's video upscaler, NVIDIA RTX Video Super Resolution on Windows. `state()` reports whether it works now (`available`) and why not (`reason`, `{ code, message }` with the stable codes of `UpscalingReasonCode`: `platform`, `no-gpu`, `driver`, `gpu-unsupported`, `rendering`, `adapter`, `decoding`, `unchecked`; a newer player may add codes), whether this plugin turned it on (`enabled`), and whether the player asked the GPU to upscale now (`requested`, with the scale `factor`). `set(enabled)` turns this plugin's request on or off and resolves with the new state; `true` rejects with `unsupported` where upscaling cannot work in this run: every reason except `decoding` (it can pass with the next file) and except `rendering` while GPU rendering is still starting, which are accepted and kept; `false` is always accepted and answered at once without the GPU check (before any check: `available: false` with `unchecked`); `invalid` for a non-boolean. The player checks the GPU once, at the first `state()` or `set(true)` (it wakes a sleeping discrete GPU, so the examples ask on the first pick of the item, not while the plugin starts), puts the upscaler last in the video filter chain scaled to the area the video is shown in (only above 1×), follows size changes, and takes it out when the last plugin turns it off or stops. There is no "upscaling active" signal: on Windows the user must also turn on Super Resolution in the NVIDIA app, which no program can read. New types `VideoUpscaling`, `UpscalingState`, `UpscalingReason`, `UpscalingReasonCode`.
|
|
17
|
+
- **New capability `video.upscaling`** ("Upscale the video with the GPU's video upscaler", not highlighted in the store: it changes only how the picture is scaled while the plugin runs and touches no data), last in `CAPABILITIES`; both requests need it and reject with `capability` without it. The player's capability table copy, `CAPABILITY_INFO`, `vestra-plugin check` and the editor plugin know it.
|
|
18
|
+
- **`TestHost.upscaling`** models the player: preset `reason` (`null`: it works) and `factor`, read `enabled` and `checked` (whether the plugin made the player check the GPU), set `renderingStarting`; `set(true)` refuses with `unsupported` unless the reason is `decoding`, or `rendering` with `renderingStarting`; `set(false)` checks nothing (`unchecked` before any check).
|
|
19
|
+
- _Video and audio processing_ and _Platforms_ describe the upscaler.
|
|
20
|
+
|
|
3
21
|
## 1.5.36
|
|
4
22
|
|
|
5
23
|
**Who must change: nobody.** No API changes, `apiVersion` stays `vestra/1`, no player version needed.
|
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.
|
package/dist/api-requests.js
CHANGED
|
@@ -143,4 +143,6 @@ export const API_REQUESTS = Object.freeze({
|
|
|
143
143
|
"Rendering.state": [{ method: "rendering.state" }],
|
|
144
144
|
"Rendering.setPreference": [{ method: "rendering.setPreference" }],
|
|
145
145
|
"Rendering.restartPlayer": [{ method: "rendering.restart" }],
|
|
146
|
+
"VideoUpscaling.state": [{ method: "video.upscaling.state" }],
|
|
147
|
+
"VideoUpscaling.set": [{ method: "video.upscaling.set" }],
|
|
146
148
|
});
|
|
@@ -354,6 +354,17 @@ export const HOST_CAPABILITIES = {
|
|
|
354
354
|
residency: "none",
|
|
355
355
|
risky: true,
|
|
356
356
|
},
|
|
357
|
+
{
|
|
358
|
+
label: "Upscale the video with the GPU's video upscaler",
|
|
359
|
+
methods: [
|
|
360
|
+
{ name: "video.upscaling.state", needs: "video.upscaling" },
|
|
361
|
+
{ name: "video.upscaling.set", needs: "video.upscaling" },
|
|
362
|
+
],
|
|
363
|
+
name: "video.upscaling",
|
|
364
|
+
requires: [],
|
|
365
|
+
residency: "live",
|
|
366
|
+
risky: false,
|
|
367
|
+
},
|
|
357
368
|
],
|
|
358
369
|
uncapable: [
|
|
359
370
|
{ name: "packages.list", needs: null },
|
|
@@ -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/scope.d.ts
CHANGED
|
@@ -15,6 +15,7 @@ export declare class ScopeImpl implements Plugin {
|
|
|
15
15
|
readonly video: {
|
|
16
16
|
view: () => import("./types.ts").VideoView;
|
|
17
17
|
render: () => import("./types.ts").RenderOptions;
|
|
18
|
+
upscaling: import("./types.ts").VideoUpscaling;
|
|
18
19
|
filter(id: string): import("./types.ts").VideoFilter;
|
|
19
20
|
};
|
|
20
21
|
readonly audio: {
|
package/dist/plugin/scope.js
CHANGED
|
@@ -17,7 +17,7 @@ import { packages } from "./packages.js";
|
|
|
17
17
|
import { osd } from "./osd.js";
|
|
18
18
|
import { mainWindow } from "./window.js";
|
|
19
19
|
import { bindingsApi } from "./bindings.js";
|
|
20
|
-
import { clipboard, host, power, rendering, secrets, storage, system, tools, } from "./services.js";
|
|
20
|
+
import { clipboard, host, power, rendering, secrets, storage, system, tools, videoUpscaling, } from "./services.js";
|
|
21
21
|
export class ScopeImpl {
|
|
22
22
|
runtime;
|
|
23
23
|
#parent;
|
|
@@ -49,6 +49,7 @@ export class ScopeImpl {
|
|
|
49
49
|
...videoFilters(this),
|
|
50
50
|
view: () => videoView(this),
|
|
51
51
|
render: () => videoRender(this),
|
|
52
|
+
upscaling: videoUpscaling(this),
|
|
52
53
|
};
|
|
53
54
|
audio = audioFilters(this);
|
|
54
55
|
subtitles = subtitles(this);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ScopeImpl } from "./scope.ts";
|
|
2
|
-
import type { Clipboard, Host, Power, Rendering, Secrets, Storage, System, Tools } from "./types.ts";
|
|
2
|
+
import type { Clipboard, Host, Power, Rendering, Secrets, Storage, System, Tools, VideoUpscaling } from "./types.ts";
|
|
3
3
|
export declare const clipboard: (scope: ScopeImpl) => Clipboard;
|
|
4
4
|
export declare const host: (scope: ScopeImpl) => Host;
|
|
5
5
|
export declare const power: (scope: ScopeImpl) => Power;
|
|
@@ -113,3 +113,9 @@ export declare const SECRET_LIMITS: Readonly<{
|
|
|
113
113
|
}>;
|
|
114
114
|
export declare const secrets: (scope: ScopeImpl) => Secrets;
|
|
115
115
|
export declare const rendering: (scope: ScopeImpl) => Rendering;
|
|
116
|
+
/**
|
|
117
|
+
* `plugin.video.upscaling` (player 0.5.16): both requests need
|
|
118
|
+
* `video.upscaling`; the player answers once its GPU check ran (the first
|
|
119
|
+
* call starts it).
|
|
120
|
+
*/
|
|
121
|
+
export declare const videoUpscaling: (scope: ScopeImpl) => VideoUpscaling;
|
package/dist/plugin/services.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Shared services: the clipboard, sleep prevention, showing and opening files
|
|
3
|
-
* and links, secrets, session storage
|
|
4
|
-
* host; external tools and settings files run in this
|
|
3
|
+
* and links, secrets, session storage, the rendering choice and the video
|
|
4
|
+
* upscaler go to the host; external tools and settings files run in this
|
|
5
|
+
* plugin's own process.
|
|
5
6
|
*/
|
|
6
7
|
import { existsSync, readFileSync, renameSync } from "node:fs";
|
|
7
8
|
import { mkdir, open, rename, rm, stat } from "node:fs/promises";
|
|
@@ -497,3 +498,26 @@ export const rendering = (scope) => {
|
|
|
497
498
|
},
|
|
498
499
|
};
|
|
499
500
|
};
|
|
501
|
+
/**
|
|
502
|
+
* `plugin.video.upscaling` (player 0.5.16): both requests need
|
|
503
|
+
* `video.upscaling`; the player answers once its GPU check ran (the first
|
|
504
|
+
* call starts it).
|
|
505
|
+
*/
|
|
506
|
+
export const videoUpscaling = (scope) => {
|
|
507
|
+
const declared = () => {
|
|
508
|
+
if (!scope.runtime.can("video.upscaling"))
|
|
509
|
+
throw failure("capability", "video.upscaling must be declared in manifest");
|
|
510
|
+
};
|
|
511
|
+
return {
|
|
512
|
+
async state() {
|
|
513
|
+
declared();
|
|
514
|
+
return (await scope.runtime.host.video.upscalingState());
|
|
515
|
+
},
|
|
516
|
+
async set(enabled) {
|
|
517
|
+
declared();
|
|
518
|
+
if (typeof enabled !== "boolean")
|
|
519
|
+
throw failure("invalid", "enabled must be true or false");
|
|
520
|
+
return (await scope.runtime.host.video.upscalingSet(enabled));
|
|
521
|
+
},
|
|
522
|
+
};
|
|
523
|
+
};
|
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
|
@@ -15,7 +15,7 @@ import { IMAGE_LIMITS, UI_VALUE_LIMITS } from "./values.ts";
|
|
|
15
15
|
import type { CommandDeclaration } from "../commands.ts";
|
|
16
16
|
import type { SourceState } from "../source.ts";
|
|
17
17
|
import type { SubtitleState } from "../subtitles.ts";
|
|
18
|
-
import type { RenderingMode, RenderingPreference, RenderingReason, RenderSettings, ViewSettings } from "./types.ts";
|
|
18
|
+
import type { RenderingMode, RenderingPreference, RenderingReason, RenderSettings, UpscalingReason, ViewSettings } from "./types.ts";
|
|
19
19
|
import type { MediaJob } from "../media.ts";
|
|
20
20
|
import type { LanguagePreference, Locale, Plugin, TemplateArgs, TextValue, ThemeId } from "./types.ts";
|
|
21
21
|
import { LOCALES, LIST_SEPARATORS, TEXT_LIMITS } from "../i18n.ts";
|
|
@@ -469,7 +469,8 @@ export interface MarkdownWire {
|
|
|
469
469
|
* {@link TestHost.app}, {@link TestHost.systemInfo},
|
|
470
470
|
* {@link TestHost.chapters}, {@link TestHost.audioDevices},
|
|
471
471
|
* {@link TestHost.secrets}, {@link TestHost.clipboard},
|
|
472
|
-
* {@link TestHost.tools}, {@link TestHost.rendering}
|
|
472
|
+
* {@link TestHost.tools}, {@link TestHost.rendering},
|
|
473
|
+
* {@link TestHost.upscaling}.
|
|
473
474
|
* - **Assert** on records: {@link TestHost.osd}, {@link TestHost.calls},
|
|
474
475
|
* {@link TestHost.sent}, {@link TestHost.conflicts},
|
|
475
476
|
* {@link TestHost.layout}, {@link TestHost.menus}, and many more.
|
|
@@ -534,6 +535,16 @@ export declare class TestHost implements Transport {
|
|
|
534
535
|
readonly calls: Call[];
|
|
535
536
|
/** The host events the plugin subscribed to (needs `events` etc.). */
|
|
536
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>;
|
|
537
548
|
/**
|
|
538
549
|
* The player's source facts (phase, position, duration, paused, rate,
|
|
539
550
|
* audio tracks, loop, ...). Wire-level shape; change it with
|
|
@@ -1703,6 +1714,65 @@ export declare class TestHost implements Transport {
|
|
|
1703
1714
|
/** Accepted `restartPlayer` calls. */
|
|
1704
1715
|
restarts: number;
|
|
1705
1716
|
};
|
|
1717
|
+
/**
|
|
1718
|
+
* The GPU's video upscaler behind `plugin.video.upscaling` (player
|
|
1719
|
+
* 0.5.16), answered as the player would:
|
|
1720
|
+
*
|
|
1721
|
+
* - `state()` and `set(true)` set `checked` (the player's one GPU check; a
|
|
1722
|
+
* plugin that should not wake the GPU while it starts can assert it is
|
|
1723
|
+
* still `false`) and report `available` from `reason` (`null`: it
|
|
1724
|
+
* works), this plugin's `enabled`, and `requested` with `factor` while
|
|
1725
|
+
* enabled, available and `factor` is above 1.
|
|
1726
|
+
* - `set(true)` refuses with `unsupported` and the reason's message while
|
|
1727
|
+
* `reason` is set, unless its code is `decoding`, or `rendering` with
|
|
1728
|
+
* `renderingStarting` (GPU rendering still starting): those can pass
|
|
1729
|
+
* during the run, so the player keeps the request. Nothing changes when
|
|
1730
|
+
* it refuses.
|
|
1731
|
+
* - `set(false)` always succeeds and does not check the GPU: before any
|
|
1732
|
+
* check it reports `available: false` with the reason `unchecked`.
|
|
1733
|
+
*
|
|
1734
|
+
* Inject other answers with `host.handle("video.upscaling.set", …)`. The
|
|
1735
|
+
* defaults are a player where upscaling works and a 1080p video shown
|
|
1736
|
+
* full screen on a 4K display (`factor` 2), whatever `systemInfo` says.
|
|
1737
|
+
*
|
|
1738
|
+
* @example Test the menu item on a computer without an RTX GPU
|
|
1739
|
+
* The plugin's upscaling item is greyed out with the player's reason after
|
|
1740
|
+
* the user first picks it. Starting the plugin checks nothing (`checked`
|
|
1741
|
+
* stays false, so no GPU wakes up); the first pick reads the state, and
|
|
1742
|
+
* `set(true)` would reject with `unsupported`, as in the player.
|
|
1743
|
+
* ```ts
|
|
1744
|
+
* import assert from "node:assert/strict";
|
|
1745
|
+
* import { startPlugin, TestHost } from "@vestra-player/sdk/testing";
|
|
1746
|
+
* import activate from "../main.ts";
|
|
1747
|
+
*
|
|
1748
|
+
* const host = new TestHost();
|
|
1749
|
+
* host.upscaling.reason = { code: "no-gpu", message: "no NVIDIA GPU" };
|
|
1750
|
+
* const { stop } = await startPlugin(activate, {
|
|
1751
|
+
* directory: new URL("..", import.meta.url),
|
|
1752
|
+
* host,
|
|
1753
|
+
* });
|
|
1754
|
+
* assert.equal(host.upscaling.checked, false); // starting woke no GPU
|
|
1755
|
+
* const item = host.command("upscaling");
|
|
1756
|
+
* await item.run(); // the first pick checks the GPU
|
|
1757
|
+
* assert.equal(item.state.enabled, false); // greyed out
|
|
1758
|
+
* assert.equal(item.state.reason, "no NVIDIA GPU"); // the player's message
|
|
1759
|
+
* await stop();
|
|
1760
|
+
* ```
|
|
1761
|
+
*
|
|
1762
|
+
* @since 1.5.37
|
|
1763
|
+
*/
|
|
1764
|
+
readonly upscaling: {
|
|
1765
|
+
/** Why upscaling does not work; `null` while it does. */
|
|
1766
|
+
reason: UpscalingReason | null;
|
|
1767
|
+
/** Whether this plugin has upscaling on (`set`); a test may preset it. */
|
|
1768
|
+
enabled: boolean;
|
|
1769
|
+
/** The factor the player would request; `null` or at most 1: the video needs no upscaling. */
|
|
1770
|
+
factor: number | null;
|
|
1771
|
+
/** Whether the plugin made the player check the GPU (`state`, `set(true)`). */
|
|
1772
|
+
checked: boolean;
|
|
1773
|
+
/** GPU rendering is still starting: a `rendering` reason then lets `set(true)` keep the request. */
|
|
1774
|
+
renderingStarting: boolean;
|
|
1775
|
+
};
|
|
1706
1776
|
/** What `player.chapters()` lists while media is ready. */
|
|
1707
1777
|
chapters: {
|
|
1708
1778
|
/** The chapter title, `null` without one. */
|