@vestra-player/sdk 1.6.17 → 1.6.19
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 +24 -0
- package/dist/docs-tree.json.gz +0 -0
- package/dist/plugin/appearance.js +1 -1
- package/dist/plugin/bindings.js +1 -1
- package/dist/plugin/commands.js +3 -2
- package/dist/plugin/connection.d.ts +119 -0
- package/dist/plugin/connection.js +402 -0
- package/dist/plugin/drops.d.ts +3 -3
- package/dist/plugin/drops.js +1 -1
- package/dist/plugin/errors.d.ts +9 -0
- package/dist/plugin/errors.js +35 -0
- package/dist/plugin/filters.js +1 -1
- package/dist/plugin/index.d.ts +1 -1
- package/dist/plugin/index.js +3 -1
- package/dist/plugin/language.js +1 -1
- package/dist/plugin/latest.d.ts +12 -0
- package/dist/plugin/latest.js +52 -0
- package/dist/plugin/local.js +1 -2
- package/dist/plugin/markdown.d.ts +2 -2
- package/dist/plugin/markdown.js +1 -1
- package/dist/plugin/media.js +1 -1
- package/dist/plugin/osd.js +2 -2
- package/dist/plugin/packages.js +1 -1
- package/dist/plugin/passes.d.ts +111 -0
- package/dist/plugin/passes.js +341 -0
- package/dist/plugin/player.d.ts +2 -3
- package/dist/plugin/player.js +2 -7
- package/dist/plugin/resolvers.js +1 -1
- package/dist/plugin/runtime.d.ts +26 -185
- package/dist/plugin/runtime.js +35 -755
- package/dist/plugin/scope.d.ts +2 -1
- package/dist/plugin/scope.js +5 -2
- package/dist/plugin/services/dialogs.d.ts +3 -0
- package/dist/plugin/services/dialogs.js +62 -0
- package/dist/plugin/services.js +1 -1
- package/dist/plugin/settings.js +2 -2
- package/dist/plugin/shaders.js +2 -1
- package/dist/plugin/stream.js +1 -2
- package/dist/plugin/subtitles.js +2 -2
- 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/dist/plugin/timeline.js +2 -1
- package/dist/plugin/view/base.d.ts +29 -0
- package/dist/plugin/view/base.js +88 -0
- package/dist/plugin/view/binder.d.ts +22 -0
- package/dist/plugin/view/binder.js +57 -0
- package/dist/plugin/view/image.d.ts +21 -0
- package/dist/plugin/view/image.js +60 -0
- package/dist/plugin/view/layout.d.ts +47 -0
- package/dist/plugin/view/layout.js +171 -0
- package/dist/plugin/view/panel.d.ts +3 -0
- package/dist/plugin/view/panel.js +178 -0
- package/dist/plugin/view/window.d.ts +3 -0
- package/dist/plugin/view/window.js +99 -0
- package/dist/plugin/view/writer.d.ts +58 -0
- package/dist/plugin/view/writer.js +634 -0
- package/dist/plugin/view.js +2 -1
- package/dist/plugin/window.js +1 -1
- package/llms.txt +1 -1
- package/package.json +6 -4
- package/dist/plugin/ui.d.ts +0 -74
- package/dist/plugin/ui.js +0 -1234
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The player's core actions (`host/<id>` commands): their enabled rules and
|
|
3
|
+
* reasons, what running one does, stepping and sequences.
|
|
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 { HostError } from "../../wire.js";
|
|
10
|
+
import { validStep } from "../player.js";
|
|
11
|
+
import { hostText } from "./i18n.js";
|
|
12
|
+
import { sourceIntent, audioCondition } from "./media.js";
|
|
13
|
+
export function coreFacts(host, _inner) {
|
|
14
|
+
const s = host.source;
|
|
15
|
+
const ready = s.phase === "ready";
|
|
16
|
+
const ended = s.phase === "ended";
|
|
17
|
+
return {
|
|
18
|
+
ready,
|
|
19
|
+
ended,
|
|
20
|
+
toggle: (s.capabilities.pause && ready) || (ended && s.capabilities.restart),
|
|
21
|
+
step: s.capabilities.seek && ready && s.position !== null,
|
|
22
|
+
rate: s.capabilities.rate && ready,
|
|
23
|
+
close: s.capabilities.close && s.phase !== "empty",
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
/** Why one frame that way is impossible (B07), or null; the player's rules. */
|
|
27
|
+
export function frameCheck(host, inner, forward) {
|
|
28
|
+
const s = host.source;
|
|
29
|
+
const unsupported = hostText(host, inner, "Not supported by the current source");
|
|
30
|
+
const noBack = hostText(host, inner, "The current source cannot step back a frame");
|
|
31
|
+
if (s.phase !== "ready" && s.phase !== "ended")
|
|
32
|
+
return unsupported;
|
|
33
|
+
if (s.kind !== "media")
|
|
34
|
+
return forward ? unsupported : noBack;
|
|
35
|
+
if (s.hasVideo === false)
|
|
36
|
+
return hostText(host, inner, "No video");
|
|
37
|
+
if (forward && s.phase !== "ready")
|
|
38
|
+
return unsupported;
|
|
39
|
+
if (!forward && !s.capabilities.seek)
|
|
40
|
+
return noBack;
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
/** The same enabled rules and reasons as the player's core actions. */
|
|
44
|
+
export function coreState(host, inner, action) {
|
|
45
|
+
const f = coreFacts(host, inner);
|
|
46
|
+
const s = host.source;
|
|
47
|
+
const unsupported = hostText(host, inner, "Not supported by the current source");
|
|
48
|
+
const rule = {
|
|
49
|
+
"play-pause": [f.toggle, f.ready ? s.paused : null, unsupported],
|
|
50
|
+
stop: [f.close, null, unsupported],
|
|
51
|
+
previous: [
|
|
52
|
+
!!host.sequence?.canPrevious,
|
|
53
|
+
null,
|
|
54
|
+
hostText(host, inner, "No previous item"),
|
|
55
|
+
],
|
|
56
|
+
next: [
|
|
57
|
+
!!host.sequence?.canNext,
|
|
58
|
+
null,
|
|
59
|
+
hostText(host, inner, "No next item"),
|
|
60
|
+
],
|
|
61
|
+
"frame-back": [
|
|
62
|
+
!frameCheck(host, inner, false),
|
|
63
|
+
null,
|
|
64
|
+
frameCheck(host, inner, false) ?? unsupported,
|
|
65
|
+
],
|
|
66
|
+
"frame-forward": [
|
|
67
|
+
!frameCheck(host, inner, true),
|
|
68
|
+
null,
|
|
69
|
+
frameCheck(host, inner, true) ?? unsupported,
|
|
70
|
+
],
|
|
71
|
+
"seek-backward": [f.step, null, unsupported],
|
|
72
|
+
"seek-forward": [f.step, null, unsupported],
|
|
73
|
+
"rate-normal": [f.rate, s.rate === 1, unsupported],
|
|
74
|
+
"rate-down": [f.rate, null, unsupported],
|
|
75
|
+
"rate-up": [f.rate, null, unsupported],
|
|
76
|
+
"volume-up": [true, null, unsupported],
|
|
77
|
+
"volume-down": [true, null, unsupported],
|
|
78
|
+
mute: [true, s.output.muted, unsupported],
|
|
79
|
+
};
|
|
80
|
+
const [enabled, checked, reason] = rule[action];
|
|
81
|
+
return { enabled, checked, busy: false, reason: enabled ? null : reason };
|
|
82
|
+
}
|
|
83
|
+
export function registerCoreActions(host, inner) {
|
|
84
|
+
// As the player declares them (host/src/core_actions.rs `declarations`,
|
|
85
|
+
// player 0.7.6): title, menu path (empty: the command palette only), order,
|
|
86
|
+
// group, default bindings, repeat, radio. Close and Play/Pause are in the
|
|
87
|
+
// menu; the rest left it for the panels and the command palette (seek,
|
|
88
|
+
// frame and speed with the playback plugin's panel, volume and mute with
|
|
89
|
+
// the audio plugin's, Previous and Next with the slim menu).
|
|
90
|
+
const table = [
|
|
91
|
+
["stop", "Close", ["Close"], 120, "open", ["F4"], false],
|
|
92
|
+
[
|
|
93
|
+
"play-pause",
|
|
94
|
+
"Play/Pause",
|
|
95
|
+
["Play/Pause"],
|
|
96
|
+
201,
|
|
97
|
+
"domain",
|
|
98
|
+
["Space", "Mouse:LeftClick"],
|
|
99
|
+
false,
|
|
100
|
+
],
|
|
101
|
+
["previous", "Previous", [], 0, "domain", ["PageUp", "Mouse:X2"], false],
|
|
102
|
+
["next", "Next", [], 0, "domain", ["PageDown", "Mouse:X1"], false],
|
|
103
|
+
["frame-back", "Previous Frame", [], 0, "domain", ["D"], true],
|
|
104
|
+
["frame-forward", "Next Frame", [], 0, "domain", ["F"], true],
|
|
105
|
+
["seek-backward", "Back 5 s", [], 0, "domain", ["ArrowLeft"], true],
|
|
106
|
+
["seek-forward", "Forward 5 s", [], 0, "domain", ["ArrowRight"], true],
|
|
107
|
+
["rate-normal", "Normal", [], 0, "domain", ["Z"], false, true],
|
|
108
|
+
["rate-down", "Slower 0.1", [], 0, "domain", ["X"], false],
|
|
109
|
+
["rate-up", "Faster 0.1", [], 0, "domain", ["C"], false],
|
|
110
|
+
[
|
|
111
|
+
"volume-up",
|
|
112
|
+
"Volume +",
|
|
113
|
+
[],
|
|
114
|
+
0,
|
|
115
|
+
"domain",
|
|
116
|
+
["ArrowUp", "Mouse:WheelUp"],
|
|
117
|
+
true,
|
|
118
|
+
],
|
|
119
|
+
[
|
|
120
|
+
"volume-down",
|
|
121
|
+
"Volume −",
|
|
122
|
+
[],
|
|
123
|
+
0,
|
|
124
|
+
"domain",
|
|
125
|
+
["ArrowDown", "Mouse:WheelDown"],
|
|
126
|
+
true,
|
|
127
|
+
],
|
|
128
|
+
["mute", "Mute", [], 0, "domain", ["M"], false],
|
|
129
|
+
];
|
|
130
|
+
for (const [id, fixed, path, order, group, bindings, repeat, radio,] of table) {
|
|
131
|
+
// The seek items carry the step (`setStepSeek`): the player
|
|
132
|
+
// registers them again when it changes (core_actions.rs `seek_title`).
|
|
133
|
+
const seek = id === "seek-forward"
|
|
134
|
+
? "Forward"
|
|
135
|
+
: id === "seek-backward"
|
|
136
|
+
? "Back"
|
|
137
|
+
: null;
|
|
138
|
+
const titleOf = () => seek === null
|
|
139
|
+
? fixed
|
|
140
|
+
: `${seek} ${String(Math.round(host.stepSeek * 1000) / 1000)} s`;
|
|
141
|
+
const declaration = {
|
|
142
|
+
path,
|
|
143
|
+
order,
|
|
144
|
+
group,
|
|
145
|
+
radio: radio ?? false,
|
|
146
|
+
bindings,
|
|
147
|
+
repeat,
|
|
148
|
+
};
|
|
149
|
+
Object.defineProperties(declaration, {
|
|
150
|
+
title: { get: titleOf, enumerable: true },
|
|
151
|
+
});
|
|
152
|
+
const command = {
|
|
153
|
+
declaration,
|
|
154
|
+
// The player's own words in the player's language.
|
|
155
|
+
get display() {
|
|
156
|
+
const leaf = titleOf();
|
|
157
|
+
const step = /^(Back|Forward) (\S+) s$/.exec(leaf);
|
|
158
|
+
const title = step
|
|
159
|
+
? hostText(host, inner, `${step[1]} {} s`, [step[2]])
|
|
160
|
+
: hostText(host, inner, leaf);
|
|
161
|
+
return {
|
|
162
|
+
title,
|
|
163
|
+
path: path.length
|
|
164
|
+
? [...path.slice(0, -1).map((s) => hostText(host, inner, s)), title]
|
|
165
|
+
: [],
|
|
166
|
+
};
|
|
167
|
+
},
|
|
168
|
+
get state() {
|
|
169
|
+
return coreState(host, inner, id);
|
|
170
|
+
},
|
|
171
|
+
set state(_value) { },
|
|
172
|
+
revision: 1,
|
|
173
|
+
async run() {
|
|
174
|
+
// Like the player's menu: a disabled or busy command does not run.
|
|
175
|
+
const state = coreState(host, inner, id);
|
|
176
|
+
if (state.busy || !state.enabled)
|
|
177
|
+
throw new HostError("command", `host/${id} is ${state.busy ? "busy" : "disabled"}`);
|
|
178
|
+
await host.request("host.run", { action: id });
|
|
179
|
+
await host.settle();
|
|
180
|
+
},
|
|
181
|
+
};
|
|
182
|
+
host.coreCommands.set(`host/${id}`, command);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
export function runCore(host, inner, action, seconds) {
|
|
186
|
+
const state = coreState(host, inner, action);
|
|
187
|
+
if (!state.enabled)
|
|
188
|
+
throw new HostError("unavailable", state.reason ?? "unavailable");
|
|
189
|
+
host.coreRuns.push(action);
|
|
190
|
+
const s = host.source;
|
|
191
|
+
const time = (t) => {
|
|
192
|
+
if (t === null)
|
|
193
|
+
return "--:--";
|
|
194
|
+
const w = Math.max(0, Math.floor(t));
|
|
195
|
+
const mm = String(Math.floor(w / 60) % 60).padStart(2, "0");
|
|
196
|
+
const ss = String(w % 60).padStart(2, "0");
|
|
197
|
+
return w >= 3600 ? `${Math.floor(w / 3600)}:${mm}:${ss}` : `${mm}:${ss}`;
|
|
198
|
+
};
|
|
199
|
+
const rateText = (r) => String(Math.round(r * 100) / 100);
|
|
200
|
+
switch (action) {
|
|
201
|
+
case "play-pause":
|
|
202
|
+
if (s.phase === "ended")
|
|
203
|
+
sourceIntent(host, inner, { kind: "restart" });
|
|
204
|
+
else
|
|
205
|
+
sourceIntent(host, inner, {
|
|
206
|
+
kind: "control",
|
|
207
|
+
control: { kind: "pause", paused: !s.paused },
|
|
208
|
+
});
|
|
209
|
+
host.osd.push(hostText(host, inner, s.phase !== "ended" && !s.paused ? "Paused" : "Playing"));
|
|
210
|
+
return;
|
|
211
|
+
case "stop":
|
|
212
|
+
sourceIntent(host, inner, { kind: "close" });
|
|
213
|
+
return;
|
|
214
|
+
case "previous":
|
|
215
|
+
case "next":
|
|
216
|
+
host.emit("sequence.step", { direction: action });
|
|
217
|
+
return;
|
|
218
|
+
case "frame-back":
|
|
219
|
+
case "frame-forward":
|
|
220
|
+
host.steps.push({
|
|
221
|
+
unit: "frame",
|
|
222
|
+
direction: action === "frame-back" ? "backward" : "forward",
|
|
223
|
+
});
|
|
224
|
+
return;
|
|
225
|
+
case "seek-backward":
|
|
226
|
+
case "seek-forward": {
|
|
227
|
+
const step = seconds ?? host.stepSeek;
|
|
228
|
+
const delta = action === "seek-forward" ? step : -step;
|
|
229
|
+
const base = s.position ?? 0;
|
|
230
|
+
let target = Math.max(0, Math.min(s.duration ?? Infinity, base + delta));
|
|
231
|
+
// Keyframe mode, as the player: forward on the first keyframe at or
|
|
232
|
+
// after the target (none: the exact target), backward on the last at
|
|
233
|
+
// or before it; only sources with moving video that can seek.
|
|
234
|
+
const keyframes = host.stepSeekKeyframes &&
|
|
235
|
+
host.keyframes !== null &&
|
|
236
|
+
s.hasVideo !== false &&
|
|
237
|
+
s.capabilities.seek;
|
|
238
|
+
if (keyframes && target !== base) {
|
|
239
|
+
const keys = host.keyframes;
|
|
240
|
+
target =
|
|
241
|
+
delta > 0
|
|
242
|
+
? (keys.find((k) => k >= target) ?? target)
|
|
243
|
+
: keys
|
|
244
|
+
.filter((k) => k <= target)
|
|
245
|
+
.reduce((a, k) => Math.max(a, k), 0);
|
|
246
|
+
}
|
|
247
|
+
sourceIntent(host, inner, {
|
|
248
|
+
kind: "control",
|
|
249
|
+
control: { kind: "seek", seconds: target },
|
|
250
|
+
});
|
|
251
|
+
host.coreSeeks.push({
|
|
252
|
+
action,
|
|
253
|
+
seconds: step,
|
|
254
|
+
keyframes,
|
|
255
|
+
landed: target,
|
|
256
|
+
});
|
|
257
|
+
host.osd.push(hostText(host, inner, delta < 0 ? "Back {} s: {} / {}" : "Forward {} s: {} / {}", [Math.abs(delta), time(target), time(s.duration)]));
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
case "rate-normal":
|
|
261
|
+
case "rate-down":
|
|
262
|
+
case "rate-up": {
|
|
263
|
+
const rate = action === "rate-normal"
|
|
264
|
+
? 1
|
|
265
|
+
: Math.max(0.25, Math.min(4, Math.round((s.rate + (action === "rate-up" ? 0.1 : -0.1)) * 100) / 100));
|
|
266
|
+
sourceIntent(host, inner, {
|
|
267
|
+
kind: "control",
|
|
268
|
+
control: { kind: "rate", rate },
|
|
269
|
+
});
|
|
270
|
+
host.osd.push(hostText(host, inner, "Speed: {}x", [rateText(rate)]));
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
case "volume-up":
|
|
274
|
+
case "volume-down": {
|
|
275
|
+
// Like the player: up stops at 100 (a louder volume set through
|
|
276
|
+
// setOutput stays), down steps by 5 to 0.
|
|
277
|
+
const volume = action === "volume-up"
|
|
278
|
+
? Math.max(s.output.volume, Math.min(100, Math.round(s.output.volume + 5)))
|
|
279
|
+
: Math.max(0, Math.min(200, Math.round(s.output.volume - 5)));
|
|
280
|
+
host.setSource({ output: { volume, muted: false } });
|
|
281
|
+
host.osd.push(hostText(host, inner, "Volume: {}%", [volume]));
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
case "mute":
|
|
285
|
+
host.setSource({ output: { ...s.output, muted: !s.output.muted } });
|
|
286
|
+
host.osd.push(hostText(host, inner, s.output.muted ? "Mute: Off" : "Mute: On"));
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
export function windowFacts(host, _inner) {
|
|
291
|
+
return {
|
|
292
|
+
fullscreen: host.mainWindow.fullscreen,
|
|
293
|
+
// Like player 0.6.3: never maximized while fullscreen.
|
|
294
|
+
maximized: host.mainWindow.maximized && !host.mainWindow.fullscreen,
|
|
295
|
+
videoArea: { ...host.mainWindow.videoArea },
|
|
296
|
+
pictureInPicture: { ...host.mainWindow.pictureInPicture },
|
|
297
|
+
appearance: host.appearance,
|
|
298
|
+
fileTypes: host.fileTypes(),
|
|
299
|
+
language: host.language,
|
|
300
|
+
locale: host.locale,
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
/** The default handlers of this area's wire methods, by method name. */
|
|
304
|
+
export function coreActionsHandlers(host, inner) {
|
|
305
|
+
return {
|
|
306
|
+
"player.setStepSeek": (p) => {
|
|
307
|
+
if (!validStep(p.seconds))
|
|
308
|
+
throw new HostError("invalid", "seconds must be 0.1..3600 in tenths");
|
|
309
|
+
if (p.keyframes !== undefined &&
|
|
310
|
+
p.keyframes !== null &&
|
|
311
|
+
typeof p.keyframes !== "boolean")
|
|
312
|
+
throw new HostError("invalid", "keyframes must be a boolean");
|
|
313
|
+
// Written as a pair, as the player does.
|
|
314
|
+
host.stepSeek = p.seconds;
|
|
315
|
+
host.stepSeekKeyframes = p.keyframes === true;
|
|
316
|
+
},
|
|
317
|
+
"player.step": (p) => {
|
|
318
|
+
audioCondition(host, inner, p.expected);
|
|
319
|
+
const s = host.source;
|
|
320
|
+
const reason = p.unit === "keyframe"
|
|
321
|
+
? s.phase === "ready" && s.capabilities.seek && s.hasVideo !== false
|
|
322
|
+
? null
|
|
323
|
+
: hostText(host, inner, "Not supported by the current source")
|
|
324
|
+
: frameCheck(host, inner, p.direction === "forward");
|
|
325
|
+
if (reason)
|
|
326
|
+
throw new HostError("unsupported", reason);
|
|
327
|
+
host.steps.push({ unit: String(p.unit), direction: String(p.direction) });
|
|
328
|
+
},
|
|
329
|
+
"sequence.set": (p) => {
|
|
330
|
+
host.sequence = p.flags;
|
|
331
|
+
host.setSource({
|
|
332
|
+
sequence: {
|
|
333
|
+
next: host.sequence?.canNext ?? false,
|
|
334
|
+
previous: host.sequence?.canPrevious ?? false,
|
|
335
|
+
},
|
|
336
|
+
});
|
|
337
|
+
},
|
|
338
|
+
"sequence.step": (p) => {
|
|
339
|
+
const direction = String(p.direction);
|
|
340
|
+
const can = direction === "next"
|
|
341
|
+
? host.sequence?.canNext
|
|
342
|
+
: host.sequence?.canPrevious;
|
|
343
|
+
if (!can)
|
|
344
|
+
throw new HostError("unavailable", `cannot go ${direction}`);
|
|
345
|
+
host.emit("sequence.step", { direction });
|
|
346
|
+
},
|
|
347
|
+
"player.chapters": () => ({
|
|
348
|
+
sourceId: host.source.stamp.generation,
|
|
349
|
+
chapters: host.source.kind === "media" &&
|
|
350
|
+
["ready", "ended"].includes(host.source.phase)
|
|
351
|
+
? structuredClone(host.chapters)
|
|
352
|
+
: [],
|
|
353
|
+
}),
|
|
354
|
+
"player.setLoop": (p) => {
|
|
355
|
+
if (host.source.kind === "producer")
|
|
356
|
+
throw new HostError("unsupported", "a generated stream cannot loop");
|
|
357
|
+
audioCondition(host, inner, p.expected);
|
|
358
|
+
const loop = p.loop;
|
|
359
|
+
if (loop && !host.source.capabilities.seek)
|
|
360
|
+
throw new HostError("unsupported", "the source cannot seek");
|
|
361
|
+
if (loop &&
|
|
362
|
+
!(loop.a >= 0 &&
|
|
363
|
+
loop.a < loop.b &&
|
|
364
|
+
loop.b <= (host.source.duration ?? 0)))
|
|
365
|
+
throw new HostError("invalid", "loop needs 0 <= a < b <= duration");
|
|
366
|
+
host.setSource({
|
|
367
|
+
loop: loop && { a: loop.a, b: loop.b, count: loop.count ?? null },
|
|
368
|
+
});
|
|
369
|
+
},
|
|
370
|
+
};
|
|
371
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The player's checks of a command declaration (capabilities/commands.rs,
|
|
3
|
+
* commands.rs), with its codes and messages.
|
|
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 { HostError } from "../../wire.ts";
|
|
10
|
+
/**
|
|
11
|
+
* The player's refusal of a declaration (commands.rs `CommandError`): code
|
|
12
|
+
* `invalid`, `conflict` or `limit`, its message prefixed as the player's.
|
|
13
|
+
*/
|
|
14
|
+
export declare const commandInvalid: (message: string) => HostError;
|
|
15
|
+
export declare const commandConflict: (message: string) => HostError;
|
|
16
|
+
/**
|
|
17
|
+
* The player's check of a command text (commands.rs `text`): 1 to `limit`
|
|
18
|
+
* UTF-8 bytes, no control character (`char::is_control`: C0, DEL and C1)
|
|
19
|
+
* and no whitespace at either end (`str::trim`: Unicode White_Space).
|
|
20
|
+
*/
|
|
21
|
+
export declare function commandText(value: string, label: string, limit: number): void;
|
|
22
|
+
/**
|
|
23
|
+
* A menu text (title or path segment) as the player keys it: a string as
|
|
24
|
+
* declared, a text value by its English rendering (capabilities/commands.rs
|
|
25
|
+
* `menu_text`).
|
|
26
|
+
*/
|
|
27
|
+
export declare function menuTextKey(value: unknown): string;
|
|
28
|
+
/**
|
|
29
|
+
* The declaration checks of the player's `Registry::register`, in its
|
|
30
|
+
* order, on the keyed title and path (`@<id>` already replaced).
|
|
31
|
+
*/
|
|
32
|
+
export declare function checkDeclaration(id: string, title: string, path: readonly string[], declaration: {
|
|
33
|
+
group?: unknown;
|
|
34
|
+
order?: unknown;
|
|
35
|
+
bindings?: unknown;
|
|
36
|
+
home?: unknown;
|
|
37
|
+
}, category?: string | null): void;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The player's checks of a command declaration (capabilities/commands.rs,
|
|
3
|
+
* commands.rs), with its codes and messages.
|
|
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 { HostError } from "../../wire.js";
|
|
10
|
+
import { COMMAND_LIMITS } from "../../commands.js";
|
|
11
|
+
import { isTextValue, textKey, textProblem } from "../../i18n.js";
|
|
12
|
+
/**
|
|
13
|
+
* The player's refusal of a declaration (commands.rs `CommandError`): code
|
|
14
|
+
* `invalid`, `conflict` or `limit`, its message prefixed as the player's.
|
|
15
|
+
*/
|
|
16
|
+
export const commandInvalid = (message) => new HostError("invalid", `invalid command: ${message}`);
|
|
17
|
+
export const commandConflict = (message) => new HostError("conflict", `command conflict: ${message}`);
|
|
18
|
+
/**
|
|
19
|
+
* The player's check of a command text (commands.rs `text`): 1 to `limit`
|
|
20
|
+
* UTF-8 bytes, no control character (`char::is_control`: C0, DEL and C1)
|
|
21
|
+
* and no whitespace at either end (`str::trim`: Unicode White_Space).
|
|
22
|
+
*/
|
|
23
|
+
export function commandText(value, label, limit) {
|
|
24
|
+
const bytes = Buffer.byteLength(value, "utf8");
|
|
25
|
+
if (bytes === 0 ||
|
|
26
|
+
bytes > limit ||
|
|
27
|
+
/^\p{White_Space}|\p{White_Space}$/u.test(value) ||
|
|
28
|
+
/\p{Cc}/u.test(value))
|
|
29
|
+
throw commandInvalid(`${label} requires 1..=${limit} bytes, no controls or surrounding whitespace`);
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* A menu text (title or path segment) as the player keys it: a string as
|
|
33
|
+
* declared, a text value by its English rendering (capabilities/commands.rs
|
|
34
|
+
* `menu_text`).
|
|
35
|
+
*/
|
|
36
|
+
export function menuTextKey(value) {
|
|
37
|
+
if (typeof value === "string")
|
|
38
|
+
return value;
|
|
39
|
+
if (!isTextValue(value))
|
|
40
|
+
throw new HostError("invalid", "title and path segments are strings or texts");
|
|
41
|
+
if ("$vestraList" in value)
|
|
42
|
+
throw new HostError("invalid", "a menu text is a string or a text, not a list");
|
|
43
|
+
const problem = textProblem(value);
|
|
44
|
+
if (problem)
|
|
45
|
+
throw new HostError("invalid", problem);
|
|
46
|
+
return textKey(value);
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* The declaration checks of the player's `Registry::register`, in its
|
|
50
|
+
* order, on the keyed title and path (`@<id>` already replaced).
|
|
51
|
+
*/
|
|
52
|
+
export function checkDeclaration(id, title, path, declaration, category = null) {
|
|
53
|
+
commandText(id, "localId", COMMAND_LIMITS.idBytes);
|
|
54
|
+
if (!/^[A-Za-z0-9._-]+$/.test(id))
|
|
55
|
+
throw commandInvalid("localId permits ASCII letters, digits, '.', '_' and '-'");
|
|
56
|
+
commandText(title, "title", COMMAND_LIMITS.titleBytes);
|
|
57
|
+
if (path.length > COMMAND_LIMITS.depth)
|
|
58
|
+
throw commandInvalid(`menu path allows at most ${COMMAND_LIMITS.depth} segments`);
|
|
59
|
+
for (const segment of path)
|
|
60
|
+
commandText(segment, "path segment", COMMAND_LIMITS.segmentBytes);
|
|
61
|
+
const group = declaration.group ?? "domain";
|
|
62
|
+
if (typeof group !== "string")
|
|
63
|
+
throw new HostError("invalid", "group is a string");
|
|
64
|
+
commandText(group, "group", COMMAND_LIMITS.groupBytes);
|
|
65
|
+
// Player 0.7.6: the category and the home command's local id.
|
|
66
|
+
if (category !== null)
|
|
67
|
+
commandText(category, "category", COMMAND_LIMITS.categoryBytes);
|
|
68
|
+
const home = declaration.home;
|
|
69
|
+
if (home !== undefined && home !== null) {
|
|
70
|
+
if (typeof home !== "string")
|
|
71
|
+
throw new HostError("invalid", "home is a command id");
|
|
72
|
+
commandText(home, "home", COMMAND_LIMITS.idBytes);
|
|
73
|
+
if (!/^[A-Za-z0-9._-]+$/.test(home))
|
|
74
|
+
throw commandInvalid("home permits ASCII letters, digits, '.', '_' and '-'");
|
|
75
|
+
}
|
|
76
|
+
const order = declaration.order ?? 0;
|
|
77
|
+
if (!Number.isInteger(order) ||
|
|
78
|
+
Math.abs(order) > COMMAND_LIMITS.order)
|
|
79
|
+
throw commandInvalid("order must be within ±1000000");
|
|
80
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { TemplateArgs, TextValue } from "../types.ts";
|
|
2
|
+
import type { TextJson } from "../../i18n.ts";
|
|
3
|
+
import type { TestHost } from "./index.ts";
|
|
4
|
+
import type { Handler, Internals } from "./internals.ts";
|
|
5
|
+
/** Texts only the test host uses, with their Chinese of SDK 1.5.8. */
|
|
6
|
+
export declare const TEST_ZH_HANS: Readonly<Record<string, string>>;
|
|
7
|
+
/**
|
|
8
|
+
* A text of a request as the player shows it, refused as the player
|
|
9
|
+
* refuses it: a string is a template (formatted with `args`, if given), a
|
|
10
|
+
* text value carries its own arguments.
|
|
11
|
+
*/
|
|
12
|
+
export declare function shownText(host: TestHost, _inner: Internals, text: unknown, args: unknown, what: string): string;
|
|
13
|
+
/** A declared text: a string is translated as it is, a text rendered. */
|
|
14
|
+
export declare function declaredText(host: TestHost, inner: Internals, text: unknown, what: string): string;
|
|
15
|
+
/** Property values with their text values shown, as the player does first. */
|
|
16
|
+
export declare function shownValues<T>(host: TestHost, inner: Internals, values: T): T;
|
|
17
|
+
/** A text of the player itself, in the player's language (see `locale`). */
|
|
18
|
+
export declare function hostText(_host: TestHost, inner: Internals, text: string, args?: TemplateArgs): string;
|
|
19
|
+
/** A declared menu path and title as the player shows them. */
|
|
20
|
+
export declare function commandDisplay(host: TestHost, inner: Internals, path: readonly (string | TextJson)[], title: string | TextJson, reason?: TextValue | null, category?: string | TextJson | null): {
|
|
21
|
+
title: string;
|
|
22
|
+
path: string[];
|
|
23
|
+
reason: string | null;
|
|
24
|
+
};
|
|
25
|
+
/** Implements `TestHost.translate`. */
|
|
26
|
+
export declare function translate(host: TestHost, inner: Internals, text: TextValue, args?: TemplateArgs): string;
|
|
27
|
+
/** The default handlers of this area's wire methods, by method name. */
|
|
28
|
+
export declare function i18nHandlers(host: TestHost, _inner: Internals): Record<string, Handler>;
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Texts as the player shows them: translation with the plugin's catalog, the
|
|
3
|
+
* player's own texts, menu display and the appearance and language settings.
|
|
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 { HostError } from "../../wire.js";
|
|
10
|
+
import { THEMES } from "../appearance.js";
|
|
11
|
+
import { format as formatTemplate, HOST_GROUPS, HOST_ZH_HANS, LOCALES, isTextValue, menuKeys, renderTemplateArgs, renderText, resolveTexts, textKey, textProblem, } from "../../i18n.js";
|
|
12
|
+
/** Texts only the test host uses, with their Chinese of SDK 1.5.8. */
|
|
13
|
+
export const TEST_ZH_HANS = {
|
|
14
|
+
"this site": "这个网站",
|
|
15
|
+
"External SRT": "外部 SRT",
|
|
16
|
+
"External ASS": "外部 ASS",
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* A text of a request as the player shows it, refused as the player
|
|
20
|
+
* refuses it: a string is a template (formatted with `args`, if given), a
|
|
21
|
+
* text value carries its own arguments.
|
|
22
|
+
*/
|
|
23
|
+
export function shownText(host, _inner, text, args, what) {
|
|
24
|
+
if (args !== undefined && args !== null) {
|
|
25
|
+
const problem = textProblem(args, "args");
|
|
26
|
+
if (problem)
|
|
27
|
+
throw new HostError("invalid", problem);
|
|
28
|
+
}
|
|
29
|
+
if (typeof text === "string")
|
|
30
|
+
return host.translate(text, (args ?? undefined));
|
|
31
|
+
if (!isTextValue(text))
|
|
32
|
+
throw new HostError("invalid", `${what} requires a string or a text`);
|
|
33
|
+
const problem = textProblem(text);
|
|
34
|
+
if (problem)
|
|
35
|
+
throw new HostError("invalid", `${what}: ${problem}`);
|
|
36
|
+
if (args !== undefined && args !== null)
|
|
37
|
+
throw new HostError("invalid", `${what}: a text value takes no args`);
|
|
38
|
+
return host.translate(text);
|
|
39
|
+
}
|
|
40
|
+
/** A declared text: a string is translated as it is, a text rendered. */
|
|
41
|
+
export function declaredText(host, inner, text, what) {
|
|
42
|
+
return shownText(host, inner, text, undefined, what);
|
|
43
|
+
}
|
|
44
|
+
/** Property values with their text values shown, as the player does first. */
|
|
45
|
+
export function shownValues(host, inner, values) {
|
|
46
|
+
const resolved = resolveTexts(values, inner.catalog, host.locale);
|
|
47
|
+
if ("problem" in resolved)
|
|
48
|
+
throw new HostError("invalid", resolved.problem);
|
|
49
|
+
return resolved.value;
|
|
50
|
+
}
|
|
51
|
+
/** A text of the player itself, in the player's language (see `locale`). */
|
|
52
|
+
export function hostText(_host, inner, text, args) {
|
|
53
|
+
const zh = inner.locale === undefined || inner.locale === "zh-Hans";
|
|
54
|
+
const shown = (zh && (HOST_ZH_HANS[text] ?? TEST_ZH_HANS[text])) || text;
|
|
55
|
+
return args === undefined
|
|
56
|
+
? shown
|
|
57
|
+
: formatTemplate(shown, args);
|
|
58
|
+
}
|
|
59
|
+
/** A declared menu path and title as the player shows them. */
|
|
60
|
+
export function commandDisplay(host, inner, path, title, reason = null, category = null) {
|
|
61
|
+
const keys = menuKeys(path.map((segment) => typeof segment === "string" ? segment : textKey(segment)));
|
|
62
|
+
const display = {
|
|
63
|
+
title: host.translate(title),
|
|
64
|
+
// The player's submenus by key, everything else as declared.
|
|
65
|
+
path: keys.map((segment, index) => index < keys.length - 1 &&
|
|
66
|
+
HOST_GROUPS.includes(keys.slice(0, index + 1).join("/"))
|
|
67
|
+
? hostText(host, inner, segment)
|
|
68
|
+
: host.translate((path[index] ?? segment))),
|
|
69
|
+
};
|
|
70
|
+
// Not enumerable: `deepEqual(display, { title, path })` keeps passing.
|
|
71
|
+
Object.defineProperty(display, "reason", {
|
|
72
|
+
value: reason === null ? null : host.translate(reason),
|
|
73
|
+
enumerable: false,
|
|
74
|
+
});
|
|
75
|
+
Object.defineProperty(display, "category", {
|
|
76
|
+
value: category === null ? null : host.translate(category),
|
|
77
|
+
enumerable: false,
|
|
78
|
+
});
|
|
79
|
+
return display;
|
|
80
|
+
}
|
|
81
|
+
/** Implements `TestHost.translate`. */
|
|
82
|
+
export function translate(host, inner, text, args) {
|
|
83
|
+
if (typeof text !== "string")
|
|
84
|
+
return renderText(text, inner.catalog, host.locale);
|
|
85
|
+
const values = renderTemplateArgs(args, inner.catalog, host.locale);
|
|
86
|
+
return inner.catalog.template(text, values ?? null);
|
|
87
|
+
}
|
|
88
|
+
/** The default handlers of this area's wire methods, by method name. */
|
|
89
|
+
export function i18nHandlers(host, _inner) {
|
|
90
|
+
return {
|
|
91
|
+
"appearance.setLanguage": (p) => {
|
|
92
|
+
const language = String(p.language);
|
|
93
|
+
if (language !== "system" &&
|
|
94
|
+
!LOCALES.includes(language))
|
|
95
|
+
throw new HostError("invalid", `unknown language ${language}`);
|
|
96
|
+
if (language !== host.language)
|
|
97
|
+
host.setLanguage(language);
|
|
98
|
+
},
|
|
99
|
+
"appearance.set": (p) => {
|
|
100
|
+
// Like the player: an id it does not have selects the default (the
|
|
101
|
+
// SDK's own `set` refuses one before it gets here).
|
|
102
|
+
const theme = THEMES.find((t) => t === p.theme) ?? THEMES[0];
|
|
103
|
+
if (theme !== host.appearance)
|
|
104
|
+
host.setAppearance(theme);
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
}
|