@vestra-player/sdk 1.5.31 → 1.5.34
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 +23 -0
- package/dist/capabilities.d.ts +6 -0
- package/dist/capabilities.js +4 -0
- package/dist/commands.d.ts +18 -0
- package/dist/commands.js +14 -0
- package/dist/i18n.d.ts +9 -0
- package/dist/i18n.js +9 -0
- package/dist/palettes.d.ts +2 -0
- package/dist/palettes.js +2 -0
- package/dist/player-runtime.d.ts +1 -0
- package/dist/player-runtime.js +1 -0
- package/dist/plugin/bindings.d.ts +2 -0
- package/dist/plugin/bindings.js +2 -0
- package/dist/plugin/osd.d.ts +2 -0
- package/dist/plugin/osd.js +2 -0
- package/dist/plugin/services.d.ts +4 -0
- package/dist/plugin/services.js +4 -0
- package/dist/plugin/subtitles.d.ts +59 -0
- package/dist/plugin/subtitles.js +70 -4
- package/dist/plugin/testing.d.ts +538 -12
- package/dist/plugin/testing.js +458 -23
- package/dist/plugin/types.d.ts +1524 -64
- package/dist/plugin/values.d.ts +5 -0
- package/dist/plugin/values.js +5 -0
- package/dist/processing.d.ts +6 -0
- package/dist/source-resolvers.d.ts +3 -0
- package/dist/subtitles.d.ts +3 -0
- package/dist/wire.d.ts +16 -1
- package/dist/wire.js +10 -0
- package/llms-full.txt +1489 -55
- package/llms.txt +22 -22
- package/package.json +1 -1
package/llms-full.txt
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# @vestra-player/sdk 1.5.
|
|
1
|
+
# @vestra-player/sdk 1.5.34: full reference
|
|
2
2
|
|
|
3
3
|
> Everything needed to write a Vestra Player plugin (plugin API `vestra/1`): the guides, every public declaration of `@vestra-player/sdk` and `@vestra-player/sdk/testing` with its documentation, and the complete source of the example plugins. Generated from the SDK's sources by scripts/llms.mjs; the short index is llms.txt.
|
|
4
4
|
|
|
@@ -740,7 +740,7 @@ plugin.video.view() returns the VideoView: aspect override, fit (keep with bars,
|
|
|
740
740
|
|
|
741
741
|
plugin.video.render() returns the RenderOptions of the GPU renderer: HDR to SDR tone mapping and the display's brightness, user shaders (`.glsl` files) and motion interpolation. Like the display layer they act at display time only, have one owner (independent of the display layer's owner) and merge writes.
|
|
742
742
|
|
|
743
|
-
- **Only the GPU renderer has them.** Under software rendering every `set` fails with `unsupported`; see Reference: platforms for where the player renders on the GPU. Treat `unsupported` as "hide these settings", not as an error.
|
|
743
|
+
- **Only the GPU renderer has them.** Under software rendering every `set` fails with `unsupported`; see Reference: platforms for where the player renders on the GPU. Treat `unsupported` as "hide these settings", not as an error. To know before creating the handle, read rendering from `plugin.app.system()` (player 0.5.12, no capability): `"gpu"` or `"software"`.
|
|
744
744
|
- A shader that does not compile is reported later: the status first becomes `applied`, then `failed` with code `failed` and the compiler's first error line once mpv first uses the shader.
|
|
745
745
|
|
|
746
746
|
## Concept: Windows, panels, settings pages, dialogs and the OSD
|
|
@@ -1134,7 +1134,7 @@ bar.on("forward30", () => plugin.host.run("seek-forward", { seconds: 30 })); //
|
|
|
1134
1134
|
|
|
1135
1135
|
#### Next and previous
|
|
1136
1136
|
|
|
1137
|
-
The player's Next and Previous items, keys and buttons ask the **sequence provider**, one plugin at a time, first come first served. The built-in playlist registers as provider when it starts
|
|
1137
|
+
The player's Next and Previous items, keys and buttons ask the **sequence provider**, one plugin at a time, first come first served. The built-in playlist registers as provider when it starts and a built-in plugin cannot be disabled, so in practice another plugin's `provideSequence` is refused (`sequence-taken`); only a development load that replaces the playlist under its id gets it. Any plugin can ask the provider with `player.next()` and `player.previous()`:
|
|
1138
1138
|
|
|
1139
1139
|
```ts
|
|
1140
1140
|
declare const items: string[];
|
|
@@ -1229,6 +1229,85 @@ Names are 1–64 of `a-z 0-9 . _ -`, values 1–1024 bytes, at most 256 names. H
|
|
|
1229
1229
|
|
|
1230
1230
|
For anything larger (a cache, downloaded files), write under `plugin.dataDirectory` with Node's `fs`. The player deletes the directory only when the plugin is uninstalled together with its data (`packages.uninstall` with `removeData`).
|
|
1231
1231
|
|
|
1232
|
+
## How-to: Load subtitles from a plugin
|
|
1233
|
+
|
|
1234
|
+
**Goal:** your plugin finds subtitles for the playing file (on disk, on a website, by converting another format), adds them as a track, selects them, and optionally styles them.
|
|
1235
|
+
|
|
1236
|
+
#### Capabilities
|
|
1237
|
+
|
|
1238
|
+
```json
|
|
1239
|
+
{
|
|
1240
|
+
"capabilities": ["subtitles.read", "subtitles.control", "events"]
|
|
1241
|
+
}
|
|
1242
|
+
```
|
|
1243
|
+
|
|
1244
|
+
`subtitles.read` reads the tracks and the selection; `subtitles.control` changes them (and needs `subtitles.read` too, because every change reads the current selection first); `events` makes the SDK read `plugin.subtitles.state` again when the source or its phase changes. The player has no subtitle event: a selection the user or another plugin changes shows only after `refresh()`.
|
|
1245
|
+
|
|
1246
|
+
#### Read the tracks
|
|
1247
|
+
|
|
1248
|
+
`plugin.subtitles.state` is `null` until first read; `refresh()` reads it from the player. Each track has an `id` (compare tracks by id), a `kind` (`embedded` in the file or `external`, loaded from a file), a `title` and a `language`.
|
|
1249
|
+
|
|
1250
|
+
```ts
|
|
1251
|
+
const state = plugin.subtitles.state ?? (await plugin.subtitles.refresh());
|
|
1252
|
+
for (const track of state.tracks)
|
|
1253
|
+
console.log(track.id, track.kind, track.language ?? "?", track.title);
|
|
1254
|
+
// The primary and the secondary subtitle; null when off.
|
|
1255
|
+
console.log(state.selected?.id, state.secondary?.id);
|
|
1256
|
+
```
|
|
1257
|
+
|
|
1258
|
+
#### Select a track, and handle `stale`
|
|
1259
|
+
|
|
1260
|
+
Without options a change reads the newest state itself and applies to it, so it almost never fails. Embedded tracks are chosen by number, though: a `select` after another file opened would pick that file's track with the same number. Pass the state you looked at as `ifCurrent`; then the change fails with `stale`, and does nothing, when the user picked another track or another file opened in between:
|
|
1261
|
+
|
|
1262
|
+
```ts
|
|
1263
|
+
const state = await plugin.subtitles.refresh();
|
|
1264
|
+
const english = state.tracks.find((track) => track.language === "en");
|
|
1265
|
+
if (english)
|
|
1266
|
+
await plugin.subtitles.select(english, { ifCurrent: state }).catch((error: PluginError) => {
|
|
1267
|
+
if (error.code !== "stale") throw error; // the user changed it meanwhile
|
|
1268
|
+
});
|
|
1269
|
+
```
|
|
1270
|
+
|
|
1271
|
+
`select(track, { role: "secondary" })` sets the second line shown together with the primary one (a text track other than the primary); `select(null)` turns a role off. A new source clears the secondary subtitle.
|
|
1272
|
+
|
|
1273
|
+
#### Upload a file
|
|
1274
|
+
|
|
1275
|
+
`load` takes the file's bytes: UTF-8 SRT, or ASS/SSA with `format: "ass"`, at most 2 MiB (SRT up to 10,000 cues, ASS up to 20,000 `Dialogue:` lines). Convert other formats (WebVTT, SAMI) to SRT first. The player adds an `external` track to the current source only, selects it unless `select: "none"`, and resolves with the track once the upload is committed. A plugin has at most 4 such tracks on a source at once and loads at most 16 files into it (removed ones count); past that `load` fails with `busy` (Limits).
|
|
1276
|
+
|
|
1277
|
+
```ts
|
|
1278
|
+
import { readFile } from "node:fs/promises";
|
|
1279
|
+
import { basename } from "node:path";
|
|
1280
|
+
declare const path: string;
|
|
1281
|
+
// ---cut---
|
|
1282
|
+
const bytes = await readFile(path);
|
|
1283
|
+
const track = await plugin.subtitles.load(bytes, {
|
|
1284
|
+
format: path.endsWith(".ass") ? "ass" : "srt",
|
|
1285
|
+
title: basename(path), // data: shown as it is
|
|
1286
|
+
select: "primary",
|
|
1287
|
+
signal: AbortSignal.timeout(30_000), // discards the upload if not committed in time
|
|
1288
|
+
});
|
|
1289
|
+
await plugin.osd.show("Loaded {}", [track.title]);
|
|
1290
|
+
```
|
|
1291
|
+
|
|
1292
|
+
A title that mixes words with data is a text value the player translates: `title: plugin.i18n.text("{} · {}", [plugin.i18n.text("Downloaded"), name])`. `remove(track)` takes away a track this plugin loaded; the next source removes it anyway.
|
|
1293
|
+
|
|
1294
|
+
#### Style the subtitles
|
|
1295
|
+
|
|
1296
|
+
`setStyle` sets this plugin's style layer: font (one of `state.fonts`), size at the 1080p reference height, colour, outline, margin, timing offset, box. Each plugin has one layer and its newest call replaces it. When several plugins set a layer, the player shows the one whose **manifest id sorts last**, whatever the order of the calls; `setStyle(null)` takes yours off.
|
|
1297
|
+
|
|
1298
|
+
```ts
|
|
1299
|
+
const { fonts } = await plugin.subtitles.refresh();
|
|
1300
|
+
if (fonts.length)
|
|
1301
|
+
await plugin.subtitles.setStyle({
|
|
1302
|
+
fontId: fonts[0]!.id,
|
|
1303
|
+
fontSize: 40,
|
|
1304
|
+
color: "white",
|
|
1305
|
+
outline: 2,
|
|
1306
|
+
marginPercent: 5,
|
|
1307
|
+
offsetMs: 0, // positive shows them later
|
|
1308
|
+
});
|
|
1309
|
+
```
|
|
1310
|
+
|
|
1232
1311
|
## How-to: Keep a slider responsive while dragging
|
|
1233
1312
|
|
|
1234
1313
|
**Goal:** a slider in your window drives something live (a video filter, the volume, a preview), the effect follows the pointer while the user drags, and nothing lags behind or queues up.
|
|
@@ -2488,6 +2567,7 @@ The table lists the API whose documentation names a player version, newest last:
|
|
|
2488
2567
|
| 0.5.3 | Scrub.seek |
|
|
2489
2568
|
| 0.5.5 | SessionStorage |
|
|
2490
2569
|
| 0.5.10 | CommandButton, CommandOptions.button |
|
|
2570
|
+
| 0.5.12 | SystemInfo.rendering |
|
|
2491
2571
|
|
|
2492
2572
|
The Slint library carries no version tags: `Theme.danger` needs player 0.3.0; `@vestra/icons.slint` and the palette `vestra-light` need 0.4.0 (Theme).
|
|
2493
2573
|
|
|
@@ -2645,6 +2725,21 @@ plugin.storage.session (player 0.5.5). A key out of its rule, or a value that is
|
|
|
2645
2725
|
| `SESSION_STORAGE_LIMITS.bytes` | 1048576 (1 MiB) | Bytes one plugin may hold at the same time: for every key, its UTF-8 bytes, its value written as compact JSON (`JSON.stringify(value)`) and `entryBytes`. |
|
|
2646
2726
|
| `SESSION_STORAGE_LIMITS.entryBytes` | 128 | What each key costs on top of its text and value (128): the player's own bookkeeping, so that many tiny entries stay within `bytes` of memory. |
|
|
2647
2727
|
|
|
2728
|
+
#### Subtitle files
|
|
2729
|
+
|
|
2730
|
+
Subtitles.load uploads a file as a track of the current source. A file over `fileBytes` fails with `invalid`; every other limit fails with `busy` and nothing is loaded. The counts are per source, so another file starts from zero. Removing a track frees its place among the tracks and their bytes, but a load stays counted until the source changes.
|
|
2731
|
+
|
|
2732
|
+
| Limit | Value | Meaning |
|
|
2733
|
+
| --- | --- | --- |
|
|
2734
|
+
| `SUBTITLE_LIMITS.fileBytes` | 2097152 (2 MiB) | UTF-8 bytes of one file; a larger one fails with `invalid`. |
|
|
2735
|
+
| `SUBTITLE_LIMITS.tracksPerPlugin` | 4 | Tracks one plugin has loaded into the current source at the same time. |
|
|
2736
|
+
| `SUBTITLE_LIMITS.tracks` | 16 | Loaded tracks of every plugin in the current source at the same time. |
|
|
2737
|
+
| `SUBTITLE_LIMITS.totalBytes` | 16777216 (16 MiB) | Bytes of the loaded tracks of every plugin in the current source together. |
|
|
2738
|
+
| `SUBTITLE_LIMITS.loadsPerPlugin` | 16 | Loads by one plugin into the current source, removed tracks included. |
|
|
2739
|
+
| `SUBTITLE_LIMITS.loads` | 128 | Loads by every plugin into the current source, removed tracks included. |
|
|
2740
|
+
| `SUBTITLE_LIMITS.uploadsPerPlugin` | 2 | Loads of one plugin in progress at the same time. |
|
|
2741
|
+
| `SUBTITLE_LIMITS.uploads` | 8 | Loads of every plugin in progress at the same time. |
|
|
2742
|
+
|
|
2648
2743
|
#### Messages
|
|
2649
2744
|
|
|
2650
2745
|
Every request a plugin sends travels as one protocol line. The plugin's side refuses a longer one with code `limit` before sending it; property values meet their own limits first.
|
|
@@ -3165,7 +3260,7 @@ A plugin uses the same API on macOS and Windows; the player does the system-spec
|
|
|
3165
3260
|
| DropModifiers | The pointer and ⌥ ⇧ ⌘ (`cmd` is ⌘). | The pointer and Alt, Shift, Ctrl (`cmd` is Ctrl). |
|
|
3166
3261
|
| plugin.system (open, reveal) | Opened by the system; revealing several files selects all of them in one Finder window. | Opened by the shell; one Explorer window per folder. |
|
|
3167
3262
|
| plugin.secrets | The login keychain. | The Credential Manager. |
|
|
3168
|
-
| App.system() | Version, build, model and displays. | Version, build, model and displays
|
|
3263
|
+
| App.system() | Version, build, model and displays; `rendering` is `"gpu"`, or `"software"` after a fallback (a window that cannot show GPU frames). | Version, build, model and displays; `rendering` is `"software"`. |
|
|
3169
3264
|
| Snapshots and exports (MediaCapture) | Available. | Available; needs FFmpeg 6.1 or newer. |
|
|
3170
3265
|
| Generated streams (Player.openStream) | A Unix socket. | Loopback TCP with a token. |
|
|
3171
3266
|
| Rendering | On the GPU (OpenGL), with hardware decoding. | In software, with hardware decoding copied back (`d3d11va-copy` and similar). |
|
|
@@ -3494,9 +3589,32 @@ flushes, a running `tools.run` process is killed and a listener is
|
|
|
3494
3589
|
unsubscribed. A Media is the exception: disposing its scope does
|
|
3495
3590
|
**not** close the media; call Media.close for that.
|
|
3496
3591
|
|
|
3592
|
+
Every handle a plugin creates is a `Disposable` owned by the scope that
|
|
3593
|
+
created it; disposing the scope (or stopping the plugin) disposes them in
|
|
3594
|
+
reverse creation order, so a plugin rarely calls `dispose` itself.
|
|
3595
|
+
|
|
3596
|
+
- @capability None of its own: that of the handle.
|
|
3597
|
+
|
|
3497
3598
|
```ts
|
|
3498
3599
|
interface Disposable {
|
|
3499
|
-
/**
|
|
3600
|
+
/**
|
|
3601
|
+
* Release the resource. Resolves once the player confirmed the release (or immediately for purely local resources). Never rejects.
|
|
3602
|
+
* @capability None of its own: that of the handle.
|
|
3603
|
+
* @throws Never.
|
|
3604
|
+
* @example Remove a command when its feature is turned off
|
|
3605
|
+
* A "Turn feature off" item removes the feature's own menu item at once,
|
|
3606
|
+
* before the plugin stops. `dispose` takes the command out of the menu and
|
|
3607
|
+
* the binding table and resolves once the player confirmed it; the id can
|
|
3608
|
+
* be added again later.
|
|
3609
|
+
* ```ts
|
|
3610
|
+
* const extra = plugin.commands.add({ id: "extra", path: ["Tools", "Extra"], run() {} });
|
|
3611
|
+
* plugin.commands.add({
|
|
3612
|
+
* id: "extra-off",
|
|
3613
|
+
* path: ["Tools", "Turn extra off"],
|
|
3614
|
+
* run: () => extra.dispose(), // never rejects
|
|
3615
|
+
* });
|
|
3616
|
+
* ```
|
|
3617
|
+
*/
|
|
3500
3618
|
dispose(): Promise<void>;
|
|
3501
3619
|
}
|
|
3502
3620
|
```
|
|
@@ -3522,7 +3640,6 @@ change).
|
|
|
3522
3640
|
| code | meaning |
|
|
3523
3641
|
| --- | --- |
|
|
3524
3642
|
| `stale` | A condition no longer held (the source or the subtitle selection changed); nothing was done. See Conditional. |
|
|
3525
|
-
| `staleDigest` | PackageCandidate.install: the package's files changed after it was validated; validate it again. |
|
|
3526
3643
|
| `capability` | The manifest does not declare the capability the call needs. |
|
|
3527
3644
|
| `invalid` | A bad argument (out of range, malformed, unknown id). |
|
|
3528
3645
|
| `missing` | The target does not exist (an empty panel position, a missing file, an unknown command). |
|
|
@@ -3542,7 +3659,7 @@ change).
|
|
|
3542
3659
|
| `backend-uncertain` | The player's backend blocked a filter stage; call `retry()`. |
|
|
3543
3660
|
| `limit` | A player-wide limit was reached (for example the number of commands), a property value is over the limits of PropValue, or a request is longer than the player's 1 MiB message. |
|
|
3544
3661
|
| `reserved` | Packages.validate, PackageCandidate.install: the id is `vestra` or starts with `vestra.` (any case), which only the player's built-in plugins use (player 0.4.1). |
|
|
3545
|
-
| `versionMismatch` | The package needs a newer player (`minPlayerVersion`)
|
|
3662
|
+
| `versionMismatch` | The package needs a newer player (`minPlayerVersion`). |
|
|
3546
3663
|
| `ui-prepare` | The player could not prepare a mounted panel or a settings page (its Slint file does not compile, a bound property is not declared, or another plugin's panel holds the slot); `message` has the diagnostics. |
|
|
3547
3664
|
| `ui` | The player could not apply a property to a tool window or a settings page, or could not build the Options window (a property the Slint file does not declare, a value of the wrong type); `message` has Slint's reason. Also reported to Scope.onError without a request: a callback whose arguments the player could not convert, a property it could not show again when it rebuilt a view (an image file removed since). |
|
|
3548
3665
|
| `host` | Any other failure inside the player; `message` is the player's reason. |
|
|
@@ -3605,6 +3722,14 @@ Reading Scope.window, Scope.app, Scope.appearance
|
|
|
3605
3722
|
`.theme`, Scope.ui, Scope.tools, Scope.storage and
|
|
3606
3723
|
Scope.system needs none.
|
|
3607
3724
|
|
|
3725
|
+
Every module of the API (`commands`, `windows`, `player`…) is reachable
|
|
3726
|
+
from a scope, and every handle made through it belongs to it. A child
|
|
3727
|
+
scope from Scope.scope is the unit for features that come and
|
|
3728
|
+
go: dispose the child and everything it created is released, while the
|
|
3729
|
+
rest of the plugin keeps running.
|
|
3730
|
+
|
|
3731
|
+
- @capability That of each module (see the modules).
|
|
3732
|
+
|
|
3608
3733
|
```ts
|
|
3609
3734
|
interface Scope extends Disposable {
|
|
3610
3735
|
/** Menu commands and their default bindings (`commands`). */
|
|
@@ -3628,10 +3753,16 @@ interface Scope extends Disposable {
|
|
|
3628
3753
|
*/
|
|
3629
3754
|
filter(id: string): VideoFilter;
|
|
3630
3755
|
/**
|
|
3631
|
-
* The display layer (needs `processing.video`). The first plugin to call it owns the layer; another plugin's view fails with `conflict` until the owner disposes or stops.
|
|
3756
|
+
* The display layer (needs `processing.video`). The first plugin to call it owns the layer; another plugin's view fails with `conflict` until the owner disposes or stops.
|
|
3757
|
+
* @throws capability (throws) The manifest does not declare `processing.video`.
|
|
3758
|
+
* @throws Error without a code (throws) A second call in the same plugin while the first view is not disposed.
|
|
3632
3759
|
*/
|
|
3633
3760
|
view(): VideoView;
|
|
3634
|
-
/**
|
|
3761
|
+
/**
|
|
3762
|
+
* The GPU renderer's options (needs `processing.video`), owned like the display layer. Under software rendering the handle is `failed` with `unsupported` after the player's first answer, and `onError` hears it.
|
|
3763
|
+
* @throws capability (throws) The manifest does not declare `processing.video`.
|
|
3764
|
+
* @throws Error without a code (throws) A second call in the same plugin while the first handle is not disposed.
|
|
3765
|
+
*/
|
|
3635
3766
|
render(): RenderOptions;
|
|
3636
3767
|
};
|
|
3637
3768
|
/** Audio processing: this plugin's filter chain and delay (`processing.audio`, `playback.read` and `events`, as for VideoFilter). */
|
|
@@ -3685,17 +3816,57 @@ interface Scope extends Disposable {
|
|
|
3685
3816
|
readonly i18n: I18n;
|
|
3686
3817
|
/** Pages in the player's options window (`ui.settings`). */
|
|
3687
3818
|
readonly settings: Settings;
|
|
3688
|
-
/**
|
|
3819
|
+
/**
|
|
3820
|
+
* A child scope; disposing it releases only what was created through it. The child is itself owned by this scope.
|
|
3821
|
+
* @capability None.
|
|
3822
|
+
* @example Tie a temporary window and its timer to one feature
|
|
3823
|
+
* A "Live stats" feature opens a window and starts a timer; turning it off
|
|
3824
|
+
* releases both together. Everything made through the child scope, and
|
|
3825
|
+
* cleanups registered on it, go when the child is disposed; the parent
|
|
3826
|
+
* scope is untouched.
|
|
3827
|
+
* ```ts
|
|
3828
|
+
* let live: Scope | null = null;
|
|
3829
|
+
* plugin.commands.add({
|
|
3830
|
+
* id: "live-stats",
|
|
3831
|
+
* path: ["Tools", "Live stats"],
|
|
3832
|
+
* checked: () => live !== null,
|
|
3833
|
+
* run: async () => {
|
|
3834
|
+
* if (live) {
|
|
3835
|
+
* await live.dispose(); // closes the window, stops the timer
|
|
3836
|
+
* live = null;
|
|
3837
|
+
* return;
|
|
3838
|
+
* }
|
|
3839
|
+
* live = plugin.scope();
|
|
3840
|
+
* const window = live.windows.create("ui/stats.slint");
|
|
3841
|
+
* const timer = setInterval(() => live?.update(), 1000);
|
|
3842
|
+
* live.onDispose(() => clearInterval(timer));
|
|
3843
|
+
* await window.show();
|
|
3844
|
+
* },
|
|
3845
|
+
* });
|
|
3846
|
+
* ```
|
|
3847
|
+
*/
|
|
3689
3848
|
scope(): Scope;
|
|
3690
3849
|
/**
|
|
3691
3850
|
* Adopt an existing handle: it is disposed together with this scope.
|
|
3692
3851
|
* @param item - Any Disposable, for example a handle created through another scope.
|
|
3693
3852
|
* @returns `item` itself, for chaining.
|
|
3853
|
+
* @capability None.
|
|
3854
|
+
* @example Dispose a handle from another scope together with this one
|
|
3855
|
+
* A filter made through the root scope is withdrawn when a child feature
|
|
3856
|
+
* ends. `add` hands an existing disposable to this scope, which then
|
|
3857
|
+
* disposes it with its own handles; it returns the same handle for
|
|
3858
|
+
* chaining.
|
|
3859
|
+
* ```ts
|
|
3860
|
+
* const feature = plugin.scope();
|
|
3861
|
+
* const blur = feature.add(plugin.video.filter("blur")); // now ends with `feature`
|
|
3862
|
+
* blur.set([{ kind: "gblur", sigma: 4, steps: 3 }]);
|
|
3863
|
+
* ```
|
|
3694
3864
|
*/
|
|
3695
3865
|
add<T extends Disposable>(item: T): T;
|
|
3696
3866
|
/**
|
|
3697
3867
|
* Run `cleanup` when this scope is disposed (for example `clearInterval` or closing a socket). It may be async; errors are logged.
|
|
3698
3868
|
* @returns A handle that cancels the registration early (without running `cleanup`).
|
|
3869
|
+
* @capability None.
|
|
3699
3870
|
* @example Stop a timer when the plugin stops
|
|
3700
3871
|
* A once-a-second refresh runs as long as the plugin does. `onDispose`
|
|
3701
3872
|
* runs the cleanup when the scope is disposed, which for `plugin` is
|
|
@@ -3714,6 +3885,7 @@ interface Scope extends Disposable {
|
|
|
3714
3885
|
* Rejections of async handlers count too, and so do failures of
|
|
3715
3886
|
* registrations made after activation (a command whose `ready` rejects,
|
|
3716
3887
|
* a property write the player refused).
|
|
3888
|
+
* @capability None.
|
|
3717
3889
|
* @example Show errors from handlers on the OSD
|
|
3718
3890
|
* When a menu command, a window callback or a listener of the plugin
|
|
3719
3891
|
* fails, the user sees why. The SDK catches what those handlers throw
|
|
@@ -3733,6 +3905,23 @@ interface Scope extends Disposable {
|
|
|
3733
3905
|
* changing state from your own timers or between awaits of a long task.
|
|
3734
3906
|
*
|
|
3735
3907
|
* Several calls within one microtask are merged into one pass.
|
|
3908
|
+
* @capability None.
|
|
3909
|
+
* @example Show progress from a long task between awaits
|
|
3910
|
+
* A progress label moves while a long download runs, not only when it
|
|
3911
|
+
* ends. The SDK re-evaluates bound values after each handler and when its
|
|
3912
|
+
* promise settles; state changed between the awaits of a long handler
|
|
3913
|
+
* shows only after an explicit `update()`.
|
|
3914
|
+
* ```ts
|
|
3915
|
+
* let done = 0;
|
|
3916
|
+
* window.bind(() => ({ progress: done }));
|
|
3917
|
+
* window.on("start", async () => {
|
|
3918
|
+
* for (let part = 0; part < 10; part++) {
|
|
3919
|
+
* await downloadPart(part);
|
|
3920
|
+
* done = part + 1;
|
|
3921
|
+
* plugin.update(); // send `progress` now, not when the handler ends
|
|
3922
|
+
* }
|
|
3923
|
+
* });
|
|
3924
|
+
* ```
|
|
3736
3925
|
*/
|
|
3737
3926
|
update(): void;
|
|
3738
3927
|
}
|
|
@@ -3781,6 +3970,12 @@ root scope is disposed for local cleanup (timers, signals, sockets). A
|
|
|
3781
3970
|
process that has not exited after 300 ms is killed. Media the plugin
|
|
3782
3971
|
opened with Player.open keeps playing.
|
|
3783
3972
|
|
|
3973
|
+
The root scope: disposing it is what stopping the plugin does. Besides
|
|
3974
|
+
the modules of Scope, it carries the plugin's `id` and its
|
|
3975
|
+
`dataDirectory`.
|
|
3976
|
+
|
|
3977
|
+
- @capability That of each module (see the modules).
|
|
3978
|
+
|
|
3784
3979
|
```ts
|
|
3785
3980
|
interface Plugin extends Scope {
|
|
3786
3981
|
/** The manifest id, e.g. `"community.bookmark"`. */
|
|
@@ -5787,6 +5982,13 @@ interface DropHandler {
|
|
|
5787
5982
|
|
|
5788
5983
|
`plugin.drops`: files dropped on the video area. Needs `ui.drop`.
|
|
5789
5984
|
|
|
5985
|
+
Files dropped on the video area go to providers by extension: each
|
|
5986
|
+
extension belongs to the provider registered for it first, and each
|
|
5987
|
+
provider gets one call with all of its paths. Files nobody takes are
|
|
5988
|
+
ignored. A panel with Panel.onDrop takes drops on itself first.
|
|
5989
|
+
|
|
5990
|
+
- @capability ui.drop
|
|
5991
|
+
|
|
5790
5992
|
```ts
|
|
5791
5993
|
interface Drops {
|
|
5792
5994
|
/**
|
|
@@ -5797,6 +5999,7 @@ interface Drops {
|
|
|
5797
5999
|
* to the provider that registered it first) in the order the file
|
|
5798
6000
|
* manager gave them, one call per provider. Folders are not expanded.
|
|
5799
6001
|
* @throws invalid synchronously for malformed extensions.
|
|
6002
|
+
* @capability ui.drop
|
|
5800
6003
|
* @example Load dropped subtitle files
|
|
5801
6004
|
* Dropping .srt or .ass files on the video loads each one as a subtitle
|
|
5802
6005
|
* track. Each extension belongs to the provider that registered it first;
|
|
@@ -5822,6 +6025,25 @@ interface Drops {
|
|
|
5822
6025
|
}
|
|
5823
6026
|
```
|
|
5824
6027
|
|
|
6028
|
+
Example: Collect dropped CHIP-8 programs
|
|
6029
|
+
|
|
6030
|
+
`.ch8` files dropped on the player are added to the plugin's list. The
|
|
6031
|
+
provider claims the extension; the player calls `drop` with the dropped
|
|
6032
|
+
paths of that type, in the order the file manager gave them. Extensions
|
|
6033
|
+
go to the first provider: the built-in playlist claims every audio and
|
|
6034
|
+
video extension when it starts, and a built-in plugin cannot be disabled,
|
|
6035
|
+
so another plugin's provider for those never gets them (except a
|
|
6036
|
+
development load that replaces the playlist under its id).
|
|
6037
|
+
```ts
|
|
6038
|
+
const queue: string[] = [];
|
|
6039
|
+
plugin.drops.provide({
|
|
6040
|
+
extensions: ["ch8"],
|
|
6041
|
+
drop: ({ paths }) => {
|
|
6042
|
+
queue.push(...paths); // only .ch8 files reach here
|
|
6043
|
+
},
|
|
6044
|
+
});
|
|
6045
|
+
```
|
|
6046
|
+
|
|
5825
6047
|
### ThemeId
|
|
5826
6048
|
|
|
5827
6049
|
The player's palettes: `vestra`, the dark default, and `vestra-light`
|
|
@@ -6693,10 +6915,18 @@ interface TimelineMark {
|
|
|
6693
6915
|
|
|
6694
6916
|
This plugin's own marks; disposing it removes them.
|
|
6695
6917
|
|
|
6918
|
+
This plugin's marks on the seek bar, from Timeline.layer. `set`
|
|
6919
|
+
replaces the whole layer and never throws: the newest write wins, and a
|
|
6920
|
+
refused one is reported to `onError`. Disposing the layer, its scope or
|
|
6921
|
+
stopping the plugin removes the marks.
|
|
6922
|
+
|
|
6923
|
+
- @capability timeline.marks
|
|
6924
|
+
|
|
6696
6925
|
```ts
|
|
6697
6926
|
interface TimelineLayer extends Disposable {
|
|
6698
6927
|
/**
|
|
6699
6928
|
* Replace the layer (last write wins; `[]` clears it). At most 1000 marks, titles ≤ 100 characters, times within the media. Failures go to `onError`; with `ifCurrent`, a write for another source is dropped silently. A title is data (a chapter's or bookmark's own name), or a Text the player translates (`text("Chapter {}", [n])`, SDK 1.5.11); other plugins read the shown string.
|
|
6929
|
+
* @capability timeline.marks
|
|
6700
6930
|
* @example Show the file's bookmarks on the timeline
|
|
6701
6931
|
* Bookmark marks appear on the seek bar at their times, titled with the
|
|
6702
6932
|
* user's notes. `set` replaces this plugin's whole layer; with `ifCurrent`
|
|
@@ -6721,22 +6951,76 @@ interface TimelineLayer extends Disposable {
|
|
|
6721
6951
|
}
|
|
6722
6952
|
```
|
|
6723
6953
|
|
|
6954
|
+
Example: Clear the plugin's marks
|
|
6955
|
+
|
|
6956
|
+
The plugin's marks disappear from the seek bar while it keeps its
|
|
6957
|
+
layer for later. An empty list clears the layer; other plugins' marks
|
|
6958
|
+
stay.
|
|
6959
|
+
```ts
|
|
6960
|
+
plugin.timeline.layer().set([]);
|
|
6961
|
+
```
|
|
6962
|
+
|
|
6724
6963
|
### Timeline
|
|
6725
6964
|
|
|
6726
6965
|
`plugin.timeline`: marks on the seek bar. Every plugin publishes one
|
|
6727
6966
|
layer; the player merges them. A new source clears every layer.
|
|
6728
6967
|
|
|
6968
|
+
Marks on the player's seek bar: chapters, bookmarks, loop points. Each
|
|
6969
|
+
plugin publishes one layer with Timeline.layer; the player merges
|
|
6970
|
+
the layers by time and shows them. A new source clears every layer, so a
|
|
6971
|
+
plugin publishes again for each file. `marks` is the merged list, which
|
|
6972
|
+
any plugin can read.
|
|
6973
|
+
|
|
6974
|
+
- @capability `timeline.marks` to publish; `playback.read` to read; `events` to follow changes.
|
|
6975
|
+
|
|
6729
6976
|
```ts
|
|
6730
6977
|
interface Timeline {
|
|
6731
6978
|
/** Every layer merged, by time (`playback.read`). Empty on a new source. */
|
|
6732
6979
|
readonly marks: readonly TimelineMark[];
|
|
6733
|
-
/**
|
|
6980
|
+
/**
|
|
6981
|
+
* The merged list changed (needs `events`; at most every 100 ms).
|
|
6982
|
+
* @capability events and playback.read
|
|
6983
|
+
* @example Follow the marks as they change
|
|
6984
|
+
* A panel lists the seek bar's marks and updates when any plugin publishes
|
|
6985
|
+
* or a new file clears them. The listener gets the merged list, at most
|
|
6986
|
+
* every 100 ms.
|
|
6987
|
+
* ```ts
|
|
6988
|
+
* plugin.timeline.onChange((marks) => {
|
|
6989
|
+
* console.log(marks.map((mark) => `${mark.time}s ${mark.title ?? mark.kind}`));
|
|
6990
|
+
* });
|
|
6991
|
+
* ```
|
|
6992
|
+
*/
|
|
6734
6993
|
onChange(listener: (marks: readonly TimelineMark[]) => void): Disposable;
|
|
6735
|
-
/**
|
|
6994
|
+
/**
|
|
6995
|
+
* This plugin's layer (`timeline.marks`); a second call throws.
|
|
6996
|
+
* @capability timeline.marks
|
|
6997
|
+
* @example Publish this plugin's marks
|
|
6998
|
+
* The plugin gets its one layer and puts two chapter marks on the seek
|
|
6999
|
+
* bar. Calling `layer()` again while the first layer is not disposed
|
|
7000
|
+
* throws; disposing the layer removes the
|
|
7001
|
+
* marks.
|
|
7002
|
+
* ```ts
|
|
7003
|
+
* const layer = plugin.timeline.layer();
|
|
7004
|
+
* layer.set([
|
|
7005
|
+
* { time: 0, kind: "chapter", title: "Intro" },
|
|
7006
|
+
* { time: 95, kind: "chapter", title: "Part 1" }, // seconds
|
|
7007
|
+
* ]);
|
|
7008
|
+
* ```
|
|
7009
|
+
*/
|
|
6736
7010
|
layer(): TimelineLayer;
|
|
6737
7011
|
}
|
|
6738
7012
|
```
|
|
6739
7013
|
|
|
7014
|
+
Example: Count the chapter marks other plugins published
|
|
7015
|
+
|
|
7016
|
+
The plugin reads how many chapter marks the seek bar shows, whoever
|
|
7017
|
+
published them. `marks` is the merged list of every layer, sorted by
|
|
7018
|
+
time, with each mark's `plugin`.
|
|
7019
|
+
```ts
|
|
7020
|
+
const chapters = plugin.timeline.marks.filter((mark) => mark.kind === "chapter");
|
|
7021
|
+
await plugin.osd.show("{} chapters", [chapters.length]);
|
|
7022
|
+
```
|
|
7023
|
+
|
|
6740
7024
|
### WindowState
|
|
6741
7025
|
|
|
6742
7026
|
The main window as `plugin.window.state` reports it (logical points).
|
|
@@ -6772,11 +7056,28 @@ With `events`, `state` follows the window from the first read or
|
|
|
6772
7056
|
`onChange`; without it, it updates only at start and after this plugin's
|
|
6773
7057
|
`setVideoArea`.
|
|
6774
7058
|
|
|
7059
|
+
`plugin.window` is the player's main window: its size, fullscreen,
|
|
7060
|
+
picture-in-picture and the video area. Reading it needs no capability;
|
|
7061
|
+
changing it needs `window.control`. Changes that last only while the
|
|
7062
|
+
plugin runs (always on top, pointer auto-hide) revert when it stops.
|
|
7063
|
+
|
|
7064
|
+
- @capability None to read; window.control to change; events to follow outside changes.
|
|
7065
|
+
|
|
6775
7066
|
```ts
|
|
6776
7067
|
interface MainWindow {
|
|
6777
7068
|
/** Latest state; zero video area without a native window. */
|
|
6778
7069
|
readonly state: WindowState;
|
|
6779
|
-
/**
|
|
7070
|
+
/**
|
|
7071
|
+
* The window state changed (needs `events` for outside changes).
|
|
7072
|
+
* @capability events (for outside changes)
|
|
7073
|
+
* @example Fit a panel to fullscreen
|
|
7074
|
+
* The plugin's panel uses larger text while the player is fullscreen. The
|
|
7075
|
+
* listener runs after the SDK updated `state` from the player's events
|
|
7076
|
+
* (with `events`; without it only after this plugin's own changes).
|
|
7077
|
+
* ```ts
|
|
7078
|
+
* plugin.window.onChange((state) => bar.set({ large: state.fullscreen }));
|
|
7079
|
+
* ```
|
|
7080
|
+
*/
|
|
6780
7081
|
onChange(listener: (state: WindowState) => void): Disposable;
|
|
6781
7082
|
/**
|
|
6782
7083
|
* Resize the window so the video column is `width`×`height` (the bottom bar
|
|
@@ -6788,6 +7089,18 @@ interface MainWindow {
|
|
|
6788
7089
|
* Sizes are 1–16384 logical points; the minimum video area is 318×120.
|
|
6789
7090
|
* The visible area excludes the menu bar and Dock on macOS; on Windows it
|
|
6790
7091
|
* is the whole display. `unavailable` without a user interface too.
|
|
7092
|
+
* @capability window.control
|
|
7093
|
+
* @throws unavailable (rejects) Fullscreen, or no user interface.
|
|
7094
|
+
* @throws invalid (rejects) A size outside 1–16384 logical points.
|
|
7095
|
+
* @throws capability (rejects) The manifest does not declare `window.control`.
|
|
7096
|
+
* @example Make the video area 1280×720 points
|
|
7097
|
+
* The window resizes so that the video column is 1280×720 logical points,
|
|
7098
|
+
* the bars and a docked sidebar added around it. A request that does not
|
|
7099
|
+
* fit the screen is scaled down keeping the aspect, and `clamped` says so.
|
|
7100
|
+
* ```ts
|
|
7101
|
+
* const applied = await plugin.window.setVideoArea({ width: 1280, height: 720 });
|
|
7102
|
+
* if (applied.clamped) await plugin.osd.show("{}×{}", [applied.width, applied.height]);
|
|
7103
|
+
* ```
|
|
6791
7104
|
*/
|
|
6792
7105
|
setVideoArea(size: {
|
|
6793
7106
|
width: number;
|
|
@@ -6800,10 +7113,42 @@ interface MainWindow {
|
|
|
6800
7113
|
/** The request was scaled down or raised to fit. */
|
|
6801
7114
|
clamped: boolean;
|
|
6802
7115
|
}>;
|
|
6803
|
-
/**
|
|
7116
|
+
/**
|
|
7117
|
+
* Float the player's windows above other applications. Newest call wins; reverts when this plugin stops. `unavailable` without a user interface.
|
|
7118
|
+
* @capability window.control
|
|
7119
|
+
* @throws unavailable (rejects) No user interface.
|
|
7120
|
+
* @throws invalid (rejects) `onTop` is not a boolean.
|
|
7121
|
+
* @throws capability (rejects) The manifest does not declare `window.control`.
|
|
7122
|
+
* @example Keep the player above other windows
|
|
7123
|
+
* The player's windows (tool windows too) float above other applications
|
|
7124
|
+
* while the plugin's "Always on top" item is checked. The newest call of
|
|
7125
|
+
* any plugin wins, and the setting reverts when this plugin stops.
|
|
7126
|
+
* ```ts
|
|
7127
|
+
* let onTop = false;
|
|
7128
|
+
* plugin.commands.add({
|
|
7129
|
+
* id: "on-top",
|
|
7130
|
+
* path: ["@screen", "Always on top"],
|
|
7131
|
+
* checked: () => onTop,
|
|
7132
|
+
* run: async () => {
|
|
7133
|
+
* onTop = !onTop;
|
|
7134
|
+
* await plugin.window.setAlwaysOnTop(onTop); // reverts when this plugin stops
|
|
7135
|
+
* },
|
|
7136
|
+
* });
|
|
7137
|
+
* ```
|
|
7138
|
+
*/
|
|
6804
7139
|
setAlwaysOnTop(onTop: boolean): Promise<void>;
|
|
6805
7140
|
/**
|
|
6806
7141
|
* `"playing"`: hide the pointer over the video after 3 idle seconds while playing, in a window too (not only fullscreen). Newest call wins; reverts to `"fullscreen"` when this plugin stops.
|
|
7142
|
+
* @capability window.control
|
|
7143
|
+
* @throws invalid (rejects) A mode other than `"fullscreen"` or `"playing"`.
|
|
7144
|
+
* @throws capability (rejects) The manifest does not declare `window.control`.
|
|
7145
|
+
* @example Hide the pointer over playing video in a window too
|
|
7146
|
+
* The pointer disappears after 3 idle seconds over playing video, not only
|
|
7147
|
+
* in fullscreen. The newest call wins; when this plugin stops the player
|
|
7148
|
+
* goes back to hiding it in fullscreen only.
|
|
7149
|
+
* ```ts
|
|
7150
|
+
* await plugin.window.setCursorAutoHide("playing");
|
|
7151
|
+
* ```
|
|
6807
7152
|
*/
|
|
6808
7153
|
setCursorAutoHide(mode: "fullscreen" | "playing"): Promise<void>;
|
|
6809
7154
|
/**
|
|
@@ -6818,6 +7163,13 @@ interface MainWindow {
|
|
|
6818
7163
|
*
|
|
6819
7164
|
* Where it is unsupported (Windows, Linux), `setVideoArea` plus
|
|
6820
7165
|
* `setAlwaysOnTop` make a reasonable "mini player" fallback.
|
|
7166
|
+
* @capability window.control
|
|
7167
|
+
* @throws unsupported (rejects) The system has no picture-in-picture (`supported` is false).
|
|
7168
|
+
* @throws unavailable (rejects) No video, or no user interface.
|
|
7169
|
+
* @throws invalid (rejects) `on` is not a boolean.
|
|
7170
|
+
* @throws busy (rejects) The previous call is still switching.
|
|
7171
|
+
* @throws failed (rejects) The system refused.
|
|
7172
|
+
* @throws capability (rejects) The manifest does not declare `window.control`.
|
|
6821
7173
|
* @example Toggle picture-in-picture where the system has it
|
|
6822
7174
|
* The video moves into a floating picture-in-picture window, or back.
|
|
6823
7175
|
* `state.pictureInPicture` says whether the system supports it and
|
|
@@ -6881,9 +7233,29 @@ interface AppInfo {
|
|
|
6881
7233
|
`plugin.app`: the fixed facts plus the system, read on request. No
|
|
6882
7234
|
capability.
|
|
6883
7235
|
|
|
7236
|
+
`plugin.app` holds facts fixed for the player's run (its version, build,
|
|
7237
|
+
library versions) and reads the system's on request: `system()` reads
|
|
7238
|
+
the displays at each call; the operating system's name and version are
|
|
7239
|
+
worked out once, at the first call, and reused.
|
|
7240
|
+
|
|
7241
|
+
- @capability None.
|
|
7242
|
+
|
|
6884
7243
|
```ts
|
|
6885
7244
|
interface App extends AppInfo {
|
|
6886
|
-
/**
|
|
7245
|
+
/**
|
|
7246
|
+
* The operating system, the displays and the renderer, read when called (displays come and go; the renderer can fall back from GPU to software once). No capability. CPU and memory: Node's `os` module.
|
|
7247
|
+
* @capability None.
|
|
7248
|
+
* @throws busy (rejects) The player could not start the request (no thread available).
|
|
7249
|
+
* @example Choose a size for the primary display
|
|
7250
|
+
* The plugin sizes its picture for the primary display. `system()` asks
|
|
7251
|
+
* the player; `displays` lists the displays present now, the primary one
|
|
7252
|
+
* first (empty without a user interface).
|
|
7253
|
+
* ```ts
|
|
7254
|
+
* const { displays } = await plugin.app.system();
|
|
7255
|
+
* const primary = displays[0];
|
|
7256
|
+
* if (primary) console.log(primary.width, primary.height);
|
|
7257
|
+
* ```
|
|
7258
|
+
*/
|
|
6887
7259
|
system(): Promise<SystemInfo>;
|
|
6888
7260
|
}
|
|
6889
7261
|
```
|
|
@@ -6920,6 +7292,42 @@ interface SystemInfo {
|
|
|
6920
7292
|
readonly arch: string;
|
|
6921
7293
|
/** Primary display first; empty without a user interface. */
|
|
6922
7294
|
readonly displays: readonly DisplayInfo[];
|
|
7295
|
+
/**
|
|
7296
|
+
* Which renderer draws the video now: `"gpu"` (the GPU renderer, so
|
|
7297
|
+
* render options apply) or `"software"` (they
|
|
7298
|
+
* fail with `unsupported`). Read at each call. It changes at most once
|
|
7299
|
+
* while the player runs, from `"gpu"` to `"software"` when a window
|
|
7300
|
+
* cannot show GPU frames; the owner of the render options hears that
|
|
7301
|
+
* switch through RenderOptions.onStatus.
|
|
7302
|
+
*
|
|
7303
|
+
* Absent (`undefined`) on players before 0.5.12, which do not report it:
|
|
7304
|
+
* the renderer is then unknown, not software.
|
|
7305
|
+
*
|
|
7306
|
+
* Today the player renders on the GPU on macOS and in software on
|
|
7307
|
+
* Windows, Linux and without a user interface (see
|
|
7308
|
+
* Platforms); test this field
|
|
7309
|
+
* rather than SystemInfo.os.
|
|
7310
|
+
* @player 0.5.12; older players leave it out (declare `"minPlayerVersion": "0.5.12"` when the plugin relies on it).
|
|
7311
|
+
* @example Offer the HDR menu only where render options work
|
|
7312
|
+
* The plugin adds its HDR command only when the player renders on the
|
|
7313
|
+
* GPU, instead of creating the render options just to find out from a
|
|
7314
|
+
* failed status. `system()` asks the player, which reports the renderer
|
|
7315
|
+
* drawing at that moment; no capability is needed to ask. A player before
|
|
7316
|
+
* 0.5.12 leaves the field out, so the test is for `"gpu"` exactly (an
|
|
7317
|
+
* older player gets no HDR command).
|
|
7318
|
+
* ```ts
|
|
7319
|
+
* const { rendering } = await plugin.app.system();
|
|
7320
|
+
* if (rendering === "gpu") {
|
|
7321
|
+
* const render = plugin.video.render(); // one handle, kept
|
|
7322
|
+
* plugin.commands.add({
|
|
7323
|
+
* id: "hdr",
|
|
7324
|
+
* path: ["@video", "HDR Tone Mapping"],
|
|
7325
|
+
* run: () => render.set({ toneMapping: "bt.2390", targetPeak: 250 }),
|
|
7326
|
+
* });
|
|
7327
|
+
* }
|
|
7328
|
+
* ```
|
|
7329
|
+
*/
|
|
7330
|
+
readonly rendering?: "gpu" | "software";
|
|
6923
7331
|
}
|
|
6924
7332
|
```
|
|
6925
7333
|
|
|
@@ -8611,6 +9019,14 @@ one request is in flight at a time and only the newest value is sent.
|
|
|
8611
9019
|
Values out of range fail with `invalid` (see VideoNode). The
|
|
8612
9020
|
same chain applies to processed snapshots and exports.
|
|
8613
9021
|
|
|
9022
|
+
Members: `set(nodes)` records the desired chain (last write wins) and
|
|
9023
|
+
sends it, one request at a time and only the newest; `status` and
|
|
9024
|
+
`onStatus` report `waiting`, `applying`, `applied` or `failed`; `retry`
|
|
9025
|
+
applies the current value again; `dispose` withdraws the chain. `set`
|
|
9026
|
+
never throws: a refused chain shows as `failed` in `status`.
|
|
9027
|
+
|
|
9028
|
+
- @capability processing.video, playback.read and events
|
|
9029
|
+
|
|
8614
9030
|
```ts
|
|
8615
9031
|
type VideoFilter = Filter<VideoNode[]>;
|
|
8616
9032
|
```
|
|
@@ -8638,6 +9054,12 @@ VideoFilter.
|
|
|
8638
9054
|
`delayMs` is −10000–10000 in multiples of 10; the positive and the
|
|
8639
9055
|
negative delays of all plugins each add up to at most 10000.
|
|
8640
9056
|
|
|
9057
|
+
The same members and rules as VideoFilter: `set` records the
|
|
9058
|
+
desired value and never throws, `status` and `onStatus` report the
|
|
9059
|
+
result, `retry` applies it again, `dispose` withdraws it.
|
|
9060
|
+
|
|
9061
|
+
- @capability processing.audio, playback.read and events
|
|
9062
|
+
|
|
8641
9063
|
```ts
|
|
8642
9064
|
type AudioFilter = Filter<{
|
|
8643
9065
|
/** The audio chain; `[]` for none. */
|
|
@@ -8688,13 +9110,46 @@ needed); disposing or stopping the owner restores the defaults.
|
|
|
8688
9110
|
Out-of-range values or unknown fields fail with `invalid` without
|
|
8689
9111
|
sending anything.
|
|
8690
9112
|
|
|
9113
|
+
Not part of snapshots or exports: it changes how the picture sits in the
|
|
9114
|
+
video area, not the picture. `set` merges into the current settings and
|
|
9115
|
+
never throws: an out-of-range value or unknown field shows as `failed`
|
|
9116
|
+
(`invalid`) in `status` and is reported to `onError`, without sending
|
|
9117
|
+
anything.
|
|
9118
|
+
|
|
9119
|
+
- @capability processing.video, playback.read and events (like VideoFilter)
|
|
9120
|
+
|
|
8691
9121
|
```ts
|
|
8692
9122
|
interface VideoView extends Disposable {
|
|
8693
|
-
/**
|
|
9123
|
+
/**
|
|
9124
|
+
* Merged into the current settings; last write wins.
|
|
9125
|
+
* @capability processing.video
|
|
9126
|
+
* @example Stretch a 4:3 video to fill the window
|
|
9127
|
+
* A 4:3 video fills a 16:9 window, distorted, as the user asked. Each
|
|
9128
|
+
* `set` merges the given fields into this plugin's view settings; the
|
|
9129
|
+
* newest write wins, and the settings stay for later files until the
|
|
9130
|
+
* plugin disposes the view or stops.
|
|
9131
|
+
* ```ts
|
|
9132
|
+
* plugin.video.view().set({ fit: "stretch" });
|
|
9133
|
+
* ```
|
|
9134
|
+
*/
|
|
8694
9135
|
set(value: Partial<ViewSettings>): void;
|
|
8695
9136
|
/** Current status (the same four states as filters). */
|
|
8696
9137
|
readonly status: FilterStatus;
|
|
8697
|
-
/**
|
|
9138
|
+
/**
|
|
9139
|
+
* The status changed.
|
|
9140
|
+
* @capability processing.video
|
|
9141
|
+
* @example Tell the user another plugin owns the view
|
|
9142
|
+
* When another plugin already controls the display layer, the OSD says
|
|
9143
|
+
* so. The first plugin to call `view()` owns it; this plugin's handle
|
|
9144
|
+
* then reports `failed` with `conflict`, and tries again on its next `set`.
|
|
9145
|
+
* ```ts
|
|
9146
|
+
* const view = plugin.video.view();
|
|
9147
|
+
* view.onStatus((status) => {
|
|
9148
|
+
* if (status.state === "failed" && status.error.code === "conflict")
|
|
9149
|
+
* void plugin.osd.show("Another plugin controls the picture size");
|
|
9150
|
+
* });
|
|
9151
|
+
* ```
|
|
9152
|
+
*/
|
|
8698
9153
|
onStatus(listener: (status: FilterStatus) => void): Disposable;
|
|
8699
9154
|
}
|
|
8700
9155
|
```
|
|
@@ -8704,7 +9159,7 @@ Example: Fill the window, then zoom in
|
|
|
8704
9159
|
The video fills the video area (cropped instead of letterboxed), then
|
|
8705
9160
|
zooms to 150 % shifted a little to the right. The first plugin to call
|
|
8706
9161
|
`view()` owns the display layer; each `set` merges into its settings,
|
|
8707
|
-
which stay across sources until the plugin
|
|
9162
|
+
which stay across sources until the plugin disposes the view or stops.
|
|
8708
9163
|
```ts
|
|
8709
9164
|
const view = plugin.video.view();
|
|
8710
9165
|
view.set({ fit: "fill" });
|
|
@@ -8739,16 +9194,50 @@ independent handles.
|
|
|
8739
9194
|
|
|
8740
9195
|
Platforms: only macOS renders on the GPU. Under software rendering
|
|
8741
9196
|
(Windows, Linux, headless, fallback) every `set` fails with
|
|
8742
|
-
`unsupported
|
|
8743
|
-
|
|
9197
|
+
`unsupported`; SystemInfo.rendering tells which renderer draws
|
|
9198
|
+
without creating the handle. macOS OpenGL 4.1 has no
|
|
9199
|
+
compute shaders, so `peakDetect` is accepted but has no effect; output
|
|
9200
|
+
is always SDR.
|
|
9201
|
+
|
|
9202
|
+
GPU renderer options (tone mapping, peak detection, shaders,
|
|
9203
|
+
interpolation). Only the GPU renderer has them, which today means macOS;
|
|
9204
|
+
under software rendering the handle is `failed` with `unsupported` after
|
|
9205
|
+
the player's first answer to `render()` (reported to `onError` too), and so is
|
|
9206
|
+
every `set`. Single owner, like VideoView. `set` never throws; the
|
|
9207
|
+
result shows in `status`.
|
|
9208
|
+
|
|
9209
|
+
- @capability processing.video, playback.read and events (like VideoFilter)
|
|
8744
9210
|
|
|
8745
9211
|
```ts
|
|
8746
9212
|
interface RenderOptions extends Disposable {
|
|
8747
|
-
/**
|
|
9213
|
+
/**
|
|
9214
|
+
* Merged into the current settings; last write wins.
|
|
9215
|
+
* @capability processing.video
|
|
9216
|
+
* @example Turn on motion interpolation
|
|
9217
|
+
* Motion becomes smoother on the GPU renderer. `set` merges the field into
|
|
9218
|
+
* the plugin's render options; under software rendering the status turns
|
|
9219
|
+
* `failed` with `unsupported` instead.
|
|
9220
|
+
* ```ts
|
|
9221
|
+
* plugin.video.render().set({ interpolation: true });
|
|
9222
|
+
* ```
|
|
9223
|
+
*/
|
|
8748
9224
|
set(value: Partial<RenderSettings>): void;
|
|
8749
9225
|
/** `failed` with `unsupported` while the player renders in software; `failed` with `failed` when mpv cannot compile a shader (pushed later). */
|
|
8750
9226
|
readonly status: FilterStatus;
|
|
8751
|
-
/**
|
|
9227
|
+
/**
|
|
9228
|
+
* The status changed (including later pushes such as shader compile errors).
|
|
9229
|
+
* @capability processing.video
|
|
9230
|
+
* @example Report a shader that does not compile
|
|
9231
|
+
* The OSD shows mpv's message when a shader the plugin set fails to
|
|
9232
|
+
* compile. The player pushes that failure after the `set` was accepted, so
|
|
9233
|
+
* it arrives through `onStatus`, not as a rejected call.
|
|
9234
|
+
* ```ts
|
|
9235
|
+
* const render = plugin.video.render();
|
|
9236
|
+
* render.onStatus((status) => {
|
|
9237
|
+
* if (status.state === "failed") void plugin.osd.show("Render: {}", [status.error.message]);
|
|
9238
|
+
* });
|
|
9239
|
+
* ```
|
|
9240
|
+
*/
|
|
8752
9241
|
onStatus(listener: (status: FilterStatus) => void): Disposable;
|
|
8753
9242
|
}
|
|
8754
9243
|
```
|
|
@@ -8850,16 +9339,79 @@ interface SubtitleConditional {
|
|
|
8850
9339
|
`subtitles.read`; changes need `subtitles.control` and also
|
|
8851
9340
|
`subtitles.read`, since they read the current selection first.
|
|
8852
9341
|
|
|
9342
|
+
The subtitle tracks of the current source, the primary and secondary
|
|
9343
|
+
selection, uploaded files and the style. `state` is the SDK's snapshot of
|
|
9344
|
+
the player's subtitle state (`null` until first read). The player has no
|
|
9345
|
+
subtitle event: the SDK reads the state again when the source, its phase
|
|
9346
|
+
or the backend changes (with `events`) and after this plugin's own
|
|
9347
|
+
changes, so a selection the user or another plugin changed shows only
|
|
9348
|
+
after `refresh()`. A change without `ifCurrent` reads the state first
|
|
9349
|
+
and applies to it; pass `ifCurrent` to make it fail with `stale` when the
|
|
9350
|
+
selection changed since that state. Each change is one operation the
|
|
9351
|
+
player confirms; `select`, `remove` and `setStyle` resolve with the new
|
|
9352
|
+
state, `load` with the new track.
|
|
9353
|
+
|
|
9354
|
+
- @capability `subtitles.read` to read; `subtitles.control` and `subtitles.read` to change; `events` to re-read on source changes.
|
|
9355
|
+
|
|
8853
9356
|
```ts
|
|
8854
9357
|
interface Subtitles {
|
|
8855
9358
|
/** `null` until first read. */
|
|
8856
9359
|
readonly state: SubtitlesState | null;
|
|
8857
|
-
/**
|
|
9360
|
+
/**
|
|
9361
|
+
* The subtitle state changed.
|
|
9362
|
+
* @capability events and subtitles.read
|
|
9363
|
+
* @example Show the subtitle language after a new file opens
|
|
9364
|
+
* When a new file starts, the OSD names the language of its primary
|
|
9365
|
+
* subtitle. The SDK reads the subtitle state again when the source or its
|
|
9366
|
+
* phase changes (there is no subtitle event, so a selection changed in the
|
|
9367
|
+
* player shows only after `refresh()`) and then calls the listener;
|
|
9368
|
+
* `selected` is `null` when subtitles are off.
|
|
9369
|
+
* ```ts
|
|
9370
|
+
* plugin.subtitles.onChange((state) => {
|
|
9371
|
+
* // A track's language is null when the file does not say.
|
|
9372
|
+
* void plugin.osd.show("Subtitles: {}", [state.selected?.language ?? "off"]);
|
|
9373
|
+
* });
|
|
9374
|
+
* ```
|
|
9375
|
+
*/
|
|
8858
9376
|
onChange(listener: (state: SubtitlesState) => void): Disposable;
|
|
8859
|
-
/**
|
|
9377
|
+
/**
|
|
9378
|
+
* Read the state from the player now.
|
|
9379
|
+
* @capability subtitles.read
|
|
9380
|
+
* @throws capability (rejects) The manifest does not declare `subtitles.read`.
|
|
9381
|
+
* @example Count the subtitle tracks of the file
|
|
9382
|
+
* The plugin reads how many subtitle tracks the file has before building a
|
|
9383
|
+
* menu. `refresh` asks the player and resolves with the current state, also
|
|
9384
|
+
* without `events`; `state` holds it afterwards.
|
|
9385
|
+
* ```ts
|
|
9386
|
+
* const { tracks } = await plugin.subtitles.refresh();
|
|
9387
|
+
* await plugin.osd.show("{} subtitle tracks", [tracks.length]);
|
|
9388
|
+
* ```
|
|
9389
|
+
*/
|
|
8860
9390
|
refresh(): Promise<SubtitlesState>;
|
|
8861
9391
|
/**
|
|
8862
9392
|
* `role: "secondary"` sets the secondary subtitle: a text track that is not the primary one (`invalid` otherwise). A new source clears it. `track: null` turns the role off.
|
|
9393
|
+
* @capability subtitles.control and subtitles.read
|
|
9394
|
+
* @throws stale (rejects) With `ifCurrent`: the source or the subtitle selection changed since that state.
|
|
9395
|
+
* @throws invalid (rejects) An unknown track; as secondary, a bitmap track or the primary one; as primary, the current secondary.
|
|
9396
|
+
* @throws missing (rejects) The track is not in the current source.
|
|
9397
|
+
* @throws unavailable (rejects) Nothing loaded, or the source is still loading or not yet confirmed.
|
|
9398
|
+
* @throws busy (rejects) Too many subtitle operations at once.
|
|
9399
|
+
* @throws capability (rejects) The manifest does not declare `subtitles.control` and `subtitles.read`.
|
|
9400
|
+
* @example Turn on the first English subtitle
|
|
9401
|
+
* The first English track becomes the primary subtitle, but only in the
|
|
9402
|
+
* file the plugin looked at. Embedded tracks are chosen by number, so
|
|
9403
|
+
* without `ifCurrent` a select after another file opened would pick that
|
|
9404
|
+
* file's track with the same number; with `ifCurrent: state` it fails with
|
|
9405
|
+
* `stale` instead. Without `ifCurrent`, `select` reads the newest state
|
|
9406
|
+
* itself and almost never fails with `stale`.
|
|
9407
|
+
* ```ts
|
|
9408
|
+
* const state = await plugin.subtitles.refresh();
|
|
9409
|
+
* const english = state.tracks.find((track) => track.language === "en");
|
|
9410
|
+
* if (english)
|
|
9411
|
+
* await plugin.subtitles.select(english, { ifCurrent: state }).catch((error: PluginError) => {
|
|
9412
|
+
* if (error.code !== "stale") throw error; // another file or selection now
|
|
9413
|
+
* });
|
|
9414
|
+
* ```
|
|
8863
9415
|
*/
|
|
8864
9416
|
select(track: SubtitleTrack | null, options?: SubtitleConditional & {
|
|
8865
9417
|
role?: "primary" | "secondary";
|
|
@@ -8875,7 +9427,29 @@ interface Subtitles {
|
|
|
8875
9427
|
*
|
|
8876
9428
|
* Limits: SRT ≤ 2 MiB and ≤ 10,000 cues; ASS ≤ 2 MiB with `[Script Info]`
|
|
8877
9429
|
* and `[Events]` and 1–20,000 `Dialogue:` lines. Convert other formats
|
|
8878
|
-
* (VTT, SAMI) to SRT first.
|
|
9430
|
+
* (VTT, SAMI) to SRT first. Per source, a plugin has at most 4 loaded
|
|
9431
|
+
* tracks at once and loads at most 16 files (removed ones count), and
|
|
9432
|
+
* every plugin together at most 16 tracks; see `SUBTITLE_LIMITS` in
|
|
9433
|
+
* `@vestra-player/sdk/testing`.
|
|
9434
|
+
* @capability subtitles.control and subtitles.read
|
|
9435
|
+
* @throws invalid (rejects) A bad `format` or `title`, or a file over the limits.
|
|
9436
|
+
* @throws stale (rejects) With `ifCurrent`: the source changed since that state.
|
|
9437
|
+
* @throws unavailable (rejects) Nothing loaded, or the source is still loading.
|
|
9438
|
+
* @throws aborted (rejects) `signal` aborted before the commit (an `AbortError`); the upload is discarded.
|
|
9439
|
+
* @throws busy (rejects) Too many subtitle operations at once, or a limit of `SUBTITLE_LIMITS` is reached (4 tracks of this plugin, 16 loads of this plugin or 16 tracks in the source, or another of its limits); nothing is loaded.
|
|
9440
|
+
* @throws capability (rejects) The manifest does not declare `subtitles.control` and `subtitles.read`.
|
|
9441
|
+
* @example Load a downloaded SRT and select it
|
|
9442
|
+
* A subtitle file the plugin downloaded becomes a track of the current file
|
|
9443
|
+
* and is selected. The plugin passes the file's bytes; the player parses
|
|
9444
|
+
* them, adds an `external` track for this source only and resolves with
|
|
9445
|
+
* it once the upload is committed.
|
|
9446
|
+
* ```ts
|
|
9447
|
+
* const response = await fetch(url);
|
|
9448
|
+
* if (!response.ok) throw new Error(`subtitles: HTTP ${response.status}`);
|
|
9449
|
+
* const bytes = new Uint8Array(await response.arrayBuffer());
|
|
9450
|
+
* const track = await plugin.subtitles.load(bytes, { title: "Downloaded" }); // selected
|
|
9451
|
+
* await plugin.osd.show("Loaded {}", [track.title]);
|
|
9452
|
+
* ```
|
|
8879
9453
|
*/
|
|
8880
9454
|
load(bytes: Uint8Array, options?: SubtitleConditional & {
|
|
8881
9455
|
format?: "srt" | "ass";
|
|
@@ -8883,10 +9457,40 @@ interface Subtitles {
|
|
|
8883
9457
|
select?: "primary" | "secondary" | "none";
|
|
8884
9458
|
signal?: AbortSignal;
|
|
8885
9459
|
}): Promise<SubtitleTrack>;
|
|
8886
|
-
/**
|
|
9460
|
+
/**
|
|
9461
|
+
* Remove a track this plugin loaded.
|
|
9462
|
+
* @capability subtitles.control and subtitles.read
|
|
9463
|
+
* @throws invalid (rejects) The track was not loaded by this plugin.
|
|
9464
|
+
* @throws stale (rejects) With `ifCurrent`: the source changed since that state.
|
|
9465
|
+
* @throws unavailable (rejects) Nothing is loaded, or the source is still loading.
|
|
9466
|
+
* @throws busy (rejects) Too many subtitle operations at once.
|
|
9467
|
+
* @throws capability (rejects) The manifest does not declare `subtitles.control` and `subtitles.read`.
|
|
9468
|
+
* @example Remove the subtitle the plugin loaded
|
|
9469
|
+
* The plugin's downloaded subtitle disappears from the track list. A plugin
|
|
9470
|
+
* can remove only tracks it loaded itself; a new source removes them
|
|
9471
|
+
* anyway.
|
|
9472
|
+
* ```ts
|
|
9473
|
+
* await plugin.subtitles.remove(loaded);
|
|
9474
|
+
* ```
|
|
9475
|
+
*/
|
|
8887
9476
|
remove(track: SubtitleTrack, options?: SubtitleConditional): Promise<SubtitlesState>;
|
|
8888
9477
|
/**
|
|
8889
9478
|
* Set this plugin's style layer; `null` removes it. The newest call of this plugin replaces its earlier layer. When several plugins have a layer, the player uses the one whose manifest id sorts last, whatever the order of the calls.
|
|
9479
|
+
* @capability subtitles.control and subtitles.read
|
|
9480
|
+
* @throws invalid (rejects) A field out of range.
|
|
9481
|
+
* @throws unsupported (rejects) A font that is not in `state.fonts`, or a primary subtitle that is a bitmap track.
|
|
9482
|
+
* @throws stale (rejects) With `ifCurrent`: the subtitle state changed since that state.
|
|
9483
|
+
* @throws unavailable (rejects) Nothing is loaded, or the source is still loading.
|
|
9484
|
+
* @throws busy (rejects) Too many subtitle operations at once.
|
|
9485
|
+
* @throws capability (rejects) The manifest does not declare `subtitles.control` and `subtitles.read`.
|
|
9486
|
+
* @example Take the plugin's style off again
|
|
9487
|
+
* The plugin removes its style layer, so the subtitles go back to the
|
|
9488
|
+
* player's default (or to another plugin's layer). `null` removes only this
|
|
9489
|
+
* plugin's layer; the effective layer is the one whose manifest id sorts
|
|
9490
|
+
* last among the plugins that still have one.
|
|
9491
|
+
* ```ts
|
|
9492
|
+
* await plugin.subtitles.setStyle(null);
|
|
9493
|
+
* ```
|
|
8890
9494
|
*/
|
|
8891
9495
|
setStyle(style: SubtitleStyle | null, options?: SubtitleConditional): Promise<SubtitlesState>;
|
|
8892
9496
|
}
|
|
@@ -8937,6 +9541,14 @@ interface CaptureOptions extends Conditional {
|
|
|
8937
9541
|
|
|
8938
9542
|
A frozen, immutable input for exports. Dispose to release it.
|
|
8939
9543
|
|
|
9544
|
+
A frozen input, from MediaCapture.capture: the source, the time
|
|
9545
|
+
and (processed) the effects and subtitle as they were. Exports read it in
|
|
9546
|
+
the player's offline worker, so one snapshot can be exported several
|
|
9547
|
+
times and playback is unaffected. Disposing it releases the player's
|
|
9548
|
+
copy; an export using it after that fails with `released`.
|
|
9549
|
+
|
|
9550
|
+
- @capability media.read and media.control
|
|
9551
|
+
|
|
8940
9552
|
```ts
|
|
8941
9553
|
interface Snapshot extends Disposable {
|
|
8942
9554
|
/** Without or with effects. */
|
|
@@ -8958,11 +9570,45 @@ interface Snapshot extends Disposable {
|
|
|
8958
9570
|
} | null;
|
|
8959
9571
|
/** The frozen processing revision (processed snapshots). */
|
|
8960
9572
|
readonly processingRevision: number | null;
|
|
8961
|
-
/**
|
|
9573
|
+
/**
|
|
9574
|
+
* Start exporting; the job runs in the host's offline worker. One snapshot can be exported several times.
|
|
9575
|
+
* @capability media.read and media.control
|
|
9576
|
+
* @throws released (rejects) The snapshot was already released.
|
|
9577
|
+
* @throws invalid (rejects) Bad options (`quality` outside 1–100, a relative destination, a clip `end` not after the snapshot time or past the duration).
|
|
9578
|
+
* @throws busy (rejects) This plugin runs 2 exports (or the player 8).
|
|
9579
|
+
* @throws unavailable (rejects) No FFmpeg, or the export worker could not start.
|
|
9580
|
+
* @throws capability (rejects) The manifest does not declare `media.read` and `media.control`.
|
|
9581
|
+
* @example Export a 30-second clip
|
|
9582
|
+
* A clip from the snapshot time to 30 seconds later is written as MP4.
|
|
9583
|
+
* `export` resolves once the job is accepted; the job then runs in the
|
|
9584
|
+
* player's offline worker, and `done` resolves with its terminal state,
|
|
9585
|
+
* whose `outcome` has the destination or the error.
|
|
9586
|
+
* ```ts
|
|
9587
|
+
* const job = await snapshot.export({ format: "mp4", destination: "/tmp/clip.mp4", end: snapshot.time + 30 });
|
|
9588
|
+
* const end = await job.done;
|
|
9589
|
+
* if (end.status !== "completed") await plugin.osd.show("Export {}", [end.status]);
|
|
9590
|
+
* ```
|
|
9591
|
+
*/
|
|
8962
9592
|
export(options: ExportOptions): Promise<ExportJob>;
|
|
8963
9593
|
}
|
|
8964
9594
|
```
|
|
8965
9595
|
|
|
9596
|
+
Example: Export one frame as PNG and as JPEG
|
|
9597
|
+
|
|
9598
|
+
The same frozen frame is written twice, lossless and compressed. Each
|
|
9599
|
+
`export` starts a job of its own; the snapshot is released after both
|
|
9600
|
+
are done.
|
|
9601
|
+
```ts
|
|
9602
|
+
const snapshot = await plugin.media.capture();
|
|
9603
|
+
try {
|
|
9604
|
+
const png = await snapshot.export({ format: "png", destination: "/tmp/frame.png" });
|
|
9605
|
+
const jpeg = await snapshot.export({ format: "jpeg", destination: "/tmp/frame.jpg", quality: 85 });
|
|
9606
|
+
await Promise.all([png.done, jpeg.done]); // terminal states, not errors
|
|
9607
|
+
} finally {
|
|
9608
|
+
await snapshot.dispose();
|
|
9609
|
+
}
|
|
9610
|
+
```
|
|
9611
|
+
|
|
8966
9612
|
### ExportOptions
|
|
8967
9613
|
|
|
8968
9614
|
One frame as PNG, JPEG (`quality` 1–100, default 90) or BMP (24-bit), or
|
|
@@ -9015,31 +9661,121 @@ type JobState = Omit<MediaJob, "jobId">;
|
|
|
9015
9661
|
|
|
9016
9662
|
An export, from Snapshot.export. Disposing an unfinished job cancels it.
|
|
9017
9663
|
|
|
9664
|
+
One export, from Snapshot.export. `state` is the SDK's last copy
|
|
9665
|
+
of the job's state: the SDK polls the player about every 350 ms until the
|
|
9666
|
+
job ends, and `refresh` reads it at once. `done` resolves
|
|
9667
|
+
once the job is terminal (`completed`, `failed` or `cancelled`) and never
|
|
9668
|
+
rejects for a failed export: the failure is in `outcome`. Disposing an
|
|
9669
|
+
unfinished job cancels it.
|
|
9670
|
+
|
|
9671
|
+
- @capability media.read and media.control
|
|
9672
|
+
|
|
9018
9673
|
```ts
|
|
9019
9674
|
interface ExportJob extends Disposable {
|
|
9020
9675
|
/** Latest state. */
|
|
9021
9676
|
readonly state: JobState;
|
|
9022
|
-
/**
|
|
9677
|
+
/**
|
|
9678
|
+
* The state changed.
|
|
9679
|
+
* @capability media.read
|
|
9680
|
+
* @example Log each state of an export
|
|
9681
|
+
* The plugin's log records each status the job goes through (`queued`,
|
|
9682
|
+
* `running`, `cancelling` after a cancel, then a terminal one). The listener runs when the SDK learns a
|
|
9683
|
+
* new state from its polling.
|
|
9684
|
+
* ```ts
|
|
9685
|
+
* job.onChange((state) => console.log("export", state.status));
|
|
9686
|
+
* ```
|
|
9687
|
+
*/
|
|
9023
9688
|
onChange(listener: (state: JobState) => void): Disposable;
|
|
9024
|
-
/**
|
|
9689
|
+
/**
|
|
9690
|
+
* Resolves with the terminal state (completed, failed or cancelled).
|
|
9691
|
+
* @throws disposed (rejects) The job handle was disposed while waiting.
|
|
9692
|
+
*/
|
|
9025
9693
|
readonly done: Promise<JobState>;
|
|
9026
|
-
/**
|
|
9694
|
+
/**
|
|
9695
|
+
* Ask to cancel: `requested`, `tooLate` (already publishing) or `terminal` (already over).
|
|
9696
|
+
* @capability media.control
|
|
9697
|
+
* @throws rejected (rejects) The player did not accept the request.
|
|
9698
|
+
* @throws capability (rejects) The manifest does not declare `media.control`.
|
|
9699
|
+
* @example Cancel an export the user no longer wants
|
|
9700
|
+
* The running export stops, unless it is already writing its file.
|
|
9701
|
+
* `cancel` resolves `requested` when the worker will stop (the job ends
|
|
9702
|
+
* `cancelled`), `tooLate` when it is already publishing the file, and
|
|
9703
|
+
* `terminal` when the job had already ended.
|
|
9704
|
+
* ```ts
|
|
9705
|
+
* const answer = await job.cancel();
|
|
9706
|
+
* if (answer === "tooLate") await plugin.osd.show("Too late: the file is being written");
|
|
9707
|
+
* ```
|
|
9708
|
+
*/
|
|
9027
9709
|
cancel(): Promise<"requested" | "tooLate" | "terminal">;
|
|
9028
|
-
/**
|
|
9710
|
+
/**
|
|
9711
|
+
* Read the state from the player now.
|
|
9712
|
+
* @capability media.read
|
|
9713
|
+
* @throws capability (rejects) The manifest does not declare `media.read`.
|
|
9714
|
+
* @example Read a job's state now
|
|
9715
|
+
* The plugin reads the newest state of an export without waiting for an
|
|
9716
|
+
* event. `refresh` asks the player and resolves with the state, which
|
|
9717
|
+
* `state` then holds.
|
|
9718
|
+
* ```ts
|
|
9719
|
+
* const state = await job.refresh();
|
|
9720
|
+
* console.log(state.status);
|
|
9721
|
+
* ```
|
|
9722
|
+
*/
|
|
9029
9723
|
refresh(): Promise<JobState>;
|
|
9030
9724
|
}
|
|
9031
9725
|
```
|
|
9032
9726
|
|
|
9727
|
+
Example: Show export progress and cancel on request
|
|
9728
|
+
|
|
9729
|
+
The OSD shows the job's status while it runs, and a Cancel command stops
|
|
9730
|
+
it. `onChange` follows the state (`queued`, `running`, `cancelling`,
|
|
9731
|
+
then a terminal one); `cancel` asks the worker to stop. After
|
|
9732
|
+
`requested` the job usually ends `cancelled`, or `failed` when the
|
|
9733
|
+
cleanup failed.
|
|
9734
|
+
```ts
|
|
9735
|
+
job.onChange((state) => void plugin.osd.show("Export: {}", [state.status]));
|
|
9736
|
+
plugin.commands.add({ id: "cancel-export", title: "Cancel export", run: () => job.cancel() });
|
|
9737
|
+
await job.done; // completed, failed or cancelled
|
|
9738
|
+
```
|
|
9739
|
+
|
|
9033
9740
|
### MediaCapture
|
|
9034
9741
|
|
|
9035
9742
|
`plugin.media`: snapshots and exports. Needs `media.read` and
|
|
9036
9743
|
`media.control` (processed snapshots also `playback.read` and
|
|
9037
9744
|
`subtitles.read`). Exports decode offline and do not disturb playback.
|
|
9038
9745
|
|
|
9746
|
+
Exports run in the player's offline worker from a frozen
|
|
9747
|
+
Snapshot: a still frame or a clip, raw or with this plugin's
|
|
9748
|
+
effects (and the primary subtitle). Playback is not disturbed. A plugin
|
|
9749
|
+
holds at most 4 snapshots at a time (16 in the player); release them
|
|
9750
|
+
when done.
|
|
9751
|
+
|
|
9752
|
+
- @capability media.read and media.control (processed snapshots also `playback.read` and `subtitles.read`)
|
|
9753
|
+
|
|
9039
9754
|
```ts
|
|
9040
9755
|
interface MediaCapture {
|
|
9041
9756
|
/**
|
|
9042
|
-
* Freeze the current source (and optionally this plugin set's effects) as an input for exports. Fails `stale` when the source changed since `ifCurrent`; `unsupported` for
|
|
9757
|
+
* Freeze the current source (and optionally this plugin set's effects) as an input for exports. Fails `stale` when the source changed since `ifCurrent`; `unsupported` for subtitles that cannot be flattened (only plain SRT text can) or a picture over the size limits; `noSource` when nothing is playing.
|
|
9758
|
+
* @capability media.read and media.control
|
|
9759
|
+
* @throws stale (rejects) The source changed since `ifCurrent`.
|
|
9760
|
+
* @throws noSource (rejects) Nothing is playing.
|
|
9761
|
+
* @throws invalid (rejects) `at` at or after the end of the source.
|
|
9762
|
+
* @throws busy (rejects) This plugin holds 4 snapshots (or the player 16); release one.
|
|
9763
|
+
* @throws unavailable (rejects) A processed snapshot while the picture is still being prepared.
|
|
9764
|
+
* @throws unsupported (rejects) A processed snapshot the player cannot freeze: subtitles that cannot be flattened (only plain SRT text can), or a picture over the size limits.
|
|
9765
|
+
* @throws capability (rejects) The manifest does not declare `media.read` and `media.control`.
|
|
9766
|
+
* @example Freeze the frame at the 10-second mark
|
|
9767
|
+
* The plugin freezes the frame at 0:10 of the open file, raw, to export it
|
|
9768
|
+
* later. `capture` resolves with a `Snapshot` once the player has frozen
|
|
9769
|
+
* that input; the snapshot stays valid when playback moves on or the file
|
|
9770
|
+
* changes, until it is released.
|
|
9771
|
+
* ```ts
|
|
9772
|
+
* const snapshot = await plugin.media.capture({ at: 10 }); // seconds
|
|
9773
|
+
* try {
|
|
9774
|
+
* // … export it …
|
|
9775
|
+
* } finally {
|
|
9776
|
+
* await snapshot.dispose(); // frees one of the plugin's 4 snapshot slots
|
|
9777
|
+
* }
|
|
9778
|
+
* ```
|
|
9043
9779
|
*/
|
|
9044
9780
|
capture(options?: CaptureOptions): Promise<Snapshot>;
|
|
9045
9781
|
}
|
|
@@ -9192,25 +9928,84 @@ interface ResolvedStream {
|
|
|
9192
9928
|
|
|
9193
9929
|
A registered resolver. Disposing it unregisters it and aborts resolutions in flight.
|
|
9194
9930
|
|
|
9931
|
+
A registered resolver, from Resolvers.add. Disposing it, its
|
|
9932
|
+
scope or stopping the plugin unregisters it and aborts resolutions in
|
|
9933
|
+
flight; the extensions or hosts are free again for other plugins.
|
|
9934
|
+
|
|
9935
|
+
- @capability source.resolve
|
|
9936
|
+
|
|
9195
9937
|
```ts
|
|
9196
9938
|
interface Resolver extends Disposable {
|
|
9197
9939
|
/**
|
|
9198
9940
|
* Resolves when the player registered it; rejects otherwise: `conflict` when another resolver has the extension or the scheme + host (another plugin's: activation goes on; this plugin's own: activation fails), `invalid` for a malformed declaration (a player before 0.4.1 also for `extensions`).
|
|
9941
|
+
* @throws conflict (rejects) Another resolver has the extension or the scheme + host, or this plugin already registered this id.
|
|
9942
|
+
* @throws busy (rejects) The player already holds 16 resolvers.
|
|
9943
|
+
* @throws invalid (rejects) A malformed declaration.
|
|
9944
|
+
* @throws capability (rejects) The manifest does not declare `source.resolve`.
|
|
9199
9945
|
*/
|
|
9200
9946
|
readonly ready: Promise<void>;
|
|
9201
9947
|
}
|
|
9202
9948
|
```
|
|
9203
9949
|
|
|
9950
|
+
Example: Report when another plugin already handles the format
|
|
9951
|
+
|
|
9952
|
+
The plugin tells the user when its `.ch8` resolver could not register
|
|
9953
|
+
because another plugin holds that extension. `ready` rejects with
|
|
9954
|
+
`conflict`; for another plugin's claim that does not fail activation, so
|
|
9955
|
+
the plugin keeps running without the resolver.
|
|
9956
|
+
```ts
|
|
9957
|
+
const resolver = plugin.resolvers.add({
|
|
9958
|
+
id: "chip8",
|
|
9959
|
+
extensions: ["ch8"],
|
|
9960
|
+
async resolve() {
|
|
9961
|
+
throw plugin.i18n.error("Not ready yet.");
|
|
9962
|
+
},
|
|
9963
|
+
});
|
|
9964
|
+
// Rejects with `conflict` when the extension is taken.
|
|
9965
|
+
resolver.ready.catch((error: PluginError) => {
|
|
9966
|
+
if (error.code === "conflict") void plugin.osd.show("Another plugin plays .ch8 files");
|
|
9967
|
+
});
|
|
9968
|
+
```
|
|
9969
|
+
|
|
9204
9970
|
### Resolvers
|
|
9205
9971
|
|
|
9206
9972
|
`plugin.resolvers`: turn web page URLs into playable streams, or local
|
|
9207
9973
|
files into streams this plugin generates. Needs `source.resolve` (file
|
|
9208
9974
|
resolvers also `producer.av`).
|
|
9209
9975
|
|
|
9976
|
+
A resolver turns something the player cannot play by itself into
|
|
9977
|
+
something it can: a web page address into stream URLs
|
|
9978
|
+
(WebResolverOptions), or a local file of a format the plugin
|
|
9979
|
+
understands into a stream the plugin generates
|
|
9980
|
+
(FileResolverOptions). The player asks the resolver whenever
|
|
9981
|
+
anyone opens a matching address or file (the Open dialog, the playlist,
|
|
9982
|
+
another plugin), so the plugin does not intercept opens itself.
|
|
9983
|
+
|
|
9984
|
+
- @capability source.resolve (file resolvers also `producer.av`)
|
|
9985
|
+
|
|
9210
9986
|
```ts
|
|
9211
9987
|
interface Resolvers {
|
|
9212
9988
|
/**
|
|
9213
9989
|
* Register a resolver. When anyone opens a matching URL or file the player calls `resolve`; `signal` aborts when the open is cancelled or replaced. One scheme + host pair, and one extension, has one resolver (`conflict` otherwise). A rejected `resolve` fails the open with the error's message.
|
|
9990
|
+
* @capability source.resolve
|
|
9991
|
+
* @example Play a site's pages through the plugin
|
|
9992
|
+
* Opening a `https://video.example.com/…` address plays the page's video.
|
|
9993
|
+
* `add` returns at once and registers in the background (`ready` reports
|
|
9994
|
+
* the answer); from then on the player calls `resolve` for every matching
|
|
9995
|
+
* open, and the error a rejected `resolve` carries is what the user sees.
|
|
9996
|
+
* ```ts
|
|
9997
|
+
* plugin.resolvers.add({
|
|
9998
|
+
* id: "example",
|
|
9999
|
+
* schemes: ["https"],
|
|
10000
|
+
* hosts: ["video.example.com"], // exact host names
|
|
10001
|
+
* async resolve(url, signal) {
|
|
10002
|
+
* const response = await fetch(url, { signal });
|
|
10003
|
+
* if (!response.ok) throw new Error(`the page answered ${response.status}`);
|
|
10004
|
+
* const page = (await response.json()) as { title: string; stream: string };
|
|
10005
|
+
* return { kind: "media", originalUrl: url, title: page.title, primary: { url: page.stream } };
|
|
10006
|
+
* },
|
|
10007
|
+
* });
|
|
10008
|
+
* ```
|
|
9214
10009
|
*/
|
|
9215
10010
|
add(options: ResolverOptions): Resolver;
|
|
9216
10011
|
/**
|
|
@@ -9363,6 +10158,15 @@ interface InstallOptions {
|
|
|
9363
10158
|
|
|
9364
10159
|
A validated package directory, from Packages.validate.
|
|
9365
10160
|
|
|
10161
|
+
A package the player has checked, from Packages.validate or
|
|
10162
|
+
Packages.validateArchive: its manifest, digest and the checks
|
|
10163
|
+
it passed. The player copies the whole package when it checks it and
|
|
10164
|
+
installs that copy, so later changes to the directory do not matter.
|
|
10165
|
+
Installing starts a PackageChange; the player keeps only the 32
|
|
10166
|
+
most recent candidates.
|
|
10167
|
+
|
|
10168
|
+
- @capability plugins.manage
|
|
10169
|
+
|
|
9366
10170
|
```ts
|
|
9367
10171
|
interface PackageCandidate {
|
|
9368
10172
|
/** The package directory. */
|
|
@@ -9375,11 +10179,38 @@ interface PackageCandidate {
|
|
|
9375
10179
|
readonly checks: string[];
|
|
9376
10180
|
/**
|
|
9377
10181
|
* Install it. The same source upgrades the installed plugin (its data stays); another source fails with `conflict` unless `replace` is set; a built-in id fails with `unsupported` (built-ins update with the player). A reserved id (`vestra` or `vestra.*`, which every built-in uses) fails with `reserved` (player 0.4.1; already when validated), as only the player installs built-in plugins.
|
|
10182
|
+
* @capability plugins.manage
|
|
10183
|
+
* @throws conflict (rejects) The plugin is installed from another source and `replace` is not set.
|
|
10184
|
+
* @throws unsupported (rejects) A built-in plugin's id.
|
|
10185
|
+
* @throws reserved (rejects) A reserved id (`vestra`, `vestra.*`).
|
|
10186
|
+
* @throws busy (rejects) The plugin library is being cleaned up, or the player already keeps 64 change records.
|
|
10187
|
+
* @throws revisionConflict (rejects) The installed revision of this plugin changed meanwhile; call `install` again (no new validation needed).
|
|
10188
|
+
* @throws stale (rejects) The candidate is older than the 32 the player keeps.
|
|
10189
|
+
* @throws capability (rejects) The manifest does not declare `plugins.manage`.
|
|
10190
|
+
* @example Replace a plugin installed from another source
|
|
10191
|
+
* The new package replaces the installed plugin even though it came from
|
|
10192
|
+
* another source. Without `replace` the player refuses with `conflict`;
|
|
10193
|
+
* the same source simply upgrades and keeps the plugin's data.
|
|
10194
|
+
* ```ts
|
|
10195
|
+
* const change = await candidate.install({ replace: true });
|
|
10196
|
+
* await change.done; // "committed" once the new plugin runs
|
|
10197
|
+
* ```
|
|
9378
10198
|
*/
|
|
9379
10199
|
install(options?: InstallOptions): Promise<PackageChange>;
|
|
9380
10200
|
}
|
|
9381
10201
|
```
|
|
9382
10202
|
|
|
10203
|
+
Example: Show what a package asks for before installing it
|
|
10204
|
+
|
|
10205
|
+
Before installing, the plugin shows the package's name and the
|
|
10206
|
+
capabilities it declares. The candidate's `manifest` is the manifest
|
|
10207
|
+
as the player read it.
|
|
10208
|
+
```ts
|
|
10209
|
+
const candidate = await plugin.packages.validate(directory);
|
|
10210
|
+
const { id, version, capabilities } = candidate.manifest;
|
|
10211
|
+
await plugin.osd.show("{} {}: {}", [id, version, capabilities.join(", ")]);
|
|
10212
|
+
```
|
|
10213
|
+
|
|
9383
10214
|
### PackageChangeStatus
|
|
9384
10215
|
|
|
9385
10216
|
The progress of an install or uninstall.
|
|
@@ -9393,21 +10224,71 @@ type PackageChangeStatus = "preparing" | "activating" | "restoring" | "committed
|
|
|
9393
10224
|
An install or uninstall in progress. The SDK never submits a
|
|
9394
10225
|
change twice: after a lost reply it only queries the same request.
|
|
9395
10226
|
|
|
10227
|
+
An install or uninstall the player carries out in steps (`preparing`,
|
|
10228
|
+
`activating`, possibly `restoring`) until a terminal status. The SDK
|
|
10229
|
+
asks the player for the status about every 400 ms until then; `done`
|
|
10230
|
+
resolves with the terminal status and does not reject for a failed
|
|
10231
|
+
change: `message` says why.
|
|
10232
|
+
|
|
10233
|
+
- @capability plugins.manage
|
|
10234
|
+
|
|
9396
10235
|
```ts
|
|
9397
10236
|
interface PackageChange {
|
|
9398
10237
|
/** Current status. */
|
|
9399
10238
|
readonly status: PackageChangeStatus;
|
|
9400
10239
|
/** Details for failures, `null` otherwise. */
|
|
9401
10240
|
readonly message: string | null;
|
|
9402
|
-
/**
|
|
10241
|
+
/**
|
|
10242
|
+
* The status changed.
|
|
10243
|
+
* @capability plugins.manage
|
|
10244
|
+
* @example Close a progress window when the change ends
|
|
10245
|
+
* The plugin's progress window closes once the install is over, whatever
|
|
10246
|
+
* the outcome. The listener receives each new status; the terminal ones
|
|
10247
|
+
* are `committed`, `cancelled`, `failed`, `failedDegraded` and `tooLate`
|
|
10248
|
+
* (`tooLate` only answers a `cancel` of a change that had committed, and
|
|
10249
|
+
* `committed` may follow it).
|
|
10250
|
+
* ```ts
|
|
10251
|
+
* change.onChange((status) => {
|
|
10252
|
+
* if (["committed", "cancelled", "failed", "failedDegraded", "tooLate"].includes(status))
|
|
10253
|
+
* void progress.close();
|
|
10254
|
+
* });
|
|
10255
|
+
* ```
|
|
10256
|
+
*/
|
|
9403
10257
|
onChange(listener: (status: PackageChangeStatus) => void): Disposable;
|
|
9404
|
-
/**
|
|
10258
|
+
/**
|
|
10259
|
+
* Resolves with the final status.
|
|
10260
|
+
* @throws missing (rejects) The player no longer knows the change (a `HostError` with that code); a failed change is a terminal status, not a rejection.
|
|
10261
|
+
*/
|
|
9405
10262
|
readonly done: Promise<PackageChangeStatus>;
|
|
9406
|
-
/**
|
|
10263
|
+
/**
|
|
10264
|
+
* Ask to cancel (it may be `tooLate`).
|
|
10265
|
+
* @capability plugins.manage
|
|
10266
|
+
* @throws capability (rejects) The manifest does not declare `plugins.manage`.
|
|
10267
|
+
* @example Cancel an install the user no longer wants
|
|
10268
|
+
* The install stops if it has not gone too far. `cancel` asks the player;
|
|
10269
|
+
* the change then ends `cancelled`. When the change had already committed
|
|
10270
|
+
* the player answers `tooLate` and `done` still resolves `committed`; a
|
|
10271
|
+
* change that is restoring cannot be cancelled and ends `failed` or
|
|
10272
|
+
* `failedDegraded`.
|
|
10273
|
+
* ```ts
|
|
10274
|
+
* await change.cancel();
|
|
10275
|
+
* console.log(await change.done); // "cancelled"; "committed" when it was too late
|
|
10276
|
+
* ```
|
|
10277
|
+
*/
|
|
9407
10278
|
cancel(): Promise<void>;
|
|
9408
10279
|
}
|
|
9409
10280
|
```
|
|
9410
10281
|
|
|
10282
|
+
Example: Report each step of an install
|
|
10283
|
+
|
|
10284
|
+
The OSD shows each step of the install and the end result. `onChange`
|
|
10285
|
+
runs for every new status; `done` resolves when the change is over.
|
|
10286
|
+
```ts
|
|
10287
|
+
change.onChange((status) => void plugin.osd.show("Install: {}", [status]));
|
|
10288
|
+
const end = await change.done;
|
|
10289
|
+
if (end !== "committed") await plugin.osd.show("Not installed: {}", [change.message ?? end]);
|
|
10290
|
+
```
|
|
10291
|
+
|
|
9411
10292
|
### PluginInfo
|
|
9412
10293
|
|
|
9413
10294
|
One plugin as the player knows it (`packages.list`).
|
|
@@ -9489,6 +10370,14 @@ interface PluginWarning {
|
|
|
9489
10370
|
`plugin.packages`: the plugins the player knows. `list` and `onChange`
|
|
9490
10371
|
need no capability; everything else needs `plugins.manage`.
|
|
9491
10372
|
|
|
10373
|
+
The plugins the player knows: installed, built-in and this run's
|
|
10374
|
+
development loads. Reading the list needs no capability; installing,
|
|
10375
|
+
enabling, reloading and uninstalling need `plugins.manage` and act on
|
|
10376
|
+
other plugins, never on the calling one. Built-in plugins update with
|
|
10377
|
+
the player and cannot be installed, disabled or uninstalled.
|
|
10378
|
+
|
|
10379
|
+
- @capability None for `list` and `onChange`; plugins.manage for the rest.
|
|
10380
|
+
|
|
9492
10381
|
```ts
|
|
9493
10382
|
interface Packages {
|
|
9494
10383
|
/**
|
|
@@ -9499,18 +10388,93 @@ interface Packages {
|
|
|
9499
10388
|
* The player keeps the 32 most recent candidates of all plugins; an
|
|
9500
10389
|
* older one's `install` fails with `stale` (validate again). Before
|
|
9501
10390
|
* player 0.4.2 the 33rd validation of a run failed with `busy` instead.
|
|
10391
|
+
* @capability plugins.manage
|
|
10392
|
+
* @throws host (rejects) Not a package directory, or a manifest that breaks the rules (another `apiVersion` included); `message` says why.
|
|
10393
|
+
* @throws invalid (rejects) A bad request (a path that is not a string).
|
|
10394
|
+
* @throws javascript (rejects) The entry module does not parse.
|
|
10395
|
+
* @throws slint (rejects) A Slint file does not compile.
|
|
10396
|
+
* @throws reserved (rejects) A reserved id (`vestra`, `vestra.*`).
|
|
10397
|
+
* @throws versionMismatch (rejects) The package's `minPlayerVersion` is newer than the player.
|
|
10398
|
+
* @throws disabled (rejects) The calling plugin is stopping.
|
|
10399
|
+
* @throws capability (rejects) The manifest does not declare `plugins.manage`.
|
|
10400
|
+
* @example Check a package directory before installing it
|
|
10401
|
+
* The plugin checks a folder it unpacked and installs it if the player
|
|
10402
|
+
* accepts it. `validate` runs the player's checks and resolves with a
|
|
10403
|
+
* candidate; a package that fails them rejects with the reason.
|
|
10404
|
+
* ```ts
|
|
10405
|
+
* try {
|
|
10406
|
+
* // Rejects with the reason when the package fails the checks.
|
|
10407
|
+
* const candidate = await plugin.packages.validate(directory);
|
|
10408
|
+
* await (await candidate.install()).done;
|
|
10409
|
+
* } catch (error) {
|
|
10410
|
+
* await plugin.osd.show("Not a valid plugin: {}", [(error as Error).message]);
|
|
10411
|
+
* }
|
|
10412
|
+
* ```
|
|
9502
10413
|
*/
|
|
9503
10414
|
validate(directory: string): Promise<PackageCandidate>;
|
|
9504
10415
|
/**
|
|
9505
10416
|
* Validate a `.vxp` package file (`plugins.manage`, player 0.2.0): the same checks as Packages.validate after unpacking. Check the file's sha256 yourself before calling when you know it. A reserved id fails with `reserved`, as in Packages.validate.
|
|
10417
|
+
* @capability plugins.manage
|
|
10418
|
+
* @throws invalid (rejects) The path does not end in `.vxp`, or the archive cannot be unpacked (over 64 MB, too many files, a damaged zip, an unsafe path).
|
|
10419
|
+
* @throws host (rejects) The unpacked package or its manifest breaks the rules; `message` says why.
|
|
10420
|
+
* @throws javascript (rejects) The entry module does not parse.
|
|
10421
|
+
* @throws slint (rejects) A Slint file does not compile.
|
|
10422
|
+
* @throws reserved (rejects) A reserved id.
|
|
10423
|
+
* @throws versionMismatch (rejects) The package's `minPlayerVersion` is newer than the player.
|
|
10424
|
+
* @throws disabled (rejects) The calling plugin is stopping.
|
|
10425
|
+
* @throws capability (rejects) The manifest does not declare `plugins.manage`.
|
|
10426
|
+
* @example Install a downloaded package file
|
|
10427
|
+
* A `.vxp` the plugin downloaded is checked and installed. The player
|
|
10428
|
+
* unpacks and checks it like a directory; checking the file's sha256
|
|
10429
|
+
* first, when the store publishes one, is up to the plugin.
|
|
10430
|
+
* ```ts
|
|
10431
|
+
* const candidate = await plugin.packages.validateArchive(file); // a .vxp path
|
|
10432
|
+
* const change = await candidate.install();
|
|
10433
|
+
* await plugin.osd.show("Install: {}", [await change.done]);
|
|
10434
|
+
* ```
|
|
9506
10435
|
*/
|
|
9507
10436
|
validateArchive(file: string): Promise<PackageCandidate>;
|
|
9508
|
-
/**
|
|
10437
|
+
/**
|
|
10438
|
+
* Every plugin: running, installed, and this run's development loads. No capability.
|
|
10439
|
+
* @capability None.
|
|
10440
|
+
* @throws Error (rejects) Only when the player cannot be asked; no capability is needed.
|
|
10441
|
+
* @example Show which plugins are running
|
|
10442
|
+
* The plugin lists the running plugins by name. `list` asks the player
|
|
10443
|
+
* and resolves with every plugin it knows, with its state, origin and
|
|
10444
|
+
* display texts in the interface language.
|
|
10445
|
+
* ```ts
|
|
10446
|
+
* const plugins = await plugin.packages.list();
|
|
10447
|
+
* const running = plugins.filter((info) => info.state === "running");
|
|
10448
|
+
* console.log(running.map((info) => info.display?.name ?? info.id));
|
|
10449
|
+
* ```
|
|
10450
|
+
*/
|
|
9509
10451
|
list(): Promise<PluginInfo[]>;
|
|
9510
|
-
/**
|
|
10452
|
+
/**
|
|
10453
|
+
* The new list after any start, stop, failure, install or uninstall. No capability.
|
|
10454
|
+
* @capability None.
|
|
10455
|
+
* @example Refresh a plugin list after any change
|
|
10456
|
+
* A store window's list updates when a plugin starts, stops, fails, is
|
|
10457
|
+
* installed or uninstalled. The listener receives the new list each time.
|
|
10458
|
+
* ```ts
|
|
10459
|
+
* plugin.packages.onChange((plugins) => console.log(plugins.length, "plugins"));
|
|
10460
|
+
* ```
|
|
10461
|
+
*/
|
|
9511
10462
|
onChange(listener: (plugins: PluginInfo[]) => void): Disposable;
|
|
9512
10463
|
/**
|
|
9513
|
-
* Stop or start a plugin (`plugins.manage`). An installed plugin stays disabled across launches; a development load only for this run. `invalid` to disable oneself, `missing` unknown. `busy` while it is being installed. `unsupported` to disable a built-in (PluginInfo.builtin): users manage third-party plugins only (players released with SDK 1.5.18 on; older ones disabled built-ins too).
|
|
10464
|
+
* Stop or start a plugin (`plugins.manage`). An installed plugin stays disabled across launches; a development load only for this run. `invalid` to disable oneself, `missing` unknown. `busy` while it is being installed or reloaded. `unsupported` to disable a built-in (PluginInfo.builtin): users manage third-party plugins only (players released with SDK 1.5.18 on; older ones disabled built-ins too).
|
|
10465
|
+
* @capability plugins.manage
|
|
10466
|
+
* @throws invalid (rejects) Disabling the calling plugin itself.
|
|
10467
|
+
* @throws missing (rejects) An unknown id.
|
|
10468
|
+
* @throws busy (rejects) The plugin is being installed or reloaded.
|
|
10469
|
+
* @throws unsupported (rejects) A built-in plugin.
|
|
10470
|
+
* @throws capability (rejects) The manifest does not declare `plugins.manage`.
|
|
10471
|
+
* @example Turn a third-party plugin off
|
|
10472
|
+
* The plugin stops another plugin and keeps it disabled across launches
|
|
10473
|
+
* until it is enabled again. Built-in plugins cannot be disabled
|
|
10474
|
+
* (`unsupported`).
|
|
10475
|
+
* ```ts
|
|
10476
|
+
* await plugin.packages.setEnabled("someone.demo", false);
|
|
10477
|
+
* ```
|
|
9514
10478
|
*/
|
|
9515
10479
|
setEnabled(id: string, enabled: boolean): Promise<void>;
|
|
9516
10480
|
/**
|
|
@@ -9528,10 +10492,38 @@ interface Packages {
|
|
|
9528
10492
|
* `invalid` for oneself or a disabled plugin (enable it instead),
|
|
9529
10493
|
* `missing` unknown, `busy` while it is being installed or reloaded.
|
|
9530
10494
|
* @player 0.5.1; older players reject it as an unknown request (`unsupported`).
|
|
10495
|
+
* @capability plugins.manage
|
|
10496
|
+
* @throws invalid (rejects) The calling plugin itself, or a disabled plugin.
|
|
10497
|
+
* @throws missing (rejects) An unknown id.
|
|
10498
|
+
* @throws busy (rejects) The plugin is being installed or reloaded.
|
|
10499
|
+
* @throws unsupported (rejects) A player before 0.5.1.
|
|
10500
|
+
* @throws capability (rejects) The manifest does not declare `plugins.manage`.
|
|
10501
|
+
* @example Restart a plugin that misbehaves
|
|
10502
|
+
* The other plugin is stopped and started again from the same package and
|
|
10503
|
+
* data. `reload` resolves once the old process is stopped and the start is
|
|
10504
|
+
* scheduled; `onChange` then shows `starting` and `running` (or `failed`).
|
|
10505
|
+
* ```ts
|
|
10506
|
+
* await plugin.packages.reload("someone.demo");
|
|
10507
|
+
* ```
|
|
9531
10508
|
*/
|
|
9532
10509
|
reload(id: string): Promise<void>;
|
|
9533
10510
|
/**
|
|
9534
10511
|
* Remove an installed plugin (`plugins.manage`); `removeData` also deletes its data directory, otherwise reinstalling it from the same source reconnects to its data. Its user bindings stay (as orphans). `unsupported` for built-ins and development loads, `missing` unknown, `invalid` for oneself.
|
|
10512
|
+
* @capability plugins.manage
|
|
10513
|
+
* @throws unsupported (rejects) A built-in plugin or a development load.
|
|
10514
|
+
* @throws missing (rejects) An unknown id.
|
|
10515
|
+
* @throws invalid (rejects) The calling plugin itself.
|
|
10516
|
+
* @throws busy (rejects) The plugin library is being cleaned up, or that plugin is being installed or reloaded.
|
|
10517
|
+
* @throws revisionConflict (rejects) The installed revision of that plugin changed meanwhile; call `uninstall` again.
|
|
10518
|
+
* @throws capability (rejects) The manifest does not declare `plugins.manage`.
|
|
10519
|
+
* @example Uninstall a plugin together with its data
|
|
10520
|
+
* The other plugin is removed and its data directory deleted, so a later
|
|
10521
|
+
* reinstall starts fresh. Without `removeData` a reinstall from the same
|
|
10522
|
+
* source finds its data again.
|
|
10523
|
+
* ```ts
|
|
10524
|
+
* const change = await plugin.packages.uninstall("someone.demo", { removeData: true });
|
|
10525
|
+
* await change.done;
|
|
10526
|
+
* ```
|
|
9535
10527
|
*/
|
|
9536
10528
|
uninstall(id: string, options?: {
|
|
9537
10529
|
removeData?: boolean;
|
|
@@ -9606,9 +10598,31 @@ interface BindingInfo extends BindingRef {
|
|
|
9606
10598
|
`plugin.bindings`: list and override every command's bindings, including
|
|
9607
10599
|
the player's. Needs `bindings.manage`.
|
|
9608
10600
|
|
|
10601
|
+
The player's binding table: every command's default keys (from
|
|
10602
|
+
CommandOptions.bindings, the player's own as `host/<action>`)
|
|
10603
|
+
and the user's overrides on top, saved across launches. A key-settings
|
|
10604
|
+
plugin lists the table, writes overrides and resets them; the player
|
|
10605
|
+
applies the change at once to every running plugin.
|
|
10606
|
+
|
|
10607
|
+
- @capability bindings.manage
|
|
10608
|
+
|
|
9609
10609
|
```ts
|
|
9610
10610
|
interface Bindings {
|
|
9611
|
-
/**
|
|
10611
|
+
/**
|
|
10612
|
+
* Every declared command, and overrides whose command is gone.
|
|
10613
|
+
* @capability bindings.manage
|
|
10614
|
+
* @throws capability (rejects) The manifest does not declare `bindings.manage`.
|
|
10615
|
+
* @example Find the commands that use a key
|
|
10616
|
+
* A key-settings window shows which commands Ctrl+K is bound to now. `list`
|
|
10617
|
+
* resolves with every declared command and its current bindings (the
|
|
10618
|
+
* defaults with the user's overrides applied) and with the overrides
|
|
10619
|
+
* whose command is not running (`orphans`).
|
|
10620
|
+
* ```ts
|
|
10621
|
+
* const { commands } = await plugin.bindings.list();
|
|
10622
|
+
* const users = commands.filter((command) => command.current.includes("Ctrl+K"));
|
|
10623
|
+
* console.log(users.map((command) => `${command.plugin}/${command.command}`)); // "host" for the player
|
|
10624
|
+
* ```
|
|
10625
|
+
*/
|
|
9612
10626
|
list(): Promise<{
|
|
9613
10627
|
/** Every declared command (running plugins and the player). */
|
|
9614
10628
|
commands: BindingInfo[];
|
|
@@ -9622,11 +10636,47 @@ interface Bindings {
|
|
|
9622
10636
|
}>;
|
|
9623
10637
|
/**
|
|
9624
10638
|
* Override a command's bindings (`[]` unbinds it); saved across launches. `invalid` for a bad binding, `missing` for an unknown command that has no override either. At most 16 inputs, syntax as CommandOptions.bindings.
|
|
10639
|
+
* @capability bindings.manage
|
|
10640
|
+
* @throws invalid (rejects) A malformed binding, or more than 16.
|
|
10641
|
+
* @throws missing (rejects) An unknown command that has no override either.
|
|
10642
|
+
* @throws capability (rejects) The manifest does not declare `bindings.manage`.
|
|
10643
|
+
* @example Give the player's Mute command a second key
|
|
10644
|
+
* Mute now answers to M and to Ctrl+M. `set` replaces the command's
|
|
10645
|
+
* bindings with the list given (an override saved across launches), so it
|
|
10646
|
+
* repeats the default it keeps; `[]` would unbind it.
|
|
10647
|
+
* ```ts
|
|
10648
|
+
* await plugin.bindings.set({ plugin: "host", command: "mute" }, ["M", "Ctrl+M"]);
|
|
10649
|
+
* ```
|
|
9625
10650
|
*/
|
|
9626
10651
|
set(ref: BindingRef, inputs: string[]): Promise<void>;
|
|
9627
|
-
/**
|
|
10652
|
+
/**
|
|
10653
|
+
* Back to the defaults: one command, or every override when omitted.
|
|
10654
|
+
* @capability bindings.manage
|
|
10655
|
+
* @throws missing (rejects) A command that has no override and is not declared.
|
|
10656
|
+
* @throws capability (rejects) The manifest does not declare `bindings.manage`.
|
|
10657
|
+
* @example Restore every default key
|
|
10658
|
+
* A "Restore defaults" button removes all of the user's overrides, so
|
|
10659
|
+
* every command has its declared keys again. Without an argument `reset`
|
|
10660
|
+
* clears every override; with a reference only that command's.
|
|
10661
|
+
* ```ts
|
|
10662
|
+
* await plugin.bindings.reset(); // every override
|
|
10663
|
+
* ```
|
|
10664
|
+
*/
|
|
9628
10665
|
reset(ref?: BindingRef): Promise<void>;
|
|
9629
|
-
/**
|
|
10666
|
+
/**
|
|
10667
|
+
* Declarations or overrides changed (read `list()` again). At most every 200 ms.
|
|
10668
|
+
* @capability bindings.manage
|
|
10669
|
+
* @example Refresh a key list when bindings change
|
|
10670
|
+
* The plugin's key list stays current when the user changes a key in the
|
|
10671
|
+
* player or another plugin starts. The listener carries no data and runs
|
|
10672
|
+
* at most every 200 ms; the plugin reads the table again with `list`.
|
|
10673
|
+
* ```ts
|
|
10674
|
+
* plugin.bindings.onChange(async () => {
|
|
10675
|
+
* const { commands } = await plugin.bindings.list(); // read the new table
|
|
10676
|
+
* render(commands);
|
|
10677
|
+
* });
|
|
10678
|
+
* ```
|
|
10679
|
+
*/
|
|
9630
10680
|
onChange(listener: () => void): Disposable;
|
|
9631
10681
|
}
|
|
9632
10682
|
```
|
|
@@ -10717,6 +11767,13 @@ A tool window as the test host keeps it: the record of a
|
|
|
10717
11767
|
`plugin.windows.create(...)` window after its first `show()`. Get one with
|
|
10718
11768
|
TestHost.window.
|
|
10719
11769
|
|
|
11770
|
+
A record, not a window: no Slint file is compiled and nothing is drawn.
|
|
11771
|
+
`props` holds what the plugin wrote, by Slint name; `emit` fires a
|
|
11772
|
+
callback the way the user's input would; `userClose` closes it the way
|
|
11773
|
+
the title-bar button would.
|
|
11774
|
+
|
|
11775
|
+
- @capability None.
|
|
11776
|
+
|
|
10720
11777
|
```ts
|
|
10721
11778
|
interface FakeWindow {
|
|
10722
11779
|
/** The `.slint` file the window was created from (package-relative). */
|
|
@@ -10731,10 +11788,32 @@ interface FakeWindow {
|
|
|
10731
11788
|
callbacks: string[];
|
|
10732
11789
|
/**
|
|
10733
11790
|
* Fire a Slint callback as if the user triggered it in the window. The name may be kebab-case or camelCase; `args` are the callback arguments. Follow it with `await host.settle()` before asserting.
|
|
11791
|
+
* @capability None.
|
|
11792
|
+
* @example Type into a window's search field
|
|
11793
|
+
* The test types "cat" into the window's search box and checks the
|
|
11794
|
+
* plugin's results. `emit` delivers the Slint callback `search-changed`
|
|
11795
|
+
* with its argument to the plugin's `on` handler; `settle()` waits for the
|
|
11796
|
+
* properties the handler wrote.
|
|
11797
|
+
* ```ts
|
|
11798
|
+
* const search = host.window("search");
|
|
11799
|
+
* search.emit("searchChanged", "cat"); // camelCase or kebab-case
|
|
11800
|
+
* await host.settle();
|
|
11801
|
+
* assert.ok((search.props["results"] as unknown[]).length > 0);
|
|
11802
|
+
* ```
|
|
10734
11803
|
*/
|
|
10735
11804
|
emit(callback: string, ...args: Json[]): void;
|
|
10736
11805
|
/**
|
|
10737
11806
|
* The user closes the window with its title-bar button: the window closes and the plugin's `window.onClose` listeners run (unlike the plugin's own `close()`).
|
|
11807
|
+
* @capability None.
|
|
11808
|
+
* @example Close a window the way the user does
|
|
11809
|
+
* The test closes the plugin's window with its title-bar button and checks
|
|
11810
|
+
* that the plugin stopped its timer. Unlike the plugin's own `close()`,
|
|
11811
|
+
* `userClose` runs the plugin's `onClose` listeners.
|
|
11812
|
+
* ```ts
|
|
11813
|
+
* host.window("clock").userClose();
|
|
11814
|
+
* await host.settle();
|
|
11815
|
+
* assert.equal(host.window("clock").visible, false);
|
|
11816
|
+
* ```
|
|
10738
11817
|
*/
|
|
10739
11818
|
userClose(): void;
|
|
10740
11819
|
}
|
|
@@ -10777,6 +11856,13 @@ A command the plugin registered with `plugin.commands.add`, as the test
|
|
|
10777
11856
|
host keeps it. Get one with TestHost.command (the plugin's own) or
|
|
10778
11857
|
TestHost.coreAction (the player's core actions).
|
|
10779
11858
|
|
|
11859
|
+
The host's record of one command: what the plugin declared, the state
|
|
11860
|
+
it last sent, and how the menu shows it in TestHost.locale.
|
|
11861
|
+
Read it again after `settle()` to see changes; `run` executes it as the
|
|
11862
|
+
menu would.
|
|
11863
|
+
|
|
11864
|
+
- @capability None.
|
|
11865
|
+
|
|
10780
11866
|
```ts
|
|
10781
11867
|
interface FakeCommand {
|
|
10782
11868
|
/** What the plugin declared: id, menu path, title, order, group, default bindings, focus and repeat. Wire-level shape; it may gain fields in minor versions. */
|
|
@@ -10809,6 +11895,15 @@ interface FakeCommand {
|
|
|
10809
11895
|
revision: number;
|
|
10810
11896
|
/**
|
|
10811
11897
|
* Execute the command the way the menu would, then wait until the host is idle. Rejects (with a HostError) when the command is disabled or busy, like the player, which does not call `run` then.
|
|
11898
|
+
* @capability None.
|
|
11899
|
+
* @throws command (rejects) A `HostError`: the command is disabled or busy (the message says which).
|
|
11900
|
+
* @example Check that a disabled item refuses to run
|
|
11901
|
+
* The test checks that Export cannot run while nothing is open. Like the
|
|
11902
|
+
* player, the host does not call `run` for a disabled or busy command;
|
|
11903
|
+
* the promise rejects with a `HostError`.
|
|
11904
|
+
* ```ts
|
|
11905
|
+
* await assert.rejects(host.command("export").run());
|
|
11906
|
+
* ```
|
|
10812
11907
|
*/
|
|
10813
11908
|
run(): Promise<void>;
|
|
10814
11909
|
}
|
|
@@ -10834,6 +11929,13 @@ assert.equal(host.command("toggle-loop").state.checked, true);
|
|
|
10834
11929
|
A command's bottom-bar button (`CommandOptions.button`) as the player shows
|
|
10835
11930
|
it: an entry of TestHost.barButtons.
|
|
10836
11931
|
|
|
11932
|
+
A bottom-bar button as the player would show it, derived from the
|
|
11933
|
+
command's declaration and state each time it is read: clickable while
|
|
11934
|
+
the command is enabled and not busy, accent-coloured while checked, with
|
|
11935
|
+
the command's title (and reason) as the tooltip.
|
|
11936
|
+
|
|
11937
|
+
- @capability None.
|
|
11938
|
+
|
|
10837
11939
|
```ts
|
|
10838
11940
|
interface FakeBarButton {
|
|
10839
11941
|
/** The command's local id. */
|
|
@@ -10852,17 +11954,45 @@ interface FakeBarButton {
|
|
|
10852
11954
|
readonly tooltip: string;
|
|
10853
11955
|
/**
|
|
10854
11956
|
* Click the button, then wait until the host is idle: runs the command as its menu item would. Rejects (with a HostError) while the button is disabled, and once the command is gone.
|
|
11957
|
+
* @capability None.
|
|
11958
|
+
* @throws command (rejects) A `HostError`: the command is disabled or busy.
|
|
11959
|
+
* @throws missing (rejects) A `HostError`: the command is gone.
|
|
11960
|
+
* @example Click the button and wait for its effect
|
|
11961
|
+
* The test clicks the plugin's button and checks the OSD. `click` runs the
|
|
11962
|
+
* command like its menu item and resolves once the host is idle.
|
|
11963
|
+
* ```ts
|
|
11964
|
+
* await host.barButtons[0]?.click();
|
|
11965
|
+
* assert.ok(host.osd.length > 0);
|
|
11966
|
+
* ```
|
|
10855
11967
|
*/
|
|
10856
11968
|
click(): Promise<void>;
|
|
10857
11969
|
}
|
|
10858
11970
|
```
|
|
10859
11971
|
|
|
11972
|
+
Example: Check a bar button's tooltip while it is disabled
|
|
11973
|
+
|
|
11974
|
+
The test checks that the Export button shows why it cannot be clicked.
|
|
11975
|
+
The tooltip is the command's title; while the command is disabled or
|
|
11976
|
+
busy and has a non-empty `reason`, the reason follows on a second line.
|
|
11977
|
+
```ts
|
|
11978
|
+
const [button] = host.barButtons;
|
|
11979
|
+
assert.equal(button?.enabled, false);
|
|
11980
|
+
assert.match(button?.tooltip ?? "", /\n/); // the command declares a reason
|
|
11981
|
+
```
|
|
11982
|
+
|
|
10860
11983
|
### FakeProducer
|
|
10861
11984
|
|
|
10862
11985
|
A generated stream (`plugin.player.openStream`) as the test host sees it:
|
|
10863
11986
|
the fake player end of the frame connection. Assertion surface; see
|
|
10864
11987
|
TestHost.producers.
|
|
10865
11988
|
|
|
11989
|
+
The player's end of a generated stream's frame connection
|
|
11990
|
+
(TestHost.producers): how many frames arrived and whether the
|
|
11991
|
+
plugin released it or the player retired it. Tests assert on it; nothing
|
|
11992
|
+
is played.
|
|
11993
|
+
|
|
11994
|
+
- @capability None.
|
|
11995
|
+
|
|
10866
11996
|
```ts
|
|
10867
11997
|
interface FakeProducer {
|
|
10868
11998
|
/** The producer id the host assigned (`1`, `2`, … per host). */
|
|
@@ -10884,6 +12014,18 @@ interface FakeProducer {
|
|
|
10884
12014
|
}
|
|
10885
12015
|
```
|
|
10886
12016
|
|
|
12017
|
+
Example: Check that the stream delivers frames
|
|
12018
|
+
|
|
12019
|
+
The test starts the plugin's generator and checks that video and audio
|
|
12020
|
+
arrive. The counters grow as the SDK pulls frames and sends them over
|
|
12021
|
+
the fake connection.
|
|
12022
|
+
```ts
|
|
12023
|
+
await host.command("start-generator").run();
|
|
12024
|
+
await host.settle(200); // let a few frames through
|
|
12025
|
+
const [producer] = host.producers.values();
|
|
12026
|
+
assert.ok((producer?.videoFrames ?? 0) > 0 && (producer?.receivedFrames ?? 0) > 0);
|
|
12027
|
+
```
|
|
12028
|
+
|
|
10887
12029
|
### MarkdownWire
|
|
10888
12030
|
|
|
10889
12031
|
A Markdown document as TestHost.markdown reads it back.
|
|
@@ -10938,6 +12080,15 @@ Fields that mirror player state at the wire level (`source`, `subtitles`,
|
|
|
10938
12080
|
`calls`) use the protocol's shapes. They are fine for assertions but are
|
|
10939
12081
|
not part of the plugin API: their shape may change in minor versions.
|
|
10940
12082
|
|
|
12083
|
+
An in-memory player that speaks the real wire protocol, so the SDK's own
|
|
12084
|
+
logic runs as it does in the player; only the player side is simulated
|
|
12085
|
+
(no decoding, no Slint, no windows). Make one per test, preset it if the
|
|
12086
|
+
plugin reads something during activation, start the plugin with
|
|
12087
|
+
startPlugin, drive it, `settle()`, and assert on what it
|
|
12088
|
+
recorded.
|
|
12089
|
+
|
|
12090
|
+
- @capability None.
|
|
12091
|
+
|
|
10941
12092
|
```ts
|
|
10942
12093
|
class TestHost implements Transport {
|
|
10943
12094
|
/** Every request the plugin sent, in order: `{ method, params }`. Wire-level; prefer TestHost.sent for one method. */
|
|
@@ -11020,6 +12171,15 @@ class TestHost implements Transport {
|
|
|
11020
12171
|
/**
|
|
11021
12172
|
* Read the plugin's translations for TestHost.locale from a package directory, as the player would (`locales/<locale>.json` of a built package, else the source `locales/<locale>.po`); `startPlugin` does this. None for English; a missing file means none; a broken `.po` throws.
|
|
11022
12173
|
* @param directory - The package directory (holding `locales/`).
|
|
12174
|
+
* @capability None.
|
|
12175
|
+
* @example Load the translations after changing the language
|
|
12176
|
+
* The test switches the host to German before starting the plugin and
|
|
12177
|
+
* loads that catalog itself (`startPlugin` does the same). Texts are then
|
|
12178
|
+
* shown in German.
|
|
12179
|
+
* ```ts
|
|
12180
|
+
* host.locale = "de";
|
|
12181
|
+
* host.loadCatalog(directory); // locales/de.po (or de.json)
|
|
12182
|
+
* ```
|
|
11023
12183
|
*/
|
|
11024
12184
|
loadCatalog(directory: string): void;
|
|
11025
12185
|
/**
|
|
@@ -11027,6 +12187,7 @@ class TestHost implements Transport {
|
|
|
11027
12187
|
* @param text - English source text or template.
|
|
11028
12188
|
* @param args - Values for its placeholders.
|
|
11029
12189
|
* @returns The shown text.
|
|
12190
|
+
* @capability None.
|
|
11030
12191
|
* @example Compare with a translated message
|
|
11031
12192
|
* The test expects the text a Chinese user sees for a template the plugin
|
|
11032
12193
|
* shows. `translate` looks the English source up in the plugin's catalog
|
|
@@ -11041,6 +12202,19 @@ class TestHost implements Transport {
|
|
|
11041
12202
|
/**
|
|
11042
12203
|
* Simulate another plugin (or the user) saving the interface language: pushes `appearance.changed`.
|
|
11043
12204
|
* @param language - `"system"` or a locale.
|
|
12205
|
+
* @capability None.
|
|
12206
|
+
* @example Simulate the user choosing French for the next start
|
|
12207
|
+
* The plugin hears that the saved interface language changed. The host
|
|
12208
|
+
* pushes `appearance.changed`, which a plugin with `events` sees in
|
|
12209
|
+
* `appearance.language` once it has read it and subscribed; this run's
|
|
12210
|
+
* language does not change.
|
|
12211
|
+
* ```ts
|
|
12212
|
+
* void plugin.appearance.language; // read once: the SDK then follows it
|
|
12213
|
+
* plugin.appearance.onChange(() => {}); // subscribes to appearance events
|
|
12214
|
+
* host.setLanguage("fr");
|
|
12215
|
+
* await host.settle();
|
|
12216
|
+
* assert.equal(plugin.appearance.language, "fr");
|
|
12217
|
+
* ```
|
|
11044
12218
|
*/
|
|
11045
12219
|
setLanguage(language: LanguagePreference): void;
|
|
11046
12220
|
/**
|
|
@@ -11197,6 +12371,7 @@ class TestHost implements Transport {
|
|
|
11197
12371
|
* Simulate another plugin publishing its timeline layer (or clearing it with `[]`); pushes `timeline.changed` when the plugin subscribed.
|
|
11198
12372
|
* @param pluginId - The other plugin's manifest id.
|
|
11199
12373
|
* @param marks - Its whole layer.
|
|
12374
|
+
* @capability None.
|
|
11200
12375
|
* @example Let another plugin publish chapters
|
|
11201
12376
|
* The plugin under test sees two chapter marks another plugin put on the
|
|
11202
12377
|
* timeline. `setTimelineLayer` stores that plugin's whole layer and pushes
|
|
@@ -11230,6 +12405,7 @@ class TestHost implements Transport {
|
|
|
11230
12405
|
}[];
|
|
11231
12406
|
/**
|
|
11232
12407
|
* The file an image property shows, or `null` when the value is not a runtime image. View properties hold images in their wire form; this reads the path back.
|
|
12408
|
+
* @capability None.
|
|
11233
12409
|
* @example Check which file a window shows as its cover
|
|
11234
12410
|
* The test checks that the Cover window's image comes from the plugin's
|
|
11235
12411
|
* data directory. An image property holds the SDK's wire form of
|
|
@@ -11255,6 +12431,8 @@ class TestHost implements Transport {
|
|
|
11255
12431
|
* Drop files like the player: on a panel with an `onDrop` listener the whole list goes there; otherwise (and on the video area) each path goes to the first provider of its extension (`folder` for existing directories), one call per provider with all of its paths. Files nobody takes are ignored. Resolves once the host is idle again.
|
|
11256
12432
|
* @param paths - The dropped paths, in the order the file manager gives.
|
|
11257
12433
|
* @param options - `target`: `"video"` (default) or `"panel"`; `at`: the drop point in panel coordinates (default `{ x: 0, y: 0 }`); `modifiers`: keys held (default all `false`).
|
|
12434
|
+
* @capability None.
|
|
12435
|
+
* @throws Never: files nobody takes are ignored, as in the player.
|
|
11258
12436
|
* @example Drop files on the video and on the panel
|
|
11259
12437
|
* The first drop lands on the video area: each extension belongs to the
|
|
11260
12438
|
* provider that registered it first, and each provider gets one call with
|
|
@@ -11285,7 +12463,20 @@ class TestHost implements Transport {
|
|
|
11285
12463
|
}): Promise<void>;
|
|
11286
12464
|
/** The palette (`plugin.appearance.theme`); `setAppearance` changes it elsewhere. */
|
|
11287
12465
|
appearance: ThemeId;
|
|
11288
|
-
/**
|
|
12466
|
+
/**
|
|
12467
|
+
* Simulate another plugin switching the palette: updates TestHost.appearance and pushes `appearance.changed`.
|
|
12468
|
+
* @capability None.
|
|
12469
|
+
* @example Simulate another plugin switching to the light palette
|
|
12470
|
+
* The plugin hears the palette change. The host updates `appearance` and
|
|
12471
|
+
* pushes `appearance.changed` to a plugin that listens with `events`.
|
|
12472
|
+
* ```ts
|
|
12473
|
+
* const themes: ThemeId[] = [];
|
|
12474
|
+
* plugin.appearance.onChange((theme) => themes.push(theme)); // subscribes (`events`)
|
|
12475
|
+
* host.setAppearance("vestra-light");
|
|
12476
|
+
* await host.settle();
|
|
12477
|
+
* assert.deepEqual(themes, ["vestra-light"]);
|
|
12478
|
+
* ```
|
|
12479
|
+
*/
|
|
11289
12480
|
setAppearance(theme: ThemeId): void;
|
|
11290
12481
|
/** Settings pages by id (`plugin.settings.page`), every `show` (page id or null), and the page the options window shows. */
|
|
11291
12482
|
readonly settings: {
|
|
@@ -11317,6 +12508,7 @@ class TestHost implements Transport {
|
|
|
11317
12508
|
};
|
|
11318
12509
|
/**
|
|
11319
12510
|
* One settings page by id. Throws when it was never registered.
|
|
12511
|
+
* @capability None.
|
|
11320
12512
|
* @example Change a value on the plugin's settings page
|
|
11321
12513
|
* The test sets the step on the General page and checks the page shows
|
|
11322
12514
|
* it. `emit` fires the page's callback as the user's input would; the
|
|
@@ -11354,13 +12546,26 @@ class TestHost implements Transport {
|
|
|
11354
12546
|
/** Whether it shows now (no video frame). `true` when mounted. */
|
|
11355
12547
|
effective: boolean;
|
|
11356
12548
|
} | null;
|
|
11357
|
-
/**
|
|
12549
|
+
/**
|
|
12550
|
+
* Simulate the backdrop's `effective` changing (`false`: a video frame appeared; `true`: there is none again); pushed to the plugin.
|
|
12551
|
+
* @capability None.
|
|
12552
|
+
* @example Make a video picture replace the backdrop
|
|
12553
|
+
* The plugin's backdrop hears that a video frame now covers it. `false`
|
|
12554
|
+
* means a picture appeared; the change is pushed like the player's
|
|
12555
|
+
* `window.state`.
|
|
12556
|
+
* ```ts
|
|
12557
|
+
* host.setBackdropEffective(false);
|
|
12558
|
+
* await host.settle();
|
|
12559
|
+
* assert.equal(plugin.panels.backdrop.state?.effective, false);
|
|
12560
|
+
* ```
|
|
12561
|
+
*/
|
|
11358
12562
|
setBackdropEffective(effective: boolean): void;
|
|
11359
12563
|
/**
|
|
11360
12564
|
* Simulate another plugin mounting a panel, before or after the plugin under test, as the player handles it: in the sidebar it is another tab (after the existing ones; the first panel's tab is current); the bottom bar and the backdrop take one panel, so the plugin's own mount there then rejects with `conflict` (not an activation failure, as in the player). Pushed as a `window.state` change.
|
|
11361
12565
|
* @param slot - The position.
|
|
11362
12566
|
* @param plugin - The other plugin's manifest id.
|
|
11363
12567
|
* @param options - `title`: its tab label (default: `plugin`).
|
|
12568
|
+
* @capability None.
|
|
11364
12569
|
* @example Put the playlist next to this plugin's sidebar panel
|
|
11365
12570
|
* The sidebar ends up with two tabs: the playlist, mounted first, and the
|
|
11366
12571
|
* plugin's own panel after it. `mountOther` before `startPlugin` mounts
|
|
@@ -11377,10 +12582,31 @@ class TestHost implements Transport {
|
|
|
11377
12582
|
mountOther(slot: "bottom" | "sidebar" | "backdrop", plugin: string, options?: {
|
|
11378
12583
|
title?: string;
|
|
11379
12584
|
}): void;
|
|
11380
|
-
/**
|
|
12585
|
+
/**
|
|
12586
|
+
* Simulate another plugin's panel going away (TestHost.mountOther): when its tab was current, the next tab (else the previous one) is.
|
|
12587
|
+
* @capability None.
|
|
12588
|
+
* @example Let the playlist's tab go away
|
|
12589
|
+
* The other plugin's sidebar tab disappears and the plugin's own tab
|
|
12590
|
+
* becomes current. When the removed tab was current, the next tab (else
|
|
12591
|
+
* the previous one) becomes current, as in the player.
|
|
12592
|
+
* ```ts
|
|
12593
|
+
* host.unmountOther("vestra.playlist");
|
|
12594
|
+
* await host.settle();
|
|
12595
|
+
* assert.equal(host.layout.sidebar?.panels?.length, 1);
|
|
12596
|
+
* ```
|
|
12597
|
+
*/
|
|
11381
12598
|
unmountOther(plugin: string): void;
|
|
11382
12599
|
/**
|
|
11383
12600
|
* Simulate the user clicking a sidebar tab: `plugin`'s panel (the plugin under test's `manifestId`, or one of TestHost.mountOther) becomes the current tab. Pushed as a `window.state` change.
|
|
12601
|
+
* @capability None.
|
|
12602
|
+
* @example Click another plugin's sidebar tab
|
|
12603
|
+
* The user switches the sidebar to the playlist; the plugin's panel is in
|
|
12604
|
+
* a background tab now. The change is pushed as `window.state`.
|
|
12605
|
+
* ```ts
|
|
12606
|
+
* host.selectTab("vestra.playlist");
|
|
12607
|
+
* await host.settle();
|
|
12608
|
+
* assert.notEqual(host.layout.sidebar?.owner, 1); // the plugin under test is owner 1
|
|
12609
|
+
* ```
|
|
11384
12610
|
*/
|
|
11385
12611
|
selectTab(plugin: string): void;
|
|
11386
12612
|
/** The display layer (`plugin.video.view()`): who owns it (this plugin is `manifestId`) and the values the player holds. */
|
|
@@ -11392,6 +12618,17 @@ class TestHost implements Transport {
|
|
|
11392
12618
|
};
|
|
11393
12619
|
/**
|
|
11394
12620
|
* Simulate another plugin taking the display layer first (the plugin's `video.view()` then fails with `conflict`), or, with `null`, releasing it (values reset to the defaults).
|
|
12621
|
+
* @capability None.
|
|
12622
|
+
* @example Let another plugin own the display layer
|
|
12623
|
+
* The plugin's `video.view()` fails with `conflict` because another plugin
|
|
12624
|
+
* took the display layer first. `null` releases it again.
|
|
12625
|
+
* ```ts
|
|
12626
|
+
* host.setVideoViewOwner("someone.zoom");
|
|
12627
|
+
* const view = plugin.video.view();
|
|
12628
|
+
* view.set({ zoom: 2 });
|
|
12629
|
+
* await host.settle();
|
|
12630
|
+
* assert.equal(view.status.state, "failed");
|
|
12631
|
+
* ```
|
|
11395
12632
|
*/
|
|
11396
12633
|
setVideoViewOwner(pluginId: string | null): void;
|
|
11397
12634
|
/**
|
|
@@ -11402,15 +12639,27 @@ class TestHost implements Transport {
|
|
|
11402
12639
|
owner: string | null;
|
|
11403
12640
|
/** The values the player holds. */
|
|
11404
12641
|
values: RenderSettings;
|
|
11405
|
-
/** `true`: GPU rendering; `false`: software rendering (`unsupported`). */
|
|
12642
|
+
/** `true`: GPU rendering; `false`: software rendering (`unsupported`). `plugin.app.system()` reports it as `rendering`. */
|
|
11406
12643
|
gpu: boolean;
|
|
11407
12644
|
};
|
|
11408
12645
|
/**
|
|
11409
12646
|
* Simulate another plugin owning the render options (the plugin's `video.render()` then fails with `conflict`), or, with `null`, releasing them (values reset to the defaults).
|
|
12647
|
+
* @capability None.
|
|
12648
|
+
* @example Let another plugin own the render options
|
|
12649
|
+
* The plugin's render options report `conflict` while another plugin owns
|
|
12650
|
+
* them; `null` releases them and resets the values to the defaults.
|
|
12651
|
+
* ```ts
|
|
12652
|
+
* host.setVideoRenderOwner("someone.hdr");
|
|
12653
|
+
* const render = plugin.video.render();
|
|
12654
|
+
* render.set({ toneMapping: "bt.2390" });
|
|
12655
|
+
* await host.settle();
|
|
12656
|
+
* assert.equal(render.status.state, "failed");
|
|
12657
|
+
* ```
|
|
11410
12658
|
*/
|
|
11411
12659
|
setVideoRenderOwner(pluginId: string | null): void;
|
|
11412
12660
|
/**
|
|
11413
12661
|
* What the player pushes when mpv cannot compile a shader (code `failed`) or switches to software rendering (`unsupported`, which also sets `videoRender.gpu` to false). Pushed only when this plugin is the owner.
|
|
12662
|
+
* @capability None.
|
|
11414
12663
|
* @example Make a shader fail to compile
|
|
11415
12664
|
* The plugin's render options report `failed`, as when mpv rejects a
|
|
11416
12665
|
* shader. The host pushes the failure only while this plugin owns the
|
|
@@ -11479,6 +12728,8 @@ class TestHost implements Transport {
|
|
|
11479
12728
|
bytes: Buffer[];
|
|
11480
12729
|
/** The announced total size in bytes. */
|
|
11481
12730
|
expected: number;
|
|
12731
|
+
/** The source generation it was begun for; like the player, a new source drops the uploads of the old one. */
|
|
12732
|
+
generation?: number;
|
|
11482
12733
|
}>;
|
|
11483
12734
|
/** Snapshots taken with `media.capture`, by snapshot id. Wire-level. */
|
|
11484
12735
|
readonly snapshots: Map<number, Json>;
|
|
@@ -11531,10 +12782,21 @@ class TestHost implements Transport {
|
|
|
11531
12782
|
/** Exact host names it resolves (a web resolver), with `schemes`. */
|
|
11532
12783
|
hosts?: string[];
|
|
11533
12784
|
}[];
|
|
11534
|
-
/**
|
|
12785
|
+
/**
|
|
12786
|
+
* What `plugin.resolvers.fileTypes` reports: the extensions of this plugin's file resolvers and of TestHost.otherResolvers, sorted, each once.
|
|
12787
|
+
* @capability None.
|
|
12788
|
+
* @example Check the file types a resolver adds
|
|
12789
|
+
* The test checks the list the plugin will read as
|
|
12790
|
+
* `resolvers.fileTypes`: the extensions of its own file resolvers and of
|
|
12791
|
+
* `otherResolvers`, sorted, each once.
|
|
12792
|
+
* ```ts
|
|
12793
|
+
* assert.deepEqual(host.fileTypes(), ["ch8"]);
|
|
12794
|
+
* ```
|
|
12795
|
+
*/
|
|
11535
12796
|
fileTypes(): string[];
|
|
11536
12797
|
/**
|
|
11537
12798
|
* Push `resolvers.fileTypes` when TestHost.fileTypes changed: called after this plugin's resolvers come and go; call it yourself after changing TestHost.otherResolvers (another plugin started or stopped).
|
|
12799
|
+
* @capability None.
|
|
11538
12800
|
* @example Let another plugin's file type appear while running
|
|
11539
12801
|
* Another plugin starts and adds `.ch8`; the plugin under test then sees
|
|
11540
12802
|
* it in `resolvers.fileTypes`. Changing `otherResolvers` alone pushes
|
|
@@ -11621,8 +12883,10 @@ class TestHost implements Transport {
|
|
|
11621
12883
|
* `plugin.storage.session`: key → value, as the player keeps them. Preset entries to simulate values an earlier run of the plugin stored in the same player session, or pass `startPlugin(..., { sessionStorage })`. Like the player, a key is 1–1024 bytes, a `set` that would make the entries larger than 1 MiB (keys, values as compact JSON and `SESSION_STORAGE_LIMITS.entryBytes` per key) fails `limit`, and an object's properties are stored sorted, as the player hands them back.
|
|
11622
12884
|
*/
|
|
11623
12885
|
readonly sessionStorage: Map<string, Json>;
|
|
11624
|
-
/**
|
|
11625
|
-
|
|
12886
|
+
/**
|
|
12887
|
+
* What `plugin.app.system()` reports. Its `rendering` follows videoRender.gpu (`"gpu"` or `"software"`), so one switch covers the render options and the fact.
|
|
12888
|
+
*/
|
|
12889
|
+
systemInfo: Omit<WireSystemInfo, "rendering">;
|
|
11626
12890
|
/** What `plugin.app` reports. Set it before startPlugin: the plugin receives it once at start-up. */
|
|
11627
12891
|
app: {
|
|
11628
12892
|
/** The player version. */
|
|
@@ -11652,6 +12916,9 @@ class TestHost implements Transport {
|
|
|
11652
12916
|
readonly dataDirectory: string;
|
|
11653
12917
|
/** The manifest id of the plugin under test (set by startPlugin; `"test.plugin"` by default). */
|
|
11654
12918
|
manifestId: string;
|
|
12919
|
+
/**
|
|
12920
|
+
* A fresh in-memory player: nothing open, no plugin, the player's core actions registered. Preset what the plugin reads during activation, then pass it to startPlugin as `host`.
|
|
12921
|
+
*/
|
|
11655
12922
|
constructor();
|
|
11656
12923
|
/** The `player.setStepSeek` value in effect (default 5). */
|
|
11657
12924
|
stepSeek: number;
|
|
@@ -11707,7 +12974,8 @@ class TestHost implements Transport {
|
|
|
11707
12974
|
/** Every core action run (`host.run`, a core command, a pressed core binding), newest last. */
|
|
11708
12975
|
readonly coreRuns: string[];
|
|
11709
12976
|
/**
|
|
11710
|
-
* One of the 14 core actions (`play-pause`, `stop`, `seek-forward`, ...) that the test host registers like the player, as `host/<id>`. Throws for an unknown id. `run()` executes it with the player's checks and writes its on-screen text to TestHost.osd.
|
|
12977
|
+
* One of the 14 core actions (`play-pause`, `stop`, `seek-forward`, ...) that the test host registers like the player, as `host/<id>`. Throws for an unknown id. `run()` executes it like its menu item: a disabled action rejects with `command`, as in the player (`plugin.host.run` rejects with `unavailable` instead); otherwise it runs with the player's checks and writes its on-screen text to TestHost.osd.
|
|
12978
|
+
* @capability None.
|
|
11711
12979
|
* @example Pause through the player's own action
|
|
11712
12980
|
* The test pauses playback the way the Space key does and checks that the
|
|
11713
12981
|
* host recorded it. Core actions belong to the player (`host/<id>`);
|
|
@@ -11734,6 +13002,7 @@ class TestHost implements Transport {
|
|
|
11734
13002
|
* find the one a call uses.
|
|
11735
13003
|
* @param method - The wire method name.
|
|
11736
13004
|
* @param handler - Produces the reply from the request parameters.
|
|
13005
|
+
* @capability None.
|
|
11737
13006
|
* @example Make opening a folder fail
|
|
11738
13007
|
* The plugin's Open Folder command meets a missing folder and must tell
|
|
11739
13008
|
* the user. `handle` replaces the host's answer to one wire method; the
|
|
@@ -11750,17 +13019,43 @@ class TestHost implements Transport {
|
|
|
11750
13019
|
* ```
|
|
11751
13020
|
*/
|
|
11752
13021
|
handle(method: string, handler: Handler): void;
|
|
11753
|
-
/**
|
|
13022
|
+
/**
|
|
13023
|
+
* Restore the default behaviour of a method replaced with TestHost.handle.
|
|
13024
|
+
* @capability None.
|
|
13025
|
+
* @example Restore the default answer after a simulated failure
|
|
13026
|
+
* The first write fails, the second succeeds. `reset` removes the handler
|
|
13027
|
+
* that `handle` installed, so the method is answered by the test host
|
|
13028
|
+
* again.
|
|
13029
|
+
* ```ts
|
|
13030
|
+
* host.handle("clipboard.writeText", () => {
|
|
13031
|
+
* throw new HostError("failed", "denied");
|
|
13032
|
+
* });
|
|
13033
|
+
* await assert.rejects(plugin.clipboard.writeText("x"));
|
|
13034
|
+
* host.reset("clipboard.writeText");
|
|
13035
|
+
* await plugin.clipboard.writeText("x"); // the default handler again
|
|
13036
|
+
* ```
|
|
13037
|
+
*/
|
|
11754
13038
|
reset(method: string): void;
|
|
11755
13039
|
/**
|
|
11756
13040
|
* Deliver a host event to the plugin. Like the player, subscription-based events (`source.state`, `playback.backend`, `window.state`, `timeline.changed`, `appearance.changed`) are delivered only when the plugin subscribed (it declared `events` and listens). Most tests use the higher-level helpers (`setSource`, `setMainWindow`, ...) instead.
|
|
11757
13041
|
* @param event - The wire event name.
|
|
11758
13042
|
* @param data - The event payload (wire-level shape).
|
|
13043
|
+
* @capability None.
|
|
13044
|
+
* @example Deliver a raw event the helpers do not cover
|
|
13045
|
+
* The test pushes a `window.state` event after changing `layout` by hand.
|
|
13046
|
+
* Like the player, the host delivers it only when the plugin subscribed;
|
|
13047
|
+
* most tests use `setSource`, `setMainWindow` and the other helpers
|
|
13048
|
+
* instead.
|
|
13049
|
+
* ```ts
|
|
13050
|
+
* host.emit("window.state", {}); // no facts: the SDK reads window.state again
|
|
13051
|
+
* await host.settle();
|
|
13052
|
+
* ```
|
|
11759
13053
|
*/
|
|
11760
13054
|
emit(event: string, data: unknown): void;
|
|
11761
13055
|
/**
|
|
11762
13056
|
* Change the source facts and broadcast them, as if the player's state changed (the user paused, playback reached the end, a file finished loading). Any phase other than `empty` also turns on the source capabilities (pause, seek, rate, ...) unless the patch sets them. `phase: "loading"` makes a mounted backdrop effective, like the player.
|
|
11763
13057
|
* @param patch - Wire-level source fields, e.g. `phase`, `position`, `duration`, `paused`, `audioTracks`.
|
|
13058
|
+
* @capability None.
|
|
11764
13059
|
* @example Load a file, then let it play to the end
|
|
11765
13060
|
* The plugin sees a one-minute source become ready, then end on its own.
|
|
11766
13061
|
* `setSource` changes the host's source facts and broadcasts them like
|
|
@@ -11777,6 +13072,7 @@ class TestHost implements Transport {
|
|
|
11777
13072
|
setSource(patch: Partial<SourceState>): void;
|
|
11778
13073
|
/**
|
|
11779
13074
|
* Simulate the user changing the main window (resizing it, entering or leaving fullscreen, closing the picture-in-picture window) and broadcast it. Like the player, entering fullscreen collapses an attached, visible sidebar (`effective: false`, `reason: "fullscreen"`) and leaving fullscreen docks it again.
|
|
13075
|
+
* @capability None.
|
|
11780
13076
|
* @example Go fullscreen, then report picture-in-picture support
|
|
11781
13077
|
* The plugin sees the main window fullscreen at 1920×1080, then learns
|
|
11782
13078
|
* that picture-in-picture is available. `setMainWindow` changes the
|
|
@@ -11803,12 +13099,23 @@ class TestHost implements Transport {
|
|
|
11803
13099
|
}>): void;
|
|
11804
13100
|
/**
|
|
11805
13101
|
* The pointer reached the right edge in fullscreen (`true`) or left the popped-out sidebar (`false`): the attached sidebar pops out or collapses (reference §6.3). No effect outside fullscreen, when hidden or floating.
|
|
13102
|
+
* @capability None.
|
|
13103
|
+
* @example Pop the sidebar out in fullscreen
|
|
13104
|
+
* The pointer reaches the right edge in fullscreen and the attached
|
|
13105
|
+
* sidebar pops out over the video. Outside fullscreen, or with the
|
|
13106
|
+
* sidebar hidden or floating, nothing happens.
|
|
13107
|
+
* ```ts
|
|
13108
|
+
* host.setMainWindow({ fullscreen: true });
|
|
13109
|
+
* host.popSidebar(true);
|
|
13110
|
+
* await host.settle();
|
|
13111
|
+
* ```
|
|
11806
13112
|
*/
|
|
11807
13113
|
popSidebar(popped: boolean): void;
|
|
11808
13114
|
/**
|
|
11809
13115
|
* Simulate another plugin (or the user) opening a source: a new `sourceId`, then the given phase. Calls the plugin made against the old state now fail with `stale`.
|
|
11810
13116
|
* @param source - The path or URL.
|
|
11811
13117
|
* @param phase - The phase to end in (default `"ready"`).
|
|
13118
|
+
* @capability None.
|
|
11812
13119
|
* @example Let the user open another file meanwhile
|
|
11813
13120
|
* A state the plugin captured before stops being current once another
|
|
11814
13121
|
* file opens. `openElsewhere` gives the source a new `sourceId`, so
|
|
@@ -11827,6 +13134,7 @@ class TestHost implements Transport {
|
|
|
11827
13134
|
* Simulate a generated stream failing in the player (its audio device lost, its frame connection broken), like the player: the source fails with `message` (`phase: "error"`) and the producer is retired at once. The stream's `onStop` listeners get a `stream` error with `message`.
|
|
11828
13135
|
* @param producerId - The producer, from TestHost.producers.
|
|
11829
13136
|
* @param message - The player's failure text.
|
|
13137
|
+
* @capability None.
|
|
11830
13138
|
* @example Break the plugin's generated stream
|
|
11831
13139
|
* The plugin's stream stops with an error, as when the player loses its
|
|
11832
13140
|
* audio device. `failStream` puts the source into `error` with the
|
|
@@ -11839,8 +13147,11 @@ class TestHost implements Transport {
|
|
|
11839
13147
|
*/
|
|
11840
13148
|
failStream(producerId: number, message: string): void;
|
|
11841
13149
|
/**
|
|
11842
|
-
* Press a binding like the host's table would: panel-focus bindings first when `panel` is true, then global ones; within a scope the earliest registration first (the player's own commands before the plugin's), as the conflict rule orders them. A disabled command lets the key pass to the next command bound to it (SDK 1.5.12, as player 0.4.3): the one it shadowed, then the global scope. The first enabled one runs; `press` resolves after it ran (`"ran"`)
|
|
13150
|
+
* Press a binding like the host's table would: panel-focus bindings first when `panel` is true, then global ones; within a scope the earliest registration first (the player's own commands before the plugin's), as the conflict rule orders them. A disabled command lets the key pass to the next command bound to it (SDK 1.5.12, as player 0.4.3): the one it shadowed, then the global scope. The first enabled one runs; `press` resolves after it ran (`"ran"`). When only disabled commands hold the key, the first of them decides, as in the player: with a `reason` it shows the reason on the OSD and eats the key (`"reason"`, see `host.osd`); without one the key goes on to the focused control (`"passed"`, nothing runs). Unlike the player, which only logs a key that reaches a busy command, `press` then rejects, so a test notices. Rejects when nothing is bound. Bindings use the reference syntax (`"Space"`, `"Ctrl+O"`, `"Mouse:WheelUp"`), and user overrides in `bindings.overlay` apply.
|
|
11843
13151
|
* @returns What the key did: `"ran"`, `"reason"` or `"passed"`.
|
|
13152
|
+
* @capability None.
|
|
13153
|
+
* @throws command (rejects) A `HostError`: the command that would run is busy (the player only logs this).
|
|
13154
|
+
* @throws Error (rejects) Nothing is bound to the key.
|
|
11844
13155
|
* @example Press a global key and a panel key
|
|
11845
13156
|
* Ctrl+B runs whatever is bound globally; Delete is pressed while the
|
|
11846
13157
|
* plugin's panel has focus, so panel bindings are tried first. `press`
|
|
@@ -11931,6 +13242,7 @@ class TestHost implements Transport {
|
|
|
11931
13242
|
readonly remoteImages: Map<string, string | null>;
|
|
11932
13243
|
/**
|
|
11933
13244
|
* The Markdown document a value holds (its wire form read back), or `null` when the value is not one. `images` maps each remote address to the downloaded file. Pass a whole property value or a field of one: a document nested in rows is read the same way.
|
|
13245
|
+
* @capability None.
|
|
11934
13246
|
* @example Read back a Markdown message in a chat window
|
|
11935
13247
|
* The test checks the Markdown source of the first chat message. Rows
|
|
11936
13248
|
* hold the document in its wire form, as `plugin.ui.markdown` produced
|
|
@@ -11948,13 +13260,47 @@ class TestHost implements Transport {
|
|
|
11948
13260
|
/** The system's name for it. */
|
|
11949
13261
|
name: string;
|
|
11950
13262
|
}[];
|
|
11951
|
-
/**
|
|
13263
|
+
/**
|
|
13264
|
+
* Replace the plugin list and push `packages.changed` (another plugin started, stopped, was installed or removed).
|
|
13265
|
+
* @capability None.
|
|
13266
|
+
* @example Simulate another plugin starting
|
|
13267
|
+
* The plugin's store window hears that a plugin is running now. The list
|
|
13268
|
+
* replaces `packages` and is pushed as `packages.changed`, which the
|
|
13269
|
+
* plugin's `packages.onChange` listeners receive.
|
|
13270
|
+
* ```ts
|
|
13271
|
+
* host.setPackages([...host.packages]); // edit a copy, then push it
|
|
13272
|
+
* await host.settle();
|
|
13273
|
+
* ```
|
|
13274
|
+
*/
|
|
11952
13275
|
setPackages(list: TestHost["packages"]): void;
|
|
11953
13276
|
/**
|
|
11954
13277
|
* Simulate another plugin calling `player.next()` / `previous()`: the host asks this plugin's sequence provider. Rejects with `unavailable` when there is no provider or its flag is false.
|
|
13278
|
+
* @capability None.
|
|
13279
|
+
* @throws unavailable (rejects) A `HostError`: there is no sequence provider, or its flag is false.
|
|
13280
|
+
* @example Ask the plugin's provider for the next item
|
|
13281
|
+
* Another plugin calls `player.next()`; the host asks the plugin under
|
|
13282
|
+
* test, which is the sequence provider, and the test checks what it
|
|
13283
|
+
* opened.
|
|
13284
|
+
* ```ts
|
|
13285
|
+
* await host.step("next");
|
|
13286
|
+
* await host.settle();
|
|
13287
|
+
* assert.equal(host.source.source, "/media/b.mp4");
|
|
13288
|
+
* ```
|
|
11955
13289
|
*/
|
|
11956
13290
|
step(direction: "next" | "previous"): Promise<unknown>;
|
|
11957
|
-
/**
|
|
13291
|
+
/**
|
|
13292
|
+
* The plugin's command with this local id. Throws when it is not registered (never added, or disposed).
|
|
13293
|
+
* @capability None.
|
|
13294
|
+
* @example Read a command's state after a change
|
|
13295
|
+
* The test checks that Export becomes enabled once a file is ready. The
|
|
13296
|
+
* record is updated as the plugin sends new state; read it after
|
|
13297
|
+
* `settle()`.
|
|
13298
|
+
* ```ts
|
|
13299
|
+
* host.setSource({ phase: "ready", duration: 60 });
|
|
13300
|
+
* await host.settle();
|
|
13301
|
+
* assert.equal(host.command("export").state.enabled, true); // throws if not registered
|
|
13302
|
+
* ```
|
|
13303
|
+
*/
|
|
11958
13304
|
command(id: string): FakeCommand;
|
|
11959
13305
|
/**
|
|
11960
13306
|
* The plugin's bottom-bar buttons (`CommandOptions.button`) as the player shows them: at most one, the first declared (`BAR_BUTTON_LIMITS`; a later one stays a menu item only). Empty once the command is disposed. Whether the bar places them is the bar's business: the list is what the player hands to `BarButtons`.
|
|
@@ -11974,6 +13320,7 @@ class TestHost implements Transport {
|
|
|
11974
13320
|
get barButtons(): readonly FakeBarButton[];
|
|
11975
13321
|
/**
|
|
11976
13322
|
* The plugin's tool window with this id (by default the file name without extension: `ui/picture.slint` → `picture`). Throws when it was never opened with `show()`.
|
|
13323
|
+
* @capability None.
|
|
11977
13324
|
* @example Press a window's Reset button from a test
|
|
11978
13325
|
* The Picture window's Reset callback runs and the brightness returns to
|
|
11979
13326
|
* 0. The window is found by its id (the `.slint` file name by default);
|
|
@@ -11988,6 +13335,7 @@ class TestHost implements Transport {
|
|
|
11988
13335
|
window(id: string): FakeWindow;
|
|
11989
13336
|
/**
|
|
11990
13337
|
* The plugin's mounted panel (bottom bar, sidebar or backdrop). Throws when none is mounted. Its `props` use Slint (kebab-case) names; `emit` fires a callback on the panel's global.
|
|
13338
|
+
* @capability None.
|
|
11991
13339
|
* @example Select a row in the plugin's sidebar panel
|
|
11992
13340
|
* The test checks the panel sits in the sidebar and selects its third
|
|
11993
13341
|
* row. `emit` fires a callback on the panel's global as a click would;
|
|
@@ -12009,6 +13357,7 @@ class TestHost implements Transport {
|
|
|
12009
13357
|
};
|
|
12010
13358
|
/**
|
|
12011
13359
|
* The parameters of every request of one wire method, oldest first.
|
|
13360
|
+
* @capability None.
|
|
12012
13361
|
* @example Check the request behind an OSD message
|
|
12013
13362
|
* The test checks the last OSD request the plugin sent, before
|
|
12014
13363
|
* translation. `sent` lists the parameters of every request of one wire
|
|
@@ -12029,6 +13378,8 @@ class TestHost implements Transport {
|
|
|
12029
13378
|
* for: on a loaded machine it can finish after `settle()` returns. Poll
|
|
12030
13379
|
* the result you assert on instead (second example).
|
|
12031
13380
|
* @param ms - Extra time to wait, for debounces and timers (default 0).
|
|
13381
|
+
* @capability None.
|
|
13382
|
+
* @throws Never.
|
|
12032
13383
|
* @example Wait past a debounced save
|
|
12033
13384
|
* The plugin's store writes its file 300 ms after the last `save`, so
|
|
12034
13385
|
* the test waits 400 ms before it checks what the plugin did next.
|
|
@@ -12054,7 +13405,22 @@ class TestHost implements Transport {
|
|
|
12054
13405
|
* ```
|
|
12055
13406
|
*/
|
|
12056
13407
|
settle(ms?: number): Promise<void>;
|
|
12057
|
-
/**
|
|
13408
|
+
/**
|
|
13409
|
+
* Close the fake stream servers and delete the host's temporary directories. `stop()` from startPlugin calls it.
|
|
13410
|
+
* @capability None.
|
|
13411
|
+
* @example Clean up a host that was never started
|
|
13412
|
+
* A test that made a host but failed before `startPlugin` removes the
|
|
13413
|
+
* host's temporary directories itself. `stop()` from `startPlugin` calls
|
|
13414
|
+
* `close` for you otherwise.
|
|
13415
|
+
* ```ts
|
|
13416
|
+
* const host = new TestHost();
|
|
13417
|
+
* try {
|
|
13418
|
+
* // … preset the host; the test fails here …
|
|
13419
|
+
* } finally {
|
|
13420
|
+
* host.close(); // deletes the host's temporary data
|
|
13421
|
+
* }
|
|
13422
|
+
* ```
|
|
13423
|
+
*/
|
|
12058
13424
|
close(): void;
|
|
12059
13425
|
}
|
|
12060
13426
|
```
|
|
@@ -12104,9 +13470,14 @@ throws, or when a registration made during activation fails (for example
|
|
|
12104
13470
|
`limit`, `conflict`, `invalid`), which is when the player would fail the
|
|
12105
13471
|
activation.
|
|
12106
13472
|
|
|
13473
|
+
The way into a plugin's tests: one call starts the plugin the way the
|
|
13474
|
+
player's runner does and returns the host to drive, the `plugin` object
|
|
13475
|
+
to call directly, and `stop` to end it like the player.
|
|
13476
|
+
|
|
12107
13477
|
- @param activate - The plugin's default-exported `activate` function.
|
|
12108
13478
|
- @param options - Where the plugin lives and how to start it.
|
|
12109
13479
|
- @returns `host`: the TestHost; `plugin`: the object passed to `activate` (call the plugin API on it directly); `stop()`: dispose the plugin like the player stopping it (scopes released, stores flushed), wait until the host is idle and remove the host's temporary files.
|
|
13480
|
+
- @capability None.
|
|
12110
13481
|
|
|
12111
13482
|
```ts
|
|
12112
13483
|
function startPlugin(activate: (plugin: Plugin) => unknown, options?: {
|
|
@@ -12751,6 +14122,61 @@ if (!token || Buffer.byteLength(token) > SECRET_LIMITS.valueBytes) {
|
|
|
12751
14122
|
}
|
|
12752
14123
|
```
|
|
12753
14124
|
|
|
14125
|
+
### SUBTITLE_LIMITS
|
|
14126
|
+
|
|
14127
|
+
The player's limits on subtitle files a plugin loads with
|
|
14128
|
+
Subtitles.load, which `TestHost` enforces with the player's codes.
|
|
14129
|
+
The track counts are per source: opening another file starts from zero.
|
|
14130
|
+
A file over `fileBytes` fails with `invalid`; every other limit fails with
|
|
14131
|
+
`busy`, and nothing is loaded.
|
|
14132
|
+
|
|
14133
|
+
Each `load` uses a new resource on the source, and removing the track
|
|
14134
|
+
frees its place among the live tracks (`tracksPerPlugin`, `tracks`,
|
|
14135
|
+
`totalBytes`) but not among the loads (`loadsPerPlugin`, `loads`). So one
|
|
14136
|
+
plugin loads at most 16 files into one source, with at most 4 of them
|
|
14137
|
+
present at once.
|
|
14138
|
+
|
|
14139
|
+
```ts
|
|
14140
|
+
const SUBTITLE_LIMITS: Readonly<{
|
|
14141
|
+
/** UTF-8 bytes of one file; a larger one fails with `invalid` (2 MiB). */
|
|
14142
|
+
readonly fileBytes: number;
|
|
14143
|
+
/** Tracks one plugin has loaded into the current source at the same time (4). */
|
|
14144
|
+
readonly tracksPerPlugin: 4;
|
|
14145
|
+
/** Loaded tracks of every plugin in the current source at the same time (16). */
|
|
14146
|
+
readonly tracks: 16;
|
|
14147
|
+
/** Bytes of the loaded tracks of every plugin in the current source together (16 MiB). */
|
|
14148
|
+
readonly totalBytes: number;
|
|
14149
|
+
/** Loads by one plugin into the current source, removed tracks included (16). */
|
|
14150
|
+
readonly loadsPerPlugin: 16;
|
|
14151
|
+
/** Loads by every plugin into the current source, removed tracks included (128). */
|
|
14152
|
+
readonly loads: 128;
|
|
14153
|
+
/** Loads of one plugin in progress at the same time (2). */
|
|
14154
|
+
readonly uploadsPerPlugin: 2;
|
|
14155
|
+
/** Loads of every plugin in progress at the same time (8). */
|
|
14156
|
+
readonly uploads: 8;
|
|
14157
|
+
}>;
|
|
14158
|
+
```
|
|
14159
|
+
|
|
14160
|
+
Example: Replace the plugin's oldest subtitle when it has four
|
|
14161
|
+
|
|
14162
|
+
The plugin keeps the files it loaded, newest last, and removes the oldest
|
|
14163
|
+
before a fifth would be refused with `busy`. A load the player still
|
|
14164
|
+
refuses (the source holds 16 tracks of every plugin, or this plugin
|
|
14165
|
+
already loaded 16 files into it) is reported on the OSD.
|
|
14166
|
+
```ts
|
|
14167
|
+
import { SUBTITLE_LIMITS } from "@vestra-player/sdk/testing";
|
|
14168
|
+
|
|
14169
|
+
const loaded: SubtitleTrack[] = []; // this source's, newest last
|
|
14170
|
+
if (loaded.length >= SUBTITLE_LIMITS.tracksPerPlugin)
|
|
14171
|
+
await plugin.subtitles.remove(loaded.shift()!);
|
|
14172
|
+
try {
|
|
14173
|
+
loaded.push(await plugin.subtitles.load(bytes));
|
|
14174
|
+
} catch (error) {
|
|
14175
|
+
if ((error as PluginError).code !== "busy") throw error;
|
|
14176
|
+
await plugin.osd.show("No room for another subtitle on this file.");
|
|
14177
|
+
}
|
|
14178
|
+
```
|
|
14179
|
+
|
|
12754
14180
|
### UI_VALUE_LIMITS
|
|
12755
14181
|
|
|
12756
14182
|
The player's limits on one property value (a `PropValue` as written
|
|
@@ -12851,11 +14277,17 @@ Common codes: `stale` (the condition no longer holds; nothing was done),
|
|
|
12851
14277
|
`timeout`, `aborted`, `limit` (a player limit such as the command count),
|
|
12852
14278
|
`disabled` (the plugin is stopping).
|
|
12853
14279
|
|
|
14280
|
+
A definite refusal, as opposed to a lost reply: the SDK does not retry
|
|
14281
|
+
it, and the plugin sees an `Error` with `code`.
|
|
14282
|
+
|
|
14283
|
+
- @capability None.
|
|
14284
|
+
|
|
12854
14285
|
```ts
|
|
12855
14286
|
class HostError extends Error {
|
|
12856
14287
|
/** The stable error code, e.g. `"invalid"`, `"stale"`, `"unavailable"`. */
|
|
12857
14288
|
code: string;
|
|
12858
14289
|
/**
|
|
14290
|
+
* A refusal with this code, as the player would send it.
|
|
12859
14291
|
* @param code - The stable error code the caller will see as `error.code`.
|
|
12860
14292
|
* @param message - Human-readable detail (not for branching on).
|
|
12861
14293
|
*/
|
|
@@ -13233,7 +14665,7 @@ interface SubtitleState {
|
|
|
13233
14665
|
|
|
13234
14666
|
### WireSystemInfo
|
|
13235
14667
|
|
|
13236
|
-
`app.system`: the operating system and the
|
|
14668
|
+
`app.system`: the operating system, the displays and the renderer.
|
|
13237
14669
|
|
|
13238
14670
|
```ts
|
|
13239
14671
|
interface WireSystemInfo {
|
|
@@ -13251,6 +14683,8 @@ interface WireSystemInfo {
|
|
|
13251
14683
|
refreshRate: number | null;
|
|
13252
14684
|
primary: boolean;
|
|
13253
14685
|
}[];
|
|
14686
|
+
/** Player 0.5.12; older players leave it out. */
|
|
14687
|
+
rendering?: "gpu" | "software";
|
|
13254
14688
|
}
|
|
13255
14689
|
```
|
|
13256
14690
|
|