@vestra-player/sdk 1.5.33 → 1.5.34
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 +8 -0
- package/dist/player-runtime.d.ts +1 -0
- package/dist/player-runtime.js +1 -0
- package/dist/plugin/subtitles.d.ts +59 -0
- package/dist/plugin/subtitles.js +70 -4
- package/dist/plugin/testing.d.ts +20 -3
- package/dist/plugin/testing.js +74 -7
- package/dist/plugin/types.d.ts +55 -6
- package/dist/wire.d.ts +3 -1
- package/llms-full.txt +131 -13
- package/llms.txt +19 -19
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.5.34
|
|
4
|
+
|
|
5
|
+
**Who must change: nobody.** Additions only, `apiVersion` stays `vestra/1`. A plugin that reads `SystemInfo.rendering` needs player 0.5.12 (`"minPlayerVersion": "0.5.12"`); older players leave the field out, and a plugin that does not read it sees no difference. Tests that load subtitles past the player's limits now fail with `busy` in `TestHost`, as they would in the player.
|
|
6
|
+
|
|
7
|
+
- **`plugin.app.system()` reports the renderer** (player 0.5.12): `rendering` is `"gpu"` (render options apply) or `"software"` (they fail with `unsupported`), read at each call (an optional field: absent on players before 0.5.12, where the renderer is unknown), so a plugin can hide its render settings without creating `plugin.video.render()` to find out. No capability. It changes at most once while the player runs, from GPU to software when a window cannot show GPU frames. `TestHost` reports it from `videoRender.gpu`, so `failVideoRender("unsupported", …)` switches both.
|
|
8
|
+
- **`SUBTITLE_LIMITS`** (`@vestra-player/sdk/testing`, `./runtime`): the player's limits on loaded subtitle files. Per source, 4 tracks of one plugin at once, 16 tracks of every plugin, 16 MiB of their files, 16 loads of one plugin and 128 of every plugin (removed tracks still count as loads); 2 loads of one plugin and 8 of every plugin in progress; a file of at most 2 MiB. A file over 2 MiB is `invalid`, the others `busy`. `TestHost` now enforces them with these codes (all but `totalBytes` and the all-plugin upload count, which one plugin cannot reach alone); external tracks a test adds to `subtitles.tracks` count as other plugins'. A new source drops the uploads of the old one (`uploads` entries carry `generation`). `Subtitles.load`, *Load subtitles from a plugin* and *Limits* name them.
|
|
9
|
+
- Fix: a subtitle load the player refused no longer keeps one of the plugin's two upload slots. After a refused commit (`busy` at a track limit, `stale`, `invalid`…) the SDK now discards the upload, as it already did for a failure before the commit; before, two refusals on one source left the plugin unable to load any subtitle there until the source changed.
|
|
10
|
+
|
|
3
11
|
## 1.5.33
|
|
4
12
|
|
|
5
13
|
1.5.32 was not published (a tag error).
|
package/dist/player-runtime.d.ts
CHANGED
|
@@ -17,6 +17,7 @@ export { BAR_BUTTON_ICONS, BAR_BUTTON_LIMITS, BUILTIN_COMMANDS, BUILTIN_SUBMENUS
|
|
|
17
17
|
export { BINDING_INPUTS } from "./plugin/bindings.ts";
|
|
18
18
|
export { OSD_LIMITS } from "./plugin/osd.ts";
|
|
19
19
|
export { SECRET_LIMITS, SESSION_STORAGE_LIMITS } from "./plugin/services.ts";
|
|
20
|
+
export { SUBTITLE_LIMITS } from "./plugin/subtitles.ts";
|
|
20
21
|
export { THEME_PALETTES } from "./palettes.ts";
|
|
21
22
|
export { Catalog, HOST_ZH_HANS, LIST_SEPARATORS, LOCALES, MENU_KEYS, matchLocale, parsePo, rewriteSlint, renderText, scanSlint, TEXT_LIMITS, textKey, textProblem, } from "./i18n.ts";
|
|
22
23
|
export { MANIFEST_FIELDS, MANIFEST_LIMITS } from "./manifest.ts";
|
package/dist/player-runtime.js
CHANGED
|
@@ -15,6 +15,7 @@ export { BAR_BUTTON_ICONS, BAR_BUTTON_LIMITS, BUILTIN_COMMANDS, BUILTIN_SUBMENUS
|
|
|
15
15
|
export { BINDING_INPUTS } from "./plugin/bindings.js";
|
|
16
16
|
export { OSD_LIMITS } from "./plugin/osd.js";
|
|
17
17
|
export { SECRET_LIMITS, SESSION_STORAGE_LIMITS } from "./plugin/services.js";
|
|
18
|
+
export { SUBTITLE_LIMITS } from "./plugin/subtitles.js";
|
|
18
19
|
export { THEME_PALETTES } from "./palettes.js";
|
|
19
20
|
export { Catalog, HOST_ZH_HANS, LIST_SEPARATORS, LOCALES, MENU_KEYS, matchLocale, parsePo, rewriteSlint, renderText, scanSlint, TEXT_LIMITS, textKey, textProblem, } from "./i18n.js";
|
|
20
21
|
export { MANIFEST_FIELDS, MANIFEST_LIMITS } from "./manifest.js";
|
|
@@ -1,3 +1,62 @@
|
|
|
1
1
|
import type { ScopeImpl } from "./scope.ts";
|
|
2
2
|
import type { Subtitles } from "./types.ts";
|
|
3
|
+
/**
|
|
4
|
+
* The player's limits on subtitle files a plugin loads with
|
|
5
|
+
* {@link Subtitles.load}, which `TestHost` enforces with the player's codes.
|
|
6
|
+
* The track counts are per source: opening another file starts from zero.
|
|
7
|
+
* A file over `fileBytes` fails with `invalid`; every other limit fails with
|
|
8
|
+
* `busy`, and nothing is loaded.
|
|
9
|
+
*
|
|
10
|
+
* @remarks
|
|
11
|
+
* Each `load` uses a new resource on the source, and removing the track
|
|
12
|
+
* frees its place among the live tracks (`tracksPerPlugin`, `tracks`,
|
|
13
|
+
* `totalBytes`) but not among the loads (`loadsPerPlugin`, `loads`). So one
|
|
14
|
+
* plugin loads at most 16 files into one source, with at most 4 of them
|
|
15
|
+
* present at once.
|
|
16
|
+
*
|
|
17
|
+
* @example Replace the plugin's oldest subtitle when it has four
|
|
18
|
+
* The plugin keeps the files it loaded, newest last, and removes the oldest
|
|
19
|
+
* before a fifth would be refused with `busy`. A load the player still
|
|
20
|
+
* refuses (the source holds 16 tracks of every plugin, or this plugin
|
|
21
|
+
* already loaded 16 files into it) is reported on the OSD.
|
|
22
|
+
* ```ts
|
|
23
|
+
* import type { Plugin, PluginError, SubtitleTrack } from "@vestra-player/sdk";
|
|
24
|
+
* declare const plugin: Plugin;
|
|
25
|
+
* declare const bytes: Uint8Array;
|
|
26
|
+
* // ---cut---
|
|
27
|
+
* import { SUBTITLE_LIMITS } from "@vestra-player/sdk/testing";
|
|
28
|
+
*
|
|
29
|
+
* const loaded: SubtitleTrack[] = []; // this source's, newest last
|
|
30
|
+
* if (loaded.length >= SUBTITLE_LIMITS.tracksPerPlugin)
|
|
31
|
+
* await plugin.subtitles.remove(loaded.shift()!);
|
|
32
|
+
* try {
|
|
33
|
+
* loaded.push(await plugin.subtitles.load(bytes));
|
|
34
|
+
* } catch (error) {
|
|
35
|
+
* if ((error as PluginError).code !== "busy") throw error;
|
|
36
|
+
* await plugin.osd.show("No room for another subtitle on this file.");
|
|
37
|
+
* }
|
|
38
|
+
* ```
|
|
39
|
+
*
|
|
40
|
+
* @since 1.5.34
|
|
41
|
+
*
|
|
42
|
+
* @category Testing
|
|
43
|
+
*/
|
|
44
|
+
export declare const SUBTITLE_LIMITS: Readonly<{
|
|
45
|
+
/** UTF-8 bytes of one file; a larger one fails with `invalid` (2 MiB). */
|
|
46
|
+
readonly fileBytes: number;
|
|
47
|
+
/** Tracks one plugin has loaded into the current source at the same time (4). */
|
|
48
|
+
readonly tracksPerPlugin: 4;
|
|
49
|
+
/** Loaded tracks of every plugin in the current source at the same time (16). */
|
|
50
|
+
readonly tracks: 16;
|
|
51
|
+
/** Bytes of the loaded tracks of every plugin in the current source together (16 MiB). */
|
|
52
|
+
readonly totalBytes: number;
|
|
53
|
+
/** Loads by one plugin into the current source, removed tracks included (16). */
|
|
54
|
+
readonly loadsPerPlugin: 16;
|
|
55
|
+
/** Loads by every plugin into the current source, removed tracks included (128). */
|
|
56
|
+
readonly loads: 128;
|
|
57
|
+
/** Loads of one plugin in progress at the same time (2). */
|
|
58
|
+
readonly uploadsPerPlugin: 2;
|
|
59
|
+
/** Loads of every plugin in progress at the same time (8). */
|
|
60
|
+
readonly uploads: 8;
|
|
61
|
+
}>;
|
|
3
62
|
export declare function subtitles(scope: ScopeImpl): Subtitles;
|
package/dist/plugin/subtitles.js
CHANGED
|
@@ -3,6 +3,65 @@ import { createHash } from "node:crypto";
|
|
|
3
3
|
import { isTextValue, textProblem } from "../i18n.js";
|
|
4
4
|
import { Latest, failure, sleep, uncertain } from "./runtime.js";
|
|
5
5
|
import { abortError } from "./player.js";
|
|
6
|
+
/**
|
|
7
|
+
* The player's limits on subtitle files a plugin loads with
|
|
8
|
+
* {@link Subtitles.load}, which `TestHost` enforces with the player's codes.
|
|
9
|
+
* The track counts are per source: opening another file starts from zero.
|
|
10
|
+
* A file over `fileBytes` fails with `invalid`; every other limit fails with
|
|
11
|
+
* `busy`, and nothing is loaded.
|
|
12
|
+
*
|
|
13
|
+
* @remarks
|
|
14
|
+
* Each `load` uses a new resource on the source, and removing the track
|
|
15
|
+
* frees its place among the live tracks (`tracksPerPlugin`, `tracks`,
|
|
16
|
+
* `totalBytes`) but not among the loads (`loadsPerPlugin`, `loads`). So one
|
|
17
|
+
* plugin loads at most 16 files into one source, with at most 4 of them
|
|
18
|
+
* present at once.
|
|
19
|
+
*
|
|
20
|
+
* @example Replace the plugin's oldest subtitle when it has four
|
|
21
|
+
* The plugin keeps the files it loaded, newest last, and removes the oldest
|
|
22
|
+
* before a fifth would be refused with `busy`. A load the player still
|
|
23
|
+
* refuses (the source holds 16 tracks of every plugin, or this plugin
|
|
24
|
+
* already loaded 16 files into it) is reported on the OSD.
|
|
25
|
+
* ```ts
|
|
26
|
+
* import type { Plugin, PluginError, SubtitleTrack } from "@vestra-player/sdk";
|
|
27
|
+
* declare const plugin: Plugin;
|
|
28
|
+
* declare const bytes: Uint8Array;
|
|
29
|
+
* // ---cut---
|
|
30
|
+
* import { SUBTITLE_LIMITS } from "@vestra-player/sdk/testing";
|
|
31
|
+
*
|
|
32
|
+
* const loaded: SubtitleTrack[] = []; // this source's, newest last
|
|
33
|
+
* if (loaded.length >= SUBTITLE_LIMITS.tracksPerPlugin)
|
|
34
|
+
* await plugin.subtitles.remove(loaded.shift()!);
|
|
35
|
+
* try {
|
|
36
|
+
* loaded.push(await plugin.subtitles.load(bytes));
|
|
37
|
+
* } catch (error) {
|
|
38
|
+
* if ((error as PluginError).code !== "busy") throw error;
|
|
39
|
+
* await plugin.osd.show("No room for another subtitle on this file.");
|
|
40
|
+
* }
|
|
41
|
+
* ```
|
|
42
|
+
*
|
|
43
|
+
* @since 1.5.34
|
|
44
|
+
*
|
|
45
|
+
* @category Testing
|
|
46
|
+
*/
|
|
47
|
+
export const SUBTITLE_LIMITS = Object.freeze({
|
|
48
|
+
/** UTF-8 bytes of one file; a larger one fails with `invalid` (2 MiB). */
|
|
49
|
+
fileBytes: 2 * 1024 * 1024,
|
|
50
|
+
/** Tracks one plugin has loaded into the current source at the same time (4). */
|
|
51
|
+
tracksPerPlugin: 4,
|
|
52
|
+
/** Loaded tracks of every plugin in the current source at the same time (16). */
|
|
53
|
+
tracks: 16,
|
|
54
|
+
/** Bytes of the loaded tracks of every plugin in the current source together (16 MiB). */
|
|
55
|
+
totalBytes: 16 * 1024 * 1024,
|
|
56
|
+
/** Loads by one plugin into the current source, removed tracks included (16). */
|
|
57
|
+
loadsPerPlugin: 16,
|
|
58
|
+
/** Loads by every plugin into the current source, removed tracks included (128). */
|
|
59
|
+
loads: 128,
|
|
60
|
+
/** Loads of one plugin in progress at the same time (2). */
|
|
61
|
+
uploadsPerPlugin: 2,
|
|
62
|
+
/** Loads of every plugin in progress at the same time (8). */
|
|
63
|
+
uploads: 8,
|
|
64
|
+
});
|
|
6
65
|
const raws = new WeakMap();
|
|
7
66
|
const targets = new WeakMap();
|
|
8
67
|
const CHUNK = 32 * 1024;
|
|
@@ -283,11 +342,18 @@ export function subtitles(scope) {
|
|
|
283
342
|
return loaded;
|
|
284
343
|
}
|
|
285
344
|
catch (error) {
|
|
345
|
+
// Free the upload's slot (2 per plugin). Before the commit the
|
|
346
|
+
// discard is awaited, as it always was (an abort resolves once the
|
|
347
|
+
// upload is gone). After a refused commit the player still holds
|
|
348
|
+
// the upload; after one that took effect it was consumed and the
|
|
349
|
+
// discard is a harmless no-op. Sent, not awaited: the player handles
|
|
350
|
+
// it before any later request of this plugin.
|
|
351
|
+
const discard = host()
|
|
352
|
+
.discard(uploadId)
|
|
353
|
+
.catch(() => { });
|
|
286
354
|
if (!committing)
|
|
287
|
-
await
|
|
288
|
-
|
|
289
|
-
.catch(() => { });
|
|
290
|
-
else if (error.code !== "missing")
|
|
355
|
+
await discard;
|
|
356
|
+
if (committing && error.code !== "missing")
|
|
291
357
|
svc.owned.delete(resourceId);
|
|
292
358
|
throw error;
|
|
293
359
|
}
|
package/dist/plugin/testing.d.ts
CHANGED
|
@@ -8,6 +8,7 @@ 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";
|
|
@@ -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. */
|
|
@@ -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
|
/**
|
|
@@ -1385,6 +1391,13 @@ export declare class TestHost implements Transport {
|
|
|
1385
1391
|
bytes: Buffer[];
|
|
1386
1392
|
/** The announced total size in bytes. */
|
|
1387
1393
|
expected: number;
|
|
1394
|
+
/**
|
|
1395
|
+
* The source generation it was begun for; like the player, a new
|
|
1396
|
+
* source drops the uploads of the old one.
|
|
1397
|
+
*
|
|
1398
|
+
* @since 1.5.34
|
|
1399
|
+
*/
|
|
1400
|
+
generation?: number;
|
|
1388
1401
|
}>;
|
|
1389
1402
|
/** Snapshots taken with `media.capture`, by snapshot id. Wire-level. */
|
|
1390
1403
|
readonly snapshots: Map<number, Json>;
|
|
@@ -1595,8 +1608,12 @@ export declare class TestHost implements Transport {
|
|
|
1595
1608
|
* @since 1.5.25
|
|
1596
1609
|
*/
|
|
1597
1610
|
readonly sessionStorage: Map<string, Json>;
|
|
1598
|
-
/**
|
|
1599
|
-
|
|
1611
|
+
/**
|
|
1612
|
+
* What `plugin.app.system()` reports. Its `rendering` follows
|
|
1613
|
+
* {@link TestHost.videoRender | videoRender.gpu} (`"gpu"` or
|
|
1614
|
+
* `"software"`), so one switch covers the render options and the fact.
|
|
1615
|
+
*/
|
|
1616
|
+
systemInfo: Omit<WireSystemInfo, "rendering">;
|
|
1600
1617
|
/**
|
|
1601
1618
|
* What `plugin.app` reports. Set it before {@link startPlugin}: the plugin
|
|
1602
1619
|
* receives it once at start-up.
|
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. */
|
|
@@ -1521,7 +1524,11 @@ export class TestHost {
|
|
|
1521
1524
|
* @since 1.5.25
|
|
1522
1525
|
*/
|
|
1523
1526
|
sessionStorage = new Map();
|
|
1524
|
-
/**
|
|
1527
|
+
/**
|
|
1528
|
+
* What `plugin.app.system()` reports. Its `rendering` follows
|
|
1529
|
+
* {@link TestHost.videoRender | videoRender.gpu} (`"gpu"` or
|
|
1530
|
+
* `"software"`), so one switch covers the render options and the fact.
|
|
1531
|
+
*/
|
|
1525
1532
|
systemInfo = {
|
|
1526
1533
|
os: "macos",
|
|
1527
1534
|
name: "macOS",
|
|
@@ -3135,12 +3142,49 @@ export class TestHost {
|
|
|
3135
3142
|
!["ready", "ended"].includes(this.source.phase))
|
|
3136
3143
|
throw new HostError("unavailable", "audio tracks need ready media");
|
|
3137
3144
|
}
|
|
3138
|
-
|
|
3145
|
+
/** `stale` unless `expected` is the current subtitle stamp. */
|
|
3146
|
+
#subtitleExpected(expected) {
|
|
3139
3147
|
const stamp = this.subtitles.selection.stamp;
|
|
3140
3148
|
const want = expected;
|
|
3141
3149
|
if (want.generation !== stamp.generation ||
|
|
3142
3150
|
want.revision !== stamp.revision)
|
|
3143
3151
|
throw new HostError("stale", "subtitle selection changed");
|
|
3152
|
+
}
|
|
3153
|
+
/**
|
|
3154
|
+
* The resources this plugin loaded into the current source (resource id →
|
|
3155
|
+
* bytes), removed ones included: like the player, a removed track frees
|
|
3156
|
+
* its place among the live tracks but not among the loads.
|
|
3157
|
+
*/
|
|
3158
|
+
#subtitleResources = { generation: -1, bytes: new Map() };
|
|
3159
|
+
#loadedSubtitles() {
|
|
3160
|
+
const generation = this.source.stamp.generation;
|
|
3161
|
+
if (this.#subtitleResources.generation !== generation)
|
|
3162
|
+
this.#subtitleResources = { generation, bytes: new Map() };
|
|
3163
|
+
return this.#subtitleResources.bytes;
|
|
3164
|
+
}
|
|
3165
|
+
/**
|
|
3166
|
+
* The player's `SUBTITLE_LIMITS` for a new resource (`busy`, as the
|
|
3167
|
+
* player reports them). External tracks in `subtitles.tracks` that this
|
|
3168
|
+
* plugin did not load count as other plugins'. `totalBytes` is not
|
|
3169
|
+
* modelled: one plugin's 4 files of at most 2 MiB stay below it, and the
|
|
3170
|
+
* bytes of other plugins' tracks are unknown.
|
|
3171
|
+
*/
|
|
3172
|
+
#subtitleRoom(resourceId) {
|
|
3173
|
+
const loaded = this.#loadedSubtitles();
|
|
3174
|
+
if (loaded.has(resourceId))
|
|
3175
|
+
return;
|
|
3176
|
+
const external = this.subtitles.tracks.filter((t) => t.target.kind === "external");
|
|
3177
|
+
const own = external.filter((t) => t.target.kind === "external" && loaded.has(t.target.resourceId));
|
|
3178
|
+
const others = external.length - own.length;
|
|
3179
|
+
if (loaded.size >= SUBTITLE_LIMITS.loadsPerPlugin ||
|
|
3180
|
+
loaded.size + others >= SUBTITLE_LIMITS.loads ||
|
|
3181
|
+
own.length >= SUBTITLE_LIMITS.tracksPerPlugin ||
|
|
3182
|
+
external.length >= SUBTITLE_LIMITS.tracks)
|
|
3183
|
+
throw new HostError("busy", "subtitle resource or counter limit exceeded");
|
|
3184
|
+
}
|
|
3185
|
+
#subtitleChange(expected) {
|
|
3186
|
+
this.#subtitleExpected(expected);
|
|
3187
|
+
const stamp = this.subtitles.selection.stamp;
|
|
3144
3188
|
this.subtitles.selection.stamp = {
|
|
3145
3189
|
generation: this.source.stamp.generation,
|
|
3146
3190
|
revision: stamp.revision + 1,
|
|
@@ -3459,7 +3503,10 @@ export class TestHost {
|
|
|
3459
3503
|
},
|
|
3460
3504
|
"storage.session.delete": (p) => void this.sessionStorage.delete(sessionKey(p.key)),
|
|
3461
3505
|
"storage.session.keys": () => [...this.sessionStorage.keys()].sort(byCodePoint),
|
|
3462
|
-
"app.system": () =>
|
|
3506
|
+
"app.system": () => ({
|
|
3507
|
+
...structuredClone(this.systemInfo),
|
|
3508
|
+
rendering: this.videoRender.gpu ? "gpu" : "software",
|
|
3509
|
+
}),
|
|
3463
3510
|
"clipboard.readText": () => this.clipboard.text,
|
|
3464
3511
|
"clipboard.writeText": (p) => {
|
|
3465
3512
|
if (typeof p.text !== "string")
|
|
@@ -4143,9 +4190,24 @@ export class TestHost {
|
|
|
4143
4190
|
"subtitles.upload.begin": (p) => {
|
|
4144
4191
|
if (p.generation !== this.source.stamp.generation)
|
|
4145
4192
|
throw new HostError("stale", "source generation changed");
|
|
4193
|
+
const expectedBytes = Number(p.expectedBytes);
|
|
4194
|
+
if (!Number.isSafeInteger(expectedBytes) ||
|
|
4195
|
+
expectedBytes < 1 ||
|
|
4196
|
+
expectedBytes > SUBTITLE_LIMITS.fileBytes)
|
|
4197
|
+
throw new HostError("invalid", `a subtitle file has 1..${SUBTITLE_LIMITS.fileBytes} bytes`);
|
|
4198
|
+
// Like the player: a new source drops the old one's uploads. A commit
|
|
4199
|
+
// the player refused leaves its upload for `subtitles.upload.discard`,
|
|
4200
|
+
// which the SDK sends after any failed load.
|
|
4201
|
+
for (const [id, upload] of this.uploads)
|
|
4202
|
+
if (upload.generation !== undefined &&
|
|
4203
|
+
upload.generation !== this.source.stamp.generation)
|
|
4204
|
+
this.uploads.delete(id);
|
|
4205
|
+
if (this.uploads.size >= SUBTITLE_LIMITS.uploadsPerPlugin)
|
|
4206
|
+
throw new HostError("busy", "upload memory, owner or active-token limit reached");
|
|
4146
4207
|
this.uploads.set(Number(p.uploadId), {
|
|
4147
4208
|
bytes: [],
|
|
4148
|
-
expected:
|
|
4209
|
+
expected: expectedBytes,
|
|
4210
|
+
generation: this.source.stamp.generation,
|
|
4149
4211
|
});
|
|
4150
4212
|
return {
|
|
4151
4213
|
generation: p.generation,
|
|
@@ -4163,9 +4225,8 @@ export class TestHost {
|
|
|
4163
4225
|
},
|
|
4164
4226
|
"subtitles.upload.discard": (p) => void this.uploads.delete(Number(p.uploadId)),
|
|
4165
4227
|
"subtitles.commit": (p) => {
|
|
4166
|
-
|
|
4167
|
-
|
|
4168
|
-
throw new HostError("missing", "upload");
|
|
4228
|
+
// The player's order: the request's own fields, then `stale`, then
|
|
4229
|
+
// the upload and its content, then the limits.
|
|
4169
4230
|
const format = p.format === "ass" ? "ass" : "srt";
|
|
4170
4231
|
const select = p.select === "none"
|
|
4171
4232
|
? "none"
|
|
@@ -4179,12 +4240,18 @@ export class TestHost {
|
|
|
4179
4240
|
: null;
|
|
4180
4241
|
if (title !== null && ([...title].length < 1 || [...title].length > 200))
|
|
4181
4242
|
throw new HostError("invalid", "subtitle title needs 1..200 characters");
|
|
4243
|
+
this.#subtitleExpected(p.expected);
|
|
4244
|
+
const upload = this.uploads.get(Number(p.uploadId));
|
|
4245
|
+
if (!upload)
|
|
4246
|
+
throw new HostError("missing", "upload");
|
|
4182
4247
|
const bytes = Buffer.concat(upload.bytes);
|
|
4183
4248
|
if (format === "ass" &&
|
|
4184
4249
|
!(/\[Script Info\]/i.test(bytes.toString()) &&
|
|
4185
4250
|
/\[Events\]/i.test(bytes.toString())))
|
|
4186
4251
|
throw new HostError("invalid", "ASS requires [Script Info] and [Events] sections");
|
|
4252
|
+
this.#subtitleRoom(String(p.resourceId));
|
|
4187
4253
|
this.#subtitleChange(p.expected);
|
|
4254
|
+
this.#loadedSubtitles().set(String(p.resourceId), bytes.length);
|
|
4188
4255
|
const target = {
|
|
4189
4256
|
kind: "external",
|
|
4190
4257
|
owner: 1,
|
package/dist/plugin/types.d.ts
CHANGED
|
@@ -3991,8 +3991,9 @@ export interface AppInfo {
|
|
|
3991
3991
|
*/
|
|
3992
3992
|
export interface App extends AppInfo {
|
|
3993
3993
|
/**
|
|
3994
|
-
* The operating system and the
|
|
3995
|
-
* and go
|
|
3994
|
+
* The operating system, the displays and the renderer, read when called
|
|
3995
|
+
* (displays come and go; the renderer can fall back from GPU to software
|
|
3996
|
+
* once). No capability. CPU and memory: Node's `os` module.
|
|
3996
3997
|
*
|
|
3997
3998
|
* @capability None.
|
|
3998
3999
|
* @throws `busy` (rejects) The player could not start the request (no
|
|
@@ -4034,6 +4035,46 @@ export interface SystemInfo {
|
|
|
4034
4035
|
readonly arch: string;
|
|
4035
4036
|
/** Primary display first; empty without a user interface. */
|
|
4036
4037
|
readonly displays: readonly DisplayInfo[];
|
|
4038
|
+
/**
|
|
4039
|
+
* Which renderer draws the video now: `"gpu"` (the GPU renderer, so
|
|
4040
|
+
* {@link RenderOptions | render options} apply) or `"software"` (they
|
|
4041
|
+
* fail with `unsupported`). Read at each call. It changes at most once
|
|
4042
|
+
* while the player runs, from `"gpu"` to `"software"` when a window
|
|
4043
|
+
* cannot show GPU frames; the owner of the render options hears that
|
|
4044
|
+
* switch through {@link RenderOptions.onStatus}.
|
|
4045
|
+
*
|
|
4046
|
+
* Absent (`undefined`) on players before 0.5.12, which do not report it:
|
|
4047
|
+
* the renderer is then unknown, not software.
|
|
4048
|
+
*
|
|
4049
|
+
* Today the player renders on the GPU on macOS and in software on
|
|
4050
|
+
* Windows, Linux and without a user interface (see
|
|
4051
|
+
* [Platforms](../../docs/reference/platforms.md)); test this field
|
|
4052
|
+
* rather than {@link SystemInfo.os}.
|
|
4053
|
+
*
|
|
4054
|
+
* @example Offer the HDR menu only where render options work
|
|
4055
|
+
* The plugin adds its HDR command only when the player renders on the
|
|
4056
|
+
* GPU, instead of creating the render options just to find out from a
|
|
4057
|
+
* failed status. `system()` asks the player, which reports the renderer
|
|
4058
|
+
* drawing at that moment; no capability is needed to ask. A player before
|
|
4059
|
+
* 0.5.12 leaves the field out, so the test is for `"gpu"` exactly (an
|
|
4060
|
+
* older player gets no HDR command).
|
|
4061
|
+
* ```ts
|
|
4062
|
+
* const { rendering } = await plugin.app.system();
|
|
4063
|
+
* if (rendering === "gpu") {
|
|
4064
|
+
* const render = plugin.video.render(); // one handle, kept
|
|
4065
|
+
* plugin.commands.add({
|
|
4066
|
+
* id: "hdr",
|
|
4067
|
+
* path: ["@video", "HDR Tone Mapping"],
|
|
4068
|
+
* run: () => render.set({ toneMapping: "bt.2390", targetPeak: 250 }),
|
|
4069
|
+
* });
|
|
4070
|
+
* }
|
|
4071
|
+
* ```
|
|
4072
|
+
*
|
|
4073
|
+
* @player 0.5.12; older players leave it out (declare
|
|
4074
|
+
* `"minPlayerVersion": "0.5.12"` when the plugin relies on it).
|
|
4075
|
+
* @since 1.5.34
|
|
4076
|
+
*/
|
|
4077
|
+
readonly rendering?: "gpu" | "software";
|
|
4037
4078
|
}
|
|
4038
4079
|
/**
|
|
4039
4080
|
* One display in {@link SystemInfo.displays}.
|
|
@@ -6294,8 +6335,10 @@ export interface RenderSettings {
|
|
|
6294
6335
|
*
|
|
6295
6336
|
* Platforms: only macOS renders on the GPU. Under software rendering
|
|
6296
6337
|
* (Windows, Linux, headless, fallback) every `set` fails with
|
|
6297
|
-
* `unsupported
|
|
6298
|
-
*
|
|
6338
|
+
* `unsupported`; {@link SystemInfo.rendering} tells which renderer draws
|
|
6339
|
+
* without creating the handle. macOS OpenGL 4.1 has no
|
|
6340
|
+
* compute shaders, so `peakDetect` is accepted but has no effect; output
|
|
6341
|
+
* is always SDR.
|
|
6299
6342
|
*
|
|
6300
6343
|
* @example Tone-map HDR video and hide the menu where it cannot work
|
|
6301
6344
|
* On a GPU-rendering player HDR video is tone-mapped with BT.2390 for a
|
|
@@ -6591,7 +6634,10 @@ export interface Subtitles {
|
|
|
6591
6634
|
*
|
|
6592
6635
|
* Limits: SRT ≤ 2 MiB and ≤ 10,000 cues; ASS ≤ 2 MiB with `[Script Info]`
|
|
6593
6636
|
* and `[Events]` and 1–20,000 `Dialogue:` lines. Convert other formats
|
|
6594
|
-
* (VTT, SAMI) to SRT first.
|
|
6637
|
+
* (VTT, SAMI) to SRT first. Per source, a plugin has at most 4 loaded
|
|
6638
|
+
* tracks at once and loads at most 16 files (removed ones count), and
|
|
6639
|
+
* every plugin together at most 16 tracks; see `SUBTITLE_LIMITS` in
|
|
6640
|
+
* `@vestra-player/sdk/testing`.
|
|
6595
6641
|
*
|
|
6596
6642
|
* @capability subtitles.control and subtitles.read
|
|
6597
6643
|
* @throws `invalid` (rejects) A bad `format` or `title`, or a file over the
|
|
@@ -6602,7 +6648,10 @@ export interface Subtitles {
|
|
|
6602
6648
|
* loading.
|
|
6603
6649
|
* @throws `aborted` (rejects) `signal` aborted before the commit (an
|
|
6604
6650
|
* `AbortError`); the upload is discarded.
|
|
6605
|
-
* @throws `busy` (rejects) Too many subtitle operations at once
|
|
6651
|
+
* @throws `busy` (rejects) Too many subtitle operations at once, or a
|
|
6652
|
+
* limit of `SUBTITLE_LIMITS` is reached (4 tracks of this plugin, 16
|
|
6653
|
+
* loads of this plugin or 16 tracks in the source, or another of its
|
|
6654
|
+
* limits); nothing is loaded.
|
|
6606
6655
|
* @throws `capability` (rejects) The manifest does not declare
|
|
6607
6656
|
* `subtitles.control` and `subtitles.read`.
|
|
6608
6657
|
*
|
package/dist/wire.d.ts
CHANGED
|
@@ -297,7 +297,7 @@ export interface LocalServices {
|
|
|
297
297
|
signal: AbortSignal;
|
|
298
298
|
}): Promise<Uint8Array>;
|
|
299
299
|
}
|
|
300
|
-
/** `app.system`: the operating system and the
|
|
300
|
+
/** `app.system`: the operating system, the displays and the renderer. */
|
|
301
301
|
export interface WireSystemInfo {
|
|
302
302
|
os: "macos" | "windows" | "linux" | "other";
|
|
303
303
|
name: string;
|
|
@@ -313,6 +313,8 @@ export interface WireSystemInfo {
|
|
|
313
313
|
refreshRate: number | null;
|
|
314
314
|
primary: boolean;
|
|
315
315
|
}[];
|
|
316
|
+
/** Player 0.5.12; older players leave it out. */
|
|
317
|
+
rendering?: "gpu" | "software";
|
|
316
318
|
}
|
|
317
319
|
/**
|
|
318
320
|
* Template arguments on the wire (`osd.show`, `osd.panel`, `dialogs.confirm`,
|
package/llms-full.txt
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# @vestra-player/sdk 1.5.
|
|
1
|
+
# @vestra-player/sdk 1.5.34: full reference
|
|
2
2
|
|
|
3
3
|
> Everything needed to write a Vestra Player plugin (plugin API `vestra/1`): the guides, every public declaration of `@vestra-player/sdk` and `@vestra-player/sdk/testing` with its documentation, and the complete source of the example plugins. Generated from the SDK's sources by scripts/llms.mjs; the short index is llms.txt.
|
|
4
4
|
|
|
@@ -740,7 +740,7 @@ plugin.video.view() returns the VideoView: aspect override, fit (keep with bars,
|
|
|
740
740
|
|
|
741
741
|
plugin.video.render() returns the RenderOptions of the GPU renderer: HDR to SDR tone mapping and the display's brightness, user shaders (`.glsl` files) and motion interpolation. Like the display layer they act at display time only, have one owner (independent of the display layer's owner) and merge writes.
|
|
742
742
|
|
|
743
|
-
- **Only the GPU renderer has them.** Under software rendering every `set` fails with `unsupported`; see Reference: platforms for where the player renders on the GPU. Treat `unsupported` as "hide these settings", not as an error.
|
|
743
|
+
- **Only the GPU renderer has them.** Under software rendering every `set` fails with `unsupported`; see Reference: platforms for where the player renders on the GPU. Treat `unsupported` as "hide these settings", not as an error. To know before creating the handle, read rendering from `plugin.app.system()` (player 0.5.12, no capability): `"gpu"` or `"software"`.
|
|
744
744
|
- A shader that does not compile is reported later: the status first becomes `applied`, then `failed` with code `failed` and the compiler's first error line once mpv first uses the shader.
|
|
745
745
|
|
|
746
746
|
## Concept: Windows, panels, settings pages, dialogs and the OSD
|
|
@@ -1272,7 +1272,7 @@ if (english)
|
|
|
1272
1272
|
|
|
1273
1273
|
#### Upload a file
|
|
1274
1274
|
|
|
1275
|
-
`load` takes the file's bytes: UTF-8 SRT, or ASS/SSA with `format: "ass"`, at most 2 MiB (SRT up to 10,000 cues, ASS up to 20,000 `Dialogue:` lines). Convert other formats (WebVTT, SAMI) to SRT first. The player adds an `external` track to the current source only, selects it unless `select: "none"`, and resolves with the track once the upload is committed.
|
|
1275
|
+
`load` takes the file's bytes: UTF-8 SRT, or ASS/SSA with `format: "ass"`, at most 2 MiB (SRT up to 10,000 cues, ASS up to 20,000 `Dialogue:` lines). Convert other formats (WebVTT, SAMI) to SRT first. The player adds an `external` track to the current source only, selects it unless `select: "none"`, and resolves with the track once the upload is committed. A plugin has at most 4 such tracks on a source at once and loads at most 16 files into it (removed ones count); past that `load` fails with `busy` (Limits).
|
|
1276
1276
|
|
|
1277
1277
|
```ts
|
|
1278
1278
|
import { readFile } from "node:fs/promises";
|
|
@@ -2567,6 +2567,7 @@ The table lists the API whose documentation names a player version, newest last:
|
|
|
2567
2567
|
| 0.5.3 | Scrub.seek |
|
|
2568
2568
|
| 0.5.5 | SessionStorage |
|
|
2569
2569
|
| 0.5.10 | CommandButton, CommandOptions.button |
|
|
2570
|
+
| 0.5.12 | SystemInfo.rendering |
|
|
2570
2571
|
|
|
2571
2572
|
The Slint library carries no version tags: `Theme.danger` needs player 0.3.0; `@vestra/icons.slint` and the palette `vestra-light` need 0.4.0 (Theme).
|
|
2572
2573
|
|
|
@@ -2724,6 +2725,21 @@ plugin.storage.session (player 0.5.5). A key out of its rule, or a value that is
|
|
|
2724
2725
|
| `SESSION_STORAGE_LIMITS.bytes` | 1048576 (1 MiB) | Bytes one plugin may hold at the same time: for every key, its UTF-8 bytes, its value written as compact JSON (`JSON.stringify(value)`) and `entryBytes`. |
|
|
2725
2726
|
| `SESSION_STORAGE_LIMITS.entryBytes` | 128 | What each key costs on top of its text and value (128): the player's own bookkeeping, so that many tiny entries stay within `bytes` of memory. |
|
|
2726
2727
|
|
|
2728
|
+
#### Subtitle files
|
|
2729
|
+
|
|
2730
|
+
Subtitles.load uploads a file as a track of the current source. A file over `fileBytes` fails with `invalid`; every other limit fails with `busy` and nothing is loaded. The counts are per source, so another file starts from zero. Removing a track frees its place among the tracks and their bytes, but a load stays counted until the source changes.
|
|
2731
|
+
|
|
2732
|
+
| Limit | Value | Meaning |
|
|
2733
|
+
| --- | --- | --- |
|
|
2734
|
+
| `SUBTITLE_LIMITS.fileBytes` | 2097152 (2 MiB) | UTF-8 bytes of one file; a larger one fails with `invalid`. |
|
|
2735
|
+
| `SUBTITLE_LIMITS.tracksPerPlugin` | 4 | Tracks one plugin has loaded into the current source at the same time. |
|
|
2736
|
+
| `SUBTITLE_LIMITS.tracks` | 16 | Loaded tracks of every plugin in the current source at the same time. |
|
|
2737
|
+
| `SUBTITLE_LIMITS.totalBytes` | 16777216 (16 MiB) | Bytes of the loaded tracks of every plugin in the current source together. |
|
|
2738
|
+
| `SUBTITLE_LIMITS.loadsPerPlugin` | 16 | Loads by one plugin into the current source, removed tracks included. |
|
|
2739
|
+
| `SUBTITLE_LIMITS.loads` | 128 | Loads by every plugin into the current source, removed tracks included. |
|
|
2740
|
+
| `SUBTITLE_LIMITS.uploadsPerPlugin` | 2 | Loads of one plugin in progress at the same time. |
|
|
2741
|
+
| `SUBTITLE_LIMITS.uploads` | 8 | Loads of every plugin in progress at the same time. |
|
|
2742
|
+
|
|
2727
2743
|
#### Messages
|
|
2728
2744
|
|
|
2729
2745
|
Every request a plugin sends travels as one protocol line. The plugin's side refuses a longer one with code `limit` before sending it; property values meet their own limits first.
|
|
@@ -3244,7 +3260,7 @@ A plugin uses the same API on macOS and Windows; the player does the system-spec
|
|
|
3244
3260
|
| DropModifiers | The pointer and ⌥ ⇧ ⌘ (`cmd` is ⌘). | The pointer and Alt, Shift, Ctrl (`cmd` is Ctrl). |
|
|
3245
3261
|
| plugin.system (open, reveal) | Opened by the system; revealing several files selects all of them in one Finder window. | Opened by the shell; one Explorer window per folder. |
|
|
3246
3262
|
| plugin.secrets | The login keychain. | The Credential Manager. |
|
|
3247
|
-
| App.system() | Version, build, model and displays. | Version, build, model and displays
|
|
3263
|
+
| App.system() | Version, build, model and displays; `rendering` is `"gpu"`, or `"software"` after a fallback (a window that cannot show GPU frames). | Version, build, model and displays; `rendering` is `"software"`. |
|
|
3248
3264
|
| Snapshots and exports (MediaCapture) | Available. | Available; needs FFmpeg 6.1 or newer. |
|
|
3249
3265
|
| Generated streams (Player.openStream) | A Unix socket. | Loopback TCP with a token. |
|
|
3250
3266
|
| Rendering | On the GPU (OpenGL), with hardware decoding. | In software, with hardware decoding copied back (`d3d11va-copy` and similar). |
|
|
@@ -7227,7 +7243,7 @@ worked out once, at the first call, and reused.
|
|
|
7227
7243
|
```ts
|
|
7228
7244
|
interface App extends AppInfo {
|
|
7229
7245
|
/**
|
|
7230
|
-
* The operating system and the
|
|
7246
|
+
* The operating system, the displays and the renderer, read when called (displays come and go; the renderer can fall back from GPU to software once). No capability. CPU and memory: Node's `os` module.
|
|
7231
7247
|
* @capability None.
|
|
7232
7248
|
* @throws busy (rejects) The player could not start the request (no thread available).
|
|
7233
7249
|
* @example Choose a size for the primary display
|
|
@@ -7276,6 +7292,42 @@ interface SystemInfo {
|
|
|
7276
7292
|
readonly arch: string;
|
|
7277
7293
|
/** Primary display first; empty without a user interface. */
|
|
7278
7294
|
readonly displays: readonly DisplayInfo[];
|
|
7295
|
+
/**
|
|
7296
|
+
* Which renderer draws the video now: `"gpu"` (the GPU renderer, so
|
|
7297
|
+
* render options apply) or `"software"` (they
|
|
7298
|
+
* fail with `unsupported`). Read at each call. It changes at most once
|
|
7299
|
+
* while the player runs, from `"gpu"` to `"software"` when a window
|
|
7300
|
+
* cannot show GPU frames; the owner of the render options hears that
|
|
7301
|
+
* switch through RenderOptions.onStatus.
|
|
7302
|
+
*
|
|
7303
|
+
* Absent (`undefined`) on players before 0.5.12, which do not report it:
|
|
7304
|
+
* the renderer is then unknown, not software.
|
|
7305
|
+
*
|
|
7306
|
+
* Today the player renders on the GPU on macOS and in software on
|
|
7307
|
+
* Windows, Linux and without a user interface (see
|
|
7308
|
+
* Platforms); test this field
|
|
7309
|
+
* rather than SystemInfo.os.
|
|
7310
|
+
* @player 0.5.12; older players leave it out (declare `"minPlayerVersion": "0.5.12"` when the plugin relies on it).
|
|
7311
|
+
* @example Offer the HDR menu only where render options work
|
|
7312
|
+
* The plugin adds its HDR command only when the player renders on the
|
|
7313
|
+
* GPU, instead of creating the render options just to find out from a
|
|
7314
|
+
* failed status. `system()` asks the player, which reports the renderer
|
|
7315
|
+
* drawing at that moment; no capability is needed to ask. A player before
|
|
7316
|
+
* 0.5.12 leaves the field out, so the test is for `"gpu"` exactly (an
|
|
7317
|
+
* older player gets no HDR command).
|
|
7318
|
+
* ```ts
|
|
7319
|
+
* const { rendering } = await plugin.app.system();
|
|
7320
|
+
* if (rendering === "gpu") {
|
|
7321
|
+
* const render = plugin.video.render(); // one handle, kept
|
|
7322
|
+
* plugin.commands.add({
|
|
7323
|
+
* id: "hdr",
|
|
7324
|
+
* path: ["@video", "HDR Tone Mapping"],
|
|
7325
|
+
* run: () => render.set({ toneMapping: "bt.2390", targetPeak: 250 }),
|
|
7326
|
+
* });
|
|
7327
|
+
* }
|
|
7328
|
+
* ```
|
|
7329
|
+
*/
|
|
7330
|
+
readonly rendering?: "gpu" | "software";
|
|
7279
7331
|
}
|
|
7280
7332
|
```
|
|
7281
7333
|
|
|
@@ -9142,8 +9194,10 @@ independent handles.
|
|
|
9142
9194
|
|
|
9143
9195
|
Platforms: only macOS renders on the GPU. Under software rendering
|
|
9144
9196
|
(Windows, Linux, headless, fallback) every `set` fails with
|
|
9145
|
-
`unsupported
|
|
9146
|
-
|
|
9197
|
+
`unsupported`; SystemInfo.rendering tells which renderer draws
|
|
9198
|
+
without creating the handle. macOS OpenGL 4.1 has no
|
|
9199
|
+
compute shaders, so `peakDetect` is accepted but has no effect; output
|
|
9200
|
+
is always SDR.
|
|
9147
9201
|
|
|
9148
9202
|
GPU renderer options (tone mapping, peak detection, shaders,
|
|
9149
9203
|
interpolation). Only the GPU renderer has them, which today means macOS;
|
|
@@ -9373,13 +9427,16 @@ interface Subtitles {
|
|
|
9373
9427
|
*
|
|
9374
9428
|
* Limits: SRT ≤ 2 MiB and ≤ 10,000 cues; ASS ≤ 2 MiB with `[Script Info]`
|
|
9375
9429
|
* and `[Events]` and 1–20,000 `Dialogue:` lines. Convert other formats
|
|
9376
|
-
* (VTT, SAMI) to SRT first.
|
|
9430
|
+
* (VTT, SAMI) to SRT first. Per source, a plugin has at most 4 loaded
|
|
9431
|
+
* tracks at once and loads at most 16 files (removed ones count), and
|
|
9432
|
+
* every plugin together at most 16 tracks; see `SUBTITLE_LIMITS` in
|
|
9433
|
+
* `@vestra-player/sdk/testing`.
|
|
9377
9434
|
* @capability subtitles.control and subtitles.read
|
|
9378
9435
|
* @throws invalid (rejects) A bad `format` or `title`, or a file over the limits.
|
|
9379
9436
|
* @throws stale (rejects) With `ifCurrent`: the source changed since that state.
|
|
9380
9437
|
* @throws unavailable (rejects) Nothing loaded, or the source is still loading.
|
|
9381
9438
|
* @throws aborted (rejects) `signal` aborted before the commit (an `AbortError`); the upload is discarded.
|
|
9382
|
-
* @throws busy (rejects) Too many subtitle operations at once.
|
|
9439
|
+
* @throws busy (rejects) Too many subtitle operations at once, or a limit of `SUBTITLE_LIMITS` is reached (4 tracks of this plugin, 16 loads of this plugin or 16 tracks in the source, or another of its limits); nothing is loaded.
|
|
9383
9440
|
* @throws capability (rejects) The manifest does not declare `subtitles.control` and `subtitles.read`.
|
|
9384
9441
|
* @example Load a downloaded SRT and select it
|
|
9385
9442
|
* A subtitle file the plugin downloaded becomes a track of the current file
|
|
@@ -12582,7 +12639,7 @@ class TestHost implements Transport {
|
|
|
12582
12639
|
owner: string | null;
|
|
12583
12640
|
/** The values the player holds. */
|
|
12584
12641
|
values: RenderSettings;
|
|
12585
|
-
/** `true`: GPU rendering; `false`: software rendering (`unsupported`). */
|
|
12642
|
+
/** `true`: GPU rendering; `false`: software rendering (`unsupported`). `plugin.app.system()` reports it as `rendering`. */
|
|
12586
12643
|
gpu: boolean;
|
|
12587
12644
|
};
|
|
12588
12645
|
/**
|
|
@@ -12671,6 +12728,8 @@ class TestHost implements Transport {
|
|
|
12671
12728
|
bytes: Buffer[];
|
|
12672
12729
|
/** The announced total size in bytes. */
|
|
12673
12730
|
expected: number;
|
|
12731
|
+
/** The source generation it was begun for; like the player, a new source drops the uploads of the old one. */
|
|
12732
|
+
generation?: number;
|
|
12674
12733
|
}>;
|
|
12675
12734
|
/** Snapshots taken with `media.capture`, by snapshot id. Wire-level. */
|
|
12676
12735
|
readonly snapshots: Map<number, Json>;
|
|
@@ -12824,8 +12883,10 @@ class TestHost implements Transport {
|
|
|
12824
12883
|
* `plugin.storage.session`: key → value, as the player keeps them. Preset entries to simulate values an earlier run of the plugin stored in the same player session, or pass `startPlugin(..., { sessionStorage })`. Like the player, a key is 1–1024 bytes, a `set` that would make the entries larger than 1 MiB (keys, values as compact JSON and `SESSION_STORAGE_LIMITS.entryBytes` per key) fails `limit`, and an object's properties are stored sorted, as the player hands them back.
|
|
12825
12884
|
*/
|
|
12826
12885
|
readonly sessionStorage: Map<string, Json>;
|
|
12827
|
-
/**
|
|
12828
|
-
|
|
12886
|
+
/**
|
|
12887
|
+
* What `plugin.app.system()` reports. Its `rendering` follows videoRender.gpu (`"gpu"` or `"software"`), so one switch covers the render options and the fact.
|
|
12888
|
+
*/
|
|
12889
|
+
systemInfo: Omit<WireSystemInfo, "rendering">;
|
|
12829
12890
|
/** What `plugin.app` reports. Set it before startPlugin: the plugin receives it once at start-up. */
|
|
12830
12891
|
app: {
|
|
12831
12892
|
/** The player version. */
|
|
@@ -14061,6 +14122,61 @@ if (!token || Buffer.byteLength(token) > SECRET_LIMITS.valueBytes) {
|
|
|
14061
14122
|
}
|
|
14062
14123
|
```
|
|
14063
14124
|
|
|
14125
|
+
### SUBTITLE_LIMITS
|
|
14126
|
+
|
|
14127
|
+
The player's limits on subtitle files a plugin loads with
|
|
14128
|
+
Subtitles.load, which `TestHost` enforces with the player's codes.
|
|
14129
|
+
The track counts are per source: opening another file starts from zero.
|
|
14130
|
+
A file over `fileBytes` fails with `invalid`; every other limit fails with
|
|
14131
|
+
`busy`, and nothing is loaded.
|
|
14132
|
+
|
|
14133
|
+
Each `load` uses a new resource on the source, and removing the track
|
|
14134
|
+
frees its place among the live tracks (`tracksPerPlugin`, `tracks`,
|
|
14135
|
+
`totalBytes`) but not among the loads (`loadsPerPlugin`, `loads`). So one
|
|
14136
|
+
plugin loads at most 16 files into one source, with at most 4 of them
|
|
14137
|
+
present at once.
|
|
14138
|
+
|
|
14139
|
+
```ts
|
|
14140
|
+
const SUBTITLE_LIMITS: Readonly<{
|
|
14141
|
+
/** UTF-8 bytes of one file; a larger one fails with `invalid` (2 MiB). */
|
|
14142
|
+
readonly fileBytes: number;
|
|
14143
|
+
/** Tracks one plugin has loaded into the current source at the same time (4). */
|
|
14144
|
+
readonly tracksPerPlugin: 4;
|
|
14145
|
+
/** Loaded tracks of every plugin in the current source at the same time (16). */
|
|
14146
|
+
readonly tracks: 16;
|
|
14147
|
+
/** Bytes of the loaded tracks of every plugin in the current source together (16 MiB). */
|
|
14148
|
+
readonly totalBytes: number;
|
|
14149
|
+
/** Loads by one plugin into the current source, removed tracks included (16). */
|
|
14150
|
+
readonly loadsPerPlugin: 16;
|
|
14151
|
+
/** Loads by every plugin into the current source, removed tracks included (128). */
|
|
14152
|
+
readonly loads: 128;
|
|
14153
|
+
/** Loads of one plugin in progress at the same time (2). */
|
|
14154
|
+
readonly uploadsPerPlugin: 2;
|
|
14155
|
+
/** Loads of every plugin in progress at the same time (8). */
|
|
14156
|
+
readonly uploads: 8;
|
|
14157
|
+
}>;
|
|
14158
|
+
```
|
|
14159
|
+
|
|
14160
|
+
Example: Replace the plugin's oldest subtitle when it has four
|
|
14161
|
+
|
|
14162
|
+
The plugin keeps the files it loaded, newest last, and removes the oldest
|
|
14163
|
+
before a fifth would be refused with `busy`. A load the player still
|
|
14164
|
+
refuses (the source holds 16 tracks of every plugin, or this plugin
|
|
14165
|
+
already loaded 16 files into it) is reported on the OSD.
|
|
14166
|
+
```ts
|
|
14167
|
+
import { SUBTITLE_LIMITS } from "@vestra-player/sdk/testing";
|
|
14168
|
+
|
|
14169
|
+
const loaded: SubtitleTrack[] = []; // this source's, newest last
|
|
14170
|
+
if (loaded.length >= SUBTITLE_LIMITS.tracksPerPlugin)
|
|
14171
|
+
await plugin.subtitles.remove(loaded.shift()!);
|
|
14172
|
+
try {
|
|
14173
|
+
loaded.push(await plugin.subtitles.load(bytes));
|
|
14174
|
+
} catch (error) {
|
|
14175
|
+
if ((error as PluginError).code !== "busy") throw error;
|
|
14176
|
+
await plugin.osd.show("No room for another subtitle on this file.");
|
|
14177
|
+
}
|
|
14178
|
+
```
|
|
14179
|
+
|
|
14064
14180
|
### UI_VALUE_LIMITS
|
|
14065
14181
|
|
|
14066
14182
|
The player's limits on one property value (a `PropValue` as written
|
|
@@ -14549,7 +14665,7 @@ interface SubtitleState {
|
|
|
14549
14665
|
|
|
14550
14666
|
### WireSystemInfo
|
|
14551
14667
|
|
|
14552
|
-
`app.system`: the operating system and the
|
|
14668
|
+
`app.system`: the operating system, the displays and the renderer.
|
|
14553
14669
|
|
|
14554
14670
|
```ts
|
|
14555
14671
|
interface WireSystemInfo {
|
|
@@ -14567,6 +14683,8 @@ interface WireSystemInfo {
|
|
|
14567
14683
|
refreshRate: number | null;
|
|
14568
14684
|
primary: boolean;
|
|
14569
14685
|
}[];
|
|
14686
|
+
/** Player 0.5.12; older players leave it out. */
|
|
14687
|
+
rendering?: "gpu" | "software";
|
|
14570
14688
|
}
|
|
14571
14689
|
```
|
|
14572
14690
|
|
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.34: write plugins for Vestra Player in TypeScript (plugin API `vestra/1`). A plugin is a folder with `manifest.json` and an entry module whose default export `activate(plugin)` the player calls; it runs in its own Node.js process. Everything a plugin creates (commands, windows, panels, filters, listeners…) is a handle that is released automatically when the plugin stops. L<n>: a line of llms-full.txt (next to this file), which has every page, declaration and example.
|
|
4
4
|
|
|
5
5
|
**Rules**
|
|
6
6
|
|
|
@@ -33,7 +33,7 @@ my-plugin/
|
|
|
33
33
|
}
|
|
34
34
|
```
|
|
35
35
|
|
|
36
|
-
Fields: `id`, `version`, `apiVersion`, `entry`, `capabilities`, `name`, `author`, `punchline`, `description`, `tags`, `icon`, `readme`, `changelog`, `sourceUrl`, `license`, `minPlayerVersion`; unknown fields and capabilities are rejected. Rules and limits: L2507–
|
|
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: L2507–2575.
|
|
37
37
|
|
|
38
38
|
**main.ts**
|
|
39
39
|
|
|
@@ -76,9 +76,9 @@ export default async function activate(plugin: Plugin) {
|
|
|
76
76
|
- `ui.settings`: Settings.page/show
|
|
77
77
|
- `ui.theme`: Appearance.set/setLanguage
|
|
78
78
|
- `secrets`: Secrets.get/set/delete
|
|
79
|
-
- No capability: `tools`, `storage`, `system`, `app`, `ui` (images and Markdown), `i18n`, reading window.state, appearance.theme and packages.list. Details:
|
|
79
|
+
- No capability: `tools`, `storage`, `system`, `app`, `ui` (images and Markdown), `i18n`, reading window.state, appearance.theme and packages.list. Details: L2576–2634.
|
|
80
80
|
|
|
81
|
-
**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: L1948–2136; TestHost:
|
|
81
|
+
**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: L1948–2136; TestHost: L12044.
|
|
82
82
|
|
|
83
83
|
**Command line**
|
|
84
84
|
|
|
@@ -106,7 +106,7 @@ export default async function activate(plugin: Plugin) {
|
|
|
106
106
|
- `limit`: A player-wide limit was reached, a property value is over the limits of PropValue
|
|
107
107
|
- `reserved`: The id is `vestra` or starts with `vestra.`
|
|
108
108
|
- `versionMismatch`: The package needs a newer player
|
|
109
|
-
- Others: PluginError, llms-full.txt
|
|
109
|
+
- Others: PluginError, llms-full.txt L3634.
|
|
110
110
|
|
|
111
111
|
## Reference
|
|
112
112
|
|
|
@@ -115,30 +115,30 @@ export default async function activate(plugin: Plugin) {
|
|
|
115
115
|
- Concepts: How a plugin runs L238–408; Commands, menus and key bindings L409–520; Playback state, controls and sources L521–666; Video and audio processing L667–745; Windows, panels, settings pages, dialogs and the OSD L746–1034
|
|
116
116
|
- How-to: Control playback L1035–1163; Store a plugin's data L1164–1231; Load subtitles from a plugin L1232–1310; Keep a slider responsive while dragging L1311–1485; Remember a window's state between runs L1486–1615; Translate a plugin L1616–1799; Migrate a plugin to English source text L1800–1947; Test a plugin L1948–2136; Publish a plugin L2137–2225; Migrating off package rollback (1.5.16, removed in 1.5.17) L2226–2301
|
|
117
117
|
- Guidelines: Interface guidelines L2302–2409; Menu guidelines L2410–2506
|
|
118
|
-
- Reference: Manifest L2507–
|
|
119
|
-
- [API: @vestra-player/sdk](https://vestrasys.github.io/vestra-player-sdk/modules/_vestra-player_sdk.html):
|
|
120
|
-
- [API: @vestra-player/sdk/testing](https://vestrasys.github.io/vestra-player-sdk/modules/_vestra-player_sdk_testing.html):
|
|
118
|
+
- Reference: Manifest L2507–2575; Capabilities L2576–2634; Limits L2635–2764; Commands, menus and bindings L2765–3247; Platforms L3248–3276; Theme L3277–3498; Command line L3499–3556
|
|
119
|
+
- [API: @vestra-player/sdk](https://vestrasys.github.io/vestra-player-sdk/modules/_vestra-player_sdk.html): L3557–11728
|
|
120
|
+
- [API: @vestra-player/sdk/testing](https://vestrasys.github.io/vestra-player-sdk/modules/_vestra-player_sdk_testing.html): L11729–14320
|
|
121
121
|
|
|
122
122
|
## API
|
|
123
123
|
|
|
124
124
|
Where each `plugin.<module>` is declared:
|
|
125
125
|
|
|
126
|
-
- Plugin
|
|
127
|
-
- Commands
|
|
126
|
+
- Plugin L3954, Scope L3709, PluginError L3634, Disposable L3572
|
|
127
|
+
- Commands L4351; Windows L5288; Panels L6734; Dialogs L7381; Player L8122; video: VideoFilter L9002, VideoView L9102, RenderOptions L9189; audio: AudioFilter L9047; Subtitles L9336; media: MediaCapture L9740; Resolvers L9970; Packages L10368; Osd L8867; Host L8709; Clipboard L10696; Power L10783; Tools L10842; Storage L10994; System L11193; Secrets L11273; window: MainWindow L7051; App L7231; Bindings L10596; Ui L4640; Timeline L6963; Drops L5981; Appearance L6062; I18n L6295; Settings L6578.
|
|
128
128
|
|
|
129
129
|
## Examples
|
|
130
130
|
|
|
131
131
|
Tested plugins in [examples/](https://github.com/vestrasys/vestra-player-sdk/tree/main/examples):
|
|
132
132
|
|
|
133
|
-
- hello: The README Quick start: one menu command and an on-screen message.
|
|
134
|
-
- blur: A tool window with one slider that blurs the playing video.
|
|
135
|
-
- signal-generator: Generates its own video and audio: colour bars and a 1 kHz tone.
|
|
136
|
-
- bookmark: A tool window bound to the playback state: remember the position and jump back to it.
|
|
137
|
-
- panel-menu: A sidebar panel with its own right-click menu.
|
|
138
|
-
- menus: Radio menu items, separator groups and a menu item whose title changes.
|
|
139
|
-
- theme-widgets: A tool window drawn with the player's theme and widgets.
|
|
140
|
-
- markdown: A window that shows a Markdown document.
|
|
141
|
-
- i18n: A plugin in the player's language: English source text, a Chinese translation.
|
|
133
|
+
- hello: The README Quick start: one menu command and an on-screen message. L14753–14842
|
|
134
|
+
- blur: A tool window with one slider that blurs the playing video. L14843–15104
|
|
135
|
+
- signal-generator: Generates its own video and audio: colour bars and a 1 kHz tone. L15105–15437
|
|
136
|
+
- bookmark: A tool window bound to the playback state: remember the position and jump back to it. L15438–15624
|
|
137
|
+
- panel-menu: A sidebar panel with its own right-click menu. L15625–15833
|
|
138
|
+
- menus: Radio menu items, separator groups and a menu item whose title changes. L15834–15993
|
|
139
|
+
- theme-widgets: A tool window drawn with the player's theme and widgets. L15994–16157
|
|
140
|
+
- markdown: A window that shows a Markdown document. L16158–16354
|
|
141
|
+
- i18n: A plugin in the player's language: English source text, a Chinese translation. L16355–16522
|
|
142
142
|
|
|
143
143
|
## Optional
|
|
144
144
|
|