@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.
- package/README.md +85 -0
- package/dist/index.d.ts +18 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +18 -1
- package/dist/index.js.map +1 -1
- package/dist/livekit-agent-room-coordinator.d.ts +220 -13
- package/dist/livekit-agent-room-coordinator.d.ts.map +1 -1
- package/dist/livekit-agent-room-coordinator.js +409 -65
- package/dist/livekit-agent-room-coordinator.js.map +1 -1
- package/dist/livekit-egress-service.d.ts +7 -0
- package/dist/livekit-egress-service.d.ts.map +1 -1
- package/dist/livekit-egress-service.js +11 -0
- package/dist/livekit-egress-service.js.map +1 -1
- package/dist/livekit-native-module.d.ts +18 -0
- package/dist/livekit-native-module.d.ts.map +1 -0
- package/dist/livekit-native-module.js +27 -0
- package/dist/livekit-native-module.js.map +1 -0
- package/dist/livekit-participant-service.d.ts +47 -0
- package/dist/livekit-participant-service.d.ts.map +1 -0
- package/dist/livekit-participant-service.js +64 -0
- package/dist/livekit-participant-service.js.map +1 -0
- package/dist/livekit-sip-service.d.ts +141 -0
- package/dist/livekit-sip-service.d.ts.map +1 -0
- package/dist/livekit-sip-service.js +169 -0
- package/dist/livekit-sip-service.js.map +1 -0
- package/dist/livekit-token-service.d.ts +26 -5
- package/dist/livekit-token-service.d.ts.map +1 -1
- package/dist/livekit-token-service.js +22 -7
- package/dist/livekit-token-service.js.map +1 -1
- package/dist/livekit-webhook.d.ts +65 -0
- package/dist/livekit-webhook.d.ts.map +1 -0
- package/dist/livekit-webhook.js +65 -0
- package/dist/livekit-webhook.js.map +1 -0
- package/dist/meeting-dial-in-service.d.ts +83 -0
- package/dist/meeting-dial-in-service.d.ts.map +1 -0
- package/dist/meeting-dial-in-service.js +202 -0
- package/dist/meeting-dial-in-service.js.map +1 -0
- package/dist/room-audio/audio-decoder.d.ts +40 -0
- package/dist/room-audio/audio-decoder.d.ts.map +1 -0
- package/dist/room-audio/audio-decoder.js +205 -0
- package/dist/room-audio/audio-decoder.js.map +1 -0
- package/dist/room-audio/comfort-tone.d.ts +23 -0
- package/dist/room-audio/comfort-tone.d.ts.map +1 -0
- package/dist/room-audio/comfort-tone.js +70 -0
- package/dist/room-audio/comfort-tone.js.map +1 -0
- package/dist/room-audio/pcm-frame-pump.d.ts +134 -0
- package/dist/room-audio/pcm-frame-pump.d.ts.map +1 -0
- package/dist/room-audio/pcm-frame-pump.js +253 -0
- package/dist/room-audio/pcm-frame-pump.js.map +1 -0
- package/dist/room-audio/room-audio-decode-cache.d.ts +37 -0
- package/dist/room-audio/room-audio-decode-cache.d.ts.map +1 -0
- package/dist/room-audio/room-audio-decode-cache.js +75 -0
- package/dist/room-audio/room-audio-decode-cache.js.map +1 -0
- package/dist/room-audio/room-audio-player.d.ts +178 -0
- package/dist/room-audio/room-audio-player.d.ts.map +1 -0
- package/dist/room-audio/room-audio-player.js +404 -0
- package/dist/room-audio/room-audio-player.js.map +1 -0
- package/dist/room-audio/room-audio-sources.d.ts +47 -0
- package/dist/room-audio/room-audio-sources.d.ts.map +1 -0
- package/dist/room-audio/room-audio-sources.js +116 -0
- package/dist/room-audio/room-audio-sources.js.map +1 -0
- package/dist/room-audio/room-speech-synthesizer.d.ts +55 -0
- package/dist/room-audio/room-speech-synthesizer.d.ts.map +1 -0
- package/dist/room-audio/room-speech-synthesizer.js +56 -0
- package/dist/room-audio/room-speech-synthesizer.js.map +1 -0
- package/dist/room-authorization.d.ts +58 -0
- package/dist/room-authorization.d.ts.map +1 -0
- package/dist/room-authorization.js +212 -0
- package/dist/room-authorization.js.map +1 -0
- package/dist/room-handoff/handoff-offer-registry.d.ts +147 -0
- package/dist/room-handoff/handoff-offer-registry.d.ts.map +1 -0
- package/dist/room-handoff/handoff-offer-registry.js +548 -0
- package/dist/room-handoff/handoff-offer-registry.js.map +1 -0
- package/dist/room-handoff/handoff-types.d.ts +172 -0
- package/dist/room-handoff/handoff-types.d.ts.map +1 -0
- package/dist/room-handoff/handoff-types.js +16 -0
- package/dist/room-handoff/handoff-types.js.map +1 -0
- package/dist/room-handoff/room-handoff-engine.d.ts +149 -0
- package/dist/room-handoff/room-handoff-engine.d.ts.map +1 -0
- package/dist/room-handoff/room-handoff-engine.js +563 -0
- package/dist/room-handoff/room-handoff-engine.js.map +1 -0
- package/package.json +15 -9
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview {@link PcmFramePump} — plays a PCM16 buffer out to a sink in real time, one 20 ms frame at a time.
|
|
3
|
+
*
|
|
4
|
+
* The native LiveKit wrapper's `publishAudio` queues whatever it is given and drains it as fast as the WebRTC audio
|
|
5
|
+
* source accepts it; it does not pace. Handing it a whole song would buffer minutes of audio in memory and make pause,
|
|
6
|
+
* stop and announcements lag by that much. So the pump paces itself: a fast ticker compares monotonic elapsed time
|
|
7
|
+
* with frames sent and emits only what is due, plus a small lead (~150 ms) that absorbs timer jitter. The lead is all
|
|
8
|
+
* that is ever outstanding downstream.
|
|
9
|
+
*
|
|
10
|
+
* - **Drift correction** — frames due are computed from the elapsed time since the timeline started, never by
|
|
11
|
+
* counting ticks, so a late tick sends two frames and the average rate stays exact.
|
|
12
|
+
* - **Bounded catch-up** — after a long stall (a blocked event loop) the pump does not burst the backlog; it drops it
|
|
13
|
+
* and restarts the timeline, so the downstream queue can never grow past the lead plus a small margin.
|
|
14
|
+
* - **Loop**, **Pause/Resume** (resume restarts the timeline — no burst), and **InsertClip**: the music fades down to a
|
|
15
|
+
* duck level, the clip plays over it, and the music fades back up.
|
|
16
|
+
*
|
|
17
|
+
* The clock and ticker are injectable so tests drive time by hand.
|
|
18
|
+
*
|
|
19
|
+
* @module @memberjunction/livekit-room-server
|
|
20
|
+
*/
|
|
21
|
+
import { LogError } from '@memberjunction/core';
|
|
22
|
+
import { FloatToPcm16 } from './audio-decoder.js';
|
|
23
|
+
/** How often the ticker fires, in ms. Shorter than a frame so lateness never exceeds about half a frame. */
|
|
24
|
+
const TICK_MS = 10;
|
|
25
|
+
/** How far behind real time the pump will catch up before it drops the backlog instead, in ms. */
|
|
26
|
+
const MAX_CATCH_UP_MS = 200;
|
|
27
|
+
/** The production clock: `performance.now()` and an unref'd `setInterval` (a playing pump never holds the process open). */
|
|
28
|
+
export const SystemPumpClock = {
|
|
29
|
+
Now: () => performance.now(),
|
|
30
|
+
StartTicker: (callback, intervalMs) => {
|
|
31
|
+
const handle = setInterval(callback, intervalMs);
|
|
32
|
+
handle.unref?.();
|
|
33
|
+
return { Cancel: () => clearInterval(handle) };
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
/** Paces mono PCM16 out to a sink in real time. See the file overview. */
|
|
37
|
+
export class PcmFramePump {
|
|
38
|
+
/** @throws {Error} when the music is empty or a timing option is not positive. */
|
|
39
|
+
constructor(options) {
|
|
40
|
+
this.ticker = null;
|
|
41
|
+
this.running = false;
|
|
42
|
+
this.paused = false;
|
|
43
|
+
this.ended = false;
|
|
44
|
+
this.timelineStart = 0;
|
|
45
|
+
this.framesOnTimeline = 0;
|
|
46
|
+
this.framesSent = 0;
|
|
47
|
+
this.musicPosition = 0;
|
|
48
|
+
this.musicDone = false;
|
|
49
|
+
this.gain = 1;
|
|
50
|
+
this.clipQueue = [];
|
|
51
|
+
this.currentClip = null;
|
|
52
|
+
this.clipPosition = 0;
|
|
53
|
+
if (options.Music.length === 0)
|
|
54
|
+
throw new Error('PcmFramePump needs non-empty audio to play.');
|
|
55
|
+
if (!(options.SampleRate > 0))
|
|
56
|
+
throw new Error(`PcmFramePump needs a positive sample rate; got ${options.SampleRate}.`);
|
|
57
|
+
this.music = options.Music;
|
|
58
|
+
this.sampleRate = options.SampleRate;
|
|
59
|
+
this.sink = options.Sink;
|
|
60
|
+
this.loop = options.Loop ?? true;
|
|
61
|
+
this.frameMs = options.FrameMs ?? 20;
|
|
62
|
+
this.samplesPerFrame = Math.max(1, Math.round((this.sampleRate * this.frameMs) / 1000));
|
|
63
|
+
this.leadFrames = Math.max(1, Math.ceil((options.LeadMs ?? 150) / this.frameMs));
|
|
64
|
+
this.maxBurstFrames = this.leadFrames + Math.ceil(MAX_CATCH_UP_MS / this.frameMs);
|
|
65
|
+
this.duckLevel = Math.min(1, Math.max(0, options.DuckLevel ?? 0.15));
|
|
66
|
+
const duckSamples = Math.max(1, Math.round(((options.DuckMs ?? 150) * this.sampleRate) / 1000));
|
|
67
|
+
this.gainStep = (1 - this.duckLevel) / duckSamples;
|
|
68
|
+
this.clock = options.Clock ?? SystemPumpClock;
|
|
69
|
+
this.onEnded = options.OnEnded;
|
|
70
|
+
}
|
|
71
|
+
/** True between {@link Start} and the pump ending (including while paused). */
|
|
72
|
+
get IsRunning() {
|
|
73
|
+
return this.running;
|
|
74
|
+
}
|
|
75
|
+
/** True while paused. */
|
|
76
|
+
get IsPaused() {
|
|
77
|
+
return this.paused;
|
|
78
|
+
}
|
|
79
|
+
/** Frames handed to the sink so far. */
|
|
80
|
+
get FramesSent() {
|
|
81
|
+
return this.framesSent;
|
|
82
|
+
}
|
|
83
|
+
/** Samples per frame at the pump's rate. */
|
|
84
|
+
get SamplesPerFrame() {
|
|
85
|
+
return this.samplesPerFrame;
|
|
86
|
+
}
|
|
87
|
+
/** Starts playback: sends the lead immediately, then paces in real time. A second call is a no-op. */
|
|
88
|
+
Start() {
|
|
89
|
+
if (this.running || this.ended) {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
this.running = true;
|
|
93
|
+
this.restartTimeline();
|
|
94
|
+
this.ticker = this.clock.StartTicker(() => this.tick(), TICK_MS);
|
|
95
|
+
this.tick();
|
|
96
|
+
}
|
|
97
|
+
/** Stops sending frames until {@link Resume}. Playback position, ducking and queued clips are kept. */
|
|
98
|
+
Pause() {
|
|
99
|
+
if (this.running) {
|
|
100
|
+
this.paused = true;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
/** Continues after {@link Pause} from where it left off, on a fresh timeline (so nothing bursts). */
|
|
104
|
+
Resume() {
|
|
105
|
+
if (!this.running || !this.paused) {
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
this.paused = false;
|
|
109
|
+
this.restartTimeline();
|
|
110
|
+
this.tick();
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* How much already-sent audio is still ahead of real time, in ms: the pump keeps the sink up to `LeadMs` ahead, so
|
|
114
|
+
* after the last frame is sent the room is still playing this much. Zero once real time has caught up.
|
|
115
|
+
*/
|
|
116
|
+
get QueuedAheadMs() {
|
|
117
|
+
const sentUntil = this.timelineStart + this.framesOnTimeline * this.frameMs;
|
|
118
|
+
return Math.max(0, sentUntil - this.clock.Now());
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Plays `pcm` over the music: the music fades down to the duck level, the clip plays, the music fades back up.
|
|
122
|
+
* Clips queue behind one another.
|
|
123
|
+
*
|
|
124
|
+
* @param pcm Mono PCM16 at the pump's sample rate.
|
|
125
|
+
* @returns Resolves `true` once the clip has been sent in full, `false` if the pump stopped first or the clip is empty.
|
|
126
|
+
*/
|
|
127
|
+
InsertClip(pcm) {
|
|
128
|
+
if (!this.running || pcm.length === 0) {
|
|
129
|
+
return Promise.resolve(false);
|
|
130
|
+
}
|
|
131
|
+
return new Promise((resolve) => this.clipQueue.push({ Pcm: pcm, Done: resolve }));
|
|
132
|
+
}
|
|
133
|
+
/** Stops the pump for good. Idempotent; pending clips resolve `false`. */
|
|
134
|
+
Stop() {
|
|
135
|
+
this.finish('Stopped');
|
|
136
|
+
}
|
|
137
|
+
restartTimeline() {
|
|
138
|
+
this.timelineStart = this.clock.Now();
|
|
139
|
+
this.framesOnTimeline = 0;
|
|
140
|
+
}
|
|
141
|
+
tick() {
|
|
142
|
+
if (!this.running || this.paused) {
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
try {
|
|
146
|
+
const due = this.framesDue();
|
|
147
|
+
for (let i = 0; i < due && this.running && !this.paused; i++) {
|
|
148
|
+
this.emitFrame();
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
catch (err) {
|
|
152
|
+
LogError(`[PcmFramePump] the sink failed; stopping playback: ${err instanceof Error ? err.message : String(err)}`);
|
|
153
|
+
this.finish('SinkError');
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
/** Frames owed to the sink now, given elapsed time and the lead. Drops the backlog after a long stall. */
|
|
157
|
+
framesDue() {
|
|
158
|
+
const elapsed = this.clock.Now() - this.timelineStart;
|
|
159
|
+
const target = Math.floor(elapsed / this.frameMs) + this.leadFrames;
|
|
160
|
+
const due = target - this.framesOnTimeline;
|
|
161
|
+
if (due > this.maxBurstFrames) {
|
|
162
|
+
this.restartTimeline(); // stalled too long: skip the missed audio rather than flood the sink
|
|
163
|
+
return this.leadFrames;
|
|
164
|
+
}
|
|
165
|
+
return Math.max(0, due);
|
|
166
|
+
}
|
|
167
|
+
emitFrame() {
|
|
168
|
+
const frame = new Int16Array(this.samplesPerFrame);
|
|
169
|
+
for (let i = 0; i < frame.length; i++) {
|
|
170
|
+
frame[i] = FloatToPcm16(this.nextSample());
|
|
171
|
+
}
|
|
172
|
+
this.framesOnTimeline++;
|
|
173
|
+
this.framesSent++;
|
|
174
|
+
this.sink(frame);
|
|
175
|
+
if (this.musicDone && !this.currentClip && this.clipQueue.length === 0) {
|
|
176
|
+
this.finish('Completed');
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
/** The next output sample in [-1, 1]: ducked music plus whatever clip is playing. */
|
|
180
|
+
nextSample() {
|
|
181
|
+
this.advanceGain();
|
|
182
|
+
this.startQueuedClipIfDucked();
|
|
183
|
+
return this.nextMusicSample() * this.gain + this.nextClipSample();
|
|
184
|
+
}
|
|
185
|
+
/** Moves the music gain one step toward its target: the duck level while a clip is queued or playing, else full. */
|
|
186
|
+
advanceGain() {
|
|
187
|
+
const ducking = this.currentClip !== null || this.clipQueue.length > 0;
|
|
188
|
+
const target = ducking ? this.duckLevel : 1;
|
|
189
|
+
if (this.gain > target) {
|
|
190
|
+
this.gain = Math.max(target, this.gain - this.gainStep);
|
|
191
|
+
}
|
|
192
|
+
else if (this.gain < target) {
|
|
193
|
+
this.gain = Math.min(target, this.gain + this.gainStep);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
startQueuedClipIfDucked() {
|
|
197
|
+
if (!this.currentClip && this.clipQueue.length > 0 && this.gain <= this.duckLevel) {
|
|
198
|
+
this.currentClip = this.clipQueue.shift() ?? null;
|
|
199
|
+
this.clipPosition = 0;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
nextMusicSample() {
|
|
203
|
+
if (this.musicDone) {
|
|
204
|
+
return 0;
|
|
205
|
+
}
|
|
206
|
+
const sample = this.music[this.musicPosition] / 32768;
|
|
207
|
+
this.musicPosition++;
|
|
208
|
+
if (this.musicPosition >= this.music.length) {
|
|
209
|
+
this.musicPosition = 0;
|
|
210
|
+
this.musicDone = !this.loop;
|
|
211
|
+
}
|
|
212
|
+
return sample;
|
|
213
|
+
}
|
|
214
|
+
nextClipSample() {
|
|
215
|
+
const clip = this.currentClip;
|
|
216
|
+
if (!clip) {
|
|
217
|
+
return 0;
|
|
218
|
+
}
|
|
219
|
+
const sample = clip.Pcm[this.clipPosition] / 32768;
|
|
220
|
+
this.clipPosition++;
|
|
221
|
+
if (this.clipPosition >= clip.Pcm.length) {
|
|
222
|
+
this.currentClip = null;
|
|
223
|
+
clip.Done(true);
|
|
224
|
+
}
|
|
225
|
+
return sample;
|
|
226
|
+
}
|
|
227
|
+
/** Ends the pump once: cancels the ticker, fails any pending clips, reports the reason. */
|
|
228
|
+
finish(reason) {
|
|
229
|
+
if (this.ended) {
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
this.ended = true;
|
|
233
|
+
this.running = false;
|
|
234
|
+
this.paused = false;
|
|
235
|
+
try {
|
|
236
|
+
this.ticker?.Cancel();
|
|
237
|
+
}
|
|
238
|
+
finally {
|
|
239
|
+
this.ticker = null;
|
|
240
|
+
this.failPendingClips();
|
|
241
|
+
this.onEnded?.(reason);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
failPendingClips() {
|
|
245
|
+
const pending = this.currentClip ? [this.currentClip, ...this.clipQueue] : [...this.clipQueue];
|
|
246
|
+
this.currentClip = null;
|
|
247
|
+
this.clipQueue.length = 0;
|
|
248
|
+
for (const clip of pending) {
|
|
249
|
+
clip.Done(false);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
//# sourceMappingURL=pcm-frame-pump.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pcm-frame-pump.js","sourceRoot":"","sources":["../../src/room-audio/pcm-frame-pump.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview {@link RoomAudioDecodeCache} — a small LRU of ready-to-play PCM keyed by source (an `MJ: Files` id or
|
|
3
|
+
* a URL), so every caller placed on hold in the same queue does not re-download and re-decode the same file.
|
|
4
|
+
*
|
|
5
|
+
* It caches the in-flight PROMISE, so ten callers arriving at once share one download. A failed load is evicted so
|
|
6
|
+
* the next caller retries. Bounded both by entry count and by total samples held.
|
|
7
|
+
*
|
|
8
|
+
* @module @memberjunction/livekit-room-server
|
|
9
|
+
*/
|
|
10
|
+
/** Default maximum number of cached sources. */
|
|
11
|
+
export declare const DEFAULT_DECODE_CACHE_ENTRIES = 8;
|
|
12
|
+
/** Default maximum total samples held across all entries (128 Mi samples = 256 MB of PCM16). */
|
|
13
|
+
export declare const DEFAULT_DECODE_CACHE_MAX_SAMPLES: number;
|
|
14
|
+
/** A bounded, promise-sharing LRU of decoded PCM. */
|
|
15
|
+
export declare class RoomAudioDecodeCache {
|
|
16
|
+
private readonly maxEntries;
|
|
17
|
+
private readonly maxSamples;
|
|
18
|
+
private readonly slots;
|
|
19
|
+
/**
|
|
20
|
+
* @param maxEntries Maximum number of cached sources.
|
|
21
|
+
* @param maxSamples Maximum total samples held across all resolved entries.
|
|
22
|
+
*/
|
|
23
|
+
constructor(maxEntries?: number, maxSamples?: number);
|
|
24
|
+
/** How many sources are cached (or loading). */
|
|
25
|
+
get Size(): number;
|
|
26
|
+
/**
|
|
27
|
+
* Returns the cached PCM for `key`, or runs `load` once and caches its result. Concurrent callers for the same key
|
|
28
|
+
* share one load. A rejected load is removed so the next call retries.
|
|
29
|
+
*/
|
|
30
|
+
GetOrLoad(key: string, load: () => Promise<Int16Array>): Promise<Int16Array>;
|
|
31
|
+
/** Empties the cache. */
|
|
32
|
+
Clear(): void;
|
|
33
|
+
/** Drops least-recently-used entries until both bounds hold (never the most recent one). */
|
|
34
|
+
private evictOverflow;
|
|
35
|
+
private totalSamples;
|
|
36
|
+
}
|
|
37
|
+
//# sourceMappingURL=room-audio-decode-cache.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"room-audio-decode-cache.d.ts","sourceRoot":"","sources":["../../src/room-audio/room-audio-decode-cache.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,gDAAgD;AAChD,eAAO,MAAM,4BAA4B,IAAI,CAAC;AAE9C,gGAAgG;AAChG,eAAO,MAAM,gCAAgC,QAAoB,CAAC;AAQlE,qDAAqD;AACrD,qBAAa,oBAAoB;IAQ7B,OAAO,CAAC,QAAQ,CAAC,UAAU;IAC3B,OAAO,CAAC,QAAQ,CAAC,UAAU;IAR7B,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAgC;IAEtD;;;OAGG;gBAEgB,UAAU,GAAE,MAAqC,EACjD,UAAU,GAAE,MAAyC;IAGxE,gDAAgD;IAChD,IAAW,IAAI,IAAI,MAAM,CAExB;IAED;;;OAGG;IACI,SAAS,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,OAAO,CAAC,UAAU,CAAC,GAAG,OAAO,CAAC,UAAU,CAAC;IAwBnF,yBAAyB;IAClB,KAAK,IAAI,IAAI;IAIpB,4FAA4F;IAC5F,OAAO,CAAC,aAAa;IAUrB,OAAO,CAAC,YAAY;CAOrB"}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview {@link RoomAudioDecodeCache} — a small LRU of ready-to-play PCM keyed by source (an `MJ: Files` id or
|
|
3
|
+
* a URL), so every caller placed on hold in the same queue does not re-download and re-decode the same file.
|
|
4
|
+
*
|
|
5
|
+
* It caches the in-flight PROMISE, so ten callers arriving at once share one download. A failed load is evicted so
|
|
6
|
+
* the next caller retries. Bounded both by entry count and by total samples held.
|
|
7
|
+
*
|
|
8
|
+
* @module @memberjunction/livekit-room-server
|
|
9
|
+
*/
|
|
10
|
+
/** Default maximum number of cached sources. */
|
|
11
|
+
export const DEFAULT_DECODE_CACHE_ENTRIES = 8;
|
|
12
|
+
/** Default maximum total samples held across all entries (128 Mi samples = 256 MB of PCM16). */
|
|
13
|
+
export const DEFAULT_DECODE_CACHE_MAX_SAMPLES = 128 * 1024 * 1024;
|
|
14
|
+
/** A bounded, promise-sharing LRU of decoded PCM. */
|
|
15
|
+
export class RoomAudioDecodeCache {
|
|
16
|
+
/**
|
|
17
|
+
* @param maxEntries Maximum number of cached sources.
|
|
18
|
+
* @param maxSamples Maximum total samples held across all resolved entries.
|
|
19
|
+
*/
|
|
20
|
+
constructor(maxEntries = DEFAULT_DECODE_CACHE_ENTRIES, maxSamples = DEFAULT_DECODE_CACHE_MAX_SAMPLES) {
|
|
21
|
+
this.maxEntries = maxEntries;
|
|
22
|
+
this.maxSamples = maxSamples;
|
|
23
|
+
this.slots = new Map();
|
|
24
|
+
}
|
|
25
|
+
/** How many sources are cached (or loading). */
|
|
26
|
+
get Size() {
|
|
27
|
+
return this.slots.size;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Returns the cached PCM for `key`, or runs `load` once and caches its result. Concurrent callers for the same key
|
|
31
|
+
* share one load. A rejected load is removed so the next call retries.
|
|
32
|
+
*/
|
|
33
|
+
GetOrLoad(key, load) {
|
|
34
|
+
const existing = this.slots.get(key);
|
|
35
|
+
if (existing) {
|
|
36
|
+
this.slots.delete(key); // re-insert: Map order is the recency order
|
|
37
|
+
this.slots.set(key, existing);
|
|
38
|
+
return existing.Load;
|
|
39
|
+
}
|
|
40
|
+
const slot = { Load: load(), Samples: 0 };
|
|
41
|
+
this.slots.set(key, slot);
|
|
42
|
+
this.evictOverflow();
|
|
43
|
+
slot.Load.then((pcm) => {
|
|
44
|
+
slot.Samples = pcm.length;
|
|
45
|
+
this.evictOverflow();
|
|
46
|
+
}, () => {
|
|
47
|
+
if (this.slots.get(key) === slot) {
|
|
48
|
+
this.slots.delete(key); // the caller sees the rejection; the cache just forgets it
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
return slot.Load;
|
|
52
|
+
}
|
|
53
|
+
/** Empties the cache. */
|
|
54
|
+
Clear() {
|
|
55
|
+
this.slots.clear();
|
|
56
|
+
}
|
|
57
|
+
/** Drops least-recently-used entries until both bounds hold (never the most recent one). */
|
|
58
|
+
evictOverflow() {
|
|
59
|
+
while (this.slots.size > 1 && (this.slots.size > this.maxEntries || this.totalSamples() > this.maxSamples)) {
|
|
60
|
+
const oldest = this.slots.keys().next().value;
|
|
61
|
+
if (oldest === undefined) {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
this.slots.delete(oldest);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
totalSamples() {
|
|
68
|
+
let total = 0;
|
|
69
|
+
for (const slot of this.slots.values()) {
|
|
70
|
+
total += slot.Samples;
|
|
71
|
+
}
|
|
72
|
+
return total;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
//# sourceMappingURL=room-audio-decode-cache.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"room-audio-decode-cache.js","sourceRoot":"","sources":["../../src/room-audio/room-audio-decode-cache.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,gDAAgD;AAChD,MAAM,CAAC,MAAM,4BAA4B,GAAG,CAAC,CAAC;AAE9C,gGAAgG;AAChG,MAAM,CAAC,MAAM,gCAAgC,GAAG,GAAG,GAAG,IAAI,GAAG,IAAI,CAAC;AAQlE,qDAAqD;AACrD,MAAM,OAAO,oBAAoB;IAG/B;;;OAGG;IACH,YACmB,aAAqB,4BAA4B,EACjD,aAAqB,gCAAgC;QADrD,eAAU,GAAV,UAAU,CAAuC;QACjD,eAAU,GAAV,UAAU,CAA2C;QARvD,UAAK,GAAG,IAAI,GAAG,EAAqB,CAAC;IASnD,CAAC;IAEJ,gDAAgD;IAChD,IAAW,IAAI;QACb,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC;IACzB,CAAC;IAED;;;OAGG;IACI,SAAS,CAAC,GAAW,EAAE,IAA+B;QAC3D,MAAM,QAAQ,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QACrC,IAAI,QAAQ,EAAE,CAAC;YACb,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,4CAA4C;YACpE,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,EAAE,QAAQ,CAAC,CAAC;YAC9B,OAAO,QAAQ,CAAC,IAAI,CAAC;QACvB,CAAC;QACD,MAAM,IAAI,GAAc,EAAE,IAAI,EAAE,IAAI,EAAE,EAAE,OAAO,EAAE,CAAC,EAAE,CAAC;QACrD,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;QAC1B,IAAI,CAAC,aAAa,EAAE,CAAC;QACrB,IAAI,CAAC,IAAI,CAAC,IAAI,CACZ,CAAC,GAAG,EAAE,EAAE;YACN,IAAI,CAAC,OAAO,GAAG,GAAG,CAAC,MAAM,CAAC;YAC1B,IAAI,CAAC,aAAa,EAAE,CAAC;QACvB,CAAC,EACD,GAAG,EAAE;YACH,IAAI,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,KAAK,IAAI,EAAE,CAAC;gBACjC,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,2DAA2D;YACrF,CAAC;QACH,CAAC,CACF,CAAC;QACF,OAAO,IAAI,CAAC,IAAI,CAAC;IACnB,CAAC;IAED,yBAAyB;IAClB,KAAK;QACV,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC;IACrB,CAAC;IAED,4FAA4F;IACpF,aAAa;QACnB,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,GAAG,IAAI,CAAC,UAAU,IAAI,IAAI,CAAC,YAAY,EAAE,GAAG,IAAI,CAAC,UAAU,CAAC,EAAE,CAAC;YAC3G,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC;YAC9C,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;gBACzB,OAAO;YACT,CAAC;YACD,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;QAC5B,CAAC;IACH,CAAC;IAEO,YAAY;QAClB,IAAI,KAAK,GAAG,CAAC,CAAC;QACd,KAAK,MAAM,IAAI,IAAI,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,EAAE,CAAC;YACvC,KAAK,IAAI,IAAI,CAAC,OAAO,CAAC;QACxB,CAAC;QACD,OAAO,KAAK,CAAC;IACf,CAAC;CACF"}
|
|
@@ -0,0 +1,178 @@
|
|
|
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 { BaseSingleton } from '@memberjunction/global';
|
|
18
|
+
import { type IMetadataProvider, type UserInfo } from '@memberjunction/core';
|
|
19
|
+
import { type NativeModuleLoader } from '@memberjunction/ai-bridge-livekit';
|
|
20
|
+
import { LiveKitTokenService } from '../livekit-token-service.js';
|
|
21
|
+
import { type PumpClock } from './pcm-frame-pump.js';
|
|
22
|
+
import { type RoomAudioFileReader, type RoomAudioUrlFetcher } from './room-audio-sources.js';
|
|
23
|
+
import type { IRoomSpeechSynthesizer } from './room-speech-synthesizer.js';
|
|
24
|
+
/** The rate the player publishes at (Hz). Music sounds noticeably duller at the 24 kHz the agent voice uses. */
|
|
25
|
+
export declare const ROOM_AUDIO_SAMPLE_RATE = 48000;
|
|
26
|
+
/** What to play: an `MJ: Files` row, an https URL, raw mono PCM16, or the built-in comfort tone. */
|
|
27
|
+
export type RoomAudioSource = {
|
|
28
|
+
Kind: 'File';
|
|
29
|
+
FileID: string;
|
|
30
|
+
} | {
|
|
31
|
+
Kind: 'Url';
|
|
32
|
+
Url: string;
|
|
33
|
+
} | {
|
|
34
|
+
Kind: 'Pcm';
|
|
35
|
+
Pcm: Int16Array;
|
|
36
|
+
SampleRate: number;
|
|
37
|
+
} | {
|
|
38
|
+
Kind: 'ComfortTone';
|
|
39
|
+
};
|
|
40
|
+
/** Parameters for {@link RoomAudioPlayer.Start}. */
|
|
41
|
+
export interface StartRoomAudioParams {
|
|
42
|
+
/** The LiveKit room to play into. */
|
|
43
|
+
RoomName: string;
|
|
44
|
+
/** What to play. */
|
|
45
|
+
Source: RoomAudioSource;
|
|
46
|
+
/** Loop the audio until stopped. Default `true`; when `false` the bot leaves the room once the audio ends. */
|
|
47
|
+
Loop?: boolean;
|
|
48
|
+
/** The bot's display name in the room. Default `'On hold'`. */
|
|
49
|
+
DisplayName?: string;
|
|
50
|
+
/** The bot's participant identity. Default `hold-<playback id>`. */
|
|
51
|
+
Identity?: string;
|
|
52
|
+
/** The user the file load (and any announcement synthesis) runs as. */
|
|
53
|
+
ContextUser: UserInfo;
|
|
54
|
+
/** The metadata provider for the file load. Default: the global provider. */
|
|
55
|
+
Provider?: IMetadataProvider;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Why a playback ended, as reported by {@link RoomAudioHandle.Ended}:
|
|
59
|
+
* - `Completed`: non-looping audio played to its end (every frame went to the room).
|
|
60
|
+
* - `Stopped`: {@link RoomAudioHandle.Stop} was called (or {@link RoomAudioPlayer.StopAllInRoom}).
|
|
61
|
+
* - `Failed`: the bot's audio sink failed mid-playback, so the audio was cut short.
|
|
62
|
+
* - `Disconnected`: the server dropped the bot (room closed, participant removed) before the audio ended.
|
|
63
|
+
*/
|
|
64
|
+
export type RoomAudioEndReason = 'Completed' | 'Stopped' | 'Failed' | 'Disconnected';
|
|
65
|
+
/** An announcement: text to speak through the speech synthesizer, or ready-made mono PCM16. */
|
|
66
|
+
export type RoomAudioClip = {
|
|
67
|
+
Text: string;
|
|
68
|
+
} | {
|
|
69
|
+
Pcm: Int16Array;
|
|
70
|
+
SampleRate: number;
|
|
71
|
+
};
|
|
72
|
+
/** A live playback in one room. */
|
|
73
|
+
export interface RoomAudioHandle {
|
|
74
|
+
/** This playback's id (also the default identity suffix of its bot). */
|
|
75
|
+
readonly PlaybackID: string;
|
|
76
|
+
/** The room it plays into. */
|
|
77
|
+
readonly RoomName: string;
|
|
78
|
+
/** True while audio is being sent (false once paused or stopped). */
|
|
79
|
+
readonly IsPlaying: boolean;
|
|
80
|
+
/** Why the playback ended, or `null` while it is still live. */
|
|
81
|
+
readonly EndReason: RoomAudioEndReason | null;
|
|
82
|
+
/**
|
|
83
|
+
* Resolves once the playback has ended and its bot has left the room, with the reason. Only `Completed` means the
|
|
84
|
+
* whole clip reached the room — use it to gate anything that must follow a full play, e.g. starting a recording
|
|
85
|
+
* only after a recording disclosure has been heard. A looping playback never completes on its own; it resolves
|
|
86
|
+
* `Stopped` when stopped. The first reason wins. Never rejects.
|
|
87
|
+
*/
|
|
88
|
+
readonly Ended: Promise<RoomAudioEndReason>;
|
|
89
|
+
/**
|
|
90
|
+
* Plays a clip over the music, ducking the music while it plays. Resolves `true` once the clip has been sent in
|
|
91
|
+
* full; `false` when it could not be played (no speech synthesizer installed, synthesis failed, or the playback
|
|
92
|
+
* stopped first). Never rejects.
|
|
93
|
+
*/
|
|
94
|
+
Announce(clip: RoomAudioClip): Promise<boolean>;
|
|
95
|
+
/** Stops sending audio, keeping the position. */
|
|
96
|
+
Pause(): void;
|
|
97
|
+
/** Continues after {@link Pause}. */
|
|
98
|
+
Resume(): void;
|
|
99
|
+
/** Stops playback and disconnects the bot. Idempotent; never rejects. */
|
|
100
|
+
Stop(): Promise<void>;
|
|
101
|
+
}
|
|
102
|
+
/** The part of {@link LiveKitTokenService} the player uses — a seam for tests. */
|
|
103
|
+
export type RoomAudioTokenMinter = Pick<LiveKitTokenService, 'MintToken'>;
|
|
104
|
+
/**
|
|
105
|
+
* Plays audio into LiveKit rooms. A process-wide singleton so the playback registry (and the `room_finished` cleanup
|
|
106
|
+
* that reads it) is shared by every caller.
|
|
107
|
+
*/
|
|
108
|
+
export declare class RoomAudioPlayer extends BaseSingleton<RoomAudioPlayer> {
|
|
109
|
+
private readonly active;
|
|
110
|
+
private readonly decodeCache;
|
|
111
|
+
private tokenMinter;
|
|
112
|
+
private loadNativeModule;
|
|
113
|
+
private nativeModuleSpecifierOverride?;
|
|
114
|
+
private fileReader;
|
|
115
|
+
private urlFetcher;
|
|
116
|
+
private clock;
|
|
117
|
+
private speechSynthesizer?;
|
|
118
|
+
private warnedNoSynthesizer;
|
|
119
|
+
private comfortTone?;
|
|
120
|
+
/** BaseSingleton requires a protected constructor. */
|
|
121
|
+
protected constructor();
|
|
122
|
+
/** The process-wide player. */
|
|
123
|
+
static get Instance(): RoomAudioPlayer;
|
|
124
|
+
/**
|
|
125
|
+
* Installs the text-to-speech used by {@link RoomAudioHandle.Announce} with `{ Text }` — e.g. an
|
|
126
|
+
* `MJRoomSpeechSynthesizer`. None is installed by default (a voice is vendor-specific); without one a text
|
|
127
|
+
* announcement returns `false` and the player logs the omission once.
|
|
128
|
+
*
|
|
129
|
+
* @param synthesizer The synthesizer, or `undefined` to remove it.
|
|
130
|
+
*/
|
|
131
|
+
SetSpeechSynthesizer(synthesizer: IRoomSpeechSynthesizer | undefined): void;
|
|
132
|
+
/** Overrides where bot tokens come from (tests; a host with its own credentials). */
|
|
133
|
+
SetTokenService(minter: RoomAudioTokenMinter): void;
|
|
134
|
+
/** Overrides the native room-module loader (tests inject a fake module). */
|
|
135
|
+
SetNativeModuleLoader(loader: NativeModuleLoader): void;
|
|
136
|
+
/** Overrides the native room-module specifier; `undefined` returns to `LIVEKIT_NATIVE_MODULE` / the default. */
|
|
137
|
+
SetNativeModuleSpecifier(specifier: string | undefined): void;
|
|
138
|
+
/** Overrides how `MJ: Files` bytes are read (tests). */
|
|
139
|
+
SetFileReader(reader: RoomAudioFileReader): void;
|
|
140
|
+
/** Overrides how URLs are fetched (tests). */
|
|
141
|
+
SetUrlFetcher(fetcher: RoomAudioUrlFetcher): void;
|
|
142
|
+
/** Overrides the pacing clock (tests). */
|
|
143
|
+
SetClock(clock: PumpClock): void;
|
|
144
|
+
/** Forgets every cached decode (e.g. after a hold file is replaced under the same id). */
|
|
145
|
+
ClearDecodeCache(): void;
|
|
146
|
+
/**
|
|
147
|
+
* Starts playing `params.Source` into `params.RoomName`: loads and decodes it (cached by source), joins the room as
|
|
148
|
+
* a publish-only bot, and paces the audio in.
|
|
149
|
+
*
|
|
150
|
+
* @returns The live playback.
|
|
151
|
+
* @throws {Error} when the source cannot be loaded or decoded, LiveKit is not configured, or the bot cannot join.
|
|
152
|
+
*/
|
|
153
|
+
Start(params: StartRoomAudioParams): Promise<RoomAudioHandle>;
|
|
154
|
+
/** The live playbacks in a room (room names compare case-insensitively). */
|
|
155
|
+
GetActive(roomName: string): RoomAudioHandle[];
|
|
156
|
+
/**
|
|
157
|
+
* Stops every playback in a room. Never throws.
|
|
158
|
+
*
|
|
159
|
+
* @returns How many playbacks were stopped.
|
|
160
|
+
*/
|
|
161
|
+
StopAllInRoom(roomName: string): Promise<number>;
|
|
162
|
+
/** Resolves the source to mono PCM16 at {@link ROOM_AUDIO_SAMPLE_RATE}, through the decode cache where it can. */
|
|
163
|
+
private resolvePcm;
|
|
164
|
+
private decodeContent;
|
|
165
|
+
/** Joins the room as the playback's bot, registers the playback, and starts the pump. */
|
|
166
|
+
private joinAndPlay;
|
|
167
|
+
/** A publish-only bot token: it never needs to hear the room, and it carries no data channel. */
|
|
168
|
+
private mintBotToken;
|
|
169
|
+
/** Connects the bot; on failure always releases the client before rethrowing. */
|
|
170
|
+
private connectClient;
|
|
171
|
+
private createPump;
|
|
172
|
+
private playbackOwner;
|
|
173
|
+
private deregister;
|
|
174
|
+
/** The server dropped the bot (room closed, participant removed): stop and forget the playback. */
|
|
175
|
+
private handleServerDisconnect;
|
|
176
|
+
private synthesize;
|
|
177
|
+
}
|
|
178
|
+
//# sourceMappingURL=room-audio-player.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"room-audio-player.d.ts","sourceRoot":"","sources":["../../src/room-audio/room-audio-player.ts"],"names":[],"mappings":"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"}
|