Switching microphone on the fly: flac and wav support
This commit is contained in:
parent
e0ac3efb5c
commit
b6e1a49d33
|
@ -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.
|
||||
*
|
||||
|
|
|
@ -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;
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -81,7 +81,7 @@ const EncoderState = Object.freeze({
|
|||
});
|
||||
|
||||
/**
|
||||
* Default compression level.
|
||||
* Default FLAC compression level.
|
||||
*/
|
||||
const FLAC_COMPRESSION_LEVEL = 5;
|
||||
|
||||
|
|
Loading…
Reference in New Issue