Skip to content

Commit 36ba9c6

Browse files
authored
feat(ts) migrate statistics/LocalStatsCollector to TS
1 parent 04c7ab1 commit 36ba9c6

File tree

2 files changed

+187
-179
lines changed

2 files changed

+187
-179
lines changed

modules/statistics/LocalStatsCollector.js

Lines changed: 0 additions & 179 deletions
This file was deleted.
Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
import { getLogger } from '@jitsi/logger';
2+
3+
const logger = getLogger('modules/statistics/LocalStatsCollector');
4+
/**
5+
* Size of the webaudio analyzer buffer.
6+
* @type {number}
7+
*/
8+
const WEBAUDIO_ANALYZER_FFT_SIZE: number = 2048;
9+
10+
/**
11+
* Value of the webaudio analyzer smoothing time parameter.
12+
* @type {number}
13+
*/
14+
const WEBAUDIO_ANALYZER_SMOOTING_TIME: number = 0.8;
15+
16+
/**
17+
* The audio context.
18+
* @type {AudioContext}
19+
*/
20+
let context: AudioContext | null = null;
21+
22+
/**
23+
* Converts time domain data array to audio level.
24+
* @param samples the time domain data array.
25+
* @returns {number} the audio level
26+
*/
27+
function timeDomainDataToAudioLevel(samples: Uint8Array): number {
28+
let maxVolume = 0;
29+
const length = samples.length;
30+
31+
for (let i = 0; i < length; i++) {
32+
if (maxVolume < samples[i]) {
33+
maxVolume = samples[i];
34+
}
35+
}
36+
37+
return Number.parseFloat(((maxVolume - 127) / 128).toFixed(3));
38+
}
39+
40+
/**
41+
* Animates audio level change
42+
* @param newLevel the new audio level
43+
* @param lastLevel the last audio level
44+
* @returns {Number} the audio level to be set
45+
*/
46+
function animateLevel(newLevel: number, lastLevel: number): number {
47+
let value = 0;
48+
const diff = lastLevel - newLevel;
49+
50+
if (diff > 0.2) {
51+
value = lastLevel - 0.2;
52+
} else if (diff < -0.4) {
53+
value = lastLevel + 0.4;
54+
} else {
55+
value = newLevel;
56+
}
57+
58+
return Number.parseFloat(value.toFixed(3));
59+
}
60+
61+
/**
62+
* Provides statistics for the local stream.
63+
*/
64+
export default class LocalStatsCollector {
65+
stream: MediaStream;
66+
intervalId: Timeout | null;
67+
intervalMilis: number;
68+
audioLevel: number;
69+
callback: (audioLevel: number) => void;
70+
source: MediaStreamAudioSourceNode | null;
71+
analyser: AnalyserNode | null;
72+
73+
/**
74+
* Creates a new instance of LocalStatsCollector.
75+
*
76+
* @param {MediaStream} stream - the local stream
77+
* @param {number} interval - stats refresh interval given in ms.
78+
* @param {Function} callback - function that receives the audio levels.
79+
* @constructor
80+
*/
81+
constructor(
82+
stream: MediaStream,
83+
interval: number,
84+
callback: (audioLevel: number) => void
85+
) {
86+
this.stream = stream;
87+
this.intervalId = null;
88+
this.intervalMilis = interval;
89+
this.audioLevel = 0;
90+
this.callback = callback;
91+
this.source = null;
92+
this.analyser = null;
93+
}
94+
95+
/**
96+
* Starts the collecting the statistics.
97+
*/
98+
start(): void {
99+
if (!LocalStatsCollector.isLocalStatsSupported()) {
100+
return;
101+
}
102+
103+
context!.resume();
104+
this.analyser = context!.createAnalyser();
105+
106+
this.analyser.smoothingTimeConstant = WEBAUDIO_ANALYZER_SMOOTING_TIME;
107+
this.analyser.fftSize = WEBAUDIO_ANALYZER_FFT_SIZE;
108+
109+
this.source = context!.createMediaStreamSource(this.stream);
110+
111+
this.source.connect(this.analyser);
112+
113+
this.intervalId = setInterval(
114+
() => {
115+
const array = new Uint8Array(this.analyser!.frequencyBinCount);
116+
117+
this.analyser!.getByteTimeDomainData(array);
118+
const audioLevel = timeDomainDataToAudioLevel(array);
119+
120+
// Set the audio levels always as NoAudioSignalDetection now
121+
// uses audio levels from LocalStatsCollector and waits for
122+
// atleast 4 secs for a no audio signal before displaying the
123+
// notification on the UI.
124+
this.audioLevel = animateLevel(audioLevel, this.audioLevel);
125+
this.callback(this.audioLevel);
126+
},
127+
this.intervalMilis
128+
);
129+
}
130+
131+
/**
132+
* Stops collecting the statistics.
133+
*/
134+
stop(): void {
135+
if (this.intervalId) {
136+
clearInterval(this.intervalId);
137+
this.intervalId = null;
138+
}
139+
140+
this.analyser?.disconnect();
141+
this.analyser = null;
142+
this.source?.disconnect();
143+
this.source = null;
144+
}
145+
146+
/**
147+
* Initialize collector.
148+
*/
149+
static init(): void {
150+
LocalStatsCollector.connectAudioContext();
151+
}
152+
153+
/**
154+
* Checks if the environment has the necessary conditions to support
155+
* collecting stats from local streams.
156+
*
157+
* @returns {boolean}
158+
*/
159+
static isLocalStatsSupported(): boolean {
160+
return Boolean(window?.AudioContext);
161+
}
162+
163+
/**
164+
* Disconnects the audio context.
165+
*/
166+
static async disconnectAudioContext(): Promise<void> {
167+
if (context) {
168+
logger.info('Disconnecting audio context');
169+
await context.close();
170+
context = null;
171+
}
172+
}
173+
174+
/**
175+
* Connects the audio context.
176+
*/
177+
static connectAudioContext(): void {
178+
if (!LocalStatsCollector.isLocalStatsSupported()) {
179+
return;
180+
}
181+
182+
logger.info('Connecting audio context');
183+
context = new AudioContext();
184+
185+
context.suspend();
186+
}
187+
}

0 commit comments

Comments
 (0)