@autoark-ai/eva-client-sdk-ts 0.0.2-dev

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.
@@ -0,0 +1,939 @@
1
+ // src/errors/errors.ts
2
+ var EvaSdkError = class extends Error {
3
+ fatal;
4
+ source = "sdk";
5
+ constructor(message, options = {}) {
6
+ super(message, { cause: options.cause });
7
+ this.name = "EvaSdkError";
8
+ this.fatal = options.fatal ?? true;
9
+ }
10
+ };
11
+ function ensureError(value, options = {}) {
12
+ if (value instanceof EvaSdkError) {
13
+ return value;
14
+ }
15
+ return new EvaSdkError(options.message ?? "SDK operation failed", {
16
+ fatal: options.fatal ?? true,
17
+ cause: value
18
+ });
19
+ }
20
+
21
+ // src/core/media/passthrough-aec.ts
22
+ function createInstrumentedPassthroughAecProcessor() {
23
+ const stats = {
24
+ nearEndProcessed: 0,
25
+ farEndPushed: 0,
26
+ reset: 0,
27
+ released: 0
28
+ };
29
+ const farEndChunks = [];
30
+ const nearEndChunks = [];
31
+ return {
32
+ descriptor: {
33
+ id: "passthrough-aec"
34
+ },
35
+ stats,
36
+ farEndChunks,
37
+ nearEndChunks,
38
+ pushFarEnd(chunk) {
39
+ stats.farEndPushed += 1;
40
+ farEndChunks.push(chunk);
41
+ },
42
+ processNearEnd(chunk) {
43
+ stats.nearEndProcessed += 1;
44
+ nearEndChunks.push(chunk);
45
+ return chunk;
46
+ },
47
+ reset() {
48
+ stats.reset += 1;
49
+ },
50
+ release() {
51
+ stats.released += 1;
52
+ }
53
+ };
54
+ }
55
+
56
+ // src/facade/media/browser/streaming-pcm16.ts
57
+ var StreamingPcm16Encoder = class {
58
+ sourceRate;
59
+ frameSamples = 0;
60
+ pendingSamples = [];
61
+ push(channels, sourceSampleRate) {
62
+ if (!Number.isFinite(sourceSampleRate) || sourceSampleRate <= 0) {
63
+ throw new RangeError("sourceSampleRate must be finite and positive");
64
+ }
65
+ if (channels.length === 0) {
66
+ return [];
67
+ }
68
+ if (this.sourceRate !== void 0 && this.sourceRate !== sourceSampleRate) {
69
+ throw new RangeError("sourceSampleRate cannot change while encoding");
70
+ }
71
+ this.sourceRate ??= sourceSampleRate;
72
+ this.frameSamples ||= Math.round(sourceSampleRate / 50);
73
+ this.pendingSamples.push(...downmix(channels));
74
+ return this.takeCompleteFrames();
75
+ }
76
+ flush() {
77
+ const frames = this.takeCompleteFrames();
78
+ const tail = this.pendingSamples.length > 0 ? [encodePcm16(this.pendingSamples.splice(0))] : [];
79
+ this.sourceRate = void 0;
80
+ this.frameSamples = 0;
81
+ return [...frames, ...tail];
82
+ }
83
+ takeCompleteFrames() {
84
+ const frames = [];
85
+ while (this.frameSamples > 0 && this.pendingSamples.length >= this.frameSamples) {
86
+ frames.push(encodePcm16(this.pendingSamples.splice(0, this.frameSamples)));
87
+ }
88
+ return frames;
89
+ }
90
+ };
91
+ function downmix(channels) {
92
+ const length = channels[0].length;
93
+ if (channels.some((channel) => channel.length !== length)) {
94
+ throw new RangeError("All source channels must contain the same number of samples");
95
+ }
96
+ const mono = new Float32Array(length);
97
+ for (let index = 0; index < length; index += 1) {
98
+ let sum = 0;
99
+ for (const channel of channels) {
100
+ sum += channel[index];
101
+ }
102
+ mono[index] = sum / channels.length;
103
+ }
104
+ return mono;
105
+ }
106
+ function encodePcm16(samples) {
107
+ const output = new Uint8Array(samples.length * 2);
108
+ const view = new DataView(output.buffer, output.byteOffset, output.byteLength);
109
+ for (let index = 0; index < samples.length; index += 1) {
110
+ const sample = Math.max(-1, Math.min(1, samples[index] ?? 0));
111
+ const value = sample < 0 ? Math.round(sample * 32768) : Math.round(sample * 32767);
112
+ view.setInt16(index * 2, value, true);
113
+ }
114
+ return output;
115
+ }
116
+
117
+ // src/facade/media/browser/audio-input.ts
118
+ var WORKLET_NAME = "eva-audio-capture";
119
+ var DEFAULT_MAX_BUFFERED_CHUNKS = 128;
120
+ var WORKLET_SOURCE = `
121
+ class EvaAudioCaptureProcessor extends AudioWorkletProcessor {
122
+ process(inputs) {
123
+ const input = inputs[0];
124
+ if (input && input.length > 0) {
125
+ const channels = input.map((channel) => new Float32Array(channel));
126
+ this.port.postMessage(channels, channels.map((channel) => channel.buffer));
127
+ }
128
+ return true;
129
+ }
130
+ }
131
+ registerProcessor("${WORKLET_NAME}", EvaAudioCaptureProcessor);
132
+ `;
133
+ function createBrowserAudioInputSource(options = {}) {
134
+ return createBrowserAudioInputSourceWithDependencies(options);
135
+ }
136
+ function createBrowserAudioInputSourceWithDependencies(options = {}, dependencies = {}) {
137
+ return new DefaultBrowserAudioInputSource(options, dependencies);
138
+ }
139
+ var DefaultBrowserAudioInputSource = class {
140
+ constructor(options, dependencies) {
141
+ this.options = options;
142
+ this.dependencies = dependencies;
143
+ }
144
+ options;
145
+ dependencies;
146
+ generation = 0;
147
+ session;
148
+ async start() {
149
+ const session = this.session ?? this.createSession();
150
+ await session.startPromise;
151
+ }
152
+ frames(signal) {
153
+ const session = this.session;
154
+ if (session === void 0) {
155
+ return failedAudioChunks(
156
+ new EvaSdkError("Browser audio input is not started", { fatal: true })
157
+ );
158
+ }
159
+ if (isAborted(signal)) {
160
+ return this.consumeAbortedSession(session);
161
+ }
162
+ if (session.consumerActive) {
163
+ return failedAudioChunks(
164
+ new EvaSdkError("Browser audio input already has an active consumer", { fatal: true })
165
+ );
166
+ }
167
+ session.consumerActive = true;
168
+ return this.consumeSession(session, signal);
169
+ }
170
+ async *consumeAbortedSession(session) {
171
+ await this.teardown(session);
172
+ }
173
+ async *consumeSession(session, signal) {
174
+ let resolveAbort;
175
+ let abortStop;
176
+ const aborted = new Promise((resolve) => {
177
+ resolveAbort = resolve;
178
+ });
179
+ const abort = () => {
180
+ abortStop = this.teardown(session);
181
+ resolveAbort?.();
182
+ };
183
+ signal?.addEventListener("abort", abort, { once: true });
184
+ try {
185
+ await Promise.race([session.startPromise, aborted]);
186
+ if (session.failure !== void 0) {
187
+ throw session.failure;
188
+ }
189
+ if (isAborted(signal) || session.cancelled) {
190
+ return;
191
+ }
192
+ while (!isAborted(signal)) {
193
+ if (session.failure !== void 0) {
194
+ throw session.failure;
195
+ }
196
+ if (session.cancelled) {
197
+ return;
198
+ }
199
+ const next = await session.queue.next();
200
+ if (next.done) {
201
+ return;
202
+ }
203
+ yield next.value;
204
+ }
205
+ } finally {
206
+ signal?.removeEventListener("abort", abort);
207
+ session.consumerActive = false;
208
+ await (abortStop ?? this.teardown(session));
209
+ }
210
+ }
211
+ async stop() {
212
+ const session = this.session;
213
+ if (session !== void 0) {
214
+ await this.teardown(session);
215
+ }
216
+ }
217
+ createSession() {
218
+ let session;
219
+ let rejectCancellation;
220
+ const cancelled = new Promise((_resolve, reject) => {
221
+ rejectCancellation = reject;
222
+ });
223
+ const queue = new AsyncChunkQueue(
224
+ normalizeQueueLimit(this.options.maxBufferedChunks),
225
+ (error) => {
226
+ session.failure = error;
227
+ void this.teardown(session);
228
+ }
229
+ );
230
+ session = {
231
+ generation: ++this.generation,
232
+ queue,
233
+ encoder: new StreamingPcm16Encoder(),
234
+ startPromise: Promise.resolve(),
235
+ consumerActive: false,
236
+ cancelled: false,
237
+ rejectCancellation,
238
+ stream: void 0,
239
+ context: void 0,
240
+ sourceNode: void 0,
241
+ workletNode: void 0,
242
+ sampleRate: void 0,
243
+ ownedWorkletUrl: void 0
244
+ };
245
+ this.session = session;
246
+ session.startPromise = Promise.race([this.startSession(session), cancelled]);
247
+ void session.startPromise.catch(() => {
248
+ });
249
+ return session;
250
+ }
251
+ async startSession(session) {
252
+ try {
253
+ const stream = await mediaDevicesOf(this.dependencies).getUserMedia({
254
+ audio: {
255
+ autoGainControl: this.options.autoGainControl ?? true,
256
+ echoCancellation: this.options.echoCancellation ?? true,
257
+ noiseSuppression: this.options.noiseSuppression ?? true
258
+ }
259
+ });
260
+ if (!this.isCurrent(session)) {
261
+ stopTracks(stream);
262
+ throw stoppedError();
263
+ }
264
+ session.stream = stream;
265
+ const context = audioContextOf(this.dependencies);
266
+ session.context = context;
267
+ session.sampleRate = context.sampleRate;
268
+ this.throwIfNotCurrent(session);
269
+ const workletUrl = this.dependencies.workletModuleUrl ?? objectUrlOf(this.dependencies).create(WORKLET_SOURCE);
270
+ if (this.dependencies.workletModuleUrl === void 0) {
271
+ session.ownedWorkletUrl = workletUrl;
272
+ }
273
+ await context.audioWorklet.addModule(workletUrl);
274
+ this.throwIfNotCurrent(session);
275
+ const sourceNode = context.createMediaStreamSource(stream);
276
+ const workletNode = workletNodeOf(this.dependencies, context);
277
+ session.sourceNode = sourceNode;
278
+ session.workletNode = workletNode;
279
+ workletNode.port.onmessage = (event) => this.handleSamples(session, event.data);
280
+ sourceNode.connect(workletNode);
281
+ workletNode.connect(context.destination);
282
+ await context.resume();
283
+ this.throwIfNotCurrent(session);
284
+ } catch (error) {
285
+ const cancelled = session.cancelled || this.session !== session;
286
+ await this.teardown(session);
287
+ throw cancelled ? stoppedError() : ensureError(error, { message: "Browser audio input failed" });
288
+ }
289
+ }
290
+ throwIfNotCurrent(session) {
291
+ if (!this.isCurrent(session)) {
292
+ throw stoppedError();
293
+ }
294
+ }
295
+ isCurrent(session) {
296
+ return !session.cancelled && this.session === session;
297
+ }
298
+ handleSamples(session, value) {
299
+ if (!this.isCurrent(session) || !isFloat32Channels(value) || session.context === void 0 || session.sampleRate === void 0) {
300
+ return;
301
+ }
302
+ try {
303
+ if (session.context.sampleRate !== session.sampleRate) {
304
+ throw new RangeError("AudioContext sampleRate changed during capture");
305
+ }
306
+ for (const data of session.encoder.push(value, session.sampleRate)) {
307
+ session.queue.push({
308
+ data,
309
+ sampleRate: session.sampleRate,
310
+ channels: 1,
311
+ format: "pcm_s16le"
312
+ });
313
+ }
314
+ } catch (error) {
315
+ const failure = ensureError(error, { message: "Browser audio input encoding failed" });
316
+ session.failure = failure;
317
+ session.queue.fail(failure);
318
+ void this.teardown(session);
319
+ }
320
+ }
321
+ teardown(session) {
322
+ if (session.teardownPromise !== void 0) {
323
+ return session.teardownPromise;
324
+ }
325
+ session.cancelled = true;
326
+ session.rejectCancellation?.(stoppedError());
327
+ session.rejectCancellation = void 0;
328
+ session.queue.close();
329
+ if (this.session === session) {
330
+ this.session = void 0;
331
+ }
332
+ const workletNode = session.workletNode;
333
+ const sourceNode = session.sourceNode;
334
+ const stream = session.stream;
335
+ const context = session.context;
336
+ const ownedWorkletUrl = session.ownedWorkletUrl;
337
+ session.workletNode = void 0;
338
+ session.sourceNode = void 0;
339
+ session.stream = void 0;
340
+ session.context = void 0;
341
+ session.sampleRate = void 0;
342
+ session.ownedWorkletUrl = void 0;
343
+ session.teardownPromise = (async () => {
344
+ if (workletNode !== void 0) {
345
+ workletNode.port.onmessage = null;
346
+ workletNode.disconnect();
347
+ }
348
+ sourceNode?.disconnect();
349
+ if (stream !== void 0) {
350
+ stopTracks(stream);
351
+ }
352
+ await context?.close();
353
+ if (ownedWorkletUrl !== void 0) {
354
+ objectUrlOf(this.dependencies).revoke(ownedWorkletUrl);
355
+ }
356
+ })();
357
+ return session.teardownPromise;
358
+ }
359
+ };
360
+ function stoppedError() {
361
+ return new EvaSdkError("Browser audio input stopped", { fatal: true });
362
+ }
363
+ function isAborted(signal) {
364
+ return signal?.aborted === true;
365
+ }
366
+ function stopTracks(stream) {
367
+ for (const track of stream.getAudioTracks()) {
368
+ track.stop();
369
+ }
370
+ }
371
+ async function* failedAudioChunks(error) {
372
+ throw error;
373
+ }
374
+ var AsyncChunkQueue = class {
375
+ constructor(limit, onOverflow) {
376
+ this.limit = limit;
377
+ this.onOverflow = onOverflow;
378
+ }
379
+ limit;
380
+ onOverflow;
381
+ items = [];
382
+ waiters = [];
383
+ closed = false;
384
+ error;
385
+ push(chunk) {
386
+ if (this.closed || this.error !== void 0) {
387
+ return;
388
+ }
389
+ const waiter = this.waiters.shift();
390
+ if (waiter !== void 0) {
391
+ waiter.resolve({ done: false, value: chunk });
392
+ return;
393
+ }
394
+ if (this.items.length >= this.limit) {
395
+ const error = new EvaSdkError("Browser audio input buffer overflow", { fatal: true });
396
+ this.error = error;
397
+ this.items.length = 0;
398
+ for (const pending of this.waiters.splice(0)) {
399
+ pending.reject(this.error);
400
+ }
401
+ this.onOverflow(error);
402
+ return;
403
+ }
404
+ this.items.push(chunk);
405
+ }
406
+ fail(error) {
407
+ if (this.closed || this.error !== void 0) {
408
+ return;
409
+ }
410
+ this.error = error;
411
+ this.items.length = 0;
412
+ for (const waiter of this.waiters.splice(0)) {
413
+ waiter.reject(error);
414
+ }
415
+ }
416
+ next() {
417
+ if (this.error !== void 0) {
418
+ return Promise.reject(this.error);
419
+ }
420
+ const item = this.items.shift();
421
+ if (item !== void 0) {
422
+ return Promise.resolve({ done: false, value: item });
423
+ }
424
+ if (this.closed) {
425
+ return Promise.resolve({ done: true, value: void 0 });
426
+ }
427
+ return new Promise((resolve, reject) => this.waiters.push({ resolve, reject }));
428
+ }
429
+ close() {
430
+ if (this.closed) {
431
+ return;
432
+ }
433
+ this.closed = true;
434
+ if (this.error === void 0) {
435
+ for (const waiter of this.waiters.splice(0)) {
436
+ waiter.resolve({ done: true, value: void 0 });
437
+ }
438
+ }
439
+ }
440
+ };
441
+ function normalizeQueueLimit(value) {
442
+ if (value === void 0) {
443
+ return DEFAULT_MAX_BUFFERED_CHUNKS;
444
+ }
445
+ if (!Number.isInteger(value) || value <= 0) {
446
+ throw new EvaSdkError("Browser audio input maxBufferedChunks must be a positive integer", {
447
+ fatal: true
448
+ });
449
+ }
450
+ return value;
451
+ }
452
+ function isFloat32Channels(value) {
453
+ return Array.isArray(value) && value.length > 0 && value.every((channel) => channel instanceof Float32Array);
454
+ }
455
+ function mediaDevicesOf(dependencies) {
456
+ const mediaDevices = dependencies.mediaDevices ?? globalThis.navigator?.mediaDevices;
457
+ if (mediaDevices === void 0) {
458
+ throw new EvaSdkError("Browser media devices unavailable", { fatal: true });
459
+ }
460
+ return mediaDevices;
461
+ }
462
+ function audioContextOf(dependencies) {
463
+ if (dependencies.audioContextFactory !== void 0) {
464
+ return dependencies.audioContextFactory();
465
+ }
466
+ if (typeof AudioContext === "undefined") {
467
+ throw new EvaSdkError("Browser AudioContext unavailable", { fatal: true });
468
+ }
469
+ return new AudioContext({ latencyHint: "interactive" });
470
+ }
471
+ function workletNodeOf(dependencies, context) {
472
+ if (dependencies.audioWorkletNodeFactory !== void 0) {
473
+ return dependencies.audioWorkletNodeFactory(context, WORKLET_NAME);
474
+ }
475
+ return new AudioWorkletNode(context, WORKLET_NAME);
476
+ }
477
+ function objectUrlOf(dependencies) {
478
+ return dependencies.objectUrl ?? {
479
+ create(source) {
480
+ return URL.createObjectURL(new Blob([source], { type: "text/javascript" }));
481
+ },
482
+ revoke(url) {
483
+ URL.revokeObjectURL(url);
484
+ }
485
+ };
486
+ }
487
+
488
+ // src/facade/media/browser/audio-output.ts
489
+ function createBrowserAudioOutputSink() {
490
+ return createBrowserAudioOutputSinkWithDependencies();
491
+ }
492
+ function createBrowserAudioOutputSinkWithDependencies(dependencies = {}) {
493
+ return new DefaultBrowserAudioOutputSink(dependencies);
494
+ }
495
+ var DefaultBrowserAudioOutputSink = class {
496
+ constructor(dependencies) {
497
+ this.dependencies = dependencies;
498
+ }
499
+ dependencies;
500
+ context;
501
+ scheduledTime = 0;
502
+ active = /* @__PURE__ */ new Set();
503
+ drainWaiters = /* @__PURE__ */ new Set();
504
+ generation = 0;
505
+ stopped = false;
506
+ stopPromise;
507
+ async enqueue(chunk) {
508
+ if (this.stopped) {
509
+ throw new EvaSdkError("Browser audio output stopped", { fatal: true });
510
+ }
511
+ if (chunk.format !== "pcm_s16le") {
512
+ throw new EvaSdkError("Browser audio output requires pcm_s16le", { fatal: true });
513
+ }
514
+ const generation = this.generation;
515
+ let context;
516
+ try {
517
+ context = this.context ?? this.createContext();
518
+ await context.resume();
519
+ if (!this.isCurrent(generation, context)) {
520
+ return;
521
+ }
522
+ const samples = Math.floor(chunk.data.byteLength / 2 / chunk.channels);
523
+ if (samples === 0) {
524
+ return;
525
+ }
526
+ const buffer = context.createBuffer(1, samples, chunk.sampleRate);
527
+ decodePcm16Mono(chunk, buffer.getChannelData(0));
528
+ const source = context.createBufferSource();
529
+ source.buffer = buffer;
530
+ source.connect(context.destination);
531
+ source.onended = () => {
532
+ this.active.delete(source);
533
+ this.resolveDrainIfIdle();
534
+ };
535
+ const startTime = Math.max(context.currentTime, this.scheduledTime);
536
+ this.active.add(source);
537
+ try {
538
+ source.start(startTime);
539
+ this.scheduledTime = startTime + buffer.duration;
540
+ } catch (error) {
541
+ source.onended = null;
542
+ this.active.delete(source);
543
+ this.resolveDrainIfIdle();
544
+ throw error;
545
+ }
546
+ } catch (error) {
547
+ if (context !== void 0 && !this.isCurrent(generation, context)) {
548
+ return;
549
+ }
550
+ throw ensureError(error, { message: "Browser audio output failed" });
551
+ }
552
+ }
553
+ flush() {
554
+ this.generation += 1;
555
+ const context = this.context;
556
+ const sources = [...this.active];
557
+ this.active.clear();
558
+ let failure;
559
+ for (const source of sources) {
560
+ source.onended = null;
561
+ try {
562
+ source.stop(0);
563
+ } catch (error) {
564
+ failure ??= error;
565
+ }
566
+ }
567
+ this.scheduledTime = context?.currentTime ?? 0;
568
+ this.resolveDrainIfIdle();
569
+ if (failure !== void 0) {
570
+ throw ensureError(failure, { message: "Browser audio output flush failed" });
571
+ }
572
+ }
573
+ drain() {
574
+ if (this.active.size === 0) {
575
+ return Promise.resolve();
576
+ }
577
+ return new Promise((resolve) => this.drainWaiters.add(resolve));
578
+ }
579
+ stop() {
580
+ this.stopPromise ??= this.stopOnce();
581
+ return this.stopPromise;
582
+ }
583
+ createContext() {
584
+ if (this.dependencies.audioContextFactory !== void 0) {
585
+ this.context = this.dependencies.audioContextFactory();
586
+ return this.context;
587
+ }
588
+ if (typeof AudioContext === "undefined") {
589
+ throw new EvaSdkError("Browser AudioContext unavailable", { fatal: true });
590
+ }
591
+ this.context = new AudioContext({ latencyHint: "interactive" });
592
+ return this.context;
593
+ }
594
+ resolveDrainIfIdle() {
595
+ if (this.active.size !== 0) {
596
+ return;
597
+ }
598
+ for (const resolve of this.drainWaiters) {
599
+ resolve();
600
+ }
601
+ this.drainWaiters.clear();
602
+ }
603
+ isCurrent(generation, context) {
604
+ return !this.stopped && generation === this.generation && context === this.context;
605
+ }
606
+ async stopOnce() {
607
+ this.stopped = true;
608
+ let failure;
609
+ try {
610
+ this.flush();
611
+ } catch (error) {
612
+ failure = error;
613
+ }
614
+ const context = this.context;
615
+ this.context = void 0;
616
+ this.scheduledTime = 0;
617
+ try {
618
+ await context?.close();
619
+ } catch (error) {
620
+ failure ??= error;
621
+ }
622
+ if (failure !== void 0) {
623
+ throw ensureError(failure, { message: "Browser audio output stop failed" });
624
+ }
625
+ }
626
+ };
627
+ function decodePcm16Mono(chunk, output) {
628
+ const view = new DataView(chunk.data.buffer, chunk.data.byteOffset, chunk.data.byteLength);
629
+ for (let sampleIndex = 0; sampleIndex < output.length; sampleIndex += 1) {
630
+ let sum = 0;
631
+ for (let channel = 0; channel < chunk.channels; channel += 1) {
632
+ const offset = (sampleIndex * chunk.channels + channel) * 2;
633
+ sum += view.getInt16(offset, true) / 32768;
634
+ }
635
+ output[sampleIndex] = sum / chunk.channels;
636
+ }
637
+ }
638
+
639
+ // src/facade/media/browser/camera.ts
640
+ function createBrowserCameraSnapshotSource(options = {}) {
641
+ return createBrowserCameraSnapshotSourceWithDependencies(options);
642
+ }
643
+ function createBrowserCameraSnapshotSourceWithDependencies(options = {}, dependencies = {}) {
644
+ assertOptions(options);
645
+ return new DefaultBrowserCameraSnapshotSource(options, dependencies);
646
+ }
647
+ var DefaultBrowserCameraSnapshotSource = class {
648
+ constructor(options, dependencies) {
649
+ this.options = options;
650
+ this.dependencies = dependencies;
651
+ }
652
+ options;
653
+ dependencies;
654
+ generation = 0;
655
+ active;
656
+ pendingStart;
657
+ pendingCapture;
658
+ teardownPromise;
659
+ start(signal) {
660
+ if (signal.aborted) return Promise.reject(abortError());
661
+ if (this.active !== void 0 || this.pendingStart !== void 0 || this.teardownPromise !== void 0) {
662
+ return Promise.reject(new EvaSdkError("Browser camera session already exists", { fatal: true }));
663
+ }
664
+ const generation = ++this.generation;
665
+ const cancellation = cancellable();
666
+ const pending = {
667
+ generation,
668
+ cancelled: false,
669
+ cancel: cancellation.cancel,
670
+ cancellation: cancellation.promise,
671
+ underlying: Promise.resolve()
672
+ };
673
+ this.pendingStart = pending;
674
+ pending.underlying = this.acquire(pending).finally(() => {
675
+ if (this.pendingStart === pending) this.pendingStart = void 0;
676
+ });
677
+ void pending.underlying.catch(() => {
678
+ });
679
+ return raceWithSignalAndCancellation(
680
+ pending.underlying,
681
+ signal,
682
+ () => this.cancelStart(pending),
683
+ pending.cancellation
684
+ );
685
+ }
686
+ capture(signal) {
687
+ if (signal.aborted) return Promise.reject(abortError());
688
+ const active = this.active;
689
+ if (active === void 0) {
690
+ return Promise.reject(new EvaSdkError("Browser camera is not started", { fatal: true }));
691
+ }
692
+ if (this.pendingCapture !== void 0) {
693
+ return Promise.reject(new EvaSdkError("Browser camera capture is already in progress", {
694
+ fatal: true
695
+ }));
696
+ }
697
+ const cancellation = cancellable();
698
+ const pending = {
699
+ generation: active.generation,
700
+ cancelled: false,
701
+ cancel: cancellation.cancel,
702
+ cancellation: cancellation.promise,
703
+ underlying: Promise.resolve(snapshotPlaceholder())
704
+ };
705
+ this.pendingCapture = pending;
706
+ pending.underlying = this.captureFrame(active, pending).finally(() => {
707
+ if (this.pendingCapture === pending) this.pendingCapture = void 0;
708
+ });
709
+ void pending.underlying.catch(() => {
710
+ });
711
+ return raceWithSignalAndCancellation(
712
+ pending.underlying,
713
+ signal,
714
+ () => this.cancelCapture(pending),
715
+ pending.cancellation
716
+ );
717
+ }
718
+ stop() {
719
+ if (this.teardownPromise !== void 0) return this.teardownPromise;
720
+ const pendingStart = this.pendingStart;
721
+ const pendingCapture = this.pendingCapture;
722
+ const active = this.active;
723
+ this.generation += 1;
724
+ if (pendingStart !== void 0) this.cancelStart(pendingStart);
725
+ if (pendingCapture !== void 0) this.cancelCapture(pendingCapture);
726
+ this.active = void 0;
727
+ if (active !== void 0) releaseActive(active);
728
+ const teardown = (async () => {
729
+ await Promise.allSettled([
730
+ pendingStart?.underlying ?? Promise.resolve(),
731
+ pendingCapture?.underlying ?? Promise.resolve()
732
+ ]);
733
+ const lateActive = this.active;
734
+ if (lateActive !== void 0) {
735
+ this.active = void 0;
736
+ releaseActive(lateActive);
737
+ }
738
+ })();
739
+ this.teardownPromise = teardown.finally(() => {
740
+ if (this.teardownPromise === teardown || this.teardownPromise !== void 0) {
741
+ this.teardownPromise = void 0;
742
+ }
743
+ });
744
+ return this.teardownPromise;
745
+ }
746
+ async acquire(pending) {
747
+ const stream = await mediaDevicesOf2(this.dependencies).getUserMedia({
748
+ audio: false,
749
+ video: this.options.video ?? true
750
+ });
751
+ if (!this.isCurrentStart(pending)) {
752
+ stopVideoTracks(stream);
753
+ throw abortError();
754
+ }
755
+ const video = createVideoOf(this.dependencies);
756
+ video.srcObject = stream;
757
+ try {
758
+ await video.play();
759
+ await waitForVideoFrameOf(this.dependencies)(video);
760
+ if (!this.isCurrentStart(pending) || video.videoWidth <= 0 || video.videoHeight <= 0) {
761
+ throw abortError();
762
+ }
763
+ this.active = { generation: pending.generation, stream, video };
764
+ } catch (error) {
765
+ video.pause();
766
+ video.srcObject = null;
767
+ stopVideoTracks(stream);
768
+ throw error;
769
+ }
770
+ }
771
+ async captureFrame(active, pending) {
772
+ const width = active.video.videoWidth;
773
+ const height = active.video.videoHeight;
774
+ if (width <= 0 || height <= 0) {
775
+ throw new EvaSdkError("Browser camera frame is not ready", { fatal: true });
776
+ }
777
+ const canvas = createCanvasOf(this.dependencies);
778
+ canvas.width = width;
779
+ canvas.height = height;
780
+ canvas.drawImage(active.video, width, height);
781
+ const requestedMime = this.options.mimeType ?? "image/png";
782
+ const blob = await canvas.encode(
783
+ requestedMime,
784
+ requestedMime === "image/jpeg" ? this.options.jpegQuality : void 0
785
+ );
786
+ if (!this.isCurrentCapture(active, pending)) throw abortError();
787
+ const data = new Uint8Array(await blob.arrayBuffer());
788
+ if (!this.isCurrentCapture(active, pending)) throw abortError();
789
+ const mimeType = blob.type || requestedMime;
790
+ if (data.byteLength === 0 || mimeType !== "image/png" && mimeType !== "image/jpeg") {
791
+ throw new EvaSdkError("Browser camera produced invalid image data", { fatal: true });
792
+ }
793
+ return { data, mimeType, width, height };
794
+ }
795
+ cancelStart(pending) {
796
+ if (pending.cancelled) return;
797
+ pending.cancelled = true;
798
+ pending.cancel();
799
+ }
800
+ cancelCapture(pending) {
801
+ if (pending.cancelled) return;
802
+ pending.cancelled = true;
803
+ pending.cancel();
804
+ }
805
+ isCurrentStart(pending) {
806
+ return this.pendingStart === pending && !pending.cancelled && pending.generation === this.generation;
807
+ }
808
+ isCurrentCapture(active, pending) {
809
+ return this.active === active && this.pendingCapture === pending && !pending.cancelled && pending.generation === this.generation;
810
+ }
811
+ };
812
+ function assertOptions(options) {
813
+ if (options.jpegQuality !== void 0 && (!Number.isFinite(options.jpegQuality) || options.jpegQuality < 0 || options.jpegQuality > 1)) {
814
+ throw new EvaSdkError("Browser camera jpegQuality must be between 0 and 1", { fatal: true });
815
+ }
816
+ }
817
+ function mediaDevicesOf2(dependencies) {
818
+ const devices = dependencies.mediaDevices ?? navigator.mediaDevices;
819
+ if (devices === void 0) {
820
+ throw new EvaSdkError("Browser camera mediaDevices is unavailable", { fatal: true });
821
+ }
822
+ return devices;
823
+ }
824
+ function createVideoOf(dependencies) {
825
+ if (dependencies.createVideo !== void 0) return dependencies.createVideo();
826
+ const video = document.createElement("video");
827
+ video.autoplay = true;
828
+ video.muted = true;
829
+ video.playsInline = true;
830
+ return video;
831
+ }
832
+ function createCanvasOf(dependencies) {
833
+ if (dependencies.createCanvas !== void 0) return dependencies.createCanvas();
834
+ const canvas = document.createElement("canvas");
835
+ return {
836
+ get width() {
837
+ return canvas.width;
838
+ },
839
+ set width(value) {
840
+ canvas.width = value;
841
+ },
842
+ get height() {
843
+ return canvas.height;
844
+ },
845
+ set height(value) {
846
+ canvas.height = value;
847
+ },
848
+ drawImage(video, width, height) {
849
+ const context = canvas.getContext("2d");
850
+ if (context === null) {
851
+ throw new EvaSdkError("Browser camera canvas 2D context is unavailable", { fatal: true });
852
+ }
853
+ context.drawImage(video, 0, 0, width, height);
854
+ },
855
+ encode(type, quality) {
856
+ return new Promise((resolve, reject) => {
857
+ canvas.toBlob((blob) => {
858
+ if (blob === null) {
859
+ reject(new EvaSdkError("Browser camera image encoding failed", { fatal: true }));
860
+ } else {
861
+ resolve(blob);
862
+ }
863
+ }, type, quality);
864
+ });
865
+ }
866
+ };
867
+ }
868
+ function waitForVideoFrameOf(dependencies) {
869
+ if (dependencies.waitForVideoFrame !== void 0) return dependencies.waitForVideoFrame;
870
+ return async (video) => {
871
+ if (video.videoWidth > 0 && video.videoHeight > 0) return;
872
+ const element = video;
873
+ await new Promise((resolve, reject) => {
874
+ const ready = () => {
875
+ cleanup();
876
+ resolve();
877
+ };
878
+ const failed = () => {
879
+ cleanup();
880
+ reject(new EvaSdkError("Browser camera video failed", { fatal: true }));
881
+ };
882
+ const cleanup = () => {
883
+ element.removeEventListener("loadeddata", ready);
884
+ element.removeEventListener("canplay", ready);
885
+ element.removeEventListener("error", failed);
886
+ };
887
+ element.addEventListener("loadeddata", ready, { once: true });
888
+ element.addEventListener("canplay", ready, { once: true });
889
+ element.addEventListener("error", failed, { once: true });
890
+ });
891
+ };
892
+ }
893
+ function releaseActive(active) {
894
+ active.video.pause();
895
+ active.video.srcObject = null;
896
+ stopVideoTracks(active.stream);
897
+ }
898
+ function stopVideoTracks(stream) {
899
+ for (const track of stream.getVideoTracks()) track.stop();
900
+ }
901
+ function cancellable() {
902
+ let cancel;
903
+ const promise = new Promise((_resolve, reject) => {
904
+ cancel = () => reject(abortError());
905
+ });
906
+ void promise.catch(() => {
907
+ });
908
+ return { promise, cancel };
909
+ }
910
+ function raceWithSignalAndCancellation(work, signal, cancel, cancellation) {
911
+ return new Promise((resolve, reject) => {
912
+ const onAbort = () => {
913
+ cancel();
914
+ reject(abortError());
915
+ };
916
+ signal.addEventListener("abort", onAbort, { once: true });
917
+ Promise.race([work, cancellation]).then(resolve, reject).finally(() => {
918
+ signal.removeEventListener("abort", onAbort);
919
+ }).catch(() => {
920
+ });
921
+ });
922
+ }
923
+ function abortError() {
924
+ return new DOMException("Browser camera operation aborted", "AbortError");
925
+ }
926
+ function snapshotPlaceholder() {
927
+ return { data: new Uint8Array([0]), mimeType: "image/png", width: 1, height: 1 };
928
+ }
929
+
930
+ // src/facade/media/browser/index.ts
931
+ function createPassthroughAecProcessor() {
932
+ return createInstrumentedPassthroughAecProcessor();
933
+ }
934
+ export {
935
+ createBrowserAudioInputSource,
936
+ createBrowserAudioOutputSink,
937
+ createBrowserCameraSnapshotSource,
938
+ createPassthroughAecProcessor
939
+ };