UNPKG

recordrtc

Version:

RecordRTC is a server-less (entire client-side) JavaScript library can be used to record WebRTC audio/video media streams. It supports cross-browser audio/video recording.

530 lines (430 loc) 16.7 kB
// source code from: http://typedarray.org/wp-content/projects/WebAudioRecorder/script.js // https://github.com/mattdiamond/Recorderjs#license-mit // ______________________ // StereoAudioRecorder.js /** * StereoAudioRecorder is a standalone class used by {@link RecordRTC} to bring "stereo" audio-recording in chrome. * @summary JavaScript standalone object for stereo audio recording. * @license {@link https://github.com/muaz-khan/RecordRTC#license|MIT} * @author {@link http://www.MuazKhan.com|Muaz Khan} * @typedef StereoAudioRecorder * @class * @example * var recorder = new StereoAudioRecorder(MediaStream, { * sampleRate: 44100, * bufferSize: 4096 * }); * recorder.record(); * recorder.stop(function(blob) { * video.src = URL.createObjectURL(blob); * }); * @see {@link https://github.com/muaz-khan/RecordRTC|RecordRTC Source Code} * @param {MediaStream} mediaStream - MediaStream object fetched using getUserMedia API or generated using captureStreamUntilEnded or WebAudio API. * @param {object} config - {sampleRate: 44100, bufferSize: 4096, numberOfAudioChannels: 1, etc.} */ function StereoAudioRecorder(mediaStream, config) { if (!mediaStream.getAudioTracks().length) { throw 'Your stream has no audio tracks.'; } config = config || {}; var self = this; // variables var leftchannel = []; var rightchannel = []; var recording = false; var recordingLength = 0; var jsAudioNode; var numberOfAudioChannels = 2; // backward compatibility if (config.leftChannel === true) { numberOfAudioChannels = 1; } if (config.numberOfAudioChannels === 1) { numberOfAudioChannels = 1; } if (!config.disableLogs) { console.debug('StereoAudioRecorder is set to record number of channels: ', numberOfAudioChannels); } function isMediaStreamActive() { if ('active' in mediaStream) { if (!mediaStream.active) { return false; } } else if ('ended' in mediaStream) { // old hack if (mediaStream.ended) { return false; } } return true; } /** * This method records MediaStream. * @method * @memberof StereoAudioRecorder * @example * recorder.record(); */ this.record = function() { if (isMediaStreamActive() === false) { throw 'Please make sure MediaStream is active.'; } // reset the buffers for the new recording leftchannel.length = rightchannel.length = 0; recordingLength = 0; if (audioInput) { audioInput.connect(jsAudioNode); } // to prevent self audio to be connected with speakers // jsAudioNode.connect(context.destination); isAudioProcessStarted = isPaused = false; recording = true; }; function mergeLeftRightBuffers(config, callback) { function mergeAudioBuffers(config, cb) { var numberOfAudioChannels = config.numberOfAudioChannels; // todo: "slice(0)" --- is it causes loop? Should be removed? var leftBuffers = config.leftBuffers.slice(0); var rightBuffers = config.rightBuffers.slice(0); var sampleRate = config.sampleRate; var internalInterleavedLength = config.internalInterleavedLength; if (numberOfAudioChannels === 2) { leftBuffers = mergeBuffers(leftBuffers, internalInterleavedLength); rightBuffers = mergeBuffers(rightBuffers, internalInterleavedLength); } if (numberOfAudioChannels === 1) { leftBuffers = mergeBuffers(leftBuffers, internalInterleavedLength); } function mergeBuffers(channelBuffer, rLength) { var result = new Float64Array(rLength); var offset = 0; var lng = channelBuffer.length; for (var i = 0; i < lng; i++) { var buffer = channelBuffer[i]; result.set(buffer, offset); offset += buffer.length; } return result; } function interleave(leftChannel, rightChannel) { var length = leftChannel.length + rightChannel.length; var result = new Float64Array(length); var inputIndex = 0; for (var index = 0; index < length;) { result[index++] = leftChannel[inputIndex]; result[index++] = rightChannel[inputIndex]; inputIndex++; } return result; } function writeUTFBytes(view, offset, string) { var lng = string.length; for (var i = 0; i < lng; i++) { view.setUint8(offset + i, string.charCodeAt(i)); } } // interleave both channels together var interleaved; if (numberOfAudioChannels === 2) { interleaved = interleave(leftBuffers, rightBuffers); } if (numberOfAudioChannels === 1) { interleaved = leftBuffers; } var interleavedLength = interleaved.length; // create wav file var resultingBufferLength = 44 + interleavedLength * 2; var buffer = new ArrayBuffer(resultingBufferLength); var view = new DataView(buffer); // RIFF chunk descriptor/identifier writeUTFBytes(view, 0, 'RIFF'); // RIFF chunk length view.setUint32(4, 44 + interleavedLength * 2, true); // RIFF type writeUTFBytes(view, 8, 'WAVE'); // format chunk identifier // FMT sub-chunk writeUTFBytes(view, 12, 'fmt '); // format chunk length view.setUint32(16, 16, true); // sample format (raw) view.setUint16(20, 1, true); // stereo (2 channels) view.setUint16(22, numberOfAudioChannels, true); // sample rate view.setUint32(24, sampleRate, true); // byte rate (sample rate * block align) view.setUint32(28, sampleRate * 2, true); // block align (channel count * bytes per sample) view.setUint16(32, numberOfAudioChannels * 2, true); // bits per sample view.setUint16(34, 16, true); // data sub-chunk // data chunk identifier writeUTFBytes(view, 36, 'data'); // data chunk length view.setUint32(40, interleavedLength * 2, true); // write the PCM samples var lng = interleavedLength; var index = 44; var volume = 1; for (var i = 0; i < lng; i++) { view.setInt16(index, interleaved[i] * (0x7FFF * volume), true); index += 2; } if (cb) { return cb({ buffer: buffer, view: view }); } postMessage({ buffer: buffer, view: view }); } if (!isChrome) { // its Microsoft Edge mergeAudioBuffers(config, function(data) { callback(data.buffer, data.view); }); return; } var webWorker = processInWebWorker(mergeAudioBuffers); webWorker.onmessage = function(event) { callback(event.data.buffer, event.data.view); // release memory URL.revokeObjectURL(webWorker.workerURL); }; webWorker.postMessage(config); } function processInWebWorker(_function) { var workerURL = URL.createObjectURL(new Blob([_function.toString(), ';this.onmessage = function (e) {' + _function.name + '(e.data);}' ], { type: 'application/javascript' })); var worker = new Worker(workerURL); worker.workerURL = workerURL; return worker; } /** * This method stops recording MediaStream. * @param {function} callback - Callback function, that is used to pass recorded blob back to the callee. * @method * @memberof StereoAudioRecorder * @example * recorder.stop(function(blob) { * video.src = URL.createObjectURL(blob); * }); */ this.stop = function(callback) { // stop recording recording = false; // to make sure onaudioprocess stops firing // audioInput.disconnect(); mergeLeftRightBuffers({ sampleRate: sampleRate, numberOfAudioChannels: numberOfAudioChannels, internalInterleavedLength: recordingLength, leftBuffers: leftchannel, rightBuffers: numberOfAudioChannels === 1 ? [] : rightchannel }, function(buffer, view) { /** * @property {Blob} blob - The recorded blob object. * @memberof StereoAudioRecorder * @example * recorder.stop(function(){ * var blob = recorder.blob; * }); */ self.blob = new Blob([view], { type: 'audio/wav' }); /** * @property {ArrayBuffer} buffer - The recorded buffer object. * @memberof StereoAudioRecorder * @example * recorder.stop(function(){ * var buffer = recorder.buffer; * }); */ self.buffer = new ArrayBuffer(view.buffer.byteLength); /** * @property {DataView} view - The recorded data-view object. * @memberof StereoAudioRecorder * @example * recorder.stop(function(){ * var view = recorder.view; * }); */ self.view = view; self.sampleRate = sampleRate; self.bufferSize = bufferSize; // recorded audio length self.length = recordingLength; if (callback) { callback(); } isAudioProcessStarted = false; }); }; if (!Storage.AudioContextConstructor) { Storage.AudioContextConstructor = new Storage.AudioContext(); } var context = Storage.AudioContextConstructor; // creates an audio node from the microphone incoming stream var audioInput = context.createMediaStreamSource(mediaStream); var legalBufferValues = [0, 256, 512, 1024, 2048, 4096, 8192, 16384]; /** * From the spec: This value controls how frequently the audioprocess event is * dispatched and how many sample-frames need to be processed each call. * Lower values for buffer size will result in a lower (better) latency. * Higher values will be necessary to avoid audio breakup and glitches * The size of the buffer (in sample-frames) which needs to * be processed each time onprocessaudio is called. * Legal values are (256, 512, 1024, 2048, 4096, 8192, 16384). * @property {number} bufferSize - Buffer-size for how frequently the audioprocess event is dispatched. * @memberof StereoAudioRecorder * @example * recorder = new StereoAudioRecorder(mediaStream, { * bufferSize: 4096 * }); */ // "0" means, let chrome decide the most accurate buffer-size for current platform. var bufferSize = typeof config.bufferSize === 'undefined' ? 4096 : config.bufferSize; if (legalBufferValues.indexOf(bufferSize) === -1) { if (!config.disableLogs) { console.warn('Legal values for buffer-size are ' + JSON.stringify(legalBufferValues, null, '\t')); } } if (context.createJavaScriptNode) { jsAudioNode = context.createJavaScriptNode(bufferSize, numberOfAudioChannels, numberOfAudioChannels); } else if (context.createScriptProcessor) { jsAudioNode = context.createScriptProcessor(bufferSize, numberOfAudioChannels, numberOfAudioChannels); } else { throw 'WebAudio API has no support on this browser.'; } // connect the stream to the gain node audioInput.connect(jsAudioNode); if (!config.bufferSize) { bufferSize = jsAudioNode.bufferSize; // device buffer-size } /** * The sample rate (in sample-frames per second) at which the * AudioContext handles audio. It is assumed that all AudioNodes * in the context run at this rate. In making this assumption, * sample-rate converters or "varispeed" processors are not supported * in real-time processing. * The sampleRate parameter describes the sample-rate of the * linear PCM audio data in the buffer in sample-frames per second. * An implementation must support sample-rates in at least * the range 22050 to 96000. * @property {number} sampleRate - Buffer-size for how frequently the audioprocess event is dispatched. * @memberof StereoAudioRecorder * @example * recorder = new StereoAudioRecorder(mediaStream, { * sampleRate: 44100 * }); */ var sampleRate = typeof config.sampleRate !== 'undefined' ? config.sampleRate : context.sampleRate || 44100; if (sampleRate < 22050 || sampleRate > 96000) { // Ref: http://stackoverflow.com/a/26303918/552182 if (!config.disableLogs) { console.warn('sample-rate must be under range 22050 and 96000.'); } } if (!config.disableLogs) { console.log('sample-rate', sampleRate); console.log('buffer-size', bufferSize); } var isPaused = false; /** * This method pauses the recording process. * @method * @memberof StereoAudioRecorder * @example * recorder.pause(); */ this.pause = function() { isPaused = true; }; /** * This method resumes the recording process. * @method * @memberof StereoAudioRecorder * @example * recorder.resume(); */ this.resume = function() { if (isMediaStreamActive() === false) { throw 'Please make sure MediaStream is active.'; } if (!recording) { if (!config.disableLogs) { console.info('Seems recording has been restarted.'); } this.record(); return; } isPaused = false; }; /** * This method resets currently recorded data. * @method * @memberof StereoAudioRecorder * @example * recorder.clearRecordedData(); */ this.clearRecordedData = function() { this.pause(); leftchannel.length = rightchannel.length = 0; recordingLength = 0; }; var isAudioProcessStarted = false; function onAudioProcessDataAvailable(e) { if (isPaused) { return; } if (isMediaStreamActive() === false) { if (!config.disableLogs) { console.log('MediaStream seems stopped.'); } jsAudioNode.disconnect(); recording = false; } if (!recording) { audioInput.disconnect(); return; } /** * This method is called on "onaudioprocess" event's first invocation. * @method {function} onAudioProcessStarted * @memberof StereoAudioRecorder * @example * recorder.onAudioProcessStarted: function() { }; */ if (!isAudioProcessStarted) { isAudioProcessStarted = true; if (config.onAudioProcessStarted) { config.onAudioProcessStarted(); } if (config.initCallback) { config.initCallback(); } } var left = e.inputBuffer.getChannelData(0); // we clone the samples leftchannel.push(new Float32Array(left)); if (numberOfAudioChannels === 2) { var right = e.inputBuffer.getChannelData(1); rightchannel.push(new Float32Array(right)); } recordingLength += bufferSize; } jsAudioNode.onaudioprocess = onAudioProcessDataAvailable; // to prevent self audio to be connected with speakers jsAudioNode.connect(context.destination); } if (typeof RecordRTC !== 'undefined') { RecordRTC.StereoAudioRecorder = StereoAudioRecorder; }