@vestra-player/sdk 1.5.21 → 1.5.24

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.24
4
+
5
+ **Who must change: nobody.** Plugins built on earlier SDKs keep working unchanged: `when: "interlaced"` and `when: "all"` do what they did. Additive; `apiVersion` stays `vestra/1`. The addition needs player 0.5.4.
6
+
7
+ - **`VideoNode` `{ kind: "deinterlace", when: "auto", output: "field" }`** (player 0.5.4; requested by the video plugin, DEINT-AUTO-1: user ruling of 2026-10-27, Auto deinterlacing must cost nothing on progressive files). The player's own deinterlacer instead of BWDIF in the chain: decided per frame from the decoder's interlaced flag, with the decoder's hardware deinterlacer where frames stay on the GPU, one output frame per field; progressive frames pass untouched. It needs no frames in memory, so a chain with only this node keeps zero-copy hardware decoding, where any other node switches to a copy back to memory. It runs before every plugin's chain wherever it is written, and it is shared: on while any plugin's chain has one, off when the last one is removed or its plugin stops. `output: "frame"` with it, or another `deinterlace` node in the same chain, fails with `invalid`. Processed snapshots and exports deinterlace the same frames with BWDIF at field rate. Players before 0.5.4 refuse `auto` with `invalid`, so set `"minPlayerVersion": "0.5.4"`.
8
+ - **`TestHost`** validates the new value as the player does (`nodeProblem`).
9
+ - Docs: *Processing* (ordering and side effects). `llms.txt` and `llms-full.txt` regenerated.
10
+
11
+ ## 1.5.23
12
+
13
+ **Who must change: nobody.** Republishes 1.5.22 with a clean API documentation build; 1.5.22 was tagged but never published to npm: TypeDoc treats warnings as errors and failed on two `{@link}` labels in *Playback* that contained a brace (`Scrub.seek(seconds, { keyframes })`). Documentation only; the code is the same as 1.5.22 below.
14
+
15
+ ## 1.5.22
16
+
17
+ **Who must change: nobody.** Plugins built on earlier SDKs keep working unchanged: a scrub without the new option seeks exactly, as before. Additive; `apiVersion` stays `vestra/1`. The addition needs player 0.5.3.
18
+
19
+ - **`Scrub.seek(seconds, { keyframes?: boolean })`** (player 0.5.3; requested by the playback plugin for the seek bar, user ruling of 2026-10-27: the bar follows the same "Jump to keyframes only" switch as the arrow keys, PotPlayer style, no second exact seek). With `keyframes: true` the seek lands on the last keyframe at or before `seconds` (mpv `absolute+keyframes`) instead of decoding up to the exact frame: the picture appears in tens of milliseconds on any file, up to one keyframe interval before the target, and the reported position is the keyframe's. Omitted or `false`: exact, as before. The option goes on the wire only when true (`control: { kind: "seek", seconds, keyframes: true }`); players before 0.5.3 ignore the field and seek exactly, so set `"minPlayerVersion": "0.5.3"` when you rely on it. The player sends a repeated target of the same gesture only once while that seek is still running (a click is a press and a release), keyed on the target and the mode: an exact seek to the target of a keyframe seek is always sent.
20
+ - **`TestHost.seeks`**: every seek a plugin submitted (`player.seek`, `Scrub.seek`), newest last, as `{ seconds, keyframes }`. The test host lands on the exact target in both modes; a non-boolean `keyframes` is `invalid`.
21
+ - Docs: *Playback* (scrubs and the keyframe switch); *Windows, panels, settings pages* gains a pitfall: an `if` that flips under the pointer (a hover highlight created and destroyed on every hover change) rebuilds the window's accessibility tree and drops the video to 10–15 frames a second while the pointer moves; keep the element and change `visible` or `opacity`. `llms.txt` and `llms-full.txt` regenerated.
22
+
3
23
  ## 1.5.21
4
24
 
5
25
  **Who must change: nobody.** Plugins built on earlier SDKs keep working unchanged; the additions need player 0.5.2. Additive and bug fixes; `apiVersion` stays `vestra/1`.
@@ -316,7 +316,7 @@ class ScrubHandle {
316
316
  !(this.#pending && sameStamp(raw.stamp, this.#pending)))
317
317
  this.end();
318
318
  });
