@vestra-player/sdk 1.5.30 → 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 +26 -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/manifest.js +1 -1
- 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 +6 -1
- package/dist/plugin/services.js +6 -1
- package/dist/plugin/testing.d.ts +518 -9
- package/dist/plugin/testing.js +385 -17
- package/dist/plugin/types.d.ts +2921 -101
- package/dist/plugin/values.d.ts +7 -2
- package/dist/plugin/values.js +7 -2
- package/dist/processing.d.ts +6 -0
- package/dist/processing.js +1 -1
- 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 +2526 -83
- package/llms.txt +23 -23
- 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
|
|
|
@@ -1032,6 +1032,282 @@ The API is the same on macOS and Windows; what differs is the system's own part:
|
|
|
1032
1032
|
|
|
1033
1033
|
`TestHost` from `@vestra-player/sdk/testing` runs a plugin without the player and records its interface: windows with their properties and callbacks, the mounted panel, settings pages, the OSD and dialogs. It does not compile Slint files; check them by running the plugin in the player. How to write such tests is in Test a plugin.
|
|
1034
1034
|
|
|
1035
|
+
## How-to: Control playback
|
|
1036
|
+
|
|
1037
|
+
**Goal:** your plugin opens files and controls playback (pause, seek, speed, next and previous), shows what is playing, and never acts on a file the user has already left.
|
|
1038
|
+
|
|
1039
|
+
The player has one source. The user, other plugins and yours all change it, so every control in `plugin.player` is **conditional**: it acts on the state your plugin last saw and fails with `stale` (doing nothing) when someone else changed the source in between. Most of this guide is about using that well.
|
|
1040
|
+
|
|
1041
|
+
#### Capabilities
|
|
1042
|
+
|
|
1043
|
+
```json
|
|
1044
|
+
{
|
|
1045
|
+
"capabilities": ["commands", "playback.read", "playback.control", "events"]
|
|
1046
|
+
}
|
|
1047
|
+
```
|
|
1048
|
+
|
|
1049
|
+
`playback.read` reads the state, `playback.control` changes it, `events` keeps `player.state` current when the change comes from elsewhere (without it, the state only moves after your own calls and `refresh()`), and `commands` gives your actions menu items and keys.
|
|
1050
|
+
|
|
1051
|
+
#### Read the state
|
|
1052
|
+
|
|
1053
|
+
`plugin.player.state` is a snapshot the SDK keeps current from the player's events. `onChange` runs after it changed; read the new snapshot there instead of asking the player.
|
|
1054
|
+
|
|
1055
|
+
```ts
|
|
1056
|
+
const info = plugin.osd.panel();
|
|
1057
|
+
plugin.player.onChange((state) => {
|
|
1058
|
+
// `position` and `duration` are seconds, null while unknown.
|
|
1059
|
+
if (state.phase !== "ready") return info.set(null);
|
|
1060
|
+
info.set("{} / {} s", [state.position?.toFixed(0) ?? "–", state.duration?.toFixed(0) ?? "–"]);
|
|
1061
|
+
});
|
|
1062
|
+
```
|
|
1063
|
+
|
|
1064
|
+
`state.can` says which controls the current source allows (`pause`, `seek`, `rate`, `restart`, `close`, `next`, `previous`). Bind menu items to it so they grey out by themselves:
|
|
1065
|
+
|
|
1066
|
+
```ts
|
|
1067
|
+
plugin.commands.add({
|
|
1068
|
+
id: "pause",
|
|
1069
|
+
path: ["@playback", "Pause"],
|
|
1070
|
+
enabled: () => plugin.player.state.can.pause, // re-evaluated after every change
|
|
1071
|
+
checked: () => plugin.player.state.paused,
|
|
1072
|
+
run: () => plugin.player.setPaused(!plugin.player.state.paused),
|
|
1073
|
+
});
|
|
1074
|
+
```
|
|
1075
|
+
|
|
1076
|
+
#### Control it, and handle `stale`
|
|
1077
|
+
|
|
1078
|
+
A control without options is conditional on `player.state` as it is when you call. `stale` means the source changed after that: another file, a close, or any pause, seek or speed change by the user or another plugin. Nothing happened, and usually that is the right outcome:
|
|
1079
|
+
|
|
1080
|
+
```ts
|
|
1081
|
+
try {
|
|
1082
|
+
await plugin.player.seek(30);
|
|
1083
|
+
} catch (error) {
|
|
1084
|
+
if ((error as PluginError).code !== "stale") throw error; // another file now
|
|
1085
|
+
}
|
|
1086
|
+
```
|
|
1087
|
+
|
|
1088
|
+
For work that takes time (a timer, a dialog, a network request), keep the snapshot from when it started and pass it as `ifCurrent`, so the call applies only to that source:
|
|
1089
|
+
|
|
1090
|
+
```ts
|
|
1091
|
+
const seen = plugin.player.state;
|
|
1092
|
+
const answer = await plugin.dialogs.confirm({ message: "Close this file?", confirm: "Close" }); // ui.dialog
|
|
1093
|
+
if (answer.confirmed)
|
|
1094
|
+
await plugin.player.close({ ifCurrent: seen }).catch((error: PluginError) => {
|
|
1095
|
+
if (error.code !== "stale") throw error; // the user opened something else meanwhile
|
|
1096
|
+
});
|
|
1097
|
+
```
|
|
1098
|
+
|
|
1099
|
+
`player.isCurrent(seen)` answers the same question without a call. Your own consecutive calls do not make each other stale: the SDK refreshes the state after each one.
|
|
1100
|
+
|
|
1101
|
+
Other codes: `invalid` for a value out of range (a negative seek, a speed outside 0.05–10) or a control while nothing is open, `unsupported` for something the source cannot do (a loop on a source that cannot seek).
|
|
1102
|
+
|
|
1103
|
+
#### Open a file and follow it
|
|
1104
|
+
|
|
1105
|
+
`open` resolves once the player accepted the load (for an address a resolver plugin handles, once the resolver has answered) and returns a `Media` handle for that source. `ready()` waits until it plays; `onEnd` fires when it ends on its own.
|
|
1106
|
+
|
|
1107
|
+
```ts
|
|
1108
|
+
const media = await plugin.player.open("/Movies/clip.mp4");
|
|
1109
|
+
try {
|
|
1110
|
+
await media.ready({ signal: AbortSignal.timeout(10_000) }); // closes it on timeout
|
|
1111
|
+
} catch (error) {
|
|
1112
|
+
await plugin.osd.show("Cannot play: {}", [(error as Error).message]);
|
|
1113
|
+
}
|
|
1114
|
+
media.onEnd((ended) =>
|
|
1115
|
+
// Play on only if nobody opened something else at the end.
|
|
1116
|
+
plugin.player.open("/Movies/next.mp4", { ifCurrent: ended }).catch(() => {}),
|
|
1117
|
+
);
|
|
1118
|
+
```
|
|
1119
|
+
|
|
1120
|
+
`ready()` rejects with `failed` (the file cannot be loaded), `replaced` (another source took over) or `ended`. Disposing your scope does not close the media: playback belongs to the user, so call `media.close()` when you mean it.
|
|
1121
|
+
|
|
1122
|
+
#### Behave exactly like the player's own keys
|
|
1123
|
+
|
|
1124
|
+
For buttons in your own bar or window, prefer the player's core actions to calling the controls yourself. `plugin.host.run` (capability `commands`) runs them with the player's checks, accumulation of repeated presses and on-screen text:
|
|
1125
|
+
|
|
1126
|
+
```ts
|
|
1127
|
+
declare const bar: Panel;
|
|
1128
|
+
// ---cut---
|
|
1129
|
+
bar.on("playPause", () => plugin.host.run("play-pause"));
|
|
1130
|
+
bar.on("forward30", () => plugin.host.run("seek-forward", { seconds: 30 })); // adds up with →
|
|
1131
|
+
```
|
|
1132
|
+
|
|
1133
|
+
`run` rejects with `unavailable` while the action is disabled, for example with nothing open.
|
|
1134
|
+
|
|
1135
|
+
#### Next and previous
|
|
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 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
|
+
|
|
1139
|
+
```ts
|
|
1140
|
+
declare const items: string[];
|
|
1141
|
+
let index = 0;
|
|
1142
|
+
// ---cut---
|
|
1143
|
+
const playAt = async (at: number) => {
|
|
1144
|
+
index = at;
|
|
1145
|
+
await plugin.player.open(items[at]!);
|
|
1146
|
+
};
|
|
1147
|
+
plugin.player.provideSequence({
|
|
1148
|
+
next: () => playAt(index + 1),
|
|
1149
|
+
previous: () => playAt(index - 1),
|
|
1150
|
+
canNext: () => index + 1 < items.length, // shown as state.can.next
|
|
1151
|
+
canPrevious: () => index > 0,
|
|
1152
|
+
});
|
|
1153
|
+
```
|
|
1154
|
+
|
|
1155
|
+
A second provider is refused with `conflict` (`sequence-taken`). During activation that does not fail the plugin: it runs without Next and Previous and the player shows a warning; a refusal after activation goes to `onError`.
|
|
1156
|
+
|
|
1157
|
+
#### Sliders, loops and the output
|
|
1158
|
+
|
|
1159
|
+
- A seek slider: start a `player.scrub()` gesture on press and `end()` it on release; see Keep a slider responsive while dragging.
|
|
1160
|
+
- An A-B loop: `setLoop({ a, b, count })`; `null` stops it, a new source clears it.
|
|
1161
|
+
- Volume and mute: `setOutput({ volume, muted })` (volume 0–200). The newest write wins; the promise resolves `false` when a newer one replaced it.
|
|
1162
|
+
- Your own step size for ← and →: `setStepSeek({ seconds, keyframes })`, withdrawn when your plugin stops.
|
|
1163
|
+
|
|
1164
|
+
## How-to: Store a plugin's data
|
|
1165
|
+
|
|
1166
|
+
**Goal:** your plugin keeps its settings across restarts, keeps work in progress across a reload, and keeps an API token safe, each in the right place.
|
|
1167
|
+
|
|
1168
|
+
| What | Where | Lasts | Capability |
|
|
1169
|
+
| --- | --- | --- | --- |
|
|
1170
|
+
| Settings, history, small state | `plugin.storage.json(name)`: a JSON file in the data directory | Across player restarts and plugin upgrades | none |
|
|
1171
|
+
| Work in progress for this run | `plugin.storage.session`: values in the player's memory | Across plugin restarts (reload, crash, upgrade); cleared when the player quits or the plugin is disabled or uninstalled | none |
|
|
1172
|
+
| Tokens, passwords | `plugin.secrets`: the system's keychain or Credential Manager | Until deleted, or until the plugin is uninstalled with its data | `secrets` |
|
|
1173
|
+
| Anything else (caches, downloads) | Your own files under `plugin.dataDirectory` | Until you delete them | none |
|
|
1174
|
+
|
|
1175
|
+
All four are private to the plugin; another plugin never reads them.
|
|
1176
|
+
|
|
1177
|
+
#### Settings: a JSON file
|
|
1178
|
+
|
|
1179
|
+
`storage.json` reads `<dataDirectory>/<name>.json` when you call it and gives you `value` at once (the defaults when the file is missing, broken or fails `validate`; a broken file is kept aside as `<name>.json.bad-<time>`). `save` replaces the value in memory and writes the file 300 ms later, through a temporary file and a rename.
|
|
1180
|
+
|
|
1181
|
+
```ts
|
|
1182
|
+
type Prefs = { step: number; recent: string[] };
|
|
1183
|
+
const prefs = plugin.storage.json<Prefs>("prefs", {
|
|
1184
|
+
defaults: { step: 5, recent: [] },
|
|
1185
|
+
// Checked when the file is read: an older version may have written it.
|
|
1186
|
+
validate: (v): v is Prefs => typeof (v as Prefs)?.step === "number" && Array.isArray((v as Prefs)?.recent),
|
|
1187
|
+
});
|
|
1188
|
+
prefs.save({ ...prefs.value, step: 10 }); // never change prefs.value in place
|
|
1189
|
+
```
|
|
1190
|
+
|
|
1191
|
+
Stopping the plugin writes a pending save first, but only within the short time the player gives a stopping plugin (about 300 ms for a plugin in its own process), and a crash loses it; call `await prefs.flush()` after a save that must not be lost. Also flush before you read or copy the file yourself. See Remember a window's state between runs for a complete window built on a store.
|
|
1192
|
+
|
|
1193
|
+
#### Work in progress: session values
|
|
1194
|
+
|
|
1195
|
+
`storage.session` keeps JSON values in the player for this run: a reload of the plugin (an upgrade, a crash, the user's Reload) finds them again; the next player start does not.
|
|
1196
|
+
|
|
1197
|
+
```ts
|
|
1198
|
+
const session = plugin.storage.session;
|
|
1199
|
+
const queue = (await session.get<string[]>("queue")) ?? []; // undefined: nothing stored
|
|
1200
|
+
queue.push("https://example.com/a.mkv");
|
|
1201
|
+
try {
|
|
1202
|
+
await session.set("queue", queue); // kept once this resolves
|
|
1203
|
+
} catch (error) {
|
|
1204
|
+
// `limit`: the plugin's values would pass 1 MiB; nothing changed.
|
|
1205
|
+
if ((error as PluginError).code !== "limit") throw error;
|
|
1206
|
+
}
|
|
1207
|
+
```
|
|
1208
|
+
|
|
1209
|
+
Each call is one request, answered in order, with no debounce. Keys are 1–1024 bytes; values come back as `JSON.stringify` wrote them (object properties may come back in another order, a `Date` as a string), and are not checked, so check their shape.
|
|
1210
|
+
|
|
1211
|
+
#### Secrets: the system's store
|
|
1212
|
+
|
|
1213
|
+
`plugin.secrets` stores short text values in the macOS keychain or the Windows Credential Manager. Never write a token to a settings file.
|
|
1214
|
+
|
|
1215
|
+
```ts
|
|
1216
|
+
let token = await plugin.secrets.get("api-token").catch(() => null); // no store: ask
|
|
1217
|
+
if (token === null) {
|
|
1218
|
+
token = "…"; // ask the user, for example in a tool window
|
|
1219
|
+
await plugin.secrets.set("api-token", token).catch((error: PluginError) => {
|
|
1220
|
+
// `unsupported` (no store), `unavailable` (store unreachable), `failed` (access denied).
|
|
1221
|
+
void plugin.osd.show("The token is not kept for next time ({})", [error.code]);
|
|
1222
|
+
});
|
|
1223
|
+
}
|
|
1224
|
+
```
|
|
1225
|
+
|
|
1226
|
+
Names are 1–64 of `a-z 0-9 . _ -`, values 1–1024 bytes, at most 256 names. Have a fallback: some systems have no store, a store can be unreachable (no keychain, read-only, no logged-in session on Windows), and the user can deny access.
|
|
1227
|
+
|
|
1228
|
+
#### Your own files
|
|
1229
|
+
|
|
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
|
+
|
|
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
|
+
|
|
1035
1311
|
## How-to: Keep a slider responsive while dragging
|
|
1036
1312
|
|
|
1037
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.
|
|
@@ -3297,9 +3573,32 @@ flushes, a running `tools.run` process is killed and a listener is
|
|
|
3297
3573
|
unsubscribed. A Media is the exception: disposing its scope does
|
|
3298
3574
|
**not** close the media; call Media.close for that.
|
|
3299
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
|
+
|
|
3300
3582
|
```ts
|
|
3301
3583
|
interface Disposable {
|
|
3302
|
-
/**
|
|
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
|
+
*/
|
|
3303
3602
|
dispose(): Promise<void>;
|
|
3304
3603
|
}
|
|
3305
3604
|
```
|
|
@@ -3325,7 +3624,6 @@ change).
|
|
|
3325
3624
|
| code | meaning |
|
|
3326
3625
|
| --- | --- |
|
|
3327
3626
|
| `stale` | A condition no longer held (the source or the subtitle selection changed); nothing was done. See Conditional. |
|
|
3328
|
-
| `staleDigest` | PackageCandidate.install: the package's files changed after it was validated; validate it again. |
|
|
3329
3627
|
| `capability` | The manifest does not declare the capability the call needs. |
|
|
3330
3628
|
| `invalid` | A bad argument (out of range, malformed, unknown id). |
|
|
3331
3629
|
| `missing` | The target does not exist (an empty panel position, a missing file, an unknown command). |
|
|
@@ -3345,7 +3643,7 @@ change).
|
|
|
3345
3643
|
| `backend-uncertain` | The player's backend blocked a filter stage; call `retry()`. |
|
|
3346
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. |
|
|
3347
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). |
|
|
3348
|
-
| `versionMismatch` | The package needs a newer player (`minPlayerVersion`)
|
|
3646
|
+
| `versionMismatch` | The package needs a newer player (`minPlayerVersion`). |
|
|
3349
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. |
|
|
3350
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). |
|
|
3351
3649
|
| `host` | Any other failure inside the player; `message` is the player's reason. |
|
|
@@ -3408,6 +3706,14 @@ Reading Scope.window, Scope.app, Scope.appearance
|
|
|
3408
3706
|
`.theme`, Scope.ui, Scope.tools, Scope.storage and
|
|
3409
3707
|
Scope.system needs none.
|
|
3410
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
|
+
|
|
3411
3717
|
```ts
|
|
3412
3718
|
interface Scope extends Disposable {
|
|
3413
3719
|
/** Menu commands and their default bindings (`commands`). */
|
|
@@ -3431,10 +3737,16 @@ interface Scope extends Disposable {
|
|
|
3431
3737
|
*/
|
|
3432
3738
|
filter(id: string): VideoFilter;
|
|
3433
3739
|
/**
|
|
3434
|
-
* 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.
|
|
3435
3743
|
*/
|
|
3436
3744
|
view(): VideoView;
|
|
3437
|
-
/**
|
|
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
|
+
*/
|
|
3438
3750
|
render(): RenderOptions;
|
|
3439
3751
|
};
|
|
3440
3752
|
/** Audio processing: this plugin's filter chain and delay (`processing.audio`, `playback.read` and `events`, as for VideoFilter). */
|
|
@@ -3488,17 +3800,57 @@ interface Scope extends Disposable {
|
|
|
3488
3800
|
readonly i18n: I18n;
|
|
3489
3801
|
/** Pages in the player's options window (`ui.settings`). */
|
|
3490
3802
|
readonly settings: Settings;
|
|
3491
|
-
/**
|
|
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
|
+
*/
|
|
3492
3832
|
scope(): Scope;
|
|
3493
3833
|
/**
|
|
3494
3834
|
* Adopt an existing handle: it is disposed together with this scope.
|
|
3495
3835
|
* @param item - Any Disposable, for example a handle created through another scope.
|
|
3496
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
|
+
* ```
|
|
3497
3848
|
*/
|
|
3498
3849
|
add<T extends Disposable>(item: T): T;
|
|
3499
3850
|
/**
|
|
3500
3851
|
* Run `cleanup` when this scope is disposed (for example `clearInterval` or closing a socket). It may be async; errors are logged.
|
|
3501
3852
|
* @returns A handle that cancels the registration early (without running `cleanup`).
|
|
3853
|
+
* @capability None.
|
|
3502
3854
|
* @example Stop a timer when the plugin stops
|
|
3503
3855
|
* A once-a-second refresh runs as long as the plugin does. `onDispose`
|
|
3504
3856
|
* runs the cleanup when the scope is disposed, which for `plugin` is
|
|
@@ -3517,6 +3869,7 @@ interface Scope extends Disposable {
|
|
|
3517
3869
|
* Rejections of async handlers count too, and so do failures of
|
|
3518
3870
|
* registrations made after activation (a command whose `ready` rejects,
|
|
3519
3871
|
* a property write the player refused).
|
|
3872
|
+
* @capability None.
|
|
3520
3873
|
* @example Show errors from handlers on the OSD
|
|
3521
3874
|
* When a menu command, a window callback or a listener of the plugin
|
|
3522
3875
|
* fails, the user sees why. The SDK catches what those handlers throw
|
|
@@ -3536,6 +3889,23 @@ interface Scope extends Disposable {
|
|
|
3536
3889
|
* changing state from your own timers or between awaits of a long task.
|
|
3537
3890
|
*
|
|
3538
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
|
+
* ```
|
|
3539
3909
|
*/
|
|
3540
3910
|
update(): void;
|
|
3541
3911
|
}
|
|
@@ -3584,6 +3954,12 @@ root scope is disposed for local cleanup (timers, signals, sockets). A
|
|
|
3584
3954
|
process that has not exited after 300 ms is killed. Media the plugin
|
|
3585
3955
|
opened with Player.open keeps playing.
|
|
3586
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
|
+
|
|
3587
3963
|
```ts
|
|
3588
3964
|
interface Plugin extends Scope {
|
|
3589
3965
|
/** The manifest id, e.g. `"community.bookmark"`. */
|
|
@@ -3889,12 +4265,23 @@ interface CommandOptions {
|
|
|
3889
4265
|
A registered command, from Commands.add. Disposing it removes it
|
|
3890
4266
|
from the menu and the binding table; the id can be added again right away.
|
|
3891
4267
|
|
|
4268
|
+
The handle of one menu item and its keys. Its state (`enabled`, `checked`,
|
|
4269
|
+
`busy`, `reason`) is bound: functions given to Commands.add or
|
|
4270
|
+
Command.set are evaluated again in every update pass and only
|
|
4271
|
+
changes reach the player. Its title, path, bindings and button cannot
|
|
4272
|
+
change in place; dispose the command (or a child Scope) and add it
|
|
4273
|
+
again. Disposing it, its scope or stopping the plugin removes the menu
|
|
4274
|
+
item, its keys and its bar button.
|
|
4275
|
+
|
|
4276
|
+
- @capability commands
|
|
4277
|
+
|
|
3892
4278
|
```ts
|
|
3893
4279
|
interface Command extends Disposable {
|
|
3894
4280
|
/** The local id given in CommandOptions.id. */
|
|
3895
4281
|
readonly id: string;
|
|
3896
4282
|
/**
|
|
3897
4283
|
* Replace some state fields (values or functions). Last write wins.
|
|
4284
|
+
* @capability commands
|
|
3898
4285
|
* @example Show a command as busy while it works
|
|
3899
4286
|
* The Export item shows as busy with "Exporting…" as its reason while the
|
|
3900
4287
|
* export runs, then returns to normal. `set` replaces only the fields it
|
|
@@ -3920,15 +4307,47 @@ interface Command extends Disposable {
|
|
|
3920
4307
|
}>): void;
|
|
3921
4308
|
/**
|
|
3922
4309
|
* Resolves when the host has registered the command. Rejects with `conflict` (path already taken), `invalid` or `limit` (more than 160 commands in this plugin, or 1024 in the whole player). You rarely need to await it: during activation a failure fails activation, later it goes to Scope.onError.
|
|
4310
|
+
* @throws conflict (rejects) Another command already has this menu path, or the path would be both a menu item and a submenu.
|
|
4311
|
+
* @throws capability (rejects) The manifest does not declare `commands`.
|
|
4312
|
+
* @throws invalid (rejects) The player refused the declaration.
|
|
4313
|
+
* @throws limit (rejects) More than 160 commands in this plugin, or 1024 in the player.
|
|
3923
4314
|
*/
|
|
3924
4315
|
readonly ready: Promise<void>;
|
|
3925
4316
|
}
|
|
3926
4317
|
```
|
|
3927
4318
|
|
|
4319
|
+
Example: Enable an item only while a file is open
|
|
4320
|
+
|
|
4321
|
+
Video ▸ Export… is greyed out until a file is open. `set` replaces the
|
|
4322
|
+
`enabled` field with a function; the SDK evaluates it again after every
|
|
4323
|
+
player state change and sends the new state when it changes, so the
|
|
4324
|
+
item follows the player without further code.
|
|
4325
|
+
```ts
|
|
4326
|
+
const exportCommand = plugin.commands.add({
|
|
4327
|
+
id: "export",
|
|
4328
|
+
path: ["@video", "Export…"],
|
|
4329
|
+
run: runExport,
|
|
4330
|
+
});
|
|
4331
|
+
// A function: re-evaluated in every update pass.
|
|
4332
|
+
exportCommand.set({ enabled: () => plugin.player.state.phase === "ready" });
|
|
4333
|
+
```
|
|
4334
|
+
|
|
3928
4335
|
### Commands
|
|
3929
4336
|
|
|
3930
4337
|
`plugin.commands`: menu commands with default bindings. Needs `commands`.
|
|
3931
4338
|
|
|
4339
|
+
A command is one action of the plugin with a menu item (`path`), default
|
|
4340
|
+
keys (`bindings`), optionally a bottom-bar button (`button`) and a state
|
|
4341
|
+
the menu shows (`enabled`, `checked`, `busy`, `reason`). The player owns
|
|
4342
|
+
the menu, the binding table and the user's key overrides: it calls `run`
|
|
4343
|
+
when the user picks the item, presses a key or clicks the button.
|
|
4344
|
+
Commands of all plugins merge into the player's menus by their English
|
|
4345
|
+
path keys. Registration is asynchronous: `add` returns at once, and
|
|
4346
|
+
Command.ready reports the player's answer (during activation a
|
|
4347
|
+
refusal fails activation).
|
|
4348
|
+
|
|
4349
|
+
- @capability commands
|
|
4350
|
+
|
|
3932
4351
|
```ts
|
|
3933
4352
|
interface Commands {
|
|
3934
4353
|
/**
|
|
@@ -3938,6 +4357,7 @@ interface Commands {
|
|
|
3938
4357
|
* Titles and paths cannot change in place: put changing items (track
|
|
3939
4358
|
* lists, presets) in a child Scope and rebuild it.
|
|
3940
4359
|
* @throws invalid synchronously for a malformed path (including an unknown `@` menu id), order, group or binding; `conflict` for an id this plugin has already declared and not disposed.
|
|
4360
|
+
* @capability commands
|
|
3941
4361
|
* @example Pause playback from a menu item
|
|
3942
4362
|
* Playback ▸ Pause (and Ctrl+P) pauses and resumes, shows a check mark
|
|
3943
4363
|
* while paused and is greyed out when nothing can be paused. The player
|
|
@@ -3960,6 +4380,20 @@ interface Commands {
|
|
|
3960
4380
|
}
|
|
3961
4381
|
```
|
|
3962
4382
|
|
|
4383
|
+
Example: Add a menu item with a default key
|
|
4384
|
+
|
|
4385
|
+
Tools ▸ Say hello, or Ctrl+H, shows a greeting. `add` declares the item;
|
|
4386
|
+
the player calls `run` when the user picks it or presses its key, and the
|
|
4387
|
+
user can change the key in the player's key settings.
|
|
4388
|
+
```ts
|
|
4389
|
+
plugin.commands.add({
|
|
4390
|
+
id: "hello",
|
|
4391
|
+
path: ["Tools", "Say hello"],
|
|
4392
|
+
bindings: ["Ctrl+H"], // a default; the user may override it
|
|
4393
|
+
run: () => plugin.osd.show("Hello"),
|
|
4394
|
+
});
|
|
4395
|
+
```
|
|
4396
|
+
|
|
3963
4397
|
### ImageRef
|
|
3964
4398
|
|
|
3965
4399
|
An image file for a Slint `image` property, made by Ui.image.
|
|
@@ -5532,6 +5966,13 @@ interface DropHandler {
|
|
|
5532
5966
|
|
|
5533
5967
|
`plugin.drops`: files dropped on the video area. Needs `ui.drop`.
|
|
5534
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
|
+
|
|
5535
5976
|
```ts
|
|
5536
5977
|
interface Drops {
|
|
5537
5978
|
/**
|
|
@@ -5542,6 +5983,7 @@ interface Drops {
|
|
|
5542
5983
|
* to the provider that registered it first) in the order the file
|
|
5543
5984
|
* manager gave them, one call per provider. Folders are not expanded.
|
|
5544
5985
|
* @throws invalid synchronously for malformed extensions.
|
|
5986
|
+
* @capability ui.drop
|
|
5545
5987
|
* @example Load dropped subtitle files
|
|
5546
5988
|
* Dropping .srt or .ass files on the video loads each one as a subtitle
|
|
5547
5989
|
* track. Each extension belongs to the provider that registered it first;
|
|
@@ -5567,6 +6009,25 @@ interface Drops {
|
|
|
5567
6009
|
}
|
|
5568
6010
|
```
|
|
5569
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
|
+
|
|
5570
6031
|
### ThemeId
|
|
5571
6032
|
|
|
5572
6033
|
The player's palettes: `vestra`, the dark default, and `vestra-light`
|
|
@@ -6438,10 +6899,18 @@ interface TimelineMark {
|
|
|
6438
6899
|
|
|
6439
6900
|
This plugin's own marks; disposing it removes them.
|
|
6440
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
|
+
|
|
6441
6909
|
```ts
|
|
6442
6910
|
interface TimelineLayer extends Disposable {
|
|
6443
6911
|
/**
|
|
6444
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
|
|
6445
6914
|
* @example Show the file's bookmarks on the timeline
|
|
6446
6915
|
* Bookmark marks appear on the seek bar at their times, titled with the
|
|
6447
6916
|
* user's notes. `set` replaces this plugin's whole layer; with `ifCurrent`
|
|
@@ -6466,22 +6935,76 @@ interface TimelineLayer extends Disposable {
|
|
|
6466
6935
|
}
|
|
6467
6936
|
```
|
|
6468
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
|
+
|
|
6469
6947
|
### Timeline
|
|
6470
6948
|
|
|
6471
6949
|
`plugin.timeline`: marks on the seek bar. Every plugin publishes one
|
|
6472
6950
|
layer; the player merges them. A new source clears every layer.
|
|
6473
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
|
+
|
|
6474
6960
|
```ts
|
|
6475
6961
|
interface Timeline {
|
|
6476
6962
|
/** Every layer merged, by time (`playback.read`). Empty on a new source. */
|
|
6477
6963
|
readonly marks: readonly TimelineMark[];
|
|
6478
|
-
/**
|
|
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
|
+
*/
|
|
6479
6977
|
onChange(listener: (marks: readonly TimelineMark[]) => void): Disposable;
|
|
6480
|
-
/**
|
|
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
|
+
*/
|
|
6481
6994
|
layer(): TimelineLayer;
|
|
6482
6995
|
}
|
|
6483
6996
|
```
|
|
6484
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
|
+
|
|
6485
7008
|
### WindowState
|
|
6486
7009
|
|
|
6487
7010
|
The main window as `plugin.window.state` reports it (logical points).
|
|
@@ -6517,11 +7040,28 @@ With `events`, `state` follows the window from the first read or
|
|
|
6517
7040
|
`onChange`; without it, it updates only at start and after this plugin's
|
|
6518
7041
|
`setVideoArea`.
|
|
6519
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
|
+
|
|
6520
7050
|
```ts
|
|
6521
7051
|
interface MainWindow {
|
|
6522
7052
|
/** Latest state; zero video area without a native window. */
|
|
6523
7053
|
readonly state: WindowState;
|
|
6524
|
-
/**
|
|
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
|
+
*/
|
|
6525
7065
|
onChange(listener: (state: WindowState) => void): Disposable;
|
|
6526
7066
|
/**
|
|
6527
7067
|
* Resize the window so the video column is `width`×`height` (the bottom bar
|
|
@@ -6533,6 +7073,18 @@ interface MainWindow {
|
|
|
6533
7073
|
* Sizes are 1–16384 logical points; the minimum video area is 318×120.
|
|
6534
7074
|
* The visible area excludes the menu bar and Dock on macOS; on Windows it
|
|
6535
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
|
+
* ```
|
|
6536
7088
|
*/
|
|
6537
7089
|
setVideoArea(size: {
|
|
6538
7090
|
width: number;
|
|
@@ -6545,10 +7097,42 @@ interface MainWindow {
|
|
|
6545
7097
|
/** The request was scaled down or raised to fit. */
|
|
6546
7098
|
clamped: boolean;
|
|
6547
7099
|
}>;
|
|
6548
|
-
/**
|
|
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
|
+
*/
|
|
6549
7123
|
setAlwaysOnTop(onTop: boolean): Promise<void>;
|
|
6550
7124
|
/**
|
|
6551
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
|
+
* ```
|
|
6552
7136
|
*/
|
|
6553
7137
|
setCursorAutoHide(mode: "fullscreen" | "playing"): Promise<void>;
|
|
6554
7138
|
/**
|
|
@@ -6563,6 +7147,13 @@ interface MainWindow {
|
|
|
6563
7147
|
*
|
|
6564
7148
|
* Where it is unsupported (Windows, Linux), `setVideoArea` plus
|
|
6565
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`.
|
|
6566
7157
|
* @example Toggle picture-in-picture where the system has it
|
|
6567
7158
|
* The video moves into a floating picture-in-picture window, or back.
|
|
6568
7159
|
* `state.pictureInPicture` says whether the system supports it and
|
|
@@ -6626,9 +7217,29 @@ interface AppInfo {
|
|
|
6626
7217
|
`plugin.app`: the fixed facts plus the system, read on request. No
|
|
6627
7218
|
capability.
|
|
6628
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
|
+
|
|
6629
7227
|
```ts
|
|
6630
7228
|
interface App extends AppInfo {
|
|
6631
|
-
/**
|
|
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
|
+
*/
|
|
6632
7243
|
system(): Promise<SystemInfo>;
|
|
6633
7244
|
}
|
|
6634
7245
|
```
|
|
@@ -7071,29 +7682,100 @@ media.onEnd(async (ended) => {
|
|
|
7071
7682
|
|
|
7072
7683
|
A source this plugin opened.
|
|
7073
7684
|
|
|
7685
|
+
A source this plugin opened with Player.open. It does not own
|
|
7686
|
+
the source: the user or another plugin can replace it at any time, and
|
|
7687
|
+
`current` then turns false. `ready` and `onEnd` follow the player state:
|
|
7688
|
+
with `events` they react to the player's events, without it `ready`
|
|
7689
|
+
polls the state every 100 ms (which needs `playback.read`). Disposing the
|
|
7690
|
+
scope that opened it does not close the media; call `close` for that.
|
|
7691
|
+
|
|
7692
|
+
- @capability That of Player.open (`playback.control`); `ready` and `onEnd` follow the state through `events` and `playback.read`.
|
|
7693
|
+
|
|
7074
7694
|
```ts
|
|
7075
7695
|
interface Media {
|
|
7076
7696
|
/** Still the player's source (a restart keeps it current). */
|
|
7077
7697
|
readonly current: boolean;
|
|
7078
7698
|
/**
|
|
7079
7699
|
* Resolves when it is playing. Rejects with code `failed` (load error), `replaced` (another source took over) or `ended`; with `AbortError` when the signal aborts, after closing the media.
|
|
7700
|
+
* @capability That of Player.open; `playback.read` or `events` to see the state change.
|
|
7701
|
+
* @throws failed (rejects) The media could not be loaded.
|
|
7702
|
+
* @throws replaced (rejects) Another source took over first.
|
|
7703
|
+
* @throws ended (rejects) The media already played to the end.
|
|
7704
|
+
* @throws aborted (rejects) `signal` aborted (an `AbortError`); the media is closed.
|
|
7705
|
+
* @example Wait at most ten seconds for a stream to start
|
|
7706
|
+
* A network stream gets ten seconds to start playing; otherwise it is
|
|
7707
|
+
* closed and the user is told. Aborting the signal makes `ready` reject
|
|
7708
|
+
* with an `AbortError` after closing the media.
|
|
7709
|
+
* ```ts
|
|
7710
|
+
* const media = await plugin.player.open("https://example.com/live.m3u8");
|
|
7711
|
+
* // The timeout's abort closes the media and rejects with an AbortError.
|
|
7712
|
+
* try {
|
|
7713
|
+
* await media.ready({ signal: AbortSignal.timeout(10_000) });
|
|
7714
|
+
* } catch (error) {
|
|
7715
|
+
* await plugin.osd.show("Not playing: {}", [(error as Error).message]);
|
|
7716
|
+
* }
|
|
7717
|
+
* ```
|
|
7080
7718
|
*/
|
|
7081
7719
|
ready(options?: {
|
|
7082
7720
|
signal?: AbortSignal;
|
|
7083
7721
|
}): Promise<void>;
|
|
7084
|
-
/**
|
|
7722
|
+
/**
|
|
7723
|
+
* Called once each time a playthrough of this media ends naturally.
|
|
7724
|
+
* @capability That of Player.open; `events` to hear the end.
|
|
7725
|
+
* @example Play the next file when this one ends
|
|
7726
|
+
* When the plugin's file ends naturally, the next one opens. The listener
|
|
7727
|
+
* gets the state at the end; passing it as `ifCurrent` makes the next
|
|
7728
|
+
* `open` fail with `stale` (and do nothing) if the user opened something
|
|
7729
|
+
* else in the meantime.
|
|
7730
|
+
* ```ts
|
|
7731
|
+
* media.onEnd((ended) =>
|
|
7732
|
+
* plugin.player.open(next, { ifCurrent: ended }).catch((error: PluginError) => {
|
|
7733
|
+
* if (error.code !== "stale") throw error; // the user opened something else
|
|
7734
|
+
* }),
|
|
7735
|
+
* );
|
|
7736
|
+
* ```
|
|
7737
|
+
*/
|
|
7085
7738
|
onEnd(listener: (state: PlayerState) => void): Disposable;
|
|
7086
|
-
/**
|
|
7739
|
+
/**
|
|
7740
|
+
* Close it if it is still the player's source; otherwise do nothing.
|
|
7741
|
+
* @capability That of Player.open (`playback.control`).
|
|
7742
|
+
* @throws capability (rejects) The manifest does not declare `playback.control`.
|
|
7743
|
+
* @example Close the preview the plugin opened
|
|
7744
|
+
* The plugin closes the preview it opened, but leaves alone anything the
|
|
7745
|
+
* user opened since. `close` acts only while this media is still the
|
|
7746
|
+
* player's source and does nothing otherwise.
|
|
7747
|
+
* ```ts
|
|
7748
|
+
* await preview.close(); // a no-op when something else is playing now
|
|
7749
|
+
* ```
|
|
7750
|
+
*/
|
|
7087
7751
|
close(): Promise<void>;
|
|
7088
7752
|
}
|
|
7089
7753
|
```
|
|
7090
7754
|
|
|
7755
|
+
Example: Open a file and say when it ends
|
|
7756
|
+
|
|
7757
|
+
The file opens and plays, and the OSD says "Finished" when it reaches its
|
|
7758
|
+
end on its own. The handle belongs to this source: `onEnd` fires only
|
|
7759
|
+
for a natural end of this media, not when someone opens another file.
|
|
7760
|
+
```ts
|
|
7761
|
+
const media = await plugin.player.open("/Movies/clip.mp4");
|
|
7762
|
+
media.onEnd(() => void plugin.osd.show("Finished")); // natural ends only
|
|
7763
|
+
```
|
|
7764
|
+
|
|
7091
7765
|
### Scrub
|
|
7092
7766
|
|
|
7093
7767
|
A drag gesture: each seek is conditional on the previous one, latest value wins.
|
|
7094
7768
|
Any outside change (another plugin opens a file, the user pauses) ends
|
|
7095
7769
|
the gesture.
|
|
7096
7770
|
|
|
7771
|
+
A drag gesture on a seek slider. Each `seek` is conditional on the
|
|
7772
|
+
previous one and only the newest target is sent while a seek is
|
|
7773
|
+
running, so a fast drag never queues seeks. Any outside change (another
|
|
7774
|
+
file, the user pausing) ends the gesture: `active` turns false and later
|
|
7775
|
+
seeks resolve `false`.
|
|
7776
|
+
|
|
7777
|
+
- @capability playback.control
|
|
7778
|
+
|
|
7097
7779
|
```ts
|
|
7098
7780
|
interface Scrub {
|
|
7099
7781
|
/**
|
|
@@ -7113,6 +7795,11 @@ interface Scrub {
|
|
|
7113
7795
|
* the target of a keyframe seek is always sent. Omitted or `false`: exact,
|
|
7114
7796
|
* as before. Older players ignore the option and seek exactly: set
|
|
7115
7797
|
* `"minPlayerVersion": "0.5.3"` when you rely on it.
|
|
7798
|
+
* @capability playback.control
|
|
7799
|
+
* @throws capability (rejects) The manifest does not declare `playback.control`.
|
|
7800
|
+
* @throws stale (rejects) The source changed from outside while this seek was being sent (a superseded target or an ended gesture resolves `false` instead).
|
|
7801
|
+
* @throws unsupported (rejects) The source is a generated stream.
|
|
7802
|
+
* @throws backend (rejects) The player failed the seek.
|
|
7116
7803
|
* @example Scrub on keyframes for a fast picture
|
|
7117
7804
|
* While dragging, the picture jumps to the nearest earlier keyframe at
|
|
7118
7805
|
* once instead of decoding up to the exact frame. With `keyframes: true`
|
|
@@ -7128,7 +7815,17 @@ interface Scrub {
|
|
|
7128
7815
|
}): Promise<boolean>;
|
|
7129
7816
|
/** False once anyone else changed the source; later seeks are ignored. */
|
|
7130
7817
|
readonly active: boolean;
|
|
7131
|
-
/**
|
|
7818
|
+
/**
|
|
7819
|
+
* End the gesture; later seeks resolve `false`.
|
|
7820
|
+
* @capability playback.control
|
|
7821
|
+
* @example End the gesture when the user lets go
|
|
7822
|
+
* Releasing the slider ends the gesture, so a late `dragMove` cannot move
|
|
7823
|
+
* playback any more. After `end`, `seek` resolves `false` without doing
|
|
7824
|
+
* anything.
|
|
7825
|
+
* ```ts
|
|
7826
|
+
* bar.on("dragEnd", () => scrub.end()); // later seeks resolve false
|
|
7827
|
+
* ```
|
|
7828
|
+
*/
|
|
7132
7829
|
end(): void;
|
|
7133
7830
|
}
|
|
7134
7831
|
```
|
|
@@ -7206,6 +7903,15 @@ type StreamStartError = PluginError & {
|
|
|
7206
7903
|
|
|
7207
7904
|
A generated source, from Player.openStream. Disposing it stops it.
|
|
7208
7905
|
|
|
7906
|
+
A source whose frames this plugin generates, from
|
|
7907
|
+
Player.openStream. The SDK pulls frames on the audio clock over
|
|
7908
|
+
a local connection and handles prefill, activation and pausing; the plugin
|
|
7909
|
+
only supplies `frame()`. A stream ends when the plugin stops it, when
|
|
7910
|
+
another source replaces it or the user closes it (`onStop` with `null`),
|
|
7911
|
+
or when it fails (`onStop` with the error).
|
|
7912
|
+
|
|
7913
|
+
- @capability producer.av and playback.control
|
|
7914
|
+
|
|
7209
7915
|
```ts
|
|
7210
7916
|
interface Stream extends Disposable {
|
|
7211
7917
|
/** The stream is producing frames. */
|
|
@@ -7222,15 +7928,51 @@ interface Stream extends Disposable {
|
|
|
7222
7928
|
* (before 1.5.7 a replaced or closed stream could get `unavailable`
|
|
7223
7929
|
* "producer absent"). Without `playback.read` a failure the SDK has not
|
|
7224
7930
|
* seen yet is reported as null.
|
|
7931
|
+
* @capability producer.av
|
|
7932
|
+
* @example Tell the user why the stream stopped
|
|
7933
|
+
* The OSD explains a failure, and nothing is said when the user simply
|
|
7934
|
+
* opened another file. The listener runs only for stops from outside the
|
|
7935
|
+
* plugin: `null` for a replacement or the user's close, an error (such as
|
|
7936
|
+
* `stream` with the player's text) for a failure.
|
|
7937
|
+
* ```ts
|
|
7938
|
+
* stream.onStop((error) => {
|
|
7939
|
+
* if (error) void plugin.osd.show("Generator stopped: {}", [error.message]);
|
|
7940
|
+
* });
|
|
7941
|
+
* ```
|
|
7225
7942
|
*/
|
|
7226
7943
|
onStop(listener: (error: PluginError | null) => void): Disposable;
|
|
7227
7944
|
/**
|
|
7228
7945
|
* Stop producing and close the stream if it is still the player's source. The source closes first, then the frame connection, then the producer, so stopping never shows as a playback failure. `dispose()` and deactivation do the same.
|
|
7946
|
+
* @capability producer.av and playback.control
|
|
7947
|
+
* @throws Whatever code closing the player's source failed with (the frame connection and the producer are released either way). A stream that is no longer the source just resolves.
|
|
7948
|
+
* @example Stop the stream when the plugin's window closes
|
|
7949
|
+
* Closing the generator window stops the stream it feeds. `stop` closes
|
|
7950
|
+
* the player's source if it is still this stream and then the frame
|
|
7951
|
+
* connection; it resolves once both are closed.
|
|
7952
|
+
* ```ts
|
|
7953
|
+
* window.onClose(() => stream.stop());
|
|
7954
|
+
* ```
|
|
7229
7955
|
*/
|
|
7230
7956
|
stop(): Promise<void>;
|
|
7231
7957
|
}
|
|
7232
7958
|
```
|
|
7233
7959
|
|
|
7960
|
+
Example: Stop the stream from a menu item
|
|
7961
|
+
|
|
7962
|
+
A menu item ends the plugin's generated stream and closes it as the
|
|
7963
|
+
player's source. `stop` closes the source first and then the frame
|
|
7964
|
+
connection, so the user never sees a playback error; `onStop` is not
|
|
7965
|
+
called for the plugin's own stop.
|
|
7966
|
+
```ts
|
|
7967
|
+
// Enabled while the stream produces frames.
|
|
7968
|
+
plugin.commands.add({
|
|
7969
|
+
id: "stop-generator",
|
|
7970
|
+
path: ["@playback", "Stop generator"],
|
|
7971
|
+
enabled: () => stream.running,
|
|
7972
|
+
run: () => stream.stop(),
|
|
7973
|
+
});
|
|
7974
|
+
```
|
|
7975
|
+
|
|
7234
7976
|
### SequenceProvider
|
|
7235
7977
|
|
|
7236
7978
|
The plugin that owns "next / previous" (a playlist).
|
|
@@ -7337,15 +8079,67 @@ Capabilities: reading (`state` at start, `refresh`, `stats`, `chapters`,
|
|
|
7337
8079
|
Controls are conditional (see Conditional): they fail with
|
|
7338
8080
|
`stale` and do nothing when the source changed since the state you saw.
|
|
7339
8081
|
|
|
8082
|
+
`plugin.player` is the player's one source: what is open, its phase and
|
|
8083
|
+
position, and the controls. `state` is a snapshot the SDK keeps current
|
|
8084
|
+
from the player's events (with `events`) and refreshes after this
|
|
8085
|
+
plugin's own calls; `onChange` runs after it changed. Every control is
|
|
8086
|
+
conditional: it acts on the state the plugin last saw and fails with
|
|
8087
|
+
`stale` when the source changed meanwhile: another file, a close, or any
|
|
8088
|
+
pause, seek or speed change by the user or another plugin (see
|
|
8089
|
+
Conditional). The player keeps the source when a plugin stops;
|
|
8090
|
+
only the plugin's own handles (a stream, its sequence provider, its
|
|
8091
|
+
output device) are withdrawn.
|
|
8092
|
+
|
|
8093
|
+
- @capability `playback.read` to read; `playback.control` to control; `events` to follow changes made elsewhere; `producer.av` for Player.openStream.
|
|
8094
|
+
|
|
7340
8095
|
```ts
|
|
7341
8096
|
interface Player {
|
|
7342
8097
|
/** Latest state; always newer than anything delivered before. */
|
|
7343
8098
|
readonly state: PlayerState;
|
|
7344
|
-
/**
|
|
8099
|
+
/**
|
|
8100
|
+
* The state changed (needs `events` for outside changes).
|
|
8101
|
+
* @capability events (for changes made elsewhere; without it the listener runs only after this plugin's own calls)
|
|
8102
|
+
* @example Show the position while it plays
|
|
8103
|
+
* The OSD block shows the position, updated as playback moves. The SDK
|
|
8104
|
+
* updates `player.state` from the player's events and then calls the
|
|
8105
|
+
* listener with the new snapshot; the handler only reads it.
|
|
8106
|
+
* ```ts
|
|
8107
|
+
* const info = plugin.osd.panel();
|
|
8108
|
+
* plugin.player.onChange((state) => {
|
|
8109
|
+
* // `position` is null while nothing is open.
|
|
8110
|
+
* info.set(state.position === null ? null : "{} s", [state.position?.toFixed(0) ?? ""]);
|
|
8111
|
+
* });
|
|
8112
|
+
* ```
|
|
8113
|
+
*/
|
|
7345
8114
|
onChange(listener: (state: PlayerState) => void): Disposable;
|
|
7346
|
-
/**
|
|
8115
|
+
/**
|
|
8116
|
+
* Read the state from the player now (`playback.read`).
|
|
8117
|
+
* @capability playback.read
|
|
8118
|
+
* @throws capability (rejects) The manifest does not declare `playback.read`.
|
|
8119
|
+
* @example Read the state before deciding
|
|
8120
|
+
* A command reads the newest state from the player before acting, even
|
|
8121
|
+
* without `events`. `refresh` asks the player and resolves with the new
|
|
8122
|
+
* snapshot; `player.state` holds it afterwards.
|
|
8123
|
+
* ```ts
|
|
8124
|
+
* const state = await plugin.player.refresh();
|
|
8125
|
+
* if (state.phase === "ready") await plugin.osd.show("{} s left", [((state.duration ?? 0) - (state.position ?? 0)).toFixed(0)]);
|
|
8126
|
+
* ```
|
|
8127
|
+
*/
|
|
7347
8128
|
refresh(): Promise<PlayerState>;
|
|
7348
|
-
/**
|
|
8129
|
+
/**
|
|
8130
|
+
* True if nothing changed the source since `state` was observed.
|
|
8131
|
+
* @capability None.
|
|
8132
|
+
* @example Drop a result that arrives after the file changed
|
|
8133
|
+
* A slow lookup for the current file is shown only if that file is still
|
|
8134
|
+
* open. `isCurrent` compares the snapshot taken before the work with the
|
|
8135
|
+
* SDK's newest state; it is false once the source changed in any way:
|
|
8136
|
+
* another file, a close, or a pause, seek or speed change by anyone.
|
|
8137
|
+
* ```ts
|
|
8138
|
+
* const seen = plugin.player.state;
|
|
8139
|
+
* const title = await lookUpTitle(seen.source ?? "");
|
|
8140
|
+
* if (plugin.player.isCurrent(seen)) await plugin.osd.show("{}", [title]); // still the same file
|
|
8141
|
+
* ```
|
|
8142
|
+
*/
|
|
7349
8143
|
isCurrent(state: PlayerState): boolean;
|
|
7350
8144
|
/**
|
|
7351
8145
|
* Open a path or URL, replacing the current source (unless `ifCurrent` no
|
|
@@ -7356,6 +8150,25 @@ interface Player {
|
|
|
7356
8150
|
* Resolver are resolved first, and so are local files of an
|
|
7357
8151
|
* extension a file resolver claims (the source is then a stream that
|
|
7358
8152
|
* plugin generates; `paused` reaches it).
|
|
8153
|
+
* @capability playback.control
|
|
8154
|
+
* @throws stale (rejects) `ifCurrent` no longer holds; nothing was opened.
|
|
8155
|
+
* @throws replaced (rejects) Accepted, then another source took over before it finished (for example while a URL was still being resolved).
|
|
8156
|
+
* @throws invalid (rejects) An empty `source`.
|
|
8157
|
+
* @throws aborted (rejects) `signal` aborted (an `AbortError`); the source is closed if it was opened.
|
|
8158
|
+
* @throws busy (rejects) The address needs a resolver plugin and the player is already resolving 32 addresses.
|
|
8159
|
+
* @throws backend (rejects) A resolver plugin handled the address or file and failed (its message), or the resolution was cancelled or expired.
|
|
8160
|
+
* @throws unsupported (rejects) The player refused what a resolver returned (for example split streams with different headers).
|
|
8161
|
+
* @throws unavailable (rejects) The stream a file resolver prepared was not ready to play.
|
|
8162
|
+
* @throws capability (rejects) The manifest does not declare `playback.control`.
|
|
8163
|
+
* @example Open a file paused
|
|
8164
|
+
* The file opens at its start without playing. `open` resolves once the
|
|
8165
|
+
* player accepted the load and returns a Media handle for that
|
|
8166
|
+
* source; `ready` then waits until it can play (here: until it is loaded
|
|
8167
|
+
* and paused).
|
|
8168
|
+
* ```ts
|
|
8169
|
+
* const media = await plugin.player.open("/Movies/clip.mp4", { paused: true });
|
|
8170
|
+
* await media.ready(); // rejects `failed` when the file cannot be loaded
|
|
8171
|
+
* ```
|
|
7359
8172
|
*/
|
|
7360
8173
|
open(source: string, options?: {
|
|
7361
8174
|
paused?: boolean;
|
|
@@ -7372,6 +8185,13 @@ interface Player {
|
|
|
7372
8185
|
* TCP with a token on Windows. Rejects with a StreamStartError
|
|
7373
8186
|
* (`timeout`), `stream`, `device`, or `replaced` when another source
|
|
7374
8187
|
* replaced it (or it was closed) before it ran.
|
|
8188
|
+
* @capability producer.av and playback.control
|
|
8189
|
+
* @throws timeout (rejects) A StreamStartError: the stream did not start in time.
|
|
8190
|
+
* @throws stream (rejects) The player failed to start the stream.
|
|
8191
|
+
* @throws device (rejects) The audio device was not ready.
|
|
8192
|
+
* @throws replaced (rejects) Another source replaced it, or it was closed, before it ran.
|
|
8193
|
+
* @throws invalid (rejects) Bad options (a size or a frame function).
|
|
8194
|
+
* @throws aborted (rejects) `signal` aborted, or the scope was disposed, while the stream was starting (an `AbortError`).
|
|
7375
8195
|
* @example Play pictures the plugin draws itself
|
|
7376
8196
|
* The player shows a 320×240 picture the plugin renders, with silence as
|
|
7377
8197
|
* its sound, as the current source. The SDK pulls frames on the audio
|
|
@@ -7392,25 +8212,143 @@ interface Player {
|
|
|
7392
8212
|
* ```
|
|
7393
8213
|
*/
|
|
7394
8214
|
openStream(options: StreamOptions): Promise<Stream>;
|
|
7395
|
-
/**
|
|
8215
|
+
/**
|
|
8216
|
+
* Pause or resume.
|
|
8217
|
+
* @capability playback.control
|
|
8218
|
+
* @throws stale (rejects) The source changed since the state the call is based on (another file, a close, or any pause, seek or speed change by the user or another plugin); nothing was done.
|
|
8219
|
+
* @throws invalid (rejects) Nothing is open.
|
|
8220
|
+
* @throws busy (rejects) A generated stream is still handling a previous control.
|
|
8221
|
+
* @throws capability (rejects) The manifest does not declare `playback.control`.
|
|
8222
|
+
* @example Pause, but only the file the user was watching
|
|
8223
|
+
* A "pause on lock" helper pauses the current file and does nothing when
|
|
8224
|
+
* another file was opened in between. The call is conditional on
|
|
8225
|
+
* `player.state` by default; `stale` means the source changed and the
|
|
8226
|
+
* pause was not applied, which is the intended outcome here.
|
|
8227
|
+
* ```ts
|
|
8228
|
+
* try {
|
|
8229
|
+
* await plugin.player.setPaused(true);
|
|
8230
|
+
* } catch (error) {
|
|
8231
|
+
* if ((error as PluginError).code !== "stale") throw error; // another file now
|
|
8232
|
+
* }
|
|
8233
|
+
* ```
|
|
8234
|
+
*/
|
|
7396
8235
|
setPaused(paused: boolean, options?: Conditional): Promise<void>;
|
|
7397
|
-
/**
|
|
8236
|
+
/**
|
|
8237
|
+
* Jump to `seconds`.
|
|
8238
|
+
* @capability playback.control
|
|
8239
|
+
* @throws stale (rejects) The source changed since the state the call is based on (another file, a close, or any pause, seek or speed change by the user or another plugin); nothing was done.
|
|
8240
|
+
* @throws invalid (rejects) Nothing is open, or `seconds` is negative or not finite.
|
|
8241
|
+
* @throws unsupported (rejects) The source is a generated stream, which cannot seek.
|
|
8242
|
+
* @throws busy (rejects) A generated stream is still handling a previous control.
|
|
8243
|
+
* @throws capability (rejects) The manifest does not declare `playback.control`.
|
|
8244
|
+
* @example Jump back ten seconds
|
|
8245
|
+
* Playback jumps ten seconds back, never before the start. The target is
|
|
8246
|
+
* computed from the snapshot the call is conditional on, so if the user
|
|
8247
|
+
* opened another file meanwhile the seek fails with `stale` instead of
|
|
8248
|
+
* landing in the wrong file.
|
|
8249
|
+
* ```ts
|
|
8250
|
+
* const { position } = plugin.player.state;
|
|
8251
|
+
* if (position !== null && plugin.player.state.can.seek)
|
|
8252
|
+
* await plugin.player.seek(Math.max(0, position - 10)); // seconds
|
|
8253
|
+
* ```
|
|
8254
|
+
*/
|
|
7398
8255
|
seek(seconds: number, options?: Conditional): Promise<void>;
|
|
7399
|
-
/**
|
|
8256
|
+
/**
|
|
8257
|
+
* Set the playback speed.
|
|
8258
|
+
* @capability playback.control
|
|
8259
|
+
* @throws stale (rejects) The source changed since the state the call is based on (another file, a close, or any pause, seek or speed change by the user or another plugin); nothing was done.
|
|
8260
|
+
* @throws invalid (rejects) Nothing is open, or `rate` is outside 0.05–10.
|
|
8261
|
+
* @throws unsupported (rejects) The source is a generated stream, which cannot change speed.
|
|
8262
|
+
* @throws busy (rejects) A generated stream is still handling a previous control.
|
|
8263
|
+
* @throws capability (rejects) The manifest does not declare `playback.control`.
|
|
8264
|
+
* @example Play at one and a half times the speed
|
|
8265
|
+
* Playback speeds up to 1.5×, with the speed on the OSD. `setRate` sets
|
|
8266
|
+
* the speed directly (the player's own keys step it by 0.1 within
|
|
8267
|
+
* 0.25–4); `state.can.rate` is false for sources that cannot change speed.
|
|
8268
|
+
* ```ts
|
|
8269
|
+
* if (plugin.player.state.can.rate) {
|
|
8270
|
+
* await plugin.player.setRate(1.5);
|
|
8271
|
+
* await plugin.osd.show("Speed {}×", [1.5]);
|
|
8272
|
+
* }
|
|
8273
|
+
* ```
|
|
8274
|
+
*/
|
|
7400
8275
|
setRate(rate: number, options?: Conditional): Promise<void>;
|
|
7401
|
-
/**
|
|
8276
|
+
/**
|
|
8277
|
+
* Play from the start (the "replay" after the end).
|
|
8278
|
+
* @capability playback.control
|
|
8279
|
+
* @throws stale (rejects) The source changed since the state the call is based on (another file, a close, or any pause, seek or speed change by the user or another plugin); nothing was done.
|
|
8280
|
+
* @throws invalid (rejects) Nothing is open.
|
|
8281
|
+
* @throws unsupported (rejects) The source is a generated stream, which cannot restart.
|
|
8282
|
+
* @throws capability (rejects) The manifest does not declare `playback.control`.
|
|
8283
|
+
* @example Replay a file after it ended
|
|
8284
|
+
* A Replay button starts the file again from its start once it has ended.
|
|
8285
|
+
* `restart` plays the current source from the beginning and keeps it the
|
|
8286
|
+
* same source, so a Media handle stays current.
|
|
8287
|
+
* ```ts
|
|
8288
|
+
* if (plugin.player.state.phase === "ended") await plugin.player.restart();
|
|
8289
|
+
* ```
|
|
8290
|
+
*/
|
|
7402
8291
|
restart(options?: Conditional): Promise<void>;
|
|
7403
|
-
/**
|
|
8292
|
+
/**
|
|
8293
|
+
* Close the current source.
|
|
8294
|
+
* @capability playback.control
|
|
8295
|
+
* @throws stale (rejects) The source changed since the state the call is based on (another file, a close, or any pause, seek or speed change by the user or another plugin); nothing was done.
|
|
8296
|
+
* @throws capability (rejects) The manifest does not declare `playback.control`.
|
|
8297
|
+
* @example Close the file when the sleep timer runs out
|
|
8298
|
+
* After 30 minutes the open file is closed, unless the user opened another
|
|
8299
|
+
* one meanwhile. The snapshot taken when the timer started is passed as
|
|
8300
|
+
* `ifCurrent`, so the close applies only to that source; `stale` means it
|
|
8301
|
+
* was replaced.
|
|
8302
|
+
* ```ts
|
|
8303
|
+
* const seen = plugin.player.state;
|
|
8304
|
+
* setTimeout(() => {
|
|
8305
|
+
* // Only the source that was open when the timer started.
|
|
8306
|
+
* plugin.player.close({ ifCurrent: seen }).catch((error: PluginError) => {
|
|
8307
|
+
* if (error.code !== "stale") plugin.osd.show("Could not close: {}", [error.code]);
|
|
8308
|
+
* });
|
|
8309
|
+
* }, 30 * 60 * 1000);
|
|
8310
|
+
* ```
|
|
8311
|
+
*/
|
|
7404
8312
|
close(options?: Conditional): Promise<void>;
|
|
7405
|
-
/**
|
|
8313
|
+
/**
|
|
8314
|
+
* Start a drag gesture from the current state.
|
|
8315
|
+
* @capability playback.control
|
|
8316
|
+
* @example Start a gesture when the user grabs the slider
|
|
8317
|
+
* A drag on the plugin's seek slider becomes one gesture whose seeks never
|
|
8318
|
+
* pile up. `scrub()` captures the current state; the gesture's seeks are
|
|
8319
|
+
* conditional on it and on each other, and it ends on its own when
|
|
8320
|
+
* someone else changes the source.
|
|
8321
|
+
* ```ts
|
|
8322
|
+
* let scrub: Scrub | null = null;
|
|
8323
|
+
* bar.on("dragStart", () => (scrub = plugin.player.scrub()));
|
|
8324
|
+
* bar.on("dragEnd", () => scrub?.end());
|
|
8325
|
+
* ```
|
|
8326
|
+
*/
|
|
7406
8327
|
scrub(): Scrub;
|
|
7407
|
-
/**
|
|
8328
|
+
/**
|
|
8329
|
+
* Device output; last write wins and volume is applied before mute. Resolves `true` when this request was applied, `false` if superseded. `volume` is 0–200.
|
|
8330
|
+
* @capability playback.control
|
|
8331
|
+
* @throws invalid (rejects) `volume` outside 0–200, or `muted` not a boolean.
|
|
8332
|
+
* @throws capability (rejects) The manifest does not declare `playback.control`.
|
|
8333
|
+
* @example Duck the volume during an announcement
|
|
8334
|
+
* The volume drops to 30 while the plugin's announcement plays, then
|
|
8335
|
+
* returns. Writes are "latest wins": the promise resolves `true` when this
|
|
8336
|
+
* write was applied and `false` when a newer one replaced it first, which
|
|
8337
|
+
* is not an error.
|
|
8338
|
+
* ```ts
|
|
8339
|
+
* const before = plugin.player.state.volume; // 0–200
|
|
8340
|
+
* await plugin.player.setOutput({ volume: 30 });
|
|
8341
|
+
* // … the announcement …
|
|
8342
|
+
* await plugin.player.setOutput({ volume: before });
|
|
8343
|
+
* ```
|
|
8344
|
+
*/
|
|
7408
8345
|
setOutput(output: {
|
|
7409
8346
|
volume?: number;
|
|
7410
8347
|
muted?: boolean;
|
|
7411
8348
|
}): Promise<boolean>;
|
|
7412
8349
|
/**
|
|
7413
|
-
* Become the sequence provider (
|
|
8350
|
+
* Become the sequence provider, first come first served: while another plugin holds it (the built-in playlist registers at activation), this one is refused with `conflict` (`sequence-taken`). That does not fail activation: the player shows a warning (PluginInfo.warnings); a refusal after activation goes to the scope's `onError`. Dispose to stop.
|
|
8351
|
+
* @capability playback.control
|
|
7414
8352
|
* @example Drive the player's Next and Previous from a playlist
|
|
7415
8353
|
* The player's Next and Previous items and keys play the plugin's list.
|
|
7416
8354
|
* The player calls `next` and `previous` when the user asks; `canNext`
|
|
@@ -7432,19 +8370,99 @@ interface Player {
|
|
|
7432
8370
|
* ```
|
|
7433
8371
|
*/
|
|
7434
8372
|
provideSequence(provider: SequenceProvider): Disposable;
|
|
7435
|
-
/**
|
|
8373
|
+
/**
|
|
8374
|
+
* Ask the provider to go forward / back. Rejects `unavailable` when it cannot.
|
|
8375
|
+
* @capability playback.control
|
|
8376
|
+
* @throws unavailable (rejects) There is no sequence provider, or it has no next item.
|
|
8377
|
+
* @throws capability (rejects) The manifest does not declare `playback.control`.
|
|
8378
|
+
* @example Skip to the next item from a plugin button
|
|
8379
|
+
* The plugin's own Next button plays the next item of whichever plugin
|
|
8380
|
+
* provides the sequence (the playlist, usually). `next` asks the
|
|
8381
|
+
* provider, the same as the player's Next key; `unavailable` means there
|
|
8382
|
+
* is no next item, which the button treats as the end of the list.
|
|
8383
|
+
* ```ts
|
|
8384
|
+
* try {
|
|
8385
|
+
* await plugin.player.next(); // as the player's Next key does
|
|
8386
|
+
* } catch (error) {
|
|
8387
|
+
* if ((error as PluginError).code !== "unavailable") throw error;
|
|
8388
|
+
* await plugin.osd.show("End of the list");
|
|
8389
|
+
* }
|
|
8390
|
+
* ```
|
|
8391
|
+
*/
|
|
7436
8392
|
next(): Promise<void>;
|
|
7437
|
-
/**
|
|
8393
|
+
/**
|
|
8394
|
+
* Ask the provider to go back. Rejects `unavailable` when it cannot.
|
|
8395
|
+
* @capability playback.control
|
|
8396
|
+
* @throws unavailable (rejects) There is no sequence provider, or it has no previous item.
|
|
8397
|
+
* @throws capability (rejects) The manifest does not declare `playback.control`.
|
|
8398
|
+
* @example Go back to the previous item
|
|
8399
|
+
* The plugin's Previous button asks the sequence provider for the item
|
|
8400
|
+
* before the current one, like the player's PageUp. It is disabled while
|
|
8401
|
+
* `state.can.previous` is false, which the provider's `canPrevious`
|
|
8402
|
+
* decides.
|
|
8403
|
+
* ```ts
|
|
8404
|
+
* plugin.commands.add({
|
|
8405
|
+
* id: "previous",
|
|
8406
|
+
* title: "Previous item",
|
|
8407
|
+
* // The provider's `canPrevious`, as the player reports it.
|
|
8408
|
+
* enabled: () => plugin.player.state.can.previous,
|
|
8409
|
+
* run: () => plugin.player.previous(),
|
|
8410
|
+
* });
|
|
8411
|
+
* ```
|
|
8412
|
+
*/
|
|
7438
8413
|
previous(): Promise<void>;
|
|
7439
|
-
/**
|
|
8414
|
+
/**
|
|
8415
|
+
* Codec, size, frame counts and backend of the current playback. `playback.read`; `video` and `audio` are `null` without a source.
|
|
8416
|
+
* @capability playback.read
|
|
8417
|
+
* @throws capability (rejects) The manifest does not declare `playback.read`.
|
|
8418
|
+
* @example Show the video codec and size
|
|
8419
|
+
* The OSD shows something like "h264 1920×1080". `stats` asks the player
|
|
8420
|
+
* for the backend's own values at that moment; `video` is `null` without
|
|
8421
|
+
* a video stream (nothing open, an audio file).
|
|
8422
|
+
* ```ts
|
|
8423
|
+
* const { video } = await plugin.player.stats();
|
|
8424
|
+
* if (video) await plugin.osd.show("{} {}×{}", [video.codec ?? "?", video.width ?? 0, video.height ?? 0]);
|
|
8425
|
+
* ```
|
|
8426
|
+
*/
|
|
7440
8427
|
stats(): Promise<PlaybackStats>;
|
|
7441
8428
|
/**
|
|
7442
8429
|
* Add an external audio file (absolute local path) to the current media and, unless `select: false`, select it. Resolves with the new track (`external`, not exportable). It stays until the source changes. Rejects `invalid` for a bad path, `unavailable` without ready media, `backend` when it cannot load.
|
|
8430
|
+
* @capability playback.control
|
|
8431
|
+
* @throws invalid (rejects) Not an absolute path to a file.
|
|
8432
|
+
* @throws unavailable (rejects) No ready media.
|
|
8433
|
+
* @throws backend (rejects) The player could not load the file.
|
|
8434
|
+
* @throws stale (rejects) `ifCurrent` no longer holds.
|
|
8435
|
+
* @throws capability (rejects) The manifest does not declare `playback.control`.
|
|
8436
|
+
* @example Add a dubbed audio file and switch to it
|
|
8437
|
+
* A separate audio file plays with the film instead of its own sound.
|
|
8438
|
+
* `addAudioTrack` attaches the file to the current media and selects it;
|
|
8439
|
+
* the track stays until another source opens.
|
|
8440
|
+
* ```ts
|
|
8441
|
+
* const track = await plugin.player.addAudioTrack(dubPath); // selected too
|
|
8442
|
+
* await plugin.osd.show("Audio: {}", [track.title ?? dubPath]);
|
|
8443
|
+
* ```
|
|
7443
8444
|
*/
|
|
7444
8445
|
addAudioTrack(path: string, options?: {
|
|
7445
8446
|
select?: boolean;
|
|
7446
8447
|
} & Conditional): Promise<AudioTrack>;
|
|
7447
|
-
/**
|
|
8448
|
+
/**
|
|
8449
|
+
* Select one of `state.audioTracks` by id; `invalid` for an unknown id.
|
|
8450
|
+
* @capability playback.control
|
|
8451
|
+
* @throws invalid (rejects) No track with this id.
|
|
8452
|
+
* @throws unavailable (rejects) No ready media.
|
|
8453
|
+
* @throws stale (rejects) The source changed since the state the call is based on (another file, a close, or any pause, seek or speed change by the user or another plugin).
|
|
8454
|
+
* @throws capability (rejects) The manifest does not declare `playback.control`.
|
|
8455
|
+
* @example Switch to the next audio track
|
|
8456
|
+
* Each press of the plugin's key moves to the next audio track of the
|
|
8457
|
+
* file. The tracks and the selected one are in `state.audioTracks`;
|
|
8458
|
+
* `selectAudioTrack` resolves with the new state.
|
|
8459
|
+
* ```ts
|
|
8460
|
+
* const audioTracks = plugin.player.state.audioTracks ?? []; // null: not known yet
|
|
8461
|
+
* const at = audioTracks.findIndex((track) => track.selected);
|
|
8462
|
+
* const next = audioTracks[(at + 1) % audioTracks.length];
|
|
8463
|
+
* if (next) await plugin.player.selectAudioTrack(next.id);
|
|
8464
|
+
* ```
|
|
8465
|
+
*/
|
|
7448
8466
|
selectAudioTrack(id: number, options?: Conditional): Promise<PlayerState>;
|
|
7449
8467
|
/**
|
|
7450
8468
|
* One frame or to the next / previous keyframe (pauses for frames). Rejects
|
|
@@ -7454,6 +8472,19 @@ interface Player {
|
|
|
7454
8472
|
* Resolves once the new position is in effect, so `player.state.position`
|
|
7455
8473
|
* is current afterwards. One step at a time: a second call while one is
|
|
7456
8474
|
* in flight rejects `busy`.
|
|
8475
|
+
* @capability playback.control
|
|
8476
|
+
* @throws unsupported (rejects) Nothing playable to step through (no moving video: audio only, cover art, a generated stream), a backward frame step on a source that cannot seek, or a keyframe step in either direction on a source that cannot seek.
|
|
8477
|
+
* @throws busy (rejects) A step is still in flight.
|
|
8478
|
+
* @throws stale (rejects) The source changed since the state the call is based on (another file, a close, or any pause, seek or speed change by the user or another plugin).
|
|
8479
|
+
* @throws capability (rejects) The manifest does not declare `playback.control`.
|
|
8480
|
+
* @example Step one frame forward for a close look
|
|
8481
|
+
* The picture advances by one frame and playback pauses. `step` resolves
|
|
8482
|
+
* once the new position is in effect, so `state.position` is current
|
|
8483
|
+
* afterwards; a second step while one is running rejects with `busy`.
|
|
8484
|
+
* ```ts
|
|
8485
|
+
* await plugin.player.step("frame", "forward");
|
|
8486
|
+
* await plugin.osd.show("{} s", [plugin.player.state.position?.toFixed(3) ?? "?"]);
|
|
8487
|
+
* ```
|
|
7457
8488
|
*/
|
|
7458
8489
|
step(unit: "frame" | "keyframe", direction: "backward" | "forward", options?: Conditional): Promise<void>;
|
|
7459
8490
|
/**
|
|
@@ -7476,12 +8507,37 @@ interface Player {
|
|
|
7476
8507
|
*
|
|
7477
8508
|
* Older players ignore `keyframes`: set `"minPlayerVersion": "0.5.1"` in
|
|
7478
8509
|
* the manifest when you rely on it.
|
|
8510
|
+
* @capability playback.control
|
|
8511
|
+
* @throws invalid (rejects) `seconds` outside 0.1–3600 or not in tenths, or `keyframes` not a boolean.
|
|
8512
|
+
* @throws capability (rejects) The manifest does not declare `playback.control`.
|
|
8513
|
+
* @example Make ← and → jump 10 seconds to keyframes
|
|
8514
|
+
* The arrow keys now jump 10 seconds and land on keyframes, which is fast
|
|
8515
|
+
* on large files. The newest call of any plugin wins and is withdrawn
|
|
8516
|
+
* when that plugin stops, so a plugin offering this as a setting applies
|
|
8517
|
+
* it on activation.
|
|
8518
|
+
* ```ts
|
|
8519
|
+
* await plugin.player.setStepSeek({ seconds: 10, keyframes: true });
|
|
8520
|
+
* ```
|
|
7479
8521
|
*/
|
|
7480
8522
|
setStepSeek(options: {
|
|
7481
8523
|
seconds: number;
|
|
7482
8524
|
keyframes?: boolean;
|
|
7483
8525
|
}): Promise<void>;
|
|
7484
|
-
/**
|
|
8526
|
+
/**
|
|
8527
|
+
* The chapters of the current media (empty without media or chapters), read on demand; `sourceId` tells which source they belong to. `playback.read`.
|
|
8528
|
+
* @capability playback.read
|
|
8529
|
+
* @throws capability (rejects) The manifest does not declare `playback.read`.
|
|
8530
|
+
* @example List the chapters of the open file
|
|
8531
|
+
* The plugin reads the chapter titles of the current file to show them in
|
|
8532
|
+
* its panel. `chapters` asks the player when called; `sourceId` says which
|
|
8533
|
+
* source they belong to, so a reply for a file that was replaced meanwhile
|
|
8534
|
+
* can be dropped.
|
|
8535
|
+
* ```ts
|
|
8536
|
+
* const { sourceId, chapters } = await plugin.player.chapters();
|
|
8537
|
+
* if (sourceId === plugin.player.state.sourceId) // still the same file
|
|
8538
|
+
* console.log(chapters.map((chapter) => chapter.title));
|
|
8539
|
+
* ```
|
|
8540
|
+
*/
|
|
7485
8541
|
chapters(): Promise<{
|
|
7486
8542
|
/** The source the chapters belong to (compare with `state.sourceId`). */
|
|
7487
8543
|
sourceId: number;
|
|
@@ -7490,13 +8546,45 @@ interface Player {
|
|
|
7490
8546
|
}>;
|
|
7491
8547
|
/**
|
|
7492
8548
|
* A-B loop: on reaching `b`, jump back to `a` (seconds, `0 ≤ a < b ≤ duration`), `count` times (positive integer; omitted = endless); `null` stops it. After the last jump back `state.loop` becomes null. `invalid` out of range, `unsupported` for a source that cannot seek, `unavailable` without ready media. A new source clears it.
|
|
8549
|
+
* @capability playback.control
|
|
8550
|
+
* @throws invalid (rejects) `a`/`b` out of range or `count` not a positive integer.
|
|
8551
|
+
* @throws unsupported (rejects) The source cannot seek, or is a generated stream.
|
|
8552
|
+
* @throws unavailable (rejects) No ready media.
|
|
8553
|
+
* @throws stale (rejects) The source changed since the state the call is based on (another file, a close, or any pause, seek or speed change by the user or another plugin).
|
|
8554
|
+
* @throws capability (rejects) The manifest does not declare `playback.control`.
|
|
8555
|
+
* @example Loop a passage three times
|
|
8556
|
+
* Playback repeats 1:00–1:30 three times, then goes on. The player jumps
|
|
8557
|
+
* back to `a` each time it reaches `b`; after the last repeat
|
|
8558
|
+
* `state.loop` becomes `null`, and a new source clears the loop.
|
|
8559
|
+
* ```ts
|
|
8560
|
+
* await plugin.player.setLoop({ a: 60, b: 90, count: 3 }); // seconds
|
|
8561
|
+
* ```
|
|
7493
8562
|
*/
|
|
7494
8563
|
setLoop(loop: {
|
|
7495
8564
|
a: number;
|
|
7496
8565
|
b: number;
|
|
7497
8566
|
count?: number;
|
|
7498
8567
|
} | null, options?: Conditional): Promise<void>;
|
|
7499
|
-
/**
|
|
8568
|
+
/**
|
|
8569
|
+
* The output devices present now (not the system default entry). `playback.read`.
|
|
8570
|
+
* @capability playback.read
|
|
8571
|
+
* @throws capability (rejects) The manifest does not declare `playback.read`.
|
|
8572
|
+
* @example Offer the audio output devices in a menu
|
|
8573
|
+
* A submenu lists the output devices present now, with a radio mark on the
|
|
8574
|
+
* one in use. `audioDevices` asks the player when called (devices come and
|
|
8575
|
+
* go), and `state.audioOutput` tells which one is active.
|
|
8576
|
+
* ```ts
|
|
8577
|
+
* // The devices present now; build the menu again when they change.
|
|
8578
|
+
* for (const device of await plugin.player.audioDevices())
|
|
8579
|
+
* plugin.commands.add({
|
|
8580
|
+
* id: `device.${device.id}`,
|
|
8581
|
+
* path: ["@audio", "Output", device.name],
|
|
8582
|
+
* radio: true,
|
|
8583
|
+
* checked: () => plugin.player.state.audioOutput?.active === device.id,
|
|
8584
|
+
* run: () => plugin.player.setAudioDevice(device.id),
|
|
8585
|
+
* });
|
|
8586
|
+
* ```
|
|
8587
|
+
*/
|
|
7500
8588
|
audioDevices(): Promise<AudioDevice[]>;
|
|
7501
8589
|
/**
|
|
7502
8590
|
* Play on this device while it is present, else on the system default
|
|
@@ -7506,6 +8594,18 @@ interface Player {
|
|
|
7506
8594
|
* Unknown ids are accepted (`state.audioOutput.active` stays `null`); a
|
|
7507
8595
|
* malformed id is `invalid`. Reverts to the default when this plugin
|
|
7508
8596
|
* stops; save the choice yourself to restore it on activation.
|
|
8597
|
+
* @capability playback.control
|
|
8598
|
+
* @throws invalid (rejects) A malformed id.
|
|
8599
|
+
* @throws capability (rejects) The manifest does not declare `playback.control`.
|
|
8600
|
+
* @example Restore the user's output device on start
|
|
8601
|
+
* The plugin plays on the device the user picked last time, or on the
|
|
8602
|
+
* system default when it is not connected. The choice lasts only for
|
|
8603
|
+
* this run and is withdrawn when the plugin stops, so the plugin saves
|
|
8604
|
+
* it itself and sets it again on activation.
|
|
8605
|
+
* ```ts
|
|
8606
|
+
* const store = plugin.storage.json("output", { defaults: { device: null as string | null } });
|
|
8607
|
+
* await plugin.player.setAudioDevice(store.value.device); // null: the system default
|
|
8608
|
+
* ```
|
|
7509
8609
|
*/
|
|
7510
8610
|
setAudioDevice(id: string | null): Promise<void>;
|
|
7511
8611
|
}
|
|
@@ -7558,6 +8658,15 @@ type HostAction = "play-pause" | "stop" | "previous" | "next" | "frame-back" | "
|
|
|
7558
8658
|
|
|
7559
8659
|
`plugin.host`: the player's own core actions.
|
|
7560
8660
|
|
|
8661
|
+
The player's core actions (play/pause, stop, step seeks, speed, volume,
|
|
8662
|
+
fullscreen…) are commands of the player itself, `host/<action>` in the
|
|
8663
|
+
binding table. Running them through `host.run` gives a plugin's own
|
|
8664
|
+
buttons exactly the player's behaviour: the same checks, the same
|
|
8665
|
+
accumulation of repeated presses and the same on-screen text, instead of
|
|
8666
|
+
reimplementing them with Player calls.
|
|
8667
|
+
|
|
8668
|
+
- @capability commands
|
|
8669
|
+
|
|
7561
8670
|
```ts
|
|
7562
8671
|
interface Host {
|
|
7563
8672
|
/**
|
|
@@ -7567,6 +8676,10 @@ interface Host {
|
|
|
7567
8676
|
* `invalid` for an unknown id. Needs `commands`.
|
|
7568
8677
|
*
|
|
7569
8678
|
* Prefer this over reimplementing playback buttons.
|
|
8679
|
+
* @capability commands
|
|
8680
|
+
* @throws unavailable (rejects) The action is disabled right now (nothing open, no window for `fullscreen`).
|
|
8681
|
+
* @throws invalid (rejects) An unknown action, or `seconds` out of range.
|
|
8682
|
+
* @throws capability (rejects) The manifest does not declare `commands`.
|
|
7570
8683
|
* @example Make a bar button play and pause like Space
|
|
7571
8684
|
* The plugin's own Play button behaves exactly like the player's Space
|
|
7572
8685
|
* key, including its on-screen text. `host.run` runs the core action
|
|
@@ -7597,6 +8710,10 @@ interface Host {
|
|
|
7597
8710
|
* once the seek is submitted, as before. `invalid` for `seconds` out of
|
|
7598
8711
|
* range.
|
|
7599
8712
|
* @player 0.5.1 (set `"minPlayerVersion": "0.5.1"`: older players ignore `seconds` and seek by the `setStepSeek` step)
|
|
8713
|
+
* @capability commands
|
|
8714
|
+
* @throws unavailable (rejects) The action is disabled right now (nothing open, no window for `fullscreen`).
|
|
8715
|
+
* @throws invalid (rejects) An unknown action, or `seconds` out of range.
|
|
8716
|
+
* @throws capability (rejects) The manifest does not declare `commands`.
|
|
7600
8717
|
* @example Add a 30-second skip that adds up with the arrow keys
|
|
7601
8718
|
* The plugin's "+30 s" button seeks forward 30 seconds and shows the same
|
|
7602
8719
|
* text as →. Passing `seconds` makes the core step seek use that step
|
|
@@ -7612,6 +8729,21 @@ interface Host {
|
|
|
7612
8729
|
}
|
|
7613
8730
|
```
|
|
7614
8731
|
|
|
8732
|
+
Example: Give a custom bar the player's own Stop and Mute
|
|
8733
|
+
|
|
8734
|
+
The plugin's bar buttons stop playback and mute exactly as the player's
|
|
8735
|
+
own keys do, with the same on-screen text. Each callback runs a core
|
|
8736
|
+
action; the player applies its checks and rejects with `unavailable`
|
|
8737
|
+
while the action is disabled, which the bar ignores.
|
|
8738
|
+
```ts
|
|
8739
|
+
const run = (action: HostAction) =>
|
|
8740
|
+
plugin.host.run(action).catch((error: PluginError) => {
|
|
8741
|
+
if (error.code !== "unavailable") throw error; // disabled right now
|
|
8742
|
+
});
|
|
8743
|
+
bar.on("stop", () => run("stop"));
|
|
8744
|
+
bar.on("mute", () => run("mute"));
|
|
8745
|
+
```
|
|
8746
|
+
|
|
7615
8747
|
### OsdPanel
|
|
7616
8748
|
|
|
7617
8749
|
This plugin's information block on the OSD: multi-line text over the
|
|
@@ -7835,6 +8967,14 @@ one request is in flight at a time and only the newest value is sent.
|
|
|
7835
8967
|
Values out of range fail with `invalid` (see VideoNode). The
|
|
7836
8968
|
same chain applies to processed snapshots and exports.
|
|
7837
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
|
+
|
|
7838
8978
|
```ts
|
|
7839
8979
|
type VideoFilter = Filter<VideoNode[]>;
|
|
7840
8980
|
```
|
|
@@ -7862,6 +9002,12 @@ VideoFilter.
|
|
|
7862
9002
|
`delayMs` is −10000–10000 in multiples of 10; the positive and the
|
|
7863
9003
|
negative delays of all plugins each add up to at most 10000.
|
|
7864
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
|
+
|
|
7865
9011
|
```ts
|
|
7866
9012
|
type AudioFilter = Filter<{
|
|
7867
9013
|
/** The audio chain; `[]` for none. */
|
|
@@ -7912,13 +9058,46 @@ needed); disposing or stopping the owner restores the defaults.
|
|
|
7912
9058
|
Out-of-range values or unknown fields fail with `invalid` without
|
|
7913
9059
|
sending anything.
|
|
7914
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
|
+
|
|
7915
9069
|
```ts
|
|
7916
9070
|
interface VideoView extends Disposable {
|
|
7917
|
-
/**
|
|
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
|
+
*/
|
|
7918
9083
|
set(value: Partial<ViewSettings>): void;
|
|
7919
9084
|
/** Current status (the same four states as filters). */
|
|
7920
9085
|
readonly status: FilterStatus;
|
|
7921
|
-
/**
|
|
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
|
+
*/
|
|
7922
9101
|
onStatus(listener: (status: FilterStatus) => void): Disposable;
|
|
7923
9102
|
}
|
|
7924
9103
|
```
|
|
@@ -7928,7 +9107,7 @@ Example: Fill the window, then zoom in
|
|
|
7928
9107
|
The video fills the video area (cropped instead of letterboxed), then
|
|
7929
9108
|
zooms to 150 % shifted a little to the right. The first plugin to call
|
|
7930
9109
|
`view()` owns the display layer; each `set` merges into its settings,
|
|
7931
|
-
which stay across sources until the plugin
|
|
9110
|
+
which stay across sources until the plugin disposes the view or stops.
|
|
7932
9111
|
```ts
|
|
7933
9112
|
const view = plugin.video.view();
|
|
7934
9113
|
view.set({ fit: "fill" });
|
|
@@ -7966,13 +9145,45 @@ Platforms: only macOS renders on the GPU. Under software rendering
|
|
|
7966
9145
|
`unsupported`. macOS OpenGL 4.1 has no compute shaders, so `peakDetect`
|
|
7967
9146
|
is accepted but has no effect; output is always SDR.
|
|
7968
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
|
+
|
|
7969
9157
|
```ts
|
|
7970
9158
|
interface RenderOptions extends Disposable {
|
|
7971
|
-
/**
|
|
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
|
+
*/
|
|
7972
9170
|
set(value: Partial<RenderSettings>): void;
|
|
7973
9171
|
/** `failed` with `unsupported` while the player renders in software; `failed` with `failed` when mpv cannot compile a shader (pushed later). */
|
|
7974
9172
|
readonly status: FilterStatus;
|
|
7975
|
-
/**
|
|
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
|
+
*/
|
|
7976
9187
|
onStatus(listener: (status: FilterStatus) => void): Disposable;
|
|
7977
9188
|
}
|
|
7978
9189
|
```
|
|
@@ -8074,16 +9285,79 @@ interface SubtitleConditional {
|
|
|
8074
9285
|
`subtitles.read`; changes need `subtitles.control` and also
|
|
8075
9286
|
`subtitles.read`, since they read the current selection first.
|
|
8076
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
|
+
|
|
8077
9302
|
```ts
|
|
8078
9303
|
interface Subtitles {
|
|
8079
9304
|
/** `null` until first read. */
|
|
8080
9305
|
readonly state: SubtitlesState | null;
|
|
8081
|
-
/**
|
|
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
|
+
*/
|
|
8082
9322
|
onChange(listener: (state: SubtitlesState) => void): Disposable;
|
|
8083
|
-
/**
|
|
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
|
+
*/
|
|
8084
9336
|
refresh(): Promise<SubtitlesState>;
|
|
8085
9337
|
/**
|
|
8086
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
|
+
* ```
|
|
8087
9361
|
*/
|
|
8088
9362
|
select(track: SubtitleTrack | null, options?: SubtitleConditional & {
|
|
8089
9363
|
role?: "primary" | "secondary";
|
|
@@ -8100,6 +9374,25 @@ interface Subtitles {
|
|
|
8100
9374
|
* Limits: SRT ≤ 2 MiB and ≤ 10,000 cues; ASS ≤ 2 MiB with `[Script Info]`
|
|
8101
9375
|
* and `[Events]` and 1–20,000 `Dialogue:` lines. Convert other formats
|
|
8102
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
|
+
* ```
|
|
8103
9396
|
*/
|
|
8104
9397
|
load(bytes: Uint8Array, options?: SubtitleConditional & {
|
|
8105
9398
|
format?: "srt" | "ass";
|
|
@@ -8107,10 +9400,40 @@ interface Subtitles {
|
|
|
8107
9400
|
select?: "primary" | "secondary" | "none";
|
|
8108
9401
|
signal?: AbortSignal;
|
|
8109
9402
|
}): Promise<SubtitleTrack>;
|
|
8110
|
-
/**
|
|
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
|
+
*/
|
|
8111
9419
|
remove(track: SubtitleTrack, options?: SubtitleConditional): Promise<SubtitlesState>;
|
|
8112
9420
|
/**
|
|
8113
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
|
+
* ```
|
|
8114
9437
|
*/
|
|
8115
9438
|
setStyle(style: SubtitleStyle | null, options?: SubtitleConditional): Promise<SubtitlesState>;
|
|
8116
9439
|
}
|
|
@@ -8161,6 +9484,14 @@ interface CaptureOptions extends Conditional {
|
|
|
8161
9484
|
|
|
8162
9485
|
A frozen, immutable input for exports. Dispose to release it.
|
|
8163
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
|
+
|
|
8164
9495
|
```ts
|
|
8165
9496
|
interface Snapshot extends Disposable {
|
|
8166
9497
|
/** Without or with effects. */
|
|
@@ -8182,11 +9513,45 @@ interface Snapshot extends Disposable {
|
|
|
8182
9513
|
} | null;
|
|
8183
9514
|
/** The frozen processing revision (processed snapshots). */
|
|
8184
9515
|
readonly processingRevision: number | null;
|
|
8185
|
-
/**
|
|
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
|
+
*/
|
|
8186
9535
|
export(options: ExportOptions): Promise<ExportJob>;
|
|
8187
9536
|
}
|
|
8188
9537
|
```
|
|
8189
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
|
+
|
|
8190
9555
|
### ExportOptions
|
|
8191
9556
|
|
|
8192
9557
|
One frame as PNG, JPEG (`quality` 1–100, default 90) or BMP (24-bit), or
|
|
@@ -8239,31 +9604,121 @@ type JobState = Omit<MediaJob, "jobId">;
|
|
|
8239
9604
|
|
|
8240
9605
|
An export, from Snapshot.export. Disposing an unfinished job cancels it.
|
|
8241
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
|
+
|
|
8242
9616
|
```ts
|
|
8243
9617
|
interface ExportJob extends Disposable {
|
|
8244
9618
|
/** Latest state. */
|
|
8245
9619
|
readonly state: JobState;
|
|
8246
|
-
/**
|
|
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
|
+
*/
|
|
8247
9631
|
onChange(listener: (state: JobState) => void): Disposable;
|
|
8248
|
-
/**
|
|
9632
|
+
/**
|
|
9633
|
+
* Resolves with the terminal state (completed, failed or cancelled).
|
|
9634
|
+
* @throws disposed (rejects) The job handle was disposed while waiting.
|
|
9635
|
+
*/
|
|
8249
9636
|
readonly done: Promise<JobState>;
|
|
8250
|
-
/**
|
|
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
|
+
*/
|
|
8251
9652
|
cancel(): Promise<"requested" | "tooLate" | "terminal">;
|
|
8252
|
-
/**
|
|
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
|
+
*/
|
|
8253
9666
|
refresh(): Promise<JobState>;
|
|
8254
9667
|
}
|
|
8255
9668
|
```
|
|
8256
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
|
+
|
|
8257
9683
|
### MediaCapture
|
|
8258
9684
|
|
|
8259
9685
|
`plugin.media`: snapshots and exports. Needs `media.read` and
|
|
8260
9686
|
`media.control` (processed snapshots also `playback.read` and
|
|
8261
9687
|
`subtitles.read`). Exports decode offline and do not disturb playback.
|
|
8262
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
|
+
|
|
8263
9697
|
```ts
|
|
8264
9698
|
interface MediaCapture {
|
|
8265
9699
|
/**
|
|
8266
|
-
* 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
|
+
* ```
|
|
8267
9722
|
*/
|
|
8268
9723
|
capture(options?: CaptureOptions): Promise<Snapshot>;
|
|
8269
9724
|
}
|
|
@@ -8416,25 +9871,84 @@ interface ResolvedStream {
|
|
|
8416
9871
|
|
|
8417
9872
|
A registered resolver. Disposing it unregisters it and aborts resolutions in flight.
|
|
8418
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
|
+
|
|
8419
9880
|
```ts
|
|
8420
9881
|
interface Resolver extends Disposable {
|
|
8421
9882
|
/**
|
|
8422
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`.
|
|
8423
9888
|
*/
|
|
8424
9889
|
readonly ready: Promise<void>;
|
|
8425
9890
|
}
|
|
8426
9891
|
```
|
|
8427
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
|
+
|
|
8428
9913
|
### Resolvers
|
|
8429
9914
|
|
|
8430
9915
|
`plugin.resolvers`: turn web page URLs into playable streams, or local
|
|
8431
9916
|
files into streams this plugin generates. Needs `source.resolve` (file
|
|
8432
9917
|
resolvers also `producer.av`).
|
|
8433
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
|
+
|
|
8434
9929
|
```ts
|
|
8435
9930
|
interface Resolvers {
|
|
8436
9931
|
/**
|
|
8437
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
|
+
* ```
|
|
8438
9952
|
*/
|
|
8439
9953
|
add(options: ResolverOptions): Resolver;
|
|
8440
9954
|
/**
|
|
@@ -8587,6 +10101,15 @@ interface InstallOptions {
|
|
|
8587
10101
|
|
|
8588
10102
|
A validated package directory, from Packages.validate.
|
|
8589
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
|
+
|
|
8590
10113
|
```ts
|
|
8591
10114
|
interface PackageCandidate {
|
|
8592
10115
|
/** The package directory. */
|
|
@@ -8599,11 +10122,38 @@ interface PackageCandidate {
|
|
|
8599
10122
|
readonly checks: string[];
|
|
8600
10123
|
/**
|
|
8601
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
|
+
* ```
|
|
8602
10141
|
*/
|
|
8603
10142
|
install(options?: InstallOptions): Promise<PackageChange>;
|
|
8604
10143
|
}
|
|
8605
10144
|
```
|
|
8606
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
|
+
|
|
8607
10157
|
### PackageChangeStatus
|
|
8608
10158
|
|
|
8609
10159
|
The progress of an install or uninstall.
|
|
@@ -8617,21 +10167,71 @@ type PackageChangeStatus = "preparing" | "activating" | "restoring" | "committed
|
|
|
8617
10167
|
An install or uninstall in progress. The SDK never submits a
|
|
8618
10168
|
change twice: after a lost reply it only queries the same request.
|
|
8619
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
|
+
|
|
8620
10178
|
```ts
|
|
8621
10179
|
interface PackageChange {
|
|
8622
10180
|
/** Current status. */
|
|
8623
10181
|
readonly status: PackageChangeStatus;
|
|
8624
10182
|
/** Details for failures, `null` otherwise. */
|
|
8625
10183
|
readonly message: string | null;
|
|
8626
|
-
/**
|
|
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
|
+
*/
|
|
8627
10200
|
onChange(listener: (status: PackageChangeStatus) => void): Disposable;
|
|
8628
|
-
/**
|
|
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
|
+
*/
|
|
8629
10205
|
readonly done: Promise<PackageChangeStatus>;
|
|
8630
|
-
/**
|
|
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
|
+
*/
|
|
8631
10221
|
cancel(): Promise<void>;
|
|
8632
10222
|
}
|
|
8633
10223
|
```
|
|
8634
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
|
+
|
|
8635
10235
|
### PluginInfo
|
|
8636
10236
|
|
|
8637
10237
|
One plugin as the player knows it (`packages.list`).
|
|
@@ -8713,6 +10313,14 @@ interface PluginWarning {
|
|
|
8713
10313
|
`plugin.packages`: the plugins the player knows. `list` and `onChange`
|
|
8714
10314
|
need no capability; everything else needs `plugins.manage`.
|
|
8715
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
|
+
|
|
8716
10324
|
```ts
|
|
8717
10325
|
interface Packages {
|
|
8718
10326
|
/**
|
|
@@ -8723,18 +10331,93 @@ interface Packages {
|
|
|
8723
10331
|
* The player keeps the 32 most recent candidates of all plugins; an
|
|
8724
10332
|
* older one's `install` fails with `stale` (validate again). Before
|
|
8725
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
|
+
* ```
|
|
8726
10356
|
*/
|
|
8727
10357
|
validate(directory: string): Promise<PackageCandidate>;
|
|
8728
10358
|
/**
|
|
8729
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
|
+
* ```
|
|
8730
10378
|
*/
|
|
8731
10379
|
validateArchive(file: string): Promise<PackageCandidate>;
|
|
8732
|
-
/**
|
|
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
|
+
*/
|
|
8733
10394
|
list(): Promise<PluginInfo[]>;
|
|
8734
|
-
/**
|
|
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
|
+
*/
|
|
8735
10405
|
onChange(listener: (plugins: PluginInfo[]) => void): Disposable;
|
|
8736
10406
|
/**
|
|
8737
|
-
* 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
|
+
* ```
|
|
8738
10421
|
*/
|
|
8739
10422
|
setEnabled(id: string, enabled: boolean): Promise<void>;
|
|
8740
10423
|
/**
|
|
@@ -8752,10 +10435,38 @@ interface Packages {
|
|
|
8752
10435
|
* `invalid` for oneself or a disabled plugin (enable it instead),
|
|
8753
10436
|
* `missing` unknown, `busy` while it is being installed or reloaded.
|
|
8754
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
|
+
* ```
|
|
8755
10451
|
*/
|
|
8756
10452
|
reload(id: string): Promise<void>;
|
|
8757
10453
|
/**
|
|
8758
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
|
+
* ```
|
|
8759
10470
|
*/
|
|
8760
10471
|
uninstall(id: string, options?: {
|
|
8761
10472
|
removeData?: boolean;
|
|
@@ -8830,9 +10541,31 @@ interface BindingInfo extends BindingRef {
|
|
|
8830
10541
|
`plugin.bindings`: list and override every command's bindings, including
|
|
8831
10542
|
the player's. Needs `bindings.manage`.
|
|
8832
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
|
+
|
|
8833
10552
|
```ts
|
|
8834
10553
|
interface Bindings {
|
|
8835
|
-
/**
|
|
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
|
+
*/
|
|
8836
10569
|
list(): Promise<{
|
|
8837
10570
|
/** Every declared command (running plugins and the player). */
|
|
8838
10571
|
commands: BindingInfo[];
|
|
@@ -8846,11 +10579,47 @@ interface Bindings {
|
|
|
8846
10579
|
}>;
|
|
8847
10580
|
/**
|
|
8848
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
|
+
* ```
|
|
8849
10593
|
*/
|
|
8850
10594
|
set(ref: BindingRef, inputs: string[]): Promise<void>;
|
|
8851
|
-
/**
|
|
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
|
+
*/
|
|
8852
10608
|
reset(ref?: BindingRef): Promise<void>;
|
|
8853
|
-
/**
|
|
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
|
+
*/
|
|
8854
10623
|
onChange(listener: () => void): Disposable;
|
|
8855
10624
|
}
|
|
8856
10625
|
```
|
|
@@ -8876,13 +10645,65 @@ PNG/JPEG bytes; Windows writes Unicode text and images as a bitmap plus
|
|
|
8876
10645
|
PNG. Other systems: `unsupported`. Failures: `invalid`, `missing`,
|
|
8877
10646
|
`unavailable`, `failed` (the system refused).
|
|
8878
10647
|
|
|
10648
|
+
The player reads and writes the system clipboard on the plugin's behalf.
|
|
10649
|
+
Text is plain text; images are written from a PNG or JPEG file.
|
|
10650
|
+
|
|
10651
|
+
- @capability clipboard
|
|
10652
|
+
|
|
8879
10653
|
```ts
|
|
8880
10654
|
interface Clipboard {
|
|
8881
|
-
/**
|
|
10655
|
+
/**
|
|
10656
|
+
* Plain text on the clipboard; `null` when it holds none.
|
|
10657
|
+
* @capability clipboard
|
|
10658
|
+
* @throws unavailable (rejects) The player runs without a user interface.
|
|
10659
|
+
* @throws unsupported (rejects) A system other than macOS and Windows.
|
|
10660
|
+
* @throws failed (rejects) The system refused (Windows).
|
|
10661
|
+
* @throws capability (rejects) The manifest does not declare `clipboard`.
|
|
10662
|
+
* @example Open a URL the user copied
|
|
10663
|
+
* A "Paste and play" command opens the address on the clipboard. `readText`
|
|
10664
|
+
* resolves with the clipboard's text, or `null` when it holds none (an
|
|
10665
|
+
* image, nothing).
|
|
10666
|
+
* ```ts
|
|
10667
|
+
* const text = await plugin.clipboard.readText();
|
|
10668
|
+
* if (text?.startsWith("https://")) await plugin.player.open(text.trim());
|
|
10669
|
+
* ```
|
|
10670
|
+
*/
|
|
8882
10671
|
readText(): Promise<string | null>;
|
|
8883
|
-
/**
|
|
10672
|
+
/**
|
|
10673
|
+
* Replace the clipboard with text (at most 1 MiB of UTF-8).
|
|
10674
|
+
* @capability clipboard
|
|
10675
|
+
* @throws invalid (rejects) More than 1 MiB of UTF-8.
|
|
10676
|
+
* @throws unavailable (rejects) The player runs without a user interface.
|
|
10677
|
+
* @throws unsupported (rejects) A system other than macOS and Windows.
|
|
10678
|
+
* @throws failed (rejects) The system refused.
|
|
10679
|
+
* @throws capability (rejects) The manifest does not declare `clipboard`.
|
|
10680
|
+
* @example Copy the playing file's path
|
|
10681
|
+
* The path of the playing file is put on the clipboard, replacing what was
|
|
10682
|
+
* there. It resolves once the system has it.
|
|
10683
|
+
* ```ts
|
|
10684
|
+
* const { source } = plugin.player.state;
|
|
10685
|
+
* if (source) await plugin.clipboard.writeText(source);
|
|
10686
|
+
* ```
|
|
10687
|
+
*/
|
|
8884
10688
|
writeText(text: string): Promise<void>;
|
|
8885
|
-
/**
|
|
10689
|
+
/**
|
|
10690
|
+
* Replace the clipboard with a PNG or JPEG file (absolute path, at most 16 MB).
|
|
10691
|
+
* @capability clipboard
|
|
10692
|
+
* @throws missing (rejects) The file does not exist.
|
|
10693
|
+
* @throws invalid (rejects) Not an absolute path, not PNG or JPEG, over 16 MB, or (Windows) an image the system cannot decode.
|
|
10694
|
+
* @throws unavailable (rejects) The player runs without a user interface.
|
|
10695
|
+
* @throws unsupported (rejects) A system other than macOS and Windows.
|
|
10696
|
+
* @throws failed (rejects) The system refused.
|
|
10697
|
+
* @throws capability (rejects) The manifest does not declare `clipboard`.
|
|
10698
|
+
* @example Copy a snapshot to the clipboard
|
|
10699
|
+
* A frame the plugin saved as PNG is put on the clipboard, ready to paste
|
|
10700
|
+
* into another application. The player reads the file and writes the
|
|
10701
|
+
* image (macOS: its original bytes; Windows: a bitmap plus PNG).
|
|
10702
|
+
* ```ts
|
|
10703
|
+
* await plugin.clipboard.writeImage(pngPath); // absolute path, at most 16 MB
|
|
10704
|
+
* await plugin.osd.show("Snapshot copied");
|
|
10705
|
+
* ```
|
|
10706
|
+
*/
|
|
8886
10707
|
writeImage(path: string): Promise<void>;
|
|
8887
10708
|
}
|
|
8888
10709
|
```
|
|
@@ -8906,6 +10727,12 @@ try {
|
|
|
8906
10727
|
|
|
8907
10728
|
Keeping the computer awake (`playback.control`).
|
|
8908
10729
|
|
|
10730
|
+
A request, not a command: the player keeps the system awake while any
|
|
10731
|
+
plugin asks for it and a source plays, and withdraws a plugin's request
|
|
10732
|
+
when it stops.
|
|
10733
|
+
|
|
10734
|
+
- @capability playback.control
|
|
10735
|
+
|
|
8909
10736
|
```ts
|
|
8910
10737
|
interface Power {
|
|
8911
10738
|
/**
|
|
@@ -8917,11 +10744,31 @@ interface Power {
|
|
|
8917
10744
|
* take the system assertion are logged by the player, not reported.
|
|
8918
10745
|
* macOS uses `caffeinate`, Windows the thread execution state; elsewhere
|
|
8919
10746
|
* it has no effect.
|
|
10747
|
+
* @capability playback.control
|
|
10748
|
+
* @throws invalid (rejects) A mode other than `"off"` or `"playing"`.
|
|
10749
|
+
* @throws capability (rejects) The manifest does not declare `playback.control`.
|
|
10750
|
+
* @example Follow the plugin's own "Keep awake" setting
|
|
10751
|
+
* The plugin keeps the display awake during playback only while its
|
|
10752
|
+
* setting is on. Each call replaces this plugin's request; `"off"`
|
|
10753
|
+
* withdraws it, and other plugins' requests still count.
|
|
10754
|
+
* ```ts
|
|
10755
|
+
* const store = plugin.storage.json("power", { defaults: { keepAwake: true } });
|
|
10756
|
+
* await plugin.power.preventSleep(store.value.keepAwake ? "playing" : "off");
|
|
10757
|
+
* ```
|
|
8920
10758
|
*/
|
|
8921
10759
|
preventSleep(mode: "off" | "playing"): Promise<void>;
|
|
8922
10760
|
}
|
|
8923
10761
|
```
|
|
8924
10762
|
|
|
10763
|
+
Example: Keep the display on while playing
|
|
10764
|
+
|
|
10765
|
+
The screen does not dim or sleep while a video plays, and may again when
|
|
10766
|
+
it is paused or ends. The player takes the system's assertion only while
|
|
10767
|
+
a source is ready and not paused.
|
|
10768
|
+
```ts
|
|
10769
|
+
await plugin.power.preventSleep("playing"); // "off" withdraws the request
|
|
10770
|
+
```
|
|
10771
|
+
|
|
8925
10772
|
### ToolResult
|
|
8926
10773
|
|
|
8927
10774
|
The output of Tools.run.
|
|
@@ -8940,11 +10787,20 @@ interface ToolResult {
|
|
|
8940
10787
|
`plugin.tools`: ffmpeg and ffprobe, run in this plugin's process. No
|
|
8941
10788
|
capability.
|
|
8942
10789
|
|
|
10790
|
+
ffmpeg and ffprobe run as child processes of the plugin. When
|
|
10791
|
+
`VESTRA_FFMPEG` / `VESTRA_FFPROBE` is set, only that path is used (an
|
|
10792
|
+
invalid one means the tool is not found); otherwise the tool is searched
|
|
10793
|
+
on `PATH`. A found path is kept for the plugin's run, a miss is looked up
|
|
10794
|
+
again next time. Disposing the
|
|
10795
|
+
scope ends a running process.
|
|
10796
|
+
|
|
10797
|
+
- @capability None.
|
|
10798
|
+
|
|
8943
10799
|
```ts
|
|
8944
10800
|
interface Tools {
|
|
8945
10801
|
/**
|
|
8946
|
-
* Run ffmpeg or ffprobe (
|
|
8947
|
-
*
|
|
10802
|
+
* Run ffmpeg or ffprobe (`VESTRA_FFMPEG` / `VESTRA_FFPROBE` when set, else
|
|
10803
|
+
* found on `PATH`) without a shell. Rejects `unavailable` (not found), `timeout`,
|
|
8948
10804
|
* `failed` (non-zero exit; `message` is the last stderr line), `invalid`
|
|
8949
10805
|
* (output over `maxOutput`, default 16 MiB), `aborted` (`signal`). Timeout,
|
|
8950
10806
|
* `signal` and disposing the scope end the process.
|
|
@@ -8952,13 +10808,50 @@ interface Tools {
|
|
|
8952
10808
|
* Arguments are passed as-is (no quoting needed). On Windows `PATHEXT` is
|
|
8953
10809
|
* honoured. Ending sends SIGTERM, then SIGKILL after 300 ms (immediate on
|
|
8954
10810
|
* Windows).
|
|
10811
|
+
* @capability None.
|
|
10812
|
+
* @throws unavailable (rejects) The tool was not found.
|
|
10813
|
+
* @throws timeout (rejects) It ran longer than `timeoutMs`; the process is ended.
|
|
10814
|
+
* @throws failed (rejects) A non-zero exit; `message` is the last stderr line.
|
|
10815
|
+
* @throws invalid (rejects) Bad arguments or options, or output over `maxOutput`.
|
|
10816
|
+
* @throws aborted (rejects) `signal` aborted, or the scope was disposed.
|
|
10817
|
+
* @example Cut a clip without re-encoding
|
|
10818
|
+
* The plugin writes the first ten seconds of the file to a new file. `run`
|
|
10819
|
+
* starts ffmpeg without a shell (arguments pass as they are, no quoting)
|
|
10820
|
+
* and resolves with its output once it exits with 0; a failure rejects
|
|
10821
|
+
* with the last line ffmpeg printed.
|
|
10822
|
+
* ```ts
|
|
10823
|
+
* await plugin.tools.run(
|
|
10824
|
+
* "ffmpeg",
|
|
10825
|
+
* ["-y", "-i", input, "-t", "10", "-c", "copy", output],
|
|
10826
|
+
* { timeoutMs: 60_000 }, // give up after a minute
|
|
10827
|
+
* );
|
|
10828
|
+
* ```
|
|
8955
10829
|
*/
|
|
8956
10830
|
run(tool: "ffmpeg" | "ffprobe", args: string[], options: {
|
|
8957
10831
|
timeoutMs: number;
|
|
8958
10832
|
maxOutput?: number;
|
|
8959
10833
|
signal?: AbortSignal;
|
|
8960
10834
|
}): Promise<ToolResult>;
|
|
8961
|
-
/**
|
|
10835
|
+
/**
|
|
10836
|
+
* Whether the tool can be found.
|
|
10837
|
+
* @capability None.
|
|
10838
|
+
* @throws Never; it resolves `false` when the tool cannot be found.
|
|
10839
|
+
* @example Grey out a command when ffmpeg is missing
|
|
10840
|
+
* The Export item is disabled with a reason when ffmpeg is not installed.
|
|
10841
|
+
* `available` looks the tool up (a found path is kept, a miss is looked
|
|
10842
|
+
* up again on the next call, so installing it later is noticed).
|
|
10843
|
+
* ```ts
|
|
10844
|
+
* const hasFfmpeg = await plugin.tools.available("ffmpeg");
|
|
10845
|
+
* plugin.commands.add({
|
|
10846
|
+
* id: "export",
|
|
10847
|
+
* path: ["@video", "Export…"],
|
|
10848
|
+
* enabled: hasFfmpeg,
|
|
10849
|
+
* // Shown with the greyed-out item.
|
|
10850
|
+
* reason: hasFfmpeg ? null : "Install ffmpeg to export",
|
|
10851
|
+
* run() {},
|
|
10852
|
+
* });
|
|
10853
|
+
* ```
|
|
10854
|
+
*/
|
|
8962
10855
|
available(tool: "ffmpeg" | "ffprobe"): Promise<boolean>;
|
|
8963
10856
|
}
|
|
8964
10857
|
```
|
|
@@ -8986,21 +10879,75 @@ if (await plugin.tools.available("ffprobe")) {
|
|
|
8986
10879
|
|
|
8987
10880
|
A JSON file `dataDirectory/<name>.json`, loaded once and saved debounced.
|
|
8988
10881
|
|
|
10882
|
+
One JSON file, `<dataDirectory>/<name>.json`, made by
|
|
10883
|
+
Storage.json. `value` is read once when the store is made;
|
|
10884
|
+
`save` replaces it in memory at once and writes the file `debounceMs`
|
|
10885
|
+
later (300 by default) through a temporary file and a rename, so a
|
|
10886
|
+
crash never leaves half a file. Writes to one file run one at a time,
|
|
10887
|
+
newest last. Disposing the store, its scope or stopping the plugin
|
|
10888
|
+
writes a pending save first.
|
|
10889
|
+
|
|
10890
|
+
- @capability None.
|
|
10891
|
+
|
|
8989
10892
|
```ts
|
|
8990
10893
|
interface JsonStore<T> extends Disposable {
|
|
8991
10894
|
/** The loaded value, or `defaults` when the file was missing or broken. */
|
|
8992
10895
|
readonly value: T;
|
|
8993
|
-
/**
|
|
10896
|
+
/**
|
|
10897
|
+
* Replace the value now and write it after `debounceMs`. Only `save` writes: mutating `value` in place is not saved.
|
|
10898
|
+
* @capability None.
|
|
10899
|
+
* @example Save a slider value without writing on every move
|
|
10900
|
+
* Dragging the plugin's volume slider updates the stored value at once but
|
|
10901
|
+
* writes the file only once the user stops for 300 ms. Each `save`
|
|
10902
|
+
* replaces the value and restarts the debounce timer.
|
|
10903
|
+
* ```ts
|
|
10904
|
+
* const store = plugin.storage.json("prefs", { defaults: { volume: 100 } });
|
|
10905
|
+
* window.on("volumeChanged", (volume: number) => store.save({ ...store.value, volume }));
|
|
10906
|
+
* ```
|
|
10907
|
+
*/
|
|
8994
10908
|
save(next: T): void;
|
|
8995
|
-
/**
|
|
10909
|
+
/**
|
|
10910
|
+
* Write a pending save now.
|
|
10911
|
+
* @capability None.
|
|
10912
|
+
* @throws Error (rejects) Node's file system error when the file cannot be written; its `code` is Node's (`ENOSPC`, `EACCES` and so on), not a player error code.
|
|
10913
|
+
* @example Write the file now before copying it
|
|
10914
|
+
* Before exporting its settings, the plugin makes sure the file on disk has
|
|
10915
|
+
* the latest value. `flush` writes a pending save at once (or waits for
|
|
10916
|
+
* the write in progress) and resolves when the file is written.
|
|
10917
|
+
* ```ts
|
|
10918
|
+
* const store = plugin.storage.json("settings", { defaults: { step: 5 } });
|
|
10919
|
+
* await store.flush(); // the file now has the newest value
|
|
10920
|
+
* await copyFile(join(plugin.dataDirectory, "settings.json"), exportPath);
|
|
10921
|
+
* ```
|
|
10922
|
+
*/
|
|
8996
10923
|
flush(): Promise<void>;
|
|
8997
10924
|
}
|
|
8998
10925
|
```
|
|
8999
10926
|
|
|
10927
|
+
Example: Count how often the plugin was started
|
|
10928
|
+
|
|
10929
|
+
The counter grows by one at every start and is kept in `stats.json`. The
|
|
10930
|
+
store reads the file when it is made (the defaults when there is none),
|
|
10931
|
+
and `save` writes the new count a moment later.
|
|
10932
|
+
```ts
|
|
10933
|
+
const stats = plugin.storage.json("stats", { defaults: { starts: 0 } });
|
|
10934
|
+
stats.save({ starts: stats.value.starts + 1 }); // written 300 ms later
|
|
10935
|
+
```
|
|
10936
|
+
|
|
9000
10937
|
### Storage
|
|
9001
10938
|
|
|
9002
10939
|
`plugin.storage`: JSON settings files. No capability.
|
|
9003
10940
|
|
|
10941
|
+
Two kinds of storage, both private to the plugin and needing no
|
|
10942
|
+
capability. Storage.json keeps settings in JSON files in the
|
|
10943
|
+
plugin's data directory: they last across player restarts and upgrades,
|
|
10944
|
+
and are written by the plugin's own process. Storage.session
|
|
10945
|
+
keeps values in the player's memory for one player run: they survive a
|
|
10946
|
+
restart of the plugin's process but not of the player. Secrets belong in
|
|
10947
|
+
Secrets, never in either.
|
|
10948
|
+
|
|
10949
|
+
- @capability None.
|
|
10950
|
+
|
|
9004
10951
|
```ts
|
|
9005
10952
|
interface Storage {
|
|
9006
10953
|
/**
|
|
@@ -9011,6 +10958,8 @@ interface Storage {
|
|
|
9011
10958
|
*
|
|
9012
10959
|
* The file is read synchronously on creation; `debounceMs` defaults to
|
|
9013
10960
|
* 300. Files are created with mode 0600. `invalid` for a bad name.
|
|
10961
|
+
* @capability None.
|
|
10962
|
+
* @throws invalid (throws) A bad `name`, or a `debounceMs` that is not a non-negative number.
|
|
9014
10963
|
* @example Keep settings and a recent list in a JSON file
|
|
9015
10964
|
* The plugin remembers whether to resume and the last ten files across
|
|
9016
10965
|
* restarts. `json` reads `settings.json` from the data directory at once
|
|
@@ -9040,6 +10989,19 @@ interface Storage {
|
|
|
9040
10989
|
}
|
|
9041
10990
|
```
|
|
9042
10991
|
|
|
10992
|
+
Example: Keep a setting across restarts and a queue for this run
|
|
10993
|
+
|
|
10994
|
+
The plugin's step size survives a restart of the player; its work queue
|
|
10995
|
+
survives a reload of the plugin but starts empty with the next player
|
|
10996
|
+
run. The JSON store reads its file at once and writes it shortly after
|
|
10997
|
+
each `save`; session values live in the player process.
|
|
10998
|
+
```ts
|
|
10999
|
+
const settings = plugin.storage.json("settings", { defaults: { step: 5 } });
|
|
11000
|
+
const queue = (await plugin.storage.session.get<string[]>("queue")) ?? [];
|
|
11001
|
+
void settings.value.step; // read synchronously, from the file or the defaults
|
|
11002
|
+
void queue;
|
|
11003
|
+
```
|
|
11004
|
+
|
|
9043
11005
|
### SessionStorage
|
|
9044
11006
|
|
|
9045
11007
|
`plugin.storage.session`: JSON values that live as long as the **player**,
|
|
@@ -9085,17 +11047,74 @@ quitting).
|
|
|
9085
11047
|
resolves, so a crash right after loses nothing; there is no debounce.
|
|
9086
11048
|
Calls answer in order.
|
|
9087
11049
|
|
|
11050
|
+
The player keeps the values in its own memory, per plugin, until it
|
|
11051
|
+
quits; the plugin's process may restart in between. Each call is one
|
|
11052
|
+
request to the player, answered in order, with no debounce.
|
|
11053
|
+
|
|
11054
|
+
- @capability None.
|
|
11055
|
+
|
|
9088
11056
|
```ts
|
|
9089
11057
|
interface SessionStorage {
|
|
9090
|
-
/**
|
|
11058
|
+
/**
|
|
11059
|
+
* The value stored under `key`, or `undefined` when there is none.
|
|
11060
|
+
* @capability None.
|
|
11061
|
+
* @throws invalid (rejects) A bad key (empty, over 1024 bytes, with NUL or a lone surrogate).
|
|
11062
|
+
* @throws disabled (rejects) The plugin is stopping.
|
|
11063
|
+
* @throws unsupported (rejects) A player before 0.5.5.
|
|
11064
|
+
* @example Pick up where the plugin stopped after a reload
|
|
11065
|
+
* After a reload the plugin resumes the item it was working on. `get`
|
|
11066
|
+
* asks the player and resolves with the value parsed from JSON, or
|
|
11067
|
+
* `undefined` when nothing is stored; the value is not checked, so its
|
|
11068
|
+
* shape is checked here.
|
|
11069
|
+
* ```ts
|
|
11070
|
+
* const current = await plugin.storage.session.get<{ url: string }>("current");
|
|
11071
|
+
* if (typeof current?.url === "string") await plugin.osd.show("Resuming {}", [current.url]);
|
|
11072
|
+
* ```
|
|
11073
|
+
*/
|
|
9091
11074
|
get<T = unknown>(key: string): Promise<T | undefined>;
|
|
9092
11075
|
/**
|
|
9093
11076
|
* Store or replace `value` under `key`. `invalid` for a bad key or a value that is not JSON, `limit` when the plugin's values would be over 1 MiB or the request over the 1 MiB message limit.
|
|
11077
|
+
* @capability None.
|
|
11078
|
+
* @throws invalid (rejects) A bad key, or a value JSON cannot hold (or a string with a lone surrogate).
|
|
11079
|
+
* @throws disabled (rejects) The plugin is stopping.
|
|
11080
|
+
* @throws unsupported (rejects) A player before 0.5.5.
|
|
11081
|
+
* @throws limit (rejects) The plugin's values would pass 1 MiB, or the request the 1 MiB message limit; nothing changes.
|
|
11082
|
+
* @example Remember the item being worked on
|
|
11083
|
+
* The plugin records the URL it is processing, so a reload can continue
|
|
11084
|
+
* with it. The value is stored as `JSON.stringify` writes it, and is kept
|
|
11085
|
+
* once the promise resolves.
|
|
11086
|
+
* ```ts
|
|
11087
|
+
* await plugin.storage.session.set("current", { url: "https://example.com/a.mkv" });
|
|
11088
|
+
* ```
|
|
9094
11089
|
*/
|
|
9095
11090
|
set(key: string, value: unknown): Promise<void>;
|
|
9096
|
-
/**
|
|
11091
|
+
/**
|
|
11092
|
+
* Remove a key; removing a missing one is fine.
|
|
11093
|
+
* @capability None.
|
|
11094
|
+
* @throws invalid (rejects) A bad key.
|
|
11095
|
+
* @throws disabled (rejects) The plugin is stopping.
|
|
11096
|
+
* @throws unsupported (rejects) A player before 0.5.5.
|
|
11097
|
+
* @example Forget the item once it is done
|
|
11098
|
+
* When the work finishes, the plugin removes the entry so that a later
|
|
11099
|
+
* reload does not resume it. Removing a key that is not there is fine.
|
|
11100
|
+
* ```ts
|
|
11101
|
+
* await plugin.storage.session.delete("current");
|
|
11102
|
+
* ```
|
|
11103
|
+
*/
|
|
9097
11104
|
delete(key: string): Promise<void>;
|
|
9098
|
-
/**
|
|
11105
|
+
/**
|
|
11106
|
+
* The keys stored, in code point order.
|
|
11107
|
+
* @capability None.
|
|
11108
|
+
* @throws disabled (rejects) The plugin is stopping.
|
|
11109
|
+
* @throws unsupported (rejects) A player before 0.5.5.
|
|
11110
|
+
* @example Clear everything the plugin stored for this run
|
|
11111
|
+
* The plugin starts its session state over. `keys` lists the plugin's own
|
|
11112
|
+
* keys in code point order; each `delete` is a request of its own.
|
|
11113
|
+
* ```ts
|
|
11114
|
+
* const session = plugin.storage.session;
|
|
11115
|
+
* for (const key of await session.keys()) await session.delete(key);
|
|
11116
|
+
* ```
|
|
11117
|
+
*/
|
|
9099
11118
|
keys(): Promise<string[]>;
|
|
9100
11119
|
}
|
|
9101
11120
|
```
|
|
@@ -9119,15 +11138,61 @@ await plugin.storage.session.set("queue", pending);
|
|
|
9119
11138
|
`plugin.system`: files and links, executed by the player. No capability.
|
|
9120
11139
|
Other systems than macOS and Windows: `unsupported`.
|
|
9121
11140
|
|
|
11141
|
+
The player opens files, folders and links with the system's own
|
|
11142
|
+
applications for the plugin, so the plugin never runs `open`,
|
|
11143
|
+
`explorer` or a shell itself.
|
|
11144
|
+
|
|
11145
|
+
- @capability None.
|
|
11146
|
+
|
|
9122
11147
|
```ts
|
|
9123
11148
|
interface System {
|
|
9124
11149
|
/**
|
|
9125
11150
|
* Select files or folders in the file manager (macOS: one Finder window for all, so several files of one folder are all highlighted; Windows: one Explorer window per folder, selecting its first path). Paths that do not exist are skipped; `missing` when none exists; at most 256.
|
|
11151
|
+
* @capability None.
|
|
11152
|
+
* @throws missing (rejects) None of the paths exists.
|
|
11153
|
+
* @throws invalid (rejects) No paths, or more than 256.
|
|
11154
|
+
* @throws failed (rejects) The system refused.
|
|
11155
|
+
* @throws timeout (rejects) macOS did not answer within 10 s.
|
|
11156
|
+
* @throws unsupported (rejects) A system other than macOS and Windows.
|
|
11157
|
+
* @example Show the exported files in the file manager
|
|
11158
|
+
* After an export, the file manager opens with the new files selected.
|
|
11159
|
+
* Paths that no longer exist are skipped; the call rejects with `missing`
|
|
11160
|
+
* only when none of them exists.
|
|
11161
|
+
* ```ts
|
|
11162
|
+
* await plugin.system.reveal(exported); // macOS: one Finder window for all
|
|
11163
|
+
* ```
|
|
9126
11164
|
*/
|
|
9127
11165
|
reveal(paths: string | string[]): Promise<void>;
|
|
9128
|
-
/**
|
|
11166
|
+
/**
|
|
11167
|
+
* Open the file or folder with its default application. `missing` if absent.
|
|
11168
|
+
* @capability None.
|
|
11169
|
+
* @throws missing (rejects) The path does not exist.
|
|
11170
|
+
* @throws invalid (rejects) An empty path.
|
|
11171
|
+
* @throws failed (rejects) The system refused.
|
|
11172
|
+
* @throws timeout (rejects) macOS did not answer within 10 s.
|
|
11173
|
+
* @throws unsupported (rejects) A system other than macOS and Windows.
|
|
11174
|
+
* @example Open the plugin's data folder
|
|
11175
|
+
* A "Show data folder" command opens the plugin's data directory in the
|
|
11176
|
+
* file manager, which is the default application for a folder.
|
|
11177
|
+
* ```ts
|
|
11178
|
+
* await plugin.system.openPath(plugin.dataDirectory);
|
|
11179
|
+
* ```
|
|
11180
|
+
*/
|
|
9129
11181
|
openPath(path: string): Promise<void>;
|
|
9130
|
-
/**
|
|
11182
|
+
/**
|
|
11183
|
+
* Open an http(s) address in the default browser. `invalid` for another protocol or a malformed address; `timeout` after 10 s, `failed` otherwise.
|
|
11184
|
+
* @capability None.
|
|
11185
|
+
* @throws invalid (rejects) Not an `http:` or `https:` address.
|
|
11186
|
+
* @throws timeout (rejects) macOS did not hand it to the browser within 10 s.
|
|
11187
|
+
* @throws failed (rejects) The system refused.
|
|
11188
|
+
* @throws unsupported (rejects) A system other than macOS and Windows.
|
|
11189
|
+
* @example Open the plugin's help page
|
|
11190
|
+
* A Help command opens the plugin's documentation in the default browser.
|
|
11191
|
+
* Only `http:` and `https:` addresses are opened.
|
|
11192
|
+
* ```ts
|
|
11193
|
+
* await plugin.system.openUrl("https://example.com/plugin/help");
|
|
11194
|
+
* ```
|
|
11195
|
+
*/
|
|
9131
11196
|
openUrl(url: string): Promise<void>;
|
|
9132
11197
|
}
|
|
9133
11198
|
```
|
|
@@ -9153,20 +11218,74 @@ try {
|
|
|
9153
11218
|
Per-plugin secrets (API keys, passwords) in the system's secret store:
|
|
9154
11219
|
the login keychain on macOS, the Credential Manager on Windows. Needs
|
|
9155
11220
|
`secrets`. `unsupported` on systems without a store, `unavailable` when
|
|
9156
|
-
the store
|
|
11221
|
+
the store cannot be reached (no keychain, a read-only store, no logged-in
|
|
11222
|
+
session on Windows): keep a fallback (or ask again).
|
|
9157
11223
|
|
|
9158
11224
|
The user denying access and other system errors reject `failed`. Never
|
|
9159
11225
|
write secrets to settings files.
|
|
9160
11226
|
|
|
11227
|
+
Values are stored by the player in the system's secret store under this
|
|
11228
|
+
plugin's id: other plugins cannot read them. Names are 1–64 of
|
|
11229
|
+
`a-z 0-9 . _ -`, values 1–1024 bytes of text, at most 256 names per
|
|
11230
|
+
plugin (`SECRET_LIMITS`).
|
|
11231
|
+
|
|
11232
|
+
- @capability secrets
|
|
11233
|
+
|
|
9161
11234
|
```ts
|
|
9162
11235
|
interface Secrets {
|
|
9163
|
-
/**
|
|
11236
|
+
/**
|
|
11237
|
+
* The stored value, `null` when there is none.
|
|
11238
|
+
* @capability secrets
|
|
11239
|
+
* @throws unsupported (rejects) The system has no secret store.
|
|
11240
|
+
* @throws unavailable (rejects) The store cannot be reached (no keychain, a read-only store, no logged-in session on Windows).
|
|
11241
|
+
* @throws failed (rejects) The user denied access, or another system error.
|
|
11242
|
+
* @throws capability (rejects) The manifest does not declare `secrets`.
|
|
11243
|
+
* @throws invalid (rejects) A bad name.
|
|
11244
|
+
* @example Read a stored token
|
|
11245
|
+
* The plugin uses the token the user saved earlier, or asks for one when
|
|
11246
|
+
* there is none. `get` resolves with the stored text or `null`.
|
|
11247
|
+
* ```ts
|
|
11248
|
+
* const token = await plugin.secrets.get("api-token");
|
|
11249
|
+
* if (token === null) await plugin.osd.show("Sign in first");
|
|
11250
|
+
* ```
|
|
11251
|
+
*/
|
|
9164
11252
|
get(name: string): Promise<string | null>;
|
|
9165
11253
|
/**
|
|
9166
11254
|
* Store or replace (`name`: 1–64 of `a-z 0-9 . _ -`; value 1–1024 bytes). At most 256 different names per plugin: a new name beyond that rejects `limit`. Uninstalling with `removeData` deletes them.
|
|
11255
|
+
* @capability secrets
|
|
11256
|
+
* @throws invalid (rejects) A bad name, or a value that is empty, has NUL or is over 1024 bytes.
|
|
11257
|
+
* @throws limit (rejects) A new name beyond 256.
|
|
11258
|
+
* @throws unsupported (rejects) The system has no secret store.
|
|
11259
|
+
* @throws unavailable (rejects) The store cannot be reached (no keychain, a read-only store, no logged-in session on Windows).
|
|
11260
|
+
* @throws failed (rejects) The user denied access, or another system error.
|
|
11261
|
+
* @throws capability (rejects) The manifest does not declare `secrets`.
|
|
11262
|
+
* @example Save the token the user entered
|
|
11263
|
+
* The token the user typed is kept in the system's secret store for later
|
|
11264
|
+
* runs. `set` stores or replaces it; the store can refuse, in which case
|
|
11265
|
+
* the plugin keeps the token for this run only.
|
|
11266
|
+
* ```ts
|
|
11267
|
+
* await plugin.secrets.set("api-token", token).catch((error: PluginError) => {
|
|
11268
|
+
* // No store, or access denied: the token is not kept for next time.
|
|
11269
|
+
* void plugin.osd.show("The token could not be saved ({})", [error.code]);
|
|
11270
|
+
* });
|
|
11271
|
+
* ```
|
|
9167
11272
|
*/
|
|
9168
11273
|
set(name: string, value: string): Promise<void>;
|
|
9169
|
-
/**
|
|
11274
|
+
/**
|
|
11275
|
+
* Remove; removing a missing secret is fine.
|
|
11276
|
+
* @capability secrets
|
|
11277
|
+
* @throws invalid (rejects) A bad name.
|
|
11278
|
+
* @throws unsupported (rejects) The system has no secret store.
|
|
11279
|
+
* @throws unavailable (rejects) The store cannot be reached (no keychain, a read-only store, no logged-in session on Windows).
|
|
11280
|
+
* @throws failed (rejects) The user denied access, or another system error.
|
|
11281
|
+
* @throws capability (rejects) The manifest does not declare `secrets`.
|
|
11282
|
+
* @example Sign out by removing the token
|
|
11283
|
+
* Signing out removes the stored token, so the next start asks again.
|
|
11284
|
+
* Removing a name that is not stored is fine.
|
|
11285
|
+
* ```ts
|
|
11286
|
+
* await plugin.secrets.delete("api-token");
|
|
11287
|
+
* ```
|
|
11288
|
+
*/
|
|
9170
11289
|
delete(name: string): Promise<void>;
|
|
9171
11290
|
}
|
|
9172
11291
|
```
|
|
@@ -9591,6 +11710,13 @@ A tool window as the test host keeps it: the record of a
|
|
|
9591
11710
|
`plugin.windows.create(...)` window after its first `show()`. Get one with
|
|
9592
11711
|
TestHost.window.
|
|
9593
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
|
+
|
|
9594
11720
|
```ts
|
|
9595
11721
|
interface FakeWindow {
|
|
9596
11722
|
/** The `.slint` file the window was created from (package-relative). */
|
|
@@ -9605,10 +11731,32 @@ interface FakeWindow {
|
|
|
9605
11731
|
callbacks: string[];
|
|
9606
11732
|
/**
|
|
9607
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
|
+
* ```
|
|
9608
11746
|
*/
|
|
9609
11747
|
emit(callback: string, ...args: Json[]): void;
|
|
9610
11748
|
/**
|
|
9611
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
|
+
* ```
|
|
9612
11760
|
*/
|
|
9613
11761
|
userClose(): void;
|
|
9614
11762
|
}
|
|
@@ -9651,6 +11799,13 @@ A command the plugin registered with `plugin.commands.add`, as the test
|
|
|
9651
11799
|
host keeps it. Get one with TestHost.command (the plugin's own) or
|
|
9652
11800
|
TestHost.coreAction (the player's core actions).
|
|
9653
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
|
+
|
|
9654
11809
|
```ts
|
|
9655
11810
|
interface FakeCommand {
|
|
9656
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. */
|
|
@@ -9683,6 +11838,15 @@ interface FakeCommand {
|
|
|
9683
11838
|
revision: number;
|
|
9684
11839
|
/**
|
|
9685
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
|
+
* ```
|
|
9686
11850
|
*/
|
|
9687
11851
|
run(): Promise<void>;
|
|
9688
11852
|
}
|
|
@@ -9708,6 +11872,13 @@ assert.equal(host.command("toggle-loop").state.checked, true);
|
|
|
9708
11872
|
A command's bottom-bar button (`CommandOptions.button`) as the player shows
|
|
9709
11873
|
it: an entry of TestHost.barButtons.
|
|
9710
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
|
+
|
|
9711
11882
|
```ts
|
|
9712
11883
|
interface FakeBarButton {
|
|
9713
11884
|
/** The command's local id. */
|
|
@@ -9726,17 +11897,45 @@ interface FakeBarButton {
|
|
|
9726
11897
|
readonly tooltip: string;
|
|
9727
11898
|
/**
|
|
9728
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
|
+
* ```
|
|
9729
11910
|
*/
|
|
9730
11911
|
click(): Promise<void>;
|
|
9731
11912
|
}
|
|
9732
11913
|
```
|
|
9733
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
|
+
|
|
9734
11926
|
### FakeProducer
|
|
9735
11927
|
|
|
9736
11928
|
A generated stream (`plugin.player.openStream`) as the test host sees it:
|
|
9737
11929
|
the fake player end of the frame connection. Assertion surface; see
|
|
9738
11930
|
TestHost.producers.
|
|
9739
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
|
+
|
|
9740
11939
|
```ts
|
|
9741
11940
|
interface FakeProducer {
|
|
9742
11941
|
/** The producer id the host assigned (`1`, `2`, … per host). */
|
|
@@ -9758,6 +11957,18 @@ interface FakeProducer {
|
|
|
9758
11957
|
}
|
|
9759
11958
|
```
|
|
9760
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
|
+
|
|
9761
11972
|
### MarkdownWire
|
|
9762
11973
|
|
|
9763
11974
|
A Markdown document as TestHost.markdown reads it back.
|
|
@@ -9812,6 +12023,15 @@ Fields that mirror player state at the wire level (`source`, `subtitles`,
|
|
|
9812
12023
|
`calls`) use the protocol's shapes. They are fine for assertions but are
|
|
9813
12024
|
not part of the plugin API: their shape may change in minor versions.
|
|
9814
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
|
+
|
|
9815
12035
|
```ts
|
|
9816
12036
|
class TestHost implements Transport {
|
|
9817
12037
|
/** Every request the plugin sent, in order: `{ method, params }`. Wire-level; prefer TestHost.sent for one method. */
|
|
@@ -9894,6 +12114,15 @@ class TestHost implements Transport {
|
|
|
9894
12114
|
/**
|
|
9895
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.
|
|
9896
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
|
+
* ```
|
|
9897
12126
|
*/
|
|
9898
12127
|
loadCatalog(directory: string): void;
|
|
9899
12128
|
/**
|
|
@@ -9901,6 +12130,7 @@ class TestHost implements Transport {
|
|
|
9901
12130
|
* @param text - English source text or template.
|
|
9902
12131
|
* @param args - Values for its placeholders.
|
|
9903
12132
|
* @returns The shown text.
|
|
12133
|
+
* @capability None.
|
|
9904
12134
|
* @example Compare with a translated message
|
|
9905
12135
|
* The test expects the text a Chinese user sees for a template the plugin
|
|
9906
12136
|
* shows. `translate` looks the English source up in the plugin's catalog
|
|
@@ -9915,6 +12145,19 @@ class TestHost implements Transport {
|
|
|
9915
12145
|
/**
|
|
9916
12146
|
* Simulate another plugin (or the user) saving the interface language: pushes `appearance.changed`.
|
|
9917
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
|
+
* ```
|
|
9918
12161
|
*/
|
|
9919
12162
|
setLanguage(language: LanguagePreference): void;
|
|
9920
12163
|
/**
|
|
@@ -10071,6 +12314,7 @@ class TestHost implements Transport {
|
|
|
10071
12314
|
* Simulate another plugin publishing its timeline layer (or clearing it with `[]`); pushes `timeline.changed` when the plugin subscribed.
|
|
10072
12315
|
* @param pluginId - The other plugin's manifest id.
|
|
10073
12316
|
* @param marks - Its whole layer.
|
|
12317
|
+
* @capability None.
|
|
10074
12318
|
* @example Let another plugin publish chapters
|
|
10075
12319
|
* The plugin under test sees two chapter marks another plugin put on the
|
|
10076
12320
|
* timeline. `setTimelineLayer` stores that plugin's whole layer and pushes
|
|
@@ -10104,6 +12348,7 @@ class TestHost implements Transport {
|
|
|
10104
12348
|
}[];
|
|
10105
12349
|
/**
|
|
10106
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.
|
|
10107
12352
|
* @example Check which file a window shows as its cover
|
|
10108
12353
|
* The test checks that the Cover window's image comes from the plugin's
|
|
10109
12354
|
* data directory. An image property holds the SDK's wire form of
|
|
@@ -10129,6 +12374,8 @@ class TestHost implements Transport {
|
|
|
10129
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.
|
|
10130
12375
|
* @param paths - The dropped paths, in the order the file manager gives.
|
|
10131
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.
|
|
10132
12379
|
* @example Drop files on the video and on the panel
|
|
10133
12380
|
* The first drop lands on the video area: each extension belongs to the
|
|
10134
12381
|
* provider that registered it first, and each provider gets one call with
|
|
@@ -10159,7 +12406,20 @@ class TestHost implements Transport {
|
|
|
10159
12406
|
}): Promise<void>;
|
|
10160
12407
|
/** The palette (`plugin.appearance.theme`); `setAppearance` changes it elsewhere. */
|
|
10161
12408
|
appearance: ThemeId;
|
|
10162
|
-
/**
|
|
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
|
+
*/
|
|
10163
12423
|
setAppearance(theme: ThemeId): void;
|
|
10164
12424
|
/** Settings pages by id (`plugin.settings.page`), every `show` (page id or null), and the page the options window shows. */
|
|
10165
12425
|
readonly settings: {
|
|
@@ -10191,6 +12451,7 @@ class TestHost implements Transport {
|
|
|
10191
12451
|
};
|
|
10192
12452
|
/**
|
|
10193
12453
|
* One settings page by id. Throws when it was never registered.
|
|
12454
|
+
* @capability None.
|
|
10194
12455
|
* @example Change a value on the plugin's settings page
|
|
10195
12456
|
* The test sets the step on the General page and checks the page shows
|
|
10196
12457
|
* it. `emit` fires the page's callback as the user's input would; the
|
|
@@ -10228,13 +12489,26 @@ class TestHost implements Transport {
|
|
|
10228
12489
|
/** Whether it shows now (no video frame). `true` when mounted. */
|
|
10229
12490
|
effective: boolean;
|
|
10230
12491
|
} | null;
|
|
10231
|
-
/**
|
|
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
|
+
*/
|
|
10232
12505
|
setBackdropEffective(effective: boolean): void;
|
|
10233
12506
|
/**
|
|
10234
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.
|
|
10235
12508
|
* @param slot - The position.
|
|
10236
12509
|
* @param plugin - The other plugin's manifest id.
|
|
10237
12510
|
* @param options - `title`: its tab label (default: `plugin`).
|
|
12511
|
+
* @capability None.
|
|
10238
12512
|
* @example Put the playlist next to this plugin's sidebar panel
|
|
10239
12513
|
* The sidebar ends up with two tabs: the playlist, mounted first, and the
|
|
10240
12514
|
* plugin's own panel after it. `mountOther` before `startPlugin` mounts
|
|
@@ -10251,10 +12525,31 @@ class TestHost implements Transport {
|
|
|
10251
12525
|
mountOther(slot: "bottom" | "sidebar" | "backdrop", plugin: string, options?: {
|
|
10252
12526
|
title?: string;
|
|
10253
12527
|
}): void;
|
|
10254
|
-
/**
|
|
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
|
+
*/
|
|
10255
12541
|
unmountOther(plugin: string): void;
|
|
10256
12542
|
/**
|
|
10257
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
|
+
* ```
|
|
10258
12553
|
*/
|
|
10259
12554
|
selectTab(plugin: string): void;
|
|
10260
12555
|
/** The display layer (`plugin.video.view()`): who owns it (this plugin is `manifestId`) and the values the player holds. */
|
|
@@ -10266,6 +12561,17 @@ class TestHost implements Transport {
|
|
|
10266
12561
|
};
|
|
10267
12562
|
/**
|
|
10268
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
|
+
* ```
|
|
10269
12575
|
*/
|
|
10270
12576
|
setVideoViewOwner(pluginId: string | null): void;
|
|
10271
12577
|
/**
|
|
@@ -10281,10 +12587,22 @@ class TestHost implements Transport {
|
|
|
10281
12587
|
};
|
|
10282
12588
|
/**
|
|
10283
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
|
+
* ```
|
|
10284
12601
|
*/
|
|
10285
12602
|
setVideoRenderOwner(pluginId: string | null): void;
|
|
10286
12603
|
/**
|
|
10287
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.
|
|
10288
12606
|
* @example Make a shader fail to compile
|
|
10289
12607
|
* The plugin's render options report `failed`, as when mpv rejects a
|
|
10290
12608
|
* shader. The host pushes the failure only while this plugin owns the
|
|
@@ -10405,10 +12723,21 @@ class TestHost implements Transport {
|
|
|
10405
12723
|
/** Exact host names it resolves (a web resolver), with `schemes`. */
|
|
10406
12724
|
hosts?: string[];
|
|
10407
12725
|
}[];
|
|
10408
|
-
/**
|
|
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
|
+
*/
|
|
10409
12737
|
fileTypes(): string[];
|
|
10410
12738
|
/**
|
|
10411
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.
|
|
10412
12741
|
* @example Let another plugin's file type appear while running
|
|
10413
12742
|
* Another plugin starts and adds `.ch8`; the plugin under test then sees
|
|
10414
12743
|
* it in `resolvers.fileTypes`. Changing `otherResolvers` alone pushes
|
|
@@ -10526,6 +12855,9 @@ class TestHost implements Transport {
|
|
|
10526
12855
|
readonly dataDirectory: string;
|
|
10527
12856
|
/** The manifest id of the plugin under test (set by startPlugin; `"test.plugin"` by default). */
|
|
10528
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
|
+
*/
|
|
10529
12861
|
constructor();
|
|
10530
12862
|
/** The `player.setStepSeek` value in effect (default 5). */
|
|
10531
12863
|
stepSeek: number;
|
|
@@ -10581,7 +12913,8 @@ class TestHost implements Transport {
|
|
|
10581
12913
|
/** Every core action run (`host.run`, a core command, a pressed core binding), newest last. */
|
|
10582
12914
|
readonly coreRuns: string[];
|
|
10583
12915
|
/**
|
|
10584
|
-
* 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.
|
|
10585
12918
|
* @example Pause through the player's own action
|
|
10586
12919
|
* The test pauses playback the way the Space key does and checks that the
|
|
10587
12920
|
* host recorded it. Core actions belong to the player (`host/<id>`);
|
|
@@ -10608,6 +12941,7 @@ class TestHost implements Transport {
|
|
|
10608
12941
|
* find the one a call uses.
|
|
10609
12942
|
* @param method - The wire method name.
|
|
10610
12943
|
* @param handler - Produces the reply from the request parameters.
|
|
12944
|
+
* @capability None.
|
|
10611
12945
|
* @example Make opening a folder fail
|
|
10612
12946
|
* The plugin's Open Folder command meets a missing folder and must tell
|
|
10613
12947
|
* the user. `handle` replaces the host's answer to one wire method; the
|
|
@@ -10624,17 +12958,43 @@ class TestHost implements Transport {
|
|
|
10624
12958
|
* ```
|
|
10625
12959
|
*/
|
|
10626
12960
|
handle(method: string, handler: Handler): void;
|
|
10627
|
-
/**
|
|
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
|
+
*/
|
|
10628
12977
|
reset(method: string): void;
|
|
10629
12978
|
/**
|
|
10630
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.
|
|
10631
12980
|
* @param event - The wire event name.
|
|
10632
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
|
+
* ```
|
|
10633
12992
|
*/
|
|
10634
12993
|
emit(event: string, data: unknown): void;
|
|
10635
12994
|
/**
|
|
10636
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.
|
|
10637
12996
|
* @param patch - Wire-level source fields, e.g. `phase`, `position`, `duration`, `paused`, `audioTracks`.
|
|
12997
|
+
* @capability None.
|
|
10638
12998
|
* @example Load a file, then let it play to the end
|
|
10639
12999
|
* The plugin sees a one-minute source become ready, then end on its own.
|
|
10640
13000
|
* `setSource` changes the host's source facts and broadcasts them like
|
|
@@ -10651,6 +13011,7 @@ class TestHost implements Transport {
|
|
|
10651
13011
|
setSource(patch: Partial<SourceState>): void;
|
|
10652
13012
|
/**
|
|
10653
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.
|
|
10654
13015
|
* @example Go fullscreen, then report picture-in-picture support
|
|
10655
13016
|
* The plugin sees the main window fullscreen at 1920×1080, then learns
|
|
10656
13017
|
* that picture-in-picture is available. `setMainWindow` changes the
|
|
@@ -10677,12 +13038,23 @@ class TestHost implements Transport {
|
|
|
10677
13038
|
}>): void;
|
|
10678
13039
|
/**
|
|
10679
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
|
+
* ```
|
|
10680
13051
|
*/
|
|
10681
13052
|
popSidebar(popped: boolean): void;
|
|
10682
13053
|
/**
|
|
10683
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`.
|
|
10684
13055
|
* @param source - The path or URL.
|
|
10685
13056
|
* @param phase - The phase to end in (default `"ready"`).
|
|
13057
|
+
* @capability None.
|
|
10686
13058
|
* @example Let the user open another file meanwhile
|
|
10687
13059
|
* A state the plugin captured before stops being current once another
|
|
10688
13060
|
* file opens. `openElsewhere` gives the source a new `sourceId`, so
|
|
@@ -10701,6 +13073,7 @@ class TestHost implements Transport {
|
|
|
10701
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`.
|
|
10702
13074
|
* @param producerId - The producer, from TestHost.producers.
|
|
10703
13075
|
* @param message - The player's failure text.
|
|
13076
|
+
* @capability None.
|
|
10704
13077
|
* @example Break the plugin's generated stream
|
|
10705
13078
|
* The plugin's stream stops with an error, as when the player loses its
|
|
10706
13079
|
* audio device. `failStream` puts the source into `error` with the
|
|
@@ -10713,8 +13086,11 @@ class TestHost implements Transport {
|
|
|
10713
13086
|
*/
|
|
10714
13087
|
failStream(producerId: number, message: string): void;
|
|
10715
13088
|
/**
|
|
10716
|
-
* 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.
|
|
10717
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.
|
|
10718
13094
|
* @example Press a global key and a panel key
|
|
10719
13095
|
* Ctrl+B runs whatever is bound globally; Delete is pressed while the
|
|
10720
13096
|
* plugin's panel has focus, so panel bindings are tried first. `press`
|
|
@@ -10805,6 +13181,7 @@ class TestHost implements Transport {
|
|
|
10805
13181
|
readonly remoteImages: Map<string, string | null>;
|
|
10806
13182
|
/**
|
|
10807
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.
|
|
10808
13185
|
* @example Read back a Markdown message in a chat window
|
|
10809
13186
|
* The test checks the Markdown source of the first chat message. Rows
|
|
10810
13187
|
* hold the document in its wire form, as `plugin.ui.markdown` produced
|
|
@@ -10822,13 +13199,47 @@ class TestHost implements Transport {
|
|
|
10822
13199
|
/** The system's name for it. */
|
|
10823
13200
|
name: string;
|
|
10824
13201
|
}[];
|
|
10825
|
-
/**
|
|
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
|
+
*/
|
|
10826
13214
|
setPackages(list: TestHost["packages"]): void;
|
|
10827
13215
|
/**
|
|
10828
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
|
+
* ```
|
|
10829
13228
|
*/
|
|
10830
13229
|
step(direction: "next" | "previous"): Promise<unknown>;
|
|
10831
|
-
/**
|
|
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
|
+
*/
|
|
10832
13243
|
command(id: string): FakeCommand;
|
|
10833
13244
|
/**
|
|
10834
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`.
|
|
@@ -10848,6 +13259,7 @@ class TestHost implements Transport {
|
|
|
10848
13259
|
get barButtons(): readonly FakeBarButton[];
|
|
10849
13260
|
/**
|
|
10850
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.
|
|
10851
13263
|
* @example Press a window's Reset button from a test
|
|
10852
13264
|
* The Picture window's Reset callback runs and the brightness returns to
|
|
10853
13265
|
* 0. The window is found by its id (the `.slint` file name by default);
|
|
@@ -10862,6 +13274,7 @@ class TestHost implements Transport {
|
|
|
10862
13274
|
window(id: string): FakeWindow;
|
|
10863
13275
|
/**
|
|
10864
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.
|
|
10865
13278
|
* @example Select a row in the plugin's sidebar panel
|
|
10866
13279
|
* The test checks the panel sits in the sidebar and selects its third
|
|
10867
13280
|
* row. `emit` fires a callback on the panel's global as a click would;
|
|
@@ -10883,6 +13296,7 @@ class TestHost implements Transport {
|
|
|
10883
13296
|
};
|
|
10884
13297
|
/**
|
|
10885
13298
|
* The parameters of every request of one wire method, oldest first.
|
|
13299
|
+
* @capability None.
|
|
10886
13300
|
* @example Check the request behind an OSD message
|
|
10887
13301
|
* The test checks the last OSD request the plugin sent, before
|
|
10888
13302
|
* translation. `sent` lists the parameters of every request of one wire
|
|
@@ -10903,6 +13317,8 @@ class TestHost implements Transport {
|
|
|
10903
13317
|
* for: on a loaded machine it can finish after `settle()` returns. Poll
|
|
10904
13318
|
* the result you assert on instead (second example).
|
|
10905
13319
|
* @param ms - Extra time to wait, for debounces and timers (default 0).
|
|
13320
|
+
* @capability None.
|
|
13321
|
+
* @throws Never.
|
|
10906
13322
|
* @example Wait past a debounced save
|
|
10907
13323
|
* The plugin's store writes its file 300 ms after the last `save`, so
|
|
10908
13324
|
* the test waits 400 ms before it checks what the plugin did next.
|
|
@@ -10928,7 +13344,22 @@ class TestHost implements Transport {
|
|
|
10928
13344
|
* ```
|
|
10929
13345
|
*/
|
|
10930
13346
|
settle(ms?: number): Promise<void>;
|
|
10931
|
-
/**
|
|
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
|
+
*/
|
|
10932
13363
|
close(): void;
|
|
10933
13364
|
}
|
|
10934
13365
|
```
|
|
@@ -10978,9 +13409,14 @@ throws, or when a registration made during activation fails (for example
|
|
|
10978
13409
|
`limit`, `conflict`, `invalid`), which is when the player would fail the
|
|
10979
13410
|
activation.
|
|
10980
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
|
+
|
|
10981
13416
|
- @param activate - The plugin's default-exported `activate` function.
|
|
10982
13417
|
- @param options - Where the plugin lives and how to start it.
|
|
10983
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.
|
|
10984
13420
|
|
|
10985
13421
|
```ts
|
|
10986
13422
|
function startPlugin(activate: (plugin: Plugin) => unknown, options?: {
|
|
@@ -11604,7 +14040,8 @@ The plugin keeps a token the user pasted, or tells them it is empty or
|
|
|
11604
14040
|
too long. Values are 1–1024 bytes counted in UTF-8, so the check uses
|
|
11605
14041
|
`Buffer.byteLength`; `secrets.set` would refuse other values with
|
|
11606
14042
|
`invalid`. The store itself can still refuse (`unsupported` without a
|
|
11607
|
-
system store, `unavailable`
|
|
14043
|
+
system store, `unavailable` when it cannot be reached, `failed` when the
|
|
14044
|
+
user denies access): only
|
|
11608
14045
|
those codes get the "could not be saved" message, anything else (a
|
|
11609
14046
|
missing `secrets` capability) is a bug and is thrown on.
|
|
11610
14047
|
```ts
|
|
@@ -11688,7 +14125,7 @@ list.on("scrolled", (row: number) => {
|
|
|
11688
14125
|
|
|
11689
14126
|
### IMAGE_LIMITS
|
|
11690
14127
|
|
|
11691
|
-
The player's runtime image limits (`host/src/ui_image.rs`): a file's
|
|
14128
|
+
The player's runtime image limits (`src/host/src/ui/ui_image.rs`): a file's
|
|
11692
14129
|
bytes and sides, the decoded pixels each plugin caches, and (SDK 1.5.19,
|
|
11693
14130
|
player 0.5.1) the decoded pixels the nested images of one view may show.
|
|
11694
14131
|
|
|
@@ -11724,11 +14161,17 @@ Common codes: `stale` (the condition no longer holds; nothing was done),
|
|
|
11724
14161
|
`timeout`, `aborted`, `limit` (a player limit such as the command count),
|
|
11725
14162
|
`disabled` (the plugin is stopping).
|
|
11726
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
|
+
|
|
11727
14169
|
```ts
|
|
11728
14170
|
class HostError extends Error {
|
|
11729
14171
|
/** The stable error code, e.g. `"invalid"`, `"stale"`, `"unavailable"`. */
|
|
11730
14172
|
code: string;
|
|
11731
14173
|
/**
|
|
14174
|
+
* A refusal with this code, as the player would send it.
|
|
11732
14175
|
* @param code - The stable error code the caller will see as `error.code`.
|
|
11733
14176
|
* @param message - Human-readable detail (not for branching on).
|
|
11734
14177
|
*/
|