Switching microphone on the fly: flac and wav support

This commit is contained in:
Radium Zheng 2018-07-31 19:53:22 +10:00
parent e0ac3efb5c
commit b6e1a49d33
7 changed files with 150 additions and 37 deletions

View File

@ -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.
*

View File

@ -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;
});

View File

@ -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);

View File

@ -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.

View File

@ -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;

View File

@ -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

View File

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