@vestra-player/sdk 1.5.38 → 1.5.40
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/bin/vestra-plugin.mjs +5 -1
- package/dist/api-requests.js +1 -0
- package/dist/commands.d.ts +12 -2
- package/dist/commands.js +4 -2
- package/dist/host-capabilities.js +1 -0
- package/dist/player-runtime.d.ts +1 -0
- package/dist/player-runtime.js +1 -0
- package/dist/plugin/commands.js +41 -8
- package/dist/plugin/runtime.d.ts +31 -1
- package/dist/plugin/runtime.js +154 -12
- package/dist/plugin/scope.d.ts +2 -0
- package/dist/plugin/scope.js +4 -0
- package/dist/plugin/shaders.d.ts +9 -0
- package/dist/plugin/shaders.js +160 -0
- package/dist/plugin/testing.d.ts +120 -4
- package/dist/plugin/testing.js +320 -7
- package/dist/plugin/types.d.ts +443 -30
- package/dist/plugin/ui.js +6 -4
- package/dist/plugin/when.d.ts +46 -0
- package/dist/plugin/when.js +106 -0
- package/dist/processing.d.ts +118 -4
- package/dist/processing.js +139 -0
- package/dist/wire.d.ts +11 -4
- package/dist/wire.js +18 -3
- package/llms-full.txt +532 -32
- package/llms.txt +20 -20
- package/package.json +1 -1
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The shader channel `plugin.video.shaders(id)` (SDK 1.5.39, player
|
|
3
|
+
* 0.5.20): this plugin's own mpv user shaders, joined by the player with
|
|
4
|
+
* every other plugin's in a stable order. Unlike `video.render()` any
|
|
5
|
+
* number of plugins hold one at a time; each handle sends its whole list
|
|
6
|
+
* (last write wins) and the player answers `applied`, `waiting` (no ready
|
|
7
|
+
* media), or a failure.
|
|
8
|
+
*/
|
|
9
|
+
import { isAbsolute } from "node:path";
|
|
10
|
+
import { failure, Latest, quiet } from "./runtime.js";
|
|
11
|
+
import { playerOf } from "./player.js";
|
|
12
|
+
import { hasControl, VIDEO_PROCESSING_LIMITS } from "../processing.js";
|
|
13
|
+
/** The handle id when `shaders()` is called without one. */
|
|
14
|
+
export const DEFAULT_SHADERS_ID = "shaders";
|
|
15
|
+
/** Why `paths` would be refused here (`null` when it may be sent). */
|
|
16
|
+
export function shaderListProblem(paths) {
|
|
17
|
+
if (!Array.isArray(paths) || paths.some((p) => typeof p !== "string"))
|
|
18
|
+
return "shaders must be an array of paths";
|
|
19
|
+
if (paths.length > VIDEO_PROCESSING_LIMITS.shaders)
|
|
20
|
+
return `at most ${VIDEO_PROCESSING_LIMITS.shaders} shaders across all plugins`;
|
|
21
|
+
const relative = paths.find((path) => !isAbsolute(path));
|
|
22
|
+
if (relative !== undefined)
|
|
23
|
+
return `shader path must be absolute: ${relative}`;
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
/** Why a handle id would be refused (`null` when valid). */
|
|
27
|
+
export function shaderIdProblem(id) {
|
|
28
|
+
return typeof id === "string" &&
|
|
29
|
+
id.length > 0 &&
|
|
30
|
+
Buffer.byteLength(id) <= VIDEO_PROCESSING_LIMITS.shaderIdBytes &&
|
|
31
|
+
!hasControl(id)
|
|
32
|
+
? null
|
|
33
|
+
: `shader handle id must be 1 to ${VIDEO_PROCESSING_LIMITS.shaderIdBytes} bytes without control characters`;
|
|
34
|
+
}
|
|
35
|
+
class ShaderHandle {
|
|
36
|
+
#scope;
|
|
37
|
+
#id;
|
|
38
|
+
#desired = [];
|
|
39
|
+
/** Something non-empty was sent: `dispose` withdraws it. */
|
|
40
|
+
#sent = false;
|
|
41
|
+
#status = { state: "waiting" };
|
|
42
|
+
#disposed = false;
|
|
43
|
+
#source = "";
|
|
44
|
+
#listeners = new Set();
|
|
45
|
+
#latest;
|
|
46
|
+
#watch;
|
|
47
|
+
#pushed;
|
|
48
|
+
constructor(scope, id) {
|
|
49
|
+
this.#scope = scope;
|
|
50
|
+
this.#id = id;
|
|
51
|
+
this.#latest = new Latest(async (paths) => {
|
|
52
|
+
if (paths.length > 0)
|
|
53
|
+
this.#sent = true;
|
|
54
|
+
const { state } = await scope.runtime.host.video.shadersSet(id, paths);
|
|
55
|
+
if (this.#desired === paths)
|
|
56
|
+
this.#report({ state });
|
|
57
|
+
});
|
|
58
|
+
// `waiting` becomes `applied` when media becomes ready: ask again.
|
|
59
|
+
this.#watch = playerOf(scope.runtime).watch((raw) => {
|
|
60
|
+
const source = `${raw.stamp.generation}:${raw.phase}`;
|
|
61
|
+
if (source === this.#source)
|
|
62
|
+
return;
|
|
63
|
+
const first = this.#source === "";
|
|
64
|
+
this.#source = source;
|
|
65
|
+
if (!first && this.#status.state === "waiting" && this.#sent)
|
|
66
|
+
this.#send();
|
|
67
|
+
});
|
|
68
|
+
this.#pushed = scope.runtime.raw("video.shaders.status", (data) => {
|
|
69
|
+
const { id: target, error } = data;
|
|
70
|
+
if (this.#disposed || target !== this.#id || !error)
|
|
71
|
+
return;
|
|
72
|
+
this.#fail(failure(error.code ?? "failed", error.message));
|
|
73
|
+
});
|
|
74
|
+
scope.add(this);
|
|
75
|
+
}
|
|
76
|
+
get status() {
|
|
77
|
+
return this.#status;
|
|
78
|
+
}
|
|
79
|
+
onStatus(listener) {
|
|
80
|
+
this.#listeners.add(listener);
|
|
81
|
+
return this.#scope.onDispose(() => {
|
|
82
|
+
this.#listeners.delete(listener);
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
set(paths) {
|
|
86
|
+
if (this.#disposed)
|
|
87
|
+
return;
|
|
88
|
+
const problem = shaderListProblem(paths);
|
|
89
|
+
if (problem) {
|
|
90
|
+
// Refused at once; what the player holds stays.
|
|
91
|
+
this.#fail(failure(problem.startsWith("at most") ? "limit" : "invalid", problem));
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
this.#desired = [...paths];
|
|
95
|
+
this.#send();
|
|
96
|
+
}
|
|
97
|
+
clear() {
|
|
98
|
+
this.set([]);
|
|
99
|
+
}
|
|
100
|
+
#send() {
|
|
101
|
+
const paths = this.#desired;
|
|
102
|
+
this.#report({ state: "applying" });
|
|
103
|
+
quiet(this.#latest.push(paths).catch((error) => {
|
|
104
|
+
if (this.#desired === paths && !this.#disposed)
|
|
105
|
+
this.#fail(error);
|
|
106
|
+
}));
|
|
107
|
+
}
|
|
108
|
+
#fail(error) {
|
|
109
|
+
this.#report({ state: "failed", error });
|
|
110
|
+
this.#scope.report(error);
|
|
111
|
+
}
|
|
112
|
+
#report(status) {
|
|
113
|
+
if (JSON.stringify(status) === JSON.stringify(this.#status) &&
|
|
114
|
+
status.state !== "failed")
|
|
115
|
+
return;
|
|
116
|
+
this.#status = status;
|
|
117
|
+
for (const listener of this.#listeners)
|
|
118
|
+
this.#scope.run(listener, status);
|
|
119
|
+
this.#scope.update();
|
|
120
|
+
}
|
|
121
|
+
async dispose() {
|
|
122
|
+
if (this.#disposed)
|
|
123
|
+
return;
|
|
124
|
+
this.#disposed = true;
|
|
125
|
+
this.#scope.forget(this);
|
|
126
|
+
this.#latest.stop();
|
|
127
|
+
await this.#watch.dispose();
|
|
128
|
+
await this.#pushed.dispose();
|
|
129
|
+
this.#scope.runtime.services.delete(`video.shaders:${this.#id}`);
|
|
130
|
+
// Withdraw this handle's shaders; refused (and moot) once stopped: the
|
|
131
|
+
// player removes a stopped plugin's shaders itself.
|
|
132
|
+
if (this.#sent)
|
|
133
|
+
await this.#scope.runtime.host.video
|
|
134
|
+
.shadersSet(this.#id, [])
|
|
135
|
+
.catch(() => { });
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
export function videoShaders(scope, id) {
|
|
139
|
+
const runtime = scope.runtime;
|
|
140
|
+
if (!runtime.can("processing.video"))
|
|
141
|
+
throw failure("capability", "processing.video must be declared in manifest");
|
|
142
|
+
const name = id ?? DEFAULT_SHADERS_ID;
|
|
143
|
+
const problem = shaderIdProblem(name);
|
|
144
|
+
if (problem)
|
|
145
|
+
throw failure("invalid", problem);
|
|
146
|
+
const service = `video.shaders:${name}`;
|
|
147
|
+
if (runtime.services.has(service))
|
|
148
|
+
throw new Error(`a shader handle "${name}" exists; reuse it`);
|
|
149
|
+
runtime.services.set(service, true);
|
|
150
|
+
const handle = new ShaderHandle(scope, name);
|
|
151
|
+
return {
|
|
152
|
+
set: (paths) => handle.set(paths),
|
|
153
|
+
clear: () => handle.clear(),
|
|
154
|
+
get status() {
|
|
155
|
+
return handle.status;
|
|
156
|
+
},
|
|
157
|
+
onStatus: (listener) => handle.onStatus(listener),
|
|
158
|
+
dispose: () => handle.dispose(),
|
|
159
|
+
};
|
|
160
|
+
}
|
package/dist/plugin/testing.d.ts
CHANGED
|
@@ -15,6 +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 { VIDEO_PROCESSING_LIMITS } from "../processing.ts";
|
|
18
19
|
import type { RenderingMode, RenderingPreference, RenderingReason, RenderSettings, UpscalingReason, ViewSettings } from "./types.ts";
|
|
19
20
|
import type { MediaJob } from "../media.ts";
|
|
20
21
|
import type { LanguagePreference, Locale, Plugin, TemplateArgs, TextValue, ThemeId } from "./types.ts";
|
|
@@ -41,6 +42,8 @@ export { SECRET_LIMITS };
|
|
|
41
42
|
export { SESSION_STORAGE_LIMITS };
|
|
42
43
|
/** The player's limits on loaded subtitle files, which the test host enforces as well. */
|
|
43
44
|
export { SUBTITLE_LIMITS };
|
|
45
|
+
/** The player's bounds on convolution, lut3d and shader-channel requests, which the test host enforces as well. */
|
|
46
|
+
export { VIDEO_PROCESSING_LIMITS };
|
|
44
47
|
/** The names bindings are made of. */
|
|
45
48
|
export { BINDING_INPUTS };
|
|
46
49
|
/** The colours of the player's palettes, by `Theme` token. */
|
|
@@ -268,7 +271,26 @@ export interface FakeCommand {
|
|
|
268
271
|
*/
|
|
269
272
|
reason?: string | null;
|
|
270
273
|
};
|
|
271
|
-
/**
|
|
274
|
+
/**
|
|
275
|
+
* The conditions the plugin sent for `enabled` and `checked`
|
|
276
|
+
* (`plugin.when`, wire form), `undefined` without. Like the player, the
|
|
277
|
+
* host evaluates them in {@link FakeCommand.state} against `source`,
|
|
278
|
+
* `mainWindow.fullscreen`, `appearance` and `language`.
|
|
279
|
+
*
|
|
280
|
+
* @since 1.5.40
|
|
281
|
+
*/
|
|
282
|
+
when?: {
|
|
283
|
+
/** The condition of `enabled`. */
|
|
284
|
+
enabled?: Json;
|
|
285
|
+
/** The condition of `checked`. */
|
|
286
|
+
checked?: Json;
|
|
287
|
+
};
|
|
288
|
+
/**
|
|
289
|
+
* The command state as the player shows it: what the plugin last sent
|
|
290
|
+
* (after the update pass), with the fields given as conditions
|
|
291
|
+
* (`plugin.when`) evaluated now. With an `enabled` condition, `reason`
|
|
292
|
+
* is `null` while the command is enabled and not busy.
|
|
293
|
+
*/
|
|
272
294
|
state: {
|
|
273
295
|
/** `false` when the plugin disabled the command. */
|
|
274
296
|
enabled: boolean;
|
|
@@ -545,6 +567,16 @@ export declare class TestHost implements Transport {
|
|
|
545
567
|
* @since 1.5.38
|
|
546
568
|
*/
|
|
547
569
|
readonly listening: Set<string>;
|
|
570
|
+
/**
|
|
571
|
+
* Why the plugin listens to each event of {@link TestHost.listening}:
|
|
572
|
+
* every listener and every binding that reads its state, as the
|
|
573
|
+
* player's log says them (`a listener added at main.ts:12`,
|
|
574
|
+
* `command "Save" enabled is a function that reads plugin.player.state
|
|
575
|
+
* (main.ts:40) — use plugin.when`), in the order they came.
|
|
576
|
+
*
|
|
577
|
+
* @since 1.5.40
|
|
578
|
+
*/
|
|
579
|
+
readonly listeningWhy: Map<string, string[]>;
|
|
548
580
|
/**
|
|
549
581
|
* The player's source facts (phase, position, duration, paused, rate,
|
|
550
582
|
* audio tracks, loop, ...). Wire-level shape; change it with
|
|
@@ -1341,6 +1373,64 @@ export declare class TestHost implements Transport {
|
|
|
1341
1373
|
* @since 1.0.0
|
|
1342
1374
|
*/
|
|
1343
1375
|
failVideoRender(code: "failed" | "unsupported", message: string): void;
|
|
1376
|
+
/**
|
|
1377
|
+
* The shader channel (`plugin.video.shaders(id)`, SDK 1.5.39): the lists
|
|
1378
|
+
* the player holds for this plugin's handles by id (the real paths, after
|
|
1379
|
+
* following links), and how many shaders other plugins hold, which count
|
|
1380
|
+
* against {@link VIDEO_PROCESSING_LIMITS}`.shaders` (`limit`). Files must
|
|
1381
|
+
* be under {@link TestHost.fileRoots}; under software rendering
|
|
1382
|
+
* (`videoRender.gpu = false`) a non-empty list fails with `unsupported`.
|
|
1383
|
+
*
|
|
1384
|
+
* @since 1.5.39
|
|
1385
|
+
*/
|
|
1386
|
+
readonly videoShaders: {
|
|
1387
|
+
/** This plugin's lists by handle id; a cleared handle has none. */
|
|
1388
|
+
handles: Map<string, string[]>;
|
|
1389
|
+
/** Shaders other plugins hold (0–16). */
|
|
1390
|
+
others: number;
|
|
1391
|
+
};
|
|
1392
|
+
/**
|
|
1393
|
+
* Where the plugin's own files may be: its package directory and its
|
|
1394
|
+
* data directory ({@link startPlugin} sets both when the list is still
|
|
1395
|
+
* empty). `lut3d` files and shader-channel files outside them fail with
|
|
1396
|
+
* `invalid`, as in the player.
|
|
1397
|
+
*
|
|
1398
|
+
* @since 1.5.39
|
|
1399
|
+
*/
|
|
1400
|
+
fileRoots: string[];
|
|
1401
|
+
/**
|
|
1402
|
+
* What the player pushes to a shader handle when mpv cannot compile a
|
|
1403
|
+
* shader (code `failed`; the player cannot tell whose shader failed, so
|
|
1404
|
+
* every handle with shaders hears it) or the player switches to software
|
|
1405
|
+
* rendering (`unsupported`; use {@link TestHost.failVideoRender}, which
|
|
1406
|
+
* also tells the handles). Pushed only to a handle that holds shaders.
|
|
1407
|
+
*
|
|
1408
|
+
* @capability None.
|
|
1409
|
+
*
|
|
1410
|
+
* @example Make the plugin's shader fail to compile
|
|
1411
|
+
* The plugin's shader handle reports `failed`, as when mpv rejects a
|
|
1412
|
+
* shader. The host pushes the failure only to a handle that holds
|
|
1413
|
+
* shaders, which its accepted `set` made it; the SDK updates the
|
|
1414
|
+
* handle's `status` when the push arrives, so the test settles first.
|
|
1415
|
+
* ```ts
|
|
1416
|
+
* import assert from "node:assert/strict";
|
|
1417
|
+
* import { join } from "node:path";
|
|
1418
|
+
* import { writeFileSync } from "node:fs";
|
|
1419
|
+
* declare const host: TestHost;
|
|
1420
|
+
* // ---cut---
|
|
1421
|
+
* const file = join(plugin.dataDirectory, "sharpen.glsl");
|
|
1422
|
+
* writeFileSync(file, "//!HOOK MAIN\n");
|
|
1423
|
+
* const shaders = plugin.video.shaders("sharpen");
|
|
1424
|
+
* shaders.set([file]); // the handle now holds a shader
|
|
1425
|
+
* await host.settle();
|
|
1426
|
+
* host.failVideoShaders("sharpen", "shader compile failed: ERROR: 0:3: ...");
|
|
1427
|
+
* await host.settle();
|
|
1428
|
+
* assert.equal(shaders.status.state, "failed");
|
|
1429
|
+
* ```
|
|
1430
|
+
*
|
|
1431
|
+
* @since 1.5.39
|
|
1432
|
+
*/
|
|
1433
|
+
failVideoShaders(id: string, message: string): void;
|
|
1344
1434
|
/**
|
|
1345
1435
|
* The player's processing (filter) state. Wire-level; may change in minor
|
|
1346
1436
|
* versions. `resources` holds each `video.filter` / `audio.filter` chain the
|
|
@@ -1681,9 +1771,9 @@ export declare class TestHost implements Transport {
|
|
|
1681
1771
|
* import activate from "../main.ts";
|
|
1682
1772
|
*
|
|
1683
1773
|
* const host = new TestHost();
|
|
1684
|
-
* host.videoRender.gpu = false; // software draws now (
|
|
1774
|
+
* host.videoRender.gpu = false; // software draws now (Linux)
|
|
1685
1775
|
* host.rendering.startedWith = "software";
|
|
1686
|
-
* host.rendering.reason = { code: "no-gpu-backend", message: "
|
|
1776
|
+
* host.rendering.reason = { code: "no-gpu-backend", message: "this player has no GPU renderer on Linux yet" };
|
|
1687
1777
|
* host.rendering.gpu = { available: false, reason: host.rendering.reason };
|
|
1688
1778
|
* const { stop } = await startPlugin(activate, {
|
|
1689
1779
|
* directory: new URL("..", import.meta.url),
|
|
@@ -1691,7 +1781,7 @@ export declare class TestHost implements Transport {
|
|
|
1691
1781
|
* });
|
|
1692
1782
|
* const item = host.command("gpu-rendering");
|
|
1693
1783
|
* assert.equal(item.state.enabled, false);
|
|
1694
|
-
* assert.equal(item.state.reason, "
|
|
1784
|
+
* assert.equal(item.state.reason, "this player has no GPU renderer on Linux yet");
|
|
1695
1785
|
* await stop();
|
|
1696
1786
|
* ```
|
|
1697
1787
|
*
|
|
@@ -2527,6 +2617,32 @@ export declare class TestHost implements Transport {
|
|
|
2527
2617
|
* @since 1.0.0
|
|
2528
2618
|
*/
|
|
2529
2619
|
close(): void;
|
|
2620
|
+
/**
|
|
2621
|
+
* Why the player would keep this plugin running while nothing uses it
|
|
2622
|
+
* (lazy activation, player 0.5.21), one line per reason, as the player's
|
|
2623
|
+
* log says it: an event it listens to that is not one the player wakes
|
|
2624
|
+
* it for (with what listens and where it was added), its mounted panel,
|
|
2625
|
+
* its open windows. Empty: the player may stop it while idle and start
|
|
2626
|
+
* it again on its next use. Filter chains, streams, jobs and the like
|
|
2627
|
+
* (which also keep a plugin running) are not listed.
|
|
2628
|
+
*
|
|
2629
|
+
* @capability None.
|
|
2630
|
+
* @since 1.5.40
|
|
2631
|
+
*
|
|
2632
|
+
* @example Check that a plugin can be stopped while idle
|
|
2633
|
+
* The test fails if the plugin keeps running for no reason after it
|
|
2634
|
+
* starts, and names the reason (a command whose `enabled` reads
|
|
2635
|
+
* `plugin.player.state`, say: give it a `plugin.when` condition). The
|
|
2636
|
+
* player stops such a plugin while nothing uses it and starts it again
|
|
2637
|
+
* on its next use, so it costs no process meanwhile.
|
|
2638
|
+
* ```ts
|
|
2639
|
+
* import assert from "node:assert/strict";
|
|
2640
|
+
* declare const host: TestHost;
|
|
2641
|
+
* // ---cut---
|
|
2642
|
+
* assert.deepEqual(host.keepsRunning, []);
|
|
2643
|
+
* ```
|
|
2644
|
+
*/
|
|
2645
|
+
get keepsRunning(): string[];
|
|
2530
2646
|
}
|
|
2531
2647
|
/**
|
|
2532
2648
|
* Activate a plugin against a test host, the way the player's runner does:
|