Pārlūkot izejas kodu

Switching microphone on the fly: flac and wav support

master
Radium Zheng 7 gadus atpakaļ
vecāks
revīzija
b6e1a49d33

+ 69
- 15
react/features/local-recording/controller/RecordingController.js Parādīt failu

@@ -72,7 +72,12 @@ const ControllerState = Object.freeze({
72 72
     /**
73 73
      * Stopping.
74 74
      */
75
-    STOPPING: Symbol('STOPPING')
75
+    STOPPING: Symbol('STOPPING'),
76
+
77
+    /**
78
+     * Failed, due to error during starting / stopping process.
79
+     */
80
+    FAILED: Symbol('FAILED')
76 81
 });
77 82
 
78 83
 /**
@@ -147,6 +152,13 @@ class RecordingController {
147 152
      */
148 153
     _isMuted = false;
149 154
 
155
+    /**
156
+     * The ID of the active microphone.
157
+     *
158
+     * @private
159
+     */
160
+    _micDeviceId = 'default';
161
+
150 162
     /**
151 163
      * Current recording format. This will be in effect from the next
152 164
      * recording session, i.e., if this value is changed during an on-going
@@ -190,14 +202,15 @@ class RecordingController {
190 202
      * @returns {void}
191 203
      */
192 204
     constructor() {
193
-        this._updateStats = this._updateStats.bind(this);
205
+        this.registerEvents = this.registerEvents.bind(this);
206
+        this.getParticipantsStats = this.getParticipantsStats.bind(this);
194 207
         this._onStartCommand = this._onStartCommand.bind(this);
195 208
         this._onStopCommand = this._onStopCommand.bind(this);
196 209
         this._onPingCommand = this._onPingCommand.bind(this);
197 210
         this._doStartRecording = this._doStartRecording.bind(this);
198 211
         this._doStopRecording = this._doStopRecording.bind(this);
199
-        this.registerEvents = this.registerEvents.bind(this);
200
-        this.getParticipantsStats = this.getParticipantsStats.bind(this);
212
+        this._updateStats = this._updateStats.bind(this);
213
+        this._switchToNewSession = this._switchToNewSession.bind(this);
201 214
     }
202 215
 
203 216
     registerEvents: () => void;
@@ -311,6 +324,34 @@ class RecordingController {
311 324
         }
312 325
     }
313 326
 
