@aelionsdk/sdk 0.1.0-beta.1

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.
Files changed (60) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +63 -0
  3. package/dist/audio-controller.d.ts +25 -0
  4. package/dist/audio-controller.d.ts.map +1 -0
  5. package/dist/audio-controller.js +235 -0
  6. package/dist/audio-mastering.d.ts +27 -0
  7. package/dist/audio-mastering.d.ts.map +1 -0
  8. package/dist/audio-mastering.js +160 -0
  9. package/dist/composition.d.ts +75 -0
  10. package/dist/composition.d.ts.map +1 -0
  11. package/dist/composition.js +146 -0
  12. package/dist/default-schemas.d.ts +7 -0
  13. package/dist/default-schemas.d.ts.map +1 -0
  14. package/dist/default-schemas.js +18 -0
  15. package/dist/export-job.d.ts +22 -0
  16. package/dist/export-job.d.ts.map +1 -0
  17. package/dist/export-job.js +126 -0
  18. package/dist/extension-host.d.ts +90 -0
  19. package/dist/extension-host.d.ts.map +1 -0
  20. package/dist/extension-host.js +367 -0
  21. package/dist/index.d.ts +18 -0
  22. package/dist/index.d.ts.map +1 -0
  23. package/dist/index.js +19 -0
  24. package/dist/media-provider.d.ts +49 -0
  25. package/dist/media-provider.d.ts.map +1 -0
  26. package/dist/media-provider.js +388 -0
  27. package/dist/migration-materials.d.ts +10 -0
  28. package/dist/migration-materials.d.ts.map +1 -0
  29. package/dist/migration-materials.js +148 -0
  30. package/dist/migration.d.ts +109 -0
  31. package/dist/migration.d.ts.map +1 -0
  32. package/dist/migration.js +1232 -0
  33. package/dist/persistence.d.ts +73 -0
  34. package/dist/persistence.d.ts.map +1 -0
  35. package/dist/persistence.js +245 -0
  36. package/dist/player.d.ts +22 -0
  37. package/dist/player.d.ts.map +1 -0
  38. package/dist/player.js +522 -0
  39. package/dist/preview-controller.d.ts +62 -0
  40. package/dist/preview-controller.d.ts.map +1 -0
  41. package/dist/preview-controller.js +377 -0
  42. package/dist/preview-quality.d.ts +7 -0
  43. package/dist/preview-quality.d.ts.map +1 -0
  44. package/dist/preview-quality.js +8 -0
  45. package/dist/production-media-provider.d.ts +112 -0
  46. package/dist/production-media-provider.d.ts.map +1 -0
  47. package/dist/production-media-provider.js +749 -0
  48. package/dist/project-builder.d.ts +249 -0
  49. package/dist/project-builder.d.ts.map +1 -0
  50. package/dist/project-builder.js +953 -0
  51. package/dist/runtime-material-registry.d.ts +10 -0
  52. package/dist/runtime-material-registry.d.ts.map +1 -0
  53. package/dist/runtime-material-registry.js +23 -0
  54. package/dist/session.d.ts +29 -0
  55. package/dist/session.d.ts.map +1 -0
  56. package/dist/session.js +1114 -0
  57. package/dist/types.d.ts +392 -0
  58. package/dist/types.d.ts.map +1 -0
  59. package/dist/types.js +1 -0
  60. package/package.json +60 -0
