@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 +26 -0
- package/dist/capabilities.js +1 -0
- package/dist/commands.js +0 -1
- package/dist/i18n-tools.d.ts +2 -2
- package/dist/i18n-tools.js +19 -1
- package/dist/player-runtime.d.ts +1 -1
- package/dist/player-runtime.js +1 -1
- package/dist/plugin/local.d.ts +7 -0
- package/dist/plugin/local.js +12 -0
- 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 +50 -10
- package/dist/plugin/testing.js +124 -23
- package/dist/plugin/types.d.ts +179 -24
- 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 +171 -31
- package/llms.txt +23 -23
- package/package.json +1 -1
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).
|
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/i18n-tools.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
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?:
|
|
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. */
|
package/dist/i18n-tools.js
CHANGED
|
@@ -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
|
|
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)
|
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";
|
package/dist/plugin/local.d.ts
CHANGED
|
@@ -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;
|
package/dist/plugin/local.js
CHANGED
|
@@ -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>;
|
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
|
|
@@ -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
|
|
921
|
-
*
|
|
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
|
|
1371
|
-
* JPEG file served for it, or `null` for a failing
|
|
1372
|
-
* 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.
|
|
1373
1413
|
*/
|
|
1374
1414
|
readonly remoteImages: Map<string, string | null>;
|
|
1375
1415
|
/**
|