@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,106 @@
|
|
|
1
|
+
import { LOCALES } from "../i18n.js";
|
|
2
|
+
import { THEMES } from "./appearance.js";
|
|
3
|
+
const PHASES = [
|
|
4
|
+
"empty",
|
|
5
|
+
"loading",
|
|
6
|
+
"ready",
|
|
7
|
+
"ended",
|
|
8
|
+
"error",
|
|
9
|
+
];
|
|
10
|
+
const CONTROLS = [
|
|
11
|
+
"pause",
|
|
12
|
+
"seek",
|
|
13
|
+
"rate",
|
|
14
|
+
"restart",
|
|
15
|
+
"close",
|
|
16
|
+
"snapshot",
|
|
17
|
+
"next",
|
|
18
|
+
"previous",
|
|
19
|
+
];
|
|
20
|
+
class ConditionImpl {
|
|
21
|
+
#json;
|
|
22
|
+
constructor(json) {
|
|
23
|
+
this.#json = json;
|
|
24
|
+
Object.freeze(this);
|
|
25
|
+
}
|
|
26
|
+
and(...others) {
|
|
27
|
+
return new ConditionImpl({
|
|
28
|
+
all: [this.#json, ...others.map(conditionJson)],
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
or(...others) {
|
|
32
|
+
return new ConditionImpl({
|
|
33
|
+
any: [this.#json, ...others.map(conditionJson)],
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
not() {
|
|
37
|
+
return new ConditionImpl({ not: this.#json });
|
|
38
|
+
}
|
|
39
|
+
toJSON() {
|
|
40
|
+
return this.#json;
|
|
41
|
+
}
|
|
42
|
+
static json(condition) {
|
|
43
|
+
return condition.#json;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/** Whether `value` is a condition made by `plugin.when`. */
|
|
47
|
+
export const isCondition = (value) => value instanceof ConditionImpl;
|
|
48
|
+
/** A condition's wire form; throws for anything else. */
|
|
49
|
+
export function conditionJson(condition) {
|
|
50
|
+
if (!(condition instanceof ConditionImpl))
|
|
51
|
+
throw new TypeError("a condition comes from plugin.when");
|
|
52
|
+
return ConditionImpl.json(condition);
|
|
53
|
+
}
|
|
54
|
+
const make = (json) => new ConditionImpl(json);
|
|
55
|
+
/** The one `plugin.when` (conditions hold no plugin state). */
|
|
56
|
+
export const WHEN = Object.freeze({
|
|
57
|
+
open: make({ not: { phase: ["empty"] } }),
|
|
58
|
+
ready: make({ phase: ["ready"] }),
|
|
59
|
+
playing: make({ all: [{ phase: ["ready"] }, { paused: false }] }),
|
|
60
|
+
paused: make({ all: [{ phase: ["ready"] }, { paused: true }] }),
|
|
61
|
+
stream: make({ kind: "stream" }),
|
|
62
|
+
fullscreen: make({ fullscreen: true }),
|
|
63
|
+
phase(...phases) {
|
|
64
|
+
if (!phases.length || phases.some((phase) => !PHASES.includes(phase)))
|
|
65
|
+
throw new TypeError(`phase is one or more of ${PHASES.join(", ")}`);
|
|
66
|
+
return make({ phase: [...phases] });
|
|
67
|
+
},
|
|
68
|
+
can(control) {
|
|
69
|
+
if (!CONTROLS.includes(control))
|
|
70
|
+
throw new TypeError(`can is one of ${CONTROLS.join(", ")}`);
|
|
71
|
+
return make({ can: control });
|
|
72
|
+
},
|
|
73
|
+
theme(theme) {
|
|
74
|
+
if (!THEMES.includes(theme))
|
|
75
|
+
throw new TypeError(`theme is ${THEMES.map((t) => `"${t}"`).join(" or ")}`);
|
|
76
|
+
return make({ theme });
|
|
77
|
+
},
|
|
78
|
+
language(language) {
|
|
79
|
+
if (language !== "system" &&
|
|
80
|
+
!LOCALES.includes(language))
|
|
81
|
+
throw new TypeError(`language is "system" or one of ${LOCALES.map((l) => `"${l}"`).join(", ")}`);
|
|
82
|
+
return make({ language });
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
/** Evaluate a wire condition as the player does (for the test host). */
|
|
86
|
+
export function evaluate(json, facts) {
|
|
87
|
+
if ("phase" in json)
|
|
88
|
+
return json.phase.includes(facts.phase);
|
|
89
|
+
if ("paused" in json)
|
|
90
|
+
return facts.paused === json.paused;
|
|
91
|
+
if ("kind" in json)
|
|
92
|
+
return facts.kind === json.kind;
|
|
93
|
+
if ("can" in json)
|
|
94
|
+
return facts.can[json.can] === true;
|
|
95
|
+
if ("fullscreen" in json)
|
|
96
|
+
return facts.fullscreen === json.fullscreen;
|
|
97
|
+
if ("theme" in json)
|
|
98
|
+
return facts.theme === json.theme;
|
|
99
|
+
if ("language" in json)
|
|
100
|
+
return facts.language === json.language;
|
|
101
|
+
if ("all" in json)
|
|
102
|
+
return json.all.every((each) => evaluate(each, facts));
|
|
103
|
+
if ("any" in json)
|
|
104
|
+
return json.any.some((each) => evaluate(each, facts));
|
|
105
|
+
return !evaluate(json.not, facts);
|
|
106
|
+
}
|
package/dist/processing.d.ts
CHANGED
|
@@ -1,7 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Processing chains for the player's video and audio filters. Applied means
|
|
3
|
-
* the backend accepted the chain, not that a frame with it is visible yet.
|
|
4
|
-
*/
|
|
5
1
|
import type { Transport } from "./wire.ts";
|
|
6
2
|
/**
|
|
7
3
|
* One node of a {@link VideoFilter} chain, by `kind`. Values outside the
|
|
@@ -165,7 +161,114 @@ export type VideoNode =
|
|
|
165
161
|
kind: "frameBlend";
|
|
166
162
|
/** Output frame rate, integer 24–120. */
|
|
167
163
|
fps: number;
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* A convolution matrix of your own per plane (FFmpeg `convolution`, SDK
|
|
167
|
+
* 1.5.39, player 0.5.20): sharpen, blur, emboss or detect edges with a
|
|
168
|
+
* 3×3, 5×5 or 7×7 kernel, or a row or column kernel. `planes[i]` is
|
|
169
|
+
* plane `i` of the decoded picture: for the usual YUV video 0 is luma
|
|
170
|
+
* (brightness), 1 and 2 are the colour planes, 3 is alpha. A missing
|
|
171
|
+
* entry or `null` leaves that plane as it is; at least one entry has a
|
|
172
|
+
* matrix. Players before 0.5.20 refuse the kind with `invalid`.
|
|
173
|
+
*/
|
|
174
|
+
| {
|
|
175
|
+
/** The node type. */
|
|
176
|
+
kind: "convolution";
|
|
177
|
+
/** 1–4 entries, by plane; `null` passes the plane through. */
|
|
178
|
+
planes: (ConvolutionPlane | null)[];
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Recolour the picture with a 3D look-up table (FFmpeg `lut3d`, SDK
|
|
182
|
+
* 1.5.39, player 0.5.20): a `.cube` file the plugin ships in its package
|
|
183
|
+
* or wrote to its {@link Plugin.dataDirectory}. The player reads the file
|
|
184
|
+
* when the chain is set (and again for each new source, snapshot or
|
|
185
|
+
* export), so it must stay there while the chain uses it. The table is
|
|
186
|
+
* applied to RGB; the player converts the picture to RGB and back.
|
|
187
|
+
* Players before 0.5.20 refuse the kind with `invalid`.
|
|
188
|
+
*/
|
|
189
|
+
| {
|
|
190
|
+
/** The node type. */
|
|
191
|
+
kind: "lut3d";
|
|
192
|
+
/**
|
|
193
|
+
* Absolute path of a `.cube` file (`LUT_3D_SIZE` 2–65, at most
|
|
194
|
+
* 16 MiB) inside the plugin's package directory or data directory
|
|
195
|
+
* after following links; a path with `[` or `]` is refused. Outside
|
|
196
|
+
* those: `invalid`; a file that does not exist: `missing`.
|
|
197
|
+
*/
|
|
198
|
+
file: string;
|
|
199
|
+
/** How colours between the table's points are computed; `tetrahedral` is FFmpeg's default. */
|
|
200
|
+
interpolation: "nearest" | "trilinear" | "tetrahedral" | "pyramid" | "prism";
|
|
201
|
+
};
|
|
202
|
+
/**
|
|
203
|
+
* One plane's kernel of a `convolution` {@link VideoNode}: each output
|
|
204
|
+
* sample is `(sum of matrix × neighbourhood) × rdiv + bias`, clipped to the
|
|
205
|
+
* sample range.
|
|
206
|
+
*
|
|
207
|
+
* @since 1.5.39
|
|
208
|
+
* @category Processing
|
|
209
|
+
*/
|
|
210
|
+
export type ConvolutionPlane = {
|
|
211
|
+
/**
|
|
212
|
+
* `square`: `matrix` is a 3×3, 5×5 or 7×7 kernel, row by row (9, 25 or
|
|
213
|
+
* 49 entries); `row` / `column`: a horizontal / vertical kernel with an
|
|
214
|
+
* odd number of entries up to 49.
|
|
215
|
+
*/
|
|
216
|
+
mode: "square" | "row" | "column";
|
|
217
|
+
/** Integers −1024–1024 (FFmpeg reads whole numbers only). */
|
|
218
|
+
matrix: number[];
|
|
219
|
+
/**
|
|
220
|
+
* Multiplier 0–256; 0 divides by the sum of the matrix (by 1 when it
|
|
221
|
+
* sums to 0), which keeps the brightness of a blur.
|
|
222
|
+
*/
|
|
223
|
+
rdiv: number;
|
|
224
|
+
/** Added after the multiplication, 0–65535 in the plane's sample units (128 is mid grey for 8-bit video). */
|
|
225
|
+
bias: number;
|
|
168
226
|
};
|
|
227
|
+
/**
|
|
228
|
+
* The player's bounds for the video primitives of SDK 1.5.39 (player
|
|
229
|
+
* 0.5.20): `convolution` and `lut3d` {@link VideoNode}s and the shader
|
|
230
|
+
* channel (`plugin.video.shaders()`). Values beyond them fail with
|
|
231
|
+
* `invalid`; more shaders than `shaders` fail with `limit`.
|
|
232
|
+
*
|
|
233
|
+
* @example Refuse a shader list the player would not take
|
|
234
|
+
* The plugin checks a shader list against the player's limit before it
|
|
235
|
+
* sets it, and says so on the OSD instead of waiting for a `limit` failure.
|
|
236
|
+
* The limit counts every plugin's shaders, so the player still decides.
|
|
237
|
+
* ```ts
|
|
238
|
+
* import type { Plugin } from "@vestra-player/sdk";
|
|
239
|
+
* declare const plugin: Plugin;
|
|
240
|
+
* declare const files: string[];
|
|
241
|
+
* // ---cut---
|
|
242
|
+
* import { VIDEO_PROCESSING_LIMITS } from "@vestra-player/sdk/testing";
|
|
243
|
+
*
|
|
244
|
+
* if (files.length > VIDEO_PROCESSING_LIMITS.shaders)
|
|
245
|
+
* void plugin.osd.show("Too many shaders");
|
|
246
|
+
* else plugin.video.shaders().set(files);
|
|
247
|
+
* ```
|
|
248
|
+
*
|
|
249
|
+
* @since 1.5.39
|
|
250
|
+
* @category Testing
|
|
251
|
+
*/
|
|
252
|
+
export declare const VIDEO_PROCESSING_LIMITS: Readonly<{
|
|
253
|
+
/** Shaders across every plugin's `plugin.video.shaders()` handles (16). */
|
|
254
|
+
shaders: 16;
|
|
255
|
+
/** Bytes of one shader file (16 MiB). */
|
|
256
|
+
shaderBytes: number;
|
|
257
|
+
/** Bytes of a shader handle's id (128). */
|
|
258
|
+
shaderIdBytes: 128;
|
|
259
|
+
/** Bytes of one `lut3d` `.cube` file (16 MiB). */
|
|
260
|
+
lutBytes: number;
|
|
261
|
+
/** Largest `LUT_3D_SIZE` of a `lut3d` file, points per axis (65). */
|
|
262
|
+
lutSize: 65;
|
|
263
|
+
/** Bytes of a `lut3d` file path (4096). */
|
|
264
|
+
lutPathBytes: 4096;
|
|
265
|
+
/** Largest magnitude of a `convolution` matrix entry (1024). */
|
|
266
|
+
matrixValue: 1024;
|
|
267
|
+
/** Largest `convolution` `rdiv` (256). */
|
|
268
|
+
rdiv: 256;
|
|
269
|
+
/** Largest `convolution` `bias` (65535). */
|
|
270
|
+
bias: 65535;
|
|
271
|
+
}>;
|
|
169
272
|
/**
|
|
170
273
|
* One node of an {@link AudioFilter} chain, by `kind`. Values outside the
|
|
171
274
|
* ranges fail with `invalid`; "live" fields change in place while playing.
|
|
@@ -236,6 +339,17 @@ export type AudioNode =
|
|
|
236
339
|
/** `stereo` leaves it unchanged. */
|
|
237
340
|
mode: "stereo" | "left" | "right" | "mono" | "swap";
|
|
238
341
|
};
|
|
342
|
+
/**
|
|
343
|
+
* Whether `text` has a control character as Rust's `char::is_control`
|
|
344
|
+
* sees one (C0, DEL and C1: U+0000–U+001F, U+007F–U+009F).
|
|
345
|
+
*/
|
|
346
|
+
export declare function hasControl(text: string): boolean;
|
|
347
|
+
/**
|
|
348
|
+
* The form of a `lut3d` path (the player's `effects.rs lut_path`); where
|
|
349
|
+
* the file is and what it holds the player checks when the request arrives
|
|
350
|
+
* (the test host too, `TestHost.fileRoots`).
|
|
351
|
+
*/
|
|
352
|
+
export declare function lutPathProblem(file: unknown): string | null;
|
|
239
353
|
/**
|
|
240
354
|
* Why `nodes` would be refused (`null` when valid): unknown kinds or fields,
|
|
241
355
|
* values out of range, more than 64 nodes. Used by the test host.
|
package/dist/processing.js
CHANGED
|
@@ -1,3 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Processing chains for the player's video and audio filters. Applied means
|
|
3
|
+
* the backend accepted the chain, not that a frame with it is visible yet.
|
|
4
|
+
*/
|
|
5
|
+
import { isAbsolute } from "node:path";
|
|
6
|
+
/**
|
|
7
|
+
* The player's bounds for the video primitives of SDK 1.5.39 (player
|
|
8
|
+
* 0.5.20): `convolution` and `lut3d` {@link VideoNode}s and the shader
|
|
9
|
+
* channel (`plugin.video.shaders()`). Values beyond them fail with
|
|
10
|
+
* `invalid`; more shaders than `shaders` fail with `limit`.
|
|
11
|
+
*
|
|
12
|
+
* @example Refuse a shader list the player would not take
|
|
13
|
+
* The plugin checks a shader list against the player's limit before it
|
|
14
|
+
* sets it, and says so on the OSD instead of waiting for a `limit` failure.
|
|
15
|
+
* The limit counts every plugin's shaders, so the player still decides.
|
|
16
|
+
* ```ts
|
|
17
|
+
* import type { Plugin } from "@vestra-player/sdk";
|
|
18
|
+
* declare const plugin: Plugin;
|
|
19
|
+
* declare const files: string[];
|
|
20
|
+
* // ---cut---
|
|
21
|
+
* import { VIDEO_PROCESSING_LIMITS } from "@vestra-player/sdk/testing";
|
|
22
|
+
*
|
|
23
|
+
* if (files.length > VIDEO_PROCESSING_LIMITS.shaders)
|
|
24
|
+
* void plugin.osd.show("Too many shaders");
|
|
25
|
+
* else plugin.video.shaders().set(files);
|
|
26
|
+
* ```
|
|
27
|
+
*
|
|
28
|
+
* @since 1.5.39
|
|
29
|
+
* @category Testing
|
|
30
|
+
*/
|
|
31
|
+
export const VIDEO_PROCESSING_LIMITS = Object.freeze({
|
|
32
|
+
/** Shaders across every plugin's `plugin.video.shaders()` handles (16). */
|
|
33
|
+
shaders: 16,
|
|
34
|
+
/** Bytes of one shader file (16 MiB). */
|
|
35
|
+
shaderBytes: 16 * 1024 * 1024,
|
|
36
|
+
/** Bytes of a shader handle's id (128). */
|
|
37
|
+
shaderIdBytes: 128,
|
|
38
|
+
/** Bytes of one `lut3d` `.cube` file (16 MiB). */
|
|
39
|
+
lutBytes: 16 * 1024 * 1024,
|
|
40
|
+
/** Largest `LUT_3D_SIZE` of a `lut3d` file, points per axis (65). */
|
|
41
|
+
lutSize: 65,
|
|
42
|
+
/** Bytes of a `lut3d` file path (4096). */
|
|
43
|
+
lutPathBytes: 4096,
|
|
44
|
+
/** Largest magnitude of a `convolution` matrix entry (1024). */
|
|
45
|
+
matrixValue: 1024,
|
|
46
|
+
/** Largest `convolution` `rdiv` (256). */
|
|
47
|
+
rdiv: 256,
|
|
48
|
+
/** Largest `convolution` `bias` (65535). */
|
|
49
|
+
bias: 65535,
|
|
50
|
+
});
|
|
1
51
|
const BOOL = [true, false];
|
|
2
52
|
/** Value ranges, the same as the host's `src/host/src/playback/effects.rs` (the host is authoritative). */
|
|
3
53
|
const RULES = {
|
|
@@ -25,6 +75,12 @@ const RULES = {
|
|
|
25
75
|
},
|
|
26
76
|
reframe: { mode: ["crop", "pad"], aspect: [0.25, 4], x: [-1, 1], y: [-1, 1] },
|
|
27
77
|
frameBlend: { fps: [24, 120] },
|
|
78
|
+
// Checked by `convolutionProblem` / `lutProblem` below.
|
|
79
|
+
convolution: { planes: [] },
|
|
80
|
+
lut3d: {
|
|
81
|
+
file: [],
|
|
82
|
+
interpolation: ["nearest", "trilinear", "tetrahedral", "pyramid", "prism"],
|
|
83
|
+
},
|
|
28
84
|
volume: { gainDb: [-60, 0] },
|
|
29
85
|
equalizer: { frequency: [20, 20000], q: [1], gainDb: [-6, 6] },
|
|
30
86
|
treble: { frequency: [3000], slope: [0.5], gainDb: [0, 6] },
|
|
@@ -43,7 +99,77 @@ const VIDEO = new Set([
|
|
|
43
99
|
"deinterlace",
|
|
44
100
|
"reframe",
|
|
45
101
|
"frameBlend",
|
|
102
|
+
"convolution",
|
|
103
|
+
"lut3d",
|
|
46
104
|
]);
|
|
105
|
+
/** `convolution` nodes: the player's rules (`effects.rs ConvolutionPlane`). */
|
|
106
|
+
function convolutionProblem(planes) {
|
|
107
|
+
if (!Array.isArray(planes) || planes.length < 1 || planes.length > 4)
|
|
108
|
+
return "convolution planes must have 1 to 4 entries";
|
|
109
|
+
if (planes.every((plane) => plane === null))
|
|
110
|
+
return "convolution needs at least one plane with a matrix";
|
|
111
|
+
const limits = VIDEO_PROCESSING_LIMITS;
|
|
112
|
+
for (const [index, plane] of planes.entries()) {
|
|
113
|
+
if (plane === null)
|
|
114
|
+
continue;
|
|
115
|
+
if (typeof plane !== "object" || Array.isArray(plane))
|
|
116
|
+
return `convolution plane ${index} must be an object or null`;
|
|
117
|
+
const fields = Object.keys(plane);
|
|
118
|
+
const unknown = fields.find((field) => !["mode", "matrix", "rdiv", "bias"].includes(field));
|
|
119
|
+
if (unknown)
|
|
120
|
+
return `convolution plane ${index}: unknown field ${unknown}`;
|
|
121
|
+
const { mode, matrix, rdiv, bias } = plane;
|
|
122
|
+
if (!["square", "row", "column"].includes(mode))
|
|
123
|
+
return `convolution plane ${index}: mode must be square, row or column`;
|
|
124
|
+
if (!Array.isArray(matrix))
|
|
125
|
+
return `convolution plane ${index}: matrix must be an array of integers`;
|
|
126
|
+
const sized = mode === "square"
|
|
127
|
+
? [9, 25, 49].includes(matrix.length)
|
|
128
|
+
: matrix.length % 2 === 1 && matrix.length <= 49;
|
|
129
|
+
if (!sized)
|
|
130
|
+
return `convolution plane ${index}: a square matrix has 9, 25 or 49 entries, a row or column an odd number up to 49 (got ${matrix.length})`;
|
|
131
|
+
if (matrix.some((value) => !Number.isInteger(value) || Math.abs(value) > limits.matrixValue))
|
|
132
|
+
return `convolution plane ${index}: matrix entries must be integers in -${limits.matrixValue}..${limits.matrixValue}`;
|
|
133
|
+
const outside = (value, max) => typeof value !== "number" ||
|
|
134
|
+
!Number.isFinite(value) ||
|
|
135
|
+
value < 0 ||
|
|
136
|
+
value > max;
|
|
137
|
+
if (outside(rdiv, limits.rdiv))
|
|
138
|
+
return `convolution plane ${index} rdiv must be finite in 0..=${limits.rdiv}`;
|
|
139
|
+
if (outside(bias, limits.bias))
|
|
140
|
+
return `convolution plane ${index} bias must be finite in 0..=${limits.bias}`;
|
|
141
|
+
}
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Whether `text` has a control character as Rust's `char::is_control`
|
|
146
|
+
* sees one (C0, DEL and C1: U+0000–U+001F, U+007F–U+009F).
|
|
147
|
+
*/
|
|
148
|
+
export function hasControl(text) {
|
|
149
|
+
return [...text].some((c) => {
|
|
150
|
+
const code = c.codePointAt(0);
|
|
151
|
+
return code < 0x20 || (code >= 0x7f && code <= 0x9f);
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* The form of a `lut3d` path (the player's `effects.rs lut_path`); where
|
|
156
|
+
* the file is and what it holds the player checks when the request arrives
|
|
157
|
+
* (the test host too, `TestHost.fileRoots`).
|
|
158
|
+
*/
|
|
159
|
+
export function lutPathProblem(file) {
|
|
160
|
+
const max = VIDEO_PROCESSING_LIMITS.lutPathBytes;
|
|
161
|
+
if (typeof file !== "string" ||
|
|
162
|
+
Buffer.byteLength(file) > max ||
|
|
163
|
+
hasControl(file))
|
|
164
|
+
return `lut3d file must be a path of at most ${max} bytes without control characters`;
|
|
165
|
+
if (/[[\]]/.test(file))
|
|
166
|
+
return "lut3d file path must not contain [ or ]";
|
|
167
|
+
if (!isAbsolute(file))
|
|
168
|
+
return "lut3d file must be an absolute path";
|
|
169
|
+
if (!/\.cube$/i.test(file))
|
|
170
|
+
return "lut3d file must be a .cube file";
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
47
173
|
/**
|
|
48
174
|
* Why `nodes` would be refused (`null` when valid): unknown kinds or fields,
|
|
49
175
|
* values out of range, more than 64 nodes. Used by the test host.
|
|
@@ -59,7 +185,20 @@ export function nodeProblem(stage, nodes) {
|
|
|
59
185
|
for (const field of fields)
|
|
60
186
|
if (!(field in rule))
|
|
61
187
|
return `${node.kind}: unknown field ${field}`;
|
|
188
|
+
if (node.kind === "convolution") {
|
|
189
|
+
const problem = convolutionProblem(node.planes);
|
|
190
|
+
if (problem)
|
|
191
|
+
return problem;
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
if (node.kind === "lut3d") {
|
|
195
|
+
const problem = lutPathProblem(node.file);
|
|
196
|
+
if (problem)
|
|
197
|
+
return problem;
|
|
198
|
+
}
|
|
62
199
|
for (const [field, allowed] of Object.entries(rule)) {
|
|
200
|
+
if (allowed.length === 0)
|
|
201
|
+
continue;
|
|
63
202
|
const value = node[field];
|
|
64
203
|
const ok = allowed.length === 2 &&
|
|
65
204
|
typeof allowed[0] === "number" &&
|
package/dist/wire.d.ts
CHANGED
|
@@ -376,11 +376,11 @@ export declare function createWire(transport: Transport): {
|
|
|
376
376
|
operation: (token: import("./processing.ts").ProcessingToken) => Promise<import("./processing.ts").ProcessingOperation>;
|
|
377
377
|
};
|
|
378
378
|
commands: {
|
|
379
|
-
register: (id: string, revision: number, declaration: import("./commands.ts").CommandDeclaration, state: import("./commands.ts").CommandState) => Promise<{
|
|
379
|
+
register: (id: string, revision: number, declaration: import("./commands.ts").CommandDeclaration, state: import("./commands.ts").CommandState, when?: import("./commands.ts").CommandWhen) => Promise<{
|
|
380
380
|
revision: number;
|
|
381
381
|
}>;
|
|
382
382
|
unregister: (id: string) => Promise<void>;
|
|
383
|
-
update: (id: string, revision: number, state: import("./commands.ts").CommandState) => Promise<{
|
|
383
|
+
update: (id: string, revision: number, state: import("./commands.ts").CommandState, when?: import("./commands.ts").CommandWhen) => Promise<{
|
|
384
384
|
revision: number;
|
|
385
385
|
}>;
|
|
386
386
|
};
|
|
@@ -481,6 +481,13 @@ export declare function createWire(transport: Transport): {
|
|
|
481
481
|
state: "applied" | "waiting";
|
|
482
482
|
}>;
|
|
483
483
|
renderRelease: () => Promise<void>;
|
|
484
|
+
/**
|
|
485
|
+
* One shader channel handle's list (`plugin.video.shaders(id)`, player
|
|
486
|
+
* 0.5.20); `[]` removes it.
|
|
487
|
+
*/
|
|
488
|
+
shadersSet: (id: string, paths: string[]) => Promise<{
|
|
489
|
+
state: "applied" | "waiting";
|
|
490
|
+
}>;
|
|
484
491
|
/** The GPU's video upscaler (`plugin.video.upscaling`, player 0.5.16). */
|
|
485
492
|
upscalingState: () => Promise<WireUpscalingState>;
|
|
486
493
|
upscalingSet: (enabled: boolean) => Promise<WireUpscalingState>;
|
|
@@ -608,14 +615,14 @@ export declare function createWire(transport: Transport): {
|
|
|
608
615
|
* only the SDK keeps a copy of the state (player 0.5.18; older
|
|
609
616
|
* players ignore it); `listen` changes it.
|
|
610
617
|
*/
|
|
611
|
-
subscribe(event: HostEvent, listener: (data: unknown) => void, mirror?: boolean): Promise<() => Promise<void>>;
|
|
618
|
+
subscribe(event: HostEvent, listener: (data: unknown) => void, mirror?: boolean, why?: string[]): Promise<() => Promise<void>>;
|
|
612
619
|
/**
|
|
613
620
|
* Tell the player whether a subscribed event has a real listener
|
|
614
621
|
* (`active`) or only the SDK's copy of the state: `events.subscribe`
|
|
615
622
|
* again, which replaces the mark (player 0.5.18; older players keep
|
|
616
623
|
* one subscription per name and ignore the mark).
|
|
617
624
|
*/
|
|
618
|
-
listen: (event: HostEvent, active: boolean) => Promise<void>;
|
|
625
|
+
listen: (event: HostEvent, active: boolean, why?: string[]) => Promise<void>;
|
|
619
626
|
};
|
|
620
627
|
ui: {
|
|
621
628
|
/** Player 0.2.0: the remote images a Markdown document shows. */
|
package/dist/wire.js
CHANGED
|
@@ -138,6 +138,11 @@ export function createWire(transport) {
|
|
|
138
138
|
/** The GPU renderer's options (`plugin.video.render()`). */
|
|
139
139
|
renderSet: (values) => transport.request("video.render.set", { values }),
|
|
140
140
|
renderRelease: () => transport.request("video.render.release"),
|
|
141
|
+
/**
|
|
142
|
+
* One shader channel handle's list (`plugin.video.shaders(id)`, player
|
|
143
|
+
* 0.5.20); `[]` removes it.
|
|
144
|
+
*/
|
|
145
|
+
shadersSet: (id, paths) => transport.request("video.shaders.set", { id, paths }),
|
|
141
146
|
/** The GPU's video upscaler (`plugin.video.upscaling`, player 0.5.16). */
|
|
142
147
|
upscalingState: () => transport.request("video.upscaling.state"),
|
|
143
148
|
upscalingSet: (enabled) => transport.request("video.upscaling.set", {
|
|
@@ -260,13 +265,19 @@ export function createWire(transport) {
|
|
|
260
265
|
* only the SDK keeps a copy of the state (player 0.5.18; older
|
|
261
266
|
* players ignore it); `listen` changes it.
|
|
262
267
|
*/
|
|
263
|
-
async subscribe(event, listener, mirror = false
|
|
268
|
+
async subscribe(event, listener, mirror = false,
|
|
269
|
+
/** What listens (player 0.5.21 logs it; older players ignore it). */
|
|
270
|
+
why) {
|
|
264
271
|
const off = transport.on(event, listener);
|
|
265
272
|
let subscription = subscriptions.get(event);
|
|
266
273
|
if (!subscription) {
|
|
267
274
|
subscription = {
|
|
268
275
|
count: 0,
|
|
269
|
-
ready: transport.request("events.subscribe", mirror
|
|
276
|
+
ready: transport.request("events.subscribe", mirror
|
|
277
|
+
? { event, mirror: true }
|
|
278
|
+
: why?.length
|
|
279
|
+
? { event, why }
|
|
280
|
+
: { event }),
|
|
270
281
|
};
|
|
271
282
|
subscriptions.set(event, subscription);
|
|
272
283
|
}
|
|
@@ -297,7 +308,11 @@ export function createWire(transport) {
|
|
|
297
308
|
* again, which replaces the mark (player 0.5.18; older players keep
|
|
298
309
|
* one subscription per name and ignore the mark).
|
|
299
310
|
*/
|
|
300
|
-
listen: (event, active) => transport.request("events.subscribe", active
|
|
311
|
+
listen: (event, active, why) => transport.request("events.subscribe", active
|
|
312
|
+
? why?.length
|
|
313
|
+
? { event, why }
|
|
314
|
+
: { event }
|
|
315
|
+
: { event, mirror: true }),
|
|
301
316
|
},
|
|
302
317
|
ui: {
|
|
303
318
|
/** Player 0.2.0: the remote images a Markdown document shows. */
|