@memberjunction/livekit-room-server 6.2.0-edge.1 → 6.2.0-edge.3

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 (78) hide show
  1. package/README.md +85 -0
  2. package/dist/index.d.ts +17 -2
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +17 -1
  5. package/dist/index.js.map +1 -1
  6. package/dist/livekit-agent-room-coordinator.d.ts +105 -8
  7. package/dist/livekit-agent-room-coordinator.d.ts.map +1 -1
  8. package/dist/livekit-agent-room-coordinator.js +125 -25
  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 +20 -0
  17. package/dist/livekit-native-module.js.map +1 -0
  18. package/dist/livekit-sip-service.d.ts +141 -0
  19. package/dist/livekit-sip-service.d.ts.map +1 -0
  20. package/dist/livekit-sip-service.js +169 -0
  21. package/dist/livekit-sip-service.js.map +1 -0
  22. package/dist/livekit-token-service.d.ts +6 -0
  23. package/dist/livekit-token-service.d.ts.map +1 -1
  24. package/dist/livekit-token-service.js +8 -0
  25. package/dist/livekit-token-service.js.map +1 -1
  26. package/dist/livekit-webhook.d.ts +65 -0
  27. package/dist/livekit-webhook.d.ts.map +1 -0
  28. package/dist/livekit-webhook.js +65 -0
  29. package/dist/livekit-webhook.js.map +1 -0
  30. package/dist/meeting-dial-in-service.d.ts +75 -0
  31. package/dist/meeting-dial-in-service.d.ts.map +1 -0
  32. package/dist/meeting-dial-in-service.js +175 -0
  33. package/dist/meeting-dial-in-service.js.map +1 -0
  34. package/dist/room-audio/audio-decoder.d.ts +40 -0
  35. package/dist/room-audio/audio-decoder.d.ts.map +1 -0
  36. package/dist/room-audio/audio-decoder.js +205 -0
  37. package/dist/room-audio/audio-decoder.js.map +1 -0
  38. package/dist/room-audio/comfort-tone.d.ts +23 -0
  39. package/dist/room-audio/comfort-tone.d.ts.map +1 -0
  40. package/dist/room-audio/comfort-tone.js +70 -0
  41. package/dist/room-audio/comfort-tone.js.map +1 -0
  42. package/dist/room-audio/pcm-frame-pump.d.ts +134 -0
  43. package/dist/room-audio/pcm-frame-pump.d.ts.map +1 -0
  44. package/dist/room-audio/pcm-frame-pump.js +253 -0
  45. package/dist/room-audio/pcm-frame-pump.js.map +1 -0
  46. package/dist/room-audio/room-audio-decode-cache.d.ts +37 -0
  47. package/dist/room-audio/room-audio-decode-cache.d.ts.map +1 -0
  48. package/dist/room-audio/room-audio-decode-cache.js +75 -0
  49. package/dist/room-audio/room-audio-decode-cache.js.map +1 -0
  50. package/dist/room-audio/room-audio-player.d.ts +178 -0
  51. package/dist/room-audio/room-audio-player.d.ts.map +1 -0
  52. package/dist/room-audio/room-audio-player.js +404 -0
  53. package/dist/room-audio/room-audio-player.js.map +1 -0
  54. package/dist/room-audio/room-audio-sources.d.ts +47 -0
  55. package/dist/room-audio/room-audio-sources.d.ts.map +1 -0
  56. package/dist/room-audio/room-audio-sources.js +116 -0
  57. package/dist/room-audio/room-audio-sources.js.map +1 -0
  58. package/dist/room-audio/room-speech-synthesizer.d.ts +55 -0
  59. package/dist/room-audio/room-speech-synthesizer.d.ts.map +1 -0
  60. package/dist/room-audio/room-speech-synthesizer.js +56 -0
  61. package/dist/room-audio/room-speech-synthesizer.js.map +1 -0
  62. package/dist/room-authorization.d.ts +58 -0
  63. package/dist/room-authorization.d.ts.map +1 -0
  64. package/dist/room-authorization.js +212 -0
  65. package/dist/room-authorization.js.map +1 -0
  66. package/dist/room-handoff/handoff-offer-registry.d.ts +137 -0
  67. package/dist/room-handoff/handoff-offer-registry.d.ts.map +1 -0
  68. package/dist/room-handoff/handoff-offer-registry.js +521 -0
  69. package/dist/room-handoff/handoff-offer-registry.js.map +1 -0
  70. package/dist/room-handoff/handoff-types.d.ts +170 -0
  71. package/dist/room-handoff/handoff-types.d.ts.map +1 -0
  72. package/dist/room-handoff/handoff-types.js +16 -0
  73. package/dist/room-handoff/handoff-types.js.map +1 -0
  74. package/dist/room-handoff/room-handoff-engine.d.ts +141 -0
  75. package/dist/room-handoff/room-handoff-engine.d.ts.map +1 -0
  76. package/dist/room-handoff/room-handoff-engine.js +530 -0
  77. package/dist/room-handoff/room-handoff-engine.js.map +1 -0
  78. package/package.json +14 -9
