diff --git a/react/features/local-recording/controller/RecordingController.js b/react/features/local-recording/controller/RecordingController.js index c79515c3e..bbb542898 100644 --- a/react/features/local-recording/controller/RecordingController.js +++ b/react/features/local-recording/controller/RecordingController.js @@ -72,7 +72,12 @@ const ControllerState = Object.freeze({ /** * Stopping. */ - STOPPING: Symbol('STOPPING') + STOPPING: Symbol('STOPPING'), + + /** + * Failed, due to error during starting / stopping process. + */ + FAILED: Symbol('FAILED') }); /** @@ -147,6 +152,13 @@ class RecordingController { */ _isMuted = false; + /** + * The ID of the active microphone. + * + * @private + */ + _micDeviceId = 'default'; + /** * Current recording format. This will be in effect from the next * recording session, i.e., if this value is changed during an on-going @@ -190,14 +202,15 @@ class RecordingController { * @returns {void} */ constructor() { - this._updateStats = this._updateStats.bind(this); + this.registerEvents = this.registerEvents.bind(this); + this.getParticipantsStats = this.getParticipantsStats.bind(this); this._onStartCommand = this._onStartCommand.bind(this); this._onStopCommand = this._onStopCommand.bind(this); this._onPingCommand = this._onPingCommand.bind(this); this._doStartRecording = this._doStartRecording.bind(this); this._doStopRecording = this._doStopRecording.bind(this); - this.registerEvents = this.registerEvents.bind(this); - this.getParticipantsStats = this.getParticipantsStats.bind(this); + this._updateStats = this._updateStats.bind(this); + this._switchToNewSession = this._switchToNewSession.bind(this); } registerEvents: () => void; @@ -311,6 +324,34 @@ class RecordingController { } } + /** + * Changes the current microphone. + * + * @param {string} micDeviceId - The new microphone device ID. + * @returns {void} + */ + setMicDevice(micDeviceId: string) { + if (micDeviceId !== this._micDeviceId) { + this._micDeviceId = String(micDeviceId); + + if (this._state === ControllerState.RECORDING) { + // sessionManager.endSegment(this._currentSessionToken); + logger.log('Before switching microphone...'); + this._adapters[this._currentSessionToken] + .setMicDevice(this._micDeviceId) + .then(() => { + logger.log('Finished switching microphone.'); + + // sessionManager.beginSegment(this._currentSesoken); + }) + .catch(() => { + logger.error('Failed to switch microphone'); + }); + } + logger.log(`Switch microphone to ${this._micDeviceId}`); + } + } + /** * Mute or unmute audio. When muted, the ongoing local recording should * produce silence. @@ -322,7 +363,7 @@ class RecordingController { this._isMuted = Boolean(muted); if (this._state === ControllerState.RECORDING) { - this._adapters[this._currentSessionToken].setMuted(muted); + this._adapters[this._currentSessionToken].setMuted(this._isMuted); } } @@ -442,11 +483,7 @@ class RecordingController { if (this._state === ControllerState.IDLE) { this._changeState(ControllerState.STARTING); - this._format = format; - this._currentSessionToken = sessionToken; - logger.log(this._currentSessionToken); - this._adapters[sessionToken] - = this._createRecordingAdapter(); + this._switchToNewSession(sessionToken, format); this._doStartRecording(); } else if (this._state === ControllerState.RECORDING && this._currentSessionToken !== sessionToken) { @@ -455,10 +492,8 @@ class RecordingController { // moderator's, so we need to restart the recording. this._changeState(ControllerState.STOPPING); this._doStopRecording().then(() => { - this._format = format; - this._currentSessionToken = sessionToken; - this._adapters[sessionToken] - = this._createRecordingAdapter(); + this._changeState(ControllerState.STARTING); + this._switchToNewSession(sessionToken, format); this._doStartRecording(); }); } @@ -518,7 +553,7 @@ class RecordingController { if (this._state === ControllerState.STARTING) { const delegate = this._adapters[this._currentSessionToken]; - delegate.start() + delegate.start(this._micDeviceId) .then(() => { this._changeState(ControllerState.RECORDING); logger.log('Local recording engaged.'); @@ -587,6 +622,25 @@ class RecordingController { } + _switchToNewSession: (string, string) => void; + + /** + * Switches to a new local recording session. + * + * @param {string} sessionToken - The session Token. + * @param {string} format - The recording format for the session. + * @returns {void} + */ + _switchToNewSession(sessionToken, format) { + this._format = format; + this._currentSessionToken = sessionToken; + logger.log(`New session: ${this._currentSessionToken}, ` + + `format: ${this._format}`); + this._adapters[sessionToken] + = this._createRecordingAdapter(); + sessionManager.createSession(sessionToken, this._format); + } + /** * Creates a recording adapter according to the current recording format. * diff --git a/react/features/local-recording/middleware.js b/react/features/local-recording/middleware.js index 0791d2b63..84bd20115 100644 --- a/react/features/local-recording/middleware.js +++ b/react/features/local-recording/middleware.js @@ -7,6 +7,7 @@ import { toggleDialog } from '../base/dialog'; import { i18next } from '../base/i18n'; import { SET_AUDIO_MUTED } from '../base/media'; import { MiddlewareRegistry } from '../base/redux'; +import { SETTINGS_UPDATED } from '../base/settings/actionTypes'; import { showNotification } from '../notifications'; import { localRecordingEngaged, localRecordingUnengaged } from './actions'; @@ -76,10 +77,15 @@ isFeatureEnabled case SET_AUDIO_MUTED: recordingController.setMuted(action.muted); break; - } + case SETTINGS_UPDATED: { + const { micDeviceId } = getState()['features/base/settings']; - // @todo: detect change in features/base/settings micDeviceID - // @todo: SET_AUDIO_MUTED, when audio is muted + if (micDeviceId) { + recordingController.setMicDevice(micDeviceId); + } + break; + } + } return result; }); diff --git a/react/features/local-recording/recording/OggAdapter.js b/react/features/local-recording/recording/OggAdapter.js index 12f8ff886..722a1ca30 100644 --- a/react/features/local-recording/recording/OggAdapter.js +++ b/react/features/local-recording/recording/OggAdapter.js @@ -24,9 +24,9 @@ export class OggAdapter extends RecordingAdapter { * * @inheritdoc */ - start() { + start(micDeviceId) { if (!this._initPromise) { - this._initPromise = this._initialize(); + this._initPromise = this._initialize(micDeviceId); } return this._initPromise.then(() => @@ -96,15 +96,16 @@ export class OggAdapter extends RecordingAdapter { * Initialize the adapter. * * @private + * @param {string} micDeviceId - The current microphone device ID. * @returns {Promise} */ - _initialize() { + _initialize(micDeviceId) { if (this._mediaRecorder) { return Promise.resolve(); } return new Promise((resolve, error) => { - this._getAudioStream(0) + this._getAudioStream(micDeviceId) .then(stream => { this._stream = stream; this._mediaRecorder = new MediaRecorder(stream); diff --git a/react/features/local-recording/recording/RecordingAdapter.js b/react/features/local-recording/recording/RecordingAdapter.js index 897623ec6..bbfc355ad 100644 --- a/react/features/local-recording/recording/RecordingAdapter.js +++ b/react/features/local-recording/recording/RecordingAdapter.js @@ -8,9 +8,11 @@ export class RecordingAdapter { /** * Starts recording. * + * @param {string} micDeviceId - The microphone to record on. * @returns {Promise} */ - start() { + start(/* eslint-disable no-unused-vars */ + micDeviceId/* eslint-enable no-unused-vars */) { throw new Error('Not implemented'); } @@ -43,6 +45,17 @@ export class RecordingAdapter { throw new Error('Not implemented'); } + /** + * Changes the current microphone. + * + * @param {string} micDeviceId - The new microphone device ID. + * @returns {Promise} + */ + setMicDevice(/* eslint-disable no-unused-vars */ + micDeviceId/* eslint-enable no-unused-vars */) { + throw new Error('Not implemented'); + } + /** * Helper method for getting an audio {@code MediaStream}. Use this instead * of calling browser APIs directly. diff --git a/react/features/local-recording/recording/WavAdapter.js b/react/features/local-recording/recording/WavAdapter.js index 602381192..1ebebcd8d 100644 --- a/react/features/local-recording/recording/WavAdapter.js +++ b/react/features/local-recording/recording/WavAdapter.js @@ -65,9 +65,9 @@ export class WavAdapter extends RecordingAdapter { * * @inheritdoc */ - start() { + start(micDeviceId) { if (!this._initPromise) { - this._initPromise = this._initialize(); + this._initPromise = this._initialize(micDeviceId); } return this._initPromise.then(() => { @@ -197,15 +197,16 @@ export class WavAdapter extends RecordingAdapter { * Initialize the adapter. * * @private + * @param {string} micDeviceId - The current microphone device ID. * @returns {Promise} */ - _initialize() { + _initialize(micDeviceId) { if (this._isInitialized) { return Promise.resolve(); } const p = new Promise((resolve, reject) => { - this._getAudioStream(0) + this._getAudioStream(micDeviceId) .then(stream => { this._stream = stream; this._audioContext = new AudioContext(); @@ -216,9 +217,9 @@ export class WavAdapter extends RecordingAdapter { this._audioProcessingNode.onaudioprocess = e => { const channelLeft = e.inputBuffer.getChannelData(0); - // https://developer.mozilla.org/en-US/docs/ - // Web/API/AudioBuffer/getChannelData - // the returned value is an Float32Array + // See: https://developer.mozilla.org/en-US/docs/Web/API/ + // AudioBuffer/getChannelData + // The returned value is an Float32Array. this._saveWavPCM(channelLeft); }; this._isInitialized = true; diff --git a/react/features/local-recording/recording/flac/FlacAdapter.js b/react/features/local-recording/recording/flac/FlacAdapter.js index dd3c3653a..e4a9ec825 100644 --- a/react/features/local-recording/recording/flac/FlacAdapter.js +++ b/react/features/local-recording/recording/flac/FlacAdapter.js @@ -38,9 +38,9 @@ export class FlacAdapter extends RecordingAdapter { * * @inheritdoc */ - start() { + start(micDeviceId) { if (!this._initPromise) { - this._initPromise = this._initialize(); + this._initPromise = this._initialize(micDeviceId); } return this._initPromise.then(() => { @@ -114,13 +114,51 @@ export class FlacAdapter extends RecordingAdapter { return Promise.resolve(); } + /** + * Implements {@link RecordingAdapter#setMicDevice()}. + * + * @inheritdoc + */ + setMicDevice(micDeviceId) { + return this._replaceMic(micDeviceId); + } + + /** + * Replaces the current microphone MediaStream. + * + * @param {*} micDeviceId - New microphone ID. + * @returns {Promise} + */ + _replaceMic(micDeviceId) { + if (this._audioContext && this._audioProcessingNode) { + return new Promise((resolve, reject) => { + this._getAudioStream(micDeviceId).then(newStream => { + const newSource = this._audioContext + .createMediaStreamSource(newStream); + + this._audioSource.disconnect(); + newSource.connect(this._audioProcessingNode); + this._stream = newStream; + this._audioSource = newSource; + resolve(); + }) + .catch(() => { + reject(); + }); + }); + } + + return Promise.resolve(); + } + /** * Initialize the adapter. * * @private + * @param {string} micDeviceId - The current microphone device ID. * @returns {Promise} */ - _initialize() { + _initialize(micDeviceId) { if (this._encoder !== null) { return Promise.resolve(); } @@ -146,7 +184,7 @@ export class FlacAdapter extends RecordingAdapter { } else if (e.data.command === DEBUG) { logger.log(e.data); } else if (e.data.command === WORKER_LIBFLAC_READY) { - logger.debug('libflac is ready.'); + logger.log('libflac is ready.'); resolve(); } else { logger.error( @@ -165,7 +203,7 @@ export class FlacAdapter extends RecordingAdapter { }); const callbackInitAudioContext = (resolve, reject) => { - this._getAudioStream(0) + this._getAudioStream(micDeviceId) .then(stream => { this._stream = stream; this._audioContext = new AudioContext(); @@ -205,7 +243,7 @@ export class FlacAdapter extends RecordingAdapter { * @returns {void} */ _loadWebWorker() { - // FIXME: workaround for different file names in development/ + // FIXME: Workaround for different file names in development/ // production environments. // We cannot import flacEncodeWorker as a webpack module, // because it is in a different bundle and should be lazy-loaded diff --git a/react/features/local-recording/recording/flac/flacEncodeWorker.js b/react/features/local-recording/recording/flac/flacEncodeWorker.js index f2448ebd9..a129296e2 100644 --- a/react/features/local-recording/recording/flac/flacEncodeWorker.js +++ b/react/features/local-recording/recording/flac/flacEncodeWorker.js @@ -81,7 +81,7 @@ const EncoderState = Object.freeze({ }); /** - * Default compression level. + * Default FLAC compression level. */ const FLAC_COMPRESSION_LEVEL = 5;