@vestra-player/sdk 1.5.33 → 1.5.35
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +38 -18
- package/dist/api-requests.js +3 -0
- package/dist/capabilities.d.ts +2 -2
- package/dist/capabilities.js +2 -2
- package/dist/host-capabilities.js +12 -0
- package/dist/player-runtime.d.ts +1 -0
- package/dist/player-runtime.js +1 -0
- package/dist/plugin/scope.d.ts +1 -0
- package/dist/plugin/scope.js +2 -1
- package/dist/plugin/services.d.ts +2 -1
- package/dist/plugin/services.js +22 -2
- package/dist/plugin/subtitles.d.ts +59 -0
- package/dist/plugin/subtitles.js +70 -4
- package/dist/plugin/testing.d.ts +93 -6
- package/dist/plugin/testing.js +210 -10
- package/dist/plugin/types.d.ts +408 -6
- package/dist/wire.d.ts +32 -1
- package/dist/wire.js +11 -0
- package/llms-full.txt +642 -19
- package/llms.txt +22 -21
- package/package.json +1 -1
package/dist/plugin/testing.d.ts
CHANGED
|
@@ -8,13 +8,14 @@ import { CAPABILITY_INFO, MESSAGE_LIMIT } from "../capabilities.ts";
|
|
|
8
8
|
import type { CapabilityInfo } from "../capabilities.ts";
|
|
9
9
|
import { OSD_LIMITS } from "./osd.ts";
|
|
10
10
|
import { SECRET_LIMITS, SESSION_STORAGE_LIMITS } from "./services.ts";
|
|
11
|
+
import { SUBTITLE_LIMITS } from "./subtitles.ts";
|
|
11
12
|
import { BINDING_INPUTS } from "./bindings.ts";
|
|
12
13
|
import { THEME_PALETTES } from "../palettes.ts";
|
|
13
14
|
import { IMAGE_LIMITS, UI_VALUE_LIMITS } from "./values.ts";
|
|
14
15
|
import type { CommandDeclaration } from "../commands.ts";
|
|
15
16
|
import type { SourceState } from "../source.ts";
|
|
16
17
|
import type { SubtitleState } from "../subtitles.ts";
|
|
17
|
-
import type { RenderSettings, ViewSettings } from "./types.ts";
|
|
18
|
+
import type { RenderingMode, RenderingPreference, RenderingReason, RenderSettings, ViewSettings } from "./types.ts";
|
|
18
19
|
import type { MediaJob } from "../media.ts";
|
|
19
20
|
import type { LanguagePreference, Locale, Plugin, TemplateArgs, TextValue, ThemeId } from "./types.ts";
|
|
20
21
|
import { LOCALES, LIST_SEPARATORS, TEXT_LIMITS } from "../i18n.ts";
|
|
@@ -38,6 +39,8 @@ export { OSD_LIMITS };
|
|
|
38
39
|
export { SECRET_LIMITS };
|
|
39
40
|
/** The player's limits on `plugin.storage.session`. */
|
|
40
41
|
export { SESSION_STORAGE_LIMITS };
|
|
42
|
+
/** The player's limits on loaded subtitle files, which the test host enforces as well. */
|
|
43
|
+
export { SUBTITLE_LIMITS };
|
|
41
44
|
/** The names bindings are made of. */
|
|
42
45
|
export { BINDING_INPUTS };
|
|
43
46
|
/** The colours of the player's palettes, by `Theme` token. */
|
|
@@ -466,7 +469,7 @@ export interface MarkdownWire {
|
|
|
466
469
|
* {@link TestHost.app}, {@link TestHost.systemInfo},
|
|
467
470
|
* {@link TestHost.chapters}, {@link TestHost.audioDevices},
|
|
468
471
|
* {@link TestHost.secrets}, {@link TestHost.clipboard},
|
|
469
|
-
* {@link TestHost.tools}.
|
|
472
|
+
* {@link TestHost.tools}, {@link TestHost.rendering}.
|
|
470
473
|
* - **Assert** on records: {@link TestHost.osd}, {@link TestHost.calls},
|
|
471
474
|
* {@link TestHost.sent}, {@link TestHost.conflicts},
|
|
472
475
|
* {@link TestHost.layout}, {@link TestHost.menus}, and many more.
|
|
@@ -1270,7 +1273,10 @@ export declare class TestHost implements Transport {
|
|
|
1270
1273
|
owner: string | null;
|
|
1271
1274
|
/** The values the player holds. */
|
|
1272
1275
|
values: RenderSettings;
|
|
1273
|
-
/**
|
|
1276
|
+
/**
|
|
1277
|
+
* `true`: GPU rendering; `false`: software rendering (`unsupported`).
|
|
1278
|
+
* `plugin.app.system()` reports it as `rendering`.
|
|
1279
|
+
*/
|
|
1274
1280
|
gpu: boolean;
|
|
1275
1281
|
};
|
|
1276
1282
|
/**
|
|
@@ -1300,7 +1306,8 @@ export declare class TestHost implements Transport {
|
|
|
1300
1306
|
/**
|
|
1301
1307
|
* What the player pushes when mpv cannot compile a shader (code `failed`)
|
|
1302
1308
|
* or switches to software rendering (`unsupported`, which also sets
|
|
1303
|
-
* `videoRender.gpu` to false
|
|
1309
|
+
* `videoRender.gpu` to false and `rendering.reason` to `window-fallback`
|
|
1310
|
+
* with `message`). Pushed only when this plugin is the owner.
|
|
1304
1311
|
*
|
|
1305
1312
|
* @example Make a shader fail to compile
|
|
1306
1313
|
* The plugin's render options report `failed`, as when mpv rejects a
|
|
@@ -1385,6 +1392,13 @@ export declare class TestHost implements Transport {
|
|
|
1385
1392
|
bytes: Buffer[];
|
|
1386
1393
|
/** The announced total size in bytes. */
|
|
1387
1394
|
expected: number;
|
|
1395
|
+
/**
|
|
1396
|
+
* The source generation it was begun for; like the player, a new
|
|
1397
|
+
* source drops the uploads of the old one.
|
|
1398
|
+
*
|
|
1399
|
+
* @since 1.5.34
|
|
1400
|
+
*/
|
|
1401
|
+
generation?: number;
|
|
1388
1402
|
}>;
|
|
1389
1403
|
/** Snapshots taken with `media.capture`, by snapshot id. Wire-level. */
|
|
1390
1404
|
readonly snapshots: Map<number, Json>;
|
|
@@ -1595,8 +1609,13 @@ export declare class TestHost implements Transport {
|
|
|
1595
1609
|
* @since 1.5.25
|
|
1596
1610
|
*/
|
|
1597
1611
|
readonly sessionStorage: Map<string, Json>;
|
|
1598
|
-
/**
|
|
1599
|
-
|
|
1612
|
+
/**
|
|
1613
|
+
* What `plugin.app.system()` reports. Its `rendering` follows
|
|
1614
|
+
* {@link TestHost.videoRender | videoRender.gpu} (`"gpu"` or
|
|
1615
|
+
* `"software"`), so one switch covers the render options and the fact;
|
|
1616
|
+
* its `renderingReason` is {@link TestHost.rendering | rendering.reason}.
|
|
1617
|
+
*/
|
|
1618
|
+
systemInfo: Omit<WireSystemInfo, "rendering" | "renderingReason">;
|
|
1600
1619
|
/**
|
|
1601
1620
|
* What `plugin.app` reports. Set it before {@link startPlugin}: the plugin
|
|
1602
1621
|
* receives it once at start-up.
|
|
@@ -1616,6 +1635,74 @@ export declare class TestHost implements Transport {
|
|
|
1616
1635
|
slint: string;
|
|
1617
1636
|
};
|
|
1618
1637
|
};
|
|
1638
|
+
/**
|
|
1639
|
+
* The player's rendering choice behind `plugin.rendering` (player
|
|
1640
|
+
* 0.5.13). The mode drawing now is {@link TestHost.videoRender |
|
|
1641
|
+
* videoRender.gpu}, as for `plugin.app.system()`; the rest is preset here
|
|
1642
|
+
* and changed by the plugin's calls, as the player would:
|
|
1643
|
+
*
|
|
1644
|
+
* - `setPreference` saves `preference` and never changes the mode. It
|
|
1645
|
+
* refuses `"gpu"` while `gpu.available` is false: with `unsupported`
|
|
1646
|
+
* when `gpu.reason.code` is `no-gpu-backend` or `headless`, otherwise
|
|
1647
|
+
* with `unavailable`, the reason's message in both cases.
|
|
1648
|
+
* - The next start uses `"software"` for the `"software"` choice, else
|
|
1649
|
+
* software with `gpu.reason` while the GPU is not available, else the
|
|
1650
|
+
* GPU (code `preference` for a `"gpu"` choice, `default` for
|
|
1651
|
+
* `"auto"`). `restartPending` compares that with `startedWith`.
|
|
1652
|
+
* - `restartPlayer` counts in `restarts` and leaves the plugin running
|
|
1653
|
+
* (call `stop()` yourself); a second one rejects with `busy`, as a
|
|
1654
|
+
* restart is then under way.
|
|
1655
|
+
*
|
|
1656
|
+
* Inject other refusals (`failed` for an unwritable settings file or a
|
|
1657
|
+
* new process that could not start) with `host.handle("rendering.setPreference", …)`
|
|
1658
|
+
* or `host.handle("rendering.restart", …)`. The defaults are a macOS
|
|
1659
|
+
* player on the GPU: `"auto"`, available, started on the GPU.
|
|
1660
|
+
*
|
|
1661
|
+
* @example Test a GPU switch on a system without a GPU renderer
|
|
1662
|
+
* The plugin's GPU menu item is greyed out with the player's reason when
|
|
1663
|
+
* the GPU cannot be chosen. The host is preset before the plugin starts,
|
|
1664
|
+
* because the plugin reads the state while activating; software
|
|
1665
|
+
* rendering is `videoRender.gpu = false`, and `gpu` says why the GPU
|
|
1666
|
+
* cannot be chosen.
|
|
1667
|
+
* ```ts
|
|
1668
|
+
* import assert from "node:assert/strict";
|
|
1669
|
+
* import { startPlugin, TestHost } from "@vestra-player/sdk/testing";
|
|
1670
|
+
* import activate from "../main.ts";
|
|
1671
|
+
*
|
|
1672
|
+
* const host = new TestHost();
|
|
1673
|
+
* host.videoRender.gpu = false; // software draws now (Windows)
|
|
1674
|
+
* host.rendering.startedWith = "software";
|
|
1675
|
+
* host.rendering.reason = { code: "no-gpu-backend", message: "No GPU renderer on Windows" };
|
|
1676
|
+
* host.rendering.gpu = { available: false, reason: host.rendering.reason };
|
|
1677
|
+
* const { stop } = await startPlugin(activate, {
|
|
1678
|
+
* directory: new URL("..", import.meta.url),
|
|
1679
|
+
* host,
|
|
1680
|
+
* });
|
|
1681
|
+
* const item = host.command("gpu-rendering");
|
|
1682
|
+
* assert.equal(item.state.enabled, false);
|
|
1683
|
+
* assert.equal(item.state.reason, "No GPU renderer on Windows");
|
|
1684
|
+
* await stop();
|
|
1685
|
+
* ```
|
|
1686
|
+
*
|
|
1687
|
+
* @since 1.5.35
|
|
1688
|
+
*/
|
|
1689
|
+
readonly rendering: {
|
|
1690
|
+
/** Why the mode drawing now is what it is (`RenderingState.reason`, `SystemInfo.renderingReason`). */
|
|
1691
|
+
reason: RenderingReason;
|
|
1692
|
+
/** The saved choice: `"auto"`, `"gpu"` or `"software"`. */
|
|
1693
|
+
preference: RenderingPreference;
|
|
1694
|
+
/** The mode this run started with; `restartPending` compares the next start's mode with it. */
|
|
1695
|
+
startedWith: RenderingMode;
|
|
1696
|
+
/** Whether the plugin may choose `"gpu"`, and why not (`null` while available). */
|
|
1697
|
+
gpu: {
|
|
1698
|
+
/** `false`: `setPreference("gpu")` is refused. */
|
|
1699
|
+
available: boolean;
|
|
1700
|
+
/** Why the GPU cannot be chosen; `null` while it can. */
|
|
1701
|
+
reason: RenderingReason | null;
|
|
1702
|
+
};
|
|
1703
|
+
/** Accepted `restartPlayer` calls. */
|
|
1704
|
+
restarts: number;
|
|
1705
|
+
};
|
|
1619
1706
|
/** What `player.chapters()` lists while media is ready. */
|
|
1620
1707
|
chapters: {
|
|
1621
1708
|
/** The chapter title, `null` without one. */
|
package/dist/plugin/testing.js
CHANGED
|
@@ -77,6 +77,7 @@ import { BAR_BUTTON_ICONS, BAR_BUTTON_LIMITS, BUILTIN_COMMANDS, BUILTIN_SUBMENUS
|
|
|
77
77
|
import { CAPABILITY_INFO, MESSAGE_LIMIT } from "../capabilities.js";
|
|
78
78
|
import { OSD_LIMITS } from "./osd.js";
|
|
79
79
|
import { SECRET_LIMITS, SESSION_STORAGE_LIMITS } from "./services.js";
|
|
80
|
+
import { SUBTITLE_LIMITS } from "./subtitles.js";
|
|
80
81
|
import { BINDING_INPUTS } from "./bindings.js";
|
|
81
82
|
import { THEME_PALETTES } from "../palettes.js";
|
|
82
83
|
import { IMAGE_LIMITS, UI_VALUE_LIMITS, valueLimitProblem } from "./values.js";
|
|
@@ -104,6 +105,8 @@ export { OSD_LIMITS };
|
|
|
104
105
|
export { SECRET_LIMITS };
|
|
105
106
|
/** The player's limits on `plugin.storage.session`. */
|
|
106
107
|
export { SESSION_STORAGE_LIMITS };
|
|
108
|
+
/** The player's limits on loaded subtitle files, which the test host enforces as well. */
|
|
109
|
+
export { SUBTITLE_LIMITS };
|
|
107
110
|
/** The names bindings are made of. */
|
|
108
111
|
export { BINDING_INPUTS };
|
|
109
112
|
/** The colours of the player's palettes, by `Theme` token. */
|
|
@@ -265,7 +268,7 @@ const MAX_SIDEBAR_TABS = 8;
|
|
|
265
268
|
* {@link TestHost.app}, {@link TestHost.systemInfo},
|
|
266
269
|
* {@link TestHost.chapters}, {@link TestHost.audioDevices},
|
|
267
270
|
* {@link TestHost.secrets}, {@link TestHost.clipboard},
|
|
268
|
-
* {@link TestHost.tools}.
|
|
271
|
+
* {@link TestHost.tools}, {@link TestHost.rendering}.
|
|
269
272
|
* - **Assert** on records: {@link TestHost.osd}, {@link TestHost.calls},
|
|
270
273
|
* {@link TestHost.sent}, {@link TestHost.conflicts},
|
|
271
274
|
* {@link TestHost.layout}, {@link TestHost.menus}, and many more.
|
|
@@ -1264,7 +1267,8 @@ export class TestHost {
|
|
|
1264
1267
|
/**
|
|
1265
1268
|
* What the player pushes when mpv cannot compile a shader (code `failed`)
|
|
1266
1269
|
* or switches to software rendering (`unsupported`, which also sets
|
|
1267
|
-
* `videoRender.gpu` to false
|
|
1270
|
+
* `videoRender.gpu` to false and `rendering.reason` to `window-fallback`
|
|
1271
|
+
* with `message`). Pushed only when this plugin is the owner.
|
|
1268
1272
|
*
|
|
1269
1273
|
* @example Make a shader fail to compile
|
|
1270
1274
|
* The plugin's render options report `failed`, as when mpv rejects a
|
|
@@ -1287,8 +1291,10 @@ export class TestHost {
|
|
|
1287
1291
|
* @since 1.0.0
|
|
1288
1292
|
*/
|
|
1289
1293
|
failVideoRender(code, message) {
|
|
1290
|
-
if (code === "unsupported")
|
|
1294
|
+
if (code === "unsupported") {
|
|
1291
1295
|
this.videoRender.gpu = false;
|
|
1296
|
+
this.rendering.reason = { code: "window-fallback", message };
|
|
1297
|
+
}
|
|
1292
1298
|
if (this.videoRender.owner === this.manifestId)
|
|
1293
1299
|
this.emit("video.render.status", {
|
|
1294
1300
|
state: "failed",
|
|
@@ -1521,7 +1527,12 @@ export class TestHost {
|
|
|
1521
1527
|
* @since 1.5.25
|
|
1522
1528
|
*/
|
|
1523
1529
|
sessionStorage = new Map();
|
|
1524
|
-
/**
|
|
1530
|
+
/**
|
|
1531
|
+
* What `plugin.app.system()` reports. Its `rendering` follows
|
|
1532
|
+
* {@link TestHost.videoRender | videoRender.gpu} (`"gpu"` or
|
|
1533
|
+
* `"software"`), so one switch covers the render options and the fact;
|
|
1534
|
+
* its `renderingReason` is {@link TestHost.rendering | rendering.reason}.
|
|
1535
|
+
*/
|
|
1525
1536
|
systemInfo = {
|
|
1526
1537
|
os: "macos",
|
|
1527
1538
|
name: "macOS",
|
|
@@ -1553,6 +1564,64 @@ export class TestHost {
|
|
|
1553
1564
|
slint: "1.18.1",
|
|
1554
1565
|
},
|
|
1555
1566
|
};
|
|
1567
|
+
/**
|
|
1568
|
+
* The player's rendering choice behind `plugin.rendering` (player
|
|
1569
|
+
* 0.5.13). The mode drawing now is {@link TestHost.videoRender |
|
|
1570
|
+
* videoRender.gpu}, as for `plugin.app.system()`; the rest is preset here
|
|
1571
|
+
* and changed by the plugin's calls, as the player would:
|
|
1572
|
+
*
|
|
1573
|
+
* - `setPreference` saves `preference` and never changes the mode. It
|
|
1574
|
+
* refuses `"gpu"` while `gpu.available` is false: with `unsupported`
|
|
1575
|
+
* when `gpu.reason.code` is `no-gpu-backend` or `headless`, otherwise
|
|
1576
|
+
* with `unavailable`, the reason's message in both cases.
|
|
1577
|
+
* - The next start uses `"software"` for the `"software"` choice, else
|
|
1578
|
+
* software with `gpu.reason` while the GPU is not available, else the
|
|
1579
|
+
* GPU (code `preference` for a `"gpu"` choice, `default` for
|
|
1580
|
+
* `"auto"`). `restartPending` compares that with `startedWith`.
|
|
1581
|
+
* - `restartPlayer` counts in `restarts` and leaves the plugin running
|
|
1582
|
+
* (call `stop()` yourself); a second one rejects with `busy`, as a
|
|
1583
|
+
* restart is then under way.
|
|
1584
|
+
*
|
|
1585
|
+
* Inject other refusals (`failed` for an unwritable settings file or a
|
|
1586
|
+
* new process that could not start) with `host.handle("rendering.setPreference", …)`
|
|
1587
|
+
* or `host.handle("rendering.restart", …)`. The defaults are a macOS
|
|
1588
|
+
* player on the GPU: `"auto"`, available, started on the GPU.
|
|
1589
|
+
*
|
|
1590
|
+
* @example Test a GPU switch on a system without a GPU renderer
|
|
1591
|
+
* The plugin's GPU menu item is greyed out with the player's reason when
|
|
1592
|
+
* the GPU cannot be chosen. The host is preset before the plugin starts,
|
|
1593
|
+
* because the plugin reads the state while activating; software
|
|
1594
|
+
* rendering is `videoRender.gpu = false`, and `gpu` says why the GPU
|
|
1595
|
+
* cannot be chosen.
|
|
1596
|
+
* ```ts
|
|
1597
|
+
* import assert from "node:assert/strict";
|
|
1598
|
+
* import { startPlugin, TestHost } from "@vestra-player/sdk/testing";
|
|
1599
|
+
* import activate from "../main.ts";
|
|
1600
|
+
*
|
|
1601
|
+
* const host = new TestHost();
|
|
1602
|
+
* host.videoRender.gpu = false; // software draws now (Windows)
|
|
1603
|
+
* host.rendering.startedWith = "software";
|
|
1604
|
+
* host.rendering.reason = { code: "no-gpu-backend", message: "No GPU renderer on Windows" };
|
|
1605
|
+
* host.rendering.gpu = { available: false, reason: host.rendering.reason };
|
|
1606
|
+
* const { stop } = await startPlugin(activate, {
|
|
1607
|
+
* directory: new URL("..", import.meta.url),
|
|
1608
|
+
* host,
|
|
1609
|
+
* });
|
|
1610
|
+
* const item = host.command("gpu-rendering");
|
|
1611
|
+
* assert.equal(item.state.enabled, false);
|
|
1612
|
+
* assert.equal(item.state.reason, "No GPU renderer on Windows");
|
|
1613
|
+
* await stop();
|
|
1614
|
+
* ```
|
|
1615
|
+
*
|
|
1616
|
+
* @since 1.5.35
|
|
1617
|
+
*/
|
|
1618
|
+
rendering = {
|
|
1619
|
+
reason: { code: "default", message: "nothing rules GPU rendering out" },
|
|
1620
|
+
preference: "auto",
|
|
1621
|
+
startedWith: "gpu",
|
|
1622
|
+
gpu: { available: true, reason: null },
|
|
1623
|
+
restarts: 0,
|
|
1624
|
+
};
|
|
1556
1625
|
/** What `player.chapters()` lists while media is ready. */
|
|
1557
1626
|
chapters = [];
|
|
1558
1627
|
#overrides = new Map();
|
|
@@ -3135,17 +3204,102 @@ export class TestHost {
|
|
|
3135
3204
|
!["ready", "ended"].includes(this.source.phase))
|
|
3136
3205
|
throw new HostError("unavailable", "audio tracks need ready media");
|
|
3137
3206
|
}
|
|
3138
|
-
|
|
3207
|
+
/** `stale` unless `expected` is the current subtitle stamp. */
|
|
3208
|
+
#subtitleExpected(expected) {
|
|
3139
3209
|
const stamp = this.subtitles.selection.stamp;
|
|
3140
3210
|
const want = expected;
|
|
3141
3211
|
if (want.generation !== stamp.generation ||
|
|
3142
3212
|
want.revision !== stamp.revision)
|
|
3143
3213
|
throw new HostError("stale", "subtitle selection changed");
|
|
3214
|
+
}
|
|
3215
|
+
/**
|
|
3216
|
+
* The resources this plugin loaded into the current source (resource id →
|
|
3217
|
+
* bytes), removed ones included: like the player, a removed track frees
|
|
3218
|
+
* its place among the live tracks but not among the loads.
|
|
3219
|
+
*/
|
|
3220
|
+
#subtitleResources = { generation: -1, bytes: new Map() };
|
|
3221
|
+
#loadedSubtitles() {
|
|
3222
|
+
const generation = this.source.stamp.generation;
|
|
3223
|
+
if (this.#subtitleResources.generation !== generation)
|
|
3224
|
+
this.#subtitleResources = { generation, bytes: new Map() };
|
|
3225
|
+
return this.#subtitleResources.bytes;
|
|
3226
|
+
}
|
|
3227
|
+
/**
|
|
3228
|
+
* The player's `SUBTITLE_LIMITS` for a new resource (`busy`, as the
|
|
3229
|
+
* player reports them). External tracks in `subtitles.tracks` that this
|
|
3230
|
+
* plugin did not load count as other plugins'. `totalBytes` is not
|
|
3231
|
+
* modelled: one plugin's 4 files of at most 2 MiB stay below it, and the
|
|
3232
|
+
* bytes of other plugins' tracks are unknown.
|
|
3233
|
+
*/
|
|
3234
|
+
#subtitleRoom(resourceId) {
|
|
3235
|
+
const loaded = this.#loadedSubtitles();
|
|
3236
|
+
if (loaded.has(resourceId))
|
|
3237
|
+
return;
|
|
3238
|
+
const external = this.subtitles.tracks.filter((t) => t.target.kind === "external");
|
|
3239
|
+
const own = external.filter((t) => t.target.kind === "external" && loaded.has(t.target.resourceId));
|
|
3240
|
+
const others = external.length - own.length;
|
|
3241
|
+
if (loaded.size >= SUBTITLE_LIMITS.loadsPerPlugin ||
|
|
3242
|
+
loaded.size + others >= SUBTITLE_LIMITS.loads ||
|
|
3243
|
+
own.length >= SUBTITLE_LIMITS.tracksPerPlugin ||
|
|
3244
|
+
external.length >= SUBTITLE_LIMITS.tracks)
|
|
3245
|
+
throw new HostError("busy", "subtitle resource or counter limit exceeded");
|
|
3246
|
+
}
|
|
3247
|
+
#subtitleChange(expected) {
|
|
3248
|
+
this.#subtitleExpected(expected);
|
|
3249
|
+
const stamp = this.subtitles.selection.stamp;
|
|
3144
3250
|
this.subtitles.selection.stamp = {
|
|
3145
3251
|
generation: this.source.stamp.generation,
|
|
3146
3252
|
revision: stamp.revision + 1,
|
|
3147
3253
|
};
|
|
3148
3254
|
}
|
|
3255
|
+
/** `rendering.state` from {@link TestHost.rendering} and `videoRender.gpu`. */
|
|
3256
|
+
#renderingState() {
|
|
3257
|
+
const { reason, preference, startedWith, gpu } = this.rendering;
|
|
3258
|
+
// The player's order (render_mode.rs decide): no windows or no GPU
|
|
3259
|
+
// renderer first, then the preference `software`, then what keeps GPU
|
|
3260
|
+
// rendering off (denylist, crash), else GPU with the reason `default`
|
|
3261
|
+
// (also when the preference is `gpu`).
|
|
3262
|
+
const system = !gpu.available &&
|
|
3263
|
+
(gpu.reason?.code === "headless" ||
|
|
3264
|
+
gpu.reason?.code === "no-gpu-backend");
|
|
3265
|
+
const next = system
|
|
3266
|
+
? { mode: "software", reason: gpu.reason }
|
|
3267
|
+
: preference === "software"
|
|
3268
|
+
? {
|
|
3269
|
+
mode: "software",
|
|
3270
|
+
reason: {
|
|
3271
|
+
code: "preference",
|
|
3272
|
+
message: "chosen in the player's settings",
|
|
3273
|
+
},
|
|
3274
|
+
}
|
|
3275
|
+
: !gpu.available
|
|
3276
|
+
? {
|
|
3277
|
+
mode: "software",
|
|
3278
|
+
reason: gpu.reason ?? {
|
|
3279
|
+
code: "default",
|
|
3280
|
+
message: "nothing rules GPU rendering out",
|
|
3281
|
+
},
|
|
3282
|
+
}
|
|
3283
|
+
: {
|
|
3284
|
+
mode: "gpu",
|
|
3285
|
+
reason: {
|
|
3286
|
+
code: "default",
|
|
3287
|
+
message: "nothing rules GPU rendering out",
|
|
3288
|
+
},
|
|
3289
|
+
};
|
|
3290
|
+
return {
|
|
3291
|
+
mode: this.videoRender.gpu ? "gpu" : "software",
|
|
3292
|
+
reason: { ...reason },
|
|
3293
|
+
preference,
|
|
3294
|
+
nextMode: next.mode,
|
|
3295
|
+
nextReason: { ...next.reason },
|
|
3296
|
+
restartPending: next.mode !== startedWith,
|
|
3297
|
+
gpu: {
|
|
3298
|
+
available: gpu.available,
|
|
3299
|
+
reason: gpu.available || !gpu.reason ? null : { ...gpu.reason },
|
|
3300
|
+
},
|
|
3301
|
+
};
|
|
3302
|
+
}
|
|
3149
3303
|
#defaults = {
|
|
3150
3304
|
"events.subscribe": (p) => void this.subscriptions.add(String(p.event)),
|
|
3151
3305
|
"events.unsubscribe": (p) => void this.subscriptions.delete(String(p.event)),
|
|
@@ -3459,7 +3613,33 @@ export class TestHost {
|
|
|
3459
3613
|
},
|
|
3460
3614
|
"storage.session.delete": (p) => void this.sessionStorage.delete(sessionKey(p.key)),
|
|
3461
3615
|
"storage.session.keys": () => [...this.sessionStorage.keys()].sort(byCodePoint),
|
|
3462
|
-
"app.system": () =>
|
|
3616
|
+
"app.system": () => ({
|
|
3617
|
+
...structuredClone(this.systemInfo),
|
|
3618
|
+
rendering: this.videoRender.gpu ? "gpu" : "software",
|
|
3619
|
+
renderingReason: { ...this.rendering.reason },
|
|
3620
|
+
}),
|
|
3621
|
+
// As the player 0.5.13 (see TestHost.rendering for the model).
|
|
3622
|
+
"rendering.state": () => this.#renderingState(),
|
|
3623
|
+
"rendering.setPreference": (p) => {
|
|
3624
|
+
const preference = p.preference;
|
|
3625
|
+
if (preference !== "auto" &&
|
|
3626
|
+
preference !== "gpu" &&
|
|
3627
|
+
preference !== "software")
|
|
3628
|
+
throw new HostError("invalid", 'preference must be "auto", "gpu" or "software"');
|
|
3629
|
+
const { available, reason } = this.rendering.gpu;
|
|
3630
|
+
if (preference === "gpu" && !available)
|
|
3631
|
+
throw new HostError(reason?.code === "no-gpu-backend" || reason?.code === "headless"
|
|
3632
|
+
? "unsupported"
|
|
3633
|
+
: "unavailable", reason?.message ?? "GPU rendering cannot be chosen");
|
|
3634
|
+
this.rendering.preference = preference;
|
|
3635
|
+
return this.#renderingState();
|
|
3636
|
+
},
|
|
3637
|
+
"rendering.restart": () => {
|
|
3638
|
+
if (this.rendering.restarts > 0)
|
|
3639
|
+
throw new HostError("busy", "a restart is already under way");
|
|
3640
|
+
this.rendering.restarts++;
|
|
3641
|
+
return null;
|
|
3642
|
+
},
|
|
3463
3643
|
"clipboard.readText": () => this.clipboard.text,
|
|
3464
3644
|
"clipboard.writeText": (p) => {
|
|
3465
3645
|
if (typeof p.text !== "string")
|
|
@@ -4143,9 +4323,24 @@ export class TestHost {
|
|
|
4143
4323
|
"subtitles.upload.begin": (p) => {
|
|
4144
4324
|
if (p.generation !== this.source.stamp.generation)
|
|
4145
4325
|
throw new HostError("stale", "source generation changed");
|
|
4326
|
+
const expectedBytes = Number(p.expectedBytes);
|
|
4327
|
+
if (!Number.isSafeInteger(expectedBytes) ||
|
|
4328
|
+
expectedBytes < 1 ||
|
|
4329
|
+
expectedBytes > SUBTITLE_LIMITS.fileBytes)
|
|
4330
|
+
throw new HostError("invalid", `a subtitle file has 1..${SUBTITLE_LIMITS.fileBytes} bytes`);
|
|
4331
|
+
// Like the player: a new source drops the old one's uploads. A commit
|
|
4332
|
+
// the player refused leaves its upload for `subtitles.upload.discard`,
|
|
4333
|
+
// which the SDK sends after any failed load.
|
|
4334
|
+
for (const [id, upload] of this.uploads)
|
|
4335
|
+
if (upload.generation !== undefined &&
|
|
4336
|
+
upload.generation !== this.source.stamp.generation)
|
|
4337
|
+
this.uploads.delete(id);
|
|
4338
|
+
if (this.uploads.size >= SUBTITLE_LIMITS.uploadsPerPlugin)
|
|
4339
|
+
throw new HostError("busy", "upload memory, owner or active-token limit reached");
|
|
4146
4340
|
this.uploads.set(Number(p.uploadId), {
|
|
4147
4341
|
bytes: [],
|
|
4148
|
-
expected:
|
|
4342
|
+
expected: expectedBytes,
|
|
4343
|
+
generation: this.source.stamp.generation,
|
|
4149
4344
|
});
|
|
4150
4345
|
return {
|
|
4151
4346
|
generation: p.generation,
|
|
@@ -4163,9 +4358,8 @@ export class TestHost {
|
|
|
4163
4358
|
},
|
|
4164
4359
|
"subtitles.upload.discard": (p) => void this.uploads.delete(Number(p.uploadId)),
|
|
4165
4360
|
"subtitles.commit": (p) => {
|
|
4166
|
-
|
|
4167
|
-
|
|
4168
|
-
throw new HostError("missing", "upload");
|
|
4361
|
+
// The player's order: the request's own fields, then `stale`, then
|
|
4362
|
+
// the upload and its content, then the limits.
|
|
4169
4363
|
const format = p.format === "ass" ? "ass" : "srt";
|
|
4170
4364
|
const select = p.select === "none"
|
|
4171
4365
|
? "none"
|
|
@@ -4179,12 +4373,18 @@ export class TestHost {
|
|
|
4179
4373
|
: null;
|
|
4180
4374
|
if (title !== null && ([...title].length < 1 || [...title].length > 200))
|
|
4181
4375
|
throw new HostError("invalid", "subtitle title needs 1..200 characters");
|
|
4376
|
+
this.#subtitleExpected(p.expected);
|
|
4377
|
+
const upload = this.uploads.get(Number(p.uploadId));
|
|
4378
|
+
if (!upload)
|
|
4379
|
+
throw new HostError("missing", "upload");
|
|
4182
4380
|
const bytes = Buffer.concat(upload.bytes);
|
|
4183
4381
|
if (format === "ass" &&
|
|
4184
4382
|
!(/\[Script Info\]/i.test(bytes.toString()) &&
|
|
4185
4383
|
/\[Events\]/i.test(bytes.toString())))
|
|
4186
4384
|
throw new HostError("invalid", "ASS requires [Script Info] and [Events] sections");
|
|
4385
|
+
this.#subtitleRoom(String(p.resourceId));
|
|
4187
4386
|
this.#subtitleChange(p.expected);
|
|
4387
|
+
this.#loadedSubtitles().set(String(p.resourceId), bytes.length);
|
|
4188
4388
|
const target = {
|
|
4189
4389
|
kind: "external",
|
|
4190
4390
|
owner: 1,
|