@vestra-player/sdk 1.5.26 → 1.5.27

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,12 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.5.27
4
+
5
+ **Who must change: nobody.** No API changes, `apiVersion` stays `vestra/1`, no player version needed. Plugins pick the fix up with the player's next SDK upgrade (the player builds the `plugin` object with its own copy); nothing in a plugin changes.
6
+
7
+ - **Generated streams reach their full frame rate on Windows.** A stream from `openStream` or a file resolver (the NES emulator: one 1/60 s picture per `frame()`) played at 23–26 frames a second on Windows instead of 60. The SDK asked the player how much audio the sound card had played, rendered at most one frame, and waited 5 ms before asking again; Windows rounds that wait up to its 15.6 ms timer tick, and with the round trip one pass took about 40 ms, so one frame per pass could not keep up. macOS timers are about 1 ms, which hid it. Each pass now calls `frame()` as many times in a row as it takes to refill the whole lead (about 50 ms of audio, unchanged, so latency is unchanged), at most 16 times, and stops early when a frame carries no audio; and a pass that rendered frames and already took longer than 5 ms is followed at once instead of after another timer tick. A pass that rendered nothing (paused, lead full) still waits 5 ms after it ends, so an idle or paused stream asks the player no more often than before. Measured against a real-time audio clock with the NES core and its homebrew ROM: with a 15–30 ms round trip 1.5.26 rendered 22–32 frames a second and let the device run dry, 1.5.27 renders 60 and keeps it fed; only a round trip near 50 ms (the whole lead) still lets it run dry now and then. Controls (start, pause, seek) still apply between frames and never while one renders, and `stop()` still ends after the frame being rendered.
8
+ - Docs: *Example: signal generator* says `frame()` may be called several times back to back. `llms.txt` and `llms-full.txt` regenerated.
9
+
3
10
  ## 1.5.26
4
11
 
5
12
  **Who must change: authors whose plugin uses a member without one of the capabilities it needs, and only to `check`, `build` or `pack` again.** No API changes, `apiVersion` stays `vestra/1`, no player version needed, and packages already built keep running as before: the player decides at run time exactly as it did. What changes is that `vestra-plugin check` (and so `build` and `pack`) now fails on such a use, including uses that never failed at run time: a listener (`onChange`) without `events` never fires instead of failing; a subtitle change (`select`, `load`, `remove`, `setStyle`) without `subtitles.read` fails only when it reads the current selection; a capability's **Also needs** (`processing.video` without `playback.read`) was documented but not enforced. Such a plugin packed with 1.5.25 and stops packing with 1.5.26 until the missing capability is added to `manifest.json` (or the use removed). The player's built-in plugins and the plugin repository's plugins were checked with this release: no findings.
@@ -5,8 +5,33 @@
5
5
  */
6
6
  import { failure, sleep, toError } from "./runtime.js";
7
7
  import { abortError } from "./player.js";
8
- /** Frames of audio buffered ahead of the device before rendering pauses. */
8
+ /**
9
+ * Frames of audio buffered ahead of the device before rendering pauses
10
+ * (50 ms). Each pump refills the whole lead, so it must outlast one pump
11
+ * period: the pump itself (a `producer.state` round trip and the frames),
12
+ * plus a Windows timer tick (15.6 ms) unless the pump rendered and already
13
+ * took {@link PERIOD}. Measured with a
14
+ * real-time clock, round trips up to about 30 ms never let the device run
15
+ * dry; near 50 ms they do now and then. A larger lead would add latency
16
+ * between input and picture.
17
+ */
9
18
  const AHEAD = 2400;
19
+ /**
20
+ * At most this many frames per pump. A lead is a few frames (three of
21
+ * 1/60 s); the cap only stops a plugin whose frames carry next to no audio
22
+ * from rendering without pause.
23
+ */
24
+ const BATCH = 16;
25
+ /**
26
+ * Milliseconds from the start of one pump to the start of the next. A pump
27
+ * that rendered frames and already took longer is followed at once: a timer
28
+ * on top would add a whole Windows tick (15.6 ms; a timer there never fires
29
+ * sooner) to every pump while playing, and the period must stay well inside
30
+ * the lead. A pump that rendered nothing (paused, lead full, a control not
31
+ * yet acknowledged) waits this long after it ends, so an idle stream polls
32
+ * the player no more often than it always did.
33
+ */
34
+ const PERIOD = 5;
10
35
  /** Frames written before the host is asked to select the stream. */
11
36
  const PREROLL = 2048;
