@whereby.com/assistant-sdk 0.0.0-canary-20250903113745

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/LICENCE.md ADDED
@@ -0,0 +1,23 @@
1
+ ## MIT License
2
+
3
+ Copyright (c) 2024 Whereby AS (https://www.whereby.com)
4
+ Permission is hereby granted, free of charge, to any person
5
+ obtaining a copy of this software and associated documentation
6
+ files (the "Software"), to deal in the Software without
7
+ restriction, including without limitation the rights to use,
8
+ copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the
10
+ Software is furnished to do so, subject to the following
11
+ conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
18
+ OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
20
+ HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
21
+ WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
22
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
23
+ OTHER DEALINGS IN THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1 @@
1
+ # Assistant SDK
package/dist/index.cjs ADDED
@@ -0,0 +1,575 @@
1
+ 'use strict';
2
+
3
+ var core = require('@whereby.com/core');
4
+ var wrtc = require('@roamhq/wrtc');
5
+ var EventEmitter = require('events');
6
+ var child_process = require('child_process');
7
+ var stream = require('stream');
8
+ var express = require('express');
9
+ var assert = require('assert');
10
+ var bodyParser = require('body-parser');
11
+ var os = require('os');
12
+
13
+ const ASSISTANT_JOIN_SUCCESS = "ASSISTANT_JOIN_SUCCESS";
14
+
15
+ const AUDIO_STREAM_READY = "AUDIO_STREAM_READY";
16
+
17
+ /******************************************************************************
18
+ Copyright (c) Microsoft Corporation.
19
+
20
+ Permission to use, copy, modify, and/or distribute this software for any
21
+ purpose with or without fee is hereby granted.
22
+
23
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
24
+ REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
25
+ AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
26
+ INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
27
+ LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
28
+ OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
29
+ PERFORMANCE OF THIS SOFTWARE.
30
+ ***************************************************************************** */
31
+ /* global Reflect, Promise, SuppressedError, Symbol, Iterator */
32
+
33
+
34
+ function __awaiter(thisArg, _arguments, P, generator) {
35
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
36
+ return new (P || (P = Promise))(function (resolve, reject) {
37
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
38
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
39
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
40
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
41
+ });
42
+ }
43
+
44
+ typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
45
+ var e = new Error(message);
46
+ return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
47
+ };
48
+
49
+ const { nonstandard: { RTCAudioSink }, } = wrtc;
50
+ class AudioSource extends stream.PassThrough {
51
+ constructor() {
52
+ super({
53
+ allowHalfOpen: true,
54
+ highWaterMark: 1 * 1024,
55
+ });
56
+ }
57
+ }
58
+ class AudioSink extends wrtc.nonstandard.RTCAudioSink {
59
+ constructor(track) {
60
+ super(track);
61
+ this._sink = new RTCAudioSink(track);
62
+ }
63
+ subscribe(cb) {
64
+ this._sink.ondata = cb;
65
+ return () => {
66
+ this._sink.ondata = undefined;
67
+ };
68
+ }
69
+ }
70
+
71
+ const PARTICIPANT_SLOTS = 20;
72
+ const STREAM_INPUT_SAMPLE_RATE_IN_HZ = 48000;
73
+ const BYTES_PER_SAMPLE = 2;
74
+ const FRAME_10MS_SAMPLES = 480;
75
+ const slotBuffers = new Map();
76
+ function appendAndDrainTo480(slot, newSamples) {
77
+ var _a;
78
+ const prev = (_a = slotBuffers.get(slot)) !== null && _a !== void 0 ? _a : new Int16Array(0);
79
+ const merged = new Int16Array(prev.length + newSamples.length);
80
+ merged.set(prev, 0);
81
+ merged.set(newSamples, prev.length);
82
+ let offset = 0;
83
+ while (merged.length - offset >= FRAME_10MS_SAMPLES) {
84
+ const chunk = merged.subarray(offset, offset + FRAME_10MS_SAMPLES);
85
+ enqueueFrame(slot, chunk);
86
+ offset += FRAME_10MS_SAMPLES;
87
+ }
88
+ slotBuffers.set(slot, merged.subarray(offset));
89
+ }
90
+ ({
91
+ enqFrames: new Array(PARTICIPANT_SLOTS).fill(0),
92
+ enqSamples: new Array(PARTICIPANT_SLOTS).fill(0),
93
+ wroteFrames: new Array(PARTICIPANT_SLOTS).fill(0),
94
+ wroteSamples: new Array(PARTICIPANT_SLOTS).fill(0),
95
+ lastFramesSeen: new Array(PARTICIPANT_SLOTS).fill(0),
96
+ });
97
+ let slots = [];
98
+ let stopPacerFn = null;
99
+ let outputPacerState = null;
100
+ function resampleTo48kHz(inputSamples, inputSampleRate, inputFrames) {
101
+ const ratio = STREAM_INPUT_SAMPLE_RATE_IN_HZ / inputSampleRate;
102
+ const outputLength = Math.floor(inputFrames * ratio);
103
+ const output = new Int16Array(outputLength);
104
+ for (let i = 0; i < outputLength; i++) {
105
+ const inputIndex = i / ratio;
106
+ const index = Math.floor(inputIndex);
107
+ const fraction = inputIndex - index;
108
+ if (index + 1 < inputSamples.length) {
109
+ const sample1 = inputSamples[index];
110
+ const sample2 = inputSamples[index + 1];
111
+ output[i] = Math.round(sample1 + (sample2 - sample1) * fraction);
112
+ }
113
+ else {
114
+ output[i] = inputSamples[Math.min(index, inputSamples.length - 1)];
115
+ }
116
+ }
117
+ return output;
118
+ }
119
+ function enqueueOutputFrame(samples) {
120
+ if (outputPacerState) {
121
+ outputPacerState.frameQueue.push(samples);
122
+ }
123
+ }
124
+ function startPacer(ff, slotCount, rtcAudioSource, onAudioStreamReady) {
125
+ if (stopPacerFn) {
126
+ stopPacerFn();
127
+ stopPacerFn = null;
128
+ }
129
+ const writers = Array.from({ length: slotCount }, (_, i) => ff.stdio[3 + i]);
130
+ const nowMs = () => Number(process.hrtime.bigint()) / 1e6;
131
+ const outputFrameMs = (FRAME_10MS_SAMPLES / STREAM_INPUT_SAMPLE_RATE_IN_HZ) * 1000;
132
+ const t0 = nowMs();
133
+ slots = Array.from({ length: slotCount }, () => ({
134
+ q: [],
135
+ lastFrames: FRAME_10MS_SAMPLES,
136
+ nextDueMs: t0 + (FRAME_10MS_SAMPLES / STREAM_INPUT_SAMPLE_RATE_IN_HZ) * 1000,
137
+ }));
138
+ outputPacerState = {
139
+ frameQueue: [],
140
+ nextDueMs: t0 + outputFrameMs,
141
+ rtcAudioSource,
142
+ onAudioStreamReady,
143
+ didEmitReadyEvent: false,
144
+ };
145
+ const iv = setInterval(() => {
146
+ const t = nowMs();
147
+ for (let s = 0; s < slotCount; s++) {
148
+ const st = slots[s];
149
+ const w = writers[s];
150
+ const frameMs = (st.lastFrames / STREAM_INPUT_SAMPLE_RATE_IN_HZ) * 1000;
151
+ if (t >= st.nextDueMs) {
152
+ const buf = st.q.length ? st.q.shift() : Buffer.alloc(st.lastFrames * BYTES_PER_SAMPLE);
153
+ if (!w.write(buf)) {
154
+ const late = t - st.nextDueMs;
155
+ const steps = Math.max(1, Math.ceil(late / frameMs));
156
+ st.nextDueMs += steps * frameMs;
157
+ continue;
158
+ }
159
+ const late = t - st.nextDueMs;
160
+ const steps = Math.max(1, Math.ceil(late / frameMs));
161
+ st.nextDueMs += steps * frameMs;
162
+ }
163
+ }
164
+ if (!outputPacerState)
165
+ return;
166
+ const state = outputPacerState;
167
+ if (t >= state.nextDueMs) {
168
+ const samples = state.frameQueue.length > 0 ? state.frameQueue.shift() : new Int16Array(FRAME_10MS_SAMPLES);
169
+ if (!state.didEmitReadyEvent) {
170
+ state.onAudioStreamReady();
171
+ state.didEmitReadyEvent = true;
172
+ }
173
+ state.rtcAudioSource.onData({
174
+ samples: samples,
175
+ sampleRate: STREAM_INPUT_SAMPLE_RATE_IN_HZ,
176
+ });
177
+ const late = t - state.nextDueMs;
178
+ const steps = Math.max(1, Math.ceil(late / outputFrameMs));
179
+ state.nextDueMs += steps * outputFrameMs;
180
+ }
181
+ }, 5);
182
+ stopPacerFn = () => clearInterval(iv);
183
+ }
184
+ function stopPacer() {
185
+ if (stopPacerFn)
186
+ stopPacerFn();
187
+ stopPacerFn = null;
188
+ slots = [];
189
+ }
190
+ function enqueueFrame(slot, samples, numberOfFrames) {
191
+ const st = slots[slot];
192
+ if (!st)
193
+ return;
194
+ const buf = Buffer.from(samples.buffer, samples.byteOffset, samples.byteLength);
195
+ st.q.push(buf);
196
+ }
197
+ function clearSlotQueue(slot) {
198
+ const st = slots[slot];
199
+ if (st) {
200
+ st.q = [];
201
+ const now = Number(process.hrtime.bigint()) / 1e6;
202
+ const frameMs = (st.lastFrames / STREAM_INPUT_SAMPLE_RATE_IN_HZ) * 1000;
203
+ st.nextDueMs = now + frameMs;
204
+ }
205
+ }
206
+ function getFFmpegArguments() {
207
+ const N = PARTICIPANT_SLOTS;
208
+ const SR = STREAM_INPUT_SAMPLE_RATE_IN_HZ;
209
+ const ffArgs = [];
210
+ for (let i = 0; i < N; i++) {
211
+ ffArgs.push("-f", "s16le", "-ar", String(SR), "-ac", "1", "-i", `pipe:${3 + i}`);
212
+ }
213
+ const pre = [];
214
+ for (let i = 0; i < N; i++) {
215
+ pre.push(`[${i}:a]aresample=async=1:first_pts=0,asetpts=N/SR/TB[a${i}]`);
216
+ }
217
+ const labels = Array.from({ length: N }, (_, i) => `[a${i}]`).join("");
218
+ const amix = `${labels}amix=inputs=${N}:duration=longest:dropout_transition=250:normalize=0[mix]`;
219
+ const filter = `${pre.join(";")};${amix}`;
220
+ ffArgs.push("-hide_banner", "-nostats", "-loglevel", "error", "-filter_complex", filter, "-map", "[mix]", "-f", "s16le", "-ar", String(SR), "-ac", "1", "-c:a", "pcm_s16le", "pipe:1");
221
+ return ffArgs;
222
+ }
223
+ function spawnFFmpegProcess(rtcAudioSource, onAudioStreamReady) {
224
+ const stdio = ["ignore", "pipe", "pipe", ...Array(PARTICIPANT_SLOTS).fill("pipe")];
225
+ const args = getFFmpegArguments();
226
+ const ffmpegProcess = child_process.spawn("ffmpeg", args, { stdio });
227
+ startPacer(ffmpegProcess, PARTICIPANT_SLOTS, rtcAudioSource, onAudioStreamReady);
228
+ ffmpegProcess.stderr.setEncoding("utf8");
229
+ ffmpegProcess.stderr.on("data", (d) => console.error("[ffmpeg]", String(d).trim()));
230
+ ffmpegProcess.on("error", () => console.error("FFmpeg process error: is ffmpeg installed?"));
231
+ let audioBuffer = Buffer.alloc(0);
232
+ const FRAME_SIZE_BYTES = FRAME_10MS_SAMPLES * BYTES_PER_SAMPLE;
233
+ ffmpegProcess.stdout.on("data", (chunk) => {
234
+ audioBuffer = Buffer.concat([audioBuffer, chunk]);
235
+ while (audioBuffer.length >= FRAME_SIZE_BYTES) {
236
+ const frameData = audioBuffer.subarray(0, FRAME_SIZE_BYTES);
237
+ const samples = new Int16Array(FRAME_10MS_SAMPLES);
238
+ for (let i = 0; i < FRAME_10MS_SAMPLES; i++) {
239
+ samples[i] = frameData.readInt16LE(i * 2);
240
+ }
241
+ enqueueOutputFrame(samples);
242
+ audioBuffer = audioBuffer.subarray(FRAME_SIZE_BYTES);
243
+ }
244
+ });
245
+ return ffmpegProcess;
246
+ }
247
+ function writeAudioDataToFFmpeg(ffmpegProcess, slot, audioTrack) {
248
+ const writer = ffmpegProcess.stdio[3 + slot];
249
+ const sink = new AudioSink(audioTrack);
250
+ const unsubscribe = sink.subscribe(({ samples, sampleRate: sr, channelCount: ch, bitsPerSample, numberOfFrames }) => {
251
+ if (ch !== 1 || bitsPerSample !== 16)
252
+ return;
253
+ let out = samples;
254
+ if (sr !== STREAM_INPUT_SAMPLE_RATE_IN_HZ) {
255
+ const resampled = resampleTo48kHz(samples, sr, numberOfFrames !== null && numberOfFrames !== void 0 ? numberOfFrames : samples.length);
256
+ out = resampled;
257
+ }
258
+ appendAndDrainTo480(slot, out);
259
+ });
260
+ const stop = () => {
261
+ try {
262
+ unsubscribe();
263
+ sink.stop();
264
+ }
265
+ catch (_a) {
266
+ console.error("Failed to stop AudioSink");
267
+ }
268
+ };
269
+ return { sink, writer, stop };
270
+ }
271
+ function stopFFmpegProcess(ffmpegProcess) {
272
+ stopPacer();
273
+ if (ffmpegProcess && !ffmpegProcess.killed) {
274
+ try {
275
+ ffmpegProcess.stdout.unpipe();
276
+ }
277
+ catch (_a) {
278
+ console.error("Failed to unpipe ffmpeg stdout");
279
+ }
280
+ for (let i = 0; i < PARTICIPANT_SLOTS; i++) {
281
+ const w = ffmpegProcess.stdio[3 + i];
282
+ try {
283
+ w.end();
284
+ }
285
+ catch (_b) {
286
+ console.error("Failed to end ffmpeg writable stream");
287
+ }
288
+ }
289
+ ffmpegProcess.kill("SIGTERM");
290
+ }
291
+ }
292
+
293
+ class AudioMixer extends EventEmitter.EventEmitter {
294
+ constructor(onStreamReady) {
295
+ super();
296
+ this.ffmpegProcess = null;
297
+ this.combinedAudioStream = null;
298
+ this.rtcAudioSource = null;
299
+ this.participantSlots = new Map();
300
+ this.activeSlots = {};
301
+ this.setupMediaStream();
302
+ this.participantSlots = new Map(Array.from({ length: PARTICIPANT_SLOTS }, (_, i) => [i, ""]));
303
+ this.onStreamReady = onStreamReady;
304
+ }
305
+ setupMediaStream() {
306
+ this.rtcAudioSource = new wrtc.nonstandard.RTCAudioSource();
307
+ const audioTrack = this.rtcAudioSource.createTrack();
308
+ this.combinedAudioStream = new wrtc.MediaStream([audioTrack]);
309
+ }
310
+ getCombinedAudioStream() {
311
+ return this.combinedAudioStream;
312
+ }
313
+ handleRemoteParticipants(participants) {
314
+ if (participants.length === 0) {
315
+ this.stopAudioMixer();
316
+ return;
317
+ }
318
+ if (!this.ffmpegProcess && this.rtcAudioSource) {
319
+ this.ffmpegProcess = spawnFFmpegProcess(this.rtcAudioSource, this.onStreamReady);
320
+ }
321
+ for (const p of participants)
322
+ this.attachParticipantIfNeeded(p);
323
+ const liveIds = new Set(participants.map((p) => p.id).filter(Boolean));
324
+ for (const [slot, pid] of this.participantSlots) {
325
+ if (pid && !liveIds.has(pid))
326
+ this.detachParticipant(pid);
327
+ }
328
+ }
329
+ stopAudioMixer() {
330
+ if (this.ffmpegProcess) {
331
+ stopFFmpegProcess(this.ffmpegProcess);
332
+ this.ffmpegProcess = null;
333
+ }
334
+ this.participantSlots = new Map(Array.from({ length: PARTICIPANT_SLOTS }, (_, i) => [i, ""]));
335
+ this.activeSlots = {};
336
+ this.setupMediaStream();
337
+ }
338
+ slotForParticipant(participantId) {
339
+ var _a;
340
+ const found = (_a = [...this.participantSlots.entries()].find(([, id]) => id === participantId)) === null || _a === void 0 ? void 0 : _a[0];
341
+ return found === undefined ? null : found;
342
+ }
343
+ acquireSlot(participantId) {
344
+ var _a;
345
+ const existing = this.slotForParticipant(participantId);
346
+ if (existing !== null)
347
+ return existing;
348
+ const empty = (_a = [...this.participantSlots.entries()].find(([, id]) => id === "")) === null || _a === void 0 ? void 0 : _a[0];
349
+ if (empty === undefined)
350
+ return null;
351
+ this.participantSlots.set(empty, participantId);
352
+ return empty;
353
+ }
354
+ attachParticipantIfNeeded(participant) {
355
+ var _a;
356
+ const { id: participantId, stream: participantStream, isAudioEnabled } = participant;
357
+ if (!participantId)
358
+ return;
359
+ if (!participantStream || !isAudioEnabled) {
360
+ this.detachParticipant(participantId);
361
+ return;
362
+ }
363
+ const audioTrack = participantStream.getTracks().find((t) => t.kind === "audio");
364
+ if (!audioTrack) {
365
+ this.detachParticipant(participantId);
366
+ return;
367
+ }
368
+ const slot = this.acquireSlot(participantId);
369
+ if (slot === null)
370
+ return;
371
+ const existing = this.activeSlots[slot];
372
+ if (existing && existing.trackId === audioTrack.id)
373
+ return;
374
+ if (existing) {
375
+ try {
376
+ existing.stop();
377
+ }
378
+ catch (e) {
379
+ console.error("Failed to stop existing audio track", { error: e });
380
+ }
381
+ this.activeSlots[slot] = undefined;
382
+ }
383
+ const { sink, writer, stop } = writeAudioDataToFFmpeg(this.ffmpegProcess, slot, audioTrack);
384
+ this.activeSlots[slot] = { sink, writer, stop, trackId: audioTrack.id };
385
+ (_a = audioTrack.addEventListener) === null || _a === void 0 ? void 0 : _a.call(audioTrack, "ended", () => this.detachParticipant(participantId));
386
+ }
387
+ detachParticipant(participantId) {
388
+ const slot = this.slotForParticipant(participantId);
389
+ if (slot === null)
390
+ return;
391
+ const binding = this.activeSlots[slot];
392
+ if (binding) {
393
+ try {
394
+ binding.stop();
395
+ }
396
+ catch (e) {
397
+ console.error("Failed to stop existing audio track", { error: e });
398
+ }
399
+ this.activeSlots[slot] = undefined;
400
+ }
401
+ clearSlotQueue(slot);
402
+ this.participantSlots.set(slot, "");
403
+ }
404
+ }
405
+
406
+ class Assistant extends EventEmitter {
407
+ constructor({ assistantKey, startCombinedAudioStream } = { startCombinedAudioStream: false }) {
408
+ super();
409
+ this.mediaStream = null;
410
+ this.audioSource = null;
411
+ this.combinedStream = null;
412
+ this.assistantKey = assistantKey;
413
+ this.client = new core.WherebyClient();
414
+ this.roomConnection = this.client.getRoomConnection();
415
+ this.localMedia = this.client.getLocalMedia();
416
+ const outputAudioSource = new wrtc.nonstandard.RTCAudioSource();
417
+ const outputMediaStream = new wrtc.MediaStream([outputAudioSource.createTrack()]);
418
+ this.mediaStream = outputMediaStream;
419
+ this.audioSource = outputAudioSource;
420
+ if (startCombinedAudioStream) {
421
+ const handleStreamReady = () => {
422
+ if (!this.combinedStream) {
423
+ console.warn("Combined stream is not available");
424
+ return;
425
+ }
426
+ this.emit(AUDIO_STREAM_READY, {
427
+ stream: this.combinedStream,
428
+ track: this.combinedStream.getAudioTracks()[0],
429
+ });
430
+ };
431
+ const audioMixer = new AudioMixer(handleStreamReady);
432
+ this.combinedStream = audioMixer.getCombinedAudioStream();
433
+ this.roomConnection.subscribeToRemoteParticipants(audioMixer.handleRemoteParticipants.bind(audioMixer));
434
+ }
435
+ }
436
+ joinRoom(roomUrl) {
437
+ return __awaiter(this, void 0, void 0, function* () {
438
+ if (this.mediaStream) {
439
+ yield this.localMedia.startMedia(this.mediaStream);
440
+ }
441
+ this.roomConnection.initialize({
442
+ localMediaOptions: {
443
+ audio: false,
444
+ video: false,
445
+ },
446
+ roomUrl,
447
+ isNodeSdk: true,
448
+ roomKey: this.assistantKey,
449
+ });
450
+ this.roomConnection.joinRoom();
451
+ });
452
+ }
453
+ getLocalMediaStream() {
454
+ return this.mediaStream;
455
+ }
456
+ getLocalAudioSource() {
457
+ return this.audioSource;
458
+ }
459
+ getRoomConnection() {
460
+ return this.roomConnection;
461
+ }
462
+ getCombinedAudioStream() {
463
+ return this.combinedStream;
464
+ }
465
+ getRemoteParticipants() {
466
+ return this.roomConnection.getState().remoteParticipants;
467
+ }
468
+ startCloudRecording() {
469
+ this.roomConnection.startCloudRecording();
470
+ }
471
+ stopCloudRecording() {
472
+ this.roomConnection.stopCloudRecording();
473
+ }
474
+ sendChatMessage(message) {
475
+ this.roomConnection.sendChatMessage(message);
476
+ }
477
+ spotlightParticipant(participantId) {
478
+ this.roomConnection.spotlightParticipant(participantId);
479
+ }
480
+ removeSpotlight(participantId) {
481
+ this.roomConnection.removeSpotlight(participantId);
482
+ }
483
+ requestAudioEnable(participantId, enable) {
484
+ if (enable) {
485
+ this.roomConnection.askToSpeak(participantId);
486
+ }
487
+ else {
488
+ this.roomConnection.muteParticipants([participantId]);
489
+ }
490
+ }
491
+ requestVideoEnable(participantId, enable) {
492
+ if (enable) {
493
+ this.roomConnection.askToTurnOnCamera(participantId);
494
+ }
495
+ else {
496
+ this.roomConnection.turnOffParticipantCameras([participantId]);
497
+ }
498
+ }
499
+ acceptWaitingParticipant(participantId) {
500
+ this.roomConnection.acceptWaitingParticipant(participantId);
501
+ }
502
+ rejectWaitingParticipant(participantId) {
503
+ this.roomConnection.rejectWaitingParticipant(participantId);
504
+ }
505
+ subscribeToRemoteParticipants(callback) {
506
+ return this.roomConnection.subscribeToRemoteParticipants(callback);
507
+ }
508
+ }
509
+
510
+ const BIND_INTERFACE = "en0";
511
+ function buildRoomUrl(roomPath, wherebySubdomain, baseDomain = "whereby.com") {
512
+ let wherebyDomain;
513
+ {
514
+ const ifaceAddrs = os.networkInterfaces()[BIND_INTERFACE];
515
+ if (!ifaceAddrs) {
516
+ throw new Error(`Unknown interface ${BIND_INTERFACE}`);
517
+ }
518
+ const [bindAddr] = ifaceAddrs.filter((iface) => iface.family === "IPv4");
519
+ if (!bindAddr) {
520
+ throw new Error(`No IPv4 address found for interface ${BIND_INTERFACE}`);
521
+ }
522
+ wherebyDomain = `${wherebySubdomain}-ip-${bindAddr.address.replace(/[.]/g, "-")}.hereby.dev:4443`;
523
+ }
524
+ return `https://${wherebyDomain}${roomPath}`;
525
+ }
526
+
527
+ const webhookRouter = (webhookTriggers, subdomain, emitter, assistantKey) => {
528
+ const router = express.Router();
529
+ const jsonParser = bodyParser.json();
530
+ router.get("/", (_, res) => {
531
+ res.status(200);
532
+ res.end();
533
+ });
534
+ router.post("/", jsonParser, (req, res) => {
535
+ var _a;
536
+ assert(req.body, "message body is required");
537
+ assert("type" in req.body, "webhook type is required");
538
+ const shouldTriggerOnReceivedWebhook = (_a = webhookTriggers[req.body.type]) === null || _a === void 0 ? void 0 : _a.call(webhookTriggers, req.body);
539
+ if (shouldTriggerOnReceivedWebhook) {
540
+ const roomUrl = buildRoomUrl(req.body.data.roomName, subdomain);
541
+ const assistant = new Assistant({ assistantKey, startCombinedAudioStream: true });
542
+ assistant.joinRoom(roomUrl);
543
+ emitter.emit(ASSISTANT_JOIN_SUCCESS, { roomUrl, triggerWebhook: req.body, assistant });
544
+ }
545
+ res.status(200);
546
+ res.end();
547
+ });
548
+ return router;
549
+ };
550
+ class Trigger extends EventEmitter.EventEmitter {
551
+ constructor({ webhookTriggers = {}, subdomain, port = 4999, assistantKey }) {
552
+ super();
553
+ this.webhookTriggers = webhookTriggers;
554
+ this.subdomain = subdomain;
555
+ this.port = port;
556
+ this.assistantKey = assistantKey;
557
+ }
558
+ start() {
559
+ const app = express();
560
+ const router = webhookRouter(this.webhookTriggers, this.subdomain, this, this.assistantKey);
561
+ app.use(router);
562
+ const server = app.listen(this.port, () => {
563
+ });
564
+ process.on("SIGTERM", () => {
565
+ server.close();
566
+ });
567
+ }
568
+ }
569
+
570
+ exports.ASSISTANT_JOIN_SUCCESS = ASSISTANT_JOIN_SUCCESS;
571
+ exports.AUDIO_STREAM_READY = AUDIO_STREAM_READY;
572
+ exports.Assistant = Assistant;
573
+ exports.AudioSink = AudioSink;
574
+ exports.AudioSource = AudioSource;
575
+ exports.Trigger = Trigger;