@vestra-player/sdk 1.5.18 → 1.5.20
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 +18 -0
- package/dist/capabilities.js +1 -0
- package/dist/commands.js +0 -1
- package/dist/player-runtime.d.ts +1 -1
- package/dist/player-runtime.js +1 -1
- package/dist/plugin/markdown.d.ts +7 -0
- package/dist/plugin/markdown.js +62 -22
- package/dist/plugin/packages.js +5 -0
- package/dist/plugin/player.d.ts +2 -0
- package/dist/plugin/player.js +11 -6
- package/dist/plugin/services.js +10 -3
- package/dist/plugin/testing.d.ts +47 -8
- package/dist/plugin/testing.js +114 -19
- package/dist/plugin/types.d.ts +173 -21
- package/dist/plugin/ui.d.ts +4 -2
- package/dist/plugin/ui.js +31 -13
- package/dist/plugin/values.d.ts +15 -0
- package/dist/plugin/values.js +18 -0
- package/dist/wire.d.ts +3 -2
- package/dist/wire.js +3 -2
- package/llms-full.txt +154 -18
- package/llms.txt +23 -23
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.5.20
|
|
4
|
+
|
|
5
|
+
Republishes 1.5.19, which was tagged but never published: its API documentation build failed (an example title and four undocumented `IMAGE_LIMITS` fields). Documentation only; the code is the same as 1.5.19 below.
|
|
6
|
+
|
|
7
|
+
## 1.5.19
|
|
8
|
+
|
|
9
|
+
Images in list rows, restarting a plugin, and keyframe step seeks (player 0.5.1). Additive; `apiVersion` stays `vestra/1`, and plugins that do not use the new members need no change. Requested by the plugin store (STORE-ROW-LOGO, STORE-RELOAD-BUILTIN) and the playback plugin (keyframe seeks); the online video plugins' search results need the same row images.
|
|
10
|
+
|
|
11
|
+
- **`ImageRef` at any depth** (`PropData` includes `ImageRef`): an image from `ui.image` goes wherever the Slint type is `image`, also a field of a struct or a row of a model, for example `rows: [{ key, name, icon: plugin.ui.image(path) }]` with `struct Row { key: string, name: string, icon: image }`. Players before 0.5.1 refuse a nested image with `invalid`, as the SDK did until now. A nested image whose file fails the player's checks is drawn empty and the rest of the value is shown; a released image is refused at any depth. Rows with images update in place in the player (a `ListView` keeps its scroll position).
|
|
12
|
+
- **`ui.image(path, { maxSide })`** and the new **`ImageOptions`**: decode the image scaled down, aspect kept, so that neither side is over `maxSide` (1–4096). Use about twice the drawn size for logos and thumbnails. Players before 0.5.1 refuse a reference with `maxSide`; without it the wire form is unchanged.
|
|
13
|
+
- **Per-view image budget** (player 0.5.1): the images nested in one view's values may show at most 32 MB of decoded pixels together; a write past it is refused with `limit`. **`IMAGE_LIMITS`** (`@vestra-player/sdk/runtime` and `/testing`) lists the player's image limits.
|
|
14
|
+
- **`ui.remoteImage(url, options?)`**: downloads an `https:` PNG or JPEG into `<dataDirectory>/.vestra-cache/images/` with the Markdown image rules (4 MB, 10 s, 4 at a time per plugin, a 32 MB cache refreshed after a day) and resolves with an `ImageRef`; rejects `unavailable` when the download fails. Works with any player; nested, with 0.5.1.
|
|
15
|
+
- **`Packages.reload(id)`** (`plugins.manage`, player 0.5.1): stop a plugin and start it again from the same package with the same data, built-ins included; a stopped or failed one is just started. The disabled flag is never written. `invalid` for oneself or a disabled plugin, `missing` unknown, `busy` while it is being installed or reloaded; `packages.onChange` shows `starting`, then `running` or `failed`. Listed in `CAPABILITY_INFO["plugins.manage"].apis`.
|
|
16
|
+
- **`Player.setStepSeek({ seconds, keyframes })`** (player 0.5.1): with `keyframes: true` the step seeks land on a keyframe (forward at or after position + step, backward at or before position − step, always moving), presses during a seek add up from the landed position, and the on-screen text shows the landed time. Sources without moving video seek exactly; the seek bar, `player.seek`, scrubs, chapters, loops and `player.step` are unaffected. A non-boolean `keyframes` is `invalid`.
|
|
17
|
+
- **`Host.run("seek-backward" | "seek-forward", { seconds })`** (player 0.5.1): one step seek of another step, with the core's checks, mode, accumulation and text. In keyframe mode it resolves once the seek has landed. `seconds` with another action, or outside 0.1–3600 in tenths, is `invalid`. Older players ignore `seconds`: set `"minPlayerVersion": "0.5.1"`.
|
|
18
|
+
- **`TestHost`**: nested images are recorded in `host.images` (with `maxSide`); `ui.remoteImage` is served from `host.remoteImages`; `packages.reload` is modelled (`starting`, then `running`; `packageCalls` method `"reload"`); `stepSeekKeyframes`, the test data `keyframes` and the log `coreSeeks` model keyframe step seeks, and `host.run` takes `seconds`. The test host lands each press at once (the player completes presses that add up together, with one text).
|
|
19
|
+
- Docs: *Windows, panels, settings pages* (images in rows), *Playback* (Step seeks: exact or keyframe); `llms.txt` and `llms-full.txt` regenerated.
|
|
20
|
+
|
|
3
21
|
## 1.5.18
|
|
4
22
|
|
|
5
23
|
Fixes for Windows and for a vendored SDK, and built-ins that can no longer be disabled. `apiVersion` stays `vestra/1`; plugins that do not manage other plugins need no change.
|
package/dist/capabilities.js
CHANGED
package/dist/commands.js
CHANGED
|
@@ -401,7 +401,6 @@ vestra.preferences | options | Settings ▸ Options… | F5
|
|
|
401
401
|
vestra.hotkeys | keys | Settings ▸ Keyboard Shortcuts…
|
|
402
402
|
vestra.hotkeys | mouse | Settings ▸ Mouse…
|
|
403
403
|
vestra.hotkeys | ignore-defaults | Settings ▸ Ignore Default Shortcuts
|
|
404
|
-
vestra.plugins | manage | Settings ▸ Plugin Manager…
|
|
405
404
|
vestra.preferences | theme.vestra | Settings ▸ Appearance ▸ Vestra Dark
|
|
406
405
|
vestra.preferences | theme.vestra-light | Settings ▸ Appearance ▸ Vestra Light
|
|
407
406
|
vestra.visualizer | idle-logo | Settings ▸ Appearance ▸ Show Logo When Idle
|
package/dist/player-runtime.d.ts
CHANGED
|
@@ -20,4 +20,4 @@ export { THEME_PALETTES } from "./palettes.ts";
|
|
|
20
20
|
export { Catalog, HOST_ZH_HANS, LIST_SEPARATORS, LOCALES, MENU_KEYS, matchLocale, parsePo, rewriteSlint, renderText, scanSlint, TEXT_LIMITS, textKey, textProblem, } from "./i18n.ts";
|
|
21
21
|
export { MANIFEST_FIELDS, MANIFEST_LIMITS } from "./manifest.ts";
|
|
22
22
|
export { MARKDOWN_LIMITS } from "./plugin/markdown.ts";
|
|
23
|
-
export { UI_VALUE_LIMITS } from "./plugin/values.ts";
|
|
23
|
+
export { IMAGE_LIMITS, UI_VALUE_LIMITS } from "./plugin/values.ts";
|
package/dist/player-runtime.js
CHANGED
|
@@ -18,4 +18,4 @@ export { THEME_PALETTES } from "./palettes.js";
|
|
|
18
18
|
export { Catalog, HOST_ZH_HANS, LIST_SEPARATORS, LOCALES, MENU_KEYS, matchLocale, parsePo, rewriteSlint, renderText, scanSlint, TEXT_LIMITS, textKey, textProblem, } from "./i18n.js";
|
|
19
19
|
export { MANIFEST_FIELDS, MANIFEST_LIMITS } from "./manifest.js";
|
|
20
20
|
export { MARKDOWN_LIMITS } from "./plugin/markdown.js";
|
|
21
|
-
export { UI_VALUE_LIMITS } from "./plugin/values.js";
|
|
21
|
+
export { IMAGE_LIMITS, UI_VALUE_LIMITS } from "./plugin/values.js";
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Json } from "../wire.ts";
|
|
2
|
+
import type { Runtime } from "./runtime.ts";
|
|
2
3
|
import type { ScopeImpl } from "./scope.ts";
|
|
3
4
|
import type { MarkdownOptions, MarkdownRef } from "./types.ts";
|
|
4
5
|
export declare const MARKDOWN_LIMITS: Readonly<{
|
|
@@ -37,4 +38,10 @@ export declare function httpsDownload(url: string, options: {
|
|
|
37
38
|
maxBytes: number;
|
|
38
39
|
signal: AbortSignal;
|
|
39
40
|
}, fetchImpl?: typeof fetch): Promise<Uint8Array>;
|
|
41
|
+
/**
|
|
42
|
+
* `ui.remoteImage`: the local copy of an `https:` image in
|
|
43
|
+
* `<dataDirectory>/.vestra-cache/images/` (the Markdown limits: 4 MB, 10 s,
|
|
44
|
+
* 4 at a time per plugin, 32 MB of cache, a day). Rejects `unavailable`.
|
|
45
|
+
*/
|
|
46
|
+
export declare function remoteImageFile(runtime: Runtime, url: string): Promise<string>;
|
|
40
47
|
export declare function markdown(scope: ScopeImpl, text: string, options?: MarkdownOptions): Promise<MarkdownRef>;
|
package/dist/plugin/markdown.js
CHANGED
|
@@ -113,33 +113,12 @@ async function fetchImages(runtime, urls) {
|
|
|
113
113
|
return {};
|
|
114
114
|
const cache = join(data, ".vestra-cache", "markdown");
|
|
115
115
|
await mkdir(cache, { recursive: true });
|
|
116
|
-
const download = runtime.local?.download ?? httpsDownload;
|
|
117
116
|
const found = {};
|
|
118
117
|
const queue = [...new Set(urls)].filter((u) => u.startsWith("https://"));
|
|
119
118
|
const worker = async () => {
|
|
120
119
|
for (let url = queue.shift(); url !== undefined; url = queue.shift()) {
|
|
121
|
-
const file = join(cache, createHash("sha256").update(url).digest("hex"));
|
|
122
120
|
try {
|
|
123
|
-
|
|
124
|
-
const now = Date.now();
|
|
125
|
-
if (info && now - info.mtimeMs < MARKDOWN_LIMITS.cacheMaxAgeMs) {
|
|
126
|
-
const time = new Date(info.mtimeMs);
|
|
127
|
-
await utimes(file, new Date(now), time).catch(() => { });
|
|
128
|
-
found[url] = file;
|
|
129
|
-
continue;
|
|
130
|
-
}
|
|
131
|
-
const bytes = await download(url, {
|
|
132
|
-
maxBytes: MARKDOWN_LIMITS.imageBytes,
|
|
133
|
-
signal: AbortSignal.timeout(MARKDOWN_LIMITS.imageTimeoutMs),
|
|
134
|
-
});
|
|
135
|
-
if (bytes.length > MARKDOWN_LIMITS.imageBytes)
|
|
136
|
-
throw new Error("image too large");
|
|
137
|
-
if (!isPng(bytes) && !isJpeg(bytes))
|
|
138
|
-
throw new Error("not a PNG or JPEG image");
|
|
139
|
-
const temporary = `${file}.${process.pid}.tmp`;
|
|
140
|
-
await writeFile(temporary, bytes);
|
|
141
|
-
await rename(temporary, file);
|
|
142
|
-
found[url] = file;
|
|
121
|
+
found[url] = (await cached(runtime, cache, url)).file;
|
|
143
122
|
}
|
|
144
123
|
catch {
|
|
145
124
|
// Shown as its alt text.
|
|
@@ -150,6 +129,67 @@ async function fetchImages(runtime, urls) {
|
|
|
150
129
|
await trim(cache, new Set(Object.values(found)));
|
|
151
130
|
return found;
|
|
152
131
|
}
|
|
132
|
+
/**
|
|
133
|
+
* The cached copy of `url` in `cache`, downloaded when missing or older than
|
|
134
|
+
* a day; throws why it could not be had. `fresh`: it was downloaded now.
|
|
135
|
+
*/
|
|
136
|
+
async function cached(runtime, cache, url) {
|
|
137
|
+
const file = join(cache, createHash("sha256").update(url).digest("hex"));
|
|
138
|
+
const info = await stat(file).catch(() => null);
|
|
139
|
+
const now = Date.now();
|
|
140
|
+
if (info && now - info.mtimeMs < MARKDOWN_LIMITS.cacheMaxAgeMs) {
|
|
141
|
+
const time = new Date(info.mtimeMs);
|
|
142
|
+
await utimes(file, new Date(now), time).catch(() => { });
|
|
143
|
+
return { file, fresh: false };
|
|
144
|
+
}
|
|
145
|
+
const download = runtime.local?.download ?? httpsDownload;
|
|
146
|
+
const bytes = await download(url, {
|
|
147
|
+
maxBytes: MARKDOWN_LIMITS.imageBytes,
|
|
148
|
+
signal: AbortSignal.timeout(MARKDOWN_LIMITS.imageTimeoutMs),
|
|
149
|
+
});
|
|
150
|
+
if (bytes.length > MARKDOWN_LIMITS.imageBytes)
|
|
151
|
+
throw new Error("image too large");
|
|
152
|
+
if (!isPng(bytes) && !isJpeg(bytes))
|
|
153
|
+
throw new Error("not a PNG or JPEG image");
|
|
154
|
+
const temporary = `${file}.${process.pid}.tmp`;
|
|
155
|
+
await writeFile(temporary, bytes);
|
|
156
|
+
await rename(temporary, file);
|
|
157
|
+
return { file, fresh: true };
|
|
158
|
+
}
|
|
159
|
+
/** `ui.remoteImage` downloads in flight and waiting, per plugin. */
|
|
160
|
+
const slots = new WeakMap();
|
|
161
|
+
/**
|
|
162
|
+
* `ui.remoteImage`: the local copy of an `https:` image in
|
|
163
|
+
* `<dataDirectory>/.vestra-cache/images/` (the Markdown limits: 4 MB, 10 s,
|
|
164
|
+
* 4 at a time per plugin, 32 MB of cache, a day). Rejects `unavailable`.
|
|
165
|
+
*/
|
|
166
|
+
export async function remoteImageFile(runtime, url) {
|
|
167
|
+
const data = runtime.dataDirectory;
|
|
168
|
+
if (!data)
|
|
169
|
+
throw failure("unavailable", "the plugin has no data directory");
|
|
170
|
+
const cache = join(data, ".vestra-cache", "images");
|
|
171
|
+
let slot = slots.get(runtime);
|
|
172
|
+
if (!slot)
|
|
173
|
+
slots.set(runtime, (slot = { active: 0, waiting: [] }));
|
|
174
|
+
const queue = slot;
|
|
175
|
+
if (queue.active >= MARKDOWN_LIMITS.concurrency)
|
|
176
|
+
await new Promise((go) => queue.waiting.push(go));
|
|
177
|
+
queue.active += 1;
|
|
178
|
+
try {
|
|
179
|
+
await mkdir(cache, { recursive: true });
|
|
180
|
+
const { file, fresh } = await cached(runtime, cache, url);
|
|
181
|
+
if (fresh)
|
|
182
|
+
await trim(cache, new Set([file]));
|
|
183
|
+
return file;
|
|
184
|
+
}
|
|
185
|
+
catch (error) {
|
|
186
|
+
throw failure("unavailable", `image ${url}: ${error instanceof Error ? error.message : String(error)}`);
|
|
187
|
+
}
|
|
188
|
+
finally {
|
|
189
|
+
queue.active -= 1;
|
|
190
|
+
queue.waiting.shift()?.();
|
|
191
|
+
}
|
|
192
|
+
}
|
|
153
193
|
/** Keep the cache under its size: drop the least recently used files not in use. */
|
|
154
194
|
async function trim(cache, keep) {
|
|
155
195
|
const files = [];
|
package/dist/plugin/packages.js
CHANGED
|
@@ -141,6 +141,11 @@ export const packages = (scope) => ({
|
|
|
141
141
|
throw failure("invalid", "setEnabled takes an id and a boolean");
|
|
142
142
|
await scope.runtime.host.packages.setEnabled(id, enabled);
|
|
143
143
|
},
|
|
144
|
+
async reload(id) {
|
|
145
|
+
if (typeof id !== "string" || !id)
|
|
146
|
+
throw failure("invalid", "reload takes a plugin id");
|
|
147
|
+
await scope.runtime.host.packages.reload(id);
|
|
148
|
+
},
|
|
144
149
|
uninstall: (id, options = {}) => change(scope, (requestId, revision) => scope.runtime.host.packages.uninstall(id, options.removeData === true, revision, requestId), id),
|
|
145
150
|
});
|
|
146
151
|
const info = (plugin) => Object.freeze({
|
package/dist/plugin/player.d.ts
CHANGED
|
@@ -4,6 +4,8 @@ import type { Runtime } from "./runtime.ts";
|
|
|
4
4
|
import type { ScopeImpl } from "./scope.ts";
|
|
5
5
|
import type { Conditional, Disposable, Player, PlayerState, PluginError } from "./types.ts";
|
|
6
6
|
export declare const rawOf: (state: PlayerState) => SourceState | undefined;
|
|
7
|
+
/** A step-seek length the player accepts: 0.1–3600 seconds in tenths. */
|
|
8
|
+
export declare const validStep: (seconds: unknown) => seconds is number;
|
|
7
9
|
export declare const sameStamp: (a: SourceStamp, b: SourceStamp) => boolean;
|
|
8
10
|
export declare function abortError(): PluginError;
|
|
9
11
|
/** One per plugin: the state store and the operation book client. */
|
package/dist/plugin/player.js
CHANGED
|
@@ -88,6 +88,12 @@ function decoding(hwdec) {
|
|
|
88
88
|
return hwdec === "no" ? "software" : "hardware";
|
|
89
89
|
}
|
|
90
90
|
export const rawOf = (state) => raws.get(state);
|
|
91
|
+
/** A step-seek length the player accepts: 0.1–3600 seconds in tenths. */
|
|
92
|
+
export const validStep = (seconds) => typeof seconds === "number" &&
|
|
93
|
+
Number.isFinite(seconds) &&
|
|
94
|
+
seconds >= 0.1 &&
|
|
95
|
+
seconds <= 3600 &&
|
|
96
|
+
Math.abs(Math.round(seconds * 10) - seconds * 10) <= 1e-9;
|
|
91
97
|
export const sameStamp = (a, b) => a.generation === b.generation && a.intentVersion === b.intentVersion;
|
|
92
98
|
function sourceFailure(error) {
|
|
93
99
|
return typeof error === "string"
|
|
@@ -529,13 +535,12 @@ export function player(scope) {
|
|
|
529
535
|
.refresh()
|
|
530
536
|
.catch(() => { });
|
|
531
537
|
},
|
|
532
|
-
async setStepSeek({ seconds }) {
|
|
533
|
-
if (!
|
|
534
|
-
seconds < 0.1 ||
|
|
535
|
-
seconds > 3600 ||
|
|
536
|
-
Math.abs(Math.round(seconds * 10) - seconds * 10) > 1e-9)
|
|
538
|
+
async setStepSeek({ seconds, keyframes }) {
|
|
539
|
+
if (!validStep(seconds))
|
|
537
540
|
throw failure("invalid", "seconds must be 0.1–3600 in tenths");
|
|
538
|
-
|
|
541
|
+
if (keyframes !== undefined && typeof keyframes !== "boolean")
|
|
542
|
+
throw failure("invalid", "keyframes must be a boolean");
|
|
543
|
+
await scope.runtime.host.step.setStepSeek(seconds, keyframes);
|
|
539
544
|
},
|
|
540
545
|
async audioDevices() {
|
|
541
546
|
const devices = await scope.runtime.host.audioOutput.devices();
|
package/dist/plugin/services.js
CHANGED
|
@@ -8,7 +8,7 @@ import { mkdir, open, rename, rm, stat } from "node:fs/promises";
|
|
|
8
8
|
import { join, resolve } from "node:path";
|
|
9
9
|
import { failure } from "./runtime.js";
|
|
10
10
|
import { systemServices } from "./local.js";
|
|
11
|
-
import { playerOf } from "./player.js";
|
|
11
|
+
import { playerOf, validStep } from "./player.js";
|
|
12
12
|
const MAX_OUTPUT = 16 * 1024 * 1024;
|
|
13
13
|
export const clipboard = (scope) => ({
|
|
14
14
|
readText: () => scope.runtime.host.clipboard.readText(),
|
|
@@ -32,10 +32,17 @@ const ACTIONS = new Set([
|
|
|
32
32
|
"mute",
|
|
33
33
|
]);
|
|
34
34
|
export const host = (scope) => ({
|
|
35
|
-
async run(action) {
|
|
35
|
+
async run(action, options) {
|
|
36
36
|
if (!ACTIONS.has(action))
|
|
37
37
|
throw failure("invalid", `unknown host action ${String(action)}`);
|
|
38
|
-
|
|
38
|
+
const seconds = options?.seconds;
|
|
39
|
+
if (options !== undefined) {
|
|
40
|
+
if (action !== "seek-backward" && action !== "seek-forward")
|
|
41
|
+
throw failure("invalid", `seconds applies only to seek-backward and seek-forward, not ${action}`);
|
|
42
|
+
if (!validStep(seconds))
|
|
43
|
+
throw failure("invalid", "seconds must be 0.1–3600 in tenths");
|
|
44
|
+
}
|
|
45
|
+
await scope.runtime.host.host.run(action, seconds);
|
|
39
46
|
// Like the player's own controls: `player.state` shows the result.
|
|
40
47
|
if (scope.runtime.can("playback.read"))
|
|
41
48
|
await playerOf(scope.runtime)
|
package/dist/plugin/testing.d.ts
CHANGED
|
@@ -10,7 +10,7 @@ import { OSD_LIMITS } from "./osd.ts";
|
|
|
10
10
|
import { SECRET_LIMITS } from "./services.ts";
|
|
11
11
|
import { BINDING_INPUTS } from "./bindings.ts";
|
|
12
12
|
import { THEME_PALETTES } from "../palettes.ts";
|
|
13
|
-
import { UI_VALUE_LIMITS } from "./values.ts";
|
|
13
|
+
import { IMAGE_LIMITS, UI_VALUE_LIMITS } from "./values.ts";
|
|
14
14
|
import type { CommandDeclaration } from "../commands.ts";
|
|
15
15
|
import type { SourceState } from "../source.ts";
|
|
16
16
|
import type { SubtitleState } from "../subtitles.ts";
|
|
@@ -39,7 +39,7 @@ export { BINDING_INPUTS };
|
|
|
39
39
|
/** The colours of the player's palettes, by `Theme` token. */
|
|
40
40
|
export { THEME_PALETTES };
|
|
41
41
|
/** The player's property value limits, which the test host enforces as well. */
|
|
42
|
-
export { UI_VALUE_LIMITS };
|
|
42
|
+
export { IMAGE_LIMITS, UI_VALUE_LIMITS };
|
|
43
43
|
/**
|
|
44
44
|
* The player's limits on one text value ({@link Text}, {@link TextList},
|
|
45
45
|
* with everything nested in it), which the test host enforces as well:
|
|
@@ -618,14 +618,19 @@ export declare class TestHost implements Transport {
|
|
|
618
618
|
title?: string | null;
|
|
619
619
|
}[]): void;
|
|
620
620
|
/**
|
|
621
|
-
* Runtime images written to views (`plugin.ui.image
|
|
622
|
-
*
|
|
621
|
+
* Runtime images written to views (`plugin.ui.image`,
|
|
622
|
+
* `plugin.ui.remoteImage`), whole values and (SDK 1.5.19) images nested
|
|
623
|
+
* in rows, newest last. The SDK refuses images of released scopes before
|
|
624
|
+
* anything is sent. The test host does not read the files, so it does not
|
|
625
|
+
* check the player's per-view budget (`IMAGE_LIMITS.viewBytes`).
|
|
623
626
|
*/
|
|
624
627
|
readonly images: {
|
|
625
628
|
/** The image file's path, as given to `ui.image`. */
|
|
626
629
|
path: string;
|
|
627
630
|
/** The SDK's reference number for that `ImageRef`. */
|
|
628
631
|
ref: number;
|
|
632
|
+
/** Its `maxSide` option, when given. */
|
|
633
|
+
maxSide?: number;
|
|
629
634
|
}[];
|
|
630
635
|
/**
|
|
631
636
|
* The file an image property shows, or `null` when the value is not a
|
|
@@ -1109,6 +1114,39 @@ export declare class TestHost implements Transport {
|
|
|
1109
1114
|
constructor();
|
|
1110
1115
|
/** The `player.setStepSeek` value in effect (default 5). */
|
|
1111
1116
|
stepSeek: number;
|
|
1117
|
+
/** The `player.setStepSeek` `keyframes` in effect (default `false`, SDK 1.5.19). */
|
|
1118
|
+
stepSeekKeyframes: boolean;
|
|
1119
|
+
/**
|
|
1120
|
+
* Test data: the keyframe times (seconds, ascending) of the current
|
|
1121
|
+
* source, for keyframe step seeks. `null` (the default): none modelled,
|
|
1122
|
+
* and step seeks are exact even with `stepSeekKeyframes`.
|
|
1123
|
+
*
|
|
1124
|
+
* @example
|
|
1125
|
+
* ```ts
|
|
1126
|
+
* host.setSource({ phase: "ready", duration: 45, position: 12 });
|
|
1127
|
+
* host.keyframes = [0, 10, 20, 30, 40];
|
|
1128
|
+
* await plugin.player.setStepSeek({ seconds: 5, keyframes: true });
|
|
1129
|
+
* await plugin.host.run("seek-forward");
|
|
1130
|
+
* assert.equal(host.source.position, 20);
|
|
1131
|
+
* assert.equal(host.osd.at(-1), "Forward 5 s: 00:20 / 00:45");
|
|
1132
|
+
* ```
|
|
1133
|
+
*/
|
|
1134
|
+
keyframes: number[] | null;
|
|
1135
|
+
/**
|
|
1136
|
+
* Every step seek the core ran (`seek-backward` / `seek-forward`, from a
|
|
1137
|
+
* key, the menu or `host.run`), newest last: the step, whether it went to
|
|
1138
|
+
* a keyframe, and where it landed.
|
|
1139
|
+
*/
|
|
1140
|
+
readonly coreSeeks: {
|
|
1141
|
+
/** `seek-backward` or `seek-forward`. */
|
|
1142
|
+
action: string;
|
|
1143
|
+
/** The step: `host.run`'s `seconds`, else `setStepSeek`'s. */
|
|
1144
|
+
seconds: number;
|
|
1145
|
+
/** It landed on a keyframe (`stepSeekKeyframes` and keyframes modelled). */
|
|
1146
|
+
keyframes: boolean;
|
|
1147
|
+
/** The new position. */
|
|
1148
|
+
landed: number;
|
|
1149
|
+
}[];
|
|
1112
1150
|
/** Every `player.step`, newest last. */
|
|
1113
1151
|
readonly steps: {
|
|
1114
1152
|
/** `"frame"` or `"keyframe"`. */
|
|
@@ -1339,7 +1377,7 @@ export declare class TestHost implements Transport {
|
|
|
1339
1377
|
/** Every `packages.setEnabled` / `uninstall` / `install`, newest last. */
|
|
1340
1378
|
readonly packageCalls: {
|
|
1341
1379
|
/** Which call. */
|
|
1342
|
-
method: "setEnabled" | "uninstall" | "install";
|
|
1380
|
+
method: "setEnabled" | "reload" | "uninstall" | "install";
|
|
1343
1381
|
/** The target plugin id. */
|
|
1344
1382
|
id: string;
|
|
1345
1383
|
/** `setEnabled`'s value. */
|
|
@@ -1368,9 +1406,10 @@ export declare class TestHost implements Transport {
|
|
|
1368
1406
|
*/
|
|
1369
1407
|
readonly candidates: Map<string, PackageManifest>;
|
|
1370
1408
|
/**
|
|
1371
|
-
* Remote
|
|
1372
|
-
* JPEG file served for it, or `null` for a failing
|
|
1373
|
-
* fetched from the network in tests; unknown
|
|
1409
|
+
* Remote images (`plugin.ui.markdown`, `plugin.ui.remoteImage`): address
|
|
1410
|
+
* → a local PNG or JPEG file served for it, or `null` for a failing
|
|
1411
|
+
* download. Nothing is fetched from the network in tests; unknown
|
|
1412
|
+
* addresses fail.
|
|
1374
1413
|
*/
|
|
1375
1414
|
readonly remoteImages: Map<string, string | null>;
|
|
1376
1415
|
/**
|
package/dist/plugin/testing.js
CHANGED
|
@@ -72,7 +72,8 @@ import { OSD_LIMITS } from "./osd.js";
|
|
|
72
72
|
import { SECRET_LIMITS } from "./services.js";
|
|
73
73
|
import { BINDING_INPUTS } from "./bindings.js";
|
|
74
74
|
import { THEME_PALETTES } from "../palettes.js";
|
|
75
|
-
import { UI_VALUE_LIMITS, valueLimitProblem } from "./values.js";
|
|
75
|
+
import { IMAGE_LIMITS, UI_VALUE_LIMITS, valueLimitProblem } from "./values.js";
|
|
76
|
+
import { validStep } from "./player.js";
|
|
76
77
|
import { nodeProblem } from "../processing.js";
|
|
77
78
|
import { RENDER_DEFAULTS, renderProblem, VIEW_DEFAULTS, viewProblem, } from "./view.js";
|
|
78
79
|
import { createPlugin } from "./index.js";
|
|
@@ -97,7 +98,7 @@ export { BINDING_INPUTS };
|
|
|
97
98
|
/** The colours of the player's palettes, by `Theme` token. */
|
|
98
99
|
export { THEME_PALETTES };
|
|
99
100
|
/** The player's property value limits, which the test host enforces as well. */
|
|
100
|
-
export { UI_VALUE_LIMITS };
|
|
101
|
+
export { IMAGE_LIMITS, UI_VALUE_LIMITS };
|
|
101
102
|
/**
|
|
102
103
|
* The player's limits on one text value ({@link Text}, {@link TextList},
|
|
103
104
|
* with everything nested in it), which the test host enforces as well:
|
|
@@ -625,8 +626,11 @@ export class TestHost {
|
|
|
625
626
|
this.emit("timeline.changed", { marks: this.timeline.marks });
|
|
626
627
|
}
|
|
627
628
|
/**
|
|
628
|
-
* Runtime images written to views (`plugin.ui.image
|
|
629
|
-
*
|
|
629
|
+
* Runtime images written to views (`plugin.ui.image`,
|
|
630
|
+
* `plugin.ui.remoteImage`), whole values and (SDK 1.5.19) images nested
|
|
631
|
+
* in rows, newest last. The SDK refuses images of released scopes before
|
|
632
|
+
* anything is sent. The test host does not read the files, so it does not
|
|
633
|
+
* check the player's per-view budget (`IMAGE_LIMITS.viewBytes`).
|
|
630
634
|
*/
|
|
631
635
|
images = [];
|
|
632
636
|
/**
|
|
@@ -700,14 +704,22 @@ export class TestHost {
|
|
|
700
704
|
return kept;
|
|
701
705
|
}
|
|
702
706
|
#seeImages(values) {
|
|
703
|
-
|
|
707
|
+
const see = (value) => {
|
|
704
708
|
const path = this.imagePath(value);
|
|
705
|
-
if (path !== null)
|
|
709
|
+
if (path !== null) {
|
|
710
|
+
const { ref, maxSide } = value;
|
|
706
711
|
this.images.push({
|
|
707
712
|
path,
|
|
708
|
-
ref: Number(
|
|
713
|
+
ref: Number(ref),
|
|
714
|
+
...(maxSide === undefined ? {} : { maxSide }),
|
|
709
715
|
});
|
|
710
|
-
|
|
716
|
+
}
|
|
717
|
+
else if (value !== null && typeof value === "object")
|
|
718
|
+
for (const item of Object.values(value))
|
|
719
|
+
see(item);
|
|
720
|
+
};
|
|
721
|
+
for (const value of Object.values(values))
|
|
722
|
+
see(value);
|
|
711
723
|
}
|
|
712
724
|
// ------------------------------------------------------------ round 3b batch 2
|
|
713
725
|
/** `drops.provide` registrations of this plugin, in order (`id` is the SDK's). */
|
|
@@ -1241,6 +1253,30 @@ export class TestHost {
|
|
|
1241
1253
|
// ------------------------------------------------------------ core actions
|
|
1242
1254
|
/** The `player.setStepSeek` value in effect (default 5). */
|
|
1243
1255
|
stepSeek = 5;
|
|
1256
|
+
/** The `player.setStepSeek` `keyframes` in effect (default `false`, SDK 1.5.19). */
|
|
1257
|
+
stepSeekKeyframes = false;
|
|
1258
|
+
/**
|
|
1259
|
+
* Test data: the keyframe times (seconds, ascending) of the current
|
|
1260
|
+
* source, for keyframe step seeks. `null` (the default): none modelled,
|
|
1261
|
+
* and step seeks are exact even with `stepSeekKeyframes`.
|
|
1262
|
+
*
|
|
1263
|
+
* @example
|
|
1264
|
+
* ```ts
|
|
1265
|
+
* host.setSource({ phase: "ready", duration: 45, position: 12 });
|
|
1266
|
+
* host.keyframes = [0, 10, 20, 30, 40];
|
|
1267
|
+
* await plugin.player.setStepSeek({ seconds: 5, keyframes: true });
|
|
1268
|
+
* await plugin.host.run("seek-forward");
|
|
1269
|
+
* assert.equal(host.source.position, 20);
|
|
1270
|
+
* assert.equal(host.osd.at(-1), "Forward 5 s: 00:20 / 00:45");
|
|
1271
|
+
* ```
|
|
1272
|
+
*/
|
|
1273
|
+
keyframes = null;
|
|
1274
|
+
/**
|
|
1275
|
+
* Every step seek the core ran (`seek-backward` / `seek-forward`, from a
|
|
1276
|
+
* key, the menu or `host.run`), newest last: the step, whether it went to
|
|
1277
|
+
* a keyframe, and where it landed.
|
|
1278
|
+
*/
|
|
1279
|
+
coreSeeks = [];
|
|
1244
1280
|
/** Every `player.step`, newest last. */
|
|
1245
1281
|
steps = [];
|
|
1246
1282
|
/** Every core action run (`host.run`, a core command, a pressed core binding), newest last. */
|
|
@@ -1470,7 +1506,7 @@ export class TestHost {
|
|
|
1470
1506
|
this.coreCommands.set(`host/${id}`, command);
|
|
1471
1507
|
}
|
|
1472
1508
|
}
|
|
1473
|
-
#runCore(action) {
|
|
1509
|
+
#runCore(action, seconds) {
|
|
1474
1510
|
const state = this.#coreState(action);
|
|
1475
1511
|
if (!state.enabled)
|
|
1476
1512
|
throw new HostError("unavailable", state.reason ?? "unavailable");
|
|
@@ -1512,13 +1548,36 @@ export class TestHost {
|
|
|
1512
1548
|
return;
|
|
1513
1549
|
case "seek-backward":
|
|
1514
1550
|
case "seek-forward": {
|
|
1515
|
-
const
|
|
1551
|
+
const step = seconds ?? this.stepSeek;
|
|
1552
|
+
const delta = action === "seek-forward" ? step : -step;
|
|
1516
1553
|
const base = s.position ?? 0;
|
|
1517
|
-
|
|
1554
|
+
let target = Math.max(0, Math.min(s.duration ?? Infinity, base + delta));
|
|
1555
|
+
// Keyframe mode, as the player: forward on the first keyframe at or
|
|
1556
|
+
// after the target (none: the exact target), backward on the last at
|
|
1557
|
+
// or before it; only sources with moving video that can seek.
|
|
1558
|
+
const keyframes = this.stepSeekKeyframes &&
|
|
1559
|
+
this.keyframes !== null &&
|
|
1560
|
+
s.hasVideo !== false &&
|
|
1561
|
+
s.capabilities.seek;
|
|
1562
|
+
if (keyframes && target !== base) {
|
|
1563
|
+
const keys = this.keyframes;
|
|
1564
|
+
target =
|
|
1565
|
+
delta > 0
|
|
1566
|
+
? (keys.find((k) => k >= target) ?? target)
|
|
1567
|
+
: keys
|
|
1568
|
+
.filter((k) => k <= target)
|
|
1569
|
+
.reduce((a, k) => Math.max(a, k), 0);
|
|
1570
|
+
}
|
|
1518
1571
|
this.#intent({
|
|
1519
1572
|
kind: "control",
|
|
1520
1573
|
control: { kind: "seek", seconds: target },
|
|
1521
1574
|
});
|
|
1575
|
+
this.coreSeeks.push({
|
|
1576
|
+
action,
|
|
1577
|
+
seconds: step,
|
|
1578
|
+
keyframes,
|
|
1579
|
+
landed: target,
|
|
1580
|
+
});
|
|
1522
1581
|
this.osd.push(this.#hostText(delta < 0 ? "Back {} s: {} / {}" : "Forward {} s: {} / {}", [Math.abs(delta), time(target), time(s.duration)]));
|
|
1523
1582
|
return;
|
|
1524
1583
|
}
|
|
@@ -1886,9 +1945,10 @@ export class TestHost {
|
|
|
1886
1945
|
*/
|
|
1887
1946
|
candidates = new Map();
|
|
1888
1947
|
/**
|
|
1889
|
-
* Remote
|
|
1890
|
-
* JPEG file served for it, or `null` for a failing
|
|
1891
|
-
* fetched from the network in tests; unknown
|
|
1948
|
+
* Remote images (`plugin.ui.markdown`, `plugin.ui.remoteImage`): address
|
|
1949
|
+
* → a local PNG or JPEG file served for it, or `null` for a failing
|
|
1950
|
+
* download. Nothing is fetched from the network in tests; unknown
|
|
1951
|
+
* addresses fail.
|
|
1892
1952
|
*/
|
|
1893
1953
|
remoteImages = new Map();
|
|
1894
1954
|
/**
|
|
@@ -2678,14 +2738,27 @@ export class TestHost {
|
|
|
2678
2738
|
];
|
|
2679
2739
|
if (!actions.includes(String(p.action)))
|
|
2680
2740
|
throw new HostError("invalid", `unknown host action ${p.action}`);
|
|
2681
|
-
|
|
2741
|
+
// SDK 1.5.19: one step seek of another step.
|
|
2742
|
+
let seconds;
|
|
2743
|
+
if (p.seconds !== undefined && p.seconds !== null) {
|
|
2744
|
+
if (p.action !== "seek-backward" && p.action !== "seek-forward")
|
|
2745
|
+
throw new HostError("invalid", `seconds applies only to seek-backward and seek-forward, not ${p.action}`);
|
|
2746
|
+
if (!validStep(p.seconds))
|
|
2747
|
+
throw new HostError("invalid", "seconds must be 0.1..3600 in tenths");
|
|
2748
|
+
seconds = p.seconds;
|
|
2749
|
+
}
|
|
2750
|
+
this.#runCore(String(p.action), seconds);
|
|
2682
2751
|
},
|
|
2683
2752
|
"player.setStepSeek": (p) => {
|
|
2684
|
-
|
|
2685
|
-
if (!(seconds >= 0.1 && seconds <= 3600) ||
|
|
2686
|
-
Math.abs(Math.round(seconds * 10) - seconds * 10) > 1e-9)
|
|
2753
|
+
if (!validStep(p.seconds))
|
|
2687
2754
|
throw new HostError("invalid", "seconds must be 0.1..3600 in tenths");
|
|
2688
|
-
|
|
2755
|
+
if (p.keyframes !== undefined &&
|
|
2756
|
+
p.keyframes !== null &&
|
|
2757
|
+
typeof p.keyframes !== "boolean")
|
|
2758
|
+
throw new HostError("invalid", "keyframes must be a boolean");
|
|
2759
|
+
// Written as a pair, as the player does.
|
|
2760
|
+
this.stepSeek = p.seconds;
|
|
2761
|
+
this.stepSeekKeyframes = p.keyframes === true;
|
|
2689
2762
|
},
|
|
2690
2763
|
"player.step": (p) => {
|
|
2691
2764
|
this.#audioCondition(p.expected);
|
|
@@ -3174,6 +3247,28 @@ export class TestHost {
|
|
|
3174
3247
|
entry.state = p.enabled === true ? "running" : "disabled";
|
|
3175
3248
|
this.setPackages(this.packages);
|
|
3176
3249
|
},
|
|
3250
|
+
// As the player (SDK 1.5.19): stop and start again, built-ins included;
|
|
3251
|
+
// `packages.changed` shows `starting`, then `running`.
|
|
3252
|
+
"packages.reload": (p) => {
|
|
3253
|
+
const id = String(p.id);
|
|
3254
|
+
if (id === this.manifestId)
|
|
3255
|
+
throw new HostError("invalid", "a plugin cannot reload itself");
|
|
3256
|
+
const entry = this.packages.find((each) => each.id === id);
|
|
3257
|
+
if (!entry)
|
|
3258
|
+
throw new HostError("missing", `no plugin ${id}`);
|
|
3259
|
+
if (entry.state === "starting")
|
|
3260
|
+
throw new HostError("busy", "the package is being installed or reloaded");
|
|
3261
|
+
if (entry.state === "disabled")
|
|
3262
|
+
throw new HostError("invalid", `${id} is disabled: enable it instead of reloading it`);
|
|
3263
|
+
this.packageCalls.push({ method: "reload", id });
|
|
3264
|
+
entry.state = "starting";
|
|
3265
|
+
entry.error = null;
|
|
3266
|
+
this.setPackages(this.packages);
|
|
3267
|
+
setTimeout(() => {
|
|
3268
|
+
entry.state = "running";
|
|
3269
|
+
this.setPackages(this.packages);
|
|
3270
|
+
}, 0);
|
|
3271
|
+
},
|
|
3177
3272
|
"packages.validate": (p) => this.#validate(String(p.directory)),
|
|
3178
3273
|
"packages.validateArchive": (p) => this.#validate(String(p.file)),
|
|
3179
3274
|
"packages.install": (p) => {
|