@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 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).
@@ -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";
@@ -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;
@@ -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 host()
288
- .discard(uploadId)
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
  }
@@ -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
- /** `true`: GPU rendering; `false`: software rendering (`unsupported`). */
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
- /** What `plugin.app.system()` reports. */
1599
- systemInfo: WireSystemInfo;
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.
@@ -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
- /** What `plugin.app.system()` reports. */
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
- #subtitleChange(expected) {
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": () => structuredClone(this.systemInfo),
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: Number(p.expectedBytes),
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
- const upload = this.uploads.get(Number(p.uploadId));
4167
- if (!upload)
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,
@@ -3991,8 +3991,9 @@ export interface AppInfo {
3991
3991
  */
3992
3992
  export interface App extends AppInfo {
3993
3993
  /**
3994
- * The operating system and the displays, read when called (displays come
3995
- * and go). No capability. CPU and memory: Node's `os` module.
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`. macOS OpenGL 4.1 has no compute shaders, so `peakDetect`
6298
- * is accepted but has no effect; output is always SDR.
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 displays. */
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.33: full reference
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 displays, read when called (displays come and go). No capability. CPU and memory: Node's `os` module.
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`. macOS OpenGL 4.1 has no compute shaders, so `peakDetect`
9146
- is accepted but has no effect; output is always SDR.
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
- /** What `plugin.app.system()` reports. */
12828
- systemInfo: WireSystemInfo;
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 displays.
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.33: 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.
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–2574.
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: L2575–2633.
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: L11987.
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 L3618.
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–2574; Capabilities L2575–2633; Limits L2634–2748; Commands, menus and bindings L2749–3231; Platforms L3232–3260; Theme L3261–3482; Command line L3483–3540
119
- - [API: @vestra-player/sdk](https://vestrasys.github.io/vestra-player-sdk/modules/_vestra-player_sdk.html): L3541–11671
120
- - [API: @vestra-player/sdk/testing](https://vestrasys.github.io/vestra-player-sdk/modules/_vestra-player_sdk_testing.html): L11672–14204
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 L3938, Scope L3693, PluginError L3618, Disposable L3556
127
- - Commands L4335; Windows L5272; Panels L6718; Dialogs L7329; Player L8070; video: VideoFilter L8950, VideoView L9050, RenderOptions L9137; audio: AudioFilter L8995; Subtitles L9282; media: MediaCapture L9683; Resolvers L9913; Packages L10311; Osd L8815; Host L8657; Clipboard L10639; Power L10726; Tools L10785; Storage L10937; System L11136; Secrets L11216; window: MainWindow L7035; App L7215; Bindings L10539; Ui L4624; Timeline L6947; Drops L5965; Appearance L6046; I18n L6279; Settings L6562.
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. L14635–14724
134
- - blur: A tool window with one slider that blurs the playing video. L14725–14986
135
- - signal-generator: Generates its own video and audio: colour bars and a 1 kHz tone. L14987–15319
136
- - bookmark: A tool window bound to the playback state: remember the position and jump back to it. L15320–15506
137
- - panel-menu: A sidebar panel with its own right-click menu. L15507–15715
138
- - menus: Radio menu items, separator groups and a menu item whose title changes. L15716–15875
139
- - theme-widgets: A tool window drawn with the player's theme and widgets. L15876–16039
140
- - markdown: A window that shows a Markdown document. L16040–16236
141
- - i18n: A plugin in the player's language: English source text, a Chinese translation. L16237–16404
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
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vestra-player/sdk",
3
- "version": "1.5.33",
3
+ "version": "1.5.34",
4
4
  "description": "Plugin API types, test host and plugin CLI for Vestra Player (apiVersion vestra/1)",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {