@vestra-player/sdk 1.5.17 → 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 CHANGED
@@ -1,5 +1,31 @@
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
+
21
+ ## 1.5.18
22
+
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.
24
+
25
+ - **`TestHost` streams on Windows**: `openStream`'s default transport listened on a socket file in the temp folder, which Windows refuses (`listen EACCES`), so stream tests failed there. It now listens on a named pipe (`\\.\pipe\vestra-test-host-…-producer-<id>`) on Windows and on a Unix socket elsewhere. `producerTransport = "tcp"` still gives the player's own Windows transport (loopback TCP with a token).
26
+ - **The CLI runs from an SDK outside any `node_modules`** (a copy with TypeScript on `NODE_PATH`, as the player's AI workshop vendors it): `dist/i18n-tools.js` resolved TypeScript with a bare ES module import, which ignores `NODE_PATH`, so `vestra-plugin` stopped at start with `ERR_MODULE_NOT_FOUND`. It now resolves TypeScript as the rest of the CLI does.
27
+ - **Built-in plugins cannot be disabled** (user ruling, 2026-10-26: users manage third-party plugins only). The player and `TestHost` answer `packages.setEnabled(builtin, false)` with `unsupported`; a built-in an older player let the user disable runs again after the upgrade. `PluginInfo.builtin`, `Packages.setEnabled` and *Migrating off package rollback* say so; a plugin manager offers no action for a built-in.
28
+
3
29
  ## 1.5.17
4
30
 
5
31
  **Package rollback is removed**, as announced in 1.5.16: the player keeps no previous plugin version. `apiVersion` stays `vestra/1` (a patch release, as when 1.5.8 removed `PackageCandidate.trial()`); plugins that do not manage other plugins need no change. A plugin manager that still calls `rollback` or reads `previousVersion` no longer compiles. Migration: [Migrating off package rollback](docs/how-to/rollback-migration.md).
@@ -131,6 +131,7 @@ export const CAPABILITY_INFO = Object.freeze({
131
131
  "Packages.validate",
132
132
  "Packages.validateArchive",
133
133
  "Packages.setEnabled",
134
+ "Packages.reload",
134
135
  "Packages.uninstall",
135
136
  ],
136
137
  requires: [],
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
@@ -1,4 +1,4 @@
1
- import ts from "typescript";
1
+ import type TS from "typescript";
2
2
  /** A translatable text found in the sources. */
3
3
  export interface Message {
4
4
  context?: string;
@@ -27,7 +27,7 @@ export interface ExtractOptions {
27
27
  /** The plugin's production `.ts` / `.mts` files, relative to `directory`. */
28
28
  typescript: readonly string[];
29
29
  /** Compiler options for a program over those files (as `check` uses). */
30
- compilerOptions?: ts.CompilerOptions;
30
+ compilerOptions?: TS.CompilerOptions;
31
31
  }
32
32
  export declare const pluralForms: (language: string) => "nplurals=1; plural=0;" | "nplurals=2; plural=(n != 1);";
33
33
  /** Find every translatable text of a plugin, in reference order. */
@@ -1,3 +1,11 @@
1
+ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
2
+ if (typeof path === "string" && /^\.\.?\//.test(path)) {
3
+ return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
4
+ return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
5
+ });
6
+ }
7
+ return path;
8
+ };
1
9
  /**
2
10
  * `vestra-plugin i18n extract` and `vestra-plugin i18n check`, and the
3
11
  * catalog step of `vestra-plugin build` (docs/how-to/i18n.md): find the
@@ -16,9 +24,19 @@
16
24
  * @module
17
25
  */
18
26
  import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync, } from "node:fs";
27
+ import { createRequire } from "node:module";
19
28
  import { join, relative, sep } from "node:path";
20
- import ts from "typescript";
29
+ import { pathToFileURL } from "node:url";
21
30
  import { compileCatalog, keyOf, LOCALES, parsePo, placeholders, scanSlint, writePo, } from "./i18n.js";
31
+ /**
32
+ * The SDK's TypeScript dependency, resolved the way the CLI resolves it
33
+ * (`compiler()` in bin/vestra-plugin.mjs): through `require` resolution from
34
+ * this package, which also searches `NODE_PATH`. A bare `import` of
35
+ * "typescript" would search only ancestor `node_modules` directories, so an
36
+ * SDK copied outside any (the player's AI workshop vendors one, and supplies
37
+ * TypeScript on `NODE_PATH`) could not start the CLI.
38
+ */
39
+ const ts = (await import(__rewriteRelativeImportExtension(pathToFileURL(createRequire(import.meta.url).resolve("typescript")).href))).default;
22
40
  /** Locales whose plural has one form (`n == 1` and other are the same). */
