@openvalle/engine 0.0.0-stage → 0.1.0
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/LICENSE +202 -0
- package/README.md +64 -2
- package/dist/connection.d.ts +36 -0
- package/dist/element.d.ts +11 -0
- package/dist/element.js +85 -0
- package/dist/export.d.ts +25 -0
- package/dist/export.js +83 -0
- package/dist/index-19acnyeh.js +12 -0
- package/dist/index-5vzwpwd6.js +388 -0
- package/dist/index-cm27tnmf.js +120 -0
- package/dist/index.d.ts +99 -0
- package/dist/index.js +12 -0
- package/dist/messages.d.ts +116 -0
- package/dist/sound.js +81 -0
- package/dist/valle_web_bg.wasm +0 -0
- package/dist/worker.js +33873 -0
- package/package.json +30 -4
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
import {
|
|
2
|
+
PlayerError2,
|
|
3
|
+
RATE,
|
|
4
|
+
Connection,
|
|
5
|
+
spawns
|
|
6
|
+
} from "./index-cm27tnmf.js";
|
|
7
|
+
|
|
8
|
+
// web/packages/engine/src/index.ts
|
|
9
|
+
async function createPlayer2(options) {
|
|
10
|
+
if (!/\.(motion\.tsx|timeline\.json)$/.test(options.entry)) {
|
|
11
|
+
throw new PlayerError2("file", `${options.entry}: an entry must end with .motion.tsx or .timeline.json`);
|
|
12
|
+
}
|
|
13
|
+
const runtime = options.runtime === undefined ? new URL("./", import.meta.url) : new URL(String(options.runtime).replace(/\/?$/, "/"), location.href);
|
|
14
|
+
const base = new URL(options.base ?? ".", location.href).href;
|
|
15
|
+
const spawn = {
|
|
16
|
+
runtime,
|
|
17
|
+
base,
|
|
18
|
+
fonts: options.fonts === undefined ? undefined : new URL(options.fonts, base).href,
|
|
19
|
+
fetch: options.fetch,
|
|
20
|
+
entry: options.entry,
|
|
21
|
+
inputs: { props: options.props ?? {}, data: options.data ?? {}, resources: {} },
|
|
22
|
+
given: { ...options.files }
|
|
23
|
+
};
|
|
24
|
+
const offscreen = options.canvas.transferControlToOffscreen();
|
|
25
|
+
const connection = await Connection.start(spawn, offscreen);
|
|
26
|
+
const player = new Player2(connection, spawn, options);
|
|
27
|
+
try {
|
|
28
|
+
await player.start(options.resources ?? {});
|
|
29
|
+
} catch (error) {
|
|
30
|
+
await connection.close();
|
|
31
|
+
throw error;
|
|
32
|
+
}
|
|
33
|
+
return player;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
class Player2 {
|
|
37
|
+
#connection;
|
|
38
|
+
#spawn;
|
|
39
|
+
#info;
|
|
40
|
+
#rate;
|
|
41
|
+
#listeners = new Map;
|
|
42
|
+
#frame = 0;
|
|
43
|
+
#clock;
|
|
44
|
+
#sound = null;
|
|
45
|
+
#audio;
|
|
46
|
+
#context;
|
|
47
|
+
#gen = 0;
|
|
48
|
+
#volume;
|
|
49
|
+
#muted;
|
|
50
|
+
#drawing;
|
|
51
|
+
#wanted;
|
|
52
|
+
#animation = 0;
|
|
53
|
+
#given = 0;
|
|
54
|
+
#stats = { shown: 0, dropped: 0, frameMs: 0, memory: 0 };
|
|
55
|
+
#lastShown;
|
|
56
|
+
#statsEpoch = 0;
|
|
57
|
+
#reached = 0;
|
|
58
|
+
#said = 0;
|
|
59
|
+
loop;
|
|
60
|
+
constructor(connection, spawn, options) {
|
|
61
|
+
this.#connection = connection;
|
|
62
|
+
this.#spawn = spawn;
|
|
63
|
+
this.loop = options.loop ?? false;
|
|
64
|
+
this.#muted = options.muted ?? false;
|
|
65
|
+
this.#volume = options.volume ?? 1;
|
|
66
|
+
spawns.set(this, spawn);
|
|
67
|
+
connection.stopped = (error) => {
|
|
68
|
+
this.pause();
|
|
69
|
+
this.#emit("error", error);
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
async start(resources) {
|
|
73
|
+
for (const [name, files] of Object.entries(resources)) {
|
|
74
|
+
this.#spawn.inputs.resources[name] = await this.#give(name, files);
|
|
75
|
+
}
|
|
76
|
+
await this.#open();
|
|
77
|
+
}
|
|
78
|
+
get info() {
|
|
79
|
+
return this.#info;
|
|
80
|
+
}
|
|
81
|
+
get controls() {
|
|
82
|
+
return this.#info.controls;
|
|
83
|
+
}
|
|
84
|
+
get currentFrame() {
|
|
85
|
+
return this.#frame;
|
|
86
|
+
}
|
|
87
|
+
get currentTime() {
|
|
88
|
+
return this.#time(this.#frame);
|
|
89
|
+
}
|
|
90
|
+
get playing() {
|
|
91
|
+
return this.#clock !== undefined;
|
|
92
|
+
}
|
|
93
|
+
get volume() {
|
|
94
|
+
return this.#volume;
|
|
95
|
+
}
|
|
96
|
+
set volume(volume) {
|
|
97
|
+
this.#volume = volume;
|
|
98
|
+
this.#audio?.then((a) => a.gain.gain.value = volume);
|
|
99
|
+
}
|
|
100
|
+
get muted() {
|
|
101
|
+
return this.#muted;
|
|
102
|
+
}
|
|
103
|
+
set muted(muted) {
|
|
104
|
+
if (muted === this.#muted) {
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
this.#muted = muted;
|
|
108
|
+
if (this.#clock) {
|
|
109
|
+
this.#run(this.#frame);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
get hasSound() {
|
|
113
|
+
return this.#sound !== null;
|
|
114
|
+
}
|
|
115
|
+
play() {
|
|
116
|
+
if (this.#clock) {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
const last = this.#info.frames - 1;
|
|
120
|
+
this.#run(this.#frame >= last && !this.loop ? 0 : this.#frame);
|
|
121
|
+
this.#reached = this.#clock.frame;
|
|
122
|
+
this.#resetStats(this.#frame);
|
|
123
|
+
this.#emit("play", undefined);
|
|
124
|
+
const tick = () => {
|
|
125
|
+
const clock = this.#clock;
|
|
126
|
+
if (!clock) {
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
const [n, d] = this.#rate;
|
|
130
|
+
const elapsed = this.#now(clock) - clock.at;
|
|
131
|
+
let index = clock.frame + (Number.isNaN(elapsed) ? 0 : Math.floor(elapsed * (n / d)));
|
|
132
|
+
if (index > last) {
|
|
133
|
+
if (!this.loop) {
|
|
134
|
+
this.#show(last);
|
|
135
|
+
this.pause();
|
|
136
|
+
this.#emit("ended", undefined);
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
this.#run(0);
|
|
140
|
+
index = this.#reached = 0;
|
|
141
|
+
}
|
|
142
|
+
this.#stats.dropped += Math.max(0, index - this.#reached - 1);
|
|
143
|
+
this.#reached = Math.max(this.#reached, index);
|
|
144
|
+
if (index !== this.#frame)
|
|
145
|
+
this.#show(index);
|
|
146
|
+
if (performance.now() - this.#said >= 1000) {
|
|
147
|
+
this.#say();
|
|
148
|
+
}
|
|
149
|
+
this.#animation = requestAnimationFrame(tick);
|
|
150
|
+
};
|
|
151
|
+
tick();
|
|
152
|
+
}
|
|
153
|
+
pause() {
|
|
154
|
+
if (!this.#clock) {
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
this.#clock = undefined;
|
|
158
|
+
cancelAnimationFrame(this.#animation);
|
|
159
|
+
this.#silence();
|
|
160
|
+
this.#say();
|
|
161
|
+
this.#emit("pause", undefined);
|
|
162
|
+
}
|
|
163
|
+
seek(seconds) {
|
|
164
|
+
this.seekFrame(this.#index(seconds));
|
|
165
|
+
}
|
|
166
|
+
seekFrame(index) {
|
|
167
|
+
const frame = Math.min(Math.max(Math.trunc(index), 0), this.#info.frames - 1);
|
|
168
|
+
if (this.#clock) {
|
|
169
|
+
this.#run(frame);
|
|
170
|
+
this.#reached = frame;
|
|
171
|
+
this.#resetStats(frame);
|
|
172
|
+
}
|
|
173
|
+
this.#wanted = undefined;
|
|
174
|
+
this.#show(frame);
|
|
175
|
+
}
|
|
176
|
+
async frame(seconds) {
|
|
177
|
+
const index = this.#index(seconds);
|
|
178
|
+
const pixels = await this.#send({ type: "still", index });
|
|
179
|
+
const { width, height } = this.#info;
|
|
180
|
+
const canvas = new OffscreenCanvas(width, height);
|
|
181
|
+
const image = new ImageData(new Uint8ClampedArray(pixels.buffer), width, height);
|
|
182
|
+
canvas.getContext("2d").putImageData(image, 0, 0);
|
|
183
|
+
return canvas.convertToBlob({ type: "image/png" });
|
|
184
|
+
}
|
|
185
|
+
async renderSound(start, seconds) {
|
|
186
|
+
const sample = Math.round(start * RATE);
|
|
187
|
+
const samples = Math.round((start + seconds) * RATE) - sample;
|
|
188
|
+
return await this.#send({ type: "render-sound", sample, samples });
|
|
189
|
+
}
|
|
190
|
+
async draw(index) {
|
|
191
|
+
this.#frame = index;
|
|
192
|
+
const started = performance.now();
|
|
193
|
+
const epoch = this.#statsEpoch;
|
|
194
|
+
const { memory } = await this.#send({ type: "frame", index });
|
|
195
|
+
const stats = this.#stats;
|
|
196
|
+
stats.memory = memory;
|
|
197
|
+
if (this.#clock && epoch === this.#statsEpoch && index !== this.#lastShown) {
|
|
198
|
+
stats.frameMs += performance.now() - started;
|
|
199
|
+
stats.shown++;
|
|
200
|
+
this.#lastShown = index;
|
|
201
|
+
}
|
|
202
|
+
this.#emit("time", { time: this.#time(index), frame: index });
|
|
203
|
+
}
|
|
204
|
+
async pixels() {
|
|
205
|
+
return await this.#send({ type: "pixels" });
|
|
206
|
+
}
|
|
207
|
+
async setProps(props) {
|
|
208
|
+
await this.#rebind("props", props);
|
|
209
|
+
}
|
|
210
|
+
async setData(data) {
|
|
211
|
+
await this.#rebind("data", data);
|
|
212
|
+
}
|
|
213
|
+
async setResource(name, files) {
|
|
214
|
+
const resources = { ...this.#spawn.inputs.resources };
|
|
215
|
+
if (files === undefined || files.length === 0) {
|
|
216
|
+
delete resources[name];
|
|
217
|
+
} else {
|
|
218
|
+
resources[name] = await this.#give(name, files);
|
|
219
|
+
}
|
|
220
|
+
await this.#rebind("resources", resources);
|
|
221
|
+
}
|
|
222
|
+
on(type, listener) {
|
|
223
|
+
const set = this.#listeners.get(type) ?? new Set;
|
|
224
|
+
this.#listeners.set(type, set);
|
|
225
|
+
set.add(listener);
|
|
226
|
+
return () => set.delete(listener);
|
|
227
|
+
}
|
|
228
|
+
async close() {
|
|
229
|
+
this.pause();
|
|
230
|
+
spawns.delete(this);
|
|
231
|
+
await this.#connection.close();
|
|
232
|
+
await this.#audio?.then((a) => a.context.close()).catch(() => {});
|
|
233
|
+
}
|
|
234
|
+
#time(index) {
|
|
235
|
+
const [n, d] = this.#rate;
|
|
236
|
+
return index * d / n;
|
|
237
|
+
}
|
|
238
|
+
#index(seconds) {
|
|
239
|
+
const [n, d] = this.#rate;
|
|
240
|
+
const index = Math.floor(seconds * (n / d) + 0.000000001);
|
|
241
|
+
return Math.min(Math.max(index, 0), this.#info.frames - 1);
|
|
242
|
+
}
|
|
243
|
+
#run(frame) {
|
|
244
|
+
this.#silence();
|
|
245
|
+
if (!this.#sound || this.#muted) {
|
|
246
|
+
this.#clock = { frame, at: performance.now() / 1000 };
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
const gen = ++this.#gen;
|
|
250
|
+
this.#clock = { frame, at: Number.NaN, gen };
|
|
251
|
+
const sample = Math.round(this.#time(frame) * RATE);
|
|
252
|
+
this.#playing().then(async (a) => {
|
|
253
|
+
await a.context.resume();
|
|
254
|
+
a.node.port.postMessage({ start: gen, sample });
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
#silence() {
|
|
258
|
+
this.#audio?.then((a) => a.node.port.postMessage({ stop: true }));
|
|
259
|
+
}
|
|
260
|
+
#now(clock) {
|
|
261
|
+
if (clock.gen === undefined) {
|
|
262
|
+
return performance.now() / 1000;
|
|
263
|
+
}
|
|
264
|
+
const context = this.#context;
|
|
265
|
+
return context ? context.currentTime - (context.outputLatency || context.baseLatency) : 0;
|
|
266
|
+
}
|
|
267
|
+
#playing() {
|
|
268
|
+
this.#audio ??= (async () => {
|
|
269
|
+
const context = new AudioContext({ sampleRate: RATE, latencyHint: "interactive" });
|
|
270
|
+
this.#context = context;
|
|
271
|
+
await context.audioWorklet.addModule(new URL("sound.js", this.#spawn.runtime));
|
|
272
|
+
const node = new AudioWorkletNode(context, "valle-sound", {
|
|
273
|
+
numberOfInputs: 0,
|
|
274
|
+
outputChannelCount: [2]
|
|
275
|
+
});
|
|
276
|
+
const gain = new GainNode(context, { gain: this.#volume });
|
|
277
|
+
node.connect(gain).connect(context.destination);
|
|
278
|
+
node.port.onmessage = (event) => {
|
|
279
|
+
const { started, frame } = event.data;
|
|
280
|
+
if (this.#clock?.gen === started) {
|
|
281
|
+
this.#clock.at = frame / context.sampleRate;
|
|
282
|
+
}
|
|
283
|
+
};
|
|
284
|
+
const channel = new MessageChannel;
|
|
285
|
+
node.port.postMessage({ port: channel.port1 }, [channel.port1]);
|
|
286
|
+
await this.#send({ type: "worklet", port: channel.port2 }, [channel.port2]);
|
|
287
|
+
return { context, node, gain };
|
|
288
|
+
})();
|
|
289
|
+
return this.#audio;
|
|
290
|
+
}
|
|
291
|
+
#say() {
|
|
292
|
+
const now = performance.now();
|
|
293
|
+
const elapsedMs = now - this.#said;
|
|
294
|
+
const { shown, dropped, frameMs, memory } = this.#stats;
|
|
295
|
+
this.#emit("stats", {
|
|
296
|
+
fps: elapsedMs > 0 ? shown * 1000 / elapsedMs : 0,
|
|
297
|
+
elapsedMs,
|
|
298
|
+
shown,
|
|
299
|
+
dropped,
|
|
300
|
+
frameMs: shown ? frameMs / shown : 0,
|
|
301
|
+
memory
|
|
302
|
+
});
|
|
303
|
+
this.#stats = { shown: 0, dropped: 0, frameMs: 0, memory };
|
|
304
|
+
this.#said = now;
|
|
305
|
+
}
|
|
306
|
+
#resetStats(frame) {
|
|
307
|
+
this.#stats = { shown: 0, dropped: 0, frameMs: 0, memory: this.#stats.memory };
|
|
308
|
+
this.#lastShown = frame;
|
|
309
|
+
this.#statsEpoch++;
|
|
310
|
+
this.#said = performance.now();
|
|
311
|
+
}
|
|
312
|
+
#show(index) {
|
|
313
|
+
this.#frame = index;
|
|
314
|
+
if (this.#drawing !== undefined) {
|
|
315
|
+
if (this.#clock && this.#wanted !== undefined && this.#wanted !== index && this.#wanted !== this.#drawing) {
|
|
316
|
+
this.#stats.dropped++;
|
|
317
|
+
}
|
|
318
|
+
this.#wanted = index;
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
this.#drawing = index;
|
|
322
|
+
this.draw(index).catch((error) => {
|
|
323
|
+
this.pause();
|
|
324
|
+
this.#emit("error", error);
|
|
325
|
+
}).finally(() => {
|
|
326
|
+
this.#drawing = undefined;
|
|
327
|
+
const wanted = this.#wanted;
|
|
328
|
+
this.#wanted = undefined;
|
|
329
|
+
if (wanted !== undefined && wanted !== index) {
|
|
330
|
+
this.#show(wanted);
|
|
331
|
+
}
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
async#open() {
|
|
335
|
+
const { entry, inputs } = this.#spawn;
|
|
336
|
+
this.#info = await this.#send({ type: "open", entry, inputs });
|
|
337
|
+
const [n, d = "1"] = this.#info.fps.split("/");
|
|
338
|
+
this.#rate = [Number(n), Number(d)];
|
|
339
|
+
this.#frame = Math.min(this.#frame, this.#info.frames - 1);
|
|
340
|
+
this.#sound = await this.#send({ type: "sound", entry });
|
|
341
|
+
await this.draw(this.#frame);
|
|
342
|
+
}
|
|
343
|
+
async#rebind(key, value) {
|
|
344
|
+
const playing = this.playing;
|
|
345
|
+
this.pause();
|
|
346
|
+
const before = this.#spawn.inputs[key];
|
|
347
|
+
this.#spawn.inputs[key] = value;
|
|
348
|
+
try {
|
|
349
|
+
await this.#send({ type: "close" });
|
|
350
|
+
await this.#open();
|
|
351
|
+
} catch (error) {
|
|
352
|
+
this.#spawn.inputs[key] = before;
|
|
353
|
+
await this.#send({ type: "close" });
|
|
354
|
+
await this.#open();
|
|
355
|
+
throw error;
|
|
356
|
+
} finally {
|
|
357
|
+
if (playing) {
|
|
358
|
+
this.play();
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
async#give(name, files) {
|
|
363
|
+
const paths = [];
|
|
364
|
+
const given = {};
|
|
365
|
+
for (const file of files) {
|
|
366
|
+
if (typeof file === "string") {
|
|
367
|
+
paths.push(file);
|
|
368
|
+
continue;
|
|
369
|
+
}
|
|
370
|
+
const path = `valle:resource/${encodeURIComponent(name)}/${this.#given++}`;
|
|
371
|
+
given[path] = file;
|
|
372
|
+
paths.push(path);
|
|
373
|
+
}
|
|
374
|
+
await this.#send({ type: "give", files: given });
|
|
375
|
+
Object.assign(this.#spawn.given, given);
|
|
376
|
+
return paths;
|
|
377
|
+
}
|
|
378
|
+
#send(request, transfer = []) {
|
|
379
|
+
return this.#connection.send(request, transfer);
|
|
380
|
+
}
|
|
381
|
+
#emit(type, value) {
|
|
382
|
+
for (const listener of this.#listeners.get(type) ?? []) {
|
|
383
|
+
listener(value);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
export { createPlayer2, Player2 };
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
// web/packages/engine/src/connection.ts
|
|
2
|
+
class PlayerError2 extends Error {
|
|
3
|
+
code;
|
|
4
|
+
constructor(code, message) {
|
|
5
|
+
super(message);
|
|
6
|
+
this.code = code;
|
|
7
|
+
this.name = "PlayerError";
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
var RATE = 48000;
|
|
11
|
+
|
|
12
|
+
class Connection {
|
|
13
|
+
worker;
|
|
14
|
+
#requests = new Map;
|
|
15
|
+
#next = 0;
|
|
16
|
+
#progress = new Map;
|
|
17
|
+
#fetch;
|
|
18
|
+
stopped = () => {};
|
|
19
|
+
static async start(spawn, canvas) {
|
|
20
|
+
const worker = new Worker(new URL("worker.js", spawn.runtime), { type: "module" });
|
|
21
|
+
const connection = new Connection(worker, spawn.fetch);
|
|
22
|
+
try {
|
|
23
|
+
await connection.send({
|
|
24
|
+
type: "start",
|
|
25
|
+
canvas,
|
|
26
|
+
base: spawn.base,
|
|
27
|
+
fonts: spawn.fonts,
|
|
28
|
+
proxied: spawn.fetch !== undefined
|
|
29
|
+
}, [canvas]);
|
|
30
|
+
if (Object.keys(spawn.given).length > 0) {
|
|
31
|
+
await connection.send({ type: "give", files: spawn.given });
|
|
32
|
+
}
|
|
33
|
+
} catch (error) {
|
|
34
|
+
worker.terminate();
|
|
35
|
+
throw error;
|
|
36
|
+
}
|
|
37
|
+
return connection;
|
|
38
|
+
}
|
|
39
|
+
constructor(worker, fetch) {
|
|
40
|
+
this.worker = worker;
|
|
41
|
+
this.#fetch = fetch;
|
|
42
|
+
worker.onmessage = (event) => {
|
|
43
|
+
const data = event.data;
|
|
44
|
+
if ("progress" in data) {
|
|
45
|
+
this.#progress.get(data.progress)?.(data.done);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
if ("decode" in data) {
|
|
49
|
+
this.#decode(data.decode, data.file);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
if ("fetch" in data) {
|
|
53
|
+
this.#fetched(data.fetch, data.url);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
const request = this.#requests.get(data.id);
|
|
57
|
+
this.#requests.delete(data.id);
|
|
58
|
+
if (data.ok) {
|
|
59
|
+
request?.resolve(data.value);
|
|
60
|
+
} else {
|
|
61
|
+
request?.reject(new PlayerError2(data.code, data.message));
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
worker.onerror = (event) => {
|
|
65
|
+
event.preventDefault();
|
|
66
|
+
const error = new PlayerError2("scene", `the engine stopped: ${event.message}`);
|
|
67
|
+
this.fail(error);
|
|
68
|
+
this.stopped(error);
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
send(request, transfer = [], progress) {
|
|
72
|
+
const id = this.#next++;
|
|
73
|
+
if (progress) {
|
|
74
|
+
this.#progress.set(id, progress);
|
|
75
|
+
}
|
|
76
|
+
return new Promise((resolve, reject) => {
|
|
77
|
+
this.#requests.set(id, { resolve, reject });
|
|
78
|
+
this.worker.postMessage({ id, ...request }, transfer);
|
|
79
|
+
}).finally(() => this.#progress.delete(id));
|
|
80
|
+
}
|
|
81
|
+
fail(error) {
|
|
82
|
+
for (const request of this.#requests.values()) {
|
|
83
|
+
request.reject(error);
|
|
84
|
+
}
|
|
85
|
+
this.#requests.clear();
|
|
86
|
+
}
|
|
87
|
+
async close() {
|
|
88
|
+
const destroyed = this.send({ type: "destroy" }).catch(() => {});
|
|
89
|
+
await Promise.race([destroyed, new Promise((r) => setTimeout(r, 1000))]);
|
|
90
|
+
this.fail(new PlayerError2("scene", "the player was closed"));
|
|
91
|
+
this.worker.terminate();
|
|
92
|
+
}
|
|
93
|
+
async#fetched(id, url) {
|
|
94
|
+
try {
|
|
95
|
+
const response = await this.#fetch(url);
|
|
96
|
+
const bytes = response.ok ? await response.arrayBuffer() : undefined;
|
|
97
|
+
this.worker.postMessage({ fetched: id, status: response.status, statusText: response.statusText, bytes }, bytes ? [bytes] : []);
|
|
98
|
+
} catch (error) {
|
|
99
|
+
this.worker.postMessage({ fetched: id, error: String(error) });
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
async#decode(id, file) {
|
|
103
|
+
let pcm = null;
|
|
104
|
+
try {
|
|
105
|
+
const decoder = new OfflineAudioContext(2, 1, RATE);
|
|
106
|
+
const buffer = await decoder.decodeAudioData(file.buffer);
|
|
107
|
+
const left = buffer.getChannelData(0);
|
|
108
|
+
const right = buffer.getChannelData(Math.min(1, buffer.numberOfChannels - 1));
|
|
109
|
+
pcm = new Float32Array(2 * buffer.length);
|
|
110
|
+
for (let i = 0;i < buffer.length; i++) {
|
|
111
|
+
pcm[2 * i] = left[i];
|
|
112
|
+
pcm[2 * i + 1] = right[i];
|
|
113
|
+
}
|
|
114
|
+
} catch {}
|
|
115
|
+
this.worker.postMessage({ decoded: id, pcm }, pcm ? [pcm.buffer] : []);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
var spawns = new WeakMap;
|
|
119
|
+
|
|
120
|
+
export { PlayerError2, RATE, Connection, spawns };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { Connection, PlayerError, type Spawn } from "./connection.ts";
|
|
2
|
+
import type { Controls, Opened, Source } from "./messages.ts";
|
|
3
|
+
export { PlayerError } from "./connection.ts";
|
|
4
|
+
export type { Code, Control, Controls, Opened, Source } from "./messages.ts";
|
|
5
|
+
export type Options = {
|
|
6
|
+
canvas: HTMLCanvasElement;
|
|
7
|
+
/** The scene, `.motion.tsx`, or the timeline, `.timeline.json`, as a path from `base`. */
|
|
8
|
+
entry: string;
|
|
9
|
+
/** Where the SDK's worker, audio worklet and wasm are: beside this module by default. */
|
|
10
|
+
runtime?: string | URL;
|
|
11
|
+
/** Where relative paths are fetched from: the page's address by default. */
|
|
12
|
+
base?: string | URL;
|
|
13
|
+
/** Files given in place of fetching their paths. */
|
|
14
|
+
files?: Record<string, Source>;
|
|
15
|
+
/** What fetches the files the engine reads, the built-in fonts among them: `fetch` by default. */
|
|
16
|
+
fetch?: (url: string) => Promise<Response>;
|
|
17
|
+
/** Where the built-in fonts are fetched from, by their file names, if not Google's addresses. */
|
|
18
|
+
fonts?: string | URL;
|
|
19
|
+
props?: Record<string, unknown>;
|
|
20
|
+
data?: Record<string, unknown>;
|
|
21
|
+
/** Each resource's files, by its name. */
|
|
22
|
+
resources?: Record<string, Source[]>;
|
|
23
|
+
loop?: boolean;
|
|
24
|
+
muted?: boolean;
|
|
25
|
+
/** From 0 to 1. */
|
|
26
|
+
volume?: number;
|
|
27
|
+
};
|
|
28
|
+
/** Playback over `elapsedMs`: distinct completed video frames and their rate (not display
|
|
29
|
+
* refreshes), skipped frames, average frame latency, and bytes of engine memory. */
|
|
30
|
+
export type Stats = {
|
|
31
|
+
fps: number;
|
|
32
|
+
elapsedMs: number;
|
|
33
|
+
shown: number;
|
|
34
|
+
dropped: number;
|
|
35
|
+
frameMs: number;
|
|
36
|
+
memory: number;
|
|
37
|
+
};
|
|
38
|
+
export type Events = {
|
|
39
|
+
/** A frame was drawn: its time and index. */
|
|
40
|
+
time: {
|
|
41
|
+
time: number;
|
|
42
|
+
frame: number;
|
|
43
|
+
};
|
|
44
|
+
play: undefined;
|
|
45
|
+
pause: undefined;
|
|
46
|
+
/** Playing reached the last frame and does not loop. */
|
|
47
|
+
ended: undefined;
|
|
48
|
+
error: PlayerError;
|
|
49
|
+
/** Each second of playing, and when it pauses. */
|
|
50
|
+
stats: Stats;
|
|
51
|
+
};
|
|
52
|
+
export declare function createPlayer(options: Options): Promise<Player>;
|
|
53
|
+
export declare class Player {
|
|
54
|
+
#private;
|
|
55
|
+
loop: boolean;
|
|
56
|
+
constructor(connection: Connection, spawn: Spawn, options: Options);
|
|
57
|
+
/** @internal */
|
|
58
|
+
start(resources: Record<string, Source[]>): Promise<void>;
|
|
59
|
+
/** The composition and the controls of what is open. */
|
|
60
|
+
get info(): Opened;
|
|
61
|
+
get controls(): Controls;
|
|
62
|
+
/** The frame shown, or being drawn. */
|
|
63
|
+
get currentFrame(): number;
|
|
64
|
+
/** The time of the frame shown, in seconds. */
|
|
65
|
+
get currentTime(): number;
|
|
66
|
+
get playing(): boolean;
|
|
67
|
+
/** The sound's volume, from 0 to 1. */
|
|
68
|
+
get volume(): number;
|
|
69
|
+
set volume(volume: number);
|
|
70
|
+
/** Whether the sound is silenced: playing then keeps to the page's clock, as it may without a
|
|
71
|
+
* user's gesture. */
|
|
72
|
+
get muted(): boolean;
|
|
73
|
+
set muted(muted: boolean);
|
|
74
|
+
/** Whether what is open has sound, which then sets the clock playing keeps to. */
|
|
75
|
+
get hasSound(): boolean;
|
|
76
|
+
play(): void;
|
|
77
|
+
pause(): void;
|
|
78
|
+
/** Shows the frame at `seconds`; playing goes on from it. */
|
|
79
|
+
seek(seconds: number): void;
|
|
80
|
+
seekFrame(index: number): void;
|
|
81
|
+
/** The frame at `seconds` as a PNG, drawn off the canvas. */
|
|
82
|
+
frame(seconds: number): Promise<Blob>;
|
|
83
|
+
/** `seconds` of the sound from `start`, interleaved stereo at 48 000 samples a second, as an
|
|
84
|
+
* export encodes it; silence past its end. Throws where what is open has no sound. */
|
|
85
|
+
renderSound(start: number, seconds: number): Promise<Float32Array>;
|
|
86
|
+
/** Draws frame `index` and resolves once it is drawn. */
|
|
87
|
+
draw(index: number): Promise<void>;
|
|
88
|
+
/** The pixels of the frame drawn last: straight sRGB, 4 bytes a pixel, rows top down. */
|
|
89
|
+
pixels(): Promise<Uint8Array>;
|
|
90
|
+
/** Binds the scene's props to `props`, in place of those given before. A value that does not
|
|
91
|
+
* fit its control fails, and what was bound stays. */
|
|
92
|
+
setProps(props: Record<string, unknown>): Promise<void>;
|
|
93
|
+
setData(data: Record<string, unknown>): Promise<void>;
|
|
94
|
+
/** Binds resource `name` to `files`, or back to its default with none. */
|
|
95
|
+
setResource(name: string, files: Source[] | undefined): Promise<void>;
|
|
96
|
+
on<K extends keyof Events>(type: K, listener: (value: Events[K]) => void): () => void;
|
|
97
|
+
/** Stops the player and lets its GPU device go. */
|
|
98
|
+
close(): Promise<void>;
|
|
99
|
+
}
|