@vestra-player/sdk 1.5.38 → 1.5.40

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.
@@ -9,11 +9,11 @@
9
9
  * object, so a built plugin has no runtime dependency on this package.
10
10
  */
11
11
  import type { Json } from "../wire.ts";
12
- import type { AudioNode, VideoNode } from "../processing.ts";
12
+ import type { AudioNode, ConvolutionPlane, VideoNode } from "../processing.ts";
13
13
  import type { SubtitleStyle } from "../subtitles.ts";
14
14
  import type { ResolvedMedia } from "../source-resolvers.ts";
15
15
  import type { MediaJob } from "../media.ts";
16
- export type { Json, AudioNode, VideoNode, SubtitleStyle, ResolvedMedia };
16
+ export type { Json, AudioNode, ConvolutionPlane, VideoNode, SubtitleStyle, ResolvedMedia, };
17
17
  /**
18
18
  * Anything that owns player-side or local resources: every handle the API
19
19
  * returns (commands, windows, panels, filters, snapshots, listeners…).
@@ -204,6 +204,13 @@ export type PluginError = Error & {
204
204
  export interface Scope extends Disposable {
205
205
  /** Menu commands and their default bindings (`commands`). */
206
206
  readonly commands: Commands;
207
+ /**
208
+ * Conditions on the player's own state for a command's `enabled` and
209
+ * `checked` (SDK 1.5.40, player 0.5.21): the player evaluates them, so
210
+ * the menu follows playback even while the plugin is stopped. See
211
+ * {@link When}.
212
+ */
213
+ readonly when: When;
207
214
  /** Tool windows from `.slint` files (`ui.window`). */
208
215
  readonly windows: Windows;
209
216
  /** Docked panels: bottom bar, sidebar and backdrop (`ui.mount`). */
