|
@@ -1,4 +1,3 @@
|
1
|
|
-import { RecordingAdapter } from '../RecordingAdapter';
|
2
|
1
|
import {
|
3
|
2
|
DEBUG,
|
4
|
3
|
MAIN_THREAD_FINISH,
|
|
@@ -8,50 +7,41 @@ import {
|
8
|
7
|
WORKER_LIBFLAC_READY
|
9
|
8
|
} from './messageTypes';
|
10
|
9
|
|
|
10
|
+import { AbstractAudioContextAdapter } from '../AbstractAudioContextAdapter';
|
|
11
|
+
|
11
|
12
|
const logger = require('jitsi-meet-logger').getLogger(__filename);
|
12
|
13
|
|
13
|
14
|
/**
|
14
|
15
|
* Recording adapter that uses libflac.js in the background.
|
15
|
16
|
*/
|
16
|
|
-export class FlacAdapter extends RecordingAdapter {
|
|
17
|
+export class FlacAdapter extends AbstractAudioContextAdapter {
|
17
|
18
|
|
18
|
19
|
/**
|
19
|
20
|
* Instance of flacEncodeWorker.
|
20
|
21
|
*/
|
21
|
22
|
_encoder = null;
|
22
|
23
|
|
23
|
|
- /**
|
24
|
|
- * The {@code AudioContext} instance.
|
25
|
|
- */
|
26
|
|
- _audioContext = null;
|
27
|
|
-
|
28
|
|
- /**
|
29
|
|
- * The {@code ScriptProcessorNode} instance.
|
30
|
|
- */
|
31
|
|
- _audioProcessingNode = null;
|
32
|
|
-
|
33
|
|
- /**
|
34
|
|
- * The {@code MediaStreamAudioSourceNode} instance.
|
35
|
|
- */
|
36
|
|
- _audioSource = null;
|
37
|
|
-
|
38
|
|
- /**
|
39
|
|
- * The {@code MediaStream} instance, representing the current audio device.
|
40
|
|
- */
|
41
|
|
- _stream = null;
|
42
|
|
-
|
43
|
24
|
/**
|
44
|
25
|
* Resolve function of the promise returned by {@code stop()}.
|
45
|
26
|
* This is called after the WebWorker sends back {@code WORKER_BLOB_READY}.
|
46
|
27
|
*/
|
47
|
28
|
_stopPromiseResolver = null;
|
48
|
29
|
|
|
30
|
+ _initPromiseResolver = null;
|
|
31
|
+
|
49
|
32
|
/**
|
50
|
33
|
* Initialization promise.
|
51
|
34
|
*/
|
52
|
35
|
_initPromise = null;
|
53
|
36
|
|
54
|
|
- _sampleRate = 44100;
|
|
37
|
+ /**
|
|
38
|
+ * Constructor.
|
|
39
|
+ */
|
|
40
|
+ constructor() {
|
|
41
|
+ super();
|
|
42
|
+ this._onAudioProcess = this._onAudioProcess.bind(this);
|
|
43
|
+ this._onWorkerMessage = this._onWorkerMessage.bind(this);
|
|
44
|
+ }
|
55
|
45
|
|
56
|
46
|
/**
|
57
|
47
|
* Implements {@link RecordingAdapter#start()}.
|
|
@@ -64,8 +54,7 @@ export class FlacAdapter extends RecordingAdapter {
|
64
|
54
|
}
|
65
|
55
|
|
66
|
56
|
return this._initPromise.then(() => {
|
67
|
|
- this._audioSource.connect(this._audioProcessingNode);
|
68
|
|
- this._audioProcessingNode.connect(this._audioContext.destination);
|
|
57
|
+ this._connectAudioGraph();
|
69
|
58
|
});
|
70
|
59
|
}
|
71
|
60
|
|
|
@@ -83,9 +72,7 @@ export class FlacAdapter extends RecordingAdapter {
|
83
|
72
|
|
84
|
73
|
return new Promise(resolve => {
|
85
|
74
|
this._initPromise = null;
|
86
|
|
- this._audioProcessingNode.onaudioprocess = undefined;
|
87
|
|
- this._audioProcessingNode.disconnect();
|
88
|
|
- this._audioSource.disconnect();
|
|
75
|
+ this._disconnectAudioGraph();
|
89
|
76
|
this._stopPromiseResolver = resolve;
|
90
|
77
|
this._encoder.postMessage({
|
91
|
78
|
command: MAIN_THREAD_FINISH
|
|
@@ -146,29 +133,6 @@ export class FlacAdapter extends RecordingAdapter {
|
146
|
133
|
return this._replaceMic(micDeviceId);
|
147
|
134
|
}
|
148
|
135
|
|
149
|
|
- /**
|
150
|
|
- * Replaces the current microphone MediaStream.
|
151
|
|
- *
|
152
|
|
- * @param {string} micDeviceId - New microphone ID.
|
153
|
|
- * @returns {Promise}
|
154
|
|
- */
|
155
|
|
- _replaceMic(micDeviceId) {
|
156
|
|
- if (this._audioContext && this._audioProcessingNode) {
|
157
|
|
- return this._getAudioStream(micDeviceId).then(newStream => {
|
158
|
|
- const newSource = this._audioContext
|
159
|
|
- .createMediaStreamSource(newStream);
|
160
|
|
-
|
161
|
|
- this._audioSource.disconnect();
|
162
|
|
- newSource.connect(this._audioProcessingNode);
|
163
|
|
- this._stream = newStream;
|
164
|
|
- this._audioSource = newSource;
|
165
|
|
-
|
166
|
|
- });
|
167
|
|
- }
|
168
|
|
-
|
169
|
|
- return Promise.resolve();
|
170
|
|
- }
|
171
|
|
-
|
172
|
136
|
/**
|
173
|
137
|
* Initialize the adapter.
|
174
|
138
|
*
|
|
@@ -181,17 +145,6 @@ export class FlacAdapter extends RecordingAdapter {
|
181
|
145
|
return Promise.resolve();
|
182
|
146
|
}
|
183
|
147
|
|
184
|
|
- // sampleRate is browser and OS dependent.
|
185
|
|
- // Setting sampleRate explicitly is in the specs but not implemented
|
186
|
|
- // by browsers.
|
187
|
|
- // See: https://developer.mozilla.org/en-US/docs/Web/API/AudioContext/
|
188
|
|
- // AudioContext#Browser_compatibility
|
189
|
|
- // And https://bugs.chromium.org/p/chromium/issues/detail?id=432248
|
190
|
|
-
|
191
|
|
- this._audioContext = new AudioContext();
|
192
|
|
- this._sampleRate = this._audioContext.sampleRate;
|
193
|
|
- logger.log(`Current sampleRate ${this._sampleRate}.`);
|
194
|
|
-
|
195
|
148
|
const promiseInitWorker = new Promise((resolve, reject) => {
|
196
|
149
|
try {
|
197
|
150
|
this._loadWebWorker();
|
|
@@ -199,28 +152,11 @@ export class FlacAdapter extends RecordingAdapter {
|
199
|
152
|
reject();
|
200
|
153
|
}
|
201
|
154
|
|
202
|
|
- // set up listen for messages from the WebWorker
|
203
|
|
- this._encoder.onmessage = e => {
|
204
|
|
- if (e.data.command === WORKER_BLOB_READY) {
|
205
|
|
- // Received a Blob representing an encoded FLAC file.
|
206
|
|
- this._data = e.data.buf;
|
207
|
|
- if (this._stopPromiseResolver !== null) {
|
208
|
|
- this._stopPromiseResolver();
|
209
|
|
- this._stopPromiseResolver = null;
|
210
|
|
- this._encoder.terminate();
|
211
|
|
- this._encoder = null;
|
212
|
|
- }
|
213
|
|
- } else if (e.data.command === DEBUG) {
|
214
|
|
- logger.log(e.data);
|
215
|
|
- } else if (e.data.command === WORKER_LIBFLAC_READY) {
|
216
|
|
- logger.log('libflac is ready.');
|
217
|
|
- resolve();
|
218
|
|
- } else {
|
219
|
|
- logger.error(
|
220
|
|
- `Unknown event
|
221
|
|
- from encoder (WebWorker): "${e.data.command}"!`);
|
222
|
|
- }
|
223
|
|
- };
|
|
155
|
+ // save the Promise's resolver to resolve it later.
|
|
156
|
+ this._initPromiseResolver = resolve;
|
|
157
|
+
|
|
158
|
+ // set up listener for messages from the WebWorker
|
|
159
|
+ this._encoder.onmessage = this._onWorkerMessage;
|
224
|
160
|
|
225
|
161
|
this._encoder.postMessage({
|
226
|
162
|
command: MAIN_THREAD_INIT,
|
|
@@ -231,38 +167,67 @@ export class FlacAdapter extends RecordingAdapter {
|
231
|
167
|
});
|
232
|
168
|
});
|
233
|
169
|
|
234
|
|
- const callbackInitAudioContext = () =>
|
235
|
|
- this._getAudioStream(micDeviceId)
|
236
|
|
- .then(stream => {
|
237
|
|
- this._stream = stream;
|
238
|
|
- this._audioSource
|
239
|
|
- = this._audioContext.createMediaStreamSource(stream);
|
240
|
|
- this._audioProcessingNode
|
241
|
|
- = this._audioContext.createScriptProcessor(4096, 1, 1);
|
242
|
|
- this._audioProcessingNode.onaudioprocess = e => {
|
243
|
|
- // Delegates to the WebWorker to do the encoding.
|
244
|
|
- // The return of getChannelData() is a Float32Array,
|
245
|
|
- // each element representing one sample.
|
246
|
|
- const channelLeft = e.inputBuffer.getChannelData(0);
|
247
|
|
-
|
248
|
|
- this._encoder.postMessage({
|
249
|
|
- command: MAIN_THREAD_NEW_DATA_ARRIVED,
|
250
|
|
- buf: channelLeft
|
251
|
|
- });
|
252
|
|
- };
|
253
|
|
- logger.debug('AudioContext is set up.');
|
254
|
|
- })
|
255
|
|
- .catch(err => {
|
256
|
|
- logger.error(`Error calling getUserMedia(): ${err}`);
|
257
|
|
-
|
258
|
|
- return Promise.reject(err);
|
259
|
|
- });
|
260
|
|
-
|
261
|
|
- // Because Promise constructor immediately executes the executor
|
262
|
|
- // function. This is undesirable, we want callbackInitAudioContext to be
|
263
|
|
- // executed only **after** promiseInitWorker is resolved.
|
|
170
|
+ // Arrow function is used here because we want AudioContext to be
|
|
171
|
+ // initialized only **after** promiseInitWorker is resolved.
|
264
|
172
|
return promiseInitWorker
|
265
|
|
- .then(callbackInitAudioContext);
|
|
173
|
+ .then(() =>
|
|
174
|
+ this._initializeAudioContext(
|
|
175
|
+ micDeviceId,
|
|
176
|
+ this._onAudioProcess
|
|
177
|
+ ));
|
|
178
|
+ }
|
|
179
|
+
|
|
180
|
+ /**
|
|
181
|
+ * Callback function for handling AudioProcessingEvents.
|
|
182
|
+ *
|
|
183
|
+ * @private
|
|
184
|
+ * @param {AudioProcessingEvent} e - The event containing the raw PCM.
|
|
185
|
+ * @returns {void}
|
|
186
|
+ */
|
|
187
|
+ _onAudioProcess(e) {
|
|
188
|
+ // Delegates to the WebWorker to do the encoding.
|
|
189
|
+ // The return of getChannelData() is a Float32Array,
|
|
190
|
+ // each element representing one sample.
|
|
191
|
+ const channelLeft = e.inputBuffer.getChannelData(0);
|
|
192
|
+
|
|
193
|
+ this._encoder.postMessage({
|
|
194
|
+ command: MAIN_THREAD_NEW_DATA_ARRIVED,
|
|
195
|
+ buf: channelLeft
|
|
196
|
+ });
|
|
197
|
+ }
|
|
198
|
+
|
|
199
|
+ /**
|
|
200
|
+ * Handler for messages from flacEncodeWorker.
|
|
201
|
+ *
|
|
202
|
+ * @private
|
|
203
|
+ * @param {MessageEvent} e - The event sent by the WebWorker.
|
|
204
|
+ * @returns {void}
|
|
205
|
+ */
|
|
206
|
+ _onWorkerMessage(e) {
|
|
207
|
+ switch (e.data.command) {
|
|
208
|
+ case WORKER_BLOB_READY:
|
|
209
|
+ // Received a Blob representing an encoded FLAC file.
|
|
210
|
+ this._data = e.data.buf;
|
|
211
|
+ if (this._stopPromiseResolver !== null) {
|
|
212
|
+ this._stopPromiseResolver();
|
|
213
|
+ this._stopPromiseResolver = null;
|
|
214
|
+ this._encoder.terminate();
|
|
215
|
+ this._encoder = null;
|
|
216
|
+ }
|
|
217
|
+ break;
|
|
218
|
+ case DEBUG:
|
|
219
|
+ logger.log(e.data);
|
|
220
|
+ break;
|
|
221
|
+ case WORKER_LIBFLAC_READY:
|
|
222
|
+ logger.log('libflac is ready.');
|
|
223
|
+ this._initPromiseResolver();
|
|
224
|
+ break;
|
|
225
|
+ default:
|
|
226
|
+ logger.error(
|
|
227
|
+ `Unknown event
|
|
228
|
+ from encoder (WebWorker): "${e.data.command}"!`);
|
|
229
|
+ break;
|
|
230
|
+ }
|
266
|
231
|
}
|
267
|
232
|
|
268
|
233
|
/**
|