@memberjunction/livekit-room-server 6.2.0-edge.2 → 6.2.0-edge.4

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 (82) hide show
  1. package/README.md +85 -0
  2. package/dist/index.d.ts +18 -2
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +18 -1
  5. package/dist/index.js.map +1 -1
  6. package/dist/livekit-agent-room-coordinator.d.ts +220 -13
  7. package/dist/livekit-agent-room-coordinator.d.ts.map +1 -1
  8. package/dist/livekit-agent-room-coordinator.js +409 -65
  9. package/dist/livekit-agent-room-coordinator.js.map +1 -1
  10. package/dist/livekit-egress-service.d.ts +7 -0
  11. package/dist/livekit-egress-service.d.ts.map +1 -1
  12. package/dist/livekit-egress-service.js +11 -0
  13. package/dist/livekit-egress-service.js.map +1 -1
  14. package/dist/livekit-native-module.d.ts +18 -0
  15. package/dist/livekit-native-module.d.ts.map +1 -0
  16. package/dist/livekit-native-module.js +27 -0
  17. package/dist/livekit-native-module.js.map +1 -0
  18. package/dist/livekit-participant-service.d.ts +47 -0
  19. package/dist/livekit-participant-service.d.ts.map +1 -0
  20. package/dist/livekit-participant-service.js +64 -0
  21. package/dist/livekit-participant-service.js.map +1 -0
  22. package/dist/livekit-sip-service.d.ts +141 -0
  23. package/dist/livekit-sip-service.d.ts.map +1 -0
  24. package/dist/livekit-sip-service.js +169 -0
  25. package/dist/livekit-sip-service.js.map +1 -0
  26. package/dist/livekit-token-service.d.ts +26 -5
  27. package/dist/livekit-token-service.d.ts.map +1 -1
  28. package/dist/livekit-token-service.js +22 -7
  29. package/dist/livekit-token-service.js.map +1 -1
  30. package/dist/livekit-webhook.d.ts +65 -0
  31. package/dist/livekit-webhook.d.ts.map +1 -0
  32. package/dist/livekit-webhook.js +65 -0
  33. package/dist/livekit-webhook.js.map +1 -0
  34. package/dist/meeting-dial-in-service.d.ts +83 -0
  35. package/dist/meeting-dial-in-service.d.ts.map +1 -0
  36. package/dist/meeting-dial-in-service.js +202 -0
  37. package/dist/meeting-dial-in-service.js.map +1 -0
  38. package/dist/room-audio/audio-decoder.d.ts +40 -0
  39. package/dist/room-audio/audio-decoder.d.ts.map +1 -0
  40. package/dist/room-audio/audio-decoder.js +205 -0
  41. package/dist/room-audio/audio-decoder.js.map +1 -0
  42. package/dist/room-audio/comfort-tone.d.ts +23 -0
  43. package/dist/room-audio/comfort-tone.d.ts.map +1 -0
  44. package/dist/room-audio/comfort-tone.js +70 -0
  45. package/dist/room-audio/comfort-tone.js.map +1 -0
  46. package/dist/room-audio/pcm-frame-pump.d.ts +134 -0
  47. package/dist/room-audio/pcm-frame-pump.d.ts.map +1 -0
  48. package/dist/room-audio/pcm-frame-pump.js +253 -0
  49. package/dist/room-audio/pcm-frame-pump.js.map +1 -0
  50. package/dist/room-audio/room-audio-decode-cache.d.ts +37 -0
  51. package/dist/room-audio/room-audio-decode-cache.d.ts.map +1 -0
  52. package/dist/room-audio/room-audio-decode-cache.js +75 -0
  53. package/dist/room-audio/room-audio-decode-cache.js.map +1 -0
  54. package/dist/room-audio/room-audio-player.d.ts +178 -0
  55. package/dist/room-audio/room-audio-player.d.ts.map +1 -0
  56. package/dist/room-audio/room-audio-player.js +404 -0
  57. package/dist/room-audio/room-audio-player.js.map +1 -0
  58. package/dist/room-audio/room-audio-sources.d.ts +47 -0
  59. package/dist/room-audio/room-audio-sources.d.ts.map +1 -0
  60. package/dist/room-audio/room-audio-sources.js +116 -0
  61. package/dist/room-audio/room-audio-sources.js.map +1 -0
  62. package/dist/room-audio/room-speech-synthesizer.d.ts +55 -0
  63. package/dist/room-audio/room-speech-synthesizer.d.ts.map +1 -0
  64. package/dist/room-audio/room-speech-synthesizer.js +56 -0
  65. package/dist/room-audio/room-speech-synthesizer.js.map +1 -0
  66. package/dist/room-authorization.d.ts +58 -0
  67. package/dist/room-authorization.d.ts.map +1 -0
  68. package/dist/room-authorization.js +212 -0
  69. package/dist/room-authorization.js.map +1 -0
  70. package/dist/room-handoff/handoff-offer-registry.d.ts +147 -0
  71. package/dist/room-handoff/handoff-offer-registry.d.ts.map +1 -0
  72. package/dist/room-handoff/handoff-offer-registry.js +548 -0
  73. package/dist/room-handoff/handoff-offer-registry.js.map +1 -0
  74. package/dist/room-handoff/handoff-types.d.ts +172 -0
  75. package/dist/room-handoff/handoff-types.d.ts.map +1 -0
  76. package/dist/room-handoff/handoff-types.js +16 -0
  77. package/dist/room-handoff/handoff-types.js.map +1 -0
  78. package/dist/room-handoff/room-handoff-engine.d.ts +149 -0
  79. package/dist/room-handoff/room-handoff-engine.d.ts.map +1 -0
  80. package/dist/room-handoff/room-handoff-engine.js +563 -0
  81. package/dist/room-handoff/room-handoff-engine.js.map +1 -0
  82. package/package.json +15 -9
