@vestra-player/sdk 1.5.53 → 1.5.55

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,25 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.5.55
4
+
5
+ **Who must change: nobody, but check `TestHost` tests that update a disabled plugin.** Additive: `InstallOptions` gains `enabled` and `TestHost` gains `installKeepsDisabled`, `apiVersion` stays `vestra/1`, no new capability, and existing code still compiles. Older players ignore the option, and an install there always enables the plugin. One `TestHost` behaviour change at run time: like player 0.6.4, it now keeps a `disabled` entry disabled when the plugin under test installs an update or reinstall over it without `enabled`, where it used to make it `running`. A test that expects the update to run (the player's Store test did) sets `host.installKeepsDisabled = false` to keep playing players up to 0.6.3, or asserts the new rule; a test that covers both runs once with each.
6
+
7
+ - **Choose whether an installed plugin runs** (`candidate.install({ enabled })`). Planned for player 0.6.4 (not released yet), where an update or reinstall (from any source, `replace` included) keeps the installed plugin's state when `enabled` is left out: a plugin the user or a store disabled stays disabled, and only its files are installed. A first install runs as before. `true` enables and starts it, for a store that disabled a revoked version and installs the update that fixes it (if the new version fails to start, the old one is restored, still disabled); `false` installs it disabled, stopping a running version and dropping its session values as `setEnabled(id, false)` does. Built-in plugins always run. The SDK sends `enabled` only when given and refuses anything but a boolean with `invalid` before sending. On players up to 0.6.3 the option is ignored and every install runs; call `packages.setEnabled` after the install is committed when the state matters there.
8
+ - **`TestHost`** models player 0.6.4 by default: a first install is `running`, an update of a `disabled` entry stays `disabled` unless `enabled: true`, `enabled: false` installs it `disabled`, and a non-boolean `enabled` on the wire is `invalid` (`null` reads as absent, as in the player). `installKeepsDisabled = false` plays players up to 0.6.3 instead: every install is `running` and `enabled` is ignored. `packageCalls` records `install`'s `enabled` when it was given.
9
+ - **Stop reasons, corrected** (SDK 1.5.54 docs, from the player's implementation for 0.6.4, still planned). `upgrade` also covers a new version stopped because its install was cancelled (the previous version comes back). `idle` comes only with lazy activation, which is off by default (`VESTRA_LAZY=1`), and also parks a plugin whose first run it judges lazy right after it is ready, not only after a while without use. `failed` is not sent for a full input queue: an own process is killed, and a member's full queue restarts the shared runner as a whole, as does an unreadable line from the runner itself. In those cases, and when an own process fails to activate or the process has already exited, the plugin gets no stop at all and its `onDispose` cleanups do not run. `host-lost` is not proof of a crash: player 0.6.4 sends `quit` on a normal exit, while an older player could close its pipes first when its window closed. [How a plugin runs › Restarts](docs/explanation/plugins.md#restarts-activate-can-run-more-than-once) now states as fact that plugins sharing one process are restarted together (player 0.5.6 and later). `StopReason`, [The plugin model](docs/explanation/plugins.md#stopping) and [Host and Node](docs/explanation/host-and-node.md#stopping-a-plugin) say so.
10
+ - _Docs_: `InstallOptions.enabled`, `PackageCandidate.install` (with an example; the `replace` example no longer promises a running plugin), `TestHost.candidates` and `TestHost.installKeepsDisabled` describe it.
11
+
12
+ ## 1.5.54
13
+
14
+ **Who must change: nobody, but check cleanup functions passed directly to `onDispose`.** Additive: `Plugin.stopReason`, the types `StopReason` and `DisposeInfo`, an optional argument for `onDispose` cleanups, `TestHost.previousCrash`, an optional `code` for `TestHost.failVideoShaders` and `stop({ reason })` / `disconnect()` from `startPlugin`; `apiVersion` stays `vestra/1`, no new capability, and existing code still compiles. One runtime difference: with this SDK in the player (or in `TestHost`) an `onDispose` cleanup is called with one argument, the `DisposeInfo`. A function that takes optional parameters of its own and is passed directly now receives it as its first argument: `plugin.onDispose(controller.abort.bind(controller))` aborts with the `DisposeInfo` as the abort reason, `onDispose(save)` with `save(force?: boolean)` gets an object as `force`. Wrap such a function (`() => controller.abort()`). The stop reasons are planned for player 0.6.4; `previous-crash` comes from player 0.6.3.
15
+
16
+ - **Why the plugin stops** (API request of the Shader Upscaling author). A plugin can tell an orderly stop from a lost player: `plugin.stopReason` is set when the stop begins, and every `onDispose` cleanup receives a `DisposeInfo` (`{ stopping, reason }`); a cleanup without parameters works as before. `stopping` is `false` for a child scope the plugin's own code disposed. Planned for player 0.6.4 (not released yet): the player sends `quit`, `disable`, `uninstall`, `reload`, `upgrade`, `idle` or `failed` with `runtime.stop`, and its runner passes `host-lost` when its input closes (or its output breaks) without a `runtime.stop` (the player crashed or was killed; requests are refused, clean up locally only). The runner hands the reason to the SDK (`PluginInstance.dispose({ reason })` in `@vestra-player/sdk/runtime`; a reason the SDK does not know reads as `undefined`).
17
+ - **Older players.** Players up to 0.6.3 give no reason for any stop, a lost player included. One whose own SDK is older than 1.5.54 (0.6.3 and earlier) calls the cleanup **without an argument** and has no `plugin.stopReason` (it reads `undefined`): the parameter is optional (`info?: DisposeInfo`), so read it as `info?.reason`.
18
+ - **`previous-crash`** (player 0.6.3) is documented on `VideoShaders`, `RenderOptions` and in the `PluginError` table: after a run of the player that ended while plugin shaders were drawing (a crash, the process being ended, a lost GPU device), the player refuses every non-empty shader list, the render options' `shaders` included, with `previous-crash` and a message naming the plugins; one such end keeps them off for the next start, two within a day until the GPU, its driver or the player changes, or for 7 days. Clearing still works, the player shows one OSD line itself, and plugins should not set the list again while it is refused (the SDK does not retry a `failed` handle). The render options send every value set so far with each `set`, the refused `shaders` included: include `shaders: []` in the next `set` to go on without them.
19
+ - **`TestHost`**: `previousCrash` (a message, default `null`) refuses non-empty `video.shaders` lists and render options `shaders` with `previous-crash` as the player does, after the files are checked and before the GPU check. `failVideoShaders(id, message, code?)` (`failed` by default, or `previous-crash`) and `failVideoRender("previous-crash", …)` are shortcuts that only set the handle's status: the real player refuses the request instead, which `previousCrash` plays. `startPlugin`'s `stop` still declares no parameter (`stop.length === 0`, `t.after(stop)`, `promise.then(stop)`) and takes an optional `{ reason }`; `disconnect()` stops the plugin as when the player is lost (`host-lost`; requests in flight and later ones are refused with `disabled`, as the runner does).
20
+ - **`TestHost` cleanup never fails a test.** `close()` (and so `stop()`) retries removing its temporary directory (`maxRetries: 5`, 100 ms apart) and leaves it to the system when Windows still refuses (an intermittent `ENOTEMPTY` on Windows CI, plugin repository run 41351136515).
21
+ - _Docs_: [The plugin model](docs/explanation/plugins.md#stopping), [Host and Node](docs/explanation/host-and-node.md#stopping-a-plugin), [Video and audio processing](docs/explanation/processing.md#shaders), [Test a plugin](docs/how-to/testing.md) and [Reference: testing](docs/reference/testing.md) describe them.
22
+
3
23
  ## 1.5.53
4
24
 
5
25
  **Who must change: nobody.** No changes to the TypeScript API, `apiVersion` stays `vestra/1`, no new capability, no player version needed.
Binary file
@@ -19,7 +19,17 @@ export interface PluginInstance {
19
19
  * `RuntimeOptions.strictDeclarations` is set.
20
20
  */
21
21
  activated(): Promise<void>;
22
- dispose(): Promise<void>;
22
+ /**
23
+ * Stop the plugin: dispose the root scope, then stop the runtime.
24
+ * `info.reason` (SDK 1.5.54) is why: the runner passes `runtime.stop`'s
25
+ * `data.reason` (planned for player 0.6.4), or `"host-lost"` when its input closed
26
+ * without a `runtime.stop`. It reaches `plugin.stopReason` and every
27
+ * `onDispose` cleanup; a reason the SDK does not know reads as
28
+ * `undefined`. A second call runs no cleanup again and keeps the first reason.
29
+ */
30
+ dispose(info?: {
31
+ reason?: unknown;
32
+ }): Promise<void>;
23
33
  }
24
34
  export declare function createPlugin(transport: Transport, manifest: PluginManifest, directory: string, dataDirectory?: string,
25
35
  /** `plugin.app` (the runner passes the player's `VESTRA_APP_INFO`). */
@@ -1,4 +1,4 @@
1
- import { failure, Runtime } from "./runtime.js";
1
+ import { failure, Runtime, stopReason } from "./runtime.js";
2
2
  import { ScopeImpl } from "./scope.js";
3
3
  import { playerOf } from "./player.js";
4
4
  import { windowOf } from "./window.js";
@@ -27,7 +27,8 @@ options) {
27
27
  return {
28
28
  plugin: root,
29
29
  activated: () => runtime.activated(),
30
- dispose: async () => {
30
+ dispose: async (info) => {
31
+ runtime.stopping ??= { reason: stopReason(info?.reason) };
31
32
  await root.dispose();
32
33
  runtime.stop();
33
34
  },
@@ -83,9 +83,11 @@ function installOptions(options) {
83
83
  return {};
84
84
  if (typeof options !== "object" || options === null)
85
85
  throw failure("invalid", "install options must be an object");
86
- const { source, replace } = options;
86
+ const { source, replace, enabled } = options;
87
87
  if (replace !== undefined && typeof replace !== "boolean")
88
88
  throw failure("invalid", "replace must be a boolean");
89
+ if (enabled !== undefined && typeof enabled !== "boolean")
90
+ throw failure("invalid", "enabled must be a boolean");
89
91
  if (source !== undefined) {
90
92
  const kinds = ["repository", "link", "file", "workshop"];
91
93
  if (typeof source !== "object" ||
@@ -99,6 +101,7 @@ function installOptions(options) {
99
101
  return {
100
102
  ...(source === undefined ? {} : { source: { ...source } }),
101
103
  ...(replace ? { replace: true } : {}),
104
+ ...(enabled === undefined ? {} : { enabled }),
102
105
  };
103
106
  }
104
107
  function candidate(scope, directory, checked) {
@@ -1,5 +1,5 @@
1
1
  import type { Json, LocalServices, Transport, Wire } from "../wire.ts";
2
- import type { App, Disposable, Locale, PluginError, SystemInfo } from "./types.ts";
2
+ import type { App, Disposable, Locale, PluginError, StopReason, SystemInfo } from "./types.ts";
3
3
  /**
4
4
  * Why something listens, for the player's log line that says why a plugin
5
5
  * keeps running (`events.subscribe { why }`, player 0.5.21): computed
@@ -46,6 +46,8 @@ export interface RuntimeOptions {
46
46
  */
47
47
  strictDeclarations?: boolean;
48
48
  }
49
+ /** A stop reason the runner passed on, if this SDK knows it. */
50
+ export declare function stopReason(value: unknown): StopReason | undefined;
49
51
  export declare const same: (a: Json | undefined, b: Json | undefined) => boolean;
50
52
  /** One value in flight, only the newest waiting. Each push learns its fate. */
51
53
  export declare class Latest<T> {
@@ -73,6 +75,13 @@ export declare class Runtime {
73
75
  readonly views: Map<string, (callback: string, args: Json[]) => void>;
74
76
  readonly commands: Map<string, () => void>;
75
77
  readonly services: Map<string, unknown>;
78
+ /**
79
+ * Set when the player stops the plugin (`PluginInstance.dispose`), before
80
+ * the root scope is disposed: `DisposeInfo` and `plugin.stopReason`.
81
+ */
82
+ stopping: {
83
+ reason: StopReason | undefined;
84
+ } | undefined;
76
85
  /** `plugin.app`, from the start-up handshake; `system()` asks the player. */
77
86
  readonly app: App;
78
87
  /** `plugin.i18n.locale`, from the start-up handshake; English when unknown. */
@@ -96,6 +96,23 @@ const DECLARATION_REFUSALS = new Set([
96
96
  "conflict",
97
97
  "limit",
98
98
  ]);
99
+ /** The reasons `StopReason` names; anything else reads as `undefined`. */
100
+ const STOP_REASONS = new Set([
101
+ "quit",
102
+ "disable",
103
+ "uninstall",
104
+ "reload",
105
+ "upgrade",
106
+ "idle",
107
+ "failed",
108
+ "host-lost",
109
+ ]);
110
+ /** A stop reason the runner passed on, if this SDK knows it. */
111
+ export function stopReason(value) {
112
+ return typeof value === "string" && STOP_REASONS.has(value)
113
+ ? value
114
+ : undefined;
115
+ }
99
116
  export const same = (a, b) => JSON.stringify(a) === JSON.stringify(b);
100
117
  /** One value in flight, only the newest waiting. Each push learns its fate. */
101
118
  export class Latest {
@@ -191,6 +208,11 @@ export class Runtime {
191
208
  views = new Map();
192
209
  commands = new Map();
193
210
  services = new Map();
211
+ /**
212
+ * Set when the player stops the plugin (`PluginInstance.dispose`), before
213
+ * the root scope is disposed: `DisposeInfo` and `plugin.stopReason`.
214
+ */
215
+ stopping;
194
216
  /** `plugin.app`, from the start-up handshake; `system()` asks the player. */
195
217
  app;
196
218
  /** `plugin.i18n.locale`, from the start-up handshake; English when unknown. */
@@ -1,11 +1,12 @@
1
1
  import type { Change, Runtime } from "./runtime.ts";
2
- import type { Disposable, Plugin, PluginError, Scope } from "./types.ts";
2
+ import type { Disposable, DisposeInfo, Plugin, PluginError, Scope, StopReason } from "./types.ts";
3
3
  export declare class ScopeImpl implements Plugin {
4
4
  #private;
5
5
  readonly runtime: Runtime;
6
6
  constructor(runtime: Runtime, parent?: ScopeImpl);
7
7
  get id(): string;
8
8
  get dataDirectory(): string;
9
+ get stopReason(): StopReason | undefined;
9
10
  get disposed(): boolean;
10
11
  readonly commands: import("./types.ts").Commands;
11
12
  readonly when: import("./types.ts").When;
@@ -49,7 +50,7 @@ export declare class ScopeImpl implements Plugin {
49
50
  add<T extends Disposable>(item: T): T;
50
51
  /** Handles call this when disposed directly, so the scope forgets them. */
51
52
  forget(item: Disposable): void;
52
- onDispose(cleanup: () => void | Promise<void>): Disposable;
53
+ onDispose(cleanup: ((info?: DisposeInfo) => void | Promise<void>) | (() => void | Promise<void>)): Disposable;
53
54
  onError(listener: (error: PluginError) => void): Disposable;
54
55
  report(error: unknown): void;
55
56
  /** Invoke plugin code on its behalf: report failures, then refresh bindings. */
@@ -39,6 +39,9 @@ export class ScopeImpl {
39
39
  throw failure("unavailable", "the player provided no data directory");
40
40
  return directory;
41
41
  }
42
+ get stopReason() {
43
+ return this.runtime.stopping?.reason;
44
+ }
42
45
  get disposed() {
43
46
  return this.#disposed || (this.#parent?.disposed ?? false);
44
47
  }
@@ -102,7 +105,12 @@ export class ScopeImpl {
102
105
  return;
103
106
  done = true;
104
107
  this.forget(item);
105
- await cleanup();
108
+ const stopping = this.runtime.stopping;
109
+ await cleanup(Object.freeze(stopping
110
+ ? stopping.reason === undefined
111
+ ? { stopping: true }
112
+ : { stopping: true, reason: stopping.reason }
113
+ : { stopping: false }));
106
114
  },
107
115
  };
108
116
  return this.add(item);
@@ -18,7 +18,7 @@ import type { SubtitleState } from "../subtitles.ts";
18
18
  import { VIDEO_PROCESSING_LIMITS } from "../processing.ts";
19
19
  import type { RenderingMode, RenderingPreference, RenderingReason, RenderSettings, UpscalingDecodingCause, UpscalingPlugin, UpscalingReason, UpscalingVideoSize, ViewSettings } from "./types.ts";
20
20
  import type { MediaJob } from "../media.ts";
21
- import type { LanguagePreference, Locale, Plugin, TemplateArgs, TextValue, ThemeId } from "./types.ts";
21
+ import type { LanguagePreference, Locale, Plugin, StopReason, TemplateArgs, TextValue, ThemeId } from "./types.ts";
22
22
  import { LOCALES, LIST_SEPARATORS, TEXT_LIMITS } from "../i18n.ts";
23
23
  /** Throw one of these from `host.handle(...)` to simulate a definite refusal. */
24
24
  export { HostError };
@@ -1351,6 +1351,10 @@ export declare class TestHost implements Transport {
1351
1351
  * or switches to software rendering (`unsupported`, which also sets
1352
1352
  * `videoRender.gpu` to false and `rendering.reason` to `window-fallback`
1353
1353
  * with `message`). Pushed only when this plugin is the owner.
1354
+ * `previous-crash` (SDK 1.5.54) is a shortcut that only sets the handle's
1355
+ * status and reaches `onError`: the real player never pushes it, it
1356
+ * refuses the `set` request itself. To test that, set
1357
+ * {@link TestHost.previousCrash}.
1354
1358
  *
1355
1359
  * @example Make a shader fail to compile
1356
1360
  * The plugin's render options report `failed`, as when mpv rejects a
@@ -1372,7 +1376,7 @@ export declare class TestHost implements Transport {
1372
1376
  * @capability None.
1373
1377
  * @since 1.0.0
1374
1378
  */
1375
- failVideoRender(code: "failed" | "unsupported", message: string): void;
1379
+ failVideoRender(code: "failed" | "unsupported" | "previous-crash", message: string): void;
1376
1380
  /**
1377
1381
  * The shader channel (`plugin.video.shaders(id)`, SDK 1.5.39): the lists
1378
1382
  * the player holds for this plugin's handles by id (the real paths, after
@@ -1405,6 +1409,12 @@ export declare class TestHost implements Transport {
1405
1409
  * rendering (`unsupported`; use {@link TestHost.failVideoRender}, which
1406
1410
  * also tells the handles). Pushed only to a handle that holds shaders.
1407
1411
  *
1412
+ * `code` (SDK 1.5.54, default `failed`) is the error code pushed.
1413
+ * `previous-crash` is a shortcut that only sets the handle's status and
1414
+ * reaches `onError`: the real player (0.6.3) never pushes it, it refuses
1415
+ * the `set` request itself after an abnormal end. To test that, set
1416
+ * {@link TestHost.previousCrash}.
1417
+ *
1408
1418
  * @capability None.
1409
1419
  *
1410
1420
  * @example Make the plugin's shader fail to compile
@@ -1430,7 +1440,37 @@ export declare class TestHost implements Transport {
1430
1440
  *
1431
1441
  * @since 1.5.39
1432
1442
  */
1433
- failVideoShaders(id: string, message: string): void;
1443
+ failVideoShaders(id: string, message: string, code?: "failed" | "previous-crash"): void;
1444
+ /**
1445
+ * Play a run after an abnormal end with plugin shaders on (player 0.6.3):
1446
+ * while set, a non-empty `video.shaders` list and a render options
1447
+ * `set` with a non-empty `shaders` list are refused with
1448
+ * `previous-crash` and this message; clearing still works. `null`
1449
+ * (default): shaders apply.
1450
+ *
1451
+ * @example Check that the plugin stops retrying while shaders are off
1452
+ * The plugin's shader handle turns `failed` with `previous-crash`, as
1453
+ * in the player after a crash while plugin shaders were drawing. The
1454
+ * player refuses the list itself (and shows its own OSD line), so the
1455
+ * plugin should report it once and not set the list again.
1456
+ * ```ts
1457
+ * import assert from "node:assert/strict";
1458
+ * import { join } from "node:path";
1459
+ * import { writeFileSync } from "node:fs";
1460
+ * declare const host: TestHost;
1461
+ * // ---cut---
1462
+ * host.previousCrash = "plugin shaders are off for this start";
1463
+ * const file = join(plugin.dataDirectory, "sharpen.glsl");
1464
+ * writeFileSync(file, "//!HOOK MAIN\n");
1465
+ * const shaders = plugin.video.shaders("sharpen");
1466
+ * shaders.set([file]); // refused: the previous run ended abnormally
1467
+ * await host.settle();
1468
+ * assert.equal(shaders.status.state, "failed");
1469
+ * ```
1470
+ *
1471
+ * @since 1.5.54
1472
+ */
1473
+ previousCrash: string | null;
1434
1474
  /**
1435
1475
  * The player's processing (filter) state. Wire-level; may change in minor
1436
1476
  * versions. `resources` holds each `video.filter` / `audio.filter` chain the
@@ -1942,6 +1982,11 @@ export declare class TestHost implements Transport {
1942
1982
  /** Start time in seconds. */
1943
1983
  start: number;
1944
1984
  }[];
1985
+ /**
1986
+ * @internal `startPlugin`'s `disconnect()`: refuse every request, the
1987
+ * ones in flight included, as the runner does once the player is gone.
1988
+ */
1989
+ loseConnection(): void;
1945
1990
  /**
1946
1991
  * `plugin.dataDirectory`: a fresh empty temporary directory, removed with
1947
1992
  * the host on `stop()`. `plugin.storage.json` reads and writes real files
@@ -2391,7 +2436,7 @@ export declare class TestHost implements Transport {
2391
2436
  method: "setEnabled" | "reload" | "uninstall" | "install";
2392
2437
  /** The target plugin id. */
2393
2438
  id: string;
2394
- /** `setEnabled`'s value. */
2439
+ /** `setEnabled`'s value, or `install`'s `enabled` option when given. */
2395
2440
  enabled?: boolean;
2396
2441
  /** `uninstall`'s `removeData` option. */
2397
2442
  removeData?: boolean;
@@ -2405,7 +2450,13 @@ export declare class TestHost implements Transport {
2405
2450
  * `.vxp` file) → its manifest. Other paths fail with `missing`. `install`
2406
2451
  * adds or upgrades the entry in {@link TestHost.packages} following the
2407
2452
  * player's source rules: the same source upgrades, another source needs
2408
- * `replace` (`conflict` otherwise), a built-in id is `unsupported`.
2453
+ * `replace` (`conflict` otherwise), a built-in id is `unsupported`. As
2454
+ * player 0.6.4 (planned), the installed entry's `state` follows
2455
+ * {@link "@vestra-player/sdk"!InstallOptions.enabled}: a first install
2456
+ * runs, an update of a `disabled` entry stays `disabled` unless `enabled`
2457
+ * is `true`, and `enabled: false` installs it `disabled`. Set
2458
+ * {@link TestHost.installKeepsDisabled} to `false` for the older players'
2459
+ * rule (every install runs).
2409
2460
  *
2410
2461
  * @example Offer a package file the plugin can install
2411
2462
  * The plugin can validate and install `/downloads/someone.demo-1.0.0.vxp`
@@ -2422,6 +2473,32 @@ export declare class TestHost implements Transport {
2422
2473
  * ```
2423
2474
  */
2424
2475
  readonly candidates: Map<string, PackageManifest>;
2476
+ /**
2477
+ * Which player's install rule `packages.install` plays. `true` (default,
2478
+ * SDK 1.5.55): player 0.6.4 (planned), where an update or reinstall
2479
+ * keeps a `disabled` plugin disabled unless the plugin passes
2480
+ * {@link "@vestra-player/sdk"!InstallOptions.enabled}. `false`: players
2481
+ * up to 0.6.3, where every install runs the plugin and `enabled` is
2482
+ * ignored (it is still checked and recorded in
2483
+ * {@link TestHost.packageCalls}). A test written for the older rule that
2484
+ * updates a disabled plugin sets it to `false`; one that keeps testing
2485
+ * both runs once with each.
2486
+ *
2487
+ * @example Keep a test written for players up to 0.6.3
2488
+ * The plugin under test installs an update over a plugin the user
2489
+ * disabled and expects it to run afterwards, as players up to 0.6.3 do.
2490
+ * With `false` the host plays that rule; the default plays player 0.6.4,
2491
+ * where the update stays disabled.
2492
+ * ```ts
2493
+ * declare const host: TestHost;
2494
+ * // ---cut---
2495
+ * host.installKeepsDisabled = false; // every install runs, as before 0.6.4
2496
+ * ```
2497
+ *
2498
+ * @capability None.
2499
+ * @since 1.5.55
2500
+ */
2501
+ installKeepsDisabled: boolean;
2425
2502
  /**
2426
2503
  * Remote images (`plugin.ui.markdown`, `plugin.ui.remoteImage`): address
2427
2504
  * → a local PNG or JPEG file served for it, or `null` for a failing
@@ -2754,7 +2831,11 @@ export declare class TestHost implements Transport {
2754
2831
  * @returns `host`: the {@link TestHost}; `plugin`: the object passed to
2755
2832
  * `activate` (call the plugin API on it directly); `stop()`: dispose the
2756
2833
  * plugin like the player stopping it (scopes released, stores flushed),
2757
- * wait until the host is idle and remove the host's temporary files.
2834
+ * wait until the host is idle and remove the host's temporary files;
2835
+ * `stop({ reason })` (SDK 1.5.54): `reason` is the `StopReason` the player
2836
+ * sends; `disconnect()` (SDK 1.5.54) stops it as when the player is lost
2837
+ * (`host-lost`, requests refused with `disabled`). Removing the
2838
+ * temporary files never fails a test.
2758
2839
  *
2759
2840
  * @example Test that a setting survives a restart
2760
2841
  * The test turns on the 10-step option, stops the plugin, starts it again
@@ -2843,6 +2924,25 @@ export declare function startPlugin(activate: (plugin: Plugin) => unknown, optio
2843
2924
  host: TestHost;
2844
2925
  /** The `plugin` object passed to `activate`. */
2845
2926
  plugin: Plugin;
2846
- /** Stop the plugin, wait for the host, and remove temporary files. */
2847
- stop(): Promise<void>;
2927
+ /**
2928
+ * Stop the plugin, wait for the host, and remove temporary files.
2929
+ * `stop({ reason })` (SDK 1.5.54): `reason` is the reason the player
2930
+ * gives (`plugin.stopReason`, the `onDispose` cleanups'
2931
+ * `info.reason`); `stop()`: none (`undefined`), as with players up to
2932
+ * 0.6.3. It still takes no declared parameter, so it can be passed as
2933
+ * a callback (`t.after(stop)`, `promise.then(stop)`).
2934
+ */
2935
+ stop: {
2936
+ (): Promise<void>;
2937
+ (options: {
2938
+ reason?: StopReason;
2939
+ }): Promise<void>;
2940
+ };
2941
+ /**
2942
+ * Lose the player (SDK 1.5.54): as when it crashes or is killed, the
2943
+ * runner stops the plugin with reason `host-lost`; requests still
2944
+ * waiting for their answer and every later request are refused with
2945
+ * `disabled`. Then waits and removes the temporary files like `stop`.
2946
+ */
2947
+ disconnect(): Promise<void>;
2848
2948
  }>;
@@ -1510,6 +1510,10 @@ export class TestHost {
1510
1510
  * or switches to software rendering (`unsupported`, which also sets
1511
1511
  * `videoRender.gpu` to false and `rendering.reason` to `window-fallback`
1512
1512
  * with `message`). Pushed only when this plugin is the owner.
1513
+ * `previous-crash` (SDK 1.5.54) is a shortcut that only sets the handle's
1514
+ * status and reaches `onError`: the real player never pushes it, it
1515
+ * refuses the `set` request itself. To test that, set
1516
+ * {@link TestHost.previousCrash}.
1513
1517
  *
1514
1518
  * @example Make a shader fail to compile
1515
1519
  * The plugin's render options report `failed`, as when mpv rejects a
@@ -1581,6 +1585,12 @@ export class TestHost {
1581
1585
  * rendering (`unsupported`; use {@link TestHost.failVideoRender}, which
1582
1586
  * also tells the handles). Pushed only to a handle that holds shaders.
1583
1587
  *
1588
+ * `code` (SDK 1.5.54, default `failed`) is the error code pushed.
1589
+ * `previous-crash` is a shortcut that only sets the handle's status and
1590
+ * reaches `onError`: the real player (0.6.3) never pushes it, it refuses
1591
+ * the `set` request itself after an abnormal end. To test that, set
1592
+ * {@link TestHost.previousCrash}.
1593
+ *
1584
1594
  * @capability None.
1585
1595
  *
1586
1596
  * @example Make the plugin's shader fail to compile
@@ -1606,14 +1616,44 @@ export class TestHost {
1606
1616
  *
1607
1617
  * @since 1.5.39
1608
1618
  */
1609
- failVideoShaders(id, message) {
1619
+ failVideoShaders(id, message, code = "failed") {
1610
1620
  if ((this.videoShaders.handles.get(id)?.length ?? 0) > 0)
1611
1621
  this.emit("video.shaders.status", {
1612
1622
  id,
1613
1623
  state: "failed",
1614
- error: { code: "failed", message },
1624
+ error: { code, message },
1615
1625
  });
1616
1626
  }
1627
+ /**
1628
+ * Play a run after an abnormal end with plugin shaders on (player 0.6.3):
1629
+ * while set, a non-empty `video.shaders` list and a render options
1630
+ * `set` with a non-empty `shaders` list are refused with
1631
+ * `previous-crash` and this message; clearing still works. `null`
1632
+ * (default): shaders apply.
1633
+ *
1634
+ * @example Check that the plugin stops retrying while shaders are off
1635
+ * The plugin's shader handle turns `failed` with `previous-crash`, as
1636
+ * in the player after a crash while plugin shaders were drawing. The
1637
+ * player refuses the list itself (and shows its own OSD line), so the
1638
+ * plugin should report it once and not set the list again.
1639
+ * ```ts
1640
+ * import assert from "node:assert/strict";
1641
+ * import { join } from "node:path";
1642
+ * import { writeFileSync } from "node:fs";
1643
+ * declare const host: TestHost;
1644
+ * // ---cut---
1645
+ * host.previousCrash = "plugin shaders are off for this start";
1646
+ * const file = join(plugin.dataDirectory, "sharpen.glsl");
1647
+ * writeFileSync(file, "//!HOOK MAIN\n");
1648
+ * const shaders = plugin.video.shaders("sharpen");
1649
+ * shaders.set([file]); // refused: the previous run ended abnormally
1650
+ * await host.settle();
1651
+ * assert.equal(shaders.status.state, "failed");
1652
+ * ```
1653
+ *
1654
+ * @since 1.5.54
1655
+ */
1656
+ previousCrash = null;
1617
1657
  /**
1618
1658
  * The player's processing (filter) state. Wire-level; may change in minor
1619
1659
  * versions. `resources` holds each `video.filter` / `audio.filter` chain the
@@ -2099,6 +2139,25 @@ export class TestHost {
2099
2139
  #overrides = new Map();
2100
2140
  #listeners = new Map();
2101
2141
  #inFlight = 0;
2142
+ /** `disconnect()` of `startPlugin`: the player is gone. */
2143
+ #lost = false;
2144
+ #lose = () => { };
2145
+ /** Rejects when the player is lost: requests in flight fail with it. */
2146
+ #gone = (() => {
2147
+ const gone = new Promise((_, reject) => {
2148
+ this.#lose = reject;
2149
+ });
2150
+ gone.catch(() => { });
2151
+ return gone;
2152
+ })();
2153
+ /**
2154
+ * @internal `startPlugin`'s `disconnect()`: refuse every request, the
2155
+ * ones in flight included, as the runner does once the player is gone.
2156
+ */
2157
+ loseConnection() {
2158
+ this.#lost = true;
2159
+ this.#lose(new HostError("disabled", "plugin stopped"));
2160
+ }
2102
2161
  #next = { snapshot: 0, job: 0, producer: 0, source: 0, resolution: 0 };
2103
2162
  #sourceBook = new Map();
2104
2163
  /** The pending resolutions' stamps and operations (beside `resolutions`). */
@@ -2573,6 +2632,10 @@ export class TestHost {
2573
2632
  * override from {@link TestHost.handle} or the default behaviour.
2574
2633
  */
2575
2634
  async request(method, params = {}) {
2635
+ // After `disconnect()`: refused as the runner refuses them once the
2636
+ // player is gone.
2637
+ if (this.#lost)
2638
+ throw new HostError("disabled", "plugin is stopping");
2576
2639
  const record = (params ?? {});
2577
2640
  // The plugin runner refuses a request over the message limit before
2578
2641
  // sending it (runtime/runner.ts): the player never sees it.
@@ -2583,10 +2646,15 @@ export class TestHost {
2583
2646
  this.#inFlight++;
2584
2647
  try {
2585
2648
  await tick();
2649
+ if (this.#lost)
2650
+ throw new HostError("disabled", "plugin stopped");
2586
2651
  const handler = this.#overrides.get(method) ?? this.#defaults[method];
2587
2652
  if (!handler)
2588
2653
  throw new HostError("unsupported", `test host has no ${method}`);
2589
- return (await handler(record));
2654
+ const work = Promise.resolve(handler(record));
2655
+ // `disconnect()` fails the requests still waiting, as the runner does.
2656
+ work.catch(() => { });
2657
+ return (await Promise.race([work, this.#gone]));
2590
2658
  }
2591
2659
  finally {
2592
2660
  this.#inFlight--;
@@ -3031,7 +3099,13 @@ export class TestHost {
3031
3099
  * `.vxp` file) → its manifest. Other paths fail with `missing`. `install`
3032
3100
  * adds or upgrades the entry in {@link TestHost.packages} following the
3033
3101
  * player's source rules: the same source upgrades, another source needs
3034
- * `replace` (`conflict` otherwise), a built-in id is `unsupported`.
3102
+ * `replace` (`conflict` otherwise), a built-in id is `unsupported`. As
3103
+ * player 0.6.4 (planned), the installed entry's `state` follows
3104
+ * {@link "@vestra-player/sdk"!InstallOptions.enabled}: a first install
3105
+ * runs, an update of a `disabled` entry stays `disabled` unless `enabled`
3106
+ * is `true`, and `enabled: false` installs it `disabled`. Set
3107
+ * {@link TestHost.installKeepsDisabled} to `false` for the older players'
3108
+ * rule (every install runs).
3035
3109
  *
3036
3110
  * @example Offer a package file the plugin can install
3037
3111
  * The plugin can validate and install `/downloads/someone.demo-1.0.0.vxp`
@@ -3048,6 +3122,32 @@ export class TestHost {
3048
3122
  * ```
3049
3123
  */
3050
3124
  candidates = new Map();
3125
+ /**
3126
+ * Which player's install rule `packages.install` plays. `true` (default,
3127
+ * SDK 1.5.55): player 0.6.4 (planned), where an update or reinstall
3128
+ * keeps a `disabled` plugin disabled unless the plugin passes
3129
+ * {@link "@vestra-player/sdk"!InstallOptions.enabled}. `false`: players
3130
+ * up to 0.6.3, where every install runs the plugin and `enabled` is
3131
+ * ignored (it is still checked and recorded in
3132
+ * {@link TestHost.packageCalls}). A test written for the older rule that
3133
+ * updates a disabled plugin sets it to `false`; one that keeps testing
3134
+ * both runs once with each.
3135
+ *
3136
+ * @example Keep a test written for players up to 0.6.3
3137
+ * The plugin under test installs an update over a plugin the user
3138
+ * disabled and expects it to run afterwards, as players up to 0.6.3 do.
3139
+ * With `false` the host plays that rule; the default plays player 0.6.4,
3140
+ * where the update stays disabled.
3141
+ * ```ts
3142
+ * declare const host: TestHost;
3143
+ * // ---cut---
3144
+ * host.installKeepsDisabled = false; // every install runs, as before 0.6.4
3145
+ * ```
3146
+ *
3147
+ * @capability None.
3148
+ * @since 1.5.55
3149
+ */
3150
+ installKeepsDisabled = true;
3051
3151
  /**
3052
3152
  * Remote images (`plugin.ui.markdown`, `plugin.ui.remoteImage`): address
3053
3153
  * → a local PNG or JPEG file served for it, or `null` for a failing
@@ -3424,7 +3524,20 @@ export class TestHost {
3424
3524
  close() {
3425
3525
  for (const producer of this.producers.values())
3426
3526
  producer.server.close();
3427
- rmSync(this.#scratch, { recursive: true, force: true });
3527
+ // Cleanup never fails a test: on Windows a file a closing handle still
3528
+ // holds makes the removal fail now and then (`ENOTEMPTY`, `EBUSY`,
3529
+ // `EPERM`); retry, then leave the directory to the system's cleanup.
3530
+ try {
3531
+ rmSync(this.#scratch, {
3532
+ recursive: true,
3533
+ force: true,
3534
+ maxRetries: 5,
3535
+ retryDelay: 100,
3536
+ });
3537
+ }
3538
+ catch {
3539
+ // Left in the temporary directory.
3540
+ }
3428
3541
  }
3429
3542
  // ------------------------------------------------------------ simulated host
3430
3543
  #intent(intent) {
@@ -4575,6 +4688,12 @@ export class TestHost {
4575
4688
  return null;
4576
4689
  },
4577
4690
  "video.render.set": (p) => {
4691
+ // The player's crash guard comes first (`processing_video.rs`).
4692
+ const listed = p.values?.shaders;
4693
+ if (this.previousCrash !== null &&
4694
+ Array.isArray(listed) &&
4695
+ listed.length > 0)
4696
+ throw new HostError("previous-crash", this.previousCrash);
4578
4697
  if (!this.videoRender.gpu)
4579
4698
  throw new HostError("unsupported", "render options need GPU rendering; the player renders in software");
4580
4699
  const owner = this.videoRender.owner;
@@ -4610,6 +4729,9 @@ export class TestHost {
4610
4729
  if (listProblem?.startsWith("at most"))
4611
4730
  throw new HostError("limit", listProblem);
4612
4731
  const paths = list.map((path) => ownFile(path, this.fileRoots, "shader", VIDEO_PROCESSING_LIMITS.shaderBytes));
4732
+ // The player's crash guard, after the files are checked.
4733
+ if (paths.length > 0 && this.previousCrash !== null)
4734
+ throw new HostError("previous-crash", this.previousCrash);
4613
4735
  if (paths.length > 0 && !this.videoRender.gpu)
4614
4736
  throw new HostError("unsupported", "shaders need GPU rendering; the player renders in software");
4615
4737
  const mine = [...this.videoShaders.handles]
@@ -5015,6 +5137,10 @@ export class TestHost {
5015
5137
  if (source.kind === "bundled")
5016
5138
  throw new HostError("invalid", "source bundled is reserved");
5017
5139
  const replace = p.replace === true;
5140
+ // As player 0.6.4: `enabled` is a boolean, or absent (`null` too).
5141
+ const requested = p.enabled ?? undefined;
5142
+ if (requested !== undefined && typeof requested !== "boolean")
5143
+ throw new HostError("invalid", "enabled must be a boolean");
5018
5144
  const entry = this.packages.find((each) => each.id === manifest.id);
5019
5145
  if (entry?.builtin)
5020
5146
  throw new HostError("unsupported", "built-in plugins are updated with the player");
@@ -5030,12 +5156,19 @@ export class TestHost {
5030
5156
  id: manifest.id,
5031
5157
  source,
5032
5158
  replace,
5159
+ ...(requested === undefined ? {} : { enabled: requested }),
5033
5160
  });
5161
+ // As player 0.6.4: a first install runs; an update or reinstall (any
5162
+ // source, `replace` included) keeps a disabled plugin disabled unless
5163
+ // `enabled` is given. Players up to 0.6.3 run every install.
5164
+ const enabled = !this.installKeepsDisabled
5165
+ ? true
5166
+ : (requested ?? entry?.state !== "disabled");
5034
5167
  const next = {
5035
5168
  id: manifest.id,
5036
5169
  version: manifest.version,
5037
5170
  origin: "installed",
5038
- state: "running",
5171
+ state: enabled ? "running" : "disabled",
5039
5172
  error: null,
5040
5173
  capabilities: [...manifest.capabilities],
5041
5174
  directory: `/test-host/packages/${manifest.id}`,
@@ -5455,7 +5588,11 @@ function tellResidency(host) {
5455
5588
  * @returns `host`: the {@link TestHost}; `plugin`: the object passed to
5456
5589
  * `activate` (call the plugin API on it directly); `stop()`: dispose the
5457
5590
  * plugin like the player stopping it (scopes released, stores flushed),
5458
- * wait until the host is idle and remove the host's temporary files.
5591
+ * wait until the host is idle and remove the host's temporary files;
5592
+ * `stop({ reason })` (SDK 1.5.54): `reason` is the `StopReason` the player
5593
+ * sends; `disconnect()` (SDK 1.5.54) stops it as when the player is lost
5594
+ * (`host-lost`, requests refused with `disabled`). Removing the
5595
+ * temporary files never fails a test.
5459
5596
  *
5460
5597
  * @example Test that a setting survives a restart
5461
5598
  * The test turns on the 10-step option, stops the plugin, starts it again
@@ -5552,14 +5689,43 @@ export async function startPlugin(activate, options = {}) {
5552
5689
  }
5553
5690
  await host.settle();
5554
5691
  tellResidency(host);
5692
+ // Zero declared parameters (`stop.length === 0`) and a `()` overload, so
5693
+ // `t.after(stop)`, `promise.then(stop)` and done-callback test runners
5694
+ // see the same function as before 1.5.54.
5695
+ const stop = async (...args) => {
5696
+ // Only an object with `reason` counts (a value `then` or a test
5697
+ // runner passes is ignored; an unknown reason reads as none).
5698
+ const first = args[0];
5699
+ const reason = typeof first === "object" && first !== null && "reason" in first
5700
+ ? first.reason
5701
+ : undefined;
5702
+ await instance.dispose({ reason });
5703
+ await host.settle();
5704
+ host.close();
5705
+ };
5555
5706
  return {
5556
5707
  /** The test host the plugin talks to. */
5557
5708
  host,
5558
5709
  /** The `plugin` object passed to `activate`. */
5559
5710
  plugin: instance.plugin,
5560
- /** Stop the plugin, wait for the host, and remove temporary files. */
5561
- async stop() {
5562
- await instance.dispose();
5711
+ /**
5712
+ * Stop the plugin, wait for the host, and remove temporary files.
5713
+ * `stop({ reason })` (SDK 1.5.54): `reason` is the reason the player
5714
+ * gives (`plugin.stopReason`, the `onDispose` cleanups'
5715
+ * `info.reason`); `stop()`: none (`undefined`), as with players up to
5716
+ * 0.6.3. It still takes no declared parameter, so it can be passed as
5717
+ * a callback (`t.after(stop)`, `promise.then(stop)`).
5718
+ */
5719
+ stop,
5720
+ /**
5721
+ * Lose the player (SDK 1.5.54): as when it crashes or is killed, the
5722
+ * runner stops the plugin with reason `host-lost`; requests still
5723
+ * waiting for their answer and every later request are refused with
5724
+ * `disabled`. Then waits and removes the temporary files like `stop`.
5725
+ */
5726
+ async disconnect() {
5727
+ host.loseConnection();
5728
+ await instance.dispose({ reason: "host-lost" });
5563
5729
  await host.settle();
5564
5730
  host.close();
5565
5731
  },
@@ -123,6 +123,7 @@ export interface Disposable {
123
123
  * | `resolution` | A file or web resolver's result could not be opened; `message` has the reason. |
124
124
  * | `revisionConflict` | {@link Packages}: the installed revision changed since it was read (another install or uninstall ran). Read again and retry; the SDK passes the revision it read, so this reaches plugins that act on old data. |
125
125
  * | `source-geometry`, `source-geometry-unsupported` | The source's picture size or rotation could not be applied: invalid decoder dimensions / a rotation that is not a multiple of 90°. |
126
+ * | `previous-crash` | {@link VideoShaders}, {@link RenderOptions} `shaders` (player 0.6.3): plugin shaders are off because the player ended abnormally while they were drawing; clearing still works. Do not retry while refused (the player shows its own OSD line). |
126
127
  * | `source-unavailable` | The source is not ready (still loading, or its time or native binding is not available yet). |
127
128
  * | `unknown-expired` | The player no longer knows the operation or snapshot (it expired or was released). The SDK maps it to `unknownExpired` results where it can; do not resubmit the operation blindly. |
128
129
  *
@@ -416,10 +417,36 @@ export interface Scope extends Disposable {
416
417
  * plugin.onDispose(() => clearInterval(timer));
417
418
  * ```
418
419
  *
420
+ * @example Save a cache at once when the player is lost
421
+ * The plugin writes its cache file at once when the player crashed or
422
+ * was killed, instead of waiting for the next periodic save. The cleanup
423
+ * may get a {@link DisposeInfo}: `stopping` says the plugin itself stops
424
+ * (not a child scope), `reason` why (planned for player 0.6.4). Players
425
+ * whose SDK is older than 1.5.54 call the cleanup without an argument,
426
+ * so read it with `?.`.
427
+ * ```ts
428
+ * declare function saveCacheNow(): void;
429
+ * // ---cut---
430
+ * plugin.onDispose((info) => {
431
+ * // The player is gone: ask it nothing, keep what is local.
432
+ * if (info?.reason === "host-lost") saveCacheNow();
433
+ * });
434
+ * ```
435
+ *
436
+ * @remarks
437
+ * From SDK 1.5.54 in the player (and in `TestHost`) the cleanup is called
438
+ * with one argument, a {@link DisposeInfo}: whether the plugin stops, and
439
+ * why. A player whose own SDK is older calls it without one (`info` is
440
+ * `undefined`). A cleanup that takes no argument works as before; a
441
+ * function with optional parameters of its own that is passed directly
442
+ * (`onDispose(controller.abort.bind(controller))`) now receives the
443
+ * `DisposeInfo` as its first argument: wrap it (`() => controller.abort()`)
444
+ * when that matters.
445
+ *
419
446
  * @capability None.
420
447
  * @since 1.0.0
421
448
  */
422
- onDispose(cleanup: () => void | Promise<void>): Disposable;
449
+ onDispose(cleanup: ((info?: DisposeInfo) => void | Promise<void>) | (() => void | Promise<void>)): Disposable;
423
450
  /**
424
451
  * Errors thrown by handlers that the SDK invoked (command `run`, window and
425
452
  * panel callbacks, `onChange` listeners) in this scope. Without a listener
@@ -547,6 +574,83 @@ export interface Plugin extends Scope {
547
574
  * none. See [How a plugin runs](../../docs/explanation/plugins.md#the-data-directory).
548
575
  */
549
576
  readonly dataDirectory: string;
577
+ /**
578
+ * Why the player stops this plugin (SDK 1.5.54; the reasons are planned
579
+ * for player 0.6.4): set when the stop begins, before any
580
+ * {@link Scope.onDispose} cleanup runs. `undefined` while the plugin
581
+ * runs, and for every stop by an older player (one whose own SDK is older
582
+ * than 1.5.54 does not have the property at all). See {@link StopReason}.
583
+ */
584
+ readonly stopReason: StopReason | undefined;
585
+ }
586
+ /**
587
+ * Why the player stopped the plugin ({@link DisposeInfo.reason},
588
+ * {@link Plugin.stopReason}).
589
+ *
590
+ * - `quit`: the player quits.
591
+ * - `disable`: the user disabled the plugin.
592
+ * - `uninstall`: the user uninstalls it.
593
+ * - `reload`: it is reloaded (from the plugin manager, or a development
594
+ * load with `--plugin`).
595
+ * - `upgrade`: an install or update replaces the running version; also a
596
+ * new version stopped because its install was cancelled (the previous
597
+ * version comes back).
598
+ * - `idle`: lazy activation parks it; its next use starts it again
599
+ * ([Idle plugins](../../docs/explanation/plugins.md#idle-plugins-are-stopped)).
600
+ * Lazy activation is off by default (`VESTRA_LAZY=1` turns it on); when
601
+ * on, it parks a plugin after a while without use, and parks one whose
602
+ * first run it judges lazy right after it is ready.
603
+ * - `failed`: the player stops it for a fault it can still tell the plugin
604
+ * about (planned to cover a malformed request, an unreadable or
605
+ * oversized line, an activation timeout, an uncaught exception the
606
+ * shared process attributes to this plugin).
607
+ * - `host-lost`: the plugin's runner saw its pipe to the player close
608
+ * without a stop: the player crashed or was killed. Requests are refused
609
+ * with `disabled`; clean up locally only. Player 0.6.4 sends `quit` on a
610
+ * normal exit; read `host-lost` as "the player is gone", not as proof of
611
+ * a crash.
612
+ *
613
+ * Some ends are no stop at all: the plugin gets no reason and its
614
+ * {@link Scope.onDispose} cleanups do not run. That is the case when its
615
+ * own process is killed because its input queue is full or a write to it
616
+ * failed, when its own process fails to activate (the runner exits without
617
+ * cleaning up), when the process has already exited, and when the shared
618
+ * runner is restarted as a whole (a hang, a fault it cannot attribute, an
619
+ * unreadable line of its own, a member's full input queue, the runner
620
+ * exiting): every member is killed with it. Keep state that must survive
621
+ * such an end in storage as it changes, not only in a cleanup.
622
+ *
623
+ * Planned for player 0.6.4: the player sends the reason with
624
+ * `runtime.stop` and its runner reports `host-lost`; the exact list may
625
+ * still change with that release. Players up to 0.6.3 give no reason for
626
+ * any stop, a lost player included (`undefined`); so does a stop that
627
+ * does not come from the player (a `TestHost` `stop()` without a reason).
628
+ * A value this SDK does not know reads as `undefined`.
629
+ *
630
+ * @player 0.6.4 (planned); older players give no reason (`undefined`).
631
+ * @since 1.5.54
632
+ * @category Plugin
633
+ */
634
+ export type StopReason = "quit" | "disable" | "uninstall" | "reload" | "upgrade" | "idle" | "failed" | "host-lost";
635
+ /**
636
+ * What a {@link Scope.onDispose} cleanup is told (SDK 1.5.54). A player
637
+ * whose own SDK is older passes nothing, so the cleanup's parameter is
638
+ * optional.
639
+ *
640
+ * @since 1.5.54
641
+ * @category Plugin
642
+ */
643
+ export interface DisposeInfo {
644
+ /**
645
+ * `true` when the plugin itself stops (the player stops it), `false`
646
+ * when the plugin's own code disposed this scope or one of its parents.
647
+ */
648
+ readonly stopping: boolean;
649
+ /**
650
+ * Why the player stops the plugin (planned for player 0.6.4); `undefined` while
651
+ * `stopping` is false, and for every stop by an older player.
652
+ */
653
+ readonly reason?: StopReason;
550
654
  }
551
655
  /**
552
656
  * The state of a command's menu item, as the player shows it.
@@ -6716,7 +6820,13 @@ export interface RenderSettings {
6716
6820
  * the player's first answer to `render()` (reported to `onError` too), and so is
6717
6821
  * every `set`. Single owner, like {@link VideoView}. `set` never throws; the
6718
6822
  * result shows in `status`. For shaders that several plugins add at the
6719
- * same time, use {@link VideoShaders} (SDK 1.5.39).
6823
+ * same time, use {@link VideoShaders} (SDK 1.5.39). After a run of the
6824
+ * player that ended abnormally while plugin shaders were drawing, a `set`
6825
+ * whose merged values hold a non-empty `shaders` list is refused with
6826
+ * `previous-crash` (player 0.6.3; the rules are on {@link VideoShaders}).
6827
+ * Every `set` sends all the values set so far, the refused `shaders`
6828
+ * included, so include `shaders: []` in the next `set` to go on without
6829
+ * them.
6720
6830
  *
6721
6831
  * @capability processing.video, playback.read and events (like
6722
6832
  * {@link VideoFilter})
@@ -6745,7 +6855,15 @@ export interface RenderOptions extends Disposable {
6745
6855
  * `failed` with `unsupported` while the player renders in software;
6746
6856
  * `failed` with `failed` when mpv cannot compile a shader (pushed later;
6747
6857
  * while other plugins' {@link VideoShaders} are on too, the player
6748
- * cannot tell whose shader failed and says so in the message).
6858
+ * cannot tell whose shader failed and says so in the message);
6859
+ * `failed` with `previous-crash` (player 0.6.3) when a `set` with a
6860
+ * non-empty `shaders` list comes after a run of the player that ended
6861
+ * abnormally while plugin shaders were drawing (the rules are the same
6862
+ * as for {@link VideoShaders}). The handle keeps the refused list among
6863
+ * its values and every `set` sends them all, so a later `set` without
6864
+ * `shaders` is refused again: include `shaders: []` in it
6865
+ * (`set({ shaders: [], toneMapping: "bt.2390" })`); `dispose` always
6866
+ * works.
6749
6867
  */
6750
6868
  readonly status: FilterStatus;
6751
6869
  /**
@@ -6805,6 +6923,20 @@ export interface RenderOptions extends Disposable {
6805
6923
  * Players before 0.5.20 refuse the request (the handle is `failed`): declare
6806
6924
  * `"minPlayerVersion": "0.5.20"`.
6807
6925
  *
6926
+ * **After an abnormal end** (player 0.6.3): when the previous run of the
6927
+ * player ended while plugin shaders were drawing (a crash, the process
6928
+ * being ended, a lost GPU device), the player refuses every non-empty
6929
+ * list of every plugin with `previous-crash`, its message naming the
6930
+ * plugins whose shaders were on: one such end keeps them off for the next
6931
+ * start only; two within a day keep them off until the GPU, its driver or
6932
+ * the player changes, or for 7 days. Clearing (`[]`, `clear`,
6933
+ * `dispose`) still works. The player shows one OSD line about it itself;
6934
+ * the handle is `failed` with `previous-crash` and the SDK does not
6935
+ * retry it. Do not set the list again in a loop while it is refused, and
6936
+ * do not show an OSD line of your own for it; a later start of the player
6937
+ * applies lists again once the guard lets it. Players before 0.6.3 never
6938
+ * refuse with it.
6939
+ *
6808
6940
  * @example Add a sharpening shader the plugin ships
6809
6941
  * The video looks sharper on the GPU renderer; under software rendering
6810
6942
  * the OSD says why it is not. The shader file lives in the plugin's
@@ -6835,7 +6967,8 @@ export interface VideoShaders extends Disposable {
6835
6967
  *
6836
6968
  * @remarks
6837
6969
  * Never throws: a refused list shows in `status` (`invalid`, `missing`,
6838
- * `limit`, `unsupported`) and goes to {@link Scope.onError}, and what the
6970
+ * `limit`, `unsupported`, and `previous-crash` from player 0.6.3: see
6971
+ * the interface's remarks) and goes to {@link Scope.onError}, and what the
6839
6972
  * player held stays.
6840
6973
  *
6841
6974
  * @capability processing.video
@@ -8401,6 +8534,27 @@ export interface InstallOptions {
8401
8534
  * reconnects to it.
8402
8535
  */
8403
8536
  replace?: boolean;
8537
+ /**
8538
+ * Whether the plugin runs once installed (SDK 1.5.55). Left out, a first
8539
+ * install runs and an update or reinstall (from any source, `replace`
8540
+ * included) keeps the installed plugin's state: one the user or a store
8541
+ * disabled stays disabled, and only its files are installed (nothing
8542
+ * starts it, so the change commits without an `activating` step). `true`
8543
+ * enables and starts it, for example a version the store disabled that
8544
+ * an update fixes; if it fails to start, the old version is restored,
8545
+ * still disabled. `false` installs it disabled: a running version is
8546
+ * stopped and its session values go, as with {@link Packages.setEnabled}.
8547
+ * Built-in plugins always run (and cannot be installed over). A value
8548
+ * that is not a boolean is `invalid`.
8549
+ *
8550
+ * Planned for player 0.6.4: older players ignore it and an install there
8551
+ * always runs the plugin, so after a committed install call
8552
+ * {@link Packages.setEnabled} when the state matters.
8553
+ *
8554
+ * @player 0.6.4 (planned); older players ignore it (an install always
8555
+ * enables the plugin).
8556
+ */
8557
+ enabled?: boolean;
8404
8558
  }
8405
8559
  /**
8406
8560
  * A validated package directory, from {@link Packages.validate}.
@@ -8445,9 +8599,14 @@ export interface PackageCandidate {
8445
8599
  * a built-in id fails with `unsupported` (built-ins update with the
8446
8600
  * player). A reserved id (`vestra` or `vestra.*`, which every built-in
8447
8601
  * uses) fails with `reserved` (player 0.4.1; already when validated), as
8448
- * only the player installs built-in plugins.
8602
+ * only the player installs built-in plugins. An update keeps a disabled
8603
+ * plugin disabled unless `enabled` says otherwise (planned for player
8604
+ * 0.6.4; see {@link InstallOptions.enabled}).
8449
8605
  *
8450
8606
  * @capability plugins.manage
8607
+ * @throws `invalid` (rejects) An option is malformed: a `source` the
8608
+ * player does not accept, or a `replace` or `enabled` that is not a
8609
+ * boolean.
8451
8610
  * @throws `conflict` (rejects) The plugin is installed from another source
8452
8611
  * and `replace` is not set.
8453
8612
  * @throws `unsupported` (rejects) A built-in plugin's id.
@@ -8470,7 +8629,19 @@ export interface PackageCandidate {
8470
8629
  * declare const candidate: PackageCandidate;
8471
8630
  * // ---cut---
8472
8631
  * const change = await candidate.install({ replace: true });
8473
- * await change.done; // "committed" once the new plugin runs
8632
+ * await change.done; // "committed" once installed (running unless it stays disabled)
8633
+ * ```
8634
+ *
8635
+ * @example Enable a fixed version of a plugin the store disabled
8636
+ * A store disabled a version it revoked; the update that fixes it
8637
+ * should run again. Without `enabled` the update keeps the plugin
8638
+ * disabled (player 0.6.4); `true` enables it once installed. Older
8639
+ * players ignore the option and enable every install.
8640
+ * ```ts
8641
+ * declare const candidate: PackageCandidate;
8642
+ * // ---cut---
8643
+ * const change = await candidate.install({ enabled: true });
8644
+ * await change.done; // "committed" once the fixed version runs
8474
8645
  * ```
8475
8646
  *
8476
8647
  * @since 1.0.0
package/dist/wire.d.ts CHANGED
@@ -731,10 +731,15 @@ export declare function createWire(transport: Transport): {
731
731
  /** Player 0.2.0: a `.vxp` file. */
732
732
  validateArchive: (file: string) => Promise<PackageValidation>;
733
733
  installed: (id: string) => Promise<InstalledPackage>;
734
- /** `source` and `replace` are sent only when given (player 0.2.0). */
734
+ /**
735
+ * `source` and `replace` are sent only when given (player 0.2.0);
736
+ * `enabled` only when given (planned for player 0.6.4, older players
737
+ * ignore it).
738
+ */
735
739
  install: (token: string, expectedRevision: number, requestId: string, options?: {
736
740
  source?: WireSource;
737
741
  replace?: boolean;
742
+ enabled?: boolean;
738
743
  }) => Promise<PackageOperation>;
739
744
  operation: (operationId: string) => Promise<PackageOperation>;
740
745
  cancel: (operationId: string) => Promise<PackageOperation>;
package/dist/wire.js CHANGED
@@ -341,13 +341,20 @@ export function createWire(transport) {
341
341
  file,
342
342
  }),
343
343
  installed: (id) => transport.request("packages.installed", { id }),
344
- /** `source` and `replace` are sent only when given (player 0.2.0). */
344
+ /**
345
+ * `source` and `replace` are sent only when given (player 0.2.0);
346
+ * `enabled` only when given (planned for player 0.6.4, older players
347
+ * ignore it).
348
+ */
345
349
  install: (token, expectedRevision, requestId, options = {}) => transport.request("packages.install", {
346
350
  token,
347
351
  expectedRevision,
348
352
  requestId,
349
353
  ...(options.source === undefined ? {} : { source: options.source }),
350
354
  ...(options.replace ? { replace: true } : {}),
355
+ ...(options.enabled === undefined
356
+ ? {}
357
+ : { enabled: options.enabled }),
351
358
  }),
352
359
  operation: (operationId) => transport.request("packages.operation", {
353
360
  operationId,
package/llms.txt CHANGED
@@ -1,6 +1,6 @@
1
1
  # Vestra Player SDK
2
2
 
3
- > @vestra-player/sdk 1.5.53: 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. The documentation is a tree of short pages read by path, like `go doc`: the same pages and API as https://vestrasys.github.io/vestra-player-sdk/.
3
+ > @vestra-player/sdk 1.5.55: 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. The documentation is a tree of short pages read by path, like `go doc`: the same pages and API as https://vestrasys.github.io/vestra-player-sdk/.
4
4
 
5
5
  ## Read the documentation
6
6
 
@@ -32,7 +32,7 @@
32
32
  - `/API/Media`: 6 entries: CaptureOptions, Snapshot, ExportOptions, JobState, ExportJob, MediaCapture
33
33
  - `/API/Packages`: 10 entries: PackageManifest, PackageSource, InstallOptions, PackageCandidate, PackageChangeStatus, PackageChange, PluginInfo, PluginDisplay, PluginWarning, Packages
34
34
  - `/API/Playback`: 18 entries: MarkKind, TimelineMark, TimelineLayer, Timeline, AudioTrack, PlayerState, AudioDevice, Chapter, Conditional, Media, Scrub, StreamFrame, StreamOptions, StreamStartError, Stream,…
35
- - `/API/Plugin`: 11 entries: Disposable, PluginError, Scope, Plugin, WindowState, MainWindow, AppInfo, App, SystemInfo, DisplayInfo, Json
35
+ - `/API/Plugin`: 13 entries: Disposable, PluginError, Scope, Plugin, StopReason, DisposeInfo, WindowState, MainWindow, AppInfo, App, SystemInfo, DisplayInfo, Json
36
36
  - `/API/Processing`: 18 entries: FilterStatus, VideoFilter, AudioFilter, ViewSettings, VideoView, RenderSettings, RenderOptions, VideoShaders, UpscalingReasonCode, UpscalingReason, UpscalingDecodingCause,…
37
37
  - `/API/Services`: 15 entries: Clipboard, Power, ToolResult, Tools, JsonStore, Storage, SessionStorage, System, Secrets, RenderingMode, RenderingPreference, RenderingReasonCode, RenderingReason, RenderingState,…
38
38
  - `/API/Sources`: 7 entries: ResolverOptions, WebResolverOptions, FileResolverOptions, ResolvedStream, Resolver, Resolvers, ResolvedMedia
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vestra-player/sdk",
3
- "version": "1.5.53",
3
+ "version": "1.5.55",
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": {