@vestra-player/sdk 1.5.33 → 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.
- package/CHANGELOG.md +38 -18
- package/dist/api-requests.js +3 -0
- package/dist/capabilities.d.ts +2 -2
- package/dist/capabilities.js +2 -2
- package/dist/host-capabilities.js +12 -0
- package/dist/player-runtime.d.ts +1 -0
- package/dist/player-runtime.js +1 -0
- package/dist/plugin/scope.d.ts +1 -0
- package/dist/plugin/scope.js +2 -1
- package/dist/plugin/services.d.ts +2 -1
- package/dist/plugin/services.js +22 -2
- package/dist/plugin/subtitles.d.ts +59 -0
- package/dist/plugin/subtitles.js +70 -4
- package/dist/plugin/testing.d.ts +93 -6
- package/dist/plugin/testing.js +210 -10
- package/dist/plugin/types.d.ts +408 -6
- package/dist/wire.d.ts +32 -1
- package/dist/wire.js +11 -0
- package/llms-full.txt +642 -19
- package/llms.txt +22 -21
- package/package.json +1 -1
package/llms-full.txt
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# @vestra-player/sdk 1.5.
|
|
1
|
+
# @vestra-player/sdk 1.5.35: 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
|
|
|
@@ -740,7 +740,7 @@ plugin.video.view() returns the VideoView: aspect override, fit (keep with bars,
|
|
|
740
740
|
|
|
741
741
|
plugin.video.render() returns the RenderOptions of the GPU renderer: HDR to SDR tone mapping and the display's brightness, user shaders (`.glsl` files) and motion interpolation. Like the display layer they act at display time only, have one owner (independent of the display layer's owner) and merge writes.
|
|
742
742
|
|
|
743
|
-
- **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.
|
|
743
|
+
- **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
744
|
- 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
745
|
|
|
746
746
|
## Concept: Windows, panels, settings pages, dialogs and the OSD
|
|
@@ -1272,7 +1272,7 @@ if (english)
|
|
|
1272
1272
|
|
|
1273
1273
|
#### Upload a file
|
|
1274
1274
|
|
|
1275
|
-
`load` takes the file's bytes: UTF-8 SRT, or ASS/SSA with `format: "ass"`, at most 2 MiB (SRT up to 10,000 cues, ASS up to 20,000 `Dialogue:` lines). Convert other formats (WebVTT, SAMI) to SRT first. The player adds an `external` track to the current source only, selects it unless `select: "none"`, and resolves with the track once the upload is committed.
|
|
1275
|
+
`load` takes the file's bytes: UTF-8 SRT, or ASS/SSA with `format: "ass"`, at most 2 MiB (SRT up to 10,000 cues, ASS up to 20,000 `Dialogue:` lines). Convert other formats (WebVTT, SAMI) to SRT first. The player adds an `external` track to the current source only, selects it unless `select: "none"`, and resolves with the track once the upload is committed. A plugin has at most 4 such tracks on a source at once and loads at most 16 files into it (removed ones count); past that `load` fails with `busy` (Limits).
|
|
1276
1276
|
|
|
1277
1277
|
```ts
|
|
1278
1278
|
import { readFile } from "node:fs/promises";
|
|
@@ -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
|
|
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
|
|
|
@@ -2567,6 +2688,8 @@ The table lists the API whose documentation names a player version, newest last:
|
|
|
2567
2688
|
| 0.5.3 | Scrub.seek |
|
|
2568
2689
|
| 0.5.5 | SessionStorage |
|
|
2569
2690
|
| 0.5.10 | CommandButton, CommandOptions.button |
|
|
2691
|
+
| 0.5.12 | SystemInfo.rendering |
|
|
2692
|
+
| 0.5.13 | SystemInfo.renderingReason, RenderingMode, RenderingPreference, RenderingReasonCode, RenderingReason, RenderingState, Rendering |
|
|
2570
2693
|
|
|
2571
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).
|
|
2572
2695
|
|
|
@@ -2603,6 +2726,7 @@ A plugin lists what it may do in the manifest's `capabilities` (Manifest). A cal
|
|
|
2603
2726
|
| `ui.settings` | Settings.page, Settings.show | — | Add pages to the Options window | |
|
|
2604
2727
|
| `ui.theme` | Appearance.set, Appearance.setLanguage | — | Change the player's palette | |
|
|
2605
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 |
|
|
2606
2730
|
|
|
2607
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:
|
|
2608
2732
|
|
|
@@ -2614,7 +2738,7 @@ A plugin lists what it may do in the manifest's `capabilities` (Manifest). A cal
|
|
|
2614
2738
|
|
|
2615
2739
|
#### Without a capability
|
|
2616
2740
|
|
|
2617
|
-
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
|
|
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).
|
|
2618
2742
|
|
|
2619
2743
|
#### Catching a missing capability
|
|
2620
2744
|
|
|
@@ -2724,6 +2848,21 @@ plugin.storage.session (player 0.5.5). A key out of its rule, or a value that is
|
|
|
2724
2848
|
| `SESSION_STORAGE_LIMITS.bytes` | 1048576 (1 MiB) | Bytes one plugin may hold at the same time: for every key, its UTF-8 bytes, its value written as compact JSON (`JSON.stringify(value)`) and `entryBytes`. |
|
|
2725
2849
|
| `SESSION_STORAGE_LIMITS.entryBytes` | 128 | What each key costs on top of its text and value (128): the player's own bookkeeping, so that many tiny entries stay within `bytes` of memory. |
|
|
2726
2850
|
|
|
2851
|
+
#### Subtitle files
|
|
2852
|
+
|
|
2853
|
+
Subtitles.load uploads a file as a track of the current source. A file over `fileBytes` fails with `invalid`; every other limit fails with `busy` and nothing is loaded. The counts are per source, so another file starts from zero. Removing a track frees its place among the tracks and their bytes, but a load stays counted until the source changes.
|
|
2854
|
+
|
|
2855
|
+
| Limit | Value | Meaning |
|
|
2856
|
+
| --- | --- | --- |
|
|
2857
|
+
| `SUBTITLE_LIMITS.fileBytes` | 2097152 (2 MiB) | UTF-8 bytes of one file; a larger one fails with `invalid`. |
|
|
2858
|
+
| `SUBTITLE_LIMITS.tracksPerPlugin` | 4 | Tracks one plugin has loaded into the current source at the same time. |
|
|
2859
|
+
| `SUBTITLE_LIMITS.tracks` | 16 | Loaded tracks of every plugin in the current source at the same time. |
|
|
2860
|
+
| `SUBTITLE_LIMITS.totalBytes` | 16777216 (16 MiB) | Bytes of the loaded tracks of every plugin in the current source together. |
|
|
2861
|
+
| `SUBTITLE_LIMITS.loadsPerPlugin` | 16 | Loads by one plugin into the current source, removed tracks included. |
|
|
2862
|
+
| `SUBTITLE_LIMITS.loads` | 128 | Loads by every plugin into the current source, removed tracks included. |
|
|
2863
|
+
| `SUBTITLE_LIMITS.uploadsPerPlugin` | 2 | Loads of one plugin in progress at the same time. |
|
|
2864
|
+
| `SUBTITLE_LIMITS.uploads` | 8 | Loads of every plugin in progress at the same time. |
|
|
2865
|
+
|
|
2727
2866
|
#### Messages
|
|
2728
2867
|
|
|
2729
2868
|
Every request a plugin sends travels as one protocol line. The plugin's side refuses a longer one with code `limit` before sending it; property values meet their own limits first.
|
|
@@ -3244,10 +3383,11 @@ A plugin uses the same API on macOS and Windows; the player does the system-spec
|
|
|
3244
3383
|
| DropModifiers | The pointer and ⌥ ⇧ ⌘ (`cmd` is ⌘). | The pointer and Alt, Shift, Ctrl (`cmd` is Ctrl). |
|
|
3245
3384
|
| plugin.system (open, reveal) | Opened by the system; revealing several files selects all of them in one Finder window. | Opened by the shell; one Explorer window per folder. |
|
|
3246
3385
|
| plugin.secrets | The login keychain. | The Credential Manager. |
|
|
3247
|
-
| App.system() | Version, build, model and displays. | Version, build, model and displays
|
|
3386
|
+
| App.system() | Version, build, model and displays; `rendering` is `"gpu"`, or `"software"` after a fallback (a window that cannot show GPU frames). | Version, build, model and displays; `rendering` is `"software"`. |
|
|
3248
3387
|
| Snapshots and exports (MediaCapture) | Available. | Available; needs FFmpeg 6.1 or newer. |
|
|
3249
3388
|
| Generated streams (Player.openStream) | A Unix socket. | Loopback TCP with a token. |
|
|
3250
3389
|
| Rendering | On the GPU (OpenGL), with hardware decoding. | In software, with hardware decoding copied back (`d3d11va-copy` and similar). |
|
|
3390
|
+
| 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`. |
|
|
3251
3391
|
| Render options (RenderOptions) | Available; `peakDetect` has no effect. | `unsupported` (software rendering). |
|
|
3252
3392
|
| Bindings | `Ctrl` also matches ⌘; `Cmd` is ⌘. | `Cmd` is the Windows logo key; `Cmd+Q` does not quit. |
|
|
3253
3393
|
| Keys taken first, standard close keys | See Keys the system takes first. | See Keys the system takes first. |
|
|
@@ -3786,6 +3926,8 @@ interface Scope extends Disposable {
|
|
|
3786
3926
|
readonly window: MainWindow;
|
|
3787
3927
|
/** The player's version and components, and the system it runs on. */
|
|
3788
3928
|
readonly app: App;
|
|
3929
|
+
/** GPU or software rendering: read the choice, change it (`rendering.manage`). */
|
|
3930
|
+
readonly rendering: Rendering;
|
|
3789
3931
|
/** Every command's bindings and the user's overrides (`bindings.manage`). */
|
|
3790
3932
|
readonly bindings: Bindings;
|
|
3791
3933
|
/** Runtime images for Slint `image` properties. */
|
|
@@ -7227,7 +7369,7 @@ worked out once, at the first call, and reused.
|
|
|
7227
7369
|
```ts
|
|
7228
7370
|
interface App extends AppInfo {
|
|
7229
7371
|
/**
|
|
7230
|
-
* The operating system and the
|
|
7372
|
+
* The operating system, the displays and the renderer, read when called (displays come and go; the renderer can fall back from GPU to software once). No capability. CPU and memory: Node's `os` module.
|
|
7231
7373
|
* @capability None.
|
|
7232
7374
|
* @throws busy (rejects) The player could not start the request (no thread available).
|
|
7233
7375
|
* @example Choose a size for the primary display
|
|
@@ -7276,6 +7418,63 @@ interface SystemInfo {
|
|
|
7276
7418
|
readonly arch: string;
|
|
7277
7419
|
/** Primary display first; empty without a user interface. */
|
|
7278
7420
|
readonly displays: readonly DisplayInfo[];
|
|
7421
|
+
/**
|
|
7422
|
+
* Which renderer draws the video now: `"gpu"` (the GPU renderer, so
|
|
7423
|
+
* render options apply) or `"software"` (they
|
|
7424
|
+
* fail with `unsupported`). Read at each call. It changes at most once
|
|
7425
|
+
* while the player runs, from `"gpu"` to `"software"` when a window
|
|
7426
|
+
* cannot show GPU frames; the owner of the render options hears that
|
|
7427
|
+
* switch through RenderOptions.onStatus.
|
|
7428
|
+
*
|
|
7429
|
+
* Absent (`undefined`) on players before 0.5.12, which do not report it:
|
|
7430
|
+
* the renderer is then unknown, not software.
|
|
7431
|
+
*
|
|
7432
|
+
* Today the player renders on the GPU on macOS and in software on
|
|
7433
|
+
* Windows, Linux and without a user interface (see
|
|
7434
|
+
* Platforms); test this field
|
|
7435
|
+
* rather than SystemInfo.os.
|
|
7436
|
+
* @player 0.5.12; older players leave it out (declare `"minPlayerVersion": "0.5.12"` when the plugin relies on it).
|
|
7437
|
+
* @example Offer the HDR menu only where render options work
|
|
7438
|
+
* The plugin adds its HDR command only when the player renders on the
|
|
7439
|
+
* GPU, instead of creating the render options just to find out from a
|
|
7440
|
+
* failed status. `system()` asks the player, which reports the renderer
|
|
7441
|
+
* drawing at that moment; no capability is needed to ask. A player before
|
|
7442
|
+
* 0.5.12 leaves the field out, so the test is for `"gpu"` exactly (an
|
|
7443
|
+
* older player gets no HDR command).
|
|
7444
|
+
* ```ts
|
|
7445
|
+
* const { rendering } = await plugin.app.system();
|
|
7446
|
+
* if (rendering === "gpu") {
|
|
7447
|
+
* const render = plugin.video.render(); // one handle, kept
|
|
7448
|
+
* plugin.commands.add({
|
|
7449
|
+
* id: "hdr",
|
|
7450
|
+
* path: ["@video", "HDR Tone Mapping"],
|
|
7451
|
+
* run: () => render.set({ toneMapping: "bt.2390", targetPeak: 250 }),
|
|
7452
|
+
* });
|
|
7453
|
+
* }
|
|
7454
|
+
* ```
|
|
7455
|
+
*/
|
|
7456
|
+
readonly rendering?: "gpu" | "software";
|
|
7457
|
+
/**
|
|
7458
|
+
* Why SystemInfo.rendering is what it is now: the same reason as
|
|
7459
|
+
* RenderingState.reason (`default`, `no-gpu-backend`,
|
|
7460
|
+
* `preference`, `denylist`, `window-fallback`…, see
|
|
7461
|
+
* RenderingReasonCode). Read at each call, like `rendering`.
|
|
7462
|
+
*
|
|
7463
|
+
* Absent (`undefined`) on players before 0.5.13.
|
|
7464
|
+
* @player 0.5.13; older players leave it out (declare `"minPlayerVersion": "0.5.13"` when the plugin relies on it).
|
|
7465
|
+
* @example Say why the video is drawn in software
|
|
7466
|
+
* An About page explains a software renderer with the player's own words,
|
|
7467
|
+
* for example that the graphics driver is on the player's denylist.
|
|
7468
|
+
* `system()` reports the renderer and its reason together, with no
|
|
7469
|
+
* capability; the message is English text that may name the adapter and
|
|
7470
|
+
* the driver version.
|
|
7471
|
+
* ```ts
|
|
7472
|
+
* const { rendering, renderingReason } = await plugin.app.system();
|
|
7473
|
+
* if (rendering === "software" && renderingReason)
|
|
7474
|
+
* console.log(`Rendering: software (${renderingReason.message})`);
|
|
7475
|
+
* ```
|
|
7476
|
+
*/
|
|
7477
|
+
readonly renderingReason?: RenderingReason;
|
|
7279
7478
|
}
|
|
7280
7479
|
```
|
|
7281
7480
|
|
|
@@ -9142,8 +9341,10 @@ independent handles.
|
|
|
9142
9341
|
|
|
9143
9342
|
Platforms: only macOS renders on the GPU. Under software rendering
|
|
9144
9343
|
(Windows, Linux, headless, fallback) every `set` fails with
|
|
9145
|
-
`unsupported
|
|
9146
|
-
|
|
9344
|
+
`unsupported`; SystemInfo.rendering tells which renderer draws
|
|
9345
|
+
without creating the handle. macOS OpenGL 4.1 has no
|
|
9346
|
+
compute shaders, so `peakDetect` is accepted but has no effect; output
|
|
9347
|
+
is always SDR.
|
|
9147
9348
|
|
|
9148
9349
|
GPU renderer options (tone mapping, peak detection, shaders,
|
|
9149
9350
|
interpolation). Only the GPU renderer has them, which today means macOS;
|
|
@@ -9373,13 +9574,16 @@ interface Subtitles {
|
|
|
9373
9574
|
*
|
|
9374
9575
|
* Limits: SRT ≤ 2 MiB and ≤ 10,000 cues; ASS ≤ 2 MiB with `[Script Info]`
|
|
9375
9576
|
* and `[Events]` and 1–20,000 `Dialogue:` lines. Convert other formats
|
|
9376
|
-
* (VTT, SAMI) to SRT first.
|
|
9577
|
+
* (VTT, SAMI) to SRT first. Per source, a plugin has at most 4 loaded
|
|
9578
|
+
* tracks at once and loads at most 16 files (removed ones count), and
|
|
9579
|
+
* every plugin together at most 16 tracks; see `SUBTITLE_LIMITS` in
|
|
9580
|
+
* `@vestra-player/sdk/testing`.
|
|
9377
9581
|
* @capability subtitles.control and subtitles.read
|
|
9378
9582
|
* @throws invalid (rejects) A bad `format` or `title`, or a file over the limits.
|
|
9379
9583
|
* @throws stale (rejects) With `ifCurrent`: the source changed since that state.
|
|
9380
9584
|
* @throws unavailable (rejects) Nothing loaded, or the source is still loading.
|
|
9381
9585
|
* @throws aborted (rejects) `signal` aborted before the commit (an `AbortError`); the upload is discarded.
|
|
9382
|
-
* @throws busy (rejects) Too many subtitle operations at once.
|
|
9586
|
+
* @throws busy (rejects) Too many subtitle operations at once, or a limit of `SUBTITLE_LIMITS` is reached (4 tracks of this plugin, 16 loads of this plugin or 16 tracks in the source, or another of its limits); nothing is loaded.
|
|
9383
9587
|
* @throws capability (rejects) The manifest does not declare `subtitles.control` and `subtitles.read`.
|
|
9384
9588
|
* @example Load a downloaded SRT and select it
|
|
9385
9589
|
* A subtitle file the plugin downloaded becomes a track of the current file
|
|
@@ -11311,6 +11515,298 @@ if (token === null) {
|
|
|
11311
11515
|
}
|
|
11312
11516
|
```
|
|
11313
11517
|
|
|
11518
|
+
### RenderingMode
|
|
11519
|
+
|
|
11520
|
+
Which renderer draws the video: `"gpu"` (the GPU renderer, where
|
|
11521
|
+
render options apply) or `"software"` (they fail
|
|
11522
|
+
with `unsupported`). The same values as SystemInfo.rendering.
|
|
11523
|
+
|
|
11524
|
+
- @player 0.5.13
|
|
11525
|
+
|
|
11526
|
+
```ts
|
|
11527
|
+
type RenderingMode = "gpu" | "software";
|
|
11528
|
+
```
|
|
11529
|
+
|
|
11530
|
+
Example: Tell the user which renderer draws the video
|
|
11531
|
+
|
|
11532
|
+
The plugin shows "Rendering: GPU" or "Rendering: software" on the
|
|
11533
|
+
video. `state()` asks the player; `mode` is the renderer drawing now,
|
|
11534
|
+
which can differ from the saved choice until the next start.
|
|
11535
|
+
```ts
|
|
11536
|
+
const { mode } = await plugin.rendering.state();
|
|
11537
|
+
const label: Record<RenderingMode, string> = { gpu: "GPU", software: "software" };
|
|
11538
|
+
await plugin.osd.show("Rendering: {}", [label[mode]]);
|
|
11539
|
+
```
|
|
11540
|
+
|
|
11541
|
+
### RenderingPreference
|
|
11542
|
+
|
|
11543
|
+
The user's saved rendering choice: `"auto"` (the player decides: the GPU
|
|
11544
|
+
where this system has a GPU renderer that is not ruled out, software
|
|
11545
|
+
elsewhere), `"gpu"` or `"software"`. It applies from the next start.
|
|
11546
|
+
|
|
11547
|
+
- @player 0.5.13
|
|
11548
|
+
|
|
11549
|
+
```ts
|
|
11550
|
+
type RenderingPreference = "auto" | "gpu" | "software";
|
|
11551
|
+
```
|
|
11552
|
+
|
|
11553
|
+
Example: Describe the saved choice on a settings page
|
|
11554
|
+
|
|
11555
|
+
The plugin turns the saved choice into the words its settings page
|
|
11556
|
+
shows. `preference` is what the user chose, not what draws now:
|
|
11557
|
+
RenderingState.nextMode says what the next start will use.
|
|
11558
|
+
```ts
|
|
11559
|
+
const { preference } = await plugin.rendering.state();
|
|
11560
|
+
// One word per choice; the Record type makes sure none is missing.
|
|
11561
|
+
const words: Record<RenderingPreference, string> = {
|
|
11562
|
+
auto: "Automatic",
|
|
11563
|
+
gpu: "GPU",
|
|
11564
|
+
software: "Software",
|
|
11565
|
+
};
|
|
11566
|
+
console.log(words[preference]);
|
|
11567
|
+
```
|
|
11568
|
+
|
|
11569
|
+
### RenderingReasonCode
|
|
11570
|
+
|
|
11571
|
+
Why the player renders the way it does, as a stable kebab-case code. A
|
|
11572
|
+
newer player may add codes: compare with the ones you handle and show
|
|
11573
|
+
RenderingReason.message for the rest. The codes of player 0.5.13:
|
|
11574
|
+
|
|
11575
|
+
| Code | Meaning |
|
|
11576
|
+
| --- | --- |
|
|
11577
|
+
| `default` | Nothing ruled the GPU out; the platform's default. |
|
|
11578
|
+
| `pending` | GPU rendering is starting. |
|
|
11579
|
+
| `headless` | The player runs without a user interface. |
|
|
11580
|
+
| `no-gpu-backend` | This player has no GPU renderer on this system (Windows today). |
|
|
11581
|
+
| `env` | The environment chose it (`VESTRA_RENDER`, `VESTRA_SOFTWARE_RENDER`). |
|
|
11582
|
+
| `preference` | The user chose software rendering (Rendering.setPreference); GPU chosen and available reports `default`. |
|
|
11583
|
+
| `denylist` | The graphics driver is on the player's denylist. |
|
|
11584
|
+
| `previous-crash` | GPU rendering crashed at an earlier start; it is tried again after a driver or player update, or after 7 days. |
|
|
11585
|
+
| `init-failed` | GPU rendering could not start. |
|
|
11586
|
+
| `window-fallback` | A window could not show GPU frames; the player switched to software during the run. |
|
|
11587
|
+
|
|
11588
|
+
- @player 0.5.13
|
|
11589
|
+
|
|
11590
|
+
```ts
|
|
11591
|
+
type RenderingReasonCode = "default" | "pending" | "headless" | "no-gpu-backend" | "env" | "preference" | "denylist" | "previous-crash" | "init-failed" | "window-fallback" | (string & {});
|
|
11592
|
+
```
|
|
11593
|
+
|
|
11594
|
+
Example: Explain a driver problem differently from a user choice
|
|
11595
|
+
|
|
11596
|
+
The plugin adds advice only when the graphics driver is the cause.
|
|
11597
|
+
`reason.code` is looked up among the codes the plugin knows; an unknown
|
|
11598
|
+
code from a newer player falls through to the player's own message.
|
|
11599
|
+
```ts
|
|
11600
|
+
const { reason } = await plugin.rendering.state();
|
|
11601
|
+
// Advice for the codes this plugin knows; any other code shows the message.
|
|
11602
|
+
const advice: Partial<Record<RenderingReasonCode, string>> = {
|
|
11603
|
+
denylist: "Update the graphics driver to use the GPU.",
|
|
11604
|
+
"previous-crash": "The GPU renderer crashed earlier; it is retried after an update.",
|
|
11605
|
+
};
|
|
11606
|
+
console.log(advice[reason.code] ?? reason.message);
|
|
11607
|
+
```
|
|
11608
|
+
|
|
11609
|
+
### RenderingReason
|
|
11610
|
+
|
|
11611
|
+
Why the player renders, or will render, the way it does: a stable
|
|
11612
|
+
RenderingReasonCode to branch on and a sentence to show.
|
|
11613
|
+
|
|
11614
|
+
- @player 0.5.13
|
|
11615
|
+
|
|
11616
|
+
```ts
|
|
11617
|
+
interface RenderingReason {
|
|
11618
|
+
/** The reason as a stable kebab-case code; branch on this. */
|
|
11619
|
+
readonly code: RenderingReasonCode;
|
|
11620
|
+
/** The reason as English text for people, for example with the graphics adapter's name and driver version; never branch on it. */
|
|
11621
|
+
readonly message: string;
|
|
11622
|
+
}
|
|
11623
|
+
```
|
|
11624
|
+
|
|
11625
|
+
Example: Show why GPU rendering is off
|
|
11626
|
+
|
|
11627
|
+
The plugin logs the player's explanation when the video is drawn in
|
|
11628
|
+
software. `message` is the player's English text and may name the
|
|
11629
|
+
graphics adapter and driver version; `code` is for decisions.
|
|
11630
|
+
```ts
|
|
11631
|
+
const { mode, reason } = await plugin.rendering.state();
|
|
11632
|
+
if (mode === "software") console.log(`${reason.code}: ${reason.message}`);
|
|
11633
|
+
```
|
|
11634
|
+
|
|
11635
|
+
### RenderingState
|
|
11636
|
+
|
|
11637
|
+
What Rendering.state and Rendering.setPreference report:
|
|
11638
|
+
the renderer drawing now and why, the saved choice, what the next start
|
|
11639
|
+
will use, and whether `"gpu"` can be chosen.
|
|
11640
|
+
|
|
11641
|
+
- @player 0.5.13
|
|
11642
|
+
|
|
11643
|
+
```ts
|
|
11644
|
+
interface RenderingState {
|
|
11645
|
+
/** The renderer drawing the video now; the same as SystemInfo.rendering. */
|
|
11646
|
+
readonly mode: RenderingMode;
|
|
11647
|
+
/** Why RenderingState.mode is what it is now. */
|
|
11648
|
+
readonly reason: RenderingReason;
|
|
11649
|
+
/** The saved choice; it applies from the next start. */
|
|
11650
|
+
readonly preference: RenderingPreference;
|
|
11651
|
+
/** 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. */
|
|
11652
|
+
readonly nextMode: RenderingMode;
|
|
11653
|
+
/** Why the next start will use RenderingState.nextMode. */
|
|
11654
|
+
readonly nextReason: RenderingReason;
|
|
11655
|
+
/**
|
|
11656
|
+
* `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.
|
|
11657
|
+
*/
|
|
11658
|
+
readonly restartPending: boolean;
|
|
11659
|
+
/** Whether the user can choose `"gpu"` now. */
|
|
11660
|
+
readonly gpu: {
|
|
11661
|
+
/** `true` when `setPreference("gpu")` would be accepted. */
|
|
11662
|
+
readonly available: boolean;
|
|
11663
|
+
/** Why `"gpu"` cannot be chosen (`no-gpu-backend`, `headless`, `denylist`, `previous-crash`…); `null` while it is available. */
|
|
11664
|
+
readonly reason: RenderingReason | null;
|
|
11665
|
+
};
|
|
11666
|
+
}
|
|
11667
|
+
```
|
|
11668
|
+
|
|
11669
|
+
Example: Label a menu item while a change waits for a restart
|
|
11670
|
+
|
|
11671
|
+
The plugin adds "(Restart to Apply)" to its GPU item after the user
|
|
11672
|
+
changed the choice. `restartPending` compares the mode the next start
|
|
11673
|
+
will use with the mode this run started with, so it turns false again
|
|
11674
|
+
when the user changes the choice back.
|
|
11675
|
+
```ts
|
|
11676
|
+
const state = await plugin.rendering.state();
|
|
11677
|
+
const title = state.restartPending
|
|
11678
|
+
? "GPU Rendering (Experimental) (Restart to Apply)"
|
|
11679
|
+
: "GPU Rendering (Experimental)";
|
|
11680
|
+
console.log(title, state.nextMode, state.nextReason.message);
|
|
11681
|
+
```
|
|
11682
|
+
|
|
11683
|
+
### Rendering
|
|
11684
|
+
|
|
11685
|
+
`plugin.rendering`: the player's rendering choice, GPU or software.
|
|
11686
|
+
Reading it needs no capability; changing the saved choice and restarting
|
|
11687
|
+
the player need `rendering.manage`.
|
|
11688
|
+
|
|
11689
|
+
The choice works like a browser's hardware acceleration switch: the
|
|
11690
|
+
player saves it in its own settings at once, and the running player
|
|
11691
|
+
keeps its renderer. The next start uses it, whether the user restarts
|
|
11692
|
+
now through Rendering.restartPlayer or quits and starts the
|
|
11693
|
+
player later. The player can still rule the GPU out at a start (an
|
|
11694
|
+
environment variable, a graphics driver on its denylist, a GPU crash at
|
|
11695
|
+
an earlier start); RenderingState.nextMode and
|
|
11696
|
+
RenderingState.nextReason say what the next start will do.
|
|
11697
|
+
|
|
11698
|
+
The renderer is a choice for the whole player and a restart stops every
|
|
11699
|
+
plugin, so change it only when the user asks. A plugin that only needs
|
|
11700
|
+
to know whether render options work reads SystemInfo.rendering.
|
|
11701
|
+
|
|
11702
|
+
Players before 0.5.13 do not know these requests: `state()` rejects with
|
|
11703
|
+
`unsupported`, and they do not load a plugin that declares
|
|
11704
|
+
`rendering.manage` (an unknown capability). Declare
|
|
11705
|
+
`"minPlayerVersion": "0.5.13"` when the plugin needs this.
|
|
11706
|
+
|
|
11707
|
+
- @capability None to read the state; `rendering.manage` to change the choice or restart the player.
|
|
11708
|
+
- @player 0.5.13; older players reject the requests as unknown (`unsupported`).
|
|
11709
|
+
|
|
11710
|
+
```ts
|
|
11711
|
+
interface Rendering {
|
|
11712
|
+
/**
|
|
11713
|
+
* Read the renderer drawing now, the saved choice, what the next start will use and whether `"gpu"` can be chosen. No capability.
|
|
11714
|
+
* @capability None.
|
|
11715
|
+
* @throws unsupported (rejects) A player before 0.5.13, which does not know the request.
|
|
11716
|
+
* @example Find out whether the GPU can be chosen
|
|
11717
|
+
* The plugin reads the state once while it starts and keeps it for its
|
|
11718
|
+
* menu. Each call asks the player; `gpu.available` is false, with a
|
|
11719
|
+
* reason, where this player has no GPU renderer, without a user
|
|
11720
|
+
* interface, or when the driver is ruled out. An older player rejects
|
|
11721
|
+
* with `unsupported`, and the plugin then offers nothing.
|
|
11722
|
+
* ```ts
|
|
11723
|
+
* const state = await plugin.rendering.state().catch((error: PluginError) => {
|
|
11724
|
+
* if (error.code === "unsupported") return null; // a player before 0.5.13
|
|
11725
|
+
* throw error;
|
|
11726
|
+
* });
|
|
11727
|
+
* if (state && !state.gpu.available) console.log(state.gpu.reason?.message);
|
|
11728
|
+
* ```
|
|
11729
|
+
*/
|
|
11730
|
+
state(): Promise<RenderingState>;
|
|
11731
|
+
/**
|
|
11732
|
+
* 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.
|
|
11733
|
+
* @param preference - `"auto"`, `"gpu"` or `"software"`.
|
|
11734
|
+
* @returns The new state; RenderingState.restartPending tells whether a restart would change the renderer.
|
|
11735
|
+
* @capability rendering.manage
|
|
11736
|
+
* @throws invalid (rejects) `preference` is not `"auto"`, `"gpu"` or `"software"`.
|
|
11737
|
+
* @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.
|
|
11738
|
+
* @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.
|
|
11739
|
+
* @throws failed (rejects) The player could not write its settings file.
|
|
11740
|
+
* @throws capability (rejects) The manifest does not declare `rendering.manage`.
|
|
11741
|
+
* @example Switch back to the automatic choice
|
|
11742
|
+
* A Reset item returns the renderer to the player's own choice and tells
|
|
11743
|
+
* the user when it takes effect. `setPreference` saves the choice and
|
|
11744
|
+
* resolves with the new state; `restartPending` is true when the next
|
|
11745
|
+
* start will draw differently from this run. A refused save leaves the
|
|
11746
|
+
* old choice in place.
|
|
11747
|
+
* ```ts
|
|
11748
|
+
* try {
|
|
11749
|
+
* const state = await plugin.rendering.setPreference("auto");
|
|
11750
|
+
* if (state.restartPending) await plugin.osd.show("Applies after a restart");
|
|
11751
|
+
* } catch (error) {
|
|
11752
|
+
* // `failed`: the player's settings file could not be written.
|
|
11753
|
+
* await plugin.osd.show("Not saved ({})", [(error as PluginError).code]);
|
|
11754
|
+
* }
|
|
11755
|
+
* ```
|
|
11756
|
+
*/
|
|
11757
|
+
setPreference(preference: RenderingPreference): Promise<RenderingState>;
|
|
11758
|
+
/**
|
|
11759
|
+
* 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.
|
|
11760
|
+
* @capability rendering.manage
|
|
11761
|
+
* @throws unsupported (rejects) The player runs without a user interface; also a player before 0.5.13.
|
|
11762
|
+
* @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).
|
|
11763
|
+
* @throws failed (rejects) The new player process could not be started; the player keeps running.
|
|
11764
|
+
* @throws capability (rejects) The manifest does not declare `rendering.manage`.
|
|
11765
|
+
* @example Restart after asking the user
|
|
11766
|
+
* After the user changed the renderer, the plugin offers to restart now.
|
|
11767
|
+
* `confirm` asks first, because a restart closes every window (it needs
|
|
11768
|
+
* `ui.dialog`); once `restartPlayer` resolves, the player is quitting and
|
|
11769
|
+
* will stop this plugin, so nothing else is started after it.
|
|
11770
|
+
* ```ts
|
|
11771
|
+
* const { confirmed } = await plugin.dialogs.confirm({
|
|
11772
|
+
* message: "Restart the player now?",
|
|
11773
|
+
* detail: "The new renderer is used from the restart on. The video continues where it is.",
|
|
11774
|
+
* confirm: "Restart",
|
|
11775
|
+
* });
|
|
11776
|
+
* if (confirmed)
|
|
11777
|
+
* await plugin.rendering.restartPlayer().catch((error: PluginError) => {
|
|
11778
|
+
* // `failed`: the new player could not start; this one keeps running.
|
|
11779
|
+
* void plugin.osd.show("Could not restart ({})", [error.code]);
|
|
11780
|
+
* });
|
|
11781
|
+
* ```
|
|
11782
|
+
*/
|
|
11783
|
+
restartPlayer(): Promise<void>;
|
|
11784
|
+
}
|
|
11785
|
+
```
|
|
11786
|
+
|
|
11787
|
+
Example: Offer GPU rendering as a menu item
|
|
11788
|
+
|
|
11789
|
+
Settings ▸ GPU Rendering (Experimental) carries a check mark while the
|
|
11790
|
+
next start will draw on the GPU, is greyed out with the player's reason
|
|
11791
|
+
where the GPU cannot be chosen, and saves the other choice when picked.
|
|
11792
|
+
`state()` reads the player's view once; `setPreference` saves the choice
|
|
11793
|
+
and resolves with the new state, which the item's bound values read
|
|
11794
|
+
again after the handler. The how-to guide adds the restart.
|
|
11795
|
+
```ts
|
|
11796
|
+
let state = await plugin.rendering.state();
|
|
11797
|
+
plugin.commands.add({
|
|
11798
|
+
id: "gpu-rendering",
|
|
11799
|
+
path: ["@settings", "GPU Rendering (Experimental)"],
|
|
11800
|
+
checked: () => state.nextMode === "gpu", // also for "auto" on a GPU system
|
|
11801
|
+
enabled: () => state.gpu.available,
|
|
11802
|
+
reason: () => state.gpu.reason?.message ?? null, // shown while disabled
|
|
11803
|
+
run: async () => {
|
|
11804
|
+
const choice = state.nextMode === "gpu" ? "software" : "gpu";
|
|
11805
|
+
state = await plugin.rendering.setPreference(choice);
|
|
11806
|
+
},
|
|
11807
|
+
});
|
|
11808
|
+
```
|
|
11809
|
+
|
|
11314
11810
|
### VideoNode
|
|
11315
11811
|
|
|
11316
11812
|
One node of a VideoFilter chain, by `kind`. Values outside the
|
|
@@ -12007,7 +12503,7 @@ Members fall into a few groups:
|
|
|
12007
12503
|
TestHost.app, TestHost.systemInfo,
|
|
12008
12504
|
TestHost.chapters, TestHost.audioDevices,
|
|
12009
12505
|
TestHost.secrets, TestHost.clipboard,
|
|
12010
|
-
TestHost.tools.
|
|
12506
|
+
TestHost.tools, TestHost.rendering.
|
|
12011
12507
|
- **Assert** on records: TestHost.osd, TestHost.calls,
|
|
12012
12508
|
TestHost.sent, TestHost.conflicts,
|
|
12013
12509
|
TestHost.layout, TestHost.menus, and many more.
|
|
@@ -12582,7 +13078,7 @@ class TestHost implements Transport {
|
|
|
12582
13078
|
owner: string | null;
|
|
12583
13079
|
/** The values the player holds. */
|
|
12584
13080
|
values: RenderSettings;
|
|
12585
|
-
/** `true`: GPU rendering; `false`: software rendering (`unsupported`). */
|
|
13081
|
+
/** `true`: GPU rendering; `false`: software rendering (`unsupported`). `plugin.app.system()` reports it as `rendering`. */
|
|
12586
13082
|
gpu: boolean;
|
|
12587
13083
|
};
|
|
12588
13084
|
/**
|
|
@@ -12601,7 +13097,7 @@ class TestHost implements Transport {
|
|
|
12601
13097
|
*/
|
|
12602
13098
|
setVideoRenderOwner(pluginId: string | null): void;
|
|
12603
13099
|
/**
|
|
12604
|
-
* 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.
|
|
13100
|
+
* 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.
|
|
12605
13101
|
* @capability None.
|
|
12606
13102
|
* @example Make a shader fail to compile
|
|
12607
13103
|
* The plugin's render options report `failed`, as when mpv rejects a
|
|
@@ -12671,6 +13167,8 @@ class TestHost implements Transport {
|
|
|
12671
13167
|
bytes: Buffer[];
|
|
12672
13168
|
/** The announced total size in bytes. */
|
|
12673
13169
|
expected: number;
|
|
13170
|
+
/** The source generation it was begun for; like the player, a new source drops the uploads of the old one. */
|
|
13171
|
+
generation?: number;
|
|
12674
13172
|
}>;
|
|
12675
13173
|
/** Snapshots taken with `media.capture`, by snapshot id. Wire-level. */
|
|
12676
13174
|
readonly snapshots: Map<number, Json>;
|
|
@@ -12824,8 +13322,10 @@ class TestHost implements Transport {
|
|
|
12824
13322
|
* `plugin.storage.session`: key → value, as the player keeps them. Preset entries to simulate values an earlier run of the plugin stored in the same player session, or pass `startPlugin(..., { sessionStorage })`. Like the player, a key is 1–1024 bytes, a `set` that would make the entries larger than 1 MiB (keys, values as compact JSON and `SESSION_STORAGE_LIMITS.entryBytes` per key) fails `limit`, and an object's properties are stored sorted, as the player hands them back.
|
|
12825
13323
|
*/
|
|
12826
13324
|
readonly sessionStorage: Map<string, Json>;
|
|
12827
|
-
/**
|
|
12828
|
-
|
|
13325
|
+
/**
|
|
13326
|
+
* 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.
|
|
13327
|
+
*/
|
|
13328
|
+
systemInfo: Omit<WireSystemInfo, "rendering" | "renderingReason">;
|
|
12829
13329
|
/** What `plugin.app` reports. Set it before startPlugin: the plugin receives it once at start-up. */
|
|
12830
13330
|
app: {
|
|
12831
13331
|
/** The player version. */
|
|
@@ -12842,6 +13342,70 @@ class TestHost implements Transport {
|
|
|
12842
13342
|
slint: string;
|
|
12843
13343
|
};
|
|
12844
13344
|
};
|
|
13345
|
+
/**
|
|
13346
|
+
* The player's rendering choice behind `plugin.rendering` (player
|
|
13347
|
+
* 0.5.13). The mode drawing now is videoRender.gpu, as for `plugin.app.system()`; the rest is preset here
|
|
13348
|
+
* and changed by the plugin's calls, as the player would:
|
|
13349
|
+
*
|
|
13350
|
+
* - `setPreference` saves `preference` and never changes the mode. It
|
|
13351
|
+
* refuses `"gpu"` while `gpu.available` is false: with `unsupported`
|
|
13352
|
+
* when `gpu.reason.code` is `no-gpu-backend` or `headless`, otherwise
|
|
13353
|
+
* with `unavailable`, the reason's message in both cases.
|
|
13354
|
+
* - The next start uses `"software"` for the `"software"` choice, else
|
|
13355
|
+
* software with `gpu.reason` while the GPU is not available, else the
|
|
13356
|
+
* GPU (code `preference` for a `"gpu"` choice, `default` for
|
|
13357
|
+
* `"auto"`). `restartPending` compares that with `startedWith`.
|
|
13358
|
+
* - `restartPlayer` counts in `restarts` and leaves the plugin running
|
|
13359
|
+
* (call `stop()` yourself); a second one rejects with `busy`, as a
|
|
13360
|
+
* restart is then under way.
|
|
13361
|
+
*
|
|
13362
|
+
* Inject other refusals (`failed` for an unwritable settings file or a
|
|
13363
|
+
* new process that could not start) with `host.handle("rendering.setPreference", …)`
|
|
13364
|
+
* or `host.handle("rendering.restart", …)`. The defaults are a macOS
|
|
13365
|
+
* player on the GPU: `"auto"`, available, started on the GPU.
|
|
13366
|
+
* @example Test a GPU switch on a system without a GPU renderer
|
|
13367
|
+
* The plugin's GPU menu item is greyed out with the player's reason when
|
|
13368
|
+
* the GPU cannot be chosen. The host is preset before the plugin starts,
|
|
13369
|
+
* because the plugin reads the state while activating; software
|
|
13370
|
+
* rendering is `videoRender.gpu = false`, and `gpu` says why the GPU
|
|
13371
|
+
* cannot be chosen.
|
|
13372
|
+
* ```ts
|
|
13373
|
+
* import assert from "node:assert/strict";
|
|
13374
|
+
* import { startPlugin, TestHost } from "@vestra-player/sdk/testing";
|
|
13375
|
+
* import activate from "../main.ts";
|
|
13376
|
+
*
|
|
13377
|
+
* const host = new TestHost();
|
|
13378
|
+
* host.videoRender.gpu = false; // software draws now (Windows)
|
|
13379
|
+
* host.rendering.startedWith = "software";
|
|
13380
|
+
* host.rendering.reason = { code: "no-gpu-backend", message: "No GPU renderer on Windows" };
|
|
13381
|
+
* host.rendering.gpu = { available: false, reason: host.rendering.reason };
|
|
13382
|
+
* const { stop } = await startPlugin(activate, {
|
|
13383
|
+
* directory: new URL("..", import.meta.url),
|
|
13384
|
+
* host,
|
|
13385
|
+
* });
|
|
13386
|
+
* const item = host.command("gpu-rendering");
|
|
13387
|
+
* assert.equal(item.state.enabled, false);
|
|
13388
|
+
* assert.equal(item.state.reason, "No GPU renderer on Windows");
|
|
13389
|
+
* await stop();
|
|
13390
|
+
* ```
|
|
13391
|
+
*/
|
|
13392
|
+
readonly rendering: {
|
|
13393
|
+
/** Why the mode drawing now is what it is (`RenderingState.reason`, `SystemInfo.renderingReason`). */
|
|
13394
|
+
reason: RenderingReason;
|
|
13395
|
+
/** The saved choice: `"auto"`, `"gpu"` or `"software"`. */
|
|
13396
|
+
preference: RenderingPreference;
|
|
13397
|
+
/** The mode this run started with; `restartPending` compares the next start's mode with it. */
|
|
13398
|
+
startedWith: RenderingMode;
|
|
13399
|
+
/** Whether the plugin may choose `"gpu"`, and why not (`null` while available). */
|
|
13400
|
+
gpu: {
|
|
13401
|
+
/** `false`: `setPreference("gpu")` is refused. */
|
|
13402
|
+
available: boolean;
|
|
13403
|
+
/** Why the GPU cannot be chosen; `null` while it can. */
|
|
13404
|
+
reason: RenderingReason | null;
|
|
13405
|
+
};
|
|
13406
|
+
/** Accepted `restartPlayer` calls. */
|
|
13407
|
+
restarts: number;
|
|
13408
|
+
};
|
|
12845
13409
|
/** What `player.chapters()` lists while media is ready. */
|
|
12846
13410
|
chapters: {
|
|
12847
13411
|
/** The chapter title, `null` without one. */
|
|
@@ -13530,8 +14094,8 @@ current.
|
|
|
13530
14094
|
|
|
13531
14095
|
A member not listed under any capability needs none (`plugin.tools`,
|
|
13532
14096
|
`plugin.storage`, `plugin.system`, `plugin.app`, `plugin.ui`,
|
|
13533
|
-
`plugin.i18n`, reading `plugin.window.state`, `plugin.appearance.theme
|
|
13534
|
-
|
|
14097
|
+
`plugin.i18n`, reading `plugin.window.state`, `plugin.appearance.theme`,
|
|
14098
|
+
`plugin.packages.list` and `plugin.rendering.state`).
|
|
13535
14099
|
|
|
13536
14100
|
```ts
|
|
13537
14101
|
const CAPABILITY_INFO: Readonly<Record<string, CapabilityInfo>>;
|
|
@@ -14061,6 +14625,61 @@ if (!token || Buffer.byteLength(token) > SECRET_LIMITS.valueBytes) {
|
|
|
14061
14625
|
}
|
|
14062
14626
|
```
|
|
14063
14627
|
|
|
14628
|
+
### SUBTITLE_LIMITS
|
|
14629
|
+
|
|
14630
|
+
The player's limits on subtitle files a plugin loads with
|
|
14631
|
+
Subtitles.load, which `TestHost` enforces with the player's codes.
|
|
14632
|
+
The track counts are per source: opening another file starts from zero.
|
|
14633
|
+
A file over `fileBytes` fails with `invalid`; every other limit fails with
|
|
14634
|
+
`busy`, and nothing is loaded.
|
|
14635
|
+
|
|
14636
|
+
Each `load` uses a new resource on the source, and removing the track
|
|
14637
|
+
frees its place among the live tracks (`tracksPerPlugin`, `tracks`,
|
|
14638
|
+
`totalBytes`) but not among the loads (`loadsPerPlugin`, `loads`). So one
|
|
14639
|
+
plugin loads at most 16 files into one source, with at most 4 of them
|
|
14640
|
+
present at once.
|
|
14641
|
+
|
|
14642
|
+
```ts
|
|
14643
|
+
const SUBTITLE_LIMITS: Readonly<{
|
|
14644
|
+
/** UTF-8 bytes of one file; a larger one fails with `invalid` (2 MiB). */
|
|
14645
|
+
readonly fileBytes: number;
|
|
14646
|
+
/** Tracks one plugin has loaded into the current source at the same time (4). */
|
|
14647
|
+
readonly tracksPerPlugin: 4;
|
|
14648
|
+
/** Loaded tracks of every plugin in the current source at the same time (16). */
|
|
14649
|
+
readonly tracks: 16;
|
|
14650
|
+
/** Bytes of the loaded tracks of every plugin in the current source together (16 MiB). */
|
|
14651
|
+
readonly totalBytes: number;
|
|
14652
|
+
/** Loads by one plugin into the current source, removed tracks included (16). */
|
|
14653
|
+
readonly loadsPerPlugin: 16;
|
|
14654
|
+
/** Loads by every plugin into the current source, removed tracks included (128). */
|
|
14655
|
+
readonly loads: 128;
|
|
14656
|
+
/** Loads of one plugin in progress at the same time (2). */
|
|
14657
|
+
readonly uploadsPerPlugin: 2;
|
|
14658
|
+
/** Loads of every plugin in progress at the same time (8). */
|
|
14659
|
+
readonly uploads: 8;
|
|
14660
|
+
}>;
|
|
14661
|
+
```
|
|
14662
|
+
|
|
14663
|
+
Example: Replace the plugin's oldest subtitle when it has four
|
|
14664
|
+
|
|
14665
|
+
The plugin keeps the files it loaded, newest last, and removes the oldest
|
|
14666
|
+
before a fifth would be refused with `busy`. A load the player still
|
|
14667
|
+
refuses (the source holds 16 tracks of every plugin, or this plugin
|
|
14668
|
+
already loaded 16 files into it) is reported on the OSD.
|
|
14669
|
+
```ts
|
|
14670
|
+
import { SUBTITLE_LIMITS } from "@vestra-player/sdk/testing";
|
|
14671
|
+
|
|
14672
|
+
const loaded: SubtitleTrack[] = []; // this source's, newest last
|
|
14673
|
+
if (loaded.length >= SUBTITLE_LIMITS.tracksPerPlugin)
|
|
14674
|
+
await plugin.subtitles.remove(loaded.shift()!);
|
|
14675
|
+
try {
|
|
14676
|
+
loaded.push(await plugin.subtitles.load(bytes));
|
|
14677
|
+
} catch (error) {
|
|
14678
|
+
if ((error as PluginError).code !== "busy") throw error;
|
|
14679
|
+
await plugin.osd.show("No room for another subtitle on this file.");
|
|
14680
|
+
}
|
|
14681
|
+
```
|
|
14682
|
+
|
|
14064
14683
|
### UI_VALUE_LIMITS
|
|
14065
14684
|
|
|
14066
14685
|
The player's limits on one property value (a `PropValue` as written
|
|
@@ -14549,7 +15168,7 @@ interface SubtitleState {
|
|
|
14549
15168
|
|
|
14550
15169
|
### WireSystemInfo
|
|
14551
15170
|
|
|
14552
|
-
`app.system`: the operating system and the
|
|
15171
|
+
`app.system`: the operating system, the displays and the renderer.
|
|
14553
15172
|
|
|
14554
15173
|
```ts
|
|
14555
15174
|
interface WireSystemInfo {
|
|
@@ -14567,6 +15186,10 @@ interface WireSystemInfo {
|
|
|
14567
15186
|
refreshRate: number | null;
|
|
14568
15187
|
primary: boolean;
|
|
14569
15188
|
}[];
|
|
15189
|
+
/** Player 0.5.12; older players leave it out. */
|
|
15190
|
+
rendering?: "gpu" | "software";
|
|
15191
|
+
/** Why `rendering` is what it is (player 0.5.13; older players leave it out). */
|
|
15192
|
+
renderingReason?: WireRenderingReason;
|
|
14570
15193
|
}
|
|
14571
15194
|
```
|
|
14572
15195
|
|