@vestra-player/sdk 1.5.30 → 1.5.33
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +26 -0
- package/dist/capabilities.d.ts +6 -0
- package/dist/capabilities.js +4 -0
- package/dist/commands.d.ts +18 -0
- package/dist/commands.js +14 -0
- package/dist/i18n.d.ts +9 -0
- package/dist/i18n.js +9 -0
- package/dist/manifest.js +1 -1
- package/dist/palettes.d.ts +2 -0
- package/dist/palettes.js +2 -0
- package/dist/plugin/bindings.d.ts +2 -0
- package/dist/plugin/bindings.js +2 -0
- package/dist/plugin/osd.d.ts +2 -0
- package/dist/plugin/osd.js +2 -0
- package/dist/plugin/services.d.ts +6 -1
- package/dist/plugin/services.js +6 -1
- package/dist/plugin/testing.d.ts +518 -9
- package/dist/plugin/testing.js +385 -17
- package/dist/plugin/types.d.ts +2921 -101
- package/dist/plugin/values.d.ts +7 -2
- package/dist/plugin/values.js +7 -2
- package/dist/processing.d.ts +6 -0
- package/dist/processing.js +1 -1
- package/dist/source-resolvers.d.ts +3 -0
- package/dist/subtitles.d.ts +3 -0
- package/dist/wire.d.ts +13 -0
- package/dist/wire.js +10 -0
- package/llms-full.txt +2526 -83
- package/llms.txt +23 -23
- package/package.json +1 -1
package/dist/plugin/types.d.ts
CHANGED
|
@@ -43,11 +43,39 @@ export type { Json, AudioNode, VideoNode, SubtitleStyle, ResolvedMedia };
|
|
|
43
43
|
* if (state.phase === "ready") void subscription.dispose();
|
|
44
44
|
* });
|
|
45
45
|
* ```
|
|
46
|
+
*
|
|
47
|
+
* @remarks
|
|
48
|
+
* Every handle a plugin creates is a `Disposable` owned by the scope that
|
|
49
|
+
* created it; disposing the scope (or stopping the plugin) disposes them in
|
|
50
|
+
* reverse creation order, so a plugin rarely calls `dispose` itself.
|
|
51
|
+
*
|
|
52
|
+
* @capability None of its own: that of the handle.
|
|
53
|
+
* @since 1.0.0
|
|
54
|
+
* @category Plugin
|
|
46
55
|
*/
|
|
47
56
|
export interface Disposable {
|
|
48
57
|
/**
|
|
49
58
|
* Release the resource. Resolves once the player confirmed the release
|
|
50
59
|
* (or immediately for purely local resources). Never rejects.
|
|
60
|
+
*
|
|
61
|
+
* @capability None of its own: that of the handle.
|
|
62
|
+
* @throws Never.
|
|
63
|
+
*
|
|
64
|
+
* @example Remove a command when its feature is turned off
|
|
65
|
+
* A "Turn feature off" item removes the feature's own menu item at once,
|
|
66
|
+
* before the plugin stops. `dispose` takes the command out of the menu and
|
|
67
|
+
* the binding table and resolves once the player confirmed it; the id can
|
|
68
|
+
* be added again later.
|
|
69
|
+
* ```ts
|
|
70
|
+
* const extra = plugin.commands.add({ id: "extra", path: ["Tools", "Extra"], run() {} });
|
|
71
|
+
* plugin.commands.add({
|
|
72
|
+
* id: "extra-off",
|
|
73
|
+
* path: ["Tools", "Turn extra off"],
|
|
74
|
+
* run: () => extra.dispose(), // never rejects
|
|
75
|
+
* });
|
|
76
|
+
* ```
|
|
77
|
+
*
|
|
78
|
+
* @since 1.0.0
|
|
51
79
|
*/
|
|
52
80
|
dispose(): Promise<void>;
|
|
53
81
|
}
|
|
@@ -59,7 +87,6 @@ export interface Disposable {
|
|
|
59
87
|
* | code | meaning |
|
|
60
88
|
* | --- | --- |
|
|
61
89
|
* | `stale` | A condition no longer held (the source or the subtitle selection changed); nothing was done. See {@link Conditional}. |
|
|
62
|
-
* | `staleDigest` | {@link PackageCandidate.install}: the package's files changed after it was validated; validate it again. |
|
|
63
90
|
* | `capability` | The manifest does not declare the capability the call needs. |
|
|
64
91
|
* | `invalid` | A bad argument (out of range, malformed, unknown id). |
|
|
65
92
|
* | `missing` | The target does not exist (an empty panel position, a missing file, an unknown command). |
|
|
@@ -79,7 +106,7 @@ export interface Disposable {
|
|
|
79
106
|
* | `backend-uncertain` | The player's backend blocked a filter stage; call `retry()`. |
|
|
80
107
|
* | `limit` | A player-wide limit was reached (for example the number of commands), a property value is over the limits of {@link PropValue}, or a request is longer than the player's 1 MiB message. |
|
|
81
108
|
* | `reserved` | {@link Packages.validate}, {@link PackageCandidate.install}: the id is `vestra` or starts with `vestra.` (any case), which only the player's built-in plugins use (player 0.4.1). |
|
|
82
|
-
* | `versionMismatch` | The package needs a newer player (`minPlayerVersion`)
|
|
109
|
+
* | `versionMismatch` | The package needs a newer player (`minPlayerVersion`). |
|
|
83
110
|
* | `ui-prepare` | The player could not prepare a mounted panel or a settings page (its Slint file does not compile, a bound property is not declared, or another plugin's panel holds the slot); `message` has the diagnostics. |
|
|
84
111
|
* | `ui` | The player could not apply a property to a tool window or a settings page, or could not build the Options window (a property the Slint file does not declare, a value of the wrong type); `message` has Slint's reason. Also reported to {@link Scope.onError} without a request: a callback whose arguments the player could not convert, a property it could not show again when it rebuilt a view (an image file removed since). |
|
|
85
112
|
* | `host` | Any other failure inside the player; `message` is the player's reason. |
|
|
@@ -117,6 +144,9 @@ export interface Disposable {
|
|
|
117
144
|
* throw error;
|
|
118
145
|
* }
|
|
119
146
|
* ```
|
|
147
|
+
*
|
|
148
|
+
* @since 1.0.0
|
|
149
|
+
* @category Plugin
|
|
120
150
|
*/
|
|
121
151
|
export type PluginError = Error & {
|
|
122
152
|
/** The stable failure code (see the table above). */
|
|
@@ -159,6 +189,17 @@ export type PluginError = Error & {
|
|
|
159
189
|
* );
|
|
160
190
|
* });
|
|
161
191
|
* ```
|
|
192
|
+
*
|
|
193
|
+
* @remarks
|
|
194
|
+
* Every module of the API (`commands`, `windows`, `player`…) is reachable
|
|
195
|
+
* from a scope, and every handle made through it belongs to it. A child
|
|
196
|
+
* scope from {@link Scope.scope} is the unit for features that come and
|
|
197
|
+
* go: dispose the child and everything it created is released, while the
|
|
198
|
+
* rest of the plugin keeps running.
|
|
199
|
+
*
|
|
200
|
+
* @capability That of each module (see the modules).
|
|
201
|
+
* @since 1.0.0
|
|
202
|
+
* @category Plugin
|
|
162
203
|
*/
|
|
163
204
|
export interface Scope extends Disposable {
|
|
164
205
|
/** Menu commands and their default bindings (`commands`). */
|
|
@@ -189,12 +230,23 @@ export interface Scope extends Disposable {
|
|
|
189
230
|
/**
|
|
190
231
|
* The display layer (needs `processing.video`). The first plugin to call
|
|
191
232
|
* it owns the layer; another plugin's view fails with `conflict` until the
|
|
192
|
-
* owner disposes or stops.
|
|
233
|
+
* owner disposes or stops.
|
|
234
|
+
*
|
|
235
|
+
* @throws `capability` (throws) The manifest does not declare
|
|
236
|
+
* `processing.video`.
|
|
237
|
+
* @throws `Error` without a code (throws) A second call in the same plugin
|
|
238
|
+
* while the first view is not disposed.
|
|
193
239
|
*/
|
|
194
240
|
view(): VideoView;
|
|
195
241
|
/**
|
|
196
242
|
* The GPU renderer's options (needs `processing.video`), owned like the
|
|
197
|
-
* display layer. `failed` with
|
|
243
|
+
* display layer. Under software rendering the handle is `failed` with
|
|
244
|
+
* `unsupported` after the player's first answer, and `onError` hears it.
|
|
245
|
+
*
|
|
246
|
+
* @throws `capability` (throws) The manifest does not declare
|
|
247
|
+
* `processing.video`.
|
|
248
|
+
* @throws `Error` without a code (throws) A second call in the same plugin
|
|
249
|
+
* while the first handle is not disposed.
|
|
198
250
|
*/
|
|
199
251
|
render(): RenderOptions;
|
|
200
252
|
};
|
|
@@ -258,6 +310,36 @@ export interface Scope extends Disposable {
|
|
|
258
310
|
/**
|
|
259
311
|
* A child scope; disposing it releases only what was created through it.
|
|
260
312
|
* The child is itself owned by this scope.
|
|
313
|
+
*
|
|
314
|
+
* @capability None.
|
|
315
|
+
*
|
|
316
|
+
* @example Tie a temporary window and its timer to one feature
|
|
317
|
+
* A "Live stats" feature opens a window and starts a timer; turning it off
|
|
318
|
+
* releases both together. Everything made through the child scope, and
|
|
319
|
+
* cleanups registered on it, go when the child is disposed; the parent
|
|
320
|
+
* scope is untouched.
|
|
321
|
+
* ```ts
|
|
322
|
+
* let live: Scope | null = null;
|
|
323
|
+
* plugin.commands.add({
|
|
324
|
+
* id: "live-stats",
|
|
325
|
+
* path: ["Tools", "Live stats"],
|
|
326
|
+
* checked: () => live !== null,
|
|
327
|
+
* run: async () => {
|
|
328
|
+
* if (live) {
|
|
329
|
+
* await live.dispose(); // closes the window, stops the timer
|
|
330
|
+
* live = null;
|
|
331
|
+
* return;
|
|
332
|
+
* }
|
|
333
|
+
* live = plugin.scope();
|
|
334
|
+
* const window = live.windows.create("ui/stats.slint");
|
|
335
|
+
* const timer = setInterval(() => live?.update(), 1000);
|
|
336
|
+
* live.onDispose(() => clearInterval(timer));
|
|
337
|
+
* await window.show();
|
|
338
|
+
* },
|
|
339
|
+
* });
|
|
340
|
+
* ```
|
|
341
|
+
*
|
|
342
|
+
* @since 1.0.0
|
|
261
343
|
*/
|
|
262
344
|
scope(): Scope;
|
|
263
345
|
/**
|
|
@@ -266,6 +348,21 @@ export interface Scope extends Disposable {
|
|
|
266
348
|
* @param item - Any {@link Disposable}, for example a handle created
|
|
267
349
|
* through another scope.
|
|
268
350
|
* @returns `item` itself, for chaining.
|
|
351
|
+
*
|
|
352
|
+
* @capability None.
|
|
353
|
+
*
|
|
354
|
+
* @example Dispose a handle from another scope together with this one
|
|
355
|
+
* A filter made through the root scope is withdrawn when a child feature
|
|
356
|
+
* ends. `add` hands an existing disposable to this scope, which then
|
|
357
|
+
* disposes it with its own handles; it returns the same handle for
|
|
358
|
+
* chaining.
|
|
359
|
+
* ```ts
|
|
360
|
+
* const feature = plugin.scope();
|
|
361
|
+
* const blur = feature.add(plugin.video.filter("blur")); // now ends with `feature`
|
|
362
|
+
* blur.set([{ kind: "gblur", sigma: 4, steps: 3 }]);
|
|
363
|
+
* ```
|
|
364
|
+
*
|
|
365
|
+
* @since 1.0.0
|
|
269
366
|
*/
|
|
270
367
|
add<T extends Disposable>(item: T): T;
|
|
271
368
|
/**
|
|
@@ -283,6 +380,9 @@ export interface Scope extends Disposable {
|
|
|
283
380
|
* const timer = setInterval(() => plugin.update(), 1000); // re-evaluate bindings
|
|
284
381
|
* plugin.onDispose(() => clearInterval(timer));
|
|
285
382
|
* ```
|
|
383
|
+
*
|
|
384
|
+
* @capability None.
|
|
385
|
+
* @since 1.0.0
|
|
286
386
|
*/
|
|
287
387
|
onDispose(cleanup: () => void | Promise<void>): Disposable;
|
|
288
388
|
/**
|
|
@@ -304,6 +404,9 @@ export interface Scope extends Disposable {
|
|
|
304
404
|
* void plugin.osd.show("Bookmarks: {}", [error.message]);
|
|
305
405
|
* });
|
|
306
406
|
* ```
|
|
407
|
+
*
|
|
408
|
+
* @capability None.
|
|
409
|
+
* @since 1.0.0
|
|
307
410
|
*/
|
|
308
411
|
onError(listener: (error: PluginError) => void): Disposable;
|
|
309
412
|
/**
|
|
@@ -313,6 +416,30 @@ export interface Scope extends Disposable {
|
|
|
313
416
|
* changing state from your own timers or between awaits of a long task.
|
|
314
417
|
*
|
|
315
418
|
* Several calls within one microtask are merged into one pass.
|
|
419
|
+
*
|
|
420
|
+
* @capability None.
|
|
421
|
+
*
|
|
422
|
+
* @example Show progress from a long task between awaits
|
|
423
|
+
* A progress label moves while a long download runs, not only when it
|
|
424
|
+
* ends. The SDK re-evaluates bound values after each handler and when its
|
|
425
|
+
* promise settles; state changed between the awaits of a long handler
|
|
426
|
+
* shows only after an explicit `update()`.
|
|
427
|
+
* ```ts
|
|
428
|
+
* declare const window: Window;
|
|
429
|
+
* declare function downloadPart(part: number): Promise<void>;
|
|
430
|
+
* // ---cut---
|
|
431
|
+
* let done = 0;
|
|
432
|
+
* window.bind(() => ({ progress: done }));
|
|
433
|
+
* window.on("start", async () => {
|
|
434
|
+
* for (let part = 0; part < 10; part++) {
|
|
435
|
+
* await downloadPart(part);
|
|
436
|
+
* done = part + 1;
|
|
437
|
+
* plugin.update(); // send `progress` now, not when the handler ends
|
|
438
|
+
* }
|
|
439
|
+
* });
|
|
440
|
+
* ```
|
|
441
|
+
*
|
|
442
|
+
* @since 1.0.0
|
|
316
443
|
*/
|
|
317
444
|
update(): void;
|
|
318
445
|
}
|
|
@@ -351,6 +478,16 @@ export interface Scope extends Disposable {
|
|
|
351
478
|
* });
|
|
352
479
|
* }
|
|
353
480
|
* ```
|
|
481
|
+
*
|
|
482
|
+
* @remarks
|
|
483
|
+
* The root scope: disposing it is what stopping the plugin does. Besides
|
|
484
|
+
* the modules of {@link Scope}, it carries the plugin's `id` and its
|
|
485
|
+
* `dataDirectory`.
|
|
486
|
+
*
|
|
487
|
+
* @capability That of each module (see the modules).
|
|
488
|
+
* @see [How a plugin runs](../../docs/concepts/plugins.md)
|
|
489
|
+
* @since 1.0.0
|
|
490
|
+
* @category Plugin
|
|
354
491
|
*/
|
|
355
492
|
export interface Plugin extends Scope {
|
|
356
493
|
/** The manifest id, e.g. `"community.bookmark"`. */
|
|
@@ -372,7 +509,13 @@ export interface Plugin extends Scope {
|
|
|
372
509
|
*/
|
|
373
510
|
readonly dataDirectory: string;
|
|
374
511
|
}
|
|
375
|
-
/**
|
|
512
|
+
/**
|
|
513
|
+
* The state of a command's menu item, as the player shows it.
|
|
514
|
+
*
|
|
515
|
+
* @see {@link Command.set}, {@link CommandOptions}
|
|
516
|
+
* @since 1.0.0
|
|
517
|
+
* @category Commands
|
|
518
|
+
*/
|
|
376
519
|
export interface CommandState {
|
|
377
520
|
/** False greys the item out; the player then never calls `run` (bound keys are swallowed). Default true. */
|
|
378
521
|
enabled: boolean;
|
|
@@ -404,6 +547,9 @@ export interface CommandState {
|
|
|
404
547
|
* run: () => plugin.player.setLoop(null),
|
|
405
548
|
* });
|
|
406
549
|
* ```
|
|
550
|
+
*
|
|
551
|
+
* @since 1.0.0
|
|
552
|
+
* @category Commands
|
|
407
553
|
*/
|
|
408
554
|
export type Bindable<T> = T | (() => T);
|
|
409
555
|
/**
|
|
@@ -411,6 +557,9 @@ export type Bindable<T> = T | (() => T);
|
|
|
411
557
|
* `@<id>` or by its English key (see {@link CommandOptions.path}): Open,
|
|
412
558
|
* Playback, Subtitles, Video, Audio, Screen, Settings. The list is
|
|
413
559
|
* `MENU_IDS` in `@vestra-player/sdk/testing`.
|
|
560
|
+
*
|
|
561
|
+
* @since 1.3.0
|
|
562
|
+
* @category Commands
|
|
414
563
|
*/
|
|
415
564
|
export type MenuId = "open" | "playback" | "subtitles" | "video" | "audio" | "screen" | "settings";
|
|
416
565
|
/**
|
|
@@ -419,6 +568,8 @@ export type MenuId = "open" | "playback" | "subtitles" | "video" | "audio" | "sc
|
|
|
419
568
|
* `@vestra-player/sdk/testing`.
|
|
420
569
|
*
|
|
421
570
|
* @since 1.5.29
|
|
571
|
+
*
|
|
572
|
+
* @category Commands
|
|
422
573
|
*/
|
|
423
574
|
export type BarButtonIcon = "play" | "pause" | "stop" | "back" | "forward" | "open" | "volume" | "mute" | "playlist" | "detach" | "attach" | "close" | "failed" | "move-top" | "move-up" | "move-down" | "move-bottom" | "search" | "chapter" | "bookmark" | "check-task" | "check-box" | "menu-check" | "submenu" | "scroll-up" | "scroll-down" | "caret-down" | "sort-asc" | "sort-desc" | "tag-remove" | "preview-play" | "add" | "remove" | "sort" | "fullscreen" | "exit-fullscreen" | "store" | "ai";
|
|
424
575
|
/**
|
|
@@ -470,6 +621,8 @@ export type BarButtonIcon = "play" | "pause" | "stop" | "back" | "forward" | "op
|
|
|
470
621
|
* @player 0.5.10; older players show the menu entry without a button
|
|
471
622
|
* (declare `"minPlayerVersion": "0.5.10"` when the button matters).
|
|
472
623
|
* @since 1.5.29
|
|
624
|
+
*
|
|
625
|
+
* @category Commands
|
|
473
626
|
*/
|
|
474
627
|
export interface CommandButton {
|
|
475
628
|
/**
|
|
@@ -518,6 +671,10 @@ export interface CommandButton {
|
|
|
518
671
|
* {@link Commands.add} throws `invalid` synchronously for a bad path,
|
|
519
672
|
* order, group or binding. The same path registered twice, or a path that
|
|
520
673
|
* is both an item and a submenu, fails with `conflict` (through `ready`).
|
|
674
|
+
*
|
|
675
|
+
* @see {@link Commands.add}, [Commands, menus and key bindings](../../docs/concepts/commands.md)
|
|
676
|
+
* @since 1.0.0
|
|
677
|
+
* @category Commands
|
|
521
678
|
*/
|
|
522
679
|
export interface CommandOptions {
|
|
523
680
|
/** Local id, unique within the plugin (the global id is `<manifest id>/<id>`). */
|
|
@@ -644,6 +801,38 @@ export interface CommandOptions {
|
|
|
644
801
|
/**
|
|
645
802
|
* A registered command, from {@link Commands.add}. Disposing it removes it
|
|
646
803
|
* from the menu and the binding table; the id can be added again right away.
|
|
804
|
+
*
|
|
805
|
+
* @remarks
|
|
806
|
+
* The handle of one menu item and its keys. Its state (`enabled`, `checked`,
|
|
807
|
+
* `busy`, `reason`) is bound: functions given to {@link Commands.add} or
|
|
808
|
+
* {@link Command.set} are evaluated again in every update pass and only
|
|
809
|
+
* changes reach the player. Its title, path, bindings and button cannot
|
|
810
|
+
* change in place; dispose the command (or a child {@link Scope}) and add it
|
|
811
|
+
* again. Disposing it, its scope or stopping the plugin removes the menu
|
|
812
|
+
* item, its keys and its bar button.
|
|
813
|
+
*
|
|
814
|
+
* @capability commands
|
|
815
|
+
*
|
|
816
|
+
* @example Enable an item only while a file is open
|
|
817
|
+
* Video ▸ Export… is greyed out until a file is open. `set` replaces the
|
|
818
|
+
* `enabled` field with a function; the SDK evaluates it again after every
|
|
819
|
+
* player state change and sends the new state when it changes, so the
|
|
820
|
+
* item follows the player without further code.
|
|
821
|
+
* ```ts
|
|
822
|
+
* declare function runExport(): Promise<void>;
|
|
823
|
+
* // ---cut---
|
|
824
|
+
* const exportCommand = plugin.commands.add({
|
|
825
|
+
* id: "export",
|
|
826
|
+
* path: ["@video", "Export…"],
|
|
827
|
+
* run: runExport,
|
|
828
|
+
* });
|
|
829
|
+
* // A function: re-evaluated in every update pass.
|
|
830
|
+
* exportCommand.set({ enabled: () => plugin.player.state.phase === "ready" });
|
|
831
|
+
* ```
|
|
832
|
+
*
|
|
833
|
+
* @see {@link Commands.add}, [Commands, menus and key bindings](../../docs/concepts/commands.md)
|
|
834
|
+
* @since 1.0.0
|
|
835
|
+
* @category Commands
|
|
647
836
|
*/
|
|
648
837
|
export interface Command extends Disposable {
|
|
649
838
|
/** The local id given in {@link CommandOptions.id}. */
|
|
@@ -672,6 +861,9 @@ export interface Command extends Disposable {
|
|
|
672
861
|
* },
|
|
673
862
|
* });
|
|
674
863
|
* ```
|
|
864
|
+
*
|
|
865
|
+
* @capability commands
|
|
866
|
+
* @since 1.0.0
|
|
675
867
|
*/
|
|
676
868
|
set(state: Partial<{
|
|
677
869
|
[K in keyof CommandState]: Bindable<CommandState[K]>;
|
|
@@ -682,10 +874,49 @@ export interface Command extends Disposable {
|
|
|
682
874
|
* commands in this plugin, or 1024 in the whole player). You rarely need
|
|
683
875
|
* to await it: during activation a failure fails activation, later it
|
|
684
876
|
* goes to {@link Scope.onError}.
|
|
877
|
+
*
|
|
878
|
+
* @throws `conflict` (rejects) Another command already has this menu path,
|
|
879
|
+
* or the path would be both a menu item and a submenu.
|
|
880
|
+
* @throws `capability` (rejects) The manifest does not declare `commands`.
|
|
881
|
+
* @throws `invalid` (rejects) The player refused the declaration.
|
|
882
|
+
* @throws `limit` (rejects) More than 160 commands in this plugin, or 1024 in
|
|
883
|
+
* the player.
|
|
685
884
|
*/
|
|
686
885
|
readonly ready: Promise<void>;
|
|
687
886
|
}
|
|
688
|
-
/**
|
|
887
|
+
/**
|
|
888
|
+
* `plugin.commands`: menu commands with default bindings. Needs `commands`.
|
|
889
|
+
*
|
|
890
|
+
* @remarks
|
|
891
|
+
* A command is one action of the plugin with a menu item (`path`), default
|
|
892
|
+
* keys (`bindings`), optionally a bottom-bar button (`button`) and a state
|
|
893
|
+
* the menu shows (`enabled`, `checked`, `busy`, `reason`). The player owns
|
|
894
|
+
* the menu, the binding table and the user's key overrides: it calls `run`
|
|
895
|
+
* when the user picks the item, presses a key or clicks the button.
|
|
896
|
+
* Commands of all plugins merge into the player's menus by their English
|
|
897
|
+
* path keys. Registration is asynchronous: `add` returns at once, and
|
|
898
|
+
* {@link Command.ready} reports the player's answer (during activation a
|
|
899
|
+
* refusal fails activation).
|
|
900
|
+
*
|
|
901
|
+
* @capability commands
|
|
902
|
+
*
|
|
903
|
+
* @example Add a menu item with a default key
|
|
904
|
+
* Tools ▸ Say hello, or Ctrl+H, shows a greeting. `add` declares the item;
|
|
905
|
+
* the player calls `run` when the user picks it or presses its key, and the
|
|
906
|
+
* user can change the key in the player's key settings.
|
|
907
|
+
* ```ts
|
|
908
|
+
* plugin.commands.add({
|
|
909
|
+
* id: "hello",
|
|
910
|
+
* path: ["Tools", "Say hello"],
|
|
911
|
+
* bindings: ["Ctrl+H"], // a default; the user may override it
|
|
912
|
+
* run: () => plugin.osd.show("Hello"),
|
|
913
|
+
* });
|
|
914
|
+
* ```
|
|
915
|
+
*
|
|
916
|
+
* @see {@link Command}, {@link CommandOptions}, [Commands, menus and key bindings](../../docs/concepts/commands.md)
|
|
917
|
+
* @since 1.0.0
|
|
918
|
+
* @category Commands
|
|
919
|
+
*/
|
|
689
920
|
export interface Commands {
|
|
690
921
|
/**
|
|
691
922
|
* Declare a command. It is registered and kept in sync; `dispose()` removes
|
|
@@ -715,6 +946,9 @@ export interface Commands {
|
|
|
715
946
|
* run: () => plugin.player.setPaused(!plugin.player.state.paused),
|
|
716
947
|
* });
|
|
717
948
|
* ```
|
|
949
|
+
*
|
|
950
|
+
* @capability commands
|
|
951
|
+
* @since 1.0.0
|
|
718
952
|
*/
|
|
719
953
|
add(options: CommandOptions): Command;
|
|
720
954
|
}
|
|
@@ -2325,7 +2559,12 @@ export interface Panel extends View, Disposable {
|
|
|
2325
2559
|
modifiers: DropModifiers;
|
|
2326
2560
|
}) => unknown): Disposable;
|
|
2327
2561
|
}
|
|
2328
|
-
/**
|
|
2562
|
+
/**
|
|
2563
|
+
* Keys held when the files were dropped.
|
|
2564
|
+
*
|
|
2565
|
+
* @since 1.0.0
|
|
2566
|
+
* @category UI
|
|
2567
|
+
*/
|
|
2329
2568
|
export interface DropModifiers {
|
|
2330
2569
|
/** Option (macOS) / Alt (Windows). */
|
|
2331
2570
|
readonly alt: boolean;
|
|
@@ -2334,7 +2573,13 @@ export interface DropModifiers {
|
|
|
2334
2573
|
/** ⌘ on macOS, Ctrl on Windows. */
|
|
2335
2574
|
readonly cmd: boolean;
|
|
2336
2575
|
}
|
|
2337
|
-
/**
|
|
2576
|
+
/**
|
|
2577
|
+
* A provider for files dropped on the video area ({@link Drops.provide}).
|
|
2578
|
+
*
|
|
2579
|
+
* @see {@link Drops.provide}
|
|
2580
|
+
* @since 1.0.0
|
|
2581
|
+
* @category UI
|
|
2582
|
+
*/
|
|
2338
2583
|
export interface DropHandler {
|
|
2339
2584
|
/**
|
|
2340
2585
|
* Lower case, without the dot; `"folder"` matches directories.
|
|
@@ -2347,7 +2592,39 @@ export interface DropHandler {
|
|
|
2347
2592
|
modifiers: DropModifiers;
|
|
2348
2593
|
}): unknown;
|
|
2349
2594
|
}
|
|
2350
|
-
/**
|
|
2595
|
+
/**
|
|
2596
|
+
* `plugin.drops`: files dropped on the video area. Needs `ui.drop`.
|
|
2597
|
+
*
|
|
2598
|
+
* @remarks
|
|
2599
|
+
* Files dropped on the video area go to providers by extension: each
|
|
2600
|
+
* extension belongs to the provider registered for it first, and each
|
|
2601
|
+
* provider gets one call with all of its paths. Files nobody takes are
|
|
2602
|
+
* ignored. A panel with {@link Panel.onDrop} takes drops on itself first.
|
|
2603
|
+
*
|
|
2604
|
+
* @capability ui.drop
|
|
2605
|
+
*
|
|
2606
|
+
* @example Collect dropped CHIP-8 programs
|
|
2607
|
+
* `.ch8` files dropped on the player are added to the plugin's list. The
|
|
2608
|
+
* provider claims the extension; the player calls `drop` with the dropped
|
|
2609
|
+
* paths of that type, in the order the file manager gave them. Extensions
|
|
2610
|
+
* go to the first provider: the built-in playlist claims every audio and
|
|
2611
|
+
* video extension when it starts, and a built-in plugin cannot be disabled,
|
|
2612
|
+
* so another plugin's provider for those never gets them (except a
|
|
2613
|
+
* development load that replaces the playlist under its id).
|
|
2614
|
+
* ```ts
|
|
2615
|
+
* const queue: string[] = [];
|
|
2616
|
+
* plugin.drops.provide({
|
|
2617
|
+
* extensions: ["ch8"],
|
|
2618
|
+
* drop: ({ paths }) => {
|
|
2619
|
+
* queue.push(...paths); // only .ch8 files reach here
|
|
2620
|
+
* },
|
|
2621
|
+
* });
|
|
2622
|
+
* ```
|
|
2623
|
+
*
|
|
2624
|
+
* @see {@link Panel.onDrop}
|
|
2625
|
+
* @since 1.0.0
|
|
2626
|
+
* @category UI
|
|
2627
|
+
*/
|
|
2351
2628
|
export interface Drops {
|
|
2352
2629
|
/**
|
|
2353
2630
|
* Take files dropped on the video area. The first provider registered for
|
|
@@ -2381,6 +2658,9 @@ export interface Drops {
|
|
|
2381
2658
|
* },
|
|
2382
2659
|
* });
|
|
2383
2660
|
* ```
|
|
2661
|
+
*
|
|
2662
|
+
* @capability ui.drop
|
|
2663
|
+
* @since 1.0.0
|
|
2384
2664
|
*/
|
|
2385
2665
|
provide(handler: DropHandler): Disposable;
|
|
2386
2666
|
}
|
|
@@ -3297,9 +3577,19 @@ export interface Panels {
|
|
|
3297
3577
|
/** The sidebar position, including its floating state; any plugin may read and toggle it. */
|
|
3298
3578
|
readonly sidebar: SidebarSlot;
|
|
3299
3579
|
}
|
|
3300
|
-
/**
|
|
3580
|
+
/**
|
|
3581
|
+
* What a timeline mark stands for; readers choose colours by it.
|
|
3582
|
+
*
|
|
3583
|
+
* @since 1.0.0
|
|
3584
|
+
* @category Playback
|
|
3585
|
+
*/
|
|
3301
3586
|
export type MarkKind = "chapter" | "bookmark" | "marker";
|
|
3302
|
-
/**
|
|
3587
|
+
/**
|
|
3588
|
+
* One mark on the seek bar.
|
|
3589
|
+
*
|
|
3590
|
+
* @since 1.0.0
|
|
3591
|
+
* @category Playback
|
|
3592
|
+
*/
|
|
3303
3593
|
export interface TimelineMark {
|
|
3304
3594
|
/** Seconds from the start. */
|
|
3305
3595
|
readonly time: number;
|
|
@@ -3310,7 +3600,28 @@ export interface TimelineMark {
|
|
|
3310
3600
|
/** The manifest id of the plugin that published it. */
|
|
3311
3601
|
readonly plugin: string;
|
|
3312
3602
|
}
|
|
3313
|
-
/**
|
|
3603
|
+
/**
|
|
3604
|
+
* This plugin's own marks; disposing it removes them.
|
|
3605
|
+
*
|
|
3606
|
+
* @remarks
|
|
3607
|
+
* This plugin's marks on the seek bar, from {@link Timeline.layer}. `set`
|
|
3608
|
+
* replaces the whole layer and never throws: the newest write wins, and a
|
|
3609
|
+
* refused one is reported to `onError`. Disposing the layer, its scope or
|
|
3610
|
+
* stopping the plugin removes the marks.
|
|
3611
|
+
*
|
|
3612
|
+
* @capability timeline.marks
|
|
3613
|
+
*
|
|
3614
|
+
* @example Clear the plugin's marks
|
|
3615
|
+
* The plugin's marks disappear from the seek bar while it keeps its
|
|
3616
|
+
* layer for later. An empty list clears the layer; other plugins' marks
|
|
3617
|
+
* stay.
|
|
3618
|
+
* ```ts
|
|
3619
|
+
* plugin.timeline.layer().set([]);
|
|
3620
|
+
* ```
|
|
3621
|
+
*
|
|
3622
|
+
* @since 1.0.0
|
|
3623
|
+
* @category Playback
|
|
3624
|
+
*/
|
|
3314
3625
|
export interface TimelineLayer extends Disposable {
|
|
3315
3626
|
/**
|
|
3316
3627
|
* Replace the layer (last write wins; `[]` clears it). At most 1000 marks,
|
|
@@ -3335,6 +3646,9 @@ export interface TimelineLayer extends Disposable {
|
|
|
3335
3646
|
* { ifCurrent: seen },
|
|
3336
3647
|
* );
|
|
3337
3648
|
* ```
|
|
3649
|
+
*
|
|
3650
|
+
* @capability timeline.marks
|
|
3651
|
+
* @since 1.0.0
|
|
3338
3652
|
*/
|
|
3339
3653
|
set(marks: {
|
|
3340
3654
|
time: number;
|
|
@@ -3347,16 +3661,79 @@ export interface TimelineLayer extends Disposable {
|
|
|
3347
3661
|
/**
|
|
3348
3662
|
* `plugin.timeline`: marks on the seek bar. Every plugin publishes one
|
|
3349
3663
|
* layer; the player merges them. A new source clears every layer.
|
|
3664
|
+
*
|
|
3665
|
+
* @remarks
|
|
3666
|
+
* Marks on the player's seek bar: chapters, bookmarks, loop points. Each
|
|
3667
|
+
* plugin publishes one layer with {@link Timeline.layer}; the player merges
|
|
3668
|
+
* the layers by time and shows them. A new source clears every layer, so a
|
|
3669
|
+
* plugin publishes again for each file. `marks` is the merged list, which
|
|
3670
|
+
* any plugin can read.
|
|
3671
|
+
*
|
|
3672
|
+
* @capability `timeline.marks` to publish; `playback.read` to read;
|
|
3673
|
+
* `events` to follow changes.
|
|
3674
|
+
*
|
|
3675
|
+
* @example Count the chapter marks other plugins published
|
|
3676
|
+
* The plugin reads how many chapter marks the seek bar shows, whoever
|
|
3677
|
+
* published them. `marks` is the merged list of every layer, sorted by
|
|
3678
|
+
* time, with each mark's `plugin`.
|
|
3679
|
+
* ```ts
|
|
3680
|
+
* const chapters = plugin.timeline.marks.filter((mark) => mark.kind === "chapter");
|
|
3681
|
+
* await plugin.osd.show("{} chapters", [chapters.length]);
|
|
3682
|
+
* ```
|
|
3683
|
+
*
|
|
3684
|
+
* @since 1.0.0
|
|
3685
|
+
* @category Playback
|
|
3350
3686
|
*/
|
|
3351
3687
|
export interface Timeline {
|
|
3352
3688
|
/** Every layer merged, by time (`playback.read`). Empty on a new source. */
|
|
3353
3689
|
readonly marks: readonly TimelineMark[];
|
|
3354
|
-
/**
|
|
3690
|
+
/**
|
|
3691
|
+
* The merged list changed (needs `events`; at most every 100 ms).
|
|
3692
|
+
*
|
|
3693
|
+
* @capability events and playback.read
|
|
3694
|
+
*
|
|
3695
|
+
* @example Follow the marks as they change
|
|
3696
|
+
* A panel lists the seek bar's marks and updates when any plugin publishes
|
|
3697
|
+
* or a new file clears them. The listener gets the merged list, at most
|
|
3698
|
+
* every 100 ms.
|
|
3699
|
+
* ```ts
|
|
3700
|
+
* plugin.timeline.onChange((marks) => {
|
|
3701
|
+
* console.log(marks.map((mark) => `${mark.time}s ${mark.title ?? mark.kind}`));
|
|
3702
|
+
* });
|
|
3703
|
+
* ```
|
|
3704
|
+
*
|
|
3705
|
+
* @since 1.0.0
|
|
3706
|
+
*/
|
|
3355
3707
|
onChange(listener: (marks: readonly TimelineMark[]) => void): Disposable;
|
|
3356
|
-
/**
|
|
3708
|
+
/**
|
|
3709
|
+
* This plugin's layer (`timeline.marks`); a second call throws.
|
|
3710
|
+
*
|
|
3711
|
+
* @capability timeline.marks
|
|
3712
|
+
*
|
|
3713
|
+
* @example Publish this plugin's marks
|
|
3714
|
+
* The plugin gets its one layer and puts two chapter marks on the seek
|
|
3715
|
+
* bar. Calling `layer()` again while the first layer is not disposed
|
|
3716
|
+
* throws; disposing the layer removes the
|
|
3717
|
+
* marks.
|
|
3718
|
+
* ```ts
|
|
3719
|
+
* const layer = plugin.timeline.layer();
|
|
3720
|
+
* layer.set([
|
|
3721
|
+
* { time: 0, kind: "chapter", title: "Intro" },
|
|
3722
|
+
* { time: 95, kind: "chapter", title: "Part 1" }, // seconds
|
|
3723
|
+
* ]);
|
|
3724
|
+
* ```
|
|
3725
|
+
*
|
|
3726
|
+
* @since 1.0.0
|
|
3727
|
+
*/
|
|
3357
3728
|
layer(): TimelineLayer;
|
|
3358
3729
|
}
|
|
3359
|
-
/**
|
|
3730
|
+
/**
|
|
3731
|
+
* The main window as `plugin.window.state` reports it (logical points).
|
|
3732
|
+
*
|
|
3733
|
+
* @see {@link MainWindow.state}
|
|
3734
|
+
* @since 1.0.0
|
|
3735
|
+
* @category Plugin
|
|
3736
|
+
*/
|
|
3360
3737
|
export interface WindowState {
|
|
3361
3738
|
/** The main window is fullscreen. */
|
|
3362
3739
|
readonly fullscreen: boolean;
|
|
@@ -3406,11 +3783,38 @@ export interface WindowState {
|
|
|
3406
3783
|
* },
|
|
3407
3784
|
* });
|
|
3408
3785
|
* ```
|
|
3786
|
+
*
|
|
3787
|
+
* @remarks
|
|
3788
|
+
* `plugin.window` is the player's main window: its size, fullscreen,
|
|
3789
|
+
* picture-in-picture and the video area. Reading it needs no capability;
|
|
3790
|
+
* changing it needs `window.control`. Changes that last only while the
|
|
3791
|
+
* plugin runs (always on top, pointer auto-hide) revert when it stops.
|
|
3792
|
+
*
|
|
3793
|
+
* @capability None to read; window.control to change; events to follow
|
|
3794
|
+
* outside changes.
|
|
3795
|
+
* @since 1.0.0
|
|
3796
|
+
* @category Plugin
|
|
3409
3797
|
*/
|
|
3410
3798
|
export interface MainWindow {
|
|
3411
3799
|
/** Latest state; zero video area without a native window. */
|
|
3412
3800
|
readonly state: WindowState;
|
|
3413
|
-
/**
|
|
3801
|
+
/**
|
|
3802
|
+
* The window state changed (needs `events` for outside changes).
|
|
3803
|
+
*
|
|
3804
|
+
* @capability events (for outside changes)
|
|
3805
|
+
*
|
|
3806
|
+
* @example Fit a panel to fullscreen
|
|
3807
|
+
* The plugin's panel uses larger text while the player is fullscreen. The
|
|
3808
|
+
* listener runs after the SDK updated `state` from the player's events
|
|
3809
|
+
* (with `events`; without it only after this plugin's own changes).
|
|
3810
|
+
* ```ts
|
|
3811
|
+
* declare const bar: Panel;
|
|
3812
|
+
* // ---cut---
|
|
3813
|
+
* plugin.window.onChange((state) => bar.set({ large: state.fullscreen }));
|
|
3814
|
+
* ```
|
|
3815
|
+
*
|
|
3816
|
+
* @since 1.0.0
|
|
3817
|
+
*/
|
|
3414
3818
|
onChange(listener: (state: WindowState) => void): Disposable;
|
|
3415
3819
|
/**
|
|
3416
3820
|
* Resize the window so the video column is `width`×`height` (the bottom bar
|
|
@@ -3422,6 +3826,23 @@ export interface MainWindow {
|
|
|
3422
3826
|
* Sizes are 1–16384 logical points; the minimum video area is 318×120.
|
|
3423
3827
|
* The visible area excludes the menu bar and Dock on macOS; on Windows it
|
|
3424
3828
|
* is the whole display. `unavailable` without a user interface too.
|
|
3829
|
+
*
|
|
3830
|
+
* @capability window.control
|
|
3831
|
+
* @throws `unavailable` (rejects) Fullscreen, or no user interface.
|
|
3832
|
+
* @throws `invalid` (rejects) A size outside 1–16384 logical points.
|
|
3833
|
+
* @throws `capability` (rejects) The manifest does not declare
|
|
3834
|
+
* `window.control`.
|
|
3835
|
+
*
|
|
3836
|
+
* @example Make the video area 1280×720 points
|
|
3837
|
+
* The window resizes so that the video column is 1280×720 logical points,
|
|
3838
|
+
* the bars and a docked sidebar added around it. A request that does not
|
|
3839
|
+
* fit the screen is scaled down keeping the aspect, and `clamped` says so.
|
|
3840
|
+
* ```ts
|
|
3841
|
+
* const applied = await plugin.window.setVideoArea({ width: 1280, height: 720 });
|
|
3842
|
+
* if (applied.clamped) await plugin.osd.show("{}×{}", [applied.width, applied.height]);
|
|
3843
|
+
* ```
|
|
3844
|
+
*
|
|
3845
|
+
* @since 1.0.0
|
|
3425
3846
|
*/
|
|
3426
3847
|
setVideoArea(size: {
|
|
3427
3848
|
width: number;
|
|
@@ -3437,12 +3858,53 @@ export interface MainWindow {
|
|
|
3437
3858
|
/**
|
|
3438
3859
|
* Float the player's windows above other applications. Newest call wins; reverts when this plugin stops.
|
|
3439
3860
|
* `unavailable` without a user interface.
|
|
3861
|
+
*
|
|
3862
|
+
* @capability window.control
|
|
3863
|
+
* @throws `unavailable` (rejects) No user interface.
|
|
3864
|
+
* @throws `invalid` (rejects) `onTop` is not a boolean.
|
|
3865
|
+
* @throws `capability` (rejects) The manifest does not declare
|
|
3866
|
+
* `window.control`.
|
|
3867
|
+
*
|
|
3868
|
+
* @example Keep the player above other windows
|
|
3869
|
+
* The player's windows (tool windows too) float above other applications
|
|
3870
|
+
* while the plugin's "Always on top" item is checked. The newest call of
|
|
3871
|
+
* any plugin wins, and the setting reverts when this plugin stops.
|
|
3872
|
+
* ```ts
|
|
3873
|
+
* let onTop = false;
|
|
3874
|
+
* plugin.commands.add({
|
|
3875
|
+
* id: "on-top",
|
|
3876
|
+
* path: ["@screen", "Always on top"],
|
|
3877
|
+
* checked: () => onTop,
|
|
3878
|
+
* run: async () => {
|
|
3879
|
+
* onTop = !onTop;
|
|
3880
|
+
* await plugin.window.setAlwaysOnTop(onTop); // reverts when this plugin stops
|
|
3881
|
+
* },
|
|
3882
|
+
* });
|
|
3883
|
+
* ```
|
|
3884
|
+
*
|
|
3885
|
+
* @since 1.0.0
|
|
3440
3886
|
*/
|
|
3441
3887
|
setAlwaysOnTop(onTop: boolean): Promise<void>;
|
|
3442
3888
|
/**
|
|
3443
3889
|
* `"playing"`: hide the pointer over the video after 3 idle seconds while
|
|
3444
3890
|
* playing, in a window too (not only fullscreen). Newest call wins;
|
|
3445
3891
|
* reverts to `"fullscreen"` when this plugin stops.
|
|
3892
|
+
*
|
|
3893
|
+
* @capability window.control
|
|
3894
|
+
* @throws `invalid` (rejects) A mode other than `"fullscreen"` or
|
|
3895
|
+
* `"playing"`.
|
|
3896
|
+
* @throws `capability` (rejects) The manifest does not declare
|
|
3897
|
+
* `window.control`.
|
|
3898
|
+
*
|
|
3899
|
+
* @example Hide the pointer over playing video in a window too
|
|
3900
|
+
* The pointer disappears after 3 idle seconds over playing video, not only
|
|
3901
|
+
* in fullscreen. The newest call wins; when this plugin stops the player
|
|
3902
|
+
* goes back to hiding it in fullscreen only.
|
|
3903
|
+
* ```ts
|
|
3904
|
+
* await plugin.window.setCursorAutoHide("playing");
|
|
3905
|
+
* ```
|
|
3906
|
+
*
|
|
3907
|
+
* @since 1.0.0
|
|
3446
3908
|
*/
|
|
3447
3909
|
setCursorAutoHide(mode: "fullscreen" | "playing"): Promise<void>;
|
|
3448
3910
|
/**
|
|
@@ -3467,10 +3929,26 @@ export interface MainWindow {
|
|
|
3467
3929
|
* if (plugin.window.state.pictureInPicture.supported)
|
|
3468
3930
|
* await plugin.window.setPictureInPicture(!plugin.window.state.pictureInPicture.active);
|
|
3469
3931
|
* ```
|
|
3932
|
+
*
|
|
3933
|
+
* @capability window.control
|
|
3934
|
+
* @throws `unsupported` (rejects) The system has no picture-in-picture
|
|
3935
|
+
* (`supported` is false).
|
|
3936
|
+
* @throws `unavailable` (rejects) No video, or no user interface.
|
|
3937
|
+
* @throws `invalid` (rejects) `on` is not a boolean.
|
|
3938
|
+
* @throws `busy` (rejects) The previous call is still switching.
|
|
3939
|
+
* @throws `failed` (rejects) The system refused.
|
|
3940
|
+
* @throws `capability` (rejects) The manifest does not declare
|
|
3941
|
+
* `window.control`.
|
|
3942
|
+
* @since 1.0.0
|
|
3470
3943
|
*/
|
|
3471
3944
|
setPictureInPicture(on: boolean): Promise<void>;
|
|
3472
3945
|
}
|
|
3473
|
-
/**
|
|
3946
|
+
/**
|
|
3947
|
+
* `plugin.app`: fixed for the life of the plugin process.
|
|
3948
|
+
*
|
|
3949
|
+
* @since 1.0.0
|
|
3950
|
+
* @category Plugin
|
|
3951
|
+
*/
|
|
3474
3952
|
export interface AppInfo {
|
|
3475
3953
|
/** The player's version, e.g. `"1.0.0"`. */
|
|
3476
3954
|
readonly version: string;
|
|
@@ -3500,15 +3978,47 @@ export interface AppInfo {
|
|
|
3500
3978
|
* const system = await plugin.app.system();
|
|
3501
3979
|
* const text = `Vestra ${plugin.app.version} on ${system.name} ${system.version ?? ""}`;
|
|
3502
3980
|
* ```
|
|
3981
|
+
*
|
|
3982
|
+
* @remarks
|
|
3983
|
+
* `plugin.app` holds facts fixed for the player's run (its version, build,
|
|
3984
|
+
* library versions) and reads the system's on request: `system()` reads
|
|
3985
|
+
* the displays at each call; the operating system's name and version are
|
|
3986
|
+
* worked out once, at the first call, and reused.
|
|
3987
|
+
*
|
|
3988
|
+
* @capability None.
|
|
3989
|
+
* @since 1.0.0
|
|
3990
|
+
* @category Plugin
|
|
3503
3991
|
*/
|
|
3504
3992
|
export interface App extends AppInfo {
|
|
3505
3993
|
/**
|
|
3506
3994
|
* The operating system and the displays, read when called (displays come
|
|
3507
3995
|
* and go). No capability. CPU and memory: Node's `os` module.
|
|
3996
|
+
*
|
|
3997
|
+
* @capability None.
|
|
3998
|
+
* @throws `busy` (rejects) The player could not start the request (no
|
|
3999
|
+
* thread available).
|
|
4000
|
+
*
|
|
4001
|
+
* @example Choose a size for the primary display
|
|
4002
|
+
* The plugin sizes its picture for the primary display. `system()` asks
|
|
4003
|
+
* the player; `displays` lists the displays present now, the primary one
|
|
4004
|
+
* first (empty without a user interface).
|
|
4005
|
+
* ```ts
|
|
4006
|
+
* const { displays } = await plugin.app.system();
|
|
4007
|
+
* const primary = displays[0];
|
|
4008
|
+
* if (primary) console.log(primary.width, primary.height);
|
|
4009
|
+
* ```
|
|
4010
|
+
*
|
|
4011
|
+
* @since 1.0.0
|
|
3508
4012
|
*/
|
|
3509
4013
|
system(): Promise<SystemInfo>;
|
|
3510
4014
|
}
|
|
3511
|
-
/**
|
|
4015
|
+
/**
|
|
4016
|
+
* What {@link App.system} reports.
|
|
4017
|
+
*
|
|
4018
|
+
* @see {@link App.system}
|
|
4019
|
+
* @since 1.0.0
|
|
4020
|
+
* @category Plugin
|
|
4021
|
+
*/
|
|
3512
4022
|
export interface SystemInfo {
|
|
3513
4023
|
/** The operating system family. */
|
|
3514
4024
|
readonly os: "macos" | "windows" | "linux" | "other";
|
|
@@ -3525,7 +4035,12 @@ export interface SystemInfo {
|
|
|
3525
4035
|
/** Primary display first; empty without a user interface. */
|
|
3526
4036
|
readonly displays: readonly DisplayInfo[];
|
|
3527
4037
|
}
|
|
3528
|
-
/**
|
|
4038
|
+
/**
|
|
4039
|
+
* One display in {@link SystemInfo.displays}.
|
|
4040
|
+
*
|
|
4041
|
+
* @since 1.0.0
|
|
4042
|
+
* @category Plugin
|
|
4043
|
+
*/
|
|
3529
4044
|
export interface DisplayInfo {
|
|
3530
4045
|
/** The system's name for it, `null` when it has none. */
|
|
3531
4046
|
readonly name: string | null;
|
|
@@ -3814,7 +4329,12 @@ export interface Dialogs {
|
|
|
3814
4329
|
fileName?: TextValue;
|
|
3815
4330
|
}): Promise<string | null>;
|
|
3816
4331
|
}
|
|
3817
|
-
/**
|
|
4332
|
+
/**
|
|
4333
|
+
* One audio track of the current media ({@link PlayerState.audioTracks}).
|
|
4334
|
+
*
|
|
4335
|
+
* @since 1.0.0
|
|
4336
|
+
* @category Playback
|
|
4337
|
+
*/
|
|
3818
4338
|
export interface AudioTrack {
|
|
3819
4339
|
/** The backend's track id; pass it to {@link Player.selectAudioTrack}. */
|
|
3820
4340
|
id: number;
|
|
@@ -3836,6 +4356,10 @@ export interface AudioTrack {
|
|
|
3836
4356
|
*
|
|
3837
4357
|
* Without `events`, {@link Player.state} only updates at start and after
|
|
3838
4358
|
* this plugin's own operations.
|
|
4359
|
+
*
|
|
4360
|
+
* @see {@link Player.state}, [Playback state, controls and sources](../../docs/concepts/playback.md)
|
|
4361
|
+
* @since 1.0.0
|
|
4362
|
+
* @category Playback
|
|
3839
4363
|
*/
|
|
3840
4364
|
export interface PlayerState {
|
|
3841
4365
|
/** Changes every time a source is opened, even the same file again. */
|
|
@@ -3913,14 +4437,24 @@ export interface PlayerState {
|
|
|
3913
4437
|
active: string | null;
|
|
3914
4438
|
};
|
|
3915
4439
|
}
|
|
3916
|
-
/**
|
|
4440
|
+
/**
|
|
4441
|
+
* An audio output device (`player.audioDevices`).
|
|
4442
|
+
*
|
|
4443
|
+
* @since 1.0.0
|
|
4444
|
+
* @category Playback
|
|
4445
|
+
*/
|
|
3917
4446
|
export interface AudioDevice {
|
|
3918
4447
|
/** Stable id to pass to `setAudioDevice` (e.g. `coreaudio/<UID>`). */
|
|
3919
4448
|
id: string;
|
|
3920
4449
|
/** What the system calls it. */
|
|
3921
4450
|
name: string;
|
|
3922
4451
|
}
|
|
3923
|
-
/**
|
|
4452
|
+
/**
|
|
4453
|
+
* One chapter of the current media.
|
|
4454
|
+
*
|
|
4455
|
+
* @since 1.0.0
|
|
4456
|
+
* @category Playback
|
|
4457
|
+
*/
|
|
3924
4458
|
export interface Chapter {
|
|
3925
4459
|
/** Chapter title, `null` without one. */
|
|
3926
4460
|
title: string | null;
|
|
@@ -3955,12 +4489,41 @@ export interface Chapter {
|
|
|
3955
4489
|
* }
|
|
3956
4490
|
* });
|
|
3957
4491
|
* ```
|
|
4492
|
+
*
|
|
4493
|
+
* @see [Playback state, controls and sources](../../docs/concepts/playback.md)
|
|
4494
|
+
* @since 1.0.0
|
|
4495
|
+
* @category Playback
|
|
3958
4496
|
*/
|
|
3959
4497
|
export interface Conditional {
|
|
3960
4498
|
/** The state the call is conditional on; default: the current `player.state`. */
|
|
3961
4499
|
ifCurrent?: PlayerState;
|
|
3962
4500
|
}
|
|
3963
|
-
/**
|
|
4501
|
+
/**
|
|
4502
|
+
* A source this plugin opened.
|
|
4503
|
+
*
|
|
4504
|
+
* @remarks
|
|
4505
|
+
* A source this plugin opened with {@link Player.open}. It does not own
|
|
4506
|
+
* the source: the user or another plugin can replace it at any time, and
|
|
4507
|
+
* `current` then turns false. `ready` and `onEnd` follow the player state:
|
|
4508
|
+
* with `events` they react to the player's events, without it `ready`
|
|
4509
|
+
* polls the state every 100 ms (which needs `playback.read`). Disposing the
|
|
4510
|
+
* scope that opened it does not close the media; call `close` for that.
|
|
4511
|
+
*
|
|
4512
|
+
* @capability That of {@link Player.open} (`playback.control`); `ready` and
|
|
4513
|
+
* `onEnd` follow the state through `events` and `playback.read`.
|
|
4514
|
+
*
|
|
4515
|
+
* @example Open a file and say when it ends
|
|
4516
|
+
* The file opens and plays, and the OSD says "Finished" when it reaches its
|
|
4517
|
+
* end on its own. The handle belongs to this source: `onEnd` fires only
|
|
4518
|
+
* for a natural end of this media, not when someone opens another file.
|
|
4519
|
+
* ```ts
|
|
4520
|
+
* const media = await plugin.player.open("/Movies/clip.mp4");
|
|
4521
|
+
* media.onEnd(() => void plugin.osd.show("Finished")); // natural ends only
|
|
4522
|
+
* ```
|
|
4523
|
+
*
|
|
4524
|
+
* @since 1.0.0
|
|
4525
|
+
* @category Playback
|
|
4526
|
+
*/
|
|
3964
4527
|
export interface Media {
|
|
3965
4528
|
/** Still the player's source (a restart keeps it current). */
|
|
3966
4529
|
readonly current: boolean;
|
|
@@ -3968,13 +4531,77 @@ export interface Media {
|
|
|
3968
4531
|
* Resolves when it is playing. Rejects with code `failed` (load error),
|
|
3969
4532
|
* `replaced` (another source took over) or `ended`; with `AbortError` when
|
|
3970
4533
|
* the signal aborts, after closing the media.
|
|
4534
|
+
*
|
|
4535
|
+
* @capability That of {@link Player.open}; `playback.read` or `events` to
|
|
4536
|
+
* see the state change.
|
|
4537
|
+
* @throws `failed` (rejects) The media could not be loaded.
|
|
4538
|
+
* @throws `replaced` (rejects) Another source took over first.
|
|
4539
|
+
* @throws `ended` (rejects) The media already played to the end.
|
|
4540
|
+
* @throws `aborted` (rejects) `signal` aborted (an `AbortError`); the media
|
|
4541
|
+
* is closed.
|
|
4542
|
+
*
|
|
4543
|
+
* @example Wait at most ten seconds for a stream to start
|
|
4544
|
+
* A network stream gets ten seconds to start playing; otherwise it is
|
|
4545
|
+
* closed and the user is told. Aborting the signal makes `ready` reject
|
|
4546
|
+
* with an `AbortError` after closing the media.
|
|
4547
|
+
* ```ts
|
|
4548
|
+
* const media = await plugin.player.open("https://example.com/live.m3u8");
|
|
4549
|
+
* // The timeout's abort closes the media and rejects with an AbortError.
|
|
4550
|
+
* try {
|
|
4551
|
+
* await media.ready({ signal: AbortSignal.timeout(10_000) });
|
|
4552
|
+
* } catch (error) {
|
|
4553
|
+
* await plugin.osd.show("Not playing: {}", [(error as Error).message]);
|
|
4554
|
+
* }
|
|
4555
|
+
* ```
|
|
4556
|
+
*
|
|
4557
|
+
* @since 1.0.0
|
|
3971
4558
|
*/
|
|
3972
4559
|
ready(options?: {
|
|
3973
4560
|
signal?: AbortSignal;
|
|
3974
4561
|
}): Promise<void>;
|
|
3975
|
-
/**
|
|
4562
|
+
/**
|
|
4563
|
+
* Called once each time a playthrough of this media ends naturally.
|
|
4564
|
+
*
|
|
4565
|
+
* @capability That of {@link Player.open}; `events` to hear the end.
|
|
4566
|
+
*
|
|
4567
|
+
* @example Play the next file when this one ends
|
|
4568
|
+
* When the plugin's file ends naturally, the next one opens. The listener
|
|
4569
|
+
* gets the state at the end; passing it as `ifCurrent` makes the next
|
|
4570
|
+
* `open` fail with `stale` (and do nothing) if the user opened something
|
|
4571
|
+
* else in the meantime.
|
|
4572
|
+
* ```ts
|
|
4573
|
+
* declare const media: Media;
|
|
4574
|
+
* declare const next: string;
|
|
4575
|
+
* // ---cut---
|
|
4576
|
+
* media.onEnd((ended) =>
|
|
4577
|
+
* plugin.player.open(next, { ifCurrent: ended }).catch((error: PluginError) => {
|
|
4578
|
+
* if (error.code !== "stale") throw error; // the user opened something else
|
|
4579
|
+
* }),
|
|
4580
|
+
* );
|
|
4581
|
+
* ```
|
|
4582
|
+
*
|
|
4583
|
+
* @since 1.0.0
|
|
4584
|
+
*/
|
|
3976
4585
|
onEnd(listener: (state: PlayerState) => void): Disposable;
|
|
3977
|
-
/**
|
|
4586
|
+
/**
|
|
4587
|
+
* Close it if it is still the player's source; otherwise do nothing.
|
|
4588
|
+
*
|
|
4589
|
+
* @capability That of {@link Player.open} (`playback.control`).
|
|
4590
|
+
* @throws `capability` (rejects) The manifest does not declare
|
|
4591
|
+
* `playback.control`.
|
|
4592
|
+
*
|
|
4593
|
+
* @example Close the preview the plugin opened
|
|
4594
|
+
* The plugin closes the preview it opened, but leaves alone anything the
|
|
4595
|
+
* user opened since. `close` acts only while this media is still the
|
|
4596
|
+
* player's source and does nothing otherwise.
|
|
4597
|
+
* ```ts
|
|
4598
|
+
* declare const preview: Media;
|
|
4599
|
+
* // ---cut---
|
|
4600
|
+
* await preview.close(); // a no-op when something else is playing now
|
|
4601
|
+
* ```
|
|
4602
|
+
*
|
|
4603
|
+
* @since 1.0.0
|
|
4604
|
+
*/
|
|
3978
4605
|
close(): Promise<void>;
|
|
3979
4606
|
}
|
|
3980
4607
|
/**
|
|
@@ -3997,6 +4624,18 @@ export interface Media {
|
|
|
3997
4624
|
* bar.on("dragMove", (seconds: number) => void scrub?.seek(seconds));
|
|
3998
4625
|
* bar.on("dragEnd", () => scrub?.end());
|
|
3999
4626
|
* ```
|
|
4627
|
+
*
|
|
4628
|
+
* @remarks
|
|
4629
|
+
* A drag gesture on a seek slider. Each `seek` is conditional on the
|
|
4630
|
+
* previous one and only the newest target is sent while a seek is
|
|
4631
|
+
* running, so a fast drag never queues seeks. Any outside change (another
|
|
4632
|
+
* file, the user pausing) ends the gesture: `active` turns false and later
|
|
4633
|
+
* seeks resolve `false`.
|
|
4634
|
+
*
|
|
4635
|
+
* @capability playback.control
|
|
4636
|
+
* @see [Keep a slider responsive while dragging](../../docs/how-to/slider.md)
|
|
4637
|
+
* @since 1.0.0
|
|
4638
|
+
* @category Playback
|
|
4000
4639
|
*/
|
|
4001
4640
|
export interface Scrub {
|
|
4002
4641
|
/**
|
|
@@ -4029,13 +4668,40 @@ export interface Scrub {
|
|
|
4029
4668
|
* const keyframes = true; // the plugin's own setting
|
|
4030
4669
|
* bar.on("dragMove", (seconds: number) => void scrub?.seek(seconds, { keyframes }));
|
|
4031
4670
|
* ```
|
|
4671
|
+
*
|
|
4672
|
+
* @capability playback.control
|
|
4673
|
+
* @throws `capability` (rejects) The manifest does not declare
|
|
4674
|
+
* `playback.control`.
|
|
4675
|
+
* @throws `stale` (rejects) The source changed from outside while this
|
|
4676
|
+
* seek was being sent (a superseded target or an ended gesture resolves
|
|
4677
|
+
* `false` instead).
|
|
4678
|
+
* @throws `unsupported` (rejects) The source is a generated stream.
|
|
4679
|
+
* @throws `backend` (rejects) The player failed the seek.
|
|
4680
|
+
* @since 1.0.0
|
|
4032
4681
|
*/
|
|
4033
4682
|
seek(seconds: number, options?: {
|
|
4034
4683
|
keyframes?: boolean;
|
|
4035
4684
|
}): Promise<boolean>;
|
|
4036
4685
|
/** False once anyone else changed the source; later seeks are ignored. */
|
|
4037
4686
|
readonly active: boolean;
|
|
4038
|
-
/**
|
|
4687
|
+
/**
|
|
4688
|
+
* End the gesture; later seeks resolve `false`.
|
|
4689
|
+
*
|
|
4690
|
+
* @capability playback.control
|
|
4691
|
+
*
|
|
4692
|
+
* @example End the gesture when the user lets go
|
|
4693
|
+
* Releasing the slider ends the gesture, so a late `dragMove` cannot move
|
|
4694
|
+
* playback any more. After `end`, `seek` resolves `false` without doing
|
|
4695
|
+
* anything.
|
|
4696
|
+
* ```ts
|
|
4697
|
+
* declare const bar: Panel;
|
|
4698
|
+
* declare const scrub: Scrub;
|
|
4699
|
+
* // ---cut---
|
|
4700
|
+
* bar.on("dragEnd", () => scrub.end()); // later seeks resolve false
|
|
4701
|
+
* ```
|
|
4702
|
+
*
|
|
4703
|
+
* @since 1.0.0
|
|
4704
|
+
*/
|
|
4039
4705
|
end(): void;
|
|
4040
4706
|
}
|
|
4041
4707
|
/**
|
|
@@ -4049,6 +4715,9 @@ export interface Scrub {
|
|
|
4049
4715
|
*
|
|
4050
4716
|
* The SDK copies the frame once before `frame()` is called again, so you
|
|
4051
4717
|
* may reuse the same buffers for every frame.
|
|
4718
|
+
*
|
|
4719
|
+
* @since 1.0.0
|
|
4720
|
+
* @category Playback
|
|
4052
4721
|
*/
|
|
4053
4722
|
export interface StreamFrame {
|
|
4054
4723
|
/** `width × height × 4` bytes of RGBA, at most 4 MiB. */
|
|
@@ -4060,7 +4729,13 @@ export interface StreamFrame {
|
|
|
4060
4729
|
*/
|
|
4061
4730
|
audio: Float32Array;
|
|
4062
4731
|
}
|
|
4063
|
-
/**
|
|
4732
|
+
/**
|
|
4733
|
+
* Options of {@link Player.openStream}.
|
|
4734
|
+
*
|
|
4735
|
+
* @see {@link Player.openStream}
|
|
4736
|
+
* @since 1.0.0
|
|
4737
|
+
* @category Playback
|
|
4738
|
+
*/
|
|
4064
4739
|
export interface StreamOptions {
|
|
4065
4740
|
/** Frame width in pixels. */
|
|
4066
4741
|
width: number;
|
|
@@ -4075,12 +4750,48 @@ export interface StreamOptions {
|
|
|
4075
4750
|
* `openStream` start-up timeouts: code `timeout` with the step that ran out
|
|
4076
4751
|
* of time, the first buffers (`prefill`) or the host's activation.
|
|
4077
4752
|
* Both limits are 10 seconds.
|
|
4753
|
+
*
|
|
4754
|
+
* @since 1.0.0
|
|
4755
|
+
* @category Playback
|
|
4078
4756
|
*/
|
|
4079
4757
|
export type StreamStartError = PluginError & {
|
|
4080
4758
|
/** The start-up step that timed out. */
|
|
4081
4759
|
stage: "prefill" | "activation";
|
|
4082
4760
|
};
|
|
4083
|
-
/**
|
|
4761
|
+
/**
|
|
4762
|
+
* A generated source, from {@link Player.openStream}. Disposing it stops it.
|
|
4763
|
+
*
|
|
4764
|
+
* @remarks
|
|
4765
|
+
* A source whose frames this plugin generates, from
|
|
4766
|
+
* {@link Player.openStream}. The SDK pulls frames on the audio clock over
|
|
4767
|
+
* a local connection and handles prefill, activation and pausing; the plugin
|
|
4768
|
+
* only supplies `frame()`. A stream ends when the plugin stops it, when
|
|
4769
|
+
* another source replaces it or the user closes it (`onStop` with `null`),
|
|
4770
|
+
* or when it fails (`onStop` with the error).
|
|
4771
|
+
*
|
|
4772
|
+
* @capability producer.av and playback.control
|
|
4773
|
+
*
|
|
4774
|
+
* @example Stop the stream from a menu item
|
|
4775
|
+
* A menu item ends the plugin's generated stream and closes it as the
|
|
4776
|
+
* player's source. `stop` closes the source first and then the frame
|
|
4777
|
+
* connection, so the user never sees a playback error; `onStop` is not
|
|
4778
|
+
* called for the plugin's own stop.
|
|
4779
|
+
* ```ts
|
|
4780
|
+
* declare const stream: Stream;
|
|
4781
|
+
* // ---cut---
|
|
4782
|
+
* // Enabled while the stream produces frames.
|
|
4783
|
+
* plugin.commands.add({
|
|
4784
|
+
* id: "stop-generator",
|
|
4785
|
+
* path: ["@playback", "Stop generator"],
|
|
4786
|
+
* enabled: () => stream.running,
|
|
4787
|
+
* run: () => stream.stop(),
|
|
4788
|
+
* });
|
|
4789
|
+
* ```
|
|
4790
|
+
*
|
|
4791
|
+
* @see {@link Player.openStream}
|
|
4792
|
+
* @since 1.0.0
|
|
4793
|
+
* @category Playback
|
|
4794
|
+
*/
|
|
4084
4795
|
export interface Stream extends Disposable {
|
|
4085
4796
|
/** The stream is producing frames. */
|
|
4086
4797
|
readonly running: boolean;
|
|
@@ -4096,6 +4807,23 @@ export interface Stream extends Disposable {
|
|
|
4096
4807
|
* (before 1.5.7 a replaced or closed stream could get `unavailable`
|
|
4097
4808
|
* "producer absent"). Without `playback.read` a failure the SDK has not
|
|
4098
4809
|
* seen yet is reported as null.
|
|
4810
|
+
*
|
|
4811
|
+
* @capability producer.av
|
|
4812
|
+
*
|
|
4813
|
+
* @example Tell the user why the stream stopped
|
|
4814
|
+
* The OSD explains a failure, and nothing is said when the user simply
|
|
4815
|
+
* opened another file. The listener runs only for stops from outside the
|
|
4816
|
+
* plugin: `null` for a replacement or the user's close, an error (such as
|
|
4817
|
+
* `stream` with the player's text) for a failure.
|
|
4818
|
+
* ```ts
|
|
4819
|
+
* declare const stream: Stream;
|
|
4820
|
+
* // ---cut---
|
|
4821
|
+
* stream.onStop((error) => {
|
|
4822
|
+
* if (error) void plugin.osd.show("Generator stopped: {}", [error.message]);
|
|
4823
|
+
* });
|
|
4824
|
+
* ```
|
|
4825
|
+
*
|
|
4826
|
+
* @since 1.0.0
|
|
4099
4827
|
*/
|
|
4100
4828
|
onStop(listener: (error: PluginError | null) => void): Disposable;
|
|
4101
4829
|
/**
|
|
@@ -4103,10 +4831,34 @@ export interface Stream extends Disposable {
|
|
|
4103
4831
|
* The source closes first, then the frame connection, then the producer,
|
|
4104
4832
|
* so stopping never shows as a playback failure. `dispose()` and
|
|
4105
4833
|
* deactivation do the same.
|
|
4834
|
+
*
|
|
4835
|
+
* @capability producer.av and playback.control
|
|
4836
|
+
* @throws Whatever code closing the player's source failed with (the
|
|
4837
|
+
* frame connection and the producer are released either way). A
|
|
4838
|
+
* stream that is no longer the source just resolves.
|
|
4839
|
+
*
|
|
4840
|
+
* @example Stop the stream when the plugin's window closes
|
|
4841
|
+
* Closing the generator window stops the stream it feeds. `stop` closes
|
|
4842
|
+
* the player's source if it is still this stream and then the frame
|
|
4843
|
+
* connection; it resolves once both are closed.
|
|
4844
|
+
* ```ts
|
|
4845
|
+
* declare const stream: Stream;
|
|
4846
|
+
* declare const window: Window;
|
|
4847
|
+
* // ---cut---
|
|
4848
|
+
* window.onClose(() => stream.stop());
|
|
4849
|
+
* ```
|
|
4850
|
+
*
|
|
4851
|
+
* @since 1.0.0
|
|
4106
4852
|
*/
|
|
4107
4853
|
stop(): Promise<void>;
|
|
4108
4854
|
}
|
|
4109
|
-
/**
|
|
4855
|
+
/**
|
|
4856
|
+
* The plugin that owns "next / previous" (a playlist).
|
|
4857
|
+
*
|
|
4858
|
+
* @see {@link Player.provideSequence}
|
|
4859
|
+
* @since 1.0.0
|
|
4860
|
+
* @category Playback
|
|
4861
|
+
*/
|
|
4110
4862
|
export interface SequenceProvider {
|
|
4111
4863
|
/** Go to the next item (called in the provider's scope). */
|
|
4112
4864
|
next(): unknown;
|
|
@@ -4120,6 +4872,10 @@ export interface SequenceProvider {
|
|
|
4120
4872
|
/**
|
|
4121
4873
|
* What {@link Player.stats} reports. Every field is the backend's own value
|
|
4122
4874
|
* (libmpv properties), `null` when it has none; nothing is converted.
|
|
4875
|
+
*
|
|
4876
|
+
* @see {@link Player.stats}
|
|
4877
|
+
* @since 1.0.0
|
|
4878
|
+
* @category Playback
|
|
4123
4879
|
*/
|
|
4124
4880
|
export interface PlaybackStats {
|
|
4125
4881
|
/**
|
|
@@ -4234,15 +4990,90 @@ export interface PlaybackStats {
|
|
|
4234
4990
|
* void plugin.osd.show("Cannot play: {}", [(error as Error).message]);
|
|
4235
4991
|
* }
|
|
4236
4992
|
* ```
|
|
4993
|
+
*
|
|
4994
|
+
* @remarks
|
|
4995
|
+
* `plugin.player` is the player's one source: what is open, its phase and
|
|
4996
|
+
* position, and the controls. `state` is a snapshot the SDK keeps current
|
|
4997
|
+
* from the player's events (with `events`) and refreshes after this
|
|
4998
|
+
* plugin's own calls; `onChange` runs after it changed. Every control is
|
|
4999
|
+
* conditional: it acts on the state the plugin last saw and fails with
|
|
5000
|
+
* `stale` when the source changed meanwhile: another file, a close, or any
|
|
5001
|
+
* pause, seek or speed change by the user or another plugin (see
|
|
5002
|
+
* {@link Conditional}). The player keeps the source when a plugin stops;
|
|
5003
|
+
* only the plugin's own handles (a stream, its sequence provider, its
|
|
5004
|
+
* output device) are withdrawn.
|
|
5005
|
+
*
|
|
5006
|
+
* @capability `playback.read` to read; `playback.control` to control;
|
|
5007
|
+
* `events` to follow changes made elsewhere; `producer.av` for
|
|
5008
|
+
* {@link Player.openStream}.
|
|
5009
|
+
*
|
|
5010
|
+
* @see [Playback state, controls and sources](../../docs/concepts/playback.md), [Control playback](../../docs/how-to/control-playback.md)
|
|
5011
|
+
* @since 1.0.0
|
|
5012
|
+
* @category Playback
|
|
4237
5013
|
*/
|
|
4238
5014
|
export interface Player {
|
|
4239
5015
|
/** Latest state; always newer than anything delivered before. */
|
|
4240
5016
|
readonly state: PlayerState;
|
|
4241
|
-
/**
|
|
5017
|
+
/**
|
|
5018
|
+
* The state changed (needs `events` for outside changes).
|
|
5019
|
+
*
|
|
5020
|
+
* @capability events (for changes made elsewhere; without it the listener
|
|
5021
|
+
* runs only after this plugin's own calls)
|
|
5022
|
+
*
|
|
5023
|
+
* @example Show the position while it plays
|
|
5024
|
+
* The OSD block shows the position, updated as playback moves. The SDK
|
|
5025
|
+
* updates `player.state` from the player's events and then calls the
|
|
5026
|
+
* listener with the new snapshot; the handler only reads it.
|
|
5027
|
+
* ```ts
|
|
5028
|
+
* const info = plugin.osd.panel();
|
|
5029
|
+
* plugin.player.onChange((state) => {
|
|
5030
|
+
* // `position` is null while nothing is open.
|
|
5031
|
+
* info.set(state.position === null ? null : "{} s", [state.position?.toFixed(0) ?? ""]);
|
|
5032
|
+
* });
|
|
5033
|
+
* ```
|
|
5034
|
+
*
|
|
5035
|
+
* @since 1.0.0
|
|
5036
|
+
*/
|
|
4242
5037
|
onChange(listener: (state: PlayerState) => void): Disposable;
|
|
4243
|
-
/**
|
|
5038
|
+
/**
|
|
5039
|
+
* Read the state from the player now (`playback.read`).
|
|
5040
|
+
*
|
|
5041
|
+
* @capability playback.read
|
|
5042
|
+
* @throws `capability` (rejects) The manifest does not declare
|
|
5043
|
+
* `playback.read`.
|
|
5044
|
+
*
|
|
5045
|
+
* @example Read the state before deciding
|
|
5046
|
+
* A command reads the newest state from the player before acting, even
|
|
5047
|
+
* without `events`. `refresh` asks the player and resolves with the new
|
|
5048
|
+
* snapshot; `player.state` holds it afterwards.
|
|
5049
|
+
* ```ts
|
|
5050
|
+
* const state = await plugin.player.refresh();
|
|
5051
|
+
* if (state.phase === "ready") await plugin.osd.show("{} s left", [((state.duration ?? 0) - (state.position ?? 0)).toFixed(0)]);
|
|
5052
|
+
* ```
|
|
5053
|
+
*
|
|
5054
|
+
* @since 1.0.0
|
|
5055
|
+
*/
|
|
4244
5056
|
refresh(): Promise<PlayerState>;
|
|
4245
|
-
/**
|
|
5057
|
+
/**
|
|
5058
|
+
* True if nothing changed the source since `state` was observed.
|
|
5059
|
+
*
|
|
5060
|
+
* @capability None.
|
|
5061
|
+
*
|
|
5062
|
+
* @example Drop a result that arrives after the file changed
|
|
5063
|
+
* A slow lookup for the current file is shown only if that file is still
|
|
5064
|
+
* open. `isCurrent` compares the snapshot taken before the work with the
|
|
5065
|
+
* SDK's newest state; it is false once the source changed in any way:
|
|
5066
|
+
* another file, a close, or a pause, seek or speed change by anyone.
|
|
5067
|
+
* ```ts
|
|
5068
|
+
* declare function lookUpTitle(path: string): Promise<string>;
|
|
5069
|
+
* // ---cut---
|
|
5070
|
+
* const seen = plugin.player.state;
|
|
5071
|
+
* const title = await lookUpTitle(seen.source ?? "");
|
|
5072
|
+
* if (plugin.player.isCurrent(seen)) await plugin.osd.show("{}", [title]); // still the same file
|
|
5073
|
+
* ```
|
|
5074
|
+
*
|
|
5075
|
+
* @since 1.0.0
|
|
5076
|
+
*/
|
|
4246
5077
|
isCurrent(state: PlayerState): boolean;
|
|
4247
5078
|
/**
|
|
4248
5079
|
* Open a path or URL, replacing the current source (unless `ifCurrent` no
|
|
@@ -4253,6 +5084,37 @@ export interface Player {
|
|
|
4253
5084
|
* {@link Resolver} are resolved first, and so are local files of an
|
|
4254
5085
|
* extension a file resolver claims (the source is then a stream that
|
|
4255
5086
|
* plugin generates; `paused` reaches it).
|
|
5087
|
+
*
|
|
5088
|
+
* @capability playback.control
|
|
5089
|
+
* @throws `stale` (rejects) `ifCurrent` no longer holds; nothing was opened.
|
|
5090
|
+
* @throws `replaced` (rejects) Accepted, then another source took over
|
|
5091
|
+
* before it finished (for example while a URL was still being resolved).
|
|
5092
|
+
* @throws `invalid` (rejects) An empty `source`.
|
|
5093
|
+
* @throws `aborted` (rejects) `signal` aborted (an `AbortError`); the
|
|
5094
|
+
* source is closed if it was opened.
|
|
5095
|
+
* @throws `busy` (rejects) The address needs a resolver plugin and the
|
|
5096
|
+
* player is already resolving 32 addresses.
|
|
5097
|
+
* @throws `backend` (rejects) A resolver plugin handled the address or
|
|
5098
|
+
* file and failed (its message), or the resolution was cancelled or
|
|
5099
|
+
* expired.
|
|
5100
|
+
* @throws `unsupported` (rejects) The player refused what a resolver
|
|
5101
|
+
* returned (for example split streams with different headers).
|
|
5102
|
+
* @throws `unavailable` (rejects) The stream a file resolver prepared was
|
|
5103
|
+
* not ready to play.
|
|
5104
|
+
* @throws `capability` (rejects) The manifest does not declare
|
|
5105
|
+
* `playback.control`.
|
|
5106
|
+
*
|
|
5107
|
+
* @example Open a file paused
|
|
5108
|
+
* The file opens at its start without playing. `open` resolves once the
|
|
5109
|
+
* player accepted the load and returns a {@link Media} handle for that
|
|
5110
|
+
* source; `ready` then waits until it can play (here: until it is loaded
|
|
5111
|
+
* and paused).
|
|
5112
|
+
* ```ts
|
|
5113
|
+
* const media = await plugin.player.open("/Movies/clip.mp4", { paused: true });
|
|
5114
|
+
* await media.ready(); // rejects `failed` when the file cannot be loaded
|
|
5115
|
+
* ```
|
|
5116
|
+
*
|
|
5117
|
+
* @since 1.0.0
|
|
4256
5118
|
*/
|
|
4257
5119
|
open(source: string, options?: {
|
|
4258
5120
|
paused?: boolean;
|
|
@@ -4290,32 +5152,219 @@ export interface Player {
|
|
|
4290
5152
|
* // `error` is null when the stream stopped normally.
|
|
4291
5153
|
* stream.onStop((error) => error && plugin.osd.show(error.message));
|
|
4292
5154
|
* ```
|
|
5155
|
+
*
|
|
5156
|
+
* @capability producer.av and playback.control
|
|
5157
|
+
* @throws `timeout` (rejects) A {@link StreamStartError}: the stream did not
|
|
5158
|
+
* start in time.
|
|
5159
|
+
* @throws `stream` (rejects) The player failed to start the stream.
|
|
5160
|
+
* @throws `device` (rejects) The audio device was not ready.
|
|
5161
|
+
* @throws `replaced` (rejects) Another source replaced it, or it was closed,
|
|
5162
|
+
* before it ran.
|
|
5163
|
+
* @throws `invalid` (rejects) Bad options (a size or a frame function).
|
|
5164
|
+
* @throws `aborted` (rejects) `signal` aborted, or the scope was disposed,
|
|
5165
|
+
* while the stream was starting (an `AbortError`).
|
|
5166
|
+
* @since 1.0.0
|
|
4293
5167
|
*/
|
|
4294
5168
|
openStream(options: StreamOptions): Promise<Stream>;
|
|
4295
|
-
/**
|
|
5169
|
+
/**
|
|
5170
|
+
* Pause or resume.
|
|
5171
|
+
*
|
|
5172
|
+
* @capability playback.control
|
|
5173
|
+
* @throws `stale` (rejects) The source changed since the state the call is
|
|
5174
|
+
* based on (another file, a close, or any pause, seek
|
|
5175
|
+
* or speed change by the user or another plugin); nothing was done.
|
|
5176
|
+
* @throws `invalid` (rejects) Nothing is open.
|
|
5177
|
+
* @throws `busy` (rejects) A generated stream is still handling a previous
|
|
5178
|
+
* control.
|
|
5179
|
+
* @throws `capability` (rejects) The manifest does not declare
|
|
5180
|
+
* `playback.control`.
|
|
5181
|
+
*
|
|
5182
|
+
* @example Pause, but only the file the user was watching
|
|
5183
|
+
* A "pause on lock" helper pauses the current file and does nothing when
|
|
5184
|
+
* another file was opened in between. The call is conditional on
|
|
5185
|
+
* `player.state` by default; `stale` means the source changed and the
|
|
5186
|
+
* pause was not applied, which is the intended outcome here.
|
|
5187
|
+
* ```ts
|
|
5188
|
+
* try {
|
|
5189
|
+
* await plugin.player.setPaused(true);
|
|
5190
|
+
* } catch (error) {
|
|
5191
|
+
* if ((error as PluginError).code !== "stale") throw error; // another file now
|
|
5192
|
+
* }
|
|
5193
|
+
* ```
|
|
5194
|
+
*
|
|
5195
|
+
* @since 1.0.0
|
|
5196
|
+
*/
|
|
4296
5197
|
setPaused(paused: boolean, options?: Conditional): Promise<void>;
|
|
4297
|
-
/**
|
|
5198
|
+
/**
|
|
5199
|
+
* Jump to `seconds`.
|
|
5200
|
+
*
|
|
5201
|
+
* @capability playback.control
|
|
5202
|
+
* @throws `stale` (rejects) The source changed since the state the call is
|
|
5203
|
+
* based on (another file, a close, or any pause, seek
|
|
5204
|
+
* or speed change by the user or another plugin); nothing was done.
|
|
5205
|
+
* @throws `invalid` (rejects) Nothing is open, or `seconds` is negative or
|
|
5206
|
+
* not finite.
|
|
5207
|
+
* @throws `unsupported` (rejects) The source is a generated stream, which
|
|
5208
|
+
* cannot seek.
|
|
5209
|
+
* @throws `busy` (rejects) A generated stream is still handling a previous
|
|
5210
|
+
* control.
|
|
5211
|
+
* @throws `capability` (rejects) The manifest does not declare
|
|
5212
|
+
* `playback.control`.
|
|
5213
|
+
*
|
|
5214
|
+
* @example Jump back ten seconds
|
|
5215
|
+
* Playback jumps ten seconds back, never before the start. The target is
|
|
5216
|
+
* computed from the snapshot the call is conditional on, so if the user
|
|
5217
|
+
* opened another file meanwhile the seek fails with `stale` instead of
|
|
5218
|
+
* landing in the wrong file.
|
|
5219
|
+
* ```ts
|
|
5220
|
+
* const { position } = plugin.player.state;
|
|
5221
|
+
* if (position !== null && plugin.player.state.can.seek)
|
|
5222
|
+
* await plugin.player.seek(Math.max(0, position - 10)); // seconds
|
|
5223
|
+
* ```
|
|
5224
|
+
*
|
|
5225
|
+
* @since 1.0.0
|
|
5226
|
+
*/
|
|
4298
5227
|
seek(seconds: number, options?: Conditional): Promise<void>;
|
|
4299
|
-
/**
|
|
5228
|
+
/**
|
|
5229
|
+
* Set the playback speed.
|
|
5230
|
+
*
|
|
5231
|
+
* @capability playback.control
|
|
5232
|
+
* @throws `stale` (rejects) The source changed since the state the call is
|
|
5233
|
+
* based on (another file, a close, or any pause, seek
|
|
5234
|
+
* or speed change by the user or another plugin); nothing was done.
|
|
5235
|
+
* @throws `invalid` (rejects) Nothing is open, or `rate` is outside
|
|
5236
|
+
* 0.05–10.
|
|
5237
|
+
* @throws `unsupported` (rejects) The source is a generated stream, which
|
|
5238
|
+
* cannot change speed.
|
|
5239
|
+
* @throws `busy` (rejects) A generated stream is still handling a previous
|
|
5240
|
+
* control.
|
|
5241
|
+
* @throws `capability` (rejects) The manifest does not declare
|
|
5242
|
+
* `playback.control`.
|
|
5243
|
+
*
|
|
5244
|
+
* @example Play at one and a half times the speed
|
|
5245
|
+
* Playback speeds up to 1.5×, with the speed on the OSD. `setRate` sets
|
|
5246
|
+
* the speed directly (the player's own keys step it by 0.1 within
|
|
5247
|
+
* 0.25–4); `state.can.rate` is false for sources that cannot change speed.
|
|
5248
|
+
* ```ts
|
|
5249
|
+
* if (plugin.player.state.can.rate) {
|
|
5250
|
+
* await plugin.player.setRate(1.5);
|
|
5251
|
+
* await plugin.osd.show("Speed {}×", [1.5]);
|
|
5252
|
+
* }
|
|
5253
|
+
* ```
|
|
5254
|
+
*
|
|
5255
|
+
* @since 1.0.0
|
|
5256
|
+
*/
|
|
4300
5257
|
setRate(rate: number, options?: Conditional): Promise<void>;
|
|
4301
|
-
/**
|
|
5258
|
+
/**
|
|
5259
|
+
* Play from the start (the "replay" after the end).
|
|
5260
|
+
*
|
|
5261
|
+
* @capability playback.control
|
|
5262
|
+
* @throws `stale` (rejects) The source changed since the state the call is
|
|
5263
|
+
* based on (another file, a close, or any pause, seek
|
|
5264
|
+
* or speed change by the user or another plugin); nothing was done.
|
|
5265
|
+
* @throws `invalid` (rejects) Nothing is open.
|
|
5266
|
+
* @throws `unsupported` (rejects) The source is a generated stream, which
|
|
5267
|
+
* cannot restart.
|
|
5268
|
+
* @throws `capability` (rejects) The manifest does not declare
|
|
5269
|
+
* `playback.control`.
|
|
5270
|
+
*
|
|
5271
|
+
* @example Replay a file after it ended
|
|
5272
|
+
* A Replay button starts the file again from its start once it has ended.
|
|
5273
|
+
* `restart` plays the current source from the beginning and keeps it the
|
|
5274
|
+
* same source, so a {@link Media} handle stays current.
|
|
5275
|
+
* ```ts
|
|
5276
|
+
* if (plugin.player.state.phase === "ended") await plugin.player.restart();
|
|
5277
|
+
* ```
|
|
5278
|
+
*
|
|
5279
|
+
* @since 1.0.0
|
|
5280
|
+
*/
|
|
4302
5281
|
restart(options?: Conditional): Promise<void>;
|
|
4303
|
-
/**
|
|
5282
|
+
/**
|
|
5283
|
+
* Close the current source.
|
|
5284
|
+
*
|
|
5285
|
+
* @capability playback.control
|
|
5286
|
+
* @throws `stale` (rejects) The source changed since the state the call is
|
|
5287
|
+
* based on (another file, a close, or any pause, seek
|
|
5288
|
+
* or speed change by the user or another plugin); nothing was done.
|
|
5289
|
+
* @throws `capability` (rejects) The manifest does not declare
|
|
5290
|
+
* `playback.control`.
|
|
5291
|
+
*
|
|
5292
|
+
* @example Close the file when the sleep timer runs out
|
|
5293
|
+
* After 30 minutes the open file is closed, unless the user opened another
|
|
5294
|
+
* one meanwhile. The snapshot taken when the timer started is passed as
|
|
5295
|
+
* `ifCurrent`, so the close applies only to that source; `stale` means it
|
|
5296
|
+
* was replaced.
|
|
5297
|
+
* ```ts
|
|
5298
|
+
* const seen = plugin.player.state;
|
|
5299
|
+
* setTimeout(() => {
|
|
5300
|
+
* // Only the source that was open when the timer started.
|
|
5301
|
+
* plugin.player.close({ ifCurrent: seen }).catch((error: PluginError) => {
|
|
5302
|
+
* if (error.code !== "stale") plugin.osd.show("Could not close: {}", [error.code]);
|
|
5303
|
+
* });
|
|
5304
|
+
* }, 30 * 60 * 1000);
|
|
5305
|
+
* ```
|
|
5306
|
+
*
|
|
5307
|
+
* @since 1.0.0
|
|
5308
|
+
*/
|
|
4304
5309
|
close(options?: Conditional): Promise<void>;
|
|
4305
|
-
/**
|
|
5310
|
+
/**
|
|
5311
|
+
* Start a drag gesture from the current state.
|
|
5312
|
+
*
|
|
5313
|
+
* @capability playback.control
|
|
5314
|
+
*
|
|
5315
|
+
* @example Start a gesture when the user grabs the slider
|
|
5316
|
+
* A drag on the plugin's seek slider becomes one gesture whose seeks never
|
|
5317
|
+
* pile up. `scrub()` captures the current state; the gesture's seeks are
|
|
5318
|
+
* conditional on it and on each other, and it ends on its own when
|
|
5319
|
+
* someone else changes the source.
|
|
5320
|
+
* ```ts
|
|
5321
|
+
* declare const bar: Panel;
|
|
5322
|
+
* // ---cut---
|
|
5323
|
+
* let scrub: Scrub | null = null;
|
|
5324
|
+
* bar.on("dragStart", () => (scrub = plugin.player.scrub()));
|
|
5325
|
+
* bar.on("dragEnd", () => scrub?.end());
|
|
5326
|
+
* ```
|
|
5327
|
+
*
|
|
5328
|
+
* @see [Keep a slider responsive while dragging](../../docs/how-to/slider.md)
|
|
5329
|
+
* @since 1.0.0
|
|
5330
|
+
*/
|
|
4306
5331
|
scrub(): Scrub;
|
|
4307
5332
|
/**
|
|
4308
5333
|
* Device output; last write wins and volume is applied before mute.
|
|
4309
5334
|
* Resolves `true` when this request was applied, `false` if superseded.
|
|
4310
5335
|
* `volume` is 0–200.
|
|
5336
|
+
*
|
|
5337
|
+
* @capability playback.control
|
|
5338
|
+
* @throws `invalid` (rejects) `volume` outside 0–200, or `muted` not a
|
|
5339
|
+
* boolean.
|
|
5340
|
+
* @throws `capability` (rejects) The manifest does not declare
|
|
5341
|
+
* `playback.control`.
|
|
5342
|
+
*
|
|
5343
|
+
* @example Duck the volume during an announcement
|
|
5344
|
+
* The volume drops to 30 while the plugin's announcement plays, then
|
|
5345
|
+
* returns. Writes are "latest wins": the promise resolves `true` when this
|
|
5346
|
+
* write was applied and `false` when a newer one replaced it first, which
|
|
5347
|
+
* is not an error.
|
|
5348
|
+
* ```ts
|
|
5349
|
+
* const before = plugin.player.state.volume; // 0–200
|
|
5350
|
+
* await plugin.player.setOutput({ volume: 30 });
|
|
5351
|
+
* // … the announcement …
|
|
5352
|
+
* await plugin.player.setOutput({ volume: before });
|
|
5353
|
+
* ```
|
|
5354
|
+
*
|
|
5355
|
+
* @since 1.0.0
|
|
4311
5356
|
*/
|
|
4312
5357
|
setOutput(output: {
|
|
4313
5358
|
volume?: number;
|
|
4314
5359
|
muted?: boolean;
|
|
4315
5360
|
}): Promise<boolean>;
|
|
4316
5361
|
/**
|
|
4317
|
-
* Become the sequence provider
|
|
4318
|
-
*
|
|
5362
|
+
* Become the sequence provider, first come first served: while another
|
|
5363
|
+
* plugin holds it (the built-in playlist registers at activation), this
|
|
5364
|
+
* one is refused with `conflict` (`sequence-taken`). That does not fail
|
|
5365
|
+
* activation: the player shows a warning ({@link PluginInfo.warnings});
|
|
5366
|
+
* a refusal after activation goes to the scope's `onError`. Dispose to
|
|
5367
|
+
* stop.
|
|
4319
5368
|
*
|
|
4320
5369
|
* @example Drive the player's Next and Previous from a playlist
|
|
4321
5370
|
* The player's Next and Previous items and keys play the plugin's list.
|
|
@@ -4339,15 +5388,82 @@ export interface Player {
|
|
|
4339
5388
|
* canPrevious: () => index > 0,
|
|
4340
5389
|
* });
|
|
4341
5390
|
* ```
|
|
5391
|
+
*
|
|
5392
|
+
* @capability playback.control
|
|
5393
|
+
* @since 1.0.0
|
|
4342
5394
|
*/
|
|
4343
5395
|
provideSequence(provider: SequenceProvider): Disposable;
|
|
4344
|
-
/**
|
|
5396
|
+
/**
|
|
5397
|
+
* Ask the provider to go forward / back. Rejects `unavailable` when it cannot.
|
|
5398
|
+
*
|
|
5399
|
+
* @capability playback.control
|
|
5400
|
+
* @throws `unavailable` (rejects) There is no sequence provider, or it has
|
|
5401
|
+
* no next item.
|
|
5402
|
+
* @throws `capability` (rejects) The manifest does not declare
|
|
5403
|
+
* `playback.control`.
|
|
5404
|
+
*
|
|
5405
|
+
* @example Skip to the next item from a plugin button
|
|
5406
|
+
* The plugin's own Next button plays the next item of whichever plugin
|
|
5407
|
+
* provides the sequence (the playlist, usually). `next` asks the
|
|
5408
|
+
* provider, the same as the player's Next key; `unavailable` means there
|
|
5409
|
+
* is no next item, which the button treats as the end of the list.
|
|
5410
|
+
* ```ts
|
|
5411
|
+
* try {
|
|
5412
|
+
* await plugin.player.next(); // as the player's Next key does
|
|
5413
|
+
* } catch (error) {
|
|
5414
|
+
* if ((error as PluginError).code !== "unavailable") throw error;
|
|
5415
|
+
* await plugin.osd.show("End of the list");
|
|
5416
|
+
* }
|
|
5417
|
+
* ```
|
|
5418
|
+
*
|
|
5419
|
+
* @since 1.0.0
|
|
5420
|
+
*/
|
|
4345
5421
|
next(): Promise<void>;
|
|
4346
|
-
/**
|
|
5422
|
+
/**
|
|
5423
|
+
* Ask the provider to go back. Rejects `unavailable` when it cannot.
|
|
5424
|
+
*
|
|
5425
|
+
* @capability playback.control
|
|
5426
|
+
* @throws `unavailable` (rejects) There is no sequence provider, or it has
|
|
5427
|
+
* no previous item.
|
|
5428
|
+
* @throws `capability` (rejects) The manifest does not declare
|
|
5429
|
+
* `playback.control`.
|
|
5430
|
+
*
|
|
5431
|
+
* @example Go back to the previous item
|
|
5432
|
+
* The plugin's Previous button asks the sequence provider for the item
|
|
5433
|
+
* before the current one, like the player's PageUp. It is disabled while
|
|
5434
|
+
* `state.can.previous` is false, which the provider's `canPrevious`
|
|
5435
|
+
* decides.
|
|
5436
|
+
* ```ts
|
|
5437
|
+
* plugin.commands.add({
|
|
5438
|
+
* id: "previous",
|
|
5439
|
+
* title: "Previous item",
|
|
5440
|
+
* // The provider's `canPrevious`, as the player reports it.
|
|
5441
|
+
* enabled: () => plugin.player.state.can.previous,
|
|
5442
|
+
* run: () => plugin.player.previous(),
|
|
5443
|
+
* });
|
|
5444
|
+
* ```
|
|
5445
|
+
*
|
|
5446
|
+
* @since 1.0.0
|
|
5447
|
+
*/
|
|
4347
5448
|
previous(): Promise<void>;
|
|
4348
5449
|
/**
|
|
4349
5450
|
* Codec, size, frame counts and backend of the current playback.
|
|
4350
5451
|
* `playback.read`; `video` and `audio` are `null` without a source.
|
|
5452
|
+
*
|
|
5453
|
+
* @capability playback.read
|
|
5454
|
+
* @throws `capability` (rejects) The manifest does not declare
|
|
5455
|
+
* `playback.read`.
|
|
5456
|
+
*
|
|
5457
|
+
* @example Show the video codec and size
|
|
5458
|
+
* The OSD shows something like "h264 1920×1080". `stats` asks the player
|
|
5459
|
+
* for the backend's own values at that moment; `video` is `null` without
|
|
5460
|
+
* a video stream (nothing open, an audio file).
|
|
5461
|
+
* ```ts
|
|
5462
|
+
* const { video } = await plugin.player.stats();
|
|
5463
|
+
* if (video) await plugin.osd.show("{} {}×{}", [video.codec ?? "?", video.width ?? 0, video.height ?? 0]);
|
|
5464
|
+
* ```
|
|
5465
|
+
*
|
|
5466
|
+
* @since 1.0.0
|
|
4351
5467
|
*/
|
|
4352
5468
|
stats(): Promise<PlaybackStats>;
|
|
4353
5469
|
/**
|
|
@@ -4355,11 +5471,56 @@ export interface Player {
|
|
|
4355
5471
|
* unless `select: false`, select it. Resolves with the new track (`external`,
|
|
4356
5472
|
* not exportable). It stays until the source changes. Rejects `invalid` for a
|
|
4357
5473
|
* bad path, `unavailable` without ready media, `backend` when it cannot load.
|
|
5474
|
+
*
|
|
5475
|
+
* @capability playback.control
|
|
5476
|
+
* @throws `invalid` (rejects) Not an absolute path to a file.
|
|
5477
|
+
* @throws `unavailable` (rejects) No ready media.
|
|
5478
|
+
* @throws `backend` (rejects) The player could not load the file.
|
|
5479
|
+
* @throws `stale` (rejects) `ifCurrent` no longer holds.
|
|
5480
|
+
* @throws `capability` (rejects) The manifest does not declare
|
|
5481
|
+
* `playback.control`.
|
|
5482
|
+
*
|
|
5483
|
+
* @example Add a dubbed audio file and switch to it
|
|
5484
|
+
* A separate audio file plays with the film instead of its own sound.
|
|
5485
|
+
* `addAudioTrack` attaches the file to the current media and selects it;
|
|
5486
|
+
* the track stays until another source opens.
|
|
5487
|
+
* ```ts
|
|
5488
|
+
* declare const dubPath: string;
|
|
5489
|
+
* // ---cut---
|
|
5490
|
+
* const track = await plugin.player.addAudioTrack(dubPath); // selected too
|
|
5491
|
+
* await plugin.osd.show("Audio: {}", [track.title ?? dubPath]);
|
|
5492
|
+
* ```
|
|
5493
|
+
*
|
|
5494
|
+
* @since 1.0.0
|
|
4358
5495
|
*/
|
|
4359
5496
|
addAudioTrack(path: string, options?: {
|
|
4360
5497
|
select?: boolean;
|
|
4361
5498
|
} & Conditional): Promise<AudioTrack>;
|
|
4362
|
-
/**
|
|
5499
|
+
/**
|
|
5500
|
+
* Select one of `state.audioTracks` by id; `invalid` for an unknown id.
|
|
5501
|
+
*
|
|
5502
|
+
* @capability playback.control
|
|
5503
|
+
* @throws `invalid` (rejects) No track with this id.
|
|
5504
|
+
* @throws `unavailable` (rejects) No ready media.
|
|
5505
|
+
* @throws `stale` (rejects) The source changed since the state the call is
|
|
5506
|
+
* based on (another file, a close, or any pause, seek
|
|
5507
|
+
* or speed change by the user or another plugin).
|
|
5508
|
+
* @throws `capability` (rejects) The manifest does not declare
|
|
5509
|
+
* `playback.control`.
|
|
5510
|
+
*
|
|
5511
|
+
* @example Switch to the next audio track
|
|
5512
|
+
* Each press of the plugin's key moves to the next audio track of the
|
|
5513
|
+
* file. The tracks and the selected one are in `state.audioTracks`;
|
|
5514
|
+
* `selectAudioTrack` resolves with the new state.
|
|
5515
|
+
* ```ts
|
|
5516
|
+
* const audioTracks = plugin.player.state.audioTracks ?? []; // null: not known yet
|
|
5517
|
+
* const at = audioTracks.findIndex((track) => track.selected);
|
|
5518
|
+
* const next = audioTracks[(at + 1) % audioTracks.length];
|
|
5519
|
+
* if (next) await plugin.player.selectAudioTrack(next.id);
|
|
5520
|
+
* ```
|
|
5521
|
+
*
|
|
5522
|
+
* @since 1.0.0
|
|
5523
|
+
*/
|
|
4363
5524
|
selectAudioTrack(id: number, options?: Conditional): Promise<PlayerState>;
|
|
4364
5525
|
/**
|
|
4365
5526
|
* One frame or to the next / previous keyframe (pauses for frames). Rejects
|
|
@@ -4369,6 +5530,29 @@ export interface Player {
|
|
|
4369
5530
|
* Resolves once the new position is in effect, so `player.state.position`
|
|
4370
5531
|
* is current afterwards. One step at a time: a second call while one is
|
|
4371
5532
|
* in flight rejects `busy`.
|
|
5533
|
+
*
|
|
5534
|
+
* @capability playback.control
|
|
5535
|
+
* @throws `unsupported` (rejects) Nothing playable to step through (no
|
|
5536
|
+
* moving video: audio only, cover art, a generated stream), a backward
|
|
5537
|
+
* frame step on a source that cannot seek, or a keyframe step in either
|
|
5538
|
+
* direction on a source that cannot seek.
|
|
5539
|
+
* @throws `busy` (rejects) A step is still in flight.
|
|
5540
|
+
* @throws `stale` (rejects) The source changed since the state the call is
|
|
5541
|
+
* based on (another file, a close, or any pause, seek
|
|
5542
|
+
* or speed change by the user or another plugin).
|
|
5543
|
+
* @throws `capability` (rejects) The manifest does not declare
|
|
5544
|
+
* `playback.control`.
|
|
5545
|
+
*
|
|
5546
|
+
* @example Step one frame forward for a close look
|
|
5547
|
+
* The picture advances by one frame and playback pauses. `step` resolves
|
|
5548
|
+
* once the new position is in effect, so `state.position` is current
|
|
5549
|
+
* afterwards; a second step while one is running rejects with `busy`.
|
|
5550
|
+
* ```ts
|
|
5551
|
+
* await plugin.player.step("frame", "forward");
|
|
5552
|
+
* await plugin.osd.show("{} s", [plugin.player.state.position?.toFixed(3) ?? "?"]);
|
|
5553
|
+
* ```
|
|
5554
|
+
*
|
|
5555
|
+
* @since 1.0.0
|
|
4372
5556
|
*/
|
|
4373
5557
|
step(unit: "frame" | "keyframe", direction: "backward" | "forward", options?: Conditional): Promise<void>;
|
|
4374
5558
|
/**
|
|
@@ -4391,6 +5575,23 @@ export interface Player {
|
|
|
4391
5575
|
*
|
|
4392
5576
|
* Older players ignore `keyframes`: set `"minPlayerVersion": "0.5.1"` in
|
|
4393
5577
|
* the manifest when you rely on it.
|
|
5578
|
+
*
|
|
5579
|
+
* @capability playback.control
|
|
5580
|
+
* @throws `invalid` (rejects) `seconds` outside 0.1–3600 or not in tenths,
|
|
5581
|
+
* or `keyframes` not a boolean.
|
|
5582
|
+
* @throws `capability` (rejects) The manifest does not declare
|
|
5583
|
+
* `playback.control`.
|
|
5584
|
+
*
|
|
5585
|
+
* @example Make ← and → jump 10 seconds to keyframes
|
|
5586
|
+
* The arrow keys now jump 10 seconds and land on keyframes, which is fast
|
|
5587
|
+
* on large files. The newest call of any plugin wins and is withdrawn
|
|
5588
|
+
* when that plugin stops, so a plugin offering this as a setting applies
|
|
5589
|
+
* it on activation.
|
|
5590
|
+
* ```ts
|
|
5591
|
+
* await plugin.player.setStepSeek({ seconds: 10, keyframes: true });
|
|
5592
|
+
* ```
|
|
5593
|
+
*
|
|
5594
|
+
* @since 1.0.0
|
|
4394
5595
|
*/
|
|
4395
5596
|
setStepSeek(options: {
|
|
4396
5597
|
seconds: number;
|
|
@@ -4400,6 +5601,23 @@ export interface Player {
|
|
|
4400
5601
|
* The chapters of the current media (empty without media or chapters),
|
|
4401
5602
|
* read on demand; `sourceId` tells which source they belong to.
|
|
4402
5603
|
* `playback.read`.
|
|
5604
|
+
*
|
|
5605
|
+
* @capability playback.read
|
|
5606
|
+
* @throws `capability` (rejects) The manifest does not declare
|
|
5607
|
+
* `playback.read`.
|
|
5608
|
+
*
|
|
5609
|
+
* @example List the chapters of the open file
|
|
5610
|
+
* The plugin reads the chapter titles of the current file to show them in
|
|
5611
|
+
* its panel. `chapters` asks the player when called; `sourceId` says which
|
|
5612
|
+
* source they belong to, so a reply for a file that was replaced meanwhile
|
|
5613
|
+
* can be dropped.
|
|
5614
|
+
* ```ts
|
|
5615
|
+
* const { sourceId, chapters } = await plugin.player.chapters();
|
|
5616
|
+
* if (sourceId === plugin.player.state.sourceId) // still the same file
|
|
5617
|
+
* console.log(chapters.map((chapter) => chapter.title));
|
|
5618
|
+
* ```
|
|
5619
|
+
*
|
|
5620
|
+
* @since 1.0.0
|
|
4403
5621
|
*/
|
|
4404
5622
|
chapters(): Promise<{
|
|
4405
5623
|
/** The source the chapters belong to (compare with `state.sourceId`). */
|
|
@@ -4413,13 +5631,59 @@ export interface Player {
|
|
|
4413
5631
|
* stops it. After the last jump back `state.loop` becomes null.
|
|
4414
5632
|
* `invalid` out of range, `unsupported` for a source that cannot seek,
|
|
4415
5633
|
* `unavailable` without ready media. A new source clears it.
|
|
5634
|
+
*
|
|
5635
|
+
* @capability playback.control
|
|
5636
|
+
* @throws `invalid` (rejects) `a`/`b` out of range or `count` not a positive
|
|
5637
|
+
* integer.
|
|
5638
|
+
* @throws `unsupported` (rejects) The source cannot seek, or is a generated
|
|
5639
|
+
* stream.
|
|
5640
|
+
* @throws `unavailable` (rejects) No ready media.
|
|
5641
|
+
* @throws `stale` (rejects) The source changed since the state the call is
|
|
5642
|
+
* based on (another file, a close, or any pause, seek
|
|
5643
|
+
* or speed change by the user or another plugin).
|
|
5644
|
+
* @throws `capability` (rejects) The manifest does not declare
|
|
5645
|
+
* `playback.control`.
|
|
5646
|
+
*
|
|
5647
|
+
* @example Loop a passage three times
|
|
5648
|
+
* Playback repeats 1:00–1:30 three times, then goes on. The player jumps
|
|
5649
|
+
* back to `a` each time it reaches `b`; after the last repeat
|
|
5650
|
+
* `state.loop` becomes `null`, and a new source clears the loop.
|
|
5651
|
+
* ```ts
|
|
5652
|
+
* await plugin.player.setLoop({ a: 60, b: 90, count: 3 }); // seconds
|
|
5653
|
+
* ```
|
|
5654
|
+
*
|
|
5655
|
+
* @since 1.0.0
|
|
4416
5656
|
*/
|
|
4417
5657
|
setLoop(loop: {
|
|
4418
5658
|
a: number;
|
|
4419
5659
|
b: number;
|
|
4420
5660
|
count?: number;
|
|
4421
5661
|
} | null, options?: Conditional): Promise<void>;
|
|
4422
|
-
/**
|
|
5662
|
+
/**
|
|
5663
|
+
* The output devices present now (not the system default entry). `playback.read`.
|
|
5664
|
+
*
|
|
5665
|
+
* @capability playback.read
|
|
5666
|
+
* @throws `capability` (rejects) The manifest does not declare
|
|
5667
|
+
* `playback.read`.
|
|
5668
|
+
*
|
|
5669
|
+
* @example Offer the audio output devices in a menu
|
|
5670
|
+
* A submenu lists the output devices present now, with a radio mark on the
|
|
5671
|
+
* one in use. `audioDevices` asks the player when called (devices come and
|
|
5672
|
+
* go), and `state.audioOutput` tells which one is active.
|
|
5673
|
+
* ```ts
|
|
5674
|
+
* // The devices present now; build the menu again when they change.
|
|
5675
|
+
* for (const device of await plugin.player.audioDevices())
|
|
5676
|
+
* plugin.commands.add({
|
|
5677
|
+
* id: `device.${device.id}`,
|
|
5678
|
+
* path: ["@audio", "Output", device.name],
|
|
5679
|
+
* radio: true,
|
|
5680
|
+
* checked: () => plugin.player.state.audioOutput?.active === device.id,
|
|
5681
|
+
* run: () => plugin.player.setAudioDevice(device.id),
|
|
5682
|
+
* });
|
|
5683
|
+
* ```
|
|
5684
|
+
*
|
|
5685
|
+
* @since 1.0.0
|
|
5686
|
+
*/
|
|
4423
5687
|
audioDevices(): Promise<AudioDevice[]>;
|
|
4424
5688
|
/**
|
|
4425
5689
|
* Play on this device while it is present, else on the system default
|
|
@@ -4429,6 +5693,23 @@ export interface Player {
|
|
|
4429
5693
|
* Unknown ids are accepted (`state.audioOutput.active` stays `null`); a
|
|
4430
5694
|
* malformed id is `invalid`. Reverts to the default when this plugin
|
|
4431
5695
|
* stops; save the choice yourself to restore it on activation.
|
|
5696
|
+
*
|
|
5697
|
+
* @capability playback.control
|
|
5698
|
+
* @throws `invalid` (rejects) A malformed id.
|
|
5699
|
+
* @throws `capability` (rejects) The manifest does not declare
|
|
5700
|
+
* `playback.control`.
|
|
5701
|
+
*
|
|
5702
|
+
* @example Restore the user's output device on start
|
|
5703
|
+
* The plugin plays on the device the user picked last time, or on the
|
|
5704
|
+
* system default when it is not connected. The choice lasts only for
|
|
5705
|
+
* this run and is withdrawn when the plugin stops, so the plugin saves
|
|
5706
|
+
* it itself and sets it again on activation.
|
|
5707
|
+
* ```ts
|
|
5708
|
+
* const store = plugin.storage.json("output", { defaults: { device: null as string | null } });
|
|
5709
|
+
* await plugin.player.setAudioDevice(store.value.device); // null: the system default
|
|
5710
|
+
* ```
|
|
5711
|
+
*
|
|
5712
|
+
* @since 1.0.0
|
|
4432
5713
|
*/
|
|
4433
5714
|
setAudioDevice(id: string | null): Promise<void>;
|
|
4434
5715
|
}
|
|
@@ -4453,9 +5734,45 @@ export interface Player {
|
|
|
4453
5734
|
* (`plugin.window.onChange`), which also changes when the user toggles it.
|
|
4454
5735
|
* Players before 0.5.2 reject it with `invalid`: set
|
|
4455
5736
|
* `"minPlayerVersion": "0.5.2"` when you use it.
|
|
5737
|
+
*
|
|
5738
|
+
* @see {@link Host.run}
|
|
5739
|
+
* @since 1.0.0
|
|
5740
|
+
* @category Commands
|
|
4456
5741
|
*/
|
|
4457
5742
|
export type HostAction = "play-pause" | "stop" | "previous" | "next" | "frame-back" | "frame-forward" | "seek-backward" | "seek-forward" | "rate-down" | "rate-up" | "rate-normal" | "volume-up" | "volume-down" | "mute" | "fullscreen";
|
|
4458
|
-
/**
|
|
5743
|
+
/**
|
|
5744
|
+
* `plugin.host`: the player's own core actions.
|
|
5745
|
+
*
|
|
5746
|
+
* @remarks
|
|
5747
|
+
* The player's core actions (play/pause, stop, step seeks, speed, volume,
|
|
5748
|
+
* fullscreen…) are commands of the player itself, `host/<action>` in the
|
|
5749
|
+
* binding table. Running them through `host.run` gives a plugin's own
|
|
5750
|
+
* buttons exactly the player's behaviour: the same checks, the same
|
|
5751
|
+
* accumulation of repeated presses and the same on-screen text, instead of
|
|
5752
|
+
* reimplementing them with {@link Player} calls.
|
|
5753
|
+
*
|
|
5754
|
+
* @capability commands
|
|
5755
|
+
*
|
|
5756
|
+
* @example Give a custom bar the player's own Stop and Mute
|
|
5757
|
+
* The plugin's bar buttons stop playback and mute exactly as the player's
|
|
5758
|
+
* own keys do, with the same on-screen text. Each callback runs a core
|
|
5759
|
+
* action; the player applies its checks and rejects with `unavailable`
|
|
5760
|
+
* while the action is disabled, which the bar ignores.
|
|
5761
|
+
* ```ts
|
|
5762
|
+
* declare const bar: Panel;
|
|
5763
|
+
* // ---cut---
|
|
5764
|
+
* const run = (action: HostAction) =>
|
|
5765
|
+
* plugin.host.run(action).catch((error: PluginError) => {
|
|
5766
|
+
* if (error.code !== "unavailable") throw error; // disabled right now
|
|
5767
|
+
* });
|
|
5768
|
+
* bar.on("stop", () => run("stop"));
|
|
5769
|
+
* bar.on("mute", () => run("mute"));
|
|
5770
|
+
* ```
|
|
5771
|
+
*
|
|
5772
|
+
* @see {@link HostAction}, {@link Player}
|
|
5773
|
+
* @since 1.0.0
|
|
5774
|
+
* @category Commands
|
|
5775
|
+
*/
|
|
4459
5776
|
export interface Host {
|
|
4460
5777
|
/**
|
|
4461
5778
|
* Run a core action exactly like its menu item or key: same checks, same
|
|
@@ -4482,6 +5799,14 @@ export interface Host {
|
|
|
4482
5799
|
* }
|
|
4483
5800
|
* });
|
|
4484
5801
|
* ```
|
|
5802
|
+
*
|
|
5803
|
+
* @capability commands
|
|
5804
|
+
* @throws `unavailable` (rejects) The action is disabled right now (nothing
|
|
5805
|
+
* open, no window for `fullscreen`).
|
|
5806
|
+
* @throws `invalid` (rejects) An unknown action, or `seconds` out of range.
|
|
5807
|
+
* @throws `capability` (rejects) The manifest does not declare `commands`.
|
|
5808
|
+
*
|
|
5809
|
+
* @since 1.0.0
|
|
4485
5810
|
*/
|
|
4486
5811
|
run(action: HostAction): Promise<void>;
|
|
4487
5812
|
/**
|
|
@@ -4511,6 +5836,12 @@ export interface Host {
|
|
|
4511
5836
|
* // ---cut---
|
|
4512
5837
|
* bar.on("forward30", () => plugin.host.run("seek-forward", { seconds: 30 }));
|
|
4513
5838
|
* ```
|
|
5839
|
+
*
|
|
5840
|
+
* @capability commands
|
|
5841
|
+
* @throws `unavailable` (rejects) The action is disabled right now (nothing
|
|
5842
|
+
* open, no window for `fullscreen`).
|
|
5843
|
+
* @throws `invalid` (rejects) An unknown action, or `seconds` out of range.
|
|
5844
|
+
* @throws `capability` (rejects) The manifest does not declare `commands`.
|
|
4514
5845
|
*/
|
|
4515
5846
|
run(action: "seek-backward" | "seek-forward", options: {
|
|
4516
5847
|
seconds: number;
|
|
@@ -4717,6 +6048,9 @@ export interface Osd {
|
|
|
4717
6048
|
* The state of a filter, display layer or render options handle.
|
|
4718
6049
|
* After `failed` nothing retries automatically until the next `set()`,
|
|
4719
6050
|
* `retry()` or a new source.
|
|
6051
|
+
*
|
|
6052
|
+
* @since 1.0.0
|
|
6053
|
+
* @category Processing
|
|
4720
6054
|
*/
|
|
4721
6055
|
export type FilterStatus =
|
|
4722
6056
|
/** No ready media; the value is kept and applied when media is ready. */
|
|
@@ -4792,6 +6126,18 @@ interface Filter<T> extends Disposable {
|
|
|
4792
6126
|
* picture.onStatus((s) => s.state === "failed" && plugin.osd.show(s.error.message));
|
|
4793
6127
|
* picture.set([{ kind: "eq", brightness: 0.1, contrast: 1.1, saturation: 1, gamma: 1 }]);
|
|
4794
6128
|
* ```
|
|
6129
|
+
*
|
|
6130
|
+
* @remarks
|
|
6131
|
+
* Members: `set(nodes)` records the desired chain (last write wins) and
|
|
6132
|
+
* sends it, one request at a time and only the newest; `status` and
|
|
6133
|
+
* `onStatus` report `waiting`, `applying`, `applied` or `failed`; `retry`
|
|
6134
|
+
* applies the current value again; `dispose` withdraws the chain. `set`
|
|
6135
|
+
* never throws: a refused chain shows as `failed` in `status`.
|
|
6136
|
+
*
|
|
6137
|
+
* @capability processing.video, playback.read and events
|
|
6138
|
+
* @see [Video and audio processing](../../docs/concepts/processing.md)
|
|
6139
|
+
* @since 1.0.0
|
|
6140
|
+
* @category Processing
|
|
4795
6141
|
*/
|
|
4796
6142
|
export type VideoFilter = Filter<VideoNode[]>;
|
|
4797
6143
|
/**
|
|
@@ -4811,6 +6157,16 @@ export type VideoFilter = Filter<VideoNode[]>;
|
|
|
4811
6157
|
* const audio = plugin.audio.filter("sound");
|
|
4812
6158
|
* audio.set({ nodes: [{ kind: "volume", gainDb: -6 }], delayMs: 120 }); // 10 ms steps
|
|
4813
6159
|
* ```
|
|
6160
|
+
*
|
|
6161
|
+
* @remarks
|
|
6162
|
+
* The same members and rules as {@link VideoFilter}: `set` records the
|
|
6163
|
+
* desired value and never throws, `status` and `onStatus` report the
|
|
6164
|
+
* result, `retry` applies it again, `dispose` withdraws it.
|
|
6165
|
+
*
|
|
6166
|
+
* @capability processing.audio, playback.read and events
|
|
6167
|
+
* @see [Video and audio processing](../../docs/concepts/processing.md)
|
|
6168
|
+
* @since 1.0.0
|
|
6169
|
+
* @category Processing
|
|
4814
6170
|
*/
|
|
4815
6171
|
export type AudioFilter = Filter<{
|
|
4816
6172
|
/** The audio chain; `[]` for none. */
|
|
@@ -4821,6 +6177,9 @@ export type AudioFilter = Filter<{
|
|
|
4821
6177
|
/**
|
|
4822
6178
|
* How the video sits in the video area; not part of snapshots or exports.
|
|
4823
6179
|
* Defaults: `{ aspect: null, fit: "keep", zoom: 1, panX: 0, panY: 0 }`.
|
|
6180
|
+
*
|
|
6181
|
+
* @since 1.0.0
|
|
6182
|
+
* @category Processing
|
|
4824
6183
|
*/
|
|
4825
6184
|
export interface ViewSettings {
|
|
4826
6185
|
/** Display aspect override 0.25–4; `null` = as the source. */
|
|
@@ -4848,24 +6207,73 @@ export interface ViewSettings {
|
|
|
4848
6207
|
* The video fills the video area (cropped instead of letterboxed), then
|
|
4849
6208
|
* zooms to 150 % shifted a little to the right. The first plugin to call
|
|
4850
6209
|
* `view()` owns the display layer; each `set` merges into its settings,
|
|
4851
|
-
* which stay across sources until the plugin
|
|
6210
|
+
* which stay across sources until the plugin disposes the view or stops.
|
|
4852
6211
|
* ```ts
|
|
4853
6212
|
* const view = plugin.video.view();
|
|
4854
6213
|
* view.set({ fit: "fill" });
|
|
4855
6214
|
* view.set({ zoom: 1.5, panX: 0.1 }); // merged with the previous write
|
|
4856
6215
|
* ```
|
|
6216
|
+
*
|
|
6217
|
+
* @remarks
|
|
6218
|
+
* Not part of snapshots or exports: it changes how the picture sits in the
|
|
6219
|
+
* video area, not the picture. `set` merges into the current settings and
|
|
6220
|
+
* never throws: an out-of-range value or unknown field shows as `failed`
|
|
6221
|
+
* (`invalid`) in `status` and is reported to `onError`, without sending
|
|
6222
|
+
* anything.
|
|
6223
|
+
*
|
|
6224
|
+
* @capability processing.video, playback.read and events (like
|
|
6225
|
+
* {@link VideoFilter})
|
|
6226
|
+
* @see [Video and audio processing](../../docs/concepts/processing.md)
|
|
6227
|
+
* @since 1.0.0
|
|
6228
|
+
* @category Processing
|
|
4857
6229
|
*/
|
|
4858
6230
|
export interface VideoView extends Disposable {
|
|
4859
|
-
/**
|
|
6231
|
+
/**
|
|
6232
|
+
* Merged into the current settings; last write wins.
|
|
6233
|
+
*
|
|
6234
|
+
* @capability processing.video
|
|
6235
|
+
*
|
|
6236
|
+
* @example Stretch a 4:3 video to fill the window
|
|
6237
|
+
* A 4:3 video fills a 16:9 window, distorted, as the user asked. Each
|
|
6238
|
+
* `set` merges the given fields into this plugin's view settings; the
|
|
6239
|
+
* newest write wins, and the settings stay for later files until the
|
|
6240
|
+
* plugin disposes the view or stops.
|
|
6241
|
+
* ```ts
|
|
6242
|
+
* plugin.video.view().set({ fit: "stretch" });
|
|
6243
|
+
* ```
|
|
6244
|
+
*
|
|
6245
|
+
* @since 1.0.0
|
|
6246
|
+
*/
|
|
4860
6247
|
set(value: Partial<ViewSettings>): void;
|
|
4861
6248
|
/** Current status (the same four states as filters). */
|
|
4862
6249
|
readonly status: FilterStatus;
|
|
4863
|
-
/**
|
|
6250
|
+
/**
|
|
6251
|
+
* The status changed.
|
|
6252
|
+
*
|
|
6253
|
+
* @capability processing.video
|
|
6254
|
+
*
|
|
6255
|
+
* @example Tell the user another plugin owns the view
|
|
6256
|
+
* When another plugin already controls the display layer, the OSD says
|
|
6257
|
+
* so. The first plugin to call `view()` owns it; this plugin's handle
|
|
6258
|
+
* then reports `failed` with `conflict`, and tries again on its next `set`.
|
|
6259
|
+
* ```ts
|
|
6260
|
+
* const view = plugin.video.view();
|
|
6261
|
+
* view.onStatus((status) => {
|
|
6262
|
+
* if (status.state === "failed" && status.error.code === "conflict")
|
|
6263
|
+
* void plugin.osd.show("Another plugin controls the picture size");
|
|
6264
|
+
* });
|
|
6265
|
+
* ```
|
|
6266
|
+
*
|
|
6267
|
+
* @since 1.0.0
|
|
6268
|
+
*/
|
|
4864
6269
|
onStatus(listener: (status: FilterStatus) => void): Disposable;
|
|
4865
6270
|
}
|
|
4866
6271
|
/**
|
|
4867
6272
|
* GPU renderer options; not part of snapshots or exports.
|
|
4868
6273
|
* Defaults: `{ toneMapping: "auto", targetPeak: 203, peakDetect: true, shaders: [], interpolation: false }`.
|
|
6274
|
+
*
|
|
6275
|
+
* @since 1.0.0
|
|
6276
|
+
* @category Processing
|
|
4869
6277
|
*/
|
|
4870
6278
|
export interface RenderSettings {
|
|
4871
6279
|
/** HDR → SDR: "auto" maps HDR sources only; "off" clips without a curve. */
|
|
@@ -4904,19 +6312,69 @@ export interface RenderSettings {
|
|
|
4904
6312
|
* });
|
|
4905
6313
|
* render.set({ toneMapping: "bt.2390", targetPeak: 250 }); // nits
|
|
4906
6314
|
* ```
|
|
6315
|
+
*
|
|
6316
|
+
* @remarks
|
|
6317
|
+
* GPU renderer options (tone mapping, peak detection, shaders,
|
|
6318
|
+
* interpolation). Only the GPU renderer has them, which today means macOS;
|
|
6319
|
+
* under software rendering the handle is `failed` with `unsupported` after
|
|
6320
|
+
* the player's first answer to `render()` (reported to `onError` too), and so is
|
|
6321
|
+
* every `set`. Single owner, like {@link VideoView}. `set` never throws; the
|
|
6322
|
+
* result shows in `status`.
|
|
6323
|
+
*
|
|
6324
|
+
* @capability processing.video, playback.read and events (like
|
|
6325
|
+
* {@link VideoFilter})
|
|
6326
|
+
* @see [Video and audio processing](../../docs/concepts/processing.md)
|
|
6327
|
+
* @since 1.0.0
|
|
6328
|
+
* @category Processing
|
|
4907
6329
|
*/
|
|
4908
6330
|
export interface RenderOptions extends Disposable {
|
|
4909
|
-
/**
|
|
6331
|
+
/**
|
|
6332
|
+
* Merged into the current settings; last write wins.
|
|
6333
|
+
*
|
|
6334
|
+
* @capability processing.video
|
|
6335
|
+
*
|
|
6336
|
+
* @example Turn on motion interpolation
|
|
6337
|
+
* Motion becomes smoother on the GPU renderer. `set` merges the field into
|
|
6338
|
+
* the plugin's render options; under software rendering the status turns
|
|
6339
|
+
* `failed` with `unsupported` instead.
|
|
6340
|
+
* ```ts
|
|
6341
|
+
* plugin.video.render().set({ interpolation: true });
|
|
6342
|
+
* ```
|
|
6343
|
+
*
|
|
6344
|
+
* @since 1.0.0
|
|
6345
|
+
*/
|
|
4910
6346
|
set(value: Partial<RenderSettings>): void;
|
|
4911
6347
|
/**
|
|
4912
6348
|
* `failed` with `unsupported` while the player renders in software;
|
|
4913
6349
|
* `failed` with `failed` when mpv cannot compile a shader (pushed later).
|
|
4914
6350
|
*/
|
|
4915
6351
|
readonly status: FilterStatus;
|
|
4916
|
-
/**
|
|
6352
|
+
/**
|
|
6353
|
+
* The status changed (including later pushes such as shader compile errors).
|
|
6354
|
+
*
|
|
6355
|
+
* @capability processing.video
|
|
6356
|
+
*
|
|
6357
|
+
* @example Report a shader that does not compile
|
|
6358
|
+
* The OSD shows mpv's message when a shader the plugin set fails to
|
|
6359
|
+
* compile. The player pushes that failure after the `set` was accepted, so
|
|
6360
|
+
* it arrives through `onStatus`, not as a rejected call.
|
|
6361
|
+
* ```ts
|
|
6362
|
+
* const render = plugin.video.render();
|
|
6363
|
+
* render.onStatus((status) => {
|
|
6364
|
+
* if (status.state === "failed") void plugin.osd.show("Render: {}", [status.error.message]);
|
|
6365
|
+
* });
|
|
6366
|
+
* ```
|
|
6367
|
+
*
|
|
6368
|
+
* @since 1.0.0
|
|
6369
|
+
*/
|
|
4917
6370
|
onStatus(listener: (status: FilterStatus) => void): Disposable;
|
|
4918
6371
|
}
|
|
4919
|
-
/**
|
|
6372
|
+
/**
|
|
6373
|
+
* A subtitle track of the current source.
|
|
6374
|
+
*
|
|
6375
|
+
* @since 1.0.0
|
|
6376
|
+
* @category Subtitles
|
|
6377
|
+
*/
|
|
4920
6378
|
export interface SubtitleTrack {
|
|
4921
6379
|
/** Stable while the track exists; compare tracks by id. */
|
|
4922
6380
|
id: string;
|
|
@@ -4944,7 +6402,13 @@ export interface SubtitleTrack {
|
|
|
4944
6402
|
*/
|
|
4945
6403
|
streamIndex: number | null;
|
|
4946
6404
|
}
|
|
4947
|
-
/**
|
|
6405
|
+
/**
|
|
6406
|
+
* The subtitle state of the current source.
|
|
6407
|
+
*
|
|
6408
|
+
* @see {@link Subtitles.state}
|
|
6409
|
+
* @since 1.0.0
|
|
6410
|
+
* @category Subtitles
|
|
6411
|
+
*/
|
|
4948
6412
|
export interface SubtitlesState {
|
|
4949
6413
|
/** The source it belongs to ({@link PlayerState.sourceId}). */
|
|
4950
6414
|
readonly sourceId: number;
|
|
@@ -4975,7 +6439,12 @@ export interface SubtitlesState {
|
|
|
4975
6439
|
/** The last error, `null` without one. */
|
|
4976
6440
|
readonly error: string | null;
|
|
4977
6441
|
}
|
|
4978
|
-
/**
|
|
6442
|
+
/**
|
|
6443
|
+
* Subtitle calls act on the selection you see; pass `ifCurrent` to pin an older one.
|
|
6444
|
+
*
|
|
6445
|
+
* @since 1.0.0
|
|
6446
|
+
* @category Subtitles
|
|
6447
|
+
*/
|
|
4979
6448
|
export interface SubtitleConditional {
|
|
4980
6449
|
/** The subtitle state the call is conditional on; `stale` otherwise. */
|
|
4981
6450
|
ifCurrent?: SubtitlesState;
|
|
@@ -5009,18 +6478,104 @@ export interface SubtitleConditional {
|
|
|
5009
6478
|
* offsetMs: 0,
|
|
5010
6479
|
* });
|
|
5011
6480
|
* ```
|
|
6481
|
+
*
|
|
6482
|
+
* @remarks
|
|
6483
|
+
* The subtitle tracks of the current source, the primary and secondary
|
|
6484
|
+
* selection, uploaded files and the style. `state` is the SDK's snapshot of
|
|
6485
|
+
* the player's subtitle state (`null` until first read). The player has no
|
|
6486
|
+
* subtitle event: the SDK reads the state again when the source, its phase
|
|
6487
|
+
* or the backend changes (with `events`) and after this plugin's own
|
|
6488
|
+
* changes, so a selection the user or another plugin changed shows only
|
|
6489
|
+
* after `refresh()`. A change without `ifCurrent` reads the state first
|
|
6490
|
+
* and applies to it; pass `ifCurrent` to make it fail with `stale` when the
|
|
6491
|
+
* selection changed since that state. Each change is one operation the
|
|
6492
|
+
* player confirms; `select`, `remove` and `setStyle` resolve with the new
|
|
6493
|
+
* state, `load` with the new track.
|
|
6494
|
+
*
|
|
6495
|
+
* @capability `subtitles.read` to read; `subtitles.control` and
|
|
6496
|
+
* `subtitles.read` to change; `events` to re-read on source changes.
|
|
6497
|
+
*
|
|
6498
|
+
* @see [Load subtitles from a plugin](../../docs/how-to/subtitles.md)
|
|
6499
|
+
* @since 1.0.0
|
|
6500
|
+
* @category Subtitles
|
|
5012
6501
|
*/
|
|
5013
6502
|
export interface Subtitles {
|
|
5014
6503
|
/** `null` until first read. */
|
|
5015
6504
|
readonly state: SubtitlesState | null;
|
|
5016
|
-
/**
|
|
6505
|
+
/**
|
|
6506
|
+
* The subtitle state changed.
|
|
6507
|
+
*
|
|
6508
|
+
* @capability events and subtitles.read
|
|
6509
|
+
*
|
|
6510
|
+
* @example Show the subtitle language after a new file opens
|
|
6511
|
+
* When a new file starts, the OSD names the language of its primary
|
|
6512
|
+
* subtitle. The SDK reads the subtitle state again when the source or its
|
|
6513
|
+
* phase changes (there is no subtitle event, so a selection changed in the
|
|
6514
|
+
* player shows only after `refresh()`) and then calls the listener;
|
|
6515
|
+
* `selected` is `null` when subtitles are off.
|
|
6516
|
+
* ```ts
|
|
6517
|
+
* plugin.subtitles.onChange((state) => {
|
|
6518
|
+
* // A track's language is null when the file does not say.
|
|
6519
|
+
* void plugin.osd.show("Subtitles: {}", [state.selected?.language ?? "off"]);
|
|
6520
|
+
* });
|
|
6521
|
+
* ```
|
|
6522
|
+
*
|
|
6523
|
+
* @since 1.0.0
|
|
6524
|
+
*/
|
|
5017
6525
|
onChange(listener: (state: SubtitlesState) => void): Disposable;
|
|
5018
|
-
/**
|
|
6526
|
+
/**
|
|
6527
|
+
* Read the state from the player now.
|
|
6528
|
+
*
|
|
6529
|
+
* @capability subtitles.read
|
|
6530
|
+
* @throws `capability` (rejects) The manifest does not declare
|
|
6531
|
+
* `subtitles.read`.
|
|
6532
|
+
*
|
|
6533
|
+
* @example Count the subtitle tracks of the file
|
|
6534
|
+
* The plugin reads how many subtitle tracks the file has before building a
|
|
6535
|
+
* menu. `refresh` asks the player and resolves with the current state, also
|
|
6536
|
+
* without `events`; `state` holds it afterwards.
|
|
6537
|
+
* ```ts
|
|
6538
|
+
* const { tracks } = await plugin.subtitles.refresh();
|
|
6539
|
+
* await plugin.osd.show("{} subtitle tracks", [tracks.length]);
|
|
6540
|
+
* ```
|
|
6541
|
+
*
|
|
6542
|
+
* @since 1.0.0
|
|
6543
|
+
*/
|
|
5019
6544
|
refresh(): Promise<SubtitlesState>;
|
|
5020
6545
|
/**
|
|
5021
6546
|
* `role: "secondary"` sets the secondary subtitle: a text track that is not
|
|
5022
6547
|
* the primary one (`invalid` otherwise). A new source clears it.
|
|
5023
6548
|
* `track: null` turns the role off.
|
|
6549
|
+
*
|
|
6550
|
+
* @capability subtitles.control and subtitles.read
|
|
6551
|
+
* @throws `stale` (rejects) With `ifCurrent`: the source or the subtitle
|
|
6552
|
+
* selection changed since that state.
|
|
6553
|
+
* @throws `invalid` (rejects) An unknown track; as secondary, a bitmap
|
|
6554
|
+
* track or the primary one; as primary, the current secondary.
|
|
6555
|
+
* @throws `missing` (rejects) The track is not in the current source.
|
|
6556
|
+
* @throws `unavailable` (rejects) Nothing loaded, or the source is still
|
|
6557
|
+
* loading or not yet confirmed.
|
|
6558
|
+
* @throws `busy` (rejects) Too many subtitle operations at once.
|
|
6559
|
+
* @throws `capability` (rejects) The manifest does not declare
|
|
6560
|
+
* `subtitles.control` and `subtitles.read`.
|
|
6561
|
+
*
|
|
6562
|
+
* @example Turn on the first English subtitle
|
|
6563
|
+
* The first English track becomes the primary subtitle, but only in the
|
|
6564
|
+
* file the plugin looked at. Embedded tracks are chosen by number, so
|
|
6565
|
+
* without `ifCurrent` a select after another file opened would pick that
|
|
6566
|
+
* file's track with the same number; with `ifCurrent: state` it fails with
|
|
6567
|
+
* `stale` instead. Without `ifCurrent`, `select` reads the newest state
|
|
6568
|
+
* itself and almost never fails with `stale`.
|
|
6569
|
+
* ```ts
|
|
6570
|
+
* const state = await plugin.subtitles.refresh();
|
|
6571
|
+
* const english = state.tracks.find((track) => track.language === "en");
|
|
6572
|
+
* if (english)
|
|
6573
|
+
* await plugin.subtitles.select(english, { ifCurrent: state }).catch((error: PluginError) => {
|
|
6574
|
+
* if (error.code !== "stale") throw error; // another file or selection now
|
|
6575
|
+
* });
|
|
6576
|
+
* ```
|
|
6577
|
+
*
|
|
6578
|
+
* @since 1.0.0
|
|
5024
6579
|
*/
|
|
5025
6580
|
select(track: SubtitleTrack | null, options?: SubtitleConditional & {
|
|
5026
6581
|
role?: "primary" | "secondary";
|
|
@@ -5037,6 +6592,36 @@ export interface Subtitles {
|
|
|
5037
6592
|
* Limits: SRT ≤ 2 MiB and ≤ 10,000 cues; ASS ≤ 2 MiB with `[Script Info]`
|
|
5038
6593
|
* and `[Events]` and 1–20,000 `Dialogue:` lines. Convert other formats
|
|
5039
6594
|
* (VTT, SAMI) to SRT first.
|
|
6595
|
+
*
|
|
6596
|
+
* @capability subtitles.control and subtitles.read
|
|
6597
|
+
* @throws `invalid` (rejects) A bad `format` or `title`, or a file over the
|
|
6598
|
+
* limits.
|
|
6599
|
+
* @throws `stale` (rejects) With `ifCurrent`: the source changed since
|
|
6600
|
+
* that state.
|
|
6601
|
+
* @throws `unavailable` (rejects) Nothing loaded, or the source is still
|
|
6602
|
+
* loading.
|
|
6603
|
+
* @throws `aborted` (rejects) `signal` aborted before the commit (an
|
|
6604
|
+
* `AbortError`); the upload is discarded.
|
|
6605
|
+
* @throws `busy` (rejects) Too many subtitle operations at once.
|
|
6606
|
+
* @throws `capability` (rejects) The manifest does not declare
|
|
6607
|
+
* `subtitles.control` and `subtitles.read`.
|
|
6608
|
+
*
|
|
6609
|
+
* @example Load a downloaded SRT and select it
|
|
6610
|
+
* A subtitle file the plugin downloaded becomes a track of the current file
|
|
6611
|
+
* and is selected. The plugin passes the file's bytes; the player parses
|
|
6612
|
+
* them, adds an `external` track for this source only and resolves with
|
|
6613
|
+
* it once the upload is committed.
|
|
6614
|
+
* ```ts
|
|
6615
|
+
* declare const url: string;
|
|
6616
|
+
* // ---cut---
|
|
6617
|
+
* const response = await fetch(url);
|
|
6618
|
+
* if (!response.ok) throw new Error(`subtitles: HTTP ${response.status}`);
|
|
6619
|
+
* const bytes = new Uint8Array(await response.arrayBuffer());
|
|
6620
|
+
* const track = await plugin.subtitles.load(bytes, { title: "Downloaded" }); // selected
|
|
6621
|
+
* await plugin.osd.show("Loaded {}", [track.title]);
|
|
6622
|
+
* ```
|
|
6623
|
+
*
|
|
6624
|
+
* @since 1.0.0
|
|
5040
6625
|
*/
|
|
5041
6626
|
load(bytes: Uint8Array, options?: SubtitleConditional & {
|
|
5042
6627
|
format?: "srt" | "ass";
|
|
@@ -5044,19 +6629,69 @@ export interface Subtitles {
|
|
|
5044
6629
|
select?: "primary" | "secondary" | "none";
|
|
5045
6630
|
signal?: AbortSignal;
|
|
5046
6631
|
}): Promise<SubtitleTrack>;
|
|
5047
|
-
/**
|
|
6632
|
+
/**
|
|
6633
|
+
* Remove a track this plugin loaded.
|
|
6634
|
+
*
|
|
6635
|
+
* @capability subtitles.control and subtitles.read
|
|
6636
|
+
* @throws `invalid` (rejects) The track was not loaded by this plugin.
|
|
6637
|
+
* @throws `stale` (rejects) With `ifCurrent`: the source changed since
|
|
6638
|
+
* that state.
|
|
6639
|
+
* @throws `unavailable` (rejects) Nothing is loaded, or the source is
|
|
6640
|
+
* still loading.
|
|
6641
|
+
* @throws `busy` (rejects) Too many subtitle operations at once.
|
|
6642
|
+
* @throws `capability` (rejects) The manifest does not declare
|
|
6643
|
+
* `subtitles.control` and `subtitles.read`.
|
|
6644
|
+
*
|
|
6645
|
+
* @example Remove the subtitle the plugin loaded
|
|
6646
|
+
* The plugin's downloaded subtitle disappears from the track list. A plugin
|
|
6647
|
+
* can remove only tracks it loaded itself; a new source removes them
|
|
6648
|
+
* anyway.
|
|
6649
|
+
* ```ts
|
|
6650
|
+
* declare const loaded: SubtitleTrack;
|
|
6651
|
+
* // ---cut---
|
|
6652
|
+
* await plugin.subtitles.remove(loaded);
|
|
6653
|
+
* ```
|
|
6654
|
+
*
|
|
6655
|
+
* @since 1.0.0
|
|
6656
|
+
*/
|
|
5048
6657
|
remove(track: SubtitleTrack, options?: SubtitleConditional): Promise<SubtitlesState>;
|
|
5049
6658
|
/**
|
|
5050
6659
|
* Set this plugin's style layer; `null` removes it. The newest call of
|
|
5051
6660
|
* this plugin replaces its earlier layer. When several plugins have a
|
|
5052
6661
|
* layer, the player uses the one whose manifest id sorts last, whatever
|
|
5053
6662
|
* the order of the calls.
|
|
6663
|
+
*
|
|
6664
|
+
* @capability subtitles.control and subtitles.read
|
|
6665
|
+
* @throws `invalid` (rejects) A field out of range.
|
|
6666
|
+
* @throws `unsupported` (rejects) A font that is not in `state.fonts`, or a
|
|
6667
|
+
* primary subtitle that is a bitmap track.
|
|
6668
|
+
* @throws `stale` (rejects) With `ifCurrent`: the subtitle state changed
|
|
6669
|
+
* since that state.
|
|
6670
|
+
* @throws `unavailable` (rejects) Nothing is loaded, or the source is
|
|
6671
|
+
* still loading.
|
|
6672
|
+
* @throws `busy` (rejects) Too many subtitle operations at once.
|
|
6673
|
+
* @throws `capability` (rejects) The manifest does not declare
|
|
6674
|
+
* `subtitles.control` and `subtitles.read`.
|
|
6675
|
+
*
|
|
6676
|
+
* @example Take the plugin's style off again
|
|
6677
|
+
* The plugin removes its style layer, so the subtitles go back to the
|
|
6678
|
+
* player's default (or to another plugin's layer). `null` removes only this
|
|
6679
|
+
* plugin's layer; the effective layer is the one whose manifest id sorts
|
|
6680
|
+
* last among the plugins that still have one.
|
|
6681
|
+
* ```ts
|
|
6682
|
+
* await plugin.subtitles.setStyle(null);
|
|
6683
|
+
* ```
|
|
6684
|
+
*
|
|
6685
|
+
* @since 1.0.0
|
|
5054
6686
|
*/
|
|
5055
6687
|
setStyle(style: SubtitleStyle | null, options?: SubtitleConditional): Promise<SubtitlesState>;
|
|
5056
6688
|
}
|
|
5057
6689
|
/**
|
|
5058
6690
|
* Options of {@link MediaCapture.capture}. Conditional on `player.state`
|
|
5059
6691
|
* like other controls.
|
|
6692
|
+
*
|
|
6693
|
+
* @since 1.0.0
|
|
6694
|
+
* @category Media
|
|
5060
6695
|
*/
|
|
5061
6696
|
export interface CaptureOptions extends Conditional {
|
|
5062
6697
|
/** Source time; defaults to the current position at the moment of capture. */
|
|
@@ -5068,7 +6703,36 @@ export interface CaptureOptions extends Conditional {
|
|
|
5068
6703
|
/** Audio to freeze with the snapshot: the selected track, none, or a track id. */
|
|
5069
6704
|
audio?: "selected" | "none" | number;
|
|
5070
6705
|
}
|
|
5071
|
-
/**
|
|
6706
|
+
/**
|
|
6707
|
+
* A frozen, immutable input for exports. Dispose to release it.
|
|
6708
|
+
*
|
|
6709
|
+
* @remarks
|
|
6710
|
+
* A frozen input, from {@link MediaCapture.capture}: the source, the time
|
|
6711
|
+
* and (processed) the effects and subtitle as they were. Exports read it in
|
|
6712
|
+
* the player's offline worker, so one snapshot can be exported several
|
|
6713
|
+
* times and playback is unaffected. Disposing it releases the player's
|
|
6714
|
+
* copy; an export using it after that fails with `released`.
|
|
6715
|
+
*
|
|
6716
|
+
* @capability media.read and media.control
|
|
6717
|
+
*
|
|
6718
|
+
* @example Export one frame as PNG and as JPEG
|
|
6719
|
+
* The same frozen frame is written twice, lossless and compressed. Each
|
|
6720
|
+
* `export` starts a job of its own; the snapshot is released after both
|
|
6721
|
+
* are done.
|
|
6722
|
+
* ```ts
|
|
6723
|
+
* const snapshot = await plugin.media.capture();
|
|
6724
|
+
* try {
|
|
6725
|
+
* const png = await snapshot.export({ format: "png", destination: "/tmp/frame.png" });
|
|
6726
|
+
* const jpeg = await snapshot.export({ format: "jpeg", destination: "/tmp/frame.jpg", quality: 85 });
|
|
6727
|
+
* await Promise.all([png.done, jpeg.done]); // terminal states, not errors
|
|
6728
|
+
* } finally {
|
|
6729
|
+
* await snapshot.dispose();
|
|
6730
|
+
* }
|
|
6731
|
+
* ```
|
|
6732
|
+
*
|
|
6733
|
+
* @since 1.0.0
|
|
6734
|
+
* @category Media
|
|
6735
|
+
*/
|
|
5072
6736
|
export interface Snapshot extends Disposable {
|
|
5073
6737
|
/** Without or with effects. */
|
|
5074
6738
|
readonly kind: "raw" | "processed";
|
|
@@ -5092,6 +6756,32 @@ export interface Snapshot extends Disposable {
|
|
|
5092
6756
|
/**
|
|
5093
6757
|
* Start exporting; the job runs in the host's offline worker.
|
|
5094
6758
|
* One snapshot can be exported several times.
|
|
6759
|
+
*
|
|
6760
|
+
* @capability media.read and media.control
|
|
6761
|
+
* @throws `released` (rejects) The snapshot was already released.
|
|
6762
|
+
* @throws `invalid` (rejects) Bad options (`quality` outside 1–100, a
|
|
6763
|
+
* relative destination, a clip `end` not after the snapshot time or
|
|
6764
|
+
* past the duration).
|
|
6765
|
+
* @throws `busy` (rejects) This plugin runs 2 exports (or the player 8).
|
|
6766
|
+
* @throws `unavailable` (rejects) No FFmpeg, or the export worker could not
|
|
6767
|
+
* start.
|
|
6768
|
+
* @throws `capability` (rejects) The manifest does not declare
|
|
6769
|
+
* `media.read` and `media.control`.
|
|
6770
|
+
*
|
|
6771
|
+
* @example Export a 30-second clip
|
|
6772
|
+
* A clip from the snapshot time to 30 seconds later is written as MP4.
|
|
6773
|
+
* `export` resolves once the job is accepted; the job then runs in the
|
|
6774
|
+
* player's offline worker, and `done` resolves with its terminal state,
|
|
6775
|
+
* whose `outcome` has the destination or the error.
|
|
6776
|
+
* ```ts
|
|
6777
|
+
* declare const snapshot: Snapshot;
|
|
6778
|
+
* // ---cut---
|
|
6779
|
+
* const job = await snapshot.export({ format: "mp4", destination: "/tmp/clip.mp4", end: snapshot.time + 30 });
|
|
6780
|
+
* const end = await job.done;
|
|
6781
|
+
* if (end.status !== "completed") await plugin.osd.show("Export {}", [end.status]);
|
|
6782
|
+
* ```
|
|
6783
|
+
*
|
|
6784
|
+
* @since 1.0.0
|
|
5095
6785
|
*/
|
|
5096
6786
|
export(options: ExportOptions): Promise<ExportJob>;
|
|
5097
6787
|
}
|
|
@@ -5103,6 +6793,9 @@ export interface Snapshot extends Disposable {
|
|
|
5103
6793
|
* choose the format. `sampleTime` picks the frame (default: the snapshot
|
|
5104
6794
|
* time); `end` is the clip's end in source seconds. On Windows the export
|
|
5105
6795
|
* worker needs FFmpeg 6.1 or newer.
|
|
6796
|
+
*
|
|
6797
|
+
* @since 1.0.0
|
|
6798
|
+
* @category Media
|
|
5106
6799
|
*/
|
|
5107
6800
|
export type ExportOptions = {
|
|
5108
6801
|
/** A lossless frame. */
|
|
@@ -5133,19 +6826,111 @@ export type ExportOptions = {
|
|
|
5133
6826
|
* `completed`, `failed` or `cancelled`; once terminal, `outcome` is
|
|
5134
6827
|
* `{ destination }`, `{ error: { code, message } }` or `{ cancelled: true }`.
|
|
5135
6828
|
* `facts` describes what the worker produced (versions, range, streams).
|
|
6829
|
+
*
|
|
6830
|
+
* @since 1.0.0
|
|
6831
|
+
* @category Media
|
|
5136
6832
|
*/
|
|
5137
6833
|
export type JobState = Omit<MediaJob, "jobId">;
|
|
5138
|
-
/**
|
|
6834
|
+
/**
|
|
6835
|
+
* An export, from {@link Snapshot.export}. Disposing an unfinished job cancels it.
|
|
6836
|
+
*
|
|
6837
|
+
* @remarks
|
|
6838
|
+
* One export, from {@link Snapshot.export}. `state` is the SDK's last copy
|
|
6839
|
+
* of the job's state: the SDK polls the player about every 350 ms until the
|
|
6840
|
+
* job ends, and `refresh` reads it at once. `done` resolves
|
|
6841
|
+
* once the job is terminal (`completed`, `failed` or `cancelled`) and never
|
|
6842
|
+
* rejects for a failed export: the failure is in `outcome`. Disposing an
|
|
6843
|
+
* unfinished job cancels it.
|
|
6844
|
+
*
|
|
6845
|
+
* @capability media.read and media.control
|
|
6846
|
+
*
|
|
6847
|
+
* @example Show export progress and cancel on request
|
|
6848
|
+
* The OSD shows the job's status while it runs, and a Cancel command stops
|
|
6849
|
+
* it. `onChange` follows the state (`queued`, `running`, `cancelling`,
|
|
6850
|
+
* then a terminal one); `cancel` asks the worker to stop. After
|
|
6851
|
+
* `requested` the job usually ends `cancelled`, or `failed` when the
|
|
6852
|
+
* cleanup failed.
|
|
6853
|
+
* ```ts
|
|
6854
|
+
* declare const job: ExportJob;
|
|
6855
|
+
* // ---cut---
|
|
6856
|
+
* job.onChange((state) => void plugin.osd.show("Export: {}", [state.status]));
|
|
6857
|
+
* plugin.commands.add({ id: "cancel-export", title: "Cancel export", run: () => job.cancel() });
|
|
6858
|
+
* await job.done; // completed, failed or cancelled
|
|
6859
|
+
* ```
|
|
6860
|
+
*
|
|
6861
|
+
* @since 1.0.0
|
|
6862
|
+
* @category Media
|
|
6863
|
+
*/
|
|
5139
6864
|
export interface ExportJob extends Disposable {
|
|
5140
6865
|
/** Latest state. */
|
|
5141
6866
|
readonly state: JobState;
|
|
5142
|
-
/**
|
|
6867
|
+
/**
|
|
6868
|
+
* The state changed.
|
|
6869
|
+
*
|
|
6870
|
+
* @capability media.read
|
|
6871
|
+
*
|
|
6872
|
+
* @example Log each state of an export
|
|
6873
|
+
* The plugin's log records each status the job goes through (`queued`,
|
|
6874
|
+
* `running`, `cancelling` after a cancel, then a terminal one). The listener runs when the SDK learns a
|
|
6875
|
+
* new state from its polling.
|
|
6876
|
+
* ```ts
|
|
6877
|
+
* declare const job: ExportJob;
|
|
6878
|
+
* // ---cut---
|
|
6879
|
+
* job.onChange((state) => console.log("export", state.status));
|
|
6880
|
+
* ```
|
|
6881
|
+
*
|
|
6882
|
+
* @since 1.0.0
|
|
6883
|
+
*/
|
|
5143
6884
|
onChange(listener: (state: JobState) => void): Disposable;
|
|
5144
|
-
/**
|
|
6885
|
+
/**
|
|
6886
|
+
* Resolves with the terminal state (completed, failed or cancelled).
|
|
6887
|
+
*
|
|
6888
|
+
* @throws `disposed` (rejects) The job handle was disposed while waiting.
|
|
6889
|
+
*/
|
|
5145
6890
|
readonly done: Promise<JobState>;
|
|
5146
|
-
/**
|
|
6891
|
+
/**
|
|
6892
|
+
* Ask to cancel: `requested`, `tooLate` (already publishing) or `terminal` (already over).
|
|
6893
|
+
*
|
|
6894
|
+
* @capability media.control
|
|
6895
|
+
* @throws `rejected` (rejects) The player did not accept the request.
|
|
6896
|
+
* @throws `capability` (rejects) The manifest does not declare
|
|
6897
|
+
* `media.control`.
|
|
6898
|
+
*
|
|
6899
|
+
* @example Cancel an export the user no longer wants
|
|
6900
|
+
* The running export stops, unless it is already writing its file.
|
|
6901
|
+
* `cancel` resolves `requested` when the worker will stop (the job ends
|
|
6902
|
+
* `cancelled`), `tooLate` when it is already publishing the file, and
|
|
6903
|
+
* `terminal` when the job had already ended.
|
|
6904
|
+
* ```ts
|
|
6905
|
+
* declare const job: ExportJob;
|
|
6906
|
+
* // ---cut---
|
|
6907
|
+
* const answer = await job.cancel();
|
|
6908
|
+
* if (answer === "tooLate") await plugin.osd.show("Too late: the file is being written");
|
|
6909
|
+
* ```
|
|
6910
|
+
*
|
|
6911
|
+
* @since 1.0.0
|
|
6912
|
+
*/
|
|
5147
6913
|
cancel(): Promise<"requested" | "tooLate" | "terminal">;
|
|
5148
|
-
/**
|
|
6914
|
+
/**
|
|
6915
|
+
* Read the state from the player now.
|
|
6916
|
+
*
|
|
6917
|
+
* @capability media.read
|
|
6918
|
+
* @throws `capability` (rejects) The manifest does not declare
|
|
6919
|
+
* `media.read`.
|
|
6920
|
+
*
|
|
6921
|
+
* @example Read a job's state now
|
|
6922
|
+
* The plugin reads the newest state of an export without waiting for an
|
|
6923
|
+
* event. `refresh` asks the player and resolves with the state, which
|
|
6924
|
+
* `state` then holds.
|
|
6925
|
+
* ```ts
|
|
6926
|
+
* declare const job: ExportJob;
|
|
6927
|
+
* // ---cut---
|
|
6928
|
+
* const state = await job.refresh();
|
|
6929
|
+
* console.log(state.status);
|
|
6930
|
+
* ```
|
|
6931
|
+
*
|
|
6932
|
+
* @since 1.0.0
|
|
6933
|
+
*/
|
|
5149
6934
|
refresh(): Promise<JobState>;
|
|
5150
6935
|
}
|
|
5151
6936
|
/**
|
|
@@ -5173,21 +6958,73 @@ export interface ExportJob extends Disposable {
|
|
|
5173
6958
|
* await snapshot.dispose();
|
|
5174
6959
|
* }
|
|
5175
6960
|
* ```
|
|
6961
|
+
*
|
|
6962
|
+
* @remarks
|
|
6963
|
+
* Exports run in the player's offline worker from a frozen
|
|
6964
|
+
* {@link Snapshot}: a still frame or a clip, raw or with this plugin's
|
|
6965
|
+
* effects (and the primary subtitle). Playback is not disturbed. A plugin
|
|
6966
|
+
* holds at most 4 snapshots at a time (16 in the player); release them
|
|
6967
|
+
* when done.
|
|
6968
|
+
*
|
|
6969
|
+
* @capability media.read and media.control (processed snapshots also
|
|
6970
|
+
* `playback.read` and `subtitles.read`)
|
|
6971
|
+
* @since 1.0.0
|
|
6972
|
+
* @category Media
|
|
5176
6973
|
*/
|
|
5177
6974
|
export interface MediaCapture {
|
|
5178
6975
|
/**
|
|
5179
6976
|
* Freeze the current source (and optionally this plugin set's effects)
|
|
5180
6977
|
* as an input for exports. Fails `stale` when the source changed since
|
|
5181
|
-
* `ifCurrent`; `unsupported` for
|
|
6978
|
+
* `ifCurrent`; `unsupported` for subtitles that cannot be flattened (only
|
|
6979
|
+
* plain SRT text can) or a picture over the size limits; `noSource` when
|
|
6980
|
+
* nothing is playing.
|
|
6981
|
+
*
|
|
6982
|
+
* @capability media.read and media.control
|
|
6983
|
+
* @throws `stale` (rejects) The source changed since `ifCurrent`.
|
|
6984
|
+
* @throws `noSource` (rejects) Nothing is playing.
|
|
6985
|
+
* @throws `invalid` (rejects) `at` at or after the end of the source.
|
|
6986
|
+
* @throws `busy` (rejects) This plugin holds 4 snapshots (or the player 16);
|
|
6987
|
+
* release one.
|
|
6988
|
+
* @throws `unavailable` (rejects) A processed snapshot while the picture is
|
|
6989
|
+
* still being prepared.
|
|
6990
|
+
* @throws `unsupported` (rejects) A processed snapshot the player cannot
|
|
6991
|
+
* freeze: subtitles that cannot be flattened (only plain SRT text can),
|
|
6992
|
+
* or a picture over the size limits.
|
|
6993
|
+
* @throws `capability` (rejects) The manifest does not declare
|
|
6994
|
+
* `media.read` and `media.control`.
|
|
6995
|
+
*
|
|
6996
|
+
* @example Freeze the frame at the 10-second mark
|
|
6997
|
+
* The plugin freezes the frame at 0:10 of the open file, raw, to export it
|
|
6998
|
+
* later. `capture` resolves with a `Snapshot` once the player has frozen
|
|
6999
|
+
* that input; the snapshot stays valid when playback moves on or the file
|
|
7000
|
+
* changes, until it is released.
|
|
7001
|
+
* ```ts
|
|
7002
|
+
* const snapshot = await plugin.media.capture({ at: 10 }); // seconds
|
|
7003
|
+
* try {
|
|
7004
|
+
* // … export it …
|
|
7005
|
+
* } finally {
|
|
7006
|
+
* await snapshot.dispose(); // frees one of the plugin's 4 snapshot slots
|
|
7007
|
+
* }
|
|
7008
|
+
* ```
|
|
7009
|
+
*
|
|
7010
|
+
* @since 1.0.0
|
|
5182
7011
|
*/
|
|
5183
7012
|
capture(options?: CaptureOptions): Promise<Snapshot>;
|
|
5184
7013
|
}
|
|
5185
7014
|
/**
|
|
5186
7015
|
* Declaration of {@link Resolvers.add}: a web resolver
|
|
5187
7016
|
* ({@link WebResolverOptions}) or a file resolver ({@link FileResolverOptions}).
|
|
7017
|
+
*
|
|
7018
|
+
* @since 1.0.0
|
|
7019
|
+
* @category Sources
|
|
5188
7020
|
*/
|
|
5189
7021
|
export type ResolverOptions = WebResolverOptions | FileResolverOptions;
|
|
5190
|
-
/**
|
|
7022
|
+
/**
|
|
7023
|
+
* A resolver for web pages: `https://host/…` to playable stream URLs.
|
|
7024
|
+
*
|
|
7025
|
+
* @since 1.0.0
|
|
7026
|
+
* @category Sources
|
|
7027
|
+
*/
|
|
5191
7028
|
export interface WebResolverOptions {
|
|
5192
7029
|
/** Local id, unique within the plugin. */
|
|
5193
7030
|
id: string;
|
|
@@ -5252,6 +7089,9 @@ export interface WebResolverOptions {
|
|
|
5252
7089
|
* },
|
|
5253
7090
|
* });
|
|
5254
7091
|
* ```
|
|
7092
|
+
*
|
|
7093
|
+
* @since 1.5.3
|
|
7094
|
+
* @category Sources
|
|
5255
7095
|
*/
|
|
5256
7096
|
export interface FileResolverOptions {
|
|
5257
7097
|
/** Local id, unique within the plugin. */
|
|
@@ -5276,6 +7116,9 @@ export interface FileResolverOptions {
|
|
|
5276
7116
|
* failures "无法启动播放". An error thrown by `frame()` keeps its message.
|
|
5277
7117
|
* To give your own text for a file you can tell is unusable (a bad header),
|
|
5278
7118
|
* check it in `resolve` and throw there.
|
|
7119
|
+
*
|
|
7120
|
+
* @since 1.5.3
|
|
7121
|
+
* @category Sources
|
|
5279
7122
|
*/
|
|
5280
7123
|
export interface ResolvedStream {
|
|
5281
7124
|
/** Always `"stream"`. */
|
|
@@ -5294,14 +7137,52 @@ export interface ResolvedStream {
|
|
|
5294
7137
|
*/
|
|
5295
7138
|
started?(stream: Stream): void;
|
|
5296
7139
|
}
|
|
5297
|
-
/**
|
|
5298
|
-
|
|
5299
|
-
|
|
5300
|
-
|
|
7140
|
+
/**
|
|
7141
|
+
* A registered resolver. Disposing it unregisters it and aborts resolutions in flight.
|
|
7142
|
+
*
|
|
7143
|
+
* @remarks
|
|
7144
|
+
* A registered resolver, from {@link Resolvers.add}. Disposing it, its
|
|
7145
|
+
* scope or stopping the plugin unregisters it and aborts resolutions in
|
|
7146
|
+
* flight; the extensions or hosts are free again for other plugins.
|
|
7147
|
+
*
|
|
7148
|
+
* @capability source.resolve
|
|
7149
|
+
*
|
|
7150
|
+
* @example Report when another plugin already handles the format
|
|
7151
|
+
* The plugin tells the user when its `.ch8` resolver could not register
|
|
7152
|
+
* because another plugin holds that extension. `ready` rejects with
|
|
7153
|
+
* `conflict`; for another plugin's claim that does not fail activation, so
|
|
7154
|
+
* the plugin keeps running without the resolver.
|
|
7155
|
+
* ```ts
|
|
7156
|
+
* const resolver = plugin.resolvers.add({
|
|
7157
|
+
* id: "chip8",
|
|
7158
|
+
* extensions: ["ch8"],
|
|
7159
|
+
* async resolve() {
|
|
7160
|
+
* throw plugin.i18n.error("Not ready yet.");
|
|
7161
|
+
* },
|
|
7162
|
+
* });
|
|
7163
|
+
* // Rejects with `conflict` when the extension is taken.
|
|
7164
|
+
* resolver.ready.catch((error: PluginError) => {
|
|
7165
|
+
* if (error.code === "conflict") void plugin.osd.show("Another plugin plays .ch8 files");
|
|
7166
|
+
* });
|
|
7167
|
+
* ```
|
|
7168
|
+
*
|
|
7169
|
+
* @since 1.0.0
|
|
7170
|
+
* @category Sources
|
|
7171
|
+
*/
|
|
7172
|
+
export interface Resolver extends Disposable {
|
|
7173
|
+
/**
|
|
7174
|
+
* Resolves when the player registered it; rejects otherwise: `conflict`
|
|
5301
7175
|
* when another resolver has the extension or the scheme + host (another
|
|
5302
7176
|
* plugin's: activation goes on; this plugin's own: activation fails),
|
|
5303
7177
|
* `invalid` for a malformed declaration (a player before 0.4.1 also for
|
|
5304
7178
|
* `extensions`).
|
|
7179
|
+
*
|
|
7180
|
+
* @throws `conflict` (rejects) Another resolver has the extension or the
|
|
7181
|
+
* scheme + host, or this plugin already registered this id.
|
|
7182
|
+
* @throws `busy` (rejects) The player already holds 16 resolvers.
|
|
7183
|
+
* @throws `invalid` (rejects) A malformed declaration.
|
|
7184
|
+
* @throws `capability` (rejects) The manifest does not declare
|
|
7185
|
+
* `source.resolve`.
|
|
5305
7186
|
*/
|
|
5306
7187
|
readonly ready: Promise<void>;
|
|
5307
7188
|
}
|
|
@@ -5328,6 +7209,20 @@ export interface Resolver extends Disposable {
|
|
|
5328
7209
|
* },
|
|
5329
7210
|
* });
|
|
5330
7211
|
* ```
|
|
7212
|
+
*
|
|
7213
|
+
* @remarks
|
|
7214
|
+
* A resolver turns something the player cannot play by itself into
|
|
7215
|
+
* something it can: a web page address into stream URLs
|
|
7216
|
+
* ({@link WebResolverOptions}), or a local file of a format the plugin
|
|
7217
|
+
* understands into a stream the plugin generates
|
|
7218
|
+
* ({@link FileResolverOptions}). The player asks the resolver whenever
|
|
7219
|
+
* anyone opens a matching address or file (the Open dialog, the playlist,
|
|
7220
|
+
* another plugin), so the plugin does not intercept opens itself.
|
|
7221
|
+
*
|
|
7222
|
+
* @capability source.resolve (file resolvers also `producer.av`)
|
|
7223
|
+
* @see [Playback state, controls and sources](../../docs/concepts/playback.md)
|
|
7224
|
+
* @since 1.0.0
|
|
7225
|
+
* @category Sources
|
|
5331
7226
|
*/
|
|
5332
7227
|
export interface Resolvers {
|
|
5333
7228
|
/**
|
|
@@ -5336,6 +7231,29 @@ export interface Resolvers {
|
|
|
5336
7231
|
* replaced. One scheme + host pair, and one extension, has one resolver
|
|
5337
7232
|
* (`conflict` otherwise). A rejected `resolve` fails the open with the
|
|
5338
7233
|
* error's message.
|
|
7234
|
+
*
|
|
7235
|
+
* @capability source.resolve
|
|
7236
|
+
*
|
|
7237
|
+
* @example Play a site's pages through the plugin
|
|
7238
|
+
* Opening a `https://video.example.com/…` address plays the page's video.
|
|
7239
|
+
* `add` returns at once and registers in the background (`ready` reports
|
|
7240
|
+
* the answer); from then on the player calls `resolve` for every matching
|
|
7241
|
+
* open, and the error a rejected `resolve` carries is what the user sees.
|
|
7242
|
+
* ```ts
|
|
7243
|
+
* plugin.resolvers.add({
|
|
7244
|
+
* id: "example",
|
|
7245
|
+
* schemes: ["https"],
|
|
7246
|
+
* hosts: ["video.example.com"], // exact host names
|
|
7247
|
+
* async resolve(url, signal) {
|
|
7248
|
+
* const response = await fetch(url, { signal });
|
|
7249
|
+
* if (!response.ok) throw new Error(`the page answered ${response.status}`);
|
|
7250
|
+
* const page = (await response.json()) as { title: string; stream: string };
|
|
7251
|
+
* return { kind: "media", originalUrl: url, title: page.title, primary: { url: page.stream } };
|
|
7252
|
+
* },
|
|
7253
|
+
* });
|
|
7254
|
+
* ```
|
|
7255
|
+
*
|
|
7256
|
+
* @since 1.0.0
|
|
5339
7257
|
*/
|
|
5340
7258
|
add(options: ResolverOptions): Resolver;
|
|
5341
7259
|
/**
|
|
@@ -5391,6 +7309,9 @@ export interface Resolvers {
|
|
|
5391
7309
|
* The manifest of a package, as validated. The display fields and
|
|
5392
7310
|
* `minPlayerVersion` are optional (player 0.2.0); see the API reference for
|
|
5393
7311
|
* their rules.
|
|
7312
|
+
*
|
|
7313
|
+
* @since 1.0.0
|
|
7314
|
+
* @category Packages
|
|
5394
7315
|
*/
|
|
5395
7316
|
export interface PackageManifest {
|
|
5396
7317
|
/** Plugin id. */
|
|
@@ -5431,6 +7352,9 @@ export interface PackageManifest {
|
|
|
5431
7352
|
* upgrade the plugin and keep its data; links and local files count as one
|
|
5432
7353
|
* source. `bundled` marks the player's built-in plugins (set by the player
|
|
5433
7354
|
* only).
|
|
7355
|
+
*
|
|
7356
|
+
* @since 1.1.0
|
|
7357
|
+
* @category Packages
|
|
5434
7358
|
*/
|
|
5435
7359
|
export type PackageSource = {
|
|
5436
7360
|
/** A repository. */
|
|
@@ -5452,7 +7376,12 @@ export type PackageSource = {
|
|
|
5452
7376
|
/** Shipped with the player (built-in). */
|
|
5453
7377
|
kind: "bundled";
|
|
5454
7378
|
};
|
|
5455
|
-
/**
|
|
7379
|
+
/**
|
|
7380
|
+
* Options of {@link PackageCandidate.install}.
|
|
7381
|
+
*
|
|
7382
|
+
* @since 1.1.0
|
|
7383
|
+
* @category Packages
|
|
7384
|
+
*/
|
|
5456
7385
|
export interface InstallOptions {
|
|
5457
7386
|
/**
|
|
5458
7387
|
* Where the package comes from (default `{ kind: "file" }`). Repository
|
|
@@ -5467,7 +7396,34 @@ export interface InstallOptions {
|
|
|
5467
7396
|
*/
|
|
5468
7397
|
replace?: boolean;
|
|
5469
7398
|
}
|
|
5470
|
-
/**
|
|
7399
|
+
/**
|
|
7400
|
+
* A validated package directory, from {@link Packages.validate}.
|
|
7401
|
+
*
|
|
7402
|
+
* @remarks
|
|
7403
|
+
* A package the player has checked, from {@link Packages.validate} or
|
|
7404
|
+
* {@link Packages.validateArchive}: its manifest, digest and the checks
|
|
7405
|
+
* it passed. The player copies the whole package when it checks it and
|
|
7406
|
+
* installs that copy, so later changes to the directory do not matter.
|
|
7407
|
+
* Installing starts a {@link PackageChange}; the player keeps only the 32
|
|
7408
|
+
* most recent candidates.
|
|
7409
|
+
*
|
|
7410
|
+
* @capability plugins.manage
|
|
7411
|
+
*
|
|
7412
|
+
* @example Show what a package asks for before installing it
|
|
7413
|
+
* Before installing, the plugin shows the package's name and the
|
|
7414
|
+
* capabilities it declares. The candidate's `manifest` is the manifest
|
|
7415
|
+
* as the player read it.
|
|
7416
|
+
* ```ts
|
|
7417
|
+
* declare const directory: string;
|
|
7418
|
+
* // ---cut---
|
|
7419
|
+
* const candidate = await plugin.packages.validate(directory);
|
|
7420
|
+
* const { id, version, capabilities } = candidate.manifest;
|
|
7421
|
+
* await plugin.osd.show("{} {}: {}", [id, version, capabilities.join(", ")]);
|
|
7422
|
+
* ```
|
|
7423
|
+
*
|
|
7424
|
+
* @since 1.0.0
|
|
7425
|
+
* @category Packages
|
|
7426
|
+
*/
|
|
5471
7427
|
export interface PackageCandidate {
|
|
5472
7428
|
/** The package directory. */
|
|
5473
7429
|
readonly directory: string;
|
|
@@ -5484,28 +7440,138 @@ export interface PackageCandidate {
|
|
|
5484
7440
|
* player). A reserved id (`vestra` or `vestra.*`, which every built-in
|
|
5485
7441
|
* uses) fails with `reserved` (player 0.4.1; already when validated), as
|
|
5486
7442
|
* only the player installs built-in plugins.
|
|
7443
|
+
*
|
|
7444
|
+
* @capability plugins.manage
|
|
7445
|
+
* @throws `conflict` (rejects) The plugin is installed from another source
|
|
7446
|
+
* and `replace` is not set.
|
|
7447
|
+
* @throws `unsupported` (rejects) A built-in plugin's id.
|
|
7448
|
+
* @throws `reserved` (rejects) A reserved id (`vestra`, `vestra.*`).
|
|
7449
|
+
* @throws `busy` (rejects) The plugin library is being cleaned up, or the
|
|
7450
|
+
* player already keeps 64 change records.
|
|
7451
|
+
* @throws `revisionConflict` (rejects) The installed revision of this
|
|
7452
|
+
* plugin changed meanwhile; call `install` again (no new validation
|
|
7453
|
+
* needed).
|
|
7454
|
+
* @throws `stale` (rejects) The candidate is older than the 32 the player
|
|
7455
|
+
* keeps.
|
|
7456
|
+
* @throws `capability` (rejects) The manifest does not declare
|
|
7457
|
+
* `plugins.manage`.
|
|
7458
|
+
*
|
|
7459
|
+
* @example Replace a plugin installed from another source
|
|
7460
|
+
* The new package replaces the installed plugin even though it came from
|
|
7461
|
+
* another source. Without `replace` the player refuses with `conflict`;
|
|
7462
|
+
* the same source simply upgrades and keeps the plugin's data.
|
|
7463
|
+
* ```ts
|
|
7464
|
+
* declare const candidate: PackageCandidate;
|
|
7465
|
+
* // ---cut---
|
|
7466
|
+
* const change = await candidate.install({ replace: true });
|
|
7467
|
+
* await change.done; // "committed" once the new plugin runs
|
|
7468
|
+
* ```
|
|
7469
|
+
*
|
|
7470
|
+
* @since 1.0.0
|
|
5487
7471
|
*/
|
|
5488
7472
|
install(options?: InstallOptions): Promise<PackageChange>;
|
|
5489
7473
|
}
|
|
5490
|
-
/**
|
|
7474
|
+
/**
|
|
7475
|
+
* The progress of an install or uninstall.
|
|
7476
|
+
*
|
|
7477
|
+
* @since 1.0.0
|
|
7478
|
+
* @category Packages
|
|
7479
|
+
*/
|
|
5491
7480
|
export type PackageChangeStatus = "preparing" | "activating" | "restoring" | "committed" | "cancelled" | "failed" | "failedDegraded" | "tooLate";
|
|
5492
7481
|
/**
|
|
5493
7482
|
* An install or uninstall in progress. The SDK never submits a
|
|
5494
7483
|
* change twice: after a lost reply it only queries the same request.
|
|
7484
|
+
*
|
|
7485
|
+
* @remarks
|
|
7486
|
+
* An install or uninstall the player carries out in steps (`preparing`,
|
|
7487
|
+
* `activating`, possibly `restoring`) until a terminal status. The SDK
|
|
7488
|
+
* asks the player for the status about every 400 ms until then; `done`
|
|
7489
|
+
* resolves with the terminal status and does not reject for a failed
|
|
7490
|
+
* change: `message` says why.
|
|
7491
|
+
*
|
|
7492
|
+
* @capability plugins.manage
|
|
7493
|
+
*
|
|
7494
|
+
* @example Report each step of an install
|
|
7495
|
+
* The OSD shows each step of the install and the end result. `onChange`
|
|
7496
|
+
* runs for every new status; `done` resolves when the change is over.
|
|
7497
|
+
* ```ts
|
|
7498
|
+
* declare const change: PackageChange;
|
|
7499
|
+
* // ---cut---
|
|
7500
|
+
* change.onChange((status) => void plugin.osd.show("Install: {}", [status]));
|
|
7501
|
+
* const end = await change.done;
|
|
7502
|
+
* if (end !== "committed") await plugin.osd.show("Not installed: {}", [change.message ?? end]);
|
|
7503
|
+
* ```
|
|
7504
|
+
*
|
|
7505
|
+
* @since 1.0.0
|
|
7506
|
+
* @category Packages
|
|
5495
7507
|
*/
|
|
5496
7508
|
export interface PackageChange {
|
|
5497
7509
|
/** Current status. */
|
|
5498
7510
|
readonly status: PackageChangeStatus;
|
|
5499
7511
|
/** Details for failures, `null` otherwise. */
|
|
5500
7512
|
readonly message: string | null;
|
|
5501
|
-
/**
|
|
7513
|
+
/**
|
|
7514
|
+
* The status changed.
|
|
7515
|
+
*
|
|
7516
|
+
* @capability plugins.manage
|
|
7517
|
+
*
|
|
7518
|
+
* @example Close a progress window when the change ends
|
|
7519
|
+
* The plugin's progress window closes once the install is over, whatever
|
|
7520
|
+
* the outcome. The listener receives each new status; the terminal ones
|
|
7521
|
+
* are `committed`, `cancelled`, `failed`, `failedDegraded` and `tooLate`
|
|
7522
|
+
* (`tooLate` only answers a `cancel` of a change that had committed, and
|
|
7523
|
+
* `committed` may follow it).
|
|
7524
|
+
* ```ts
|
|
7525
|
+
* declare const change: PackageChange;
|
|
7526
|
+
* declare const progress: Window;
|
|
7527
|
+
* // ---cut---
|
|
7528
|
+
* change.onChange((status) => {
|
|
7529
|
+
* if (["committed", "cancelled", "failed", "failedDegraded", "tooLate"].includes(status))
|
|
7530
|
+
* void progress.close();
|
|
7531
|
+
* });
|
|
7532
|
+
* ```
|
|
7533
|
+
*
|
|
7534
|
+
* @since 1.0.0
|
|
7535
|
+
*/
|
|
5502
7536
|
onChange(listener: (status: PackageChangeStatus) => void): Disposable;
|
|
5503
|
-
/**
|
|
7537
|
+
/**
|
|
7538
|
+
* Resolves with the final status.
|
|
7539
|
+
*
|
|
7540
|
+
* @throws `missing` (rejects) The player no longer knows the change (a
|
|
7541
|
+
* `HostError` with that code); a failed change is a terminal status,
|
|
7542
|
+
* not a rejection.
|
|
7543
|
+
*/
|
|
5504
7544
|
readonly done: Promise<PackageChangeStatus>;
|
|
5505
|
-
/**
|
|
7545
|
+
/**
|
|
7546
|
+
* Ask to cancel (it may be `tooLate`).
|
|
7547
|
+
*
|
|
7548
|
+
* @capability plugins.manage
|
|
7549
|
+
* @throws `capability` (rejects) The manifest does not declare
|
|
7550
|
+
* `plugins.manage`.
|
|
7551
|
+
*
|
|
7552
|
+
* @example Cancel an install the user no longer wants
|
|
7553
|
+
* The install stops if it has not gone too far. `cancel` asks the player;
|
|
7554
|
+
* the change then ends `cancelled`. When the change had already committed
|
|
7555
|
+
* the player answers `tooLate` and `done` still resolves `committed`; a
|
|
7556
|
+
* change that is restoring cannot be cancelled and ends `failed` or
|
|
7557
|
+
* `failedDegraded`.
|
|
7558
|
+
* ```ts
|
|
7559
|
+
* declare const change: PackageChange;
|
|
7560
|
+
* // ---cut---
|
|
7561
|
+
* await change.cancel();
|
|
7562
|
+
* console.log(await change.done); // "cancelled"; "committed" when it was too late
|
|
7563
|
+
* ```
|
|
7564
|
+
*
|
|
7565
|
+
* @since 1.0.0
|
|
7566
|
+
*/
|
|
5506
7567
|
cancel(): Promise<void>;
|
|
5507
7568
|
}
|
|
5508
|
-
/**
|
|
7569
|
+
/**
|
|
7570
|
+
* One plugin as the player knows it (`packages.list`).
|
|
7571
|
+
*
|
|
7572
|
+
* @since 1.0.0
|
|
7573
|
+
* @category Packages
|
|
7574
|
+
*/
|
|
5509
7575
|
export interface PluginInfo {
|
|
5510
7576
|
/** Manifest id. */
|
|
5511
7577
|
id: string;
|
|
@@ -5564,6 +7630,8 @@ export interface PluginInfo {
|
|
|
5564
7630
|
* `description` and `tags` as the player shows them.
|
|
5565
7631
|
*
|
|
5566
7632
|
* @since 1.5.9
|
|
7633
|
+
*
|
|
7634
|
+
* @category Packages
|
|
5567
7635
|
*/
|
|
5568
7636
|
export interface PluginDisplay {
|
|
5569
7637
|
/** `name`, translated; `null` when the manifest has none. */
|
|
@@ -5579,6 +7647,8 @@ export interface PluginDisplay {
|
|
|
5579
7647
|
* One refusal in {@link PluginInfo.warnings}.
|
|
5580
7648
|
*
|
|
5581
7649
|
* @since 1.5.2
|
|
7650
|
+
*
|
|
7651
|
+
* @category Packages
|
|
5582
7652
|
*/
|
|
5583
7653
|
export interface PluginWarning {
|
|
5584
7654
|
/**
|
|
@@ -5615,6 +7685,17 @@ export interface PluginWarning {
|
|
|
5615
7685
|
* // `message` says why a change did not commit.
|
|
5616
7686
|
* if ((await change.done) !== "committed") throw new Error(change.message ?? "install failed");
|
|
5617
7687
|
* ```
|
|
7688
|
+
*
|
|
7689
|
+
* @remarks
|
|
7690
|
+
* The plugins the player knows: installed, built-in and this run's
|
|
7691
|
+
* development loads. Reading the list needs no capability; installing,
|
|
7692
|
+
* enabling, reloading and uninstalling need `plugins.manage` and act on
|
|
7693
|
+
* other plugins, never on the calling one. Built-in plugins update with
|
|
7694
|
+
* the player and cannot be installed, disabled or uninstalled.
|
|
7695
|
+
*
|
|
7696
|
+
* @capability None for `list` and `onChange`; plugins.manage for the rest.
|
|
7697
|
+
* @since 1.0.0
|
|
7698
|
+
* @category Packages
|
|
5618
7699
|
*/
|
|
5619
7700
|
export interface Packages {
|
|
5620
7701
|
/**
|
|
@@ -5625,6 +7706,37 @@ export interface Packages {
|
|
|
5625
7706
|
* The player keeps the 32 most recent candidates of all plugins; an
|
|
5626
7707
|
* older one's `install` fails with `stale` (validate again). Before
|
|
5627
7708
|
* player 0.4.2 the 33rd validation of a run failed with `busy` instead.
|
|
7709
|
+
*
|
|
7710
|
+
* @capability plugins.manage
|
|
7711
|
+
* @throws `host` (rejects) Not a package directory, or a manifest that
|
|
7712
|
+
* breaks the rules (another `apiVersion` included); `message` says why.
|
|
7713
|
+
* @throws `invalid` (rejects) A bad request (a path that is not a string).
|
|
7714
|
+
* @throws `javascript` (rejects) The entry module does not parse.
|
|
7715
|
+
* @throws `slint` (rejects) A Slint file does not compile.
|
|
7716
|
+
* @throws `reserved` (rejects) A reserved id (`vestra`, `vestra.*`).
|
|
7717
|
+
* @throws `versionMismatch` (rejects) The package's `minPlayerVersion` is
|
|
7718
|
+
* newer than the player.
|
|
7719
|
+
* @throws `disabled` (rejects) The calling plugin is stopping.
|
|
7720
|
+
* @throws `capability` (rejects) The manifest does not declare
|
|
7721
|
+
* `plugins.manage`.
|
|
7722
|
+
*
|
|
7723
|
+
* @example Check a package directory before installing it
|
|
7724
|
+
* The plugin checks a folder it unpacked and installs it if the player
|
|
7725
|
+
* accepts it. `validate` runs the player's checks and resolves with a
|
|
7726
|
+
* candidate; a package that fails them rejects with the reason.
|
|
7727
|
+
* ```ts
|
|
7728
|
+
* declare const directory: string;
|
|
7729
|
+
* // ---cut---
|
|
7730
|
+
* try {
|
|
7731
|
+
* // Rejects with the reason when the package fails the checks.
|
|
7732
|
+
* const candidate = await plugin.packages.validate(directory);
|
|
7733
|
+
* await (await candidate.install()).done;
|
|
7734
|
+
* } catch (error) {
|
|
7735
|
+
* await plugin.osd.show("Not a valid plugin: {}", [(error as Error).message]);
|
|
7736
|
+
* }
|
|
7737
|
+
* ```
|
|
7738
|
+
*
|
|
7739
|
+
* @since 1.0.0
|
|
5628
7740
|
*/
|
|
5629
7741
|
validate(directory: string): Promise<PackageCandidate>;
|
|
5630
7742
|
/**
|
|
@@ -5632,19 +7744,97 @@ export interface Packages {
|
|
|
5632
7744
|
* same checks as {@link Packages.validate} after unpacking. Check the
|
|
5633
7745
|
* file's sha256 yourself before calling when you know it. A reserved id
|
|
5634
7746
|
* fails with `reserved`, as in {@link Packages.validate}.
|
|
7747
|
+
*
|
|
7748
|
+
* @capability plugins.manage
|
|
7749
|
+
* @throws `invalid` (rejects) The path does not end in `.vxp`, or the
|
|
7750
|
+
* archive cannot be unpacked (over 64 MB, too many files, a damaged zip,
|
|
7751
|
+
* an unsafe path).
|
|
7752
|
+
* @throws `host` (rejects) The unpacked package or its manifest breaks the
|
|
7753
|
+
* rules; `message` says why.
|
|
7754
|
+
* @throws `javascript` (rejects) The entry module does not parse.
|
|
7755
|
+
* @throws `slint` (rejects) A Slint file does not compile.
|
|
7756
|
+
* @throws `reserved` (rejects) A reserved id.
|
|
7757
|
+
* @throws `versionMismatch` (rejects) The package's `minPlayerVersion` is
|
|
7758
|
+
* newer than the player.
|
|
7759
|
+
* @throws `disabled` (rejects) The calling plugin is stopping.
|
|
7760
|
+
* @throws `capability` (rejects) The manifest does not declare
|
|
7761
|
+
* `plugins.manage`.
|
|
7762
|
+
*
|
|
7763
|
+
* @example Install a downloaded package file
|
|
7764
|
+
* A `.vxp` the plugin downloaded is checked and installed. The player
|
|
7765
|
+
* unpacks and checks it like a directory; checking the file's sha256
|
|
7766
|
+
* first, when the store publishes one, is up to the plugin.
|
|
7767
|
+
* ```ts
|
|
7768
|
+
* declare const file: string;
|
|
7769
|
+
* // ---cut---
|
|
7770
|
+
* const candidate = await plugin.packages.validateArchive(file); // a .vxp path
|
|
7771
|
+
* const change = await candidate.install();
|
|
7772
|
+
* await plugin.osd.show("Install: {}", [await change.done]);
|
|
7773
|
+
* ```
|
|
7774
|
+
*
|
|
7775
|
+
* @since 1.1.0
|
|
5635
7776
|
*/
|
|
5636
7777
|
validateArchive(file: string): Promise<PackageCandidate>;
|
|
5637
|
-
/**
|
|
7778
|
+
/**
|
|
7779
|
+
* Every plugin: running, installed, and this run's development loads. No capability.
|
|
7780
|
+
*
|
|
7781
|
+
* @capability None.
|
|
7782
|
+
* @throws `Error` (rejects) Only when the player cannot be asked; no
|
|
7783
|
+
* capability is needed.
|
|
7784
|
+
*
|
|
7785
|
+
* @example Show which plugins are running
|
|
7786
|
+
* The plugin lists the running plugins by name. `list` asks the player
|
|
7787
|
+
* and resolves with every plugin it knows, with its state, origin and
|
|
7788
|
+
* display texts in the interface language.
|
|
7789
|
+
* ```ts
|
|
7790
|
+
* const plugins = await plugin.packages.list();
|
|
7791
|
+
* const running = plugins.filter((info) => info.state === "running");
|
|
7792
|
+
* console.log(running.map((info) => info.display?.name ?? info.id));
|
|
7793
|
+
* ```
|
|
7794
|
+
*
|
|
7795
|
+
* @since 1.0.0
|
|
7796
|
+
*/
|
|
5638
7797
|
list(): Promise<PluginInfo[]>;
|
|
5639
|
-
/**
|
|
7798
|
+
/**
|
|
7799
|
+
* The new list after any start, stop, failure, install or uninstall. No capability.
|
|
7800
|
+
*
|
|
7801
|
+
* @capability None.
|
|
7802
|
+
*
|
|
7803
|
+
* @example Refresh a plugin list after any change
|
|
7804
|
+
* A store window's list updates when a plugin starts, stops, fails, is
|
|
7805
|
+
* installed or uninstalled. The listener receives the new list each time.
|
|
7806
|
+
* ```ts
|
|
7807
|
+
* plugin.packages.onChange((plugins) => console.log(plugins.length, "plugins"));
|
|
7808
|
+
* ```
|
|
7809
|
+
*
|
|
7810
|
+
* @since 1.0.0
|
|
7811
|
+
*/
|
|
5640
7812
|
onChange(listener: (plugins: PluginInfo[]) => void): Disposable;
|
|
5641
7813
|
/**
|
|
5642
7814
|
* Stop or start a plugin (`plugins.manage`). An installed plugin stays
|
|
5643
7815
|
* disabled across launches; a development load only for this run.
|
|
5644
7816
|
* `invalid` to disable oneself, `missing` unknown.
|
|
5645
|
-
* `busy` while it is being installed. `unsupported` to disable a built-in
|
|
7817
|
+
* `busy` while it is being installed or reloaded. `unsupported` to disable a built-in
|
|
5646
7818
|
* ({@link PluginInfo.builtin}): users manage third-party plugins only
|
|
5647
7819
|
* (players released with SDK 1.5.18 on; older ones disabled built-ins too).
|
|
7820
|
+
*
|
|
7821
|
+
* @capability plugins.manage
|
|
7822
|
+
* @throws `invalid` (rejects) Disabling the calling plugin itself.
|
|
7823
|
+
* @throws `missing` (rejects) An unknown id.
|
|
7824
|
+
* @throws `busy` (rejects) The plugin is being installed or reloaded.
|
|
7825
|
+
* @throws `unsupported` (rejects) A built-in plugin.
|
|
7826
|
+
* @throws `capability` (rejects) The manifest does not declare
|
|
7827
|
+
* `plugins.manage`.
|
|
7828
|
+
*
|
|
7829
|
+
* @example Turn a third-party plugin off
|
|
7830
|
+
* The plugin stops another plugin and keeps it disabled across launches
|
|
7831
|
+
* until it is enabled again. Built-in plugins cannot be disabled
|
|
7832
|
+
* (`unsupported`).
|
|
7833
|
+
* ```ts
|
|
7834
|
+
* await plugin.packages.setEnabled("someone.demo", false);
|
|
7835
|
+
* ```
|
|
7836
|
+
*
|
|
7837
|
+
* @since 1.0.0
|
|
5648
7838
|
*/
|
|
5649
7839
|
setEnabled(id: string, enabled: boolean): Promise<void>;
|
|
5650
7840
|
/**
|
|
@@ -5665,6 +7855,23 @@ export interface Packages {
|
|
|
5665
7855
|
* @player 0.5.1; older players reject it as an unknown request
|
|
5666
7856
|
* (`unsupported`).
|
|
5667
7857
|
* @since 1.5.19
|
|
7858
|
+
*
|
|
7859
|
+
* @capability plugins.manage
|
|
7860
|
+
* @throws `invalid` (rejects) The calling plugin itself, or a disabled
|
|
7861
|
+
* plugin.
|
|
7862
|
+
* @throws `missing` (rejects) An unknown id.
|
|
7863
|
+
* @throws `busy` (rejects) The plugin is being installed or reloaded.
|
|
7864
|
+
* @throws `unsupported` (rejects) A player before 0.5.1.
|
|
7865
|
+
* @throws `capability` (rejects) The manifest does not declare
|
|
7866
|
+
* `plugins.manage`.
|
|
7867
|
+
*
|
|
7868
|
+
* @example Restart a plugin that misbehaves
|
|
7869
|
+
* The other plugin is stopped and started again from the same package and
|
|
7870
|
+
* data. `reload` resolves once the old process is stopped and the start is
|
|
7871
|
+
* scheduled; `onChange` then shows `starting` and `running` (or `failed`).
|
|
7872
|
+
* ```ts
|
|
7873
|
+
* await plugin.packages.reload("someone.demo");
|
|
7874
|
+
* ```
|
|
5668
7875
|
*/
|
|
5669
7876
|
reload(id: string): Promise<void>;
|
|
5670
7877
|
/**
|
|
@@ -5673,19 +7880,51 @@ export interface Packages {
|
|
|
5673
7880
|
* reconnects to its data. Its user bindings stay (as orphans).
|
|
5674
7881
|
* `unsupported` for built-ins and development loads, `missing`
|
|
5675
7882
|
* unknown, `invalid` for oneself.
|
|
7883
|
+
*
|
|
7884
|
+
* @capability plugins.manage
|
|
7885
|
+
* @throws `unsupported` (rejects) A built-in plugin or a development load.
|
|
7886
|
+
* @throws `missing` (rejects) An unknown id.
|
|
7887
|
+
* @throws `invalid` (rejects) The calling plugin itself.
|
|
7888
|
+
* @throws `busy` (rejects) The plugin library is being cleaned up, or that
|
|
7889
|
+
* plugin is being installed or reloaded.
|
|
7890
|
+
* @throws `revisionConflict` (rejects) The installed revision of that
|
|
7891
|
+
* plugin changed meanwhile; call `uninstall` again.
|
|
7892
|
+
* @throws `capability` (rejects) The manifest does not declare
|
|
7893
|
+
* `plugins.manage`.
|
|
7894
|
+
*
|
|
7895
|
+
* @example Uninstall a plugin together with its data
|
|
7896
|
+
* The other plugin is removed and its data directory deleted, so a later
|
|
7897
|
+
* reinstall starts fresh. Without `removeData` a reinstall from the same
|
|
7898
|
+
* source finds its data again.
|
|
7899
|
+
* ```ts
|
|
7900
|
+
* const change = await plugin.packages.uninstall("someone.demo", { removeData: true });
|
|
7901
|
+
* await change.done;
|
|
7902
|
+
* ```
|
|
7903
|
+
*
|
|
7904
|
+
* @since 1.0.0
|
|
5676
7905
|
*/
|
|
5677
7906
|
uninstall(id: string, options?: {
|
|
5678
7907
|
removeData?: boolean;
|
|
5679
7908
|
}): Promise<PackageChange>;
|
|
5680
7909
|
}
|
|
5681
|
-
/**
|
|
7910
|
+
/**
|
|
7911
|
+
* A command: manifest id (`"host"` for the player's own) and command id.
|
|
7912
|
+
*
|
|
7913
|
+
* @since 1.0.0
|
|
7914
|
+
* @category Commands
|
|
7915
|
+
*/
|
|
5682
7916
|
export interface BindingRef {
|
|
5683
7917
|
/** Manifest id, or `"host"`. */
|
|
5684
7918
|
plugin: string;
|
|
5685
7919
|
/** Command id within that plugin. */
|
|
5686
7920
|
command: string;
|
|
5687
7921
|
}
|
|
5688
|
-
/**
|
|
7922
|
+
/**
|
|
7923
|
+
* One command's bindings, from {@link Bindings.list}.
|
|
7924
|
+
*
|
|
7925
|
+
* @since 1.0.0
|
|
7926
|
+
* @category Commands
|
|
7927
|
+
*/
|
|
5689
7928
|
export interface BindingInfo extends BindingRef {
|
|
5690
7929
|
/** Menu title. */
|
|
5691
7930
|
title: string;
|
|
@@ -5735,9 +7974,40 @@ export interface BindingInfo extends BindingRef {
|
|
|
5735
7974
|
* // later
|
|
5736
7975
|
* await plugin.bindings.reset({ plugin: "host", command: "mute" });
|
|
5737
7976
|
* ```
|
|
7977
|
+
*
|
|
7978
|
+
* @remarks
|
|
7979
|
+
* The player's binding table: every command's default keys (from
|
|
7980
|
+
* {@link CommandOptions.bindings}, the player's own as `host/<action>`)
|
|
7981
|
+
* and the user's overrides on top, saved across launches. A key-settings
|
|
7982
|
+
* plugin lists the table, writes overrides and resets them; the player
|
|
7983
|
+
* applies the change at once to every running plugin.
|
|
7984
|
+
*
|
|
7985
|
+
* @capability bindings.manage
|
|
7986
|
+
* @see [Commands, menus and key bindings](../../docs/concepts/commands.md)
|
|
7987
|
+
* @since 1.0.0
|
|
7988
|
+
* @category Commands
|
|
5738
7989
|
*/
|
|
5739
7990
|
export interface Bindings {
|
|
5740
|
-
/**
|
|
7991
|
+
/**
|
|
7992
|
+
* Every declared command, and overrides whose command is gone.
|
|
7993
|
+
*
|
|
7994
|
+
* @capability bindings.manage
|
|
7995
|
+
* @throws `capability` (rejects) The manifest does not declare
|
|
7996
|
+
* `bindings.manage`.
|
|
7997
|
+
*
|
|
7998
|
+
* @example Find the commands that use a key
|
|
7999
|
+
* A key-settings window shows which commands Ctrl+K is bound to now. `list`
|
|
8000
|
+
* resolves with every declared command and its current bindings (the
|
|
8001
|
+
* defaults with the user's overrides applied) and with the overrides
|
|
8002
|
+
* whose command is not running (`orphans`).
|
|
8003
|
+
* ```ts
|
|
8004
|
+
* const { commands } = await plugin.bindings.list();
|
|
8005
|
+
* const users = commands.filter((command) => command.current.includes("Ctrl+K"));
|
|
8006
|
+
* console.log(users.map((command) => `${command.plugin}/${command.command}`)); // "host" for the player
|
|
8007
|
+
* ```
|
|
8008
|
+
*
|
|
8009
|
+
* @since 1.0.0
|
|
8010
|
+
*/
|
|
5741
8011
|
list(): Promise<{
|
|
5742
8012
|
/** Every declared command (running plugins and the player). */
|
|
5743
8013
|
commands: BindingInfo[];
|
|
@@ -5754,11 +8024,65 @@ export interface Bindings {
|
|
|
5754
8024
|
* `invalid` for a bad binding, `missing` for an unknown command that has
|
|
5755
8025
|
* no override either.
|
|
5756
8026
|
* At most 16 inputs, syntax as {@link CommandOptions.bindings}.
|
|
8027
|
+
*
|
|
8028
|
+
* @capability bindings.manage
|
|
8029
|
+
* @throws `invalid` (rejects) A malformed binding, or more than 16.
|
|
8030
|
+
* @throws `missing` (rejects) An unknown command that has no override
|
|
8031
|
+
* either.
|
|
8032
|
+
* @throws `capability` (rejects) The manifest does not declare
|
|
8033
|
+
* `bindings.manage`.
|
|
8034
|
+
*
|
|
8035
|
+
* @example Give the player's Mute command a second key
|
|
8036
|
+
* Mute now answers to M and to Ctrl+M. `set` replaces the command's
|
|
8037
|
+
* bindings with the list given (an override saved across launches), so it
|
|
8038
|
+
* repeats the default it keeps; `[]` would unbind it.
|
|
8039
|
+
* ```ts
|
|
8040
|
+
* await plugin.bindings.set({ plugin: "host", command: "mute" }, ["M", "Ctrl+M"]);
|
|
8041
|
+
* ```
|
|
8042
|
+
*
|
|
8043
|
+
* @since 1.0.0
|
|
5757
8044
|
*/
|
|
5758
8045
|
set(ref: BindingRef, inputs: string[]): Promise<void>;
|
|
5759
|
-
/**
|
|
8046
|
+
/**
|
|
8047
|
+
* Back to the defaults: one command, or every override when omitted.
|
|
8048
|
+
*
|
|
8049
|
+
* @capability bindings.manage
|
|
8050
|
+
* @throws `missing` (rejects) A command that has no override and is not
|
|
8051
|
+
* declared.
|
|
8052
|
+
* @throws `capability` (rejects) The manifest does not declare
|
|
8053
|
+
* `bindings.manage`.
|
|
8054
|
+
*
|
|
8055
|
+
* @example Restore every default key
|
|
8056
|
+
* A "Restore defaults" button removes all of the user's overrides, so
|
|
8057
|
+
* every command has its declared keys again. Without an argument `reset`
|
|
8058
|
+
* clears every override; with a reference only that command's.
|
|
8059
|
+
* ```ts
|
|
8060
|
+
* await plugin.bindings.reset(); // every override
|
|
8061
|
+
* ```
|
|
8062
|
+
*
|
|
8063
|
+
* @since 1.0.0
|
|
8064
|
+
*/
|
|
5760
8065
|
reset(ref?: BindingRef): Promise<void>;
|
|
5761
|
-
/**
|
|
8066
|
+
/**
|
|
8067
|
+
* Declarations or overrides changed (read `list()` again). At most every 200 ms.
|
|
8068
|
+
*
|
|
8069
|
+
* @capability bindings.manage
|
|
8070
|
+
*
|
|
8071
|
+
* @example Refresh a key list when bindings change
|
|
8072
|
+
* The plugin's key list stays current when the user changes a key in the
|
|
8073
|
+
* player or another plugin starts. The listener carries no data and runs
|
|
8074
|
+
* at most every 200 ms; the plugin reads the table again with `list`.
|
|
8075
|
+
* ```ts
|
|
8076
|
+
* declare function render(commands: BindingInfo[]): void;
|
|
8077
|
+
* // ---cut---
|
|
8078
|
+
* plugin.bindings.onChange(async () => {
|
|
8079
|
+
* const { commands } = await plugin.bindings.list(); // read the new table
|
|
8080
|
+
* render(commands);
|
|
8081
|
+
* });
|
|
8082
|
+
* ```
|
|
8083
|
+
*
|
|
8084
|
+
* @since 1.0.0
|
|
8085
|
+
*/
|
|
5762
8086
|
onChange(listener: () => void): Disposable;
|
|
5763
8087
|
}
|
|
5764
8088
|
/**
|
|
@@ -5782,16 +8106,109 @@ export interface Bindings {
|
|
|
5782
8106
|
* void plugin.osd.show("Could not copy: {}", [(error as PluginError).code]);
|
|
5783
8107
|
* }
|
|
5784
8108
|
* ```
|
|
8109
|
+
*
|
|
8110
|
+
* @remarks
|
|
8111
|
+
* The player reads and writes the system clipboard on the plugin's behalf.
|
|
8112
|
+
* Text is plain text; images are written from a PNG or JPEG file.
|
|
8113
|
+
*
|
|
8114
|
+
* @capability clipboard
|
|
8115
|
+
* @since 1.0.0
|
|
8116
|
+
* @category Services
|
|
5785
8117
|
*/
|
|
5786
8118
|
export interface Clipboard {
|
|
5787
|
-
/**
|
|
8119
|
+
/**
|
|
8120
|
+
* Plain text on the clipboard; `null` when it holds none.
|
|
8121
|
+
*
|
|
8122
|
+
* @capability clipboard
|
|
8123
|
+
* @throws `unavailable` (rejects) The player runs without a user interface.
|
|
8124
|
+
* @throws `unsupported` (rejects) A system other than macOS and Windows.
|
|
8125
|
+
* @throws `failed` (rejects) The system refused (Windows).
|
|
8126
|
+
* @throws `capability` (rejects) The manifest does not declare
|
|
8127
|
+
* `clipboard`.
|
|
8128
|
+
*
|
|
8129
|
+
* @example Open a URL the user copied
|
|
8130
|
+
* A "Paste and play" command opens the address on the clipboard. `readText`
|
|
8131
|
+
* resolves with the clipboard's text, or `null` when it holds none (an
|
|
8132
|
+
* image, nothing).
|
|
8133
|
+
* ```ts
|
|
8134
|
+
* const text = await plugin.clipboard.readText();
|
|
8135
|
+
* if (text?.startsWith("https://")) await plugin.player.open(text.trim());
|
|
8136
|
+
* ```
|
|
8137
|
+
*
|
|
8138
|
+
* @since 1.0.0
|
|
8139
|
+
*/
|
|
5788
8140
|
readText(): Promise<string | null>;
|
|
5789
|
-
/**
|
|
8141
|
+
/**
|
|
8142
|
+
* Replace the clipboard with text (at most 1 MiB of UTF-8).
|
|
8143
|
+
*
|
|
8144
|
+
* @capability clipboard
|
|
8145
|
+
* @throws `invalid` (rejects) More than 1 MiB of UTF-8.
|
|
8146
|
+
* @throws `unavailable` (rejects) The player runs without a user interface.
|
|
8147
|
+
* @throws `unsupported` (rejects) A system other than macOS and Windows.
|
|
8148
|
+
* @throws `failed` (rejects) The system refused.
|
|
8149
|
+
* @throws `capability` (rejects) The manifest does not declare
|
|
8150
|
+
* `clipboard`.
|
|
8151
|
+
*
|
|
8152
|
+
* @example Copy the playing file's path
|
|
8153
|
+
* The path of the playing file is put on the clipboard, replacing what was
|
|
8154
|
+
* there. It resolves once the system has it.
|
|
8155
|
+
* ```ts
|
|
8156
|
+
* const { source } = plugin.player.state;
|
|
8157
|
+
* if (source) await plugin.clipboard.writeText(source);
|
|
8158
|
+
* ```
|
|
8159
|
+
*
|
|
8160
|
+
* @since 1.0.0
|
|
8161
|
+
*/
|
|
5790
8162
|
writeText(text: string): Promise<void>;
|
|
5791
|
-
/**
|
|
8163
|
+
/**
|
|
8164
|
+
* Replace the clipboard with a PNG or JPEG file (absolute path, at most 16 MB).
|
|
8165
|
+
*
|
|
8166
|
+
* @capability clipboard
|
|
8167
|
+
* @throws `missing` (rejects) The file does not exist.
|
|
8168
|
+
* @throws `invalid` (rejects) Not an absolute path, not PNG or JPEG, over
|
|
8169
|
+
* 16 MB, or (Windows) an image the system cannot decode.
|
|
8170
|
+
* @throws `unavailable` (rejects) The player runs without a user interface.
|
|
8171
|
+
* @throws `unsupported` (rejects) A system other than macOS and Windows.
|
|
8172
|
+
* @throws `failed` (rejects) The system refused.
|
|
8173
|
+
* @throws `capability` (rejects) The manifest does not declare
|
|
8174
|
+
* `clipboard`.
|
|
8175
|
+
*
|
|
8176
|
+
* @example Copy a snapshot to the clipboard
|
|
8177
|
+
* A frame the plugin saved as PNG is put on the clipboard, ready to paste
|
|
8178
|
+
* into another application. The player reads the file and writes the
|
|
8179
|
+
* image (macOS: its original bytes; Windows: a bitmap plus PNG).
|
|
8180
|
+
* ```ts
|
|
8181
|
+
* declare const pngPath: string;
|
|
8182
|
+
* // ---cut---
|
|
8183
|
+
* await plugin.clipboard.writeImage(pngPath); // absolute path, at most 16 MB
|
|
8184
|
+
* await plugin.osd.show("Snapshot copied");
|
|
8185
|
+
* ```
|
|
8186
|
+
*
|
|
8187
|
+
* @since 1.0.0
|
|
8188
|
+
*/
|
|
5792
8189
|
writeImage(path: string): Promise<void>;
|
|
5793
8190
|
}
|
|
5794
|
-
/**
|
|
8191
|
+
/**
|
|
8192
|
+
* Keeping the computer awake (`playback.control`).
|
|
8193
|
+
*
|
|
8194
|
+
* @remarks
|
|
8195
|
+
* A request, not a command: the player keeps the system awake while any
|
|
8196
|
+
* plugin asks for it and a source plays, and withdraws a plugin's request
|
|
8197
|
+
* when it stops.
|
|
8198
|
+
*
|
|
8199
|
+
* @capability playback.control
|
|
8200
|
+
*
|
|
8201
|
+
* @example Keep the display on while playing
|
|
8202
|
+
* The screen does not dim or sleep while a video plays, and may again when
|
|
8203
|
+
* it is paused or ends. The player takes the system's assertion only while
|
|
8204
|
+
* a source is ready and not paused.
|
|
8205
|
+
* ```ts
|
|
8206
|
+
* await plugin.power.preventSleep("playing"); // "off" withdraws the request
|
|
8207
|
+
* ```
|
|
8208
|
+
*
|
|
8209
|
+
* @since 1.0.0
|
|
8210
|
+
* @category Services
|
|
8211
|
+
*/
|
|
5795
8212
|
export interface Power {
|
|
5796
8213
|
/**
|
|
5797
8214
|
* `"playing"`: while a source is ready and not paused, keep the display
|
|
@@ -5802,10 +8219,32 @@ export interface Power {
|
|
|
5802
8219
|
* take the system assertion are logged by the player, not reported.
|
|
5803
8220
|
* macOS uses `caffeinate`, Windows the thread execution state; elsewhere
|
|
5804
8221
|
* it has no effect.
|
|
8222
|
+
*
|
|
8223
|
+
* @capability playback.control
|
|
8224
|
+
* @throws `invalid` (rejects) A mode other than `"off"` or `"playing"`.
|
|
8225
|
+
* @throws `capability` (rejects) The manifest does not declare
|
|
8226
|
+
* `playback.control`.
|
|
8227
|
+
*
|
|
8228
|
+
* @example Follow the plugin's own "Keep awake" setting
|
|
8229
|
+
* The plugin keeps the display awake during playback only while its
|
|
8230
|
+
* setting is on. Each call replaces this plugin's request; `"off"`
|
|
8231
|
+
* withdraws it, and other plugins' requests still count.
|
|
8232
|
+
* ```ts
|
|
8233
|
+
* const store = plugin.storage.json("power", { defaults: { keepAwake: true } });
|
|
8234
|
+
* await plugin.power.preventSleep(store.value.keepAwake ? "playing" : "off");
|
|
8235
|
+
* ```
|
|
8236
|
+
*
|
|
8237
|
+
* @since 1.0.0
|
|
5805
8238
|
*/
|
|
5806
8239
|
preventSleep(mode: "off" | "playing"): Promise<void>;
|
|
5807
8240
|
}
|
|
5808
|
-
/**
|
|
8241
|
+
/**
|
|
8242
|
+
* The output of {@link Tools.run}.
|
|
8243
|
+
*
|
|
8244
|
+
* @see {@link Tools.run}
|
|
8245
|
+
* @since 1.0.0
|
|
8246
|
+
* @category Services
|
|
8247
|
+
*/
|
|
5809
8248
|
export interface ToolResult {
|
|
5810
8249
|
/** Standard output (at most `maxOutput` bytes). */
|
|
5811
8250
|
stdout: Uint8Array;
|
|
@@ -5835,11 +8274,23 @@ export interface ToolResult {
|
|
|
5835
8274
|
* void info.format; // container, duration, bit rate…
|
|
5836
8275
|
* }
|
|
5837
8276
|
* ```
|
|
8277
|
+
*
|
|
8278
|
+
* @remarks
|
|
8279
|
+
* ffmpeg and ffprobe run as child processes of the plugin. When
|
|
8280
|
+
* `VESTRA_FFMPEG` / `VESTRA_FFPROBE` is set, only that path is used (an
|
|
8281
|
+
* invalid one means the tool is not found); otherwise the tool is searched
|
|
8282
|
+
* on `PATH`. A found path is kept for the plugin's run, a miss is looked up
|
|
8283
|
+
* again next time. Disposing the
|
|
8284
|
+
* scope ends a running process.
|
|
8285
|
+
*
|
|
8286
|
+
* @capability None.
|
|
8287
|
+
* @since 1.0.0
|
|
8288
|
+
* @category Services
|
|
5838
8289
|
*/
|
|
5839
8290
|
export interface Tools {
|
|
5840
8291
|
/**
|
|
5841
|
-
* Run ffmpeg or ffprobe (
|
|
5842
|
-
*
|
|
8292
|
+
* Run ffmpeg or ffprobe (`VESTRA_FFMPEG` / `VESTRA_FFPROBE` when set, else
|
|
8293
|
+
* found on `PATH`) without a shell. Rejects `unavailable` (not found), `timeout`,
|
|
5843
8294
|
* `failed` (non-zero exit; `message` is the last stderr line), `invalid`
|
|
5844
8295
|
* (output over `maxOutput`, default 16 MiB), `aborted` (`signal`). Timeout,
|
|
5845
8296
|
* `signal` and disposing the scope end the process.
|
|
@@ -5847,28 +8298,171 @@ export interface Tools {
|
|
|
5847
8298
|
* Arguments are passed as-is (no quoting needed). On Windows `PATHEXT` is
|
|
5848
8299
|
* honoured. Ending sends SIGTERM, then SIGKILL after 300 ms (immediate on
|
|
5849
8300
|
* Windows).
|
|
8301
|
+
*
|
|
8302
|
+
* @capability None.
|
|
8303
|
+
* @throws `unavailable` (rejects) The tool was not found.
|
|
8304
|
+
* @throws `timeout` (rejects) It ran longer than `timeoutMs`; the process is
|
|
8305
|
+
* ended.
|
|
8306
|
+
* @throws `failed` (rejects) A non-zero exit; `message` is the last stderr
|
|
8307
|
+
* line.
|
|
8308
|
+
* @throws `invalid` (rejects) Bad arguments or options, or output over
|
|
8309
|
+
* `maxOutput`.
|
|
8310
|
+
* @throws `aborted` (rejects) `signal` aborted, or the scope was disposed.
|
|
8311
|
+
*
|
|
8312
|
+
* @example Cut a clip without re-encoding
|
|
8313
|
+
* The plugin writes the first ten seconds of the file to a new file. `run`
|
|
8314
|
+
* starts ffmpeg without a shell (arguments pass as they are, no quoting)
|
|
8315
|
+
* and resolves with its output once it exits with 0; a failure rejects
|
|
8316
|
+
* with the last line ffmpeg printed.
|
|
8317
|
+
* ```ts
|
|
8318
|
+
* declare const input: string;
|
|
8319
|
+
* declare const output: string;
|
|
8320
|
+
* // ---cut---
|
|
8321
|
+
* await plugin.tools.run(
|
|
8322
|
+
* "ffmpeg",
|
|
8323
|
+
* ["-y", "-i", input, "-t", "10", "-c", "copy", output],
|
|
8324
|
+
* { timeoutMs: 60_000 }, // give up after a minute
|
|
8325
|
+
* );
|
|
8326
|
+
* ```
|
|
8327
|
+
*
|
|
8328
|
+
* @since 1.0.0
|
|
5850
8329
|
*/
|
|
5851
8330
|
run(tool: "ffmpeg" | "ffprobe", args: string[], options: {
|
|
5852
8331
|
timeoutMs: number;
|
|
5853
8332
|
maxOutput?: number;
|
|
5854
8333
|
signal?: AbortSignal;
|
|
5855
8334
|
}): Promise<ToolResult>;
|
|
5856
|
-
/**
|
|
8335
|
+
/**
|
|
8336
|
+
* Whether the tool can be found.
|
|
8337
|
+
*
|
|
8338
|
+
* @capability None.
|
|
8339
|
+
* @throws Never; it resolves `false` when the tool cannot be found.
|
|
8340
|
+
*
|
|
8341
|
+
* @example Grey out a command when ffmpeg is missing
|
|
8342
|
+
* The Export item is disabled with a reason when ffmpeg is not installed.
|
|
8343
|
+
* `available` looks the tool up (a found path is kept, a miss is looked
|
|
8344
|
+
* up again on the next call, so installing it later is noticed).
|
|
8345
|
+
* ```ts
|
|
8346
|
+
* const hasFfmpeg = await plugin.tools.available("ffmpeg");
|
|
8347
|
+
* plugin.commands.add({
|
|
8348
|
+
* id: "export",
|
|
8349
|
+
* path: ["@video", "Export…"],
|
|
8350
|
+
* enabled: hasFfmpeg,
|
|
8351
|
+
* // Shown with the greyed-out item.
|
|
8352
|
+
* reason: hasFfmpeg ? null : "Install ffmpeg to export",
|
|
8353
|
+
* run() {},
|
|
8354
|
+
* });
|
|
8355
|
+
* ```
|
|
8356
|
+
*
|
|
8357
|
+
* @since 1.0.0
|
|
8358
|
+
*/
|
|
5857
8359
|
available(tool: "ffmpeg" | "ffprobe"): Promise<boolean>;
|
|
5858
8360
|
}
|
|
5859
|
-
/**
|
|
8361
|
+
/**
|
|
8362
|
+
* A JSON file `dataDirectory/<name>.json`, loaded once and saved debounced.
|
|
8363
|
+
*
|
|
8364
|
+
* @remarks
|
|
8365
|
+
* One JSON file, `<dataDirectory>/<name>.json`, made by
|
|
8366
|
+
* {@link Storage.json}. `value` is read once when the store is made;
|
|
8367
|
+
* `save` replaces it in memory at once and writes the file `debounceMs`
|
|
8368
|
+
* later (300 by default) through a temporary file and a rename, so a
|
|
8369
|
+
* crash never leaves half a file. Writes to one file run one at a time,
|
|
8370
|
+
* newest last. Disposing the store, its scope or stopping the plugin
|
|
8371
|
+
* writes a pending save first.
|
|
8372
|
+
*
|
|
8373
|
+
* @capability None.
|
|
8374
|
+
*
|
|
8375
|
+
* @example Count how often the plugin was started
|
|
8376
|
+
* The counter grows by one at every start and is kept in `stats.json`. The
|
|
8377
|
+
* store reads the file when it is made (the defaults when there is none),
|
|
8378
|
+
* and `save` writes the new count a moment later.
|
|
8379
|
+
* ```ts
|
|
8380
|
+
* const stats = plugin.storage.json("stats", { defaults: { starts: 0 } });
|
|
8381
|
+
* stats.save({ starts: stats.value.starts + 1 }); // written 300 ms later
|
|
8382
|
+
* ```
|
|
8383
|
+
*
|
|
8384
|
+
* @since 1.0.0
|
|
8385
|
+
* @category Services
|
|
8386
|
+
*/
|
|
5860
8387
|
export interface JsonStore<T> extends Disposable {
|
|
5861
8388
|
/** The loaded value, or `defaults` when the file was missing or broken. */
|
|
5862
8389
|
readonly value: T;
|
|
5863
8390
|
/**
|
|
5864
8391
|
* Replace the value now and write it after `debounceMs`.
|
|
5865
8392
|
* Only `save` writes: mutating `value` in place is not saved.
|
|
8393
|
+
*
|
|
8394
|
+
* @capability None.
|
|
8395
|
+
*
|
|
8396
|
+
* @example Save a slider value without writing on every move
|
|
8397
|
+
* Dragging the plugin's volume slider updates the stored value at once but
|
|
8398
|
+
* writes the file only once the user stops for 300 ms. Each `save`
|
|
8399
|
+
* replaces the value and restarts the debounce timer.
|
|
8400
|
+
* ```ts
|
|
8401
|
+
* declare const window: Window;
|
|
8402
|
+
* // ---cut---
|
|
8403
|
+
* const store = plugin.storage.json("prefs", { defaults: { volume: 100 } });
|
|
8404
|
+
* window.on("volumeChanged", (volume: number) => store.save({ ...store.value, volume }));
|
|
8405
|
+
* ```
|
|
8406
|
+
*
|
|
8407
|
+
* @since 1.0.0
|
|
5866
8408
|
*/
|
|
5867
8409
|
save(next: T): void;
|
|
5868
|
-
/**
|
|
8410
|
+
/**
|
|
8411
|
+
* Write a pending save now.
|
|
8412
|
+
*
|
|
8413
|
+
* @capability None.
|
|
8414
|
+
* @throws `Error` (rejects) Node's file system error when the file cannot
|
|
8415
|
+
* be written; its `code` is Node's (`ENOSPC`, `EACCES` and so on), not a
|
|
8416
|
+
* player error code.
|
|
8417
|
+
*
|
|
8418
|
+
* @example Write the file now before copying it
|
|
8419
|
+
* Before exporting its settings, the plugin makes sure the file on disk has
|
|
8420
|
+
* the latest value. `flush` writes a pending save at once (or waits for
|
|
8421
|
+
* the write in progress) and resolves when the file is written.
|
|
8422
|
+
* ```ts
|
|
8423
|
+
* import { copyFile } from "node:fs/promises";
|
|
8424
|
+
* import { join } from "node:path";
|
|
8425
|
+
* declare const exportPath: string;
|
|
8426
|
+
* // ---cut---
|
|
8427
|
+
* const store = plugin.storage.json("settings", { defaults: { step: 5 } });
|
|
8428
|
+
* await store.flush(); // the file now has the newest value
|
|
8429
|
+
* await copyFile(join(plugin.dataDirectory, "settings.json"), exportPath);
|
|
8430
|
+
* ```
|
|
8431
|
+
*
|
|
8432
|
+
* @since 1.0.0
|
|
8433
|
+
*/
|
|
5869
8434
|
flush(): Promise<void>;
|
|
5870
8435
|
}
|
|
5871
|
-
/**
|
|
8436
|
+
/**
|
|
8437
|
+
* `plugin.storage`: JSON settings files. No capability.
|
|
8438
|
+
*
|
|
8439
|
+
* @remarks
|
|
8440
|
+
* Two kinds of storage, both private to the plugin and needing no
|
|
8441
|
+
* capability. {@link Storage.json} keeps settings in JSON files in the
|
|
8442
|
+
* plugin's data directory: they last across player restarts and upgrades,
|
|
8443
|
+
* and are written by the plugin's own process. {@link Storage.session}
|
|
8444
|
+
* keeps values in the player's memory for one player run: they survive a
|
|
8445
|
+
* restart of the plugin's process but not of the player. Secrets belong in
|
|
8446
|
+
* {@link Secrets}, never in either.
|
|
8447
|
+
*
|
|
8448
|
+
* @capability None.
|
|
8449
|
+
*
|
|
8450
|
+
* @example Keep a setting across restarts and a queue for this run
|
|
8451
|
+
* The plugin's step size survives a restart of the player; its work queue
|
|
8452
|
+
* survives a reload of the plugin but starts empty with the next player
|
|
8453
|
+
* run. The JSON store reads its file at once and writes it shortly after
|
|
8454
|
+
* each `save`; session values live in the player process.
|
|
8455
|
+
* ```ts
|
|
8456
|
+
* const settings = plugin.storage.json("settings", { defaults: { step: 5 } });
|
|
8457
|
+
* const queue = (await plugin.storage.session.get<string[]>("queue")) ?? [];
|
|
8458
|
+
* void settings.value.step; // read synchronously, from the file or the defaults
|
|
8459
|
+
* void queue;
|
|
8460
|
+
* ```
|
|
8461
|
+
*
|
|
8462
|
+
* @see [Store a plugin's data](../../docs/how-to/store-data.md), [Remember a window's state between runs](../../docs/how-to/window-state.md)
|
|
8463
|
+
* @since 1.0.0
|
|
8464
|
+
* @category Services
|
|
8465
|
+
*/
|
|
5872
8466
|
export interface Storage {
|
|
5873
8467
|
/**
|
|
5874
8468
|
* Open `<name>.json` (`name`: 1–64 of `a-z 0-9 -`). A file that cannot be
|
|
@@ -5897,6 +8491,11 @@ export interface Storage {
|
|
|
5897
8491
|
* // Only `save` writes: changing `store.value` in place is not saved.
|
|
5898
8492
|
* store.save({ ...store.value, recent: [path, ...store.value.recent].slice(0, 10) });
|
|
5899
8493
|
* ```
|
|
8494
|
+
*
|
|
8495
|
+
* @capability None.
|
|
8496
|
+
* @throws `invalid` (throws) A bad `name`, or a `debounceMs` that is not a
|
|
8497
|
+
* non-negative number.
|
|
8498
|
+
* @since 1.0.0
|
|
5900
8499
|
*/
|
|
5901
8500
|
json<T>(name: string, options: {
|
|
5902
8501
|
defaults: T;
|
|
@@ -5970,19 +8569,97 @@ export interface Storage {
|
|
|
5970
8569
|
* ```
|
|
5971
8570
|
*
|
|
5972
8571
|
* @since 1.5.25
|
|
8572
|
+
*
|
|
8573
|
+
* @remarks
|
|
8574
|
+
* The player keeps the values in its own memory, per plugin, until it
|
|
8575
|
+
* quits; the plugin's process may restart in between. Each call is one
|
|
8576
|
+
* request to the player, answered in order, with no debounce.
|
|
8577
|
+
*
|
|
8578
|
+
* @capability None.
|
|
8579
|
+
* @category Services
|
|
5973
8580
|
*/
|
|
5974
8581
|
export interface SessionStorage {
|
|
5975
|
-
/**
|
|
8582
|
+
/**
|
|
8583
|
+
* The value stored under `key`, or `undefined` when there is none.
|
|
8584
|
+
*
|
|
8585
|
+
* @capability None.
|
|
8586
|
+
* @throws `invalid` (rejects) A bad key (empty, over 1024 bytes, with NUL
|
|
8587
|
+
* or a lone surrogate).
|
|
8588
|
+
* @throws `disabled` (rejects) The plugin is stopping.
|
|
8589
|
+
* @throws `unsupported` (rejects) A player before 0.5.5.
|
|
8590
|
+
*
|
|
8591
|
+
* @example Pick up where the plugin stopped after a reload
|
|
8592
|
+
* After a reload the plugin resumes the item it was working on. `get`
|
|
8593
|
+
* asks the player and resolves with the value parsed from JSON, or
|
|
8594
|
+
* `undefined` when nothing is stored; the value is not checked, so its
|
|
8595
|
+
* shape is checked here.
|
|
8596
|
+
* ```ts
|
|
8597
|
+
* const current = await plugin.storage.session.get<{ url: string }>("current");
|
|
8598
|
+
* if (typeof current?.url === "string") await plugin.osd.show("Resuming {}", [current.url]);
|
|
8599
|
+
* ```
|
|
8600
|
+
*
|
|
8601
|
+
* @since 1.5.25
|
|
8602
|
+
*/
|
|
5976
8603
|
get<T = unknown>(key: string): Promise<T | undefined>;
|
|
5977
8604
|
/**
|
|
5978
8605
|
* Store or replace `value` under `key`. `invalid` for a bad key or a value
|
|
5979
8606
|
* that is not JSON, `limit` when the plugin's values would be over 1 MiB
|
|
5980
8607
|
* or the request over the 1 MiB message limit.
|
|
8608
|
+
*
|
|
8609
|
+
* @capability None.
|
|
8610
|
+
* @throws `invalid` (rejects) A bad key, or a value JSON cannot hold (or a
|
|
8611
|
+
* string with a lone surrogate).
|
|
8612
|
+
* @throws `disabled` (rejects) The plugin is stopping.
|
|
8613
|
+
* @throws `unsupported` (rejects) A player before 0.5.5.
|
|
8614
|
+
* @throws `limit` (rejects) The plugin's values would pass 1 MiB, or the
|
|
8615
|
+
* request the 1 MiB message limit; nothing changes.
|
|
8616
|
+
*
|
|
8617
|
+
* @example Remember the item being worked on
|
|
8618
|
+
* The plugin records the URL it is processing, so a reload can continue
|
|
8619
|
+
* with it. The value is stored as `JSON.stringify` writes it, and is kept
|
|
8620
|
+
* once the promise resolves.
|
|
8621
|
+
* ```ts
|
|
8622
|
+
* await plugin.storage.session.set("current", { url: "https://example.com/a.mkv" });
|
|
8623
|
+
* ```
|
|
8624
|
+
*
|
|
8625
|
+
* @since 1.5.25
|
|
5981
8626
|
*/
|
|
5982
8627
|
set(key: string, value: unknown): Promise<void>;
|
|
5983
|
-
/**
|
|
8628
|
+
/**
|
|
8629
|
+
* Remove a key; removing a missing one is fine.
|
|
8630
|
+
*
|
|
8631
|
+
* @capability None.
|
|
8632
|
+
* @throws `invalid` (rejects) A bad key.
|
|
8633
|
+
* @throws `disabled` (rejects) The plugin is stopping.
|
|
8634
|
+
* @throws `unsupported` (rejects) A player before 0.5.5.
|
|
8635
|
+
*
|
|
8636
|
+
* @example Forget the item once it is done
|
|
8637
|
+
* When the work finishes, the plugin removes the entry so that a later
|
|
8638
|
+
* reload does not resume it. Removing a key that is not there is fine.
|
|
8639
|
+
* ```ts
|
|
8640
|
+
* await plugin.storage.session.delete("current");
|
|
8641
|
+
* ```
|
|
8642
|
+
*
|
|
8643
|
+
* @since 1.5.25
|
|
8644
|
+
*/
|
|
5984
8645
|
delete(key: string): Promise<void>;
|
|
5985
|
-
/**
|
|
8646
|
+
/**
|
|
8647
|
+
* The keys stored, in code point order.
|
|
8648
|
+
*
|
|
8649
|
+
* @capability None.
|
|
8650
|
+
* @throws `disabled` (rejects) The plugin is stopping.
|
|
8651
|
+
* @throws `unsupported` (rejects) A player before 0.5.5.
|
|
8652
|
+
*
|
|
8653
|
+
* @example Clear everything the plugin stored for this run
|
|
8654
|
+
* The plugin starts its session state over. `keys` lists the plugin's own
|
|
8655
|
+
* keys in code point order; each `delete` is a request of its own.
|
|
8656
|
+
* ```ts
|
|
8657
|
+
* const session = plugin.storage.session;
|
|
8658
|
+
* for (const key of await session.keys()) await session.delete(key);
|
|
8659
|
+
* ```
|
|
8660
|
+
*
|
|
8661
|
+
* @since 1.5.25
|
|
8662
|
+
*/
|
|
5986
8663
|
keys(): Promise<string[]>;
|
|
5987
8664
|
}
|
|
5988
8665
|
/**
|
|
@@ -6005,6 +8682,15 @@ export interface SessionStorage {
|
|
|
6005
8682
|
* await plugin.osd.show(`Saved to ${exportedFile}`); // no file manager to open
|
|
6006
8683
|
* }
|
|
6007
8684
|
* ```
|
|
8685
|
+
*
|
|
8686
|
+
* @remarks
|
|
8687
|
+
* The player opens files, folders and links with the system's own
|
|
8688
|
+
* applications for the plugin, so the plugin never runs `open`,
|
|
8689
|
+
* `explorer` or a shell itself.
|
|
8690
|
+
*
|
|
8691
|
+
* @capability None.
|
|
8692
|
+
* @since 1.0.0
|
|
8693
|
+
* @category Services
|
|
6008
8694
|
*/
|
|
6009
8695
|
export interface System {
|
|
6010
8696
|
/**
|
|
@@ -6012,13 +8698,66 @@ export interface System {
|
|
|
6012
8698
|
* for all, so several files of one folder are all highlighted; Windows:
|
|
6013
8699
|
* one Explorer window per folder, selecting its first path). Paths that do
|
|
6014
8700
|
* not exist are skipped; `missing` when none exists; at most 256.
|
|
8701
|
+
*
|
|
8702
|
+
* @capability None.
|
|
8703
|
+
* @throws `missing` (rejects) None of the paths exists.
|
|
8704
|
+
* @throws `invalid` (rejects) No paths, or more than 256.
|
|
8705
|
+
* @throws `failed` (rejects) The system refused.
|
|
8706
|
+
* @throws `timeout` (rejects) macOS did not answer within 10 s.
|
|
8707
|
+
* @throws `unsupported` (rejects) A system other than macOS and Windows.
|
|
8708
|
+
*
|
|
8709
|
+
* @example Show the exported files in the file manager
|
|
8710
|
+
* After an export, the file manager opens with the new files selected.
|
|
8711
|
+
* Paths that no longer exist are skipped; the call rejects with `missing`
|
|
8712
|
+
* only when none of them exists.
|
|
8713
|
+
* ```ts
|
|
8714
|
+
* declare const exported: string[];
|
|
8715
|
+
* // ---cut---
|
|
8716
|
+
* await plugin.system.reveal(exported); // macOS: one Finder window for all
|
|
8717
|
+
* ```
|
|
8718
|
+
*
|
|
8719
|
+
* @since 1.0.0
|
|
6015
8720
|
*/
|
|
6016
8721
|
reveal(paths: string | string[]): Promise<void>;
|
|
6017
|
-
/**
|
|
8722
|
+
/**
|
|
8723
|
+
* Open the file or folder with its default application. `missing` if absent.
|
|
8724
|
+
*
|
|
8725
|
+
* @capability None.
|
|
8726
|
+
* @throws `missing` (rejects) The path does not exist.
|
|
8727
|
+
* @throws `invalid` (rejects) An empty path.
|
|
8728
|
+
* @throws `failed` (rejects) The system refused.
|
|
8729
|
+
* @throws `timeout` (rejects) macOS did not answer within 10 s.
|
|
8730
|
+
* @throws `unsupported` (rejects) A system other than macOS and Windows.
|
|
8731
|
+
*
|
|
8732
|
+
* @example Open the plugin's data folder
|
|
8733
|
+
* A "Show data folder" command opens the plugin's data directory in the
|
|
8734
|
+
* file manager, which is the default application for a folder.
|
|
8735
|
+
* ```ts
|
|
8736
|
+
* await plugin.system.openPath(plugin.dataDirectory);
|
|
8737
|
+
* ```
|
|
8738
|
+
*
|
|
8739
|
+
* @since 1.0.0
|
|
8740
|
+
*/
|
|
6018
8741
|
openPath(path: string): Promise<void>;
|
|
6019
8742
|
/**
|
|
6020
8743
|
* Open an http(s) address in the default browser. `invalid` for another
|
|
6021
8744
|
* protocol or a malformed address; `timeout` after 10 s, `failed` otherwise.
|
|
8745
|
+
*
|
|
8746
|
+
* @capability None.
|
|
8747
|
+
* @throws `invalid` (rejects) Not an `http:` or `https:` address.
|
|
8748
|
+
* @throws `timeout` (rejects) macOS did not hand it to the browser within
|
|
8749
|
+
* 10 s.
|
|
8750
|
+
* @throws `failed` (rejects) The system refused.
|
|
8751
|
+
* @throws `unsupported` (rejects) A system other than macOS and Windows.
|
|
8752
|
+
*
|
|
8753
|
+
* @example Open the plugin's help page
|
|
8754
|
+
* A Help command opens the plugin's documentation in the default browser.
|
|
8755
|
+
* Only `http:` and `https:` addresses are opened.
|
|
8756
|
+
* ```ts
|
|
8757
|
+
* await plugin.system.openUrl("https://example.com/plugin/help");
|
|
8758
|
+
* ```
|
|
8759
|
+
*
|
|
8760
|
+
* @since 1.0.0
|
|
6022
8761
|
*/
|
|
6023
8762
|
openUrl(url: string): Promise<void>;
|
|
6024
8763
|
}
|
|
@@ -6026,7 +8765,8 @@ export interface System {
|
|
|
6026
8765
|
* Per-plugin secrets (API keys, passwords) in the system's secret store:
|
|
6027
8766
|
* the login keychain on macOS, the Credential Manager on Windows. Needs
|
|
6028
8767
|
* `secrets`. `unsupported` on systems without a store, `unavailable` when
|
|
6029
|
-
* the store
|
|
8768
|
+
* the store cannot be reached (no keychain, a read-only store, no logged-in
|
|
8769
|
+
* session on Windows): keep a fallback (or ask again).
|
|
6030
8770
|
*
|
|
6031
8771
|
* The user denying access and other system errors reject `failed`. Never
|
|
6032
8772
|
* write secrets to settings files.
|
|
@@ -6052,16 +8792,96 @@ export interface System {
|
|
|
6052
8792
|
* await plugin.secrets.set("api-token", token).catch(() => {}); // best effort
|
|
6053
8793
|
* }
|
|
6054
8794
|
* ```
|
|
8795
|
+
*
|
|
8796
|
+
* @remarks
|
|
8797
|
+
* Values are stored by the player in the system's secret store under this
|
|
8798
|
+
* plugin's id: other plugins cannot read them. Names are 1–64 of
|
|
8799
|
+
* `a-z 0-9 . _ -`, values 1–1024 bytes of text, at most 256 names per
|
|
8800
|
+
* plugin (`SECRET_LIMITS`).
|
|
8801
|
+
*
|
|
8802
|
+
* @capability secrets
|
|
8803
|
+
* @since 1.0.0
|
|
8804
|
+
* @category Services
|
|
6055
8805
|
*/
|
|
6056
8806
|
export interface Secrets {
|
|
6057
|
-
/**
|
|
8807
|
+
/**
|
|
8808
|
+
* The stored value, `null` when there is none.
|
|
8809
|
+
*
|
|
8810
|
+
* @capability secrets
|
|
8811
|
+
* @throws `unsupported` (rejects) The system has no secret store.
|
|
8812
|
+
* @throws `unavailable` (rejects) The store cannot be reached (no keychain,
|
|
8813
|
+
* a read-only store, no logged-in session on Windows).
|
|
8814
|
+
* @throws `failed` (rejects) The user denied access, or another system
|
|
8815
|
+
* error.
|
|
8816
|
+
* @throws `capability` (rejects) The manifest does not declare
|
|
8817
|
+
* `secrets`.
|
|
8818
|
+
* @throws `invalid` (rejects) A bad name.
|
|
8819
|
+
*
|
|
8820
|
+
* @example Read a stored token
|
|
8821
|
+
* The plugin uses the token the user saved earlier, or asks for one when
|
|
8822
|
+
* there is none. `get` resolves with the stored text or `null`.
|
|
8823
|
+
* ```ts
|
|
8824
|
+
* const token = await plugin.secrets.get("api-token");
|
|
8825
|
+
* if (token === null) await plugin.osd.show("Sign in first");
|
|
8826
|
+
* ```
|
|
8827
|
+
*
|
|
8828
|
+
* @since 1.0.0
|
|
8829
|
+
*/
|
|
6058
8830
|
get(name: string): Promise<string | null>;
|
|
6059
8831
|
/**
|
|
6060
8832
|
* Store or replace (`name`: 1–64 of `a-z 0-9 . _ -`; value 1–1024 bytes).
|
|
6061
8833
|
* At most 256 different names per plugin: a new name beyond that rejects
|
|
6062
8834
|
* `limit`. Uninstalling with `removeData` deletes them.
|
|
8835
|
+
*
|
|
8836
|
+
* @capability secrets
|
|
8837
|
+
* @throws `invalid` (rejects) A bad name, or a value that is empty, has NUL
|
|
8838
|
+
* or is over 1024 bytes.
|
|
8839
|
+
* @throws `limit` (rejects) A new name beyond 256.
|
|
8840
|
+
* @throws `unsupported` (rejects) The system has no secret store.
|
|
8841
|
+
* @throws `unavailable` (rejects) The store cannot be reached (no keychain,
|
|
8842
|
+
* a read-only store, no logged-in session on Windows).
|
|
8843
|
+
* @throws `failed` (rejects) The user denied access, or another system
|
|
8844
|
+
* error.
|
|
8845
|
+
* @throws `capability` (rejects) The manifest does not declare
|
|
8846
|
+
* `secrets`.
|
|
8847
|
+
*
|
|
8848
|
+
* @example Save the token the user entered
|
|
8849
|
+
* The token the user typed is kept in the system's secret store for later
|
|
8850
|
+
* runs. `set` stores or replaces it; the store can refuse, in which case
|
|
8851
|
+
* the plugin keeps the token for this run only.
|
|
8852
|
+
* ```ts
|
|
8853
|
+
* declare const token: string;
|
|
8854
|
+
* // ---cut---
|
|
8855
|
+
* await plugin.secrets.set("api-token", token).catch((error: PluginError) => {
|
|
8856
|
+
* // No store, or access denied: the token is not kept for next time.
|
|
8857
|
+
* void plugin.osd.show("The token could not be saved ({})", [error.code]);
|
|
8858
|
+
* });
|
|
8859
|
+
* ```
|
|
8860
|
+
*
|
|
8861
|
+
* @since 1.0.0
|
|
6063
8862
|
*/
|
|
6064
8863
|
set(name: string, value: string): Promise<void>;
|
|
6065
|
-
/**
|
|
8864
|
+
/**
|
|
8865
|
+
* Remove; removing a missing secret is fine.
|
|
8866
|
+
*
|
|
8867
|
+
* @capability secrets
|
|
8868
|
+
* @throws `invalid` (rejects) A bad name.
|
|
8869
|
+
* @throws `unsupported` (rejects) The system has no secret store.
|
|
8870
|
+
* @throws `unavailable` (rejects) The store cannot be reached (no keychain,
|
|
8871
|
+
* a read-only store, no logged-in session on Windows).
|
|
8872
|
+
* @throws `failed` (rejects) The user denied access, or another system
|
|
8873
|
+
* error.
|
|
8874
|
+
* @throws `capability` (rejects) The manifest does not declare
|
|
8875
|
+
* `secrets`.
|
|
8876
|
+
*
|
|
8877
|
+
* @example Sign out by removing the token
|
|
8878
|
+
* Signing out removes the stored token, so the next start asks again.
|
|
8879
|
+
* Removing a name that is not stored is fine.
|
|
8880
|
+
* ```ts
|
|
8881
|
+
* await plugin.secrets.delete("api-token");
|
|
8882
|
+
* ```
|
|
8883
|
+
*
|
|
8884
|
+
* @since 1.0.0
|
|
8885
|
+
*/
|
|
6066
8886
|
delete(name: string): Promise<void>;
|
|
6067
8887
|
}
|