@@ -0,0 +1,404 @@
1
+ /**
2
+ * @fileoverview {@link RoomAudioPlayer} — joins a LiveKit room as a small publish-only bot and plays audio into it:
3
+ * contact-center hold music, with spoken announcements ducked over the top ("you are caller number three").
4
+ *
5
+ * The audio comes from an `MJ: Files` row, an https URL, raw PCM, or the built-in comfort tone. It is decoded once
6
+ * (WAV or MP3), resampled to {@link ROOM_AUDIO_SAMPLE_RATE}, cached by source, and paced into the room in 20 ms
7
+ * frames by a {@link PcmFramePump}. Each playback is a {@link RoomAudioHandle} the caller pauses, resumes, announces
8
+ * through and stops; the player keeps a registry so a room's playbacks can be found and stopped together (the
9
+ * `room_finished` webhook does this), and a playback whose bot is disconnected by the server removes itself.
10
+ *
11
+ * It reuses the agent bridge's plumbing rather than its own: bot tokens from {@link LiveKitTokenService}, the native
12
+ * room client through `@memberjunction/ai-bridge-livekit`'s module loader at the same specifier the agent coordinator
13
+ * resolves, and the PCM16 resampler from `@memberjunction/ai-bridge-base`.
14
+ *
15
+ * @module @memberjunction/livekit-room-server
16
+ */
17
+ import { randomUUID } from 'node:crypto';
18
+ import { BaseSingleton, NormalizeUUID } from '@memberjunction/global';
19
+ import { LogError, LogStatus } from '@memberjunction/core';
20
+ import { ResamplePcm16 } from '@memberjunction/ai-bridge-base';
21
+ import { DefaultNativeLoader } from '@memberjunction/ai-bridge-livekit';
22
+ import { LiveKitTokenService } from '../livekit-token-service.js';
23
+ import { ResolveLiveKitNativeModuleSpecifier } from '../livekit-native-module.js';
24
+ import { AssertAudioDurationWithinCap, DecodeAudio } from './audio-decoder.js';
25
+ import { GenerateComfortTone } from './comfort-tone.js';
26
+ import { PcmFramePump, SystemPumpClock } from './pcm-frame-pump.js';
27
+ import { RoomAudioDecodeCache } from './room-audio-decode-cache.js';
28
+ import { AssertHttpsAudioUrl, FetchRoomAudioUrl, ReadMJFileAudio, } from './room-audio-sources.js';
29
+ /** How often a finished playback checks whether its already-sent audio has played out, in ms. */
30
+ const DRAIN_TICK_MS = 10;
31
+ /** The rate the player publishes at (Hz). Music sounds noticeably duller at the 24 kHz the agent voice uses. */
32
+ export const ROOM_AUDIO_SAMPLE_RATE = 48000;
33
+ /** One playback: its bot's room client and its frame pump. Created only by {@link RoomAudioPlayer}. */
34
+ class RoomAudioPlayback {
35
+ constructor(PlaybackID, RoomName, client, contextUser, owner) {
36
+ this.PlaybackID = PlaybackID;
37
+ this.RoomName = RoomName;
38
+ this.client = client;
39
+ this.contextUser = contextUser;
40
+ this.owner = owner;
41
+ this.pump = null;
42
+ this.stopPromise = null;
43
+ this.endReason = null;
44
+ this.drainTicker = null;
45
+ this.resolveEnded = () => undefined;
46
+ this.Ended = new Promise((resolve) => {
47
+ this.resolveEnded = resolve;
48
+ });
49
+ }
50
+ get EndReason() {
51
+ return this.endReason;
52
+ }
53
+ get IsPlaying() {
54
+ return this.stopPromise === null && this.pump !== null && this.pump.IsRunning && !this.pump.IsPaused;
55
+ }
56
+ /** True once {@link Stop} has begun. */
57
+ get IsStopped() {
58
+ return this.stopPromise !== null;
59
+ }
60
+ /** Attaches and starts the pump (once the bot is in the room). */
61
+ Play(pump) {
62
+ this.pump = pump;
63
+ pump.Start();
64
+ }
65
+ async Announce(clip) {
66
+ if (this.IsStopped || !this.pump) {
67
+ return false;
68
+ }
69
+ try {
70
+ const audio = 'Text' in clip ? await this.owner.Synthesize(clip.Text, this.contextUser) : validateClipPcm(clip);
71
+ if (!audio || this.IsStopped) {
72
+ return false;
73
+ }
74
+ return await this.pump.InsertClip(ResamplePcm16(audio.Pcm, audio.SampleRate, ROOM_AUDIO_SAMPLE_RATE));
75
+ }
76
+ catch (err) {
77
+ LogError(`[RoomAudioPlayer] announcement in room ${this.RoomName} (playback ${this.PlaybackID}) failed: ${errorText(err)}`);
78
+ return false;
79
+ }
80
+ }
81
+ Pause() {
82
+ this.pump?.Pause();
83
+ }
84
+ Resume() {
85
+ this.pump?.Resume();
86
+ }
87
+ Stop() {
88
+ return this.EndWith('Stopped');
89
+ }
90
+ /**
91
+ * Ends the playback for `reason` (the first reason recorded wins), stops the pump, disconnects the bot, and
92
+ * resolves {@link Ended} once the bot has left. Idempotent; never rejects.
93
+ */
94
+ EndWith(reason) {
95
+ this.endReason ??= reason;
96
+ this.stopPromise ??= this.stopOnce();
97
+ return this.stopPromise;
98
+ }
99
+ /**
100
+ * The audio ran out: wait until the frames already sent ahead of real time have played, then end `Completed`, so
101
+ * the bot does not leave (dropping its queued tail) before the last of the clip is heard. A Stop or server
102
+ * disconnect during the wait wins instead.
103
+ */
104
+ CompleteAfterDrain(clock) {
105
+ const drainMs = this.pump?.QueuedAheadMs ?? 0;
106
+ if (drainMs <= 0 || this.IsStopped) {
107
+ void this.EndWith('Completed');
108
+ return;
109
+ }
110
+ const deadline = clock.Now() + drainMs;
111
+ this.drainTicker = clock.StartTicker(() => {
112
+ if (clock.Now() >= deadline) {
113
+ void this.EndWith('Completed');
114
+ }
115
+ }, DRAIN_TICK_MS);
116
+ }
117
+ /** Stops the pump, then always disconnects the bot and leaves the registry, whatever failed before. */
118
+ async stopOnce() {
119
+ this.drainTicker?.Cancel();
120
+ this.drainTicker = null;
121
+ try {
122
+ this.pump?.Stop();
123
+ }
124
+ catch (err) {
125
+ LogError(`[RoomAudioPlayer] stopping the pump for playback ${this.PlaybackID} failed: ${errorText(err)}`);
126
+ }
127
+ finally {
128
+ try {
129
+ await this.client.disconnect();
130
+ }
131
+ catch (err) {
132
+ LogError(`[RoomAudioPlayer] disconnecting playback ${this.PlaybackID} from room ${this.RoomName} failed: ${errorText(err)}`);
133
+ }
134
+ finally {
135
+ this.owner.Deregister(this);
136
+ this.resolveEnded(this.endReason ?? 'Stopped');
137
+ }
138
+ }
139
+ }
140
+ }
141
+ /**
142
+ * Plays audio into LiveKit rooms. A process-wide singleton so the playback registry (and the `room_finished` cleanup
143
+ * that reads it) is shared by every caller.
144
+ */
145
+ export class RoomAudioPlayer extends BaseSingleton {
146
+ /** BaseSingleton requires a protected constructor. */
147
+ constructor() {
148
+ super();
149
+ this.active = new Map();
150
+ this.decodeCache = new RoomAudioDecodeCache();
151
+ this.tokenMinter = new LiveKitTokenService();
152
+ this.loadNativeModule = DefaultNativeLoader;
153
+ this.fileReader = ReadMJFileAudio;
154
+ this.urlFetcher = FetchRoomAudioUrl;
155
+ this.clock = SystemPumpClock;
156
+ this.warnedNoSynthesizer = false;
157
+ }
158
+ /** The process-wide player. */
159
+ static get Instance() {
160
+ return super.getInstance();
161
+ }
162
+ /**
163
+ * Installs the text-to-speech used by {@link RoomAudioHandle.Announce} with `{ Text }` — e.g. an
164
+ * `MJRoomSpeechSynthesizer`. None is installed by default (a voice is vendor-specific); without one a text
165
+ * announcement returns `false` and the player logs the omission once.
166
+ *
167
+ * @param synthesizer The synthesizer, or `undefined` to remove it.
168
+ */
169
+ SetSpeechSynthesizer(synthesizer) {
170
+ this.speechSynthesizer = synthesizer;
171
+ this.warnedNoSynthesizer = false;
172
+ }
173
+ /** Overrides where bot tokens come from (tests; a host with its own credentials). */
174
+ SetTokenService(minter) {
175
+ this.tokenMinter = minter;
176
+ }
177
+ /** Overrides the native room-module loader (tests inject a fake module). */
178
+ SetNativeModuleLoader(loader) {
179
+ this.loadNativeModule = loader;
180
+ }
181
+ /** Overrides the native room-module specifier; `undefined` returns to `LIVEKIT_NATIVE_MODULE` / the default. */
182
+ SetNativeModuleSpecifier(specifier) {
183
+ this.nativeModuleSpecifierOverride = specifier;
184
+ }
185
+ /** Overrides how `MJ: Files` bytes are read (tests). */
186
+ SetFileReader(reader) {
187
+ this.fileReader = reader;
188
+ }
189
+ /** Overrides how URLs are fetched (tests). */
190
+ SetUrlFetcher(fetcher) {
191
+ this.urlFetcher = fetcher;
192
+ }
193
+ /** Overrides the pacing clock (tests). */
194
+ SetClock(clock) {
195
+ this.clock = clock;
196
+ }
197
+ /** Forgets every cached decode (e.g. after a hold file is replaced under the same id). */
198
+ ClearDecodeCache() {
199
+ this.decodeCache.Clear();
200
+ }
201
+ /**
202
+ * Starts playing `params.Source` into `params.RoomName`: loads and decodes it (cached by source), joins the room as
203
+ * a publish-only bot, and paces the audio in.
204
+ *
205
+ * @returns The live playback.
206
+ * @throws {Error} when the source cannot be loaded or decoded, LiveKit is not configured, or the bot cannot join.
207
+ */
208
+ async Start(params) {
209
+ assertStartParams(params);
210
+ const pcm = await this.resolvePcm(params);
211
+ return this.joinAndPlay(params, pcm);
212
+ }
213
+ /** The live playbacks in a room (room names compare case-insensitively). */
214
+ GetActive(roomName) {
215
+ const key = roomKey(roomName);
216
+ return [...this.active.values()].filter((playback) => roomKey(playback.RoomName) === key);
217
+ }
218
+ /**
219
+ * Stops every playback in a room. Never throws.
220
+ *
221
+ * @returns How many playbacks were stopped.
222
+ */
223
+ async StopAllInRoom(roomName) {
224
+ const playbacks = this.GetActive(roomName);
225
+ await Promise.all(playbacks.map((playback) => playback.Stop()));
226
+ if (playbacks.length > 0) {
227
+ LogStatus(`[RoomAudioPlayer] stopped ${playbacks.length} playback(s) in room ${roomName}`);
228
+ }
229
+ return playbacks.length;
230
+ }
231
+ // ── Loading ────────────────────────────────────────────────────────────────────────────────────────────────────
232
+ /** Resolves the source to mono PCM16 at {@link ROOM_AUDIO_SAMPLE_RATE}, through the decode cache where it can. */
233
+ async resolvePcm(params) {
234
+ const source = params.Source;
235
+ switch (source.Kind) {
236
+ case 'ComfortTone':
237
+ this.comfortTone ??= GenerateComfortTone(ROOM_AUDIO_SAMPLE_RATE);
238
+ return this.comfortTone;
239
+ case 'Pcm':
240
+ return resampleToRoomRate(validateClipPcm(source));
241
+ case 'File':
242
+ return this.decodeCache.GetOrLoad(`file:${NormalizeUUID(source.FileID)}`, async () => this.decodeContent(await this.fileReader(source.FileID, params.ContextUser, params.Provider)));
243
+ case 'Url': {
244
+ const url = AssertHttpsAudioUrl(source.Url).href; // refuse plain http before anything is fetched
245
+ return this.decodeCache.GetOrLoad(`url:${url}`, async () => this.decodeContent(await this.urlFetcher(url)));
246
+ }
247
+ default:
248
+ return assertNeverSource(source);
249
+ }
250
+ }
251
+ async decodeContent(content) {
252
+ const decoded = await DecodeAudio(content.Bytes, { MimeType: content.MimeType, FileName: content.FileName });
253
+ return resampleToRoomRate(decoded);
254
+ }
255
+ // ── Joining ────────────────────────────────────────────────────────────────────────────────────────────────────
256
+ /** Joins the room as the playback's bot, registers the playback, and starts the pump. */
257
+ async joinAndPlay(params, pcm) {
258
+ const playbackID = randomUUID();
259
+ const displayName = params.DisplayName ?? 'On hold';
260
+ const token = await this.mintBotToken(params.RoomName, params.Identity ?? `hold-${playbackID}`, displayName);
261
+ const roomModule = await this.loadNativeModule(ResolveLiveKitNativeModuleSpecifier(this.nativeModuleSpecifierOverride));
262
+ const client = roomModule.createRoomClient({ OutboundSampleRate: ROOM_AUDIO_SAMPLE_RATE, InboundSampleRate: ROOM_AUDIO_SAMPLE_RATE });
263
+ const playback = new RoomAudioPlayback(playbackID, params.RoomName, client, params.ContextUser, this.playbackOwner());
264
+ client.onDisconnected(() => this.handleServerDisconnect(playback));
265
+ await this.connectClient(client, token, displayName);
266
+ if (playback.IsStopped) {
267
+ throw new Error(`Room audio bot was disconnected from room ${params.RoomName} as soon as it joined.`);
268
+ }
269
+ this.active.set(playbackID, playback);
270
+ playback.Play(this.createPump(playback, client, pcm, params.Loop ?? true));
271
+ LogStatus(`[RoomAudioPlayer] playing ${describeSource(params.Source)} into room ${params.RoomName} (playback ${playbackID})`);
272
+ return playback;
273
+ }
274
+ /** A publish-only bot token: it never needs to hear the room, and it carries no data channel. */
275
+ mintBotToken(roomName, identity, displayName) {
276
+ return this.tokenMinter.MintToken({
277
+ RoomName: roomName,
278
+ Identity: identity,
279
+ DisplayName: displayName,
280
+ Role: 'agent',
281
+ CanSubscribe: false,
282
+ CanPublishData: false,
283
+ Metadata: { mjRoomAudio: true },
284
+ });
285
+ }
286
+ /** Connects the bot; on failure always releases the client before rethrowing. */
287
+ async connectClient(client, token, displayName) {
288
+ try {
289
+ await client.connect({ url: token.ServerUrl, token: token.Token, name: displayName });
290
+ }
291
+ catch (err) {
292
+ try {
293
+ await client.disconnect();
294
+ }
295
+ catch (disconnectErr) {
296
+ LogError(`[RoomAudioPlayer] releasing a client that failed to join room ${token.RoomName} also failed: ${errorText(disconnectErr)}`);
297
+ }
298
+ throw new Error(`Room audio could not join room ${token.RoomName}: ${errorText(err)}`);
299
+ }
300
+ }
301
+ createPump(playback, client, pcm, loop) {
302
+ return new PcmFramePump({
303
+ Music: pcm,
304
+ SampleRate: ROOM_AUDIO_SAMPLE_RATE,
305
+ Loop: loop,
306
+ Clock: this.clock,
307
+ Sink: (frame) => client.publishAudio(frame.buffer),
308
+ OnEnded: (reason) => {
309
+ // 'Stopped' means the playback stopped the pump itself; otherwise the audio ran out (no loop) or the sink
310
+ // broke, and the bot leaves the room with that reason.
311
+ if (reason === 'Completed') {
312
+ playback.CompleteAfterDrain(this.clock);
313
+ }
314
+ else if (reason === 'SinkError') {
315
+ void playback.EndWith('Failed');
316
+ }
317
+ },
318
+ });
319
+ }
320
+ // ── Registry + speech ──────────────────────────────────────────────────────────────────────────────────────────
321
+ playbackOwner() {
322
+ return {
323
+ Synthesize: (text, contextUser) => this.synthesize(text, contextUser),
324
+ Deregister: (playback) => this.deregister(playback),
325
+ };
326
+ }
327
+ deregister(playback) {
328
+ if (this.active.get(playback.PlaybackID) === playback) {
329
+ this.active.delete(playback.PlaybackID);
330
+ }
331
+ }
332
+ /** The server dropped the bot (room closed, participant removed): stop and forget the playback. */
333
+ handleServerDisconnect(playback) {
334
+ if (!playback.IsStopped) {
335
+ LogStatus(`[RoomAudioPlayer] playback ${playback.PlaybackID} was disconnected from room ${playback.RoomName}; stopping it`);
336
+ void playback.EndWith('Disconnected');
337
+ }
338
+ }
339
+ async synthesize(text, contextUser) {
340
+ if (!this.speechSynthesizer) {
341
+ if (!this.warnedNoSynthesizer) {
342
+ this.warnedNoSynthesizer = true;
343
+ LogError('[RoomAudioPlayer] a text announcement was requested but no speech synthesizer is installed; it was skipped. ' +
344
+ 'Call RoomAudioPlayer.Instance.SetSpeechSynthesizer(new MJRoomSpeechSynthesizer({ Voice: ... })) at startup.');
345
+ }
346
+ return null;
347
+ }
348
+ try {
349
+ return await this.speechSynthesizer.Synthesize(text, contextUser);
350
+ }
351
+ catch (err) {
352
+ LogError(`[RoomAudioPlayer] the speech synthesizer failed: ${errorText(err)}`);
353
+ return null;
354
+ }
355
+ }
356
+ }
357
+ // ── Helpers ──────────────────────────────────────────────────────────────────────────────────────────────────────
358
+ function assertStartParams(params) {
359
+ if (!params.RoomName?.trim()) {
360
+ throw new Error('RoomAudioPlayer.Start needs a RoomName.');
361
+ }
362
+ if (!params.ContextUser) {
363
+ throw new Error('RoomAudioPlayer.Start needs a ContextUser (the user the audio is loaded as).');
364
+ }
365
+ if (!params.Source) {
366
+ throw new Error('RoomAudioPlayer.Start needs a Source.');
367
+ }
368
+ }
369
+ /** Checks caller-supplied PCM: non-empty, a positive rate, and within the duration cap. */
370
+ function validateClipPcm(clip) {
371
+ if (!(clip.SampleRate > 0)) {
372
+ throw new Error(`Room audio PCM needs a positive SampleRate; got ${clip.SampleRate}.`);
373
+ }
374
+ if (clip.Pcm.length === 0) {
375
+ throw new Error('Room audio PCM is empty.');
376
+ }
377
+ AssertAudioDurationWithinCap(clip.Pcm.length, clip.SampleRate);
378
+ return { Pcm: clip.Pcm, SampleRate: clip.SampleRate };
379
+ }
380
+ function resampleToRoomRate(audio) {
381
+ return ResamplePcm16(audio.Pcm, audio.SampleRate, ROOM_AUDIO_SAMPLE_RATE);
382
+ }
383
+ function describeSource(source) {
384
+ switch (source.Kind) {
385
+ case 'File':
386
+ return `file ${source.FileID}`;
387
+ case 'Url':
388
+ return `url ${source.Url}`;
389
+ case 'Pcm':
390
+ return 'caller-supplied PCM';
391
+ default:
392
+ return 'the comfort tone';
393
+ }
394
+ }
395
+ function assertNeverSource(source) {
396
+ throw new Error(`Unknown room audio source kind: ${JSON.stringify(source)}`);
397
+ }
398
+ function roomKey(roomName) {
399
+ return roomName.trim().toLowerCase();
400
+ }
401
+ function errorText(err) {
402
+ return err instanceof Error ? err.message : String(err);
403
+ }
404
+ //# sourceMappingURL=room-audio-player.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"room-audio-player.js","sourceRoot":"","sources":["../../src/room-audio/room-audio-player.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,47 @@
1
+ /**
2
+ * @fileoverview Where the room audio player gets an audio file's bytes: an `MJ: Files` row (read from its storage
3
+ * provider through `@memberjunction/storage`), or an https URL (fetched through the SSRF-safe `SafeFetch`, with a
4
+ * timeout and a byte cap). Both return {@link RoomAudioFileContent}, which `DecodeAudio` turns into PCM.
5
+ *
6
+ * @module @memberjunction/livekit-room-server
7
+ */
8
+ import { type IMetadataProvider, type UserInfo } from '@memberjunction/core';
9
+ /** An audio file's bytes plus what is known about its format. */
10
+ export interface RoomAudioFileContent {
11
+ /** The file's bytes. */
12
+ Bytes: Uint8Array;
13
+ /** The MIME type, when known. */
14
+ MimeType?: string;
15
+ /** The file name, when known. */
16
+ FileName?: string;
17
+ }
18
+ /** Reads an `MJ: Files` row's bytes. The default is {@link ReadMJFileAudio}; tests inject a fake. */
19
+ export type RoomAudioFileReader = (fileID: string, contextUser: UserInfo, provider?: IMetadataProvider) => Promise<RoomAudioFileContent>;
20
+ /** Fetches an https URL's bytes. The default is {@link FetchRoomAudioUrl}; tests inject a fake. */
21
+ export type RoomAudioUrlFetcher = (url: string) => Promise<RoomAudioFileContent>;
22
+ /** The largest file the player will download from a URL, in bytes (25 MB). */
23
+ export declare const MAX_ROOM_AUDIO_URL_BYTES: number;
24
+ /** How long a URL download may take before it is abandoned, in ms. */
25
+ export declare const ROOM_AUDIO_URL_TIMEOUT_MS = 20000;
26
+ /**
27
+ * Reads an `MJ: Files` row's bytes from its storage provider, as the given user (so the user's read permission on
28
+ * the file applies).
29
+ *
30
+ * @throws {Error} when the row cannot be loaded, no storage account serves its provider, or the download fails.
31
+ */
32
+ export declare function ReadMJFileAudio(fileID: string, contextUser: UserInfo, provider?: IMetadataProvider): Promise<RoomAudioFileContent>;
33
+ /**
34
+ * Checks that `url` is an absolute https URL. The player refuses plain http: hold audio is fetched by the server, and
35
+ * a cleartext fetch would let anyone on the path substitute what callers hear.
36
+ *
37
+ * @throws {Error} when the URL is malformed or not https.
38
+ */
39
+ export declare function AssertHttpsAudioUrl(url: string): URL;
40
+ /**
41
+ * Downloads an https audio URL through `SafeFetch` (every redirect hop re-checked against private/reserved
42
+ * addresses), abandoning it after {@link ROOM_AUDIO_URL_TIMEOUT_MS} or once it passes {@link MAX_ROOM_AUDIO_URL_BYTES}.
43
+ *
44
+ * @throws {Error} on a non-https URL, an HTTP error status, a timeout, or a body over the byte cap.
45
+ */
46
+ export declare function FetchRoomAudioUrl(url: string): Promise<RoomAudioFileContent>;
47
+ //# sourceMappingURL=room-audio-sources.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"room-audio-sources.d.ts","sourceRoot":"","sources":["../../src/room-audio/room-audio-sources.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAsB,KAAK,iBAAiB,EAAE,KAAK,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AAKjG,iEAAiE;AACjE,MAAM,WAAW,oBAAoB;IACnC,wBAAwB;IACxB,KAAK,EAAE,UAAU,CAAC;IAClB,iCAAiC;IACjC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,iCAAiC;IACjC,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,qGAAqG;AACrG,MAAM,MAAM,mBAAmB,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,QAAQ,EAAE,QAAQ,CAAC,EAAE,iBAAiB,KAAK,OAAO,CAAC,oBAAoB,CAAC,CAAC;AAEzI,mGAAmG;AACnG,MAAM,MAAM,mBAAmB,GAAG,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,oBAAoB,CAAC,CAAC;AAEjF,8EAA8E;AAC9E,eAAO,MAAM,wBAAwB,QAAmB,CAAC;AAEzD,sEAAsE;AACtE,eAAO,MAAM,yBAAyB,QAAS,CAAC;AAEhD;;;;;GAKG;AACH,wBAAsB,eAAe,CAAC,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,QAAQ,EAAE,QAAQ,CAAC,EAAE,iBAAiB,GAAG,OAAO,CAAC,oBAAoB,CAAC,CAoBxI;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAWpD;AAED;;;;;GAKG;AACH,wBAAsB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,oBAAoB,CAAC,CAclF"}
@@ -0,0 +1,116 @@
1
+ /**
2
+ * @fileoverview Where the room audio player gets an audio file's bytes: an `MJ: Files` row (read from its storage
3
+ * provider through `@memberjunction/storage`), or an https URL (fetched through the SSRF-safe `SafeFetch`, with a
4
+ * timeout and a byte cap). Both return {@link RoomAudioFileContent}, which `DecodeAudio` turns into PCM.
5
+ *
6
+ * @module @memberjunction/livekit-room-server
7
+ */
8
+ import { LogError, Metadata } from '@memberjunction/core';
9
+ import { FileStorageEngine } from '@memberjunction/storage';
10
+ import { SafeFetch } from '@memberjunction/network-utils';
11
+ /** The largest file the player will download from a URL, in bytes (25 MB). */
12
+ export const MAX_ROOM_AUDIO_URL_BYTES = 25 * 1024 * 1024;
13
+ /** How long a URL download may take before it is abandoned, in ms. */
14
+ export const ROOM_AUDIO_URL_TIMEOUT_MS = 20_000;
15
+ /**
16
+ * Reads an `MJ: Files` row's bytes from its storage provider, as the given user (so the user's read permission on
17
+ * the file applies).
18
+ *
19
+ * @throws {Error} when the row cannot be loaded, no storage account serves its provider, or the download fails.
20
+ */
21
+ export async function ReadMJFileAudio(fileID, contextUser, provider) {
22
+ const md = provider ?? Metadata.Provider;
23
+ const file = await md.GetEntityObject('MJ: Files', contextUser);
24
+ const loaded = await file.Load(fileID);
25
+ if (!loaded) {
26
+ const detail = file.LatestResult?.CompleteMessage || 'not found, or not readable by this user';
27
+ throw new Error(`Hold audio file ${fileID} could not be loaded from MJ: Files: ${detail}.`);
28
+ }
29
+ await FileStorageEngine.Instance.Config(false, contextUser, provider);
30
+ const accounts = FileStorageEngine.Instance.GetAccountsByProviderID(file.ProviderID);
31
+ if (accounts.length === 0) {
32
+ throw new Error(`Hold audio file ${fileID} ('${file.Name}') is on storage provider ${file.Provider}, which has no configured storage account.`);
33
+ }
34
+ const driver = await FileStorageEngine.Instance.GetDriver(accounts[0].ID, contextUser);
35
+ const buffer = await driver.GetObject({ fullPath: file.ProviderKey ?? file.Name });
36
+ return {
37
+ Bytes: new Uint8Array(buffer.buffer, buffer.byteOffset, buffer.byteLength),
38
+ MimeType: file.ContentType ?? undefined,
39
+ FileName: file.Name,
40
+ };
41
+ }
42
+ /**
43
+ * Checks that `url` is an absolute https URL. The player refuses plain http: hold audio is fetched by the server, and
44
+ * a cleartext fetch would let anyone on the path substitute what callers hear.
45
+ *
46
+ * @throws {Error} when the URL is malformed or not https.
47
+ */
48
+ export function AssertHttpsAudioUrl(url) {
49
+ let parsed;
50
+ try {
51
+ parsed = new URL(url);
52
+ }
53
+ catch {
54
+ throw new Error(`Hold audio URL is not a valid URL: '${url}'.`);
55
+ }
56
+ if (parsed.protocol !== 'https:') {
57
+ throw new Error(`Hold audio URL must use https; got '${parsed.protocol}' in '${url}'.`);
58
+ }
59
+ return parsed;
60
+ }
61
+ /**
62
+ * Downloads an https audio URL through `SafeFetch` (every redirect hop re-checked against private/reserved
63
+ * addresses), abandoning it after {@link ROOM_AUDIO_URL_TIMEOUT_MS} or once it passes {@link MAX_ROOM_AUDIO_URL_BYTES}.
64
+ *
65
+ * @throws {Error} on a non-https URL, an HTTP error status, a timeout, or a body over the byte cap.
66
+ */
67
+ export async function FetchRoomAudioUrl(url) {
68
+ const parsed = AssertHttpsAudioUrl(url);
69
+ const response = await SafeFetch(parsed.href, { signal: AbortSignal.timeout(ROOM_AUDIO_URL_TIMEOUT_MS) });
70
+ if (!response.ok) {
71
+ void response.body?.cancel();
72
+ throw new Error(`Hold audio URL returned HTTP ${response.status}: '${url}'.`);
73
+ }
74
+ const declared = Number(response.headers.get('content-length') ?? '');
75
+ if (Number.isFinite(declared) && declared > MAX_ROOM_AUDIO_URL_BYTES) {
76
+ void response.body?.cancel();
77
+ throw new Error(`Hold audio URL is ${declared} bytes; the limit is ${MAX_ROOM_AUDIO_URL_BYTES} bytes: '${url}'.`);
78
+ }
79
+ const bytes = await readBodyWithCap(response, url);
80
+ return { Bytes: bytes, MimeType: response.headers.get('content-type') ?? undefined, FileName: parsed.pathname.split('/').pop() };
81
+ }
82
+ /** Reads a response body, stopping (and cancelling the stream) the moment it passes the byte cap. */
83
+ async function readBodyWithCap(response, url) {
84
+ if (!response.body) {
85
+ return new Uint8Array(0);
86
+ }
87
+ const reader = response.body.getReader();
88
+ const chunks = [];
89
+ let total = 0;
90
+ try {
91
+ for (let chunk = await reader.read(); !chunk.done; chunk = await reader.read()) {
92
+ total += chunk.value.byteLength;
93
+ if (total > MAX_ROOM_AUDIO_URL_BYTES) {
94
+ throw new Error(`Hold audio URL exceeded the ${MAX_ROOM_AUDIO_URL_BYTES}-byte limit: '${url}'.`);
95
+ }
96
+ chunks.push(chunk.value);
97
+ }
98
+ }
99
+ finally {
100
+ if (total > MAX_ROOM_AUDIO_URL_BYTES) {
101
+ await reader.cancel().catch((err) => LogError(`[RoomAudioPlayer] cancelling the oversize download of '${url}' failed: ${err.message}`));
102
+ }
103
+ reader.releaseLock();
104
+ }
105
+ return concatChunks(chunks, total);
106
+ }
107
+ function concatChunks(chunks, total) {
108
+ const out = new Uint8Array(total);
109
+ let offset = 0;
110
+ for (const chunk of chunks) {
111
+ out.set(chunk, offset);
112
+ offset += chunk.byteLength;
113
+ }
114
+ return out;
115
+ }
116
+ //# sourceMappingURL=room-audio-sources.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"room-audio-sources.js","sourceRoot":"","sources":["../../src/room-audio/room-audio-sources.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,55 @@
1
+ /**
2
+ * @fileoverview The text-to-speech port the room audio player announces through ({@link IRoomSpeechSynthesizer}),
3
+ * and {@link MJRoomSpeechSynthesizer}, an implementation over MJ's own `AITextToSpeechRunner` (model selection,
4
+ * credentials, failover and an `MJ: AI Prompt Runs` row, all handled there).
5
+ *
6
+ * The player ships with NO synthesizer installed, because a voice is vendor-specific and only the host knows which
7
+ * one its callers should hear. A host opts in once at startup:
8
+ *
9
+ * ```ts
10
+ * RoomAudioPlayer.Instance.SetSpeechSynthesizer(new MJRoomSpeechSynthesizer({ Voice: 'alloy' }));
11
+ * ```
12
+ *
13
+ * @module @memberjunction/livekit-room-server
14
+ */
15
+ import { type UserInfo } from '@memberjunction/core';
16
+ import { AITextToSpeechRunner } from '@memberjunction/ai-prompts';
17
+ import { type DecodedAudio } from './audio-decoder.js';
18
+ /** Turns announcement text into audio. Return `null` when speech could not be produced (and log why). */
19
+ export interface IRoomSpeechSynthesizer {
20
+ /**
21
+ * Speaks `text`.
22
+ *
23
+ * @param text What to say.
24
+ * @param contextUser The user the synthesis runs (and is logged) as.
25
+ * @returns The audio, or `null` when it could not be produced.
26
+ */
27
+ Synthesize(text: string, contextUser: UserInfo): Promise<DecodedAudio | null>;
28
+ }
29
+ /** Options for {@link MJRoomSpeechSynthesizer}. */
30
+ export interface MJRoomSpeechSynthesizerOptions {
31
+ /** The provider-native voice id or name (required: voices belong to a vendor). */
32
+ Voice: string;
33
+ /** Pins the TTS model by ID. Recommended whenever the voice matters, since voices are vendor-specific. */
34
+ ModelID?: string;
35
+ /** The carrier `MJ: AI Prompts` row whose bindings choose the model. Default: the runner's `Default Text To Speech`. */
36
+ PromptID?: string;
37
+ /** The vendor's output format string (e.g. `mp3_44100_128`). Default: the vendor's default (MP3 or WAV decode either way). */
38
+ OutputFormat?: string;
39
+ }
40
+ /** The part of `AITextToSpeechRunner` the synthesizer uses — a seam so tests inject a fake. */
41
+ export type TextToSpeechRunnerLike = Pick<AITextToSpeechRunner, 'RunTextToSpeech'>;
42
+ /** An {@link IRoomSpeechSynthesizer} backed by MJ's `AITextToSpeechRunner`. Never throws: failures log and return `null`. */
43
+ export declare class MJRoomSpeechSynthesizer implements IRoomSpeechSynthesizer {
44
+ private readonly options;
45
+ private readonly runner;
46
+ /**
47
+ * @param options The voice and model selection.
48
+ * @param runner The runner (tests); defaults to a new `AITextToSpeechRunner`.
49
+ * @throws {Error} when no voice is given.
50
+ */
51
+ constructor(options: MJRoomSpeechSynthesizerOptions, runner?: TextToSpeechRunnerLike);
52
+ /** Speaks `text` through the configured TTS model and decodes the result to PCM. */
53
+ Synthesize(text: string, contextUser: UserInfo): Promise<DecodedAudio | null>;
54
+ }
55
+ //# sourceMappingURL=room-speech-synthesizer.d.ts.map