@vestra-player/sdk 1.5.42 → 1.5.43
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 +11 -0
- package/dist/api-requests.js +1 -0
- package/dist/host-capabilities.js +2 -0
- package/dist/plugin/services.js +24 -2
- package/dist/plugin/testing.d.ts +65 -1
- package/dist/plugin/testing.js +106 -1
- package/dist/plugin/types.d.ts +192 -1
- package/dist/wire.d.ts +31 -4
- package/llms-full.txt +204 -4
- package/llms.txt +23 -23
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.5.43
|
|
4
|
+
|
|
5
|
+
**Who must change: nobody.** Additions only, `apiVersion` stays `vestra/1`, no new capability. The new API needs player 0.5.25 (`"minPlayerVersion": "0.5.25"`); older players leave the new fields out and refuse the new event (the listener never fires). The `decoding` reason code stays as it is: the cause is a new optional field beside it.
|
|
6
|
+
|
|
7
|
+
- **`UpscalingReason.cause`** (player 0.5.25): with code `decoding`, why the video is not decoded on the GPU that renders it, as a stable `UpscalingDecodingCause`: `copy-back` (decoded on the GPU and copied back to memory, usually because a plugin's video filter needs the frames there), `hwdec-off` (hardware decoding off), `software` (this video is decoded in software: a cover picture, an unsupported format), `other-hwdec` (another hardware decoder). A newer player may add causes. With `copy-back`, **`UpscalingReason.plugins`** lists whose video filters made the player switch (`UpscalingPlugin`: manifest `id` and `name`; empty when the copy-back mode is the user's own setting), and the message names them ("…because the video filter of Picture Adjustments needs the frames in memory").
|
|
8
|
+
- **Monitoring fields in `UpscalingState`** (player 0.5.25, optional: absent from older players): `video` (the video's stored and display size as the upscaler gets it, `UpscalingVideoSize`), `area` (the video area in pixels), `gpu` (the rendering GPU's name), `hwdecCurrent` (mpv's decoder for this video), `inChain` (the upscaler is in the filter chain now, read from the chain). The driver still gives no sign that it upscales.
|
|
9
|
+
- **`plugin.video.upscaling.onChange(listener)`** (player 0.5.25, `video.upscaling`): the state whenever it changes while some plugin has upscaling on (the player looks about five times a second and sends only differences), so a window can show it live without polling. Wire: event `video.upscaling.changed` with the state without `enabled` (the SDK adds this plugin's own); subscribing needs `video.upscaling`, not `events`. A listener keeps the plugin running (it is not an event the player wakes a stopped plugin for): listen only while a window shows the state. The manifest names in `plugins` and the message are not translated.
|
|
10
|
+
- **`TestHost`**: `upscalingDecoding(cause, plugins?)` fakes each cause with the player's message (and a matching `upscaling.hwdecCurrent`); `upscaling.video`, `area`, `gpu`, `hwdecCurrent` (defaults: 1080p on a 4K area, an RTX 4070, `d3d11va`; `inChain` follows `requested`); `upscalingChanged()` sends the event to a listening plugin.
|
|
11
|
+
- The player's capability table copy (`video.upscaling.changed` under `video.upscaling`) and `CAPABILITY_INFO` (`VideoUpscaling.onChange`) follow.
|
|
12
|
+
|
|
13
|
+
|
|
3
14
|
## 1.5.42
|
|
4
15
|
|
|
5
16
|
**Who must change: nobody.** Documentation only: no API changes, `apiVersion` stays `vestra/1`, no player version needed. The package's code is the same as 1.5.41; the doc comments' `@see` links (27 in `src/plugin/types.ts`) now point at `docs/explanation/` instead of `docs/concepts/`. The site's page addresses stay the same, and the old _Concepts_ group address redirects to _Explanation_; `llms.txt` and `llms-full.txt` follow the new pages.
|
package/dist/api-requests.js
CHANGED
|
@@ -146,4 +146,5 @@ export const API_REQUESTS = Object.freeze({
|
|
|
146
146
|
"Rendering.restartPlayer": [{ method: "rendering.restart" }],
|
|
147
147
|
"VideoUpscaling.state": [{ method: "video.upscaling.state" }],
|
|
148
148
|
"VideoUpscaling.set": [{ method: "video.upscaling.set" }],
|
|
149
|
+
"VideoUpscaling.onChange": [listen("video.upscaling.changed")],
|
|
149
150
|
});
|
|
@@ -85,6 +85,7 @@ export const HOST_CAPABILITIES = {
|
|
|
85
85
|
cases: {
|
|
86
86
|
"bindings.changed": "bindings.manage",
|
|
87
87
|
"packages.changed": null,
|
|
88
|
+
"video.upscaling.changed": "video.upscaling",
|
|
88
89
|
},
|
|
89
90
|
otherwise: "events",
|
|
90
91
|
param: "event",
|
|
@@ -96,6 +97,7 @@ export const HOST_CAPABILITIES = {
|
|
|
96
97
|
cases: {
|
|
97
98
|
"bindings.changed": "bindings.manage",
|
|
98
99
|
"packages.changed": null,
|
|
100
|
+
"video.upscaling.changed": "video.upscaling",
|
|
99
101
|
},
|
|
100
102
|
otherwise: "events",
|
|
101
103
|
param: "event",
|
package/dist/plugin/services.js
CHANGED
|
@@ -508,16 +508,38 @@ export const videoUpscaling = (scope) => {
|
|
|
508
508
|
if (!scope.runtime.can("video.upscaling"))
|
|
509
509
|
throw failure("capability", "video.upscaling must be declared in manifest");
|
|
510
510
|
};
|
|
511
|
+
// This plugin's own request, kept from every answer for `onChange`: the
|
|
512
|
+
// event leaves it out (it is each plugin's own), and only this plugin's
|
|
513
|
+
// `set` changes it.
|
|
514
|
+
const noted = (state) => {
|
|
515
|
+
enabledOf.set(scope.runtime, state.enabled);
|
|
516
|
+
return state;
|
|
517
|
+
};
|
|
511
518
|
return {
|
|
512
519
|
async state() {
|
|
513
520
|
declared();
|
|
514
|
-
return (await scope.runtime.host.video.upscalingState());
|
|
521
|
+
return noted(await scope.runtime.host.video.upscalingState());
|
|
515
522
|
},
|
|
516
523
|
async set(enabled) {
|
|
517
524
|
declared();
|
|
518
525
|
if (typeof enabled !== "boolean")
|
|
519
526
|
throw failure("invalid", "enabled must be true or false");
|
|
520
|
-
return (await scope.runtime.host.video.upscalingSet(enabled));
|
|
527
|
+
return noted(await scope.runtime.host.video.upscalingSet(enabled));
|
|
528
|
+
},
|
|
529
|
+
onChange(listener) {
|
|
530
|
+
declared();
|
|
531
|
+
const watch = scope.runtime.on("video.upscaling.changed", (data) => scope.run(listener, {
|
|
532
|
+
...data,
|
|
533
|
+
enabled: enabledOf.get(scope.runtime) ?? false,
|
|
534
|
+
}), "video.upscaling");
|
|
535
|
+
return scope.add({
|
|
536
|
+
dispose: async () => {
|
|
537
|
+
scope.forget(watch);
|
|
538
|
+
await watch.dispose();
|
|
539
|
+
},
|
|
540
|
+
});
|
|
521
541
|
},
|
|
522
542
|
};
|
|
523
543
|
};
|
|
544
|
+
/** Each plugin's own upscaling request, as its last answer said. */
|
|
545
|
+
const enabledOf = new WeakMap();
|
package/dist/plugin/testing.d.ts
CHANGED
|
@@ -16,7 +16,7 @@ import type { CommandDeclaration } from "../commands.ts";
|
|
|
16
16
|
import type { SourceState } from "../source.ts";
|
|
17
17
|
import type { SubtitleState } from "../subtitles.ts";
|
|
18
18
|
import { VIDEO_PROCESSING_LIMITS } from "../processing.ts";
|
|
19
|
-
import type { RenderingMode, RenderingPreference, RenderingReason, RenderSettings, UpscalingReason, ViewSettings } from "./types.ts";
|
|
19
|
+
import type { RenderingMode, RenderingPreference, RenderingReason, RenderSettings, UpscalingDecodingCause, UpscalingPlugin, UpscalingReason, UpscalingVideoSize, ViewSettings } from "./types.ts";
|
|
20
20
|
import type { MediaJob } from "../media.ts";
|
|
21
21
|
import type { LanguagePreference, Locale, Plugin, TemplateArgs, TextValue, ThemeId } from "./types.ts";
|
|
22
22
|
import { LOCALES, LIST_SEPARATORS, TEXT_LIMITS } from "../i18n.ts";
|
|
@@ -1862,7 +1862,71 @@ export declare class TestHost implements Transport {
|
|
|
1862
1862
|
checked: boolean;
|
|
1863
1863
|
/** GPU rendering is still starting: a `rendering` reason then lets `set(true)` keep the request. */
|
|
1864
1864
|
renderingStarting: boolean;
|
|
1865
|
+
/** The video's size the state reports (player 0.5.25); `null`: no video. */
|
|
1866
|
+
video: UpscalingVideoSize | null;
|
|
1867
|
+
/** The video area the state reports (player 0.5.25); `null`: none. */
|
|
1868
|
+
area: {
|
|
1869
|
+
/** Width in physical pixels. */
|
|
1870
|
+
width: number;
|
|
1871
|
+
/** Height in physical pixels. */
|
|
1872
|
+
height: number;
|
|
1873
|
+
} | null;
|
|
1874
|
+
/** The rendering GPU's name the state reports (player 0.5.25). */
|
|
1875
|
+
gpu: string | null;
|
|
1876
|
+
/** mpv's decoder the state reports (player 0.5.25); `null`: no video. */
|
|
1877
|
+
hwdecCurrent: string | null;
|
|
1865
1878
|
};
|
|
1879
|
+
/**
|
|
1880
|
+
* Make the upscaler's reason `decoding` with `cause`, with the player's
|
|
1881
|
+
* message (player 0.5.25), and set `upscaling.hwdecCurrent` to match
|
|
1882
|
+
* (`d3d11va-copy`, `no`, `nvdec`). `plugins` are named for `copy-back`
|
|
1883
|
+
* only, as the player does. Returns the reason; call
|
|
1884
|
+
* {@link TestHost.upscalingChanged} to tell a listening plugin.
|
|
1885
|
+
*
|
|
1886
|
+
* @example Test the window's advice for each cause
|
|
1887
|
+
* The plugin's window explains why upscaling waits. The test fakes the
|
|
1888
|
+
* copy-back cause that Picture Adjustments' filters cause and checks
|
|
1889
|
+
* that the window names the plugin.
|
|
1890
|
+
* ```ts
|
|
1891
|
+
* import assert from "node:assert/strict";
|
|
1892
|
+
* declare const host: TestHost;
|
|
1893
|
+
* // ---cut---
|
|
1894
|
+
* host.upscalingDecoding("copy-back", [{ id: "vestra.video", name: "Picture Adjustments" }]);
|
|
1895
|
+
* host.upscalingChanged();
|
|
1896
|
+
* await host.settle();
|
|
1897
|
+
* assert.match(String(host.window("upscale").props.blocker), /Picture Adjustments/);
|
|
1898
|
+
* ```
|
|
1899
|
+
*
|
|
1900
|
+
* @capability None.
|
|
1901
|
+
* @since 1.5.43
|
|
1902
|
+
*/
|
|
1903
|
+
upscalingDecoding(cause: UpscalingDecodingCause, plugins?: readonly UpscalingPlugin[]): UpscalingReason;
|
|
1904
|
+
/**
|
|
1905
|
+
* Send `video.upscaling.changed` with the state {@link TestHost.upscaling}
|
|
1906
|
+
* describes now (without `enabled`), as the player does when it changes
|
|
1907
|
+
* (player 0.5.25); a plugin listening with
|
|
1908
|
+
* `plugin.video.upscaling.onChange` receives it. Nothing reaches a plugin
|
|
1909
|
+
* that does not listen.
|
|
1910
|
+
*
|
|
1911
|
+
* @capability None.
|
|
1912
|
+
*
|
|
1913
|
+
* @example Show the new factor in the plugin's window
|
|
1914
|
+
* The window resized: the test changes the area and the factor, sends
|
|
1915
|
+
* the change, and checks that the plugin's window shows the new factor.
|
|
1916
|
+
* ```ts
|
|
1917
|
+
* import assert from "node:assert/strict";
|
|
1918
|
+
* declare const host: TestHost;
|
|
1919
|
+
* // ---cut---
|
|
1920
|
+
* host.upscaling.area = { width: 2560, height: 1440 };
|
|
1921
|
+
* host.upscaling.factor = 1.33; // 1080p in a 1440p area
|
|
1922
|
+
* host.upscalingChanged(); // as the player after the resize settled
|
|
1923
|
+
* await host.settle();
|
|
1924
|
+
* assert.equal(host.window("upscale").props.factor, "×1.33");
|
|
1925
|
+
* ```
|
|
1926
|
+
*
|
|
1927
|
+
* @since 1.5.43
|
|
1928
|
+
*/
|
|
1929
|
+
upscalingChanged(): void;
|
|
1866
1930
|
/** What `player.chapters()` lists while media is ready. */
|
|
1867
1931
|
chapters: {
|
|
1868
1932
|
/** The chapter title, `null` without one. */
|
package/dist/plugin/testing.js
CHANGED
|
@@ -1837,7 +1837,104 @@ export class TestHost {
|
|
|
1837
1837
|
factor: 2,
|
|
1838
1838
|
checked: false,
|
|
1839
1839
|
renderingStarting: false,
|
|
1840
|
+
video: {
|
|
1841
|
+
width: 1920,
|
|
1842
|
+
height: 1080,
|
|
1843
|
+
displayWidth: 1920,
|
|
1844
|
+
displayHeight: 1080,
|
|
1845
|
+
},
|
|
1846
|
+
area: { width: 3840, height: 2160 },
|
|
1847
|
+
gpu: "NVIDIA GeForce RTX 4070 Laptop GPU",
|
|
1848
|
+
hwdecCurrent: "d3d11va",
|
|
1840
1849
|
};
|
|
1850
|
+
/**
|
|
1851
|
+
* Make the upscaler's reason `decoding` with `cause`, with the player's
|
|
1852
|
+
* message (player 0.5.25), and set `upscaling.hwdecCurrent` to match
|
|
1853
|
+
* (`d3d11va-copy`, `no`, `nvdec`). `plugins` are named for `copy-back`
|
|
1854
|
+
* only, as the player does. Returns the reason; call
|
|
1855
|
+
* {@link TestHost.upscalingChanged} to tell a listening plugin.
|
|
1856
|
+
*
|
|
1857
|
+
* @example Test the window's advice for each cause
|
|
1858
|
+
* The plugin's window explains why upscaling waits. The test fakes the
|
|
1859
|
+
* copy-back cause that Picture Adjustments' filters cause and checks
|
|
1860
|
+
* that the window names the plugin.
|
|
1861
|
+
* ```ts
|
|
1862
|
+
* import assert from "node:assert/strict";
|
|
1863
|
+
* declare const host: TestHost;
|
|
1864
|
+
* // ---cut---
|
|
1865
|
+
* host.upscalingDecoding("copy-back", [{ id: "vestra.video", name: "Picture Adjustments" }]);
|
|
1866
|
+
* host.upscalingChanged();
|
|
1867
|
+
* await host.settle();
|
|
1868
|
+
* assert.match(String(host.window("upscale").props.blocker), /Picture Adjustments/);
|
|
1869
|
+
* ```
|
|
1870
|
+
*
|
|
1871
|
+
* @capability None.
|
|
1872
|
+
* @since 1.5.43
|
|
1873
|
+
*/
|
|
1874
|
+
upscalingDecoding(cause, plugins = []) {
|
|
1875
|
+
const needs = "upscaling needs it decoded on the GPU that renders it (d3d11va)";
|
|
1876
|
+
const names = plugins.map((p) => p.name);
|
|
1877
|
+
const because = names.length === 0
|
|
1878
|
+
? "for example because a video filter needs the frames in memory"
|
|
1879
|
+
: names.length === 1
|
|
1880
|
+
? `because the video filter of ${names[0]} needs the frames in memory`
|
|
1881
|
+
: `because the video filters of ${names.join(", ")} need the frames in memory`;
|
|
1882
|
+
const [message, current] = cause === "copy-back"
|
|
1883
|
+
? [
|
|
1884
|
+
`the video is decoded with copy-back (auto-copy-safe), ${because}; ${needs}`,
|
|
1885
|
+
"d3d11va-copy",
|
|
1886
|
+
]
|
|
1887
|
+
: cause === "hwdec-off"
|
|
1888
|
+
? [
|
|
1889
|
+
"hardware decoding is off; upscaling needs the video decoded on the GPU that renders it (d3d11va)",
|
|
1890
|
+
"no",
|
|
1891
|
+
]
|
|
1892
|
+
: cause === "software"
|
|
1893
|
+
? [`this video is decoded in software; ${needs}`, "no"]
|
|
1894
|
+
: [`this video is decoded with nvdec; ${needs}`, "nvdec"];
|
|
1895
|
+
const reason = cause === "copy-back"
|
|
1896
|
+
? {
|
|
1897
|
+
code: "decoding",
|
|
1898
|
+
message,
|
|
1899
|
+
cause,
|
|
1900
|
+
plugins: plugins.map(({ id, name }) => ({ id, name })),
|
|
1901
|
+
}
|
|
1902
|
+
: { code: "decoding", message, cause };
|
|
1903
|
+
this.upscaling.reason = reason;
|
|
1904
|
+
this.upscaling.hwdecCurrent = current;
|
|
1905
|
+
return reason;
|
|
1906
|
+
}
|
|
1907
|
+
/**
|
|
1908
|
+
* Send `video.upscaling.changed` with the state {@link TestHost.upscaling}
|
|
1909
|
+
* describes now (without `enabled`), as the player does when it changes
|
|
1910
|
+
* (player 0.5.25); a plugin listening with
|
|
1911
|
+
* `plugin.video.upscaling.onChange` receives it. Nothing reaches a plugin
|
|
1912
|
+
* that does not listen.
|
|
1913
|
+
*
|
|
1914
|
+
* @capability None.
|
|
1915
|
+
*
|
|
1916
|
+
* @example Show the new factor in the plugin's window
|
|
1917
|
+
* The window resized: the test changes the area and the factor, sends
|
|
1918
|
+
* the change, and checks that the plugin's window shows the new factor.
|
|
1919
|
+
* ```ts
|
|
1920
|
+
* import assert from "node:assert/strict";
|
|
1921
|
+
* declare const host: TestHost;
|
|
1922
|
+
* // ---cut---
|
|
1923
|
+
* host.upscaling.area = { width: 2560, height: 1440 };
|
|
1924
|
+
* host.upscaling.factor = 1.33; // 1080p in a 1440p area
|
|
1925
|
+
* host.upscalingChanged(); // as the player after the resize settled
|
|
1926
|
+
* await host.settle();
|
|
1927
|
+
* assert.equal(host.window("upscale").props.factor, "×1.33");
|
|
1928
|
+
* ```
|
|
1929
|
+
*
|
|
1930
|
+
* @since 1.5.43
|
|
1931
|
+
*/
|
|
1932
|
+
upscalingChanged() {
|
|
1933
|
+
if (!this.subscriptions.has("video.upscaling.changed"))
|
|
1934
|
+
return;
|
|
1935
|
+
const { enabled: _, ...state } = this.#upscalingState(false);
|
|
1936
|
+
this.emit("video.upscaling.changed", state);
|
|
1937
|
+
}
|
|
1841
1938
|
/** What `player.chapters()` lists while media is ready. */
|
|
1842
1939
|
chapters = [];
|
|
1843
1940
|
#overrides = new Map();
|
|
@@ -3481,12 +3578,20 @@ export class TestHost {
|
|
|
3481
3578
|
message: "the player has not checked the GPU yet; state() or set(true) checks it",
|
|
3482
3579
|
};
|
|
3483
3580
|
const requested = enabled && reason === null && factor !== null && factor > 1;
|
|
3581
|
+
const { video, area, gpu, hwdecCurrent } = this.upscaling;
|
|
3484
3582
|
return {
|
|
3485
3583
|
available: reason === null,
|
|
3486
|
-
reason: reason
|
|
3584
|
+
reason: reason
|
|
3585
|
+
? structuredClone(reason)
|
|
3586
|
+
: null,
|
|
3487
3587
|
enabled,
|
|
3488
3588
|
requested,
|
|
3489
3589
|
factor: requested ? factor : null,
|
|
3590
|
+
video: video ? { ...video } : null,
|
|
3591
|
+
area: area ? { ...area } : null,
|
|
3592
|
+
gpu,
|
|
3593
|
+
hwdecCurrent,
|
|
3594
|
+
inChain: requested,
|
|
3490
3595
|
};
|
|
3491
3596
|
}
|
|
3492
3597
|
/** `rendering.state` from {@link TestHost.rendering} and `videoRender.gpu`. */
|
package/dist/plugin/types.d.ts
CHANGED
|
@@ -6870,7 +6870,7 @@ export interface VideoShaders extends Disposable {
|
|
|
6870
6870
|
* | `gpu-unsupported` | The driver does not offer upscaling on this GPU (Windows: not an RTX GPU), or the check failed. |
|
|
6871
6871
|
* | `rendering` | The player renders in software, or GPU rendering is still starting; upscaling needs GPU rendering ({@link Rendering}). |
|
|
6872
6872
|
* | `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). |
|
|
6873
|
-
* | `decoding` | The video is not decoded on the GPU that renders it: copy-back or software decoding, for example while a {@link VideoFilter} needs the frames in memory. |
|
|
6873
|
+
* | `decoding` | The video is not decoded on the GPU that renders it: copy-back or software decoding, for example while a {@link VideoFilter} needs the frames in memory. {@link UpscalingReason.cause} says which, and {@link UpscalingReason.plugins} whose filters (newer players; see there). |
|
|
6874
6874
|
* | `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. |
|
|
6875
6875
|
*
|
|
6876
6876
|
* `decoding` and `rendering` while GPU rendering is still starting can pass
|
|
@@ -6925,6 +6925,93 @@ export interface UpscalingReason {
|
|
|
6925
6925
|
* and what to change; never branch on it.
|
|
6926
6926
|
*/
|
|
6927
6927
|
readonly message: string;
|
|
6928
|
+
/**
|
|
6929
|
+
* With code `decoding` only: why the video is not decoded on the GPU
|
|
6930
|
+
* that renders it ({@link UpscalingDecodingCause}). Absent with every
|
|
6931
|
+
* other code, and from players before 0.5.25.
|
|
6932
|
+
*
|
|
6933
|
+
* @player 0.5.25; older players send `decoding` without a cause.
|
|
6934
|
+
* @since 1.5.43
|
|
6935
|
+
*/
|
|
6936
|
+
readonly cause?: UpscalingDecodingCause;
|
|
6937
|
+
/**
|
|
6938
|
+
* With cause `copy-back` only: the plugins whose video filters need the
|
|
6939
|
+
* frames in memory, which is why the player switched to copy-back
|
|
6940
|
+
* decoding, in the order the plugins started (each plugin once). Name
|
|
6941
|
+
* them to the user, who can turn the filters off there. Empty when
|
|
6942
|
+
* the copy-back decoding is not one the player chose for a filter (the
|
|
6943
|
+
* user's own `hwdec` setting). Absent with any other cause, and from
|
|
6944
|
+
* players before 0.5.25.
|
|
6945
|
+
*
|
|
6946
|
+
* @player 0.5.25
|
|
6947
|
+
* @since 1.5.43
|
|
6948
|
+
*/
|
|
6949
|
+
readonly plugins?: readonly UpscalingPlugin[];
|
|
6950
|
+
}
|
|
6951
|
+
/**
|
|
6952
|
+
* Why the video is not decoded on the GPU that renders it: the
|
|
6953
|
+
* {@link UpscalingReason.cause} of a `decoding` reason (player 0.5.25). A
|
|
6954
|
+
* newer player may add causes: show {@link UpscalingReason.message} for the
|
|
6955
|
+
* ones you do not handle.
|
|
6956
|
+
*
|
|
6957
|
+
* | Cause | Meaning |
|
|
6958
|
+
* | --- | --- |
|
|
6959
|
+
* | `copy-back` | Decoded on the GPU and copied back to memory, because a plugin's video filter needs the frames there ({@link UpscalingReason.plugins} names it), or because of the user's `hwdec` setting. |
|
|
6960
|
+
* | `hwdec-off` | Hardware decoding is turned off (mpv's `hwdec` is `no`). |
|
|
6961
|
+
* | `software` | This video is decoded in software: a cover picture, a format the GPU does not decode, or a hardware decoder that failed. It passes with the next video. |
|
|
6962
|
+
* | `other-hwdec` | This video is decoded with another hardware decoder than the one the upscaler needs. |
|
|
6963
|
+
*
|
|
6964
|
+
* @example Name the plugins whose filters block upscaling
|
|
6965
|
+
* A plugin's upscaling window explains a `decoding` reason by its cause,
|
|
6966
|
+
* and for `copy-back` names the plugins whose filters stand in the way, so
|
|
6967
|
+
* the user can turn them off there (a plugin cannot run another plugin's
|
|
6968
|
+
* commands).
|
|
6969
|
+
* ```ts
|
|
6970
|
+
* const { reason } = await plugin.video.upscaling.state();
|
|
6971
|
+
* if (reason?.code === "decoding" && reason.cause === "copy-back") {
|
|
6972
|
+
* const names = (reason.plugins ?? []).map((p) => p.name).join(", ");
|
|
6973
|
+
* // Ask the user to reset those filters; the manifest names are not translated.
|
|
6974
|
+
* console.log(names ? `Turn off the video filters of ${names}` : reason.message);
|
|
6975
|
+
* } else if (reason) console.log(reason.message);
|
|
6976
|
+
* ```
|
|
6977
|
+
*
|
|
6978
|
+
* @player 0.5.25
|
|
6979
|
+
* @since 1.5.43
|
|
6980
|
+
* @category Processing
|
|
6981
|
+
*/
|
|
6982
|
+
export type UpscalingDecodingCause = "copy-back" | "hwdec-off" | "software" | "other-hwdec" | (string & {});
|
|
6983
|
+
/**
|
|
6984
|
+
* A plugin named in an {@link UpscalingReason}: its manifest id and its
|
|
6985
|
+
* name for people, as its manifest gives it (not translated).
|
|
6986
|
+
*
|
|
6987
|
+
* @player 0.5.25
|
|
6988
|
+
* @since 1.5.43
|
|
6989
|
+
* @category Processing
|
|
6990
|
+
*/
|
|
6991
|
+
export interface UpscalingPlugin {
|
|
6992
|
+
/** The plugin's manifest id, for example `vestra.video`. */
|
|
6993
|
+
readonly id: string;
|
|
6994
|
+
/** The manifest's `name` as is (its id without one), for people. */
|
|
6995
|
+
readonly name: string;
|
|
6996
|
+
}
|
|
6997
|
+
/**
|
|
6998
|
+
* The video's size as the upscaler gets it ({@link UpscalingState.video}):
|
|
6999
|
+
* the decoder's frame size, width and height swapped for the file's
|
|
7000
|
+
* quarter-turn rotation metadata.
|
|
7001
|
+
*
|
|
7002
|
+
* @player 0.5.25
|
|
7003
|
+
* @since 1.5.43
|
|
7004
|
+
* @category Processing
|
|
7005
|
+
*/
|
|
7006
|
+
export interface UpscalingVideoSize {
|
|
7007
|
+
/** The frames' stored width in pixels. */
|
|
7008
|
+
readonly width: number;
|
|
7009
|
+
/** The frames' stored height in pixels. */
|
|
7010
|
+
readonly height: number;
|
|
7011
|
+
/** The width the video is meant to be shown at (non-square pixels). */
|
|
7012
|
+
readonly displayWidth: number;
|
|
7013
|
+
/** The height the video is meant to be shown at. */
|
|
7014
|
+
readonly displayHeight: number;
|
|
6928
7015
|
}
|
|
6929
7016
|
/**
|
|
6930
7017
|
* What {@link VideoUpscaling.state} and {@link VideoUpscaling.set} report:
|
|
@@ -6980,6 +7067,59 @@ export interface UpscalingState {
|
|
|
6980
7067
|
* nothing is requested.
|
|
6981
7068
|
*/
|
|
6982
7069
|
readonly factor: number | null;
|
|
7070
|
+
/**
|
|
7071
|
+
* The video's size as the upscaler gets it; `null` while no video
|
|
7072
|
+
* decodes. With {@link UpscalingState.area} it explains the factor.
|
|
7073
|
+
* Absent from players before 0.5.25.
|
|
7074
|
+
*
|
|
7075
|
+
* @player 0.5.25
|
|
7076
|
+
* @since 1.5.43
|
|
7077
|
+
*/
|
|
7078
|
+
readonly video?: UpscalingVideoSize | null;
|
|
7079
|
+
/**
|
|
7080
|
+
* The area the video is shown in, in pixels (the upscaler scales the
|
|
7081
|
+
* video to fit it); `null` while the player shows no video area. Absent
|
|
7082
|
+
* from players before 0.5.25.
|
|
7083
|
+
*
|
|
7084
|
+
* @player 0.5.25
|
|
7085
|
+
* @since 1.5.43
|
|
7086
|
+
*/
|
|
7087
|
+
readonly area?: {
|
|
7088
|
+
/** Width in physical pixels. */
|
|
7089
|
+
readonly width: number;
|
|
7090
|
+
/** Height in physical pixels. */
|
|
7091
|
+
readonly height: number;
|
|
7092
|
+
} | null;
|
|
7093
|
+
/**
|
|
7094
|
+
* The name of the GPU that renders, for example `NVIDIA GeForce RTX 4070
|
|
7095
|
+
* Laptop GPU`; `null` in software rendering and where the player does not
|
|
7096
|
+
* know it (macOS, Linux). Absent from players before 0.5.25.
|
|
7097
|
+
*
|
|
7098
|
+
* @player 0.5.25
|
|
7099
|
+
* @since 1.5.43
|
|
7100
|
+
*/
|
|
7101
|
+
readonly gpu?: string | null;
|
|
7102
|
+
/**
|
|
7103
|
+
* The hardware decoder in use for this video, as mpv names it (`d3d11va`,
|
|
7104
|
+
* `d3d11va-copy`; `no` for software decoding); `null` while no video
|
|
7105
|
+
* decodes. For people and logs: branch on {@link UpscalingReason.cause}.
|
|
7106
|
+
* Absent from players before 0.5.25.
|
|
7107
|
+
*
|
|
7108
|
+
* @player 0.5.25
|
|
7109
|
+
* @since 1.5.43
|
|
7110
|
+
*/
|
|
7111
|
+
readonly hwdecCurrent?: string | null;
|
|
7112
|
+
/**
|
|
7113
|
+
* The upscaler is in the player's video filter chain now, read from the
|
|
7114
|
+
* chain itself (`requested` is what the player asked for; they differ
|
|
7115
|
+
* only for a moment, while the chain changes). Still not a sign that the
|
|
7116
|
+
* GPU upscales: the driver gives none (see the remarks). Absent from
|
|
7117
|
+
* players before 0.5.25.
|
|
7118
|
+
*
|
|
7119
|
+
* @player 0.5.25
|
|
7120
|
+
* @since 1.5.43
|
|
7121
|
+
*/
|
|
7122
|
+
readonly inChain?: boolean;
|
|
6983
7123
|
}
|
|
6984
7124
|
/**
|
|
6985
7125
|
* `plugin.video.upscaling`: the GPU's video upscaler, NVIDIA RTX Video
|
|
@@ -7119,6 +7259,57 @@ export interface VideoUpscaling {
|
|
|
7119
7259
|
* @since 1.5.37
|
|
7120
7260
|
*/
|
|
7121
7261
|
set(enabled: boolean): Promise<UpscalingState>;
|
|
7262
|
+
/**
|
|
7263
|
+
* Call `listener` with the new state whenever it changes while some
|
|
7264
|
+
* plugin has upscaling on: the factor, whether the upscaler is in the
|
|
7265
|
+
* chain, the reason, the video's and the area's size, the decoder. A
|
|
7266
|
+
* window that shows how upscaling runs listens instead of polling
|
|
7267
|
+
* {@link VideoUpscaling.state}.
|
|
7268
|
+
*
|
|
7269
|
+
* @remarks
|
|
7270
|
+
* The player looks about five times a second while upscaling is on for
|
|
7271
|
+
* any plugin (a window resize sends at most that many), and sends a
|
|
7272
|
+
* change only when something differs; after the last plugin turns it
|
|
7273
|
+
* off one more change says so, then nothing until it is on again: read
|
|
7274
|
+
* {@link VideoUpscaling.state} when you need the state then. Nothing is
|
|
7275
|
+
* sent before the player's GPU check ran (the first `state()` or
|
|
7276
|
+
* `set(true)` of any plugin). `enabled` is this plugin's own request, as
|
|
7277
|
+
* in `state()`. The message of a `rendering` reason can be shorter than
|
|
7278
|
+
* the one `state()` gives (it may leave out what to do).
|
|
7279
|
+
*
|
|
7280
|
+
* **A listener keeps the plugin running.** `video.upscaling.changed` is
|
|
7281
|
+
* not an event the player wakes a stopped plugin for: while the plugin
|
|
7282
|
+
* listens, the player does not stop it while idle (lazy activation), even
|
|
7283
|
+
* with upscaling off. Listen only while a window that shows the state is
|
|
7284
|
+
* open, and dispose of the listener when it closes. Players before 0.5.25 do not know the event: the listener
|
|
7285
|
+
* never fires and the SDK logs that the player refused the subscription.
|
|
7286
|
+
*
|
|
7287
|
+
* @capability video.upscaling
|
|
7288
|
+
* @param listener - Receives the new state.
|
|
7289
|
+
* @returns Stops listening; also when the scope that added it is
|
|
7290
|
+
* disposed.
|
|
7291
|
+
* @throws `capability` (synchronously) The manifest does not declare
|
|
7292
|
+
* `video.upscaling`.
|
|
7293
|
+
*
|
|
7294
|
+
* @example Show how upscaling runs while a window is open
|
|
7295
|
+
* The plugin's window shows the factor, the sizes and the GPU while it
|
|
7296
|
+
* is open; the listener goes when the window closes, so the plugin can
|
|
7297
|
+
* be stopped while idle again.
|
|
7298
|
+
* ```ts
|
|
7299
|
+
* // When the window opens:
|
|
7300
|
+
* const watch = plugin.video.upscaling.onChange((state) => {
|
|
7301
|
+
* const video = state.video ? `${state.video.width}×${state.video.height}` : "no video";
|
|
7302
|
+
* const area = state.area ? `${state.area.width}×${state.area.height}` : "—";
|
|
7303
|
+
* console.log(`${video} → ${area}, ×${state.factor ?? 1}, ${state.gpu ?? "no GPU"}`);
|
|
7304
|
+
* });
|
|
7305
|
+
* // When it closes: stop listening (it keeps the plugin running).
|
|
7306
|
+
* await watch.dispose();
|
|
7307
|
+
* ```
|
|
7308
|
+
*
|
|
7309
|
+
* @player 0.5.25
|
|
7310
|
+
* @since 1.5.43
|
|
7311
|
+
*/
|
|
7312
|
+
onChange(listener: (state: UpscalingState) => void): Disposable;
|
|
7122
7313
|
}
|
|
7123
7314
|
/**
|
|
7124
7315
|
* A subtitle track of the current source.
|
package/dist/wire.d.ts
CHANGED
|
@@ -338,14 +338,41 @@ export interface WireRenderingState {
|
|
|
338
338
|
reason: WireRenderingReason | null;
|
|
339
339
|
};
|
|
340
340
|
}
|
|
341
|
-
/**
|
|
341
|
+
/**
|
|
342
|
+
* An upscaling reason: `{ code, message }` as a rendering reason; with
|
|
343
|
+
* `decoding` a `cause`, with cause `copy-back` the `plugins` (player 0.5.25).
|
|
344
|
+
*/
|
|
345
|
+
export interface WireUpscalingReason extends WireRenderingReason {
|
|
346
|
+
cause?: string;
|
|
347
|
+
plugins?: {
|
|
348
|
+
id: string;
|
|
349
|
+
name: string;
|
|
350
|
+
}[];
|
|
351
|
+
}
|
|
352
|
+
/**
|
|
353
|
+
* `video.upscaling.state` and `video.upscaling.set` (player 0.5.16); the
|
|
354
|
+
* monitoring fields from player 0.5.25, which also sends the state without
|
|
355
|
+
* `enabled` as the event `video.upscaling.changed`.
|
|
356
|
+
*/
|
|
342
357
|
export interface WireUpscalingState {
|
|
343
358
|
available: boolean;
|
|
344
|
-
|
|
345
|
-
reason: WireRenderingReason | null;
|
|
359
|
+
reason: WireUpscalingReason | null;
|
|
346
360
|
enabled: boolean;
|
|
347
361
|
requested: boolean;
|
|
348
362
|
factor: number | null;
|
|
363
|
+
video?: {
|
|
364
|
+
width: number;
|
|
365
|
+
height: number;
|
|
366
|
+
displayWidth: number;
|
|
367
|
+
displayHeight: number;
|
|
368
|
+
} | null;
|
|
369
|
+
area?: {
|
|
370
|
+
width: number;
|
|
371
|
+
height: number;
|
|
372
|
+
} | null;
|
|
373
|
+
gpu?: string | null;
|
|
374
|
+
hwdecCurrent?: string | null;
|
|
375
|
+
inChain?: boolean;
|
|
349
376
|
}
|
|
350
377
|
/**
|
|
351
378
|
* Template arguments on the wire (`osd.show`, `osd.panel`, `dialogs.confirm`,
|
|
@@ -368,7 +395,7 @@ export interface Transport {
|
|
|
368
395
|
locale?: string;
|
|
369
396
|
}
|
|
370
397
|
/** The events a plugin subscribes to (`events.subscribe`). */
|
|
371
|
-
type HostEvent = "source.state" | "playback.backend" | "window.state" | "packages.changed" | "bindings.changed" | "timeline.changed" | "appearance.changed" | "resolvers.fileTypes";
|
|
398
|
+
type HostEvent = "source.state" | "playback.backend" | "window.state" | "packages.changed" | "bindings.changed" | "timeline.changed" | "appearance.changed" | "resolvers.fileTypes" | "video.upscaling.changed";
|
|
372
399
|
export declare function createWire(transport: Transport): {
|
|
373
400
|
processing: {
|
|
374
401
|
state: () => Promise<import("./processing.ts").ProcessingState>;
|
package/llms-full.txt
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# @vestra-player/sdk 1.5.
|
|
1
|
+
# @vestra-player/sdk 1.5.43: 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
|
|
|
@@ -2281,6 +2281,7 @@ The table lists the API whose documentation names a player version, newest last:
|
|
|
2281
2281
|
| 0.5.16 | UpscalingReasonCode, UpscalingReason, UpscalingState, VideoUpscaling |
|
|
2282
2282
|
| 0.5.20 | VideoShaders |
|
|
2283
2283
|
| 0.5.21 | Scope.when, When, Bindings.onChange |
|
|
2284
|
+
| 0.5.25 | UpscalingReason.cause, UpscalingReason.plugins, UpscalingDecodingCause, UpscalingPlugin, UpscalingVideoSize, UpscalingState.video, UpscalingState.area, UpscalingState.gpu, UpscalingState.hwdecCurrent, UpscalingState.inChain, VideoUpscaling.onChange |
|
|
2284
2285
|
|
|
2285
2286
|
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).
|
|
2286
2287
|
|
|
@@ -2318,7 +2319,7 @@ A plugin lists what it may do in the manifest's `capabilities` (Manifest). A cal
|
|
|
2318
2319
|
| `ui.theme` | Appearance.set, Appearance.setLanguage | — | Change the player's palette | |
|
|
2319
2320
|
| `secrets` | Secrets.get, Secrets.set, Secrets.delete | — | Store sign-in details in the system keychain | yes |
|
|
2320
2321
|
| `rendering.manage` | Rendering.setPreference, Rendering.restartPlayer | — | Choose GPU or software rendering and restart the player | yes |
|
|
2321
|
-
| `video.upscaling` | VideoUpscaling.state, VideoUpscaling.set | — | Upscale the video with the GPU's video upscaler | |
|
|
2322
|
+
| `video.upscaling` | VideoUpscaling.state, VideoUpscaling.set, VideoUpscaling.onChange | — | Upscale the video with the GPU's video upscaler | |
|
|
2322
2323
|
|
|
2323
2324
|
**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:
|
|
2324
2325
|
|
|
@@ -3654,6 +3655,8 @@ It rejects when nothing is bound to the key or the command it would run is busy.
|
|
|
3654
3655
|
| TestHost.app | What `plugin.app` reports. |
|
|
3655
3656
|
| TestHost.rendering | The player's rendering choice behind `plugin.rendering` (player 0.5.13). |
|
|
3656
3657
|
| TestHost.upscaling | The GPU's video upscaler behind `plugin.video.upscaling` (player 0.5.16), answered as the player would: - `state()` and `set(true)` set `checked` (the player's one GPU check; a plugin that should not wake the GPU while it starts can assert it is still `false`) and report `available` from `reason` (`null`: it works), this plugin's `enabled`, and `requested` with `factor` while enabled, available and `factor` is above 1. |
|
|
3658
|
+
| TestHost.upscalingDecoding() | Make the upscaler's reason `decoding` with `cause`, with the player's message (player 0.5.25), and set `upscaling.hwdecCurrent` to match (`d3d11va-copy`, `no`, `nvdec`). |
|
|
3659
|
+
| TestHost.upscalingChanged() | Send `video.upscaling.changed` with the state `TestHost.upscaling` describes now (without `enabled`), as the player does when it changes (player 0.5.25); a plugin listening with `plugin.video.upscaling.onChange` receives it. |
|
|
3657
3660
|
| TestHost.chapters | What `player.chapters()` lists while media is ready. |
|
|
3658
3661
|
| TestHost.dataDirectory | `plugin.dataDirectory`: a fresh empty temporary directory, removed with the host on `stop()`. |
|
|
3659
3662
|
| TestHost.manifestId | The manifest id of the plugin under test (set by `startPlugin`; `"test.plugin"` by default). |
|
|
@@ -4324,8 +4327,9 @@ plugin.video.shaders(id) (player 0.5.20) returns a VideoShaders handle: a list o
|
|
|
4324
4327
|
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.
|
|
4325
4328
|
|
|
4326
4329
|
- **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.
|
|
4327
|
-
- **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.
|
|
4330
|
+
- **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. From player 0.5.25 the reason's `cause` says which decoding stands in the way (`copy-back`, `hwdec-off`, `software`, `other-hwdec`), and with `copy-back` its `plugins` say whose filters, so a window can ask the user to turn them off there.
|
|
4328
4331
|
- **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.
|
|
4332
|
+
- **Watch it while it runs.** From player 0.5.25 the state also has the video's and the area's size, the rendering GPU, the decoder and `inChain`, and onChange sends it as it changes while some plugin has upscaling on. A listener keeps the plugin running, so listen only while a window shows it.
|
|
4329
4333
|
|
|
4330
4334
|
## Explanation: Windows, panels, settings pages, dialogs and the OSD
|
|
4331
4335
|
|
|
@@ -5708,6 +5712,7 @@ Every SDK release's CHANGELOG entry opens with who must change and what happens
|
|
|
5708
5712
|
|
|
5709
5713
|
| Release | Who must change | Guide |
|
|
5710
5714
|
| --- | --- | --- |
|
|
5715
|
+
| [1.5.43](https://github.com/vestrasys/vestra-player-sdk/blob/main/CHANGELOG.md#1543) | **Nobody.** Additions only, `apiVersion` stays `vestra/1`, no new capability. | — |
|
|
5711
5716
|
| [1.5.42](https://github.com/vestrasys/vestra-player-sdk/blob/main/CHANGELOG.md#1542) | **Nobody.** Documentation only: no API changes, `apiVersion` stays `vestra/1`, no player version needed. | — |
|
|
5712
5717
|
| [1.5.41](https://github.com/vestrasys/vestra-player-sdk/blob/main/CHANGELOG.md#1541) | **Nobody.** No API changes, `apiVersion` stays `vestra/1`, no player version needed. | — |
|
|
5713
5718
|
| [1.5.40](https://github.com/vestrasys/vestra-player-sdk/blob/main/CHANGELOG.md#1540) | **Nobody.** Additions only, `apiVersion` stays `vestra/1`. | — |
|
|
@@ -11839,7 +11844,7 @@ in the order the player checks them (the first that holds is reported):
|
|
|
11839
11844
|
| `gpu-unsupported` | The driver does not offer upscaling on this GPU (Windows: not an RTX GPU), or the check failed. |
|
|
11840
11845
|
| `rendering` | The player renders in software, or GPU rendering is still starting; upscaling needs GPU rendering (Rendering). |
|
|
11841
11846
|
| `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). |
|
|
11842
|
-
| `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. |
|
|
11847
|
+
| `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. UpscalingReason.cause says which, and UpscalingReason.plugins whose filters (newer players; see there). |
|
|
11843
11848
|
| `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. |
|
|
11844
11849
|
|
|
11845
11850
|
`decoding` and `rendering` while GPU rendering is still starting can pass
|
|
@@ -11883,6 +11888,16 @@ interface UpscalingReason {
|
|
|
11883
11888
|
readonly code: UpscalingReasonCode;
|
|
11884
11889
|
/** The reason as English text for people, for example with the GPU's name and what to change; never branch on it. */
|
|
11885
11890
|
readonly message: string;
|
|
11891
|
+
/**
|
|
11892
|
+
* With code `decoding` only: why the video is not decoded on the GPU that renders it (UpscalingDecodingCause). Absent with every other code, and from players before 0.5.25.
|
|
11893
|
+
* @player 0.5.25; older players send `decoding` without a cause.
|
|
11894
|
+
*/
|
|
11895
|
+
readonly cause?: UpscalingDecodingCause;
|
|
11896
|
+
/**
|
|
11897
|
+
* With cause `copy-back` only: the plugins whose video filters need the frames in memory, which is why the player switched to copy-back decoding, in the order the plugins started (each plugin once). Name them to the user, who can turn the filters off there. Empty when the copy-back decoding is not one the player chose for a filter (the user's own `hwdec` setting). Absent with any other cause, and from players before 0.5.25.
|
|
11898
|
+
* @player 0.5.25
|
|
11899
|
+
*/
|
|
11900
|
+
readonly plugins?: readonly UpscalingPlugin[];
|
|
11886
11901
|
}
|
|
11887
11902
|
```
|
|
11888
11903
|
|
|
@@ -11897,6 +11912,78 @@ const { available, reason } = await plugin.video.upscaling.state();
|
|
|
11897
11912
|
if (!available && reason) console.log(`${reason.code}: ${reason.message}`);
|
|
11898
11913
|
```
|
|
11899
11914
|
|
|
11915
|
+
### UpscalingDecodingCause
|
|
11916
|
+
|
|
11917
|
+
Why the video is not decoded on the GPU that renders it: the
|
|
11918
|
+
UpscalingReason.cause of a `decoding` reason (player 0.5.25). A
|
|
11919
|
+
newer player may add causes: show UpscalingReason.message for the
|
|
11920
|
+
ones you do not handle.
|
|
11921
|
+
|
|
11922
|
+
| Cause | Meaning |
|
|
11923
|
+
| --- | --- |
|
|
11924
|
+
| `copy-back` | Decoded on the GPU and copied back to memory, because a plugin's video filter needs the frames there (UpscalingReason.plugins names it), or because of the user's `hwdec` setting. |
|
|
11925
|
+
| `hwdec-off` | Hardware decoding is turned off (mpv's `hwdec` is `no`). |
|
|
11926
|
+
| `software` | This video is decoded in software: a cover picture, a format the GPU does not decode, or a hardware decoder that failed. It passes with the next video. |
|
|
11927
|
+
| `other-hwdec` | This video is decoded with another hardware decoder than the one the upscaler needs. |
|
|
11928
|
+
|
|
11929
|
+
- @player 0.5.25
|
|
11930
|
+
|
|
11931
|
+
```ts
|
|
11932
|
+
type UpscalingDecodingCause = "copy-back" | "hwdec-off" | "software" | "other-hwdec" | (string & {});
|
|
11933
|
+
```
|
|
11934
|
+
|
|
11935
|
+
Example: Name the plugins whose filters block upscaling
|
|
11936
|
+
|
|
11937
|
+
A plugin's upscaling window explains a `decoding` reason by its cause,
|
|
11938
|
+
and for `copy-back` names the plugins whose filters stand in the way, so
|
|
11939
|
+
the user can turn them off there (a plugin cannot run another plugin's
|
|
11940
|
+
commands).
|
|
11941
|
+
```ts
|
|
11942
|
+
const { reason } = await plugin.video.upscaling.state();
|
|
11943
|
+
if (reason?.code === "decoding" && reason.cause === "copy-back") {
|
|
11944
|
+
const names = (reason.plugins ?? []).map((p) => p.name).join(", ");
|
|
11945
|
+
// Ask the user to reset those filters; the manifest names are not translated.
|
|
11946
|
+
console.log(names ? `Turn off the video filters of ${names}` : reason.message);
|
|
11947
|
+
} else if (reason) console.log(reason.message);
|
|
11948
|
+
```
|
|
11949
|
+
|
|
11950
|
+
### UpscalingPlugin
|
|
11951
|
+
|
|
11952
|
+
A plugin named in an UpscalingReason: its manifest id and its
|
|
11953
|
+
name for people, as its manifest gives it (not translated).
|
|
11954
|
+
|
|
11955
|
+
- @player 0.5.25
|
|
11956
|
+
|
|
11957
|
+
```ts
|
|
11958
|
+
interface UpscalingPlugin {
|
|
11959
|
+
/** The plugin's manifest id, for example `vestra.video`. */
|
|
11960
|
+
readonly id: string;
|
|
11961
|
+
/** The manifest's `name` as is (its id without one), for people. */
|
|
11962
|
+
readonly name: string;
|
|
11963
|
+
}
|
|
11964
|
+
```
|
|
11965
|
+
|
|
11966
|
+
### UpscalingVideoSize
|
|
11967
|
+
|
|
11968
|
+
The video's size as the upscaler gets it (UpscalingState.video):
|
|
11969
|
+
the decoder's frame size, width and height swapped for the file's
|
|
11970
|
+
quarter-turn rotation metadata.
|
|
11971
|
+
|
|
11972
|
+
- @player 0.5.25
|
|
11973
|
+
|
|
11974
|
+
```ts
|
|
11975
|
+
interface UpscalingVideoSize {
|
|
11976
|
+
/** The frames' stored width in pixels. */
|
|
11977
|
+
readonly width: number;
|
|
11978
|
+
/** The frames' stored height in pixels. */
|
|
11979
|
+
readonly height: number;
|
|
11980
|
+
/** The width the video is meant to be shown at (non-square pixels). */
|
|
11981
|
+
readonly displayWidth: number;
|
|
11982
|
+
/** The height the video is meant to be shown at. */
|
|
11983
|
+
readonly displayHeight: number;
|
|
11984
|
+
}
|
|
11985
|
+
```
|
|
11986
|
+
|
|
11900
11987
|
### UpscalingState
|
|
11901
11988
|
|
|
11902
11989
|
What VideoUpscaling.state and VideoUpscaling.set report:
|
|
@@ -11928,6 +12015,36 @@ interface UpscalingState {
|
|
|
11928
12015
|
* 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.
|
|
11929
12016
|
*/
|
|
11930
12017
|
readonly factor: number | null;
|
|
12018
|
+
/**
|
|
12019
|
+
* The video's size as the upscaler gets it; `null` while no video decodes. With UpscalingState.area it explains the factor. Absent from players before 0.5.25.
|
|
12020
|
+
* @player 0.5.25
|
|
12021
|
+
*/
|
|
12022
|
+
readonly video?: UpscalingVideoSize | null;
|
|
12023
|
+
/**
|
|
12024
|
+
* The area the video is shown in, in pixels (the upscaler scales the video to fit it); `null` while the player shows no video area. Absent from players before 0.5.25.
|
|
12025
|
+
* @player 0.5.25
|
|
12026
|
+
*/
|
|
12027
|
+
readonly area?: {
|
|
12028
|
+
/** Width in physical pixels. */
|
|
12029
|
+
readonly width: number;
|
|
12030
|
+
/** Height in physical pixels. */
|
|
12031
|
+
readonly height: number;
|
|
12032
|
+
} | null;
|
|
12033
|
+
/**
|
|
12034
|
+
* The name of the GPU that renders, for example `NVIDIA GeForce RTX 4070 Laptop GPU`; `null` in software rendering and where the player does not know it (macOS, Linux). Absent from players before 0.5.25.
|
|
12035
|
+
* @player 0.5.25
|
|
12036
|
+
*/
|
|
12037
|
+
readonly gpu?: string | null;
|
|
12038
|
+
/**
|
|
12039
|
+
* The hardware decoder in use for this video, as mpv names it (`d3d11va`, `d3d11va-copy`; `no` for software decoding); `null` while no video decodes. For people and logs: branch on UpscalingReason.cause. Absent from players before 0.5.25.
|
|
12040
|
+
* @player 0.5.25
|
|
12041
|
+
*/
|
|
12042
|
+
readonly hwdecCurrent?: string | null;
|
|
12043
|
+
/**
|
|
12044
|
+
* The upscaler is in the player's video filter chain now, read from the chain itself (`requested` is what the player asked for; they differ only for a moment, while the chain changes). Still not a sign that the GPU upscales: the driver gives none (see the remarks). Absent from players before 0.5.25.
|
|
12045
|
+
* @player 0.5.25
|
|
12046
|
+
*/
|
|
12047
|
+
readonly inChain?: boolean;
|
|
11931
12048
|
}
|
|
11932
12049
|
```
|
|
11933
12050
|
|
|
@@ -12036,6 +12153,46 @@ interface VideoUpscaling {
|
|
|
12036
12153
|
* ```
|
|
12037
12154
|
*/
|
|
12038
12155
|
set(enabled: boolean): Promise<UpscalingState>;
|
|
12156
|
+
/**
|
|
12157
|
+
* Call `listener` with the new state whenever it changes while some plugin has upscaling on: the factor, whether the upscaler is in the chain, the reason, the video's and the area's size, the decoder. A window that shows how upscaling runs listens instead of polling VideoUpscaling.state.
|
|
12158
|
+
*
|
|
12159
|
+
* The player looks about five times a second while upscaling is on for
|
|
12160
|
+
* any plugin (a window resize sends at most that many), and sends a
|
|
12161
|
+
* change only when something differs; after the last plugin turns it
|
|
12162
|
+
* off one more change says so, then nothing until it is on again: read
|
|
12163
|
+
* VideoUpscaling.state when you need the state then. Nothing is
|
|
12164
|
+
* sent before the player's GPU check ran (the first `state()` or
|
|
12165
|
+
* `set(true)` of any plugin). `enabled` is this plugin's own request, as
|
|
12166
|
+
* in `state()`. The message of a `rendering` reason can be shorter than
|
|
12167
|
+
* the one `state()` gives (it may leave out what to do).
|
|
12168
|
+
*
|
|
12169
|
+
* **A listener keeps the plugin running.** `video.upscaling.changed` is
|
|
12170
|
+
* not an event the player wakes a stopped plugin for: while the plugin
|
|
12171
|
+
* listens, the player does not stop it while idle (lazy activation), even
|
|
12172
|
+
* with upscaling off. Listen only while a window that shows the state is
|
|
12173
|
+
* open, and dispose of the listener when it closes. Players before 0.5.25 do not know the event: the listener
|
|
12174
|
+
* never fires and the SDK logs that the player refused the subscription.
|
|
12175
|
+
* @capability video.upscaling
|
|
12176
|
+
* @param listener - Receives the new state.
|
|
12177
|
+
* @returns Stops listening; also when the scope that added it is disposed.
|
|
12178
|
+
* @throws capability (synchronously) The manifest does not declare `video.upscaling`.
|
|
12179
|
+
* @player 0.5.25
|
|
12180
|
+
* @example Show how upscaling runs while a window is open
|
|
12181
|
+
* The plugin's window shows the factor, the sizes and the GPU while it
|
|
12182
|
+
* is open; the listener goes when the window closes, so the plugin can
|
|
12183
|
+
* be stopped while idle again.
|
|
12184
|
+
* ```ts
|
|
12185
|
+
* // When the window opens:
|
|
12186
|
+
* const watch = plugin.video.upscaling.onChange((state) => {
|
|
12187
|
+
* const video = state.video ? `${state.video.width}×${state.video.height}` : "no video";
|
|
12188
|
+
* const area = state.area ? `${state.area.width}×${state.area.height}` : "—";
|
|
12189
|
+
* console.log(`${video} → ${area}, ×${state.factor ?? 1}, ${state.gpu ?? "no GPU"}`);
|
|
12190
|
+
* });
|
|
12191
|
+
* // When it closes: stop listening (it keeps the plugin running).
|
|
12192
|
+
* await watch.dispose();
|
|
12193
|
+
* ```
|
|
12194
|
+
*/
|
|
12195
|
+
onChange(listener: (state: UpscalingState) => void): Disposable;
|
|
12039
12196
|
}
|
|
12040
12197
|
```
|
|
12041
12198
|
|
|
@@ -16227,7 +16384,50 @@ class TestHost implements Transport {
|
|
|
16227
16384
|
checked: boolean;
|
|
16228
16385
|
/** GPU rendering is still starting: a `rendering` reason then lets `set(true)` keep the request. */
|
|
16229
16386
|
renderingStarting: boolean;
|
|
16387
|
+
/** The video's size the state reports (player 0.5.25); `null`: no video. */
|
|
16388
|
+
video: UpscalingVideoSize | null;
|
|
16389
|
+
/** The video area the state reports (player 0.5.25); `null`: none. */
|
|
16390
|
+
area: {
|
|
16391
|
+
/** Width in physical pixels. */
|
|
16392
|
+
width: number;
|
|
16393
|
+
/** Height in physical pixels. */
|
|
16394
|
+
height: number;
|
|
16395
|
+
} | null;
|
|
16396
|
+
/** The rendering GPU's name the state reports (player 0.5.25). */
|
|
16397
|
+
gpu: string | null;
|
|
16398
|
+
/** mpv's decoder the state reports (player 0.5.25); `null`: no video. */
|
|
16399
|
+
hwdecCurrent: string | null;
|
|
16230
16400
|
};
|
|
16401
|
+
/**
|
|
16402
|
+
* Make the upscaler's reason `decoding` with `cause`, with the player's message (player 0.5.25), and set `upscaling.hwdecCurrent` to match (`d3d11va-copy`, `no`, `nvdec`). `plugins` are named for `copy-back` only, as the player does. Returns the reason; call TestHost.upscalingChanged to tell a listening plugin.
|
|
16403
|
+
* @capability None.
|
|
16404
|
+
* @example Test the window's advice for each cause
|
|
16405
|
+
* The plugin's window explains why upscaling waits. The test fakes the
|
|
16406
|
+
* copy-back cause that Picture Adjustments' filters cause and checks
|
|
16407
|
+
* that the window names the plugin.
|
|
16408
|
+
* ```ts
|
|
16409
|
+
* host.upscalingDecoding("copy-back", [{ id: "vestra.video", name: "Picture Adjustments" }]);
|
|
16410
|
+
* host.upscalingChanged();
|
|
16411
|
+
* await host.settle();
|
|
16412
|
+
* assert.match(String(host.window("upscale").props.blocker), /Picture Adjustments/);
|
|
16413
|
+
* ```
|
|
16414
|
+
*/
|
|
16415
|
+
upscalingDecoding(cause: UpscalingDecodingCause, plugins?: readonly UpscalingPlugin[]): UpscalingReason;
|
|
16416
|
+
/**
|
|
16417
|
+
* Send `video.upscaling.changed` with the state TestHost.upscaling describes now (without `enabled`), as the player does when it changes (player 0.5.25); a plugin listening with `plugin.video.upscaling.onChange` receives it. Nothing reaches a plugin that does not listen.
|
|
16418
|
+
* @capability None.
|
|
16419
|
+
* @example Show the new factor in the plugin's window
|
|
16420
|
+
* The window resized: the test changes the area and the factor, sends
|
|
16421
|
+
* the change, and checks that the plugin's window shows the new factor.
|
|
16422
|
+
* ```ts
|
|
16423
|
+
* host.upscaling.area = { width: 2560, height: 1440 };
|
|
16424
|
+
* host.upscaling.factor = 1.33; // 1080p in a 1440p area
|
|
16425
|
+
* host.upscalingChanged(); // as the player after the resize settled
|
|
16426
|
+
* await host.settle();
|
|
16427
|
+
* assert.equal(host.window("upscale").props.factor, "×1.33");
|
|
16428
|
+
* ```
|
|
16429
|
+
*/
|
|
16430
|
+
upscalingChanged(): void;
|
|
16231
16431
|
/** What `player.chapters()` lists while media is ready. */
|
|
16232
16432
|
chapters: {
|
|
16233
16433
|
/** 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.
|
|
3
|
+
> @vestra-player/sdk 1.5.43: 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 (the player's built-in plugins share one). 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: L2216–
|
|
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: L2216–2289.
|
|
37
37
|
|
|
38
38
|
**main.ts**
|
|
39
39
|
|
|
@@ -77,10 +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
|
-
- `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:
|
|
80
|
+
- `video.upscaling`: VideoUpscaling.state/set/onChange
|
|
81
|
+
- No capability: `tools`, `storage`, `system`, `app`, `ui` (images and Markdown), `i18n`, reading window.state, appearance.theme, packages.list and rendering.state. Details: L2290–2350.
|
|
82
82
|
|
|
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: L1861–2019; TestHost:
|
|
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: L1861–2019; TestHost: L15364.
|
|
84
84
|
|
|
85
85
|
**Command line**
|
|
86
86
|
|
|
@@ -108,40 +108,40 @@ export default async function activate(plugin: Plugin) {
|
|
|
108
108
|
- `limit`: A player-wide limit was reached, a property value is over the limits of PropValue
|
|
109
109
|
- `reserved`: The id is `vestra` or starts with `vestra.`
|
|
110
110
|
- `versionMismatch`: The package needs a newer player
|
|
111
|
-
- Others: PluginError, llms-full.txt
|
|
111
|
+
- Others: PluginError, llms-full.txt L5846.
|
|
112
112
|
|
|
113
113
|
## Reference
|
|
114
114
|
|
|
115
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/).
|
|
116
116
|
- Tutorials: Getting started L9–134; Add a window L135–213; Add settings and translations L214–367; Test, check, pack and publish L368–424; Write your own blur kernel L425–685
|
|
117
117
|
- How-to: Control playback L686–854; Store a plugin's data L855–961; Load subtitles from a plugin L962–1074; Keep a slider responsive while dragging L1075–1262; Remember a window's state between runs L1263–1414; Offer a GPU rendering switch L1415–1540; Keep a command's state without keeping the plugin running L1541–1697; Translate a plugin L1698–1860; Test a plugin L1861–2019; Publish a plugin L2020–2121; Troubleshooting L2122–2215
|
|
118
|
-
- Reference: Manifest L2216–
|
|
119
|
-
- Explanation: How a plugin runs
|
|
120
|
-
- Guidelines: Plugin rules
|
|
121
|
-
- Upgrading: Migrate a plugin to English source text
|
|
122
|
-
- [API: @vestra-player/sdk](https://vestrasys.github.io/vestra-player-sdk/modules/_vestra-player_sdk.html):
|
|
123
|
-
- [API: @vestra-player/sdk/testing](https://vestrasys.github.io/vestra-player-sdk/modules/_vestra-player_sdk_testing.html):
|
|
118
|
+
- Reference: Manifest L2216–2289; Capabilities L2290–2350; Limits L2351–2496; Commands, menus and bindings L2497–3022; Platforms L3023–3053; Theme L3054–3312; Command line L3313–3370; Error codes L3371–3418; Translations L3419–3511; Test host L3512–3698
|
|
119
|
+
- Explanation: How a plugin runs L3699–3897; Commands, menus and key bindings L3898–4052; Playback state, controls and sources L4053–4212; Video and audio processing L4213–4333; Windows, panels, settings pages, dialogs and the OSD L4334–4589; GPU and software rendering L4590–4606; The player's Rust core L4607–4948; How the player runs and talks to plugins L4949–5288
|
|
120
|
+
- Guidelines: Plugin rules L5289–5300; Interface guidelines L5301–5417; Menu guidelines L5418–5476
|
|
121
|
+
- Upgrading: Migrate a plugin to English source text L5477–5626; Migrating off package rollback (1.5.16, removed in 1.5.17) L5627–5704; Who must change, release by release L5705–5768
|
|
122
|
+
- [API: @vestra-player/sdk](https://vestrasys.github.io/vestra-player-sdk/modules/_vestra-player_sdk.html): L5769–15037
|
|
123
|
+
- [API: @vestra-player/sdk/testing](https://vestrasys.github.io/vestra-player-sdk/modules/_vestra-player_sdk_testing.html): L15038–17905
|
|
124
124
|
|
|
125
125
|
## API
|
|
126
126
|
|
|
127
127
|
Where each `plugin.<module>` is declared:
|
|
128
128
|
|
|
129
|
-
- Plugin
|
|
130
|
-
- Commands
|
|
129
|
+
- Plugin L6185, Scope L5921, PluginError L5846, Disposable L5784
|
|
130
|
+
- Commands L6772; When L6370; Windows L7709; Panels L9155; Dialogs L9825; Player L10566; video: VideoFilter L11446, VideoView L11546, RenderOptions L11633, VideoShaders L11711, VideoUpscaling L12069; audio: AudioFilter L11491; Subtitles L12305; media: MediaCapture L12709; Resolvers L12939; Packages L13341; Osd L11311; Host L11153; Clipboard L13670; Power L13757; Tools L13816; Storage L13968; System L14167; Secrets L14247; window: MainWindow L9472; App L9652; Rendering L14510; Bindings L13569; Ui L7061; Timeline L9384; Drops L8402; Appearance L8483; I18n L8716; Settings L8999.
|
|
131
131
|
|
|
132
132
|
## Examples
|
|
133
133
|
|
|
134
134
|
Tested plugins in [examples/](https://github.com/vestrasys/vestra-player-sdk/tree/main/examples):
|
|
135
135
|
|
|
136
|
-
- hello: The README Quick start: one menu command and an on-screen message.
|
|
137
|
-
- blur: A tool window with one slider that blurs the playing video.
|
|
138
|
-
- signal-generator: Generates its own video and audio: colour bars and a 1 kHz tone.
|
|
139
|
-
- bookmark: A tool window bound to the playback state: remember the position and jump back to it.
|
|
140
|
-
- panel-menu: A sidebar panel with its own right-click menu.
|
|
141
|
-
- menus: Radio menu items, separator groups and a menu item whose title changes.
|
|
142
|
-
- theme-widgets: A tool window drawn with the player's theme and widgets.
|
|
143
|
-
- markdown: A window that shows a Markdown document.
|
|
144
|
-
- i18n: A plugin in the player's language: English source text, a Chinese translation.
|
|
136
|
+
- hello: The README Quick start: one menu command and an on-screen message. L18340–18435
|
|
137
|
+
- blur: A tool window with one slider that blurs the playing video. L18436–18703
|
|
138
|
+
- signal-generator: Generates its own video and audio: colour bars and a 1 kHz tone. L18704–19042
|
|
139
|
+
- bookmark: A tool window bound to the playback state: remember the position and jump back to it. L19043–19235
|
|
140
|
+
- panel-menu: A sidebar panel with its own right-click menu. L19236–19450
|
|
141
|
+
- menus: Radio menu items, separator groups and a menu item whose title changes. L19451–19616
|
|
142
|
+
- theme-widgets: A tool window drawn with the player's theme and widgets. L19617–19786
|
|
143
|
+
- markdown: A window that shows a Markdown document. L19787–19989
|
|
144
|
+
- i18n: A plugin in the player's language: English source text, a Chinese translation. L19990–20163
|
|
145
145
|
|
|
146
146
|
## Optional
|
|
147
147
|
|