@vestra-player/sdk 1.5.38 → 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.
- package/CHANGELOG.md +10 -0
- package/dist/api-requests.js +1 -0
- 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/scope.d.ts +1 -0
- package/dist/plugin/scope.js +2 -0
- package/dist/plugin/shaders.d.ts +9 -0
- package/dist/plugin/shaders.js +160 -0
- package/dist/plugin/testing.d.ts +64 -3
- package/dist/plugin/testing.js +199 -6
- package/dist/plugin/types.d.ts +202 -22
- package/dist/processing.d.ts +118 -4
- package/dist/processing.js +139 -0
- package/dist/wire.d.ts +7 -0
- package/dist/wire.js +5 -0
- package/llms-full.txt +297 -22
- package/llms.txt +20 -20
- package/package.json +1 -1
package/dist/plugin/types.d.ts
CHANGED
|
@@ -9,11 +9,11 @@
|
|
|
9
9
|
* object, so a built plugin has no runtime dependency on this package.
|
|
10
10
|
*/
|
|
11
11
|
import type { Json } from "../wire.ts";
|
|
12
|
-
import type { AudioNode, VideoNode } from "../processing.ts";
|
|
12
|
+
import type { AudioNode, ConvolutionPlane, VideoNode } from "../processing.ts";
|
|
13
13
|
import type { SubtitleStyle } from "../subtitles.ts";
|
|
14
14
|
import type { ResolvedMedia } from "../source-resolvers.ts";
|
|
15
15
|
import type { MediaJob } from "../media.ts";
|
|
16
|
-
export type { Json, AudioNode, VideoNode, SubtitleStyle, ResolvedMedia };
|
|
16
|
+
export type { Json, AudioNode, ConvolutionPlane, VideoNode, SubtitleStyle, ResolvedMedia, };
|
|
17
17
|
/**
|
|
18
18
|
* Anything that owns player-side or local resources: every handle the API
|
|
19
19
|
* returns (commands, windows, panels, filters, snapshots, listeners…).
|
|
@@ -214,10 +214,11 @@ export interface Scope extends Disposable {
|
|
|
214
214
|
readonly player: Player;
|
|
215
215
|
/**
|
|
216
216
|
* Video processing: this plugin's filter chain, the display layer, the
|
|
217
|
-
* GPU render options and the GPU's video
|
|
218
|
-
* `processing.video` and `playback.read`;
|
|
219
|
-
* `events` (without it the SDK never sees
|
|
220
|
-
* chain stays `waiting`). The upscaler
|
|
217
|
+
* GPU render options, this plugin's shaders and the GPU's video
|
|
218
|
+
* upscaler. The first four need `processing.video` and `playback.read`;
|
|
219
|
+
* the filter chain also needs `events` (without it the SDK never sees
|
|
220
|
+
* the media become ready and the chain stays `waiting`). The upscaler
|
|
221
|
+
* needs `video.upscaling` only.
|
|
221
222
|
*/
|
|
222
223
|
readonly video: {
|
|
223
224
|
/**
|
|
@@ -250,6 +251,24 @@ export interface Scope extends Disposable {
|
|
|
250
251
|
* while the first handle is not disposed.
|
|
251
252
|
*/
|
|
252
253
|
render(): RenderOptions;
|
|
254
|
+
/**
|
|
255
|
+
* This plugin's own GPU shaders named `id` (default `"shaders"`;
|
|
256
|
+
* needs `processing.video`; SDK 1.5.39, player 0.5.20). Unlike
|
|
257
|
+
* {@link Scope.video | render()}, every plugin may hold shader
|
|
258
|
+
* handles at the same time; the player joins their lists. Nothing is
|
|
259
|
+
* sent until the first `set()`.
|
|
260
|
+
*
|
|
261
|
+
* @param id - Local name of the handle, 1–128 bytes, unique within
|
|
262
|
+
* the plugin.
|
|
263
|
+
* @returns A {@link VideoShaders} owned by this scope.
|
|
264
|
+
* @throws `capability` (throws) The manifest does not declare
|
|
265
|
+
* `processing.video`.
|
|
266
|
+
* @throws `invalid` (throws) `id` is empty, longer than 128 bytes or
|
|
267
|
+
* has control characters.
|
|
268
|
+
* @throws `Error` without a code (throws) A second handle with the
|
|
269
|
+
* same `id` while the first is not disposed.
|
|
270
|
+
*/
|
|
271
|
+
shaders(id?: string): VideoShaders;
|
|
253
272
|
/**
|
|
254
273
|
* The GPU's video upscaler (NVIDIA RTX Video Super Resolution on
|
|
255
274
|
* Windows; needs `video.upscaling`, player 0.5.16): whether it works
|
|
@@ -4055,8 +4074,10 @@ export interface SystemInfo {
|
|
|
4055
4074
|
* Absent (`undefined`) on players before 0.5.12, which do not report it:
|
|
4056
4075
|
* the renderer is then unknown, not software.
|
|
4057
4076
|
*
|
|
4058
|
-
* Today the player renders on the GPU on macOS
|
|
4059
|
-
*
|
|
4077
|
+
* Today the player renders on the GPU on macOS, on Windows when the
|
|
4078
|
+
* user turned on Video ▸ GPU Rendering (Experimental) (since version 0.5.14;
|
|
4079
|
+
* off by default), and in software otherwise: on Windows by default, on
|
|
4080
|
+
* Linux and without a user interface (see
|
|
4060
4081
|
* [Platforms](../../docs/reference/platforms.md)); test this field
|
|
4061
4082
|
* rather than {@link SystemInfo.os}.
|
|
4062
4083
|
*
|
|
@@ -4954,9 +4975,11 @@ export interface SequenceProvider {
|
|
|
4954
4975
|
*/
|
|
4955
4976
|
export interface PlaybackStats {
|
|
4956
4977
|
/**
|
|
4957
|
-
* The rendering path: "libmpv · GPU 渲染(OpenGL)" (GPU
|
|
4958
|
-
* "libmpv ·
|
|
4959
|
-
*
|
|
4978
|
+
* The rendering path: "libmpv · GPU 渲染(OpenGL)" (GPU on macOS, the
|
|
4979
|
+
* default there), "libmpv · GPU 渲染(Direct3D)" (GPU on Windows, when
|
|
4980
|
+
* turned on; version 0.5.14 and later) or "libmpv · 软件渲染" (software: Windows by
|
|
4981
|
+
* default, Linux, headless, fallback). The strings are the player's
|
|
4982
|
+
* display text in the interface language (shown here in Chinese).
|
|
4960
4983
|
*/
|
|
4961
4984
|
backend: string;
|
|
4962
4985
|
/** The playback session number. */
|
|
@@ -6357,7 +6380,11 @@ export interface RenderSettings {
|
|
|
6357
6380
|
targetPeak: number;
|
|
6358
6381
|
/** Per-scene peak estimation (where the GPU supports it). */
|
|
6359
6382
|
peakDetect: boolean;
|
|
6360
|
-
/**
|
|
6383
|
+
/**
|
|
6384
|
+
* mpv user shaders (`.glsl`): absolute paths of existing files, at most
|
|
6385
|
+
* 16, in order. They run before the shaders of every plugin's
|
|
6386
|
+
* {@link VideoShaders} handles.
|
|
6387
|
+
*/
|
|
6361
6388
|
shaders: string[];
|
|
6362
6389
|
/** Motion interpolation, resampled to the display's refresh rate. */
|
|
6363
6390
|
interpolation: boolean;
|
|
@@ -6367,10 +6394,12 @@ export interface RenderSettings {
|
|
|
6367
6394
|
* (`processing.video`). Single owner, like {@link VideoView}; the two are
|
|
6368
6395
|
* independent handles.
|
|
6369
6396
|
*
|
|
6370
|
-
* Platforms:
|
|
6371
|
-
*
|
|
6372
|
-
*
|
|
6373
|
-
*
|
|
6397
|
+
* Platforms: macOS renders on the GPU by default; Windows does since
|
|
6398
|
+
* version 0.5.14 of the player when the user turns on Video ▸ GPU Rendering
|
|
6399
|
+
* (Experimental) (off by default, {@link Rendering}). Under software
|
|
6400
|
+
* rendering (Windows by default, Linux, headless, fallback) every `set`
|
|
6401
|
+
* fails with `unsupported`; {@link SystemInfo.rendering} tells which
|
|
6402
|
+
* renderer draws without creating the handle. macOS OpenGL 4.1 has no
|
|
6374
6403
|
* compute shaders, so `peakDetect` is accepted but has no effect; output
|
|
6375
6404
|
* is always SDR.
|
|
6376
6405
|
*
|
|
@@ -6379,7 +6408,8 @@ export interface RenderSettings {
|
|
|
6379
6408
|
* 250-nit display; under software rendering the plugin hides its HDR menu.
|
|
6380
6409
|
* `set` merges the values into the plugin's render options and the
|
|
6381
6410
|
* player reports the result through `onStatus`: `failed` with
|
|
6382
|
-
* `unsupported` means the player renders in software (
|
|
6411
|
+
* `unsupported` means the player renders in software (Linux, or Windows
|
|
6412
|
+
* while GPU rendering is off).
|
|
6383
6413
|
* ```ts
|
|
6384
6414
|
* declare function hideHdrMenu(): void;
|
|
6385
6415
|
* // ---cut---
|
|
@@ -6392,11 +6422,13 @@ export interface RenderSettings {
|
|
|
6392
6422
|
*
|
|
6393
6423
|
* @remarks
|
|
6394
6424
|
* GPU renderer options (tone mapping, peak detection, shaders,
|
|
6395
|
-
* interpolation). Only the GPU renderer has them
|
|
6425
|
+
* interpolation). Only the GPU renderer has them (macOS; Windows with GPU
|
|
6426
|
+
* rendering turned on, version 0.5.14 and later);
|
|
6396
6427
|
* under software rendering the handle is `failed` with `unsupported` after
|
|
6397
6428
|
* the player's first answer to `render()` (reported to `onError` too), and so is
|
|
6398
6429
|
* every `set`. Single owner, like {@link VideoView}. `set` never throws; the
|
|
6399
|
-
* result shows in `status`.
|
|
6430
|
+
* result shows in `status`. For shaders that several plugins add at the
|
|
6431
|
+
* same time, use {@link VideoShaders} (SDK 1.5.39).
|
|
6400
6432
|
*
|
|
6401
6433
|
* @capability processing.video, playback.read and events (like
|
|
6402
6434
|
* {@link VideoFilter})
|
|
@@ -6423,7 +6455,9 @@ export interface RenderOptions extends Disposable {
|
|
|
6423
6455
|
set(value: Partial<RenderSettings>): void;
|
|
6424
6456
|
/**
|
|
6425
6457
|
* `failed` with `unsupported` while the player renders in software;
|
|
6426
|
-
* `failed` with `failed` when mpv cannot compile a shader (pushed later
|
|
6458
|
+
* `failed` with `failed` when mpv cannot compile a shader (pushed later;
|
|
6459
|
+
* while other plugins' {@link VideoShaders} are on too, the player
|
|
6460
|
+
* cannot tell whose shader failed and says so in the message).
|
|
6427
6461
|
*/
|
|
6428
6462
|
readonly status: FilterStatus;
|
|
6429
6463
|
/**
|
|
@@ -6446,6 +6480,152 @@ export interface RenderOptions extends Disposable {
|
|
|
6446
6480
|
*/
|
|
6447
6481
|
onStatus(listener: (status: FilterStatus) => void): Disposable;
|
|
6448
6482
|
}
|
|
6483
|
+
/**
|
|
6484
|
+
* This plugin's own GPU shaders, from `plugin.video.shaders(id)`
|
|
6485
|
+
* (`processing.video`, SDK 1.5.39, player 0.5.20): mpv user shaders
|
|
6486
|
+
* (`.glsl`, `//!HOOK` …) that the GPU renderer runs on every frame.
|
|
6487
|
+
*
|
|
6488
|
+
* @remarks
|
|
6489
|
+
* Any number of plugins hold shader handles at the same time (unlike
|
|
6490
|
+
* {@link RenderOptions}, which has one owner). The player joins every
|
|
6491
|
+
* handle's list in a stable order, after the render options' own
|
|
6492
|
+
* `shaders`: built-in plugins first, then installed plugins in the order
|
|
6493
|
+
* they were first installed, then development loads (`--plugin`); ties by
|
|
6494
|
+
* plugin id, and a plugin's handles by `id`. At most 16 shaders across all
|
|
6495
|
+
* handles (`limit` beyond, nothing changes); the render options keep their
|
|
6496
|
+
* own 16.
|
|
6497
|
+
*
|
|
6498
|
+
* Files must be this plugin's own: absolute paths inside its package
|
|
6499
|
+
* directory or {@link Plugin.dataDirectory} after following links, each
|
|
6500
|
+
* at most 16 MiB. Elsewhere: `invalid`; a file that does not exist:
|
|
6501
|
+
* `missing`. The player reads the files when the list is set.
|
|
6502
|
+
*
|
|
6503
|
+
* GPU rendering only (macOS; Windows with Video ▸ GPU Rendering
|
|
6504
|
+
* (Experimental) turned on): under software rendering a non-empty list is
|
|
6505
|
+
* `failed` with `unsupported`, and a switch to software during the run
|
|
6506
|
+
* pushes `unsupported` to every handle with shaders and removes every
|
|
6507
|
+
* list (they no longer count against the 16); set them again once the
|
|
6508
|
+
* player draws on the GPU (after a restart). Shaders act when a
|
|
6509
|
+
* frame is drawn, so processed snapshots and exports never contain them
|
|
6510
|
+
* (use a {@link VideoNode} for that). When mpv cannot compile a shader the
|
|
6511
|
+
* handle turns `failed` with `failed` (pushed later); mpv does not say
|
|
6512
|
+
* which file failed, so while several plugins' shaders are on, each of
|
|
6513
|
+
* them hears it and the message says the player cannot tell whose.
|
|
6514
|
+
*
|
|
6515
|
+
* `set` sends the whole list (last write wins); `clear` and `dispose`
|
|
6516
|
+
* withdraw it; the player removes a stopped plugin's shaders itself.
|
|
6517
|
+
* Players before 0.5.20 refuse the request (the handle is `failed`): declare
|
|
6518
|
+
* `"minPlayerVersion": "0.5.20"`.
|
|
6519
|
+
*
|
|
6520
|
+
* @example Add a sharpening shader the plugin ships
|
|
6521
|
+
* The video looks sharper on the GPU renderer; under software rendering
|
|
6522
|
+
* the OSD says why it is not. The shader file lives in the plugin's
|
|
6523
|
+
* package, and the player puts it after other plugins' shaders that come
|
|
6524
|
+
* earlier in its order.
|
|
6525
|
+
* ```ts
|
|
6526
|
+
* import { join } from "node:path";
|
|
6527
|
+
* declare const packageDirectory: string;
|
|
6528
|
+
* // ---cut---
|
|
6529
|
+
* const shaders = plugin.video.shaders("sharpen");
|
|
6530
|
+
* shaders.onStatus((s) => {
|
|
6531
|
+
* // `unsupported`: software rendering; `failed`: mpv could not compile it.
|
|
6532
|
+
* if (s.state === "failed") void plugin.osd.show("Sharpen: {}", [s.error.message]);
|
|
6533
|
+
* });
|
|
6534
|
+
* shaders.set([join(packageDirectory, "shaders", "sharpen.glsl")]);
|
|
6535
|
+
* ```
|
|
6536
|
+
*
|
|
6537
|
+
* @capability processing.video
|
|
6538
|
+
* @player 0.5.20; older players refuse the request.
|
|
6539
|
+
* @see [Video and audio processing](../../docs/concepts/processing.md)
|
|
6540
|
+
* @since 1.5.39
|
|
6541
|
+
* @category Processing
|
|
6542
|
+
*/
|
|
6543
|
+
export interface VideoShaders extends Disposable {
|
|
6544
|
+
/**
|
|
6545
|
+
* Replace this handle's shader list (last write wins); `[]` withdraws
|
|
6546
|
+
* it.
|
|
6547
|
+
*
|
|
6548
|
+
* @remarks
|
|
6549
|
+
* Never throws: a refused list shows in `status` (`invalid`, `missing`,
|
|
6550
|
+
* `limit`, `unsupported`) and goes to {@link Scope.onError}, and what the
|
|
6551
|
+
* player held stays.
|
|
6552
|
+
*
|
|
6553
|
+
* @capability processing.video
|
|
6554
|
+
* @param paths - Absolute paths of shader files in this plugin's package
|
|
6555
|
+
* or data directory, in the order they run.
|
|
6556
|
+
*
|
|
6557
|
+
* @example Switch between two shader presets
|
|
6558
|
+
* The plugin offers a light and a strong preset; picking one replaces
|
|
6559
|
+
* the handle's list in one request. The files were written to the
|
|
6560
|
+
* plugin's data directory when the user imported them.
|
|
6561
|
+
* ```ts
|
|
6562
|
+
* import { join } from "node:path";
|
|
6563
|
+
* // ---cut---
|
|
6564
|
+
* const presets: Record<string, string[]> = {
|
|
6565
|
+
* light: [join(plugin.dataDirectory, "denoise.glsl")],
|
|
6566
|
+
* strong: [join(plugin.dataDirectory, "denoise.glsl"), join(plugin.dataDirectory, "sharpen.glsl")],
|
|
6567
|
+
* };
|
|
6568
|
+
* plugin.video.shaders("presets").set(presets.strong!);
|
|
6569
|
+
* ```
|
|
6570
|
+
*
|
|
6571
|
+
* @since 1.5.39
|
|
6572
|
+
*/
|
|
6573
|
+
set(paths: readonly string[]): void;
|
|
6574
|
+
/**
|
|
6575
|
+
* Withdraw this handle's shaders (the same as `set([])`).
|
|
6576
|
+
*
|
|
6577
|
+
* @capability processing.video
|
|
6578
|
+
*
|
|
6579
|
+
* @example Turn the plugin's shaders off from a menu item
|
|
6580
|
+
* A Video menu item turns the plugin's shader on and off. Clearing
|
|
6581
|
+
* keeps the handle, so the next pick sets the list again.
|
|
6582
|
+
* ```ts
|
|
6583
|
+
* declare const file: string;
|
|
6584
|
+
* // ---cut---
|
|
6585
|
+
* const shaders = plugin.video.shaders();
|
|
6586
|
+
* let on = false;
|
|
6587
|
+
* plugin.commands.add({
|
|
6588
|
+
* id: "toggle-shader",
|
|
6589
|
+
* path: ["@video", "Sharpen (Shader)"],
|
|
6590
|
+
* checked: () => on,
|
|
6591
|
+
* run: () => {
|
|
6592
|
+
* on = !on;
|
|
6593
|
+
* // Clearing keeps the handle; the next pick sets the list again.
|
|
6594
|
+
* if (on) shaders.set([file]);
|
|
6595
|
+
* else shaders.clear();
|
|
6596
|
+
* },
|
|
6597
|
+
* });
|
|
6598
|
+
* ```
|
|
6599
|
+
*
|
|
6600
|
+
* @since 1.5.39
|
|
6601
|
+
*/
|
|
6602
|
+
clear(): void;
|
|
6603
|
+
/**
|
|
6604
|
+
* Current status: `waiting` before ready media, `applying`, `applied`,
|
|
6605
|
+
* or `failed` with the reason (see the remarks).
|
|
6606
|
+
*/
|
|
6607
|
+
readonly status: FilterStatus;
|
|
6608
|
+
/**
|
|
6609
|
+
* The status changed, including later pushes (a compile failure, a
|
|
6610
|
+
* switch to software rendering).
|
|
6611
|
+
*
|
|
6612
|
+
* @capability processing.video
|
|
6613
|
+
*
|
|
6614
|
+
* @example Report a shader that does not compile
|
|
6615
|
+
* The OSD shows mpv's message when the plugin's shader fails to compile.
|
|
6616
|
+
* The player pushes that failure after the list was accepted, so it
|
|
6617
|
+
* arrives through `onStatus`, not as a refused `set`.
|
|
6618
|
+
* ```ts
|
|
6619
|
+
* const shaders = plugin.video.shaders();
|
|
6620
|
+
* shaders.onStatus((status) => {
|
|
6621
|
+
* if (status.state === "failed") void plugin.osd.show("Shader: {}", [status.error.message]);
|
|
6622
|
+
* });
|
|
6623
|
+
* ```
|
|
6624
|
+
*
|
|
6625
|
+
* @since 1.5.39
|
|
6626
|
+
*/
|
|
6627
|
+
onStatus(listener: (status: FilterStatus) => void): Disposable;
|
|
6628
|
+
}
|
|
6449
6629
|
/**
|
|
6450
6630
|
* Why the GPU's video upscaler does not work, as a stable kebab-case code.
|
|
6451
6631
|
* A newer player may add codes: compare with the ones you handle and show
|
|
@@ -9295,10 +9475,10 @@ export type RenderingPreference = "auto" | "gpu" | "software";
|
|
|
9295
9475
|
*
|
|
9296
9476
|
* | Code | Meaning |
|
|
9297
9477
|
* | --- | --- |
|
|
9298
|
-
* | `default` | Nothing ruled the GPU out; the platform's default. |
|
|
9478
|
+
* | `default` | Nothing ruled the GPU out; the platform's default (macOS: GPU; Windows since version 0.5.14: software until the user turns on GPU rendering). |
|
|
9299
9479
|
* | `pending` | GPU rendering is starting. |
|
|
9300
9480
|
* | `headless` | The player runs without a user interface. |
|
|
9301
|
-
* | `no-gpu-backend` | This player has no GPU renderer on this system (Windows
|
|
9481
|
+
* | `no-gpu-backend` | This player has no GPU renderer on this system (Linux today; Windows has one since version 0.5.14, off by default, which reports `default`). |
|
|
9302
9482
|
* | `env` | The environment chose it (`VESTRA_RENDER`, `VESTRA_SOFTWARE_RENDER`). |
|
|
9303
9483
|
* | `preference` | The user chose software rendering ({@link Rendering.setPreference}); GPU chosen and available reports `default`. |
|
|
9304
9484
|
* | `denylist` | The graphics driver is on the player's denylist. |
|
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
|
@@ -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>;
|
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", {
|