@vestra-player/sdk 1.5.34 → 1.5.35

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.
@@ -268,7 +268,7 @@ const MAX_SIDEBAR_TABS = 8;
268
268
  * {@link TestHost.app}, {@link TestHost.systemInfo},
269
269
  * {@link TestHost.chapters}, {@link TestHost.audioDevices},
270
270
  * {@link TestHost.secrets}, {@link TestHost.clipboard},
271
- * {@link TestHost.tools}.
271
+ * {@link TestHost.tools}, {@link TestHost.rendering}.
272
272
  * - **Assert** on records: {@link TestHost.osd}, {@link TestHost.calls},
273
273
  * {@link TestHost.sent}, {@link TestHost.conflicts},
274
274
  * {@link TestHost.layout}, {@link TestHost.menus}, and many more.
@@ -1267,7 +1267,8 @@ export class TestHost {
1267
1267
  /**
1268
1268
  * What the player pushes when mpv cannot compile a shader (code `failed`)
1269
1269
  * or switches to software rendering (`unsupported`, which also sets
1270
- * `videoRender.gpu` to false). Pushed only when this plugin is the owner.
1270
+ * `videoRender.gpu` to false and `rendering.reason` to `window-fallback`
1271
+ * with `message`). Pushed only when this plugin is the owner.
1271
1272
  *
1272
1273
  * @example Make a shader fail to compile
1273
1274
  * The plugin's render options report `failed`, as when mpv rejects a
@@ -1290,8 +1291,10 @@ export class TestHost {
1290
1291
  * @since 1.0.0
1291
1292
  */
1292
1293
  failVideoRender(code, message) {
1293
- if (code === "unsupported")
1294
+ if (code === "unsupported") {
1294
1295
  this.videoRender.gpu = false;
1296
+ this.rendering.reason = { code: "window-fallback", message };
1297
+ }
1295
1298
  if (this.videoRender.owner === this.manifestId)
1296
1299
  this.emit("video.render.status", {
1297
1300
  state: "failed",
@@ -1527,7 +1530,8 @@ export class TestHost {
1527
1530
  /**
1528
1531
  * What `plugin.app.system()` reports. Its `rendering` follows
1529
1532
  * {@link TestHost.videoRender | videoRender.gpu} (`"gpu"` or
1530
- * `"software"`), so one switch covers the render options and the fact.
1533
+ * `"software"`), so one switch covers the render options and the fact;
1534
+ * its `renderingReason` is {@link TestHost.rendering | rendering.reason}.
1531
1535
  */
1532
1536
  systemInfo = {
1533
1537
  os: "macos",
@@ -1560,6 +1564,64 @@ export class TestHost {
1560
1564
  slint: "1.18.1",
1561
1565
  },
1562
1566
  };
1567
+ /**
1568
+ * The player's rendering choice behind `plugin.rendering` (player
1569
+ * 0.5.13). The mode drawing now is {@link TestHost.videoRender |
1570
+ * videoRender.gpu}, as for `plugin.app.system()`; the rest is preset here
1571
+ * and changed by the plugin's calls, as the player would:
1572
+ *
1573
+ * - `setPreference` saves `preference` and never changes the mode. It
1574
+ * refuses `"gpu"` while `gpu.available` is false: with `unsupported`
1575
+ * when `gpu.reason.code` is `no-gpu-backend` or `headless`, otherwise
1576
+ * with `unavailable`, the reason's message in both cases.
1577
+ * - The next start uses `"software"` for the `"software"` choice, else
1578
+ * software with `gpu.reason` while the GPU is not available, else the
1579
+ * GPU (code `preference` for a `"gpu"` choice, `default` for
1580
+ * `"auto"`). `restartPending` compares that with `startedWith`.
1581
+ * - `restartPlayer` counts in `restarts` and leaves the plugin running
1582
+ * (call `stop()` yourself); a second one rejects with `busy`, as a
1583
+ * restart is then under way.
1584
+ *
1585
+ * Inject other refusals (`failed` for an unwritable settings file or a
1586
+ * new process that could not start) with `host.handle("rendering.setPreference", …)`
1587
+ * or `host.handle("rendering.restart", …)`. The defaults are a macOS
1588
+ * player on the GPU: `"auto"`, available, started on the GPU.
1589
+ *
1590
+ * @example Test a GPU switch on a system without a GPU renderer
1591
+ * The plugin's GPU menu item is greyed out with the player's reason when
1592
+ * the GPU cannot be chosen. The host is preset before the plugin starts,
1593
+ * because the plugin reads the state while activating; software
1594
+ * rendering is `videoRender.gpu = false`, and `gpu` says why the GPU
1595
+ * cannot be chosen.
1596
+ * ```ts
1597
+ * import assert from "node:assert/strict";
1598
+ * import { startPlugin, TestHost } from "@vestra-player/sdk/testing";
1599
+ * import activate from "../main.ts";
1600
+ *
1601
+ * const host = new TestHost();
1602
+ * host.videoRender.gpu = false; // software draws now (Windows)
1603
+ * host.rendering.startedWith = "software";
1604
+ * host.rendering.reason = { code: "no-gpu-backend", message: "No GPU renderer on Windows" };
1605
+ * host.rendering.gpu = { available: false, reason: host.rendering.reason };
1606
+ * const { stop } = await startPlugin(activate, {
1607
+ * directory: new URL("..", import.meta.url),
1608
+ * host,
1609
+ * });
1610
+ * const item = host.command("gpu-rendering");
1611
+ * assert.equal(item.state.enabled, false);
1612
+ * assert.equal(item.state.reason, "No GPU renderer on Windows");
1613
+ * await stop();
1614
+ * ```
1615
+ *
1616
+ * @since 1.5.35
1617
+ */
1618
+ rendering = {
1619
+ reason: { code: "default", message: "nothing rules GPU rendering out" },
1620
+ preference: "auto",
1621
+ startedWith: "gpu",
1622
+ gpu: { available: true, reason: null },
1623
+ restarts: 0,
1624
+ };
1563
1625
  /** What `player.chapters()` lists while media is ready. */
1564
1626
  chapters = [];
1565
1627
  #overrides = new Map();
@@ -3190,6 +3252,54 @@ export class TestHost {
3190
3252
  revision: stamp.revision + 1,
3191
3253
  };
3192
3254
  }
3255
+ /** `rendering.state` from {@link TestHost.rendering} and `videoRender.gpu`. */
3256
+ #renderingState() {
3257
+ const { reason, preference, startedWith, gpu } = this.rendering;
3258
+ // The player's order (render_mode.rs decide): no windows or no GPU
3259
+ // renderer first, then the preference `software`, then what keeps GPU
3260
+ // rendering off (denylist, crash), else GPU with the reason `default`
3261
+ // (also when the preference is `gpu`).
3262
+ const system = !gpu.available &&
3263
+ (gpu.reason?.code === "headless" ||
3264
+ gpu.reason?.code === "no-gpu-backend");
3265
+ const next = system
3266
+ ? { mode: "software", reason: gpu.reason }
3267
+ : preference === "software"
3268
+ ? {
3269
+ mode: "software",
3270
+ reason: {
3271
+ code: "preference",
3272
+ message: "chosen in the player's settings",
3273
+ },
3274
+ }
3275
+ : !gpu.available
3276
+ ? {
3277
+ mode: "software",
3278
+ reason: gpu.reason ?? {
3279
+ code: "default",
3280
+ message: "nothing rules GPU rendering out",
3281
+ },
3282
+ }
3283
+ : {
3284
+ mode: "gpu",
3285
+ reason: {
3286
+ code: "default",
3287
+ message: "nothing rules GPU rendering out",
3288
+ },
3289
+ };
3290
+ return {
3291
+ mode: this.videoRender.gpu ? "gpu" : "software",
3292
+ reason: { ...reason },
3293
+ preference,
3294
+ nextMode: next.mode,
3295
+ nextReason: { ...next.reason },
3296
+ restartPending: next.mode !== startedWith,
3297
+ gpu: {
3298
+ available: gpu.available,
3299
+ reason: gpu.available || !gpu.reason ? null : { ...gpu.reason },
3300
+ },
3301
+ };
3302
+ }
3193
3303
  #defaults = {
3194
3304
  "events.subscribe": (p) => void this.subscriptions.add(String(p.event)),
3195
3305
  "events.unsubscribe": (p) => void this.subscriptions.delete(String(p.event)),
@@ -3506,7 +3616,30 @@ export class TestHost {
3506
3616
  "app.system": () => ({
3507
3617
  ...structuredClone(this.systemInfo),
3508
3618
  rendering: this.videoRender.gpu ? "gpu" : "software",
3619
+ renderingReason: { ...this.rendering.reason },
3509
3620
  }),
3621
+ // As the player 0.5.13 (see TestHost.rendering for the model).
3622
+ "rendering.state": () => this.#renderingState(),
3623
+ "rendering.setPreference": (p) => {
3624
+ const preference = p.preference;
3625
+ if (preference !== "auto" &&
3626
+ preference !== "gpu" &&
3627
+ preference !== "software")
3628
+ throw new HostError("invalid", 'preference must be "auto", "gpu" or "software"');
3629
+ const { available, reason } = this.rendering.gpu;
3630
+ if (preference === "gpu" && !available)
3631
+ throw new HostError(reason?.code === "no-gpu-backend" || reason?.code === "headless"
3632
+ ? "unsupported"
3633
+ : "unavailable", reason?.message ?? "GPU rendering cannot be chosen");
3634
+ this.rendering.preference = preference;
3635
+ return this.#renderingState();
3636
+ },
3637
+ "rendering.restart": () => {
3638
+ if (this.rendering.restarts > 0)
3639
+ throw new HostError("busy", "a restart is already under way");
3640
+ this.rendering.restarts++;
3641
+ return null;
3642
+ },
3510
3643
  "clipboard.readText": () => this.clipboard.text,
3511
3644
  "clipboard.writeText": (p) => {
3512
3645
  if (typeof p.text !== "string")
@@ -293,6 +293,8 @@ export interface Scope extends Disposable {
293
293
  readonly window: MainWindow;
294
294
  /** The player's version and components, and the system it runs on. */
295
295
  readonly app: App;
296
+ /** GPU or software rendering: read the choice, change it (`rendering.manage`). */
297
+ readonly rendering: Rendering;
296
298
  /** Every command's bindings and the user's overrides (`bindings.manage`). */
297
299
  readonly bindings: Bindings;
298
300
  /** Runtime images for Slint `image` properties. */
@@ -4075,6 +4077,31 @@ export interface SystemInfo {
4075
4077
  * @since 1.5.34
4076
4078
  */
4077
4079
  readonly rendering?: "gpu" | "software";
4080
+ /**
4081
+ * Why {@link SystemInfo.rendering} is what it is now: the same reason as
4082
+ * {@link RenderingState.reason} (`default`, `no-gpu-backend`,
4083
+ * `preference`, `denylist`, `window-fallback`…, see
4084
+ * {@link RenderingReasonCode}). Read at each call, like `rendering`.
4085
+ *
4086
+ * Absent (`undefined`) on players before 0.5.13.
4087
+ *
4088
+ * @example Say why the video is drawn in software
4089
+ * An About page explains a software renderer with the player's own words,
4090
+ * for example that the graphics driver is on the player's denylist.
4091
+ * `system()` reports the renderer and its reason together, with no
4092
+ * capability; the message is English text that may name the adapter and
4093
+ * the driver version.
4094
+ * ```ts
4095
+ * const { rendering, renderingReason } = await plugin.app.system();
4096
+ * if (rendering === "software" && renderingReason)
4097
+ * console.log(`Rendering: software (${renderingReason.message})`);
4098
+ * ```
4099
+ *
4100
+ * @player 0.5.13; older players leave it out (declare
4101
+ * `"minPlayerVersion": "0.5.13"` when the plugin relies on it).
4102
+ * @since 1.5.35
4103
+ */
4104
+ readonly renderingReason?: RenderingReason;
4078
4105
  }
4079
4106
  /**
4080
4107
  * One display in {@link SystemInfo.displays}.
@@ -8934,3 +8961,329 @@ export interface Secrets {
8934
8961
  */
8935
8962
  delete(name: string): Promise<void>;
8936
8963
  }
8964
+ /**
8965
+ * Which renderer draws the video: `"gpu"` (the GPU renderer, where
8966
+ * {@link RenderOptions | render options} apply) or `"software"` (they fail
8967
+ * with `unsupported`). The same values as {@link SystemInfo.rendering}.
8968
+ *
8969
+ * @example Tell the user which renderer draws the video
8970
+ * The plugin shows "Rendering: GPU" or "Rendering: software" on the
8971
+ * video. `state()` asks the player; `mode` is the renderer drawing now,
8972
+ * which can differ from the saved choice until the next start.
8973
+ * ```ts
8974
+ * const { mode } = await plugin.rendering.state();
8975
+ * const label: Record<RenderingMode, string> = { gpu: "GPU", software: "software" };
8976
+ * await plugin.osd.show("Rendering: {}", [label[mode]]);
8977
+ * ```
8978
+ *
8979
+ * @player 0.5.13
8980
+ * @since 1.5.35
8981
+ * @category Services
8982
+ */
8983
+ export type RenderingMode = "gpu" | "software";
8984
+ /**
8985
+ * The user's saved rendering choice: `"auto"` (the player decides: the GPU
8986
+ * where this system has a GPU renderer that is not ruled out, software
8987
+ * elsewhere), `"gpu"` or `"software"`. It applies from the next start.
8988
+ *
8989
+ * @example Describe the saved choice on a settings page
8990
+ * The plugin turns the saved choice into the words its settings page
8991
+ * shows. `preference` is what the user chose, not what draws now:
8992
+ * {@link RenderingState.nextMode} says what the next start will use.
8993
+ * ```ts
8994
+ * const { preference } = await plugin.rendering.state();
8995
+ * // One word per choice; the Record type makes sure none is missing.
8996
+ * const words: Record<RenderingPreference, string> = {
8997
+ * auto: "Automatic",
8998
+ * gpu: "GPU",
8999
+ * software: "Software",
9000
+ * };
9001
+ * console.log(words[preference]);
9002
+ * ```
9003
+ *
9004
+ * @player 0.5.13
9005
+ * @since 1.5.35
9006
+ * @category Services
9007
+ */
9008
+ export type RenderingPreference = "auto" | "gpu" | "software";
9009
+ /**
9010
+ * Why the player renders the way it does, as a stable kebab-case code. A
9011
+ * newer player may add codes: compare with the ones you handle and show
9012
+ * {@link RenderingReason.message} for the rest. The codes of player 0.5.13:
9013
+ *
9014
+ * | Code | Meaning |
9015
+ * | --- | --- |
9016
+ * | `default` | Nothing ruled the GPU out; the platform's default. |
9017
+ * | `pending` | GPU rendering is starting. |
9018
+ * | `headless` | The player runs without a user interface. |
9019
+ * | `no-gpu-backend` | This player has no GPU renderer on this system (Windows today). |
9020
+ * | `env` | The environment chose it (`VESTRA_RENDER`, `VESTRA_SOFTWARE_RENDER`). |
9021
+ * | `preference` | The user chose software rendering ({@link Rendering.setPreference}); GPU chosen and available reports `default`. |
9022
+ * | `denylist` | The graphics driver is on the player's denylist. |
9023
+ * | `previous-crash` | GPU rendering crashed at an earlier start; it is tried again after a driver or player update, or after 7 days. |
9024
+ * | `init-failed` | GPU rendering could not start. |
9025
+ * | `window-fallback` | A window could not show GPU frames; the player switched to software during the run. |
9026
+ *
9027
+ * @example Explain a driver problem differently from a user choice
9028
+ * The plugin adds advice only when the graphics driver is the cause.
9029
+ * `reason.code` is looked up among the codes the plugin knows; an unknown
9030
+ * code from a newer player falls through to the player's own message.
9031
+ * ```ts
9032
+ * const { reason } = await plugin.rendering.state();
9033
+ * // Advice for the codes this plugin knows; any other code shows the message.
9034
+ * const advice: Partial<Record<RenderingReasonCode, string>> = {
9035
+ * denylist: "Update the graphics driver to use the GPU.",
9036
+ * "previous-crash": "The GPU renderer crashed earlier; it is retried after an update.",
9037
+ * };
9038
+ * console.log(advice[reason.code] ?? reason.message);
9039
+ * ```
9040
+ *
9041
+ * @player 0.5.13
9042
+ * @since 1.5.35
9043
+ * @category Services
9044
+ */
9045
+ export type RenderingReasonCode = "default" | "pending" | "headless" | "no-gpu-backend" | "env" | "preference" | "denylist" | "previous-crash" | "init-failed" | "window-fallback" | (string & {});
9046
+ /**
9047
+ * Why the player renders, or will render, the way it does: a stable
9048
+ * {@link RenderingReasonCode} to branch on and a sentence to show.
9049
+ *
9050
+ * @example Show why GPU rendering is off
9051
+ * The plugin logs the player's explanation when the video is drawn in
9052
+ * software. `message` is the player's English text and may name the
9053
+ * graphics adapter and driver version; `code` is for decisions.
9054
+ * ```ts
9055
+ * const { mode, reason } = await plugin.rendering.state();
9056
+ * if (mode === "software") console.log(`${reason.code}: ${reason.message}`);
9057
+ * ```
9058
+ *
9059
+ * @player 0.5.13
9060
+ * @since 1.5.35
9061
+ * @category Services
9062
+ */
9063
+ export interface RenderingReason {
9064
+ /** The reason as a stable kebab-case code; branch on this. */
9065
+ readonly code: RenderingReasonCode;
9066
+ /**
9067
+ * The reason as English text for people, for example with the graphics
9068
+ * adapter's name and driver version; never branch on it.
9069
+ */
9070
+ readonly message: string;
9071
+ }
9072
+ /**
9073
+ * What {@link Rendering.state} and {@link Rendering.setPreference} report:
9074
+ * the renderer drawing now and why, the saved choice, what the next start
9075
+ * will use, and whether `"gpu"` can be chosen.
9076
+ *
9077
+ * @example Label a menu item while a change waits for a restart
9078
+ * The plugin adds "(Restart to Apply)" to its GPU item after the user
9079
+ * changed the choice. `restartPending` compares the mode the next start
9080
+ * will use with the mode this run started with, so it turns false again
9081
+ * when the user changes the choice back.
9082
+ * ```ts
9083
+ * const state = await plugin.rendering.state();
9084
+ * const title = state.restartPending
9085
+ * ? "GPU Rendering (Experimental) (Restart to Apply)"
9086
+ * : "GPU Rendering (Experimental)";
9087
+ * console.log(title, state.nextMode, state.nextReason.message);
9088
+ * ```
9089
+ *
9090
+ * @player 0.5.13
9091
+ * @since 1.5.35
9092
+ * @category Services
9093
+ */
9094
+ export interface RenderingState {
9095
+ /** The renderer drawing the video now; the same as {@link SystemInfo.rendering}. */
9096
+ readonly mode: RenderingMode;
9097
+ /** Why {@link RenderingState.mode} is what it is now. */
9098
+ readonly reason: RenderingReason;
9099
+ /** The saved choice; it applies from the next start. */
9100
+ readonly preference: RenderingPreference;
9101
+ /**
9102
+ * The renderer the next start will use, as far as the player knows now:
9103
+ * from the saved choice, the environment, the driver denylist and an
9104
+ * earlier GPU crash.
9105
+ */
9106
+ readonly nextMode: RenderingMode;
9107
+ /** Why the next start will use {@link RenderingState.nextMode}. */
9108
+ readonly nextReason: RenderingReason;
9109
+ /**
9110
+ * `true` while {@link RenderingState.nextMode} differs from the mode this
9111
+ * run started with, so a restart would change the renderer; `false` again
9112
+ * when the user changes the choice back.
9113
+ */
9114
+ readonly restartPending: boolean;
9115
+ /** Whether the user can choose `"gpu"` now. */
9116
+ readonly gpu: {
9117
+ /** `true` when `setPreference("gpu")` would be accepted. */
9118
+ readonly available: boolean;
9119
+ /**
9120
+ * Why `"gpu"` cannot be chosen (`no-gpu-backend`, `headless`,
9121
+ * `denylist`, `previous-crash`…); `null` while it is available.
9122
+ */
9123
+ readonly reason: RenderingReason | null;
9124
+ };
9125
+ }
9126
+ /**
9127
+ * `plugin.rendering`: the player's rendering choice, GPU or software.
9128
+ * Reading it needs no capability; changing the saved choice and restarting
9129
+ * the player need `rendering.manage`.
9130
+ *
9131
+ * @remarks
9132
+ * The choice works like a browser's hardware acceleration switch: the
9133
+ * player saves it in its own settings at once, and the running player
9134
+ * keeps its renderer. The next start uses it, whether the user restarts
9135
+ * now through {@link Rendering.restartPlayer} or quits and starts the
9136
+ * player later. The player can still rule the GPU out at a start (an
9137
+ * environment variable, a graphics driver on its denylist, a GPU crash at
9138
+ * an earlier start); {@link RenderingState.nextMode} and
9139
+ * {@link RenderingState.nextReason} say what the next start will do.
9140
+ *
9141
+ * The renderer is a choice for the whole player and a restart stops every
9142
+ * plugin, so change it only when the user asks. A plugin that only needs
9143
+ * to know whether render options work reads {@link SystemInfo.rendering}.
9144
+ *
9145
+ * Players before 0.5.13 do not know these requests: `state()` rejects with
9146
+ * `unsupported`, and they do not load a plugin that declares
9147
+ * `rendering.manage` (an unknown capability). Declare
9148
+ * `"minPlayerVersion": "0.5.13"` when the plugin needs this.
9149
+ *
9150
+ * @example Offer GPU rendering as a menu item
9151
+ * Settings ▸ GPU Rendering (Experimental) carries a check mark while the
9152
+ * next start will draw on the GPU, is greyed out with the player's reason
9153
+ * where the GPU cannot be chosen, and saves the other choice when picked.
9154
+ * `state()` reads the player's view once; `setPreference` saves the choice
9155
+ * and resolves with the new state, which the item's bound values read
9156
+ * again after the handler. The how-to guide adds the restart.
9157
+ * ```ts
9158
+ * let state = await plugin.rendering.state();
9159
+ * plugin.commands.add({
9160
+ * id: "gpu-rendering",
9161
+ * path: ["@settings", "GPU Rendering (Experimental)"],
9162
+ * checked: () => state.nextMode === "gpu", // also for "auto" on a GPU system
9163
+ * enabled: () => state.gpu.available,
9164
+ * reason: () => state.gpu.reason?.message ?? null, // shown while disabled
9165
+ * run: async () => {
9166
+ * const choice = state.nextMode === "gpu" ? "software" : "gpu";
9167
+ * state = await plugin.rendering.setPreference(choice);
9168
+ * },
9169
+ * });
9170
+ * ```
9171
+ *
9172
+ * @capability None to read the state; `rendering.manage` to change the
9173
+ * choice or restart the player.
9174
+ * @see [Offer a GPU rendering switch](../../docs/how-to/gpu-rendering.md)
9175
+ * @player 0.5.13; older players reject the requests as unknown
9176
+ * (`unsupported`).
9177
+ * @since 1.5.35
9178
+ * @category Services
9179
+ */
9180
+ export interface Rendering {
9181
+ /**
9182
+ * Read the renderer drawing now, the saved choice, what the next start
9183
+ * will use and whether `"gpu"` can be chosen. No capability.
9184
+ *
9185
+ * @capability None.
9186
+ * @throws `unsupported` (rejects) A player before 0.5.13, which does not
9187
+ * know the request.
9188
+ *
9189
+ * @example Find out whether the GPU can be chosen
9190
+ * The plugin reads the state once while it starts and keeps it for its
9191
+ * menu. Each call asks the player; `gpu.available` is false, with a
9192
+ * reason, where this player has no GPU renderer, without a user
9193
+ * interface, or when the driver is ruled out. An older player rejects
9194
+ * with `unsupported`, and the plugin then offers nothing.
9195
+ * ```ts
9196
+ * const state = await plugin.rendering.state().catch((error: PluginError) => {
9197
+ * if (error.code === "unsupported") return null; // a player before 0.5.13
9198
+ * throw error;
9199
+ * });
9200
+ * if (state && !state.gpu.available) console.log(state.gpu.reason?.message);
9201
+ * ```
9202
+ *
9203
+ * @since 1.5.35
9204
+ */
9205
+ state(): Promise<RenderingState>;
9206
+ /**
9207
+ * Save the user's rendering choice. The player writes it to its settings
9208
+ * at once; the running player keeps its renderer, and the next start
9209
+ * uses the choice whether or not the user restarts now. `"auto"` and
9210
+ * `"software"` are always accepted.
9211
+ *
9212
+ * @param preference - `"auto"`, `"gpu"` or `"software"`.
9213
+ * @returns The new state; {@link RenderingState.restartPending} tells
9214
+ * whether a restart would change the renderer.
9215
+ *
9216
+ * @capability rendering.manage
9217
+ * @throws `invalid` (rejects) `preference` is not `"auto"`, `"gpu"` or
9218
+ * `"software"`.
9219
+ * @throws `unsupported` (rejects) `"gpu"` where this player has no GPU
9220
+ * renderer on this system, or runs without a user interface; also any
9221
+ * value on a player before 0.5.13.
9222
+ * @throws `unavailable` (rejects) `"gpu"` while it is ruled out: the
9223
+ * graphics driver is on the player's denylist, or GPU rendering crashed
9224
+ * at an earlier start. The message says which, with the adapter's name
9225
+ * and the driver version.
9226
+ * @throws `failed` (rejects) The player could not write its settings file.
9227
+ * @throws `capability` (rejects) The manifest does not declare
9228
+ * `rendering.manage`.
9229
+ *
9230
+ * @example Switch back to the automatic choice
9231
+ * A Reset item returns the renderer to the player's own choice and tells
9232
+ * the user when it takes effect. `setPreference` saves the choice and
9233
+ * resolves with the new state; `restartPending` is true when the next
9234
+ * start will draw differently from this run. A refused save leaves the
9235
+ * old choice in place.
9236
+ * ```ts
9237
+ * try {
9238
+ * const state = await plugin.rendering.setPreference("auto");
9239
+ * if (state.restartPending) await plugin.osd.show("Applies after a restart");
9240
+ * } catch (error) {
9241
+ * // `failed`: the player's settings file could not be written.
9242
+ * await plugin.osd.show("Not saved ({})", [(error as PluginError).code]);
9243
+ * }
9244
+ * ```
9245
+ *
9246
+ * @since 1.5.35
9247
+ */
9248
+ setPreference(preference: RenderingPreference): Promise<RenderingState>;
9249
+ /**
9250
+ * Restart the player so that a changed choice applies now. Resolves once
9251
+ * the player has accepted; the player then quits normally, which stops
9252
+ * every plugin (this one included), and starts again with the same
9253
+ * plugins. A local file that was open continues at its position, paused
9254
+ * or playing as it was; a stream or another source that is not a file is
9255
+ * not reopened.
9256
+ *
9257
+ * @capability rendering.manage
9258
+ * @throws `unsupported` (rejects) The player runs without a user
9259
+ * interface; also a player before 0.5.13.
9260
+ * @throws `busy` (rejects) A restart is already under way, or this player
9261
+ * was itself started by a restart less than 30 seconds ago (so a plugin
9262
+ * that restarts on activation cannot make a loop).
9263
+ * @throws `failed` (rejects) The new player process could not be started;
9264
+ * the player keeps running.
9265
+ * @throws `capability` (rejects) The manifest does not declare
9266
+ * `rendering.manage`.
9267
+ *
9268
+ * @example Restart after asking the user
9269
+ * After the user changed the renderer, the plugin offers to restart now.
9270
+ * `confirm` asks first, because a restart closes every window (it needs
9271
+ * `ui.dialog`); once `restartPlayer` resolves, the player is quitting and
9272
+ * will stop this plugin, so nothing else is started after it.
9273
+ * ```ts
9274
+ * const { confirmed } = await plugin.dialogs.confirm({
9275
+ * message: "Restart the player now?",
9276
+ * detail: "The new renderer is used from the restart on. The video continues where it is.",
9277
+ * confirm: "Restart",
9278
+ * });
9279
+ * if (confirmed)
9280
+ * await plugin.rendering.restartPlayer().catch((error: PluginError) => {
9281
+ * // `failed`: the new player could not start; this one keeps running.
9282
+ * void plugin.osd.show("Could not restart ({})", [error.code]);
9283
+ * });
9284
+ * ```
9285
+ *
9286
+ * @since 1.5.35
9287
+ */
9288
+ restartPlayer(): Promise<void>;
9289
+ }
package/dist/wire.d.ts CHANGED
@@ -315,6 +315,26 @@ export interface WireSystemInfo {
315
315
  }[];
316
316
  /** Player 0.5.12; older players leave it out. */
317
317
  rendering?: "gpu" | "software";
318
+ /** Why `rendering` is what it is (player 0.5.13; older players leave it out). */
319
+ renderingReason?: WireRenderingReason;
320
+ }
321
+ /** A rendering reason: a stable kebab-case `code` and English text. */
322
+ export interface WireRenderingReason {
323
+ code: string;
324
+ message: string;
325
+ }
326
+ /** `rendering.state` and `rendering.setPreference` (player 0.5.13). */
327
+ export interface WireRenderingState {
328
+ mode: "gpu" | "software";
329
+ reason: WireRenderingReason;
330
+ preference: "auto" | "gpu" | "software";
331
+ nextMode: "gpu" | "software";
332
+ nextReason: WireRenderingReason;
333
+ restartPending: boolean;
334
+ gpu: {
335
+ available: boolean;
336
+ reason: WireRenderingReason | null;
337
+ };
318
338
  }
319
339
  /**
320
340
  * Template arguments on the wire (`osd.show`, `osd.panel`, `dialogs.confirm`,
@@ -527,6 +547,15 @@ export declare function createWire(transport: Transport): {
527
547
  app: {
528
548
  system: () => Promise<WireSystemInfo>;
529
549
  };
550
+ /**
551
+ * The rendering choice (player 0.5.13): `state` needs no capability,
552
+ * `setPreference` and `restart` need `rendering.manage`.
553
+ */
554
+ rendering: {
555
+ state: () => Promise<WireRenderingState>;
556
+ setPreference: (preference: string) => Promise<WireRenderingState>;
557
+ restart: () => Promise<null>;
558
+ };
530
559
  power: {
531
560
  preventSleep: (mode: "off" | "playing") => Promise<void>;
532
561
  };
package/dist/wire.js CHANGED
@@ -214,6 +214,17 @@ export function createWire(transport) {
214
214
  app: {
215
215
  system: () => transport.request("app.system"),
216
216
  },
217
+ /**
218
+ * The rendering choice (player 0.5.13): `state` needs no capability,
219
+ * `setPreference` and `restart` need `rendering.manage`.
220
+ */
221
+ rendering: {
222
+ state: () => transport.request("rendering.state"),
223
+ setPreference: (preference) => transport.request("rendering.setPreference", {
224
+ preference,
225
+ }),
226
+ restart: () => transport.request("rendering.restart"),
227
+ },
217
228
  power: {
218
229
  preventSleep: (mode) => transport.request("power.preventSleep", { mode }),
219
230
  },