23
41
  const ONE_FORM = new Set(["zh-Hans", "ja", "ko"]);
24
42
  export const pluralForms = (language) => ONE_FORM.has(language)
@@ -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";
@@ -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";
@@ -4,4 +4,11 @@ import type { LocalServices } from "../wire.ts";
4
4
  * `ffmpeg` with each `PATHEXT` extension (`ffmpeg.exe` first by default).
5
5
  */
6
6
  export declare function executableNames(tool: string, platform?: NodeJS.Platform, pathext?: string | undefined): string[];
7
+ /**
8
+ * Where a local stream listens: a Unix socket file `<name>.sock` in
9
+ * `directory` on macOS and Linux; on Windows a named pipe
10
+ * (`\\.\pipe\<directory's name>-<name>`), which Node's `net` listens on and
11
+ * connects to by that path (a socket file there fails with `EACCES`).
12
+ */
13
+ export declare function localSocketPath(directory: string, name: string, platform?: NodeJS.Platform): string;
7
14
  export declare const systemServices: LocalServices;
@@ -31,6 +31,18 @@ export function executableNames(tool, platform = process.platform, pathext = pro
31
31
  .filter((extension) => extension.startsWith("."));
32
32
  return [...new Set(extensions)].map((extension) => tool + extension);
33
33
  }
34
+ /**
35
+ * Where a local stream listens: a Unix socket file `<name>.sock` in
36
+ * `directory` on macOS and Linux; on Windows a named pipe
37
+ * (`\\.\pipe\<directory's name>-<name>`), which Node's `net` listens on and
38
+ * connects to by that path (a socket file there fails with `EACCES`).
39
+ */
40
+ export function localSocketPath(directory, name, platform = process.platform) {
41
+ if (platform !== "win32")
42
+ return join(directory, `${name}.sock`);
43
+ const scope = directory.split(/[\\/]/).filter(Boolean).at(-1) ?? "vestra";
44
+ return `\\\\.\\pipe\\${scope}-${name}`;
45
+ }
34
46
  async function executable(path) {
35
47
  try {
36
48
  await access(path, constants.X_OK);
@@ -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>;
@@ -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
- const info = await stat(file).catch(() => null);
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 = [];
@@ -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({
@@ -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. */
@@ -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 (!Number.isFinite(seconds) ||
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
- await scope.runtime.host.step.setStepSeek(seconds);
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();
@@ -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
- await scope.runtime.host.host.run(action);
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)
@@ -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`), newest last. The
622
- * SDK refuses images of released scopes before anything is sent.
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
@@ -917,8 +922,9 @@ export declare class TestHost implements Transport {
917
922
  */
918
923
  readonly jobs: Map<number, MediaJob>;
919
924
  /**
920
- * `openStream`'s transport: a Unix socket (macOS, Linux) or, with `"tcp"`,
921
- * loopback TCP where the plugin must send the token first (Windows).
925
+ * `openStream`'s transport: a local stream (a Unix socket on macOS and
926
+ * Linux, a named pipe on Windows) or, with `"tcp"`, loopback TCP where the
927
+ * plugin must send the token first, as the player on Windows answers.
922
928
  * Set it before the plugin calls `openStream`.
923
929
  */
924
930
  producerTransport: "unix" | "tcp";
@@ -1108,6 +1114,39 @@ export declare class TestHost implements Transport {
1108
1114
  constructor();
1109
1115
  /** The `player.setStepSeek` value in effect (default 5). */
1110
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
+ }[];
1111
1150
  /** Every `player.step`, newest last. */
1112
1151
  readonly steps: {
1113
1152
  /** `"frame"` or `"keyframe"`. */
@@ -1338,7 +1377,7 @@ export declare class TestHost implements Transport {
1338
1377
  /** Every `packages.setEnabled` / `uninstall` / `install`, newest last. */
1339
1378
  readonly packageCalls: {
1340
1379
  /** Which call. */
1341
- method: "setEnabled" | "uninstall" | "install";
1380
+ method: "setEnabled" | "reload" | "uninstall" | "install";
1342
1381
  /** The target plugin id. */
1343
1382
  id: string;
1344
1383
  /** `setEnabled`'s value. */
@@ -1367,9 +1406,10 @@ export declare class TestHost implements Transport {
1367
1406
  */
1368
1407
  readonly candidates: Map<string, PackageManifest>;
1369
1408
  /**
1370
- * Remote Markdown images (`plugin.ui.markdown`): address → a local PNG or
1371
- * JPEG file served for it, or `null` for a failing download. Nothing is
1372
- * fetched from the network in tests; unknown addresses fail.
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.
1373
1413
  */
1374
1414
  readonly remoteImages: Map<string, string | null>;
1375
1415
  /**