327
+    /**
328
+     * Changes the current microphone.
329
+     *
330
+     * @param {string} micDeviceId - The new microphone device ID.
331
+     * @returns {void}
332
+     */
333
+    setMicDevice(micDeviceId: string) {
334
+        if (micDeviceId !== this._micDeviceId) {
335
+            this._micDeviceId = String(micDeviceId);
336
+
337
+            if (this._state === ControllerState.RECORDING) {
338
+                // sessionManager.endSegment(this._currentSessionToken);
339
+                logger.log('Before switching microphone...');
340
+                this._adapters[this._currentSessionToken]
341
+                    .setMicDevice(this._micDeviceId)
342
+                    .then(() => {
343
+                        logger.log('Finished switching microphone.');
344
+
345
+                        // sessionManager.beginSegment(this._currentSesoken);
346
+                    })
347
+                    .catch(() => {
348
+                        logger.error('Failed to switch microphone');
349
+                    });
350
+            }
351
+            logger.log(`Switch microphone to ${this._micDeviceId}`);
352
+        }
353
+    }
354
+
314 355
     /**
315 356
      * Mute or unmute audio. When muted, the ongoing local recording should
316 357
      * produce silence.
@@ -322,7 +363,7 @@ class RecordingController {
322 363
         this._isMuted = Boolean(muted);
323 364
 
324 365
         if (this._state === ControllerState.RECORDING) {
325
-            this._adapters[this._currentSessionToken].setMuted(muted);
366
+            this._adapters[this._currentSessionToken].setMuted(this._isMuted);
326 367
         }
327 368
     }
328 369
 
@@ -442,11 +483,7 @@ class RecordingController {
442 483
 
443 484
         if (this._state === ControllerState.IDLE) {
444 485
             this._changeState(ControllerState.STARTING);
445
-            this._format = format;
446
-            this._currentSessionToken = sessionToken;
447
-            logger.log(this._currentSessionToken);
448
-            this._adapters[sessionToken]
449
-                 = this._createRecordingAdapter();
486
+            this._switchToNewSession(sessionToken, format);
450 487
             this._doStartRecording();
451 488
         } else if (this._state === ControllerState.RECORDING
452 489
             && this._currentSessionToken !== sessionToken) {
@@ -455,10 +492,8 @@ class RecordingController {
455 492
             // moderator's, so we need to restart the recording.
456 493
             this._changeState(ControllerState.STOPPING);
457 494
             this._doStopRecording().then(() => {
458
-                this._format = format;
459
-                this._currentSessionToken = sessionToken;
460
-                this._adapters[sessionToken]
461
-                    = this._createRecordingAdapter();
495
+                this._changeState(ControllerState.STARTING);
496
+                this._switchToNewSession(sessionToken, format);
462 497
                 this._doStartRecording();
463 498
             });
464 499
         }
@@ -518,7 +553,7 @@ class RecordingController {
518 553
         if (this._state === ControllerState.STARTING) {
519 554
             const delegate = this._adapters[this._currentSessionToken];
520 555
 
521
-            delegate.start()
556
+            delegate.start(this._micDeviceId)
522 557
             .then(() => {
523 558
                 this._changeState(ControllerState.RECORDING);
524 559
                 logger.log('Local recording engaged.');
@@ -587,6 +622,25 @@ class RecordingController {
587 622
 
588 623
     }
589 624
 
625
+    _switchToNewSession: (string, string) => void;
626
+
627
+    /**
628
+     * Switches to a new local recording session.
629
+     *
630
+     * @param {string} sessionToken - The session Token.
631
+     * @param {string} format - The recording format for the session.
632
+     * @returns {void}
633
+     */
634
+    _switchToNewSession(sessionToken, format) {
635
+        this._format = format;
636
+        this._currentSessionToken = sessionToken;
637
+        logger.log(`New session: ${this._currentSessionToken}, `
638
+            + `format: ${this._format}`);
639
+        this._adapters[sessionToken]
640
+             = this._createRecordingAdapter();
641
+        sessionManager.createSession(sessionToken, this._format);
642
+    }
643
+
590 644
     /**
591 645
      * Creates a recording adapter according to the current recording format.
592 646
      *

+ 9
- 3
react/features/local-recording/middleware.js Parādīt failu

@@ -7,6 +7,7 @@ import { toggleDialog } from '../base/dialog';
7 7
 import { i18next } from '../base/i18n';
8 8
 import { SET_AUDIO_MUTED } from '../base/media';
9 9
 import { MiddlewareRegistry } from '../base/redux';
10
+import { SETTINGS_UPDATED } from '../base/settings/actionTypes';
10 11
 import { showNotification } from '../notifications';
11 12
 
12 13
 import { localRecordingEngaged, localRecordingUnengaged } from './actions';
@@ -76,10 +77,15 @@ isFeatureEnabled
76 77
     case SET_AUDIO_MUTED:
77 78
         recordingController.setMuted(action.muted);
78 79
         break;
79
-    }
80
+    case SETTINGS_UPDATED: {
81
+        const { micDeviceId } = getState()['features/base/settings'];
80 82
 
81
-    // @todo: detect change in features/base/settings micDeviceID
82
-    // @todo: SET_AUDIO_MUTED, when audio is muted
83
+        if (micDeviceId) {
84
+            recordingController.setMicDevice(micDeviceId);
85
+        }
86
+        break;
87
+    }
88
+    }
83 89
 
84 90
     return result;
85 91
 });

+ 5
- 4
react/features/local-recording/recording/OggAdapter.js Parādīt failu

@@ -24,9 +24,9 @@ export class OggAdapter extends RecordingAdapter {
24 24
      *
25 25
      * @inheritdoc
26 26
      */
