@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.
- package/CHANGELOG.md +13 -0
- package/dist/docs-tree.json.gz +0 -0
- package/dist/plugin/runtime.d.ts +26 -0
- package/dist/plugin/runtime.js +16 -0
- package/dist/plugin/testing/commands.d.ts +65 -0
- package/dist/plugin/testing/commands.js +412 -0
- package/dist/plugin/testing/core-actions.d.ts +39 -0
- package/dist/plugin/testing/core-actions.js +371 -0
- package/dist/plugin/testing/declarations.d.ts +37 -0
- package/dist/plugin/testing/declarations.js +80 -0
- package/dist/plugin/testing/i18n.d.ts +28 -0
- package/dist/plugin/testing/i18n.js +107 -0
- package/dist/plugin/testing/index.d.ts +3140 -0
- package/dist/plugin/testing/index.js +2796 -0
- package/dist/plugin/testing/internals.d.ts +76 -0
- package/dist/plugin/testing/internals.js +60 -0
- package/dist/plugin/testing/jobs.d.ts +9 -0
- package/dist/plugin/testing/jobs.js +304 -0
- package/dist/plugin/testing/layout.d.ts +85 -0
- package/dist/plugin/testing/layout.js +622 -0
- package/dist/plugin/testing/media.d.ts +55 -0
- package/dist/plugin/testing/media.js +615 -0
- package/dist/plugin/testing/osd.d.ts +4 -0
- package/dist/plugin/testing/osd.js +57 -0
- package/dist/plugin/testing/processing.d.ts +56 -0
- package/dist/plugin/testing/processing.js +491 -0
- package/dist/plugin/testing/services.d.ts +50 -0
- package/dist/plugin/testing/services.js +466 -0
- package/dist/plugin/testing/transport.d.ts +29 -0
- package/dist/plugin/testing/transport.js +140 -0
- package/dist/plugin/testing.d.ts +1 -3140
- package/dist/plugin/testing.js +1 -6203
- package/llms.txt +1 -1
- package/package.json +5 -3
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type { Json } from "../../wire.ts";
|
|
2
|
+
import type { SourceStamp } from "../../source.ts";
|
|
3
|
+
import type { Locale } from "../types.ts";
|
|
4
|
+
import { Catalog } from "../../i18n.ts";
|
|
5
|
+
import type { TestHost } from "./index.ts";
|
|
6
|
+
/** A wire method's default or overriding handler (as `TestHost.handle` takes it). */
|
|
7
|
+
export type Handler = Parameters<TestHost["handle"]>[1];
|
|
8
|
+
/** The private state of one `TestHost`. */
|
|
9
|
+
export declare class Internals {
|
|
10
|
+
locale: Locale | undefined;
|
|
11
|
+
/** The plugin's translations for `locale` (see `loadCatalog`). */
|
|
12
|
+
catalog: Catalog;
|
|
13
|
+
/**
|
|
14
|
+
* Row patches (player 0.6.0): the `seq` of each property's last write, by
|
|
15
|
+
* view and property (none: 0, the initial properties).
|
|
16
|
+
*/
|
|
17
|
+
readonly writes: Map<string, number>;
|
|
18
|
+
/** The parts of a whole value held until its last part, by view and property. */
|
|
19
|
+
readonly parts: Map<string, {
|
|
20
|
+
seq: number;
|
|
21
|
+
rows: Json[];
|
|
22
|
+
}>;
|
|
23
|
+
nextOwner: number;
|
|
24
|
+
/** Owners of other plugins' panels, by plugin id ({@link TestHost.mountOther}). */
|
|
25
|
+
readonly others: Map<string, {
|
|
26
|
+
slot: string;
|
|
27
|
+
owner: number;
|
|
28
|
+
}>;
|
|
29
|
+
/** What each job was asked to write, for its outcome. */
|
|
30
|
+
readonly exportJobs: Map<number, {
|
|
31
|
+
destination: string;
|
|
32
|
+
clip: {
|
|
33
|
+
start: number;
|
|
34
|
+
end: number;
|
|
35
|
+
container: "mp4" | "mkv" | null;
|
|
36
|
+
} | null;
|
|
37
|
+
}>;
|
|
38
|
+
sentTypes: string;
|
|
39
|
+
readonly overrides: Map<string, (params: Record<string, Json>) => unknown>;
|
|
40
|
+
readonly listeners: Map<string, Set<(data: unknown) => void>>;
|
|
41
|
+
inFlight: number;
|
|
42
|
+
/** `disconnect()` of `startPlugin`: the player is gone. */
|
|
43
|
+
lost: boolean;
|
|
44
|
+
lose: (error: Error) => void;
|
|
45
|
+
/** Rejects when the player is lost: requests in flight fail with it. */
|
|
46
|
+
readonly gone: Promise<never>;
|
|
47
|
+
next: {
|
|
48
|
+
snapshot: number;
|
|
49
|
+
job: number;
|
|
50
|
+
producer: number;
|
|
51
|
+
source: number;
|
|
52
|
+
resolution: number;
|
|
53
|
+
};
|
|
54
|
+
sourceBook: Map<number, Json>;
|
|
55
|
+
/** The pending resolutions' stamps and operations (beside `resolutions`). */
|
|
56
|
+
resolving: Map<number, {
|
|
57
|
+
stamp: SourceStamp;
|
|
58
|
+
operation?: number;
|
|
59
|
+
}>;
|
|
60
|
+
/** The producer the source is (`openProvider`, a resolved stream). */
|
|
61
|
+
selected: number | null;
|
|
62
|
+
readonly scratch: string;
|
|
63
|
+
/** The labels `commands.keys` last answered or pushed, as JSON. */
|
|
64
|
+
keysSent: string;
|
|
65
|
+
/** The binding table revision of `commands.keys` (a counter here). */
|
|
66
|
+
keysRevision: number;
|
|
67
|
+
/**
|
|
68
|
+
* The resources this plugin loaded into the current source (resource id →
|
|
69
|
+
* bytes), removed ones included: like the player, a removed track frees
|
|
70
|
+
* its place among the live tracks but not among the loads.
|
|
71
|
+
*/
|
|
72
|
+
subtitleResources: {
|
|
73
|
+
generation: number;
|
|
74
|
+
bytes: Map<string, number>;
|
|
75
|
+
};
|
|
76
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The private state of a `TestHost` (`./index.ts`): what the class keeps
|
|
3
|
+
* out of its public members. The host holds one in its private `#inner`
|
|
4
|
+
* field and passes it to the area modules' functions, so it is reachable
|
|
5
|
+
* only from inside the SDK.
|
|
6
|
+
*/
|
|
7
|
+
import { mkdtempSync } from "node:fs";
|
|
8
|
+
import { tmpdir } from "node:os";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
import { Catalog } from "../../i18n.js";
|
|
11
|
+
/** The private state of one `TestHost`. */
|
|
12
|
+
export class Internals {
|
|
13
|
+
locale;
|
|
14
|
+
/** The plugin's translations for `locale` (see `loadCatalog`). */
|
|
15
|
+
catalog = new Catalog();
|
|
16
|
+
/**
|
|
17
|
+
* Row patches (player 0.6.0): the `seq` of each property's last write, by
|
|
18
|
+
* view and property (none: 0, the initial properties).
|
|
19
|
+
*/
|
|
20
|
+
writes = new Map();
|
|
21
|
+
/** The parts of a whole value held until its last part, by view and property. */
|
|
22
|
+
parts = new Map();
|
|
23
|
+
nextOwner = 2;
|
|
24
|
+
/** Owners of other plugins' panels, by plugin id ({@link TestHost.mountOther}). */
|
|
25
|
+
others = new Map();
|
|
26
|
+
/** What each job was asked to write, for its outcome. */
|
|
27
|
+
exportJobs = new Map();
|
|
28
|
+
sentTypes = "[]";
|
|
29
|
+
overrides = new Map();
|
|
30
|
+
listeners = new Map();
|
|
31
|
+
inFlight = 0;
|
|
32
|
+
/** `disconnect()` of `startPlugin`: the player is gone. */
|
|
33
|
+
lost = false;
|
|
34
|
+
lose = () => { };
|
|
35
|
+
/** Rejects when the player is lost: requests in flight fail with it. */
|
|
36
|
+
gone = (() => {
|
|
37
|
+
const gone = new Promise((_, reject) => {
|
|
38
|
+
this.lose = reject;
|
|
39
|
+
});
|
|
40
|
+
gone.catch(() => { });
|
|
41
|
+
return gone;
|
|
42
|
+
})();
|
|
43
|
+
next = { snapshot: 0, job: 0, producer: 0, source: 0, resolution: 0 };
|
|
44
|
+
sourceBook = new Map();
|
|
45
|
+
/** The pending resolutions' stamps and operations (beside `resolutions`). */
|
|
46
|
+
resolving = new Map();
|
|
47
|
+
/** The producer the source is (`openProvider`, a resolved stream). */
|
|
48
|
+
selected = null;
|
|
49
|
+
scratch = mkdtempSync(join(tmpdir(), "vestra-test-host-"));
|
|
50
|
+
/** The labels `commands.keys` last answered or pushed, as JSON. */
|
|
51
|
+
keysSent = "";
|
|
52
|
+
/** The binding table revision of `commands.keys` (a counter here). */
|
|
53
|
+
keysRevision = 0;
|
|
54
|
+
/**
|
|
55
|
+
* The resources this plugin loaded into the current source (resource id →
|
|
56
|
+
* bytes), removed ones included: like the player, a removed track frees
|
|
57
|
+
* its place among the live tracks but not among the loads.
|
|
58
|
+
*/
|
|
59
|
+
subtitleResources = { generation: -1, bytes: new Map() };
|
|
60
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Json } from "../../wire.ts";
|
|
2
|
+
import type { TestHost } from "./index.ts";
|
|
3
|
+
import type { Handler, Internals } from "./internals.ts";
|
|
4
|
+
/** `jobs.start`'s `encoding`, checked against the player's sets. */
|
|
5
|
+
export declare function validEncoding(value: Json | undefined): boolean;
|
|
6
|
+
/** Implements `TestHost.failStream`. */
|
|
7
|
+
export declare function failStream(host: TestHost, inner: Internals, producerId: number, message: string): void;
|
|
8
|
+
/** The default handlers of this area's wire methods, by method name. */
|
|
9
|
+
export declare function jobsHandlers(host: TestHost, inner: Internals): Record<string, Handler>;
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Snapshots, export jobs and stream producers (`snapshot.*`, `jobs.*`,
|
|
3
|
+
* `producer.*`).
|
|
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 { createServer } from "node:net";
|
|
10
|
+
import { randomBytes } from "node:crypto";
|
|
11
|
+
import { HostError } from "../../wire.js";
|
|
12
|
+
import { localSocketPath } from "../local.js";
|
|
13
|
+
import { isCurrentProducer } from "./media.js";
|
|
14
|
+
/** `jobs.start`'s `encoding`, checked against the player's sets. */
|
|
15
|
+
export function validEncoding(value) {
|
|
16
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
17
|
+
return false;
|
|
18
|
+
const sets = {
|
|
19
|
+
crf: Array.from({ length: 17 }, (_, i) => i + 14),
|
|
20
|
+
speed: ["faster", "balanced", "smaller"],
|
|
21
|
+
maxHeight: [1080, 720, 480],
|
|
22
|
+
fps: [60, 30],
|
|
23
|
+
audioBitrate: [128, 192, 256],
|
|
24
|
+
threads: ["single", "auto"],
|
|
25
|
+
videoBitrate: ["source"],
|
|
26
|
+
};
|
|
27
|
+
const fields = value;
|
|
28
|
+
if (fields.crf !== undefined && fields.videoBitrate !== undefined)
|
|
29
|
+
return false;
|
|
30
|
+
return Object.entries(value).every(([key, v]) => sets[key]?.includes(v) ?? false);
|
|
31
|
+
}
|
|
32
|
+
/** Implements `TestHost.failStream`. */
|
|
33
|
+
export function failStream(host, inner, producerId, message) {
|
|
34
|
+
const producer = host.producers.get(producerId);
|
|
35
|
+
if (!producer || !isCurrentProducer(host, inner, producerId))
|
|
36
|
+
throw new Error(`producer ${producerId} is not the player's source`);
|
|
37
|
+
host.setSource({
|
|
38
|
+
phase: "error",
|
|
39
|
+
terminalReason: { failed: message },
|
|
40
|
+
});
|
|
41
|
+
producer.retired = true;
|
|
42
|
+
}
|
|
43
|
+
/** The default handlers of this area's wire methods, by method name. */
|
|
44
|
+
export function jobsHandlers(host, inner) {
|
|
45
|
+
return {
|
|
46
|
+
"snapshot.capture": (p) => {
|
|
47
|
+
const expected = p.expected;
|
|
48
|
+
if (expected.generation !== host.source.stamp.generation ||
|
|
49
|
+
expected.intentVersion !== host.source.stamp.intentVersion)
|
|
50
|
+
throw new HostError("stale", "source changed");
|
|
51
|
+
const snapshotId = ++inner.next.snapshot;
|
|
52
|
+
const snapshot = {
|
|
53
|
+
snapshotId,
|
|
54
|
+
kind: p.mode === "processed" ? "processed" : "raw",
|
|
55
|
+
sourceStamp: expected,
|
|
56
|
+
time: Number(p.time ?? host.source.position ?? 0),
|
|
57
|
+
source: host.source.source ?? "",
|
|
58
|
+
duration: host.source.duration ?? 0,
|
|
59
|
+
audio: null,
|
|
60
|
+
...(p.mode === "processed"
|
|
61
|
+
? {
|
|
62
|
+
presentation: { width: 1920, height: 1080 },
|
|
63
|
+
processing: { revision: host.processing.configurationRevision },
|
|
64
|
+
}
|
|
65
|
+
: {}),
|
|
66
|
+
};
|
|
67
|
+
host.snapshots.set(snapshotId, snapshot);
|
|
68
|
+
return snapshot;
|
|
69
|
+
},
|
|
70
|
+
"snapshot.state": (p) => host.snapshots.get(Number(p.snapshotId)) ?? null,
|
|
71
|
+
"snapshot.release": (p) => void host.snapshots.delete(Number(p.snapshotId)),
|
|
72
|
+
"jobs.start": (p) => {
|
|
73
|
+
if (!host.snapshots.has(Number(p.snapshotId)))
|
|
74
|
+
throw new HostError("missing", "snapshot released");
|
|
75
|
+
const kind = String(p.kind);
|
|
76
|
+
const snapshot = host.snapshots.get(Number(p.snapshotId));
|
|
77
|
+
const clip = /^(rawMp4|processedMp4|rawCopy)$/.test(kind);
|
|
78
|
+
if (!(/^(raw|processed)(Png|Jpeg|Bmp)$/.test(kind) || clip) ||
|
|
79
|
+
(p.quality !== undefined && !kind.endsWith("Jpeg")) ||
|
|
80
|
+
(p.encoding !== undefined &&
|
|
81
|
+
(!kind.endsWith("Mp4") || !validEncoding(p.encoding))) ||
|
|
82
|
+
(p.container !== undefined &&
|
|
83
|
+
(kind !== "rawCopy" ||
|
|
84
|
+
!["auto", "mp4", "mkv"].includes(String(p.container)))) ||
|
|
85
|
+
(clip && typeof p.end !== "number") ||
|
|
86
|
+
!kind.startsWith(snapshot.kind))
|
|
87
|
+
throw new HostError("invalid", `unknown job kind ${p.kind}`);
|
|
88
|
+
host.exports.push(structuredClone(p));
|
|
89
|
+
const jobId = ++inner.next.job;
|
|
90
|
+
host.jobs.set(jobId, {
|
|
91
|
+
jobId,
|
|
92
|
+
status: "queued",
|
|
93
|
+
publicationStarted: false,
|
|
94
|
+
facts: null,
|
|
95
|
+
outcome: null,
|
|
96
|
+
progress: null,
|
|
97
|
+
});
|
|
98
|
+
inner.exportJobs.set(jobId, {
|
|
99
|
+
destination: String(p.destination),
|
|
100
|
+
// A copy starts 1 s early (on a "keyframe"), the player's own way of
|
|
101
|
+
// saying the start moved; a re-encode starts on the frame.
|
|
102
|
+
clip: clip
|
|
103
|
+
? {
|
|
104
|
+
start: kind === "rawCopy"
|
|
105
|
+
? Math.max(0, snapshot.time - 1)
|
|
106
|
+
: snapshot.time,
|
|
107
|
+
end: Number(p.end),
|
|
108
|
+
container: kind !== "rawCopy"
|
|
109
|
+
? null
|
|
110
|
+
: p.container === "mkv"
|
|
111
|
+
? "mkv"
|
|
112
|
+
: "mp4",
|
|
113
|
+
}
|
|
114
|
+
: null,
|
|
115
|
+
});
|
|
116
|
+
return { jobId, status: "queued", snapshotId: p.snapshotId };
|
|
117
|
+
},
|
|
118
|
+
// Each read advances a job one step: queued → running → completed. A
|
|
119
|
+
// clip reports half its progress while running.
|
|
120
|
+
"jobs.state": (p) => {
|
|
121
|
+
const job = host.jobs.get(Number(p.jobId));
|
|
122
|
+
if (!job)
|
|
123
|
+
throw new HostError("missing", "job");
|
|
124
|
+
const current = structuredClone(job);
|
|
125
|
+
const made = inner.exportJobs.get(job.jobId);
|
|
126
|
+
const clip = made?.clip;
|
|
127
|
+
if (job.status === "queued") {
|
|
128
|
+
job.status = "running";
|
|
129
|
+
if (clip) {
|
|
130
|
+
const total = clip.end - clip.start;
|
|
131
|
+
job.progress = { done: total / 2, total };
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
else if (job.status === "running" && host.exportFailure)
|
|
135
|
+
Object.assign(job, {
|
|
136
|
+
status: "failed",
|
|
137
|
+
outcome: { error: { ...host.exportFailure } },
|
|
138
|
+
});
|
|
139
|
+
else if (job.status === "running") {
|
|
140
|
+
const destination = clip?.container === "mkv"
|
|
141
|
+
? made.destination.replace(/(\.(mp4|m4v|mov|mkv|webm))?$/i, ".mkv")
|
|
142
|
+
: clip?.container === "mp4"
|
|
143
|
+
? made.destination.replace(/(\.(mp4|m4v|mov|mkv|webm))?$/i, ".mp4")
|
|
144
|
+
: "/tmp/output";
|
|
145
|
+
Object.assign(job, {
|
|
146
|
+
status: "completed",
|
|
147
|
+
publicationStarted: true,
|
|
148
|
+
outcome: clip
|
|
149
|
+
? { destination, start: clip.start, end: clip.end }
|
|
150
|
+
: { destination },
|
|
151
|
+
...(job.progress
|
|
152
|
+
? {
|
|
153
|
+
progress: {
|
|
154
|
+
done: job.progress.total,
|
|
155
|
+
total: job.progress.total,
|
|
156
|
+
},
|
|
157
|
+
}
|
|
158
|
+
: {}),
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
else if (job.status === "cancelling")
|
|
162
|
+
Object.assign(job, {
|
|
163
|
+
status: "cancelled",
|
|
164
|
+
outcome: { cancelled: true },
|
|
165
|
+
});
|
|
166
|
+
return current;
|
|
167
|
+
},
|
|
168
|
+
"jobs.cancel": (p) => {
|
|
169
|
+
const job = host.jobs.get(Number(p.jobId));
|
|
170
|
+
if (!job)
|
|
171
|
+
throw new HostError("missing", "job");
|
|
172
|
+
if (["completed", "failed", "cancelled"].includes(job.status))
|
|
173
|
+
return { jobId: job.jobId, cancel: "terminal" };
|
|
174
|
+
job.status = "cancelling";
|
|
175
|
+
return { jobId: job.jobId, cancel: "requested" };
|
|
176
|
+
},
|
|
177
|
+
"jobs.operation": () => ({ status: "unknown" }),
|
|
178
|
+
"producer.prepare": async () => {
|
|
179
|
+
const id = ++inner.next.producer;
|
|
180
|
+
const tcp = host.producerTransport === "tcp";
|
|
181
|
+
const token = tcp ? randomBytes(32) : null;
|
|
182
|
+
// A Unix socket, or a named pipe on Windows (`localSocketPath`).
|
|
183
|
+
const path = localSocketPath(inner.scratch, `producer-${id}`);
|
|
184
|
+
const producer = {
|
|
185
|
+
id,
|
|
186
|
+
receivedFrames: 0,
|
|
187
|
+
videoFrames: 0,
|
|
188
|
+
released: false,
|
|
189
|
+
retired: false,
|
|
190
|
+
droppedWhileSelected: false,
|
|
191
|
+
path,
|
|
192
|
+
admitted: !tcp,
|
|
193
|
+
epoch: 1,
|
|
194
|
+
pending: null,
|
|
195
|
+
acknowledged: true,
|
|
196
|
+
active: false,
|
|
197
|
+
server: createServer(),
|
|
198
|
+
};
|
|
199
|
+
producer.server.on("connection", (socket) => {
|
|
200
|
+
let buffer = Buffer.alloc(0);
|
|
201
|
+
let admitted = !token;
|
|
202
|
+
socket.on("data", (data) => {
|
|
203
|
+
buffer = Buffer.concat([buffer, data]);
|
|
204
|
+
if (!admitted && token) {
|
|
205
|
+
if (buffer.length < token.length)
|
|
206
|
+
return;
|
|
207
|
+
if (!buffer.subarray(0, token.length).equals(token))
|
|
208
|
+
return void socket.destroy();
|
|
209
|
+
admitted = producer.admitted = true;
|
|
210
|
+
buffer = buffer.subarray(token.length);
|
|
211
|
+
}
|
|
212
|
+
while (buffer.length >= 32) {
|
|
213
|
+
const length = buffer.readUInt32LE(24);
|
|
214
|
+
if (buffer.length < 32 + length)
|
|
215
|
+
break;
|
|
216
|
+
if (buffer[4] === 1)
|
|
217
|
+
producer.receivedFrames += length / 8;
|
|
218
|
+
else
|
|
219
|
+
producer.videoFrames++;
|
|
220
|
+
buffer = buffer.subarray(32 + length);
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
socket.on("error", () => { });
|
|
224
|
+
socket.on("close", () => {
|
|
225
|
+
if (isCurrentProducer(host, inner, producer.id))
|
|
226
|
+
producer.droppedWhileSelected = true;
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
let socket = path;
|
|
230
|
+
if (tcp) {
|
|
231
|
+
await new Promise((resolve) => producer.server.listen(0, "127.0.0.1", resolve));
|
|
232
|
+
const { port } = producer.server.address();
|
|
233
|
+
socket = `tcp://127.0.0.1:${port}`;
|
|
234
|
+
}
|
|
235
|
+
else
|
|
236
|
+
producer.server.listen(path);
|
|
237
|
+
host.producers.set(id, producer);
|
|
238
|
+
return {
|
|
239
|
+
producerId: id,
|
|
240
|
+
socket,
|
|
241
|
+
...(token ? { token: token.toString("hex") } : {}),
|
|
242
|
+
epoch: 1,
|
|
243
|
+
sampleRate: 48000,
|
|
244
|
+
channels: 2,
|
|
245
|
+
sampleFormat: "f32le",
|
|
246
|
+
videoFormat: "rgba8",
|
|
247
|
+
maxAudioFrames: 4800,
|
|
248
|
+
audioCapacityFrames: 9600,
|
|
249
|
+
videoCapacity: 8,
|
|
250
|
+
};
|
|
251
|
+
},
|
|
252
|
+
"producer.state": (p) => {
|
|
253
|
+
const producer = host.producers.get(Number(p.producerId));
|
|
254
|
+
if (!producer)
|
|
255
|
+
throw new HostError("missing", "producer");
|
|
256
|
+
if (producer.retired || producer.released)
|
|
257
|
+
throw new HostError("unavailable", "producer absent");
|
|
258
|
+
const current = isCurrentProducer(host, inner, producer.id);
|
|
259
|
+
return {
|
|
260
|
+
input: null,
|
|
261
|
+
pendingControl: producer.pending,
|
|
262
|
+
producerId: producer.id,
|
|
263
|
+
epoch: producer.epoch,
|
|
264
|
+
acknowledged: producer.acknowledged,
|
|
265
|
+
paused: host.source.paused,
|
|
266
|
+
// The fake device consumes everything immediately.
|
|
267
|
+
consumedFrames: producer.receivedFrames,
|
|
268
|
+
underrunFrames: 0,
|
|
269
|
+
deviceErrors: 0,
|
|
270
|
+
health: "ok",
|
|
271
|
+
xrunEvents: 0,
|
|
272
|
+
deviceChangeEvents: 0,
|
|
273
|
+
errorDetails: [],
|
|
274
|
+
errorOverflow: 0,
|
|
275
|
+
buffer: {},
|
|
276
|
+
receivedFrames: producer.receivedFrames,
|
|
277
|
+
hasVideo: producer.videoFrames > 0,
|
|
278
|
+
error: null,
|
|
279
|
+
active: producer.active && current,
|
|
280
|
+
};
|
|
281
|
+
},
|
|
282
|
+
"producer.ack": (p) => {
|
|
283
|
+
const control = (p.control ?? {});
|
|
284
|
+
const producer = host.producers.get(Number(control.producerId));
|
|
285
|
+
if (!producer)
|
|
286
|
+
throw new HostError("missing", "producer");
|
|
287
|
+
if (producer.retired || producer.released)
|
|
288
|
+
throw new HostError("stale", "producer control changed");
|
|
289
|
+
producer.pending = null;
|
|
290
|
+
producer.acknowledged = true;
|
|
291
|
+
producer.epoch = Number(control.epoch);
|
|
292
|
+
producer.active = true;
|
|
293
|
+
return { status: "acknowledged", control };
|
|
294
|
+
},
|
|
295
|
+
"producer.ackInput": () => ({ status: "acknowledged", sequence: 0 }),
|
|
296
|
+
"producer.release": (p) => {
|
|
297
|
+
const producer = host.producers.get(Number(p.producerId));
|
|
298
|
+
if (!producer)
|
|
299
|
+
return;
|
|
300
|
+
producer.released = true;
|
|
301
|
+
producer.server.close();
|
|
302
|
+
},
|
|
303
|
+
};
|
|
304
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import type { Json, LayoutPanel } from "../../wire.ts";
|
|
2
|
+
import type { FakePanel, TestHost } from "./index.ts";
|
|
3
|
+
import type { Handler, Internals } from "./internals.ts";
|
|
4
|
+
export declare const kebab: (name: string) => string;
|
|
5
|
+
export declare const SLOT_LABELS: Record<string, string>;
|
|
6
|
+
/** The view a panel's writes are kept under (the player's, per slot). */
|
|
7
|
+
export declare const mountView: (slot: string) => string;
|
|
8
|
+
/** The positions that hold tabs: any number of plugins, one tab each. */
|
|
9
|
+
export type TabSlot = "bottom" | "sidebar" | "adjust";
|
|
10
|
+
export type WireRowOp = {
|
|
11
|
+
op: "set";
|
|
12
|
+
at: number;
|
|
13
|
+
row: Json;
|
|
14
|
+
} | {
|
|
15
|
+
op: "insert";
|
|
16
|
+
at: number;
|
|
17
|
+
rows: Json[];
|
|
18
|
+
} | {
|
|
19
|
+
op: "remove";
|
|
20
|
+
at: number;
|
|
21
|
+
count: number;
|
|
22
|
+
} | {
|
|
23
|
+
op: "move";
|
|
24
|
+
from: number;
|
|
25
|
+
count: number;
|
|
26
|
+
to: number;
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* A patch's ops checked against an array of `length` rows, as the player
|
|
30
|
+
* checks them (`ui_patch.rs`): in order, each on the result of the ones
|
|
31
|
+
* before; at most {@link PATCH_OPS}; the result at most
|
|
32
|
+
* `UI_VALUE_LIMITS.arrayRows` rows.
|
|
33
|
+
*/
|
|
34
|
+
/** The player takes no Markdown documents in parts of a value (`invalid`). */
|
|
35
|
+
export declare function noMarkdownParts(rows: Json[]): void;
|
|
36
|
+
export declare function patchOps(ops: unknown, length: number): WireRowOp[];
|
|
37
|
+
export declare function timelineChanged(host: TestHost, _inner: Internals): void;
|
|
38
|
+
export declare function resetWrites(_host: TestHost, inner: Internals, view: string): void;
|
|
39
|
+
/**
|
|
40
|
+
* A property write as the player takes it, returning the value the
|
|
41
|
+
* property shows after it: a whole `value`, or a `patch` of the array
|
|
42
|
+
* `current` applying on the write numbered `base` (code `stale`
|
|
43
|
+
* otherwise). The result is checked against the value limits whole: the
|
|
44
|
+
* player counts the rows a patch keeps too.
|
|
45
|
+
*/
|
|
46
|
+
export declare function writeProperty(host: TestHost, inner: Internals, view: string, property: string, p: Record<string, Json>, current: Json | undefined): Json | undefined;
|
|
47
|
+
/**
|
|
48
|
+
* Refuse property values over the player's limits ({@link UI_VALUE_LIMITS})
|
|
49
|
+
* with `limit`, naming the property, as the player does.
|
|
50
|
+
*/
|
|
51
|
+
export declare function checkValues(_host: TestHost, _inner: Internals, values: Record<string, Json>): void;
|
|
52
|
+
/**
|
|
53
|
+
* A panel's or settings page's initial properties, as the player keeps
|
|
54
|
+
* them: one over the limits is left out (the property keeps its default)
|
|
55
|
+
* and reported with `ui.error`, the request itself succeeds.
|
|
56
|
+
*/
|
|
57
|
+
export declare function initialValues(host: TestHost, _inner: Internals, values: Record<string, Json>): Record<string, Json>;
|
|
58
|
+
export declare function seeImages(host: TestHost, _inner: Internals, values: Record<string, Json> | Json[]): void;
|
|
59
|
+
export declare function addPanel(host: TestHost, _inner: Internals, slot: TabSlot, panel: LayoutPanel): void;
|
|
60
|
+
export declare function removePanel(host: TestHost, _inner: Internals, slot: TabSlot, owner: number): void;
|
|
61
|
+
/** The panel a request is about: its `slot`, else the only one. */
|
|
62
|
+
export declare function panelFor(host: TestHost, _inner: Internals, p: Record<string, unknown>): FakePanel;
|
|
63
|
+
/** Implements `TestHost.mountOther`. */
|
|
64
|
+
export declare function mountOther(host: TestHost, inner: Internals, slot: "bottom" | "sidebar" | "adjust" | "backdrop", plugin: string, options?: {
|
|
65
|
+
title?: string;
|
|
66
|
+
}): void;
|
|
67
|
+
/** Implements `TestHost.unmountOther`. */
|
|
68
|
+
export declare function unmountOther(host: TestHost, inner: Internals, plugin: string): void;
|
|
69
|
+
/** Implements `TestHost.mounted`. */
|
|
70
|
+
export declare function mounted(host: TestHost, _inner: Internals, slot?: "bottom" | "sidebar" | "adjust" | "backdrop"): FakePanel;
|
|
71
|
+
/** Implements `TestHost.setTimelineRanges`. */
|
|
72
|
+
export declare function setTimelineRanges(host: TestHost, inner: Internals, pluginId: string, ranges: {
|
|
73
|
+
start: number;
|
|
74
|
+
end: number;
|
|
75
|
+
style: string;
|
|
76
|
+
title?: string | null;
|
|
77
|
+
}[]): void;
|
|
78
|
+
/** Implements `TestHost.setTimelineLayer`. */
|
|
79
|
+
export declare function setTimelineLayer(host: TestHost, inner: Internals, pluginId: string, marks: {
|
|
80
|
+
time: number;
|
|
81
|
+
kind: string;
|
|
82
|
+
title?: string | null;
|
|
83
|
+
}[]): void;
|
|
84
|
+
/** The default handlers of this area's wire methods, by method name. */
|
|
85
|
+
export declare function layoutHandlers(host: TestHost, inner: Internals): Record<string, Handler>;
|