@vestra-player/sdk 1.5.31 → 1.5.33

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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,47 @@ 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
3994
  * The operating system and the displays, read when called (displays come
3604
3995
  * and go). No capability. CPU and memory: Node's `os` module.
3996
+ *
3997
+ * @capability None.
3998
+ * @throws `busy` (rejects) The player could not start the request (no
3999
+ * thread available).
4000
+ *
4001
+ * @example Choose a size for the primary display
4002
+ * The plugin sizes its picture for the primary display. `system()` asks
4003
+ * the player; `displays` lists the displays present now, the primary one
4004
+ * first (empty without a user interface).
4005
+ * ```ts
4006
+ * const { displays } = await plugin.app.system();
4007
+ * const primary = displays[0];
4008
+ * if (primary) console.log(primary.width, primary.height);
4009
+ * ```
4010
+ *
4011
+ * @since 1.0.0
3605
4012
  */
3606
4013
  system(): Promise<SystemInfo>;
3607
4014
  }
3608
- /** What {@link App.system} reports. */
4015
+ /**
4016
+ * What {@link App.system} reports.
4017
+ *
4018
+ * @see {@link App.system}
4019
+ * @since 1.0.0
4020
+ * @category Plugin
4021
+ */
3609
4022
  export interface SystemInfo {
3610
4023
  /** The operating system family. */
3611
4024
  readonly os: "macos" | "windows" | "linux" | "other";
@@ -3622,7 +4035,12 @@ export interface SystemInfo {
3622
4035
  /** Primary display first; empty without a user interface. */
3623
4036
  readonly displays: readonly DisplayInfo[];
3624
4037
  }
3625
- /** One display in {@link SystemInfo.displays}. */
4038
+ /**
4039
+ * One display in {@link SystemInfo.displays}.
4040
+ *
4041
+ * @since 1.0.0
4042
+ * @category Plugin
4043
+ */
3626
4044
  export interface DisplayInfo {
3627
4045
  /** The system's name for it, `null` when it has none. */
3628
4046
  readonly name: string | null;
@@ -5630,6 +6048,9 @@ export interface Osd {
5630
6048
  * The state of a filter, display layer or render options handle.
5631
6049
  * After `failed` nothing retries automatically until the next `set()`,
5632
6050
  * `retry()` or a new source.
6051
+ *
6052
+ * @since 1.0.0
6053
+ * @category Processing
5633
6054
  */
5634
6055
  export type FilterStatus =
5635
6056
  /** No ready media; the value is kept and applied when media is ready. */
@@ -5705,6 +6126,18 @@ interface Filter<T> extends Disposable {
5705
6126
  * picture.onStatus((s) => s.state === "failed" && plugin.osd.show(s.error.message));
5706
6127
  * picture.set([{ kind: "eq", brightness: 0.1, contrast: 1.1, saturation: 1, gamma: 1 }]);
5707
6128
  * ```
6129
+ *
6130
+ * @remarks
6131
+ * Members: `set(nodes)` records the desired chain (last write wins) and
6132
+ * sends it, one request at a time and only the newest; `status` and
6133
+ * `onStatus` report `waiting`, `applying`, `applied` or `failed`; `retry`
6134
+ * applies the current value again; `dispose` withdraws the chain. `set`
6135
+ * never throws: a refused chain shows as `failed` in `status`.
6136
+ *
6137
+ * @capability processing.video, playback.read and events
6138
+ * @see [Video and audio processing](../../docs/concepts/processing.md)
6139
+ * @since 1.0.0
6140
+ * @category Processing
5708
6141
  */
5709
6142
  export type VideoFilter = Filter<VideoNode[]>;
5710
6143
  /**
@@ -5724,6 +6157,16 @@ export type VideoFilter = Filter<VideoNode[]>;
5724
6157
  * const audio = plugin.audio.filter("sound");
5725
6158
  * audio.set({ nodes: [{ kind: "volume", gainDb: -6 }], delayMs: 120 }); // 10 ms steps
5726
6159
  * ```
6160
+ *
6161
+ * @remarks
6162
+ * The same members and rules as {@link VideoFilter}: `set` records the
6163
+ * desired value and never throws, `status` and `onStatus` report the
6164
+ * result, `retry` applies it again, `dispose` withdraws it.
6165
+ *
6166
+ * @capability processing.audio, playback.read and events
6167
+ * @see [Video and audio processing](../../docs/concepts/processing.md)
6168
+ * @since 1.0.0
6169
+ * @category Processing
5727
6170
  */
5728
6171
  export type AudioFilter = Filter<{
5729
6172
  /** The audio chain; `[]` for none. */
@@ -5734,6 +6177,9 @@ export type AudioFilter = Filter<{
5734
6177
  /**
5735
6178
  * How the video sits in the video area; not part of snapshots or exports.
5736
6179
  * Defaults: `{ aspect: null, fit: "keep", zoom: 1, panX: 0, panY: 0 }`.
6180
+ *
6181
+ * @since 1.0.0
6182
+ * @category Processing
5737
6183
  */
5738
6184
  export interface ViewSettings {
5739
6185
  /** Display aspect override 0.25–4; `null` = as the source. */
@@ -5761,24 +6207,73 @@ export interface ViewSettings {
5761
6207
  * The video fills the video area (cropped instead of letterboxed), then
5762
6208
  * zooms to 150 % shifted a little to the right. The first plugin to call
5763
6209
  * `view()` owns the display layer; each `set` merges into its settings,
5764
- * which stay across sources until the plugin resets them or stops.
6210
+ * which stay across sources until the plugin disposes the view or stops.
5765
6211
  * ```ts
5766
6212
  * const view = plugin.video.view();
5767
6213
  * view.set({ fit: "fill" });
5768
6214
  * view.set({ zoom: 1.5, panX: 0.1 }); // merged with the previous write
5769
6215
  * ```
6216
+ *
6217
+ * @remarks
6218
+ * Not part of snapshots or exports: it changes how the picture sits in the
6219
+ * video area, not the picture. `set` merges into the current settings and
6220
+ * never throws: an out-of-range value or unknown field shows as `failed`
6221
+ * (`invalid`) in `status` and is reported to `onError`, without sending
6222
+ * anything.
6223
+ *
6224
+ * @capability processing.video, playback.read and events (like
6225
+ * {@link VideoFilter})
6226
+ * @see [Video and audio processing](../../docs/concepts/processing.md)
6227
+ * @since 1.0.0
6228
+ * @category Processing
5770
6229
  */
5771
6230
  export interface VideoView extends Disposable {
5772
- /** Merged into the current settings; last write wins. */
6231
+ /**
6232
+ * Merged into the current settings; last write wins.
6233
+ *
6234
+ * @capability processing.video
6235
+ *
6236
+ * @example Stretch a 4:3 video to fill the window
6237
+ * A 4:3 video fills a 16:9 window, distorted, as the user asked. Each
6238
+ * `set` merges the given fields into this plugin's view settings; the
6239
+ * newest write wins, and the settings stay for later files until the
6240
+ * plugin disposes the view or stops.
6241
+ * ```ts
6242
+ * plugin.video.view().set({ fit: "stretch" });
6243
+ * ```
6244
+ *
6245
+ * @since 1.0.0
6246
+ */
5773
6247
  set(value: Partial<ViewSettings>): void;
5774
6248
  /** Current status (the same four states as filters). */
5775
6249
  readonly status: FilterStatus;
5776
- /** The status changed. */
6250
+ /**
6251
+ * The status changed.
6252
+ *
6253
+ * @capability processing.video
6254
+ *
6255
+ * @example Tell the user another plugin owns the view
6256
+ * When another plugin already controls the display layer, the OSD says
6257
+ * so. The first plugin to call `view()` owns it; this plugin's handle
6258
+ * then reports `failed` with `conflict`, and tries again on its next `set`.
6259
+ * ```ts
6260
+ * const view = plugin.video.view();
6261
+ * view.onStatus((status) => {
6262
+ * if (status.state === "failed" && status.error.code === "conflict")
6263
+ * void plugin.osd.show("Another plugin controls the picture size");
6264
+ * });
6265
+ * ```
6266
+ *
6267
+ * @since 1.0.0
6268
+ */
5777
6269
  onStatus(listener: (status: FilterStatus) => void): Disposable;
5778
6270
  }
5779
6271
  /**
5780
6272
  * GPU renderer options; not part of snapshots or exports.
5781
6273
  * Defaults: `{ toneMapping: "auto", targetPeak: 203, peakDetect: true, shaders: [], interpolation: false }`.
6274
+ *
6275
+ * @since 1.0.0
6276
+ * @category Processing
5782
6277
  */
5783
6278
  export interface RenderSettings {
5784
6279
  /** HDR → SDR: "auto" maps HDR sources only; "off" clips without a curve. */
@@ -5817,24 +6312,74 @@ export interface RenderSettings {
5817
6312
  * });
5818
6313
  * render.set({ toneMapping: "bt.2390", targetPeak: 250 }); // nits
5819
6314
  * ```
6315
+ *
6316
+ * @remarks
6317
+ * GPU renderer options (tone mapping, peak detection, shaders,
6318
+ * interpolation). Only the GPU renderer has them, which today means macOS;
6319
+ * under software rendering the handle is `failed` with `unsupported` after
6320
+ * the player's first answer to `render()` (reported to `onError` too), and so is
6321
+ * every `set`. Single owner, like {@link VideoView}. `set` never throws; the
6322
+ * result shows in `status`.
6323
+ *
6324
+ * @capability processing.video, playback.read and events (like
6325
+ * {@link VideoFilter})
6326
+ * @see [Video and audio processing](../../docs/concepts/processing.md)
6327
+ * @since 1.0.0
6328
+ * @category Processing
5820
6329
  */
5821
6330
  export interface RenderOptions extends Disposable {
5822
- /** Merged into the current settings; last write wins. */
6331
+ /**
6332
+ * Merged into the current settings; last write wins.
6333
+ *
6334
+ * @capability processing.video
6335
+ *
6336
+ * @example Turn on motion interpolation
6337
+ * Motion becomes smoother on the GPU renderer. `set` merges the field into
6338
+ * the plugin's render options; under software rendering the status turns
6339
+ * `failed` with `unsupported` instead.
6340
+ * ```ts
6341
+ * plugin.video.render().set({ interpolation: true });
6342
+ * ```
6343
+ *
6344
+ * @since 1.0.0
6345
+ */
5823
6346
  set(value: Partial<RenderSettings>): void;
5824
6347
  /**
5825
6348
  * `failed` with `unsupported` while the player renders in software;
5826
6349
  * `failed` with `failed` when mpv cannot compile a shader (pushed later).
5827
6350
  */
5828
6351
  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. */
5833
- export interface SubtitleTrack {
5834
- /** Stable while the track exists; compare tracks by id. */
5835
- id: string;
5836
- /** Inside the container, or loaded from a file. */
5837
- kind: "embedded" | "external";
6352
+ /**
6353
+ * The status changed (including later pushes such as shader compile errors).
6354
+ *
6355
+ * @capability processing.video
6356
+ *
6357
+ * @example Report a shader that does not compile
6358
+ * The OSD shows mpv's message when a shader the plugin set fails to
6359
+ * compile. The player pushes that failure after the `set` was accepted, so
6360
+ * it arrives through `onStatus`, not as a rejected call.
6361
+ * ```ts
6362
+ * const render = plugin.video.render();
6363
+ * render.onStatus((status) => {
6364
+ * if (status.state === "failed") void plugin.osd.show("Render: {}", [status.error.message]);
6365
+ * });
6366
+ * ```
6367
+ *
6368
+ * @since 1.0.0
6369
+ */
6370
+ onStatus(listener: (status: FilterStatus) => void): Disposable;
6371
+ }
6372
+ /**
6373
+ * A subtitle track of the current source.
6374
+ *
6375
+ * @since 1.0.0
6376
+ * @category Subtitles
6377
+ */
6378
+ export interface SubtitleTrack {
6379
+ /** Stable while the track exists; compare tracks by id. */
6380
+ id: string;
6381
+ /** Inside the container, or loaded from a file. */
6382
+ kind: "embedded" | "external";
5838
6383
  /** The container's subtitle track number, for embedded tracks only. */
5839
6384
  embeddedId?: number;
5840
6385
  /** Track title. */
@@ -5857,7 +6402,13 @@ export interface SubtitleTrack {
5857
6402
  */
5858
6403
  streamIndex: number | null;
5859
6404
  }
5860
- /** The subtitle state of the current source. */
6405
+ /**
6406
+ * The subtitle state of the current source.
6407
+ *
6408
+ * @see {@link Subtitles.state}
6409
+ * @since 1.0.0
6410
+ * @category Subtitles
6411
+ */
5861
6412
  export interface SubtitlesState {
5862
6413
  /** The source it belongs to ({@link PlayerState.sourceId}). */
5863
6414
  readonly sourceId: number;
@@ -5888,7 +6439,12 @@ export interface SubtitlesState {
5888
6439
  /** The last error, `null` without one. */
5889
6440
  readonly error: string | null;
5890
6441
  }
5891
- /** Subtitle calls act on the selection you see; pass `ifCurrent` to pin an older one. */
6442
+ /**
6443
+ * Subtitle calls act on the selection you see; pass `ifCurrent` to pin an older one.
6444
+ *
6445
+ * @since 1.0.0
6446
+ * @category Subtitles
6447
+ */
5892
6448
  export interface SubtitleConditional {
5893
6449
  /** The subtitle state the call is conditional on; `stale` otherwise. */
5894
6450
  ifCurrent?: SubtitlesState;
@@ -5922,18 +6478,104 @@ export interface SubtitleConditional {
5922
6478
  * offsetMs: 0,
5923
6479
  * });
5924
6480
  * ```
6481
+ *
6482
+ * @remarks
6483
+ * The subtitle tracks of the current source, the primary and secondary
6484
+ * selection, uploaded files and the style. `state` is the SDK's snapshot of
6485
+ * the player's subtitle state (`null` until first read). The player has no
6486
+ * subtitle event: the SDK reads the state again when the source, its phase
6487
+ * or the backend changes (with `events`) and after this plugin's own
6488
+ * changes, so a selection the user or another plugin changed shows only
6489
+ * after `refresh()`. A change without `ifCurrent` reads the state first
6490
+ * and applies to it; pass `ifCurrent` to make it fail with `stale` when the
6491
+ * selection changed since that state. Each change is one operation the
6492
+ * player confirms; `select`, `remove` and `setStyle` resolve with the new
6493
+ * state, `load` with the new track.
6494
+ *
6495
+ * @capability `subtitles.read` to read; `subtitles.control` and
6496
+ * `subtitles.read` to change; `events` to re-read on source changes.
6497
+ *
6498
+ * @see [Load subtitles from a plugin](../../docs/how-to/subtitles.md)
6499
+ * @since 1.0.0
6500
+ * @category Subtitles
5925
6501
  */
5926
6502
  export interface Subtitles {
5927
6503
  /** `null` until first read. */
5928
6504
  readonly state: SubtitlesState | null;
5929
- /** The subtitle state changed. */
6505
+ /**
6506
+ * The subtitle state changed.
6507
+ *
6508
+ * @capability events and subtitles.read
6509
+ *
6510
+ * @example Show the subtitle language after a new file opens
6511
+ * When a new file starts, the OSD names the language of its primary
6512
+ * subtitle. The SDK reads the subtitle state again when the source or its
6513
+ * phase changes (there is no subtitle event, so a selection changed in the
6514
+ * player shows only after `refresh()`) and then calls the listener;
6515
+ * `selected` is `null` when subtitles are off.
6516
+ * ```ts
6517
+ * plugin.subtitles.onChange((state) => {
6518
+ * // A track's language is null when the file does not say.
6519
+ * void plugin.osd.show("Subtitles: {}", [state.selected?.language ?? "off"]);
6520
+ * });
6521
+ * ```
6522
+ *
6523
+ * @since 1.0.0
6524
+ */
5930
6525
  onChange(listener: (state: SubtitlesState) => void): Disposable;
5931
- /** Read the state from the player now. */
6526
+ /**
6527
+ * Read the state from the player now.
6528
+ *
6529
+ * @capability subtitles.read
6530
+ * @throws `capability` (rejects) The manifest does not declare
6531
+ * `subtitles.read`.
6532
+ *
6533
+ * @example Count the subtitle tracks of the file
6534
+ * The plugin reads how many subtitle tracks the file has before building a
6535
+ * menu. `refresh` asks the player and resolves with the current state, also
6536
+ * without `events`; `state` holds it afterwards.
6537
+ * ```ts
6538
+ * const { tracks } = await plugin.subtitles.refresh();
6539
+ * await plugin.osd.show("{} subtitle tracks", [tracks.length]);
6540
+ * ```
6541
+ *
6542
+ * @since 1.0.0
6543
+ */
5932
6544
  refresh(): Promise<SubtitlesState>;
5933
6545
  /**
5934
6546
  * `role: "secondary"` sets the secondary subtitle: a text track that is not
5935
6547
  * the primary one (`invalid` otherwise). A new source clears it.
5936
6548
  * `track: null` turns the role off.
6549
+ *
6550
+ * @capability subtitles.control and subtitles.read
6551
+ * @throws `stale` (rejects) With `ifCurrent`: the source or the subtitle
6552
+ * selection changed since that state.
6553
+ * @throws `invalid` (rejects) An unknown track; as secondary, a bitmap
6554
+ * track or the primary one; as primary, the current secondary.
6555
+ * @throws `missing` (rejects) The track is not in the current source.
6556
+ * @throws `unavailable` (rejects) Nothing loaded, or the source is still
6557
+ * loading or not yet confirmed.
6558
+ * @throws `busy` (rejects) Too many subtitle operations at once.
6559
+ * @throws `capability` (rejects) The manifest does not declare
6560
+ * `subtitles.control` and `subtitles.read`.
6561
+ *
6562
+ * @example Turn on the first English subtitle
6563
+ * The first English track becomes the primary subtitle, but only in the
6564
+ * file the plugin looked at. Embedded tracks are chosen by number, so
6565
+ * without `ifCurrent` a select after another file opened would pick that
6566
+ * file's track with the same number; with `ifCurrent: state` it fails with
6567
+ * `stale` instead. Without `ifCurrent`, `select` reads the newest state
6568
+ * itself and almost never fails with `stale`.
6569
+ * ```ts
6570
+ * const state = await plugin.subtitles.refresh();
6571
+ * const english = state.tracks.find((track) => track.language === "en");
6572
+ * if (english)
6573
+ * await plugin.subtitles.select(english, { ifCurrent: state }).catch((error: PluginError) => {
6574
+ * if (error.code !== "stale") throw error; // another file or selection now
6575
+ * });
6576
+ * ```
6577
+ *
6578
+ * @since 1.0.0
5937
6579
  */
5938
6580
  select(track: SubtitleTrack | null, options?: SubtitleConditional & {
5939
6581
  role?: "primary" | "secondary";
@@ -5950,6 +6592,36 @@ export interface Subtitles {
5950
6592
  * Limits: SRT ≤ 2 MiB and ≤ 10,000 cues; ASS ≤ 2 MiB with `[Script Info]`
5951
6593
  * and `[Events]` and 1–20,000 `Dialogue:` lines. Convert other formats
5952
6594
  * (VTT, SAMI) to SRT first.
6595
+ *
6596
+ * @capability subtitles.control and subtitles.read
6597
+ * @throws `invalid` (rejects) A bad `format` or `title`, or a file over the
6598
+ * limits.
6599
+ * @throws `stale` (rejects) With `ifCurrent`: the source changed since
6600
+ * that state.
6601
+ * @throws `unavailable` (rejects) Nothing loaded, or the source is still
6602
+ * loading.
6603
+ * @throws `aborted` (rejects) `signal` aborted before the commit (an
6604
+ * `AbortError`); the upload is discarded.
6605
+ * @throws `busy` (rejects) Too many subtitle operations at once.
6606
+ * @throws `capability` (rejects) The manifest does not declare
6607
+ * `subtitles.control` and `subtitles.read`.
6608
+ *
6609
+ * @example Load a downloaded SRT and select it
6610
+ * A subtitle file the plugin downloaded becomes a track of the current file
6611
+ * and is selected. The plugin passes the file's bytes; the player parses
6612
+ * them, adds an `external` track for this source only and resolves with
6613
+ * it once the upload is committed.
6614
+ * ```ts
6615
+ * declare const url: string;
6616
+ * // ---cut---
6617
+ * const response = await fetch(url);
6618
+ * if (!response.ok) throw new Error(`subtitles: HTTP ${response.status}`);
6619
+ * const bytes = new Uint8Array(await response.arrayBuffer());
6620
+ * const track = await plugin.subtitles.load(bytes, { title: "Downloaded" }); // selected
6621
+ * await plugin.osd.show("Loaded {}", [track.title]);
6622
+ * ```
6623
+ *
6624
+ * @since 1.0.0
5953
6625
  */
5954
6626
  load(bytes: Uint8Array, options?: SubtitleConditional & {
5955
6627
  format?: "srt" | "ass";
@@ -5957,19 +6629,69 @@ export interface Subtitles {
5957
6629
  select?: "primary" | "secondary" | "none";
5958
6630
  signal?: AbortSignal;
5959
6631
  }): Promise<SubtitleTrack>;
5960
- /** Remove a track this plugin loaded. */
6632
+ /**
6633
+ * Remove a track this plugin loaded.
6634
+ *
6635
+ * @capability subtitles.control and subtitles.read
6636
+ * @throws `invalid` (rejects) The track was not loaded by this plugin.
6637
+ * @throws `stale` (rejects) With `ifCurrent`: the source changed since
6638
+ * that state.
6639
+ * @throws `unavailable` (rejects) Nothing is loaded, or the source is
6640
+ * still loading.
6641
+ * @throws `busy` (rejects) Too many subtitle operations at once.
6642
+ * @throws `capability` (rejects) The manifest does not declare
6643
+ * `subtitles.control` and `subtitles.read`.
6644
+ *
6645
+ * @example Remove the subtitle the plugin loaded
6646
+ * The plugin's downloaded subtitle disappears from the track list. A plugin
6647
+ * can remove only tracks it loaded itself; a new source removes them
6648
+ * anyway.
6649
+ * ```ts
6650
+ * declare const loaded: SubtitleTrack;
6651
+ * // ---cut---
6652
+ * await plugin.subtitles.remove(loaded);
6653
+ * ```
6654
+ *
6655
+ * @since 1.0.0
6656
+ */
5961
6657
  remove(track: SubtitleTrack, options?: SubtitleConditional): Promise<SubtitlesState>;
5962
6658
  /**
5963
6659
  * Set this plugin's style layer; `null` removes it. The newest call of
5964
6660
  * this plugin replaces its earlier layer. When several plugins have a
5965
6661
  * layer, the player uses the one whose manifest id sorts last, whatever
5966
6662
  * the order of the calls.
6663
+ *
6664
+ * @capability subtitles.control and subtitles.read
6665
+ * @throws `invalid` (rejects) A field out of range.
6666
+ * @throws `unsupported` (rejects) A font that is not in `state.fonts`, or a
6667
+ * primary subtitle that is a bitmap track.
6668
+ * @throws `stale` (rejects) With `ifCurrent`: the subtitle state changed
6669
+ * since that state.
6670
+ * @throws `unavailable` (rejects) Nothing is loaded, or the source is
6671
+ * still loading.
6672
+ * @throws `busy` (rejects) Too many subtitle operations at once.
6673
+ * @throws `capability` (rejects) The manifest does not declare
6674
+ * `subtitles.control` and `subtitles.read`.
6675
+ *
6676
+ * @example Take the plugin's style off again
6677
+ * The plugin removes its style layer, so the subtitles go back to the
6678
+ * player's default (or to another plugin's layer). `null` removes only this
6679
+ * plugin's layer; the effective layer is the one whose manifest id sorts
6680
+ * last among the plugins that still have one.
6681
+ * ```ts
6682
+ * await plugin.subtitles.setStyle(null);
6683
+ * ```
6684
+ *
6685
+ * @since 1.0.0
5967
6686
  */
5968
6687
  setStyle(style: SubtitleStyle | null, options?: SubtitleConditional): Promise<SubtitlesState>;
5969
6688
  }
5970
6689
  /**
5971
6690
  * Options of {@link MediaCapture.capture}. Conditional on `player.state`
5972
6691
  * like other controls.
6692
+ *
6693
+ * @since 1.0.0
6694
+ * @category Media
5973
6695
  */
5974
6696
  export interface CaptureOptions extends Conditional {
5975
6697
  /** Source time; defaults to the current position at the moment of capture. */
@@ -5981,7 +6703,36 @@ export interface CaptureOptions extends Conditional {
5981
6703
  /** Audio to freeze with the snapshot: the selected track, none, or a track id. */
5982
6704
  audio?: "selected" | "none" | number;
5983
6705
  }
5984
- /** A frozen, immutable input for exports. Dispose to release it. */
6706
+ /**
6707
+ * A frozen, immutable input for exports. Dispose to release it.
6708
+ *
6709
+ * @remarks
6710
+ * A frozen input, from {@link MediaCapture.capture}: the source, the time
6711
+ * and (processed) the effects and subtitle as they were. Exports read it in
6712
+ * the player's offline worker, so one snapshot can be exported several
6713
+ * times and playback is unaffected. Disposing it releases the player's
6714
+ * copy; an export using it after that fails with `released`.
6715
+ *
6716
+ * @capability media.read and media.control
6717
+ *
6718
+ * @example Export one frame as PNG and as JPEG
6719
+ * The same frozen frame is written twice, lossless and compressed. Each
6720
+ * `export` starts a job of its own; the snapshot is released after both
6721
+ * are done.
6722
+ * ```ts
6723
+ * const snapshot = await plugin.media.capture();
6724
+ * try {
6725
+ * const png = await snapshot.export({ format: "png", destination: "/tmp/frame.png" });
6726
+ * const jpeg = await snapshot.export({ format: "jpeg", destination: "/tmp/frame.jpg", quality: 85 });
6727
+ * await Promise.all([png.done, jpeg.done]); // terminal states, not errors
6728
+ * } finally {
6729
+ * await snapshot.dispose();
6730
+ * }
6731
+ * ```
6732
+ *
6733
+ * @since 1.0.0
6734
+ * @category Media
6735
+ */
5985
6736
  export interface Snapshot extends Disposable {
5986
6737
  /** Without or with effects. */
5987
6738
  readonly kind: "raw" | "processed";
@@ -6005,6 +6756,32 @@ export interface Snapshot extends Disposable {
6005
6756
  /**
6006
6757
  * Start exporting; the job runs in the host's offline worker.
6007
6758
  * One snapshot can be exported several times.
6759
+ *
6760
+ * @capability media.read and media.control
6761
+ * @throws `released` (rejects) The snapshot was already released.
6762
+ * @throws `invalid` (rejects) Bad options (`quality` outside 1–100, a
6763
+ * relative destination, a clip `end` not after the snapshot time or
6764
+ * past the duration).
6765
+ * @throws `busy` (rejects) This plugin runs 2 exports (or the player 8).
6766
+ * @throws `unavailable` (rejects) No FFmpeg, or the export worker could not
6767
+ * start.
6768
+ * @throws `capability` (rejects) The manifest does not declare
6769
+ * `media.read` and `media.control`.
6770
+ *
6771
+ * @example Export a 30-second clip
6772
+ * A clip from the snapshot time to 30 seconds later is written as MP4.
6773
+ * `export` resolves once the job is accepted; the job then runs in the
6774
+ * player's offline worker, and `done` resolves with its terminal state,
6775
+ * whose `outcome` has the destination or the error.
6776
+ * ```ts
6777
+ * declare const snapshot: Snapshot;
6778
+ * // ---cut---
6779
+ * const job = await snapshot.export({ format: "mp4", destination: "/tmp/clip.mp4", end: snapshot.time + 30 });
6780
+ * const end = await job.done;
6781
+ * if (end.status !== "completed") await plugin.osd.show("Export {}", [end.status]);
6782
+ * ```
6783
+ *
6784
+ * @since 1.0.0
6008
6785
  */
6009
6786
  export(options: ExportOptions): Promise<ExportJob>;
6010
6787
  }
@@ -6016,6 +6793,9 @@ export interface Snapshot extends Disposable {
6016
6793
  * choose the format. `sampleTime` picks the frame (default: the snapshot
6017
6794
  * time); `end` is the clip's end in source seconds. On Windows the export
6018
6795
  * worker needs FFmpeg 6.1 or newer.
6796
+ *
6797
+ * @since 1.0.0
6798
+ * @category Media
6019
6799
  */
6020
6800
  export type ExportOptions = {
6021
6801
  /** A lossless frame. */
@@ -6046,19 +6826,111 @@ export type ExportOptions = {
6046
6826
  * `completed`, `failed` or `cancelled`; once terminal, `outcome` is
6047
6827
  * `{ destination }`, `{ error: { code, message } }` or `{ cancelled: true }`.
6048
6828
  * `facts` describes what the worker produced (versions, range, streams).
6829
+ *
6830
+ * @since 1.0.0
6831
+ * @category Media
6049
6832
  */
6050
6833
  export type JobState = Omit<MediaJob, "jobId">;
6051
- /** An export, from {@link Snapshot.export}. Disposing an unfinished job cancels it. */
6834
+ /**
6835
+ * An export, from {@link Snapshot.export}. Disposing an unfinished job cancels it.
6836
+ *
6837
+ * @remarks
6838
+ * One export, from {@link Snapshot.export}. `state` is the SDK's last copy
6839
+ * of the job's state: the SDK polls the player about every 350 ms until the
6840
+ * job ends, and `refresh` reads it at once. `done` resolves
6841
+ * once the job is terminal (`completed`, `failed` or `cancelled`) and never
6842
+ * rejects for a failed export: the failure is in `outcome`. Disposing an
6843
+ * unfinished job cancels it.
6844
+ *
6845
+ * @capability media.read and media.control
6846
+ *
6847
+ * @example Show export progress and cancel on request
6848
+ * The OSD shows the job's status while it runs, and a Cancel command stops
6849
+ * it. `onChange` follows the state (`queued`, `running`, `cancelling`,
6850
+ * then a terminal one); `cancel` asks the worker to stop. After
6851
+ * `requested` the job usually ends `cancelled`, or `failed` when the
6852
+ * cleanup failed.
6853
+ * ```ts
6854
+ * declare const job: ExportJob;
6855
+ * // ---cut---
6856
+ * job.onChange((state) => void plugin.osd.show("Export: {}", [state.status]));
6857
+ * plugin.commands.add({ id: "cancel-export", title: "Cancel export", run: () => job.cancel() });
6858
+ * await job.done; // completed, failed or cancelled
6859
+ * ```
6860
+ *
6861
+ * @since 1.0.0
6862
+ * @category Media
6863
+ */
6052
6864
  export interface ExportJob extends Disposable {
6053
6865
  /** Latest state. */
6054
6866
  readonly state: JobState;
6055
- /** The state changed. */
6867
+ /**
6868
+ * The state changed.
6869
+ *
6870
+ * @capability media.read
6871
+ *
6872
+ * @example Log each state of an export
6873
+ * The plugin's log records each status the job goes through (`queued`,
6874
+ * `running`, `cancelling` after a cancel, then a terminal one). The listener runs when the SDK learns a
6875
+ * new state from its polling.
6876
+ * ```ts
6877
+ * declare const job: ExportJob;
6878
+ * // ---cut---
6879
+ * job.onChange((state) => console.log("export", state.status));
6880
+ * ```
6881
+ *
6882
+ * @since 1.0.0
6883
+ */
6056
6884
  onChange(listener: (state: JobState) => void): Disposable;
6057
- /** Resolves with the terminal state (completed, failed or cancelled). */
6885
+ /**
6886
+ * Resolves with the terminal state (completed, failed or cancelled).
6887
+ *
6888
+ * @throws `disposed` (rejects) The job handle was disposed while waiting.
6889
+ */
6058
6890
  readonly done: Promise<JobState>;
6059
- /** Ask to cancel: `requested`, `tooLate` (already publishing) or `terminal` (already over). */
6891
+ /**
6892
+ * Ask to cancel: `requested`, `tooLate` (already publishing) or `terminal` (already over).
6893
+ *
6894
+ * @capability media.control
6895
+ * @throws `rejected` (rejects) The player did not accept the request.
6896
+ * @throws `capability` (rejects) The manifest does not declare
6897
+ * `media.control`.
6898
+ *
6899
+ * @example Cancel an export the user no longer wants
6900
+ * The running export stops, unless it is already writing its file.
6901
+ * `cancel` resolves `requested` when the worker will stop (the job ends
6902
+ * `cancelled`), `tooLate` when it is already publishing the file, and
6903
+ * `terminal` when the job had already ended.
6904
+ * ```ts
6905
+ * declare const job: ExportJob;
6906
+ * // ---cut---
6907
+ * const answer = await job.cancel();
6908
+ * if (answer === "tooLate") await plugin.osd.show("Too late: the file is being written");
6909
+ * ```
6910
+ *
6911
+ * @since 1.0.0
6912
+ */
6060
6913
  cancel(): Promise<"requested" | "tooLate" | "terminal">;
6061
- /** Read the state from the player now. */
6914
+ /**
6915
+ * Read the state from the player now.
6916
+ *
6917
+ * @capability media.read
6918
+ * @throws `capability` (rejects) The manifest does not declare
6919
+ * `media.read`.
6920
+ *
6921
+ * @example Read a job's state now
6922
+ * The plugin reads the newest state of an export without waiting for an
6923
+ * event. `refresh` asks the player and resolves with the state, which
6924
+ * `state` then holds.
6925
+ * ```ts
6926
+ * declare const job: ExportJob;
6927
+ * // ---cut---
6928
+ * const state = await job.refresh();
6929
+ * console.log(state.status);
6930
+ * ```
6931
+ *
6932
+ * @since 1.0.0
6933
+ */
6062
6934
  refresh(): Promise<JobState>;
6063
6935
  }
6064
6936
  /**
@@ -6086,21 +6958,73 @@ export interface ExportJob extends Disposable {
6086
6958
  * await snapshot.dispose();
6087
6959
  * }
6088
6960
  * ```
6961
+ *
6962
+ * @remarks
6963
+ * Exports run in the player's offline worker from a frozen
6964
+ * {@link Snapshot}: a still frame or a clip, raw or with this plugin's
6965
+ * effects (and the primary subtitle). Playback is not disturbed. A plugin
6966
+ * holds at most 4 snapshots at a time (16 in the player); release them
6967
+ * when done.
6968
+ *
6969
+ * @capability media.read and media.control (processed snapshots also
6970
+ * `playback.read` and `subtitles.read`)
6971
+ * @since 1.0.0
6972
+ * @category Media
6089
6973
  */
6090
6974
  export interface MediaCapture {
6091
6975
  /**
6092
6976
  * Freeze the current source (and optionally this plugin set's effects)
6093
6977
  * 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.
6978
+ * `ifCurrent`; `unsupported` for subtitles that cannot be flattened (only
6979
+ * plain SRT text can) or a picture over the size limits; `noSource` when
6980
+ * nothing is playing.
6981
+ *
6982
+ * @capability media.read and media.control
6983
+ * @throws `stale` (rejects) The source changed since `ifCurrent`.
6984
+ * @throws `noSource` (rejects) Nothing is playing.
6985
+ * @throws `invalid` (rejects) `at` at or after the end of the source.
6986
+ * @throws `busy` (rejects) This plugin holds 4 snapshots (or the player 16);
6987
+ * release one.
6988
+ * @throws `unavailable` (rejects) A processed snapshot while the picture is
6989
+ * still being prepared.
6990
+ * @throws `unsupported` (rejects) A processed snapshot the player cannot
6991
+ * freeze: subtitles that cannot be flattened (only plain SRT text can),
6992
+ * or a picture over the size limits.
6993
+ * @throws `capability` (rejects) The manifest does not declare
6994
+ * `media.read` and `media.control`.
6995
+ *
6996
+ * @example Freeze the frame at the 10-second mark
6997
+ * The plugin freezes the frame at 0:10 of the open file, raw, to export it
6998
+ * later. `capture` resolves with a `Snapshot` once the player has frozen
6999
+ * that input; the snapshot stays valid when playback moves on or the file
7000
+ * changes, until it is released.
7001
+ * ```ts
7002
+ * const snapshot = await plugin.media.capture({ at: 10 }); // seconds
7003
+ * try {
7004
+ * // … export it …
7005
+ * } finally {
7006
+ * await snapshot.dispose(); // frees one of the plugin's 4 snapshot slots
7007
+ * }
7008
+ * ```
7009
+ *
7010
+ * @since 1.0.0
6095
7011
  */
6096
7012
  capture(options?: CaptureOptions): Promise<Snapshot>;
6097
7013
  }
6098
7014
  /**
6099
7015
  * Declaration of {@link Resolvers.add}: a web resolver
6100
7016
  * ({@link WebResolverOptions}) or a file resolver ({@link FileResolverOptions}).
7017
+ *
7018
+ * @since 1.0.0
7019
+ * @category Sources
6101
7020
  */
6102
7021
  export type ResolverOptions = WebResolverOptions | FileResolverOptions;
6103
- /** A resolver for web pages: `https://host/…` to playable stream URLs. */
7022
+ /**
7023
+ * A resolver for web pages: `https://host/…` to playable stream URLs.
7024
+ *
7025
+ * @since 1.0.0
7026
+ * @category Sources
7027
+ */
6104
7028
  export interface WebResolverOptions {
6105
7029
  /** Local id, unique within the plugin. */
6106
7030
  id: string;
@@ -6165,6 +7089,9 @@ export interface WebResolverOptions {
6165
7089
  * },
6166
7090
  * });
6167
7091
  * ```
7092
+ *
7093
+ * @since 1.5.3
7094
+ * @category Sources
6168
7095
  */
6169
7096
  export interface FileResolverOptions {
6170
7097
  /** Local id, unique within the plugin. */
@@ -6189,6 +7116,9 @@ export interface FileResolverOptions {
6189
7116
  * failures "无法启动播放". An error thrown by `frame()` keeps its message.
6190
7117
  * To give your own text for a file you can tell is unusable (a bad header),
6191
7118
  * check it in `resolve` and throw there.
7119
+ *
7120
+ * @since 1.5.3
7121
+ * @category Sources
6192
7122
  */
6193
7123
  export interface ResolvedStream {
6194
7124
  /** Always `"stream"`. */
@@ -6207,7 +7137,38 @@ export interface ResolvedStream {
6207
7137
  */
6208
7138
  started?(stream: Stream): void;
6209
7139
  }
6210
- /** A registered resolver. Disposing it unregisters it and aborts resolutions in flight. */
7140
+ /**
7141
+ * A registered resolver. Disposing it unregisters it and aborts resolutions in flight.
7142
+ *
7143
+ * @remarks
7144
+ * A registered resolver, from {@link Resolvers.add}. Disposing it, its
7145
+ * scope or stopping the plugin unregisters it and aborts resolutions in
7146
+ * flight; the extensions or hosts are free again for other plugins.
7147
+ *
7148
+ * @capability source.resolve
7149
+ *
7150
+ * @example Report when another plugin already handles the format
7151
+ * The plugin tells the user when its `.ch8` resolver could not register
7152
+ * because another plugin holds that extension. `ready` rejects with
7153
+ * `conflict`; for another plugin's claim that does not fail activation, so
7154
+ * the plugin keeps running without the resolver.
7155
+ * ```ts
7156
+ * const resolver = plugin.resolvers.add({
7157
+ * id: "chip8",
7158
+ * extensions: ["ch8"],
7159
+ * async resolve() {
7160
+ * throw plugin.i18n.error("Not ready yet.");
7161
+ * },
7162
+ * });
7163
+ * // Rejects with `conflict` when the extension is taken.
7164
+ * resolver.ready.catch((error: PluginError) => {
7165
+ * if (error.code === "conflict") void plugin.osd.show("Another plugin plays .ch8 files");
7166
+ * });
7167
+ * ```
7168
+ *
7169
+ * @since 1.0.0
7170
+ * @category Sources
7171
+ */
6211
7172
  export interface Resolver extends Disposable {
6212
7173
  /**
6213
7174
  * Resolves when the player registered it; rejects otherwise: `conflict`
@@ -6215,6 +7176,13 @@ export interface Resolver extends Disposable {
6215
7176
  * plugin's: activation goes on; this plugin's own: activation fails),
6216
7177
  * `invalid` for a malformed declaration (a player before 0.4.1 also for
6217
7178
  * `extensions`).
7179
+ *
7180
+ * @throws `conflict` (rejects) Another resolver has the extension or the
7181
+ * scheme + host, or this plugin already registered this id.
7182
+ * @throws `busy` (rejects) The player already holds 16 resolvers.
7183
+ * @throws `invalid` (rejects) A malformed declaration.
7184
+ * @throws `capability` (rejects) The manifest does not declare
7185
+ * `source.resolve`.
6218
7186
  */
6219
7187
  readonly ready: Promise<void>;
6220
7188
  }
@@ -6241,6 +7209,20 @@ export interface Resolver extends Disposable {
6241
7209
  * },
6242
7210
  * });
6243
7211
  * ```
7212
+ *
7213
+ * @remarks
7214
+ * A resolver turns something the player cannot play by itself into
7215
+ * something it can: a web page address into stream URLs
7216
+ * ({@link WebResolverOptions}), or a local file of a format the plugin
7217
+ * understands into a stream the plugin generates
7218
+ * ({@link FileResolverOptions}). The player asks the resolver whenever
7219
+ * anyone opens a matching address or file (the Open dialog, the playlist,
7220
+ * another plugin), so the plugin does not intercept opens itself.
7221
+ *
7222
+ * @capability source.resolve (file resolvers also `producer.av`)
7223
+ * @see [Playback state, controls and sources](../../docs/concepts/playback.md)
7224
+ * @since 1.0.0
7225
+ * @category Sources
6244
7226
  */
6245
7227
  export interface Resolvers {
6246
7228
  /**
@@ -6249,6 +7231,29 @@ export interface Resolvers {
6249
7231
  * replaced. One scheme + host pair, and one extension, has one resolver
6250
7232
  * (`conflict` otherwise). A rejected `resolve` fails the open with the
6251
7233
  * error's message.
7234
+ *
7235
+ * @capability source.resolve
7236
+ *
7237
+ * @example Play a site's pages through the plugin
7238
+ * Opening a `https://video.example.com/…` address plays the page's video.
7239
+ * `add` returns at once and registers in the background (`ready` reports
7240
+ * the answer); from then on the player calls `resolve` for every matching
7241
+ * open, and the error a rejected `resolve` carries is what the user sees.
7242
+ * ```ts
7243
+ * plugin.resolvers.add({
7244
+ * id: "example",
7245
+ * schemes: ["https"],
7246
+ * hosts: ["video.example.com"], // exact host names
7247
+ * async resolve(url, signal) {
7248
+ * const response = await fetch(url, { signal });
7249
+ * if (!response.ok) throw new Error(`the page answered ${response.status}`);
7250
+ * const page = (await response.json()) as { title: string; stream: string };
7251
+ * return { kind: "media", originalUrl: url, title: page.title, primary: { url: page.stream } };
7252
+ * },
7253
+ * });
7254
+ * ```
7255
+ *
7256
+ * @since 1.0.0
6252
7257
  */
6253
7258
  add(options: ResolverOptions): Resolver;
6254
7259
  /**
@@ -6304,6 +7309,9 @@ export interface Resolvers {
6304
7309
  * The manifest of a package, as validated. The display fields and
6305
7310
  * `minPlayerVersion` are optional (player 0.2.0); see the API reference for
6306
7311
  * their rules.
7312
+ *
7313
+ * @since 1.0.0
7314
+ * @category Packages
6307
7315
  */
6308
7316
  export interface PackageManifest {
6309
7317
  /** Plugin id. */
@@ -6344,6 +7352,9 @@ export interface PackageManifest {
6344
7352
  * upgrade the plugin and keep its data; links and local files count as one
6345
7353
  * source. `bundled` marks the player's built-in plugins (set by the player
6346
7354
  * only).
7355
+ *
7356
+ * @since 1.1.0
7357
+ * @category Packages
6347
7358
  */
6348
7359
  export type PackageSource = {
6349
7360
  /** A repository. */
@@ -6365,7 +7376,12 @@ export type PackageSource = {
6365
7376
  /** Shipped with the player (built-in). */
6366
7377
  kind: "bundled";
6367
7378
  };
6368
- /** Options of {@link PackageCandidate.install}. */
7379
+ /**
7380
+ * Options of {@link PackageCandidate.install}.
7381
+ *
7382
+ * @since 1.1.0
7383
+ * @category Packages
7384
+ */
6369
7385
  export interface InstallOptions {
6370
7386
  /**
6371
7387
  * Where the package comes from (default `{ kind: "file" }`). Repository
@@ -6380,7 +7396,34 @@ export interface InstallOptions {
6380
7396
  */
6381
7397
  replace?: boolean;
6382
7398
  }
6383
- /** A validated package directory, from {@link Packages.validate}. */
7399
+ /**
7400
+ * A validated package directory, from {@link Packages.validate}.
7401
+ *
7402
+ * @remarks
7403
+ * A package the player has checked, from {@link Packages.validate} or
7404
+ * {@link Packages.validateArchive}: its manifest, digest and the checks
7405
+ * it passed. The player copies the whole package when it checks it and
7406
+ * installs that copy, so later changes to the directory do not matter.
7407
+ * Installing starts a {@link PackageChange}; the player keeps only the 32
7408
+ * most recent candidates.
7409
+ *
7410
+ * @capability plugins.manage
7411
+ *
7412
+ * @example Show what a package asks for before installing it
7413
+ * Before installing, the plugin shows the package's name and the
7414
+ * capabilities it declares. The candidate's `manifest` is the manifest
7415
+ * as the player read it.
7416
+ * ```ts
7417
+ * declare const directory: string;
7418
+ * // ---cut---
7419
+ * const candidate = await plugin.packages.validate(directory);
7420
+ * const { id, version, capabilities } = candidate.manifest;
7421
+ * await plugin.osd.show("{} {}: {}", [id, version, capabilities.join(", ")]);
7422
+ * ```
7423
+ *
7424
+ * @since 1.0.0
7425
+ * @category Packages
7426
+ */
6384
7427
  export interface PackageCandidate {
6385
7428
  /** The package directory. */
6386
7429
  readonly directory: string;
@@ -6397,28 +7440,138 @@ export interface PackageCandidate {
6397
7440
  * player). A reserved id (`vestra` or `vestra.*`, which every built-in
6398
7441
  * uses) fails with `reserved` (player 0.4.1; already when validated), as
6399
7442
  * only the player installs built-in plugins.
7443
+ *
7444
+ * @capability plugins.manage
7445
+ * @throws `conflict` (rejects) The plugin is installed from another source
7446
+ * and `replace` is not set.
7447
+ * @throws `unsupported` (rejects) A built-in plugin's id.
7448
+ * @throws `reserved` (rejects) A reserved id (`vestra`, `vestra.*`).
7449
+ * @throws `busy` (rejects) The plugin library is being cleaned up, or the
7450
+ * player already keeps 64 change records.
7451
+ * @throws `revisionConflict` (rejects) The installed revision of this
7452
+ * plugin changed meanwhile; call `install` again (no new validation
7453
+ * needed).
7454
+ * @throws `stale` (rejects) The candidate is older than the 32 the player
7455
+ * keeps.
7456
+ * @throws `capability` (rejects) The manifest does not declare
7457
+ * `plugins.manage`.
7458
+ *
7459
+ * @example Replace a plugin installed from another source
7460
+ * The new package replaces the installed plugin even though it came from
7461
+ * another source. Without `replace` the player refuses with `conflict`;
7462
+ * the same source simply upgrades and keeps the plugin's data.
7463
+ * ```ts
7464
+ * declare const candidate: PackageCandidate;
7465
+ * // ---cut---
7466
+ * const change = await candidate.install({ replace: true });
7467
+ * await change.done; // "committed" once the new plugin runs
7468
+ * ```
7469
+ *
7470
+ * @since 1.0.0
6400
7471
  */
6401
7472
  install(options?: InstallOptions): Promise<PackageChange>;
6402
7473
  }
6403
- /** The progress of an install or uninstall. */
7474
+ /**
7475
+ * The progress of an install or uninstall.
7476
+ *
7477
+ * @since 1.0.0
7478
+ * @category Packages
7479
+ */
6404
7480
  export type PackageChangeStatus = "preparing" | "activating" | "restoring" | "committed" | "cancelled" | "failed" | "failedDegraded" | "tooLate";
6405
7481
  /**
6406
7482
  * An install or uninstall in progress. The SDK never submits a
6407
7483
  * change twice: after a lost reply it only queries the same request.
7484
+ *
7485
+ * @remarks
7486
+ * An install or uninstall the player carries out in steps (`preparing`,
7487
+ * `activating`, possibly `restoring`) until a terminal status. The SDK
7488
+ * asks the player for the status about every 400 ms until then; `done`
7489
+ * resolves with the terminal status and does not reject for a failed
7490
+ * change: `message` says why.
7491
+ *
7492
+ * @capability plugins.manage
7493
+ *
7494
+ * @example Report each step of an install
7495
+ * The OSD shows each step of the install and the end result. `onChange`
7496
+ * runs for every new status; `done` resolves when the change is over.
7497
+ * ```ts
7498
+ * declare const change: PackageChange;
7499
+ * // ---cut---
7500
+ * change.onChange((status) => void plugin.osd.show("Install: {}", [status]));
7501
+ * const end = await change.done;
7502
+ * if (end !== "committed") await plugin.osd.show("Not installed: {}", [change.message ?? end]);
7503
+ * ```
7504
+ *
7505
+ * @since 1.0.0
7506
+ * @category Packages
6408
7507
  */
6409
7508
  export interface PackageChange {
6410
7509
  /** Current status. */
6411
7510
  readonly status: PackageChangeStatus;
6412
7511
  /** Details for failures, `null` otherwise. */
6413
7512
  readonly message: string | null;
6414
- /** The status changed. */
7513
+ /**
7514
+ * The status changed.
7515
+ *
7516
+ * @capability plugins.manage
7517
+ *
7518
+ * @example Close a progress window when the change ends
7519
+ * The plugin's progress window closes once the install is over, whatever
7520
+ * the outcome. The listener receives each new status; the terminal ones
7521
+ * are `committed`, `cancelled`, `failed`, `failedDegraded` and `tooLate`
7522
+ * (`tooLate` only answers a `cancel` of a change that had committed, and
7523
+ * `committed` may follow it).
7524
+ * ```ts
7525
+ * declare const change: PackageChange;
7526
+ * declare const progress: Window;
7527
+ * // ---cut---
7528
+ * change.onChange((status) => {
7529
+ * if (["committed", "cancelled", "failed", "failedDegraded", "tooLate"].includes(status))
7530
+ * void progress.close();
7531
+ * });
7532
+ * ```
7533
+ *
7534
+ * @since 1.0.0
7535
+ */
6415
7536
  onChange(listener: (status: PackageChangeStatus) => void): Disposable;
6416
- /** Resolves with the final status. */
7537
+ /**
7538
+ * Resolves with the final status.
7539
+ *
7540
+ * @throws `missing` (rejects) The player no longer knows the change (a
7541
+ * `HostError` with that code); a failed change is a terminal status,
7542
+ * not a rejection.
7543
+ */
6417
7544
  readonly done: Promise<PackageChangeStatus>;
6418
- /** Ask to cancel (it may be `tooLate`). */
7545
+ /**
7546
+ * Ask to cancel (it may be `tooLate`).
7547
+ *
7548
+ * @capability plugins.manage
7549
+ * @throws `capability` (rejects) The manifest does not declare
7550
+ * `plugins.manage`.
7551
+ *
7552
+ * @example Cancel an install the user no longer wants
7553
+ * The install stops if it has not gone too far. `cancel` asks the player;
7554
+ * the change then ends `cancelled`. When the change had already committed
7555
+ * the player answers `tooLate` and `done` still resolves `committed`; a
7556
+ * change that is restoring cannot be cancelled and ends `failed` or
7557
+ * `failedDegraded`.
7558
+ * ```ts
7559
+ * declare const change: PackageChange;
7560
+ * // ---cut---
7561
+ * await change.cancel();
7562
+ * console.log(await change.done); // "cancelled"; "committed" when it was too late
7563
+ * ```
7564
+ *
7565
+ * @since 1.0.0
7566
+ */
6419
7567
  cancel(): Promise<void>;
6420
7568
  }
6421
- /** One plugin as the player knows it (`packages.list`). */
7569
+ /**
7570
+ * One plugin as the player knows it (`packages.list`).
7571
+ *
7572
+ * @since 1.0.0
7573
+ * @category Packages
7574
+ */
6422
7575
  export interface PluginInfo {
6423
7576
  /** Manifest id. */
6424
7577
  id: string;
@@ -6477,6 +7630,8 @@ export interface PluginInfo {
6477
7630
  * `description` and `tags` as the player shows them.
6478
7631
  *
6479
7632
  * @since 1.5.9
7633
+ *
7634
+ * @category Packages
6480
7635
  */
6481
7636
  export interface PluginDisplay {
6482
7637
  /** `name`, translated; `null` when the manifest has none. */
@@ -6492,6 +7647,8 @@ export interface PluginDisplay {
6492
7647
  * One refusal in {@link PluginInfo.warnings}.
6493
7648
  *
6494
7649
  * @since 1.5.2
7650
+ *
7651
+ * @category Packages
6495
7652
  */
6496
7653
  export interface PluginWarning {
6497
7654
  /**
@@ -6528,6 +7685,17 @@ export interface PluginWarning {
6528
7685
  * // `message` says why a change did not commit.
6529
7686
  * if ((await change.done) !== "committed") throw new Error(change.message ?? "install failed");
6530
7687
  * ```
7688
+ *
7689
+ * @remarks
7690
+ * The plugins the player knows: installed, built-in and this run's
7691
+ * development loads. Reading the list needs no capability; installing,
7692
+ * enabling, reloading and uninstalling need `plugins.manage` and act on
7693
+ * other plugins, never on the calling one. Built-in plugins update with
7694
+ * the player and cannot be installed, disabled or uninstalled.
7695
+ *
7696
+ * @capability None for `list` and `onChange`; plugins.manage for the rest.
7697
+ * @since 1.0.0
7698
+ * @category Packages
6531
7699
  */
6532
7700
  export interface Packages {
6533
7701
  /**
@@ -6538,6 +7706,37 @@ export interface Packages {
6538
7706
  * The player keeps the 32 most recent candidates of all plugins; an
6539
7707
  * older one's `install` fails with `stale` (validate again). Before
6540
7708
  * player 0.4.2 the 33rd validation of a run failed with `busy` instead.
7709
+ *
7710
+ * @capability plugins.manage
7711
+ * @throws `host` (rejects) Not a package directory, or a manifest that
7712
+ * breaks the rules (another `apiVersion` included); `message` says why.
7713
+ * @throws `invalid` (rejects) A bad request (a path that is not a string).
7714
+ * @throws `javascript` (rejects) The entry module does not parse.
7715
+ * @throws `slint` (rejects) A Slint file does not compile.
7716
+ * @throws `reserved` (rejects) A reserved id (`vestra`, `vestra.*`).
7717
+ * @throws `versionMismatch` (rejects) The package's `minPlayerVersion` is
7718
+ * newer than the player.
7719
+ * @throws `disabled` (rejects) The calling plugin is stopping.
7720
+ * @throws `capability` (rejects) The manifest does not declare
7721
+ * `plugins.manage`.
7722
+ *
7723
+ * @example Check a package directory before installing it
7724
+ * The plugin checks a folder it unpacked and installs it if the player
7725
+ * accepts it. `validate` runs the player's checks and resolves with a
7726
+ * candidate; a package that fails them rejects with the reason.
7727
+ * ```ts
7728
+ * declare const directory: string;
7729
+ * // ---cut---
7730
+ * try {
7731
+ * // Rejects with the reason when the package fails the checks.
7732
+ * const candidate = await plugin.packages.validate(directory);
7733
+ * await (await candidate.install()).done;
7734
+ * } catch (error) {
7735
+ * await plugin.osd.show("Not a valid plugin: {}", [(error as Error).message]);
7736
+ * }
7737
+ * ```
7738
+ *
7739
+ * @since 1.0.0
6541
7740
  */
6542
7741
  validate(directory: string): Promise<PackageCandidate>;
6543
7742
  /**
@@ -6545,19 +7744,97 @@ export interface Packages {
6545
7744
  * same checks as {@link Packages.validate} after unpacking. Check the
6546
7745
  * file's sha256 yourself before calling when you know it. A reserved id
6547
7746
  * fails with `reserved`, as in {@link Packages.validate}.
7747
+ *
7748
+ * @capability plugins.manage
7749
+ * @throws `invalid` (rejects) The path does not end in `.vxp`, or the
7750
+ * archive cannot be unpacked (over 64 MB, too many files, a damaged zip,
7751
+ * an unsafe path).
7752
+ * @throws `host` (rejects) The unpacked package or its manifest breaks the
7753
+ * rules; `message` says why.
7754
+ * @throws `javascript` (rejects) The entry module does not parse.
7755
+ * @throws `slint` (rejects) A Slint file does not compile.
7756
+ * @throws `reserved` (rejects) A reserved id.
7757
+ * @throws `versionMismatch` (rejects) The package's `minPlayerVersion` is
7758
+ * newer than the player.
7759
+ * @throws `disabled` (rejects) The calling plugin is stopping.
7760
+ * @throws `capability` (rejects) The manifest does not declare
7761
+ * `plugins.manage`.
7762
+ *
7763
+ * @example Install a downloaded package file
7764
+ * A `.vxp` the plugin downloaded is checked and installed. The player
7765
+ * unpacks and checks it like a directory; checking the file's sha256
7766
+ * first, when the store publishes one, is up to the plugin.
7767
+ * ```ts
7768
+ * declare const file: string;
7769
+ * // ---cut---
7770
+ * const candidate = await plugin.packages.validateArchive(file); // a .vxp path
7771
+ * const change = await candidate.install();
7772
+ * await plugin.osd.show("Install: {}", [await change.done]);
7773
+ * ```
7774
+ *
7775
+ * @since 1.1.0
6548
7776
  */
6549
7777
  validateArchive(file: string): Promise<PackageCandidate>;
6550
- /** Every plugin: running, installed, and this run's development loads. No capability. */
7778
+ /**
7779
+ * Every plugin: running, installed, and this run's development loads. No capability.
7780
+ *
7781
+ * @capability None.
7782
+ * @throws `Error` (rejects) Only when the player cannot be asked; no
7783
+ * capability is needed.
7784
+ *
7785
+ * @example Show which plugins are running
7786
+ * The plugin lists the running plugins by name. `list` asks the player
7787
+ * and resolves with every plugin it knows, with its state, origin and
7788
+ * display texts in the interface language.
7789
+ * ```ts
7790
+ * const plugins = await plugin.packages.list();
7791
+ * const running = plugins.filter((info) => info.state === "running");
7792
+ * console.log(running.map((info) => info.display?.name ?? info.id));
7793
+ * ```
7794
+ *
7795
+ * @since 1.0.0
7796
+ */
6551
7797
  list(): Promise<PluginInfo[]>;
6552
- /** The new list after any start, stop, failure, install or uninstall. No capability. */
7798
+ /**
7799
+ * The new list after any start, stop, failure, install or uninstall. No capability.
7800
+ *
7801
+ * @capability None.
7802
+ *
7803
+ * @example Refresh a plugin list after any change
7804
+ * A store window's list updates when a plugin starts, stops, fails, is
7805
+ * installed or uninstalled. The listener receives the new list each time.
7806
+ * ```ts
7807
+ * plugin.packages.onChange((plugins) => console.log(plugins.length, "plugins"));
7808
+ * ```
7809
+ *
7810
+ * @since 1.0.0
7811
+ */
6553
7812
  onChange(listener: (plugins: PluginInfo[]) => void): Disposable;
6554
7813
  /**
6555
7814
  * Stop or start a plugin (`plugins.manage`). An installed plugin stays
6556
7815
  * disabled across launches; a development load only for this run.
6557
7816
  * `invalid` to disable oneself, `missing` unknown.
6558
- * `busy` while it is being installed. `unsupported` to disable a built-in
7817
+ * `busy` while it is being installed or reloaded. `unsupported` to disable a built-in
6559
7818
  * ({@link PluginInfo.builtin}): users manage third-party plugins only
6560
7819
  * (players released with SDK 1.5.18 on; older ones disabled built-ins too).
7820
+ *
7821
+ * @capability plugins.manage
7822
+ * @throws `invalid` (rejects) Disabling the calling plugin itself.
7823
+ * @throws `missing` (rejects) An unknown id.
7824
+ * @throws `busy` (rejects) The plugin is being installed or reloaded.
7825
+ * @throws `unsupported` (rejects) A built-in plugin.
7826
+ * @throws `capability` (rejects) The manifest does not declare
7827
+ * `plugins.manage`.
7828
+ *
7829
+ * @example Turn a third-party plugin off
7830
+ * The plugin stops another plugin and keeps it disabled across launches
7831
+ * until it is enabled again. Built-in plugins cannot be disabled
7832
+ * (`unsupported`).
7833
+ * ```ts
7834
+ * await plugin.packages.setEnabled("someone.demo", false);
7835
+ * ```
7836
+ *
7837
+ * @since 1.0.0
6561
7838
  */
6562
7839
  setEnabled(id: string, enabled: boolean): Promise<void>;
6563
7840
  /**
@@ -6578,6 +7855,23 @@ export interface Packages {
6578
7855
  * @player 0.5.1; older players reject it as an unknown request
6579
7856
  * (`unsupported`).
6580
7857
  * @since 1.5.19
7858
+ *
7859
+ * @capability plugins.manage
7860
+ * @throws `invalid` (rejects) The calling plugin itself, or a disabled
7861
+ * plugin.
7862
+ * @throws `missing` (rejects) An unknown id.
7863
+ * @throws `busy` (rejects) The plugin is being installed or reloaded.
7864
+ * @throws `unsupported` (rejects) A player before 0.5.1.
7865
+ * @throws `capability` (rejects) The manifest does not declare
7866
+ * `plugins.manage`.
7867
+ *
7868
+ * @example Restart a plugin that misbehaves
7869
+ * The other plugin is stopped and started again from the same package and
7870
+ * data. `reload` resolves once the old process is stopped and the start is
7871
+ * scheduled; `onChange` then shows `starting` and `running` (or `failed`).
7872
+ * ```ts
7873
+ * await plugin.packages.reload("someone.demo");
7874
+ * ```
6581
7875
  */
6582
7876
  reload(id: string): Promise<void>;
6583
7877
  /**
@@ -6586,19 +7880,51 @@ export interface Packages {
6586
7880
  * reconnects to its data. Its user bindings stay (as orphans).
6587
7881
  * `unsupported` for built-ins and development loads, `missing`
6588
7882
  * unknown, `invalid` for oneself.
7883
+ *
7884
+ * @capability plugins.manage
7885
+ * @throws `unsupported` (rejects) A built-in plugin or a development load.
7886
+ * @throws `missing` (rejects) An unknown id.
7887
+ * @throws `invalid` (rejects) The calling plugin itself.
7888
+ * @throws `busy` (rejects) The plugin library is being cleaned up, or that
7889
+ * plugin is being installed or reloaded.
7890
+ * @throws `revisionConflict` (rejects) The installed revision of that
7891
+ * plugin changed meanwhile; call `uninstall` again.
7892
+ * @throws `capability` (rejects) The manifest does not declare
7893
+ * `plugins.manage`.
7894
+ *
7895
+ * @example Uninstall a plugin together with its data
7896
+ * The other plugin is removed and its data directory deleted, so a later
7897
+ * reinstall starts fresh. Without `removeData` a reinstall from the same
7898
+ * source finds its data again.
7899
+ * ```ts
7900
+ * const change = await plugin.packages.uninstall("someone.demo", { removeData: true });
7901
+ * await change.done;
7902
+ * ```
7903
+ *
7904
+ * @since 1.0.0
6589
7905
  */
6590
7906
  uninstall(id: string, options?: {
6591
7907
  removeData?: boolean;
6592
7908
  }): Promise<PackageChange>;
6593
7909
  }
6594
- /** A command: manifest id (`"host"` for the player's own) and command id. */
7910
+ /**
7911
+ * A command: manifest id (`"host"` for the player's own) and command id.
7912
+ *
7913
+ * @since 1.0.0
7914
+ * @category Commands
7915
+ */
6595
7916
  export interface BindingRef {
6596
7917
  /** Manifest id, or `"host"`. */
6597
7918
  plugin: string;
6598
7919
  /** Command id within that plugin. */
6599
7920
  command: string;
6600
7921
  }
6601
- /** One command's bindings, from {@link Bindings.list}. */
7922
+ /**
7923
+ * One command's bindings, from {@link Bindings.list}.
7924
+ *
7925
+ * @since 1.0.0
7926
+ * @category Commands
7927
+ */
6602
7928
  export interface BindingInfo extends BindingRef {
6603
7929
  /** Menu title. */
6604
7930
  title: string;
@@ -6648,9 +7974,40 @@ export interface BindingInfo extends BindingRef {
6648
7974
  * // later
6649
7975
  * await plugin.bindings.reset({ plugin: "host", command: "mute" });
6650
7976
  * ```
7977
+ *
7978
+ * @remarks
7979
+ * The player's binding table: every command's default keys (from
7980
+ * {@link CommandOptions.bindings}, the player's own as `host/<action>`)
7981
+ * and the user's overrides on top, saved across launches. A key-settings
7982
+ * plugin lists the table, writes overrides and resets them; the player
7983
+ * applies the change at once to every running plugin.
7984
+ *
7985
+ * @capability bindings.manage
7986
+ * @see [Commands, menus and key bindings](../../docs/concepts/commands.md)
7987
+ * @since 1.0.0
7988
+ * @category Commands
6651
7989
  */
6652
7990
  export interface Bindings {
6653
- /** Every declared command, and overrides whose command is gone. */
7991
+ /**
7992
+ * Every declared command, and overrides whose command is gone.
7993
+ *
7994
+ * @capability bindings.manage
7995
+ * @throws `capability` (rejects) The manifest does not declare
7996
+ * `bindings.manage`.
7997
+ *
7998
+ * @example Find the commands that use a key
7999
+ * A key-settings window shows which commands Ctrl+K is bound to now. `list`
8000
+ * resolves with every declared command and its current bindings (the
8001
+ * defaults with the user's overrides applied) and with the overrides
8002
+ * whose command is not running (`orphans`).
8003
+ * ```ts
8004
+ * const { commands } = await plugin.bindings.list();
8005
+ * const users = commands.filter((command) => command.current.includes("Ctrl+K"));
8006
+ * console.log(users.map((command) => `${command.plugin}/${command.command}`)); // "host" for the player
8007
+ * ```
8008
+ *
8009
+ * @since 1.0.0
8010
+ */
6654
8011
  list(): Promise<{
6655
8012
  /** Every declared command (running plugins and the player). */
6656
8013
  commands: BindingInfo[];
@@ -6667,11 +8024,65 @@ export interface Bindings {
6667
8024
  * `invalid` for a bad binding, `missing` for an unknown command that has
6668
8025
  * no override either.
6669
8026
  * At most 16 inputs, syntax as {@link CommandOptions.bindings}.
8027
+ *
8028
+ * @capability bindings.manage
8029
+ * @throws `invalid` (rejects) A malformed binding, or more than 16.
8030
+ * @throws `missing` (rejects) An unknown command that has no override
8031
+ * either.
8032
+ * @throws `capability` (rejects) The manifest does not declare
8033
+ * `bindings.manage`.
8034
+ *
8035
+ * @example Give the player's Mute command a second key
8036
+ * Mute now answers to M and to Ctrl+M. `set` replaces the command's
8037
+ * bindings with the list given (an override saved across launches), so it
8038
+ * repeats the default it keeps; `[]` would unbind it.
8039
+ * ```ts
8040
+ * await plugin.bindings.set({ plugin: "host", command: "mute" }, ["M", "Ctrl+M"]);
8041
+ * ```
8042
+ *
8043
+ * @since 1.0.0
6670
8044
  */
6671
8045
  set(ref: BindingRef, inputs: string[]): Promise<void>;
6672
- /** Back to the defaults: one command, or every override when omitted. */
8046
+ /**
8047
+ * Back to the defaults: one command, or every override when omitted.
8048
+ *
8049
+ * @capability bindings.manage
8050
+ * @throws `missing` (rejects) A command that has no override and is not
8051
+ * declared.
8052
+ * @throws `capability` (rejects) The manifest does not declare
8053
+ * `bindings.manage`.
8054
+ *
8055
+ * @example Restore every default key
8056
+ * A "Restore defaults" button removes all of the user's overrides, so
8057
+ * every command has its declared keys again. Without an argument `reset`
8058
+ * clears every override; with a reference only that command's.
8059
+ * ```ts
8060
+ * await plugin.bindings.reset(); // every override
8061
+ * ```
8062
+ *
8063
+ * @since 1.0.0
8064
+ */
6673
8065
  reset(ref?: BindingRef): Promise<void>;
6674
- /** Declarations or overrides changed (read `list()` again). At most every 200 ms. */
8066
+ /**
8067
+ * Declarations or overrides changed (read `list()` again). At most every 200 ms.
8068
+ *
8069
+ * @capability bindings.manage
8070
+ *
8071
+ * @example Refresh a key list when bindings change
8072
+ * The plugin's key list stays current when the user changes a key in the
8073
+ * player or another plugin starts. The listener carries no data and runs
8074
+ * at most every 200 ms; the plugin reads the table again with `list`.
8075
+ * ```ts
8076
+ * declare function render(commands: BindingInfo[]): void;
8077
+ * // ---cut---
8078
+ * plugin.bindings.onChange(async () => {
8079
+ * const { commands } = await plugin.bindings.list(); // read the new table
8080
+ * render(commands);
8081
+ * });
8082
+ * ```
8083
+ *
8084
+ * @since 1.0.0
8085
+ */
6675
8086
  onChange(listener: () => void): Disposable;
6676
8087
  }
6677
8088
  /**