react/features/local-recording/recording/flac/FlacAdapter.js (141 lines of code) (raw):
import logger from '../../logger';
import { AbstractAudioContextAdapter } from '../AbstractAudioContextAdapter';
import {
DEBUG,
MAIN_THREAD_FINISH,
MAIN_THREAD_INIT,
MAIN_THREAD_NEW_DATA_ARRIVED,
WORKER_BLOB_READY,
WORKER_LIBFLAC_READY
} from './messageTypes';
/**
* Recording adapter that uses libflac.js in the background.
*/
export class FlacAdapter extends AbstractAudioContextAdapter {
/**
* Instance of WebWorker (flacEncodeWorker).
*/
_encoder = null;
/**
* Resolve function of the Promise returned by {@code stop()}.
* This is called after the WebWorker sends back {@code WORKER_BLOB_READY}.
*/
_stopPromiseResolver = null;
/**
* Resolve function of the Promise that initializes the flacEncodeWorker.
*/
_initWorkerPromiseResolver = null;
/**
* Initialization promise.
*/
_initPromise = null;
/**
* Constructor.
*/
constructor() {
super();
this._onAudioProcess = this._onAudioProcess.bind(this);
this._onWorkerMessage = this._onWorkerMessage.bind(this);
}
/**
* Implements {@link RecordingAdapter#start()}.
*
* @inheritdoc
*/
start(micDeviceId) {
if (!this._initPromise) {
this._initPromise = this._initialize(micDeviceId);
}
return this._initPromise.then(() => {
this._connectAudioGraph();
});
}
/**
* Implements {@link RecordingAdapter#stop()}.
*
* @inheritdoc
*/
stop() {
if (!this._encoder) {
logger.error('Attempting to stop but has nothing to stop.');
return Promise.reject();
}
return new Promise(resolve => {
this._initPromise = null;
this._disconnectAudioGraph();
this._stopPromiseResolver = resolve;
this._encoder.postMessage({
command: MAIN_THREAD_FINISH
});
});
}
/**
* Implements {@link RecordingAdapter#exportRecordedData()}.
*
* @inheritdoc
*/
exportRecordedData() {
if (this._data !== null) {
return Promise.resolve({
data: this._data,
format: 'flac'
});
}
return Promise.reject('No audio data recorded.');
}
/**
* Implements {@link RecordingAdapter#setMuted()}.
*
* @inheritdoc
*/
setMuted(muted) {
const shouldEnable = !muted;
if (!this._stream) {
return Promise.resolve();
}
const track = this._stream.getAudioTracks()[0];
if (!track) {
logger.error('Cannot mute/unmute. Track not found!');
return Promise.resolve();
}
if (track.enabled !== shouldEnable) {
track.enabled = shouldEnable;
logger.log(muted ? 'Mute' : 'Unmute');
}
return Promise.resolve();
}
/**
* Implements {@link RecordingAdapter#setMicDevice()}.
*
* @inheritdoc
*/
setMicDevice(micDeviceId) {
return this._replaceMic(micDeviceId);
}
/**
* Initialize the adapter.
*
* @private
* @param {string} micDeviceId - The current microphone device ID.
* @returns {Promise}
*/
_initialize(micDeviceId) {
if (this._encoder !== null) {
return Promise.resolve();
}
const promiseInitWorker = new Promise((resolve, reject) => {
try {
this._loadWebWorker();
} catch (e) {
reject();
}
// Save the Promise's resolver to resolve it later.
// This Promise is only resolved in _onWorkerMessage when we
// receive WORKER_LIBFLAC_READY from the WebWorker.
this._initWorkerPromiseResolver = resolve;
// set up listener for messages from the WebWorker
this._encoder.onmessage = this._onWorkerMessage;
this._encoder.postMessage({
command: MAIN_THREAD_INIT,
config: {
sampleRate: this._sampleRate,
bps: 16
}
});
});
// Arrow function is used here because we want AudioContext to be
// initialized only **after** promiseInitWorker is resolved.
return promiseInitWorker
.then(() =>
this._initializeAudioContext(
micDeviceId,
this._onAudioProcess
));
}
/**
* Callback function for handling AudioProcessingEvents.
*
* @private
* @param {AudioProcessingEvent} e - The event containing the raw PCM.
* @returns {void}
*/
_onAudioProcess(e) {
// Delegates to the WebWorker to do the encoding.
// The return of getChannelData() is a Float32Array,
// each element representing one sample.
const channelLeft = e.inputBuffer.getChannelData(0);
this._encoder.postMessage({
command: MAIN_THREAD_NEW_DATA_ARRIVED,
buf: channelLeft
});
}
/**
* Handler for messages from flacEncodeWorker.
*
* @private
* @param {MessageEvent} e - The event sent by the WebWorker.
* @returns {void}
*/
_onWorkerMessage(e) {
switch (e.data.command) {
case WORKER_BLOB_READY:
// Received a Blob representing an encoded FLAC file.
this._data = e.data.buf;
if (this._stopPromiseResolver !== null) {
this._stopPromiseResolver();
this._stopPromiseResolver = null;
this._encoder.terminate();
this._encoder = null;
}
break;
case DEBUG:
logger.log(e.data);
break;
case WORKER_LIBFLAC_READY:
logger.log('libflac is ready.');
this._initWorkerPromiseResolver();
break;
default:
logger.error(
`Unknown event
from encoder (WebWorker): "${e.data.command}"!`);
break;
}
}
/**
* Loads the WebWorker.
*
* @private
* @returns {void}
*/
_loadWebWorker() {
// 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
// only when flac recording is in use.
try {
// try load the minified version first
this._encoder = new Worker('/libs/flacEncodeWorker.min.js', { name: 'FLAC encoder worker' });
} catch (exception1) {
// if failed, try unminified version
try {
this._encoder = new Worker('/libs/flacEncodeWorker.js', { name: 'FLAC encoder worker' });
} catch (exception2) {
throw new Error('Failed to load flacEncodeWorker.');
}
}
}
}