27
-    start() {
27
+    start(micDeviceId) {
28 28
         if (!this._initPromise) {
29
-            this._initPromise = this._initialize();
29
+            this._initPromise = this._initialize(micDeviceId);
30 30
         }
31 31
 
32 32
         return this._initPromise.then(() =>
@@ -96,15 +96,16 @@ export class OggAdapter extends RecordingAdapter {
96 96
      * Initialize the adapter.
97 97
      *
98 98
      * @private
99
+     * @param {string} micDeviceId - The current microphone device ID.
99 100
      * @returns {Promise}
100 101
      */
101
-    _initialize() {
102
+    _initialize(micDeviceId) {
102 103
         if (this._mediaRecorder) {
103 104
             return Promise.resolve();
104 105
         }
105 106
 
106 107
         return new Promise((resolve, error) => {
107
-            this._getAudioStream(0)
108
+            this._getAudioStream(micDeviceId)
108 109
             .then(stream => {
109 110
                 this._stream = stream;
110 111
                 this._mediaRecorder = new MediaRecorder(stream);

+ 14
- 1
react/features/local-recording/recording/RecordingAdapter.js Parādīt failu

@@ -8,9 +8,11 @@ export class RecordingAdapter {
8 8
     /**
9 9
      * Starts recording.
10 10
      *
11
+     * @param {string} micDeviceId - The microphone to record on.
11 12
      * @returns {Promise}
12 13
      */
13
-    start() {
14
+    start(/* eslint-disable no-unused-vars */
15
+            micDeviceId/* eslint-enable no-unused-vars */) {
14 16
         throw new Error('Not implemented');
15 17
     }
16 18
 
@@ -43,6 +45,17 @@ export class RecordingAdapter {
43 45
         throw new Error('Not implemented');
44 46
     }
45 47
 
48
+    /**
49
+     * Changes the current microphone.
50
+     *
51
+     * @param {string} micDeviceId - The new microphone device ID.
52
+     * @returns {Promise}
53
+     */
54
+    setMicDevice(/* eslint-disable no-unused-vars */
55
+            micDeviceId/* eslint-enable no-unused-vars */) {
56
+        throw new Error('Not implemented');
57
+    }
58
+
46 59
     /**
47 60
      * Helper method for getting an audio {@code MediaStream}. Use this instead
48 61
      * of calling browser APIs directly.

+ 8
- 7
react/features/local-recording/recording/WavAdapter.js Parādīt failu

@@ -65,9 +65,9 @@ export class WavAdapter extends RecordingAdapter {
65 65
      *
66 66
      * @inheritdoc
67 67
      */
68
-    start() {
68
+    start(micDeviceId) {
69 69
         if (!this._initPromise) {
70
-            this._initPromise = this._initialize();
70
+            this._initPromise = this._initialize(micDeviceId);
71 71
         }
72 72
 
73 73
         return this._initPromise.then(() => {
@@ -197,15 +197,16 @@ export class WavAdapter extends RecordingAdapter {
197 197
      * Initialize the adapter.
198 198
      *
199 199
      * @private
200
+     * @param {string} micDeviceId - The current microphone device ID.
200 201
      * @returns {Promise}
201 202
      */
202
-    _initialize() {
203
+    _initialize(micDeviceId) {
203 204
         if (this._isInitialized) {
204 205
             return Promise.resolve();
205 206
         }
206 207
 
207 208
         const p = new Promise((resolve, reject) => {
208
-            this._getAudioStream(0)
209
+            this._getAudioStream(micDeviceId)
209 210
             .then(stream => {
210 211
                 this._stream = stream;
211 212
                 this._audioContext = new AudioContext();
@@ -216,9 +217,9 @@ export class WavAdapter extends RecordingAdapter {
216 217
                 this._audioProcessingNode.onaudioprocess = e => {
217 218
                     const channelLeft = e.inputBuffer.getChannelData(0);
218 219
 
219
-                    // https://developer.mozilla.org/en-US/docs/
220
-                    // Web/API/AudioBuffer/getChannelData
221
-                    // the returned value is an Float32Array
220
+                    // See: https://developer.mozilla.org/en-US/docs/Web/API/
221
+                    //      AudioBuffer/getChannelData
222
+                    // The returned value is an Float32Array.
222 223
                     this._saveWavPCM(channelLeft);
223 224
                 };
224 225
                 this._isInitialized = true;

+ 44
- 6
react/features/local-recording/recording/flac/FlacAdapter.js Parādīt failu

@@ -38,9 +38,9 @@ export class FlacAdapter extends RecordingAdapter {
38 38
      *
39 39
      * @inheritdoc
40 40
      */
41
-    start() {
41
+    start(micDeviceId) {
42 42
         if (!this._initPromise) {
43
-            this._initPromise = this._initialize();
43
+            this._initPromise = this._initialize(micDeviceId);
44 44
         }
45 45
 
46 46
         return this._initPromise.then(() => {
@@ -114,13 +114,51 @@ export class FlacAdapter extends RecordingAdapter {
114 114
         return Promise.resolve();
115 115
     }
116 116
 
117
+    /**
118
+     * Implements {@link RecordingAdapter#setMicDevice()}.
119
+     *
120
+     * @inheritdoc
121
+     */
122
+    setMicDevice(micDeviceId) {
123
+        return this._replaceMic(micDeviceId);
124
+    }
125
+
126
+    /**
127
+     * Replaces the current microphone MediaStream.
128
+     *
129
+     * @param {*} micDeviceId - New microphone ID.
130
+     * @returns {Promise}
131
+     */
132
+    _replaceMic(micDeviceId) {
133
+        if (this._audioContext && this._audioProcessingNode) {
134
+            return new Promise((resolve, reject) => {
135
+                this._getAudioStream(micDeviceId).then(newStream => {
136
+                    const newSource = this._audioContext
137
+                        .createMediaStreamSource(newStream);
138
+
139
+                    this._audioSource.disconnect();
140
+                    newSource.connect(this._audioProcessingNode);
141
+                    this._stream = newStream;
142
+                    this._audioSource = newSource;
143
+                    resolve();
144
+                })
145
+                .catch(() => {
146
+                    reject();
147
+                });
148
+            });
149
+        }
150
+
151
+        return Promise.resolve();
152
+    }
153
+
117 154
     /**
118 155
      * Initialize the adapter.
119 156
      *
120 157
      * @private
158
+     * @param {string} micDeviceId - The current microphone device ID.
121 159
      * @returns {Promise}
122 160
      */
123
-    _initialize() {
161
+    _initialize(micDeviceId) {
124 162
         if (this._encoder !== null) {
125 163
             return Promise.resolve();
126 164
         }
@@ -146,7 +184,7 @@ export class FlacAdapter extends RecordingAdapter {
146 184
                 } else if (e.data.command === DEBUG) {
147 185
                     logger.log(e.data);
148 186
                 } else if (e.data.command === WORKER_LIBFLAC_READY) {
149
-                    logger.debug('libflac is ready.');
187
+                    logger.log('libflac is ready.');
150 188
                     resolve();
151 189
                 } else {
152 190
                     logger.error(
@@ -165,7 +203,7 @@ export class FlacAdapter extends RecordingAdapter {
165 203
         });
166 204
 
167 205
         const callbackInitAudioContext = (resolve, reject) => {
168
-            this._getAudioStream(0)
206
+            this._getAudioStream(micDeviceId)
169 207
             .then(stream => {
170 208
                 this._stream = stream;
171 209
                 this._audioContext = new AudioContext();
@@ -205,7 +243,7 @@ export class FlacAdapter extends RecordingAdapter {
205 243
      * @returns {void}
206 244
      */
207 245
     _loadWebWorker() {
208
-        // FIXME: workaround for different file names in development/
246
+        // FIXME: Workaround for different file names in development/
209 247
         // production environments.
210 248
         // We cannot import flacEncodeWorker as a webpack module,
211 249
         // because it is in a different bundle and should be lazy-loaded

+ 1
- 1
react/features/local-recording/recording/flac/flacEncodeWorker.js Parādīt failu

@@ -81,7 +81,7 @@ const EncoderState = Object.freeze({
81 81
 });
82 82
 
83 83
 /**
84
- * Default compression level.
84
+ * Default FLAC compression level.
85 85
  */
86 86
 const FLAC_COMPRESSION_LEVEL = 5;
87 87
 

Notiek ielāde…
Atcelt
Saglabāt