@vestra-player/sdk 1.5.37 → 1.5.39

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.
@@ -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
@@ -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>;
@@ -477,6 +481,13 @@ export declare function createWire(transport: Transport): {
477
481
  state: "applied" | "waiting";
478
482
  }>;
479
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
+ }>;
480
491
  /** The GPU's video upscaler (`plugin.video.upscaling`, player 0.5.16). */
481
492
  upscalingState: () => Promise<WireUpscalingState>;
482
493
  upscalingSet: (enabled: boolean) => Promise<WireUpscalingState>;
@@ -599,8 +610,19 @@ export declare function createWire(transport: Transport): {
599
610
  }) => Promise<void>;
600
611
  };
601
612
  events: {
602
- /** One `events.subscribe` per name however many listeners. */
603
- subscribe(event: "source.state" | "playback.backend" | "window.state" | "packages.changed" | "bindings.changed" | "timeline.changed" | "appearance.changed", listener: (data: unknown) => void): Promise<() => Promise<void>>;
613
+ /**
614
+ * One `events.subscribe` per name however many listeners. `mirror`:
615
+ * only the SDK keeps a copy of the state (player 0.5.18; older
616
+ * players ignore it); `listen` changes it.
617
+ */
618
+ subscribe(event: HostEvent, listener: (data: unknown) => void, mirror?: boolean): Promise<() => Promise<void>>;
619
+ /**
620
+ * Tell the player whether a subscribed event has a real listener
621
+ * (`active`) or only the SDK's copy of the state: `events.subscribe`
622
+ * again, which replaces the mark (player 0.5.18; older players keep
623
+ * one subscription per name and ignore the mark).
624
+ */
625
+ listen: (event: HostEvent, active: boolean) => Promise<void>;
604
626
  };
605
627
  ui: {
606
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", {
@@ -255,14 +260,18 @@ export function createWire(transport) {
255
260
  set: (options) => transport.request("layout.set", options),
256
261
  },
257
262
  events: {
258
- /** One `events.subscribe` per name however many listeners. */
259
- async subscribe(event, listener) {
263
+ /**
264
+ * One `events.subscribe` per name however many listeners. `mirror`:
265
+ * only the SDK keeps a copy of the state (player 0.5.18; older
266
+ * players ignore it); `listen` changes it.
267
+ */
268
+ async subscribe(event, listener, mirror = false) {
260
269
  const off = transport.on(event, listener);
261
270
  let subscription = subscriptions.get(event);
262
271
  if (!subscription) {
263
272
  subscription = {
264
273
  count: 0,
265
- ready: transport.request("events.subscribe", { event }),
274
+ ready: transport.request("events.subscribe", mirror ? { event, mirror: true } : { event }),
266
275
  };
267
276
  subscriptions.set(event, subscription);
268
277
  }
@@ -287,6 +296,13 @@ export function createWire(transport) {
287
296
  }
288
297
  };
289
298
  },
299
+ /**
300
+ * Tell the player whether a subscribed event has a real listener
301
+ * (`active`) or only the SDK's copy of the state: `events.subscribe`
302
+ * again, which replaces the mark (player 0.5.18; older players keep
303
+ * one subscription per name and ignore the mark).
304
+ */
305
+ listen: (event, active) => transport.request("events.subscribe", active ? { event } : { event, mirror: true }),
290
306
  },
291
307
  ui: {
292
308
  /** Player 0.2.0: the remote images a Markdown document shows. */