@vestra-player/sdk 1.5.31 → 1.5.34

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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`) or another `apiVersion`. |
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. A second call in the same plugin throws.
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 `unsupported` under software rendering.
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"`. */
@@ -2422,7 +2559,12 @@ export interface Panel extends View, Disposable {
2422
2559
  modifiers: DropModifiers;
2423
2560
  }) => unknown): Disposable;
2424
2561
  }
2425
- /** Keys held when the files were dropped. */
2562
+ /**
2563
+ * Keys held when the files were dropped.
2564
+ *
2565
+ * @since 1.0.0
2566
+ * @category UI
2567
+ */
2426
2568
  export interface DropModifiers {
2427
2569
  /** Option (macOS) / Alt (Windows). */
2428
2570
  readonly alt: boolean;
@@ -2431,7 +2573,13 @@ export interface DropModifiers {
2431
2573
  /** ⌘ on macOS, Ctrl on Windows. */
2432
2574
  readonly cmd: boolean;
2433
2575
  }
2434
- /** A provider for files dropped on the video area ({@link Drops.provide}). */
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
+ */
2435
2583
  export interface DropHandler {
2436
2584
  /**
2437
2585
  * Lower case, without the dot; `"folder"` matches directories.
@@ -2444,7 +2592,39 @@ export interface DropHandler {
2444
2592
  modifiers: DropModifiers;
2445
2593
  }): unknown;
2446
2594
  }
2447
- /** `plugin.drops`: files dropped on the video area. Needs `ui.drop`. */
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
+ */
2448
2628
  export interface Drops {
2449
2629
  /**
2450
2630
  * Take files dropped on the video area. The first provider registered for
@@ -2478,6 +2658,9 @@ export interface Drops {
2478
2658
  * },
2479
2659
  * });
2480
2660
  * ```
2661
+ *
2662
+ * @capability ui.drop
2663
+ * @since 1.0.0
2481
2664
  */
2482
2665
  provide(handler: DropHandler): Disposable;
2483
2666
  }
@@ -3394,9 +3577,19 @@ export interface Panels {
3394
3577
  /** The sidebar position, including its floating state; any plugin may read and toggle it. */
3395
3578
  readonly sidebar: SidebarSlot;
3396
3579
  }
3397
- /** What a timeline mark stands for; readers choose colours by it. */
3580
+ /**
3581
+ * What a timeline mark stands for; readers choose colours by it.
3582
+ *
3583
+ * @since 1.0.0
3584
+ * @category Playback
3585
+ */
3398
3586
  export type MarkKind = "chapter" | "bookmark" | "marker";
3399
- /** One mark on the seek bar. */
3587
+ /**
3588
+ * One mark on the seek bar.
3589
+ *
3590
+ * @since 1.0.0
3591
+ * @category Playback
3592
+ */
3400
3593
  export interface TimelineMark {
3401
3594
  /** Seconds from the start. */
3402
3595
  readonly time: number;
@@ -3407,7 +3600,28 @@ export interface TimelineMark {
3407
3600
  /** The manifest id of the plugin that published it. */
3408
3601
  readonly plugin: string;
3409
3602
  }
3410
- /** This plugin's own marks; disposing it removes them. */
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
+ */
3411
3625
  export interface TimelineLayer extends Disposable {
3412
3626
  /**
3413
3627
  * Replace the layer (last write wins; `[]` clears it). At most 1000 marks,
@@ -3432,6 +3646,9 @@ export interface TimelineLayer extends Disposable {
3432
3646
  * { ifCurrent: seen },
3433
3647
  * );
3434
3648
  * ```
3649
+ *
3650
+ * @capability timeline.marks
3651
+ * @since 1.0.0
3435
3652
  */
3436
3653
  set(marks: {
3437
3654
  time: number;
@@ -3444,16 +3661,79 @@ export interface TimelineLayer extends Disposable {
3444
3661
  /**
3445
3662
  * `plugin.timeline`: marks on the seek bar. Every plugin publishes one
3446
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
3447
3686
  */
3448
3687
  export interface Timeline {
3449
3688
  /** Every layer merged, by time (`playback.read`). Empty on a new source. */
3450
3689
  readonly marks: readonly TimelineMark[];
3451
- /** The merged list changed (needs `events`; at most every 100 ms). */
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
+ */
3452
3707
  onChange(listener: (marks: readonly TimelineMark[]) => void): Disposable;
3453
- /** This plugin's layer (`timeline.marks`); a second call throws. */
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
+ */
3454
3728
  layer(): TimelineLayer;
3455
3729
  }
3456
- /** The main window as `plugin.window.state` reports it (logical points). */
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
+ */
3457
3737
  export interface WindowState {
3458
3738
  /** The main window is fullscreen. */
3459
3739
  readonly fullscreen: boolean;
@@ -3503,11 +3783,38 @@ export interface WindowState {
3503
3783
  * },
3504
3784
  * });
3505
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
3506
3797
  */
3507
3798
  export interface MainWindow {
3508
3799
  /** Latest state; zero video area without a native window. */
3509
3800
  readonly state: WindowState;
3510
- /** The window state changed (needs `events` for outside changes). */
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
+ */
3511
3818
  onChange(listener: (state: WindowState) => void): Disposable;
3512
3819
  /**
3513
3820
  * Resize the window so the video column is `width`×`height` (the bottom bar
@@ -3519,6 +3826,23 @@ export interface MainWindow {
3519
3826
  * Sizes are 1–16384 logical points; the minimum video area is 318×120.
3520
3827
  * The visible area excludes the menu bar and Dock on macOS; on Windows it
3521
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
3522
3846
  */
3523
3847
  setVideoArea(size: {
3524
3848
  width: number;
@@ -3534,12 +3858,53 @@ export interface MainWindow {
3534
3858
  /**
3535
3859
  * Float the player's windows above other applications. Newest call wins; reverts when this plugin stops.
3536
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
3537
3886
  */
3538
3887
  setAlwaysOnTop(onTop: boolean): Promise<void>;
3539
3888
  /**
3540
3889
  * `"playing"`: hide the pointer over the video after 3 idle seconds while
3541
3890
  * playing, in a window too (not only fullscreen). Newest call wins;
3542
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
3543
3908
  */
3544
3909
  setCursorAutoHide(mode: "fullscreen" | "playing"): Promise<void>;
3545
3910
  /**
@@ -3564,10 +3929,26 @@ export interface MainWindow {
3564
3929
  * if (plugin.window.state.pictureInPicture.supported)
3565
3930
  * await plugin.window.setPictureInPicture(!plugin.window.state.pictureInPicture.active);
3566
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
3567
3943
  */
3568
3944
  setPictureInPicture(on: boolean): Promise<void>;
3569
3945
  }
3570
- /** `plugin.app`: fixed for the life of the plugin process. */
3946
+ /**
3947
+ * `plugin.app`: fixed for the life of the plugin process.
3948
+ *
3949
+ * @since 1.0.0
3950
+ * @category Plugin
3951
+ */
3571
3952
  export interface AppInfo {
3572
3953
  /** The player's version, e.g. `"1.0.0"`. */
3573
3954
  readonly version: string;
@@ -3597,15 +3978,48 @@ export interface AppInfo {
3597
3978
  * const system = await plugin.app.system();
3598
3979
  * const text = `Vestra ${plugin.app.version} on ${system.name} ${system.version ?? ""}`;
3599
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
3600
3991
  */
3601
3992
  export interface App extends AppInfo {
3602
3993
  /**
3603
- * The operating system and the displays, read when called (displays come
3604
- * and go). No capability. CPU and memory: Node's `os` module.
3994
+ * The operating system, the displays and the renderer, read when called
3995
+ * (displays come and go; the renderer can fall back from GPU to software
3996
+ * once). No capability. CPU and memory: Node's `os` module.
3997
+ *
3998
+ * @capability None.
3999
+ * @throws `busy` (rejects) The player could not start the request (no
4000
+ * thread available).
4001
+ *
4002
+ * @example Choose a size for the primary display
4003
+ * The plugin sizes its picture for the primary display. `system()` asks
4004
+ * the player; `displays` lists the displays present now, the primary one
4005
+ * first (empty without a user interface).
4006
+ * ```ts
4007
+ * const { displays } = await plugin.app.system();
4008
+ * const primary = displays[0];
4009
+ * if (primary) console.log(primary.width, primary.height);
4010
+ * ```
4011
+ *
4012
+ * @since 1.0.0
3605
4013
  */
3606
4014
  system(): Promise<SystemInfo>;
3607
4015
  }
3608
- /** What {@link App.system} reports. */
4016
+ /**
4017
+ * What {@link App.system} reports.
4018
+ *
4019
+ * @see {@link App.system}
4020
+ * @since 1.0.0
4021
+ * @category Plugin
4022
+ */
3609
4023
  export interface SystemInfo {
3610
4024
  /** The operating system family. */
3611
4025
  readonly os: "macos" | "windows" | "linux" | "other";
@@ -3621,8 +4035,53 @@ export interface SystemInfo {
3621
4035
  readonly arch: string;
3622
4036
  /** Primary display first; empty without a user interface. */
3623
4037
  readonly displays: readonly DisplayInfo[];
4038
+ /**
4039
+ * Which renderer draws the video now: `"gpu"` (the GPU renderer, so
4040
+ * {@link RenderOptions | render options} apply) or `"software"` (they
4041
+ * fail with `unsupported`). Read at each call. It changes at most once
4042
+ * while the player runs, from `"gpu"` to `"software"` when a window
4043
+ * cannot show GPU frames; the owner of the render options hears that
4044
+ * switch through {@link RenderOptions.onStatus}.
4045
+ *
4046
+ * Absent (`undefined`) on players before 0.5.12, which do not report it:
4047
+ * the renderer is then unknown, not software.
4048
+ *
4049
+ * Today the player renders on the GPU on macOS and in software on
4050
+ * Windows, Linux and without a user interface (see
4051
+ * [Platforms](../../docs/reference/platforms.md)); test this field
4052
+ * rather than {@link SystemInfo.os}.
4053
+ *
4054
+ * @example Offer the HDR menu only where render options work
4055
+ * The plugin adds its HDR command only when the player renders on the
4056
+ * GPU, instead of creating the render options just to find out from a
4057
+ * failed status. `system()` asks the player, which reports the renderer
4058
+ * drawing at that moment; no capability is needed to ask. A player before
4059
+ * 0.5.12 leaves the field out, so the test is for `"gpu"` exactly (an
4060
+ * older player gets no HDR command).
4061
+ * ```ts
4062
+ * const { rendering } = await plugin.app.system();
4063
+ * if (rendering === "gpu") {
4064
+ * const render = plugin.video.render(); // one handle, kept
4065
+ * plugin.commands.add({
4066
+ * id: "hdr",
4067
+ * path: ["@video", "HDR Tone Mapping"],
4068
+ * run: () => render.set({ toneMapping: "bt.2390", targetPeak: 250 }),
4069
+ * });
4070
+ * }
4071
+ * ```
4072
+ *
4073
+ * @player 0.5.12; older players leave it out (declare
4074
+ * `"minPlayerVersion": "0.5.12"` when the plugin relies on it).
4075
+ * @since 1.5.34
4076
+ */
4077
+ readonly rendering?: "gpu" | "software";
3624
4078
  }
3625
- /** One display in {@link SystemInfo.displays}. */
4079
+ /**
4080
+ * One display in {@link SystemInfo.displays}.
4081
+ *
4082
+ * @since 1.0.0
4083
+ * @category Plugin
4084
+ */
3626
4085
  export interface DisplayInfo {
3627
4086
  /** The system's name for it, `null` when it has none. */
3628
4087
  readonly name: string | null;
@@ -5630,6 +6089,9 @@ export interface Osd {
5630
6089
  * The state of a filter, display layer or render options handle.
5631
6090
  * After `failed` nothing retries automatically until the next `set()`,
5632
6091
  * `retry()` or a new source.
6092
+ *
6093
+ * @since 1.0.0
6094
+ * @category Processing
5633
6095
  */
5634
6096
  export type FilterStatus =
5635
6097
  /** No ready media; the value is kept and applied when media is ready. */
@@ -5705,6 +6167,18 @@ interface Filter<T> extends Disposable {
5705
6167
  * picture.onStatus((s) => s.state === "failed" && plugin.osd.show(s.error.message));
5706
6168
  * picture.set([{ kind: "eq", brightness: 0.1, contrast: 1.1, saturation: 1, gamma: 1 }]);
5707
6169
  * ```
6170
+ *
6171
+ * @remarks
6172
+ * Members: `set(nodes)` records the desired chain (last write wins) and
6173
+ * sends it, one request at a time and only the newest; `status` and
6174
+ * `onStatus` report `waiting`, `applying`, `applied` or `failed`; `retry`
6175
+ * applies the current value again; `dispose` withdraws the chain. `set`
6176
+ * never throws: a refused chain shows as `failed` in `status`.
6177
+ *
6178
+ * @capability processing.video, playback.read and events
6179
+ * @see [Video and audio processing](../../docs/concepts/processing.md)
6180
+ * @since 1.0.0
6181
+ * @category Processing
5708
6182
  */
5709
6183
  export type VideoFilter = Filter<VideoNode[]>;
5710
6184
  /**
@@ -5724,6 +6198,16 @@ export type VideoFilter = Filter<VideoNode[]>;
5724
6198
  * const audio = plugin.audio.filter("sound");
5725
6199
  * audio.set({ nodes: [{ kind: "volume", gainDb: -6 }], delayMs: 120 }); // 10 ms steps
5726
6200
  * ```
6201
+ *
6202
+ * @remarks
6203
+ * The same members and rules as {@link VideoFilter}: `set` records the
6204
+ * desired value and never throws, `status` and `onStatus` report the
6205
+ * result, `retry` applies it again, `dispose` withdraws it.
6206
+ *
6207
+ * @capability processing.audio, playback.read and events
6208
+ * @see [Video and audio processing](../../docs/concepts/processing.md)
6209
+ * @since 1.0.0
6210
+ * @category Processing
5727
6211
  */
5728
6212
  export type AudioFilter = Filter<{
5729
6213
  /** The audio chain; `[]` for none. */
@@ -5734,6 +6218,9 @@ export type AudioFilter = Filter<{
5734
6218
  /**
5735
6219
  * How the video sits in the video area; not part of snapshots or exports.
5736
6220
  * Defaults: `{ aspect: null, fit: "keep", zoom: 1, panX: 0, panY: 0 }`.
6221
+ *
6222
+ * @since 1.0.0
6223
+ * @category Processing
5737
6224
  */
5738
6225
  export interface ViewSettings {
5739
6226
  /** Display aspect override 0.25–4; `null` = as the source. */
@@ -5761,24 +6248,73 @@ export interface ViewSettings {
5761
6248
  * The video fills the video area (cropped instead of letterboxed), then
5762
6249
  * zooms to 150 % shifted a little to the right. The first plugin to call
5763
6250
  * `view()` owns the display layer; each `set` merges into its settings,
5764
- * which stay across sources until the plugin resets them or stops.
6251
+ * which stay across sources until the plugin disposes the view or stops.
5765
6252
  * ```ts
5766
6253
  * const view = plugin.video.view();
5767
6254
  * view.set({ fit: "fill" });
5768
6255
  * view.set({ zoom: 1.5, panX: 0.1 }); // merged with the previous write
5769
6256
  * ```
6257
+ *
6258
+ * @remarks
6259
+ * Not part of snapshots or exports: it changes how the picture sits in the
6260
+ * video area, not the picture. `set` merges into the current settings and
6261
+ * never throws: an out-of-range value or unknown field shows as `failed`
6262
+ * (`invalid`) in `status` and is reported to `onError`, without sending
6263
+ * anything.
6264
+ *
6265
+ * @capability processing.video, playback.read and events (like
6266
+ * {@link VideoFilter})
6267
+ * @see [Video and audio processing](../../docs/concepts/processing.md)
6268
+ * @since 1.0.0
6269
+ * @category Processing
5770
6270
  */
5771
6271
  export interface VideoView extends Disposable {
5772
- /** Merged into the current settings; last write wins. */
6272
+ /**
6273
+ * Merged into the current settings; last write wins.
6274
+ *
6275
+ * @capability processing.video
6276
+ *
6277
+ * @example Stretch a 4:3 video to fill the window
6278
+ * A 4:3 video fills a 16:9 window, distorted, as the user asked. Each
6279
+ * `set` merges the given fields into this plugin's view settings; the
6280
+ * newest write wins, and the settings stay for later files until the
6281
+ * plugin disposes the view or stops.
6282
+ * ```ts
6283
+ * plugin.video.view().set({ fit: "stretch" });
6284
+ * ```
6285
+ *
6286
+ * @since 1.0.0
6287
+ */
5773
6288
  set(value: Partial<ViewSettings>): void;
5774
6289
  /** Current status (the same four states as filters). */
5775
6290
  readonly status: FilterStatus;
5776
- /** The status changed. */
6291
+ /**
6292
+ * The status changed.
6293
+ *
6294
+ * @capability processing.video
6295
+ *
6296
+ * @example Tell the user another plugin owns the view
6297
+ * When another plugin already controls the display layer, the OSD says
6298
+ * so. The first plugin to call `view()` owns it; this plugin's handle
6299
+ * then reports `failed` with `conflict`, and tries again on its next `set`.
6300
+ * ```ts
6301
+ * const view = plugin.video.view();
6302
+ * view.onStatus((status) => {
6303
+ * if (status.state === "failed" && status.error.code === "conflict")
6304
+ * void plugin.osd.show("Another plugin controls the picture size");
6305
+ * });
6306
+ * ```
6307
+ *
6308
+ * @since 1.0.0
6309
+ */
5777
6310
  onStatus(listener: (status: FilterStatus) => void): Disposable;
5778
6311
  }
5779
6312
  /**
5780
6313
  * GPU renderer options; not part of snapshots or exports.
5781
6314
  * Defaults: `{ toneMapping: "auto", targetPeak: 203, peakDetect: true, shaders: [], interpolation: false }`.
6315
+ *
6316
+ * @since 1.0.0
6317
+ * @category Processing
5782
6318
  */
5783
6319
  export interface RenderSettings {
5784
6320
  /** HDR → SDR: "auto" maps HDR sources only; "off" clips without a curve. */
@@ -5799,8 +6335,10 @@ export interface RenderSettings {
5799
6335
  *
5800
6336
  * Platforms: only macOS renders on the GPU. Under software rendering
5801
6337
  * (Windows, Linux, headless, fallback) every `set` fails with
5802
- * `unsupported`. macOS OpenGL 4.1 has no compute shaders, so `peakDetect`
5803
- * is accepted but has no effect; output is always SDR.
6338
+ * `unsupported`; {@link SystemInfo.rendering} tells which renderer draws
6339
+ * without creating the handle. macOS OpenGL 4.1 has no
6340
+ * compute shaders, so `peakDetect` is accepted but has no effect; output
6341
+ * is always SDR.
5804
6342
  *
5805
6343
  * @example Tone-map HDR video and hide the menu where it cannot work
5806
6344
  * On a GPU-rendering player HDR video is tone-mapped with BT.2390 for a
@@ -5817,19 +6355,69 @@ export interface RenderSettings {
5817
6355
  * });
5818
6356
  * render.set({ toneMapping: "bt.2390", targetPeak: 250 }); // nits
5819
6357
  * ```
6358
+ *
6359
+ * @remarks
6360
+ * GPU renderer options (tone mapping, peak detection, shaders,
6361
+ * interpolation). Only the GPU renderer has them, which today means macOS;
6362
+ * under software rendering the handle is `failed` with `unsupported` after
6363
+ * the player's first answer to `render()` (reported to `onError` too), and so is
6364
+ * every `set`. Single owner, like {@link VideoView}. `set` never throws; the
6365
+ * result shows in `status`.
6366
+ *
6367
+ * @capability processing.video, playback.read and events (like
6368
+ * {@link VideoFilter})
6369
+ * @see [Video and audio processing](../../docs/concepts/processing.md)
6370
+ * @since 1.0.0
6371
+ * @category Processing
5820
6372
  */
5821
6373
  export interface RenderOptions extends Disposable {
5822
- /** Merged into the current settings; last write wins. */
5823
- set(value: Partial<RenderSettings>): void;
5824
6374
  /**
5825
- * `failed` with `unsupported` while the player renders in software;
5826
- * `failed` with `failed` when mpv cannot compile a shader (pushed later).
5827
- */
5828
- readonly status: FilterStatus;
5829
- /** The status changed (including later pushes such as shader compile errors). */
5830
- onStatus(listener: (status: FilterStatus) => void): Disposable;
5831
- }
5832
- /** A subtitle track of the current source. */
6375
+ * Merged into the current settings; last write wins.
6376
+ *
6377
+ * @capability processing.video
6378
+ *
6379
+ * @example Turn on motion interpolation
6380
+ * Motion becomes smoother on the GPU renderer. `set` merges the field into
6381
+ * the plugin's render options; under software rendering the status turns
6382
+ * `failed` with `unsupported` instead.
6383
+ * ```ts
6384
+ * plugin.video.render().set({ interpolation: true });
6385
+ * ```
6386
+ *
6387
+ * @since 1.0.0
6388
+ */
6389
+ set(value: Partial<RenderSettings>): void;
6390
+ /**
6391
+ * `failed` with `unsupported` while the player renders in software;
6392
+ * `failed` with `failed` when mpv cannot compile a shader (pushed later).
6393
+ */
6394
+ readonly status: FilterStatus;
6395
+ /**
6396
+ * The status changed (including later pushes such as shader compile errors).
6397
+ *
6398
+ * @capability processing.video
6399
+ *
6400
+ * @example Report a shader that does not compile
6401
+ * The OSD shows mpv's message when a shader the plugin set fails to
6402
+ * compile. The player pushes that failure after the `set` was accepted, so
6403
+ * it arrives through `onStatus`, not as a rejected call.
6404
+ * ```ts
6405
+ * const render = plugin.video.render();
6406
+ * render.onStatus((status) => {
6407
+ * if (status.state === "failed") void plugin.osd.show("Render: {}", [status.error.message]);
6408
+ * });
6409
+ * ```
6410
+ *
6411
+ * @since 1.0.0
6412
+ */
6413
+ onStatus(listener: (status: FilterStatus) => void): Disposable;
6414
+ }
6415
+ /**
6416
+ * A subtitle track of the current source.
6417
+ *
6418
+ * @since 1.0.0
6419
+ * @category Subtitles
6420
+ */
5833
6421
  export interface SubtitleTrack {
5834
6422
  /** Stable while the track exists; compare tracks by id. */
5835
6423
  id: string;
@@ -5857,7 +6445,13 @@ export interface SubtitleTrack {
5857
6445
  */
5858
6446
  streamIndex: number | null;
5859
6447
  }
5860
- /** The subtitle state of the current source. */
6448
+ /**
6449
+ * The subtitle state of the current source.
6450
+ *
6451
+ * @see {@link Subtitles.state}
6452
+ * @since 1.0.0
6453
+ * @category Subtitles
6454
+ */
5861
6455
  export interface SubtitlesState {
5862
6456
  /** The source it belongs to ({@link PlayerState.sourceId}). */
5863
6457
  readonly sourceId: number;
@@ -5888,7 +6482,12 @@ export interface SubtitlesState {
5888
6482
  /** The last error, `null` without one. */
5889
6483
  readonly error: string | null;
5890
6484
  }
5891
- /** Subtitle calls act on the selection you see; pass `ifCurrent` to pin an older one. */
6485
+ /**
6486
+ * Subtitle calls act on the selection you see; pass `ifCurrent` to pin an older one.
6487
+ *
6488
+ * @since 1.0.0
6489
+ * @category Subtitles
6490
+ */
5892
6491
  export interface SubtitleConditional {
5893
6492
  /** The subtitle state the call is conditional on; `stale` otherwise. */
5894
6493
  ifCurrent?: SubtitlesState;
@@ -5922,18 +6521,104 @@ export interface SubtitleConditional {
5922
6521
  * offsetMs: 0,
5923
6522
  * });
5924
6523
  * ```
6524
+ *
6525
+ * @remarks
6526
+ * The subtitle tracks of the current source, the primary and secondary
6527
+ * selection, uploaded files and the style. `state` is the SDK's snapshot of
6528
+ * the player's subtitle state (`null` until first read). The player has no
6529
+ * subtitle event: the SDK reads the state again when the source, its phase
6530
+ * or the backend changes (with `events`) and after this plugin's own
6531
+ * changes, so a selection the user or another plugin changed shows only
6532
+ * after `refresh()`. A change without `ifCurrent` reads the state first
6533
+ * and applies to it; pass `ifCurrent` to make it fail with `stale` when the
6534
+ * selection changed since that state. Each change is one operation the
6535
+ * player confirms; `select`, `remove` and `setStyle` resolve with the new
6536
+ * state, `load` with the new track.
6537
+ *
6538
+ * @capability `subtitles.read` to read; `subtitles.control` and
6539
+ * `subtitles.read` to change; `events` to re-read on source changes.
6540
+ *
6541
+ * @see [Load subtitles from a plugin](../../docs/how-to/subtitles.md)
6542
+ * @since 1.0.0
6543
+ * @category Subtitles
5925
6544
  */
5926
6545
  export interface Subtitles {
5927
6546
  /** `null` until first read. */
5928
6547
  readonly state: SubtitlesState | null;
5929
- /** The subtitle state changed. */
6548
+ /**
6549
+ * The subtitle state changed.
6550
+ *
6551
+ * @capability events and subtitles.read
6552
+ *
6553
+ * @example Show the subtitle language after a new file opens
6554
+ * When a new file starts, the OSD names the language of its primary
6555
+ * subtitle. The SDK reads the subtitle state again when the source or its
6556
+ * phase changes (there is no subtitle event, so a selection changed in the
6557
+ * player shows only after `refresh()`) and then calls the listener;
6558
+ * `selected` is `null` when subtitles are off.
6559
+ * ```ts
6560
+ * plugin.subtitles.onChange((state) => {
6561
+ * // A track's language is null when the file does not say.
6562
+ * void plugin.osd.show("Subtitles: {}", [state.selected?.language ?? "off"]);
6563
+ * });
6564
+ * ```
6565
+ *
6566
+ * @since 1.0.0
6567
+ */
5930
6568
  onChange(listener: (state: SubtitlesState) => void): Disposable;
5931
- /** Read the state from the player now. */
6569
+ /**
6570
+ * Read the state from the player now.
6571
+ *
6572
+ * @capability subtitles.read
6573
+ * @throws `capability` (rejects) The manifest does not declare
6574
+ * `subtitles.read`.
6575
+ *
6576
+ * @example Count the subtitle tracks of the file
6577
+ * The plugin reads how many subtitle tracks the file has before building a
6578
+ * menu. `refresh` asks the player and resolves with the current state, also
6579
+ * without `events`; `state` holds it afterwards.
6580
+ * ```ts
6581
+ * const { tracks } = await plugin.subtitles.refresh();
6582
+ * await plugin.osd.show("{} subtitle tracks", [tracks.length]);
6583
+ * ```
6584
+ *
6585
+ * @since 1.0.0
6586
+ */
5932
6587
  refresh(): Promise<SubtitlesState>;
5933
6588
  /**
5934
6589
  * `role: "secondary"` sets the secondary subtitle: a text track that is not
5935
6590
  * the primary one (`invalid` otherwise). A new source clears it.
5936
6591
  * `track: null` turns the role off.
6592
+ *
6593
+ * @capability subtitles.control and subtitles.read
6594
+ * @throws `stale` (rejects) With `ifCurrent`: the source or the subtitle
6595
+ * selection changed since that state.
6596
+ * @throws `invalid` (rejects) An unknown track; as secondary, a bitmap
6597
+ * track or the primary one; as primary, the current secondary.
6598
+ * @throws `missing` (rejects) The track is not in the current source.
6599
+ * @throws `unavailable` (rejects) Nothing loaded, or the source is still
6600
+ * loading or not yet confirmed.
6601
+ * @throws `busy` (rejects) Too many subtitle operations at once.
6602
+ * @throws `capability` (rejects) The manifest does not declare
6603
+ * `subtitles.control` and `subtitles.read`.
6604
+ *
6605
+ * @example Turn on the first English subtitle
6606
+ * The first English track becomes the primary subtitle, but only in the
6607
+ * file the plugin looked at. Embedded tracks are chosen by number, so
6608
+ * without `ifCurrent` a select after another file opened would pick that
6609
+ * file's track with the same number; with `ifCurrent: state` it fails with
6610
+ * `stale` instead. Without `ifCurrent`, `select` reads the newest state
6611
+ * itself and almost never fails with `stale`.
6612
+ * ```ts
6613
+ * const state = await plugin.subtitles.refresh();
6614
+ * const english = state.tracks.find((track) => track.language === "en");
6615
+ * if (english)
6616
+ * await plugin.subtitles.select(english, { ifCurrent: state }).catch((error: PluginError) => {
6617
+ * if (error.code !== "stale") throw error; // another file or selection now
6618
+ * });
6619
+ * ```
6620
+ *
6621
+ * @since 1.0.0
5937
6622
  */
5938
6623
  select(track: SubtitleTrack | null, options?: SubtitleConditional & {
5939
6624
  role?: "primary" | "secondary";
@@ -5949,7 +6634,43 @@ export interface Subtitles {
5949
6634
  *
5950
6635
  * Limits: SRT ≤ 2 MiB and ≤ 10,000 cues; ASS ≤ 2 MiB with `[Script Info]`
5951
6636
  * and `[Events]` and 1–20,000 `Dialogue:` lines. Convert other formats
5952
- * (VTT, SAMI) to SRT first.
6637
+ * (VTT, SAMI) to SRT first. Per source, a plugin has at most 4 loaded
6638
+ * tracks at once and loads at most 16 files (removed ones count), and
6639
+ * every plugin together at most 16 tracks; see `SUBTITLE_LIMITS` in
6640
+ * `@vestra-player/sdk/testing`.
6641
+ *
6642
+ * @capability subtitles.control and subtitles.read
6643
+ * @throws `invalid` (rejects) A bad `format` or `title`, or a file over the
6644
+ * limits.
6645
+ * @throws `stale` (rejects) With `ifCurrent`: the source changed since
6646
+ * that state.
6647
+ * @throws `unavailable` (rejects) Nothing loaded, or the source is still
6648
+ * loading.
6649
+ * @throws `aborted` (rejects) `signal` aborted before the commit (an
6650
+ * `AbortError`); the upload is discarded.
6651
+ * @throws `busy` (rejects) Too many subtitle operations at once, or a
6652
+ * limit of `SUBTITLE_LIMITS` is reached (4 tracks of this plugin, 16
6653
+ * loads of this plugin or 16 tracks in the source, or another of its
6654
+ * limits); nothing is loaded.
6655
+ * @throws `capability` (rejects) The manifest does not declare
6656
+ * `subtitles.control` and `subtitles.read`.
6657
+ *
6658
+ * @example Load a downloaded SRT and select it
6659
+ * A subtitle file the plugin downloaded becomes a track of the current file
6660
+ * and is selected. The plugin passes the file's bytes; the player parses
6661
+ * them, adds an `external` track for this source only and resolves with
6662
+ * it once the upload is committed.
6663
+ * ```ts
6664
+ * declare const url: string;
6665
+ * // ---cut---
6666
+ * const response = await fetch(url);
6667
+ * if (!response.ok) throw new Error(`subtitles: HTTP ${response.status}`);
6668
+ * const bytes = new Uint8Array(await response.arrayBuffer());
6669
+ * const track = await plugin.subtitles.load(bytes, { title: "Downloaded" }); // selected
6670
+ * await plugin.osd.show("Loaded {}", [track.title]);
6671
+ * ```
6672
+ *
6673
+ * @since 1.0.0
5953
6674
  */
5954
6675
  load(bytes: Uint8Array, options?: SubtitleConditional & {
5955
6676
  format?: "srt" | "ass";
@@ -5957,19 +6678,69 @@ export interface Subtitles {
5957
6678
  select?: "primary" | "secondary" | "none";
5958
6679
  signal?: AbortSignal;
5959
6680
  }): Promise<SubtitleTrack>;
5960
- /** Remove a track this plugin loaded. */
6681
+ /**
6682
+ * Remove a track this plugin loaded.
6683
+ *
6684
+ * @capability subtitles.control and subtitles.read
6685
+ * @throws `invalid` (rejects) The track was not loaded by this plugin.
6686
+ * @throws `stale` (rejects) With `ifCurrent`: the source changed since
6687
+ * that state.
6688
+ * @throws `unavailable` (rejects) Nothing is loaded, or the source is
6689
+ * still loading.
6690
+ * @throws `busy` (rejects) Too many subtitle operations at once.
6691
+ * @throws `capability` (rejects) The manifest does not declare
6692
+ * `subtitles.control` and `subtitles.read`.
6693
+ *
6694
+ * @example Remove the subtitle the plugin loaded
6695
+ * The plugin's downloaded subtitle disappears from the track list. A plugin
6696
+ * can remove only tracks it loaded itself; a new source removes them
6697
+ * anyway.
6698
+ * ```ts
6699
+ * declare const loaded: SubtitleTrack;
6700
+ * // ---cut---
6701
+ * await plugin.subtitles.remove(loaded);
6702
+ * ```
6703
+ *
6704
+ * @since 1.0.0
6705
+ */
5961
6706
  remove(track: SubtitleTrack, options?: SubtitleConditional): Promise<SubtitlesState>;
5962
6707
  /**
5963
6708
  * Set this plugin's style layer; `null` removes it. The newest call of
5964
6709
  * this plugin replaces its earlier layer. When several plugins have a
5965
6710
  * layer, the player uses the one whose manifest id sorts last, whatever
5966
6711
  * the order of the calls.
6712
+ *
6713
+ * @capability subtitles.control and subtitles.read
6714
+ * @throws `invalid` (rejects) A field out of range.
6715
+ * @throws `unsupported` (rejects) A font that is not in `state.fonts`, or a
6716
+ * primary subtitle that is a bitmap track.
6717
+ * @throws `stale` (rejects) With `ifCurrent`: the subtitle state changed
6718
+ * since that state.
6719
+ * @throws `unavailable` (rejects) Nothing is loaded, or the source is
6720
+ * still loading.
6721
+ * @throws `busy` (rejects) Too many subtitle operations at once.
6722
+ * @throws `capability` (rejects) The manifest does not declare
6723
+ * `subtitles.control` and `subtitles.read`.
6724
+ *
6725
+ * @example Take the plugin's style off again
6726
+ * The plugin removes its style layer, so the subtitles go back to the
6727
+ * player's default (or to another plugin's layer). `null` removes only this
6728
+ * plugin's layer; the effective layer is the one whose manifest id sorts
6729
+ * last among the plugins that still have one.
6730
+ * ```ts
6731
+ * await plugin.subtitles.setStyle(null);
6732
+ * ```
6733
+ *
6734
+ * @since 1.0.0
5967
6735
  */
5968
6736
  setStyle(style: SubtitleStyle | null, options?: SubtitleConditional): Promise<SubtitlesState>;
5969
6737
  }
5970
6738
  /**
5971
6739
  * Options of {@link MediaCapture.capture}. Conditional on `player.state`
5972
6740
  * like other controls.
6741
+ *
6742
+ * @since 1.0.0
6743
+ * @category Media
5973
6744
  */
5974
6745
  export interface CaptureOptions extends Conditional {
5975
6746
  /** Source time; defaults to the current position at the moment of capture. */
@@ -5981,7 +6752,36 @@ export interface CaptureOptions extends Conditional {
5981
6752
  /** Audio to freeze with the snapshot: the selected track, none, or a track id. */
5982
6753
  audio?: "selected" | "none" | number;
5983
6754
  }
5984
- /** A frozen, immutable input for exports. Dispose to release it. */
6755
+ /**
6756
+ * A frozen, immutable input for exports. Dispose to release it.
6757
+ *
6758
+ * @remarks
6759
+ * A frozen input, from {@link MediaCapture.capture}: the source, the time
6760
+ * and (processed) the effects and subtitle as they were. Exports read it in
6761
+ * the player's offline worker, so one snapshot can be exported several
6762
+ * times and playback is unaffected. Disposing it releases the player's
6763
+ * copy; an export using it after that fails with `released`.
6764
+ *
6765
+ * @capability media.read and media.control
6766
+ *
6767
+ * @example Export one frame as PNG and as JPEG
6768
+ * The same frozen frame is written twice, lossless and compressed. Each
6769
+ * `export` starts a job of its own; the snapshot is released after both
6770
+ * are done.
6771
+ * ```ts
6772
+ * const snapshot = await plugin.media.capture();
6773
+ * try {
6774
+ * const png = await snapshot.export({ format: "png", destination: "/tmp/frame.png" });
6775
+ * const jpeg = await snapshot.export({ format: "jpeg", destination: "/tmp/frame.jpg", quality: 85 });
6776
+ * await Promise.all([png.done, jpeg.done]); // terminal states, not errors
6777
+ * } finally {
6778
+ * await snapshot.dispose();
6779
+ * }
6780
+ * ```
6781
+ *
6782
+ * @since 1.0.0
6783
+ * @category Media
6784
+ */
5985
6785
  export interface Snapshot extends Disposable {
5986
6786
  /** Without or with effects. */
5987
6787
  readonly kind: "raw" | "processed";
@@ -6005,6 +6805,32 @@ export interface Snapshot extends Disposable {
6005
6805
  /**
6006
6806
  * Start exporting; the job runs in the host's offline worker.
6007
6807
  * One snapshot can be exported several times.
6808
+ *
6809
+ * @capability media.read and media.control
6810
+ * @throws `released` (rejects) The snapshot was already released.
6811
+ * @throws `invalid` (rejects) Bad options (`quality` outside 1–100, a
6812
+ * relative destination, a clip `end` not after the snapshot time or
6813
+ * past the duration).
6814
+ * @throws `busy` (rejects) This plugin runs 2 exports (or the player 8).
6815
+ * @throws `unavailable` (rejects) No FFmpeg, or the export worker could not
6816
+ * start.
6817
+ * @throws `capability` (rejects) The manifest does not declare
6818
+ * `media.read` and `media.control`.
6819
+ *
6820
+ * @example Export a 30-second clip
6821
+ * A clip from the snapshot time to 30 seconds later is written as MP4.
6822
+ * `export` resolves once the job is accepted; the job then runs in the
6823
+ * player's offline worker, and `done` resolves with its terminal state,
6824
+ * whose `outcome` has the destination or the error.
6825
+ * ```ts
6826
+ * declare const snapshot: Snapshot;
6827
+ * // ---cut---
6828
+ * const job = await snapshot.export({ format: "mp4", destination: "/tmp/clip.mp4", end: snapshot.time + 30 });
6829
+ * const end = await job.done;
6830
+ * if (end.status !== "completed") await plugin.osd.show("Export {}", [end.status]);
6831
+ * ```
6832
+ *
6833
+ * @since 1.0.0
6008
6834
  */
6009
6835
  export(options: ExportOptions): Promise<ExportJob>;
6010
6836
  }
@@ -6016,6 +6842,9 @@ export interface Snapshot extends Disposable {
6016
6842
  * choose the format. `sampleTime` picks the frame (default: the snapshot
6017
6843
  * time); `end` is the clip's end in source seconds. On Windows the export
6018
6844
  * worker needs FFmpeg 6.1 or newer.
6845
+ *
6846
+ * @since 1.0.0
6847
+ * @category Media
6019
6848
  */
6020
6849
  export type ExportOptions = {
6021
6850
  /** A lossless frame. */
@@ -6046,19 +6875,111 @@ export type ExportOptions = {
6046
6875
  * `completed`, `failed` or `cancelled`; once terminal, `outcome` is
6047
6876
  * `{ destination }`, `{ error: { code, message } }` or `{ cancelled: true }`.
6048
6877
  * `facts` describes what the worker produced (versions, range, streams).
6878
+ *
6879
+ * @since 1.0.0
6880
+ * @category Media
6049
6881
  */
6050
6882
  export type JobState = Omit<MediaJob, "jobId">;
6051
- /** An export, from {@link Snapshot.export}. Disposing an unfinished job cancels it. */
6883
+ /**
6884
+ * An export, from {@link Snapshot.export}. Disposing an unfinished job cancels it.
6885
+ *
6886
+ * @remarks
6887
+ * One export, from {@link Snapshot.export}. `state` is the SDK's last copy
6888
+ * of the job's state: the SDK polls the player about every 350 ms until the
6889
+ * job ends, and `refresh` reads it at once. `done` resolves
6890
+ * once the job is terminal (`completed`, `failed` or `cancelled`) and never
6891
+ * rejects for a failed export: the failure is in `outcome`. Disposing an
6892
+ * unfinished job cancels it.
6893
+ *
6894
+ * @capability media.read and media.control
6895
+ *
6896
+ * @example Show export progress and cancel on request
6897
+ * The OSD shows the job's status while it runs, and a Cancel command stops
6898
+ * it. `onChange` follows the state (`queued`, `running`, `cancelling`,
6899
+ * then a terminal one); `cancel` asks the worker to stop. After
6900
+ * `requested` the job usually ends `cancelled`, or `failed` when the
6901
+ * cleanup failed.
6902
+ * ```ts
6903
+ * declare const job: ExportJob;
6904
+ * // ---cut---
6905
+ * job.onChange((state) => void plugin.osd.show("Export: {}", [state.status]));
6906
+ * plugin.commands.add({ id: "cancel-export", title: "Cancel export", run: () => job.cancel() });
6907
+ * await job.done; // completed, failed or cancelled
6908
+ * ```
6909
+ *
6910
+ * @since 1.0.0
6911
+ * @category Media
6912
+ */
6052
6913
  export interface ExportJob extends Disposable {
6053
6914
  /** Latest state. */
6054
6915
  readonly state: JobState;
6055
- /** The state changed. */
6916
+ /**
6917
+ * The state changed.
6918
+ *
6919
+ * @capability media.read
6920
+ *
6921
+ * @example Log each state of an export
6922
+ * The plugin's log records each status the job goes through (`queued`,
6923
+ * `running`, `cancelling` after a cancel, then a terminal one). The listener runs when the SDK learns a
6924
+ * new state from its polling.
6925
+ * ```ts
6926
+ * declare const job: ExportJob;
6927
+ * // ---cut---
6928
+ * job.onChange((state) => console.log("export", state.status));
6929
+ * ```
6930
+ *
6931
+ * @since 1.0.0
6932
+ */
6056
6933
  onChange(listener: (state: JobState) => void): Disposable;
6057
- /** Resolves with the terminal state (completed, failed or cancelled). */
6934
+ /**
6935
+ * Resolves with the terminal state (completed, failed or cancelled).
6936
+ *
6937
+ * @throws `disposed` (rejects) The job handle was disposed while waiting.
6938
+ */
6058
6939
  readonly done: Promise<JobState>;
6059
- /** Ask to cancel: `requested`, `tooLate` (already publishing) or `terminal` (already over). */
6940
+ /**
6941
+ * Ask to cancel: `requested`, `tooLate` (already publishing) or `terminal` (already over).
6942
+ *
6943
+ * @capability media.control
6944
+ * @throws `rejected` (rejects) The player did not accept the request.
6945
+ * @throws `capability` (rejects) The manifest does not declare
6946
+ * `media.control`.
6947
+ *
6948
+ * @example Cancel an export the user no longer wants
6949
+ * The running export stops, unless it is already writing its file.
6950
+ * `cancel` resolves `requested` when the worker will stop (the job ends
6951
+ * `cancelled`), `tooLate` when it is already publishing the file, and
6952
+ * `terminal` when the job had already ended.
6953
+ * ```ts
6954
+ * declare const job: ExportJob;
6955
+ * // ---cut---
6956
+ * const answer = await job.cancel();
6957
+ * if (answer === "tooLate") await plugin.osd.show("Too late: the file is being written");
6958
+ * ```
6959
+ *
6960
+ * @since 1.0.0
6961
+ */
6060
6962
  cancel(): Promise<"requested" | "tooLate" | "terminal">;
6061
- /** Read the state from the player now. */
6963
+ /**
6964
+ * Read the state from the player now.
6965
+ *
6966
+ * @capability media.read
6967
+ * @throws `capability` (rejects) The manifest does not declare
6968
+ * `media.read`.
6969
+ *
6970
+ * @example Read a job's state now
6971
+ * The plugin reads the newest state of an export without waiting for an
6972
+ * event. `refresh` asks the player and resolves with the state, which
6973
+ * `state` then holds.
6974
+ * ```ts
6975
+ * declare const job: ExportJob;
6976
+ * // ---cut---
6977
+ * const state = await job.refresh();
6978
+ * console.log(state.status);
6979
+ * ```
6980
+ *
6981
+ * @since 1.0.0
6982
+ */
6062
6983
  refresh(): Promise<JobState>;
6063
6984
  }
6064
6985
  /**
@@ -6086,21 +7007,73 @@ export interface ExportJob extends Disposable {
6086
7007
  * await snapshot.dispose();
6087
7008
  * }
6088
7009
  * ```
7010
+ *
7011
+ * @remarks
7012
+ * Exports run in the player's offline worker from a frozen
7013
+ * {@link Snapshot}: a still frame or a clip, raw or with this plugin's
7014
+ * effects (and the primary subtitle). Playback is not disturbed. A plugin
7015
+ * holds at most 4 snapshots at a time (16 in the player); release them
7016
+ * when done.
7017
+ *
7018
+ * @capability media.read and media.control (processed snapshots also
7019
+ * `playback.read` and `subtitles.read`)
7020
+ * @since 1.0.0
7021
+ * @category Media
6089
7022
  */
6090
7023
  export interface MediaCapture {
6091
7024
  /**
6092
7025
  * Freeze the current source (and optionally this plugin set's effects)
6093
7026
  * as an input for exports. Fails `stale` when the source changed since
6094
- * `ifCurrent`; `unsupported` for an external ASS track with subtitles; `noSource` when nothing is playing.
7027
+ * `ifCurrent`; `unsupported` for subtitles that cannot be flattened (only
7028
+ * plain SRT text can) or a picture over the size limits; `noSource` when
7029
+ * nothing is playing.
7030
+ *
7031
+ * @capability media.read and media.control
7032
+ * @throws `stale` (rejects) The source changed since `ifCurrent`.
7033
+ * @throws `noSource` (rejects) Nothing is playing.
7034
+ * @throws `invalid` (rejects) `at` at or after the end of the source.
7035
+ * @throws `busy` (rejects) This plugin holds 4 snapshots (or the player 16);
7036
+ * release one.
7037
+ * @throws `unavailable` (rejects) A processed snapshot while the picture is
7038
+ * still being prepared.
7039
+ * @throws `unsupported` (rejects) A processed snapshot the player cannot
7040
+ * freeze: subtitles that cannot be flattened (only plain SRT text can),
7041
+ * or a picture over the size limits.
7042
+ * @throws `capability` (rejects) The manifest does not declare
7043
+ * `media.read` and `media.control`.
7044
+ *
7045
+ * @example Freeze the frame at the 10-second mark
7046
+ * The plugin freezes the frame at 0:10 of the open file, raw, to export it
7047
+ * later. `capture` resolves with a `Snapshot` once the player has frozen
7048
+ * that input; the snapshot stays valid when playback moves on or the file
7049
+ * changes, until it is released.
7050
+ * ```ts
7051
+ * const snapshot = await plugin.media.capture({ at: 10 }); // seconds
7052
+ * try {
7053
+ * // … export it …
7054
+ * } finally {
7055
+ * await snapshot.dispose(); // frees one of the plugin's 4 snapshot slots
7056
+ * }
7057
+ * ```
7058
+ *
7059
+ * @since 1.0.0
6095
7060
  */
6096
7061
  capture(options?: CaptureOptions): Promise<Snapshot>;
6097
7062
  }
6098
7063
  /**
6099
7064
  * Declaration of {@link Resolvers.add}: a web resolver
6100
7065
  * ({@link WebResolverOptions}) or a file resolver ({@link FileResolverOptions}).
7066
+ *
7067
+ * @since 1.0.0
7068
+ * @category Sources
6101
7069
  */
6102
7070
  export type ResolverOptions = WebResolverOptions | FileResolverOptions;
6103
- /** A resolver for web pages: `https://host/…` to playable stream URLs. */
7071
+ /**
7072
+ * A resolver for web pages: `https://host/…` to playable stream URLs.
7073
+ *
7074
+ * @since 1.0.0
7075
+ * @category Sources
7076
+ */
6104
7077
  export interface WebResolverOptions {
6105
7078
  /** Local id, unique within the plugin. */
6106
7079
  id: string;
@@ -6165,6 +7138,9 @@ export interface WebResolverOptions {
6165
7138
  * },
6166
7139
  * });
6167
7140
  * ```
7141
+ *
7142
+ * @since 1.5.3
7143
+ * @category Sources
6168
7144
  */
6169
7145
  export interface FileResolverOptions {
6170
7146
  /** Local id, unique within the plugin. */
@@ -6189,6 +7165,9 @@ export interface FileResolverOptions {
6189
7165
  * failures "无法启动播放". An error thrown by `frame()` keeps its message.
6190
7166
  * To give your own text for a file you can tell is unusable (a bad header),
6191
7167
  * check it in `resolve` and throw there.
7168
+ *
7169
+ * @since 1.5.3
7170
+ * @category Sources
6192
7171
  */
6193
7172
  export interface ResolvedStream {
6194
7173
  /** Always `"stream"`. */
@@ -6207,7 +7186,38 @@ export interface ResolvedStream {
6207
7186
  */
6208
7187
  started?(stream: Stream): void;
6209
7188
  }
6210
- /** A registered resolver. Disposing it unregisters it and aborts resolutions in flight. */
7189
+ /**
7190
+ * A registered resolver. Disposing it unregisters it and aborts resolutions in flight.
7191
+ *
7192
+ * @remarks
7193
+ * A registered resolver, from {@link Resolvers.add}. Disposing it, its
7194
+ * scope or stopping the plugin unregisters it and aborts resolutions in
7195
+ * flight; the extensions or hosts are free again for other plugins.
7196
+ *
7197
+ * @capability source.resolve
7198
+ *
7199
+ * @example Report when another plugin already handles the format
7200
+ * The plugin tells the user when its `.ch8` resolver could not register
7201
+ * because another plugin holds that extension. `ready` rejects with
7202
+ * `conflict`; for another plugin's claim that does not fail activation, so
7203
+ * the plugin keeps running without the resolver.
7204
+ * ```ts
7205
+ * const resolver = plugin.resolvers.add({
7206
+ * id: "chip8",
7207
+ * extensions: ["ch8"],
7208
+ * async resolve() {
7209
+ * throw plugin.i18n.error("Not ready yet.");
7210
+ * },
7211
+ * });
7212
+ * // Rejects with `conflict` when the extension is taken.
7213
+ * resolver.ready.catch((error: PluginError) => {
7214
+ * if (error.code === "conflict") void plugin.osd.show("Another plugin plays .ch8 files");
7215
+ * });
7216
+ * ```
7217
+ *
7218
+ * @since 1.0.0
7219
+ * @category Sources
7220
+ */
6211
7221
  export interface Resolver extends Disposable {
6212
7222
  /**
6213
7223
  * Resolves when the player registered it; rejects otherwise: `conflict`
@@ -6215,6 +7225,13 @@ export interface Resolver extends Disposable {
6215
7225
  * plugin's: activation goes on; this plugin's own: activation fails),
6216
7226
  * `invalid` for a malformed declaration (a player before 0.4.1 also for
6217
7227
  * `extensions`).
7228
+ *
7229
+ * @throws `conflict` (rejects) Another resolver has the extension or the
7230
+ * scheme + host, or this plugin already registered this id.
7231
+ * @throws `busy` (rejects) The player already holds 16 resolvers.
7232
+ * @throws `invalid` (rejects) A malformed declaration.
7233
+ * @throws `capability` (rejects) The manifest does not declare
7234
+ * `source.resolve`.
6218
7235
  */
6219
7236
  readonly ready: Promise<void>;
6220
7237
  }
@@ -6241,6 +7258,20 @@ export interface Resolver extends Disposable {
6241
7258
  * },
6242
7259
  * });
6243
7260
  * ```
7261
+ *
7262
+ * @remarks
7263
+ * A resolver turns something the player cannot play by itself into
7264
+ * something it can: a web page address into stream URLs
7265
+ * ({@link WebResolverOptions}), or a local file of a format the plugin
7266
+ * understands into a stream the plugin generates
7267
+ * ({@link FileResolverOptions}). The player asks the resolver whenever
7268
+ * anyone opens a matching address or file (the Open dialog, the playlist,
7269
+ * another plugin), so the plugin does not intercept opens itself.
7270
+ *
7271
+ * @capability source.resolve (file resolvers also `producer.av`)
7272
+ * @see [Playback state, controls and sources](../../docs/concepts/playback.md)
7273
+ * @since 1.0.0
7274
+ * @category Sources
6244
7275
  */
6245
7276
  export interface Resolvers {
6246
7277
  /**
@@ -6249,6 +7280,29 @@ export interface Resolvers {
6249
7280
  * replaced. One scheme + host pair, and one extension, has one resolver
6250
7281
  * (`conflict` otherwise). A rejected `resolve` fails the open with the
6251
7282
  * error's message.
7283
+ *
7284
+ * @capability source.resolve
7285
+ *
7286
+ * @example Play a site's pages through the plugin
7287
+ * Opening a `https://video.example.com/…` address plays the page's video.
7288
+ * `add` returns at once and registers in the background (`ready` reports
7289
+ * the answer); from then on the player calls `resolve` for every matching
7290
+ * open, and the error a rejected `resolve` carries is what the user sees.
7291
+ * ```ts
7292
+ * plugin.resolvers.add({
7293
+ * id: "example",
7294
+ * schemes: ["https"],
7295
+ * hosts: ["video.example.com"], // exact host names
7296
+ * async resolve(url, signal) {
7297
+ * const response = await fetch(url, { signal });
7298
+ * if (!response.ok) throw new Error(`the page answered ${response.status}`);
7299
+ * const page = (await response.json()) as { title: string; stream: string };
7300
+ * return { kind: "media", originalUrl: url, title: page.title, primary: { url: page.stream } };
7301
+ * },
7302
+ * });
7303
+ * ```
7304
+ *
7305
+ * @since 1.0.0
6252
7306
  */
6253
7307
  add(options: ResolverOptions): Resolver;
6254
7308
  /**
@@ -6304,6 +7358,9 @@ export interface Resolvers {
6304
7358
  * The manifest of a package, as validated. The display fields and
6305
7359
  * `minPlayerVersion` are optional (player 0.2.0); see the API reference for
6306
7360
  * their rules.
7361
+ *
7362
+ * @since 1.0.0
7363
+ * @category Packages
6307
7364
  */
6308
7365
  export interface PackageManifest {
6309
7366
  /** Plugin id. */
@@ -6344,6 +7401,9 @@ export interface PackageManifest {
6344
7401
  * upgrade the plugin and keep its data; links and local files count as one
6345
7402
  * source. `bundled` marks the player's built-in plugins (set by the player
6346
7403
  * only).
7404
+ *
7405
+ * @since 1.1.0
7406
+ * @category Packages
6347
7407
  */
6348
7408
  export type PackageSource = {
6349
7409
  /** A repository. */
@@ -6365,7 +7425,12 @@ export type PackageSource = {
6365
7425
  /** Shipped with the player (built-in). */
6366
7426
  kind: "bundled";
6367
7427
  };
6368
- /** Options of {@link PackageCandidate.install}. */
7428
+ /**
7429
+ * Options of {@link PackageCandidate.install}.
7430
+ *
7431
+ * @since 1.1.0
7432
+ * @category Packages
7433
+ */
6369
7434
  export interface InstallOptions {
6370
7435
  /**
6371
7436
  * Where the package comes from (default `{ kind: "file" }`). Repository
@@ -6380,7 +7445,34 @@ export interface InstallOptions {
6380
7445
  */
6381
7446
  replace?: boolean;
6382
7447
  }
6383
- /** A validated package directory, from {@link Packages.validate}. */
7448
+ /**
7449
+ * A validated package directory, from {@link Packages.validate}.
7450
+ *
7451
+ * @remarks
7452
+ * A package the player has checked, from {@link Packages.validate} or
7453
+ * {@link Packages.validateArchive}: its manifest, digest and the checks
7454
+ * it passed. The player copies the whole package when it checks it and
7455
+ * installs that copy, so later changes to the directory do not matter.
7456
+ * Installing starts a {@link PackageChange}; the player keeps only the 32
7457
+ * most recent candidates.
7458
+ *
7459
+ * @capability plugins.manage
7460
+ *
7461
+ * @example Show what a package asks for before installing it
7462
+ * Before installing, the plugin shows the package's name and the
7463
+ * capabilities it declares. The candidate's `manifest` is the manifest
7464
+ * as the player read it.
7465
+ * ```ts
7466
+ * declare const directory: string;
7467
+ * // ---cut---
7468
+ * const candidate = await plugin.packages.validate(directory);
7469
+ * const { id, version, capabilities } = candidate.manifest;
7470
+ * await plugin.osd.show("{} {}: {}", [id, version, capabilities.join(", ")]);
7471
+ * ```
7472
+ *
7473
+ * @since 1.0.0
7474
+ * @category Packages
7475
+ */
6384
7476
  export interface PackageCandidate {
6385
7477
  /** The package directory. */
6386
7478
  readonly directory: string;
@@ -6397,28 +7489,138 @@ export interface PackageCandidate {
6397
7489
  * player). A reserved id (`vestra` or `vestra.*`, which every built-in
6398
7490
  * uses) fails with `reserved` (player 0.4.1; already when validated), as
6399
7491
  * only the player installs built-in plugins.
7492
+ *
7493
+ * @capability plugins.manage
7494
+ * @throws `conflict` (rejects) The plugin is installed from another source
7495
+ * and `replace` is not set.
7496
+ * @throws `unsupported` (rejects) A built-in plugin's id.
7497
+ * @throws `reserved` (rejects) A reserved id (`vestra`, `vestra.*`).
7498
+ * @throws `busy` (rejects) The plugin library is being cleaned up, or the
7499
+ * player already keeps 64 change records.
7500
+ * @throws `revisionConflict` (rejects) The installed revision of this
7501
+ * plugin changed meanwhile; call `install` again (no new validation
7502
+ * needed).
7503
+ * @throws `stale` (rejects) The candidate is older than the 32 the player
7504
+ * keeps.
7505
+ * @throws `capability` (rejects) The manifest does not declare
7506
+ * `plugins.manage`.
7507
+ *
7508
+ * @example Replace a plugin installed from another source
7509
+ * The new package replaces the installed plugin even though it came from
7510
+ * another source. Without `replace` the player refuses with `conflict`;
7511
+ * the same source simply upgrades and keeps the plugin's data.
7512
+ * ```ts
7513
+ * declare const candidate: PackageCandidate;
7514
+ * // ---cut---
7515
+ * const change = await candidate.install({ replace: true });
7516
+ * await change.done; // "committed" once the new plugin runs
7517
+ * ```
7518
+ *
7519
+ * @since 1.0.0
6400
7520
  */
6401
7521
  install(options?: InstallOptions): Promise<PackageChange>;
6402
7522
  }
6403
- /** The progress of an install or uninstall. */
7523
+ /**
7524
+ * The progress of an install or uninstall.
7525
+ *
7526
+ * @since 1.0.0
7527
+ * @category Packages
7528
+ */
6404
7529
  export type PackageChangeStatus = "preparing" | "activating" | "restoring" | "committed" | "cancelled" | "failed" | "failedDegraded" | "tooLate";
6405
7530
  /**
6406
7531
  * An install or uninstall in progress. The SDK never submits a
6407
7532
  * change twice: after a lost reply it only queries the same request.
7533
+ *
7534
+ * @remarks
7535
+ * An install or uninstall the player carries out in steps (`preparing`,
7536
+ * `activating`, possibly `restoring`) until a terminal status. The SDK
7537
+ * asks the player for the status about every 400 ms until then; `done`
7538
+ * resolves with the terminal status and does not reject for a failed
7539
+ * change: `message` says why.
7540
+ *
7541
+ * @capability plugins.manage
7542
+ *
7543
+ * @example Report each step of an install
7544
+ * The OSD shows each step of the install and the end result. `onChange`
7545
+ * runs for every new status; `done` resolves when the change is over.
7546
+ * ```ts
7547
+ * declare const change: PackageChange;
7548
+ * // ---cut---
7549
+ * change.onChange((status) => void plugin.osd.show("Install: {}", [status]));
7550
+ * const end = await change.done;
7551
+ * if (end !== "committed") await plugin.osd.show("Not installed: {}", [change.message ?? end]);
7552
+ * ```
7553
+ *
7554
+ * @since 1.0.0
7555
+ * @category Packages
6408
7556
  */
6409
7557
  export interface PackageChange {
6410
7558
  /** Current status. */
6411
7559
  readonly status: PackageChangeStatus;
6412
7560
  /** Details for failures, `null` otherwise. */
6413
7561
  readonly message: string | null;
6414
- /** The status changed. */
7562
+ /**
7563
+ * The status changed.
7564
+ *
7565
+ * @capability plugins.manage
7566
+ *
7567
+ * @example Close a progress window when the change ends
7568
+ * The plugin's progress window closes once the install is over, whatever
7569
+ * the outcome. The listener receives each new status; the terminal ones
7570
+ * are `committed`, `cancelled`, `failed`, `failedDegraded` and `tooLate`
7571
+ * (`tooLate` only answers a `cancel` of a change that had committed, and
7572
+ * `committed` may follow it).
7573
+ * ```ts
7574
+ * declare const change: PackageChange;
7575
+ * declare const progress: Window;
7576
+ * // ---cut---
7577
+ * change.onChange((status) => {
7578
+ * if (["committed", "cancelled", "failed", "failedDegraded", "tooLate"].includes(status))
7579
+ * void progress.close();
7580
+ * });
7581
+ * ```
7582
+ *
7583
+ * @since 1.0.0
7584
+ */
6415
7585
  onChange(listener: (status: PackageChangeStatus) => void): Disposable;
6416
- /** Resolves with the final status. */
7586
+ /**
7587
+ * Resolves with the final status.
7588
+ *
7589
+ * @throws `missing` (rejects) The player no longer knows the change (a
7590
+ * `HostError` with that code); a failed change is a terminal status,
7591
+ * not a rejection.
7592
+ */
6417
7593
  readonly done: Promise<PackageChangeStatus>;
6418
- /** Ask to cancel (it may be `tooLate`). */
7594
+ /**
7595
+ * Ask to cancel (it may be `tooLate`).
7596
+ *
7597
+ * @capability plugins.manage
7598
+ * @throws `capability` (rejects) The manifest does not declare
7599
+ * `plugins.manage`.
7600
+ *
7601
+ * @example Cancel an install the user no longer wants
7602
+ * The install stops if it has not gone too far. `cancel` asks the player;
7603
+ * the change then ends `cancelled`. When the change had already committed
7604
+ * the player answers `tooLate` and `done` still resolves `committed`; a
7605
+ * change that is restoring cannot be cancelled and ends `failed` or
7606
+ * `failedDegraded`.
7607
+ * ```ts
7608
+ * declare const change: PackageChange;
7609
+ * // ---cut---
7610
+ * await change.cancel();
7611
+ * console.log(await change.done); // "cancelled"; "committed" when it was too late
7612
+ * ```
7613
+ *
7614
+ * @since 1.0.0
7615
+ */
6419
7616
  cancel(): Promise<void>;
6420
7617
  }
6421
- /** One plugin as the player knows it (`packages.list`). */
7618
+ /**
7619
+ * One plugin as the player knows it (`packages.list`).
7620
+ *
7621
+ * @since 1.0.0
7622
+ * @category Packages
7623
+ */
6422
7624
  export interface PluginInfo {
6423
7625
  /** Manifest id. */
6424
7626
  id: string;
@@ -6477,6 +7679,8 @@ export interface PluginInfo {
6477
7679
  * `description` and `tags` as the player shows them.
6478
7680
  *
6479
7681
  * @since 1.5.9
7682
+ *
7683
+ * @category Packages
6480
7684
  */
6481
7685
  export interface PluginDisplay {
6482
7686
  /** `name`, translated; `null` when the manifest has none. */
@@ -6492,6 +7696,8 @@ export interface PluginDisplay {
6492
7696
  * One refusal in {@link PluginInfo.warnings}.
6493
7697
  *
6494
7698
  * @since 1.5.2
7699
+ *
7700
+ * @category Packages
6495
7701
  */
6496
7702
  export interface PluginWarning {
6497
7703
  /**
@@ -6528,6 +7734,17 @@ export interface PluginWarning {
6528
7734
  * // `message` says why a change did not commit.
6529
7735
  * if ((await change.done) !== "committed") throw new Error(change.message ?? "install failed");
6530
7736
  * ```
7737
+ *
7738
+ * @remarks
7739
+ * The plugins the player knows: installed, built-in and this run's
7740
+ * development loads. Reading the list needs no capability; installing,
7741
+ * enabling, reloading and uninstalling need `plugins.manage` and act on
7742
+ * other plugins, never on the calling one. Built-in plugins update with
7743
+ * the player and cannot be installed, disabled or uninstalled.
7744
+ *
7745
+ * @capability None for `list` and `onChange`; plugins.manage for the rest.
7746
+ * @since 1.0.0
7747
+ * @category Packages
6531
7748
  */
6532
7749
  export interface Packages {
6533
7750
  /**
@@ -6538,6 +7755,37 @@ export interface Packages {
6538
7755
  * The player keeps the 32 most recent candidates of all plugins; an
6539
7756
  * older one's `install` fails with `stale` (validate again). Before
6540
7757
  * player 0.4.2 the 33rd validation of a run failed with `busy` instead.
7758
+ *
7759
+ * @capability plugins.manage
7760
+ * @throws `host` (rejects) Not a package directory, or a manifest that
7761
+ * breaks the rules (another `apiVersion` included); `message` says why.
7762
+ * @throws `invalid` (rejects) A bad request (a path that is not a string).
7763
+ * @throws `javascript` (rejects) The entry module does not parse.
7764
+ * @throws `slint` (rejects) A Slint file does not compile.
7765
+ * @throws `reserved` (rejects) A reserved id (`vestra`, `vestra.*`).
7766
+ * @throws `versionMismatch` (rejects) The package's `minPlayerVersion` is
7767
+ * newer than the player.
7768
+ * @throws `disabled` (rejects) The calling plugin is stopping.
7769
+ * @throws `capability` (rejects) The manifest does not declare
7770
+ * `plugins.manage`.
7771
+ *
7772
+ * @example Check a package directory before installing it
7773
+ * The plugin checks a folder it unpacked and installs it if the player
7774
+ * accepts it. `validate` runs the player's checks and resolves with a
7775
+ * candidate; a package that fails them rejects with the reason.
7776
+ * ```ts
7777
+ * declare const directory: string;
7778
+ * // ---cut---
7779
+ * try {
7780
+ * // Rejects with the reason when the package fails the checks.
7781
+ * const candidate = await plugin.packages.validate(directory);
7782
+ * await (await candidate.install()).done;
7783
+ * } catch (error) {
7784
+ * await plugin.osd.show("Not a valid plugin: {}", [(error as Error).message]);
7785
+ * }
7786
+ * ```
7787
+ *
7788
+ * @since 1.0.0
6541
7789
  */
6542
7790
  validate(directory: string): Promise<PackageCandidate>;
6543
7791
  /**
@@ -6545,19 +7793,97 @@ export interface Packages {
6545
7793
  * same checks as {@link Packages.validate} after unpacking. Check the
6546
7794
  * file's sha256 yourself before calling when you know it. A reserved id
6547
7795
  * fails with `reserved`, as in {@link Packages.validate}.
7796
+ *
7797
+ * @capability plugins.manage
7798
+ * @throws `invalid` (rejects) The path does not end in `.vxp`, or the
7799
+ * archive cannot be unpacked (over 64 MB, too many files, a damaged zip,
7800
+ * an unsafe path).
7801
+ * @throws `host` (rejects) The unpacked package or its manifest breaks the
7802
+ * rules; `message` says why.
7803
+ * @throws `javascript` (rejects) The entry module does not parse.
7804
+ * @throws `slint` (rejects) A Slint file does not compile.
7805
+ * @throws `reserved` (rejects) A reserved id.
7806
+ * @throws `versionMismatch` (rejects) The package's `minPlayerVersion` is
7807
+ * newer than the player.
7808
+ * @throws `disabled` (rejects) The calling plugin is stopping.
7809
+ * @throws `capability` (rejects) The manifest does not declare
7810
+ * `plugins.manage`.
7811
+ *
7812
+ * @example Install a downloaded package file
7813
+ * A `.vxp` the plugin downloaded is checked and installed. The player
7814
+ * unpacks and checks it like a directory; checking the file's sha256
7815
+ * first, when the store publishes one, is up to the plugin.
7816
+ * ```ts
7817
+ * declare const file: string;
7818
+ * // ---cut---
7819
+ * const candidate = await plugin.packages.validateArchive(file); // a .vxp path
7820
+ * const change = await candidate.install();
7821
+ * await plugin.osd.show("Install: {}", [await change.done]);
7822
+ * ```
7823
+ *
7824
+ * @since 1.1.0
6548
7825
  */
6549
7826
  validateArchive(file: string): Promise<PackageCandidate>;
6550
- /** Every plugin: running, installed, and this run's development loads. No capability. */
7827
+ /**
7828
+ * Every plugin: running, installed, and this run's development loads. No capability.
7829
+ *
7830
+ * @capability None.
7831
+ * @throws `Error` (rejects) Only when the player cannot be asked; no
7832
+ * capability is needed.
7833
+ *
7834
+ * @example Show which plugins are running
7835
+ * The plugin lists the running plugins by name. `list` asks the player
7836
+ * and resolves with every plugin it knows, with its state, origin and
7837
+ * display texts in the interface language.
7838
+ * ```ts
7839
+ * const plugins = await plugin.packages.list();
7840
+ * const running = plugins.filter((info) => info.state === "running");
7841
+ * console.log(running.map((info) => info.display?.name ?? info.id));
7842
+ * ```
7843
+ *
7844
+ * @since 1.0.0
7845
+ */
6551
7846
  list(): Promise<PluginInfo[]>;
6552
- /** The new list after any start, stop, failure, install or uninstall. No capability. */
7847
+ /**
7848
+ * The new list after any start, stop, failure, install or uninstall. No capability.
7849
+ *
7850
+ * @capability None.
7851
+ *
7852
+ * @example Refresh a plugin list after any change
7853
+ * A store window's list updates when a plugin starts, stops, fails, is
7854
+ * installed or uninstalled. The listener receives the new list each time.
7855
+ * ```ts
7856
+ * plugin.packages.onChange((plugins) => console.log(plugins.length, "plugins"));
7857
+ * ```
7858
+ *
7859
+ * @since 1.0.0
7860
+ */
6553
7861
  onChange(listener: (plugins: PluginInfo[]) => void): Disposable;
6554
7862
  /**
6555
7863
  * Stop or start a plugin (`plugins.manage`). An installed plugin stays
6556
7864
  * disabled across launches; a development load only for this run.
6557
7865
  * `invalid` to disable oneself, `missing` unknown.
6558
- * `busy` while it is being installed. `unsupported` to disable a built-in
7866
+ * `busy` while it is being installed or reloaded. `unsupported` to disable a built-in
6559
7867
  * ({@link PluginInfo.builtin}): users manage third-party plugins only
6560
7868
  * (players released with SDK 1.5.18 on; older ones disabled built-ins too).
7869
+ *
7870
+ * @capability plugins.manage
7871
+ * @throws `invalid` (rejects) Disabling the calling plugin itself.
7872
+ * @throws `missing` (rejects) An unknown id.
7873
+ * @throws `busy` (rejects) The plugin is being installed or reloaded.
7874
+ * @throws `unsupported` (rejects) A built-in plugin.
7875
+ * @throws `capability` (rejects) The manifest does not declare
7876
+ * `plugins.manage`.
7877
+ *
7878
+ * @example Turn a third-party plugin off
7879
+ * The plugin stops another plugin and keeps it disabled across launches
7880
+ * until it is enabled again. Built-in plugins cannot be disabled
7881
+ * (`unsupported`).
7882
+ * ```ts
7883
+ * await plugin.packages.setEnabled("someone.demo", false);
7884
+ * ```
7885
+ *
7886
+ * @since 1.0.0
6561
7887
  */
6562
7888
  setEnabled(id: string, enabled: boolean): Promise<void>;
6563
7889
  /**
@@ -6578,6 +7904,23 @@ export interface Packages {
6578
7904
  * @player 0.5.1; older players reject it as an unknown request
6579
7905
  * (`unsupported`).
6580
7906
  * @since 1.5.19
7907
+ *
7908
+ * @capability plugins.manage
7909
+ * @throws `invalid` (rejects) The calling plugin itself, or a disabled
7910
+ * plugin.
7911
+ * @throws `missing` (rejects) An unknown id.
7912
+ * @throws `busy` (rejects) The plugin is being installed or reloaded.
7913
+ * @throws `unsupported` (rejects) A player before 0.5.1.
7914
+ * @throws `capability` (rejects) The manifest does not declare
7915
+ * `plugins.manage`.
7916
+ *
7917
+ * @example Restart a plugin that misbehaves
7918
+ * The other plugin is stopped and started again from the same package and
7919
+ * data. `reload` resolves once the old process is stopped and the start is
7920
+ * scheduled; `onChange` then shows `starting` and `running` (or `failed`).
7921
+ * ```ts
7922
+ * await plugin.packages.reload("someone.demo");
7923
+ * ```
6581
7924
  */
6582
7925
  reload(id: string): Promise<void>;
6583
7926
  /**
@@ -6586,19 +7929,51 @@ export interface Packages {
6586
7929
  * reconnects to its data. Its user bindings stay (as orphans).
6587
7930
  * `unsupported` for built-ins and development loads, `missing`
6588
7931
  * unknown, `invalid` for oneself.
7932
+ *
7933
+ * @capability plugins.manage
7934
+ * @throws `unsupported` (rejects) A built-in plugin or a development load.
7935
+ * @throws `missing` (rejects) An unknown id.
7936
+ * @throws `invalid` (rejects) The calling plugin itself.
7937
+ * @throws `busy` (rejects) The plugin library is being cleaned up, or that
7938
+ * plugin is being installed or reloaded.
7939
+ * @throws `revisionConflict` (rejects) The installed revision of that
7940
+ * plugin changed meanwhile; call `uninstall` again.
7941
+ * @throws `capability` (rejects) The manifest does not declare
7942
+ * `plugins.manage`.
7943
+ *
7944
+ * @example Uninstall a plugin together with its data
7945
+ * The other plugin is removed and its data directory deleted, so a later
7946
+ * reinstall starts fresh. Without `removeData` a reinstall from the same
7947
+ * source finds its data again.
7948
+ * ```ts
7949
+ * const change = await plugin.packages.uninstall("someone.demo", { removeData: true });
7950
+ * await change.done;
7951
+ * ```
7952
+ *
7953
+ * @since 1.0.0
6589
7954
  */
6590
7955
  uninstall(id: string, options?: {
6591
7956
  removeData?: boolean;
6592
7957
  }): Promise<PackageChange>;
6593
7958
  }
6594
- /** A command: manifest id (`"host"` for the player's own) and command id. */
7959
+ /**
7960
+ * A command: manifest id (`"host"` for the player's own) and command id.
7961
+ *
7962
+ * @since 1.0.0
7963
+ * @category Commands
7964
+ */
6595
7965
  export interface BindingRef {
6596
7966
  /** Manifest id, or `"host"`. */
6597
7967
  plugin: string;
6598
7968
  /** Command id within that plugin. */
6599
7969
  command: string;
6600
7970
  }
6601
- /** One command's bindings, from {@link Bindings.list}. */
7971
+ /**
7972
+ * One command's bindings, from {@link Bindings.list}.
7973
+ *
7974
+ * @since 1.0.0
7975
+ * @category Commands
7976
+ */
6602
7977
  export interface BindingInfo extends BindingRef {
6603
7978
  /** Menu title. */
6604
7979
  title: string;
@@ -6648,9 +8023,40 @@ export interface BindingInfo extends BindingRef {
6648
8023
  * // later
6649
8024
  * await plugin.bindings.reset({ plugin: "host", command: "mute" });
6650
8025
  * ```
8026
+ *
8027
+ * @remarks
8028
+ * The player's binding table: every command's default keys (from
8029
+ * {@link CommandOptions.bindings}, the player's own as `host/<action>`)
8030
+ * and the user's overrides on top, saved across launches. A key-settings
8031
+ * plugin lists the table, writes overrides and resets them; the player
8032
+ * applies the change at once to every running plugin.
8033
+ *
8034
+ * @capability bindings.manage
8035
+ * @see [Commands, menus and key bindings](../../docs/concepts/commands.md)
8036
+ * @since 1.0.0
8037
+ * @category Commands
6651
8038
  */
6652
8039
  export interface Bindings {
6653
- /** Every declared command, and overrides whose command is gone. */
8040
+ /**
8041
+ * Every declared command, and overrides whose command is gone.
8042
+ *
8043
+ * @capability bindings.manage
8044
+ * @throws `capability` (rejects) The manifest does not declare
8045
+ * `bindings.manage`.
8046
+ *
8047
+ * @example Find the commands that use a key
8048
+ * A key-settings window shows which commands Ctrl+K is bound to now. `list`
8049
+ * resolves with every declared command and its current bindings (the
8050
+ * defaults with the user's overrides applied) and with the overrides
8051
+ * whose command is not running (`orphans`).
8052
+ * ```ts
8053
+ * const { commands } = await plugin.bindings.list();
8054
+ * const users = commands.filter((command) => command.current.includes("Ctrl+K"));
8055
+ * console.log(users.map((command) => `${command.plugin}/${command.command}`)); // "host" for the player
8056
+ * ```
8057
+ *
8058
+ * @since 1.0.0
8059
+ */
6654
8060
  list(): Promise<{
6655
8061
  /** Every declared command (running plugins and the player). */
6656
8062
  commands: BindingInfo[];
@@ -6667,11 +8073,65 @@ export interface Bindings {
6667
8073
  * `invalid` for a bad binding, `missing` for an unknown command that has
6668
8074
  * no override either.
6669
8075
  * At most 16 inputs, syntax as {@link CommandOptions.bindings}.
8076
+ *
8077
+ * @capability bindings.manage
8078
+ * @throws `invalid` (rejects) A malformed binding, or more than 16.
8079
+ * @throws `missing` (rejects) An unknown command that has no override
8080
+ * either.
8081
+ * @throws `capability` (rejects) The manifest does not declare
8082
+ * `bindings.manage`.
8083
+ *
8084
+ * @example Give the player's Mute command a second key
8085
+ * Mute now answers to M and to Ctrl+M. `set` replaces the command's
8086
+ * bindings with the list given (an override saved across launches), so it
8087
+ * repeats the default it keeps; `[]` would unbind it.
8088
+ * ```ts
8089
+ * await plugin.bindings.set({ plugin: "host", command: "mute" }, ["M", "Ctrl+M"]);
8090
+ * ```
8091
+ *
8092
+ * @since 1.0.0
6670
8093
  */
6671
8094
  set(ref: BindingRef, inputs: string[]): Promise<void>;
6672
- /** Back to the defaults: one command, or every override when omitted. */
8095
+ /**
8096
+ * Back to the defaults: one command, or every override when omitted.
8097
+ *
8098
+ * @capability bindings.manage
8099
+ * @throws `missing` (rejects) A command that has no override and is not
8100
+ * declared.
8101
+ * @throws `capability` (rejects) The manifest does not declare
8102
+ * `bindings.manage`.
8103
+ *
8104
+ * @example Restore every default key
8105
+ * A "Restore defaults" button removes all of the user's overrides, so
8106
+ * every command has its declared keys again. Without an argument `reset`
8107
+ * clears every override; with a reference only that command's.
8108
+ * ```ts
8109
+ * await plugin.bindings.reset(); // every override
8110
+ * ```
8111
+ *
8112
+ * @since 1.0.0
8113
+ */
6673
8114
  reset(ref?: BindingRef): Promise<void>;
6674
- /** Declarations or overrides changed (read `list()` again). At most every 200 ms. */
8115
+ /**
8116
+ * Declarations or overrides changed (read `list()` again). At most every 200 ms.
8117
+ *
8118
+ * @capability bindings.manage
8119
+ *
8120
+ * @example Refresh a key list when bindings change
8121
+ * The plugin's key list stays current when the user changes a key in the
8122
+ * player or another plugin starts. The listener carries no data and runs
8123
+ * at most every 200 ms; the plugin reads the table again with `list`.
8124
+ * ```ts
8125
+ * declare function render(commands: BindingInfo[]): void;
8126
+ * // ---cut---
8127
+ * plugin.bindings.onChange(async () => {
8128
+ * const { commands } = await plugin.bindings.list(); // read the new table
8129
+ * render(commands);
8130
+ * });
8131
+ * ```
8132
+ *
8133
+ * @since 1.0.0
8134
+ */
6675
8135
  onChange(listener: () => void): Disposable;
6676
8136
  }
6677
8137
  /**