12
37
  class StreamHandle {
@@ -23,6 +48,7 @@ class StreamHandle {
23
48
  #sample;
24
49
  #sequence;
25
50
  #timer;
51
+ #immediate;
26
52
  #pumping = Promise.resolve();
27
53
  constructor(scope, service, setup) {
28
54
  this.#scope = scope;
@@ -48,6 +74,7 @@ class StreamHandle {
48
74
  }
49
75
  async #pump() {
50
76
  const producer = this.#service.runtime.host.producer;
77
+ const begun = performance.now();
51
78
  try {
52
79
  const state = await producer.state(this.#producer);
53
80
  if (!this.#running)
@@ -67,16 +94,36 @@ class StreamHandle {
67
94
  if (state.input?.pending)
68
95
  await producer.ackInput(state.input.pending);
69
96
  const paused = control ? control.paused : state.paused;
97
+ let rendered = 0;
98
+ // Refill the whole lead now, not one frame per pump: a pump period (a
99
+ // round trip, and a timer tick after a short pump: 16–40 ms on Windows)
100
+ // is longer than a 1/60 s frame. No control lands while this loop runs: the next one is
101
+ // read by the next pump, at a frame boundary.
70
102
  if (!control &&
71
103
  state.acknowledged &&
72
104
  state.epoch === this.#epoch &&
73
- !paused &&
74
- this.#sample - state.consumedFrames < AHEAD)
75
- await this.#render();
76
- if (this.#running)
77
- this.#timer = setTimeout(() => {
78
- this.#pumping = this.#pump();
79
- }, 5);
105
+ !paused)
106
+ while (this.#running &&
107
+ rendered < BATCH &&
108
+ this.#sample - state.consumedFrames < AHEAD) {
109
+ const before = this.#sample;
110
+ await this.#render();
111
+ // A frame without audio does not move the clock: wait for the next
112
+ // pump, after a full PERIOD (it does not count as rendered).
113
+ if (this.#sample === before)
114
+ break;
115
+ rendered++;
116
+ }
117
+ if (!this.#running)
118
+ return;
119
+ const next = () => {
120
+ this.#pumping = this.#pump();
121
+ };
122
+ const wait = rendered ? PERIOD - (performance.now() - begun) : PERIOD;
123
+ if (wait > 0)
124
+ this.#timer = setTimeout(next, wait);
125
+ else
126
+ this.#immediate = setImmediate(next);
80
127
  }
81
128
  catch (error) {
82
129
  const retired = await retirement(this.#service, this.#generation, error);
@@ -114,6 +161,7 @@ class StreamHandle {
114
161
  this.#stopping ??= (async () => {
115
162
  this.#running = false;
116
163
  clearTimeout(this.#timer);
164
+ clearImmediate(this.#immediate);
117
165
  await this.#pumping.catch(() => { });
118
166
  try {
119
167
  await this.#service.closeIfCurrent(this.#generation);
package/llms-full.txt CHANGED
@@ -1,4 +1,4 @@
1
- # @vestra-player/sdk 1.5.26: full reference
1
+ # @vestra-player/sdk 1.5.27: 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
 
@@ -9096,7 +9096,7 @@ Usually the player plays a file: it decodes the picture and the sound and keeps
9096
9096
  The plugin does not push frames on a timer of its own. **The player pulls them:** the SDK calls `frame()` whenever the player needs more.
9097
9097
 
9098
9098
  - `frame()` is never called twice at the same time, and not at all while the player is paused.
9099
- - It is paced by the **audio clock**. The SDK keeps a little audio queued ahead of the sound card (about 50 ms) and calls `frame()` when the queue runs low.
9099
+ - It is paced by the **audio clock**. The SDK keeps a little audio queued ahead of the sound card (about 50 ms). Regularly, several times per frame, it checks how much the sound card has played and calls `frame()` as many times in a row as it takes to fill the queue again: two or three calls back to back are normal, so do not time anything by the gap between calls.
9100
9100
  - It is **synchronous**: it cannot `await`, so everything it needs must already be in memory. If it is slow, the sound drops out.
9101
9101
 
9102
9102
  Each call returns one picture and **the audio that plays while that picture is on screen**. The stream's timeline is its audio: the player plays the samples at 48 000 per second and shows each picture when the audio reaches it. A frame that carries 1600 samples therefore lasts 1600 / 48 000 s = 1/30 s. The number of samples per frame _is_ the frame rate; there is no separate setting.
package/llms.txt CHANGED
@@ -1,6 +1,6 @@
1
1
  # Vestra Player SDK
2
2
 
3
- > @vestra-player/sdk 1.5.26: 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.27: 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
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vestra-player/sdk",
3
- "version": "1.5.26",
3
+ "version": "1.5.27",
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": {