package/dist/player.js ADDED
@@ -0,0 +1,522 @@
1
+ import { AudioDrivenVideoScheduler, AudioWorkletClock, TransferableAudioWorkletClock, renderIrAudio, } from '@aelionsdk/audio';
2
+ import { sampleIndexAtTime } from '@aelionsdk/core';
3
+ import { normalizePreviewQuality } from './preview-quality.js';
4
+ function runtimeErrorCode(error) {
5
+ if (error !== null && typeof error === 'object') {
6
+ const diagnostics = Reflect.get(error, 'diagnostics');
7
+ if (Array.isArray(diagnostics)) {
8
+ const first = diagnostics[0];
9
+ const code = first !== null && typeof first === 'object' ? Reflect.get(first, 'code') : undefined;
10
+ if (typeof code === 'string')
11
+ return code;
12
+ }
13
+ const code = Reflect.get(error, 'code');
14
+ if (typeof code === 'string')
15
+ return code;
16
+ }
17
+ return 'PLAYER_RUNTIME_FAILED';
18
+ }
19
+ export class AelionPlayer {
20
+ #session;
21
+ #onRuntimeError;
22
+ #runtimeAssets;
23
+ #listeners = new Set();
24
+ #state = 'idle';
25
+ #clock;
26
+ #scheduler;
27
+ #fillHandle;
28
+ #nextAudioFrame = 0;
29
+ #fillTask;
30
+ #fillGeneration = -1;
31
+ #runtimeInitTask;
32
+ #runtimeDisposeTask;
33
+ #disposeTask;
34
+ #fillController = new AbortController();
35
+ #videoController = new AbortController();
36
+ #generation = 0;
37
+ #lastTimeUs = 0;
38
+ #renderedFrames = 0;
39
+ #droppedFrames = 0;
40
+ #errors = 0;
41
+ #lastErrorCode;
42
+ #previewQuality = normalizePreviewQuality();
43
+ #lastDisposedRuntime = null;
44
+ constructor(session, onRuntimeError, runtimeAssets) {
45
+ this.#session = session;
46
+ this.#onRuntimeError = onRuntimeError;
47
+ this.#runtimeAssets = runtimeAssets;
48
+ }
49
+ get state() {
50
+ return this.#state;
51
+ }
52
+ get currentTimeUs() {
53
+ if (this.#state === 'ended' || this.#state === 'error' || this.#state === 'disposed') {
54
+ return this.#lastTimeUs;
55
+ }
56
+ return this.#clock?.nowUs() ?? this.#lastTimeUs;
57
+ }
58
+ async play() {
59
+ try {
60
+ if (this.#state === 'disposed')
61
+ throw new ReferenceError('AelionPlayer is disposed');
62
+ const ir = this.#session.requireIr();
63
+ const generation = this.#generation;
64
+ await this.#ensureRuntime();
65
+ if (!this.#playContinuationCurrent(generation)) {
66
+ throw new DOMException('Player play became stale', 'AbortError');
67
+ }
68
+ await this.#requestAudioFill(true);
69
+ if (!this.#playContinuationCurrent(generation)) {
70
+ throw new DOMException('Player play became stale', 'AbortError');
71
+ }
72
+ if (this.#state === 'paused')
73
+ await this.#clock?.resume();
74
+ else
75
+ await this.#clock?.start();
76
+ if (!this.#playContinuationCurrent(generation)) {
77
+ throw new DOMException('Player play became stale', 'AbortError');
78
+ }
79
+ this.#state = 'playing';
80
+ this.#session.notifyStatsChanged();
81
+ this.#scheduler?.start();
82
+ this.#fillHandle ??= globalThis.setInterval(() => {
83
+ void this.#requestAudioFill(false);
84
+ }, 20);
85
+ if (this.currentTimeUs >= ir.durationUs)
86
+ await this.seek(0);
87
+ }
88
+ catch (error) {
89
+ this.#failRuntime(error);
90
+ throw error;
91
+ }
92
+ }
93
+ async pause() {
94
+ if (this.#state === 'disposed')
95
+ throw new ReferenceError('AelionPlayer is disposed');
96
+ const generation = this.#generation;
97
+ this.#scheduler?.pause();
98
+ if (this.#clock !== undefined)
99
+ await this.#clock.pause();
100
+ if (this.#disposeTask !== undefined || generation !== this.#generation)
101
+ return;
102
+ if (this.#state !== 'idle')
103
+ this.#state = 'paused';
104
+ this.#lastTimeUs = this.currentTimeUs;
105
+ this.#session.notifyStatsChanged();
106
+ }
107
+ async seek(timeUs) {
108
+ try {
109
+ const ir = this.#session.requireIr();
110
+ if (!Number.isSafeInteger(timeUs) || timeUs < 0 || timeUs >= ir.durationUs) {
111
+ throw new RangeError('Player seek target is outside the sequence duration');
112
+ }
113
+ await this.#ensureRuntime();
114
+ this.#advanceGeneration();
115
+ const generation = this.#generation;
116
+ const clock = this.#clock;
117
+ if (clock instanceof AudioWorkletClock)
118
+ clock.resetForSeek(timeUs);
119
+ else
120
+ clock?.seek(timeUs);
121
+ this.#nextAudioFrame = sampleIndexAtTime(timeUs, ir.sampleRate);
122
+ this.#lastTimeUs = timeUs;
123
+ await this.#requestAudioFill(true);
124
+ if (generation !== this.#generation)
125
+ return;
126
+ const signal = this.#videoController.signal;
127
+ if (signal.aborted)
128
+ return;
129
+ const result = await this.#session.preview.renderFrame({
130
+ timeUs,
131
+ signal,
132
+ ...this.#previewQuality,
133
+ });
134
+ if (generation !== this.#generation) {
135
+ result.bitmap.close();
136
+ return;
137
+ }
138
+ this.#publish({ generation, frameIndex: -1, timestampUs: timeUs, droppedFrames: 0 }, result);
139
+ }
140
+ catch (error) {
141
+ this.#failRuntime(error);
142
+ throw error;
143
+ }
144
+ }
145
+ scrub(timeUs) {
146
+ return this.#session.preview.renderFrame({ timeUs, ...this.#previewQuality });
147
+ }
148
+ setPreviewQuality(options) {
149
+ if (this.#state === 'disposed')
150
+ throw new ReferenceError('AelionPlayer is disposed');
151
+ this.#previewQuality = normalizePreviewQuality(options);
152
+ this.#advanceGeneration();
153
+ this.#session.notifyStatsChanged();
154
+ }
155
+ getStats() {
156
+ return Object.freeze({
157
+ state: this.#state,
158
+ currentTimeUs: this.currentTimeUs,
159
+ generation: this.#generation,
160
+ renderedFrames: this.#renderedFrames,
161
+ droppedFrames: this.#droppedFrames,
162
+ errors: this.#errors,
163
+ lastErrorCode: this.#lastErrorCode ?? null,
164
+ previewQuality: this.#previewQuality,
165
+ resources: this.#resourceStats(),
166
+ });
167
+ }
168
+ #resourceStats() {
169
+ const scheduler = this.#scheduler?.snapshot();
170
+ const clock = this.#clock;
171
+ const queue = clock instanceof AudioWorkletClock
172
+ ? clock.ring.snapshot()
173
+ : clock instanceof TransferableAudioWorkletClock
174
+ ? clock.snapshot()
175
+ : undefined;
176
+ return Object.freeze({
177
+ listeners: this.#listeners.size,
178
+ runtimeInitializing: this.#runtimeInitTask !== undefined,
179
+ audioFillScheduled: this.#fillHandle !== undefined,
180
+ audioFillInFlight: this.#fillTask !== undefined,
181
+ scheduler: Object.freeze({
182
+ present: scheduler !== undefined,
183
+ disposed: scheduler?.disposed ?? true,
184
+ scheduled: scheduler?.scheduled ?? false,
185
+ rendering: scheduler?.rendering ?? false,
186
+ }),
187
+ audio: Object.freeze({
188
+ mode: clock instanceof AudioWorkletClock
189
+ ? 'shared-ring'
190
+ : clock instanceof TransferableAudioWorkletClock
191
+ ? 'transferable-queue'
192
+ : 'none',
193
+ disposed: clock?.disposed ?? true,
194
+ contextState: clock?.context.state ?? null,
195
+ bufferedFrames: queue === undefined
196
+ ? 0
197
+ : 'availableReadFrames' in queue
198
+ ? queue.availableReadFrames
199
+ : queue.queuedFrames,
200
+ closed: queue === undefined ? true : 'state' in queue ? queue.state === 'closed' : queue.closed,
201
+ }),
202
+ lastDisposedRuntime: this.#lastDisposedRuntime,
203
+ });
204
+ }
205
+ subscribe(listener) {
206
+ if (this.#state === 'disposed')
207
+ throw new ReferenceError('AelionPlayer is disposed');
208
+ if (this.#listeners.size > 0) {
209
+ throw new Error('AelionPlayer supports one frame owner; unsubscribe before replacing it');
210
+ }
211
+ this.#listeners.add(listener);
212
+ return () => this.#listeners.delete(listener);
213
+ }
214
+ invalidate(changeSet) {
215
+ void changeSet;
216
+ const ir = this.#session.requireIr();
217
+ const timeUs = Math.min(Math.max(0, this.currentTimeUs), ir.durationUs - 1);
218
+ this.#advanceGeneration();
219
+ const clock = this.#clock;
220
+ if (clock instanceof AudioWorkletClock)
221
+ clock.resetForSeek(timeUs);
222
+ else
223
+ clock?.seek(timeUs);
224
+ this.#nextAudioFrame = sampleIndexAtTime(timeUs, ir.sampleRate);
225
+ this.#lastTimeUs = timeUs;
226
+ void this.#requestAudioFill(false);
227
+ }
228
+ async reset() {
229
+ if (this.#state === 'disposed')
230
+ return;
231
+ this.#advanceGeneration();
232
+ const generation = this.#generation;
233
+ await this.#disposeRuntime();
234
+ // The awaited runtime teardown permits a concurrent dispose() even though
235
+ // TypeScript narrows the pre-await state check.
236
+ if (this.#disposeTask !== undefined || generation !== this.#generation)
237
+ return;
238
+ this.#state = 'idle';
239
+ this.#lastTimeUs = 0;
240
+ this.#nextAudioFrame = 0;
241
+ }
242
+ dispose() {
243
+ this.#disposeTask ??= this.#dispose();
244
+ return this.#disposeTask;
245
+ }
246
+ async #dispose() {
247
+ if (this.#state === 'disposed')
248
+ return;
249
+ this.#advanceGeneration();
250
+ // Publish the terminal state before the first await so concurrent reset or
251
+ // runtime initialization cannot revive the Player while Session disposal runs.
252
+ this.#state = 'disposed';
253
+ this.#listeners.clear();
254
+ await this.#disposeRuntime();
255
+ }
256
+ async #ensureRuntime() {
257
+ if (this.#scheduler !== undefined)
258
+ return;
259
+ const existing = this.#runtimeInitTask;
260
+ if (existing !== undefined)
261
+ return existing;
262
+ const task = this.#initializeRuntime().finally(() => {
263
+ if (this.#runtimeInitTask === task)
264
+ this.#runtimeInitTask = undefined;
265
+ });
266
+ this.#runtimeInitTask = task;
267
+ return task;
268
+ }
269
+ async #initializeRuntime() {
270
+ const ir = this.#session.requireIr();
271
+ const generation = this.#generation;
272
+ const clock = globalThis.crossOriginIsolated && typeof SharedArrayBuffer === 'function'
273
+ ? new AudioWorkletClock({
274
+ capacityFrames: ir.sampleRate * 2,
275
+ channelCount: ir.channelLayout === 'mono' ? 1 : 2,
276
+ sampleRate: ir.sampleRate,
277
+ ...(this.#runtimeAssets?.sharedAudioWorklet === undefined
278
+ ? {}
279
+ : { moduleUrl: this.#runtimeAssets.sharedAudioWorklet }),
280
+ })
281
+ : new TransferableAudioWorkletClock({
282
+ capacityFrames: ir.sampleRate * 2,
283
+ channelCount: ir.channelLayout === 'mono' ? 1 : 2,
284
+ sampleRate: ir.sampleRate,
285
+ ...(this.#runtimeAssets?.transferableAudioWorklet === undefined
286
+ ? {}
287
+ : { moduleUrl: this.#runtimeAssets.transferableAudioWorklet }),
288
+ });
289
+ this.#clock = clock;
290
+ if (clock.context.sampleRate !== ir.sampleRate) {
291
+ const actual = clock.context.sampleRate;
292
+ await clock.dispose();
293
+ if (this.#clock === clock)
294
+ this.#clock = undefined;
295
+ throw new Error(`AudioContext sample rate ${actual.toString()} does not match Project ${ir.sampleRate.toString()}`);
296
+ }
297
+ try {
298
+ await clock.initialize(1_024);
299
+ }
300
+ catch (error) {
301
+ await clock.dispose().catch(() => undefined);
302
+ if (this.#clock === clock)
303
+ this.#clock = undefined;
304
+ throw error;
305
+ }
306
+ if (this.#clock !== clock || generation !== this.#generation || this.#state === 'disposed') {
307
+ await clock.dispose();
308
+ if (this.#clock === clock)
309
+ this.#clock = undefined;
310
+ throw new DOMException('Player runtime initialization became stale', 'AbortError');
311
+ }
312
+ this.#scheduler = new AudioDrivenVideoScheduler({
313
+ clock,
314
+ frameRate: ir.frameRate,
315
+ durationUs: ir.durationUs,
316
+ onFrame: scheduled => this.#renderScheduled(scheduled),
317
+ onError: error => this.#failRuntime(error),
318
+ onEnd: () => {
319
+ this.#state = 'ended';
320
+ this.#lastTimeUs = ir.durationUs;
321
+ if (this.#fillHandle !== undefined) {
322
+ globalThis.clearInterval(this.#fillHandle);
323
+ this.#fillHandle = undefined;
324
+ }
325
+ void this.#clock?.pause().catch((error) => this.#failRuntime(error));
326
+ this.#session.notifyStatsChanged();
327
+ },
328
+ });
329
+ }
330
+ #requestAudioFill(observeFailure) {
331
+ const existing = this.#fillTask;
332
+ if (existing !== undefined) {
333
+ if (this.#fillGeneration === this.#generation)
334
+ return existing;
335
+ const next = existing.then(() => this.#requestAudioFill(observeFailure), () => this.#requestAudioFill(observeFailure));
336
+ if (!observeFailure)
337
+ void next.catch((error) => this.#failRuntime(error));
338
+ return next;
339
+ }
340
+ const clock = this.#clock;
341
+ if (clock === undefined)
342
+ return Promise.resolve();
343
+ const generation = this.#generation;
344
+ const signal = this.#fillController.signal;
345
+ const task = this.#fillAudio(clock, generation, signal).finally(() => {
346
+ if (this.#fillTask === task)
347
+ this.#fillTask = undefined;
348
+ });
349
+ this.#fillTask = task;
350
+ this.#fillGeneration = generation;
351
+ if (!observeFailure)
352
+ void task.catch((error) => this.#failRuntime(error));
353
+ return task;
354
+ }
355
+ async #fillAudio(clock, generation, signal) {
356
+ const ir = this.#session.requireIr();
357
+ const channelCount = ir.channelLayout === 'mono' ? 1 : 2;
358
+ const totalFrames = Math.floor((ir.durationUs * ir.sampleRate) / 1_000_000);
359
+ while (this.#nextAudioFrame < totalFrames) {
360
+ if (!this.#runtimeCurrent(clock, generation, signal))
361
+ return;
362
+ const snapshot = clock instanceof AudioWorkletClock ? clock.ring.snapshot() : clock.snapshot();
363
+ const startFrame = this.#nextAudioFrame;
364
+ const frameCount = Math.min(4_096, snapshot.availableWriteFrames, totalFrames - startFrame);
365
+ if (frameCount <= 0)
366
+ break;
367
+ const pcm = await renderIrAudio({
368
+ ir,
369
+ startFrame,
370
+ frameCount,
371
+ channelCount,
372
+ source: this.#session.requireMedia(),
373
+ signal,
374
+ });
375
+ if (!this.#runtimeCurrent(clock, generation, signal))
376
+ return;
377
+ const accepted = clock instanceof AudioWorkletClock
378
+ ? clock.ring.writeInterleaved(pcm) === frameCount
379
+ : clock.enqueueInterleaved(pcm);
380
+ if (!accepted)
381
+ break;
382
+ if (this.#nextAudioFrame !== startFrame)
383
+ return;
384
+ this.#nextAudioFrame = startFrame + frameCount;
385
+ }
386
+ }
387
+ async #renderScheduled(scheduled) {
388
+ const signal = this.#videoController.signal;
389
+ const result = await this.#session.preview.renderFrame({
390
+ timeUs: scheduled.timestampUs,
391
+ signal,
392
+ ...this.#previewQuality,
393
+ });
394
+ if (signal.aborted || scheduled.generation !== this.#scheduler?.generation) {
395
+ result.bitmap.close();
396
+ return;
397
+ }
398
+ this.#publish(scheduled, result);
399
+ }
400
+ #publish(scheduled, result) {
401
+ this.#lastTimeUs = scheduled.timestampUs;
402
+ this.#renderedFrames += 1;
403
+ this.#droppedFrames += scheduled.droppedFrames;
404
+ this.#session.notifyStatsChanged();
405
+ if (this.#listeners.size === 0) {
406
+ result.bitmap.close();
407
+ return;
408
+ }
409
+ const event = { ...scheduled, result };
410
+ for (const listener of this.#listeners) {
411
+ try {
412
+ listener(event);
413
+ }
414
+ catch (error) {
415
+ result.bitmap.close();
416
+ this.#failRuntime(error);
417
+ return;
418
+ }
419
+ }
420
+ }
421
+ #disposeRuntime() {
422
+ const existing = this.#runtimeDisposeTask;
423
+ if (existing !== undefined)
424
+ return existing;
425
+ const task = this.#disposeRuntimeOnce().finally(() => {
426
+ if (this.#runtimeDisposeTask === task)
427
+ this.#runtimeDisposeTask = undefined;
428
+ });
429
+ this.#runtimeDisposeTask = task;
430
+ return task;
431
+ }
432
+ async #disposeRuntimeOnce() {
433
+ if (this.#fillHandle !== undefined) {
434
+ globalThis.clearInterval(this.#fillHandle);
435
+ this.#fillHandle = undefined;
436
+ }
437
+ const scheduler = this.#scheduler;
438
+ const initializingClock = this.#clock;
439
+ scheduler?.dispose();
440
+ await this.#runtimeInitTask?.catch(() => undefined);
441
+ const clock = this.#clock ?? initializingClock;
442
+ this.#clock = undefined;
443
+ await clock?.dispose();
444
+ await this.#fillTask?.catch(() => undefined);
445
+ this.#fillTask = undefined;
446
+ this.#fillGeneration = -1;
447
+ this.#scheduler = undefined;
448
+ if (scheduler !== undefined || clock !== undefined) {
449
+ const transport = clock instanceof AudioWorkletClock
450
+ ? clock.ring.snapshot()
451
+ : clock instanceof TransferableAudioWorkletClock
452
+ ? clock.snapshot()
453
+ : undefined;
454
+ this.#lastDisposedRuntime = Object.freeze({
455
+ schedulerDisposed: scheduler?.snapshot().disposed ?? true,
456
+ audioDisposed: clock?.disposed ?? true,
457
+ audioContextClosed: clock === undefined || !clock.ownsContext || clock.context.state === 'closed',
458
+ transportClosed: transport === undefined
459
+ ? true
460
+ : 'state' in transport
461
+ ? transport.state === 'closed'
462
+ : transport.closed,
463
+ bufferedFrames: transport === undefined
464
+ ? 0
465
+ : 'availableReadFrames' in transport
466
+ ? transport.availableReadFrames
467
+ : transport.queuedFrames,
468
+ });
469
+ }
470
+ }
471
+ #advanceGeneration() {
472
+ this.#generation += 1;
473
+ this.#scheduler?.seek();
474
+ this.#fillController.abort(new DOMException('Player generation changed', 'AbortError'));
475
+ this.#videoController.abort(new DOMException('Player generation changed', 'AbortError'));
476
+ this.#fillController = new AbortController();
477
+ this.#videoController = new AbortController();
478
+ }
479
+ #runtimeCurrent(clock, generation, signal) {
480
+ return (!signal.aborted &&
481
+ this.#state !== 'disposed' &&
482
+ this.#clock === clock &&
483
+ this.#generation === generation);
484
+ }
485
+ #playContinuationCurrent(generation) {
486
+ return this.#disposeTask === undefined && generation === this.#generation;
487
+ }
488
+ #failRuntime(error) {
489
+ if (this.#state === 'disposed')
490
+ return;
491
+ if (error instanceof DOMException && error.name === 'AbortError')
492
+ return;
493
+ if (error !== null && typeof error === 'object') {
494
+ const diagnostics = Reflect.get(error, 'diagnostics');
495
+ if (Array.isArray(diagnostics) &&
496
+ diagnostics.some(value => value !== null &&
497
+ typeof value === 'object' &&
498
+ Reflect.get(value, 'code') === 'OPERATION_ABORTED')) {
499
+ return;
500
+ }
501
+ }
502
+ const failureTimeUs = this.currentTimeUs;
503
+ this.#errors += 1;
504
+ this.#lastErrorCode = runtimeErrorCode(error);
505
+ this.#state = 'error';
506
+ this.#lastTimeUs = failureTimeUs;
507
+ this.#scheduler?.pause();
508
+ if (this.#fillHandle !== undefined) {
509
+ globalThis.clearInterval(this.#fillHandle);
510
+ this.#fillHandle = undefined;
511
+ }
512
+ this.#fillController.abort(error);
513
+ void this.#clock?.pause().catch(() => undefined);
514
+ try {
515
+ this.#onRuntimeError?.(error);
516
+ }
517
+ catch {
518
+ // Error reporting cannot create a second unhandled runtime failure.
519
+ }
520
+ this.#session.notifyStatsChanged();
521
+ }
522
+ }
@@ -0,0 +1,62 @@
1
+ import type { AelionSessionApi } from './types.js';
2
+ export type PreviewCanvasQuality = 'adaptive' | 'draft' | 'full';
3
+ export interface PreviewCanvasControllerOptions {
4
+ readonly quality?: PreviewCanvasQuality;
5
+ readonly renderScale?: number;
6
+ readonly fit?: 'contain' | 'cover' | 'fill';
7
+ readonly background?: string;
8
+ readonly pixelRatio?: number;
9
+ /** Own Player frames and draw them to the Canvas. Defaults to true. */
10
+ readonly subscribePlayer?: boolean;
11
+ /** Pause playback while the page is hidden and resume it when visible. */
12
+ readonly pauseWhenHidden?: boolean;
13
+ readonly renderOnResize?: boolean;
14
+ readonly targetFrameMs?: number;
15
+ readonly adaptiveScales?: readonly number[];
16
+ readonly onPointer?: (event: PreviewCanvasPointerEvent) => void;
17
+ readonly onFrame?: (frame: PreviewCanvasFrame) => void;
18
+ readonly onError?: (error: unknown) => void;
19
+ }
20
+ export interface PreviewCanvasPoint {
21
+ readonly x: number;
22
+ readonly y: number;
23
+ readonly inside: boolean;
24
+ }
25
+ export interface PreviewCanvasPointerEvent {
26
+ readonly type: 'down' | 'move' | 'up' | 'cancel';
27
+ readonly point: PreviewCanvasPoint;
28
+ readonly pointerId: number;
29
+ readonly buttons: number;
30
+ readonly originalEvent: PointerEvent;
31
+ }
32
+ export interface PreviewCanvasFrame {
33
+ readonly timeUs: number;
34
+ readonly width: number;
35
+ readonly height: number;
36
+ readonly renderScale: number;
37
+ readonly backend: 'webgpu' | 'webgl2';
38
+ }
39
+ export interface PreviewCanvasControllerSnapshot {
40
+ readonly disposed: boolean;
41
+ readonly pending: boolean;
42
+ readonly generation: number;
43
+ readonly currentTimeUs: number | null;
44
+ readonly quality: PreviewCanvasQuality;
45
+ readonly renderScale: number;
46
+ readonly renderedFrames: number;
47
+ readonly cancelledFrames: number;
48
+ readonly failedFrames: number;
49
+ readonly canvasWidth: number;
50
+ readonly canvasHeight: number;
51
+ }
52
+ export interface PreviewCanvasController {
53
+ render(timeUs: number): Promise<void>;
54
+ setQuality(quality: PreviewCanvasQuality, renderScale?: number): void;
55
+ resize(): void;
56
+ toProjectPoint(clientX: number, clientY: number): PreviewCanvasPoint;
57
+ captureStream(frameRate?: number): MediaStream;
58
+ snapshot(): PreviewCanvasControllerSnapshot;
59
+ dispose(): void;
60
+ }
61
+ export declare function attachPreviewCanvas(session: AelionSessionApi, canvas: HTMLCanvasElement, options?: PreviewCanvasControllerOptions): PreviewCanvasController;
62
+ //# sourceMappingURL=preview-controller.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"preview-controller.d.ts","sourceRoot":"","sources":["../src/preview-controller.ts"],"names":[],"mappings":"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"}