@vestra-player/sdk 1.5.29 → 1.5.30

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.
@@ -17,12 +17,36 @@ import type { Json } from "../wire.ts";
17
17
  * bound (a playlist, a history) should send the rows near the view and say
18
18
  * how many there are, not the whole list.
19
19
  *
20
- * @example
20
+ * @example Send a long playlist as a window of rows
21
+ * A list view shows any number of entries while each write stays small:
22
+ * the plugin sends only the rows around the scroll position plus the total,
23
+ * and the view reports where it scrolled to. After the `scrolled` handler
24
+ * returns, the SDK evaluates the `bind` function again and sends the new
25
+ * window. Each row object and each of its fields count as one value toward
26
+ * `values`, and so does the array holding them.
21
27
  * ```ts
28
+ * declare const entries: { title: string; duration: number }[];
29
+ * import type { View } from "@vestra-player/sdk";
30
+ * declare const list: View;
31
+ * // ---cut---
22
32
  * import { UI_VALUE_LIMITS } from "@vestra-player/sdk/testing";
23
33
  *
24
- * // 8 fields per row: 9 values (the row and its fields).
25
- * const window = Math.floor(UI_VALUE_LIMITS.values / 9) - 1;
34
+ * // 200 rows, or fewer if the limits were smaller: 2 fields per row make
35
+ * // 3 values (the row and its fields), plus 1 for the array.
36
+ * const size = Math.min(
37
+ * 200,
38
+ * UI_VALUE_LIMITS.arrayRows,
39
+ * Math.floor((UI_VALUE_LIMITS.values - 1) / 3),
40
+ * );
41
+ * let first = 0;
42
+ * list.bind(() => ({
43
+ * total: entries.length,
44
+ * rows: entries.slice(first, first + size),
45
+ * }));
46
+ * // The Slint view reports its first visible row; the window follows it.
47
+ * list.on("scrolled", (row: number) => {
48
+ * first = Math.max(0, row - Math.floor(size / 2));
49
+ * });
26
50
  * ```
27
51
  *
28
52
  * @since 1.5.10
@@ -15,12 +15,36 @@
15
15
  * bound (a playlist, a history) should send the rows near the view and say
16
16
  * how many there are, not the whole list.
17
17
  *
18
- * @example
18
+ * @example Send a long playlist as a window of rows
19
+ * A list view shows any number of entries while each write stays small:
20
+ * the plugin sends only the rows around the scroll position plus the total,
21
+ * and the view reports where it scrolled to. After the `scrolled` handler
22
+ * returns, the SDK evaluates the `bind` function again and sends the new
23
+ * window. Each row object and each of its fields count as one value toward
24
+ * `values`, and so does the array holding them.
19
25
  * ```ts
26
+ * declare const entries: { title: string; duration: number }[];
27
+ * import type { View } from "@vestra-player/sdk";
28
+ * declare const list: View;
29
+ * // ---cut---
20
30
  * import { UI_VALUE_LIMITS } from "@vestra-player/sdk/testing";
21
31
  *
22
- * // 8 fields per row: 9 values (the row and its fields).
23
- * const window = Math.floor(UI_VALUE_LIMITS.values / 9) - 1;
32
+ * // 200 rows, or fewer if the limits were smaller: 2 fields per row make
33
+ * // 3 values (the row and its fields), plus 1 for the array.
34
+ * const size = Math.min(
35
+ * 200,
36
+ * UI_VALUE_LIMITS.arrayRows,
37
+ * Math.floor((UI_VALUE_LIMITS.values - 1) / 3),
38
+ * );
39
+ * let first = 0;
40
+ * list.bind(() => ({
41
+ * total: entries.length,
42
+ * rows: entries.slice(first, first + size),
43
+ * }));
44
+ * // The Slint view reports its first visible row; the window follows it.
45
+ * list.on("scrolled", (row: number) => {
46
+ * first = Math.max(0, row - Math.floor(size / 2));
47
+ * });
24
48
  * ```
25
49
  *
26
50
  * @since 1.5.10
@@ -13,10 +13,17 @@ import type { Transport } from "./wire.ts";
13
13
  * rotation): a portrait video is upright before `transform` runs. At most
14
14
  * 64 nodes across all plugins.
15
15
  *
16
- * @example
16
+ * @example Brighten, sharpen and rotate the picture
17
+ * The video plays a little brighter and more saturated, slightly sharper
18
+ * and turned a quarter clockwise. The nodes run in array order; `set` sends
19
+ * the whole chain as this plugin's desired value (last write wins), and the
20
+ * player applies it to every source until it changes, needing
21
+ * `processing.video`, `playback.read` and `events`.
17
22
  * ```ts
18
23
  * const chain: VideoNode[] = [
24
+ * // Live fields: moving them later changes the picture in place.
19
25
  * { kind: "eq", brightness: 0.05, contrast: 1.1, saturation: 1.2, gamma: 1 },
26
+ * // Not live: changing `amount` later rebuilds the stage (a one-frame stall).
20
27
  * { kind: "sharpen", amount: 0.5, size: "small" },
21
28
  * { kind: "transform", rotate: 90, flipH: false, flipV: false },
22
29
  * ];
@@ -160,12 +167,17 @@ export type VideoNode =
160
167
  * One node of an {@link AudioFilter} chain, by `kind`. Values outside the
161
168
  * ranges fail with `invalid`; "live" fields change in place while playing.
162
169
  *
163
- * @example
170
+ * @example Lift the voices and play in mono
171
+ * Speech around 1 kHz is 3 dB louder and both speakers play the same mixed
172
+ * channel. `set` sends this plugin's whole audio chain (the nodes in order,
173
+ * plus an optional `delayMs`); the player joins it with other plugins'
174
+ * chains and applies it to every source (`processing.audio`,
175
+ * `playback.read`, `events`).
164
176
  * ```ts
165
177
  * plugin.audio.filter("sound").set({
166
178
  * nodes: [
167
- * { kind: "equalizer", frequency: 1000, q: 1, gainDb: 3 },
168
- * { kind: "channels", mode: "mono" },
179
+ * { kind: "equalizer", frequency: 1000, q: 1, gainDb: 3 }, // +3 dB at 1 kHz
180
+ * { kind: "channels", mode: "mono" }, // both channels get (L + R) / 2
169
181
  * ],
170
182
  * });
171
183
  * ```
@@ -11,15 +11,40 @@ export type ResolvedResource = {
11
11
  * stream(s) behind a page URL. Both streams must use identical `headers`;
12
12
  * different maps fail explicitly (nothing is dropped silently).
13
13
  *
14
- * @example
14
+ * @example Play a page whose video and audio are separate streams
15
+ * Opening a `https://video.example/…` page plays it with its title. When
16
+ * the user opens a URL whose scheme and host a web resolver declares, the
17
+ * player calls its `resolve(url, signal)`; the plugin looks up the streams
18
+ * and returns them, and the player plays `primary` with the separate
19
+ * `audio` track, sending the `headers` with every request for both.
15
20
  * ```ts
16
- * const media: ResolvedMedia = {
17
- * kind: "media",
18
- * originalUrl: url,
19
- * title: "Talk",
20
- * primary: { url: videoUrl, headers: { Referer: url } },
21
- * audio: { url: audioUrl, headers: { Referer: url } },
22
- * };
21
+ * plugin.resolvers.add({
22
+ * id: "video-example",
23
+ * schemes: ["https"],
24
+ * hosts: ["video.example"],
25
+ * async resolve(url, signal) {
26
+ * // The site's own API (made up here) names the streams of a page.
27
+ * const api = `https://video.example/api/streams?page=${encodeURIComponent(url)}`;
28
+ * const response = await fetch(api, { signal });
29
+ * // The player shows a failed resolve's message to the user.
30
+ * if (!response.ok)
31
+ * throw new Error(`video.example answered ${response.status} for this page`);
32
+ * const info = (await response.json()) as {
33
+ * title: string;
34
+ * video: string;
35
+ * audio: string;
36
+ * };
37
+ * const headers = { Referer: url }; // identical for both streams
38
+ * const media: ResolvedMedia = {
39
+ * kind: "media",
40
+ * originalUrl: url,
41
+ * title: info.title,
42
+ * primary: { url: info.video, headers },
43
+ * audio: { url: info.audio, headers },
44
+ * };
45
+ * return media;
46
+ * },
47
+ * });
23
48
  * ```
24
49
  */
25
50
  export type ResolvedMedia = {
@@ -16,20 +16,30 @@ export type SubtitleStamp = {
16
16
  };
17
17
  /**
18
18
  * A subtitle style layer ({@link Subtitles.setStyle}). Sizes are at the
19
- * 1080p reference height and scale with the video. The effective style is
20
- * the layer of the plugin that set one last; it also applies to the
21
- * secondary subtitle.
19
+ * 1080p reference height and scale with the video. Each plugin has one
20
+ * layer; when several plugins set one, the effective style is the layer of
21
+ * the plugin whose manifest id sorts last (the order of the calls does not
22
+ * matter). It also applies to the secondary subtitle.
22
23
  *
23
- * @example
24
+ * @example Show large yellow subtitles a quarter second earlier
25
+ * Subtitles turn yellow at size 44 on a 60 % opaque box, 6 % above the
26
+ * bottom of the video, and appear 250 ms earlier. `refresh` reads the
27
+ * subtitle state from the player, whose `fonts` lists the font ids a style
28
+ * may name; `setStyle` then sends this plugin's style layer
29
+ * (`subtitles.control`, `subtitles.read`). It shows unless another plugin
30
+ * with a manifest id that sorts later has a layer too: the player picks the
31
+ * effective layer by manifest id, not by which call came last.
24
32
  * ```ts
33
+ * const { fonts } = await plugin.subtitles.refresh();
34
+ * if (!fonts.length) return; // no font to name: leave the style alone
25
35
  * const style: SubtitleStyle = {
26
- * fontId: plugin.subtitles.state!.fonts[0]!.id,
27
- * fontSize: 44,
36
+ * fontId: fonts[0]!.id, // a font the player offers
37
+ * fontSize: 44, // at the 1080p reference height; scales with the video
28
38
  * color: "yellow",
29
39
  * outline: 2,
30
- * marginPercent: 6,
31
- * offsetMs: -250,
32
- * box: 60,
40
+ * marginPercent: 6, // bottom margin, % of the video height
41
+ * offsetMs: -250, // negative: shown earlier
42
+ * box: 60, // background box opacity
33
43
  * };
34
44
  * await plugin.subtitles.setStyle(style);
35
45
  * ```
package/dist/wire.d.ts CHANGED
@@ -71,16 +71,25 @@ export interface UiCallback {
71
71
  * `timeout`, `aborted`, `limit` (a player limit such as the command count),
72
72
  * `disabled` (the plugin is stopping).
73
73
  *
74
- * @example
74
+ * @example Simulate the player refusing a clipboard write in a test
75
+ * The test proves the plugin sees a refused write as an error with code
76
+ * `failed`. `host.handle` replaces the test host's handler for one request
77
+ * method; the `HostError` it throws travels back like the player's own
78
+ * refusal, so the call rejects with that `code` and is not retried.
79
+ * `host.reset` puts the default handler back for later steps.
75
80
  * ```ts
81
+ * import assert from "node:assert/strict";
76
82
  * import { HostError, startPlugin } from "@vestra-player/sdk/testing";
83
+ * import activate from "../main.ts";
77
84
  *
78
- * const { host, plugin, stop } = await startPlugin(activate, { directory });
85
+ * const { host, plugin, stop } = await startPlugin(activate, {
86
+ * directory: new URL("..", import.meta.url),
87
+ * });
79
88
  * host.handle("clipboard.writeText", () => {
80
89
  * throw new HostError("failed", "the system refused the write");
81
90
  * });
82
91
  * await assert.rejects(plugin.clipboard.writeText("x"), { code: "failed" });
83
- * host.reset("clipboard.writeText");
92
+ * host.reset("clipboard.writeText"); // writes succeed again
84
93
  * await stop();
85
94
  * ```
86
95
  */
package/dist/wire.js CHANGED
@@ -31,16 +31,25 @@ export const API_VERSION = "vestra/1";
31
31
  * `timeout`, `aborted`, `limit` (a player limit such as the command count),
32
32
  * `disabled` (the plugin is stopping).
33
33
  *
34
- * @example
34
+ * @example Simulate the player refusing a clipboard write in a test
35
+ * The test proves the plugin sees a refused write as an error with code
36
+ * `failed`. `host.handle` replaces the test host's handler for one request
37
+ * method; the `HostError` it throws travels back like the player's own
38
+ * refusal, so the call rejects with that `code` and is not retried.
39
+ * `host.reset` puts the default handler back for later steps.
35
40
  * ```ts
41
+ * import assert from "node:assert/strict";
36
42
  * import { HostError, startPlugin } from "@vestra-player/sdk/testing";
43
+ * import activate from "../main.ts";
37
44
  *
38
- * const { host, plugin, stop } = await startPlugin(activate, { directory });
45
+ * const { host, plugin, stop } = await startPlugin(activate, {
46
+ * directory: new URL("..", import.meta.url),
47
+ * });
39
48
  * host.handle("clipboard.writeText", () => {
40
49
  * throw new HostError("failed", "the system refused the write");
41
50
  * });
42
51
  * await assert.rejects(plugin.clipboard.writeText("x"), { code: "failed" });
43
- * host.reset("clipboard.writeText");
52
+ * host.reset("clipboard.writeText"); // writes succeed again
44
53
  * await stop();
45
54
  * ```
46
55
  */