@@ -214,10 +221,11 @@ export interface Scope extends Disposable {
214
221
  readonly player: Player;
215
222
  /**
216
223
  * Video processing: this plugin's filter chain, the display layer, the
217
- * GPU render options and the GPU's video upscaler. The first three need
218
- * `processing.video` and `playback.read`; the filter chain also needs
219
- * `events` (without it the SDK never sees the media become ready and the
220
- * chain stays `waiting`). The upscaler needs `video.upscaling` only.
224
+ * GPU render options, this plugin's shaders and the GPU's video
225
+ * upscaler. The first four need `processing.video` and `playback.read`;
226
+ * the filter chain also needs `events` (without it the SDK never sees
227
+ * the media become ready and the chain stays `waiting`). The upscaler
228
+ * needs `video.upscaling` only.
221
229
  */
222
230
  readonly video: {
223
231
  /**
@@ -250,6 +258,24 @@ export interface Scope extends Disposable {
250
258
  * while the first handle is not disposed.
251
259
  */
252
260
  render(): RenderOptions;
261
+ /**
262
+ * This plugin's own GPU shaders named `id` (default `"shaders"`;
263
+ * needs `processing.video`; SDK 1.5.39, player 0.5.20). Unlike
264
+ * {@link Scope.video | render()}, every plugin may hold shader
265
+ * handles at the same time; the player joins their lists. Nothing is
266
+ * sent until the first `set()`.
267
+ *
268
+ * @param id - Local name of the handle, 1–128 bytes, unique within
269
+ * the plugin.
270
+ * @returns A {@link VideoShaders} owned by this scope.
271
+ * @throws `capability` (throws) The manifest does not declare
272
+ * `processing.video`.
273
+ * @throws `invalid` (throws) `id` is empty, longer than 128 bytes or
274
+ * has control characters.
275
+ * @throws `Error` without a code (throws) A second handle with the
276
+ * same `id` while the first is not disposed.
277
+ */
278
+ shaders(id?: string): VideoShaders;
253
279
  /**
254
280
  * The GPU's video upscaler (NVIDIA RTX Video Super Resolution on
255
281
  * Windows; needs `video.upscaling`, player 0.5.16): whether it works
@@ -561,6 +587,217 @@ export interface CommandState {
561
587
  * @category Commands
562
588
  */
563
589
  export type Bindable<T> = T | (() => T);
590
+ /**
591
+ * A condition on the player's own state, from {@link Scope.when}, for a
592
+ * command's `enabled` or `checked`. The player evaluates it, again on
593
+ * every change of that state, whether or not the plugin runs.
594
+ *
595
+ * @remarks
596
+ * Combine conditions with `and`, `or` and `not`; each returns a new
597
+ * condition and leaves the original as it was. A condition is data: it
598
+ * reads nothing in the plugin and keeps no listener, so a plugin whose
599
+ * menu state is made of conditions may be stopped while idle (lazy
600
+ * activation) and its menu stays current. A function (see {@link Bindable})
601
+ * keeps working; one that reads `plugin.player.state` or another service's
602
+ * state keeps the plugin running so the SDK can evaluate it.
603
+ *
604
+ * @capability commands
605
+ *
606
+ * @example Enable an item while a frame can be saved
607
+ * "Save Snapshot" is greyed out with its reason unless a media file is
608
+ * open at a frame the player can capture. The condition is sent with the
609
+ * command and evaluated by the player, so the item follows playback even
610
+ * while the plugin is stopped; with an `enabled` condition the player
611
+ * shows the `reason` only while the item is disabled.
612
+ * ```ts
613
+ * declare function saveSnapshot(): Promise<void>;
614
+ * // ---cut---
615
+ * const { when } = plugin;
616
+ * plugin.commands.add({
617
+ * id: "save",
618
+ * path: ["@video", "Save Snapshot"],
619
+ * enabled: when.can("snapshot").and(when.phase("ready", "ended"), when.stream.not()),
620
+ * reason: "No frame to capture", // shown only while disabled
621
+ * run: saveSnapshot,
622
+ * });
623
+ * ```
624
+ *
625
+ * @since 1.5.40
626
+ * @category Commands
627
+ */
628
+ export interface Condition {
629
+ /**
630
+ * True when this condition and every one of `others` hold.
631
+ *
632
+ * @example Combine two conditions with and
633
+ * The item is enabled only while a file plays in fullscreen. `and`
634
+ * returns a new condition; `when.playing` itself is unchanged.
635
+ * ```ts
636
+ * const { when } = plugin;
637
+ * const playingFullscreen = when.playing.and(when.fullscreen);
638
+ * ```
639
+ *
640
+ * @capability None (the command it is given to needs `commands`).
641
+ * @since 1.5.40
642
+ */
643
+ and(...others: Condition[]): Condition;
644
+ /**
645
+ * True when this condition or one of `others` holds.
646
+ *
647
+ * @example Combine two conditions with or
648
+ * The item is enabled while a file plays or is paused. `or` returns a
649
+ * new condition that holds when either of the two holds.
650
+ * ```ts
651
+ * const { when } = plugin;
652
+ * const active = when.playing.or(when.paused);
653
+ * ```
654
+ *
655
+ * @capability None (the command it is given to needs `commands`).
656
+ * @since 1.5.40
657
+ */
658
+ or(...others: Condition[]): Condition;
659
+ /**
660
+ * True when this condition does not hold.
661
+ *
662
+ * @example Negate a condition with not
663
+ * An "Enter Fullscreen" item is enabled only outside fullscreen. `not`
664
+ * returns the opposite condition; the player evaluates it on every
665
+ * window change.
666
+ * ```ts
667
+ * const { when } = plugin;
668
+ * const windowed = when.fullscreen.not();
669
+ * ```
670
+ *
671
+ * @capability None (the command it is given to needs `commands`).
672
+ * @since 1.5.40
673
+ */
674
+ not(): Condition;
675
+ }
676
+ /**
677
+ * `plugin.when`: the vocabulary of conditions (SDK 1.5.40, player 0.5.21).
678
+ * It names only state the player owns: the source's phase, kind, pause
679
+ * and controls (the same names as {@link PlayerState}), fullscreen, the
680
+ * palette and the saved interface language.
681
+ *
682
+ * @remarks
683
+ * Give a condition as a command's `enabled` or `checked`
684
+ * ({@link CommandOptions}, {@link Command.set}). The player evaluates it
685
+ * and shows the result in menus, on keys and on bar buttons, again
686
+ * whenever that state changes, also while the plugin is stopped because
687
+ * nothing used it. With an `enabled` condition the player shows the
688
+ * command's `reason` only while the condition is false (or the command
689
+ * is busy), so a fixed reason is enough. Players before 0.5.21 have no
690
+ * `plugin.when` (declare `"minPlayerVersion": "0.5.21"`).
691
+ *
692
+ * State the vocabulary does not name (a plugin's own settings, an open
693
+ * window, a track list) stays a function or a value set with
694
+ * {@link Command.set}.
695
+ *
696
+ * @capability commands
697
+ *
698
+ * @example Check a palette item and follow the palette
699
+ * The Appearance submenu has one radio item per palette; the player
700
+ * checks the one in use, also after another plugin or the user changed
701
+ * it, and while this plugin is stopped. Nothing in the plugin listens to
702
+ * the palette.
703
+ * ```ts
704
+ * const { when } = plugin;
705
+ * for (const [id, title] of [["vestra", "Dark"], ["vestra-light", "Light"]] as const)
706
+ * plugin.commands.add({
707
+ * id: `theme.${id}`,
708
+ * path: ["Settings", "Appearance", title],
709
+ * radio: true,
710
+ * checked: when.theme(id), // evaluated by the player, not the plugin
711
+ * run: () => plugin.appearance.set(id),
712
+ * });
713
+ * ```
714
+ *
715
+ * @since 1.5.40
716
+ * @category Commands
717
+ */
718
+ export interface When {
719
+ /** A source is open: any phase but `empty` (loading, ready, ended, error). */
720
+ readonly open: Condition;
721
+ /** The source is loaded and can play (`phase` is `ready`). */
722
+ readonly ready: Condition;
723
+ /** The source is ready and not paused. */
724
+ readonly playing: Condition;
725
+ /** The source is ready and paused. */
726
+ readonly paused: Condition;
727
+ /** The source is a generated stream (`PlayerState.kind` is `stream`). */
728
+ readonly stream: Condition;
729
+ /** The main window is fullscreen. */
730
+ readonly fullscreen: Condition;
731
+ /**
732
+ * The source's `phase` is one of `phases`.
733
+ *
734
+ * @throws `TypeError` (throws) No phase, or one that is not a phase.
735
+ *
736
+ * @example Enable an item while a file is loaded or finished
737
+ * The item is enabled while the source is ready or has ended, as a
738
+ * snapshot needs a frame. The phases are those of `PlayerState.phase`.
739
+ * ```ts
740
+ * const { when } = plugin;
741
+ * const hasFrame = when.phase("ready", "ended");
742
+ * ```
743
+ *
744
+ * @capability None (the command it is given to needs `commands`).
745
+ * @since 1.5.40
746
+ */
747
+ phase(...phases: PlayerState["phase"][]): Condition;
748
+ /**
749
+ * The source allows `control`: `PlayerState.can[control]` is true.
750
+ *
751
+ * @throws `TypeError` (throws) `control` is not one of `PlayerState.can`.
752
+ *
753
+ * @example Enable a seek item only where seeking works
754
+ * "Jump to Middle" is greyed out on live streams that cannot seek.
755
+ * The player reads its own `can.seek` for the current source.
756
+ * ```ts
757
+ * const { when } = plugin;
758
+ * const seekable = when.can("seek");
759
+ * ```
760
+ *
761
+ * @capability None (the command it is given to needs `commands`).
762
+ * @since 1.5.40
763
+ */
764
+ can(control: keyof PlayerState["can"]): Condition;
765
+ /**
766
+ * The palette in use is `theme` (`plugin.appearance.theme`).
767
+ *
768
+ * @throws `TypeError` (throws) `theme` is not a palette of the player.
769
+ *
770
+ * @example Check the light palette's item
771
+ * The item carries a check mark while the light palette is in use,
772
+ * whoever switched to it.
773
+ * ```ts
774
+ * const { when } = plugin;
775
+ * const light = when.theme("vestra-light");
776
+ * ```
777
+ *
778
+ * @capability None (the command it is given to needs `commands`).
779
+ * @since 1.5.40
780
+ */
781
+ theme(theme: ThemeId): Condition;
782
+ /**
783
+ * The saved interface language is `language`
784
+ * (`plugin.appearance.language`: `"system"` or a locale).
785
+ *
786
+ * @throws `TypeError` (throws) `language` is not `"system"` or a locale.
787
+ *
788
+ * @example Check the Follow System item
789
+ * The item carries a check mark while the player follows the system's
790
+ * language; the player reads its own saved preference.
791
+ * ```ts
792
+ * const { when } = plugin;
793
+ * const followSystem = when.language("system");
794
+ * ```
795
+ *
796
+ * @capability None (the command it is given to needs `commands`).
797
+ * @since 1.5.40
798
+ */
799
+ language(language: LanguagePreference): Condition;
800
+ }
564
801
  /**
565
802
  * A built-in top-level menu of the player, named in a command path as
566
803
  * `@<id>` or by its English key (see {@link CommandOptions.path}): Open,
@@ -788,15 +1025,24 @@ export interface CommandOptions {
788
1025
  * a button without a path.
789
1026
  */
790
1027
  button?: CommandButton;
791
- /** Whether the command can run (default true). */
792
- enabled?: Bindable<boolean>;
793
- /** Check mark (or radio dot with `radio`); `null` (default) draws none. */
794
- checked?: Bindable<boolean | null>;
1028
+ /**
1029
+ * Whether the command can run (default true): a value, a function, or a
1030
+ * {@link Condition} the player evaluates (`plugin.when`, SDK 1.5.40).
1031
+ */
1032
+ enabled?: Bindable<boolean> | Condition;
1033
+ /**
1034
+ * Check mark (or radio dot with `radio`); `null` (default) draws none.
1035
+ * A {@link Condition} (`plugin.when`, SDK 1.5.40) draws the mark while
1036
+ * it holds.
1037
+ */
1038
+ checked?: Bindable<boolean | null> | Condition;
795
1039
  /** Busy state (default false); the player does not call `run` while busy. */
796
1040
  busy?: Bindable<boolean>;
797
1041
  /**
798
1042
  * Why the command is disabled or busy (shown to the user): English
799
1043
  * source text, or a {@link Text} for one with data in it (SDK 1.5.11).
1044
+ * With an `enabled` {@link Condition} the player shows it only while the
1045
+ * condition is false or the command is busy, so a fixed text is enough.
800
1046
  */
801
1047
  reason?: Bindable<TextValue | null>;
802
1048
  /**
@@ -875,7 +1121,10 @@ export interface Command extends Disposable {
875
1121
  * @since 1.0.0
876
1122
  */
877
1123
  set(state: Partial<{
878
- [K in keyof CommandState]: Bindable<CommandState[K]>;
1124
+ [K in "busy" | "reason"]: Bindable<CommandState[K]>;
1125
+ } & {
1126
+ enabled: Bindable<boolean> | Condition;
1127
+ checked: Bindable<boolean | null> | Condition;
879
1128
  }>): void;
880
1129
  /**
881
1130
  * Resolves when the host has registered the command. Rejects with
@@ -4055,8 +4304,10 @@ export interface SystemInfo {
4055
4304
  * Absent (`undefined`) on players before 0.5.12, which do not report it:
4056
4305
  * the renderer is then unknown, not software.
4057
4306
  *
4058
- * Today the player renders on the GPU on macOS and in software on
4059
- * Windows, Linux and without a user interface (see
4307
+ * Today the player renders on the GPU on macOS, on Windows when the
4308
+ * user turned on Video ▸ GPU Rendering (Experimental) (since version 0.5.14;
4309
+ * off by default), and in software otherwise: on Windows by default, on
4310
+ * Linux and without a user interface (see
4060
4311
  * [Platforms](../../docs/reference/platforms.md)); test this field
4061
4312
  * rather than {@link SystemInfo.os}.
4062
4313
  *
@@ -4954,9 +5205,11 @@ export interface SequenceProvider {
4954
5205
  */
4955
5206
  export interface PlaybackStats {
4956
5207
  /**
4957
- * The rendering path: "libmpv · GPU 渲染(OpenGL)" (GPU, macOS default) or
4958
- * "libmpv · 软件渲染" (software: Windows, Linux, headless, fallback).
4959
- * The strings are the player's display text.
5208
+ * The rendering path: "libmpv · GPU 渲染(OpenGL)" (GPU on macOS, the
5209
+ * default there), "libmpv · GPU 渲染(Direct3D)" (GPU on Windows, when
5210
+ * turned on; version 0.5.14 and later) or "libmpv · 软件渲染" (software: Windows by
5211
+ * default, Linux, headless, fallback). The strings are the player's
5212
+ * display text in the interface language (shown here in Chinese).
4960
5213
  */
4961
5214
  backend: string;
4962
5215
  /** The playback session number. */
@@ -6357,7 +6610,11 @@ export interface RenderSettings {
6357
6610
  targetPeak: number;
6358
6611
  /** Per-scene peak estimation (where the GPU supports it). */
6359
6612
  peakDetect: boolean;
6360
- /** mpv user shaders (`.glsl`): absolute paths of existing files, at most 16, in order. */
6613
+ /**
6614
+ * mpv user shaders (`.glsl`): absolute paths of existing files, at most
6615
+ * 16, in order. They run before the shaders of every plugin's
6616
+ * {@link VideoShaders} handles.
6617
+ */
6361
6618
  shaders: string[];
6362
6619
  /** Motion interpolation, resampled to the display's refresh rate. */
6363
6620
  interpolation: boolean;
@@ -6367,10 +6624,12 @@ export interface RenderSettings {
6367
6624
  * (`processing.video`). Single owner, like {@link VideoView}; the two are
6368
6625
  * independent handles.
6369
6626
  *
6370
- * Platforms: only macOS renders on the GPU. Under software rendering
6371
- * (Windows, Linux, headless, fallback) every `set` fails with
6372
- * `unsupported`; {@link SystemInfo.rendering} tells which renderer draws
6373
- * without creating the handle. macOS OpenGL 4.1 has no
6627
+ * Platforms: macOS renders on the GPU by default; Windows does since
6628
+ * version 0.5.14 of the player when the user turns on Video ▸ GPU Rendering
6629
+ * (Experimental) (off by default, {@link Rendering}). Under software
6630
+ * rendering (Windows by default, Linux, headless, fallback) every `set`
6631
+ * fails with `unsupported`; {@link SystemInfo.rendering} tells which
6632
+ * renderer draws without creating the handle. macOS OpenGL 4.1 has no
6374
6633
  * compute shaders, so `peakDetect` is accepted but has no effect; output
6375
6634
  * is always SDR.
6376
6635
  *
@@ -6379,7 +6638,8 @@ export interface RenderSettings {
6379
6638
  * 250-nit display; under software rendering the plugin hides its HDR menu.
6380
6639
  * `set` merges the values into the plugin's render options and the
6381
6640
  * player reports the result through `onStatus`: `failed` with
6382
- * `unsupported` means the player renders in software (Windows, Linux).
6641
+ * `unsupported` means the player renders in software (Linux, or Windows
6642
+ * while GPU rendering is off).
6383
6643
  * ```ts
6384
6644
  * declare function hideHdrMenu(): void;
6385
6645
  * // ---cut---
@@ -6392,11 +6652,13 @@ export interface RenderSettings {
6392
6652
  *
6393
6653
  * @remarks
6394
6654
  * GPU renderer options (tone mapping, peak detection, shaders,
6395
- * interpolation). Only the GPU renderer has them, which today means macOS;
6655
+ * interpolation). Only the GPU renderer has them (macOS; Windows with GPU
6656
+ * rendering turned on, version 0.5.14 and later);
6396
6657
  * under software rendering the handle is `failed` with `unsupported` after
6397
6658
  * the player's first answer to `render()` (reported to `onError` too), and so is
6398
6659
  * every `set`. Single owner, like {@link VideoView}. `set` never throws; the
6399
- * result shows in `status`.
6660
+ * result shows in `status`. For shaders that several plugins add at the
6661
+ * same time, use {@link VideoShaders} (SDK 1.5.39).
6400
6662
  *
6401
6663
  * @capability processing.video, playback.read and events (like
6402
6664
  * {@link VideoFilter})
@@ -6423,7 +6685,9 @@ export interface RenderOptions extends Disposable {
6423
6685
  set(value: Partial<RenderSettings>): void;
6424
6686
  /**
6425
6687
  * `failed` with `unsupported` while the player renders in software;
6426
- * `failed` with `failed` when mpv cannot compile a shader (pushed later).
6688
+ * `failed` with `failed` when mpv cannot compile a shader (pushed later;
6689
+ * while other plugins' {@link VideoShaders} are on too, the player
6690
+ * cannot tell whose shader failed and says so in the message).
6427
6691
  */
6428
6692
  readonly status: FilterStatus;
6429
6693
  /**
@@ -6446,6 +6710,152 @@ export interface RenderOptions extends Disposable {
6446
6710
  */
6447
6711
  onStatus(listener: (status: FilterStatus) => void): Disposable;
6448
6712
  }
6713
+ /**
6714
+ * This plugin's own GPU shaders, from `plugin.video.shaders(id)`
6715
+ * (`processing.video`, SDK 1.5.39, player 0.5.20): mpv user shaders
6716
+ * (`.glsl`, `//!HOOK` …) that the GPU renderer runs on every frame.
6717
+ *
6718
+ * @remarks
6719
+ * Any number of plugins hold shader handles at the same time (unlike
6720
+ * {@link RenderOptions}, which has one owner). The player joins every
6721
+ * handle's list in a stable order, after the render options' own
6722
+ * `shaders`: built-in plugins first, then installed plugins in the order
6723
+ * they were first installed, then development loads (`--plugin`); ties by
6724
+ * plugin id, and a plugin's handles by `id`. At most 16 shaders across all
6725
+ * handles (`limit` beyond, nothing changes); the render options keep their
6726
+ * own 16.
6727
+ *
6728
+ * Files must be this plugin's own: absolute paths inside its package
6729
+ * directory or {@link Plugin.dataDirectory} after following links, each
6730
+ * at most 16 MiB. Elsewhere: `invalid`; a file that does not exist:
6731
+ * `missing`. The player reads the files when the list is set.
6732
+ *
6733
+ * GPU rendering only (macOS; Windows with Video ▸ GPU Rendering
6734
+ * (Experimental) turned on): under software rendering a non-empty list is
6735
+ * `failed` with `unsupported`, and a switch to software during the run
6736
+ * pushes `unsupported` to every handle with shaders and removes every
6737
+ * list (they no longer count against the 16); set them again once the
6738
+ * player draws on the GPU (after a restart). Shaders act when a
6739
+ * frame is drawn, so processed snapshots and exports never contain them
6740
+ * (use a {@link VideoNode} for that). When mpv cannot compile a shader the
6741
+ * handle turns `failed` with `failed` (pushed later); mpv does not say
6742
+ * which file failed, so while several plugins' shaders are on, each of
6743
+ * them hears it and the message says the player cannot tell whose.
6744
+ *
6745
+ * `set` sends the whole list (last write wins); `clear` and `dispose`
6746
+ * withdraw it; the player removes a stopped plugin's shaders itself.
6747
+ * Players before 0.5.20 refuse the request (the handle is `failed`): declare
6748
+ * `"minPlayerVersion": "0.5.20"`.
6749
+ *
6750
+ * @example Add a sharpening shader the plugin ships
6751
+ * The video looks sharper on the GPU renderer; under software rendering
6752
+ * the OSD says why it is not. The shader file lives in the plugin's
6753
+ * package, and the player puts it after other plugins' shaders that come
6754
+ * earlier in its order.
6755
+ * ```ts
6756
+ * import { join } from "node:path";
6757
+ * declare const packageDirectory: string;
6758
+ * // ---cut---
6759
+ * const shaders = plugin.video.shaders("sharpen");
6760
+ * shaders.onStatus((s) => {
6761
+ * // `unsupported`: software rendering; `failed`: mpv could not compile it.
6762
+ * if (s.state === "failed") void plugin.osd.show("Sharpen: {}", [s.error.message]);
6763
+ * });
6764
+ * shaders.set([join(packageDirectory, "shaders", "sharpen.glsl")]);
6765
+ * ```
6766
+ *
6767
+ * @capability processing.video
6768
+ * @player 0.5.20; older players refuse the request.
6769
+ * @see [Video and audio processing](../../docs/concepts/processing.md)
6770
+ * @since 1.5.39
6771
+ * @category Processing
6772
+ */
6773
+ export interface VideoShaders extends Disposable {
6774
+ /**
6775
+ * Replace this handle's shader list (last write wins); `[]` withdraws
6776
+ * it.
6777
+ *
6778
+ * @remarks
6779
+ * Never throws: a refused list shows in `status` (`invalid`, `missing`,
6780
+ * `limit`, `unsupported`) and goes to {@link Scope.onError}, and what the
6781
+ * player held stays.
6782
+ *
6783
+ * @capability processing.video
6784
+ * @param paths - Absolute paths of shader files in this plugin's package
6785
+ * or data directory, in the order they run.
6786
+ *
6787
+ * @example Switch between two shader presets
6788
+ * The plugin offers a light and a strong preset; picking one replaces
6789
+ * the handle's list in one request. The files were written to the
6790
+ * plugin's data directory when the user imported them.
6791
+ * ```ts
6792
+ * import { join } from "node:path";
6793
+ * // ---cut---
6794
+ * const presets: Record<string, string[]> = {
6795
+ * light: [join(plugin.dataDirectory, "denoise.glsl")],
6796
+ * strong: [join(plugin.dataDirectory, "denoise.glsl"), join(plugin.dataDirectory, "sharpen.glsl")],
6797
+ * };
6798
+ * plugin.video.shaders("presets").set(presets.strong!);
6799
+ * ```
6800
+ *
6801
+ * @since 1.5.39
6802
+ */
6803
+ set(paths: readonly string[]): void;
6804
+ /**
6805
+ * Withdraw this handle's shaders (the same as `set([])`).
6806
+ *
6807
+ * @capability processing.video
6808
+ *
6809
+ * @example Turn the plugin's shaders off from a menu item
6810
+ * A Video menu item turns the plugin's shader on and off. Clearing
6811
+ * keeps the handle, so the next pick sets the list again.
6812
+ * ```ts
6813
+ * declare const file: string;
6814
+ * // ---cut---
6815
+ * const shaders = plugin.video.shaders();
6816
+ * let on = false;
6817
+ * plugin.commands.add({
6818
+ * id: "toggle-shader",
6819
+ * path: ["@video", "Sharpen (Shader)"],
6820
+ * checked: () => on,
6821
+ * run: () => {
6822
+ * on = !on;
6823
+ * // Clearing keeps the handle; the next pick sets the list again.
6824
+ * if (on) shaders.set([file]);
6825
+ * else shaders.clear();
6826
+ * },
6827
+ * });
6828
+ * ```
6829
+ *
6830
+ * @since 1.5.39
6831
+ */
6832
+ clear(): void;
6833
+ /**
6834
+ * Current status: `waiting` before ready media, `applying`, `applied`,
6835
+ * or `failed` with the reason (see the remarks).
6836
+ */
6837
+ readonly status: FilterStatus;
6838
+ /**
6839
+ * The status changed, including later pushes (a compile failure, a
6840
+ * switch to software rendering).
6841
+ *
6842
+ * @capability processing.video
6843
+ *
6844
+ * @example Report a shader that does not compile
6845
+ * The OSD shows mpv's message when the plugin's shader fails to compile.
6846
+ * The player pushes that failure after the list was accepted, so it
6847
+ * arrives through `onStatus`, not as a refused `set`.
6848
+ * ```ts
6849
+ * const shaders = plugin.video.shaders();
6850
+ * shaders.onStatus((status) => {
6851
+ * if (status.state === "failed") void plugin.osd.show("Shader: {}", [status.error.message]);
6852
+ * });
6853
+ * ```
6854
+ *
6855
+ * @since 1.5.39
6856
+ */
6857
+ onStatus(listener: (status: FilterStatus) => void): Disposable;
6858
+ }
6449
6859
  /**
6450
6860
  * Why the GPU's video upscaler does not work, as a stable kebab-case code.
6451
6861
  * A newer player may add codes: compare with the ones you handle and show
@@ -8422,14 +8832,17 @@ export interface Bindings {
8422
8832
  */
8423
8833
  reset(ref?: BindingRef): Promise<void>;
8424
8834
  /**
8425
- * Declarations or overrides changed (read `list()` again). At most every 200 ms.
8835
+ * The binding list changed (read `list()` again): a declaration or an
8836
+ * override. At most every 200 ms; since player 0.5.21 only when the list
8837
+ * itself changed, not when a plugin starts again with the same commands.
8426
8838
  *
8427
8839
  * @capability bindings.manage
8428
8840
  *
8429
8841
  * @example Refresh a key list when bindings change
8430
8842
  * The plugin's key list stays current when the user changes a key in the
8431
- * player or another plugin starts. The listener carries no data and runs
8432
- * at most every 200 ms; the plugin reads the table again with `list`.
8843
+ * player or a plugin with other commands is installed. The listener
8844
+ * carries no data and runs at most every 200 ms; the plugin reads the
8845
+ * table again with `list`.
8433
8846
  * ```ts
8434
8847
  * declare function render(commands: BindingInfo[]): void;
8435
8848
  * // ---cut---
@@ -9295,10 +9708,10 @@ export type RenderingPreference = "auto" | "gpu" | "software";
9295
9708
  *
9296
9709
  * | Code | Meaning |
9297
9710
  * | --- | --- |
9298
- * | `default` | Nothing ruled the GPU out; the platform's default. |
9711
+ * | `default` | Nothing ruled the GPU out; the platform's default (macOS: GPU; Windows since version 0.5.14: software until the user turns on GPU rendering). |
9299
9712
  * | `pending` | GPU rendering is starting. |
9300
9713
  * | `headless` | The player runs without a user interface. |
9301
- * | `no-gpu-backend` | This player has no GPU renderer on this system (Windows today). |
9714
+ * | `no-gpu-backend` | This player has no GPU renderer on this system (Linux today; Windows has one since version 0.5.14, off by default, which reports `default`). |
9302
9715
  * | `env` | The environment chose it (`VESTRA_RENDER`, `VESTRA_SOFTWARE_RENDER`). |
9303
9716
  * | `preference` | The user chose software rendering ({@link Rendering.setPreference}); GPU chosen and available reports `default`. |
9304
9717
  * | `denylist` | The graphics driver is on the player's denylist. |
package/dist/plugin/ui.js CHANGED
@@ -92,7 +92,8 @@ export class ViewBase {
92
92
  scope;
93
93
  handlers = new Map();
94
94
  #values = new Map();
95
- #binders = new Set();
95
+ /** Each binding, with where the plugin added it (for the player's log). */
96
+ #binders = new Map();
96
97
  /** What the host currently shows, as far as this side knows. */
97
98
  #sent = new Map();
98
99
  /** Written by `set()`: sent even if equal to what was sent before. */
@@ -144,7 +145,7 @@ export class ViewBase {
144
145
  this.flush();
145
146
  }
146
147
  bind(props) {
147
- this.#binders.add(props);
148
+ this.#binders.set(props, this.scope.runtime.callSite());
148
149
  this.scope.update();
149
150
  return this.scope.onDispose(() => {
150
151
  this.#binders.delete(props);
@@ -152,9 +153,10 @@ export class ViewBase {
152
153
  }
153
154
  /** Current desired properties (bound values win over earlier `set`). */
154
155
  snapshot() {
155
- for (const binder of this.#binders) {
156
+ for (const [binder, site] of this.#binders) {
156
157
  try {
157
- for (const [key, value] of Object.entries(encode(binder())))
158
+ const props = this.scope.runtime.reader("a view's bind", site, binder);
159
+ for (const [key, value] of Object.entries(encode(props)))
158
160
  this.#values.set(key, value);
159
161
  }
160
162
  catch (error) {
@@ -0,0 +1,46 @@
1
+ /**
2
+ * `plugin.when`: conditions on the player's own state that the player
3
+ * evaluates for a command's `enabled` and `checked` (player 0.5.21). The
4
+ * wire form is `commands.register` / `commands.update` `when`.
5
+ */
6
+ import type { Condition, LanguagePreference, PlayerState, ThemeId, When } from "./types.ts";
7
+ /** A condition in the wire form: an object of exactly one key. */
8
+ export type ConditionJson = {
9
+ phase: PlayerState["phase"][];
10
+ } | {
11
+ paused: boolean;
12
+ } | {
13
+ kind: PlayerState["kind"];
14
+ } | {
15
+ can: keyof PlayerState["can"];
16
+ } | {
17
+ fullscreen: boolean;
18
+ } | {
19
+ theme: ThemeId;
20
+ } | {
21
+ language: LanguagePreference;
22
+ } | {
23
+ all: ConditionJson[];
24
+ } | {
25
+ any: ConditionJson[];
26
+ } | {
27
+ not: ConditionJson;
28
+ };
29
+ /** What conditions read, in the SDK's names. */
30
+ export interface ConditionFacts {
31
+ phase: PlayerState["phase"];
32
+ paused: boolean;
33
+ kind: PlayerState["kind"];
34
+ can: Readonly<Record<string, boolean>>;
35
+ fullscreen: boolean;
36
+ theme: string;
37
+ language: string;
38
+ }
39
+ /** Whether `value` is a condition made by `plugin.when`. */
40
+ export declare const isCondition: (value: unknown) => value is Condition;
41
+ /** A condition's wire form; throws for anything else. */
42
+ export declare function conditionJson(condition: Condition): ConditionJson;
43
+ /** The one `plugin.when` (conditions hold no plugin state). */
44
+ export declare const WHEN: When;
45
+ /** Evaluate a wire condition as the player does (for the test host). */
46
+ export declare function evaluate(json: ConditionJson, facts: ConditionFacts): boolean;