319
- this.#queue = new Latest(async (seconds) => {
319
+ this.#queue = new Latest(async ({ seconds, keyframes }) => {
320
320
  if (!this.#active)
321
321
  throw superseded;
322
322
  const started = Date.now();
@@ -326,9 +326,15 @@ class ScrubHandle {
326
326
  intentVersion: expected.intentVersion + 1,
327
327
  };
328
328
  try {
329
+ // `keyframes` goes on the wire only when set, so players before
330
+ // 0.5.3 (which ignore the field) see the same intent as before.
329
331
  const ticket = await service.submit({
330
332
  kind: "control",
331
- control: { kind: "seek", seconds },
333
+ control: {
334
+ kind: "seek",
335
+ seconds,
336
+ ...(keyframes ? { keyframes } : {}),
337
+ },
332
338
  expected,
333
339
  });
334
340
  if (!this.#active)
@@ -354,11 +360,11 @@ class ScrubHandle {
354
360
  get active() {
355
361
  return this.#active;
356
362
  }
357
- seek(seconds) {
363
+ seek(seconds, options = {}) {
358
364
  if (!this.#active)
359
365
  return Promise.resolve(false);
360
366
  return this.#queue
361
- .push(seconds)
367
+ .push({ seconds, keyframes: options.keyframes === true })
362
368
  .catch((error) => (error === superseded ? false : Promise.reject(error)));
363
369
  }
364
370
  end() {
@@ -1147,6 +1147,18 @@ export declare class TestHost implements Transport {
1147
1147
  /** The new position. */
1148
1148
  landed: number;
1149
1149
  }[];
1150
+ /**
1151
+ * Every seek a plugin submitted (`player.seek`, `Scrub.seek`), newest
1152
+ * last, with the mode it asked for. The test host lands on the exact
1153
+ * target either way (the player lands a `keyframes` seek on the last
1154
+ * keyframe at or before it); the core's step seeks are in `coreSeeks`.
1155
+ */
1156
+ readonly seeks: {
1157
+ /** The target. */
1158
+ seconds: number;
1159
+ /** `Scrub.seek(seconds, { keyframes: true })` (SDK 1.5.22). */
1160
+ keyframes: boolean;
1161
+ }[];
1150
1162
  /** Every `player.step`, newest last. */
1151
1163
  readonly steps: {
1152
1164
  /** `"frame"` or `"keyframe"`. */
@@ -1277,6 +1277,13 @@ export class TestHost {
1277
1277
  * a keyframe, and where it landed.
1278
1278
  */
1279
1279
  coreSeeks = [];
1280
+ /**
1281
+ * Every seek a plugin submitted (`player.seek`, `Scrub.seek`), newest
1282
+ * last, with the mode it asked for. The test host lands on the exact
1283
+ * target either way (the player lands a `keyframes` seek on the last
1284
+ * keyframe at or before it); the core's step seeks are in `coreSeeks`.
1285
+ */
1286
+ seeks = [];
1280
1287
  /** Every `player.step`, newest last. */
1281
1288
  steps = [];
1282
1289
  /** Every core action run (`host.run`, a core command, a pressed core binding), newest last. */
@@ -3067,6 +3074,16 @@ export class TestHost {
3067
3074
  "source.state": () => this.source,
3068
3075
  "source.submit": (p) => {
3069
3076
  const intent = p.intent;
3077
+ const control = intent.control;
3078
+ if (intent.kind === "control" && control?.kind === "seek") {
3079
+ if (control.keyframes !== undefined &&
3080
+ typeof control.keyframes !== "boolean")
3081
+ throw new HostError("invalid", "keyframes must be a boolean");
3082
+ this.seeks.push({
3083
+ seconds: Number(control.seconds),
3084
+ keyframes: control.keyframes === true,
3085
+ });
3086
+ }
3070
3087
  const ticket = this.#intent(intent);
3071
3088
  const operation = Number(p.operationId);
3072
3089
  // A file this plugin's file resolver claims: pending until it answers.
@@ -3501,10 +3501,32 @@ export interface Media {
3501
3501
  */
3502
3502
  export interface Scrub {
3503
3503
  /**
3504
- * Resolves `true` when this exact target was applied, `false` when a newer
3505
- * target superseded it or the gesture ended. Rejects on a host failure.
3504
+ * Seek within the gesture. Resolves `true` when this target was applied,
3505
+ * `false` when a newer target superseded it or the gesture ended. Rejects
3506
+ * on a host failure.
3507
+ *
3508
+ * `keyframes: true` (player 0.5.3, SDK 1.5.22): land on the last keyframe
3509
+ * at or before `seconds` instead of decoding up to that exact frame. The
3510
+ * picture appears at once on any file (tens of milliseconds instead of
3511
+ * seconds on 4K video with keyframes far apart), up to one keyframe
3512
+ * interval before the target; the position the player reports is the
3513
+ * keyframe's. Use it when the user chose keyframe seeks (the same switch
3514
+ * as {@link Player.setStepSeek}), and the exact seek otherwise. The player
3515
+ * sends a repeated target of the same gesture only once while the seek is
3516
+ * still running (a click is a press and a release), but an exact seek to
3517
+ * the target of a keyframe seek is always sent. Omitted or `false`: exact,
3518
+ * as before. Older players ignore the option and seek exactly: set
3519
+ * `"minPlayerVersion": "0.5.3"` when you rely on it.
3520
+ *
3521
+ * @example
3522
+ * ```ts
3523
+ * const keyframes = true; // the plugin's own setting
3524
+ * bar.on("dragMove", (seconds: number) => void scrub?.seek(seconds, { keyframes }));
3525
+ * ```
3506
3526
  */
3507
- seek(seconds: number): Promise<boolean>;
3527
+ seek(seconds: number, options?: {
3528
+ keyframes?: boolean;
3529
+ }): Promise<boolean>;
3508
3530
  /** False once anyone else changed the source; later seeks are ignored. */
3509
3531
  readonly active: boolean;
3510
3532
  /** End the gesture; later seeks resolve `false`. */
@@ -3828,10 +3850,11 @@ export interface Player {
3828
3850
  * step, backward on the last at or before position − step, and they
3829
3851
  * always move. Presses during a seek add up and continue from where it
3830
3852
  * landed; the on-screen text shows the landed time. Sources without moving
3831
- * video seek exactly. The seek bar, {@link Player.seek}, scrubs, chapters,
3832
- * loops and {@link Player.step} are unaffected. Omitted or `false`: exact
3833
- * seeks, as before. `seconds` and `keyframes` are written as a pair: the
3834
- * newest call sets both.
3853
+ * video seek exactly. {@link Player.seek}, chapters, loops and
3854
+ * {@link Player.step} are unaffected, and so is a scrub unless it asks for
3855
+ * keyframes itself ({@link Scrub.seek} with `keyframes: true`, SDK 1.5.22).
3856
+ * Omitted or `false`: exact seeks, as before. `seconds` and `keyframes`
3857
+ * are written as a pair: the newest call sets both.
3835
3858
  *
3836
3859
  * Older players ignore `keyframes`: set `"minPlayerVersion": "0.5.1"` in
3837
3860
  * the manifest when you rely on it.
@@ -102,13 +102,35 @@ export type VideoNode =
102
102
  /** Mirror vertically. */
103
103
  flipV: boolean;
104
104
  }
105
- /** BWDIF; `field` doubles the frame rate. */
105
+ /**
106
+ * Deinterlacing. `interlaced` and `all` are FFmpeg BWDIF in the chain;
107
+ * `field` doubles the frame rate.
108
+ *
109
+ * `when: "auto"` (player 0.5.4) is the player's own deinterlacer instead:
110
+ * decided per frame from the decoder's interlaced flag, with the decoder's
111
+ * hardware deinterlacer where frames stay on the GPU; progressive frames
112
+ * pass untouched. It needs no frames in memory, so a chain with only this
113
+ * node keeps zero-copy hardware decoding (any other node still needs a
114
+ * copy back to memory). It always outputs one frame per field, so it
115
+ * takes `output: "field"` only (`"frame"` fails with `invalid`). It runs
116
+ * before every plugin's chain wherever it is written, and is shared: on
117
+ * while any plugin's chain has one. A chain with it has no other
118
+ * `deinterlace` node. Snapshots and exports deinterlace the same frames
119
+ * with BWDIF at field rate. Players before 0.5.4 refuse `auto` with
120
+ * `invalid`.
121
+ */
106
122
  | {
107
123
  /** The node type. */
108
124
  kind: "deinterlace";
109
- /** `interlaced`: only frames flagged interlaced; `all`: every frame. */
110
- when: "interlaced" | "all";
111
- /** One frame per frame, or one per field (double rate). */
125
+ /**
126
+ * `interlaced`: only frames flagged interlaced; `all`: every frame;
127
+ * `auto`: the player's deinterlacer, on frames flagged interlaced.
128
+ */
129
+ when: "interlaced" | "all" | "auto";
130
+ /**
131
+ * One frame per frame, or one per field (double rate); `when: "auto"`
132
+ * takes `field` only.
133
+ */
112
134
  output: "frame" | "field";
113
135
  }
114
136
  /** Crop to / pad to a display aspect; x, y place it (−1 left/top … 1 right/bottom). */
@@ -19,7 +19,10 @@ const RULES = {
19
19
  deblock: { strength: ["weak", "strong"] },
20
20
  deband: { threshold: [0.003, 0.5], range: [1, 64] },
21
21
  transform: { rotate: [0, 90, 180, 270], flipH: BOOL, flipV: BOOL },
22
- deinterlace: { when: ["interlaced", "all"], output: ["frame", "field"] },
22
+ deinterlace: {
23
+ when: ["interlaced", "all", "auto"],
24
+ output: ["frame", "field"],
25
+ },
23
26
  reframe: { mode: ["crop", "pad"], aspect: [0.25, 4], x: [-1, 1], y: [-1, 1] },
24
27
  frameBlend: { fps: [24, 120] },
25
28
  volume: { gainDb: [-60, 0] },
@@ -73,7 +76,15 @@ export function nodeProblem(stage, nodes) {
73
76
  if (node.kind === "frameBlend" &&
74
77
  !Number.isInteger(node.fps))
75
78
  return "frameBlend.fps must be an integer";
79
+ if (node.kind === "deinterlace" &&
80
+ node.when === "auto" &&
81
+ node.output !== "field")
82
+ return "deinterlace when auto outputs one frame per field: output must be field";
76
83
  }
84
+ const deinterlace = nodes.filter((node) => node.kind === "deinterlace");
85
+ if (deinterlace.length > 1 &&
86
+ deinterlace.some((node) => node.when === "auto"))
87
+ return "a chain with a deinterlace node when auto has no other deinterlace node";
77
88
  if (nodes.filter((node) => node.kind === "frameBlend").length > 1)
78
89
  return "only one frameBlend node across all plugins";
79
90
  return null;
package/dist/source.d.ts CHANGED
@@ -30,6 +30,12 @@ export type SourceIntent = {
30
30
  } | {
31
31
  kind: "seek";
32
32
  seconds: number;
33
+ /**
34
+ * Land on the last keyframe at or before `seconds` instead of the
35
+ * exact time (player 0.5.3). Sent only when true: older players
36
+ * ignore the field and seek exactly.
37
+ */
38
+ keyframes?: boolean;
33
39
  } | {
34
40
  kind: "rate";
35
41
  rate: number;
package/llms-full.txt CHANGED
@@ -1,4 +1,4 @@
1
- # @vestra-player/sdk 1.5.21: full reference
1
+ # @vestra-player/sdk 1.5.24: 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
 
@@ -524,7 +524,7 @@ Without this rule, a "jump to bookmark" that arrives a moment late would seek a
524
524
  Two controls follow other rules because they are about the device or a gesture, not the source:
525
525
 
526
526
  - setOutput (volume and mute) is last-write-wins and resolves `false` when a newer value replaced it.
527
- - scrub() starts a drag gesture on a seek bar: each seek in the gesture is conditional on the previous one, only the newest target is kept, and any outside change ends the gesture. Use it for a slider instead of calling `seek` on every move.
527
+ - scrub() starts a drag gesture on a seek bar: each seek in the gesture is conditional on the previous one, only the newest target is kept, and any outside change ends the gesture. Use it for a slider instead of calling `seek` on every move. Scrub.seek with `{ keyframes: true }` (player 0.5.3, SDK 1.5.22) lands on the last keyframe at or before the target instead of the exact frame; see Step seeks: exact or keyframe.
528
528
 
529
529
  #### Sources
530
530
 
@@ -585,10 +585,12 @@ The difference from the `Player` methods: a `Player` control is a precise operat
585
585
 
586
586
  ##### Step seeks: exact or keyframe
587
587
 
588
- ← / → (`seek-backward` / `seek-forward`) seek by the step that player.setStepSeek sets (5 s by default). By default they land on the exact time, which on a file with keyframes far apart means decoding from the previous keyframe first: a visible stall on 4K video. `setStepSeek({ seconds, keyframes: true })` (player 0.5.1, SDK 1.5.19) makes them land on a keyframe instead: forward on the first keyframe at or after position + step, backward on the last at or before position − step, and every press moves. Presses during a seek add up and continue from where it landed, and the on-screen text shows the landed time. Sources without moving video still seek exactly. The seek bar, `player.seek`, scrubs, chapters, loops and `player.step` are never affected.
588
+ ← / → (`seek-backward` / `seek-forward`) seek by the step that player.setStepSeek sets (5 s by default). By default they land on the exact time, which on a file with keyframes far apart means decoding from the previous keyframe first: a visible stall on 4K video. `setStepSeek({ seconds, keyframes: true })` (player 0.5.1, SDK 1.5.19) makes them land on a keyframe instead: forward on the first keyframe at or after position + step, backward on the last at or before position − step, and every press moves. Presses during a seek add up and continue from where it landed, and the on-screen text shows the landed time. Sources without moving video still seek exactly. `player.seek`, chapters, loops and `player.step` are never affected.
589
589
 
590
590
  For more steps of your own (Ctrl+→ and the like), call `plugin.host.run("seek-forward", { seconds: 30 })`: the same checks, mode, accumulation and text as →. In keyframe mode it resolves once the seek has landed, so `player.state.position` is current afterwards; do not show your own text for it. Set `"minPlayerVersion": "0.5.1"` when you rely on either option: older players ignore them.
591
591
 
592
+ A seek bar follows the same switch with Scrub.seek and its `keyframes` option, `scrub.seek(seconds, { keyframes })` (player 0.5.3, SDK 1.5.22): with `keyframes: true` every click and drag step lands on the last keyframe at or before the pointer, so the picture follows the pointer on any file (an exact seek decodes from the keyframe to the target first: seconds on 4K video with keyframes far apart, and a drag freezes); the position is up to one keyframe interval early, and that is what the player shows, as PotPlayer does. Do not seek twice (keyframes, then exactly): one mode per gesture, the user's choice. Older players ignore the option and seek exactly (`"minPlayerVersion": "0.5.3"` when you rely on it).
593
+
592
594
  ```ts
593
595
  const keyframes = true; // the plugin's own setting
594
596
  await plugin.player.setStepSeek({ seconds: 5, keyframes });
@@ -596,9 +598,13 @@ const seek = (delta: number) =>
596
598
  plugin.host.run(delta < 0 ? "seek-backward" : "seek-forward", {
597
599
  seconds: Math.abs(delta),
598
600
  });
601
+ // The seek bar: one gesture per press, every click and drag step in the same mode.
602
+ const scrub = plugin.player.scrub();
603
+ await scrub.seek(27.6, { keyframes });
604
+ scrub.end();
599
605
  ```
600
606
 
601
- In tests, `TestHost.keyframes` models the source's keyframes, and `host.coreSeeks` records each step seek with where it landed.
607
+ In tests, `TestHost.keyframes` models the source's keyframes, `host.coreSeeks` records each step seek with where it landed, and `host.seeks` records each `player.seek` and `Scrub.seek` with the mode it asked for (the test host lands on the exact target in both modes).
602
608
 
603
609
  ## Concept: Video and audio processing
604
610
 
@@ -661,7 +667,8 @@ How smoothly the change shows depends on the field. Some fields, marked **live**
661
667
 
662
668
  - Video nodes act on the picture after the player has applied the container's rotation, so a portrait video is upright before a plugin's `transform` rotates it.
663
669
  - `frameBlend` (frame-rate conversion by blending) always runs **last**, after every plugin's chain, wherever it is written, so temporal filters such as denoise and deinterlace still see the original frames. It changes the frame rate of exports too, and costs a lot of CPU: decide from `stats()` whether to offer it.
664
- - Filters need frames in memory. While any plugin has a video chain, the player decodes in hardware with a copy back to memory instead of zero-copy decoding; when the last chain is removed it switches back. The decoder restarts at each switch, so the picture may pause briefly. stats().video.hwdec shows the decoder in use.
670
+ - Filters need frames in memory. While any plugin has a video chain, the player decodes in hardware with a copy back to memory instead of zero-copy decoding; when the last chain is removed it switches back. The decoder restarts at each switch, so the picture may pause briefly. stats().video.hwdec shows the decoder in use. The one exception is the next item.
671
+ - `{ kind: "deinterlace", when: "auto", output: "field" }` (player 0.5.4) is not a filter in the chain but the player's own deinterlacer. The player decides per frame: frames flagged interlaced are deinterlaced, at one frame per field, by the decoder's hardware deinterlacer where frames stay on the GPU (BWDIF otherwise); progressive frames pass untouched. It needs no frames in memory, so a chain with only this node keeps zero-copy decoding: this is the node for an "Auto" setting that must cost nothing on progressive files. It runs **first**, before every plugin's chain, wherever it is written, and it is shared: it stays on while any plugin's chain has one. `output: "frame"` fails with `invalid`, and so does another `deinterlace` node in the same chain; use `when: "interlaced"` (BWDIF in the chain, with a copy back to memory) for frame-rate output. Snapshots and exports deinterlace the same frames with BWDIF at field rate.
665
672
 
666
673
  #### The display layer
667
674
 
@@ -960,6 +967,7 @@ The API is the same on macOS and Windows; what differs is the system's own part:
960
967
  - **Hard-coded colours.** They do not follow the palette. Use `Theme.*`, and `Theme.danger` for errors.
961
968
  - **A `Ctrl+W` default binding.** It never fires in tool windows and floating panels. Pick another key.
962
969
  - **Two of a kind.** One panel per plugin, one backdrop per player, one OSD info block per plugin, one open dialog at a time (`busy`).
970
+ - **An `if` that flips under the pointer.** `if touch.has-hover: Rectangle { … }` (a hover highlight, an accent bar, a tooltip-like box that follows the pointer) creates and destroys an element on every hover change. While a video plays, each such change costs the player's UI thread far more than a property change: the player's accessibility tree (screen readers, UI Automation) is rebuilt for the whole window each time, measured at 100–160 ms per hover on Windows, enough to drop the video to 10–15 frames a second while the pointer moves. Keep the element and change properties instead: `Rectangle { visible: touch.has-hover; … }` or `opacity:`. Use `if` for things that change rarely (a mode, a selection made with a click, a list that was loaded), never for something that follows the pointer or a hover.
963
971
  - **Slot state without `events`.** PanelSlot.state, appearance.theme and their `onChange` listeners follow other plugins and the user only with the `events` capability.
964
972
 
965
973
  #### Testing
@@ -2177,6 +2185,7 @@ The table lists the API whose documentation names a player version, newest last:
2177
2185
  | 0.4.3 | CommandOptions.path, PropValue, View.set, Appearance.setLanguage, TemplateArgs, Text, ConfirmOptions.args, OsdPanel.set, OsdPanelOptions.maxColumns, Osd.show |
2178
2186
  | 0.5.1 | ImageRef, ImageOptions.maxSide, PropValue, Ui.image, Ui.remoteImage, SlotState.reason, Panel.setInteraction, PlaybackStats.presentedFps, Player.setStepSeek, Host.run, Packages.reload |
2179
2187
  | 0.5.2 | HostAction |
2188
+ | 0.5.3 | Scrub.seek |
2180
2189
 
2181
2190
  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).
2182
2191
 
@@ -4948,8 +4957,27 @@ the gesture.
4948
4957
 
4949
4958
  ```ts
4950
4959
  interface Scrub {
4951
- /** Resolves `true` when this exact target was applied, `false` when a newer target superseded it or the gesture ended. Rejects on a host failure. */
4952
- seek(seconds: number): Promise<boolean>;
4960
+ /**
4961
+ * Seek within the gesture. Resolves `true` when this target was applied,
4962
+ * `false` when a newer target superseded it or the gesture ended. Rejects
4963
+ * on a host failure.
4964
+ *
4965
+ * `keyframes: true` (player 0.5.3, SDK 1.5.22): land on the last keyframe
4966
+ * at or before `seconds` instead of decoding up to that exact frame. The
4967
+ * picture appears at once on any file (tens of milliseconds instead of
4968
+ * seconds on 4K video with keyframes far apart), up to one keyframe
4969
+ * interval before the target; the position the player reports is the
4970
+ * keyframe's. Use it when the user chose keyframe seeks (the same switch
4971
+ * as Player.setStepSeek), and the exact seek otherwise. The player
4972
+ * sends a repeated target of the same gesture only once while the seek is
4973
+ * still running (a click is a press and a release), but an exact seek to
4974
+ * the target of a keyframe seek is always sent. Omitted or `false`: exact,
4975
+ * as before. Older players ignore the option and seek exactly: set
4976
+ * `"minPlayerVersion": "0.5.3"` when you rely on it.
4977
+ */
4978
+ seek(seconds: number, options?: {
4979
+ keyframes?: boolean;
4980
+ }): Promise<boolean>;
4953
4981
  /** False once anyone else changed the source; later seeks are ignored. */
4954
4982
  readonly active: boolean;
4955
4983
  /** End the gesture; later seeks resolve `false`. */
@@ -5238,10 +5266,11 @@ interface Player {
5238
5266
  * step, backward on the last at or before position − step, and they
5239
5267
  * always move. Presses during a seek add up and continue from where it
5240
5268
  * landed; the on-screen text shows the landed time. Sources without moving
5241
- * video seek exactly. The seek bar, Player.seek, scrubs, chapters,
5242
- * loops and Player.step are unaffected. Omitted or `false`: exact
5243
- * seeks, as before. `seconds` and `keyframes` are written as a pair: the
5244
- * newest call sets both.
5269
+ * video seek exactly. Player.seek, chapters, loops and
5270
+ * Player.step are unaffected, and so is a scrub unless it asks for
5271
+ * keyframes itself (Scrub.seek with `keyframes: true`, SDK 1.5.22).
5272
+ * Omitted or `false`: exact seeks, as before. `seconds` and `keyframes`
5273
+ * are written as a pair: the newest call sets both.
5245
5274
  *
5246
5275
  * Older players ignore `keyframes`: set `"minPlayerVersion": "0.5.1"` in
5247
5276
  * the manifest when you rely on it.
@@ -6533,13 +6562,29 @@ type VideoNode =
6533
6562
  /** Mirror vertically. */
6534
6563
  flipV: boolean;
6535
6564
  }
6536
- /** BWDIF; `field` doubles the frame rate. */
6565
+ /**
6566
+ * Deinterlacing. `interlaced` and `all` are FFmpeg BWDIF in the chain;
6567
+ * `field` doubles the frame rate.
6568
+ *
6569
+ * `when: "auto"` (player 0.5.4) is the player's own deinterlacer instead:
6570
+ * decided per frame from the decoder's interlaced flag, with the decoder's
6571
+ * hardware deinterlacer where frames stay on the GPU; progressive frames
6572
+ * pass untouched. It needs no frames in memory, so a chain with only this
6573
+ * node keeps zero-copy hardware decoding (any other node still needs a
6574
+ * copy back to memory). It always outputs one frame per field, so it
6575
+ * takes `output: "field"` only (`"frame"` fails with `invalid`). It runs
6576
+ * before every plugin's chain wherever it is written, and is shared: on
6577
+ * while any plugin's chain has one. A chain with it has no other
6578
+ * `deinterlace` node. Snapshots and exports deinterlace the same frames
6579
+ * with BWDIF at field rate. Players before 0.5.4 refuse `auto` with
6580
+ * `invalid`.
6581
+ */
6537
6582
  | {
6538
6583
  /** The node type. */
6539
6584
  kind: "deinterlace";
6540
- /** `interlaced`: only frames flagged interlaced; `all`: every frame. */
6541
- when: "interlaced" | "all";
6542
- /** One frame per frame, or one per field (double rate). */
6585
+ /** `interlaced`: only frames flagged interlaced; `all`: every frame; `auto`: the player's deinterlacer, on frames flagged interlaced. */
6586
+ when: "interlaced" | "all" | "auto";
6587
+ /** One frame per frame, or one per field (double rate); `when: "auto"` takes `field` only. */
6543
6588
  output: "frame" | "field";
6544
6589
  }
6545
6590
  /** Crop to / pad to a display aspect; x, y place it (−1 left/top … 1 right/bottom). */
@@ -7444,6 +7489,15 @@ class TestHost implements Transport {
7444
7489
  /** The new position. */
7445
7490
  landed: number;
7446
7491
  }[];
7492
+ /**
7493
+ * Every seek a plugin submitted (`player.seek`, `Scrub.seek`), newest last, with the mode it asked for. The test host lands on the exact target either way (the player lands a `keyframes` seek on the last keyframe at or before it); the core's step seeks are in `coreSeeks`.
7494
+ */
7495
+ readonly seeks: {
7496
+ /** The target. */
7497
+ seconds: number;
7498
+ /** `Scrub.seek(seconds, { keyframes: true })` (SDK 1.5.22). */
7499
+ keyframes: boolean;
7500
+ }[];
7447
7501
  /** Every `player.step`, newest last. */
7448
7502
  readonly steps: {
7449
7503
  /** `"frame"` or `"keyframe"`. */
package/llms.txt CHANGED
@@ -1,6 +1,6 @@
1
1
  # Vestra Player SDK
2
2
 
3
- > @vestra-player/sdk 1.5.21: 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> are line numbers in llms-full.txt, next to this file: every page, declaration and example.
3
+ > @vestra-player/sdk 1.5.24: 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> are line numbers in llms-full.txt, next to this file: 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: L2120–2184.
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: L2128–2193.
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: L2185–2227.
79
+ - No capability: `tools`, `storage`, `system`, `app`, `ui` (images and Markdown), `i18n`, reading window.state, appearance.theme and packages.list. Details: L2194–2236.
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` is a TestHost (an in-memory player): `host.setSource(…)`, `host.command(id).run()`, `host.press(key)`, `await host.settle()`, then assert on `host.osd`, `host.window(id).props`… Guide: L1606–1771; TestHost: L6855.
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` is a TestHost (an in-memory player): `host.setSource(…)`, `host.command(id).run()`, `host.press(key)`, `await host.settle()`, then assert on `host.osd`, `host.window(id).props`… Guide: L1614–1779; TestHost: L6900.
82
82
 
83
83
  **Command line**
84
84
 
@@ -106,39 +106,39 @@ 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 or another `apiVersion`
109
- - Others: PluginError, llms-full.txt L3126.
109
+ - Others: PluginError, llms-full.txt L3135.
110
110
 
111
111
  ## Reference
112
112
 
113
113
  - [llms-full.txt](https://vestrasys.github.io/vestra-player-sdk/llms-full.txt), also node_modules/@vestra-player/sdk/llms-full.txt; the pages are on the [site](https://vestrasys.github.io/vestra-player-sdk/).
114
114
  - Tutorials: Getting started L9–214
115
- - Concepts: How a plugin runs L215–364; Commands, menus and key bindings L365–462; Playback state, controls and sources L463–602; Video and audio processing L603–680; Windows, panels, settings pages, dialogs and the OSD L681–968
116
- - How-to: Keep a slider responsive while dragging L969–1143; Remember a window's state between runs L1144–1273; Translate a plugin L1274–1457; Migrate a plugin to English source text L1458–1605; Test a plugin L1606–1771; Publish a plugin L1772–1860; Migrating off package rollback (1.5.16, removed in 1.5.17) L1861–1936
117
- - Guidelines: Interface guidelines L1937–2021; Menu guidelines L2022–2119
118
- - Reference: Manifest L2120–2184; Capabilities L2185–2227; Limits L2228–2322; Commands, menus and bindings L2323–2791; Platforms L2792–2820; Theme L2821–3027; Command line L3028–3083
119
- - [API: @vestra-player/sdk](https://vestrasys.github.io/vestra-player-sdk/modules/_vestra-player_sdk.html): L3084–6690
120
- - [API: @vestra-player/sdk/testing](https://vestrasys.github.io/vestra-player-sdk/modules/_vestra-player_sdk_testing.html): L6691–8055
115
+ - Concepts: How a plugin runs L215–364; Commands, menus and key bindings L365–462; Playback state, controls and sources L463–608; Video and audio processing L609–687; Windows, panels, settings pages, dialogs and the OSD L688–976
116
+ - How-to: Keep a slider responsive while dragging L977–1151; Remember a window's state between runs L1152–1281; Translate a plugin L1282–1465; Migrate a plugin to English source text L1466–1613; Test a plugin L1614–1779; Publish a plugin L1780–1868; Migrating off package rollback (1.5.16, removed in 1.5.17) L1869–1944
117
+ - Guidelines: Interface guidelines L1945–2029; Menu guidelines L2030–2127
118
+ - Reference: Manifest L2128–2193; Capabilities L2194–2236; Limits L2237–2331; Commands, menus and bindings L2332–2800; Platforms L2801–2829; Theme L2830–3036; Command line L3037–3092
119
+ - [API: @vestra-player/sdk](https://vestrasys.github.io/vestra-player-sdk/modules/_vestra-player_sdk.html): L3093–6735
120
+ - [API: @vestra-player/sdk/testing](https://vestrasys.github.io/vestra-player-sdk/modules/_vestra-player_sdk_testing.html): L6736–8109
121
121
 
122
122
  ## API
123
123
 
124
124
  Where each module of `plugin` (`plugin.<module>`) is declared:
125
125
 
126
- - Plugin L3318, Scope L3187, PluginError L3126, Disposable L3099
127
- - Commands L3530; Windows L3760; Panels L4403; Dialogs L4689; Player L5137; video: VideoFilter L5443, VideoView L5506, RenderOptions L5548; audio: AudioFilter L5467; Subtitles L5646; media: MediaCapture L5806; Resolvers L5911; Packages L6153; Osd L5379; Host L5310; Clipboard L6285; Power L6305; Tools L6338; Storage L6381; System L6404; Secrets L6422; window: MainWindow L4526; App L4607; Bindings L6255; Ui L3635; Timeline L4483; Drops L4059; Appearance L4088; I18n L4230; Settings L4336.
126
+ - Plugin L3327, Scope L3196, PluginError L3135, Disposable L3108
127
+ - Commands L3539; Windows L3769; Panels L4412; Dialogs L4698; Player L5165; video: VideoFilter L5472, VideoView L5535, RenderOptions L5577; audio: AudioFilter L5496; Subtitles L5675; media: MediaCapture L5835; Resolvers L5940; Packages L6182; Osd L5408; Host L5339; Clipboard L6314; Power L6334; Tools L6367; Storage L6410; System L6433; Secrets L6451; window: MainWindow L4535; App L4616; Bindings L6284; Ui L3644; Timeline L4492; Drops L4068; Appearance L4097; I18n L4239; Settings L4345.
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. L8479–8568
134
- - blur: A tool window with one slider that blurs the playing video. L8569–8830
135
- - signal-generator: Generates its own video and audio: colour bars and a 1 kHz tone. L8831–9163
136
- - bookmark: A tool window bound to the playback state: remember the position and jump back to it. L9164–9348
137
- - panel-menu: A sidebar panel with its own right-click menu. L9349–9554
138
- - menus: Radio menu items, separator groups and a menu item whose title changes. L9555–9714
139
- - theme-widgets: A tool window drawn with the player's theme and widgets. L9715–9878
140
- - markdown: A window that shows a Markdown document. L9879–10073
141
- - i18n: A plugin in the player's language: English source text, a Chinese translation. L10074–10241
133
+ - hello: The README Quick start: one menu command and an on-screen message. L8533–8622
134
+ - blur: A tool window with one slider that blurs the playing video. L8623–8884
135
+ - signal-generator: Generates its own video and audio: colour bars and a 1 kHz tone. L8885–9217
136
+ - bookmark: A tool window bound to the playback state: remember the position and jump back to it. L9218–9402
137
+ - panel-menu: A sidebar panel with its own right-click menu. L9403–9608
138
+ - menus: Radio menu items, separator groups and a menu item whose title changes. L9609–9768
139
+ - theme-widgets: A tool window drawn with the player's theme and widgets. L9769–9932
140
+ - markdown: A window that shows a Markdown document. L9933–10127
141
+ - i18n: A plugin in the player's language: English source text, a Chinese translation. L10128–10295
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.21",
3
+ "version": "1.5.24",
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": {