@vestra-player/sdk 1.5.36 → 1.5.38

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/dist/wire.js CHANGED
@@ -138,6 +138,11 @@ export function createWire(transport) {
138
138
  /** The GPU renderer's options (`plugin.video.render()`). */
139
139
  renderSet: (values) => transport.request("video.render.set", { values }),
140
140
  renderRelease: () => transport.request("video.render.release"),
141
+ /** The GPU's video upscaler (`plugin.video.upscaling`, player 0.5.16). */
142
+ upscalingState: () => transport.request("video.upscaling.state"),
143
+ upscalingSet: (enabled) => transport.request("video.upscaling.set", {
144
+ enabled,
145
+ }),
141
146
  },
142
147
  /** Mark layers on the seek bar (`plugin.timeline`). */
143
148
  timeline: {
@@ -250,14 +255,18 @@ export function createWire(transport) {
250
255
  set: (options) => transport.request("layout.set", options),
251
256
  },
252
257
  events: {
253
- /** One `events.subscribe` per name however many listeners. */
254
- async subscribe(event, listener) {
258
+ /**
259
+ * One `events.subscribe` per name however many listeners. `mirror`:
260
+ * only the SDK keeps a copy of the state (player 0.5.18; older
261
+ * players ignore it); `listen` changes it.
262
+ */
263
+ async subscribe(event, listener, mirror = false) {
255
264
  const off = transport.on(event, listener);
256
265
  let subscription = subscriptions.get(event);
257
266
  if (!subscription) {
258
267
  subscription = {
259
268
  count: 0,
260
- ready: transport.request("events.subscribe", { event }),
269
+ ready: transport.request("events.subscribe", mirror ? { event, mirror: true } : { event }),
261
270
  };
262
271
  subscriptions.set(event, subscription);
263
272
  }
@@ -282,6 +291,13 @@ export function createWire(transport) {
282
291
  }
283
292
  };
284
293
  },
294
+ /**
295
+ * Tell the player whether a subscribed event has a real listener
296
+ * (`active`) or only the SDK's copy of the state: `events.subscribe`
297
+ * again, which replaces the mark (player 0.5.18; older players keep
298
+ * one subscription per name and ignore the mark).
299
+ */
300
+ listen: (event, active) => transport.request("events.subscribe", active ? { event } : { event, mirror: true }),
285
301
  },
286
302
  ui: {
287
303
  /** Player 0.2.0: the remote images a Markdown document shows. */
package/llms-full.txt CHANGED
@@ -1,4 +1,4 @@
1
- # @vestra-player/sdk 1.5.36: full reference
1
+ # @vestra-player/sdk 1.5.38: 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
 
@@ -329,11 +329,11 @@ The rule that follows: **state you change outside a handler, or after an `await`
329
329
 
330
330
  #### Stopping
331
331
 
332
- The player stops a plugin when the user disables or uninstalls it, when it is reloaded or upgraded, and when the player quits.
332
+ The player stops a plugin when the user disables or uninstalls it, when it is reloaded or upgraded, when the player quits, and, with lazy activation, when nothing has used it for a while (Idle plugins).
333
333
 
334
334
  1. The player first removes everything the plugin owns on its side: menu items, windows, panels, settings pages, filter chains, subtitle layers, snapshots, export jobs, resolvers, generated streams.
335
335
  2. The SDK then refuses new requests, fails pending ones with `code: "disabled"` and disposes the root scope. That disposal only cleans up locally (timers, abort signals, sockets): the player-side resources are already gone.
336
- 3. A process that has not exited 300 ms later is killed.
336
+ 3. A process that has not exited 300 ms later is killed (2 s later when it stops because it is idle).
337
337
 
338
338
  So `onDispose` cleanup must be quick and must not rely on the player. Media the plugin opened with player.open **keeps playing** after it stops.
339
339
 
@@ -344,7 +344,7 @@ A plugin's process does not live as long as the player. Within one player sessio
344
344
  - the plugin is **reloaded** (from the plugin manager, or a development load with `--plugin`) or **upgraded**;
345
345
  - it **crashed**, or failed to start, and is started again.
346
346
 
347
- A later player may restart a plugin in more cases: restarting plugins that share one process together when one of them faults or hangs, or stopping a plugin nobody uses and starting it on its next use. Write the plugin for those too.
347
+ A later player may restart a plugin in more cases: restarting plugins that share one process together when one of them faults or hangs, or stopping a plugin nobody uses and starting it on its next use (Idle plugins). Write the plugin for those too.
348
348
 
349
349
  Everything in the process is gone after such a restart: variables, timers, caches, open sockets. The menus, windows and panels come back because `activate` creates them again. **Keep state that must survive a restart in session storage (or persistent storage):**
350
350
 
@@ -358,6 +358,16 @@ Session storage is for state that belongs to this run of the player: a queue the
358
358
 
359
359
  Write `activate` so that running it twice in one session is normal: read what you kept, then build the interface from it.
360
360
 
361
+ #### Idle plugins are stopped
362
+
363
+ With lazy activation (player 0.5.18 and later, while the player turns it on), the player works like a browser's extension service workers (Chrome Manifest V3): a plugin that holds nothing only its process can serve is stopped after about a minute without use, and started again by its next use.
364
+
365
+ - **What stays while it is stopped**: its menu items and keys (with the last `enabled`, `checked` and `reason` it set), its settings pages, its file and link resolvers and its drop targets. Using one of them, or showing one of its settings pages, starts the plugin; it runs `activate`, registers them again, and then receives the click, the open or the drop. The player also remembers these between runs, so a plugin nobody uses is not started at launch at all.
366
+ - **What keeps it running**: anything the player must deliver to it or that is its running work: a listener (`player.onChange`, `window.onChange`, `bindings.onChange`, `packages.onChange` and the other `onChange`, `onEnd`), a command state, panel or window binding that reads `plugin.player.state` or another state, an open window, a mounted panel, an OSD panel, a timeline layer, a filter chain, a stream or subtitle track it added, a next/previous provider, a sleep lease, a window setting, a job or snapshot in progress. Reading `plugin.player.state` inside a handler does not: the SDK reads the state again when the plugin starts.
367
+ - **What does not keep it running**: work the player does not see. A `fetch`, a socket, a `setTimeout` or `setInterval`, or a promise the plugin awaits is lost when an idle plugin is stopped, as in Chrome. Set the command's `busy` while its work runs (a busy command keeps the plugin running), or keep what it needs in session storage and pick it up in `activate`.
368
+
369
+ `TestHost.listening` lists the events a plugin really listens to; empty means listeners do not keep it running.
370
+
361
371
  #### The data directory
362
372
 
363
373
  plugin.dataDirectory is a writable directory for the plugin's own files (settings, caches, remembered folders). It needs no capability; the player creates it before it starts the plugin.
@@ -743,6 +753,14 @@ plugin.video.render() returns the RenderOptions of the GPU renderer: HDR to SDR
743
753
  - **Only the GPU renderer has them.** Under software rendering every `set` fails with `unsupported`; see Reference: platforms for where the player renders on the GPU. Treat `unsupported` as "hide these settings", not as an error. To know before creating the handle, read rendering from `plugin.app.system()` (player 0.5.12, no capability): `"gpu"` or `"software"`.
744
754
  - A shader that does not compile is reported later: the status first becomes `applied`, then `failed` with code `failed` and the compiler's first error line once mpv first uses the shader.
745
755
 
756
+ #### The GPU's video upscaler
757
+
758
+ plugin.video.upscaling (capability `video.upscaling`, player 0.5.16) turns on the GPU's AI upscaler, NVIDIA RTX Video Super Resolution on Windows. It is not a handle: `set(true)` is the plugin's request, and the player keeps the upscaler last in the filter chain, after every plugin's chain, while any plugin has it on and it can work, scaled to the area the video is shown in. Like render options it acts at display time only.
759
+
760
+ - **Ask when needed.** The player checks the GPU at the first `state()` or `set(true)` and keeps the answer; the check wakes a sleeping discrete GPU, so ask on the first pick of the item, not while the plugin starts. `set(false)` never checks.
761
+ - **It needs GPU rendering on the upscaler's GPU, with the video decoded there.** A filter chain that needs the frames in memory switches decoding to copy-back, and the upscaler waits (`decoding`) until it is gone.
762
+ - **No sign that it runs.** `requested` says the player asked the driver; on Windows the user must also turn on Super Resolution in the NVIDIA app, which no program can read.
763
+
746
764
  ## Concept: Windows, panels, settings pages, dialogs and the OSD
747
765
 
748
766
  A plugin can show its own user interface in five places. Three of them are drawn by the plugin's own Slint files: **tool windows**, **panels** and **settings pages**. Two are drawn by the player or the system from plain values: **dialogs** and the **on-screen display (OSD)**. This page explains what each one is, how to choose, how each one lives and dies, how Slint and TypeScript talk to each other, and what to watch out for.
@@ -2690,6 +2708,7 @@ The table lists the API whose documentation names a player version, newest last:
2690
2708
  | 0.5.10 | CommandButton, CommandOptions.button |
2691
2709
  | 0.5.12 | SystemInfo.rendering |
2692
2710
  | 0.5.13 | SystemInfo.renderingReason, RenderingMode, RenderingPreference, RenderingReasonCode, RenderingReason, RenderingState, Rendering |
2711
+ | 0.5.16 | UpscalingReasonCode, UpscalingReason, UpscalingState, VideoUpscaling |
2693
2712
 
2694
2713
  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).
2695
2714
 
@@ -2727,6 +2746,7 @@ A plugin lists what it may do in the manifest's `capabilities` (Manifest). A cal
2727
2746
  | `ui.theme` | Appearance.set, Appearance.setLanguage | — | Change the player's palette | |
2728
2747
  | `secrets` | Secrets.get, Secrets.set, Secrets.delete | — | Store sign-in details in the system keychain | yes |
2729
2748
  | `rendering.manage` | Rendering.setPreference, Rendering.restartPlayer | — | Choose GPU or software rendering and restart the player | yes |
2749
+ | `video.upscaling` | VideoUpscaling.state, VideoUpscaling.set | — | Upscale the video with the GPU's video upscaler | |
2730
2750
 
2731
2751
  **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:
2732
2752
 
@@ -3390,6 +3410,7 @@ A plugin uses the same API on macOS and Windows; the player does the system-spec
3390
3410
  | Rendering | On the GPU (OpenGL), with hardware decoding. | In software, with hardware decoding copied back (`d3d11va-copy` and similar). |
3391
3411
  | 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`. |
3392
3412
  | Render options (RenderOptions) | Available; `peakDetect` has no effect. | `unsupported` (software rendering). |
3413
+ | plugin.video.upscaling (player 0.5.16) | `available` is `false` (`platform`); `set(true)` fails with `unsupported`. | NVIDIA RTX Video Super Resolution: an RTX GPU with driver 530 or newer, GPU rendering on that GPU, the video decoded on it (`d3d11va`); the user must also turn on Super Resolution in the NVIDIA app, which no program can read. |
3393
3414
  | Bindings | `Ctrl` also matches ⌘; `Cmd` is ⌘. | `Cmd` is the Windows logo key; `Cmd+Q` does not quit. |
3394
3415
  | Keys taken first, standard close keys | See Keys the system takes first. | See Keys the system takes first. |
3395
3416
  | Interface font | The system font; CJK falls back per character to the system's fonts (PingFang, Hiragino). | The system font; CJK falls back per character to Windows' CJK fonts in the interface language's order, then the bundled Noto Sans CJK SC (the chain). |
@@ -3868,7 +3889,7 @@ interface Scope extends Disposable {
3868
3889
  /** Playback state and controls (`playback.read`, `events`, `playback.control`). */
3869
3890
  readonly player: Player;
3870
3891
  /**
3871
- * Video processing: this plugin's filter chain, the display layer and the GPU render options. All three need `processing.video` and `playback.read`; the filter chain also needs `events` (without it the SDK never sees the media become ready and the chain stays `waiting`).
3892
+ * Video processing: this plugin's filter chain, the display layer, the GPU render options and the GPU's video upscaler. The first three need `processing.video` and `playback.read`; the filter chain also needs `events` (without it the SDK never sees the media become ready and the chain stays `waiting`). The upscaler needs `video.upscaling` only.
3872
3893
  */
3873
3894
  readonly video: {
3874
3895
  /**
@@ -3889,6 +3910,10 @@ interface Scope extends Disposable {
3889
3910
  * @throws Error without a code (throws) A second call in the same plugin while the first handle is not disposed.
3890
3911
  */
3891
3912
  render(): RenderOptions;
3913
+ /**
3914
+ * The GPU's video upscaler (NVIDIA RTX Video Super Resolution on Windows; needs `video.upscaling`, player 0.5.16): whether it works here and why not, and turning it on or off for this plugin.
3915
+ */
3916
+ readonly upscaling: VideoUpscaling;
3892
3917
  };
3893
3918
  /** Audio processing: this plugin's filter chain and delay (`processing.audio`, `playback.read` and `events`, as for VideoFilter). */
3894
3919
  readonly audio: {
@@ -9405,6 +9430,251 @@ render.onStatus((s) => {
9405
9430
  render.set({ toneMapping: "bt.2390", targetPeak: 250 }); // nits
9406
9431
  ```
9407
9432
 
9433
+ ### UpscalingReasonCode
9434
+
9435
+ Why the GPU's video upscaler does not work, as a stable kebab-case code.
9436
+ A newer player may add codes: compare with the ones you handle and show
9437
+ UpscalingReason.message for the rest. The codes of player 0.5.16,
9438
+ in the order the player checks them (the first that holds is reported):
9439
+
9440
+ | Code | Meaning |
9441
+ | --- | --- |
9442
+ | `platform` | The player has no video upscaler on this system (macOS, Linux). |
9443
+ | `no-gpu` | No GPU that can upscale video (Windows: no NVIDIA GPU). |
9444
+ | `driver` | The graphics driver is too old (Windows: an NVIDIA driver before 530). |
9445
+ | `gpu-unsupported` | The driver does not offer upscaling on this GPU (Windows: not an RTX GPU), or the check failed. |
9446
+ | `rendering` | The player renders in software, or GPU rendering is still starting; upscaling needs GPU rendering (Rendering). |
9447
+ | `adapter` | GPU rendering runs on another GPU than the one that can upscale (a laptop with two GPUs; Windows: choose the high-performance GPU for Vestra in Settings ▸ System ▸ Display ▸ Graphics). |
9448
+ | `decoding` | The video is not decoded on the GPU that renders it: copy-back or software decoding, for example while a VideoFilter needs the frames in memory. |
9449
+ | `unchecked` | The player has not checked the GPU yet. Only in the answer to `set(false)`, which never starts the check; `state()` and `set(true)` always check first. |
9450
+
9451
+ `decoding` and `rendering` while GPU rendering is still starting can pass
9452
+ during the run, so `set(true)` accepts them and keeps the request; every
9453
+ other reason holds until the player restarts, and `set(true)` rejects it.
9454
+ Turning upscaling off is always accepted.
9455
+
9456
+ - @player 0.5.16
9457
+
9458
+ ```ts
9459
+ type UpscalingReasonCode = "platform" | "no-gpu" | "driver" | "gpu-unsupported" | "rendering" | "adapter" | "decoding" | "unchecked" | (string & {});
9460
+ ```
9461
+
9462
+ Example: Advise the user by reason
9463
+
9464
+ The plugin shows its own advice for the reasons the user can act on and
9465
+ the player's message for the rest. `reason.code` is looked up among the
9466
+ codes the plugin knows; an unknown code from a newer player falls
9467
+ through to the message, which names the GPU and what to do.
9468
+ ```ts
9469
+ const { reason } = await plugin.video.upscaling.state();
9470
+ // Advice for the codes this plugin knows; any other code shows the message.
9471
+ const advice: Partial<Record<UpscalingReasonCode, string>> = {
9472
+ driver: "Update the NVIDIA driver to 530 or newer.",
9473
+ rendering: "Turn on Video ▸ GPU Rendering (Experimental) and restart.",
9474
+ };
9475
+ if (reason) console.log(advice[reason.code] ?? reason.message);
9476
+ ```
9477
+
9478
+ ### UpscalingReason
9479
+
9480
+ Why the GPU's video upscaler does not work: a stable
9481
+ UpscalingReasonCode to branch on and a sentence to show, the same
9482
+ shape as RenderingReason.
9483
+
9484
+ - @player 0.5.16
9485
+
9486
+ ```ts
9487
+ interface UpscalingReason {
9488
+ /** The reason as a stable kebab-case code; branch on this. */
9489
+ readonly code: UpscalingReasonCode;
9490
+ /** The reason as English text for people, for example with the GPU's name and what to change; never branch on it. */
9491
+ readonly message: string;
9492
+ }
9493
+ ```
9494
+
9495
+ Example: Grey out a menu item with the player's reason
9496
+
9497
+ The plugin disables its upscaling item and shows why. `message` is the
9498
+ player's English text, naming the GPU and what the user can do (turn on
9499
+ GPU rendering, choose the other GPU, update the driver); `code` is for
9500
+ decisions.
9501
+ ```ts
9502
+ const { available, reason } = await plugin.video.upscaling.state();
9503
+ if (!available && reason) console.log(`${reason.code}: ${reason.message}`);
9504
+ ```
9505
+
9506
+ ### UpscalingState
9507
+
9508
+ What VideoUpscaling.state and VideoUpscaling.set report:
9509
+ whether the upscaler works now and why not, whether this plugin turned
9510
+ it on, and whether the player asked the GPU to upscale.
9511
+
9512
+ `requested` is as far as the player can see. The player puts the
9513
+ upscaler last in the video filter chain, scaling the video to the area it
9514
+ is shown in; the driver gives no sign that it upscales. On Windows the
9515
+ user must also turn on Super Resolution in the NVIDIA app (System ▸ Video
9516
+ ▸ RTX Video Enhancement), which no program can read: say so next to the
9517
+ switch.
9518
+
9519
+ - @player 0.5.16
9520
+
9521
+ ```ts
9522
+ interface UpscalingState {
9523
+ /** Whether the upscaler works now; `false` with UpscalingState.reason. */
9524
+ readonly available: boolean;
9525
+ /** Why it does not work now; `null` while it does. */
9526
+ readonly reason: UpscalingReason | null;
9527
+ /** This plugin asked for upscaling (VideoUpscaling.set) and has not turned it off. */
9528
+ readonly enabled: boolean;
9529
+ /**
9530
+ * The player has the upscaler in the video filter chain now, for this plugin or another one: upscaling is enabled and available and the video is shown larger than its own size. Not a sign that the GPU upscales (see the remarks).
9531
+ */
9532
+ readonly requested: boolean;
9533
+ /**
9534
+ * The scale factor the upscaler was asked for (the area the video is shown in over the video's size, in hundredths, above 1); `null` while nothing is requested.
9535
+ */
9536
+ readonly factor: number | null;
9537
+ }
9538
+ ```
9539
+
9540
+ Example: Label the item with what happens now
9541
+
9542
+ The plugin's menu item says whether upscaling is on and whether the
9543
+ video is being upscaled. `enabled` is this plugin's own request;
9544
+ `requested` is true while the player has the upscaler in the filter
9545
+ chain, which needs a video shown larger than its own size (`factor`
9546
+ above 1).
9547
+ ```ts
9548
+ const state = await plugin.video.upscaling.state();
9549
+ // `enabled`: this plugin's request; `requested`: in the filter chain now.
9550
+ const label = !state.enabled
9551
+ ? "AI Upscaling: Off"
9552
+ : state.requested
9553
+ ? `AI Upscaling: On (×${state.factor?.toFixed(2)})`
9554
+ : "AI Upscaling: On (not needed for this video)";
9555
+ console.log(label);
9556
+ ```
9557
+
9558
+ ### VideoUpscaling
9559
+
9560
+ `plugin.video.upscaling`: the GPU's video upscaler, NVIDIA RTX Video
9561
+ Super Resolution on Windows (`video.upscaling`). Read whether it works
9562
+ here and why not; turn it on or off for this plugin.
9563
+
9564
+ The player checks the GPU once, when a plugin first asks, and keeps the
9565
+ answer: the check wakes a sleeping discrete GPU, so call
9566
+ VideoUpscaling.state when the plugin first needs the answer (the
9567
+ first time the user picks its upscaling item), not while the plugin
9568
+ starts, and do not poll it. `set(false)` never starts the check. It works
9569
+ only with GPU rendering, on the GPU the upscaler runs on, with the video decoded on that GPU; UpscalingReasonCode
9570
+ lists what can stand in the way.
9571
+
9572
+ While some plugin has it on and it works, the player puts the upscaler
9573
+ last in the video filter chain and scales the video to the area it is
9574
+ shown in, following window and video size changes (a resize waits until
9575
+ the size holds). A video already shown at its own size or smaller is not
9576
+ upscaled. The upscaler stays while any plugin has it on; it goes when the
9577
+ last one turns it off or stops: the player revokes a stopped plugin's
9578
+ request.
9579
+
9580
+ **Windows:** the user must turn on Super Resolution in the NVIDIA app
9581
+ (System ▸ Video ▸ RTX Video Enhancement); no program can read that
9582
+ switch, and the driver gives no sign that it upscales
9583
+ (UpscalingState.requested is as far as the player can see). It
9584
+ uses the NVIDIA GPU, so power use and heat go up, and on a laptop with
9585
+ two GPUs the whole player must run on the NVIDIA one.
9586
+ **macOS, Linux:** `available` is `false` with `platform`.
9587
+
9588
+ Players before 0.5.16 do not know `video.upscaling` and do not load a
9589
+ plugin that declares it: declare `"minPlayerVersion": "0.5.16"`.
9590
+
9591
+ - @capability video.upscaling
9592
+ - @player 0.5.16; older players do not load a plugin that declares `video.upscaling`.
9593
+
9594
+ ```ts
9595
+ interface VideoUpscaling {
9596
+ /**
9597
+ * Read whether the upscaler works now and why not, whether this plugin turned it on and what the player asked the GPU for.
9598
+ * @capability video.upscaling
9599
+ * @returns The state, once the player's GPU check ran (the first call starts it and may take a moment).
9600
+ * @throws capability (rejects) The manifest does not declare `video.upscaling`.
9601
+ * @example Find out whether upscaling can work here
9602
+ * The plugin reads the state the first time the user picks its upscaling
9603
+ * item and keeps it for the item. The first call makes the player check the GPU (which
9604
+ * wakes a sleeping discrete GPU) and later calls reuse the answer; the
9605
+ * rest (GPU rendering, the GPU it runs on, the decoding) is read at
9606
+ * each call.
9607
+ * ```ts
9608
+ * const { available, reason } = await plugin.video.upscaling.state();
9609
+ * if (!available) console.log(reason?.message);
9610
+ * ```
9611
+ */
9612
+ state(): Promise<UpscalingState>;
9613
+ /**
9614
+ * Turn upscaling on or off for this plugin.
9615
+ *
9616
+ * On stays this plugin's request until it turns it off or stops; the
9617
+ * player upscales whenever the request can take effect (a `decoding`
9618
+ * reason can pass with the next file). Off takes the upscaler out at
9619
+ * once unless another plugin has it on. Upscaling is a request of the
9620
+ * whole plugin, not of a Scope: disposing a scope does not turn
9621
+ * it off.
9622
+ * @capability video.upscaling
9623
+ * @param enabled - `true` to ask for upscaling, `false` to withdraw the request.
9624
+ * @returns The new state.
9625
+ * @throws capability (rejects) The manifest does not declare `video.upscaling`.
9626
+ * @throws invalid (rejects) `enabled` is not a boolean.
9627
+ * @throws unsupported (rejects) `true` where upscaling cannot work in this run: every UpscalingReasonCode except `decoding`, and except `rendering` while GPU rendering is still starting (those two are accepted and the request is kept until they pass); `message` is the reason. Nothing changes. `false` is always accepted and answered at once without the GPU check (before any check, `available` is `false` with `unchecked`).
9628
+ * @example Turn upscaling on and explain what is needed
9629
+ * The plugin turns upscaling on from its settings and tells the user what
9630
+ * else to switch on. `set(true)` records the request and resolves with the
9631
+ * new state; where upscaling cannot work in this run it rejects with
9632
+ * `unsupported` and the player's reason instead.
9633
+ * ```ts
9634
+ * try {
9635
+ * const state = await plugin.video.upscaling.set(true);
9636
+ * // The driver's own switch cannot be read: remind the user once.
9637
+ * if (state.enabled) await plugin.osd.show("Turn on Super Resolution in the NVIDIA app");
9638
+ * } catch (error) {
9639
+ * if ((error as PluginError).code !== "unsupported") throw error;
9640
+ * await plugin.osd.show("AI upscaling: {}", [(error as PluginError).message]);
9641
+ * }
9642
+ * ```
9643
+ */
9644
+ set(enabled: boolean): Promise<UpscalingState>;
9645
+ }
9646
+ ```
9647
+
9648
+ Example: Offer AI upscaling as a menu item
9649
+
9650
+ A Video menu item turns upscaling on and off; once the player said it
9651
+ cannot work here, the item is greyed out with the player's reason. The
9652
+ state is read the first time the user picks the item (that runs the
9653
+ player's GPU check, so nothing wakes the GPU while the plugin starts) and
9654
+ again after each pick; `set` resolves with the new state, or rejects with
9655
+ `unsupported` where upscaling cannot work in this run.
9656
+ ```ts
9657
+ let state: UpscalingState | null = null; // unknown until first picked
9658
+ plugin.commands.add({
9659
+ id: "upscaling",
9660
+ path: ["@video", "AI Upscaling"],
9661
+ checked: () => state?.enabled ?? false,
9662
+ // Greyed only once the player said it cannot work (`decoding` can pass).
9663
+ enabled: () => state === null || state.available || state.reason?.code === "decoding",
9664
+ reason: () => state?.reason?.message ?? null, // shown while disabled
9665
+ run: async () => {
9666
+ state ??= await plugin.video.upscaling.state(); // the first pick checks the GPU
9667
+ try {
9668
+ state = await plugin.video.upscaling.set(!(state?.enabled ?? false));
9669
+ } catch (error) {
9670
+ // Cannot work in this run (no RTX GPU, software rendering…).
9671
+ if ((error as PluginError).code !== "unsupported") throw error;
9672
+ state = await plugin.video.upscaling.state();
9673
+ }
9674
+ },
9675
+ });
9676
+ ```
9677
+
9408
9678
  ### SubtitleTrack
9409
9679
 
9410
9680
  A subtitle track of the current source.
@@ -10475,6 +10745,10 @@ interface PluginInfo {
10475
10745
  * The manifest's display texts in the interface language, translated with that package's catalog; absent from players before 0.4.3 (read `manifest.json` there).
10476
10746
  */
10477
10747
  display?: PluginDisplay;
10748
+ /**
10749
+ * For `state: "running"`: `"running"` while its process runs, `"idle"` while the player stopped it because nothing used it (lazy activation: it starts again on its next use, its menu items, keys, settings pages, resolvers and drop targets stay). Absent in other states and from players before 0.5.18. A change between the two alone does not fire Packages.onChange.
10750
+ */
10751
+ process?: "running" | "idle";
10478
10752
  }
10479
10753
  ```
10480
10754
 
@@ -12504,7 +12778,8 @@ Members fall into a few groups:
12504
12778
  TestHost.app, TestHost.systemInfo,
12505
12779
  TestHost.chapters, TestHost.audioDevices,
12506
12780
  TestHost.secrets, TestHost.clipboard,
12507
- TestHost.tools, TestHost.rendering.
12781
+ TestHost.tools, TestHost.rendering,
12782
+ TestHost.upscaling.
12508
12783
  - **Assert** on records: TestHost.osd, TestHost.calls,
12509
12784
  TestHost.sent, TestHost.conflicts,
12510
12785
  TestHost.layout, TestHost.menus, and many more.
@@ -12535,6 +12810,10 @@ class TestHost implements Transport {
12535
12810
  readonly calls: Call[];
12536
12811
  /** The host events the plugin subscribed to (needs `events` etc.). */
12537
12812
  readonly subscriptions: Set<string>;
12813
+ /**
12814
+ * The subscribed events the plugin really listens to: an `onChange` and the like, or a binding that reads the state. The SDK's own copies of the state (what keeps `plugin.player.state` current) are left out. Empty means the player may stop the plugin while nothing uses it (unless it holds something else: a window, a panel, a filter…).
12815
+ */
12816
+ readonly listening: Set<string>;
12538
12817
  /**
12539
12818
  * The player's source facts (phase, position, duration, paused, rate, audio tracks, loop, ...). Wire-level shape; change it with TestHost.setSource, which also broadcasts it.
12540
12819
  */
@@ -13407,6 +13686,62 @@ class TestHost implements Transport {
13407
13686
  /** Accepted `restartPlayer` calls. */
13408
13687
  restarts: number;
13409
13688
  };
13689
+ /**
13690
+ * The GPU's video upscaler behind `plugin.video.upscaling` (player
13691
+ * 0.5.16), answered as the player would:
13692
+ *
13693
+ * - `state()` and `set(true)` set `checked` (the player's one GPU check; a
13694
+ * plugin that should not wake the GPU while it starts can assert it is
13695
+ * still `false`) and report `available` from `reason` (`null`: it
13696
+ * works), this plugin's `enabled`, and `requested` with `factor` while
13697
+ * enabled, available and `factor` is above 1.
13698
+ * - `set(true)` refuses with `unsupported` and the reason's message while
13699
+ * `reason` is set, unless its code is `decoding`, or `rendering` with
13700
+ * `renderingStarting` (GPU rendering still starting): those can pass
13701
+ * during the run, so the player keeps the request. Nothing changes when
13702
+ * it refuses.
13703
+ * - `set(false)` always succeeds and does not check the GPU: before any
13704
+ * check it reports `available: false` with the reason `unchecked`.
13705
+ *
13706
+ * Inject other answers with `host.handle("video.upscaling.set", …)`. The
13707
+ * defaults are a player where upscaling works and a 1080p video shown
13708
+ * full screen on a 4K display (`factor` 2), whatever `systemInfo` says.
13709
+ * @example Test the menu item on a computer without an RTX GPU
13710
+ * The plugin's upscaling item is greyed out with the player's reason after
13711
+ * the user first picks it. Starting the plugin checks nothing (`checked`
13712
+ * stays false, so no GPU wakes up); the first pick reads the state, and
13713
+ * `set(true)` would reject with `unsupported`, as in the player.
13714
+ * ```ts
13715
+ * import assert from "node:assert/strict";
13716
+ * import { startPlugin, TestHost } from "@vestra-player/sdk/testing";
13717
+ * import activate from "../main.ts";
13718
+ *
13719
+ * const host = new TestHost();
13720
+ * host.upscaling.reason = { code: "no-gpu", message: "no NVIDIA GPU" };
13721
+ * const { stop } = await startPlugin(activate, {
13722
+ * directory: new URL("..", import.meta.url),
13723
+ * host,
13724
+ * });
13725
+ * assert.equal(host.upscaling.checked, false); // starting woke no GPU
13726
+ * const item = host.command("upscaling");
13727
+ * await item.run(); // the first pick checks the GPU
13728
+ * assert.equal(item.state.enabled, false); // greyed out
13729
+ * assert.equal(item.state.reason, "no NVIDIA GPU"); // the player's message
13730
+ * await stop();
13731
+ * ```
13732
+ */
13733
+ readonly upscaling: {
13734
+ /** Why upscaling does not work; `null` while it does. */
13735
+ reason: UpscalingReason | null;
13736
+ /** Whether this plugin has upscaling on (`set`); a test may preset it. */
13737
+ enabled: boolean;
13738
+ /** The factor the player would request; `null` or at most 1: the video needs no upscaling. */
13739
+ factor: number | null;
13740
+ /** Whether the plugin made the player check the GPU (`state`, `set(true)`). */
13741
+ checked: boolean;
13742
+ /** GPU rendering is still starting: a `rendering` reason then lets `set(true)` keep the request. */
13743
+ renderingStarting: boolean;
13744
+ };
13410
13745
  /** What `player.chapters()` lists while media is ready. */
13411
13746
  chapters: {
13412
13747
  /** The chapter title, `null` without one. */
package/llms.txt CHANGED
@@ -1,6 +1,6 @@
1
1
  # Vestra Player SDK
2
2
 
3
- > @vestra-player/sdk 1.5.36: write plugins for Vestra Player in TypeScript (plugin API `vestra/1`). A plugin is a folder with `manifest.json` and an entry module whose default export `activate(plugin)` the player calls; it runs in its own Node.js process. Everything a plugin creates (commands, windows, panels, filters, listeners…) is a handle that is released automatically when the plugin stops. L<n>: a line of llms-full.txt (next to this file), which has every page, declaration and example.
3
+ > @vestra-player/sdk 1.5.38: write plugins for Vestra Player in TypeScript (plugin API `vestra/1`). A plugin is a folder with `manifest.json` and an entry module whose default export `activate(plugin)` the player calls; it runs in its own Node.js process. Everything a plugin creates (commands, windows, panels, filters, listeners…) is a handle that is released automatically when the plugin stops. L<n>: a line of llms-full.txt (next to this file), which has every page, declaration and example.
4
4
 
5
5
  **Rules**
6
6
 
@@ -33,7 +33,7 @@ my-plugin/
33
33
  }
34
34
  ```
35
35
 
36
- Fields: `id`, `version`, `apiVersion`, `entry`, `capabilities`, `name`, `author`, `punchline`, `description`, `tags`, `icon`, `readme`, `changelog`, `sourceUrl`, `license`, `minPlayerVersion`; unknown fields and capabilities are rejected. Rules and limits: L2628–2697.
36
+ Fields: `id`, `version`, `apiVersion`, `entry`, `capabilities`, `name`, `author`, `punchline`, `description`, `tags`, `icon`, `readme`, `changelog`, `sourceUrl`, `license`, `minPlayerVersion`; unknown fields and capabilities are rejected. Rules and limits: L2646–2716.
37
37
 
38
38
  **main.ts**
39
39
 
@@ -77,9 +77,10 @@ export default async function activate(plugin: Plugin) {
77
77
  - `ui.theme`: Appearance.set/setLanguage
78
78
  - `secrets`: Secrets.get/set/delete
79
79
  - `rendering.manage`: Rendering.setPreference/restartPlayer
80
- - No capability: `tools`, `storage`, `system`, `app`, `ui` (images and Markdown), `i18n`, reading window.state, appearance.theme, packages.list and rendering.state. Details: L2698–2757.
80
+ - `video.upscaling`: VideoUpscaling.state/set
81
+ - No capability: `tools`, `storage`, `system`, `app`, `ui` (images and Markdown), `i18n`, reading window.state, appearance.theme, packages.list and rendering.state. Details: L2717–2777.
81
82
 
82
- **Test**: `tests/*.test.ts` with `node:test`, run by `vestra-plugin test`. `startPlugin(activate, { directory: new URL("..", import.meta.url) })` from `@vestra-player/sdk/testing` returns `{ host, stop }`, `host` an in-memory player (TestHost): act (`host.command(id).run()`, `host.press(key)`), `await host.settle()`, assert on `host.osd`, `host.window(id).props`… Guide: L2069–2257; TestHost: L12484.
83
+ **Test**: `tests/*.test.ts` with `node:test`, run by `vestra-plugin test`. `startPlugin(activate, { directory: new URL("..", import.meta.url) })` from `@vestra-player/sdk/testing` returns `{ host, stop }`, `host` an in-memory player (TestHost): act (`host.command(id).run()`, `host.press(key)`), `await host.settle()`, assert on `host.osd`, `host.window(id).props`… Guide: L2087–2275; TestHost: L12758.
83
84
 
84
85
  **Command line**
85
86
 
@@ -107,39 +108,39 @@ export default async function activate(plugin: Plugin) {
107
108
  - `limit`: A player-wide limit was reached, a property value is over the limits of PropValue
108
109
  - `reserved`: The id is `vestra` or starts with `vestra.`
109
110
  - `versionMismatch`: The package needs a newer player
110
- - Others: PluginError, llms-full.txt L3759.
111
+ - Others: PluginError, llms-full.txt L3780.
111
112
 
112
113
  ## Reference
113
114
 
114
115
  - [llms-full.txt](https://vestrasys.github.io/vestra-player-sdk/llms-full.txt), also node_modules/@vestra-player/sdk/llms-full.txt; pages: [site](https://vestrasys.github.io/vestra-player-sdk/).
115
116
  - Tutorials: Getting started L9–237
116
- - Concepts: How a plugin runs L238–408; Commands, menus and key bindings L409–520; Playback state, controls and sources L521–666; Video and audio processing L667–745; Windows, panels, settings pages, dialogs and the OSD L746–1034
117
- - How-to: Control playback L1035–1163; Store a plugin's data L1164–1231; Load subtitles from a plugin L1232–1310; Keep a slider responsive while dragging L1311–1485; Remember a window's state between runs L1486–1615; Offer a GPU rendering switch L1616–1736; Translate a plugin L1737–1920; Migrate a plugin to English source text L1921–2068; Test a plugin L2069–2257; Publish a plugin L2258–2346; Migrating off package rollback (1.5.16, removed in 1.5.17) L2347–2422
118
- - Guidelines: Interface guidelines L2423–2530; Menu guidelines L2531–2627
119
- - Reference: Manifest L2628–2697; Capabilities L2698–2757; Limits L2758–2887; Commands, menus and bindings L2888–3371; Platforms L3372–3401; Theme L3402–3623; Command line L3624–3681
120
- - [API: @vestra-player/sdk](https://vestrasys.github.io/vestra-player-sdk/modules/_vestra-player_sdk.html): L3682–12168
121
- - [API: @vestra-player/sdk/testing](https://vestrasys.github.io/vestra-player-sdk/modules/_vestra-player_sdk_testing.html): L12169–14827
117
+ - Concepts: How a plugin runs L238–418; Commands, menus and key bindings L419–530; Playback state, controls and sources L531–676; Video and audio processing L677–763; Windows, panels, settings pages, dialogs and the OSD L764–1052
118
+ - How-to: Control playback L1053–1181; Store a plugin's data L1182–1249; Load subtitles from a plugin L1250–1328; Keep a slider responsive while dragging L1329–1503; Remember a window's state between runs L1504–1633; Offer a GPU rendering switch L1634–1754; Translate a plugin L1755–1938; Migrate a plugin to English source text L1939–2086; Test a plugin L2087–2275; Publish a plugin L2276–2364; Migrating off package rollback (1.5.16, removed in 1.5.17) L2365–2440
119
+ - Guidelines: Interface guidelines L2441–2548; Menu guidelines L2549–2645
120
+ - Reference: Manifest L2646–2716; Capabilities L2717–2777; Limits L2778–2907; Commands, menus and bindings L2908–3391; Platforms L3392–3422; Theme L3423–3644; Command line L3645–3702
121
+ - [API: @vestra-player/sdk](https://vestrasys.github.io/vestra-player-sdk/modules/_vestra-player_sdk.html): L3703–12442
122
+ - [API: @vestra-player/sdk/testing](https://vestrasys.github.io/vestra-player-sdk/modules/_vestra-player_sdk_testing.html): L12443–15162
122
123
 
123
124
  ## API
124
125
 
125
126
  Where each `plugin.<module>` is declared:
126
127
 
127
- - Plugin L4081, Scope L3834, PluginError L3759, Disposable L3697
128
- - Commands L4478; Windows L5415; Panels L6861; Dialogs L7529; Player L8270; video: VideoFilter L9150, VideoView L9250, RenderOptions L9337; audio: AudioFilter L9195; Subtitles L9484; media: MediaCapture L9888; Resolvers L10118; Packages L10516; Osd L9015; Host L8857; Clipboard L10844; Power L10931; Tools L10990; Storage L11142; System L11341; Secrets L11421; window: MainWindow L7178; App L7358; Rendering L11684; Bindings L10744; Ui L4767; Timeline L7090; Drops L6108; Appearance L6189; I18n L6422; Settings L6705.
128
+ - Plugin L4106, Scope L3855, PluginError L3780, Disposable L3718
129
+ - Commands L4503; Windows L5440; Panels L6886; Dialogs L7554; Player L8295; video: VideoFilter L9175, VideoView L9275, RenderOptions L9362, VideoUpscaling L9558; audio: AudioFilter L9220; Subtitles L9754; media: MediaCapture L10158; Resolvers L10388; Packages L10790; Osd L9040; Host L8882; Clipboard L11118; Power L11205; Tools L11264; Storage L11416; System L11615; Secrets L11695; window: MainWindow L7203; App L7383; Rendering L11958; Bindings L11018; Ui L4792; Timeline L7115; Drops L6133; Appearance L6214; I18n L6447; Settings L6730.
129
130
 
130
131
  ## Examples
131
132
 
132
133
  Tested plugins in [examples/](https://github.com/vestrasys/vestra-player-sdk/tree/main/examples):
133
134
 
134
- - hello: The README Quick start: one menu command and an on-screen message. L15262–15351
135
- - blur: A tool window with one slider that blurs the playing video. L15352–15613
136
- - signal-generator: Generates its own video and audio: colour bars and a 1 kHz tone. L15614–15946
137
- - bookmark: A tool window bound to the playback state: remember the position and jump back to it. L15947–16133
138
- - panel-menu: A sidebar panel with its own right-click menu. L16134–16342
139
- - menus: Radio menu items, separator groups and a menu item whose title changes. L16343–16502
140
- - theme-widgets: A tool window drawn with the player's theme and widgets. L16503–16666
141
- - markdown: A window that shows a Markdown document. L16667–16863
142
- - i18n: A plugin in the player's language: English source text, a Chinese translation. L16864–17031
135
+ - hello: The README Quick start: one menu command and an on-screen message. L15597–15686
136
+ - blur: A tool window with one slider that blurs the playing video. L15687–15948
137
+ - signal-generator: Generates its own video and audio: colour bars and a 1 kHz tone. L15949–16281
138
+ - bookmark: A tool window bound to the playback state: remember the position and jump back to it. L16282–16468
139
+ - panel-menu: A sidebar panel with its own right-click menu. L16469–16677
140
+ - menus: Radio menu items, separator groups and a menu item whose title changes. L16678–16837
141
+ - theme-widgets: A tool window drawn with the player's theme and widgets. L16838–17001
142
+ - markdown: A window that shows a Markdown document. L17002–17198
143
+ - i18n: A plugin in the player's language: English source text, a Chinese translation. L17199–17366
143
144
 
144
145
  ## Optional
145
146
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vestra-player/sdk",
3
- "version": "1.5.36",
3
+ "version": "1.5.38",
4
4
  "description": "Plugin API types, test host and plugin CLI for Vestra Player (apiVersion vestra/1)",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {