@vestra-player/sdk 1.5.42 → 1.5.44
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 +20 -0
- package/dist/api-requests.js +1 -0
- package/dist/host-capabilities.js +2 -0
- package/dist/player-runtime.d.ts +1 -0
- package/dist/player-runtime.js +1 -0
- package/dist/plugin/patch.d.ts +101 -0
- package/dist/plugin/patch.js +337 -0
- package/dist/plugin/runtime.d.ts +6 -0
- package/dist/plugin/runtime.js +11 -0
- package/dist/plugin/services.js +24 -2
- package/dist/plugin/settings.js +2 -2
- package/dist/plugin/testing.d.ts +65 -1
- package/dist/plugin/testing.js +238 -15
- package/dist/plugin/types.d.ts +192 -1
- package/dist/plugin/ui.d.ts +2 -2
- package/dist/plugin/ui.js +217 -17
- package/dist/wire.d.ts +73 -7
- package/dist/wire.js +3 -3
- package/llms-full.txt +305 -6
- package/llms.txt +23 -23
- package/package.json +1 -1
- package/slint/vestra/markdown.slint +17 -11
- package/slint/vestra/widgets.slint +770 -1
package/dist/plugin/testing.js
CHANGED
|
@@ -86,6 +86,7 @@ import { nodeProblem, VIDEO_PROCESSING_LIMITS } from "../processing.js";
|
|
|
86
86
|
import { shaderIdProblem, shaderListProblem } from "./shaders.js";
|
|
87
87
|
import { RENDER_DEFAULTS, renderProblem, VIEW_DEFAULTS, viewProblem, } from "./view.js";
|
|
88
88
|
import { createPlugin } from "./index.js";
|
|
89
|
+
import { applyOps, PATCH_FEATURE, PATCH_OPS } from "./patch.js";
|
|
89
90
|
import { evaluate } from "./when.js";
|
|
90
91
|
import { WAKE_EVENTS } from "./runtime.js";
|
|
91
92
|
import { THEMES } from "./appearance.js";
|
|
@@ -929,6 +930,54 @@ export class TestHost {
|
|
|
929
930
|
if (reason !== null && reason !== undefined)
|
|
930
931
|
this.#declared(reason, "reason");
|
|
931
932
|
}
|
|
933
|
+
/**
|
|
934
|
+
* Row patches (player 0.6.0): the `seq` of each property's last write, by
|
|
935
|
+
* view and property (none: 0, the initial properties).
|
|
936
|
+
*/
|
|
937
|
+
#writes = new Map();
|
|
938
|
+
#resetWrites(view) {
|
|
939
|
+
for (const slot of [...this.#writes.keys()])
|
|
940
|
+
if (slot.startsWith(`${view}\u0000`))
|
|
941
|
+
this.#writes.delete(slot);
|
|
942
|
+
}
|
|
943
|
+
/**
|
|
944
|
+
* A property write as the player takes it, returning the value the
|
|
945
|
+
* property shows after it: a whole `value`, or a `patch` of the array
|
|
946
|
+
* `current` applying on the write numbered `base` (code `stale`
|
|
947
|
+
* otherwise). The result is checked against the value limits whole: the
|
|
948
|
+
* player counts the rows a patch keeps too.
|
|
949
|
+
*/
|
|
950
|
+
#write(view, property, p, current) {
|
|
951
|
+
const slot = `${view}\u0000${property}`;
|
|
952
|
+
const patch = p.patch;
|
|
953
|
+
if (patch === undefined) {
|
|
954
|
+
const value = this.#shownValues(p.value);
|
|
955
|
+
this.#checkValues({ [property]: value });
|
|
956
|
+
this.#seeImages([value]);
|
|
957
|
+
this.#writes.set(slot, typeof p.seq === "number" ? p.seq : 0);
|
|
958
|
+
return value;
|
|
959
|
+
}
|
|
960
|
+
const { base, seq } = patch;
|
|
961
|
+
if (!Number.isInteger(base) ||
|
|
962
|
+
!Number.isInteger(seq) ||
|
|
963
|
+
base < 0 ||
|
|
964
|
+
seq < 1)
|
|
965
|
+
throw new HostError("invalid", "patch.base and patch.seq require non-negative integers (seq ≥ 1)");
|
|
966
|
+
const stale = () => new HostError("stale", "the property changed since the write this patch applies on; send the whole value");
|
|
967
|
+
if ((this.#writes.get(slot) ?? 0) !== base || !Array.isArray(current))
|
|
968
|
+
throw stale();
|
|
969
|
+
const ops = patchOps(patch.ops, current.length);
|
|
970
|
+
const resolved = ops.map((op) => op.op === "set"
|
|
971
|
+
? { ...op, row: this.#shownValues(op.row) }
|
|
972
|
+
: op.op === "insert"
|
|
973
|
+
? { ...op, rows: op.rows.map((row) => this.#shownValues(row)) }
|
|
974
|
+
: op);
|
|
975
|
+
const value = applyOps(current, resolved);
|
|
976
|
+
this.#checkValues({ [property]: value });
|
|
977
|
+
this.#seeImages(resolved.flatMap((op) => op.op === "set" ? [op.row] : op.op === "insert" ? op.rows : []));
|
|
978
|
+
this.#writes.set(slot, seq);
|
|
979
|
+
return value;
|
|
980
|
+
}
|
|
932
981
|
/**
|
|
933
982
|
* Refuse property values over the player's limits ({@link UI_VALUE_LIMITS})
|
|
934
983
|
* with `limit`, naming the property, as the player does.
|
|
@@ -1837,7 +1886,104 @@ export class TestHost {
|
|
|
1837
1886
|
factor: 2,
|
|
1838
1887
|
checked: false,
|
|
1839
1888
|
renderingStarting: false,
|
|
1889
|
+
video: {
|
|
1890
|
+
width: 1920,
|
|
1891
|
+
height: 1080,
|
|
1892
|
+
displayWidth: 1920,
|
|
1893
|
+
displayHeight: 1080,
|
|
1894
|
+
},
|
|
1895
|
+
area: { width: 3840, height: 2160 },
|
|
1896
|
+
gpu: "NVIDIA GeForce RTX 4070 Laptop GPU",
|
|
1897
|
+
hwdecCurrent: "d3d11va",
|
|
1840
1898
|
};
|
|
1899
|
+
/**
|
|
1900
|
+
* Make the upscaler's reason `decoding` with `cause`, with the player's
|
|
1901
|
+
* message (player 0.5.25), and set `upscaling.hwdecCurrent` to match
|
|
1902
|
+
* (`d3d11va-copy`, `no`, `nvdec`). `plugins` are named for `copy-back`
|
|
1903
|
+
* only, as the player does. Returns the reason; call
|
|
1904
|
+
* {@link TestHost.upscalingChanged} to tell a listening plugin.
|
|
1905
|
+
*
|
|
1906
|
+
* @example Test the window's advice for each cause
|
|
1907
|
+
* The plugin's window explains why upscaling waits. The test fakes the
|
|
1908
|
+
* copy-back cause that Picture Adjustments' filters cause and checks
|
|
1909
|
+
* that the window names the plugin.
|
|
1910
|
+
* ```ts
|
|
1911
|
+
* import assert from "node:assert/strict";
|
|
1912
|
+
* declare const host: TestHost;
|
|
1913
|
+
* // ---cut---
|
|
1914
|
+
* host.upscalingDecoding("copy-back", [{ id: "vestra.video", name: "Picture Adjustments" }]);
|
|
1915
|
+
* host.upscalingChanged();
|
|
1916
|
+
* await host.settle();
|
|
1917
|
+
* assert.match(String(host.window("upscale").props.blocker), /Picture Adjustments/);
|
|
1918
|
+
* ```
|
|
1919
|
+
*
|
|
1920
|
+
* @capability None.
|
|
1921
|
+
* @since 1.5.43
|
|
1922
|
+
*/
|
|
1923
|
+
upscalingDecoding(cause, plugins = []) {
|
|
1924
|
+
const needs = "upscaling needs it decoded on the GPU that renders it (d3d11va)";
|
|
1925
|
+
const names = plugins.map((p) => p.name);
|
|
1926
|
+
const because = names.length === 0
|
|
1927
|
+
? "for example because a video filter needs the frames in memory"
|
|
1928
|
+
: names.length === 1
|
|
1929
|
+
? `because the video filter of ${names[0]} needs the frames in memory`
|
|
1930
|
+
: `because the video filters of ${names.join(", ")} need the frames in memory`;
|
|
1931
|
+
const [message, current] = cause === "copy-back"
|
|
1932
|
+
? [
|
|
1933
|
+
`the video is decoded with copy-back (auto-copy-safe), ${because}; ${needs}`,
|
|
1934
|
+
"d3d11va-copy",
|
|
1935
|
+
]
|
|
1936
|
+
: cause === "hwdec-off"
|
|
1937
|
+
? [
|
|
1938
|
+
"hardware decoding is off; upscaling needs the video decoded on the GPU that renders it (d3d11va)",
|
|
1939
|
+
"no",
|
|
1940
|
+
]
|
|
1941
|
+
: cause === "software"
|
|
1942
|
+
? [`this video is decoded in software; ${needs}`, "no"]
|
|
1943
|
+
: [`this video is decoded with nvdec; ${needs}`, "nvdec"];
|
|
1944
|
+
const reason = cause === "copy-back"
|
|
1945
|
+
? {
|
|
1946
|
+
code: "decoding",
|
|
1947
|
+
message,
|
|
1948
|
+
cause,
|
|
1949
|
+
plugins: plugins.map(({ id, name }) => ({ id, name })),
|
|
1950
|
+
}
|
|
1951
|
+
: { code: "decoding", message, cause };
|
|
1952
|
+
this.upscaling.reason = reason;
|
|
1953
|
+
this.upscaling.hwdecCurrent = current;
|
|
1954
|
+
return reason;
|
|
1955
|
+
}
|
|
1956
|
+
/**
|
|
1957
|
+
* Send `video.upscaling.changed` with the state {@link TestHost.upscaling}
|
|
1958
|
+
* describes now (without `enabled`), as the player does when it changes
|
|
1959
|
+
* (player 0.5.25); a plugin listening with
|
|
1960
|
+
* `plugin.video.upscaling.onChange` receives it. Nothing reaches a plugin
|
|
1961
|
+
* that does not listen.
|
|
1962
|
+
*
|
|
1963
|
+
* @capability None.
|
|
1964
|
+
*
|
|
1965
|
+
* @example Show the new factor in the plugin's window
|
|
1966
|
+
* The window resized: the test changes the area and the factor, sends
|
|
1967
|
+
* the change, and checks that the plugin's window shows the new factor.
|
|
1968
|
+
* ```ts
|
|
1969
|
+
* import assert from "node:assert/strict";
|
|
1970
|
+
* declare const host: TestHost;
|
|
1971
|
+
* // ---cut---
|
|
1972
|
+
* host.upscaling.area = { width: 2560, height: 1440 };
|
|
1973
|
+
* host.upscaling.factor = 1.33; // 1080p in a 1440p area
|
|
1974
|
+
* host.upscalingChanged(); // as the player after the resize settled
|
|
1975
|
+
* await host.settle();
|
|
1976
|
+
* assert.equal(host.window("upscale").props.factor, "×1.33");
|
|
1977
|
+
* ```
|
|
1978
|
+
*
|
|
1979
|
+
* @since 1.5.43
|
|
1980
|
+
*/
|
|
1981
|
+
upscalingChanged() {
|
|
1982
|
+
if (!this.subscriptions.has("video.upscaling.changed"))
|
|
1983
|
+
return;
|
|
1984
|
+
const { enabled: _, ...state } = this.#upscalingState(false);
|
|
1985
|
+
this.emit("video.upscaling.changed", state);
|
|
1986
|
+
}
|
|
1841
1987
|
/** What `player.chapters()` lists while media is ready. */
|
|
1842
1988
|
chapters = [];
|
|
1843
1989
|
#overrides = new Map();
|
|
@@ -3481,12 +3627,20 @@ export class TestHost {
|
|
|
3481
3627
|
message: "the player has not checked the GPU yet; state() or set(true) checks it",
|
|
3482
3628
|
};
|
|
3483
3629
|
const requested = enabled && reason === null && factor !== null && factor > 1;
|
|
3630
|
+
const { video, area, gpu, hwdecCurrent } = this.upscaling;
|
|
3484
3631
|
return {
|
|
3485
3632
|
available: reason === null,
|
|
3486
|
-
reason: reason
|
|
3633
|
+
reason: reason
|
|
3634
|
+
? structuredClone(reason)
|
|
3635
|
+
: null,
|
|
3487
3636
|
enabled,
|
|
3488
3637
|
requested,
|
|
3489
3638
|
factor: requested ? factor : null,
|
|
3639
|
+
video: video ? { ...video } : null,
|
|
3640
|
+
area: area ? { ...area } : null,
|
|
3641
|
+
gpu,
|
|
3642
|
+
hwdecCurrent,
|
|
3643
|
+
inChain: requested,
|
|
3490
3644
|
};
|
|
3491
3645
|
}
|
|
3492
3646
|
/** `rendering.state` from {@link TestHost.rendering} and `videoRender.gpu`. */
|
|
@@ -3711,6 +3865,7 @@ export class TestHost {
|
|
|
3711
3865
|
const id = String(p.id);
|
|
3712
3866
|
if (this.windows.has(id))
|
|
3713
3867
|
throw new HostError("conflict", "view already exists");
|
|
3868
|
+
this.#resetWrites(`w:${id}`);
|
|
3714
3869
|
const properties = this.#shownValues(p.properties);
|
|
3715
3870
|
this.#checkValues(properties);
|
|
3716
3871
|
this.#seeImages(properties);
|
|
@@ -3736,10 +3891,8 @@ export class TestHost {
|
|
|
3736
3891
|
const window = this.windows.get(String(p.id));
|
|
3737
3892
|
if (!window)
|
|
3738
3893
|
throw new HostError("missing", "view not found");
|
|
3739
|
-
const
|
|
3740
|
-
this.#
|
|
3741
|
-
this.#seeImages([value]);
|
|
3742
|
-
window.props[String(p.property)] = value;
|
|
3894
|
+
const property = String(p.property);
|
|
3895
|
+
window.props[property] = this.#write(`w:${String(p.id)}`, property, p, window.props[property]);
|
|
3743
3896
|
},
|
|
3744
3897
|
"ui.show": (p) => {
|
|
3745
3898
|
const window = this.windows.get(String(p.id));
|
|
@@ -3773,6 +3926,7 @@ export class TestHost {
|
|
|
3773
3926
|
]));
|
|
3774
3927
|
const properties = this.#initialValues(this.#shownValues(p.properties));
|
|
3775
3928
|
this.#seeImages(properties);
|
|
3929
|
+
this.#resetWrites("$mount");
|
|
3776
3930
|
const host = this;
|
|
3777
3931
|
this.panel = {
|
|
3778
3932
|
slot: String(p.slot),
|
|
@@ -4097,11 +4251,9 @@ export class TestHost {
|
|
|
4097
4251
|
this.emit("sequence.step", { direction });
|
|
4098
4252
|
},
|
|
4099
4253
|
"ui.setMount": (p) => {
|
|
4100
|
-
this.mounted();
|
|
4101
|
-
const
|
|
4102
|
-
this.#
|
|
4103
|
-
this.#seeImages([value]);
|
|
4104
|
-
this.mounted().props[String(p.property)] = value;
|
|
4254
|
+
const panel = this.mounted();
|
|
4255
|
+
const property = String(p.property);
|
|
4256
|
+
panel.props[property] = this.#write("$mount", property, p, panel.props[property]);
|
|
4105
4257
|
},
|
|
4106
4258
|
"timeline.marks": () => ({ marks: this.timeline.marks }),
|
|
4107
4259
|
"drops.provide": (p) => {
|
|
@@ -4140,6 +4292,7 @@ export class TestHost {
|
|
|
4140
4292
|
throw new HostError("invalid", "a plugin has at most 8 settings pages");
|
|
4141
4293
|
const properties = this.#initialValues(this.#shownValues(p.properties));
|
|
4142
4294
|
this.#seeImages(properties);
|
|
4295
|
+
this.#resetWrites(`$settings:${id}`);
|
|
4143
4296
|
const host = this;
|
|
4144
4297
|
this.settings.pages.set(id, {
|
|
4145
4298
|
category: String(p.category),
|
|
@@ -4162,10 +4315,8 @@ export class TestHost {
|
|
|
4162
4315
|
const page = this.settings.pages.get(String(p.id));
|
|
4163
4316
|
if (!page)
|
|
4164
4317
|
throw new HostError("missing", "page not registered");
|
|
4165
|
-
const
|
|
4166
|
-
this.#
|
|
4167
|
-
this.#seeImages([value]);
|
|
4168
|
-
page.props[String(p.property)] = value;
|
|
4318
|
+
const property = String(p.property);
|
|
4319
|
+
page.props[property] = this.#write(`$settings:${String(p.id)}`, property, p, page.props[property]);
|
|
4169
4320
|
},
|
|
4170
4321
|
"settings.unregister": (p) => {
|
|
4171
4322
|
this.settings.pages.delete(String(p.id));
|
|
@@ -5156,7 +5307,10 @@ export async function startPlugin(activate, options = {}) {
|
|
|
5156
5307
|
mkdirSync(data, { recursive: true });
|
|
5157
5308
|
if (host.fileRoots.length === 0)
|
|
5158
5309
|
host.fileRoots = [directory, data];
|
|
5159
|
-
const instance = await createPlugin(host, manifest, directory, data,
|
|
5310
|
+
const instance = await createPlugin(host, manifest, directory, data,
|
|
5311
|
+
// Like the player: protocol additions it takes (`app.features` set on
|
|
5312
|
+
// the host replaces them, e.g. `[]` for an older player's behaviour).
|
|
5313
|
+
{ features: [PATCH_FEATURE], ...host.app });
|
|
5160
5314
|
await activate(instance.plugin);
|
|
5161
5315
|
// Like the player: a position or resolver another plugin holds is not an
|
|
5162
5316
|
// activation failure.
|
|
@@ -5185,3 +5339,72 @@ export async function startPlugin(activate, options = {}) {
|
|
|
5185
5339
|
},
|
|
5186
5340
|
};
|
|
5187
5341
|
}
|
|
5342
|
+
/**
|
|
5343
|
+
* A patch's ops checked against an array of `length` rows, as the player
|
|
5344
|
+
* checks them (`ui_patch.rs`): in order, each on the result of the ones
|
|
5345
|
+
* before; at most {@link PATCH_OPS}; the result at most
|
|
5346
|
+
* `UI_VALUE_LIMITS.arrayRows` rows.
|
|
5347
|
+
*/
|
|
5348
|
+
function patchOps(ops, length) {
|
|
5349
|
+
if (!Array.isArray(ops))
|
|
5350
|
+
throw new HostError("invalid", "patch.ops requires an array");
|
|
5351
|
+
if (ops.length > PATCH_OPS)
|
|
5352
|
+
throw new HostError("limit", `a patch carries at most ${PATCH_OPS} operations`);
|
|
5353
|
+
const index = (op, key) => {
|
|
5354
|
+
const value = op[key];
|
|
5355
|
+
if (!Number.isInteger(value) || value < 0)
|
|
5356
|
+
throw new HostError("invalid", `patch op ${key} requires a non-negative integer`);
|
|
5357
|
+
return value;
|
|
5358
|
+
};
|
|
5359
|
+
const count = (op) => {
|
|
5360
|
+
const value = index(op, "count");
|
|
5361
|
+
if (value === 0)
|
|
5362
|
+
throw new HostError("invalid", "patch op count must be at least 1");
|
|
5363
|
+
return value;
|
|
5364
|
+
};
|
|
5365
|
+
const past = (what) => new HostError("invalid", `${what} is past the end of the list`);
|
|
5366
|
+
let len = length;
|
|
5367
|
+
return ops.map((raw) => {
|
|
5368
|
+
const op = (raw ?? {});
|
|
5369
|
+
switch (op.op) {
|
|
5370
|
+
case "set": {
|
|
5371
|
+
const at = index(op, "at");
|
|
5372
|
+
if (at >= len)
|
|
5373
|
+
throw past(`set at ${at}`);
|
|
5374
|
+
if (!("row" in op))
|
|
5375
|
+
throw new HostError("invalid", "patch op set requires row");
|
|
5376
|
+
return { op: "set", at, row: op.row };
|
|
5377
|
+
}
|
|
5378
|
+
case "insert": {
|
|
5379
|
+
const at = index(op, "at");
|
|
5380
|
+
const rows = op.rows;
|
|
5381
|
+
if (!Array.isArray(rows) || rows.length === 0)
|
|
5382
|
+
throw new HostError("invalid", "patch op insert requires rows");
|
|
5383
|
+
if (at > len)
|
|
5384
|
+
throw past(`insert at ${at}`);
|
|
5385
|
+
len += rows.length;
|
|
5386
|
+
if (len > UI_VALUE_LIMITS.arrayRows)
|
|
5387
|
+
throw new HostError("limit", `UI array exceeds ${UI_VALUE_LIMITS.arrayRows} rows`);
|
|
5388
|
+
return { op: "insert", at, rows: rows };
|
|
5389
|
+
}
|
|
5390
|
+
case "remove": {
|
|
5391
|
+
const at = index(op, "at");
|
|
5392
|
+
const n = count(op);
|
|
5393
|
+
if (at + n > len)
|
|
5394
|
+
throw past(`remove ${n} at ${at}`);
|
|
5395
|
+
len -= n;
|
|
5396
|
+
return { op: "remove", at, count: n };
|
|
5397
|
+
}
|
|
5398
|
+
case "move": {
|
|
5399
|
+
const from = index(op, "from");
|
|
5400
|
+
const n = count(op);
|
|
5401
|
+
const to = index(op, "to");
|
|
5402
|
+
if (from + n > len || to + n > len)
|
|
5403
|
+
throw past(`move ${n} from ${from} to ${to}`);
|
|
5404
|
+
return { op: "move", from, count: n, to };
|
|
5405
|
+
}
|
|
5406
|
+
default:
|
|
5407
|
+
throw new HostError("invalid", "patch op must be set, insert, remove or move");
|
|
5408
|
+
}
|
|
5409
|
+
});
|
|
5410
|
+
}
|
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/plugin/ui.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Json } from "../wire.ts";
|
|
1
|
+
import type { Json, PropertyWrite } from "../wire.ts";
|
|
2
2
|
import type { ScopeImpl } from "./scope.ts";
|
|
3
3
|
import type { Dialogs, Disposable, ImageOptions, ImageRef, Panels, PropValue, Props, Ui, View, Windows } from "./types.ts";
|
|
4
4
|
/** Slint names are kebab-case; plugin code may use camelCase. */
|
|
@@ -29,7 +29,7 @@ export declare abstract class ViewBase implements View {
|
|
|
29
29
|
protected closed: boolean;
|
|
30
30
|
constructor(scope: ScopeImpl);
|
|
31
31
|
protected abstract live(): boolean;
|
|
32
|
-
protected abstract write(property: string,
|
|
32
|
+
protected abstract write(property: string, write: PropertyWrite): Promise<void>;
|
|
33
33
|
/**
|
|
34
34
|
* Initial properties in wire form. A released image is left out (the
|
|
35
35
|
* property keeps its default) and reported, like a failed write.
|