@vestra-player/sdk 1.6.17 → 1.6.18

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.
Files changed (34) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/dist/docs-tree.json.gz +0 -0
  3. package/dist/plugin/runtime.d.ts +26 -0
  4. package/dist/plugin/runtime.js +16 -0
  5. package/dist/plugin/testing/commands.d.ts +65 -0
  6. package/dist/plugin/testing/commands.js +412 -0
  7. package/dist/plugin/testing/core-actions.d.ts +39 -0
  8. package/dist/plugin/testing/core-actions.js +371 -0
  9. package/dist/plugin/testing/declarations.d.ts +37 -0
  10. package/dist/plugin/testing/declarations.js +80 -0
  11. package/dist/plugin/testing/i18n.d.ts +28 -0
  12. package/dist/plugin/testing/i18n.js +107 -0
  13. package/dist/plugin/testing/index.d.ts +3140 -0
  14. package/dist/plugin/testing/index.js +2796 -0
  15. package/dist/plugin/testing/internals.d.ts +76 -0
  16. package/dist/plugin/testing/internals.js +60 -0
  17. package/dist/plugin/testing/jobs.d.ts +9 -0
  18. package/dist/plugin/testing/jobs.js +304 -0
  19. package/dist/plugin/testing/layout.d.ts +85 -0
  20. package/dist/plugin/testing/layout.js +622 -0
  21. package/dist/plugin/testing/media.d.ts +55 -0
  22. package/dist/plugin/testing/media.js +615 -0
  23. package/dist/plugin/testing/osd.d.ts +4 -0
  24. package/dist/plugin/testing/osd.js +57 -0
  25. package/dist/plugin/testing/processing.d.ts +56 -0
  26. package/dist/plugin/testing/processing.js +491 -0
  27. package/dist/plugin/testing/services.d.ts +50 -0
  28. package/dist/plugin/testing/services.js +466 -0
  29. package/dist/plugin/testing/transport.d.ts +29 -0
  30. package/dist/plugin/testing/transport.js +140 -0
  31. package/dist/plugin/testing.d.ts +1 -3140
  32. package/dist/plugin/testing.js +1 -6203
  33. package/llms.txt +1 -1
  34. package/package.json +5 -3
