@vestra-player/sdk 1.5.30 → 1.5.31
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 +11 -0
- package/dist/manifest.js +1 -1
- package/dist/plugin/services.d.ts +2 -1
- package/dist/plugin/services.js +2 -1
- package/dist/plugin/testing.js +1 -1
- package/dist/plugin/types.d.ts +1458 -49
- package/dist/plugin/values.d.ts +2 -2
- package/dist/plugin/values.js +2 -2
- package/dist/processing.js +1 -1
- package/llms-full.txt +1166 -39
- package/llms.txt +22 -22
- package/package.json +1 -1
package/llms-full.txt
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# @vestra-player/sdk 1.5.
|
|
1
|
+
# @vestra-player/sdk 1.5.31: 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,203 @@ 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, so a plugin with its own list becomes the provider only while no other plugin holds it (for example with the playlist disabled). 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
|
+
|
|
1035
1232
|
## How-to: Keep a slider responsive while dragging
|
|
1036
1233
|
|
|
1037
1234
|
**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.
|
|
@@ -3889,12 +4086,23 @@ interface CommandOptions {
|
|
|
3889
4086
|
A registered command, from Commands.add. Disposing it removes it
|
|
3890
4087
|
from the menu and the binding table; the id can be added again right away.
|
|
3891
4088
|
|
|
4089
|
+
The handle of one menu item and its keys. Its state (`enabled`, `checked`,
|
|
4090
|
+
`busy`, `reason`) is bound: functions given to Commands.add or
|
|
4091
|
+
Command.set are evaluated again in every update pass and only
|
|
4092
|
+
changes reach the player. Its title, path, bindings and button cannot
|
|
4093
|
+
change in place; dispose the command (or a child Scope) and add it
|
|
4094
|
+
again. Disposing it, its scope or stopping the plugin removes the menu
|
|
4095
|
+
item, its keys and its bar button.
|
|
4096
|
+
|
|
4097
|
+
- @capability commands
|
|
4098
|
+
|
|
3892
4099
|
```ts
|
|
3893
4100
|
interface Command extends Disposable {
|
|
3894
4101
|
/** The local id given in CommandOptions.id. */
|
|
3895
4102
|
readonly id: string;
|
|
3896
4103
|
/**
|
|
3897
4104
|
* Replace some state fields (values or functions). Last write wins.
|
|
4105
|
+
* @capability commands
|
|
3898
4106
|
* @example Show a command as busy while it works
|
|
3899
4107
|
* The Export item shows as busy with "Exporting…" as its reason while the
|
|
3900
4108
|
* export runs, then returns to normal. `set` replaces only the fields it
|
|
@@ -3920,15 +4128,47 @@ interface Command extends Disposable {
|
|
|
3920
4128
|
}>): void;
|
|
3921
4129
|
/**
|
|
3922
4130
|
* 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.
|
|
4131
|
+
* @throws conflict (rejects) Another command already has this menu path, or the path would be both a menu item and a submenu.
|
|
4132
|
+
* @throws capability (rejects) The manifest does not declare `commands`.
|
|
4133
|
+
* @throws invalid (rejects) The player refused the declaration.
|
|
4134
|
+
* @throws limit (rejects) More than 160 commands in this plugin, or 1024 in the player.
|
|
3923
4135
|
*/
|
|
3924
4136
|
readonly ready: Promise<void>;
|
|
3925
4137
|
}
|
|
3926
4138
|
```
|
|
3927
4139
|
|
|
4140
|
+
Example: Enable an item only while a file is open
|
|
4141
|
+
|
|
4142
|
+
Video ▸ Export… is greyed out until a file is open. `set` replaces the
|
|
4143
|
+
`enabled` field with a function; the SDK evaluates it again after every
|
|
4144
|
+
player state change and sends the new state when it changes, so the
|
|
4145
|
+
item follows the player without further code.
|
|
4146
|
+
```ts
|
|
4147
|
+
const exportCommand = plugin.commands.add({
|
|
4148
|
+
id: "export",
|
|
4149
|
+
path: ["@video", "Export…"],
|
|
4150
|
+
run: runExport,
|
|
4151
|
+
});
|
|
4152
|
+
// A function: re-evaluated in every update pass.
|
|
4153
|
+
exportCommand.set({ enabled: () => plugin.player.state.phase === "ready" });
|
|
4154
|
+
```
|
|
4155
|
+
|
|
3928
4156
|
### Commands
|
|
3929
4157
|
|
|
3930
4158
|
`plugin.commands`: menu commands with default bindings. Needs `commands`.
|
|
3931
4159
|
|
|
4160
|
+
A command is one action of the plugin with a menu item (`path`), default
|
|
4161
|
+
keys (`bindings`), optionally a bottom-bar button (`button`) and a state
|
|
4162
|
+
the menu shows (`enabled`, `checked`, `busy`, `reason`). The player owns
|
|
4163
|
+
the menu, the binding table and the user's key overrides: it calls `run`
|
|
4164
|
+
when the user picks the item, presses a key or clicks the button.
|
|
4165
|
+
Commands of all plugins merge into the player's menus by their English
|
|
4166
|
+
path keys. Registration is asynchronous: `add` returns at once, and
|
|
4167
|
+
Command.ready reports the player's answer (during activation a
|
|
4168
|
+
refusal fails activation).
|
|
4169
|
+
|
|
4170
|
+
- @capability commands
|
|
4171
|
+
|
|
3932
4172
|
```ts
|
|
3933
4173
|
interface Commands {
|
|
3934
4174
|
/**
|
|
@@ -3938,6 +4178,7 @@ interface Commands {
|
|
|
3938
4178
|
* Titles and paths cannot change in place: put changing items (track
|
|
3939
4179
|
* lists, presets) in a child Scope and rebuild it.
|
|
3940
4180
|
* @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.
|
|
4181
|
+
* @capability commands
|
|
3941
4182
|
* @example Pause playback from a menu item
|
|
3942
4183
|
* Playback ▸ Pause (and Ctrl+P) pauses and resumes, shows a check mark
|
|
3943
4184
|
* while paused and is greyed out when nothing can be paused. The player
|
|
@@ -3960,6 +4201,20 @@ interface Commands {
|
|
|
3960
4201
|
}
|
|
3961
4202
|
```
|
|
3962
4203
|
|
|
4204
|
+
Example: Add a menu item with a default key
|
|
4205
|
+
|
|
4206
|
+
Tools ▸ Say hello, or Ctrl+H, shows a greeting. `add` declares the item;
|
|
4207
|
+
the player calls `run` when the user picks it or presses its key, and the
|
|
4208
|
+
user can change the key in the player's key settings.
|
|
4209
|
+
```ts
|
|
4210
|
+
plugin.commands.add({
|
|
4211
|
+
id: "hello",
|
|
4212
|
+
path: ["Tools", "Say hello"],
|
|
4213
|
+
bindings: ["Ctrl+H"], // a default; the user may override it
|
|
4214
|
+
run: () => plugin.osd.show("Hello"),
|
|
4215
|
+
});
|
|
4216
|
+
```
|
|
4217
|
+
|
|
3963
4218
|
### ImageRef
|
|
3964
4219
|
|
|
3965
4220
|
An image file for a Slint `image` property, made by Ui.image.
|
|
@@ -7071,29 +7326,100 @@ media.onEnd(async (ended) => {
|
|
|
7071
7326
|
|
|
7072
7327
|
A source this plugin opened.
|
|
7073
7328
|
|
|
7329
|
+
A source this plugin opened with Player.open. It does not own
|
|
7330
|
+
the source: the user or another plugin can replace it at any time, and
|
|
7331
|
+
`current` then turns false. `ready` and `onEnd` follow the player state:
|
|
7332
|
+
with `events` they react to the player's events, without it `ready`
|
|
7333
|
+
polls the state every 100 ms (which needs `playback.read`). Disposing the
|
|
7334
|
+
scope that opened it does not close the media; call `close` for that.
|
|
7335
|
+
|
|
7336
|
+
- @capability That of Player.open (`playback.control`); `ready` and `onEnd` follow the state through `events` and `playback.read`.
|
|
7337
|
+
|
|
7074
7338
|
```ts
|
|
7075
7339
|
interface Media {
|
|
7076
7340
|
/** Still the player's source (a restart keeps it current). */
|
|
7077
7341
|
readonly current: boolean;
|
|
7078
7342
|
/**
|
|
7079
7343
|
* 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.
|
|
7344
|
+
* @capability That of Player.open; `playback.read` or `events` to see the state change.
|
|
7345
|
+
* @throws failed (rejects) The media could not be loaded.
|
|
7346
|
+
* @throws replaced (rejects) Another source took over first.
|
|
7347
|
+
* @throws ended (rejects) The media already played to the end.
|
|
7348
|
+
* @throws aborted (rejects) `signal` aborted (an `AbortError`); the media is closed.
|
|
7349
|
+
* @example Wait at most ten seconds for a stream to start
|
|
7350
|
+
* A network stream gets ten seconds to start playing; otherwise it is
|
|
7351
|
+
* closed and the user is told. Aborting the signal makes `ready` reject
|
|
7352
|
+
* with an `AbortError` after closing the media.
|
|
7353
|
+
* ```ts
|
|
7354
|
+
* const media = await plugin.player.open("https://example.com/live.m3u8");
|
|
7355
|
+
* // The timeout's abort closes the media and rejects with an AbortError.
|
|
7356
|
+
* try {
|
|
7357
|
+
* await media.ready({ signal: AbortSignal.timeout(10_000) });
|
|
7358
|
+
* } catch (error) {
|
|
7359
|
+
* await plugin.osd.show("Not playing: {}", [(error as Error).message]);
|
|
7360
|
+
* }
|
|
7361
|
+
* ```
|
|
7080
7362
|
*/
|
|
7081
7363
|
ready(options?: {
|
|
7082
7364
|
signal?: AbortSignal;
|
|
7083
7365
|
}): Promise<void>;
|
|
7084
|
-
/**
|
|
7366
|
+
/**
|
|
7367
|
+
* Called once each time a playthrough of this media ends naturally.
|
|
7368
|
+
* @capability That of Player.open; `events` to hear the end.
|
|
7369
|
+
* @example Play the next file when this one ends
|
|
7370
|
+
* When the plugin's file ends naturally, the next one opens. The listener
|
|
7371
|
+
* gets the state at the end; passing it as `ifCurrent` makes the next
|
|
7372
|
+
* `open` fail with `stale` (and do nothing) if the user opened something
|
|
7373
|
+
* else in the meantime.
|
|
7374
|
+
* ```ts
|
|
7375
|
+
* media.onEnd((ended) =>
|
|
7376
|
+
* plugin.player.open(next, { ifCurrent: ended }).catch((error: PluginError) => {
|
|
7377
|
+
* if (error.code !== "stale") throw error; // the user opened something else
|
|
7378
|
+
* }),
|
|
7379
|
+
* );
|
|
7380
|
+
* ```
|
|
7381
|
+
*/
|
|
7085
7382
|
onEnd(listener: (state: PlayerState) => void): Disposable;
|
|
7086
|
-
/**
|
|
7383
|
+
/**
|
|
7384
|
+
* Close it if it is still the player's source; otherwise do nothing.
|
|
7385
|
+
* @capability That of Player.open (`playback.control`).
|
|
7386
|
+
* @throws capability (rejects) The manifest does not declare `playback.control`.
|
|
7387
|
+
* @example Close the preview the plugin opened
|
|
7388
|
+
* The plugin closes the preview it opened, but leaves alone anything the
|
|
7389
|
+
* user opened since. `close` acts only while this media is still the
|
|
7390
|
+
* player's source and does nothing otherwise.
|
|
7391
|
+
* ```ts
|
|
7392
|
+
* await preview.close(); // a no-op when something else is playing now
|
|
7393
|
+
* ```
|
|
7394
|
+
*/
|
|
7087
7395
|
close(): Promise<void>;
|
|
7088
7396
|
}
|
|
7089
7397
|
```
|
|
7090
7398
|
|
|
7399
|
+
Example: Open a file and say when it ends
|
|
7400
|
+
|
|
7401
|
+
The file opens and plays, and the OSD says "Finished" when it reaches its
|
|
7402
|
+
end on its own. The handle belongs to this source: `onEnd` fires only
|
|
7403
|
+
for a natural end of this media, not when someone opens another file.
|
|
7404
|
+
```ts
|
|
7405
|
+
const media = await plugin.player.open("/Movies/clip.mp4");
|
|
7406
|
+
media.onEnd(() => void plugin.osd.show("Finished")); // natural ends only
|
|
7407
|
+
```
|
|
7408
|
+
|
|
7091
7409
|
### Scrub
|
|
7092
7410
|
|
|
7093
7411
|
A drag gesture: each seek is conditional on the previous one, latest value wins.
|
|
7094
7412
|
Any outside change (another plugin opens a file, the user pauses) ends
|
|
7095
7413
|
the gesture.
|
|
7096
7414
|
|
|
7415
|
+
A drag gesture on a seek slider. Each `seek` is conditional on the
|
|
7416
|
+
previous one and only the newest target is sent while a seek is
|
|
7417
|
+
running, so a fast drag never queues seeks. Any outside change (another
|
|
7418
|
+
file, the user pausing) ends the gesture: `active` turns false and later
|
|
7419
|
+
seeks resolve `false`.
|
|
7420
|
+
|
|
7421
|
+
- @capability playback.control
|
|
7422
|
+
|
|
7097
7423
|
```ts
|
|
7098
7424
|
interface Scrub {
|
|
7099
7425
|
/**
|
|
@@ -7113,6 +7439,11 @@ interface Scrub {
|
|
|
7113
7439
|
* the target of a keyframe seek is always sent. Omitted or `false`: exact,
|
|
7114
7440
|
* as before. Older players ignore the option and seek exactly: set
|
|
7115
7441
|
* `"minPlayerVersion": "0.5.3"` when you rely on it.
|
|
7442
|
+
* @capability playback.control
|
|
7443
|
+
* @throws capability (rejects) The manifest does not declare `playback.control`.
|
|
7444
|
+
* @throws stale (rejects) The source changed from outside while this seek was being sent (a superseded target or an ended gesture resolves `false` instead).
|
|
7445
|
+
* @throws unsupported (rejects) The source is a generated stream.
|
|
7446
|
+
* @throws backend (rejects) The player failed the seek.
|
|
7116
7447
|
* @example Scrub on keyframes for a fast picture
|
|
7117
7448
|
* While dragging, the picture jumps to the nearest earlier keyframe at
|
|
7118
7449
|
* once instead of decoding up to the exact frame. With `keyframes: true`
|
|
@@ -7128,7 +7459,17 @@ interface Scrub {
|
|
|
7128
7459
|
}): Promise<boolean>;
|
|
7129
7460
|
/** False once anyone else changed the source; later seeks are ignored. */
|
|
7130
7461
|
readonly active: boolean;
|
|
7131
|
-
/**
|
|
7462
|
+
/**
|
|
7463
|
+
* End the gesture; later seeks resolve `false`.
|
|
7464
|
+
* @capability playback.control
|
|
7465
|
+
* @example End the gesture when the user lets go
|
|
7466
|
+
* Releasing the slider ends the gesture, so a late `dragMove` cannot move
|
|
7467
|
+
* playback any more. After `end`, `seek` resolves `false` without doing
|
|
7468
|
+
* anything.
|
|
7469
|
+
* ```ts
|
|
7470
|
+
* bar.on("dragEnd", () => scrub.end()); // later seeks resolve false
|
|
7471
|
+
* ```
|
|
7472
|
+
*/
|
|
7132
7473
|
end(): void;
|
|
7133
7474
|
}
|
|
7134
7475
|
```
|
|
@@ -7206,6 +7547,15 @@ type StreamStartError = PluginError & {
|
|
|
7206
7547
|
|
|
7207
7548
|
A generated source, from Player.openStream. Disposing it stops it.
|
|
7208
7549
|
|
|
7550
|
+
A source whose frames this plugin generates, from
|
|
7551
|
+
Player.openStream. The SDK pulls frames on the audio clock over
|
|
7552
|
+
a local connection and handles prefill, activation and pausing; the plugin
|
|
7553
|
+
only supplies `frame()`. A stream ends when the plugin stops it, when
|
|
7554
|
+
another source replaces it or the user closes it (`onStop` with `null`),
|
|
7555
|
+
or when it fails (`onStop` with the error).
|
|
7556
|
+
|
|
7557
|
+
- @capability producer.av and playback.control
|
|
7558
|
+
|
|
7209
7559
|
```ts
|
|
7210
7560
|
interface Stream extends Disposable {
|
|
7211
7561
|
/** The stream is producing frames. */
|
|
@@ -7222,15 +7572,51 @@ interface Stream extends Disposable {
|
|
|
7222
7572
|
* (before 1.5.7 a replaced or closed stream could get `unavailable`
|
|
7223
7573
|
* "producer absent"). Without `playback.read` a failure the SDK has not
|
|
7224
7574
|
* seen yet is reported as null.
|
|
7575
|
+
* @capability producer.av
|
|
7576
|
+
* @example Tell the user why the stream stopped
|
|
7577
|
+
* The OSD explains a failure, and nothing is said when the user simply
|
|
7578
|
+
* opened another file. The listener runs only for stops from outside the
|
|
7579
|
+
* plugin: `null` for a replacement or the user's close, an error (such as
|
|
7580
|
+
* `stream` with the player's text) for a failure.
|
|
7581
|
+
* ```ts
|
|
7582
|
+
* stream.onStop((error) => {
|
|
7583
|
+
* if (error) void plugin.osd.show("Generator stopped: {}", [error.message]);
|
|
7584
|
+
* });
|
|
7585
|
+
* ```
|
|
7225
7586
|
*/
|
|
7226
7587
|
onStop(listener: (error: PluginError | null) => void): Disposable;
|
|
7227
7588
|
/**
|
|
7228
7589
|
* 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.
|
|
7590
|
+
* @capability producer.av and playback.control
|
|
7591
|
+
* @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.
|
|
7592
|
+
* @example Stop the stream when the plugin's window closes
|
|
7593
|
+
* Closing the generator window stops the stream it feeds. `stop` closes
|
|
7594
|
+
* the player's source if it is still this stream and then the frame
|
|
7595
|
+
* connection; it resolves once both are closed.
|
|
7596
|
+
* ```ts
|
|
7597
|
+
* window.onClose(() => stream.stop());
|
|
7598
|
+
* ```
|
|
7229
7599
|
*/
|
|
7230
7600
|
stop(): Promise<void>;
|
|
7231
7601
|
}
|
|
7232
7602
|
```
|
|
7233
7603
|
|
|
7604
|
+
Example: Stop the stream from a menu item
|
|
7605
|
+
|
|
7606
|
+
A menu item ends the plugin's generated stream and closes it as the
|
|
7607
|
+
player's source. `stop` closes the source first and then the frame
|
|
7608
|
+
connection, so the user never sees a playback error; `onStop` is not
|
|
7609
|
+
called for the plugin's own stop.
|
|
7610
|
+
```ts
|
|
7611
|
+
// Enabled while the stream produces frames.
|
|
7612
|
+
plugin.commands.add({
|
|
7613
|
+
id: "stop-generator",
|
|
7614
|
+
path: ["@playback", "Stop generator"],
|
|
7615
|
+
enabled: () => stream.running,
|
|
7616
|
+
run: () => stream.stop(),
|
|
7617
|
+
});
|
|
7618
|
+
```
|
|
7619
|
+
|
|
7234
7620
|
### SequenceProvider
|
|
7235
7621
|
|
|
7236
7622
|
The plugin that owns "next / previous" (a playlist).
|
|
@@ -7337,15 +7723,67 @@ Capabilities: reading (`state` at start, `refresh`, `stats`, `chapters`,
|
|
|
7337
7723
|
Controls are conditional (see Conditional): they fail with
|
|
7338
7724
|
`stale` and do nothing when the source changed since the state you saw.
|
|
7339
7725
|
|
|
7726
|
+
`plugin.player` is the player's one source: what is open, its phase and
|
|
7727
|
+
position, and the controls. `state` is a snapshot the SDK keeps current
|
|
7728
|
+
from the player's events (with `events`) and refreshes after this
|
|
7729
|
+
plugin's own calls; `onChange` runs after it changed. Every control is
|
|
7730
|
+
conditional: it acts on the state the plugin last saw and fails with
|
|
7731
|
+
`stale` when the source changed meanwhile: another file, a close, or any
|
|
7732
|
+
pause, seek or speed change by the user or another plugin (see
|
|
7733
|
+
Conditional). The player keeps the source when a plugin stops;
|
|
7734
|
+
only the plugin's own handles (a stream, its sequence provider, its
|
|
7735
|
+
output device) are withdrawn.
|
|
7736
|
+
|
|
7737
|
+
- @capability `playback.read` to read; `playback.control` to control; `events` to follow changes made elsewhere; `producer.av` for Player.openStream.
|
|
7738
|
+
|
|
7340
7739
|
```ts
|
|
7341
7740
|
interface Player {
|
|
7342
7741
|
/** Latest state; always newer than anything delivered before. */
|
|
7343
7742
|
readonly state: PlayerState;
|
|
7344
|
-
/**
|
|
7743
|
+
/**
|
|
7744
|
+
* The state changed (needs `events` for outside changes).
|
|
7745
|
+
* @capability events (for changes made elsewhere; without it the listener runs only after this plugin's own calls)
|
|
7746
|
+
* @example Show the position while it plays
|
|
7747
|
+
* The OSD block shows the position, updated as playback moves. The SDK
|
|
7748
|
+
* updates `player.state` from the player's events and then calls the
|
|
7749
|
+
* listener with the new snapshot; the handler only reads it.
|
|
7750
|
+
* ```ts
|
|
7751
|
+
* const info = plugin.osd.panel();
|
|
7752
|
+
* plugin.player.onChange((state) => {
|
|
7753
|
+
* // `position` is null while nothing is open.
|
|
7754
|
+
* info.set(state.position === null ? null : "{} s", [state.position?.toFixed(0) ?? ""]);
|
|
7755
|
+
* });
|
|
7756
|
+
* ```
|
|
7757
|
+
*/
|
|
7345
7758
|
onChange(listener: (state: PlayerState) => void): Disposable;
|
|
7346
|
-
/**
|
|
7759
|
+
/**
|
|
7760
|
+
* Read the state from the player now (`playback.read`).
|
|
7761
|
+
* @capability playback.read
|
|
7762
|
+
* @throws capability (rejects) The manifest does not declare `playback.read`.
|
|
7763
|
+
* @example Read the state before deciding
|
|
7764
|
+
* A command reads the newest state from the player before acting, even
|
|
7765
|
+
* without `events`. `refresh` asks the player and resolves with the new
|
|
7766
|
+
* snapshot; `player.state` holds it afterwards.
|
|
7767
|
+
* ```ts
|
|
7768
|
+
* const state = await plugin.player.refresh();
|
|
7769
|
+
* if (state.phase === "ready") await plugin.osd.show("{} s left", [((state.duration ?? 0) - (state.position ?? 0)).toFixed(0)]);
|
|
7770
|
+
* ```
|
|
7771
|
+
*/
|
|
7347
7772
|
refresh(): Promise<PlayerState>;
|
|
7348
|
-
/**
|
|
7773
|
+
/**
|
|
7774
|
+
* True if nothing changed the source since `state` was observed.
|
|
7775
|
+
* @capability None.
|
|
7776
|
+
* @example Drop a result that arrives after the file changed
|
|
7777
|
+
* A slow lookup for the current file is shown only if that file is still
|
|
7778
|
+
* open. `isCurrent` compares the snapshot taken before the work with the
|
|
7779
|
+
* SDK's newest state; it is false once the source changed in any way:
|
|
7780
|
+
* another file, a close, or a pause, seek or speed change by anyone.
|
|
7781
|
+
* ```ts
|
|
7782
|
+
* const seen = plugin.player.state;
|
|
7783
|
+
* const title = await lookUpTitle(seen.source ?? "");
|
|
7784
|
+
* if (plugin.player.isCurrent(seen)) await plugin.osd.show("{}", [title]); // still the same file
|
|
7785
|
+
* ```
|
|
7786
|
+
*/
|
|
7349
7787
|
isCurrent(state: PlayerState): boolean;
|
|
7350
7788
|
/**
|
|
7351
7789
|
* Open a path or URL, replacing the current source (unless `ifCurrent` no
|
|
@@ -7356,6 +7794,25 @@ interface Player {
|
|
|
7356
7794
|
* Resolver are resolved first, and so are local files of an
|
|
7357
7795
|
* extension a file resolver claims (the source is then a stream that
|
|
7358
7796
|
* plugin generates; `paused` reaches it).
|
|
7797
|
+
* @capability playback.control
|
|
7798
|
+
* @throws stale (rejects) `ifCurrent` no longer holds; nothing was opened.
|
|
7799
|
+
* @throws replaced (rejects) Accepted, then another source took over before it finished (for example while a URL was still being resolved).
|
|
7800
|
+
* @throws invalid (rejects) An empty `source`.
|
|
7801
|
+
* @throws aborted (rejects) `signal` aborted (an `AbortError`); the source is closed if it was opened.
|
|
7802
|
+
* @throws busy (rejects) The address needs a resolver plugin and the player is already resolving 32 addresses.
|
|
7803
|
+
* @throws backend (rejects) A resolver plugin handled the address or file and failed (its message), or the resolution was cancelled or expired.
|
|
7804
|
+
* @throws unsupported (rejects) The player refused what a resolver returned (for example split streams with different headers).
|
|
7805
|
+
* @throws unavailable (rejects) The stream a file resolver prepared was not ready to play.
|
|
7806
|
+
* @throws capability (rejects) The manifest does not declare `playback.control`.
|
|
7807
|
+
* @example Open a file paused
|
|
7808
|
+
* The file opens at its start without playing. `open` resolves once the
|
|
7809
|
+
* player accepted the load and returns a Media handle for that
|
|
7810
|
+
* source; `ready` then waits until it can play (here: until it is loaded
|
|
7811
|
+
* and paused).
|
|
7812
|
+
* ```ts
|
|
7813
|
+
* const media = await plugin.player.open("/Movies/clip.mp4", { paused: true });
|
|
7814
|
+
* await media.ready(); // rejects `failed` when the file cannot be loaded
|
|
7815
|
+
* ```
|
|
7359
7816
|
*/
|
|
7360
7817
|
open(source: string, options?: {
|
|
7361
7818
|
paused?: boolean;
|
|
@@ -7372,6 +7829,13 @@ interface Player {
|
|
|
7372
7829
|
* TCP with a token on Windows. Rejects with a StreamStartError
|
|
7373
7830
|
* (`timeout`), `stream`, `device`, or `replaced` when another source
|
|
7374
7831
|
* replaced it (or it was closed) before it ran.
|
|
7832
|
+
* @capability producer.av and playback.control
|
|
7833
|
+
* @throws timeout (rejects) A StreamStartError: the stream did not start in time.
|
|
7834
|
+
* @throws stream (rejects) The player failed to start the stream.
|
|
7835
|
+
* @throws device (rejects) The audio device was not ready.
|
|
7836
|
+
* @throws replaced (rejects) Another source replaced it, or it was closed, before it ran.
|
|
7837
|
+
* @throws invalid (rejects) Bad options (a size or a frame function).
|
|
7838
|
+
* @throws aborted (rejects) `signal` aborted, or the scope was disposed, while the stream was starting (an `AbortError`).
|
|
7375
7839
|
* @example Play pictures the plugin draws itself
|
|
7376
7840
|
* The player shows a 320×240 picture the plugin renders, with silence as
|
|
7377
7841
|
* its sound, as the current source. The SDK pulls frames on the audio
|
|
@@ -7392,25 +7856,143 @@ interface Player {
|
|
|
7392
7856
|
* ```
|
|
7393
7857
|
*/
|
|
7394
7858
|
openStream(options: StreamOptions): Promise<Stream>;
|
|
7395
|
-
/**
|
|
7859
|
+
/**
|
|
7860
|
+
* Pause or resume.
|
|
7861
|
+
* @capability playback.control
|
|
7862
|
+
* @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.
|
|
7863
|
+
* @throws invalid (rejects) Nothing is open.
|
|
7864
|
+
* @throws busy (rejects) A generated stream is still handling a previous control.
|
|
7865
|
+
* @throws capability (rejects) The manifest does not declare `playback.control`.
|
|
7866
|
+
* @example Pause, but only the file the user was watching
|
|
7867
|
+
* A "pause on lock" helper pauses the current file and does nothing when
|
|
7868
|
+
* another file was opened in between. The call is conditional on
|
|
7869
|
+
* `player.state` by default; `stale` means the source changed and the
|
|
7870
|
+
* pause was not applied, which is the intended outcome here.
|
|
7871
|
+
* ```ts
|
|
7872
|
+
* try {
|
|
7873
|
+
* await plugin.player.setPaused(true);
|
|
7874
|
+
* } catch (error) {
|
|
7875
|
+
* if ((error as PluginError).code !== "stale") throw error; // another file now
|
|
7876
|
+
* }
|
|
7877
|
+
* ```
|
|
7878
|
+
*/
|
|
7396
7879
|
setPaused(paused: boolean, options?: Conditional): Promise<void>;
|
|
7397
|
-
/**
|
|
7880
|
+
/**
|
|
7881
|
+
* Jump to `seconds`.
|
|
7882
|
+
* @capability playback.control
|
|
7883
|
+
* @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.
|
|
7884
|
+
* @throws invalid (rejects) Nothing is open, or `seconds` is negative or not finite.
|
|
7885
|
+
* @throws unsupported (rejects) The source is a generated stream, which cannot seek.
|
|
7886
|
+
* @throws busy (rejects) A generated stream is still handling a previous control.
|
|
7887
|
+
* @throws capability (rejects) The manifest does not declare `playback.control`.
|
|
7888
|
+
* @example Jump back ten seconds
|
|
7889
|
+
* Playback jumps ten seconds back, never before the start. The target is
|
|
7890
|
+
* computed from the snapshot the call is conditional on, so if the user
|
|
7891
|
+
* opened another file meanwhile the seek fails with `stale` instead of
|
|
7892
|
+
* landing in the wrong file.
|
|
7893
|
+
* ```ts
|
|
7894
|
+
* const { position } = plugin.player.state;
|
|
7895
|
+
* if (position !== null && plugin.player.state.can.seek)
|
|
7896
|
+
* await plugin.player.seek(Math.max(0, position - 10)); // seconds
|
|
7897
|
+
* ```
|
|
7898
|
+
*/
|
|
7398
7899
|
seek(seconds: number, options?: Conditional): Promise<void>;
|
|
7399
|
-
/**
|
|
7900
|
+
/**
|
|
7901
|
+
* Set the playback speed.
|
|
7902
|
+
* @capability playback.control
|
|
7903
|
+
* @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.
|
|
7904
|
+
* @throws invalid (rejects) Nothing is open, or `rate` is outside 0.05–10.
|
|
7905
|
+
* @throws unsupported (rejects) The source is a generated stream, which cannot change speed.
|
|
7906
|
+
* @throws busy (rejects) A generated stream is still handling a previous control.
|
|
7907
|
+
* @throws capability (rejects) The manifest does not declare `playback.control`.
|
|
7908
|
+
* @example Play at one and a half times the speed
|
|
7909
|
+
* Playback speeds up to 1.5×, with the speed on the OSD. `setRate` sets
|
|
7910
|
+
* the speed directly (the player's own keys step it by 0.1 within
|
|
7911
|
+
* 0.25–4); `state.can.rate` is false for sources that cannot change speed.
|
|
7912
|
+
* ```ts
|
|
7913
|
+
* if (plugin.player.state.can.rate) {
|
|
7914
|
+
* await plugin.player.setRate(1.5);
|
|
7915
|
+
* await plugin.osd.show("Speed {}×", [1.5]);
|
|
7916
|
+
* }
|
|
7917
|
+
* ```
|
|
7918
|
+
*/
|
|
7400
7919
|
setRate(rate: number, options?: Conditional): Promise<void>;
|
|
7401
|
-
/**
|
|
7920
|
+
/**
|
|
7921
|
+
* Play from the start (the "replay" after the end).
|
|
7922
|
+
* @capability playback.control
|
|
7923
|
+
* @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.
|
|
7924
|
+
* @throws invalid (rejects) Nothing is open.
|
|
7925
|
+
* @throws unsupported (rejects) The source is a generated stream, which cannot restart.
|
|
7926
|
+
* @throws capability (rejects) The manifest does not declare `playback.control`.
|
|
7927
|
+
* @example Replay a file after it ended
|
|
7928
|
+
* A Replay button starts the file again from its start once it has ended.
|
|
7929
|
+
* `restart` plays the current source from the beginning and keeps it the
|
|
7930
|
+
* same source, so a Media handle stays current.
|
|
7931
|
+
* ```ts
|
|
7932
|
+
* if (plugin.player.state.phase === "ended") await plugin.player.restart();
|
|
7933
|
+
* ```
|
|
7934
|
+
*/
|
|
7402
7935
|
restart(options?: Conditional): Promise<void>;
|
|
7403
|
-
/**
|
|
7936
|
+
/**
|
|
7937
|
+
* Close the current source.
|
|
7938
|
+
* @capability playback.control
|
|
7939
|
+
* @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.
|
|
7940
|
+
* @throws capability (rejects) The manifest does not declare `playback.control`.
|
|
7941
|
+
* @example Close the file when the sleep timer runs out
|
|
7942
|
+
* After 30 minutes the open file is closed, unless the user opened another
|
|
7943
|
+
* one meanwhile. The snapshot taken when the timer started is passed as
|
|
7944
|
+
* `ifCurrent`, so the close applies only to that source; `stale` means it
|
|
7945
|
+
* was replaced.
|
|
7946
|
+
* ```ts
|
|
7947
|
+
* const seen = plugin.player.state;
|
|
7948
|
+
* setTimeout(() => {
|
|
7949
|
+
* // Only the source that was open when the timer started.
|
|
7950
|
+
* plugin.player.close({ ifCurrent: seen }).catch((error: PluginError) => {
|
|
7951
|
+
* if (error.code !== "stale") plugin.osd.show("Could not close: {}", [error.code]);
|
|
7952
|
+
* });
|
|
7953
|
+
* }, 30 * 60 * 1000);
|
|
7954
|
+
* ```
|
|
7955
|
+
*/
|
|
7404
7956
|
close(options?: Conditional): Promise<void>;
|
|
7405
|
-
/**
|
|
7957
|
+
/**
|
|
7958
|
+
* Start a drag gesture from the current state.
|
|
7959
|
+
* @capability playback.control
|
|
7960
|
+
* @example Start a gesture when the user grabs the slider
|
|
7961
|
+
* A drag on the plugin's seek slider becomes one gesture whose seeks never
|
|
7962
|
+
* pile up. `scrub()` captures the current state; the gesture's seeks are
|
|
7963
|
+
* conditional on it and on each other, and it ends on its own when
|
|
7964
|
+
* someone else changes the source.
|
|
7965
|
+
* ```ts
|
|
7966
|
+
* let scrub: Scrub | null = null;
|
|
7967
|
+
* bar.on("dragStart", () => (scrub = plugin.player.scrub()));
|
|
7968
|
+
* bar.on("dragEnd", () => scrub?.end());
|
|
7969
|
+
* ```
|
|
7970
|
+
*/
|
|
7406
7971
|
scrub(): Scrub;
|
|
7407
|
-
/**
|
|
7972
|
+
/**
|
|
7973
|
+
* 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.
|
|
7974
|
+
* @capability playback.control
|
|
7975
|
+
* @throws invalid (rejects) `volume` outside 0–200, or `muted` not a boolean.
|
|
7976
|
+
* @throws capability (rejects) The manifest does not declare `playback.control`.
|
|
7977
|
+
* @example Duck the volume during an announcement
|
|
7978
|
+
* The volume drops to 30 while the plugin's announcement plays, then
|
|
7979
|
+
* returns. Writes are "latest wins": the promise resolves `true` when this
|
|
7980
|
+
* write was applied and `false` when a newer one replaced it first, which
|
|
7981
|
+
* is not an error.
|
|
7982
|
+
* ```ts
|
|
7983
|
+
* const before = plugin.player.state.volume; // 0–200
|
|
7984
|
+
* await plugin.player.setOutput({ volume: 30 });
|
|
7985
|
+
* // … the announcement …
|
|
7986
|
+
* await plugin.player.setOutput({ volume: before });
|
|
7987
|
+
* ```
|
|
7988
|
+
*/
|
|
7408
7989
|
setOutput(output: {
|
|
7409
7990
|
volume?: number;
|
|
7410
7991
|
muted?: boolean;
|
|
7411
7992
|
}): Promise<boolean>;
|
|
7412
7993
|
/**
|
|
7413
|
-
* Become the sequence provider (
|
|
7994
|
+
* 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.
|
|
7995
|
+
* @capability playback.control
|
|
7414
7996
|
* @example Drive the player's Next and Previous from a playlist
|
|
7415
7997
|
* The player's Next and Previous items and keys play the plugin's list.
|
|
7416
7998
|
* The player calls `next` and `previous` when the user asks; `canNext`
|
|
@@ -7432,19 +8014,99 @@ interface Player {
|
|
|
7432
8014
|
* ```
|
|
7433
8015
|
*/
|
|
7434
8016
|
provideSequence(provider: SequenceProvider): Disposable;
|
|
7435
|
-
/**
|
|
8017
|
+
/**
|
|
8018
|
+
* Ask the provider to go forward / back. Rejects `unavailable` when it cannot.
|
|
8019
|
+
* @capability playback.control
|
|
8020
|
+
* @throws unavailable (rejects) There is no sequence provider, or it has no next item.
|
|
8021
|
+
* @throws capability (rejects) The manifest does not declare `playback.control`.
|
|
8022
|
+
* @example Skip to the next item from a plugin button
|
|
8023
|
+
* The plugin's own Next button plays the next item of whichever plugin
|
|
8024
|
+
* provides the sequence (the playlist, usually). `next` asks the
|
|
8025
|
+
* provider, the same as the player's Next key; `unavailable` means there
|
|
8026
|
+
* is no next item, which the button treats as the end of the list.
|
|
8027
|
+
* ```ts
|
|
8028
|
+
* try {
|
|
8029
|
+
* await plugin.player.next(); // as the player's Next key does
|
|
8030
|
+
* } catch (error) {
|
|
8031
|
+
* if ((error as PluginError).code !== "unavailable") throw error;
|
|
8032
|
+
* await plugin.osd.show("End of the list");
|
|
8033
|
+
* }
|
|
8034
|
+
* ```
|
|
8035
|
+
*/
|
|
7436
8036
|
next(): Promise<void>;
|
|
7437
|
-
/**
|
|
8037
|
+
/**
|
|
8038
|
+
* Ask the provider to go back. Rejects `unavailable` when it cannot.
|
|
8039
|
+
* @capability playback.control
|
|
8040
|
+
* @throws unavailable (rejects) There is no sequence provider, or it has no previous item.
|
|
8041
|
+
* @throws capability (rejects) The manifest does not declare `playback.control`.
|
|
8042
|
+
* @example Go back to the previous item
|
|
8043
|
+
* The plugin's Previous button asks the sequence provider for the item
|
|
8044
|
+
* before the current one, like the player's PageUp. It is disabled while
|
|
8045
|
+
* `state.can.previous` is false, which the provider's `canPrevious`
|
|
8046
|
+
* decides.
|
|
8047
|
+
* ```ts
|
|
8048
|
+
* plugin.commands.add({
|
|
8049
|
+
* id: "previous",
|
|
8050
|
+
* title: "Previous item",
|
|
8051
|
+
* // The provider's `canPrevious`, as the player reports it.
|
|
8052
|
+
* enabled: () => plugin.player.state.can.previous,
|
|
8053
|
+
* run: () => plugin.player.previous(),
|
|
8054
|
+
* });
|
|
8055
|
+
* ```
|
|
8056
|
+
*/
|
|
7438
8057
|
previous(): Promise<void>;
|
|
7439
|
-
/**
|
|
8058
|
+
/**
|
|
8059
|
+
* Codec, size, frame counts and backend of the current playback. `playback.read`; `video` and `audio` are `null` without a source.
|
|
8060
|
+
* @capability playback.read
|
|
8061
|
+
* @throws capability (rejects) The manifest does not declare `playback.read`.
|
|
8062
|
+
* @example Show the video codec and size
|
|
8063
|
+
* The OSD shows something like "h264 1920×1080". `stats` asks the player
|
|
8064
|
+
* for the backend's own values at that moment; `video` is `null` without
|
|
8065
|
+
* a video stream (nothing open, an audio file).
|
|
8066
|
+
* ```ts
|
|
8067
|
+
* const { video } = await plugin.player.stats();
|
|
8068
|
+
* if (video) await plugin.osd.show("{} {}×{}", [video.codec ?? "?", video.width ?? 0, video.height ?? 0]);
|
|
8069
|
+
* ```
|
|
8070
|
+
*/
|
|
7440
8071
|
stats(): Promise<PlaybackStats>;
|
|
7441
8072
|
/**
|
|
7442
8073
|
* 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.
|
|
8074
|
+
* @capability playback.control
|
|
8075
|
+
* @throws invalid (rejects) Not an absolute path to a file.
|
|
8076
|
+
* @throws unavailable (rejects) No ready media.
|
|
8077
|
+
* @throws backend (rejects) The player could not load the file.
|
|
8078
|
+
* @throws stale (rejects) `ifCurrent` no longer holds.
|
|
8079
|
+
* @throws capability (rejects) The manifest does not declare `playback.control`.
|
|
8080
|
+
* @example Add a dubbed audio file and switch to it
|
|
8081
|
+
* A separate audio file plays with the film instead of its own sound.
|
|
8082
|
+
* `addAudioTrack` attaches the file to the current media and selects it;
|
|
8083
|
+
* the track stays until another source opens.
|
|
8084
|
+
* ```ts
|
|
8085
|
+
* const track = await plugin.player.addAudioTrack(dubPath); // selected too
|
|
8086
|
+
* await plugin.osd.show("Audio: {}", [track.title ?? dubPath]);
|
|
8087
|
+
* ```
|
|
7443
8088
|
*/
|
|
7444
8089
|
addAudioTrack(path: string, options?: {
|
|
7445
8090
|
select?: boolean;
|
|
7446
8091
|
} & Conditional): Promise<AudioTrack>;
|
|
7447
|
-
/**
|
|
8092
|
+
/**
|
|
8093
|
+
* Select one of `state.audioTracks` by id; `invalid` for an unknown id.
|
|
8094
|
+
* @capability playback.control
|
|
8095
|
+
* @throws invalid (rejects) No track with this id.
|
|
8096
|
+
* @throws unavailable (rejects) No ready media.
|
|
8097
|
+
* @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).
|
|
8098
|
+
* @throws capability (rejects) The manifest does not declare `playback.control`.
|
|
8099
|
+
* @example Switch to the next audio track
|
|
8100
|
+
* Each press of the plugin's key moves to the next audio track of the
|
|
8101
|
+
* file. The tracks and the selected one are in `state.audioTracks`;
|
|
8102
|
+
* `selectAudioTrack` resolves with the new state.
|
|
8103
|
+
* ```ts
|
|
8104
|
+
* const audioTracks = plugin.player.state.audioTracks ?? []; // null: not known yet
|
|
8105
|
+
* const at = audioTracks.findIndex((track) => track.selected);
|
|
8106
|
+
* const next = audioTracks[(at + 1) % audioTracks.length];
|
|
8107
|
+
* if (next) await plugin.player.selectAudioTrack(next.id);
|
|
8108
|
+
* ```
|
|
8109
|
+
*/
|
|
7448
8110
|
selectAudioTrack(id: number, options?: Conditional): Promise<PlayerState>;
|
|
7449
8111
|
/**
|
|
7450
8112
|
* One frame or to the next / previous keyframe (pauses for frames). Rejects
|
|
@@ -7454,6 +8116,19 @@ interface Player {
|
|
|
7454
8116
|
* Resolves once the new position is in effect, so `player.state.position`
|
|
7455
8117
|
* is current afterwards. One step at a time: a second call while one is
|
|
7456
8118
|
* in flight rejects `busy`.
|
|
8119
|
+
* @capability playback.control
|
|
8120
|
+
* @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.
|
|
8121
|
+
* @throws busy (rejects) A step is still in flight.
|
|
8122
|
+
* @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).
|
|
8123
|
+
* @throws capability (rejects) The manifest does not declare `playback.control`.
|
|
8124
|
+
* @example Step one frame forward for a close look
|
|
8125
|
+
* The picture advances by one frame and playback pauses. `step` resolves
|
|
8126
|
+
* once the new position is in effect, so `state.position` is current
|
|
8127
|
+
* afterwards; a second step while one is running rejects with `busy`.
|
|
8128
|
+
* ```ts
|
|
8129
|
+
* await plugin.player.step("frame", "forward");
|
|
8130
|
+
* await plugin.osd.show("{} s", [plugin.player.state.position?.toFixed(3) ?? "?"]);
|
|
8131
|
+
* ```
|
|
7457
8132
|
*/
|
|
7458
8133
|
step(unit: "frame" | "keyframe", direction: "backward" | "forward", options?: Conditional): Promise<void>;
|
|
7459
8134
|
/**
|
|
@@ -7476,12 +8151,37 @@ interface Player {
|
|
|
7476
8151
|
*
|
|
7477
8152
|
* Older players ignore `keyframes`: set `"minPlayerVersion": "0.5.1"` in
|
|
7478
8153
|
* the manifest when you rely on it.
|
|
8154
|
+
* @capability playback.control
|
|
8155
|
+
* @throws invalid (rejects) `seconds` outside 0.1–3600 or not in tenths, or `keyframes` not a boolean.
|
|
8156
|
+
* @throws capability (rejects) The manifest does not declare `playback.control`.
|
|
8157
|
+
* @example Make ← and → jump 10 seconds to keyframes
|
|
8158
|
+
* The arrow keys now jump 10 seconds and land on keyframes, which is fast
|
|
8159
|
+
* on large files. The newest call of any plugin wins and is withdrawn
|
|
8160
|
+
* when that plugin stops, so a plugin offering this as a setting applies
|
|
8161
|
+
* it on activation.
|
|
8162
|
+
* ```ts
|
|
8163
|
+
* await plugin.player.setStepSeek({ seconds: 10, keyframes: true });
|
|
8164
|
+
* ```
|
|
7479
8165
|
*/
|
|
7480
8166
|
setStepSeek(options: {
|
|
7481
8167
|
seconds: number;
|
|
7482
8168
|
keyframes?: boolean;
|
|
7483
8169
|
}): Promise<void>;
|
|
7484
|
-
/**
|
|
8170
|
+
/**
|
|
8171
|
+
* The chapters of the current media (empty without media or chapters), read on demand; `sourceId` tells which source they belong to. `playback.read`.
|
|
8172
|
+
* @capability playback.read
|
|
8173
|
+
* @throws capability (rejects) The manifest does not declare `playback.read`.
|
|
8174
|
+
* @example List the chapters of the open file
|
|
8175
|
+
* The plugin reads the chapter titles of the current file to show them in
|
|
8176
|
+
* its panel. `chapters` asks the player when called; `sourceId` says which
|
|
8177
|
+
* source they belong to, so a reply for a file that was replaced meanwhile
|
|
8178
|
+
* can be dropped.
|
|
8179
|
+
* ```ts
|
|
8180
|
+
* const { sourceId, chapters } = await plugin.player.chapters();
|
|
8181
|
+
* if (sourceId === plugin.player.state.sourceId) // still the same file
|
|
8182
|
+
* console.log(chapters.map((chapter) => chapter.title));
|
|
8183
|
+
* ```
|
|
8184
|
+
*/
|
|
7485
8185
|
chapters(): Promise<{
|
|
7486
8186
|
/** The source the chapters belong to (compare with `state.sourceId`). */
|
|
7487
8187
|
sourceId: number;
|
|
@@ -7490,13 +8190,45 @@ interface Player {
|
|
|
7490
8190
|
}>;
|
|
7491
8191
|
/**
|
|
7492
8192
|
* 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.
|
|
8193
|
+
* @capability playback.control
|
|
8194
|
+
* @throws invalid (rejects) `a`/`b` out of range or `count` not a positive integer.
|
|
8195
|
+
* @throws unsupported (rejects) The source cannot seek, or is a generated stream.
|
|
8196
|
+
* @throws unavailable (rejects) No ready media.
|
|
8197
|
+
* @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).
|
|
8198
|
+
* @throws capability (rejects) The manifest does not declare `playback.control`.
|
|
8199
|
+
* @example Loop a passage three times
|
|
8200
|
+
* Playback repeats 1:00–1:30 three times, then goes on. The player jumps
|
|
8201
|
+
* back to `a` each time it reaches `b`; after the last repeat
|
|
8202
|
+
* `state.loop` becomes `null`, and a new source clears the loop.
|
|
8203
|
+
* ```ts
|
|
8204
|
+
* await plugin.player.setLoop({ a: 60, b: 90, count: 3 }); // seconds
|
|
8205
|
+
* ```
|
|
7493
8206
|
*/
|
|
7494
8207
|
setLoop(loop: {
|
|
7495
8208
|
a: number;
|
|
7496
8209
|
b: number;
|
|
7497
8210
|
count?: number;
|
|
7498
8211
|
} | null, options?: Conditional): Promise<void>;
|
|
7499
|
-
/**
|
|
8212
|
+
/**
|
|
8213
|
+
* The output devices present now (not the system default entry). `playback.read`.
|
|
8214
|
+
* @capability playback.read
|
|
8215
|
+
* @throws capability (rejects) The manifest does not declare `playback.read`.
|
|
8216
|
+
* @example Offer the audio output devices in a menu
|
|
8217
|
+
* A submenu lists the output devices present now, with a radio mark on the
|
|
8218
|
+
* one in use. `audioDevices` asks the player when called (devices come and
|
|
8219
|
+
* go), and `state.audioOutput` tells which one is active.
|
|
8220
|
+
* ```ts
|
|
8221
|
+
* // The devices present now; build the menu again when they change.
|
|
8222
|
+
* for (const device of await plugin.player.audioDevices())
|
|
8223
|
+
* plugin.commands.add({
|
|
8224
|
+
* id: `device.${device.id}`,
|
|
8225
|
+
* path: ["@audio", "Output", device.name],
|
|
8226
|
+
* radio: true,
|
|
8227
|
+
* checked: () => plugin.player.state.audioOutput?.active === device.id,
|
|
8228
|
+
* run: () => plugin.player.setAudioDevice(device.id),
|
|
8229
|
+
* });
|
|
8230
|
+
* ```
|
|
8231
|
+
*/
|
|
7500
8232
|
audioDevices(): Promise<AudioDevice[]>;
|
|
7501
8233
|
/**
|
|
7502
8234
|
* Play on this device while it is present, else on the system default
|
|
@@ -7506,6 +8238,18 @@ interface Player {
|
|
|
7506
8238
|
* Unknown ids are accepted (`state.audioOutput.active` stays `null`); a
|
|
7507
8239
|
* malformed id is `invalid`. Reverts to the default when this plugin
|
|
7508
8240
|
* stops; save the choice yourself to restore it on activation.
|
|
8241
|
+
* @capability playback.control
|
|
8242
|
+
* @throws invalid (rejects) A malformed id.
|
|
8243
|
+
* @throws capability (rejects) The manifest does not declare `playback.control`.
|
|
8244
|
+
* @example Restore the user's output device on start
|
|
8245
|
+
* The plugin plays on the device the user picked last time, or on the
|
|
8246
|
+
* system default when it is not connected. The choice lasts only for
|
|
8247
|
+
* this run and is withdrawn when the plugin stops, so the plugin saves
|
|
8248
|
+
* it itself and sets it again on activation.
|
|
8249
|
+
* ```ts
|
|
8250
|
+
* const store = plugin.storage.json("output", { defaults: { device: null as string | null } });
|
|
8251
|
+
* await plugin.player.setAudioDevice(store.value.device); // null: the system default
|
|
8252
|
+
* ```
|
|
7509
8253
|
*/
|
|
7510
8254
|
setAudioDevice(id: string | null): Promise<void>;
|
|
7511
8255
|
}
|
|
@@ -7558,6 +8302,15 @@ type HostAction = "play-pause" | "stop" | "previous" | "next" | "frame-back" | "
|
|
|
7558
8302
|
|
|
7559
8303
|
`plugin.host`: the player's own core actions.
|
|
7560
8304
|
|
|
8305
|
+
The player's core actions (play/pause, stop, step seeks, speed, volume,
|
|
8306
|
+
fullscreen…) are commands of the player itself, `host/<action>` in the
|
|
8307
|
+
binding table. Running them through `host.run` gives a plugin's own
|
|
8308
|
+
buttons exactly the player's behaviour: the same checks, the same
|
|
8309
|
+
accumulation of repeated presses and the same on-screen text, instead of
|
|
8310
|
+
reimplementing them with Player calls.
|
|
8311
|
+
|
|
8312
|
+
- @capability commands
|
|
8313
|
+
|
|
7561
8314
|
```ts
|
|
7562
8315
|
interface Host {
|
|
7563
8316
|
/**
|
|
@@ -7567,6 +8320,10 @@ interface Host {
|
|
|
7567
8320
|
* `invalid` for an unknown id. Needs `commands`.
|
|
7568
8321
|
*
|
|
7569
8322
|
* Prefer this over reimplementing playback buttons.
|
|
8323
|
+
* @capability commands
|
|
8324
|
+
* @throws unavailable (rejects) The action is disabled right now (nothing open, no window for `fullscreen`).
|
|
8325
|
+
* @throws invalid (rejects) An unknown action, or `seconds` out of range.
|
|
8326
|
+
* @throws capability (rejects) The manifest does not declare `commands`.
|
|
7570
8327
|
* @example Make a bar button play and pause like Space
|
|
7571
8328
|
* The plugin's own Play button behaves exactly like the player's Space
|
|
7572
8329
|
* key, including its on-screen text. `host.run` runs the core action
|
|
@@ -7597,6 +8354,10 @@ interface Host {
|
|
|
7597
8354
|
* once the seek is submitted, as before. `invalid` for `seconds` out of
|
|
7598
8355
|
* range.
|
|
7599
8356
|
* @player 0.5.1 (set `"minPlayerVersion": "0.5.1"`: older players ignore `seconds` and seek by the `setStepSeek` step)
|
|
8357
|
+
* @capability commands
|
|
8358
|
+
* @throws unavailable (rejects) The action is disabled right now (nothing open, no window for `fullscreen`).
|
|
8359
|
+
* @throws invalid (rejects) An unknown action, or `seconds` out of range.
|
|
8360
|
+
* @throws capability (rejects) The manifest does not declare `commands`.
|
|
7600
8361
|
* @example Add a 30-second skip that adds up with the arrow keys
|
|
7601
8362
|
* The plugin's "+30 s" button seeks forward 30 seconds and shows the same
|
|
7602
8363
|
* text as →. Passing `seconds` makes the core step seek use that step
|
|
@@ -7612,6 +8373,21 @@ interface Host {
|
|
|
7612
8373
|
}
|
|
7613
8374
|
```
|
|
7614
8375
|
|
|
8376
|
+
Example: Give a custom bar the player's own Stop and Mute
|
|
8377
|
+
|
|
8378
|
+
The plugin's bar buttons stop playback and mute exactly as the player's
|
|
8379
|
+
own keys do, with the same on-screen text. Each callback runs a core
|
|
8380
|
+
action; the player applies its checks and rejects with `unavailable`
|
|
8381
|
+
while the action is disabled, which the bar ignores.
|
|
8382
|
+
```ts
|
|
8383
|
+
const run = (action: HostAction) =>
|
|
8384
|
+
plugin.host.run(action).catch((error: PluginError) => {
|
|
8385
|
+
if (error.code !== "unavailable") throw error; // disabled right now
|
|
8386
|
+
});
|
|
8387
|
+
bar.on("stop", () => run("stop"));
|
|
8388
|
+
bar.on("mute", () => run("mute"));
|
|
8389
|
+
```
|
|
8390
|
+
|
|
7615
8391
|
### OsdPanel
|
|
7616
8392
|
|
|
7617
8393
|
This plugin's information block on the OSD: multi-line text over the
|
|
@@ -8876,13 +9652,65 @@ PNG/JPEG bytes; Windows writes Unicode text and images as a bitmap plus
|
|
|
8876
9652
|
PNG. Other systems: `unsupported`. Failures: `invalid`, `missing`,
|
|
8877
9653
|
`unavailable`, `failed` (the system refused).
|
|
8878
9654
|
|
|
9655
|
+
The player reads and writes the system clipboard on the plugin's behalf.
|
|
9656
|
+
Text is plain text; images are written from a PNG or JPEG file.
|
|
9657
|
+
|
|
9658
|
+
- @capability clipboard
|
|
9659
|
+
|
|
8879
9660
|
```ts
|
|
8880
9661
|
interface Clipboard {
|
|
8881
|
-
/**
|
|
9662
|
+
/**
|
|
9663
|
+
* Plain text on the clipboard; `null` when it holds none.
|
|
9664
|
+
* @capability clipboard
|
|
9665
|
+
* @throws unavailable (rejects) The player runs without a user interface.
|
|
9666
|
+
* @throws unsupported (rejects) A system other than macOS and Windows.
|
|
9667
|
+
* @throws failed (rejects) The system refused (Windows).
|
|
9668
|
+
* @throws capability (rejects) The manifest does not declare `clipboard`.
|
|
9669
|
+
* @example Open a URL the user copied
|
|
9670
|
+
* A "Paste and play" command opens the address on the clipboard. `readText`
|
|
9671
|
+
* resolves with the clipboard's text, or `null` when it holds none (an
|
|
9672
|
+
* image, nothing).
|
|
9673
|
+
* ```ts
|
|
9674
|
+
* const text = await plugin.clipboard.readText();
|
|
9675
|
+
* if (text?.startsWith("https://")) await plugin.player.open(text.trim());
|
|
9676
|
+
* ```
|
|
9677
|
+
*/
|
|
8882
9678
|
readText(): Promise<string | null>;
|
|
8883
|
-
/**
|
|
9679
|
+
/**
|
|
9680
|
+
* Replace the clipboard with text (at most 1 MiB of UTF-8).
|
|
9681
|
+
* @capability clipboard
|
|
9682
|
+
* @throws invalid (rejects) More than 1 MiB of UTF-8.
|
|
9683
|
+
* @throws unavailable (rejects) The player runs without a user interface.
|
|
9684
|
+
* @throws unsupported (rejects) A system other than macOS and Windows.
|
|
9685
|
+
* @throws failed (rejects) The system refused.
|
|
9686
|
+
* @throws capability (rejects) The manifest does not declare `clipboard`.
|
|
9687
|
+
* @example Copy the playing file's path
|
|
9688
|
+
* The path of the playing file is put on the clipboard, replacing what was
|
|
9689
|
+
* there. It resolves once the system has it.
|
|
9690
|
+
* ```ts
|
|
9691
|
+
* const { source } = plugin.player.state;
|
|
9692
|
+
* if (source) await plugin.clipboard.writeText(source);
|
|
9693
|
+
* ```
|
|
9694
|
+
*/
|
|
8884
9695
|
writeText(text: string): Promise<void>;
|
|
8885
|
-
/**
|
|
9696
|
+
/**
|
|
9697
|
+
* Replace the clipboard with a PNG or JPEG file (absolute path, at most 16 MB).
|
|
9698
|
+
* @capability clipboard
|
|
9699
|
+
* @throws missing (rejects) The file does not exist.
|
|
9700
|
+
* @throws invalid (rejects) Not an absolute path, not PNG or JPEG, over 16 MB, or (Windows) an image the system cannot decode.
|
|
9701
|
+
* @throws unavailable (rejects) The player runs without a user interface.
|
|
9702
|
+
* @throws unsupported (rejects) A system other than macOS and Windows.
|
|
9703
|
+
* @throws failed (rejects) The system refused.
|
|
9704
|
+
* @throws capability (rejects) The manifest does not declare `clipboard`.
|
|
9705
|
+
* @example Copy a snapshot to the clipboard
|
|
9706
|
+
* A frame the plugin saved as PNG is put on the clipboard, ready to paste
|
|
9707
|
+
* into another application. The player reads the file and writes the
|
|
9708
|
+
* image (macOS: its original bytes; Windows: a bitmap plus PNG).
|
|
9709
|
+
* ```ts
|
|
9710
|
+
* await plugin.clipboard.writeImage(pngPath); // absolute path, at most 16 MB
|
|
9711
|
+
* await plugin.osd.show("Snapshot copied");
|
|
9712
|
+
* ```
|
|
9713
|
+
*/
|
|
8886
9714
|
writeImage(path: string): Promise<void>;
|
|
8887
9715
|
}
|
|
8888
9716
|
```
|
|
@@ -8906,6 +9734,12 @@ try {
|
|
|
8906
9734
|
|
|
8907
9735
|
Keeping the computer awake (`playback.control`).
|
|
8908
9736
|
|
|
9737
|
+
A request, not a command: the player keeps the system awake while any
|
|
9738
|
+
plugin asks for it and a source plays, and withdraws a plugin's request
|
|
9739
|
+
when it stops.
|
|
9740
|
+
|
|
9741
|
+
- @capability playback.control
|
|
9742
|
+
|
|
8909
9743
|
```ts
|
|
8910
9744
|
interface Power {
|
|
8911
9745
|
/**
|
|
@@ -8917,11 +9751,31 @@ interface Power {
|
|
|
8917
9751
|
* take the system assertion are logged by the player, not reported.
|
|
8918
9752
|
* macOS uses `caffeinate`, Windows the thread execution state; elsewhere
|
|
8919
9753
|
* it has no effect.
|
|
9754
|
+
* @capability playback.control
|
|
9755
|
+
* @throws invalid (rejects) A mode other than `"off"` or `"playing"`.
|
|
9756
|
+
* @throws capability (rejects) The manifest does not declare `playback.control`.
|
|
9757
|
+
* @example Follow the plugin's own "Keep awake" setting
|
|
9758
|
+
* The plugin keeps the display awake during playback only while its
|
|
9759
|
+
* setting is on. Each call replaces this plugin's request; `"off"`
|
|
9760
|
+
* withdraws it, and other plugins' requests still count.
|
|
9761
|
+
* ```ts
|
|
9762
|
+
* const store = plugin.storage.json("power", { defaults: { keepAwake: true } });
|
|
9763
|
+
* await plugin.power.preventSleep(store.value.keepAwake ? "playing" : "off");
|
|
9764
|
+
* ```
|
|
8920
9765
|
*/
|
|
8921
9766
|
preventSleep(mode: "off" | "playing"): Promise<void>;
|
|
8922
9767
|
}
|
|
8923
9768
|
```
|
|
8924
9769
|
|
|
9770
|
+
Example: Keep the display on while playing
|
|
9771
|
+
|
|
9772
|
+
The screen does not dim or sleep while a video plays, and may again when
|
|
9773
|
+
it is paused or ends. The player takes the system's assertion only while
|
|
9774
|
+
a source is ready and not paused.
|
|
9775
|
+
```ts
|
|
9776
|
+
await plugin.power.preventSleep("playing"); // "off" withdraws the request
|
|
9777
|
+
```
|
|
9778
|
+
|
|
8925
9779
|
### ToolResult
|
|
8926
9780
|
|
|
8927
9781
|
The output of Tools.run.
|
|
@@ -8940,11 +9794,20 @@ interface ToolResult {
|
|
|
8940
9794
|
`plugin.tools`: ffmpeg and ffprobe, run in this plugin's process. No
|
|
8941
9795
|
capability.
|
|
8942
9796
|
|
|
9797
|
+
ffmpeg and ffprobe run as child processes of the plugin. When
|
|
9798
|
+
`VESTRA_FFMPEG` / `VESTRA_FFPROBE` is set, only that path is used (an
|
|
9799
|
+
invalid one means the tool is not found); otherwise the tool is searched
|
|
9800
|
+
on `PATH`. A found path is kept for the plugin's run, a miss is looked up
|
|
9801
|
+
again next time. Disposing the
|
|
9802
|
+
scope ends a running process.
|
|
9803
|
+
|
|
9804
|
+
- @capability None.
|
|
9805
|
+
|
|
8943
9806
|
```ts
|
|
8944
9807
|
interface Tools {
|
|
8945
9808
|
/**
|
|
8946
|
-
* Run ffmpeg or ffprobe (
|
|
8947
|
-
*
|
|
9809
|
+
* Run ffmpeg or ffprobe (`VESTRA_FFMPEG` / `VESTRA_FFPROBE` when set, else
|
|
9810
|
+
* found on `PATH`) without a shell. Rejects `unavailable` (not found), `timeout`,
|
|
8948
9811
|
* `failed` (non-zero exit; `message` is the last stderr line), `invalid`
|
|
8949
9812
|
* (output over `maxOutput`, default 16 MiB), `aborted` (`signal`). Timeout,
|
|
8950
9813
|
* `signal` and disposing the scope end the process.
|
|
@@ -8952,13 +9815,50 @@ interface Tools {
|
|
|
8952
9815
|
* Arguments are passed as-is (no quoting needed). On Windows `PATHEXT` is
|
|
8953
9816
|
* honoured. Ending sends SIGTERM, then SIGKILL after 300 ms (immediate on
|
|
8954
9817
|
* Windows).
|
|
9818
|
+
* @capability None.
|
|
9819
|
+
* @throws unavailable (rejects) The tool was not found.
|
|
9820
|
+
* @throws timeout (rejects) It ran longer than `timeoutMs`; the process is ended.
|
|
9821
|
+
* @throws failed (rejects) A non-zero exit; `message` is the last stderr line.
|
|
9822
|
+
* @throws invalid (rejects) Bad arguments or options, or output over `maxOutput`.
|
|
9823
|
+
* @throws aborted (rejects) `signal` aborted, or the scope was disposed.
|
|
9824
|
+
* @example Cut a clip without re-encoding
|
|
9825
|
+
* The plugin writes the first ten seconds of the file to a new file. `run`
|
|
9826
|
+
* starts ffmpeg without a shell (arguments pass as they are, no quoting)
|
|
9827
|
+
* and resolves with its output once it exits with 0; a failure rejects
|
|
9828
|
+
* with the last line ffmpeg printed.
|
|
9829
|
+
* ```ts
|
|
9830
|
+
* await plugin.tools.run(
|
|
9831
|
+
* "ffmpeg",
|
|
9832
|
+
* ["-y", "-i", input, "-t", "10", "-c", "copy", output],
|
|
9833
|
+
* { timeoutMs: 60_000 }, // give up after a minute
|
|
9834
|
+
* );
|
|
9835
|
+
* ```
|
|
8955
9836
|
*/
|
|
8956
9837
|
run(tool: "ffmpeg" | "ffprobe", args: string[], options: {
|
|
8957
9838
|
timeoutMs: number;
|
|
8958
9839
|
maxOutput?: number;
|
|
8959
9840
|
signal?: AbortSignal;
|
|
8960
9841
|
}): Promise<ToolResult>;
|
|
8961
|
-
/**
|
|
9842
|
+
/**
|
|
9843
|
+
* Whether the tool can be found.
|
|
9844
|
+
* @capability None.
|
|
9845
|
+
* @throws Never; it resolves `false` when the tool cannot be found.
|
|
9846
|
+
* @example Grey out a command when ffmpeg is missing
|
|
9847
|
+
* The Export item is disabled with a reason when ffmpeg is not installed.
|
|
9848
|
+
* `available` looks the tool up (a found path is kept, a miss is looked
|
|
9849
|
+
* up again on the next call, so installing it later is noticed).
|
|
9850
|
+
* ```ts
|
|
9851
|
+
* const hasFfmpeg = await plugin.tools.available("ffmpeg");
|
|
9852
|
+
* plugin.commands.add({
|
|
9853
|
+
* id: "export",
|
|
9854
|
+
* path: ["@video", "Export…"],
|
|
9855
|
+
* enabled: hasFfmpeg,
|
|
9856
|
+
* // Shown with the greyed-out item.
|
|
9857
|
+
* reason: hasFfmpeg ? null : "Install ffmpeg to export",
|
|
9858
|
+
* run() {},
|
|
9859
|
+
* });
|
|
9860
|
+
* ```
|
|
9861
|
+
*/
|
|
8962
9862
|
available(tool: "ffmpeg" | "ffprobe"): Promise<boolean>;
|
|
8963
9863
|
}
|
|
8964
9864
|
```
|
|
@@ -8986,21 +9886,75 @@ if (await plugin.tools.available("ffprobe")) {
|
|
|
8986
9886
|
|
|
8987
9887
|
A JSON file `dataDirectory/<name>.json`, loaded once and saved debounced.
|
|
8988
9888
|
|
|
9889
|
+
One JSON file, `<dataDirectory>/<name>.json`, made by
|
|
9890
|
+
Storage.json. `value` is read once when the store is made;
|
|
9891
|
+
`save` replaces it in memory at once and writes the file `debounceMs`
|
|
9892
|
+
later (300 by default) through a temporary file and a rename, so a
|
|
9893
|
+
crash never leaves half a file. Writes to one file run one at a time,
|
|
9894
|
+
newest last. Disposing the store, its scope or stopping the plugin
|
|
9895
|
+
writes a pending save first.
|
|
9896
|
+
|
|
9897
|
+
- @capability None.
|
|
9898
|
+
|
|
8989
9899
|
```ts
|
|
8990
9900
|
interface JsonStore<T> extends Disposable {
|
|
8991
9901
|
/** The loaded value, or `defaults` when the file was missing or broken. */
|
|
8992
9902
|
readonly value: T;
|
|
8993
|
-
/**
|
|
9903
|
+
/**
|
|
9904
|
+
* Replace the value now and write it after `debounceMs`. Only `save` writes: mutating `value` in place is not saved.
|
|
9905
|
+
* @capability None.
|
|
9906
|
+
* @example Save a slider value without writing on every move
|
|
9907
|
+
* Dragging the plugin's volume slider updates the stored value at once but
|
|
9908
|
+
* writes the file only once the user stops for 300 ms. Each `save`
|
|
9909
|
+
* replaces the value and restarts the debounce timer.
|
|
9910
|
+
* ```ts
|
|
9911
|
+
* const store = plugin.storage.json("prefs", { defaults: { volume: 100 } });
|
|
9912
|
+
* window.on("volumeChanged", (volume: number) => store.save({ ...store.value, volume }));
|
|
9913
|
+
* ```
|
|
9914
|
+
*/
|
|
8994
9915
|
save(next: T): void;
|
|
8995
|
-
/**
|
|
9916
|
+
/**
|
|
9917
|
+
* Write a pending save now.
|
|
9918
|
+
* @capability None.
|
|
9919
|
+
* @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.
|
|
9920
|
+
* @example Write the file now before copying it
|
|
9921
|
+
* Before exporting its settings, the plugin makes sure the file on disk has
|
|
9922
|
+
* the latest value. `flush` writes a pending save at once (or waits for
|
|
9923
|
+
* the write in progress) and resolves when the file is written.
|
|
9924
|
+
* ```ts
|
|
9925
|
+
* const store = plugin.storage.json("settings", { defaults: { step: 5 } });
|
|
9926
|
+
* await store.flush(); // the file now has the newest value
|
|
9927
|
+
* await copyFile(join(plugin.dataDirectory, "settings.json"), exportPath);
|
|
9928
|
+
* ```
|
|
9929
|
+
*/
|
|
8996
9930
|
flush(): Promise<void>;
|
|
8997
9931
|
}
|
|
8998
9932
|
```
|
|
8999
9933
|
|
|
9934
|
+
Example: Count how often the plugin was started
|
|
9935
|
+
|
|
9936
|
+
The counter grows by one at every start and is kept in `stats.json`. The
|
|
9937
|
+
store reads the file when it is made (the defaults when there is none),
|
|
9938
|
+
and `save` writes the new count a moment later.
|
|
9939
|
+
```ts
|
|
9940
|
+
const stats = plugin.storage.json("stats", { defaults: { starts: 0 } });
|
|
9941
|
+
stats.save({ starts: stats.value.starts + 1 }); // written 300 ms later
|
|
9942
|
+
```
|
|
9943
|
+
|
|
9000
9944
|
### Storage
|
|
9001
9945
|
|
|
9002
9946
|
`plugin.storage`: JSON settings files. No capability.
|
|
9003
9947
|
|
|
9948
|
+
Two kinds of storage, both private to the plugin and needing no
|
|
9949
|
+
capability. Storage.json keeps settings in JSON files in the
|
|
9950
|
+
plugin's data directory: they last across player restarts and upgrades,
|
|
9951
|
+
and are written by the plugin's own process. Storage.session
|
|
9952
|
+
keeps values in the player's memory for one player run: they survive a
|
|
9953
|
+
restart of the plugin's process but not of the player. Secrets belong in
|
|
9954
|
+
Secrets, never in either.
|
|
9955
|
+
|
|
9956
|
+
- @capability None.
|
|
9957
|
+
|
|
9004
9958
|
```ts
|
|
9005
9959
|
interface Storage {
|
|
9006
9960
|
/**
|
|
@@ -9011,6 +9965,8 @@ interface Storage {
|
|
|
9011
9965
|
*
|
|
9012
9966
|
* The file is read synchronously on creation; `debounceMs` defaults to
|
|
9013
9967
|
* 300. Files are created with mode 0600. `invalid` for a bad name.
|
|
9968
|
+
* @capability None.
|
|
9969
|
+
* @throws invalid (throws) A bad `name`, or a `debounceMs` that is not a non-negative number.
|
|
9014
9970
|
* @example Keep settings and a recent list in a JSON file
|
|
9015
9971
|
* The plugin remembers whether to resume and the last ten files across
|
|
9016
9972
|
* restarts. `json` reads `settings.json` from the data directory at once
|
|
@@ -9040,6 +9996,19 @@ interface Storage {
|
|
|
9040
9996
|
}
|
|
9041
9997
|
```
|
|
9042
9998
|
|
|
9999
|
+
Example: Keep a setting across restarts and a queue for this run
|
|
10000
|
+
|
|
10001
|
+
The plugin's step size survives a restart of the player; its work queue
|
|
10002
|
+
survives a reload of the plugin but starts empty with the next player
|
|
10003
|
+
run. The JSON store reads its file at once and writes it shortly after
|
|
10004
|
+
each `save`; session values live in the player process.
|
|
10005
|
+
```ts
|
|
10006
|
+
const settings = plugin.storage.json("settings", { defaults: { step: 5 } });
|
|
10007
|
+
const queue = (await plugin.storage.session.get<string[]>("queue")) ?? [];
|
|
10008
|
+
void settings.value.step; // read synchronously, from the file or the defaults
|
|
10009
|
+
void queue;
|
|
10010
|
+
```
|
|
10011
|
+
|
|
9043
10012
|
### SessionStorage
|
|
9044
10013
|
|
|
9045
10014
|
`plugin.storage.session`: JSON values that live as long as the **player**,
|
|
@@ -9085,17 +10054,74 @@ quitting).
|
|
|
9085
10054
|
resolves, so a crash right after loses nothing; there is no debounce.
|
|
9086
10055
|
Calls answer in order.
|
|
9087
10056
|
|
|
10057
|
+
The player keeps the values in its own memory, per plugin, until it
|
|
10058
|
+
quits; the plugin's process may restart in between. Each call is one
|
|
10059
|
+
request to the player, answered in order, with no debounce.
|
|
10060
|
+
|
|
10061
|
+
- @capability None.
|
|
10062
|
+
|
|
9088
10063
|
```ts
|
|
9089
10064
|
interface SessionStorage {
|
|
9090
|
-
/**
|
|
10065
|
+
/**
|
|
10066
|
+
* The value stored under `key`, or `undefined` when there is none.
|
|
10067
|
+
* @capability None.
|
|
10068
|
+
* @throws invalid (rejects) A bad key (empty, over 1024 bytes, with NUL or a lone surrogate).
|
|
10069
|
+
* @throws disabled (rejects) The plugin is stopping.
|
|
10070
|
+
* @throws unsupported (rejects) A player before 0.5.5.
|
|
10071
|
+
* @example Pick up where the plugin stopped after a reload
|
|
10072
|
+
* After a reload the plugin resumes the item it was working on. `get`
|
|
10073
|
+
* asks the player and resolves with the value parsed from JSON, or
|
|
10074
|
+
* `undefined` when nothing is stored; the value is not checked, so its
|
|
10075
|
+
* shape is checked here.
|
|
10076
|
+
* ```ts
|
|
10077
|
+
* const current = await plugin.storage.session.get<{ url: string }>("current");
|
|
10078
|
+
* if (typeof current?.url === "string") await plugin.osd.show("Resuming {}", [current.url]);
|
|
10079
|
+
* ```
|
|
10080
|
+
*/
|
|
9091
10081
|
get<T = unknown>(key: string): Promise<T | undefined>;
|
|
9092
10082
|
/**
|
|
9093
10083
|
* 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.
|
|
10084
|
+
* @capability None.
|
|
10085
|
+
* @throws invalid (rejects) A bad key, or a value JSON cannot hold (or a string with a lone surrogate).
|
|
10086
|
+
* @throws disabled (rejects) The plugin is stopping.
|
|
10087
|
+
* @throws unsupported (rejects) A player before 0.5.5.
|
|
10088
|
+
* @throws limit (rejects) The plugin's values would pass 1 MiB, or the request the 1 MiB message limit; nothing changes.
|
|
10089
|
+
* @example Remember the item being worked on
|
|
10090
|
+
* The plugin records the URL it is processing, so a reload can continue
|
|
10091
|
+
* with it. The value is stored as `JSON.stringify` writes it, and is kept
|
|
10092
|
+
* once the promise resolves.
|
|
10093
|
+
* ```ts
|
|
10094
|
+
* await plugin.storage.session.set("current", { url: "https://example.com/a.mkv" });
|
|
10095
|
+
* ```
|
|
9094
10096
|
*/
|
|
9095
10097
|
set(key: string, value: unknown): Promise<void>;
|
|
9096
|
-
/**
|
|
10098
|
+
/**
|
|
10099
|
+
* Remove a key; removing a missing one is fine.
|
|
10100
|
+
* @capability None.
|
|
10101
|
+
* @throws invalid (rejects) A bad key.
|
|
10102
|
+
* @throws disabled (rejects) The plugin is stopping.
|
|
10103
|
+
* @throws unsupported (rejects) A player before 0.5.5.
|
|
10104
|
+
* @example Forget the item once it is done
|
|
10105
|
+
* When the work finishes, the plugin removes the entry so that a later
|
|
10106
|
+
* reload does not resume it. Removing a key that is not there is fine.
|
|
10107
|
+
* ```ts
|
|
10108
|
+
* await plugin.storage.session.delete("current");
|
|
10109
|
+
* ```
|
|
10110
|
+
*/
|
|
9097
10111
|
delete(key: string): Promise<void>;
|
|
9098
|
-
/**
|
|
10112
|
+
/**
|
|
10113
|
+
* The keys stored, in code point order.
|
|
10114
|
+
* @capability None.
|
|
10115
|
+
* @throws disabled (rejects) The plugin is stopping.
|
|
10116
|
+
* @throws unsupported (rejects) A player before 0.5.5.
|
|
10117
|
+
* @example Clear everything the plugin stored for this run
|
|
10118
|
+
* The plugin starts its session state over. `keys` lists the plugin's own
|
|
10119
|
+
* keys in code point order; each `delete` is a request of its own.
|
|
10120
|
+
* ```ts
|
|
10121
|
+
* const session = plugin.storage.session;
|
|
10122
|
+
* for (const key of await session.keys()) await session.delete(key);
|
|
10123
|
+
* ```
|
|
10124
|
+
*/
|
|
9099
10125
|
keys(): Promise<string[]>;
|
|
9100
10126
|
}
|
|
9101
10127
|
```
|
|
@@ -9119,15 +10145,61 @@ await plugin.storage.session.set("queue", pending);
|
|
|
9119
10145
|
`plugin.system`: files and links, executed by the player. No capability.
|
|
9120
10146
|
Other systems than macOS and Windows: `unsupported`.
|
|
9121
10147
|
|
|
10148
|
+
The player opens files, folders and links with the system's own
|
|
10149
|
+
applications for the plugin, so the plugin never runs `open`,
|
|
10150
|
+
`explorer` or a shell itself.
|
|
10151
|
+
|
|
10152
|
+
- @capability None.
|
|
10153
|
+
|
|
9122
10154
|
```ts
|
|
9123
10155
|
interface System {
|
|
9124
10156
|
/**
|
|
9125
10157
|
* 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.
|
|
10158
|
+
* @capability None.
|
|
10159
|
+
* @throws missing (rejects) None of the paths exists.
|
|
10160
|
+
* @throws invalid (rejects) No paths, or more than 256.
|
|
10161
|
+
* @throws failed (rejects) The system refused.
|
|
10162
|
+
* @throws timeout (rejects) macOS did not answer within 10 s.
|
|
10163
|
+
* @throws unsupported (rejects) A system other than macOS and Windows.
|
|
10164
|
+
* @example Show the exported files in the file manager
|
|
10165
|
+
* After an export, the file manager opens with the new files selected.
|
|
10166
|
+
* Paths that no longer exist are skipped; the call rejects with `missing`
|
|
10167
|
+
* only when none of them exists.
|
|
10168
|
+
* ```ts
|
|
10169
|
+
* await plugin.system.reveal(exported); // macOS: one Finder window for all
|
|
10170
|
+
* ```
|
|
9126
10171
|
*/
|
|
9127
10172
|
reveal(paths: string | string[]): Promise<void>;
|
|
9128
|
-
/**
|
|
10173
|
+
/**
|
|
10174
|
+
* Open the file or folder with its default application. `missing` if absent.
|
|
10175
|
+
* @capability None.
|
|
10176
|
+
* @throws missing (rejects) The path does not exist.
|
|
10177
|
+
* @throws invalid (rejects) An empty path.
|
|
10178
|
+
* @throws failed (rejects) The system refused.
|
|
10179
|
+
* @throws timeout (rejects) macOS did not answer within 10 s.
|
|
10180
|
+
* @throws unsupported (rejects) A system other than macOS and Windows.
|
|
10181
|
+
* @example Open the plugin's data folder
|
|
10182
|
+
* A "Show data folder" command opens the plugin's data directory in the
|
|
10183
|
+
* file manager, which is the default application for a folder.
|
|
10184
|
+
* ```ts
|
|
10185
|
+
* await plugin.system.openPath(plugin.dataDirectory);
|
|
10186
|
+
* ```
|
|
10187
|
+
*/
|
|
9129
10188
|
openPath(path: string): Promise<void>;
|
|
9130
|
-
/**
|
|
10189
|
+
/**
|
|
10190
|
+
* Open an http(s) address in the default browser. `invalid` for another protocol or a malformed address; `timeout` after 10 s, `failed` otherwise.
|
|
10191
|
+
* @capability None.
|
|
10192
|
+
* @throws invalid (rejects) Not an `http:` or `https:` address.
|
|
10193
|
+
* @throws timeout (rejects) macOS did not hand it to the browser within 10 s.
|
|
10194
|
+
* @throws failed (rejects) The system refused.
|
|
10195
|
+
* @throws unsupported (rejects) A system other than macOS and Windows.
|
|
10196
|
+
* @example Open the plugin's help page
|
|
10197
|
+
* A Help command opens the plugin's documentation in the default browser.
|
|
10198
|
+
* Only `http:` and `https:` addresses are opened.
|
|
10199
|
+
* ```ts
|
|
10200
|
+
* await plugin.system.openUrl("https://example.com/plugin/help");
|
|
10201
|
+
* ```
|
|
10202
|
+
*/
|
|
9131
10203
|
openUrl(url: string): Promise<void>;
|
|
9132
10204
|
}
|
|
9133
10205
|
```
|
|
@@ -9153,20 +10225,74 @@ try {
|
|
|
9153
10225
|
Per-plugin secrets (API keys, passwords) in the system's secret store:
|
|
9154
10226
|
the login keychain on macOS, the Credential Manager on Windows. Needs
|
|
9155
10227
|
`secrets`. `unsupported` on systems without a store, `unavailable` when
|
|
9156
|
-
the store
|
|
10228
|
+
the store cannot be reached (no keychain, a read-only store, no logged-in
|
|
10229
|
+
session on Windows): keep a fallback (or ask again).
|
|
9157
10230
|
|
|
9158
10231
|
The user denying access and other system errors reject `failed`. Never
|
|
9159
10232
|
write secrets to settings files.
|
|
9160
10233
|
|
|
10234
|
+
Values are stored by the player in the system's secret store under this
|
|
10235
|
+
plugin's id: other plugins cannot read them. Names are 1–64 of
|
|
10236
|
+
`a-z 0-9 . _ -`, values 1–1024 bytes of text, at most 256 names per
|
|
10237
|
+
plugin (`SECRET_LIMITS`).
|
|
10238
|
+
|
|
10239
|
+
- @capability secrets
|
|
10240
|
+
|
|
9161
10241
|
```ts
|
|
9162
10242
|
interface Secrets {
|
|
9163
|
-
/**
|
|
10243
|
+
/**
|
|
10244
|
+
* The stored value, `null` when there is none.
|
|
10245
|
+
* @capability secrets
|
|
10246
|
+
* @throws unsupported (rejects) The system has no secret store.
|
|
10247
|
+
* @throws unavailable (rejects) The store cannot be reached (no keychain, a read-only store, no logged-in session on Windows).
|
|
10248
|
+
* @throws failed (rejects) The user denied access, or another system error.
|
|
10249
|
+
* @throws capability (rejects) The manifest does not declare `secrets`.
|
|
10250
|
+
* @throws invalid (rejects) A bad name.
|
|
10251
|
+
* @example Read a stored token
|
|
10252
|
+
* The plugin uses the token the user saved earlier, or asks for one when
|
|
10253
|
+
* there is none. `get` resolves with the stored text or `null`.
|
|
10254
|
+
* ```ts
|
|
10255
|
+
* const token = await plugin.secrets.get("api-token");
|
|
10256
|
+
* if (token === null) await plugin.osd.show("Sign in first");
|
|
10257
|
+
* ```
|
|
10258
|
+
*/
|
|
9164
10259
|
get(name: string): Promise<string | null>;
|
|
9165
10260
|
/**
|
|
9166
10261
|
* 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.
|
|
10262
|
+
* @capability secrets
|
|
10263
|
+
* @throws invalid (rejects) A bad name, or a value that is empty, has NUL or is over 1024 bytes.
|
|
10264
|
+
* @throws limit (rejects) A new name beyond 256.
|
|
10265
|
+
* @throws unsupported (rejects) The system has no secret store.
|
|
10266
|
+
* @throws unavailable (rejects) The store cannot be reached (no keychain, a read-only store, no logged-in session on Windows).
|
|
10267
|
+
* @throws failed (rejects) The user denied access, or another system error.
|
|
10268
|
+
* @throws capability (rejects) The manifest does not declare `secrets`.
|
|
10269
|
+
* @example Save the token the user entered
|
|
10270
|
+
* The token the user typed is kept in the system's secret store for later
|
|
10271
|
+
* runs. `set` stores or replaces it; the store can refuse, in which case
|
|
10272
|
+
* the plugin keeps the token for this run only.
|
|
10273
|
+
* ```ts
|
|
10274
|
+
* await plugin.secrets.set("api-token", token).catch((error: PluginError) => {
|
|
10275
|
+
* // No store, or access denied: the token is not kept for next time.
|
|
10276
|
+
* void plugin.osd.show("The token could not be saved ({})", [error.code]);
|
|
10277
|
+
* });
|
|
10278
|
+
* ```
|
|
9167
10279
|
*/
|
|
9168
10280
|
set(name: string, value: string): Promise<void>;
|
|
9169
|
-
/**
|
|
10281
|
+
/**
|
|
10282
|
+
* Remove; removing a missing secret is fine.
|
|
10283
|
+
* @capability secrets
|
|
10284
|
+
* @throws invalid (rejects) A bad name.
|
|
10285
|
+
* @throws unsupported (rejects) The system has no secret store.
|
|
10286
|
+
* @throws unavailable (rejects) The store cannot be reached (no keychain, a read-only store, no logged-in session on Windows).
|
|
10287
|
+
* @throws failed (rejects) The user denied access, or another system error.
|
|
10288
|
+
* @throws capability (rejects) The manifest does not declare `secrets`.
|
|
10289
|
+
* @example Sign out by removing the token
|
|
10290
|
+
* Signing out removes the stored token, so the next start asks again.
|
|
10291
|
+
* Removing a name that is not stored is fine.
|
|
10292
|
+
* ```ts
|
|
10293
|
+
* await plugin.secrets.delete("api-token");
|
|
10294
|
+
* ```
|
|
10295
|
+
*/
|
|
9170
10296
|
delete(name: string): Promise<void>;
|
|
9171
10297
|
}
|
|
9172
10298
|
```
|
|
@@ -11604,7 +12730,8 @@ The plugin keeps a token the user pasted, or tells them it is empty or
|
|
|
11604
12730
|
too long. Values are 1–1024 bytes counted in UTF-8, so the check uses
|
|
11605
12731
|
`Buffer.byteLength`; `secrets.set` would refuse other values with
|
|
11606
12732
|
`invalid`. The store itself can still refuse (`unsupported` without a
|
|
11607
|
-
system store, `unavailable`
|
|
12733
|
+
system store, `unavailable` when it cannot be reached, `failed` when the
|
|
12734
|
+
user denies access): only
|
|
11608
12735
|
those codes get the "could not be saved" message, anything else (a
|
|
11609
12736
|
missing `secrets` capability) is a bug and is thrown on.
|
|
11610
12737
|
```ts
|
|
@@ -11688,7 +12815,7 @@ list.on("scrolled", (row: number) => {
|
|
|
11688
12815
|
|
|
11689
12816
|
### IMAGE_LIMITS
|
|
11690
12817
|
|
|
11691
|
-
The player's runtime image limits (`host/src/ui_image.rs`): a file's
|
|
12818
|
+
The player's runtime image limits (`src/host/src/ui/ui_image.rs`): a file's
|
|
11692
12819
|
bytes and sides, the decoded pixels each plugin caches, and (SDK 1.5.19,
|
|
11693
12820
|
player 0.5.1) the decoded pixels the nested images of one view may show.
|
|
11694
12821
|
|