@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.
- package/CHANGELOG.md +19 -0
- package/README.md +1 -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/appearance.js +11 -2
- package/dist/plugin/packages.js +3 -0
- package/dist/plugin/player.js +6 -1
- package/dist/plugin/resolvers.js +10 -2
- package/dist/plugin/runtime.d.ts +14 -1
- package/dist/plugin/runtime.js +107 -16
- package/dist/plugin/scope.d.ts +1 -0
- package/dist/plugin/scope.js +2 -0
- package/dist/plugin/shaders.d.ts +9 -0
- package/dist/plugin/shaders.js +160 -0
- package/dist/plugin/subtitles.js +14 -2
- package/dist/plugin/testing.d.ts +74 -3
- package/dist/plugin/testing.js +221 -8
- package/dist/plugin/timeline.d.ts +1 -1
- package/dist/plugin/timeline.js +10 -2
- package/dist/plugin/types.d.ts +213 -22
- package/dist/plugin/ui.js +9 -2
- package/dist/plugin/window.d.ts +1 -1
- package/dist/plugin/window.js +10 -2
- package/dist/processing.d.ts +118 -4
- package/dist/processing.js +139 -0
- package/dist/wire.d.ts +24 -2
- package/dist/wire.js +19 -3
- package/llms-full.txt +318 -25
- package/llms.txt +23 -23
- package/package.json +1 -1
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. */
|
|
@@ -535,6 +538,16 @@ export declare class TestHost implements Transport {
|
|
|
535
538
|
readonly calls: Call[];
|
|
536
539
|
/** The host events the plugin subscribed to (needs `events` etc.). */
|
|
537
540
|
readonly subscriptions: Set<string>;
|
|
541
|
+
/**
|
|
542
|
+
* The subscribed events the plugin really listens to: an `onChange` and
|
|
543
|
+
* the like, or a binding that reads the state. The SDK's own copies of
|
|
544
|
+
* the state (what keeps `plugin.player.state` current) are left out.
|
|
545
|
+
* Empty means the player may stop the plugin while nothing uses it
|
|
546
|
+
* (unless it holds something else: a window, a panel, a filter…).
|
|
547
|
+
*
|
|
548
|
+
* @since 1.5.38
|
|
549
|
+
*/
|
|
550
|
+
readonly listening: Set<string>;
|
|
538
551
|
/**
|
|
539
552
|
* The player's source facts (phase, position, duration, paused, rate,
|
|
540
553
|
* audio tracks, loop, ...). Wire-level shape; change it with
|
|
@@ -1331,6 +1344,64 @@ export declare class TestHost implements Transport {
|
|
|
1331
1344
|
* @since 1.0.0
|
|
1332
1345
|
*/
|
|
1333
1346
|
failVideoRender(code: "failed" | "unsupported", message: string): void;
|
|
1347
|
+
/**
|
|
1348
|
+
* The shader channel (`plugin.video.shaders(id)`, SDK 1.5.39): the lists
|
|
1349
|
+
* the player holds for this plugin's handles by id (the real paths, after
|
|
1350
|
+
* following links), and how many shaders other plugins hold, which count
|
|
1351
|
+
* against {@link VIDEO_PROCESSING_LIMITS}`.shaders` (`limit`). Files must
|
|
1352
|
+
* be under {@link TestHost.fileRoots}; under software rendering
|
|
1353
|
+
* (`videoRender.gpu = false`) a non-empty list fails with `unsupported`.
|
|
1354
|
+
*
|
|
1355
|
+
* @since 1.5.39
|
|
1356
|
+
*/
|
|
1357
|
+
readonly videoShaders: {
|
|
1358
|
+
/** This plugin's lists by handle id; a cleared handle has none. */
|
|
1359
|
+
handles: Map<string, string[]>;
|
|
1360
|
+
/** Shaders other plugins hold (0–16). */
|
|
1361
|
+
others: number;
|
|
1362
|
+
};
|
|
1363
|
+
/**
|
|
1364
|
+
* Where the plugin's own files may be: its package directory and its
|
|
1365
|
+
* data directory ({@link startPlugin} sets both when the list is still
|
|
1366
|
+
* empty). `lut3d` files and shader-channel files outside them fail with
|
|
1367
|
+
* `invalid`, as in the player.
|
|
1368
|
+
*
|
|
1369
|
+
* @since 1.5.39
|
|
1370
|
+
*/
|
|
1371
|
+
fileRoots: string[];
|
|
1372
|
+
/**
|
|
1373
|
+
* What the player pushes to a shader handle when mpv cannot compile a
|
|
1374
|
+
* shader (code `failed`; the player cannot tell whose shader failed, so
|
|
1375
|
+
* every handle with shaders hears it) or the player switches to software
|
|
1376
|
+
* rendering (`unsupported`; use {@link TestHost.failVideoRender}, which
|
|
1377
|
+
* also tells the handles). Pushed only to a handle that holds shaders.
|
|
1378
|
+
*
|
|
1379
|
+
* @capability None.
|
|
1380
|
+
*
|
|
1381
|
+
* @example Make the plugin's shader fail to compile
|
|
1382
|
+
* The plugin's shader handle reports `failed`, as when mpv rejects a
|
|
1383
|
+
* shader. The host pushes the failure only to a handle that holds
|
|
1384
|
+
* shaders, which its accepted `set` made it; the SDK updates the
|
|
1385
|
+
* handle's `status` when the push arrives, so the test settles first.
|
|
1386
|
+
* ```ts
|
|
1387
|
+
* import assert from "node:assert/strict";
|
|
1388
|
+
* import { join } from "node:path";
|
|
1389
|
+
* import { writeFileSync } from "node:fs";
|
|
1390
|
+
* declare const host: TestHost;
|
|
1391
|
+
* // ---cut---
|
|
1392
|
+
* const file = join(plugin.dataDirectory, "sharpen.glsl");
|
|
1393
|
+
* writeFileSync(file, "//!HOOK MAIN\n");
|
|
1394
|
+
* const shaders = plugin.video.shaders("sharpen");
|
|
1395
|
+
* shaders.set([file]); // the handle now holds a shader
|
|
1396
|
+
* await host.settle();
|
|
1397
|
+
* host.failVideoShaders("sharpen", "shader compile failed: ERROR: 0:3: ...");
|
|
1398
|
+
* await host.settle();
|
|
1399
|
+
* assert.equal(shaders.status.state, "failed");
|
|
1400
|
+
* ```
|
|
1401
|
+
*
|
|
1402
|
+
* @since 1.5.39
|
|
1403
|
+
*/
|
|
1404
|
+
failVideoShaders(id: string, message: string): void;
|
|
1334
1405
|
/**
|
|
1335
1406
|
* The player's processing (filter) state. Wire-level; may change in minor
|
|
1336
1407
|
* versions. `resources` holds each `video.filter` / `audio.filter` chain the
|
|
@@ -1671,9 +1742,9 @@ export declare class TestHost implements Transport {
|
|
|
1671
1742
|
* import activate from "../main.ts";
|
|
1672
1743
|
*
|
|
1673
1744
|
* const host = new TestHost();
|
|
1674
|
-
* host.videoRender.gpu = false; // software draws now (
|
|
1745
|
+
* host.videoRender.gpu = false; // software draws now (Linux)
|
|
1675
1746
|
* host.rendering.startedWith = "software";
|
|
1676
|
-
* host.rendering.reason = { code: "no-gpu-backend", message: "
|
|
1747
|
+
* host.rendering.reason = { code: "no-gpu-backend", message: "this player has no GPU renderer on Linux yet" };
|
|
1677
1748
|
* host.rendering.gpu = { available: false, reason: host.rendering.reason };
|
|
1678
1749
|
* const { stop } = await startPlugin(activate, {
|
|
1679
1750
|
* directory: new URL("..", import.meta.url),
|
|
@@ -1681,7 +1752,7 @@ export declare class TestHost implements Transport {
|
|
|
1681
1752
|
* });
|
|
1682
1753
|
* const item = host.command("gpu-rendering");
|
|
1683
1754
|
* assert.equal(item.state.enabled, false);
|
|
1684
|
-
* assert.equal(item.state.reason, "
|
|
1755
|
+
* assert.equal(item.state.reason, "this player has no GPU renderer on Linux yet");
|
|
1685
1756
|
* await stop();
|
|
1686
1757
|
* ```
|
|
1687
1758
|
*
|
package/dist/plugin/testing.js
CHANGED
|
@@ -64,12 +64,12 @@
|
|
|
64
64
|
* @packageDocumentation
|
|
65
65
|
* @module @vestra-player/sdk/testing
|
|
66
66
|
*/
|
|
67
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
67
|
+
import { existsSync, openSync, readFileSync, readSync, closeSync, realpathSync, } from "node:fs";
|
|
68
68
|
import { mkdirSync, mkdtempSync, rmSync, statSync } from "node:fs";
|
|
69
69
|
import { createServer } from "node:net";
|
|
70
70
|
import { randomBytes } from "node:crypto";
|
|
71
71
|
import { tmpdir } from "node:os";
|
|
72
|
-
import { extname, isAbsolute, join } from "node:path";
|
|
72
|
+
import { extname, isAbsolute, join, relative } from "node:path";
|
|
73
73
|
import { fileURLToPath } from "node:url";
|
|
74
74
|
import { HostError } from "../wire.js";
|
|
75
75
|
import { localSocketPath } from "./local.js";
|
|
@@ -82,7 +82,8 @@ import { BINDING_INPUTS } from "./bindings.js";
|
|
|
82
82
|
import { THEME_PALETTES } from "../palettes.js";
|
|
83
83
|
import { IMAGE_LIMITS, UI_VALUE_LIMITS, valueLimitProblem } from "./values.js";
|
|
84
84
|
import { validStep } from "./player.js";
|
|
85
|
-
import { nodeProblem } from "../processing.js";
|
|
85
|
+
import { nodeProblem, VIDEO_PROCESSING_LIMITS } from "../processing.js";
|
|
86
|
+
import { shaderIdProblem, shaderListProblem } from "./shaders.js";
|
|
86
87
|
import { RENDER_DEFAULTS, renderProblem, VIEW_DEFAULTS, viewProblem, } from "./view.js";
|
|
87
88
|
import { createPlugin } from "./index.js";
|
|
88
89
|
import { THEMES } from "./appearance.js";
|
|
@@ -107,6 +108,8 @@ export { SECRET_LIMITS };
|
|
|
107
108
|
export { SESSION_STORAGE_LIMITS };
|
|
108
109
|
/** The player's limits on loaded subtitle files, which the test host enforces as well. */
|
|
109
110
|
export { SUBTITLE_LIMITS };
|
|
111
|
+
/** The player's bounds on convolution, lut3d and shader-channel requests, which the test host enforces as well. */
|
|
112
|
+
export { VIDEO_PROCESSING_LIMITS };
|
|
110
113
|
/** The names bindings are made of. */
|
|
111
114
|
export { BINDING_INPUTS };
|
|
112
115
|
/** The colours of the player's palettes, by `Theme` token. */
|
|
@@ -188,6 +191,70 @@ const sessionCost = (key, value) => Buffer.byteLength(key) +
|
|
|
188
191
|
SESSION_STORAGE_LIMITS.entryBytes;
|
|
189
192
|
const kebab = (name) => name.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`);
|
|
190
193
|
const tick = () => new Promise((resolve) => setImmediate(resolve));
|
|
194
|
+
/**
|
|
195
|
+
* The player's `playback/plugin_files.rs`: a file the plugin names for its
|
|
196
|
+
* video processing is an absolute path inside one of `roots` after
|
|
197
|
+
* following links, a regular file of at most `max` bytes. Returns the real
|
|
198
|
+
* path or throws the player's error.
|
|
199
|
+
*/
|
|
200
|
+
function ownFile(path, roots, what, max) {
|
|
201
|
+
if (!isAbsolute(path))
|
|
202
|
+
throw new HostError("invalid", `${what} path must be absolute: ${path}`);
|
|
203
|
+
let real;
|
|
204
|
+
try {
|
|
205
|
+
real = realpathSync(path);
|
|
206
|
+
}
|
|
207
|
+
catch {
|
|
208
|
+
throw new HostError("missing", `${what} not found: ${path}`);
|
|
209
|
+
}
|
|
210
|
+
const inside = roots.some((root) => {
|
|
211
|
+
let base;
|
|
212
|
+
try {
|
|
213
|
+
base = realpathSync(root);
|
|
214
|
+
}
|
|
215
|
+
catch {
|
|
216
|
+
return false;
|
|
217
|
+
}
|
|
218
|
+
const rel = relative(base, real);
|
|
219
|
+
return rel === "" || (!/^\.\.([\\/]|$)/.test(rel) && !isAbsolute(rel));
|
|
220
|
+
});
|
|
221
|
+
if (!inside)
|
|
222
|
+
throw new HostError("invalid", `${what} must be in the plugin's package or data directory: ${path}`);
|
|
223
|
+
const stat = statSync(real);
|
|
224
|
+
if (!stat.isFile())
|
|
225
|
+
throw new HostError("invalid", `${what} is not a file: ${path}`);
|
|
226
|
+
if (stat.size > max)
|
|
227
|
+
throw new HostError("invalid", `${what} is larger than ${max / 1024 / 1024} MiB: ${path}`);
|
|
228
|
+
return real;
|
|
229
|
+
}
|
|
230
|
+
/** The `LUT_3D_SIZE` check of `plugin_files::lut` on the first 64 KiB. */
|
|
231
|
+
function cubeProblem(file) {
|
|
232
|
+
const buffer = Buffer.alloc(64 * 1024);
|
|
233
|
+
const fd = openSync(file, "r");
|
|
234
|
+
let read;
|
|
235
|
+
try {
|
|
236
|
+
read = readSync(fd, buffer, 0, buffer.length, 0);
|
|
237
|
+
}
|
|
238
|
+
finally {
|
|
239
|
+
closeSync(fd);
|
|
240
|
+
}
|
|
241
|
+
for (const line of buffer.subarray(0, read).toString("utf8").split("\n")) {
|
|
242
|
+
const [word, value] = line.trim().split(/\s+/);
|
|
243
|
+
if (word === "LUT_3D_SIZE") {
|
|
244
|
+
const size = Number(value);
|
|
245
|
+
return Number.isInteger(size) &&
|
|
246
|
+
size >= 2 &&
|
|
247
|
+
size <= VIDEO_PROCESSING_LIMITS.lutSize
|
|
248
|
+
? null
|
|
249
|
+
: `LUT_3D_SIZE must be 2 to ${VIDEO_PROCESSING_LIMITS.lutSize}`;
|
|
250
|
+
}
|
|
251
|
+
if (word === "LUT_1D_SIZE")
|
|
252
|
+
return "a 1D LUT; lut3d needs LUT_3D_SIZE";
|
|
253
|
+
if (word !== undefined && word !== "" && Number.isFinite(Number(word)))
|
|
254
|
+
break;
|
|
255
|
+
}
|
|
256
|
+
return "no LUT_3D_SIZE line before the data";
|
|
257
|
+
}
|
|
191
258
|
function initialSource() {
|
|
192
259
|
return {
|
|
193
260
|
kind: "media",
|
|
@@ -333,6 +400,16 @@ export class TestHost {
|
|
|
333
400
|
calls = [];
|
|
334
401
|
/** The host events the plugin subscribed to (needs `events` etc.). */
|
|
335
402
|
subscriptions = new Set();
|
|
403
|
+
/**
|
|
404
|
+
* The subscribed events the plugin really listens to: an `onChange` and
|
|
405
|
+
* the like, or a binding that reads the state. The SDK's own copies of
|
|
406
|
+
* the state (what keeps `plugin.player.state` current) are left out.
|
|
407
|
+
* Empty means the player may stop the plugin while nothing uses it
|
|
408
|
+
* (unless it holds something else: a window, a panel, a filter…).
|
|
409
|
+
*
|
|
410
|
+
* @since 1.5.38
|
|
411
|
+
*/
|
|
412
|
+
listening = new Set();
|
|
336
413
|
/**
|
|
337
414
|
* The player's source facts (phase, position, duration, paused, rate,
|
|
338
415
|
* audio tracks, loop, ...). Wire-level shape; change it with
|
|
@@ -1301,6 +1378,78 @@ export class TestHost {
|
|
|
1301
1378
|
state: "failed",
|
|
1302
1379
|
error: { code, message },
|
|
1303
1380
|
});
|
|
1381
|
+
// The player tells every shader handle too (`failVideoShaders`).
|
|
1382
|
+
// and, like the player, removes their lists.
|
|
1383
|
+
if (code === "unsupported") {
|
|
1384
|
+
for (const [id, paths] of this.videoShaders.handles)
|
|
1385
|
+
if (paths.length > 0)
|
|
1386
|
+
this.emit("video.shaders.status", {
|
|
1387
|
+
id,
|
|
1388
|
+
state: "failed",
|
|
1389
|
+
error: { code, message },
|
|
1390
|
+
});
|
|
1391
|
+
this.videoShaders.handles.clear();
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1394
|
+
/**
|
|
1395
|
+
* The shader channel (`plugin.video.shaders(id)`, SDK 1.5.39): the lists
|
|
1396
|
+
* the player holds for this plugin's handles by id (the real paths, after
|
|
1397
|
+
* following links), and how many shaders other plugins hold, which count
|
|
1398
|
+
* against {@link VIDEO_PROCESSING_LIMITS}`.shaders` (`limit`). Files must
|
|
1399
|
+
* be under {@link TestHost.fileRoots}; under software rendering
|
|
1400
|
+
* (`videoRender.gpu = false`) a non-empty list fails with `unsupported`.
|
|
1401
|
+
*
|
|
1402
|
+
* @since 1.5.39
|
|
1403
|
+
*/
|
|
1404
|
+
videoShaders = { handles: new Map(), others: 0 };
|
|
1405
|
+
/**
|
|
1406
|
+
* Where the plugin's own files may be: its package directory and its
|
|
1407
|
+
* data directory ({@link startPlugin} sets both when the list is still
|
|
1408
|
+
* empty). `lut3d` files and shader-channel files outside them fail with
|
|
1409
|
+
* `invalid`, as in the player.
|
|
1410
|
+
*
|
|
1411
|
+
* @since 1.5.39
|
|
1412
|
+
*/
|
|
1413
|
+
fileRoots = [];
|
|
1414
|
+
/**
|
|
1415
|
+
* What the player pushes to a shader handle when mpv cannot compile a
|
|
1416
|
+
* shader (code `failed`; the player cannot tell whose shader failed, so
|
|
1417
|
+
* every handle with shaders hears it) or the player switches to software
|
|
1418
|
+
* rendering (`unsupported`; use {@link TestHost.failVideoRender}, which
|
|
1419
|
+
* also tells the handles). Pushed only to a handle that holds shaders.
|
|
1420
|
+
*
|
|
1421
|
+
* @capability None.
|
|
1422
|
+
*
|
|
1423
|
+
* @example Make the plugin's shader fail to compile
|
|
1424
|
+
* The plugin's shader handle reports `failed`, as when mpv rejects a
|
|
1425
|
+
* shader. The host pushes the failure only to a handle that holds
|
|
1426
|
+
* shaders, which its accepted `set` made it; the SDK updates the
|
|
1427
|
+
* handle's `status` when the push arrives, so the test settles first.
|
|
1428
|
+
* ```ts
|
|
1429
|
+
* import assert from "node:assert/strict";
|
|
1430
|
+
* import { join } from "node:path";
|
|
1431
|
+
* import { writeFileSync } from "node:fs";
|
|
1432
|
+
* declare const host: TestHost;
|
|
1433
|
+
* // ---cut---
|
|
1434
|
+
* const file = join(plugin.dataDirectory, "sharpen.glsl");
|
|
1435
|
+
* writeFileSync(file, "//!HOOK MAIN\n");
|
|
1436
|
+
* const shaders = plugin.video.shaders("sharpen");
|
|
1437
|
+
* shaders.set([file]); // the handle now holds a shader
|
|
1438
|
+
* await host.settle();
|
|
1439
|
+
* host.failVideoShaders("sharpen", "shader compile failed: ERROR: 0:3: ...");
|
|
1440
|
+
* await host.settle();
|
|
1441
|
+
* assert.equal(shaders.status.state, "failed");
|
|
1442
|
+
* ```
|
|
1443
|
+
*
|
|
1444
|
+
* @since 1.5.39
|
|
1445
|
+
*/
|
|
1446
|
+
failVideoShaders(id, message) {
|
|
1447
|
+
if ((this.videoShaders.handles.get(id)?.length ?? 0) > 0)
|
|
1448
|
+
this.emit("video.shaders.status", {
|
|
1449
|
+
id,
|
|
1450
|
+
state: "failed",
|
|
1451
|
+
error: { code: "failed", message },
|
|
1452
|
+
});
|
|
1304
1453
|
}
|
|
1305
1454
|
/**
|
|
1306
1455
|
* The player's processing (filter) state. Wire-level; may change in minor
|
|
@@ -1600,9 +1749,9 @@ export class TestHost {
|
|
|
1600
1749
|
* import activate from "../main.ts";
|
|
1601
1750
|
*
|
|
1602
1751
|
* const host = new TestHost();
|
|
1603
|
-
* host.videoRender.gpu = false; // software draws now (
|
|
1752
|
+
* host.videoRender.gpu = false; // software draws now (Linux)
|
|
1604
1753
|
* host.rendering.startedWith = "software";
|
|
1605
|
-
* host.rendering.reason = { code: "no-gpu-backend", message: "
|
|
1754
|
+
* host.rendering.reason = { code: "no-gpu-backend", message: "this player has no GPU renderer on Linux yet" };
|
|
1606
1755
|
* host.rendering.gpu = { available: false, reason: host.rendering.reason };
|
|
1607
1756
|
* const { stop } = await startPlugin(activate, {
|
|
1608
1757
|
* directory: new URL("..", import.meta.url),
|
|
@@ -1610,7 +1759,7 @@ export class TestHost {
|
|
|
1610
1759
|
* });
|
|
1611
1760
|
* const item = host.command("gpu-rendering");
|
|
1612
1761
|
* assert.equal(item.state.enabled, false);
|
|
1613
|
-
* assert.equal(item.state.reason, "
|
|
1762
|
+
* assert.equal(item.state.reason, "this player has no GPU renderer on Linux yet");
|
|
1614
1763
|
* await stop();
|
|
1615
1764
|
* ```
|
|
1616
1765
|
*
|
|
@@ -3377,8 +3526,18 @@ export class TestHost {
|
|
|
3377
3526
|
};
|
|
3378
3527
|
}
|
|
3379
3528
|
#defaults = {
|
|
3380
|
-
"events.subscribe": (p) =>
|
|
3381
|
-
|
|
3529
|
+
"events.subscribe": (p) => {
|
|
3530
|
+
const event = String(p.event);
|
|
3531
|
+
this.subscriptions.add(event);
|
|
3532
|
+
if (p.mirror === true)
|
|
3533
|
+
this.listening.delete(event);
|
|
3534
|
+
else
|
|
3535
|
+
this.listening.add(event);
|
|
3536
|
+
},
|
|
3537
|
+
"events.unsubscribe": (p) => {
|
|
3538
|
+
this.subscriptions.delete(String(p.event));
|
|
3539
|
+
this.listening.delete(String(p.event));
|
|
3540
|
+
},
|
|
3382
3541
|
"commands.register": (p) => {
|
|
3383
3542
|
const id = String(p.id);
|
|
3384
3543
|
// The player's built-in menu ids (MENU_IDS) in the first path segment.
|
|
@@ -3965,6 +4124,34 @@ export class TestHost {
|
|
|
3965
4124
|
this.setVideoRenderOwner(null);
|
|
3966
4125
|
return null;
|
|
3967
4126
|
},
|
|
4127
|
+
// The player's capability layer and `video_shaders.rs`.
|
|
4128
|
+
"video.shaders.set": (p) => {
|
|
4129
|
+
const idProblem = shaderIdProblem(p.id);
|
|
4130
|
+
if (idProblem)
|
|
4131
|
+
throw new HostError("invalid", idProblem);
|
|
4132
|
+
if (!Array.isArray(p.paths) ||
|
|
4133
|
+
p.paths.some((path) => typeof path !== "string"))
|
|
4134
|
+
throw new HostError("invalid", "paths must be an array of paths");
|
|
4135
|
+
const id = p.id;
|
|
4136
|
+
const list = p.paths;
|
|
4137
|
+
const listProblem = shaderListProblem(list);
|
|
4138
|
+
if (listProblem?.startsWith("at most"))
|
|
4139
|
+
throw new HostError("limit", listProblem);
|
|
4140
|
+
const paths = list.map((path) => ownFile(path, this.fileRoots, "shader", VIDEO_PROCESSING_LIMITS.shaderBytes));
|
|
4141
|
+
if (paths.length > 0 && !this.videoRender.gpu)
|
|
4142
|
+
throw new HostError("unsupported", "shaders need GPU rendering; the player renders in software");
|
|
4143
|
+
const mine = [...this.videoShaders.handles]
|
|
4144
|
+
.filter(([other]) => other !== id)
|
|
4145
|
+
.reduce((sum, [, held]) => sum + held.length, 0);
|
|
4146
|
+
const others = mine + this.videoShaders.others;
|
|
4147
|
+
if (others + paths.length > VIDEO_PROCESSING_LIMITS.shaders)
|
|
4148
|
+
throw new HostError("limit", `at most ${VIDEO_PROCESSING_LIMITS.shaders} shaders across all plugins: ${others} are in use, this list has ${paths.length}`);
|
|
4149
|
+
if (paths.length > 0)
|
|
4150
|
+
this.videoShaders.handles.set(id, paths);
|
|
4151
|
+
else
|
|
4152
|
+
this.videoShaders.handles.delete(id);
|
|
4153
|
+
return { state: this.source.phase === "ready" ? "applied" : "waiting" };
|
|
4154
|
+
},
|
|
3968
4155
|
"timeline.set": (p) => {
|
|
3969
4156
|
if (p.sourceId !== undefined &&
|
|
3970
4157
|
p.sourceId !== this.source.stamp.generation)
|
|
@@ -4192,6 +4379,30 @@ export class TestHost {
|
|
|
4192
4379
|
status: "failed",
|
|
4193
4380
|
error: { code: "invalid", message: (problem ?? blend ?? delay) },
|
|
4194
4381
|
});
|
|
4382
|
+
// `lut3d` files: the plugin's own `.cube` files (`plugin_files.rs`).
|
|
4383
|
+
if (stage === "video")
|
|
4384
|
+
for (const node of (p.nodes ?? []))
|
|
4385
|
+
if (node.kind === "lut3d") {
|
|
4386
|
+
let cube;
|
|
4387
|
+
try {
|
|
4388
|
+
const real = ownFile(node.file, this.fileRoots, "lut3d file", VIDEO_PROCESSING_LIMITS.lutBytes);
|
|
4389
|
+
cube = /\.cube$/i.test(real)
|
|
4390
|
+
? cubeProblem(real)
|
|
4391
|
+
: "lut3d file must be a .cube file";
|
|
4392
|
+
}
|
|
4393
|
+
catch (error) {
|
|
4394
|
+
const { code, message } = error;
|
|
4395
|
+
return finish({ status: "failed", error: { code, message } });
|
|
4396
|
+
}
|
|
4397
|
+
if (cube)
|
|
4398
|
+
return finish({
|
|
4399
|
+
status: "failed",
|
|
4400
|
+
error: {
|
|
4401
|
+
code: "invalid",
|
|
4402
|
+
message: `lut3d file ${node.file}: ${cube}`,
|
|
4403
|
+
},
|
|
4404
|
+
});
|
|
4405
|
+
}
|
|
4195
4406
|
if (this.source.phase !== "ready")
|
|
4196
4407
|
return finish({
|
|
4197
4408
|
status: "failed",
|
|
@@ -4824,6 +5035,8 @@ export async function startPlugin(activate, options = {}) {
|
|
|
4824
5035
|
for (const [key, value] of options.sessionStorage ?? [])
|
|
4825
5036
|
host.sessionStorage.set(key, sortedJson(value));
|
|
4826
5037
|
mkdirSync(data, { recursive: true });
|
|
5038
|
+
if (host.fileRoots.length === 0)
|
|
5039
|
+
host.fileRoots = [directory, data];
|
|
4827
5040
|
const instance = await createPlugin(host, manifest, directory, data, host.app);
|
|
4828
5041
|
await activate(instance.plugin);
|
|
4829
5042
|
// Like the player: a position or resolver another plugin holds is not an
|
|
@@ -16,7 +16,7 @@ export declare class TimelineService {
|
|
|
16
16
|
accept(marks: WireMark[]): void;
|
|
17
17
|
read(): Promise<void>;
|
|
18
18
|
watch(listener: (marks: readonly TimelineMark[]) => void): {
|
|
19
|
-
dispose: () => Promise<
|
|
19
|
+
dispose: () => Promise<void>;
|
|
20
20
|
};
|
|
21
21
|
}
|
|
22
22
|
export declare const timelineOf: (runtime: Runtime) => TimelineService;
|
package/dist/plugin/timeline.js
CHANGED
|
@@ -27,13 +27,14 @@ export class TimelineService {
|
|
|
27
27
|
if (this.#started)
|
|
28
28
|
return;
|
|
29
29
|
this.#started = true;
|
|
30
|
+
// A mirror: a plugin listens through `watch` and bindings.
|
|
30
31
|
this.#runtime.on("timeline.changed", (data) => {
|
|
31
32
|
const marks = data?.marks;
|
|
32
33
|
if (Array.isArray(marks))
|
|
33
34
|
this.accept(marks);
|
|
34
35
|
else
|
|
35
36
|
void this.read();
|
|
36
|
-
});
|
|
37
|
+
}, "events", true);
|
|
37
38
|
}
|
|
38
39
|
accept(marks) {
|
|
39
40
|
const next = view(marks);
|
|
@@ -51,7 +52,13 @@ export class TimelineService {
|
|
|
51
52
|
}
|
|
52
53
|
watch(listener) {
|
|
53
54
|
this.#listeners.add(listener);
|
|
54
|
-
|
|
55
|
+
const held = this.#runtime.hold("timeline.changed");
|
|
56
|
+
return {
|
|
57
|
+
dispose: async () => {
|
|
58
|
+
this.#listeners.delete(listener);
|
|
59
|
+
await held.dispose();
|
|
60
|
+
},
|
|
61
|
+
};
|
|
55
62
|
}
|
|
56
63
|
}
|
|
57
64
|
export const timelineOf = (runtime) => runtime.service("timeline", () => new TimelineService(runtime));
|
|
@@ -60,6 +67,7 @@ export function timeline(scope) {
|
|
|
60
67
|
return {
|
|
61
68
|
get marks() {
|
|
62
69
|
service().start();
|
|
70
|
+
scope.runtime.reading("timeline.changed");
|
|
63
71
|
return service().marks;
|
|
64
72
|
},
|
|
65
73
|
onChange(listener) {
|