@@ -0,0 +1,56 @@
1
+ import type { Json, WireRenderingState, WireUpscalingState } from "../../wire.ts";
2
+ import type { UpscalingDecodingCause, UpscalingPlugin, UpscalingReason } from "../types.ts";
3
+ import type { TestHost } from "./index.ts";
4
+ import type { Handler, Internals } from "./internals.ts";
5
+ /**
6
+ * The player's `playback/plugin_files.rs`: a file the plugin names for its
7
+ * video processing is an absolute path inside one of `roots` after
8
+ * following links, a regular file of at most `max` bytes. Returns the real
9
+ * path or throws the player's error.
10
+ */
11
+ export declare function ownFile(path: string, roots: string[], what: string, max: number): string;
12
+ /** The `LUT_3D_SIZE` check of `plugin_files::lut` on the first 64 KiB. */
13
+ export declare function cubeProblem(file: string): string | null;
14
+ export declare function processingState(host: TestHost, _inner: Internals): {
15
+ configurationRevision: number;
16
+ audioOffsetMs: number;
17
+ source: {
18
+ generation: number;
19
+ phase: "empty" | "loading" | "ready" | "ended" | "error";
20
+ load: null;
21
+ stages: {};
22
+ };
23
+ video: {
24
+ offsetMs?: number | undefined;
25
+ owner: number;
26
+ revision: number;
27
+ resource: {
28
+ manifestId: string;
29
+ resourceId: string;
30
+ nodes: Json;
31
+ };
32
+ }[];
33
+ audio: {
34
+ offsetMs?: number | undefined;
35
+ owner: number;
36
+ revision: number;
37
+ resource: {
38
+ manifestId: string;
39
+ resourceId: string;
40
+ nodes: Json;
41
+ };
42
+ }[];
43
+ backendUncertain: Partial<Record<"video" | "audio", string>>;
44
+ };
45
+ /** `video.upscaling.state` from {@link TestHost.upscaling}. */
46
+ export declare function upscalingState(host: TestHost, _inner: Internals, check?: boolean): WireUpscalingState;
47
+ /** `rendering.state` from {@link TestHost.rendering} and `videoRender.gpu`. */
48
+ export declare function renderingState(host: TestHost, _inner: Internals): WireRenderingState;
49
+ /** Implements `TestHost.failVideoRender`. */
50
+ export declare function failVideoRender(host: TestHost, _inner: Internals, code: "failed" | "unsupported" | "previous-crash", message: string): void;
51
+ /** Implements `TestHost.failVideoShaders`. */
52
+ export declare function failVideoShaders(host: TestHost, _inner: Internals, id: string, message: string, code?: "failed" | "previous-crash"): void;
53
+ /** Implements `TestHost.upscalingDecoding`. */
54
+ export declare function upscalingDecoding(host: TestHost, _inner: Internals, cause: UpscalingDecodingCause, plugins?: readonly UpscalingPlugin[]): UpscalingReason;
55
+ /** The default handlers of this area's wire methods, by method name. */
56
+ export declare function processingHandlers(host: TestHost, inner: Internals): Record<string, Handler>;
@@ -0,0 +1,491 @@
1
+ /**
2
+ * Video and audio processing, video view/render/shader ownership, rendering
3
+ * and AI upscaling, with the player's validation of files and cubes.
4
+ *
5
+ * Part of `TestHost` (`./index.ts`): functions over the host and its private
6
+ * state (`Internals`, `./internals.ts`) that the class delegates to.
7
+ * Internal to the SDK; not exported from `@vestra-player/sdk/testing`.
8
+ */
9
+ import { existsSync, openSync, readSync, closeSync, realpathSync, } from "node:fs";
10
+ import { statSync } from "node:fs";
11
+ import { isAbsolute, relative } from "node:path";
12
+ import { HostError } from "../../wire.js";
13
+ import { nodeProblem, VIDEO_PROCESSING_LIMITS } from "../../processing.js";
14
+ import { shaderIdProblem, shaderListProblem } from "../shaders.js";
15
+ import { renderProblem, viewProblem } from "../view.js";
16
+ /**
17
+ * The player's `playback/plugin_files.rs`: a file the plugin names for its
18
+ * video processing is an absolute path inside one of `roots` after
19
+ * following links, a regular file of at most `max` bytes. Returns the real
20
+ * path or throws the player's error.
21
+ */
22
+ export function ownFile(path, roots, what, max) {
23
+ if (!isAbsolute(path))
24
+ throw new HostError("invalid", `${what} path must be absolute: ${path}`);
25
+ let real;
26
+ try {
27
+ real = realpathSync(path);
28
+ }
29
+ catch {
30
+ throw new HostError("missing", `${what} not found: ${path}`);
31
+ }
32
+ const inside = roots.some((root) => {
33
+ let base;
34
+ try {
35
+ base = realpathSync(root);
36
+ }
37
+ catch {
38
+ return false;
39
+ }
40
+ const rel = relative(base, real);
41
+ return rel === "" || (!/^\.\.([\\/]|$)/.test(rel) && !isAbsolute(rel));
42
+ });
43
+ if (!inside)
44
+ throw new HostError("invalid", `${what} must be in the plugin's package or data directory: ${path}`);
45
+ const stat = statSync(real);
46
+ if (!stat.isFile())
47
+ throw new HostError("invalid", `${what} is not a file: ${path}`);
48
+ if (stat.size > max)
49
+ throw new HostError("invalid", `${what} is larger than ${max / 1024 / 1024} MiB: ${path}`);
50
+ return real;
51
+ }
52
+ /** The `LUT_3D_SIZE` check of `plugin_files::lut` on the first 64 KiB. */
53
+ export function cubeProblem(file) {
54
+ const buffer = Buffer.alloc(64 * 1024);
55
+ const fd = openSync(file, "r");
56
+ let read;
57
+ try {
58
+ read = readSync(fd, buffer, 0, buffer.length, 0);
59
+ }
60
+ finally {
61
+ closeSync(fd);
62
+ }
63
+ for (const line of buffer.subarray(0, read).toString("utf8").split("\n")) {
64
+ const [word, value] = line.trim().split(/\s+/);
65
+ if (word === "LUT_3D_SIZE") {
66
+ const size = Number(value);
67
+ return Number.isInteger(size) &&
68
+ size >= 2 &&
69
+ size <= VIDEO_PROCESSING_LIMITS.lutSize
70
+ ? null
71
+ : `LUT_3D_SIZE must be 2 to ${VIDEO_PROCESSING_LIMITS.lutSize}`;
72
+ }
73
+ if (word === "LUT_1D_SIZE")
74
+ return "a 1D LUT; lut3d needs LUT_3D_SIZE";
75
+ if (word !== undefined && word !== "" && Number.isFinite(Number(word)))
76
+ break;
77
+ }
78
+ return "no LUT_3D_SIZE line before the data";
79
+ }
80
+ export function processingState(host, _inner) {
81
+ const list = (stage) => [...host.processing.resources]
82
+ .filter(([, value]) => value.stage === stage)
83
+ .map(([key, value]) => ({
84
+ owner: 1,
85
+ revision: value.revision,
86
+ resource: {
87
+ manifestId: host.manifestId,
88
+ resourceId: key.split(":")[1],
89
+ nodes: value.nodes,
90
+ },
91
+ ...(stage === "audio" ? { offsetMs: value.offsetMs ?? 0 } : {}),
92
+ }));
93
+ return {
94
+ configurationRevision: host.processing.configurationRevision,
95
+ audioOffsetMs: 0,
96
+ source: {
97
+ generation: host.source.stamp.generation,
98
+ phase: host.source.phase,
99
+ load: null,
100
+ stages: {},
101
+ },
102
+ video: list("video"),
103
+ audio: list("audio"),
104
+ backendUncertain: host.processing.backendUncertain,
105
+ };
106
+ }
107
+ /** `video.upscaling.state` from {@link TestHost.upscaling}. */
108
+ export function upscalingState(host, _inner, check = true) {
109
+ if (check)
110
+ host.upscaling.checked = true;
111
+ const { enabled, factor } = host.upscaling;
112
+ // `set(false)` before any check: the player answers without checking.
113
+ const reason = host.upscaling.checked
114
+ ? host.upscaling.reason
115
+ : {
116
+ code: "unchecked",
117
+ message: "the player has not checked the GPU yet; state() or set(true) checks it",
118
+ };
119
+ const requested = enabled && reason === null && factor !== null && factor > 1;
120
+ const { video, area, gpu, hwdecCurrent } = host.upscaling;
121
+ return {
122
+ available: reason === null,
123
+ reason: reason
124
+ ? structuredClone(reason)
125
+ : null,
126
+ enabled,
127
+ requested,
128
+ factor: requested ? factor : null,
129
+ video: video ? { ...video } : null,
130
+ area: area ? { ...area } : null,
131
+ gpu,
132
+ hwdecCurrent,
133
+ inChain: requested,
134
+ };
135
+ }
136
+ /** `rendering.state` from {@link TestHost.rendering} and `videoRender.gpu`. */
137
+ export function renderingState(host, _inner) {
138
+ const { reason, preference, startedWith, gpu } = host.rendering;
139
+ // The player's order (render_mode.rs decide): no windows or no GPU
140
+ // renderer first, then the preference `software`, then what keeps GPU
141
+ // rendering off (denylist, crash), else GPU with the reason `default`
142
+ // (also when the preference is `gpu`).
143
+ const system = !gpu.available &&
144
+ (gpu.reason?.code === "headless" || gpu.reason?.code === "no-gpu-backend");
145
+ const next = system
146
+ ? { mode: "software", reason: gpu.reason }
147
+ : preference === "software"
148
+ ? {
149
+ mode: "software",
150
+ reason: {
151
+ code: "preference",
152
+ message: "chosen in the player's settings",
153
+ },
154
+ }
155
+ : !gpu.available
156
+ ? {
157
+ mode: "software",
158
+ reason: gpu.reason ?? {
159
+ code: "default",
160
+ message: "nothing rules GPU rendering out",
161
+ },
162
+ }
163
+ : {
164
+ mode: "gpu",
165
+ reason: {
166
+ code: "default",
167
+ message: "nothing rules GPU rendering out",
168
+ },
169
+ };
170
+ return {
171
+ mode: host.videoRender.gpu ? "gpu" : "software",
172
+ reason: { ...reason },
173
+ preference,
174
+ nextMode: next.mode,
175
+ nextReason: { ...next.reason },
176
+ restartPending: next.mode !== startedWith,
177
+ gpu: {
178
+ available: gpu.available,
179
+ reason: gpu.available || !gpu.reason ? null : { ...gpu.reason },
180
+ },
181
+ };
182
+ }
183
+ /** Implements `TestHost.failVideoRender`. */
184
+ export function failVideoRender(host, _inner, code, message) {
185
+ if (code === "unsupported") {
186
+ host.videoRender.gpu = false;
187
+ host.rendering.reason = { code: "window-fallback", message };
188
+ }
189
+ if (host.videoRender.owner === host.manifestId)
190
+ host.emit("video.render.status", {
191
+ state: "failed",
192
+ error: { code, message },
193
+ });
194
+ // The player tells every shader handle too (`failVideoShaders`).
195
+ // and, like the player, removes their lists.
196
+ if (code === "unsupported") {
197
+ for (const [id, paths] of host.videoShaders.handles)
198
+ if (paths.length > 0)
199
+ host.emit("video.shaders.status", {
200
+ id,
201
+ state: "failed",
202
+ error: { code, message },
203
+ });
204
+ host.videoShaders.handles.clear();
205
+ }
206
+ }
207
+ /** Implements `TestHost.failVideoShaders`. */
208
+ export function failVideoShaders(host, _inner, id, message, code = "failed") {
209
+ if ((host.videoShaders.handles.get(id)?.length ?? 0) > 0)
210
+ host.emit("video.shaders.status", {
211
+ id,
212
+ state: "failed",
213
+ error: { code, message },
214
+ });
215
+ }
216
+ /** Implements `TestHost.upscalingDecoding`. */
217
+ export function upscalingDecoding(host, _inner, cause, plugins = []) {
218
+ const needs = "upscaling needs it decoded on the GPU that renders it (d3d11va)";
219
+ const names = plugins.map((p) => p.name);
220
+ const because = names.length === 0
221
+ ? "for example because a video filter needs the frames in memory"
222
+ : names.length === 1
223
+ ? `because the video filter of ${names[0]} needs the frames in memory`
224
+ : `because the video filters of ${names.join(", ")} need the frames in memory`;
225
+ const [message, current] = cause === "copy-back"
226
+ ? [
227
+ `the video is decoded with copy-back (auto-copy-safe), ${because}; ${needs}`,
228
+ "d3d11va-copy",
229
+ ]
230
+ : cause === "hwdec-off"
231
+ ? [
232
+ "hardware decoding is off; upscaling needs the video decoded on the GPU that renders it (d3d11va)",
233
+ "no",
234
+ ]
235
+ : cause === "software"
236
+ ? [`this video is decoded in software; ${needs}`, "no"]
237
+ : [`this video is decoded with nvdec; ${needs}`, "nvdec"];
238
+ const reason = cause === "copy-back"
239
+ ? {
240
+ code: "decoding",
241
+ message,
242
+ cause,
243
+ plugins: plugins.map(({ id, name }) => ({ id, name })),
244
+ }
245
+ : { code: "decoding", message, cause };
246
+ host.upscaling.reason = reason;
247
+ host.upscaling.hwdecCurrent = current;
248
+ return reason;
249
+ }
250
+ /** The default handlers of this area's wire methods, by method name. */
251
+ export function processingHandlers(host, inner) {
252
+ return {
253
+ // As the player 0.5.13 (see TestHost.rendering for the model).
254
+ "rendering.state": () => renderingState(host, inner),
255
+ "rendering.setPreference": (p) => {
256
+ const preference = p.preference;
257
+ if (preference !== "auto" &&
258
+ preference !== "gpu" &&
259
+ preference !== "software")
260
+ throw new HostError("invalid", 'preference must be "auto", "gpu" or "software"');
261
+ const { available, reason } = host.rendering.gpu;
262
+ if (preference === "gpu" && !available)
263
+ throw new HostError(reason?.code === "no-gpu-backend" || reason?.code === "headless"
264
+ ? "unsupported"
265
+ : "unavailable", reason?.message ?? "GPU rendering cannot be chosen");
266
+ host.rendering.preference = preference;
267
+ return renderingState(host, inner);
268
+ },
269
+ "rendering.restart": () => {
270
+ if (host.rendering.restarts > 0)
271
+ throw new HostError("busy", "a restart is already under way");
272
+ host.rendering.restarts++;
273
+ return null;
274
+ },
275
+ // As the player 0.5.16 (see TestHost.upscaling for the model).
276
+ "video.upscaling.state": () => upscalingState(host, inner),
277
+ "video.upscaling.set": (p) => {
278
+ if (typeof p.enabled !== "boolean")
279
+ throw new HostError("invalid", "enabled must be a boolean");
280
+ const { reason, renderingStarting } = host.upscaling;
281
+ // Off: always, without the GPU check.
282
+ if (!p.enabled) {
283
+ host.upscaling.enabled = false;
284
+ return upscalingState(host, inner, false);
285
+ }
286
+ const passes = reason?.code === "decoding" ||
287
+ (reason?.code === "rendering" && renderingStarting);
288
+ if (reason && !passes) {
289
+ host.upscaling.checked = true;
290
+ throw new HostError("unsupported", `video upscaling cannot work here: ${reason.message}`);
291
+ }
292
+ host.upscaling.enabled = true;
293
+ return upscalingState(host, inner);
294
+ },
295
+ "video.view.set": (p) => {
296
+ const owner = host.videoView.owner;
297
+ if (owner !== null && owner !== host.manifestId)
298
+ throw new HostError("conflict", "another plugin owns the display layer");
299
+ const problem = viewProblem(p.values);
300
+ if (problem)
301
+ throw new HostError("invalid", problem);
302
+ host.videoView.owner = host.manifestId;
303
+ host.videoView.values = {
304
+ ...host.videoView.values,
305
+ ...p.values,
306
+ };
307
+ return { state: host.source.phase === "ready" ? "applied" : "waiting" };
308
+ },
309
+ "video.view.release": () => {
310
+ if (host.videoView.owner === host.manifestId)
311
+ host.setVideoViewOwner(null);
312
+ return null;
313
+ },
314
+ "video.render.set": (p) => {
315
+ // The player's crash guard comes first (`processing_video.rs`).
316
+ const listed = p.values?.shaders;
317
+ if (host.previousCrash !== null &&
318
+ Array.isArray(listed) &&
319
+ listed.length > 0)
320
+ throw new HostError("previous-crash", host.previousCrash);
321
+ if (!host.videoRender.gpu)
322
+ throw new HostError("unsupported", "render options need GPU rendering; the player renders in software");
323
+ const owner = host.videoRender.owner;
324
+ if (owner !== null && owner !== host.manifestId)
325
+ throw new HostError("conflict", "another plugin owns the render options");
326
+ const problem = renderProblem(p.values);
327
+ if (problem)
328
+ throw new HostError("invalid", problem);
329
+ const values = p.values;
330
+ const missing = values.shaders?.find((path) => !existsSync(path));
331
+ if (missing !== undefined)
332
+ throw new HostError("invalid", `shader file does not exist: ${missing}`);
333
+ host.videoRender.owner = host.manifestId;
334
+ host.videoRender.values = { ...host.videoRender.values, ...values };
335
+ return { state: host.source.phase === "ready" ? "applied" : "waiting" };
336
+ },
337
+ "video.render.release": () => {
338
+ if (host.videoRender.owner === host.manifestId)
339
+ host.setVideoRenderOwner(null);
340
+ return null;
341
+ },
342
+ // The player's capability layer and `video_shaders.rs`.
343
+ "video.shaders.set": (p) => {
344
+ const idProblem = shaderIdProblem(p.id);
345
+ if (idProblem)
346
+ throw new HostError("invalid", idProblem);
347
+ if (!Array.isArray(p.paths) ||
348
+ p.paths.some((path) => typeof path !== "string"))
349
+ throw new HostError("invalid", "paths must be an array of paths");
350
+ const id = p.id;
351
+ const list = p.paths;
352
+ const listProblem = shaderListProblem(list);
353
+ if (listProblem?.startsWith("at most"))
354
+ throw new HostError("limit", listProblem);
355
+ const paths = list.map((path) => ownFile(path, host.fileRoots, "shader", VIDEO_PROCESSING_LIMITS.shaderBytes));
356
+ // The player's crash guard, after the files are checked.
357
+ if (paths.length > 0 && host.previousCrash !== null)
358
+ throw new HostError("previous-crash", host.previousCrash);
359
+ if (paths.length > 0 && !host.videoRender.gpu)
360
+ throw new HostError("unsupported", "shaders need GPU rendering; the player renders in software");
361
+ const mine = [...host.videoShaders.handles]
362
+ .filter(([other]) => other !== id)
363
+ .reduce((sum, [, held]) => sum + held.length, 0);
364
+ const others = mine + host.videoShaders.others;
365
+ if (others + paths.length > VIDEO_PROCESSING_LIMITS.shaders)
366
+ throw new HostError("limit", `at most ${VIDEO_PROCESSING_LIMITS.shaders} shaders across all plugins: ${others} are in use, this list has ${paths.length}`);
367
+ if (paths.length > 0)
368
+ host.videoShaders.handles.set(id, paths);
369
+ else
370
+ host.videoShaders.handles.delete(id);
371
+ return { state: host.source.phase === "ready" ? "applied" : "waiting" };
372
+ },
373
+ "processing.state": () => processingState(host, inner),
374
+ "processing.replace": (p) => {
375
+ const stage = String(p.stage);
376
+ const key = `${stage}:${p.resourceId}`;
377
+ const token = `${key}:${p.revision}`;
378
+ const finish = (outcome) => {
379
+ host.processing.book.set(token, outcome);
380
+ if (outcome.error)
381
+ throw new HostError(outcome.error.code, outcome.error.message);
382
+ return outcome.result;
383
+ };
384
+ if (host.processing.backendUncertain[stage])
385
+ return finish({
386
+ status: "failed",
387
+ error: {
388
+ code: "backend-uncertain",
389
+ message: host.processing.backendUncertain[stage],
390
+ },
391
+ });
392
+ // The same node and delay rules as the player.
393
+ const problem = nodeProblem(stage, (p.nodes ?? []));
394
+ // frameBlend: at most one across every chain (this plugin's others too).
395
+ const blends = [...host.processing.resources]
396
+ .filter(([other]) => other !== key)
397
+ .flatMap(([, resource]) => resource.nodes)
398
+ .concat((p.nodes ?? []))
399
+ .filter((node) => node.kind === "frameBlend").length;
400
+ const blend = !problem && blends > 1
401
+ ? "only one frameBlend node across all plugins"
402
+ : null;
403
+ const offset = Number(p.offsetMs ?? 0);
404
+ const delay = stage === "audio" &&
405
+ (!Number.isInteger(offset) ||
406
+ Math.abs(offset) > 10_000 ||
407
+ offset % 10 !== 0)
408
+ ? "audio offsetMs must be -10000..10000 in 10ms increments"
409
+ : null;
410
+ if (problem || blend || delay)
411
+ return finish({
412
+ status: "failed",
413
+ error: { code: "invalid", message: (problem ?? blend ?? delay) },
414
+ });
415
+ // `lut3d` files: the plugin's own `.cube` files (`plugin_files.rs`).
416
+ if (stage === "video")
417
+ for (const node of (p.nodes ?? []))
418
+ if (node.kind === "lut3d") {
419
+ let cube;
420
+ try {
421
+ const real = ownFile(node.file, host.fileRoots, "lut3d file", VIDEO_PROCESSING_LIMITS.lutBytes);
422
+ cube = /\.cube$/i.test(real)
423
+ ? cubeProblem(real)
424
+ : "lut3d file must be a .cube file";
425
+ }
426
+ catch (error) {
427
+ const { code, message } = error;
428
+ return finish({ status: "failed", error: { code, message } });
429
+ }
430
+ if (cube)
431
+ return finish({
432
+ status: "failed",
433
+ error: {
434
+ code: "invalid",
435
+ message: `lut3d file ${node.file}: ${cube}`,
436
+ },
437
+ });
438
+ }
439
+ if (host.source.phase !== "ready")
440
+ return finish({
441
+ status: "failed",
442
+ error: { code: "unavailable", message: "source must be ready" },
443
+ });
444
+ // An empty chain removes the resource (audio: with no delay either).
445
+ const removal = (p.nodes ?? []).length === 0 &&
446
+ (stage === "video" || offset === 0);
447
+ // A generated stream's frames never reach the player's filters.
448
+ if (host.source.kind === "producer" && !removal)
449
+ return finish({
450
+ status: "failed",
451
+ error: {
452
+ code: "unsupported",
453
+ message: "generated streams (openStream) do not pass through the player's filters; the chain applies to the next media the player decodes",
454
+ },
455
+ });
456
+ if (p.expectedGeneration !== host.source.stamp.generation)
457
+ return finish({
458
+ status: "failed",
459
+ error: { code: "stale", message: "source generation changed" },
460
+ });
461
+ const old = host.processing.resources.get(key);
462
+ if (old && Number(p.revision) <= old.revision)
463
+ return finish({
464
+ status: "failed",
465
+ error: { code: "stale", message: "resource revision must increase" },
466
+ });
467
+ if (removal)
468
+ host.processing.resources.delete(key);
469
+ else
470
+ host.processing.resources.set(key, {
471
+ stage,
472
+ revision: Number(p.revision),
473
+ nodes: p.nodes,
474
+ ...(stage === "audio" ? { offsetMs: Number(p.offsetMs ?? 0) } : {}),
475
+ });
476
+ host.processing.configurationRevision++;
477
+ return finish({
478
+ status: "completed",
479
+ result: {
480
+ stage,
481
+ revision: p.revision,
482
+ generation: host.source.stamp.generation,
483
+ status: "applied",
484
+ },
485
+ });
486
+ },
487
+ "processing.operation": (p) => host.processing.book.get(`${p.stage}:${p.resourceId}:${p.revision}`) ?? {
488
+ status: "unknown",
489
+ },
490
+ };
491
+ }
@@ -0,0 +1,50 @@
1
+ import type { Json } from "../../wire.ts";
2
+ import type { TestHost } from "./index.ts";
3
+ import type { Handler, Internals } from "./internals.ts";
4
+ /** A `plugin.storage.session` key, checked as the player does. */
5
+ export declare function sessionKey(key: Json | undefined): string;
6
+ /** Code point (UTF-8 byte) order, as the player sorts session keys. */
7
+ export declare const byCodePoint: (a: string, b: string) => 0 | 1 | -1;
8
+ /**
9
+ * A JSON value as the player hands it back: the player does not keep the
10
+ * order of object properties, it sorts them.
11
+ */
12
+ export declare function sortedJson(value: Json): Json;
13
+ /** What one session entry counts towards the player's quota. */
14
+ export declare const sessionCost: (key: string, value: Json) => number;
15
+ export declare function validatePackage(host: TestHost, _inner: Internals, path: string): {
16
+ token: string;
17
+ digest: string;
18
+ apiVersion: string;
19
+ manifest: import("../types.ts").PackageManifest;
20
+ checks: string[];
21
+ trustedExecution: boolean;
22
+ };
23
+ /** Implements `TestHost.drop`. */
24
+ export declare function drop(host: TestHost, _inner: Internals, paths: string[], options?: {
25
+ target?: "video" | "panel";
26
+ at?: {
27
+ x: number;
28
+ y: number;
29
+ };
30
+ modifiers?: Partial<{
31
+ alt: boolean;
32
+ shift: boolean;
33
+ cmd: boolean;
34
+ }>;
35
+ }): Promise<void>;
36
+ /** Implements `TestHost.setMainWindow`. */
37
+ export declare function setMainWindow(host: TestHost, inner: Internals, patch: Partial<{
38
+ fullscreen: boolean;
39
+ maximized: boolean;
40
+ videoArea: {
41
+ width: number;
42
+ height: number;
43
+ };
44
+ pictureInPicture: {
45
+ supported: boolean;
46
+ active: boolean;
47
+ };
48
+ }>): void;
49
+ /** The default handlers of this area's wire methods, by method name. */
50
+ export declare function servicesHandlers(host: TestHost, inner: Internals): Record<string, Handler>;