@vestra-player/sdk 1.5.34 → 1.5.36

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/llms-full.txt CHANGED
@@ -1,4 +1,4 @@
1
- # @vestra-player/sdk 1.5.34: full reference
1
+ # @vestra-player/sdk 1.5.36: full reference
2
2
 
3
3
  > Everything needed to write a Vestra Player plugin (plugin API `vestra/1`): the guides, every public declaration of `@vestra-player/sdk` and `@vestra-player/sdk/testing` with its documentation, and the complete source of the example plugins. Generated from the SDK's sources by scripts/llms.mjs; the short index is llms.txt.
4
4
 
@@ -1613,6 +1613,127 @@ export default async function activate(plugin: Plugin) {
1613
1613
  - **Preferences rather than state:** a choice such as "reopen my windows at startup" belongs on a settings page (plugin.settings.page) that reads and writes the same store (the `SettingsPage` reference has an example).
1614
1614
  - **Large or binary data:** write your own files in plugin.dataDirectory (to a temporary name, then rename) and keep only small settings in `storage.json`.
1615
1615
 
1616
+ ## How-to: Offer a GPU rendering switch
1617
+
1618
+ **Goal:** a settings menu item "GPU Rendering (Experimental)" that shows whether the player will draw on the GPU, is greyed out with the player's reason where the GPU cannot be chosen, saves the user's choice, says "(Restart to Apply)" while the choice waits for a restart, and offers that restart.
1619
+
1620
+ The player keeps the choice itself, like a browser's hardware acceleration switch: setPreference saves it in the player's settings at once, the running player keeps its renderer, and the next start uses the choice **whether or not the user restarts now**. Your plugin stores nothing. Reading the state needs no capability; saving a choice and restarting need `rendering.manage`, which needs player 0.5.13.
1621
+
1622
+ #### The complete example
1623
+
1624
+ `manifest.json`:
1625
+
1626
+ ```json
1627
+ {
1628
+ "id": "examples.gpu-switch",
1629
+ "version": "1.0.0",
1630
+ "apiVersion": "vestra/1",
1631
+ "entry": "main.ts",
1632
+ "minPlayerVersion": "0.5.13",
1633
+ "capabilities": ["commands", "rendering.manage"]
1634
+ }
1635
+ ```
1636
+
1637
+ `main.ts`:
1638
+
1639
+ ```ts
1640
+ import type { Plugin, PluginError, RenderingState, Scope } from "@vestra-player/sdk";
1641
+
1642
+ const TITLE = "GPU Rendering (Experimental)";
1643
+
1644
+ export default async function activate(plugin: Plugin) {
1645
+ let state: RenderingState = await plugin.rendering.state();
1646
+ let menu: Scope | null = null;
1647
+ let shownPending: boolean | null = null;
1648
+
1649
+ // A command's title cannot change in place, so the items live in a child
1650
+ // scope that is rebuilt when `restartPending` flips.
1651
+ async function build() {
1652
+ if (state.restartPending === shownPending) return;
1653
+ shownPending = state.restartPending;
1654
+ await menu?.dispose();
1655
+ menu = plugin.scope();
1656
+ menu.commands.add({
1657
+ id: "gpu-rendering",
1658
+ path: ["@settings", state.restartPending ? `${TITLE} (Restart to Apply)` : TITLE],
1659
+ // Checked when the next start draws on the GPU (also for "auto").
1660
+ checked: () => state.nextMode === "gpu",
1661
+ // Greyed out with the player's reason where "gpu" cannot be chosen.
1662
+ enabled: () => state.gpu.available,
1663
+ reason: () => state.gpu.reason?.message ?? null,
1664
+ run: async () => {
1665
+ const choice = state.nextMode === "gpu" ? "software" : "gpu";
1666
+ try {
1667
+ state = await plugin.rendering.setPreference(choice);
1668
+ } catch (error) {
1669
+ // `unavailable`: the GPU was ruled out after the state was read.
1670
+ state = await plugin.rendering.state(); // the item now says why
1671
+ if ((error as PluginError).code !== "unavailable") throw error; // `failed`: not saved
1672
+ } finally {
1673
+ await build();
1674
+ }
1675
+ },
1676
+ });
1677
+ if (state.restartPending)
1678
+ menu.commands.add({
1679
+ id: "restart",
1680
+ path: ["@settings", "Restart Player to Apply"],
1681
+ run: async () => {
1682
+ // Resolves once the player accepted; it then quits and starts again.
1683
+ await plugin.rendering.restartPlayer();
1684
+ },
1685
+ });
1686
+ }
1687
+
1688
+ await build();
1689
+ }
1690
+ ```
1691
+
1692
+ #### How it works
1693
+
1694
+ - **`state()` describes now and the next start.** `mode` and `reason` are the renderer drawing now and why; `preference` is the saved choice (`"auto"`, `"gpu"` or `"software"`); `nextMode` and `nextReason` are what the next start will use as far as the player knows, after the environment, the driver denylist and an earlier GPU crash. The item is checked by `nextMode`, so with `"auto"` on a system where the GPU is the default it shows checked.
1695
+ - **`gpu.available` decides whether the item can be used.** Where this player has no GPU renderer (`no-gpu-backend`, Windows today), without a user interface (`headless`), for a graphics driver on the player's denylist (`denylist`) or after a GPU crash at an earlier start (`previous-crash`), it is `false` and `gpu.reason.message` explains why; the item shows that as its disabled reason. `"software"` and `"auto"` are always accepted, so a user can always leave the GPU.
1696
+ - **`restartPending`** is true while the next start's mode differs from the mode this run started with. When the user changes the choice back, it turns false again and the "(Restart to Apply)" title and the restart item go away.
1697
+ - **`restartPlayer()`** resolves once the player has accepted. The player then quits normally, which stops every plugin, yours included, and starts again with the same plugins; a local file continues at its position, paused or playing as it was (a stream is not reopened). It rejects with `busy` while a restart is under way and with `failed` when the new process could not start (the player keeps running).
1698
+
1699
+ The restart is only a shortcut. A user who ignores it gets the new renderer the next time they start the player.
1700
+
1701
+ #### Older players
1702
+
1703
+ A player before 0.5.13 does not load a plugin that declares `rendering.manage`, so the manifest above sets `minPlayerVersion`. A plugin that only reads the state, without the capability, gets `unsupported` from `state()` on an older player:
1704
+
1705
+ ```ts
1706
+ const state = await plugin.rendering.state().catch((error: PluginError) => {
1707
+ if (error.code === "unsupported") return null; // a player before 0.5.13
1708
+ throw error;
1709
+ });
1710
+ ```
1711
+
1712
+ To know only whether render options work now, read rendering from `plugin.app.system()` instead; `renderingReason` there is the same reason as `state().reason`.
1713
+
1714
+ #### Test it
1715
+
1716
+ TestHost.rendering models the player: preset `gpu`, `reason` and `startedWith` before the plugin starts (`videoRender.gpu` is the mode drawing now), then drive the item and check the saved choice.
1717
+
1718
+ ```ts
1719
+ import assert from "node:assert/strict";
1720
+ import { startPlugin, TestHost } from "@vestra-player/sdk/testing";
1721
+ import activate from "../main.ts";
1722
+
1723
+ const host = new TestHost(); // a GPU player, "auto", started on the GPU
1724
+ const { stop } = await startPlugin(activate, {
1725
+ directory: new URL("..", import.meta.url),
1726
+ host,
1727
+ });
1728
+ await host.command("gpu-rendering").run(); // unchecks: saves "software"
1729
+ assert.equal(host.rendering.preference, "software");
1730
+ await host.command("restart").run(); // offered while the restart is pending
1731
+ assert.equal(host.rendering.restarts, 1);
1732
+ await stop();
1733
+ ```
1734
+
1735
+ Set `host.rendering.gpu = { available: false, reason: { code: "denylist", message: "…" } }` to see the item greyed out, and refuse a call with `host.handle("rendering.restart", () => { throw new HostError("failed", "…"); })`.
1736
+
1616
1737
  ## How-to: Translate a plugin
1617
1738
 
1618
1739
  Vestra Player (0.4.3 or later) shows its interface in one of seven languages. The user picks one in **Settings ▸ Language** (or follows the system); the choice takes effect at the next start, and the language stays the same for the whole run. A plugin joins in by shipping one gettext file per language. It writes **no translation code**: the player translates.
@@ -2044,7 +2165,7 @@ The test decides what the player is doing:
2044
2165
  - `host.openElsewhere(path, phase?)`: the user or another plugin opened a source (a new source id, then `phase`, `"ready"` by default). Calls the plugin made against the old state now fail with `stale`.
2045
2166
  - `host.setSource({ phase, position, duration, paused, audioTracks, … })`: the state changed (the user paused, playback reached the end). Before `startPlugin`, it presets the source the plugin finds at activation.
2046
2167
  - `host.setMainWindow({ fullscreen, videoArea })`, `host.setAppearance(theme)`, `host.setPackages(list)`: the main window, the colour scheme, the installed plugins changed.
2047
- - Preset data the player would return: `host.stats` (`player.stats()`), `host.chapters`, `host.clipboard.text`, `host.secrets`, `host.systemInfo`; `host.tools.handle("ffprobe", (args) => ({ stdout }))` makes a tool available and answers its runs.
2168
+ - Preset data the player would return: `host.stats` (`player.stats()`), `host.chapters`, `host.clipboard.text`, `host.secrets`, `host.systemInfo`, `host.rendering` (the GPU or software choice, see Offer a GPU rendering switch); `host.tools.handle("ffprobe", (args) => ({ stdout }))` makes a tool available and answers its runs.
2048
2169
 
2049
2170
  #### Inject failures
2050
2171
 
@@ -2568,6 +2689,7 @@ The table lists the API whose documentation names a player version, newest last:
2568
2689
  | 0.5.5 | SessionStorage |
2569
2690
  | 0.5.10 | CommandButton, CommandOptions.button |
2570
2691
  | 0.5.12 | SystemInfo.rendering |
2692
+ | 0.5.13 | SystemInfo.renderingReason, RenderingMode, RenderingPreference, RenderingReasonCode, RenderingReason, RenderingState, Rendering |
2571
2693
 
2572
2694
  The Slint library carries no version tags: `Theme.danger` needs player 0.3.0; `@vestra/icons.slint` and the palette `vestra-light` need 0.4.0 (Theme).
2573
2695
 
@@ -2604,6 +2726,7 @@ A plugin lists what it may do in the manifest's `capabilities` (Manifest). A cal
2604
2726
  | `ui.settings` | Settings.page, Settings.show | — | Add pages to the Options window | |
2605
2727
  | `ui.theme` | Appearance.set, Appearance.setLanguage | — | Change the player's palette | |
2606
2728
  | `secrets` | Secrets.get, Secrets.set, Secrets.delete | — | Store sign-in details in the system keychain | yes |
2729
+ | `rendering.manage` | Rendering.setPreference, Rendering.restartPlayer | — | Choose GPU or software rendering and restart the player | yes |
2607
2730
 
2608
2731
  **Also needs** applies to every member of the row: declare those capabilities as well. A few members or options need one more, documented at the member:
2609
2732
 
@@ -2615,7 +2738,7 @@ A plugin lists what it may do in the manifest's `capabilities` (Manifest). A cal
2615
2738
 
2616
2739
  #### Without a capability
2617
2740
 
2618
- A member not in the table needs no capability: `plugin.tools`, `plugin.storage`, `plugin.system`, `plugin.app`, `plugin.ui` (images and Markdown), `plugin.i18n`, reading window.state, appearance.theme and packages.list. The table is `CAPABILITY_INFO`, which tests can import from `@vestra-player/sdk/testing` to check a manifest (CAPABILITY_INFO).
2741
+ A member not in the table needs no capability: `plugin.tools`, `plugin.storage`, `plugin.system`, `plugin.app`, `plugin.ui` (images and Markdown), `plugin.i18n`, reading window.state, appearance.theme, packages.list and rendering.state. The table is `CAPABILITY_INFO`, which tests can import from `@vestra-player/sdk/testing` to check a manifest (CAPABILITY_INFO).
2619
2742
 
2620
2743
  #### Catching a missing capability
2621
2744
 
@@ -3052,6 +3175,7 @@ Every command the player (`host`) and its built-in plugins register when the pla
3052
3175
  | Video ▸ Saturation +1% | `U` | `vestra.video/video.saturation.up` |
3053
3176
  | Video ▸ Hue -1% | `I` | `vestra.video/video.hue.down` |
3054
3177
  | Video ▸ Hue +1% | `O` | `vestra.video/video.hue.up` |
3178
+ | Video ▸ GPU Rendering (Experimental) | | `vestra.video/video.gpu-rendering` |
3055
3179
  | Audio ▸ Volume + | `ArrowUp`, `Mouse:WheelUp` | `host/volume-up` |
3056
3180
  | Audio ▸ Volume − | `ArrowDown`, `Mouse:WheelDown` | `host/volume-down` |
3057
3181
  | Audio ▸ Mute | `M` | `host/mute` |
@@ -3264,6 +3388,7 @@ A plugin uses the same API on macOS and Windows; the player does the system-spec
3264
3388
  | Snapshots and exports (MediaCapture) | Available. | Available; needs FFmpeg 6.1 or newer. |
3265
3389
  | Generated streams (Player.openStream) | A Unix socket. | Loopback TCP with a token. |
3266
3390
  | Rendering | On the GPU (OpenGL), with hardware decoding. | In software, with hardware decoding copied back (`d3d11va-copy` and similar). |
3391
+ | plugin.rendering (player 0.5.13) | `"gpu"` can be chosen unless the driver is ruled out (`denylist`, `previous-crash`). | `gpu.available` is `false` (`no-gpu-backend`); `setPreference("gpu")` fails with `unsupported`. |
3267
3392
  | Render options (RenderOptions) | Available; `peakDetect` has no effect. | `unsupported` (software rendering). |
3268
3393
  | Bindings | `Ctrl` also matches ⌘; `Cmd` is ⌘. | `Cmd` is the Windows logo key; `Cmd+Q` does not quit. |
3269
3394
  | Keys taken first, standard close keys | See Keys the system takes first. | See Keys the system takes first. |
@@ -3802,6 +3927,8 @@ interface Scope extends Disposable {
3802
3927
  readonly window: MainWindow;
3803
3928
  /** The player's version and components, and the system it runs on. */
3804
3929
  readonly app: App;
3930
+ /** GPU or software rendering: read the choice, change it (`rendering.manage`). */
3931
+ readonly rendering: Rendering;
3805
3932
  /** Every command's bindings and the user's overrides (`bindings.manage`). */
3806
3933
  readonly bindings: Bindings;
3807
3934
  /** Runtime images for Slint `image` properties. */
@@ -7328,6 +7455,27 @@ interface SystemInfo {
7328
7455
  * ```
7329
7456
  */
7330
7457
  readonly rendering?: "gpu" | "software";
7458
+ /**
7459
+ * Why SystemInfo.rendering is what it is now: the same reason as
7460
+ * RenderingState.reason (`default`, `no-gpu-backend`,
7461
+ * `preference`, `denylist`, `window-fallback`…, see
7462
+ * RenderingReasonCode). Read at each call, like `rendering`.
7463
+ *
7464
+ * Absent (`undefined`) on players before 0.5.13.
7465
+ * @player 0.5.13; older players leave it out (declare `"minPlayerVersion": "0.5.13"` when the plugin relies on it).
7466
+ * @example Say why the video is drawn in software
7467
+ * An About page explains a software renderer with the player's own words,
7468
+ * for example that the graphics driver is on the player's denylist.
7469
+ * `system()` reports the renderer and its reason together, with no
7470
+ * capability; the message is English text that may name the adapter and
7471
+ * the driver version.
7472
+ * ```ts
7473
+ * const { rendering, renderingReason } = await plugin.app.system();
7474
+ * if (rendering === "software" && renderingReason)
7475
+ * console.log(`Rendering: software (${renderingReason.message})`);
7476
+ * ```
7477
+ */
7478
+ readonly renderingReason?: RenderingReason;
7331
7479
  }
7332
7480
  ```
7333
7481
 
@@ -11368,6 +11516,298 @@ if (token === null) {
11368
11516
  }
11369
11517
  ```
11370
11518
 
11519
+ ### RenderingMode
11520
+
11521
+ Which renderer draws the video: `"gpu"` (the GPU renderer, where
11522
+ render options apply) or `"software"` (they fail
11523
+ with `unsupported`). The same values as SystemInfo.rendering.
11524
+
11525
+ - @player 0.5.13
11526
+
11527
+ ```ts
11528
+ type RenderingMode = "gpu" | "software";
11529
+ ```
11530
+
11531
+ Example: Tell the user which renderer draws the video
11532
+
11533
+ The plugin shows "Rendering: GPU" or "Rendering: software" on the
11534
+ video. `state()` asks the player; `mode` is the renderer drawing now,
11535
+ which can differ from the saved choice until the next start.
11536
+ ```ts
11537
+ const { mode } = await plugin.rendering.state();
11538
+ const label: Record<RenderingMode, string> = { gpu: "GPU", software: "software" };
11539
+ await plugin.osd.show("Rendering: {}", [label[mode]]);
11540
+ ```
11541
+
11542
+ ### RenderingPreference
11543
+
11544
+ The user's saved rendering choice: `"auto"` (the player decides: the GPU
11545
+ where this system has a GPU renderer that is not ruled out, software
11546
+ elsewhere), `"gpu"` or `"software"`. It applies from the next start.
11547
+
11548
+ - @player 0.5.13
11549
+
11550
+ ```ts
11551
+ type RenderingPreference = "auto" | "gpu" | "software";
11552
+ ```
11553
+
11554
+ Example: Describe the saved choice on a settings page
11555
+
11556
+ The plugin turns the saved choice into the words its settings page
11557
+ shows. `preference` is what the user chose, not what draws now:
11558
+ RenderingState.nextMode says what the next start will use.
11559
+ ```ts
11560
+ const { preference } = await plugin.rendering.state();
11561
+ // One word per choice; the Record type makes sure none is missing.
11562
+ const words: Record<RenderingPreference, string> = {
11563
+ auto: "Automatic",
11564
+ gpu: "GPU",
11565
+ software: "Software",
11566
+ };
11567
+ console.log(words[preference]);
11568
+ ```
11569
+
11570
+ ### RenderingReasonCode
11571
+
11572
+ Why the player renders the way it does, as a stable kebab-case code. A
11573
+ newer player may add codes: compare with the ones you handle and show
11574
+ RenderingReason.message for the rest. The codes of player 0.5.13:
11575
+
11576
+ | Code | Meaning |
11577
+ | --- | --- |
11578
+ | `default` | Nothing ruled the GPU out; the platform's default. |
11579
+ | `pending` | GPU rendering is starting. |
11580
+ | `headless` | The player runs without a user interface. |
11581
+ | `no-gpu-backend` | This player has no GPU renderer on this system (Windows today). |
11582
+ | `env` | The environment chose it (`VESTRA_RENDER`, `VESTRA_SOFTWARE_RENDER`). |
11583
+ | `preference` | The user chose software rendering (Rendering.setPreference); GPU chosen and available reports `default`. |
11584
+ | `denylist` | The graphics driver is on the player's denylist. |
11585
+ | `previous-crash` | GPU rendering crashed at an earlier start; it is tried again after a driver or player update, or after 7 days. |
11586
+ | `init-failed` | GPU rendering could not start. |
11587
+ | `window-fallback` | A window could not show GPU frames; the player switched to software during the run. |
11588
+
11589
+ - @player 0.5.13
11590
+
11591
+ ```ts
11592
+ type RenderingReasonCode = "default" | "pending" | "headless" | "no-gpu-backend" | "env" | "preference" | "denylist" | "previous-crash" | "init-failed" | "window-fallback" | (string & {});
11593
+ ```
11594
+
11595
+ Example: Explain a driver problem differently from a user choice
11596
+
11597
+ The plugin adds advice only when the graphics driver is the cause.
11598
+ `reason.code` is looked up among the codes the plugin knows; an unknown
11599
+ code from a newer player falls through to the player's own message.
11600
+ ```ts
11601
+ const { reason } = await plugin.rendering.state();
11602
+ // Advice for the codes this plugin knows; any other code shows the message.
11603
+ const advice: Partial<Record<RenderingReasonCode, string>> = {
11604
+ denylist: "Update the graphics driver to use the GPU.",
11605
+ "previous-crash": "The GPU renderer crashed earlier; it is retried after an update.",
11606
+ };
11607
+ console.log(advice[reason.code] ?? reason.message);
11608
+ ```
11609
+
11610
+ ### RenderingReason
11611
+
11612
+ Why the player renders, or will render, the way it does: a stable
11613
+ RenderingReasonCode to branch on and a sentence to show.
11614
+
11615
+ - @player 0.5.13
11616
+
11617
+ ```ts
11618
+ interface RenderingReason {
11619
+ /** The reason as a stable kebab-case code; branch on this. */
11620
+ readonly code: RenderingReasonCode;
11621
+ /** The reason as English text for people, for example with the graphics adapter's name and driver version; never branch on it. */
11622
+ readonly message: string;
11623
+ }
11624
+ ```
11625
+
11626
+ Example: Show why GPU rendering is off
11627
+
11628
+ The plugin logs the player's explanation when the video is drawn in
11629
+ software. `message` is the player's English text and may name the
11630
+ graphics adapter and driver version; `code` is for decisions.
11631
+ ```ts
11632
+ const { mode, reason } = await plugin.rendering.state();
11633
+ if (mode === "software") console.log(`${reason.code}: ${reason.message}`);
11634
+ ```
11635
+
11636
+ ### RenderingState
11637
+
11638
+ What Rendering.state and Rendering.setPreference report:
11639
+ the renderer drawing now and why, the saved choice, what the next start
11640
+ will use, and whether `"gpu"` can be chosen.
11641
+
11642
+ - @player 0.5.13
11643
+
11644
+ ```ts
11645
+ interface RenderingState {
11646
+ /** The renderer drawing the video now; the same as SystemInfo.rendering. */
11647
+ readonly mode: RenderingMode;
11648
+ /** Why RenderingState.mode is what it is now. */
11649
+ readonly reason: RenderingReason;
11650
+ /** The saved choice; it applies from the next start. */
11651
+ readonly preference: RenderingPreference;
11652
+ /** The renderer the next start will use, as far as the player knows now: from the saved choice, the environment, the driver denylist and an earlier GPU crash. */
11653
+ readonly nextMode: RenderingMode;
11654
+ /** Why the next start will use RenderingState.nextMode. */
11655
+ readonly nextReason: RenderingReason;
11656
+ /**
11657
+ * `true` while RenderingState.nextMode differs from the mode this run started with, so a restart would change the renderer; `false` again when the user changes the choice back.
11658
+ */
11659
+ readonly restartPending: boolean;
11660
+ /** Whether the user can choose `"gpu"` now. */
11661
+ readonly gpu: {
11662
+ /** `true` when `setPreference("gpu")` would be accepted. */
11663
+ readonly available: boolean;
11664
+ /** Why `"gpu"` cannot be chosen (`no-gpu-backend`, `headless`, `denylist`, `previous-crash`…); `null` while it is available. */
11665
+ readonly reason: RenderingReason | null;
11666
+ };
11667
+ }
11668
+ ```
11669
+
11670
+ Example: Label a menu item while a change waits for a restart
11671
+
11672
+ The plugin adds "(Restart to Apply)" to its GPU item after the user
11673
+ changed the choice. `restartPending` compares the mode the next start
11674
+ will use with the mode this run started with, so it turns false again
11675
+ when the user changes the choice back.
11676
+ ```ts
11677
+ const state = await plugin.rendering.state();
11678
+ const title = state.restartPending
11679
+ ? "GPU Rendering (Experimental) (Restart to Apply)"
11680
+ : "GPU Rendering (Experimental)";
11681
+ console.log(title, state.nextMode, state.nextReason.message);
11682
+ ```
11683
+
11684
+ ### Rendering
11685
+
11686
+ `plugin.rendering`: the player's rendering choice, GPU or software.
11687
+ Reading it needs no capability; changing the saved choice and restarting
11688
+ the player need `rendering.manage`.
11689
+
11690
+ The choice works like a browser's hardware acceleration switch: the
11691
+ player saves it in its own settings at once, and the running player
11692
+ keeps its renderer. The next start uses it, whether the user restarts
11693
+ now through Rendering.restartPlayer or quits and starts the
11694
+ player later. The player can still rule the GPU out at a start (an
11695
+ environment variable, a graphics driver on its denylist, a GPU crash at
11696
+ an earlier start); RenderingState.nextMode and
11697
+ RenderingState.nextReason say what the next start will do.
11698
+
11699
+ The renderer is a choice for the whole player and a restart stops every
11700
+ plugin, so change it only when the user asks. A plugin that only needs
11701
+ to know whether render options work reads SystemInfo.rendering.
11702
+
11703
+ Players before 0.5.13 do not know these requests: `state()` rejects with
11704
+ `unsupported`, and they do not load a plugin that declares
11705
+ `rendering.manage` (an unknown capability). Declare
11706
+ `"minPlayerVersion": "0.5.13"` when the plugin needs this.
11707
+
11708
+ - @capability None to read the state; `rendering.manage` to change the choice or restart the player.
11709
+ - @player 0.5.13; older players reject the requests as unknown (`unsupported`).
11710
+
11711
+ ```ts
11712
+ interface Rendering {
11713
+ /**
11714
+ * Read the renderer drawing now, the saved choice, what the next start will use and whether `"gpu"` can be chosen. No capability.
11715
+ * @capability None.
11716
+ * @throws unsupported (rejects) A player before 0.5.13, which does not know the request.
11717
+ * @example Find out whether the GPU can be chosen
11718
+ * The plugin reads the state once while it starts and keeps it for its
11719
+ * menu. Each call asks the player; `gpu.available` is false, with a
11720
+ * reason, where this player has no GPU renderer, without a user
11721
+ * interface, or when the driver is ruled out. An older player rejects
11722
+ * with `unsupported`, and the plugin then offers nothing.
11723
+ * ```ts
11724
+ * const state = await plugin.rendering.state().catch((error: PluginError) => {
11725
+ * if (error.code === "unsupported") return null; // a player before 0.5.13
11726
+ * throw error;
11727
+ * });
11728
+ * if (state && !state.gpu.available) console.log(state.gpu.reason?.message);
11729
+ * ```
11730
+ */
11731
+ state(): Promise<RenderingState>;
11732
+ /**
11733
+ * Save the user's rendering choice. The player writes it to its settings at once; the running player keeps its renderer, and the next start uses the choice whether or not the user restarts now. `"auto"` and `"software"` are always accepted.
11734
+ * @param preference - `"auto"`, `"gpu"` or `"software"`.
11735
+ * @returns The new state; RenderingState.restartPending tells whether a restart would change the renderer.
11736
+ * @capability rendering.manage
11737
+ * @throws invalid (rejects) `preference` is not `"auto"`, `"gpu"` or `"software"`.
11738
+ * @throws unsupported (rejects) `"gpu"` where this player has no GPU renderer on this system, or runs without a user interface; also any value on a player before 0.5.13.
11739
+ * @throws unavailable (rejects) `"gpu"` while it is ruled out: the graphics driver is on the player's denylist, or GPU rendering crashed at an earlier start. The message says which, with the adapter's name and the driver version.
11740
+ * @throws failed (rejects) The player could not write its settings file.
11741
+ * @throws capability (rejects) The manifest does not declare `rendering.manage`.
11742
+ * @example Switch back to the automatic choice
11743
+ * A Reset item returns the renderer to the player's own choice and tells
11744
+ * the user when it takes effect. `setPreference` saves the choice and
11745
+ * resolves with the new state; `restartPending` is true when the next
11746
+ * start will draw differently from this run. A refused save leaves the
11747
+ * old choice in place.
11748
+ * ```ts
11749
+ * try {
11750
+ * const state = await plugin.rendering.setPreference("auto");
11751
+ * if (state.restartPending) await plugin.osd.show("Applies after a restart");
11752
+ * } catch (error) {
11753
+ * // `failed`: the player's settings file could not be written.
11754
+ * await plugin.osd.show("Not saved ({})", [(error as PluginError).code]);
11755
+ * }
11756
+ * ```
11757
+ */
11758
+ setPreference(preference: RenderingPreference): Promise<RenderingState>;
11759
+ /**
11760
+ * Restart the player so that a changed choice applies now. Resolves once the player has accepted; the player then quits normally, which stops every plugin (this one included), and starts again with the same plugins. A local file that was open continues at its position, paused or playing as it was; a stream or another source that is not a file is not reopened.
11761
+ * @capability rendering.manage
11762
+ * @throws unsupported (rejects) The player runs without a user interface; also a player before 0.5.13.
11763
+ * @throws busy (rejects) A restart is already under way, or this player was itself started by a restart less than 30 seconds ago (so a plugin that restarts on activation cannot make a loop).
11764
+ * @throws failed (rejects) The new player process could not be started; the player keeps running.
11765
+ * @throws capability (rejects) The manifest does not declare `rendering.manage`.
11766
+ * @example Restart after asking the user
11767
+ * After the user changed the renderer, the plugin offers to restart now.
11768
+ * `confirm` asks first, because a restart closes every window (it needs
11769
+ * `ui.dialog`); once `restartPlayer` resolves, the player is quitting and
11770
+ * will stop this plugin, so nothing else is started after it.
11771
+ * ```ts
11772
+ * const { confirmed } = await plugin.dialogs.confirm({
11773
+ * message: "Restart the player now?",
11774
+ * detail: "The new renderer is used from the restart on. The video continues where it is.",
11775
+ * confirm: "Restart",
11776
+ * });
11777
+ * if (confirmed)
11778
+ * await plugin.rendering.restartPlayer().catch((error: PluginError) => {
11779
+ * // `failed`: the new player could not start; this one keeps running.
11780
+ * void plugin.osd.show("Could not restart ({})", [error.code]);
11781
+ * });
11782
+ * ```
11783
+ */
11784
+ restartPlayer(): Promise<void>;
11785
+ }
11786
+ ```
11787
+
11788
+ Example: Offer GPU rendering as a menu item
11789
+
11790
+ Settings ▸ GPU Rendering (Experimental) carries a check mark while the
11791
+ next start will draw on the GPU, is greyed out with the player's reason
11792
+ where the GPU cannot be chosen, and saves the other choice when picked.
11793
+ `state()` reads the player's view once; `setPreference` saves the choice
11794
+ and resolves with the new state, which the item's bound values read
11795
+ again after the handler. The how-to guide adds the restart.
11796
+ ```ts
11797
+ let state = await plugin.rendering.state();
11798
+ plugin.commands.add({
11799
+ id: "gpu-rendering",
11800
+ path: ["@settings", "GPU Rendering (Experimental)"],
11801
+ checked: () => state.nextMode === "gpu", // also for "auto" on a GPU system
11802
+ enabled: () => state.gpu.available,
11803
+ reason: () => state.gpu.reason?.message ?? null, // shown while disabled
11804
+ run: async () => {
11805
+ const choice = state.nextMode === "gpu" ? "software" : "gpu";
11806
+ state = await plugin.rendering.setPreference(choice);
11807
+ },
11808
+ });
11809
+ ```
11810
+
11371
11811
  ### VideoNode
11372
11812
 
11373
11813
  One node of a VideoFilter chain, by `kind`. Values outside the
@@ -12064,7 +12504,7 @@ Members fall into a few groups:
12064
12504
  TestHost.app, TestHost.systemInfo,
12065
12505
  TestHost.chapters, TestHost.audioDevices,
12066
12506
  TestHost.secrets, TestHost.clipboard,
12067
- TestHost.tools.
12507
+ TestHost.tools, TestHost.rendering.
12068
12508
  - **Assert** on records: TestHost.osd, TestHost.calls,
12069
12509
  TestHost.sent, TestHost.conflicts,
12070
12510
  TestHost.layout, TestHost.menus, and many more.
@@ -12658,7 +13098,7 @@ class TestHost implements Transport {
12658
13098
  */
12659
13099
  setVideoRenderOwner(pluginId: string | null): void;
12660
13100
  /**
12661
- * What the player pushes when mpv cannot compile a shader (code `failed`) or switches to software rendering (`unsupported`, which also sets `videoRender.gpu` to false). Pushed only when this plugin is the owner.
13101
+ * What the player pushes when mpv cannot compile a shader (code `failed`) or switches to software rendering (`unsupported`, which also sets `videoRender.gpu` to false and `rendering.reason` to `window-fallback` with `message`). Pushed only when this plugin is the owner.
12662
13102
  * @capability None.
12663
13103
  * @example Make a shader fail to compile
12664
13104
  * The plugin's render options report `failed`, as when mpv rejects a
@@ -12884,9 +13324,9 @@ class TestHost implements Transport {
12884
13324
  */
12885
13325
  readonly sessionStorage: Map<string, Json>;
12886
13326
  /**
12887
- * What `plugin.app.system()` reports. Its `rendering` follows videoRender.gpu (`"gpu"` or `"software"`), so one switch covers the render options and the fact.
13327
+ * What `plugin.app.system()` reports. Its `rendering` follows videoRender.gpu (`"gpu"` or `"software"`), so one switch covers the render options and the fact; its `renderingReason` is rendering.reason.
12888
13328
  */
12889
- systemInfo: Omit<WireSystemInfo, "rendering">;
13329
+ systemInfo: Omit<WireSystemInfo, "rendering" | "renderingReason">;
12890
13330
  /** What `plugin.app` reports. Set it before startPlugin: the plugin receives it once at start-up. */
12891
13331
  app: {
12892
13332
  /** The player version. */
@@ -12903,6 +13343,70 @@ class TestHost implements Transport {
12903
13343
  slint: string;
12904
13344
  };
12905
13345
  };
13346
+ /**
13347
+ * The player's rendering choice behind `plugin.rendering` (player
13348
+ * 0.5.13). The mode drawing now is videoRender.gpu, as for `plugin.app.system()`; the rest is preset here
13349
+ * and changed by the plugin's calls, as the player would:
13350
+ *
13351
+ * - `setPreference` saves `preference` and never changes the mode. It
13352
+ * refuses `"gpu"` while `gpu.available` is false: with `unsupported`
13353
+ * when `gpu.reason.code` is `no-gpu-backend` or `headless`, otherwise
13354
+ * with `unavailable`, the reason's message in both cases.
13355
+ * - The next start uses `"software"` for the `"software"` choice, else
13356
+ * software with `gpu.reason` while the GPU is not available, else the
13357
+ * GPU (code `preference` for a `"gpu"` choice, `default` for
13358
+ * `"auto"`). `restartPending` compares that with `startedWith`.
13359
+ * - `restartPlayer` counts in `restarts` and leaves the plugin running
13360
+ * (call `stop()` yourself); a second one rejects with `busy`, as a
13361
+ * restart is then under way.
13362
+ *
13363
+ * Inject other refusals (`failed` for an unwritable settings file or a
13364
+ * new process that could not start) with `host.handle("rendering.setPreference", …)`
13365
+ * or `host.handle("rendering.restart", …)`. The defaults are a macOS
13366
+ * player on the GPU: `"auto"`, available, started on the GPU.
13367
+ * @example Test a GPU switch on a system without a GPU renderer
13368
+ * The plugin's GPU menu item is greyed out with the player's reason when
13369
+ * the GPU cannot be chosen. The host is preset before the plugin starts,
13370
+ * because the plugin reads the state while activating; software
13371
+ * rendering is `videoRender.gpu = false`, and `gpu` says why the GPU
13372
+ * cannot be chosen.
13373
+ * ```ts
13374
+ * import assert from "node:assert/strict";
13375
+ * import { startPlugin, TestHost } from "@vestra-player/sdk/testing";
13376
+ * import activate from "../main.ts";
13377
+ *
13378
+ * const host = new TestHost();
13379
+ * host.videoRender.gpu = false; // software draws now (Windows)
13380
+ * host.rendering.startedWith = "software";
13381
+ * host.rendering.reason = { code: "no-gpu-backend", message: "No GPU renderer on Windows" };
13382
+ * host.rendering.gpu = { available: false, reason: host.rendering.reason };
13383
+ * const { stop } = await startPlugin(activate, {
13384
+ * directory: new URL("..", import.meta.url),
13385
+ * host,
13386
+ * });
13387
+ * const item = host.command("gpu-rendering");
13388
+ * assert.equal(item.state.enabled, false);
13389
+ * assert.equal(item.state.reason, "No GPU renderer on Windows");
13390
+ * await stop();
13391
+ * ```
13392
+ */
13393
+ readonly rendering: {
13394
+ /** Why the mode drawing now is what it is (`RenderingState.reason`, `SystemInfo.renderingReason`). */
13395
+ reason: RenderingReason;
13396
+ /** The saved choice: `"auto"`, `"gpu"` or `"software"`. */
13397
+ preference: RenderingPreference;
13398
+ /** The mode this run started with; `restartPending` compares the next start's mode with it. */
13399
+ startedWith: RenderingMode;
13400
+ /** Whether the plugin may choose `"gpu"`, and why not (`null` while available). */
13401
+ gpu: {
13402
+ /** `false`: `setPreference("gpu")` is refused. */
13403
+ available: boolean;
13404
+ /** Why the GPU cannot be chosen; `null` while it can. */
13405
+ reason: RenderingReason | null;
13406
+ };
13407
+ /** Accepted `restartPlayer` calls. */
13408
+ restarts: number;
13409
+ };
12906
13410
  /** What `player.chapters()` lists while media is ready. */
12907
13411
  chapters: {
12908
13412
  /** The chapter title, `null` without one. */
@@ -13591,8 +14095,8 @@ current.
13591
14095
 
13592
14096
  A member not listed under any capability needs none (`plugin.tools`,
13593
14097
  `plugin.storage`, `plugin.system`, `plugin.app`, `plugin.ui`,
13594
- `plugin.i18n`, reading `plugin.window.state`, `plugin.appearance.theme`
13595
- and `plugin.packages.list`).
14098
+ `plugin.i18n`, reading `plugin.window.state`, `plugin.appearance.theme`,
14099
+ `plugin.packages.list` and `plugin.rendering.state`).
13596
14100
 
13597
14101
  ```ts
13598
14102
  const CAPABILITY_INFO: Readonly<Record<string, CapabilityInfo>>;
@@ -13826,7 +14330,10 @@ menu last): their key paths, default bindings and owners. Use it to pick
13826
14330
  a menu place and default keys that do not collide with the player's (the
13827
14331
  user's own changes come on top; see Bindings). Menu items built
13828
14332
  from content (tracks, recent files, output devices) are not listed; their
13829
- placeholders (`No Audio Tracks`) are. The player's tests check that this
14333
+ placeholders (`No Audio Tracks`) are. Items that appear only in a
14334
+ passing state are not listed either: the Picture Adjustments plugin's
14335
+ `Video ▸ Restart Player to Apply` (`video.gpu-rendering.restart`), shown
14336
+ while a rendering choice waits for the next start. The player's tests check that this
13830
14337
  list agrees with what the player and its built-in plugins register.
13831
14338
 
13832
14339
  ```ts
@@ -14685,6 +15192,8 @@ interface WireSystemInfo {
14685
15192
  }[];
14686
15193
  /** Player 0.5.12; older players leave it out. */
14687
15194
  rendering?: "gpu" | "software";
15195
+ /** Why `rendering` is what it is (player 0.5.13; older players leave it out). */
15196
+ renderingReason?: WireRenderingReason;
14688
15197
  }
14689
15198
  ```
14690
15199