@vestra-player/sdk 1.5.31 → 1.5.33
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 +15 -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/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/testing.d.ts +518 -9
- package/dist/plugin/testing.js +384 -16
- package/dist/plugin/types.d.ts +1470 -59
- 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 +13 -0
- package/dist/wire.js +10 -0
- package/llms-full.txt +1362 -46
- 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.33: 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
|
|
|
@@ -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.
|
|
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.
|
|
@@ -3494,9 +3573,32 @@ flushes, a running `tools.run` process is killed and a listener is
|
|
|
3494
3573
|
unsubscribed. A Media is the exception: disposing its scope does
|
|
3495
3574
|
**not** close the media; call Media.close for that.
|
|
3496
3575
|
|
|
3576
|
+
Every handle a plugin creates is a `Disposable` owned by the scope that
|
|
3577
|
+
created it; disposing the scope (or stopping the plugin) disposes them in
|
|
3578
|
+
reverse creation order, so a plugin rarely calls `dispose` itself.
|
|
3579
|
+
|
|
3580
|
+
- @capability None of its own: that of the handle.
|
|
3581
|
+
|
|
3497
3582
|
```ts
|
|
3498
3583
|
interface Disposable {
|
|
3499
|
-
/**
|
|
3584
|
+
/**
|
|
3585
|
+
* Release the resource. Resolves once the player confirmed the release (or immediately for purely local resources). Never rejects.
|
|
3586
|
+
* @capability None of its own: that of the handle.
|
|
3587
|
+
* @throws Never.
|
|
3588
|
+
* @example Remove a command when its feature is turned off
|
|
3589
|
+
* A "Turn feature off" item removes the feature's own menu item at once,
|
|
3590
|
+
* before the plugin stops. `dispose` takes the command out of the menu and
|
|
3591
|
+
* the binding table and resolves once the player confirmed it; the id can
|
|
3592
|
+
* be added again later.
|
|
3593
|
+
* ```ts
|
|
3594
|
+
* const extra = plugin.commands.add({ id: "extra", path: ["Tools", "Extra"], run() {} });
|
|
3595
|
+
* plugin.commands.add({
|
|
3596
|
+
* id: "extra-off",
|
|
3597
|
+
* path: ["Tools", "Turn extra off"],
|
|
3598
|
+
* run: () => extra.dispose(), // never rejects
|
|
3599
|
+
* });
|
|
3600
|
+
* ```
|
|
3601
|
+
*/
|
|
3500
3602
|
dispose(): Promise<void>;
|
|
3501
3603
|
}
|
|
3502
3604
|
```
|
|
@@ -3522,7 +3624,6 @@ change).
|
|
|
3522
3624
|
| code | meaning |
|
|
3523
3625
|
| --- | --- |
|
|
3524
3626
|
| `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
3627
|
| `capability` | The manifest does not declare the capability the call needs. |
|
|
3527
3628
|
| `invalid` | A bad argument (out of range, malformed, unknown id). |
|
|
3528
3629
|
| `missing` | The target does not exist (an empty panel position, a missing file, an unknown command). |
|
|
@@ -3542,7 +3643,7 @@ change).
|
|
|
3542
3643
|
| `backend-uncertain` | The player's backend blocked a filter stage; call `retry()`. |
|
|
3543
3644
|
| `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
3645
|
| `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`)
|
|
3646
|
+
| `versionMismatch` | The package needs a newer player (`minPlayerVersion`). |
|
|
3546
3647
|
| `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
3648
|
| `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
3649
|
| `host` | Any other failure inside the player; `message` is the player's reason. |
|
|
@@ -3605,6 +3706,14 @@ Reading Scope.window, Scope.app, Scope.appearance
|
|
|
3605
3706
|
`.theme`, Scope.ui, Scope.tools, Scope.storage and
|
|
3606
3707
|
Scope.system needs none.
|
|
3607
3708
|
|
|
3709
|
+
Every module of the API (`commands`, `windows`, `player`…) is reachable
|
|
3710
|
+
from a scope, and every handle made through it belongs to it. A child
|
|
3711
|
+
scope from Scope.scope is the unit for features that come and
|
|
3712
|
+
go: dispose the child and everything it created is released, while the
|
|
3713
|
+
rest of the plugin keeps running.
|
|
3714
|
+
|
|
3715
|
+
- @capability That of each module (see the modules).
|
|
3716
|
+
|
|
3608
3717
|
```ts
|
|
3609
3718
|
interface Scope extends Disposable {
|
|
3610
3719
|
/** Menu commands and their default bindings (`commands`). */
|
|
@@ -3628,10 +3737,16 @@ interface Scope extends Disposable {
|
|
|
3628
3737
|
*/
|
|
3629
3738
|
filter(id: string): VideoFilter;
|
|
3630
3739
|
/**
|
|
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.
|
|
3740
|
+
* 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.
|
|
3741
|
+
* @throws capability (throws) The manifest does not declare `processing.video`.
|
|
3742
|
+
* @throws Error without a code (throws) A second call in the same plugin while the first view is not disposed.
|
|
3632
3743
|
*/
|
|
3633
3744
|
view(): VideoView;
|
|
3634
|
-
/**
|
|
3745
|
+
/**
|
|
3746
|
+
* 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.
|
|
3747
|
+
* @throws capability (throws) The manifest does not declare `processing.video`.
|
|
3748
|
+
* @throws Error without a code (throws) A second call in the same plugin while the first handle is not disposed.
|
|
3749
|
+
*/
|
|
3635
3750
|
render(): RenderOptions;
|
|
3636
3751
|
};
|
|
3637
3752
|
/** Audio processing: this plugin's filter chain and delay (`processing.audio`, `playback.read` and `events`, as for VideoFilter). */
|
|
@@ -3685,17 +3800,57 @@ interface Scope extends Disposable {
|
|
|
3685
3800
|
readonly i18n: I18n;
|
|
3686
3801
|
/** Pages in the player's options window (`ui.settings`). */
|
|
3687
3802
|
readonly settings: Settings;
|
|
3688
|
-
/**
|
|
3803
|
+
/**
|
|
3804
|
+
* A child scope; disposing it releases only what was created through it. The child is itself owned by this scope.
|
|
3805
|
+
* @capability None.
|
|
3806
|
+
* @example Tie a temporary window and its timer to one feature
|
|
3807
|
+
* A "Live stats" feature opens a window and starts a timer; turning it off
|
|
3808
|
+
* releases both together. Everything made through the child scope, and
|
|
3809
|
+
* cleanups registered on it, go when the child is disposed; the parent
|
|
3810
|
+
* scope is untouched.
|
|
3811
|
+
* ```ts
|
|
3812
|
+
* let live: Scope | null = null;
|
|
3813
|
+
* plugin.commands.add({
|
|
3814
|
+
* id: "live-stats",
|
|
3815
|
+
* path: ["Tools", "Live stats"],
|
|
3816
|
+
* checked: () => live !== null,
|
|
3817
|
+
* run: async () => {
|
|
3818
|
+
* if (live) {
|
|
3819
|
+
* await live.dispose(); // closes the window, stops the timer
|
|
3820
|
+
* live = null;
|
|
3821
|
+
* return;
|
|
3822
|
+
* }
|
|
3823
|
+
* live = plugin.scope();
|
|
3824
|
+
* const window = live.windows.create("ui/stats.slint");
|
|
3825
|
+
* const timer = setInterval(() => live?.update(), 1000);
|
|
3826
|
+
* live.onDispose(() => clearInterval(timer));
|
|
3827
|
+
* await window.show();
|
|
3828
|
+
* },
|
|
3829
|
+
* });
|
|
3830
|
+
* ```
|
|
3831
|
+
*/
|
|
3689
3832
|
scope(): Scope;
|
|
3690
3833
|
/**
|
|
3691
3834
|
* Adopt an existing handle: it is disposed together with this scope.
|
|
3692
3835
|
* @param item - Any Disposable, for example a handle created through another scope.
|
|
3693
3836
|
* @returns `item` itself, for chaining.
|
|
3837
|
+
* @capability None.
|
|
3838
|
+
* @example Dispose a handle from another scope together with this one
|
|
3839
|
+
* A filter made through the root scope is withdrawn when a child feature
|
|
3840
|
+
* ends. `add` hands an existing disposable to this scope, which then
|
|
3841
|
+
* disposes it with its own handles; it returns the same handle for
|
|
3842
|
+
* chaining.
|
|
3843
|
+
* ```ts
|
|
3844
|
+
* const feature = plugin.scope();
|
|
3845
|
+
* const blur = feature.add(plugin.video.filter("blur")); // now ends with `feature`
|
|
3846
|
+
* blur.set([{ kind: "gblur", sigma: 4, steps: 3 }]);
|
|
3847
|
+
* ```
|
|
3694
3848
|
*/
|
|
3695
3849
|
add<T extends Disposable>(item: T): T;
|
|
3696
3850
|
/**
|
|
3697
3851
|
* Run `cleanup` when this scope is disposed (for example `clearInterval` or closing a socket). It may be async; errors are logged.
|
|
3698
3852
|
* @returns A handle that cancels the registration early (without running `cleanup`).
|
|
3853
|
+
* @capability None.
|
|
3699
3854
|
* @example Stop a timer when the plugin stops
|
|
3700
3855
|
* A once-a-second refresh runs as long as the plugin does. `onDispose`
|
|
3701
3856
|
* runs the cleanup when the scope is disposed, which for `plugin` is
|
|
@@ -3714,6 +3869,7 @@ interface Scope extends Disposable {
|
|
|
3714
3869
|
* Rejections of async handlers count too, and so do failures of
|
|
3715
3870
|
* registrations made after activation (a command whose `ready` rejects,
|
|
3716
3871
|
* a property write the player refused).
|
|
3872
|
+
* @capability None.
|
|
3717
3873
|
* @example Show errors from handlers on the OSD
|
|
3718
3874
|
* When a menu command, a window callback or a listener of the plugin
|
|
3719
3875
|
* fails, the user sees why. The SDK catches what those handlers throw
|
|
@@ -3733,6 +3889,23 @@ interface Scope extends Disposable {
|
|
|
3733
3889
|
* changing state from your own timers or between awaits of a long task.
|
|
3734
3890
|
*
|
|
3735
3891
|
* Several calls within one microtask are merged into one pass.
|
|
3892
|
+
* @capability None.
|
|
3893
|
+
* @example Show progress from a long task between awaits
|
|
3894
|
+
* A progress label moves while a long download runs, not only when it
|
|
3895
|
+
* ends. The SDK re-evaluates bound values after each handler and when its
|
|
3896
|
+
* promise settles; state changed between the awaits of a long handler
|
|
3897
|
+
* shows only after an explicit `update()`.
|
|
3898
|
+
* ```ts
|
|
3899
|
+
* let done = 0;
|
|
3900
|
+
* window.bind(() => ({ progress: done }));
|
|
3901
|
+
* window.on("start", async () => {
|
|
3902
|
+
* for (let part = 0; part < 10; part++) {
|
|
3903
|
+
* await downloadPart(part);
|
|
3904
|
+
* done = part + 1;
|
|
3905
|
+
* plugin.update(); // send `progress` now, not when the handler ends
|
|
3906
|
+
* }
|
|
3907
|
+
* });
|
|
3908
|
+
* ```
|
|
3736
3909
|
*/
|
|
3737
3910
|
update(): void;
|
|
3738
3911
|
}
|
|
@@ -3781,6 +3954,12 @@ root scope is disposed for local cleanup (timers, signals, sockets). A
|
|
|
3781
3954
|
process that has not exited after 300 ms is killed. Media the plugin
|
|
3782
3955
|
opened with Player.open keeps playing.
|
|
3783
3956
|
|
|
3957
|
+
The root scope: disposing it is what stopping the plugin does. Besides
|
|
3958
|
+
the modules of Scope, it carries the plugin's `id` and its
|
|
3959
|
+
`dataDirectory`.
|
|
3960
|
+
|
|
3961
|
+
- @capability That of each module (see the modules).
|
|
3962
|
+
|
|
3784
3963
|
```ts
|
|
3785
3964
|
interface Plugin extends Scope {
|
|
3786
3965
|
/** The manifest id, e.g. `"community.bookmark"`. */
|
|
@@ -5787,6 +5966,13 @@ interface DropHandler {
|
|
|
5787
5966
|
|
|
5788
5967
|
`plugin.drops`: files dropped on the video area. Needs `ui.drop`.
|
|
5789
5968
|
|
|
5969
|
+
Files dropped on the video area go to providers by extension: each
|
|
5970
|
+
extension belongs to the provider registered for it first, and each
|
|
5971
|
+
provider gets one call with all of its paths. Files nobody takes are
|
|
5972
|
+
ignored. A panel with Panel.onDrop takes drops on itself first.
|
|
5973
|
+
|
|
5974
|
+
- @capability ui.drop
|
|
5975
|
+
|
|
5790
5976
|
```ts
|
|
5791
5977
|
interface Drops {
|
|
5792
5978
|
/**
|
|
@@ -5797,6 +5983,7 @@ interface Drops {
|
|
|
5797
5983
|
* to the provider that registered it first) in the order the file
|
|
5798
5984
|
* manager gave them, one call per provider. Folders are not expanded.
|
|
5799
5985
|
* @throws invalid synchronously for malformed extensions.
|
|
5986
|
+
* @capability ui.drop
|
|
5800
5987
|
* @example Load dropped subtitle files
|
|
5801
5988
|
* Dropping .srt or .ass files on the video loads each one as a subtitle
|
|
5802
5989
|
* track. Each extension belongs to the provider that registered it first;
|
|
@@ -5822,6 +6009,25 @@ interface Drops {
|
|
|
5822
6009
|
}
|
|
5823
6010
|
```
|
|
5824
6011
|
|
|
6012
|
+
Example: Collect dropped CHIP-8 programs
|
|
6013
|
+
|
|
6014
|
+
`.ch8` files dropped on the player are added to the plugin's list. The
|
|
6015
|
+
provider claims the extension; the player calls `drop` with the dropped
|
|
6016
|
+
paths of that type, in the order the file manager gave them. Extensions
|
|
6017
|
+
go to the first provider: the built-in playlist claims every audio and
|
|
6018
|
+
video extension when it starts, and a built-in plugin cannot be disabled,
|
|
6019
|
+
so another plugin's provider for those never gets them (except a
|
|
6020
|
+
development load that replaces the playlist under its id).
|
|
6021
|
+
```ts
|
|
6022
|
+
const queue: string[] = [];
|
|
6023
|
+
plugin.drops.provide({
|
|
6024
|
+
extensions: ["ch8"],
|
|
6025
|
+
drop: ({ paths }) => {
|
|
6026
|
+
queue.push(...paths); // only .ch8 files reach here
|
|
6027
|
+
},
|
|
6028
|
+
});
|
|
6029
|
+
```
|
|
6030
|
+
|
|
5825
6031
|
### ThemeId
|
|
5826
6032
|
|
|
5827
6033
|
The player's palettes: `vestra`, the dark default, and `vestra-light`
|
|
@@ -6693,10 +6899,18 @@ interface TimelineMark {
|
|
|
6693
6899
|
|
|
6694
6900
|
This plugin's own marks; disposing it removes them.
|
|
6695
6901
|
|
|
6902
|
+
This plugin's marks on the seek bar, from Timeline.layer. `set`
|
|
6903
|
+
replaces the whole layer and never throws: the newest write wins, and a
|
|
6904
|
+
refused one is reported to `onError`. Disposing the layer, its scope or
|
|
6905
|
+
stopping the plugin removes the marks.
|
|
6906
|
+
|
|
6907
|
+
- @capability timeline.marks
|
|
6908
|
+
|
|
6696
6909
|
```ts
|
|
6697
6910
|
interface TimelineLayer extends Disposable {
|
|
6698
6911
|
/**
|
|
6699
6912
|
* 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.
|
|
6913
|
+
* @capability timeline.marks
|
|
6700
6914
|
* @example Show the file's bookmarks on the timeline
|
|
6701
6915
|
* Bookmark marks appear on the seek bar at their times, titled with the
|
|
6702
6916
|
* user's notes. `set` replaces this plugin's whole layer; with `ifCurrent`
|
|
@@ -6721,22 +6935,76 @@ interface TimelineLayer extends Disposable {
|
|
|
6721
6935
|
}
|
|
6722
6936
|
```
|
|
6723
6937
|
|
|
6938
|
+
Example: Clear the plugin's marks
|
|
6939
|
+
|
|
6940
|
+
The plugin's marks disappear from the seek bar while it keeps its
|
|
6941
|
+
layer for later. An empty list clears the layer; other plugins' marks
|
|
6942
|
+
stay.
|
|
6943
|
+
```ts
|
|
6944
|
+
plugin.timeline.layer().set([]);
|
|
6945
|
+
```
|
|
6946
|
+
|
|
6724
6947
|
### Timeline
|
|
6725
6948
|
|
|
6726
6949
|
`plugin.timeline`: marks on the seek bar. Every plugin publishes one
|
|
6727
6950
|
layer; the player merges them. A new source clears every layer.
|
|
6728
6951
|
|
|
6952
|
+
Marks on the player's seek bar: chapters, bookmarks, loop points. Each
|
|
6953
|
+
plugin publishes one layer with Timeline.layer; the player merges
|
|
6954
|
+
the layers by time and shows them. A new source clears every layer, so a
|
|
6955
|
+
plugin publishes again for each file. `marks` is the merged list, which
|
|
6956
|
+
any plugin can read.
|
|
6957
|
+
|
|
6958
|
+
- @capability `timeline.marks` to publish; `playback.read` to read; `events` to follow changes.
|
|
6959
|
+
|
|
6729
6960
|
```ts
|
|
6730
6961
|
interface Timeline {
|
|
6731
6962
|
/** Every layer merged, by time (`playback.read`). Empty on a new source. */
|
|
6732
6963
|
readonly marks: readonly TimelineMark[];
|
|
6733
|
-
/**
|
|
6964
|
+
/**
|
|
6965
|
+
* The merged list changed (needs `events`; at most every 100 ms).
|
|
6966
|
+
* @capability events and playback.read
|
|
6967
|
+
* @example Follow the marks as they change
|
|
6968
|
+
* A panel lists the seek bar's marks and updates when any plugin publishes
|
|
6969
|
+
* or a new file clears them. The listener gets the merged list, at most
|
|
6970
|
+
* every 100 ms.
|
|
6971
|
+
* ```ts
|
|
6972
|
+
* plugin.timeline.onChange((marks) => {
|
|
6973
|
+
* console.log(marks.map((mark) => `${mark.time}s ${mark.title ?? mark.kind}`));
|
|
6974
|
+
* });
|
|
6975
|
+
* ```
|
|
6976
|
+
*/
|
|
6734
6977
|
onChange(listener: (marks: readonly TimelineMark[]) => void): Disposable;
|
|
6735
|
-
/**
|
|
6978
|
+
/**
|
|
6979
|
+
* This plugin's layer (`timeline.marks`); a second call throws.
|
|
6980
|
+
* @capability timeline.marks
|
|
6981
|
+
* @example Publish this plugin's marks
|
|
6982
|
+
* The plugin gets its one layer and puts two chapter marks on the seek
|
|
6983
|
+
* bar. Calling `layer()` again while the first layer is not disposed
|
|
6984
|
+
* throws; disposing the layer removes the
|
|
6985
|
+
* marks.
|
|
6986
|
+
* ```ts
|
|
6987
|
+
* const layer = plugin.timeline.layer();
|
|
6988
|
+
* layer.set([
|
|
6989
|
+
* { time: 0, kind: "chapter", title: "Intro" },
|
|
6990
|
+
* { time: 95, kind: "chapter", title: "Part 1" }, // seconds
|
|
6991
|
+
* ]);
|
|
6992
|
+
* ```
|
|
6993
|
+
*/
|
|
6736
6994
|
layer(): TimelineLayer;
|
|
6737
6995
|
}
|
|
6738
6996
|
```
|
|
6739
6997
|
|
|
6998
|
+
Example: Count the chapter marks other plugins published
|
|
6999
|
+
|
|
7000
|
+
The plugin reads how many chapter marks the seek bar shows, whoever
|
|
7001
|
+
published them. `marks` is the merged list of every layer, sorted by
|
|
7002
|
+
time, with each mark's `plugin`.
|
|
7003
|
+
```ts
|
|
7004
|
+
const chapters = plugin.timeline.marks.filter((mark) => mark.kind === "chapter");
|
|
7005
|
+
await plugin.osd.show("{} chapters", [chapters.length]);
|
|
7006
|
+
```
|
|
7007
|
+
|
|
6740
7008
|
### WindowState
|
|
6741
7009
|
|
|
6742
7010
|
The main window as `plugin.window.state` reports it (logical points).
|
|
@@ -6772,11 +7040,28 @@ With `events`, `state` follows the window from the first read or
|
|
|
6772
7040
|
`onChange`; without it, it updates only at start and after this plugin's
|
|
6773
7041
|
`setVideoArea`.
|
|
6774
7042
|
|
|
7043
|
+
`plugin.window` is the player's main window: its size, fullscreen,
|
|
7044
|
+
picture-in-picture and the video area. Reading it needs no capability;
|
|
7045
|
+
changing it needs `window.control`. Changes that last only while the
|
|
7046
|
+
plugin runs (always on top, pointer auto-hide) revert when it stops.
|
|
7047
|
+
|
|
7048
|
+
- @capability None to read; window.control to change; events to follow outside changes.
|
|
7049
|
+
|
|
6775
7050
|
```ts
|
|
6776
7051
|
interface MainWindow {
|
|
6777
7052
|
/** Latest state; zero video area without a native window. */
|
|
6778
7053
|
readonly state: WindowState;
|
|
6779
|
-
/**
|
|
7054
|
+
/**
|
|
7055
|
+
* The window state changed (needs `events` for outside changes).
|
|
7056
|
+
* @capability events (for outside changes)
|
|
7057
|
+
* @example Fit a panel to fullscreen
|
|
7058
|
+
* The plugin's panel uses larger text while the player is fullscreen. The
|
|
7059
|
+
* listener runs after the SDK updated `state` from the player's events
|
|
7060
|
+
* (with `events`; without it only after this plugin's own changes).
|
|
7061
|
+
* ```ts
|
|
7062
|
+
* plugin.window.onChange((state) => bar.set({ large: state.fullscreen }));
|
|
7063
|
+
* ```
|
|
7064
|
+
*/
|
|
6780
7065
|
onChange(listener: (state: WindowState) => void): Disposable;
|
|
6781
7066
|
/**
|
|
6782
7067
|
* Resize the window so the video column is `width`×`height` (the bottom bar
|
|
@@ -6788,6 +7073,18 @@ interface MainWindow {
|
|
|
6788
7073
|
* Sizes are 1–16384 logical points; the minimum video area is 318×120.
|
|
6789
7074
|
* The visible area excludes the menu bar and Dock on macOS; on Windows it
|
|
6790
7075
|
* is the whole display. `unavailable` without a user interface too.
|
|
7076
|
+
* @capability window.control
|
|
7077
|
+
* @throws unavailable (rejects) Fullscreen, or no user interface.
|
|
7078
|
+
* @throws invalid (rejects) A size outside 1–16384 logical points.
|
|
7079
|
+
* @throws capability (rejects) The manifest does not declare `window.control`.
|
|
7080
|
+
* @example Make the video area 1280×720 points
|
|
7081
|
+
* The window resizes so that the video column is 1280×720 logical points,
|
|
7082
|
+
* the bars and a docked sidebar added around it. A request that does not
|
|
7083
|
+
* fit the screen is scaled down keeping the aspect, and `clamped` says so.
|
|
7084
|
+
* ```ts
|
|
7085
|
+
* const applied = await plugin.window.setVideoArea({ width: 1280, height: 720 });
|
|
7086
|
+
* if (applied.clamped) await plugin.osd.show("{}×{}", [applied.width, applied.height]);
|
|
7087
|
+
* ```
|
|
6791
7088
|
*/
|
|
6792
7089
|
setVideoArea(size: {
|
|
6793
7090
|
width: number;
|
|
@@ -6800,10 +7097,42 @@ interface MainWindow {
|
|
|
6800
7097
|
/** The request was scaled down or raised to fit. */
|
|
6801
7098
|
clamped: boolean;
|
|
6802
7099
|
}>;
|
|
6803
|
-
/**
|
|
7100
|
+
/**
|
|
7101
|
+
* Float the player's windows above other applications. Newest call wins; reverts when this plugin stops. `unavailable` without a user interface.
|
|
7102
|
+
* @capability window.control
|
|
7103
|
+
* @throws unavailable (rejects) No user interface.
|
|
7104
|
+
* @throws invalid (rejects) `onTop` is not a boolean.
|
|
7105
|
+
* @throws capability (rejects) The manifest does not declare `window.control`.
|
|
7106
|
+
* @example Keep the player above other windows
|
|
7107
|
+
* The player's windows (tool windows too) float above other applications
|
|
7108
|
+
* while the plugin's "Always on top" item is checked. The newest call of
|
|
7109
|
+
* any plugin wins, and the setting reverts when this plugin stops.
|
|
7110
|
+
* ```ts
|
|
7111
|
+
* let onTop = false;
|
|
7112
|
+
* plugin.commands.add({
|
|
7113
|
+
* id: "on-top",
|
|
7114
|
+
* path: ["@screen", "Always on top"],
|
|
7115
|
+
* checked: () => onTop,
|
|
7116
|
+
* run: async () => {
|
|
7117
|
+
* onTop = !onTop;
|
|
7118
|
+
* await plugin.window.setAlwaysOnTop(onTop); // reverts when this plugin stops
|
|
7119
|
+
* },
|
|
7120
|
+
* });
|
|
7121
|
+
* ```
|
|
7122
|
+
*/
|
|
6804
7123
|
setAlwaysOnTop(onTop: boolean): Promise<void>;
|
|
6805
7124
|
/**
|
|
6806
7125
|
* `"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.
|
|
7126
|
+
* @capability window.control
|
|
7127
|
+
* @throws invalid (rejects) A mode other than `"fullscreen"` or `"playing"`.
|
|
7128
|
+
* @throws capability (rejects) The manifest does not declare `window.control`.
|
|
7129
|
+
* @example Hide the pointer over playing video in a window too
|
|
7130
|
+
* The pointer disappears after 3 idle seconds over playing video, not only
|
|
7131
|
+
* in fullscreen. The newest call wins; when this plugin stops the player
|
|
7132
|
+
* goes back to hiding it in fullscreen only.
|
|
7133
|
+
* ```ts
|
|
7134
|
+
* await plugin.window.setCursorAutoHide("playing");
|
|
7135
|
+
* ```
|
|
6807
7136
|
*/
|
|
6808
7137
|
setCursorAutoHide(mode: "fullscreen" | "playing"): Promise<void>;
|
|
6809
7138
|
/**
|
|
@@ -6818,6 +7147,13 @@ interface MainWindow {
|
|
|
6818
7147
|
*
|
|
6819
7148
|
* Where it is unsupported (Windows, Linux), `setVideoArea` plus
|
|
6820
7149
|
* `setAlwaysOnTop` make a reasonable "mini player" fallback.
|
|
7150
|
+
* @capability window.control
|
|
7151
|
+
* @throws unsupported (rejects) The system has no picture-in-picture (`supported` is false).
|
|
7152
|
+
* @throws unavailable (rejects) No video, or no user interface.
|
|
7153
|
+
* @throws invalid (rejects) `on` is not a boolean.
|
|
7154
|
+
* @throws busy (rejects) The previous call is still switching.
|
|
7155
|
+
* @throws failed (rejects) The system refused.
|
|
7156
|
+
* @throws capability (rejects) The manifest does not declare `window.control`.
|
|
6821
7157
|
* @example Toggle picture-in-picture where the system has it
|
|
6822
7158
|
* The video moves into a floating picture-in-picture window, or back.
|
|
6823
7159
|
* `state.pictureInPicture` says whether the system supports it and
|
|
@@ -6881,9 +7217,29 @@ interface AppInfo {
|
|
|
6881
7217
|
`plugin.app`: the fixed facts plus the system, read on request. No
|
|
6882
7218
|
capability.
|
|
6883
7219
|
|
|
7220
|
+
`plugin.app` holds facts fixed for the player's run (its version, build,
|
|
7221
|
+
library versions) and reads the system's on request: `system()` reads
|
|
7222
|
+
the displays at each call; the operating system's name and version are
|
|
7223
|
+
worked out once, at the first call, and reused.
|
|
7224
|
+
|
|
7225
|
+
- @capability None.
|
|
7226
|
+
|
|
6884
7227
|
```ts
|
|
6885
7228
|
interface App extends AppInfo {
|
|
6886
|
-
/**
|
|
7229
|
+
/**
|
|
7230
|
+
* The operating system and the displays, read when called (displays come and go). No capability. CPU and memory: Node's `os` module.
|
|
7231
|
+
* @capability None.
|
|
7232
|
+
* @throws busy (rejects) The player could not start the request (no thread available).
|
|
7233
|
+
* @example Choose a size for the primary display
|
|
7234
|
+
* The plugin sizes its picture for the primary display. `system()` asks
|
|
7235
|
+
* the player; `displays` lists the displays present now, the primary one
|
|
7236
|
+
* first (empty without a user interface).
|
|
7237
|
+
* ```ts
|
|
7238
|
+
* const { displays } = await plugin.app.system();
|
|
7239
|
+
* const primary = displays[0];
|
|
7240
|
+
* if (primary) console.log(primary.width, primary.height);
|
|
7241
|
+
* ```
|
|
7242
|
+
*/
|
|
6887
7243
|
system(): Promise<SystemInfo>;
|
|
6888
7244
|
}
|
|
6889
7245
|
```
|
|
@@ -8611,6 +8967,14 @@ one request is in flight at a time and only the newest value is sent.
|
|
|
8611
8967
|
Values out of range fail with `invalid` (see VideoNode). The
|
|
8612
8968
|
same chain applies to processed snapshots and exports.
|
|
8613
8969
|
|
|
8970
|
+
Members: `set(nodes)` records the desired chain (last write wins) and
|
|
8971
|
+
sends it, one request at a time and only the newest; `status` and
|
|
8972
|
+
`onStatus` report `waiting`, `applying`, `applied` or `failed`; `retry`
|
|
8973
|
+
applies the current value again; `dispose` withdraws the chain. `set`
|
|
8974
|
+
never throws: a refused chain shows as `failed` in `status`.
|
|
8975
|
+
|
|
8976
|
+
- @capability processing.video, playback.read and events
|
|
8977
|
+
|
|
8614
8978
|
```ts
|
|
8615
8979
|
type VideoFilter = Filter<VideoNode[]>;
|
|
8616
8980
|
```
|
|
@@ -8638,6 +9002,12 @@ VideoFilter.
|
|
|
8638
9002
|
`delayMs` is −10000–10000 in multiples of 10; the positive and the
|
|
8639
9003
|
negative delays of all plugins each add up to at most 10000.
|
|
8640
9004
|
|
|
9005
|
+
The same members and rules as VideoFilter: `set` records the
|
|
9006
|
+
desired value and never throws, `status` and `onStatus` report the
|
|
9007
|
+
result, `retry` applies it again, `dispose` withdraws it.
|
|
9008
|
+
|
|
9009
|
+
- @capability processing.audio, playback.read and events
|
|
9010
|
+
|
|
8641
9011
|
```ts
|
|
8642
9012
|
type AudioFilter = Filter<{
|
|
8643
9013
|
/** The audio chain; `[]` for none. */
|
|
@@ -8688,13 +9058,46 @@ needed); disposing or stopping the owner restores the defaults.
|
|
|
8688
9058
|
Out-of-range values or unknown fields fail with `invalid` without
|
|
8689
9059
|
sending anything.
|
|
8690
9060
|
|
|
9061
|
+
Not part of snapshots or exports: it changes how the picture sits in the
|
|
9062
|
+
video area, not the picture. `set` merges into the current settings and
|
|
9063
|
+
never throws: an out-of-range value or unknown field shows as `failed`
|
|
9064
|
+
(`invalid`) in `status` and is reported to `onError`, without sending
|
|
9065
|
+
anything.
|
|
9066
|
+
|
|
9067
|
+
- @capability processing.video, playback.read and events (like VideoFilter)
|
|
9068
|
+
|
|
8691
9069
|
```ts
|
|
8692
9070
|
interface VideoView extends Disposable {
|
|
8693
|
-
/**
|
|
9071
|
+
/**
|
|
9072
|
+
* Merged into the current settings; last write wins.
|
|
9073
|
+
* @capability processing.video
|
|
9074
|
+
* @example Stretch a 4:3 video to fill the window
|
|
9075
|
+
* A 4:3 video fills a 16:9 window, distorted, as the user asked. Each
|
|
9076
|
+
* `set` merges the given fields into this plugin's view settings; the
|
|
9077
|
+
* newest write wins, and the settings stay for later files until the
|
|
9078
|
+
* plugin disposes the view or stops.
|
|
9079
|
+
* ```ts
|
|
9080
|
+
* plugin.video.view().set({ fit: "stretch" });
|
|
9081
|
+
* ```
|
|
9082
|
+
*/
|
|
8694
9083
|
set(value: Partial<ViewSettings>): void;
|
|
8695
9084
|
/** Current status (the same four states as filters). */
|
|
8696
9085
|
readonly status: FilterStatus;
|
|
8697
|
-
/**
|
|
9086
|
+
/**
|
|
9087
|
+
* The status changed.
|
|
9088
|
+
* @capability processing.video
|
|
9089
|
+
* @example Tell the user another plugin owns the view
|
|
9090
|
+
* When another plugin already controls the display layer, the OSD says
|
|
9091
|
+
* so. The first plugin to call `view()` owns it; this plugin's handle
|
|
9092
|
+
* then reports `failed` with `conflict`, and tries again on its next `set`.
|
|
9093
|
+
* ```ts
|
|
9094
|
+
* const view = plugin.video.view();
|
|
9095
|
+
* view.onStatus((status) => {
|
|
9096
|
+
* if (status.state === "failed" && status.error.code === "conflict")
|
|
9097
|
+
* void plugin.osd.show("Another plugin controls the picture size");
|
|
9098
|
+
* });
|
|
9099
|
+
* ```
|
|
9100
|
+
*/
|
|
8698
9101
|
onStatus(listener: (status: FilterStatus) => void): Disposable;
|
|
8699
9102
|
}
|
|
8700
9103
|
```
|
|
@@ -8704,7 +9107,7 @@ Example: Fill the window, then zoom in
|
|
|
8704
9107
|
The video fills the video area (cropped instead of letterboxed), then
|
|
8705
9108
|
zooms to 150 % shifted a little to the right. The first plugin to call
|
|
8706
9109
|
`view()` owns the display layer; each `set` merges into its settings,
|
|
8707
|
-
which stay across sources until the plugin
|
|
9110
|
+
which stay across sources until the plugin disposes the view or stops.
|
|
8708
9111
|
```ts
|
|
8709
9112
|
const view = plugin.video.view();
|
|
8710
9113
|
view.set({ fit: "fill" });
|
|
@@ -8742,13 +9145,45 @@ Platforms: only macOS renders on the GPU. Under software rendering
|
|
|
8742
9145
|
`unsupported`. macOS OpenGL 4.1 has no compute shaders, so `peakDetect`
|
|
8743
9146
|
is accepted but has no effect; output is always SDR.
|
|
8744
9147
|
|
|
9148
|
+
GPU renderer options (tone mapping, peak detection, shaders,
|
|
9149
|
+
interpolation). Only the GPU renderer has them, which today means macOS;
|
|
9150
|
+
under software rendering the handle is `failed` with `unsupported` after
|
|
9151
|
+
the player's first answer to `render()` (reported to `onError` too), and so is
|
|
9152
|
+
every `set`. Single owner, like VideoView. `set` never throws; the
|
|
9153
|
+
result shows in `status`.
|
|
9154
|
+
|
|
9155
|
+
- @capability processing.video, playback.read and events (like VideoFilter)
|
|
9156
|
+
|
|
8745
9157
|
```ts
|
|
8746
9158
|
interface RenderOptions extends Disposable {
|
|
8747
|
-
/**
|
|
9159
|
+
/**
|
|
9160
|
+
* Merged into the current settings; last write wins.
|
|
9161
|
+
* @capability processing.video
|
|
9162
|
+
* @example Turn on motion interpolation
|
|
9163
|
+
* Motion becomes smoother on the GPU renderer. `set` merges the field into
|
|
9164
|
+
* the plugin's render options; under software rendering the status turns
|
|
9165
|
+
* `failed` with `unsupported` instead.
|
|
9166
|
+
* ```ts
|
|
9167
|
+
* plugin.video.render().set({ interpolation: true });
|
|
9168
|
+
* ```
|
|
9169
|
+
*/
|
|
8748
9170
|
set(value: Partial<RenderSettings>): void;
|
|
8749
9171
|
/** `failed` with `unsupported` while the player renders in software; `failed` with `failed` when mpv cannot compile a shader (pushed later). */
|
|
8750
9172
|
readonly status: FilterStatus;
|
|
8751
|
-
/**
|
|
9173
|
+
/**
|
|
9174
|
+
* The status changed (including later pushes such as shader compile errors).
|
|
9175
|
+
* @capability processing.video
|
|
9176
|
+
* @example Report a shader that does not compile
|
|
9177
|
+
* The OSD shows mpv's message when a shader the plugin set fails to
|
|
9178
|
+
* compile. The player pushes that failure after the `set` was accepted, so
|
|
9179
|
+
* it arrives through `onStatus`, not as a rejected call.
|
|
9180
|
+
* ```ts
|
|
9181
|
+
* const render = plugin.video.render();
|
|
9182
|
+
* render.onStatus((status) => {
|
|
9183
|
+
* if (status.state === "failed") void plugin.osd.show("Render: {}", [status.error.message]);
|
|
9184
|
+
* });
|
|
9185
|
+
* ```
|
|
9186
|
+
*/
|
|
8752
9187
|
onStatus(listener: (status: FilterStatus) => void): Disposable;
|
|
8753
9188
|
}
|
|
8754
9189
|
```
|
|
@@ -8850,16 +9285,79 @@ interface SubtitleConditional {
|
|
|
8850
9285
|
`subtitles.read`; changes need `subtitles.control` and also
|
|
8851
9286
|
`subtitles.read`, since they read the current selection first.
|
|
8852
9287
|
|
|
9288
|
+
The subtitle tracks of the current source, the primary and secondary
|
|
9289
|
+
selection, uploaded files and the style. `state` is the SDK's snapshot of
|
|
9290
|
+
the player's subtitle state (`null` until first read). The player has no
|
|
9291
|
+
subtitle event: the SDK reads the state again when the source, its phase
|
|
9292
|
+
or the backend changes (with `events`) and after this plugin's own
|
|
9293
|
+
changes, so a selection the user or another plugin changed shows only
|
|
9294
|
+
after `refresh()`. A change without `ifCurrent` reads the state first
|
|
9295
|
+
and applies to it; pass `ifCurrent` to make it fail with `stale` when the
|
|
9296
|
+
selection changed since that state. Each change is one operation the
|
|
9297
|
+
player confirms; `select`, `remove` and `setStyle` resolve with the new
|
|
9298
|
+
state, `load` with the new track.
|
|
9299
|
+
|
|
9300
|
+
- @capability `subtitles.read` to read; `subtitles.control` and `subtitles.read` to change; `events` to re-read on source changes.
|
|
9301
|
+
|
|
8853
9302
|
```ts
|
|
8854
9303
|
interface Subtitles {
|
|
8855
9304
|
/** `null` until first read. */
|
|
8856
9305
|
readonly state: SubtitlesState | null;
|
|
8857
|
-
/**
|
|
9306
|
+
/**
|
|
9307
|
+
* The subtitle state changed.
|
|
9308
|
+
* @capability events and subtitles.read
|
|
9309
|
+
* @example Show the subtitle language after a new file opens
|
|
9310
|
+
* When a new file starts, the OSD names the language of its primary
|
|
9311
|
+
* subtitle. The SDK reads the subtitle state again when the source or its
|
|
9312
|
+
* phase changes (there is no subtitle event, so a selection changed in the
|
|
9313
|
+
* player shows only after `refresh()`) and then calls the listener;
|
|
9314
|
+
* `selected` is `null` when subtitles are off.
|
|
9315
|
+
* ```ts
|
|
9316
|
+
* plugin.subtitles.onChange((state) => {
|
|
9317
|
+
* // A track's language is null when the file does not say.
|
|
9318
|
+
* void plugin.osd.show("Subtitles: {}", [state.selected?.language ?? "off"]);
|
|
9319
|
+
* });
|
|
9320
|
+
* ```
|
|
9321
|
+
*/
|
|
8858
9322
|
onChange(listener: (state: SubtitlesState) => void): Disposable;
|
|
8859
|
-
/**
|
|
9323
|
+
/**
|
|
9324
|
+
* Read the state from the player now.
|
|
9325
|
+
* @capability subtitles.read
|
|
9326
|
+
* @throws capability (rejects) The manifest does not declare `subtitles.read`.
|
|
9327
|
+
* @example Count the subtitle tracks of the file
|
|
9328
|
+
* The plugin reads how many subtitle tracks the file has before building a
|
|
9329
|
+
* menu. `refresh` asks the player and resolves with the current state, also
|
|
9330
|
+
* without `events`; `state` holds it afterwards.
|
|
9331
|
+
* ```ts
|
|
9332
|
+
* const { tracks } = await plugin.subtitles.refresh();
|
|
9333
|
+
* await plugin.osd.show("{} subtitle tracks", [tracks.length]);
|
|
9334
|
+
* ```
|
|
9335
|
+
*/
|
|
8860
9336
|
refresh(): Promise<SubtitlesState>;
|
|
8861
9337
|
/**
|
|
8862
9338
|
* `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.
|
|
9339
|
+
* @capability subtitles.control and subtitles.read
|
|
9340
|
+
* @throws stale (rejects) With `ifCurrent`: the source or the subtitle selection changed since that state.
|
|
9341
|
+
* @throws invalid (rejects) An unknown track; as secondary, a bitmap track or the primary one; as primary, the current secondary.
|
|
9342
|
+
* @throws missing (rejects) The track is not in the current source.
|
|
9343
|
+
* @throws unavailable (rejects) Nothing loaded, or the source is still loading or not yet confirmed.
|
|
9344
|
+
* @throws busy (rejects) Too many subtitle operations at once.
|
|
9345
|
+
* @throws capability (rejects) The manifest does not declare `subtitles.control` and `subtitles.read`.
|
|
9346
|
+
* @example Turn on the first English subtitle
|
|
9347
|
+
* The first English track becomes the primary subtitle, but only in the
|
|
9348
|
+
* file the plugin looked at. Embedded tracks are chosen by number, so
|
|
9349
|
+
* without `ifCurrent` a select after another file opened would pick that
|
|
9350
|
+
* file's track with the same number; with `ifCurrent: state` it fails with
|
|
9351
|
+
* `stale` instead. Without `ifCurrent`, `select` reads the newest state
|
|
9352
|
+
* itself and almost never fails with `stale`.
|
|
9353
|
+
* ```ts
|
|
9354
|
+
* const state = await plugin.subtitles.refresh();
|
|
9355
|
+
* const english = state.tracks.find((track) => track.language === "en");
|
|
9356
|
+
* if (english)
|
|
9357
|
+
* await plugin.subtitles.select(english, { ifCurrent: state }).catch((error: PluginError) => {
|
|
9358
|
+
* if (error.code !== "stale") throw error; // another file or selection now
|
|
9359
|
+
* });
|
|
9360
|
+
* ```
|
|
8863
9361
|
*/
|
|
8864
9362
|
select(track: SubtitleTrack | null, options?: SubtitleConditional & {
|
|
8865
9363
|
role?: "primary" | "secondary";
|
|
@@ -8876,6 +9374,25 @@ interface Subtitles {
|
|
|
8876
9374
|
* Limits: SRT ≤ 2 MiB and ≤ 10,000 cues; ASS ≤ 2 MiB with `[Script Info]`
|
|
8877
9375
|
* and `[Events]` and 1–20,000 `Dialogue:` lines. Convert other formats
|
|
8878
9376
|
* (VTT, SAMI) to SRT first.
|
|
9377
|
+
* @capability subtitles.control and subtitles.read
|
|
9378
|
+
* @throws invalid (rejects) A bad `format` or `title`, or a file over the limits.
|
|
9379
|
+
* @throws stale (rejects) With `ifCurrent`: the source changed since that state.
|
|
9380
|
+
* @throws unavailable (rejects) Nothing loaded, or the source is still loading.
|
|
9381
|
+
* @throws aborted (rejects) `signal` aborted before the commit (an `AbortError`); the upload is discarded.
|
|
9382
|
+
* @throws busy (rejects) Too many subtitle operations at once.
|
|
9383
|
+
* @throws capability (rejects) The manifest does not declare `subtitles.control` and `subtitles.read`.
|
|
9384
|
+
* @example Load a downloaded SRT and select it
|
|
9385
|
+
* A subtitle file the plugin downloaded becomes a track of the current file
|
|
9386
|
+
* and is selected. The plugin passes the file's bytes; the player parses
|
|
9387
|
+
* them, adds an `external` track for this source only and resolves with
|
|
9388
|
+
* it once the upload is committed.
|
|
9389
|
+
* ```ts
|
|
9390
|
+
* const response = await fetch(url);
|
|
9391
|
+
* if (!response.ok) throw new Error(`subtitles: HTTP ${response.status}`);
|
|
9392
|
+
* const bytes = new Uint8Array(await response.arrayBuffer());
|
|
9393
|
+
* const track = await plugin.subtitles.load(bytes, { title: "Downloaded" }); // selected
|
|
9394
|
+
* await plugin.osd.show("Loaded {}", [track.title]);
|
|
9395
|
+
* ```
|
|
8879
9396
|
*/
|
|
8880
9397
|
load(bytes: Uint8Array, options?: SubtitleConditional & {
|
|
8881
9398
|
format?: "srt" | "ass";
|
|
@@ -8883,10 +9400,40 @@ interface Subtitles {
|
|
|
8883
9400
|
select?: "primary" | "secondary" | "none";
|
|
8884
9401
|
signal?: AbortSignal;
|
|
8885
9402
|
}): Promise<SubtitleTrack>;
|
|
8886
|
-
/**
|
|
9403
|
+
/**
|
|
9404
|
+
* Remove a track this plugin loaded.
|
|
9405
|
+
* @capability subtitles.control and subtitles.read
|
|
9406
|
+
* @throws invalid (rejects) The track was not loaded by this plugin.
|
|
9407
|
+
* @throws stale (rejects) With `ifCurrent`: the source changed since that state.
|
|
9408
|
+
* @throws unavailable (rejects) Nothing is loaded, or the source is still loading.
|
|
9409
|
+
* @throws busy (rejects) Too many subtitle operations at once.
|
|
9410
|
+
* @throws capability (rejects) The manifest does not declare `subtitles.control` and `subtitles.read`.
|
|
9411
|
+
* @example Remove the subtitle the plugin loaded
|
|
9412
|
+
* The plugin's downloaded subtitle disappears from the track list. A plugin
|
|
9413
|
+
* can remove only tracks it loaded itself; a new source removes them
|
|
9414
|
+
* anyway.
|
|
9415
|
+
* ```ts
|
|
9416
|
+
* await plugin.subtitles.remove(loaded);
|
|
9417
|
+
* ```
|
|
9418
|
+
*/
|
|
8887
9419
|
remove(track: SubtitleTrack, options?: SubtitleConditional): Promise<SubtitlesState>;
|
|
8888
9420
|
/**
|
|
8889
9421
|
* 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.
|
|
9422
|
+
* @capability subtitles.control and subtitles.read
|
|
9423
|
+
* @throws invalid (rejects) A field out of range.
|
|
9424
|
+
* @throws unsupported (rejects) A font that is not in `state.fonts`, or a primary subtitle that is a bitmap track.
|
|
9425
|
+
* @throws stale (rejects) With `ifCurrent`: the subtitle state changed since that state.
|
|
9426
|
+
* @throws unavailable (rejects) Nothing is loaded, or the source is still loading.
|
|
9427
|
+
* @throws busy (rejects) Too many subtitle operations at once.
|
|
9428
|
+
* @throws capability (rejects) The manifest does not declare `subtitles.control` and `subtitles.read`.
|
|
9429
|
+
* @example Take the plugin's style off again
|
|
9430
|
+
* The plugin removes its style layer, so the subtitles go back to the
|
|
9431
|
+
* player's default (or to another plugin's layer). `null` removes only this
|
|
9432
|
+
* plugin's layer; the effective layer is the one whose manifest id sorts
|
|
9433
|
+
* last among the plugins that still have one.
|
|
9434
|
+
* ```ts
|
|
9435
|
+
* await plugin.subtitles.setStyle(null);
|
|
9436
|
+
* ```
|
|
8890
9437
|
*/
|
|
8891
9438
|
setStyle(style: SubtitleStyle | null, options?: SubtitleConditional): Promise<SubtitlesState>;
|
|
8892
9439
|
}
|
|
@@ -8937,6 +9484,14 @@ interface CaptureOptions extends Conditional {
|
|
|
8937
9484
|
|
|
8938
9485
|
A frozen, immutable input for exports. Dispose to release it.
|
|
8939
9486
|
|
|
9487
|
+
A frozen input, from MediaCapture.capture: the source, the time
|
|
9488
|
+
and (processed) the effects and subtitle as they were. Exports read it in
|
|
9489
|
+
the player's offline worker, so one snapshot can be exported several
|
|
9490
|
+
times and playback is unaffected. Disposing it releases the player's
|
|
9491
|
+
copy; an export using it after that fails with `released`.
|
|
9492
|
+
|
|
9493
|
+
- @capability media.read and media.control
|
|
9494
|
+
|
|
8940
9495
|
```ts
|
|
8941
9496
|
interface Snapshot extends Disposable {
|
|
8942
9497
|
/** Without or with effects. */
|
|
@@ -8958,11 +9513,45 @@ interface Snapshot extends Disposable {
|
|
|
8958
9513
|
} | null;
|
|
8959
9514
|
/** The frozen processing revision (processed snapshots). */
|
|
8960
9515
|
readonly processingRevision: number | null;
|
|
8961
|
-
/**
|
|
9516
|
+
/**
|
|
9517
|
+
* Start exporting; the job runs in the host's offline worker. One snapshot can be exported several times.
|
|
9518
|
+
* @capability media.read and media.control
|
|
9519
|
+
* @throws released (rejects) The snapshot was already released.
|
|
9520
|
+
* @throws invalid (rejects) Bad options (`quality` outside 1–100, a relative destination, a clip `end` not after the snapshot time or past the duration).
|
|
9521
|
+
* @throws busy (rejects) This plugin runs 2 exports (or the player 8).
|
|
9522
|
+
* @throws unavailable (rejects) No FFmpeg, or the export worker could not start.
|
|
9523
|
+
* @throws capability (rejects) The manifest does not declare `media.read` and `media.control`.
|
|
9524
|
+
* @example Export a 30-second clip
|
|
9525
|
+
* A clip from the snapshot time to 30 seconds later is written as MP4.
|
|
9526
|
+
* `export` resolves once the job is accepted; the job then runs in the
|
|
9527
|
+
* player's offline worker, and `done` resolves with its terminal state,
|
|
9528
|
+
* whose `outcome` has the destination or the error.
|
|
9529
|
+
* ```ts
|
|
9530
|
+
* const job = await snapshot.export({ format: "mp4", destination: "/tmp/clip.mp4", end: snapshot.time + 30 });
|
|
9531
|
+
* const end = await job.done;
|
|
9532
|
+
* if (end.status !== "completed") await plugin.osd.show("Export {}", [end.status]);
|
|
9533
|
+
* ```
|
|
9534
|
+
*/
|
|
8962
9535
|
export(options: ExportOptions): Promise<ExportJob>;
|
|
8963
9536
|
}
|
|
8964
9537
|
```
|
|
8965
9538
|
|
|
9539
|
+
Example: Export one frame as PNG and as JPEG
|
|
9540
|
+
|
|
9541
|
+
The same frozen frame is written twice, lossless and compressed. Each
|
|
9542
|
+
`export` starts a job of its own; the snapshot is released after both
|
|
9543
|
+
are done.
|
|
9544
|
+
```ts
|
|
9545
|
+
const snapshot = await plugin.media.capture();
|
|
9546
|
+
try {
|
|
9547
|
+
const png = await snapshot.export({ format: "png", destination: "/tmp/frame.png" });
|
|
9548
|
+
const jpeg = await snapshot.export({ format: "jpeg", destination: "/tmp/frame.jpg", quality: 85 });
|
|
9549
|
+
await Promise.all([png.done, jpeg.done]); // terminal states, not errors
|
|
9550
|
+
} finally {
|
|
9551
|
+
await snapshot.dispose();
|
|
9552
|
+
}
|
|
9553
|
+
```
|
|
9554
|
+
|
|
8966
9555
|
### ExportOptions
|
|
8967
9556
|
|
|
8968
9557
|
One frame as PNG, JPEG (`quality` 1–100, default 90) or BMP (24-bit), or
|
|
@@ -9015,31 +9604,121 @@ type JobState = Omit<MediaJob, "jobId">;
|
|
|
9015
9604
|
|
|
9016
9605
|
An export, from Snapshot.export. Disposing an unfinished job cancels it.
|
|
9017
9606
|
|
|
9607
|
+
One export, from Snapshot.export. `state` is the SDK's last copy
|
|
9608
|
+
of the job's state: the SDK polls the player about every 350 ms until the
|
|
9609
|
+
job ends, and `refresh` reads it at once. `done` resolves
|
|
9610
|
+
once the job is terminal (`completed`, `failed` or `cancelled`) and never
|
|
9611
|
+
rejects for a failed export: the failure is in `outcome`. Disposing an
|
|
9612
|
+
unfinished job cancels it.
|
|
9613
|
+
|
|
9614
|
+
- @capability media.read and media.control
|
|
9615
|
+
|
|
9018
9616
|
```ts
|
|
9019
9617
|
interface ExportJob extends Disposable {
|
|
9020
9618
|
/** Latest state. */
|
|
9021
9619
|
readonly state: JobState;
|
|
9022
|
-
/**
|
|
9620
|
+
/**
|
|
9621
|
+
* The state changed.
|
|
9622
|
+
* @capability media.read
|
|
9623
|
+
* @example Log each state of an export
|
|
9624
|
+
* The plugin's log records each status the job goes through (`queued`,
|
|
9625
|
+
* `running`, `cancelling` after a cancel, then a terminal one). The listener runs when the SDK learns a
|
|
9626
|
+
* new state from its polling.
|
|
9627
|
+
* ```ts
|
|
9628
|
+
* job.onChange((state) => console.log("export", state.status));
|
|
9629
|
+
* ```
|
|
9630
|
+
*/
|
|
9023
9631
|
onChange(listener: (state: JobState) => void): Disposable;
|
|
9024
|
-
/**
|
|
9632
|
+
/**
|
|
9633
|
+
* Resolves with the terminal state (completed, failed or cancelled).
|
|
9634
|
+
* @throws disposed (rejects) The job handle was disposed while waiting.
|
|
9635
|
+
*/
|
|
9025
9636
|
readonly done: Promise<JobState>;
|
|
9026
|
-
/**
|
|
9637
|
+
/**
|
|
9638
|
+
* Ask to cancel: `requested`, `tooLate` (already publishing) or `terminal` (already over).
|
|
9639
|
+
* @capability media.control
|
|
9640
|
+
* @throws rejected (rejects) The player did not accept the request.
|
|
9641
|
+
* @throws capability (rejects) The manifest does not declare `media.control`.
|
|
9642
|
+
* @example Cancel an export the user no longer wants
|
|
9643
|
+
* The running export stops, unless it is already writing its file.
|
|
9644
|
+
* `cancel` resolves `requested` when the worker will stop (the job ends
|
|
9645
|
+
* `cancelled`), `tooLate` when it is already publishing the file, and
|
|
9646
|
+
* `terminal` when the job had already ended.
|
|
9647
|
+
* ```ts
|
|
9648
|
+
* const answer = await job.cancel();
|
|
9649
|
+
* if (answer === "tooLate") await plugin.osd.show("Too late: the file is being written");
|
|
9650
|
+
* ```
|
|
9651
|
+
*/
|
|
9027
9652
|
cancel(): Promise<"requested" | "tooLate" | "terminal">;
|
|
9028
|
-
/**
|
|
9653
|
+
/**
|
|
9654
|
+
* Read the state from the player now.
|
|
9655
|
+
* @capability media.read
|
|
9656
|
+
* @throws capability (rejects) The manifest does not declare `media.read`.
|
|
9657
|
+
* @example Read a job's state now
|
|
9658
|
+
* The plugin reads the newest state of an export without waiting for an
|
|
9659
|
+
* event. `refresh` asks the player and resolves with the state, which
|
|
9660
|
+
* `state` then holds.
|
|
9661
|
+
* ```ts
|
|
9662
|
+
* const state = await job.refresh();
|
|
9663
|
+
* console.log(state.status);
|
|
9664
|
+
* ```
|
|
9665
|
+
*/
|
|
9029
9666
|
refresh(): Promise<JobState>;
|
|
9030
9667
|
}
|
|
9031
9668
|
```
|
|
9032
9669
|
|
|
9670
|
+
Example: Show export progress and cancel on request
|
|
9671
|
+
|
|
9672
|
+
The OSD shows the job's status while it runs, and a Cancel command stops
|
|
9673
|
+
it. `onChange` follows the state (`queued`, `running`, `cancelling`,
|
|
9674
|
+
then a terminal one); `cancel` asks the worker to stop. After
|
|
9675
|
+
`requested` the job usually ends `cancelled`, or `failed` when the
|
|
9676
|
+
cleanup failed.
|
|
9677
|
+
```ts
|
|
9678
|
+
job.onChange((state) => void plugin.osd.show("Export: {}", [state.status]));
|
|
9679
|
+
plugin.commands.add({ id: "cancel-export", title: "Cancel export", run: () => job.cancel() });
|
|
9680
|
+
await job.done; // completed, failed or cancelled
|
|
9681
|
+
```
|
|
9682
|
+
|
|
9033
9683
|
### MediaCapture
|
|
9034
9684
|
|
|
9035
9685
|
`plugin.media`: snapshots and exports. Needs `media.read` and
|
|
9036
9686
|
`media.control` (processed snapshots also `playback.read` and
|
|
9037
9687
|
`subtitles.read`). Exports decode offline and do not disturb playback.
|
|
9038
9688
|
|
|
9689
|
+
Exports run in the player's offline worker from a frozen
|
|
9690
|
+
Snapshot: a still frame or a clip, raw or with this plugin's
|
|
9691
|
+
effects (and the primary subtitle). Playback is not disturbed. A plugin
|
|
9692
|
+
holds at most 4 snapshots at a time (16 in the player); release them
|
|
9693
|
+
when done.
|
|
9694
|
+
|
|
9695
|
+
- @capability media.read and media.control (processed snapshots also `playback.read` and `subtitles.read`)
|
|
9696
|
+
|
|
9039
9697
|
```ts
|
|
9040
9698
|
interface MediaCapture {
|
|
9041
9699
|
/**
|
|
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
|
|
9700
|
+
* 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.
|
|
9701
|
+
* @capability media.read and media.control
|
|
9702
|
+
* @throws stale (rejects) The source changed since `ifCurrent`.
|
|
9703
|
+
* @throws noSource (rejects) Nothing is playing.
|
|
9704
|
+
* @throws invalid (rejects) `at` at or after the end of the source.
|
|
9705
|
+
* @throws busy (rejects) This plugin holds 4 snapshots (or the player 16); release one.
|
|
9706
|
+
* @throws unavailable (rejects) A processed snapshot while the picture is still being prepared.
|
|
9707
|
+
* @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.
|
|
9708
|
+
* @throws capability (rejects) The manifest does not declare `media.read` and `media.control`.
|
|
9709
|
+
* @example Freeze the frame at the 10-second mark
|
|
9710
|
+
* The plugin freezes the frame at 0:10 of the open file, raw, to export it
|
|
9711
|
+
* later. `capture` resolves with a `Snapshot` once the player has frozen
|
|
9712
|
+
* that input; the snapshot stays valid when playback moves on or the file
|
|
9713
|
+
* changes, until it is released.
|
|
9714
|
+
* ```ts
|
|
9715
|
+
* const snapshot = await plugin.media.capture({ at: 10 }); // seconds
|
|
9716
|
+
* try {
|
|
9717
|
+
* // … export it …
|
|
9718
|
+
* } finally {
|
|
9719
|
+
* await snapshot.dispose(); // frees one of the plugin's 4 snapshot slots
|
|
9720
|
+
* }
|
|
9721
|
+
* ```
|
|
9043
9722
|
*/
|
|
9044
9723
|
capture(options?: CaptureOptions): Promise<Snapshot>;
|
|
9045
9724
|
}
|
|
@@ -9192,25 +9871,84 @@ interface ResolvedStream {
|
|
|
9192
9871
|
|
|
9193
9872
|
A registered resolver. Disposing it unregisters it and aborts resolutions in flight.
|
|
9194
9873
|
|
|
9874
|
+
A registered resolver, from Resolvers.add. Disposing it, its
|
|
9875
|
+
scope or stopping the plugin unregisters it and aborts resolutions in
|
|
9876
|
+
flight; the extensions or hosts are free again for other plugins.
|
|
9877
|
+
|
|
9878
|
+
- @capability source.resolve
|
|
9879
|
+
|
|
9195
9880
|
```ts
|
|
9196
9881
|
interface Resolver extends Disposable {
|
|
9197
9882
|
/**
|
|
9198
9883
|
* 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`).
|
|
9884
|
+
* @throws conflict (rejects) Another resolver has the extension or the scheme + host, or this plugin already registered this id.
|
|
9885
|
+
* @throws busy (rejects) The player already holds 16 resolvers.
|
|
9886
|
+
* @throws invalid (rejects) A malformed declaration.
|
|
9887
|
+
* @throws capability (rejects) The manifest does not declare `source.resolve`.
|
|
9199
9888
|
*/
|
|
9200
9889
|
readonly ready: Promise<void>;
|
|
9201
9890
|
}
|
|
9202
9891
|
```
|
|
9203
9892
|
|
|
9893
|
+
Example: Report when another plugin already handles the format
|
|
9894
|
+
|
|
9895
|
+
The plugin tells the user when its `.ch8` resolver could not register
|
|
9896
|
+
because another plugin holds that extension. `ready` rejects with
|
|
9897
|
+
`conflict`; for another plugin's claim that does not fail activation, so
|
|
9898
|
+
the plugin keeps running without the resolver.
|
|
9899
|
+
```ts
|
|
9900
|
+
const resolver = plugin.resolvers.add({
|
|
9901
|
+
id: "chip8",
|
|
9902
|
+
extensions: ["ch8"],
|
|
9903
|
+
async resolve() {
|
|
9904
|
+
throw plugin.i18n.error("Not ready yet.");
|
|
9905
|
+
},
|
|
9906
|
+
});
|
|
9907
|
+
// Rejects with `conflict` when the extension is taken.
|
|
9908
|
+
resolver.ready.catch((error: PluginError) => {
|
|
9909
|
+
if (error.code === "conflict") void plugin.osd.show("Another plugin plays .ch8 files");
|
|
9910
|
+
});
|
|
9911
|
+
```
|
|
9912
|
+
|
|
9204
9913
|
### Resolvers
|
|
9205
9914
|
|
|
9206
9915
|
`plugin.resolvers`: turn web page URLs into playable streams, or local
|
|
9207
9916
|
files into streams this plugin generates. Needs `source.resolve` (file
|
|
9208
9917
|
resolvers also `producer.av`).
|
|
9209
9918
|
|
|
9919
|
+
A resolver turns something the player cannot play by itself into
|
|
9920
|
+
something it can: a web page address into stream URLs
|
|
9921
|
+
(WebResolverOptions), or a local file of a format the plugin
|
|
9922
|
+
understands into a stream the plugin generates
|
|
9923
|
+
(FileResolverOptions). The player asks the resolver whenever
|
|
9924
|
+
anyone opens a matching address or file (the Open dialog, the playlist,
|
|
9925
|
+
another plugin), so the plugin does not intercept opens itself.
|
|
9926
|
+
|
|
9927
|
+
- @capability source.resolve (file resolvers also `producer.av`)
|
|
9928
|
+
|
|
9210
9929
|
```ts
|
|
9211
9930
|
interface Resolvers {
|
|
9212
9931
|
/**
|
|
9213
9932
|
* 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.
|
|
9933
|
+
* @capability source.resolve
|
|
9934
|
+
* @example Play a site's pages through the plugin
|
|
9935
|
+
* Opening a `https://video.example.com/…` address plays the page's video.
|
|
9936
|
+
* `add` returns at once and registers in the background (`ready` reports
|
|
9937
|
+
* the answer); from then on the player calls `resolve` for every matching
|
|
9938
|
+
* open, and the error a rejected `resolve` carries is what the user sees.
|
|
9939
|
+
* ```ts
|
|
9940
|
+
* plugin.resolvers.add({
|
|
9941
|
+
* id: "example",
|
|
9942
|
+
* schemes: ["https"],
|
|
9943
|
+
* hosts: ["video.example.com"], // exact host names
|
|
9944
|
+
* async resolve(url, signal) {
|
|
9945
|
+
* const response = await fetch(url, { signal });
|
|
9946
|
+
* if (!response.ok) throw new Error(`the page answered ${response.status}`);
|
|
9947
|
+
* const page = (await response.json()) as { title: string; stream: string };
|
|
9948
|
+
* return { kind: "media", originalUrl: url, title: page.title, primary: { url: page.stream } };
|
|
9949
|
+
* },
|
|
9950
|
+
* });
|
|
9951
|
+
* ```
|
|
9214
9952
|
*/
|
|
9215
9953
|
add(options: ResolverOptions): Resolver;
|
|
9216
9954
|
/**
|
|
@@ -9363,6 +10101,15 @@ interface InstallOptions {
|
|
|
9363
10101
|
|
|
9364
10102
|
A validated package directory, from Packages.validate.
|
|
9365
10103
|
|
|
10104
|
+
A package the player has checked, from Packages.validate or
|
|
10105
|
+
Packages.validateArchive: its manifest, digest and the checks
|
|
10106
|
+
it passed. The player copies the whole package when it checks it and
|
|
10107
|
+
installs that copy, so later changes to the directory do not matter.
|
|
10108
|
+
Installing starts a PackageChange; the player keeps only the 32
|
|
10109
|
+
most recent candidates.
|
|
10110
|
+
|
|
10111
|
+
- @capability plugins.manage
|
|
10112
|
+
|
|
9366
10113
|
```ts
|
|
9367
10114
|
interface PackageCandidate {
|
|
9368
10115
|
/** The package directory. */
|
|
@@ -9375,11 +10122,38 @@ interface PackageCandidate {
|
|
|
9375
10122
|
readonly checks: string[];
|
|
9376
10123
|
/**
|
|
9377
10124
|
* 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.
|
|
10125
|
+
* @capability plugins.manage
|
|
10126
|
+
* @throws conflict (rejects) The plugin is installed from another source and `replace` is not set.
|
|
10127
|
+
* @throws unsupported (rejects) A built-in plugin's id.
|
|
10128
|
+
* @throws reserved (rejects) A reserved id (`vestra`, `vestra.*`).
|
|
10129
|
+
* @throws busy (rejects) The plugin library is being cleaned up, or the player already keeps 64 change records.
|
|
10130
|
+
* @throws revisionConflict (rejects) The installed revision of this plugin changed meanwhile; call `install` again (no new validation needed).
|
|
10131
|
+
* @throws stale (rejects) The candidate is older than the 32 the player keeps.
|
|
10132
|
+
* @throws capability (rejects) The manifest does not declare `plugins.manage`.
|
|
10133
|
+
* @example Replace a plugin installed from another source
|
|
10134
|
+
* The new package replaces the installed plugin even though it came from
|
|
10135
|
+
* another source. Without `replace` the player refuses with `conflict`;
|
|
10136
|
+
* the same source simply upgrades and keeps the plugin's data.
|
|
10137
|
+
* ```ts
|
|
10138
|
+
* const change = await candidate.install({ replace: true });
|
|
10139
|
+
* await change.done; // "committed" once the new plugin runs
|
|
10140
|
+
* ```
|
|
9378
10141
|
*/
|
|
9379
10142
|
install(options?: InstallOptions): Promise<PackageChange>;
|
|
9380
10143
|
}
|
|
9381
10144
|
```
|
|
9382
10145
|
|
|
10146
|
+
Example: Show what a package asks for before installing it
|
|
10147
|
+
|
|
10148
|
+
Before installing, the plugin shows the package's name and the
|
|
10149
|
+
capabilities it declares. The candidate's `manifest` is the manifest
|
|
10150
|
+
as the player read it.
|
|
10151
|
+
```ts
|
|
10152
|
+
const candidate = await plugin.packages.validate(directory);
|
|
10153
|
+
const { id, version, capabilities } = candidate.manifest;
|
|
10154
|
+
await plugin.osd.show("{} {}: {}", [id, version, capabilities.join(", ")]);
|
|
10155
|
+
```
|
|
10156
|
+
|
|
9383
10157
|
### PackageChangeStatus
|
|
9384
10158
|
|
|
9385
10159
|
The progress of an install or uninstall.
|
|
@@ -9393,21 +10167,71 @@ type PackageChangeStatus = "preparing" | "activating" | "restoring" | "committed
|
|
|
9393
10167
|
An install or uninstall in progress. The SDK never submits a
|
|
9394
10168
|
change twice: after a lost reply it only queries the same request.
|
|
9395
10169
|
|
|
10170
|
+
An install or uninstall the player carries out in steps (`preparing`,
|
|
10171
|
+
`activating`, possibly `restoring`) until a terminal status. The SDK
|
|
10172
|
+
asks the player for the status about every 400 ms until then; `done`
|
|
10173
|
+
resolves with the terminal status and does not reject for a failed
|
|
10174
|
+
change: `message` says why.
|
|
10175
|
+
|
|
10176
|
+
- @capability plugins.manage
|
|
10177
|
+
|
|
9396
10178
|
```ts
|
|
9397
10179
|
interface PackageChange {
|
|
9398
10180
|
/** Current status. */
|
|
9399
10181
|
readonly status: PackageChangeStatus;
|
|
9400
10182
|
/** Details for failures, `null` otherwise. */
|
|
9401
10183
|
readonly message: string | null;
|
|
9402
|
-
/**
|
|
10184
|
+
/**
|
|
10185
|
+
* The status changed.
|
|
10186
|
+
* @capability plugins.manage
|
|
10187
|
+
* @example Close a progress window when the change ends
|
|
10188
|
+
* The plugin's progress window closes once the install is over, whatever
|
|
10189
|
+
* the outcome. The listener receives each new status; the terminal ones
|
|
10190
|
+
* are `committed`, `cancelled`, `failed`, `failedDegraded` and `tooLate`
|
|
10191
|
+
* (`tooLate` only answers a `cancel` of a change that had committed, and
|
|
10192
|
+
* `committed` may follow it).
|
|
10193
|
+
* ```ts
|
|
10194
|
+
* change.onChange((status) => {
|
|
10195
|
+
* if (["committed", "cancelled", "failed", "failedDegraded", "tooLate"].includes(status))
|
|
10196
|
+
* void progress.close();
|
|
10197
|
+
* });
|
|
10198
|
+
* ```
|
|
10199
|
+
*/
|
|
9403
10200
|
onChange(listener: (status: PackageChangeStatus) => void): Disposable;
|
|
9404
|
-
/**
|
|
10201
|
+
/**
|
|
10202
|
+
* Resolves with the final status.
|
|
10203
|
+
* @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.
|
|
10204
|
+
*/
|
|
9405
10205
|
readonly done: Promise<PackageChangeStatus>;
|
|
9406
|
-
/**
|
|
10206
|
+
/**
|
|
10207
|
+
* Ask to cancel (it may be `tooLate`).
|
|
10208
|
+
* @capability plugins.manage
|
|
10209
|
+
* @throws capability (rejects) The manifest does not declare `plugins.manage`.
|
|
10210
|
+
* @example Cancel an install the user no longer wants
|
|
10211
|
+
* The install stops if it has not gone too far. `cancel` asks the player;
|
|
10212
|
+
* the change then ends `cancelled`. When the change had already committed
|
|
10213
|
+
* the player answers `tooLate` and `done` still resolves `committed`; a
|
|
10214
|
+
* change that is restoring cannot be cancelled and ends `failed` or
|
|
10215
|
+
* `failedDegraded`.
|
|
10216
|
+
* ```ts
|
|
10217
|
+
* await change.cancel();
|
|
10218
|
+
* console.log(await change.done); // "cancelled"; "committed" when it was too late
|
|
10219
|
+
* ```
|
|
10220
|
+
*/
|
|
9407
10221
|
cancel(): Promise<void>;
|
|
9408
10222
|
}
|
|
9409
10223
|
```
|
|
9410
10224
|
|
|
10225
|
+
Example: Report each step of an install
|
|
10226
|
+
|
|
10227
|
+
The OSD shows each step of the install and the end result. `onChange`
|
|
10228
|
+
runs for every new status; `done` resolves when the change is over.
|
|
10229
|
+
```ts
|
|
10230
|
+
change.onChange((status) => void plugin.osd.show("Install: {}", [status]));
|
|
10231
|
+
const end = await change.done;
|
|
10232
|
+
if (end !== "committed") await plugin.osd.show("Not installed: {}", [change.message ?? end]);
|
|
10233
|
+
```
|
|
10234
|
+
|
|
9411
10235
|
### PluginInfo
|
|
9412
10236
|
|
|
9413
10237
|
One plugin as the player knows it (`packages.list`).
|
|
@@ -9489,6 +10313,14 @@ interface PluginWarning {
|
|
|
9489
10313
|
`plugin.packages`: the plugins the player knows. `list` and `onChange`
|
|
9490
10314
|
need no capability; everything else needs `plugins.manage`.
|
|
9491
10315
|
|
|
10316
|
+
The plugins the player knows: installed, built-in and this run's
|
|
10317
|
+
development loads. Reading the list needs no capability; installing,
|
|
10318
|
+
enabling, reloading and uninstalling need `plugins.manage` and act on
|
|
10319
|
+
other plugins, never on the calling one. Built-in plugins update with
|
|
10320
|
+
the player and cannot be installed, disabled or uninstalled.
|
|
10321
|
+
|
|
10322
|
+
- @capability None for `list` and `onChange`; plugins.manage for the rest.
|
|
10323
|
+
|
|
9492
10324
|
```ts
|
|
9493
10325
|
interface Packages {
|
|
9494
10326
|
/**
|
|
@@ -9499,18 +10331,93 @@ interface Packages {
|
|
|
9499
10331
|
* The player keeps the 32 most recent candidates of all plugins; an
|
|
9500
10332
|
* older one's `install` fails with `stale` (validate again). Before
|
|
9501
10333
|
* player 0.4.2 the 33rd validation of a run failed with `busy` instead.
|
|
10334
|
+
* @capability plugins.manage
|
|
10335
|
+
* @throws host (rejects) Not a package directory, or a manifest that breaks the rules (another `apiVersion` included); `message` says why.
|
|
10336
|
+
* @throws invalid (rejects) A bad request (a path that is not a string).
|
|
10337
|
+
* @throws javascript (rejects) The entry module does not parse.
|
|
10338
|
+
* @throws slint (rejects) A Slint file does not compile.
|
|
10339
|
+
* @throws reserved (rejects) A reserved id (`vestra`, `vestra.*`).
|
|
10340
|
+
* @throws versionMismatch (rejects) The package's `minPlayerVersion` is newer than the player.
|
|
10341
|
+
* @throws disabled (rejects) The calling plugin is stopping.
|
|
10342
|
+
* @throws capability (rejects) The manifest does not declare `plugins.manage`.
|
|
10343
|
+
* @example Check a package directory before installing it
|
|
10344
|
+
* The plugin checks a folder it unpacked and installs it if the player
|
|
10345
|
+
* accepts it. `validate` runs the player's checks and resolves with a
|
|
10346
|
+
* candidate; a package that fails them rejects with the reason.
|
|
10347
|
+
* ```ts
|
|
10348
|
+
* try {
|
|
10349
|
+
* // Rejects with the reason when the package fails the checks.
|
|
10350
|
+
* const candidate = await plugin.packages.validate(directory);
|
|
10351
|
+
* await (await candidate.install()).done;
|
|
10352
|
+
* } catch (error) {
|
|
10353
|
+
* await plugin.osd.show("Not a valid plugin: {}", [(error as Error).message]);
|
|
10354
|
+
* }
|
|
10355
|
+
* ```
|
|
9502
10356
|
*/
|
|
9503
10357
|
validate(directory: string): Promise<PackageCandidate>;
|
|
9504
10358
|
/**
|
|
9505
10359
|
* 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.
|
|
10360
|
+
* @capability plugins.manage
|
|
10361
|
+
* @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).
|
|
10362
|
+
* @throws host (rejects) The unpacked package or its manifest breaks the rules; `message` says why.
|
|
10363
|
+
* @throws javascript (rejects) The entry module does not parse.
|
|
10364
|
+
* @throws slint (rejects) A Slint file does not compile.
|
|
10365
|
+
* @throws reserved (rejects) A reserved id.
|
|
10366
|
+
* @throws versionMismatch (rejects) The package's `minPlayerVersion` is newer than the player.
|
|
10367
|
+
* @throws disabled (rejects) The calling plugin is stopping.
|
|
10368
|
+
* @throws capability (rejects) The manifest does not declare `plugins.manage`.
|
|
10369
|
+
* @example Install a downloaded package file
|
|
10370
|
+
* A `.vxp` the plugin downloaded is checked and installed. The player
|
|
10371
|
+
* unpacks and checks it like a directory; checking the file's sha256
|
|
10372
|
+
* first, when the store publishes one, is up to the plugin.
|
|
10373
|
+
* ```ts
|
|
10374
|
+
* const candidate = await plugin.packages.validateArchive(file); // a .vxp path
|
|
10375
|
+
* const change = await candidate.install();
|
|
10376
|
+
* await plugin.osd.show("Install: {}", [await change.done]);
|
|
10377
|
+
* ```
|
|
9506
10378
|
*/
|
|
9507
10379
|
validateArchive(file: string): Promise<PackageCandidate>;
|
|
9508
|
-
/**
|
|
10380
|
+
/**
|
|
10381
|
+
* Every plugin: running, installed, and this run's development loads. No capability.
|
|
10382
|
+
* @capability None.
|
|
10383
|
+
* @throws Error (rejects) Only when the player cannot be asked; no capability is needed.
|
|
10384
|
+
* @example Show which plugins are running
|
|
10385
|
+
* The plugin lists the running plugins by name. `list` asks the player
|
|
10386
|
+
* and resolves with every plugin it knows, with its state, origin and
|
|
10387
|
+
* display texts in the interface language.
|
|
10388
|
+
* ```ts
|
|
10389
|
+
* const plugins = await plugin.packages.list();
|
|
10390
|
+
* const running = plugins.filter((info) => info.state === "running");
|
|
10391
|
+
* console.log(running.map((info) => info.display?.name ?? info.id));
|
|
10392
|
+
* ```
|
|
10393
|
+
*/
|
|
9509
10394
|
list(): Promise<PluginInfo[]>;
|
|
9510
|
-
/**
|
|
10395
|
+
/**
|
|
10396
|
+
* The new list after any start, stop, failure, install or uninstall. No capability.
|
|
10397
|
+
* @capability None.
|
|
10398
|
+
* @example Refresh a plugin list after any change
|
|
10399
|
+
* A store window's list updates when a plugin starts, stops, fails, is
|
|
10400
|
+
* installed or uninstalled. The listener receives the new list each time.
|
|
10401
|
+
* ```ts
|
|
10402
|
+
* plugin.packages.onChange((plugins) => console.log(plugins.length, "plugins"));
|
|
10403
|
+
* ```
|
|
10404
|
+
*/
|
|
9511
10405
|
onChange(listener: (plugins: PluginInfo[]) => void): Disposable;
|
|
9512
10406
|
/**
|
|
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).
|
|
10407
|
+
* 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).
|
|
10408
|
+
* @capability plugins.manage
|
|
10409
|
+
* @throws invalid (rejects) Disabling the calling plugin itself.
|
|
10410
|
+
* @throws missing (rejects) An unknown id.
|
|
10411
|
+
* @throws busy (rejects) The plugin is being installed or reloaded.
|
|
10412
|
+
* @throws unsupported (rejects) A built-in plugin.
|
|
10413
|
+
* @throws capability (rejects) The manifest does not declare `plugins.manage`.
|
|
10414
|
+
* @example Turn a third-party plugin off
|
|
10415
|
+
* The plugin stops another plugin and keeps it disabled across launches
|
|
10416
|
+
* until it is enabled again. Built-in plugins cannot be disabled
|
|
10417
|
+
* (`unsupported`).
|
|
10418
|
+
* ```ts
|
|
10419
|
+
* await plugin.packages.setEnabled("someone.demo", false);
|
|
10420
|
+
* ```
|
|
9514
10421
|
*/
|
|
9515
10422
|
setEnabled(id: string, enabled: boolean): Promise<void>;
|
|
9516
10423
|
/**
|
|
@@ -9528,10 +10435,38 @@ interface Packages {
|
|
|
9528
10435
|
* `invalid` for oneself or a disabled plugin (enable it instead),
|
|
9529
10436
|
* `missing` unknown, `busy` while it is being installed or reloaded.
|
|
9530
10437
|
* @player 0.5.1; older players reject it as an unknown request (`unsupported`).
|
|
10438
|
+
* @capability plugins.manage
|
|
10439
|
+
* @throws invalid (rejects) The calling plugin itself, or a disabled plugin.
|
|
10440
|
+
* @throws missing (rejects) An unknown id.
|
|
10441
|
+
* @throws busy (rejects) The plugin is being installed or reloaded.
|
|
10442
|
+
* @throws unsupported (rejects) A player before 0.5.1.
|
|
10443
|
+
* @throws capability (rejects) The manifest does not declare `plugins.manage`.
|
|
10444
|
+
* @example Restart a plugin that misbehaves
|
|
10445
|
+
* The other plugin is stopped and started again from the same package and
|
|
10446
|
+
* data. `reload` resolves once the old process is stopped and the start is
|
|
10447
|
+
* scheduled; `onChange` then shows `starting` and `running` (or `failed`).
|
|
10448
|
+
* ```ts
|
|
10449
|
+
* await plugin.packages.reload("someone.demo");
|
|
10450
|
+
* ```
|
|
9531
10451
|
*/
|
|
9532
10452
|
reload(id: string): Promise<void>;
|
|
9533
10453
|
/**
|
|
9534
10454
|
* 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.
|
|
10455
|
+
* @capability plugins.manage
|
|
10456
|
+
* @throws unsupported (rejects) A built-in plugin or a development load.
|
|
10457
|
+
* @throws missing (rejects) An unknown id.
|
|
10458
|
+
* @throws invalid (rejects) The calling plugin itself.
|
|
10459
|
+
* @throws busy (rejects) The plugin library is being cleaned up, or that plugin is being installed or reloaded.
|
|
10460
|
+
* @throws revisionConflict (rejects) The installed revision of that plugin changed meanwhile; call `uninstall` again.
|
|
10461
|
+
* @throws capability (rejects) The manifest does not declare `plugins.manage`.
|
|
10462
|
+
* @example Uninstall a plugin together with its data
|
|
10463
|
+
* The other plugin is removed and its data directory deleted, so a later
|
|
10464
|
+
* reinstall starts fresh. Without `removeData` a reinstall from the same
|
|
10465
|
+
* source finds its data again.
|
|
10466
|
+
* ```ts
|
|
10467
|
+
* const change = await plugin.packages.uninstall("someone.demo", { removeData: true });
|
|
10468
|
+
* await change.done;
|
|
10469
|
+
* ```
|
|
9535
10470
|
*/
|
|
9536
10471
|
uninstall(id: string, options?: {
|
|
9537
10472
|
removeData?: boolean;
|
|
@@ -9606,9 +10541,31 @@ interface BindingInfo extends BindingRef {
|
|
|
9606
10541
|
`plugin.bindings`: list and override every command's bindings, including
|
|
9607
10542
|
the player's. Needs `bindings.manage`.
|
|
9608
10543
|
|
|
10544
|
+
The player's binding table: every command's default keys (from
|
|
10545
|
+
CommandOptions.bindings, the player's own as `host/<action>`)
|
|
10546
|
+
and the user's overrides on top, saved across launches. A key-settings
|
|
10547
|
+
plugin lists the table, writes overrides and resets them; the player
|
|
10548
|
+
applies the change at once to every running plugin.
|
|
10549
|
+
|
|
10550
|
+
- @capability bindings.manage
|
|
10551
|
+
|
|
9609
10552
|
```ts
|
|
9610
10553
|
interface Bindings {
|
|
9611
|
-
/**
|
|
10554
|
+
/**
|
|
10555
|
+
* Every declared command, and overrides whose command is gone.
|
|
10556
|
+
* @capability bindings.manage
|
|
10557
|
+
* @throws capability (rejects) The manifest does not declare `bindings.manage`.
|
|
10558
|
+
* @example Find the commands that use a key
|
|
10559
|
+
* A key-settings window shows which commands Ctrl+K is bound to now. `list`
|
|
10560
|
+
* resolves with every declared command and its current bindings (the
|
|
10561
|
+
* defaults with the user's overrides applied) and with the overrides
|
|
10562
|
+
* whose command is not running (`orphans`).
|
|
10563
|
+
* ```ts
|
|
10564
|
+
* const { commands } = await plugin.bindings.list();
|
|
10565
|
+
* const users = commands.filter((command) => command.current.includes("Ctrl+K"));
|
|
10566
|
+
* console.log(users.map((command) => `${command.plugin}/${command.command}`)); // "host" for the player
|
|
10567
|
+
* ```
|
|
10568
|
+
*/
|
|
9612
10569
|
list(): Promise<{
|
|
9613
10570
|
/** Every declared command (running plugins and the player). */
|
|
9614
10571
|
commands: BindingInfo[];
|
|
@@ -9622,11 +10579,47 @@ interface Bindings {
|
|
|
9622
10579
|
}>;
|
|
9623
10580
|
/**
|
|
9624
10581
|
* 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.
|
|
10582
|
+
* @capability bindings.manage
|
|
10583
|
+
* @throws invalid (rejects) A malformed binding, or more than 16.
|
|
10584
|
+
* @throws missing (rejects) An unknown command that has no override either.
|
|
10585
|
+
* @throws capability (rejects) The manifest does not declare `bindings.manage`.
|
|
10586
|
+
* @example Give the player's Mute command a second key
|
|
10587
|
+
* Mute now answers to M and to Ctrl+M. `set` replaces the command's
|
|
10588
|
+
* bindings with the list given (an override saved across launches), so it
|
|
10589
|
+
* repeats the default it keeps; `[]` would unbind it.
|
|
10590
|
+
* ```ts
|
|
10591
|
+
* await plugin.bindings.set({ plugin: "host", command: "mute" }, ["M", "Ctrl+M"]);
|
|
10592
|
+
* ```
|
|
9625
10593
|
*/
|
|
9626
10594
|
set(ref: BindingRef, inputs: string[]): Promise<void>;
|
|
9627
|
-
/**
|
|
10595
|
+
/**
|
|
10596
|
+
* Back to the defaults: one command, or every override when omitted.
|
|
10597
|
+
* @capability bindings.manage
|
|
10598
|
+
* @throws missing (rejects) A command that has no override and is not declared.
|
|
10599
|
+
* @throws capability (rejects) The manifest does not declare `bindings.manage`.
|
|
10600
|
+
* @example Restore every default key
|
|
10601
|
+
* A "Restore defaults" button removes all of the user's overrides, so
|
|
10602
|
+
* every command has its declared keys again. Without an argument `reset`
|
|
10603
|
+
* clears every override; with a reference only that command's.
|
|
10604
|
+
* ```ts
|
|
10605
|
+
* await plugin.bindings.reset(); // every override
|
|
10606
|
+
* ```
|
|
10607
|
+
*/
|
|
9628
10608
|
reset(ref?: BindingRef): Promise<void>;
|
|
9629
|
-
/**
|
|
10609
|
+
/**
|
|
10610
|
+
* Declarations or overrides changed (read `list()` again). At most every 200 ms.
|
|
10611
|
+
* @capability bindings.manage
|
|
10612
|
+
* @example Refresh a key list when bindings change
|
|
10613
|
+
* The plugin's key list stays current when the user changes a key in the
|
|
10614
|
+
* player or another plugin starts. The listener carries no data and runs
|
|
10615
|
+
* at most every 200 ms; the plugin reads the table again with `list`.
|
|
10616
|
+
* ```ts
|
|
10617
|
+
* plugin.bindings.onChange(async () => {
|
|
10618
|
+
* const { commands } = await plugin.bindings.list(); // read the new table
|
|
10619
|
+
* render(commands);
|
|
10620
|
+
* });
|
|
10621
|
+
* ```
|
|
10622
|
+
*/
|
|
9630
10623
|
onChange(listener: () => void): Disposable;
|
|
9631
10624
|
}
|
|
9632
10625
|
```
|
|
@@ -10717,6 +11710,13 @@ A tool window as the test host keeps it: the record of a
|
|
|
10717
11710
|
`plugin.windows.create(...)` window after its first `show()`. Get one with
|
|
10718
11711
|
TestHost.window.
|
|
10719
11712
|
|
|
11713
|
+
A record, not a window: no Slint file is compiled and nothing is drawn.
|
|
11714
|
+
`props` holds what the plugin wrote, by Slint name; `emit` fires a
|
|
11715
|
+
callback the way the user's input would; `userClose` closes it the way
|
|
11716
|
+
the title-bar button would.
|
|
11717
|
+
|
|
11718
|
+
- @capability None.
|
|
11719
|
+
|
|
10720
11720
|
```ts
|
|
10721
11721
|
interface FakeWindow {
|
|
10722
11722
|
/** The `.slint` file the window was created from (package-relative). */
|
|
@@ -10731,10 +11731,32 @@ interface FakeWindow {
|
|
|
10731
11731
|
callbacks: string[];
|
|
10732
11732
|
/**
|
|
10733
11733
|
* 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.
|
|
11734
|
+
* @capability None.
|
|
11735
|
+
* @example Type into a window's search field
|
|
11736
|
+
* The test types "cat" into the window's search box and checks the
|
|
11737
|
+
* plugin's results. `emit` delivers the Slint callback `search-changed`
|
|
11738
|
+
* with its argument to the plugin's `on` handler; `settle()` waits for the
|
|
11739
|
+
* properties the handler wrote.
|
|
11740
|
+
* ```ts
|
|
11741
|
+
* const search = host.window("search");
|
|
11742
|
+
* search.emit("searchChanged", "cat"); // camelCase or kebab-case
|
|
11743
|
+
* await host.settle();
|
|
11744
|
+
* assert.ok((search.props["results"] as unknown[]).length > 0);
|
|
11745
|
+
* ```
|
|
10734
11746
|
*/
|
|
10735
11747
|
emit(callback: string, ...args: Json[]): void;
|
|
10736
11748
|
/**
|
|
10737
11749
|
* 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()`).
|
|
11750
|
+
* @capability None.
|
|
11751
|
+
* @example Close a window the way the user does
|
|
11752
|
+
* The test closes the plugin's window with its title-bar button and checks
|
|
11753
|
+
* that the plugin stopped its timer. Unlike the plugin's own `close()`,
|
|
11754
|
+
* `userClose` runs the plugin's `onClose` listeners.
|
|
11755
|
+
* ```ts
|
|
11756
|
+
* host.window("clock").userClose();
|
|
11757
|
+
* await host.settle();
|
|
11758
|
+
* assert.equal(host.window("clock").visible, false);
|
|
11759
|
+
* ```
|
|
10738
11760
|
*/
|
|
10739
11761
|
userClose(): void;
|
|
10740
11762
|
}
|
|
@@ -10777,6 +11799,13 @@ A command the plugin registered with `plugin.commands.add`, as the test
|
|
|
10777
11799
|
host keeps it. Get one with TestHost.command (the plugin's own) or
|
|
10778
11800
|
TestHost.coreAction (the player's core actions).
|
|
10779
11801
|
|
|
11802
|
+
The host's record of one command: what the plugin declared, the state
|
|
11803
|
+
it last sent, and how the menu shows it in TestHost.locale.
|
|
11804
|
+
Read it again after `settle()` to see changes; `run` executes it as the
|
|
11805
|
+
menu would.
|
|
11806
|
+
|
|
11807
|
+
- @capability None.
|
|
11808
|
+
|
|
10780
11809
|
```ts
|
|
10781
11810
|
interface FakeCommand {
|
|
10782
11811
|
/** 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 +11838,15 @@ interface FakeCommand {
|
|
|
10809
11838
|
revision: number;
|
|
10810
11839
|
/**
|
|
10811
11840
|
* 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.
|
|
11841
|
+
* @capability None.
|
|
11842
|
+
* @throws command (rejects) A `HostError`: the command is disabled or busy (the message says which).
|
|
11843
|
+
* @example Check that a disabled item refuses to run
|
|
11844
|
+
* The test checks that Export cannot run while nothing is open. Like the
|
|
11845
|
+
* player, the host does not call `run` for a disabled or busy command;
|
|
11846
|
+
* the promise rejects with a `HostError`.
|
|
11847
|
+
* ```ts
|
|
11848
|
+
* await assert.rejects(host.command("export").run());
|
|
11849
|
+
* ```
|
|
10812
11850
|
*/
|
|
10813
11851
|
run(): Promise<void>;
|
|
10814
11852
|
}
|
|
@@ -10834,6 +11872,13 @@ assert.equal(host.command("toggle-loop").state.checked, true);
|
|
|
10834
11872
|
A command's bottom-bar button (`CommandOptions.button`) as the player shows
|
|
10835
11873
|
it: an entry of TestHost.barButtons.
|
|
10836
11874
|
|
|
11875
|
+
A bottom-bar button as the player would show it, derived from the
|
|
11876
|
+
command's declaration and state each time it is read: clickable while
|
|
11877
|
+
the command is enabled and not busy, accent-coloured while checked, with
|
|
11878
|
+
the command's title (and reason) as the tooltip.
|
|
11879
|
+
|
|
11880
|
+
- @capability None.
|
|
11881
|
+
|
|
10837
11882
|
```ts
|
|
10838
11883
|
interface FakeBarButton {
|
|
10839
11884
|
/** The command's local id. */
|
|
@@ -10852,17 +11897,45 @@ interface FakeBarButton {
|
|
|
10852
11897
|
readonly tooltip: string;
|
|
10853
11898
|
/**
|
|
10854
11899
|
* 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.
|
|
11900
|
+
* @capability None.
|
|
11901
|
+
* @throws command (rejects) A `HostError`: the command is disabled or busy.
|
|
11902
|
+
* @throws missing (rejects) A `HostError`: the command is gone.
|
|
11903
|
+
* @example Click the button and wait for its effect
|
|
11904
|
+
* The test clicks the plugin's button and checks the OSD. `click` runs the
|
|
11905
|
+
* command like its menu item and resolves once the host is idle.
|
|
11906
|
+
* ```ts
|
|
11907
|
+
* await host.barButtons[0]?.click();
|
|
11908
|
+
* assert.ok(host.osd.length > 0);
|
|
11909
|
+
* ```
|
|
10855
11910
|
*/
|
|
10856
11911
|
click(): Promise<void>;
|
|
10857
11912
|
}
|
|
10858
11913
|
```
|
|
10859
11914
|
|
|
11915
|
+
Example: Check a bar button's tooltip while it is disabled
|
|
11916
|
+
|
|
11917
|
+
The test checks that the Export button shows why it cannot be clicked.
|
|
11918
|
+
The tooltip is the command's title; while the command is disabled or
|
|
11919
|
+
busy and has a non-empty `reason`, the reason follows on a second line.
|
|
11920
|
+
```ts
|
|
11921
|
+
const [button] = host.barButtons;
|
|
11922
|
+
assert.equal(button?.enabled, false);
|
|
11923
|
+
assert.match(button?.tooltip ?? "", /\n/); // the command declares a reason
|
|
11924
|
+
```
|
|
11925
|
+
|
|
10860
11926
|
### FakeProducer
|
|
10861
11927
|
|
|
10862
11928
|
A generated stream (`plugin.player.openStream`) as the test host sees it:
|
|
10863
11929
|
the fake player end of the frame connection. Assertion surface; see
|
|
10864
11930
|
TestHost.producers.
|
|
10865
11931
|
|
|
11932
|
+
The player's end of a generated stream's frame connection
|
|
11933
|
+
(TestHost.producers): how many frames arrived and whether the
|
|
11934
|
+
plugin released it or the player retired it. Tests assert on it; nothing
|
|
11935
|
+
is played.
|
|
11936
|
+
|
|
11937
|
+
- @capability None.
|
|
11938
|
+
|
|
10866
11939
|
```ts
|
|
10867
11940
|
interface FakeProducer {
|
|
10868
11941
|
/** The producer id the host assigned (`1`, `2`, … per host). */
|
|
@@ -10884,6 +11957,18 @@ interface FakeProducer {
|
|
|
10884
11957
|
}
|
|
10885
11958
|
```
|
|
10886
11959
|
|
|
11960
|
+
Example: Check that the stream delivers frames
|
|
11961
|
+
|
|
11962
|
+
The test starts the plugin's generator and checks that video and audio
|
|
11963
|
+
arrive. The counters grow as the SDK pulls frames and sends them over
|
|
11964
|
+
the fake connection.
|
|
11965
|
+
```ts
|
|
11966
|
+
await host.command("start-generator").run();
|
|
11967
|
+
await host.settle(200); // let a few frames through
|
|
11968
|
+
const [producer] = host.producers.values();
|
|
11969
|
+
assert.ok((producer?.videoFrames ?? 0) > 0 && (producer?.receivedFrames ?? 0) > 0);
|
|
11970
|
+
```
|
|
11971
|
+
|
|
10887
11972
|
### MarkdownWire
|
|
10888
11973
|
|
|
10889
11974
|
A Markdown document as TestHost.markdown reads it back.
|
|
@@ -10938,6 +12023,15 @@ Fields that mirror player state at the wire level (`source`, `subtitles`,
|
|
|
10938
12023
|
`calls`) use the protocol's shapes. They are fine for assertions but are
|
|
10939
12024
|
not part of the plugin API: their shape may change in minor versions.
|
|
10940
12025
|
|
|
12026
|
+
An in-memory player that speaks the real wire protocol, so the SDK's own
|
|
12027
|
+
logic runs as it does in the player; only the player side is simulated
|
|
12028
|
+
(no decoding, no Slint, no windows). Make one per test, preset it if the
|
|
12029
|
+
plugin reads something during activation, start the plugin with
|
|
12030
|
+
startPlugin, drive it, `settle()`, and assert on what it
|
|
12031
|
+
recorded.
|
|
12032
|
+
|
|
12033
|
+
- @capability None.
|
|
12034
|
+
|
|
10941
12035
|
```ts
|
|
10942
12036
|
class TestHost implements Transport {
|
|
10943
12037
|
/** Every request the plugin sent, in order: `{ method, params }`. Wire-level; prefer TestHost.sent for one method. */
|
|
@@ -11020,6 +12114,15 @@ class TestHost implements Transport {
|
|
|
11020
12114
|
/**
|
|
11021
12115
|
* 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
12116
|
* @param directory - The package directory (holding `locales/`).
|
|
12117
|
+
* @capability None.
|
|
12118
|
+
* @example Load the translations after changing the language
|
|
12119
|
+
* The test switches the host to German before starting the plugin and
|
|
12120
|
+
* loads that catalog itself (`startPlugin` does the same). Texts are then
|
|
12121
|
+
* shown in German.
|
|
12122
|
+
* ```ts
|
|
12123
|
+
* host.locale = "de";
|
|
12124
|
+
* host.loadCatalog(directory); // locales/de.po (or de.json)
|
|
12125
|
+
* ```
|
|
11023
12126
|
*/
|
|
11024
12127
|
loadCatalog(directory: string): void;
|
|
11025
12128
|
/**
|
|
@@ -11027,6 +12130,7 @@ class TestHost implements Transport {
|
|
|
11027
12130
|
* @param text - English source text or template.
|
|
11028
12131
|
* @param args - Values for its placeholders.
|
|
11029
12132
|
* @returns The shown text.
|
|
12133
|
+
* @capability None.
|
|
11030
12134
|
* @example Compare with a translated message
|
|
11031
12135
|
* The test expects the text a Chinese user sees for a template the plugin
|
|
11032
12136
|
* shows. `translate` looks the English source up in the plugin's catalog
|
|
@@ -11041,6 +12145,19 @@ class TestHost implements Transport {
|
|
|
11041
12145
|
/**
|
|
11042
12146
|
* Simulate another plugin (or the user) saving the interface language: pushes `appearance.changed`.
|
|
11043
12147
|
* @param language - `"system"` or a locale.
|
|
12148
|
+
* @capability None.
|
|
12149
|
+
* @example Simulate the user choosing French for the next start
|
|
12150
|
+
* The plugin hears that the saved interface language changed. The host
|
|
12151
|
+
* pushes `appearance.changed`, which a plugin with `events` sees in
|
|
12152
|
+
* `appearance.language` once it has read it and subscribed; this run's
|
|
12153
|
+
* language does not change.
|
|
12154
|
+
* ```ts
|
|
12155
|
+
* void plugin.appearance.language; // read once: the SDK then follows it
|
|
12156
|
+
* plugin.appearance.onChange(() => {}); // subscribes to appearance events
|
|
12157
|
+
* host.setLanguage("fr");
|
|
12158
|
+
* await host.settle();
|
|
12159
|
+
* assert.equal(plugin.appearance.language, "fr");
|
|
12160
|
+
* ```
|
|
11044
12161
|
*/
|
|
11045
12162
|
setLanguage(language: LanguagePreference): void;
|
|
11046
12163
|
/**
|
|
@@ -11197,6 +12314,7 @@ class TestHost implements Transport {
|
|
|
11197
12314
|
* Simulate another plugin publishing its timeline layer (or clearing it with `[]`); pushes `timeline.changed` when the plugin subscribed.
|
|
11198
12315
|
* @param pluginId - The other plugin's manifest id.
|
|
11199
12316
|
* @param marks - Its whole layer.
|
|
12317
|
+
* @capability None.
|
|
11200
12318
|
* @example Let another plugin publish chapters
|
|
11201
12319
|
* The plugin under test sees two chapter marks another plugin put on the
|
|
11202
12320
|
* timeline. `setTimelineLayer` stores that plugin's whole layer and pushes
|
|
@@ -11230,6 +12348,7 @@ class TestHost implements Transport {
|
|
|
11230
12348
|
}[];
|
|
11231
12349
|
/**
|
|
11232
12350
|
* 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.
|
|
12351
|
+
* @capability None.
|
|
11233
12352
|
* @example Check which file a window shows as its cover
|
|
11234
12353
|
* The test checks that the Cover window's image comes from the plugin's
|
|
11235
12354
|
* data directory. An image property holds the SDK's wire form of
|
|
@@ -11255,6 +12374,8 @@ class TestHost implements Transport {
|
|
|
11255
12374
|
* 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
12375
|
* @param paths - The dropped paths, in the order the file manager gives.
|
|
11257
12376
|
* @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`).
|
|
12377
|
+
* @capability None.
|
|
12378
|
+
* @throws Never: files nobody takes are ignored, as in the player.
|
|
11258
12379
|
* @example Drop files on the video and on the panel
|
|
11259
12380
|
* The first drop lands on the video area: each extension belongs to the
|
|
11260
12381
|
* provider that registered it first, and each provider gets one call with
|
|
@@ -11285,7 +12406,20 @@ class TestHost implements Transport {
|
|
|
11285
12406
|
}): Promise<void>;
|
|
11286
12407
|
/** The palette (`plugin.appearance.theme`); `setAppearance` changes it elsewhere. */
|
|
11287
12408
|
appearance: ThemeId;
|
|
11288
|
-
/**
|
|
12409
|
+
/**
|
|
12410
|
+
* Simulate another plugin switching the palette: updates TestHost.appearance and pushes `appearance.changed`.
|
|
12411
|
+
* @capability None.
|
|
12412
|
+
* @example Simulate another plugin switching to the light palette
|
|
12413
|
+
* The plugin hears the palette change. The host updates `appearance` and
|
|
12414
|
+
* pushes `appearance.changed` to a plugin that listens with `events`.
|
|
12415
|
+
* ```ts
|
|
12416
|
+
* const themes: ThemeId[] = [];
|
|
12417
|
+
* plugin.appearance.onChange((theme) => themes.push(theme)); // subscribes (`events`)
|
|
12418
|
+
* host.setAppearance("vestra-light");
|
|
12419
|
+
* await host.settle();
|
|
12420
|
+
* assert.deepEqual(themes, ["vestra-light"]);
|
|
12421
|
+
* ```
|
|
12422
|
+
*/
|
|
11289
12423
|
setAppearance(theme: ThemeId): void;
|
|
11290
12424
|
/** Settings pages by id (`plugin.settings.page`), every `show` (page id or null), and the page the options window shows. */
|
|
11291
12425
|
readonly settings: {
|
|
@@ -11317,6 +12451,7 @@ class TestHost implements Transport {
|
|
|
11317
12451
|
};
|
|
11318
12452
|
/**
|
|
11319
12453
|
* One settings page by id. Throws when it was never registered.
|
|
12454
|
+
* @capability None.
|
|
11320
12455
|
* @example Change a value on the plugin's settings page
|
|
11321
12456
|
* The test sets the step on the General page and checks the page shows
|
|
11322
12457
|
* it. `emit` fires the page's callback as the user's input would; the
|
|
@@ -11354,13 +12489,26 @@ class TestHost implements Transport {
|
|
|
11354
12489
|
/** Whether it shows now (no video frame). `true` when mounted. */
|
|
11355
12490
|
effective: boolean;
|
|
11356
12491
|
} | null;
|
|
11357
|
-
/**
|
|
12492
|
+
/**
|
|
12493
|
+
* Simulate the backdrop's `effective` changing (`false`: a video frame appeared; `true`: there is none again); pushed to the plugin.
|
|
12494
|
+
* @capability None.
|
|
12495
|
+
* @example Make a video picture replace the backdrop
|
|
12496
|
+
* The plugin's backdrop hears that a video frame now covers it. `false`
|
|
12497
|
+
* means a picture appeared; the change is pushed like the player's
|
|
12498
|
+
* `window.state`.
|
|
12499
|
+
* ```ts
|
|
12500
|
+
* host.setBackdropEffective(false);
|
|
12501
|
+
* await host.settle();
|
|
12502
|
+
* assert.equal(plugin.panels.backdrop.state?.effective, false);
|
|
12503
|
+
* ```
|
|
12504
|
+
*/
|
|
11358
12505
|
setBackdropEffective(effective: boolean): void;
|
|
11359
12506
|
/**
|
|
11360
12507
|
* 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
12508
|
* @param slot - The position.
|
|
11362
12509
|
* @param plugin - The other plugin's manifest id.
|
|
11363
12510
|
* @param options - `title`: its tab label (default: `plugin`).
|
|
12511
|
+
* @capability None.
|
|
11364
12512
|
* @example Put the playlist next to this plugin's sidebar panel
|
|
11365
12513
|
* The sidebar ends up with two tabs: the playlist, mounted first, and the
|
|
11366
12514
|
* plugin's own panel after it. `mountOther` before `startPlugin` mounts
|
|
@@ -11377,10 +12525,31 @@ class TestHost implements Transport {
|
|
|
11377
12525
|
mountOther(slot: "bottom" | "sidebar" | "backdrop", plugin: string, options?: {
|
|
11378
12526
|
title?: string;
|
|
11379
12527
|
}): void;
|
|
11380
|
-
/**
|
|
12528
|
+
/**
|
|
12529
|
+
* Simulate another plugin's panel going away (TestHost.mountOther): when its tab was current, the next tab (else the previous one) is.
|
|
12530
|
+
* @capability None.
|
|
12531
|
+
* @example Let the playlist's tab go away
|
|
12532
|
+
* The other plugin's sidebar tab disappears and the plugin's own tab
|
|
12533
|
+
* becomes current. When the removed tab was current, the next tab (else
|
|
12534
|
+
* the previous one) becomes current, as in the player.
|
|
12535
|
+
* ```ts
|
|
12536
|
+
* host.unmountOther("vestra.playlist");
|
|
12537
|
+
* await host.settle();
|
|
12538
|
+
* assert.equal(host.layout.sidebar?.panels?.length, 1);
|
|
12539
|
+
* ```
|
|
12540
|
+
*/
|
|
11381
12541
|
unmountOther(plugin: string): void;
|
|
11382
12542
|
/**
|
|
11383
12543
|
* 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.
|
|
12544
|
+
* @capability None.
|
|
12545
|
+
* @example Click another plugin's sidebar tab
|
|
12546
|
+
* The user switches the sidebar to the playlist; the plugin's panel is in
|
|
12547
|
+
* a background tab now. The change is pushed as `window.state`.
|
|
12548
|
+
* ```ts
|
|
12549
|
+
* host.selectTab("vestra.playlist");
|
|
12550
|
+
* await host.settle();
|
|
12551
|
+
* assert.notEqual(host.layout.sidebar?.owner, 1); // the plugin under test is owner 1
|
|
12552
|
+
* ```
|
|
11384
12553
|
*/
|
|
11385
12554
|
selectTab(plugin: string): void;
|
|
11386
12555
|
/** The display layer (`plugin.video.view()`): who owns it (this plugin is `manifestId`) and the values the player holds. */
|
|
@@ -11392,6 +12561,17 @@ class TestHost implements Transport {
|
|
|
11392
12561
|
};
|
|
11393
12562
|
/**
|
|
11394
12563
|
* 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).
|
|
12564
|
+
* @capability None.
|
|
12565
|
+
* @example Let another plugin own the display layer
|
|
12566
|
+
* The plugin's `video.view()` fails with `conflict` because another plugin
|
|
12567
|
+
* took the display layer first. `null` releases it again.
|
|
12568
|
+
* ```ts
|
|
12569
|
+
* host.setVideoViewOwner("someone.zoom");
|
|
12570
|
+
* const view = plugin.video.view();
|
|
12571
|
+
* view.set({ zoom: 2 });
|
|
12572
|
+
* await host.settle();
|
|
12573
|
+
* assert.equal(view.status.state, "failed");
|
|
12574
|
+
* ```
|
|
11395
12575
|
*/
|
|
11396
12576
|
setVideoViewOwner(pluginId: string | null): void;
|
|
11397
12577
|
/**
|
|
@@ -11407,10 +12587,22 @@ class TestHost implements Transport {
|
|
|
11407
12587
|
};
|
|
11408
12588
|
/**
|
|
11409
12589
|
* 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).
|
|
12590
|
+
* @capability None.
|
|
12591
|
+
* @example Let another plugin own the render options
|
|
12592
|
+
* The plugin's render options report `conflict` while another plugin owns
|
|
12593
|
+
* them; `null` releases them and resets the values to the defaults.
|
|
12594
|
+
* ```ts
|
|
12595
|
+
* host.setVideoRenderOwner("someone.hdr");
|
|
12596
|
+
* const render = plugin.video.render();
|
|
12597
|
+
* render.set({ toneMapping: "bt.2390" });
|
|
12598
|
+
* await host.settle();
|
|
12599
|
+
* assert.equal(render.status.state, "failed");
|
|
12600
|
+
* ```
|
|
11410
12601
|
*/
|
|
11411
12602
|
setVideoRenderOwner(pluginId: string | null): void;
|
|
11412
12603
|
/**
|
|
11413
12604
|
* 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.
|
|
12605
|
+
* @capability None.
|
|
11414
12606
|
* @example Make a shader fail to compile
|
|
11415
12607
|
* The plugin's render options report `failed`, as when mpv rejects a
|
|
11416
12608
|
* shader. The host pushes the failure only while this plugin owns the
|
|
@@ -11531,10 +12723,21 @@ class TestHost implements Transport {
|
|
|
11531
12723
|
/** Exact host names it resolves (a web resolver), with `schemes`. */
|
|
11532
12724
|
hosts?: string[];
|
|
11533
12725
|
}[];
|
|
11534
|
-
/**
|
|
12726
|
+
/**
|
|
12727
|
+
* What `plugin.resolvers.fileTypes` reports: the extensions of this plugin's file resolvers and of TestHost.otherResolvers, sorted, each once.
|
|
12728
|
+
* @capability None.
|
|
12729
|
+
* @example Check the file types a resolver adds
|
|
12730
|
+
* The test checks the list the plugin will read as
|
|
12731
|
+
* `resolvers.fileTypes`: the extensions of its own file resolvers and of
|
|
12732
|
+
* `otherResolvers`, sorted, each once.
|
|
12733
|
+
* ```ts
|
|
12734
|
+
* assert.deepEqual(host.fileTypes(), ["ch8"]);
|
|
12735
|
+
* ```
|
|
12736
|
+
*/
|
|
11535
12737
|
fileTypes(): string[];
|
|
11536
12738
|
/**
|
|
11537
12739
|
* 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).
|
|
12740
|
+
* @capability None.
|
|
11538
12741
|
* @example Let another plugin's file type appear while running
|
|
11539
12742
|
* Another plugin starts and adds `.ch8`; the plugin under test then sees
|
|
11540
12743
|
* it in `resolvers.fileTypes`. Changing `otherResolvers` alone pushes
|
|
@@ -11652,6 +12855,9 @@ class TestHost implements Transport {
|
|
|
11652
12855
|
readonly dataDirectory: string;
|
|
11653
12856
|
/** The manifest id of the plugin under test (set by startPlugin; `"test.plugin"` by default). */
|
|
11654
12857
|
manifestId: string;
|
|
12858
|
+
/**
|
|
12859
|
+
* 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`.
|
|
12860
|
+
*/
|
|
11655
12861
|
constructor();
|
|
11656
12862
|
/** The `player.setStepSeek` value in effect (default 5). */
|
|
11657
12863
|
stepSeek: number;
|
|
@@ -11707,7 +12913,8 @@ class TestHost implements Transport {
|
|
|
11707
12913
|
/** Every core action run (`host.run`, a core command, a pressed core binding), newest last. */
|
|
11708
12914
|
readonly coreRuns: string[];
|
|
11709
12915
|
/**
|
|
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.
|
|
12916
|
+
* 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.
|
|
12917
|
+
* @capability None.
|
|
11711
12918
|
* @example Pause through the player's own action
|
|
11712
12919
|
* The test pauses playback the way the Space key does and checks that the
|
|
11713
12920
|
* host recorded it. Core actions belong to the player (`host/<id>`);
|
|
@@ -11734,6 +12941,7 @@ class TestHost implements Transport {
|
|
|
11734
12941
|
* find the one a call uses.
|
|
11735
12942
|
* @param method - The wire method name.
|
|
11736
12943
|
* @param handler - Produces the reply from the request parameters.
|
|
12944
|
+
* @capability None.
|
|
11737
12945
|
* @example Make opening a folder fail
|
|
11738
12946
|
* The plugin's Open Folder command meets a missing folder and must tell
|
|
11739
12947
|
* the user. `handle` replaces the host's answer to one wire method; the
|
|
@@ -11750,17 +12958,43 @@ class TestHost implements Transport {
|
|
|
11750
12958
|
* ```
|
|
11751
12959
|
*/
|
|
11752
12960
|
handle(method: string, handler: Handler): void;
|
|
11753
|
-
/**
|
|
12961
|
+
/**
|
|
12962
|
+
* Restore the default behaviour of a method replaced with TestHost.handle.
|
|
12963
|
+
* @capability None.
|
|
12964
|
+
* @example Restore the default answer after a simulated failure
|
|
12965
|
+
* The first write fails, the second succeeds. `reset` removes the handler
|
|
12966
|
+
* that `handle` installed, so the method is answered by the test host
|
|
12967
|
+
* again.
|
|
12968
|
+
* ```ts
|
|
12969
|
+
* host.handle("clipboard.writeText", () => {
|
|
12970
|
+
* throw new HostError("failed", "denied");
|
|
12971
|
+
* });
|
|
12972
|
+
* await assert.rejects(plugin.clipboard.writeText("x"));
|
|
12973
|
+
* host.reset("clipboard.writeText");
|
|
12974
|
+
* await plugin.clipboard.writeText("x"); // the default handler again
|
|
12975
|
+
* ```
|
|
12976
|
+
*/
|
|
11754
12977
|
reset(method: string): void;
|
|
11755
12978
|
/**
|
|
11756
12979
|
* 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
12980
|
* @param event - The wire event name.
|
|
11758
12981
|
* @param data - The event payload (wire-level shape).
|
|
12982
|
+
* @capability None.
|
|
12983
|
+
* @example Deliver a raw event the helpers do not cover
|
|
12984
|
+
* The test pushes a `window.state` event after changing `layout` by hand.
|
|
12985
|
+
* Like the player, the host delivers it only when the plugin subscribed;
|
|
12986
|
+
* most tests use `setSource`, `setMainWindow` and the other helpers
|
|
12987
|
+
* instead.
|
|
12988
|
+
* ```ts
|
|
12989
|
+
* host.emit("window.state", {}); // no facts: the SDK reads window.state again
|
|
12990
|
+
* await host.settle();
|
|
12991
|
+
* ```
|
|
11759
12992
|
*/
|
|
11760
12993
|
emit(event: string, data: unknown): void;
|
|
11761
12994
|
/**
|
|
11762
12995
|
* 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
12996
|
* @param patch - Wire-level source fields, e.g. `phase`, `position`, `duration`, `paused`, `audioTracks`.
|
|
12997
|
+
* @capability None.
|
|
11764
12998
|
* @example Load a file, then let it play to the end
|
|
11765
12999
|
* The plugin sees a one-minute source become ready, then end on its own.
|
|
11766
13000
|
* `setSource` changes the host's source facts and broadcasts them like
|
|
@@ -11777,6 +13011,7 @@ class TestHost implements Transport {
|
|
|
11777
13011
|
setSource(patch: Partial<SourceState>): void;
|
|
11778
13012
|
/**
|
|
11779
13013
|
* 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.
|
|
13014
|
+
* @capability None.
|
|
11780
13015
|
* @example Go fullscreen, then report picture-in-picture support
|
|
11781
13016
|
* The plugin sees the main window fullscreen at 1920×1080, then learns
|
|
11782
13017
|
* that picture-in-picture is available. `setMainWindow` changes the
|
|
@@ -11803,12 +13038,23 @@ class TestHost implements Transport {
|
|
|
11803
13038
|
}>): void;
|
|
11804
13039
|
/**
|
|
11805
13040
|
* 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.
|
|
13041
|
+
* @capability None.
|
|
13042
|
+
* @example Pop the sidebar out in fullscreen
|
|
13043
|
+
* The pointer reaches the right edge in fullscreen and the attached
|
|
13044
|
+
* sidebar pops out over the video. Outside fullscreen, or with the
|
|
13045
|
+
* sidebar hidden or floating, nothing happens.
|
|
13046
|
+
* ```ts
|
|
13047
|
+
* host.setMainWindow({ fullscreen: true });
|
|
13048
|
+
* host.popSidebar(true);
|
|
13049
|
+
* await host.settle();
|
|
13050
|
+
* ```
|
|
11806
13051
|
*/
|
|
11807
13052
|
popSidebar(popped: boolean): void;
|
|
11808
13053
|
/**
|
|
11809
13054
|
* 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
13055
|
* @param source - The path or URL.
|
|
11811
13056
|
* @param phase - The phase to end in (default `"ready"`).
|
|
13057
|
+
* @capability None.
|
|
11812
13058
|
* @example Let the user open another file meanwhile
|
|
11813
13059
|
* A state the plugin captured before stops being current once another
|
|
11814
13060
|
* file opens. `openElsewhere` gives the source a new `sourceId`, so
|
|
@@ -11827,6 +13073,7 @@ class TestHost implements Transport {
|
|
|
11827
13073
|
* 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
13074
|
* @param producerId - The producer, from TestHost.producers.
|
|
11829
13075
|
* @param message - The player's failure text.
|
|
13076
|
+
* @capability None.
|
|
11830
13077
|
* @example Break the plugin's generated stream
|
|
11831
13078
|
* The plugin's stream stops with an error, as when the player loses its
|
|
11832
13079
|
* audio device. `failStream` puts the source into `error` with the
|
|
@@ -11839,8 +13086,11 @@ class TestHost implements Transport {
|
|
|
11839
13086
|
*/
|
|
11840
13087
|
failStream(producerId: number, message: string): void;
|
|
11841
13088
|
/**
|
|
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"`)
|
|
13089
|
+
* 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
13090
|
* @returns What the key did: `"ran"`, `"reason"` or `"passed"`.
|
|
13091
|
+
* @capability None.
|
|
13092
|
+
* @throws command (rejects) A `HostError`: the command that would run is busy (the player only logs this).
|
|
13093
|
+
* @throws Error (rejects) Nothing is bound to the key.
|
|
11844
13094
|
* @example Press a global key and a panel key
|
|
11845
13095
|
* Ctrl+B runs whatever is bound globally; Delete is pressed while the
|
|
11846
13096
|
* plugin's panel has focus, so panel bindings are tried first. `press`
|
|
@@ -11931,6 +13181,7 @@ class TestHost implements Transport {
|
|
|
11931
13181
|
readonly remoteImages: Map<string, string | null>;
|
|
11932
13182
|
/**
|
|
11933
13183
|
* 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.
|
|
13184
|
+
* @capability None.
|
|
11934
13185
|
* @example Read back a Markdown message in a chat window
|
|
11935
13186
|
* The test checks the Markdown source of the first chat message. Rows
|
|
11936
13187
|
* hold the document in its wire form, as `plugin.ui.markdown` produced
|
|
@@ -11948,13 +13199,47 @@ class TestHost implements Transport {
|
|
|
11948
13199
|
/** The system's name for it. */
|
|
11949
13200
|
name: string;
|
|
11950
13201
|
}[];
|
|
11951
|
-
/**
|
|
13202
|
+
/**
|
|
13203
|
+
* Replace the plugin list and push `packages.changed` (another plugin started, stopped, was installed or removed).
|
|
13204
|
+
* @capability None.
|
|
13205
|
+
* @example Simulate another plugin starting
|
|
13206
|
+
* The plugin's store window hears that a plugin is running now. The list
|
|
13207
|
+
* replaces `packages` and is pushed as `packages.changed`, which the
|
|
13208
|
+
* plugin's `packages.onChange` listeners receive.
|
|
13209
|
+
* ```ts
|
|
13210
|
+
* host.setPackages([...host.packages]); // edit a copy, then push it
|
|
13211
|
+
* await host.settle();
|
|
13212
|
+
* ```
|
|
13213
|
+
*/
|
|
11952
13214
|
setPackages(list: TestHost["packages"]): void;
|
|
11953
13215
|
/**
|
|
11954
13216
|
* 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.
|
|
13217
|
+
* @capability None.
|
|
13218
|
+
* @throws unavailable (rejects) A `HostError`: there is no sequence provider, or its flag is false.
|
|
13219
|
+
* @example Ask the plugin's provider for the next item
|
|
13220
|
+
* Another plugin calls `player.next()`; the host asks the plugin under
|
|
13221
|
+
* test, which is the sequence provider, and the test checks what it
|
|
13222
|
+
* opened.
|
|
13223
|
+
* ```ts
|
|
13224
|
+
* await host.step("next");
|
|
13225
|
+
* await host.settle();
|
|
13226
|
+
* assert.equal(host.source.source, "/media/b.mp4");
|
|
13227
|
+
* ```
|
|
11955
13228
|
*/
|
|
11956
13229
|
step(direction: "next" | "previous"): Promise<unknown>;
|
|
11957
|
-
/**
|
|
13230
|
+
/**
|
|
13231
|
+
* The plugin's command with this local id. Throws when it is not registered (never added, or disposed).
|
|
13232
|
+
* @capability None.
|
|
13233
|
+
* @example Read a command's state after a change
|
|
13234
|
+
* The test checks that Export becomes enabled once a file is ready. The
|
|
13235
|
+
* record is updated as the plugin sends new state; read it after
|
|
13236
|
+
* `settle()`.
|
|
13237
|
+
* ```ts
|
|
13238
|
+
* host.setSource({ phase: "ready", duration: 60 });
|
|
13239
|
+
* await host.settle();
|
|
13240
|
+
* assert.equal(host.command("export").state.enabled, true); // throws if not registered
|
|
13241
|
+
* ```
|
|
13242
|
+
*/
|
|
11958
13243
|
command(id: string): FakeCommand;
|
|
11959
13244
|
/**
|
|
11960
13245
|
* 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 +13259,7 @@ class TestHost implements Transport {
|
|
|
11974
13259
|
get barButtons(): readonly FakeBarButton[];
|
|
11975
13260
|
/**
|
|
11976
13261
|
* 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()`.
|
|
13262
|
+
* @capability None.
|
|
11977
13263
|
* @example Press a window's Reset button from a test
|
|
11978
13264
|
* The Picture window's Reset callback runs and the brightness returns to
|
|
11979
13265
|
* 0. The window is found by its id (the `.slint` file name by default);
|
|
@@ -11988,6 +13274,7 @@ class TestHost implements Transport {
|
|
|
11988
13274
|
window(id: string): FakeWindow;
|
|
11989
13275
|
/**
|
|
11990
13276
|
* 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.
|
|
13277
|
+
* @capability None.
|
|
11991
13278
|
* @example Select a row in the plugin's sidebar panel
|
|
11992
13279
|
* The test checks the panel sits in the sidebar and selects its third
|
|
11993
13280
|
* row. `emit` fires a callback on the panel's global as a click would;
|
|
@@ -12009,6 +13296,7 @@ class TestHost implements Transport {
|
|
|
12009
13296
|
};
|
|
12010
13297
|
/**
|
|
12011
13298
|
* The parameters of every request of one wire method, oldest first.
|
|
13299
|
+
* @capability None.
|
|
12012
13300
|
* @example Check the request behind an OSD message
|
|
12013
13301
|
* The test checks the last OSD request the plugin sent, before
|
|
12014
13302
|
* translation. `sent` lists the parameters of every request of one wire
|
|
@@ -12029,6 +13317,8 @@ class TestHost implements Transport {
|
|
|
12029
13317
|
* for: on a loaded machine it can finish after `settle()` returns. Poll
|
|
12030
13318
|
* the result you assert on instead (second example).
|
|
12031
13319
|
* @param ms - Extra time to wait, for debounces and timers (default 0).
|
|
13320
|
+
* @capability None.
|
|
13321
|
+
* @throws Never.
|
|
12032
13322
|
* @example Wait past a debounced save
|
|
12033
13323
|
* The plugin's store writes its file 300 ms after the last `save`, so
|
|
12034
13324
|
* the test waits 400 ms before it checks what the plugin did next.
|
|
@@ -12054,7 +13344,22 @@ class TestHost implements Transport {
|
|
|
12054
13344
|
* ```
|
|
12055
13345
|
*/
|
|
12056
13346
|
settle(ms?: number): Promise<void>;
|
|
12057
|
-
/**
|
|
13347
|
+
/**
|
|
13348
|
+
* Close the fake stream servers and delete the host's temporary directories. `stop()` from startPlugin calls it.
|
|
13349
|
+
* @capability None.
|
|
13350
|
+
* @example Clean up a host that was never started
|
|
13351
|
+
* A test that made a host but failed before `startPlugin` removes the
|
|
13352
|
+
* host's temporary directories itself. `stop()` from `startPlugin` calls
|
|
13353
|
+
* `close` for you otherwise.
|
|
13354
|
+
* ```ts
|
|
13355
|
+
* const host = new TestHost();
|
|
13356
|
+
* try {
|
|
13357
|
+
* // … preset the host; the test fails here …
|
|
13358
|
+
* } finally {
|
|
13359
|
+
* host.close(); // deletes the host's temporary data
|
|
13360
|
+
* }
|
|
13361
|
+
* ```
|
|
13362
|
+
*/
|
|
12058
13363
|
close(): void;
|
|
12059
13364
|
}
|
|
12060
13365
|
```
|
|
@@ -12104,9 +13409,14 @@ throws, or when a registration made during activation fails (for example
|
|
|
12104
13409
|
`limit`, `conflict`, `invalid`), which is when the player would fail the
|
|
12105
13410
|
activation.
|
|
12106
13411
|
|
|
13412
|
+
The way into a plugin's tests: one call starts the plugin the way the
|
|
13413
|
+
player's runner does and returns the host to drive, the `plugin` object
|
|
13414
|
+
to call directly, and `stop` to end it like the player.
|
|
13415
|
+
|
|
12107
13416
|
- @param activate - The plugin's default-exported `activate` function.
|
|
12108
13417
|
- @param options - Where the plugin lives and how to start it.
|
|
12109
13418
|
- @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.
|
|
13419
|
+
- @capability None.
|
|
12110
13420
|
|
|
12111
13421
|
```ts
|
|
12112
13422
|
function startPlugin(activate: (plugin: Plugin) => unknown, options?: {
|
|
@@ -12851,11 +14161,17 @@ Common codes: `stale` (the condition no longer holds; nothing was done),
|
|
|
12851
14161
|
`timeout`, `aborted`, `limit` (a player limit such as the command count),
|
|
12852
14162
|
`disabled` (the plugin is stopping).
|
|
12853
14163
|
|
|
14164
|
+
A definite refusal, as opposed to a lost reply: the SDK does not retry
|
|
14165
|
+
it, and the plugin sees an `Error` with `code`.
|
|
14166
|
+
|
|
14167
|
+
- @capability None.
|
|
14168
|
+
|
|
12854
14169
|
```ts
|
|
12855
14170
|
class HostError extends Error {
|
|
12856
14171
|
/** The stable error code, e.g. `"invalid"`, `"stale"`, `"unavailable"`. */
|
|
12857
14172
|
code: string;
|
|
12858
14173
|
/**
|
|
14174
|
+
* A refusal with this code, as the player would send it.
|
|
12859
14175
|
* @param code - The stable error code the caller will see as `error.code`.
|
|
12860
14176
|
* @param message - Human-readable detail (not for branching on).
|
|
12861
14177
|
*/
|