@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 +7 -0
- package/dist/plugin/stream.js +56 -8
- package/llms-full.txt +2 -2
- package/llms.txt +1 -1
- package/package.json +1 -1
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.
|
package/dist/plugin/stream.js
CHANGED
|
@@ -5,8 +5,33 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { failure, sleep, toError } from "./runtime.js";
|
|
7
7
|
import { abortError } from "./player.js";
|
|
8
|
-
/**
|
|
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.#
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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.
|
|
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()`
|
|
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.
|
|
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
|
|