@agatx/serenada-core 0.6.10
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/dist/ConsoleLogger.d.ts +6 -0
- package/dist/ConsoleLogger.d.ts.map +1 -0
- package/dist/ConsoleLogger.js +21 -0
- package/dist/ConsoleLogger.js.map +1 -0
- package/dist/RoomWatcher.d.ts +34 -0
- package/dist/RoomWatcher.d.ts.map +1 -0
- package/dist/RoomWatcher.js +103 -0
- package/dist/RoomWatcher.js.map +1 -0
- package/dist/SerenadaCore.d.ts +47 -0
- package/dist/SerenadaCore.d.ts.map +1 -0
- package/dist/SerenadaCore.js +141 -0
- package/dist/SerenadaCore.js.map +1 -0
- package/dist/SerenadaDiagnostics.d.ts +49 -0
- package/dist/SerenadaDiagnostics.d.ts.map +1 -0
- package/dist/SerenadaDiagnostics.js +421 -0
- package/dist/SerenadaDiagnostics.js.map +1 -0
- package/dist/SerenadaServerProvider.d.ts +48 -0
- package/dist/SerenadaServerProvider.d.ts.map +1 -0
- package/dist/SerenadaServerProvider.js +296 -0
- package/dist/SerenadaServerProvider.js.map +1 -0
- package/dist/SerenadaSession.d.ts +180 -0
- package/dist/SerenadaSession.d.ts.map +1 -0
- package/dist/SerenadaSession.js +1082 -0
- package/dist/SerenadaSession.js.map +1 -0
- package/dist/SignalingProvider.d.ts +132 -0
- package/dist/SignalingProvider.d.ts.map +1 -0
- package/dist/SignalingProvider.js +50 -0
- package/dist/SignalingProvider.js.map +1 -0
- package/dist/api/roomApi.d.ts +2 -0
- package/dist/api/roomApi.d.ts.map +1 -0
- package/dist/api/roomApi.js +14 -0
- package/dist/api/roomApi.js.map +1 -0
- package/dist/cameraModes.d.ts +13 -0
- package/dist/cameraModes.d.ts.map +1 -0
- package/dist/cameraModes.js +35 -0
- package/dist/cameraModes.js.map +1 -0
- package/dist/configValidation.d.ts +10 -0
- package/dist/configValidation.d.ts.map +1 -0
- package/dist/configValidation.js +24 -0
- package/dist/configValidation.js.map +1 -0
- package/dist/constants.d.ts +33 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +65 -0
- package/dist/constants.js.map +1 -0
- package/dist/formatError.d.ts +3 -0
- package/dist/formatError.d.ts.map +1 -0
- package/dist/formatError.js +7 -0
- package/dist/formatError.js.map +1 -0
- package/dist/iceServers.d.ts +2 -0
- package/dist/iceServers.d.ts.map +1 -0
- package/dist/iceServers.js +21 -0
- package/dist/iceServers.js.map +1 -0
- package/dist/index.d.ts +55 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +44 -0
- package/dist/index.js.map +1 -0
- package/dist/layout/computeLayout.d.ts +81 -0
- package/dist/layout/computeLayout.d.ts.map +1 -0
- package/dist/layout/computeLayout.js +380 -0
- package/dist/layout/computeLayout.js.map +1 -0
- package/dist/media/AudioLevelMonitor.d.ts +51 -0
- package/dist/media/AudioLevelMonitor.d.ts.map +1 -0
- package/dist/media/AudioLevelMonitor.js +179 -0
- package/dist/media/AudioLevelMonitor.js.map +1 -0
- package/dist/media/MediaEngine.d.ts +137 -0
- package/dist/media/MediaEngine.d.ts.map +1 -0
- package/dist/media/MediaEngine.js +1224 -0
- package/dist/media/MediaEngine.js.map +1 -0
- package/dist/media/callStats.d.ts +16 -0
- package/dist/media/callStats.d.ts.map +1 -0
- package/dist/media/callStats.js +214 -0
- package/dist/media/callStats.js.map +1 -0
- package/dist/media/localVideoRecovery.d.ts +16 -0
- package/dist/media/localVideoRecovery.d.ts.map +1 -0
- package/dist/media/localVideoRecovery.js +14 -0
- package/dist/media/localVideoRecovery.js.map +1 -0
- package/dist/recoveryStorage.d.ts +33 -0
- package/dist/recoveryStorage.d.ts.map +1 -0
- package/dist/recoveryStorage.js +88 -0
- package/dist/recoveryStorage.js.map +1 -0
- package/dist/serverUrls.d.ts +8 -0
- package/dist/serverUrls.d.ts.map +1 -0
- package/dist/serverUrls.js +65 -0
- package/dist/serverUrls.js.map +1 -0
- package/dist/signaling/SignalingEngine.d.ts +126 -0
- package/dist/signaling/SignalingEngine.d.ts.map +1 -0
- package/dist/signaling/SignalingEngine.js +720 -0
- package/dist/signaling/SignalingEngine.js.map +1 -0
- package/dist/signaling/payloads.d.ts +76 -0
- package/dist/signaling/payloads.d.ts.map +1 -0
- package/dist/signaling/payloads.js +160 -0
- package/dist/signaling/payloads.js.map +1 -0
- package/dist/signaling/roomStatuses.d.ts +9 -0
- package/dist/signaling/roomStatuses.d.ts.map +1 -0
- package/dist/signaling/roomStatuses.js +71 -0
- package/dist/signaling/roomStatuses.js.map +1 -0
- package/dist/signaling/transportConfig.d.ts +3 -0
- package/dist/signaling/transportConfig.d.ts.map +1 -0
- package/dist/signaling/transportConfig.js +27 -0
- package/dist/signaling/transportConfig.js.map +1 -0
- package/dist/signaling/transports/index.d.ts +13 -0
- package/dist/signaling/transports/index.d.ts.map +1 -0
- package/dist/signaling/transports/index.js +11 -0
- package/dist/signaling/transports/index.js.map +1 -0
- package/dist/signaling/transports/sse.d.ts +26 -0
- package/dist/signaling/transports/sse.d.ts.map +1 -0
- package/dist/signaling/transports/sse.js +131 -0
- package/dist/signaling/transports/sse.js.map +1 -0
- package/dist/signaling/transports/types.d.ts +17 -0
- package/dist/signaling/transports/types.d.ts.map +1 -0
- package/dist/signaling/transports/types.js +2 -0
- package/dist/signaling/transports/types.js.map +1 -0
- package/dist/signaling/transports/ws.d.ts +21 -0
- package/dist/signaling/transports/ws.d.ts.map +1 -0
- package/dist/signaling/transports/ws.js +93 -0
- package/dist/signaling/transports/ws.js.map +1 -0
- package/dist/signaling/types.d.ts +53 -0
- package/dist/signaling/types.d.ts.map +1 -0
- package/dist/signaling/types.js +2 -0
- package/dist/signaling/types.js.map +1 -0
- package/dist/types.d.ts +279 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/package.json +43 -0
- package/src/ConsoleLogger.ts +14 -0
- package/src/RoomWatcher.ts +127 -0
- package/src/SerenadaCore.ts +163 -0
- package/src/SerenadaDiagnostics.ts +485 -0
- package/src/SerenadaServerProvider.ts +362 -0
- package/src/SerenadaSession.ts +1258 -0
- package/src/SignalingProvider.ts +207 -0
- package/src/api/roomApi.ts +16 -0
- package/src/cameraModes.ts +34 -0
- package/src/configValidation.ts +35 -0
- package/src/constants.ts +77 -0
- package/src/formatError.ts +5 -0
- package/src/iceServers.ts +20 -0
- package/src/index.ts +155 -0
- package/src/layout/computeLayout.ts +639 -0
- package/src/media/AudioLevelMonitor.ts +190 -0
- package/src/media/MediaEngine.ts +1183 -0
- package/src/media/callStats.ts +260 -0
- package/src/media/localVideoRecovery.ts +39 -0
- package/src/recoveryStorage.ts +101 -0
- package/src/serverUrls.ts +69 -0
- package/src/signaling/SignalingEngine.ts +762 -0
- package/src/signaling/payloads.ts +215 -0
- package/src/signaling/roomStatuses.ts +89 -0
- package/src/signaling/transportConfig.ts +30 -0
- package/src/signaling/transports/index.ts +26 -0
- package/src/signaling/transports/sse.ts +146 -0
- package/src/signaling/transports/types.ts +19 -0
- package/src/signaling/transports/ws.ts +108 -0
- package/src/signaling/types.ts +68 -0
- package/src/types.ts +299 -0
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import type { SerenadaLogger } from '../types.js';
|
|
2
|
+
|
|
3
|
+
/** Update frequency for audio level subscribers. 10 Hz pairs well with 100 ms CSS transitions. */
|
|
4
|
+
const DEFAULT_UPDATE_INTERVAL_MS = 100;
|
|
5
|
+
/** Default FFT size — small for low CPU; we only need a coarse RMS. */
|
|
6
|
+
const DEFAULT_FFT_SIZE = 1024;
|
|
7
|
+
/** Default smoothing applied by the analyser node (0..1). */
|
|
8
|
+
const DEFAULT_SMOOTHING = 0.5;
|
|
9
|
+
/** dBFS at which the indicator reads zero. Quieter than this is treated as silence. */
|
|
10
|
+
const NOISE_FLOOR_DB = -60;
|
|
11
|
+
/** dBFS at which the indicator reads full. Normal speech peaks around -20 to -15 dBFS. */
|
|
12
|
+
const SPEECH_PEAK_DB = -15;
|
|
13
|
+
/** Attack/release smoothing across ticks (0..1). Higher = stickier; 0 = no smoothing. */
|
|
14
|
+
const ATTACK_SMOOTHING = 0.4;
|
|
15
|
+
const RELEASE_SMOOTHING = 0.7;
|
|
16
|
+
|
|
17
|
+
export interface AudioLevelMonitorOptions {
|
|
18
|
+
/** AnalyserNode smoothingTimeConstant (0..1). Defaults to 0.5. */
|
|
19
|
+
smoothing?: number;
|
|
20
|
+
/** AnalyserNode FFT size — power of two. Defaults to 1024. */
|
|
21
|
+
fftSize?: number;
|
|
22
|
+
/** Subscriber notification interval in ms. Defaults to 100. */
|
|
23
|
+
updateIntervalMs?: number;
|
|
24
|
+
/** Reuse an existing AudioContext (the monitor will not close it on dispose). */
|
|
25
|
+
audioContext?: AudioContext;
|
|
26
|
+
/** Optional logger for diagnostics. */
|
|
27
|
+
logger?: SerenadaLogger;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
type LevelCallback = (level: number) => void;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Computes a normalized speech audio level (0..1) from a MediaStream's audio
|
|
34
|
+
* track using the Web Audio API. Use it to drive UI activity indicators.
|
|
35
|
+
*
|
|
36
|
+
* The monitor is lazy: it only runs the analysis loop while at least one
|
|
37
|
+
* subscriber is attached. Call {@link dispose} when the stream is gone or
|
|
38
|
+
* the consumer goes away.
|
|
39
|
+
*/
|
|
40
|
+
export class AudioLevelMonitor {
|
|
41
|
+
private context: AudioContext | null = null;
|
|
42
|
+
private analyser: AnalyserNode | null = null;
|
|
43
|
+
private source: MediaStreamAudioSourceNode | null = null;
|
|
44
|
+
private buffer: Uint8Array<ArrayBuffer> | null = null;
|
|
45
|
+
private timer: number | null = null;
|
|
46
|
+
private subscribers = new Set<LevelCallback>();
|
|
47
|
+
private currentLevel = 0;
|
|
48
|
+
private disposed = false;
|
|
49
|
+
private readonly ownsContext: boolean;
|
|
50
|
+
private readonly updateIntervalMs: number;
|
|
51
|
+
private readonly logger?: SerenadaLogger;
|
|
52
|
+
|
|
53
|
+
constructor(stream: MediaStream, options: AudioLevelMonitorOptions = {}) {
|
|
54
|
+
this.updateIntervalMs = options.updateIntervalMs ?? DEFAULT_UPDATE_INTERVAL_MS;
|
|
55
|
+
this.logger = options.logger;
|
|
56
|
+
this.ownsContext = !options.audioContext;
|
|
57
|
+
|
|
58
|
+
const Ctx = typeof globalThis !== 'undefined'
|
|
59
|
+
? ((globalThis as { AudioContext?: typeof AudioContext; webkitAudioContext?: typeof AudioContext })
|
|
60
|
+
.AudioContext
|
|
61
|
+
?? (globalThis as { webkitAudioContext?: typeof AudioContext }).webkitAudioContext)
|
|
62
|
+
: undefined;
|
|
63
|
+
|
|
64
|
+
if (!Ctx && !options.audioContext) {
|
|
65
|
+
this.logger?.log('debug', 'AudioLevelMonitor', 'AudioContext unavailable; monitor will report zero level');
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (stream.getAudioTracks().length === 0) {
|
|
70
|
+
this.logger?.log('debug', 'AudioLevelMonitor', 'Stream has no audio tracks');
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
this.context = options.audioContext ?? new (Ctx as typeof AudioContext)();
|
|
76
|
+
this.analyser = this.context.createAnalyser();
|
|
77
|
+
this.analyser.fftSize = options.fftSize ?? DEFAULT_FFT_SIZE;
|
|
78
|
+
this.analyser.smoothingTimeConstant = options.smoothing ?? DEFAULT_SMOOTHING;
|
|
79
|
+
this.source = this.context.createMediaStreamSource(stream);
|
|
80
|
+
this.source.connect(this.analyser);
|
|
81
|
+
this.buffer = new Uint8Array(new ArrayBuffer(this.analyser.fftSize));
|
|
82
|
+
// Browsers may create the context in a suspended state until a user gesture;
|
|
83
|
+
// resume it so analysis starts producing samples immediately.
|
|
84
|
+
if (this.context.state === 'suspended') {
|
|
85
|
+
void this.context.resume().catch((err) => {
|
|
86
|
+
this.logger?.log('debug', 'AudioLevelMonitor', `resume() failed: ${err}`);
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
} catch (err) {
|
|
90
|
+
this.logger?.log('warning', 'AudioLevelMonitor', `Failed to attach to stream: ${err}`);
|
|
91
|
+
this.cleanup();
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Returns the most recent level computed by the monitor (0..1). */
|
|
96
|
+
get level(): number {
|
|
97
|
+
return this.currentLevel;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Subscribe for level updates. The callback fires immediately with the
|
|
102
|
+
* current level, then at {@link AudioLevelMonitorOptions.updateIntervalMs}.
|
|
103
|
+
* Returns an unsubscribe function.
|
|
104
|
+
*/
|
|
105
|
+
subscribe(callback: LevelCallback): () => void {
|
|
106
|
+
if (this.disposed) {
|
|
107
|
+
callback(0);
|
|
108
|
+
return () => { /* noop */ };
|
|
109
|
+
}
|
|
110
|
+
this.subscribers.add(callback);
|
|
111
|
+
callback(this.currentLevel);
|
|
112
|
+
if (this.analyser && this.subscribers.size === 1) {
|
|
113
|
+
this.startLoop();
|
|
114
|
+
}
|
|
115
|
+
return () => {
|
|
116
|
+
this.subscribers.delete(callback);
|
|
117
|
+
if (this.subscribers.size === 0) {
|
|
118
|
+
this.stopLoop();
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
dispose(): void {
|
|
124
|
+
if (this.disposed) return;
|
|
125
|
+
this.disposed = true;
|
|
126
|
+
this.cleanup();
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
private startLoop(): void {
|
|
130
|
+
if (this.timer !== null) return;
|
|
131
|
+
const setIntervalFn = typeof globalThis !== 'undefined' && typeof globalThis.setInterval === 'function'
|
|
132
|
+
? globalThis.setInterval.bind(globalThis)
|
|
133
|
+
: null;
|
|
134
|
+
if (!setIntervalFn) return;
|
|
135
|
+
this.timer = setIntervalFn(() => this.tick(), this.updateIntervalMs) as unknown as number;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
private stopLoop(): void {
|
|
139
|
+
if (this.timer === null) return;
|
|
140
|
+
const clearIntervalFn = typeof globalThis !== 'undefined' && typeof globalThis.clearInterval === 'function'
|
|
141
|
+
? globalThis.clearInterval.bind(globalThis)
|
|
142
|
+
: null;
|
|
143
|
+
clearIntervalFn?.(this.timer as unknown as ReturnType<typeof setInterval>);
|
|
144
|
+
this.timer = null;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
private tick(): void {
|
|
148
|
+
if (this.disposed || !this.analyser || !this.buffer) return;
|
|
149
|
+
if (this.context?.state === 'suspended') {
|
|
150
|
+
void this.context.resume().catch(() => {});
|
|
151
|
+
}
|
|
152
|
+
this.analyser.getByteTimeDomainData(this.buffer);
|
|
153
|
+
let sumSquares = 0;
|
|
154
|
+
for (let i = 0; i < this.buffer.length; i++) {
|
|
155
|
+
const sample = (this.buffer[i] - 128) / 128;
|
|
156
|
+
sumSquares += sample * sample;
|
|
157
|
+
}
|
|
158
|
+
const rms = Math.sqrt(sumSquares / this.buffer.length);
|
|
159
|
+
// Map RMS to dBFS, then to a perceptual 0..1 between the noise floor and speech peak.
|
|
160
|
+
const dbfs = rms > 0 ? 20 * Math.log10(rms) : NOISE_FLOOR_DB;
|
|
161
|
+
const target = Math.max(0, Math.min(1, (dbfs - NOISE_FLOOR_DB) / (SPEECH_PEAK_DB - NOISE_FLOOR_DB)));
|
|
162
|
+
// Asymmetric smoothing: snap up quickly (attack), decay slowly (release).
|
|
163
|
+
const smoothing = target > this.currentLevel ? ATTACK_SMOOTHING : RELEASE_SMOOTHING;
|
|
164
|
+
const level = this.currentLevel * smoothing + target * (1 - smoothing);
|
|
165
|
+
this.currentLevel = level;
|
|
166
|
+
this.subscribers.forEach((cb) => {
|
|
167
|
+
try { cb(level); } catch (err) {
|
|
168
|
+
this.logger?.log('warning', 'AudioLevelMonitor', `Subscriber threw: ${err}`);
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
private cleanup(): void {
|
|
174
|
+
this.stopLoop();
|
|
175
|
+
if (this.source) {
|
|
176
|
+
try { this.source.disconnect(); } catch { /* ignore */ }
|
|
177
|
+
this.source = null;
|
|
178
|
+
}
|
|
179
|
+
if (this.analyser) {
|
|
180
|
+
try { this.analyser.disconnect(); } catch { /* ignore */ }
|
|
181
|
+
this.analyser = null;
|
|
182
|
+
}
|
|
183
|
+
if (this.context && this.ownsContext) {
|
|
184
|
+
try { void this.context.close(); } catch { /* ignore */ }
|
|
185
|
+
}
|
|
186
|
+
this.context = null;
|
|
187
|
+
this.buffer = null;
|
|
188
|
+
this.subscribers.clear();
|
|
189
|
+
}
|
|
190
|
+
}
|