@@ -0,0 +1,75 @@
1
+ /**
2
+ * @fileoverview Server-side service for verifying and generating phone dial-in codes for meetings.
3
+ *
4
+ * Security architecture:
5
+ * - Dial-in codes are NOT verified through a public GraphQL mutation.
6
+ * - Verification is performed server-side by the LiveKit SIP inbound path upon receiving verified webhooks.
7
+ * - Dual rate-limiting:
8
+ * 1. Keyed on the caller's verified phone number (`CallerNumber`).
9
+ * 2. Keyed on the dialed trunk number (`DialedNumber`) to prevent distributed brute-forcing against a single line.
10
+ * - Code generation uses `crypto.randomInt` (cryptographically secure pseudo-random number generator), never `Math.random`.
11
+ *
12
+ * @module @memberjunction/livekit-room-server
13
+ */
14
+ import { BaseSingleton } from '@memberjunction/global';
15
+ import { type IMetadataProvider, type UserInfo } from '@memberjunction/core';
16
+ /** Rate limiter with configurable attempt limits and lockout windows. */
17
+ export declare class DialInRateLimiter {
18
+ private readonly maxAttempts;
19
+ private readonly windowMs;
20
+ private readonly lockoutMs;
21
+ private readonly attempts;
22
+ constructor(maxAttempts?: number, windowMs?: number, lockoutMs?: number);
23
+ CheckAllowed(key: string): {
24
+ allowed: boolean;
25
+ remainingMs?: number;
26
+ };
27
+ RecordFailure(key: string): void;
28
+ RecordSuccess(key: string): void;
29
+ Clear(): void;
30
+ }
31
+ /** Input parameters for verifying a dial-in code. */
32
+ export interface VerifyDialInCodeParams {
33
+ /** The verified caller number from the SIP carrier webhook. */
34
+ CallerNumber: string;
35
+ /** The dialed phone number or Phone Numbers row ID. */
36
+ DialedNumber: string;
37
+ /** The DTMF dial-in code entered by the caller. */
38
+ DialInCode: string;
39
+ /** Optional authenticated context user. */
40
+ ContextUser?: UserInfo;
41
+ /** Optional metadata provider. */
42
+ Provider?: IMetadataProvider;
43
+ }
44
+ /** Result of dial-in code verification. */
45
+ export interface VerifyDialInCodeOutcome {
46
+ Success: boolean;
47
+ ErrorMessage?: string;
48
+ RoomName?: string;
49
+ MeetingID?: string;
50
+ }
51
+ /**
52
+ * Server-side singleton service for meeting dial-in verification and code generation.
53
+ */
54
+ export declare class MeetingDialInService extends BaseSingleton<MeetingDialInService> {
55
+ private readonly callerLimiter;
56
+ private readonly dialedLimiter;
57
+ constructor();
58
+ static get Instance(): MeetingDialInService;
59
+ /**
60
+ * Generates a cryptographically secure 6-digit numeric dial-in code.
61
+ */
62
+ GenerateDialInCode(): string;
63
+ /**
64
+ * Generates a unique 6-digit dial-in code for a given phone number among active/scheduled meetings.
65
+ */
66
+ GenerateUniqueDialInCode(phoneNumberID: string, user: UserInfo, provider?: IMetadataProvider): Promise<string | undefined>;
67
+ /**
68
+ * Verifies a DTMF dial-in code against active/scheduled meetings on the dialed line.
69
+ * Rate limits on both the verified caller number and the dialed number.
70
+ */
71
+ VerifyDialInCode(params: VerifyDialInCodeParams): Promise<VerifyDialInCodeOutcome>;
72
+ /** Clears rate limiter entries (test harness). */
73
+ ClearRateLimitsForTesting(): void;
74
+ }
75
+ //# sourceMappingURL=meeting-dial-in-service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"meeting-dial-in-service.d.ts","sourceRoot":"","sources":["../src/meeting-dial-in-service.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAGH,OAAO,EAAE,aAAa,EAAmB,MAAM,wBAAwB,CAAC;AACxE,OAAO,EAAqB,KAAK,iBAAiB,EAAE,KAAK,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AAUhG,yEAAyE;AACzE,qBAAa,iBAAiB;IAI1B,OAAO,CAAC,QAAQ,CAAC,WAAW;IAC5B,OAAO,CAAC,QAAQ,CAAC,QAAQ;IACzB,OAAO,CAAC,QAAQ,CAAC,SAAS;IAL5B,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAqC;gBAG3C,WAAW,GAAE,MAAU,EACvB,QAAQ,GAAE,MAAuB,EACjC,SAAS,GAAE,MAAuB;IAG9C,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,WAAW,CAAC,EAAE,MAAM,CAAA;KAAE;IAoBrE,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI;IAahC,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI;IAIhC,KAAK,IAAI,IAAI;CAGrB;AAED,qDAAqD;AACrD,MAAM,WAAW,sBAAsB;IACrC,+DAA+D;IAC/D,YAAY,EAAE,MAAM,CAAC;IACrB,uDAAuD;IACvD,YAAY,EAAE,MAAM,CAAC;IACrB,mDAAmD;IACnD,UAAU,EAAE,MAAM,CAAC;IACnB,2CAA2C;IAC3C,WAAW,CAAC,EAAE,QAAQ,CAAC;IACvB,kCAAkC;IAClC,QAAQ,CAAC,EAAE,iBAAiB,CAAC;CAC9B;AAED,2CAA2C;AAC3C,MAAM,WAAW,uBAAuB;IACtC,OAAO,EAAE,OAAO,CAAC;IACjB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;GAEG;AACH,qBAAa,oBAAqB,SAAQ,aAAa,CAAC,oBAAoB,CAAC;IAE3E,OAAO,CAAC,QAAQ,CAAC,aAAa,CAA4D;IAE1F,OAAO,CAAC,QAAQ,CAAC,aAAa,CAA6D;;IAM3F,WAAkB,QAAQ,IAAI,oBAAoB,CAEjD;IAED;;OAEG;IACI,kBAAkB,IAAI,MAAM;IAInC;;OAEG;IACU,wBAAwB,CACnC,aAAa,EAAE,MAAM,EACrB,IAAI,EAAE,QAAQ,EACd,QAAQ,CAAC,EAAE,iBAAiB,GAC3B,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC;IAqB9B;;;OAGG;IACU,gBAAgB,CAAC,MAAM,EAAE,sBAAsB,GAAG,OAAO,CAAC,uBAAuB,CAAC;IA0E/F,kDAAkD;IAC3C,yBAAyB,IAAI,IAAI;CAIzC"}
@@ -0,0 +1,175 @@
1
+ /**
2
+ * @fileoverview Server-side service for verifying and generating phone dial-in codes for meetings.
3
+ *
4
+ * Security architecture:
5
+ * - Dial-in codes are NOT verified through a public GraphQL mutation.
6
+ * - Verification is performed server-side by the LiveKit SIP inbound path upon receiving verified webhooks.
7
+ * - Dual rate-limiting:
8
+ * 1. Keyed on the caller's verified phone number (`CallerNumber`).
9
+ * 2. Keyed on the dialed trunk number (`DialedNumber`) to prevent distributed brute-forcing against a single line.
10
+ * - Code generation uses `crypto.randomInt` (cryptographically secure pseudo-random number generator), never `Math.random`.
11
+ *
12
+ * @module @memberjunction/livekit-room-server
13
+ */
14
+ import { randomInt } from 'node:crypto';
15
+ import { BaseSingleton, EscapeSQLString } from '@memberjunction/global';
16
+ import { LogError, RunView } from '@memberjunction/core';
17
+ /** Rate limiter with configurable attempt limits and lockout windows. */
18
+ export class DialInRateLimiter {
19
+ constructor(maxAttempts = 5, windowMs = 15 * 60 * 1000, lockoutMs = 15 * 60 * 1000) {
20
+ this.maxAttempts = maxAttempts;
21
+ this.windowMs = windowMs;
22
+ this.lockoutMs = lockoutMs;
23
+ this.attempts = new Map();
24
+ }
25
+ CheckAllowed(key) {
26
+ const entry = this.attempts.get(key);
27
+ if (!entry) {
28
+ return { allowed: true };
29
+ }
30
+ const now = Date.now();
31
+ if (entry.lockedUntil && entry.lockedUntil > now) {
32
+ return { allowed: false, remainingMs: entry.lockedUntil - now };
33
+ }
34
+ if (now - entry.firstAttemptAt > this.windowMs) {
35
+ this.attempts.delete(key);
36
+ return { allowed: true };
37
+ }
38
+ if (entry.attempts >= this.maxAttempts) {
39
+ entry.lockedUntil = now + this.lockoutMs;
40
+ return { allowed: false, remainingMs: this.lockoutMs };
41
+ }
42
+ return { allowed: true };
43
+ }
44
+ RecordFailure(key) {
45
+ const now = Date.now();
46
+ const entry = this.attempts.get(key);
47
+ if (!entry || now - entry.firstAttemptAt > this.windowMs) {
48
+ this.attempts.set(key, { attempts: 1, firstAttemptAt: now });
49
+ }
50
+ else {
51
+ entry.attempts++;
52
+ if (entry.attempts >= this.maxAttempts) {
53
+ entry.lockedUntil = now + this.lockoutMs;
54
+ }
55
+ }
56
+ }
57
+ RecordSuccess(key) {
58
+ this.attempts.delete(key);
59
+ }
60
+ Clear() {
61
+ this.attempts.clear();
62
+ }
63
+ }
64
+ /**
65
+ * Server-side singleton service for meeting dial-in verification and code generation.
66
+ */
67
+ export class MeetingDialInService extends BaseSingleton {
68
+ constructor() {
69
+ super();
70
+ // Caller-based limiter: 5 attempts per 15 minutes
71
+ this.callerLimiter = new DialInRateLimiter(5, 15 * 60 * 1000, 15 * 60 * 1000);
72
+ // Dialed-number limiter: 20 failed attempts per 15 minutes across all callers
73
+ this.dialedLimiter = new DialInRateLimiter(20, 15 * 60 * 1000, 15 * 60 * 1000);
74
+ }
75
+ static get Instance() {
76
+ return MeetingDialInService.getInstance();
77
+ }
78
+ /**
79
+ * Generates a cryptographically secure 6-digit numeric dial-in code.
80
+ */
81
+ GenerateDialInCode() {
82
+ return randomInt(100000, 1000000).toString();
83
+ }
84
+ /**
85
+ * Generates a unique 6-digit dial-in code for a given phone number among active/scheduled meetings.
86
+ */
87
+ async GenerateUniqueDialInCode(phoneNumberID, user, provider) {
88
+ const rv = provider ? RunView.FromMetadataProvider(provider) : new RunView();
89
+ const escapedPhoneID = EscapeSQLString(phoneNumberID);
90
+ for (let attempt = 0; attempt < 10; attempt++) {
91
+ const candidate = this.GenerateDialInCode();
92
+ const res = await rv.RunView({
93
+ EntityName: 'MJ: Meetings',
94
+ ExtraFilter: `DialInPhoneNumberID = '${escapedPhoneID}' AND DialInCode = '${candidate}' AND Status IN ('Live', 'Scheduled')`,
95
+ ResultType: 'entity_object',
96
+ }, user);
97
+ if (res.Success && (!res.Results || res.Results.length === 0)) {
98
+ return candidate;
99
+ }
100
+ }
101
+ return undefined;
102
+ }
103
+ /**
104
+ * Verifies a DTMF dial-in code against active/scheduled meetings on the dialed line.
105
+ * Rate limits on both the verified caller number and the dialed number.
106
+ */
107
+ async VerifyDialInCode(params) {
108
+ try {
109
+ const callerNumber = (params.CallerNumber ?? '').trim().toLowerCase();
110
+ const dialedNumber = (params.DialedNumber ?? '').trim();
111
+ const dialInCode = (params.DialInCode ?? '').trim();
112
+ if (!callerNumber) {
113
+ return { Success: false, ErrorMessage: 'Caller number is required.' };
114
+ }
115
+ if (!dialedNumber) {
116
+ return { Success: false, ErrorMessage: 'Dialed number is required.' };
117
+ }
118
+ if (!dialInCode) {
119
+ return { Success: false, ErrorMessage: 'Dial-in code is required.' };
120
+ }
121
+ // Check caller-based rate limit
122
+ const callerLimit = this.callerLimiter.CheckAllowed(callerNumber);
123
+ if (!callerLimit.allowed) {
124
+ const remainingSeconds = Math.ceil((callerLimit.remainingMs ?? 0) / 1000);
125
+ return {
126
+ Success: false,
127
+ ErrorMessage: `Too many invalid attempts from this caller. Please try again in ${remainingSeconds} seconds.`,
128
+ };
129
+ }
130
+ // Check dialed-number-based rate limit
131
+ const dialedKey = dialedNumber.toLowerCase();
132
+ const dialedLimit = this.dialedLimiter.CheckAllowed(dialedKey);
133
+ if (!dialedLimit.allowed) {
134
+ const remainingSeconds = Math.ceil((dialedLimit.remainingMs ?? 0) / 1000);
135
+ return {
136
+ Success: false,
137
+ ErrorMessage: `Too many invalid attempts on this line. Please try again in ${remainingSeconds} seconds.`,
138
+ };
139
+ }
140
+ const escapedDialed = EscapeSQLString(dialedNumber);
141
+ const escapedCode = EscapeSQLString(dialInCode);
142
+ const rv = params.Provider ? RunView.FromMetadataProvider(params.Provider) : new RunView();
143
+ const filter = `(DialInPhoneNumberID = '${escapedDialed}' OR DialInPhoneNumber = '${escapedDialed}') AND DialInCode = '${escapedCode}' AND Status IN ('Live', 'Scheduled')`;
144
+ const result = await rv.RunView({
145
+ EntityName: 'MJ: Meetings',
146
+ ExtraFilter: filter,
147
+ ResultType: 'entity_object',
148
+ }, params.ContextUser);
149
+ if (!result.Success || !result.Results || result.Results.length === 0) {
150
+ this.callerLimiter.RecordFailure(callerNumber);
151
+ this.dialedLimiter.RecordFailure(dialedKey);
152
+ return { Success: false, ErrorMessage: 'Invalid dial-in code.' };
153
+ }
154
+ this.callerLimiter.RecordSuccess(callerNumber);
155
+ this.dialedLimiter.RecordSuccess(dialedKey);
156
+ const meeting = result.Results[0];
157
+ return {
158
+ Success: true,
159
+ RoomName: meeting.RoomName,
160
+ MeetingID: meeting.ID,
161
+ };
162
+ }
163
+ catch (error) {
164
+ const msg = error instanceof Error ? error.message : String(error);
165
+ LogError(`[MeetingDialInService] VerifyDialInCode exception: ${msg}`);
166
+ return { Success: false, ErrorMessage: msg };
167
+ }
168
+ }
169
+ /** Clears rate limiter entries (test harness). */
170
+ ClearRateLimitsForTesting() {
171
+ this.callerLimiter.Clear();
172
+ this.dialedLimiter.Clear();
173
+ }
174
+ }
175
+ //# sourceMappingURL=meeting-dial-in-service.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"meeting-dial-in-service.js","sourceRoot":"","sources":["../src/meeting-dial-in-service.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,40 @@
1
+ /**
2
+ * @fileoverview {@link DecodeAudio} — turns an audio file's bytes (WAV or MP3) into mono PCM16, the one shape the
3
+ * room audio player plays. WAV is parsed here (RIFF chunk walk, integer PCM 8/16/24/32-bit and IEEE float 32/64-bit,
4
+ * any channel count downmixed); MP3 is decoded by `mpg123-decoder`, a WebAssembly build of libmpg123 with no native
5
+ * build step. Anything else is refused with an error that names the format it saw.
6
+ *
7
+ * @module @memberjunction/livekit-room-server
8
+ */
9
+ /** Decoded audio: mono 16-bit PCM and its sample rate. */
10
+ export interface DecodedAudio {
11
+ /** Mono PCM16 samples. */
12
+ Pcm: Int16Array;
13
+ /** The rate of {@link Pcm}, in Hz. */
14
+ SampleRate: number;
15
+ }
16
+ /** What the caller knows about the bytes, used only when the bytes themselves do not say what they are. */
17
+ export interface AudioFormatHint {
18
+ /** The MIME type (e.g. `audio/mpeg`, `audio/wav`). */
19
+ MimeType?: string;
20
+ /** The file name (its extension is read). */
21
+ FileName?: string;
22
+ }
23
+ /** The longest audio {@link DecodeAudio} accepts, in seconds (15 minutes). Hold music loops; a longer file is a mistake. */
24
+ export declare const MAX_DECODED_AUDIO_SECONDS: number;
25
+ /**
26
+ * Decodes WAV or MP3 bytes into mono PCM16. The format is read from the bytes' own signature first and from the hint
27
+ * only when the bytes carry none (a bare MP3 frame stream).
28
+ *
29
+ * @param bytes The file's bytes.
30
+ * @param hint The MIME type and/or file name, when known.
31
+ * @returns Mono PCM16 at the file's own sample rate.
32
+ * @throws {Error} when the format is not WAV or MP3, the file is malformed or empty, or it is longer than
33
+ * {@link MAX_DECODED_AUDIO_SECONDS}.
34
+ */
35
+ export declare function DecodeAudio(bytes: Uint8Array, hint?: AudioFormatHint): Promise<DecodedAudio>;
36
+ /** Throws when `sampleCount` samples at `sampleRate` run longer than {@link MAX_DECODED_AUDIO_SECONDS}. */
37
+ export declare function AssertAudioDurationWithinCap(sampleCount: number, sampleRate: number): void;
38
+ /** Converts one [-1, 1] float sample to PCM16, clamping anything outside the range. */
39
+ export declare function FloatToPcm16(sample: number): number;
40
+ //# sourceMappingURL=audio-decoder.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"audio-decoder.d.ts","sourceRoot":"","sources":["../../src/room-audio/audio-decoder.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAIH,0DAA0D;AAC1D,MAAM,WAAW,YAAY;IAC3B,0BAA0B;IAC1B,GAAG,EAAE,UAAU,CAAC;IAChB,sCAAsC;IACtC,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,2GAA2G;AAC3G,MAAM,WAAW,eAAe;IAC9B,sDAAsD;IACtD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,6CAA6C;IAC7C,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,4HAA4H;AAC5H,eAAO,MAAM,yBAAyB,QAAU,CAAC;AAoBjD;;;;;;;;;GASG;AACH,wBAAsB,WAAW,CAAC,KAAK,EAAE,UAAU,EAAE,IAAI,CAAC,EAAE,eAAe,GAAG,OAAO,CAAC,YAAY,CAAC,CAQlG;AAED,2GAA2G;AAC3G,wBAAgB,4BAA4B,CAAC,WAAW,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,IAAI,CAQ1F;AAqID,uFAAuF;AACvF,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAGnD"}
@@ -0,0 +1,205 @@
1
+ /**
2
+ * @fileoverview {@link DecodeAudio} — turns an audio file's bytes (WAV or MP3) into mono PCM16, the one shape the
3
+ * room audio player plays. WAV is parsed here (RIFF chunk walk, integer PCM 8/16/24/32-bit and IEEE float 32/64-bit,
4
+ * any channel count downmixed); MP3 is decoded by `mpg123-decoder`, a WebAssembly build of libmpg123 with no native
5
+ * build step. Anything else is refused with an error that names the format it saw.
6
+ *
7
+ * @module @memberjunction/livekit-room-server
8
+ */
9
+ import { MPEGDecoder } from 'mpg123-decoder';
10
+ /** The longest audio {@link DecodeAudio} accepts, in seconds (15 minutes). Hold music loops; a longer file is a mistake. */
11
+ export const MAX_DECODED_AUDIO_SECONDS = 15 * 60;
12
+ const WAV_FORMAT_PCM = 1;
13
+ const WAV_FORMAT_FLOAT = 3;
14
+ const WAV_FORMAT_EXTENSIBLE = 0xfffe;
15
+ /**
16
+ * Decodes WAV or MP3 bytes into mono PCM16. The format is read from the bytes' own signature first and from the hint
17
+ * only when the bytes carry none (a bare MP3 frame stream).
18
+ *
19
+ * @param bytes The file's bytes.
20
+ * @param hint The MIME type and/or file name, when known.
21
+ * @returns Mono PCM16 at the file's own sample rate.
22
+ * @throws {Error} when the format is not WAV or MP3, the file is malformed or empty, or it is longer than
23
+ * {@link MAX_DECODED_AUDIO_SECONDS}.
24
+ */
25
+ export async function DecodeAudio(bytes, hint) {
26
+ if (bytes.byteLength === 0) {
27
+ throw new Error('Cannot decode audio: the file is empty.');
28
+ }
29
+ const format = detectFormat(bytes, hint);
30
+ const decoded = format === 'wav' ? decodeWav(bytes) : await decodeMp3(bytes);
31
+ AssertAudioDurationWithinCap(decoded.Pcm.length, decoded.SampleRate);
32
+ return decoded;
33
+ }
34
+ /** Throws when `sampleCount` samples at `sampleRate` run longer than {@link MAX_DECODED_AUDIO_SECONDS}. */
35
+ export function AssertAudioDurationWithinCap(sampleCount, sampleRate) {
36
+ const seconds = sampleCount / sampleRate;
37
+ if (seconds > MAX_DECODED_AUDIO_SECONDS) {
38
+ throw new Error(`Audio is ${Math.round(seconds)} s long; the room audio player accepts at most ${MAX_DECODED_AUDIO_SECONDS} s ` +
39
+ '(15 minutes). Trim the file — hold music loops, so a few minutes is plenty.');
40
+ }
41
+ }
42
+ // ── Format detection ─────────────────────────────────────────────────────────────────────────────────────────────
43
+ function ascii(bytes, start, length) {
44
+ return String.fromCharCode(...bytes.subarray(start, start + length));
45
+ }
46
+ function isMp3Signature(bytes) {
47
+ if (bytes.byteLength >= 3 && ascii(bytes, 0, 3) === 'ID3') {
48
+ return true;
49
+ }
50
+ // An MPEG audio frame header starts with 11 set sync bits.
51
+ return bytes.byteLength >= 2 && bytes[0] === 0xff && (bytes[1] & 0xe0) === 0xe0;
52
+ }
53
+ /** Names a recognisable but unsupported container from its signature, for the error message. */
54
+ function describeUnsupportedSignature(bytes) {
55
+ if (bytes.byteLength >= 4 && ascii(bytes, 0, 4) === 'OggS')
56
+ return 'Ogg (Vorbis/Opus)';
57
+ if (bytes.byteLength >= 4 && ascii(bytes, 0, 4) === 'fLaC')
58
+ return 'FLAC';
59
+ if (bytes.byteLength >= 8 && ascii(bytes, 4, 4) === 'ftyp')
60
+ return 'MP4/M4A (AAC)';
61
+ if (bytes.byteLength >= 4 && ascii(bytes, 0, 4) === 'FORM')
62
+ return 'AIFF';
63
+ if (bytes.byteLength >= 4 && bytes[0] === 0x1a && bytes[1] === 0x45 && bytes[2] === 0xdf && bytes[3] === 0xa3)
64
+ return 'WebM/Matroska';
65
+ return undefined;
66
+ }
67
+ function formatFromHint(hint) {
68
+ const mime = hint?.MimeType?.toLowerCase().split(';')[0].trim() ?? '';
69
+ const name = hint?.FileName?.toLowerCase() ?? '';
70
+ if (['audio/wav', 'audio/x-wav', 'audio/wave', 'audio/vnd.wave'].includes(mime) || name.endsWith('.wav'))
71
+ return 'wav';
72
+ if (['audio/mpeg', 'audio/mp3', 'audio/mpeg3', 'audio/x-mpeg-3'].includes(mime) || name.endsWith('.mp3'))
73
+ return 'mp3';
74
+ return undefined;
75
+ }
76
+ function detectFormat(bytes, hint) {
77
+ if (bytes.byteLength >= 12 && ascii(bytes, 0, 4) === 'RIFF' && ascii(bytes, 8, 4) === 'WAVE') {
78
+ return 'wav';
79
+ }
80
+ if (isMp3Signature(bytes)) {
81
+ return 'mp3';
82
+ }
83
+ const unsupported = describeUnsupportedSignature(bytes);
84
+ const hinted = unsupported ? undefined : formatFromHint(hint);
85
+ if (hinted === 'mp3') {
86
+ return 'mp3'; // an MP3 stream may begin mid-frame; let the decoder resync
87
+ }
88
+ const seen = unsupported ?? (hint?.MimeType || hint?.FileName || 'unrecognised bytes');
89
+ throw new Error(`Unsupported audio format: ${seen}. The room audio player plays WAV (PCM or float) and MP3.`);
90
+ }
91
+ // ── WAV ──────────────────────────────────────────────────────────────────────────────────────────────────────────
92
+ /** Walks the RIFF chunks and returns the `fmt ` description and the `data` byte range. */
93
+ function readWavChunks(bytes) {
94
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
95
+ let format;
96
+ let offset = 12;
97
+ while (offset + 8 <= bytes.byteLength) {
98
+ const id = ascii(bytes, offset, 4);
99
+ const size = view.getUint32(offset + 4, true);
100
+ const body = offset + 8;
101
+ if (id === 'fmt ') {
102
+ format = readWavFormat(view, body, size);
103
+ }
104
+ else if (id === 'data') {
105
+ if (!format) {
106
+ throw new Error('Malformed WAV: the data chunk comes before the fmt chunk.');
107
+ }
108
+ // A streaming writer may leave the size as 0 or 0xFFFFFFFF; clamp to what is actually there.
109
+ return { Format: format, DataOffset: body, DataLength: Math.min(size, bytes.byteLength - body) };
110
+ }
111
+ offset = body + size + (size & 1); // chunks are word-aligned: an odd-sized chunk carries one pad byte
112
+ }
113
+ throw new Error(format ? 'Malformed WAV: no data chunk.' : 'Malformed WAV: no fmt chunk.');
114
+ }
115
+ function readWavFormat(view, body, size) {
116
+ if (size < 16 || body + 16 > view.byteLength) {
117
+ throw new Error('Malformed WAV: the fmt chunk is truncated.');
118
+ }
119
+ let formatCode = view.getUint16(body, true);
120
+ if (formatCode === WAV_FORMAT_EXTENSIBLE && size >= 40 && body + 26 <= view.byteLength) {
121
+ formatCode = view.getUint16(body + 24, true); // the first two bytes of the SubFormat GUID are the real code
122
+ }
123
+ return {
124
+ FormatCode: formatCode,
125
+ Channels: view.getUint16(body + 2, true),
126
+ SampleRate: view.getUint32(body + 4, true),
127
+ BitsPerSample: view.getUint16(body + 14, true),
128
+ };
129
+ }
130
+ function sampleReaderFor(format) {
131
+ const { FormatCode: code, BitsPerSample: bits } = format;
132
+ if (code === WAV_FORMAT_PCM && bits === 8)
133
+ return (v, o) => (v.getUint8(o) - 128) / 128;
134
+ if (code === WAV_FORMAT_PCM && bits === 16)
135
+ return (v, o) => v.getInt16(o, true) / 32768;
136
+ if (code === WAV_FORMAT_PCM && bits === 24) {
137
+ return (v, o) => ((v.getUint8(o) | (v.getUint8(o + 1) << 8) | (v.getInt8(o + 2) << 16)) / 8388608);
138
+ }
139
+ if (code === WAV_FORMAT_PCM && bits === 32)
140
+ return (v, o) => v.getInt32(o, true) / 2147483648;
141
+ if (code === WAV_FORMAT_FLOAT && bits === 32)
142
+ return (v, o) => v.getFloat32(o, true);
143
+ if (code === WAV_FORMAT_FLOAT && bits === 64)
144
+ return (v, o) => v.getFloat64(o, true);
145
+ throw new Error(`Unsupported WAV encoding: format code ${code}, ${bits}-bit. The room audio player plays integer PCM ` +
146
+ '(8/16/24/32-bit) and IEEE float (32/64-bit) WAV.');
147
+ }
148
+ function decodeWav(bytes) {
149
+ const { Format: format, DataOffset: dataOffset, DataLength: dataLength } = readWavChunks(bytes);
150
+ if (format.Channels < 1 || format.SampleRate < 1) {
151
+ throw new Error(`Malformed WAV: ${format.Channels} channel(s) at ${format.SampleRate} Hz.`);
152
+ }
153
+ const read = sampleReaderFor(format);
154
+ const bytesPerSample = format.BitsPerSample / 8;
155
+ const frameBytes = bytesPerSample * format.Channels;
156
+ const frames = Math.floor(dataLength / frameBytes);
157
+ if (frames === 0) {
158
+ throw new Error('Cannot decode audio: the WAV data chunk holds no samples.');
159
+ }
160
+ AssertAudioDurationWithinCap(frames, format.SampleRate); // before allocating the output for an over-long file
161
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
162
+ const pcm = new Int16Array(frames);
163
+ for (let frame = 0; frame < frames; frame++) {
164
+ const base = dataOffset + frame * frameBytes;
165
+ let sum = 0;
166
+ for (let channel = 0; channel < format.Channels; channel++) {
167
+ sum += read(view, base + channel * bytesPerSample);
168
+ }
169
+ pcm[frame] = FloatToPcm16(sum / format.Channels);
170
+ }
171
+ return { Pcm: pcm, SampleRate: format.SampleRate };
172
+ }
173
+ /** Converts one [-1, 1] float sample to PCM16, clamping anything outside the range. */
174
+ export function FloatToPcm16(sample) {
175
+ const clamped = sample > 1 ? 1 : sample < -1 ? -1 : sample;
176
+ return clamped < 0 ? Math.round(clamped * 32768) : Math.round(clamped * 32767);
177
+ }
178
+ // ── MP3 ──────────────────────────────────────────────────────────────────────────────────────────────────────────
179
+ async function decodeMp3(bytes) {
180
+ const decoder = new MPEGDecoder();
181
+ try {
182
+ await decoder.ready;
183
+ const result = decoder.decode(bytes);
184
+ if (result.samplesDecoded === 0 || result.channelData.length === 0) {
185
+ const reason = result.errors.length > 0 ? result.errors[0].message : 'no audio frames found';
186
+ throw new Error(`Cannot decode MP3: ${reason}.`);
187
+ }
188
+ return { Pcm: downmixFloatChannels(result.channelData, result.samplesDecoded), SampleRate: result.sampleRate };
189
+ }
190
+ finally {
191
+ decoder.free();
192
+ }
193
+ }
194
+ function downmixFloatChannels(channels, sampleCount) {
195
+ const pcm = new Int16Array(sampleCount);
196
+ for (let i = 0; i < sampleCount; i++) {
197
+ let sum = 0;
198
+ for (const channel of channels) {
199
+ sum += channel[i];
200
+ }
201
+ pcm[i] = FloatToPcm16(sum / channels.length);
202
+ }
203
+ return pcm;
204
+ }
205
+ //# sourceMappingURL=audio-decoder.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"audio-decoder.js","sourceRoot":"","sources":["../../src/room-audio/audio-decoder.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,23 @@
1
+ /**
2
+ * @fileoverview {@link GenerateComfortTone} — a procedurally generated hold loop for when a queue has no music
3
+ * configured. Four soft major/minor pads (C – Am – F – G, the progression everyone has heard a thousand times and
4
+ * nobody minds) with slow raised-cosine swells, a quiet bass an octave below, and a touch of second harmonic for
5
+ * warmth. Each chord fades fully out before the next fades in, so the loop seam is silent and never clicks.
6
+ *
7
+ * Synthesized at runtime: no audio asset ships, so there is nothing to license.
8
+ *
9
+ * @module @memberjunction/livekit-room-server
10
+ */
11
+ /** Length of one pass of the comfort tone, in seconds. */
12
+ export declare const COMFORT_TONE_LOOP_SECONDS = 10;
13
+ /** Peak level of the comfort tone as a fraction of full scale (about −15 dBFS — quieter than speech). */
14
+ export declare const COMFORT_TONE_PEAK = 0.18;
15
+ /**
16
+ * Generates one {@link COMFORT_TONE_LOOP_SECONDS}-second pass of the comfort tone as mono PCM16. Loop it end to end.
17
+ *
18
+ * @param sampleRate The rate to synthesize at, in Hz (generate at the playback rate to avoid resampling).
19
+ * @returns Mono PCM16 samples; every sample's magnitude is at most {@link COMFORT_TONE_PEAK} of full scale.
20
+ * @throws {Error} when `sampleRate` is not a positive integer.
21
+ */
22
+ export declare function GenerateComfortTone(sampleRate: number): Int16Array;
23
+ //# sourceMappingURL=comfort-tone.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"comfort-tone.d.ts","sourceRoot":"","sources":["../../src/room-audio/comfort-tone.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,0DAA0D;AAC1D,eAAO,MAAM,yBAAyB,KAAK,CAAC;AAE5C,yGAAyG;AACzG,eAAO,MAAM,iBAAiB,OAAO,CAAC;AAiBtC;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,UAAU,EAAE,MAAM,GAAG,UAAU,CAQlE"}
@@ -0,0 +1,70 @@
1
+ /**
2
+ * @fileoverview {@link GenerateComfortTone} — a procedurally generated hold loop for when a queue has no music
3
+ * configured. Four soft major/minor pads (C – Am – F – G, the progression everyone has heard a thousand times and
4
+ * nobody minds) with slow raised-cosine swells, a quiet bass an octave below, and a touch of second harmonic for
5
+ * warmth. Each chord fades fully out before the next fades in, so the loop seam is silent and never clicks.
6
+ *
7
+ * Synthesized at runtime: no audio asset ships, so there is nothing to license.
8
+ *
9
+ * @module @memberjunction/livekit-room-server
10
+ */
11
+ /** Length of one pass of the comfort tone, in seconds. */
12
+ export const COMFORT_TONE_LOOP_SECONDS = 10;
13
+ /** Peak level of the comfort tone as a fraction of full scale (about −15 dBFS — quieter than speech). */
14
+ export const COMFORT_TONE_PEAK = 0.18;
15
+ /** One chord of the progression: the frequencies of its voices, root first. */
16
+ const CHORDS = [
17
+ [261.63, 329.63, 392.0], // C major (C4 E4 G4)
18
+ [220.0, 261.63, 329.63], // A minor (A3 C4 E4)
19
+ [174.61, 220.0, 261.63], // F major (F3 A3 C4)
20
+ [196.0, 246.94, 293.66], // G major (G3 B3 D4)
21
+ ];
22
+ /** Fraction of each chord spent swelling in (and, symmetrically, fading out). */
23
+ const SWELL_FRACTION = 0.3;
24
+ /** Relative weight of the bass voice (the chord root an octave down) and of each voice's second harmonic. */
25
+ const BASS_WEIGHT = 0.5;
26
+ const HARMONIC_WEIGHT = 0.15;
27
+ /**
28
+ * Generates one {@link COMFORT_TONE_LOOP_SECONDS}-second pass of the comfort tone as mono PCM16. Loop it end to end.
29
+ *
30
+ * @param sampleRate The rate to synthesize at, in Hz (generate at the playback rate to avoid resampling).
31
+ * @returns Mono PCM16 samples; every sample's magnitude is at most {@link COMFORT_TONE_PEAK} of full scale.
32
+ * @throws {Error} when `sampleRate` is not a positive integer.
33
+ */
34
+ export function GenerateComfortTone(sampleRate) {
35
+ if (!Number.isInteger(sampleRate) || sampleRate <= 0) {
36
+ throw new Error(`GenerateComfortTone needs a positive integer sample rate; got ${sampleRate}.`);
37
+ }
38
+ const chordSamples = Math.round((COMFORT_TONE_LOOP_SECONDS / CHORDS.length) * sampleRate);
39
+ const out = new Int16Array(chordSamples * CHORDS.length);
40
+ CHORDS.forEach((chord, index) => renderChord(out, index * chordSamples, chordSamples, chord, sampleRate));
41
+ return out;
42
+ }
43
+ /** Writes one enveloped chord into `out` starting at `start`. */
44
+ function renderChord(out, start, length, chord, sampleRate) {
45
+ const voices = [...chord, chord[0] / 2];
46
+ const weights = [...chord.map(() => 1), BASS_WEIGHT];
47
+ // Normalise so the worst case (every voice and harmonic in phase at the envelope peak) stays at COMFORT_TONE_PEAK.
48
+ const worstCase = weights.reduce((total, weight) => total + weight * (1 + HARMONIC_WEIGHT), 0);
49
+ const scale = (COMFORT_TONE_PEAK * 32767) / worstCase;
50
+ for (let i = 0; i < length; i++) {
51
+ const t = i / sampleRate;
52
+ let sample = 0;
53
+ for (let v = 0; v < voices.length; v++) {
54
+ const phase = 2 * Math.PI * voices[v] * t;
55
+ sample += weights[v] * (Math.sin(phase) + HARMONIC_WEIGHT * Math.sin(2 * phase));
56
+ }
57
+ out[start + i] = Math.round(sample * swellEnvelope(i / length) * scale);
58
+ }
59
+ }
60
+ /** A raised-cosine swell: 0 at both ends of the chord, 1 across the middle. */
61
+ function swellEnvelope(position) {
62
+ if (position < SWELL_FRACTION) {
63
+ return 0.5 - 0.5 * Math.cos((Math.PI * position) / SWELL_FRACTION);
64
+ }
65
+ if (position > 1 - SWELL_FRACTION) {
66
+ return 0.5 - 0.5 * Math.cos((Math.PI * (1 - position)) / SWELL_FRACTION);
67
+ }
68
+ return 1;
69
+ }
70
+ //# sourceMappingURL=comfort-tone.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"comfort-tone.js","sourceRoot":"","sources":["../../src/room-audio/comfort-tone.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,0DAA0D;AAC1D,MAAM,CAAC,MAAM,yBAAyB,GAAG,EAAE,CAAC;AAE5C,yGAAyG;AACzG,MAAM,CAAC,MAAM,iBAAiB,GAAG,IAAI,CAAC;AAEtC,+EAA+E;AAC/E,MAAM,MAAM,GAAmC;IAC7C,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,qBAAqB;IAC9C,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,qBAAqB;IAC9C,CAAC,MAAM,EAAE,KAAK,EAAE,MAAM,CAAC,EAAE,qBAAqB;IAC9C,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,qBAAqB;CAC/C,CAAC;AAEF,iFAAiF;AACjF,MAAM,cAAc,GAAG,GAAG,CAAC;AAE3B,6GAA6G;AAC7G,MAAM,WAAW,GAAG,GAAG,CAAC;AACxB,MAAM,eAAe,GAAG,IAAI,CAAC;AAE7B;;;;;;GAMG;AACH,MAAM,UAAU,mBAAmB,CAAC,UAAkB;IACpD,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,UAAU,CAAC,IAAI,UAAU,IAAI,CAAC,EAAE,CAAC;QACrD,MAAM,IAAI,KAAK,CAAC,iEAAiE,UAAU,GAAG,CAAC,CAAC;IAClG,CAAC;IACD,MAAM,YAAY,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,yBAAyB,GAAG,MAAM,CAAC,MAAM,CAAC,GAAG,UAAU,CAAC,CAAC;IAC1F,MAAM,GAAG,GAAG,IAAI,UAAU,CAAC,YAAY,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC;IACzD,MAAM,CAAC,OAAO,CAAC,CAAC,KAAK,EAAE,KAAK,EAAE,EAAE,CAAC,WAAW,CAAC,GAAG,EAAE,KAAK,GAAG,YAAY,EAAE,YAAY,EAAE,KAAK,EAAE,UAAU,CAAC,CAAC,CAAC;IAC1G,OAAO,GAAG,CAAC;AACb,CAAC;AAED,iEAAiE;AACjE,SAAS,WAAW,CAAC,GAAe,EAAE,KAAa,EAAE,MAAc,EAAE,KAAwB,EAAE,UAAkB;IAC/G,MAAM,MAAM,GAAG,CAAC,GAAG,KAAK,EAAE,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;IACxC,MAAM,OAAO,GAAG,CAAC,GAAG,KAAK,CAAC,GAAG,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,EAAE,WAAW,CAAC,CAAC;IACrD,mHAAmH;IACnH,MAAM,SAAS,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,CAAC,KAAK,GAAG,MAAM,GAAG,CAAC,CAAC,GAAG,eAAe,CAAC,EAAE,CAAC,CAAC,CAAC;IAC/F,MAAM,KAAK,GAAG,CAAC,iBAAiB,GAAG,KAAK,CAAC,GAAG,SAAS,CAAC;IACtD,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QAChC,MAAM,CAAC,GAAG,CAAC,GAAG,UAAU,CAAC;QACzB,IAAI,MAAM,GAAG,CAAC,CAAC;QACf,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;YACvC,MAAM,KAAK,GAAG,CAAC,GAAG,IAAI,CAAC,EAAE,GAAG,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;YAC1C,MAAM,IAAI,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,GAAG,eAAe,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,KAAK,CAAC,CAAC,CAAC;QACnF,CAAC;QACD,GAAG,CAAC,KAAK,GAAG,CAAC,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,GAAG,aAAa,CAAC,CAAC,GAAG,MAAM,CAAC,GAAG,KAAK,CAAC,CAAC;IAC1E,CAAC;AACH,CAAC;AAED,+EAA+E;AAC/E,SAAS,aAAa,CAAC,QAAgB;IACrC,IAAI,QAAQ,GAAG,cAAc,EAAE,CAAC;QAC9B,OAAO,GAAG,GAAG,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,GAAG,QAAQ,CAAC,GAAG,cAAc,CAAC,CAAC;IACrE,CAAC;IACD,IAAI,QAAQ,GAAG,CAAC,GAAG,cAAc,EAAE,CAAC;QAClC,OAAO,GAAG,GAAG,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,GAAG,CAAC,CAAC,GAAG,QAAQ,CAAC,CAAC,GAAG,cAAc,CAAC,CAAC;IAC3E,CAAC;IACD,OAAO,CAAC,CAAC;AACX,CAAC"}