UNPKG

media-recorder-js

Version:
745 lines (624 loc) 23.1 kB
'use strict'; var ERRORS = require('./errors'); /** * @constructor QBMediaRecorder * @param {Object} [opts] - Object of parameters. * @param {String} opts[].mimeType=video - Specifies the media type and container format for the recording. You can set simply: 'video' or 'audio' or 'audio/webm' ('audio/wav' or 'audio/mp3' mimeTypes uses AudioContext API instead of MediaRecorder API); * @param {String} opts[].workerPath - Relative path from index.html. * @param {Number} opts[].timeslice=1000 - The minimum number of milliseconds of data to return in a single Blob, fire 'ondataavaible' callback (isn't need to use with 'audio/wav' of 'audio/mp3'). * @param {Boolean} opts[].ignoreMutedMedia=true - What to do with a muted input MediaStreamTrack, e.g. insert black frames/zero audio volume in the recording or ignore altogether. * @param {Function} opts[].onstart - Called to handle the start event. * @param {Function} opts[].onstop - Called to handle the stop event. * @param {Function} opts[].onpause - Called to handle the pause event. * @param {Function} opts[].onresume - Called to handle the resume event. * @param {Function} opts[].onerror - Called to handle an ErrorEvent. * @param {Function} opts[].onchange - Called to handle the change a stream event. * @param {Function} opts[].ondataavailable - Called to handle the dataavailable event. The Blob of recorded data is contained in this event (Callback isn't supported if use 'audio/wav' of 'audio/mp3' for recording). * * @example * var opts = { * onstart: function onStart() { // Use named function. * console.log('Recorder is started'); * }, * onstop: function onStop(Blob) { * videoElement.src = URL.createObjectURL(blob); * }, * mimeType: 'video/mp4' * }; * * // uses as global variable, QBMediaRecorder is built as a UMD module. * var recorder = new QBMediaRecorder(opts); * * @example * // For record 'audio/mp3' or 'audio/wav' need to add {@link https://github.com/QuickBlox/javascript-media-recorder/blob/master/qbAudioRecorderWorker.js|'qbAudioRecorderWorker.js'} file to your project. * var opts = { * // use named function * onstart: function onStart() { * console.log('Recorder is started'); * }, * onstop: function onStop(Blob) { * videoElement.src = URL.createObjectURL(blob); * }, * // 'audio/wav' or 'audio/mp3' * mimeType: 'audio/mp3', * // set relative path (from folder node_modules for example) * workerPath: '../node_modules/javascript-media-recorder/qbAudioRecorderWorker.js' * }; * * // uses as global variable, QBMediaRecorder is built as a UMD module. * var recorder = new QBMediaRecorder(opts); */ function QBMediaRecorder(opts) { this.toggleMimeType(opts.mimeType); if (opts.workerPath) { this._setCustomRecorderTools(opts.workerPath); } this.timeslice = opts && opts.timeslice && isNaN(+opts.timeslice) ? opts.timeslice : 1000; this.callbacks = opts ? this._getCallbacks(opts) : {}; this.recordedBlobs = []; this.ignoreMutedMedia = opts && typeof(opts.ignoreMutedMedia) === 'boolean' ? opts.ignoreMutedMedia : true; this._stream = null; this._mediaRecorder = null; this._recordedChunks = []; this._keepRecording = false; } /** * @param {String} mimeType - The mimeType to set as option. * @return {Boolean} - True if the MediaRecorder implementation is capable of recording Blob objects for the specified MIME type. * * @example * var opts = { * onstart: function onStart() { * console.log('Recorder is started'); * }, * onstop: function onStop(Blob) { * videoElement.src = URL.createObjectURL(blob); * }, * mimeType: 'video/mp4', * // set the path to the worker before if 'audio/wav' or 'audio/mp3' mimeTypes will be used. * workerPath: '../node_modules/javascript-media-recorder/qbAudioRecorderWorker.js' * }; * * var recorder = new QBMediaRecorder(opts); * * recorder.toggleMimeType('audio/mp3'); */ QBMediaRecorder.prototype.toggleMimeType = function(mimeType) { var prefferedMimeType = mimeType ? mimeType : false; this._customMimeType = (prefferedMimeType === 'audio/wav') ? 'audio/wav' : (prefferedMimeType === 'audio/mp3') ? 'audio/mp3' : false; if (!QBMediaRecorder.isAvailable() && !this._customMimeType) { throw new Error(ERRORS.unsupport); } this.mimeType = this._customMimeType ? this._customMimeType : this._getMimeType(prefferedMimeType); }; QBMediaRecorder.prototype._setCustomRecorderTools = function(path) { var self = this; // init worker for custom audio types (audio/wav, audio/mp3) try { self._worker = new Worker(path); self._postMessageToWorker({ cmd: 'init', mimeType: self.mimeType }); self._worker.onmessage = function(event) { self._createBlob(event.data); self._closeAudioProcess(); }; if (!QBMediaRecorder.isAudioContext()) { throw new Error(ERRORS.unsupportAudioContext); } /* * context = new AudioContext(); * context.createScriptProcessor(bufferSize, numberOfInputChannels, numberOfOutputChannels); * * link: https://developer.mozilla.org/ru/docs/Web/API/AudioContext/createScriptProcessor */ self.BUFFER_SIZE = 2048; // the buffer size in units of sample-frames. self.INPUT_CHANNELS = 1; // the number of channels for this node's input, defaults to 2 self.OUTPUT_CHANNELS = 1; // the number of channels for this node's output, defaults to 2 self._audioContext = null; } catch(e) { throw new Error(ERRORS.unsupportCustomAudioRecorder, e); } }; QBMediaRecorder.prototype._getMimeType = function (preffered) { var mimeType, type = 'video'; if(preffered && QBMediaRecorder.isTypeSupported(preffered)) { mimeType = preffered; } else if(preffered) { type = preffered.toString().toLowerCase().indexOf('audio') === -1 ? 'video' : 'audio'; mimeType = QBMediaRecorder.getSupportedMimeTypes(type)[0]; } else { mimeType = QBMediaRecorder.getSupportedMimeTypes(type)[0]; } return mimeType; }; QBMediaRecorder.prototype._getCallbacks = function(opts) { var callbacks = {}, callbackNames = ['onstart', 'onstop', 'onpause', 'onresume', 'onerror', 'onchange', 'ondataavailable']; callbackNames.forEach(function(name) { if (name in opts) { callbacks[name] = opts[name]; } }); return callbacks; }; QBMediaRecorder._mimeTypes = require('./mimeTypes'); QBMediaRecorder._STATES = ['inactive', 'recording', 'paused']; /** * It checks capability of recording in the environment. * Checks MediaRecorder, MediaRecorder.isTypeSupported and Blob. * @return {Boolean} Returns true if the QBMediaRecorder is available and can run, or false otherwise. * * @example * if(QBMediaRecorder.isAvailable()) { * // ... show UI for recording * } */ QBMediaRecorder.isAvailable = function() { return !!(window && window.MediaRecorder && typeof window.MediaRecorder.isTypeSupported === 'function' && window.Blob); }; /** * It checks the AudioContext API. * Checks window.AudioContext or window.webkitAudioContext. * @return {Boolean} Returns true if the AudioContext API is available in a browser, or false otherwise. * * @example * if(QBMediaRecorder.isAudioContext()) { * // ... the QBMediaRecorder is available for recording 'audio/mp3' or 'audio/wav' * } */ QBMediaRecorder.isAudioContext = function() { return !!(window && (window.AudioContext || window.webkitAudioContext)); }; /** * Returns a Boolean which is true if the MIME type specified is one the user agent can record. * @param {String} mimeType - The mimeType to check. * @return {Boolean} - True if the MediaRecorder implementation is capable of recording Blob objects for the specified MIME type. * * @example * if( QBMediaRecorder.isTypeSupported('video/mp4') ) { * el.textContent = 'Will be record in video/mp4'; * } */ QBMediaRecorder.isTypeSupported = function(mimeType) { var result = false; if(!QBMediaRecorder.isAvailable()) { throw new Error(ERRORS.unsupport); } if(!mimeType) { throw new Error(ERRORS.requreArgument); } switch(mimeType) { case 'audio/wav': if (QBMediaRecorder.isAudioContext()) { result = true; } break; case 'audio/mp3': if (QBMediaRecorder.isAudioContext()) { result = true; } break; default: result = window.MediaRecorder.isTypeSupported(mimeType); break; } return result; }; /** * Return all supported mime types and container format. * @param {String} [type=video] Type of media. * @return {Array} Array of supported mimetypes. Recommended mimetype has 0 index. * * @example * var type = QBMediaRecorder.getSupportedMimeTypes('audio'); * console.info(`Call will recording in ${type[0]}`); */ QBMediaRecorder.getSupportedMimeTypes = function(type) { var typeMedia = type || 'video'; if (!QBMediaRecorder.isAvailable()) { throw new Error(ERRORS.unsupport); } return QBMediaRecorder._mimeTypes[typeMedia].filter(function(mimeType) { return QBMediaRecorder.isTypeSupported(mimeType); }); }; /** * Return the current [state of QBMediaRecorder instance](https://w3c.github.io/mediacapture-record/MediaRecorder.html#idl-def-recordingstate). * Possibly states: **inactive**, **recording**, **paused**. * @return {String} Name of a state. * * @example * var recorder = new QBMediaRecorder(); * // ...some code * * if(recorder.getState() == 'recording') { * console.info('You are still recording.'); * } */ QBMediaRecorder.prototype.getState = function() { return this._mediaRecorder ? this._mediaRecorder.state : QBMediaRecorder._STATES[0]; }; /** * Start to recording a stream. * Fire the method `stop` if an instance inprogress (has a state recording or paused). * Fire onstart callback. * @param {MediaStream} stream - Stream object representing a flux of audio- or video-related data. * @returns {void} * * @example * var options = { * onstart: function onStart() { * var time = 0, * step = 1000; * * setTimeout(function () { * time += step; * console.info(`You are recording ${time} sec.`); * }, step); * } * } * * var rec = new qbRecorder(options); * // ... * rec.start(stream); */ QBMediaRecorder.prototype.start = function(stream) { var self = this; if(!stream) { throw new Error(ERRORS.requreArgument); } var mediaRecorderState = this.getState(); if(mediaRecorderState === QBMediaRecorder._STATES[1] || mediaRecorderState === QBMediaRecorder._STATES[2]) { this._mediaRecorder.stop(); } if(this._stream) { this._stream = null; } // TODO: need to stream.clone self._stream = stream; self._mediaRecorder = null; self._recordedChunks.length = 0; if (self._customMimeType) { self._setCustomRecorder(); } else { self._setMediaRecorder(); } self._setEvents(); }; QBMediaRecorder.prototype._setMediaRecorder = function () { var self = this; try { self._mediaRecorder = new window.MediaRecorder(self._stream, { 'mimeType': self.mimeType, 'ignoreMutedMedia': self.ignoreMutedMedia }); } catch(e) { console.warn(ERRORS.unsupportMediaRecorderWithOptions, e); self._mediaRecorder = new window.MediaRecorder(self._stream); } }; QBMediaRecorder.prototype._setCustomRecorder = function() { var self = this; self._closeAudioProcess(); self._mediaRecorder = { start: function() { try { this.state = QBMediaRecorder._STATES[1]; self._startAudioProcess(); this.onstart(); } catch(error) { this.onerror(error); } }, stop: function() { try { this.state = QBMediaRecorder._STATES[0]; self._stopAudioProcess(); this.onstop(); } catch(error) { this.onerror(error); } }, pause: function() { try { this.state = QBMediaRecorder._STATES[2]; this.onpause(); } catch(error) { this.onerror(error); } }, resume: function() { try { this.state = QBMediaRecorder._STATES[1]; this.onresume(); } catch(error) { this.onerror(error); } }, /* callbacks */ onstart: function() { if (this.state !== 'recording') { this.state = QBMediaRecorder._STATES[1]; } }, onstop: function() { if (this.state !== 'inactive') { this.state = QBMediaRecorder._STATES[0]; } }, onpause: function() { if (this.state !== 'paused') { this.state = QBMediaRecorder._STATES[2]; } }, onresume: function() { if (this.state !== 'recording') { this.state = QBMediaRecorder._STATES[1]; } }, onerror: function() { try { self._closeAudioProcess(); } catch(error) { throw new Error(error); } } }; }; QBMediaRecorder.prototype._fireCallback = function(name, args) { var self = this; if(Object.keys(self.callbacks).length !== 0 && typeof self.callbacks[name] === 'function') { try { self.callbacks[name](args); } catch(e) { console.error('Founded an error in callback:' + name, e); } } }; QBMediaRecorder.prototype._setEvents = function() { var self = this; if (!self._customMimeType) { self._mediaRecorder.ondataavailable = function(e) { if(e.data && e.data.size > 0) { self._recordedChunks.push(e.data); self._fireCallback('ondataavailable', e); } }; } self._mediaRecorder.onpause = function() { self._fireCallback('onpause'); }; self._mediaRecorder.onresume = function() { self._fireCallback('onresume'); }; self._mediaRecorder.onerror = function(error) { switch(error.name) { case 'InvalidState': console.error(ERRORS[error.name]); break; case 'OutOfMemory': console.error(ERRORS[error.name]); break; case 'IllegalStreamModification': console.error(ERRORS[error.name]); break; case 'OtherRecordingError': console.error(ERRORS[error.name]); break; case 'GenericError': console.error(ERRORS[error.name]); break; default: console.error('MediaRecorder Error', error); break; } if (self._mediaRecorder.state !== 'inactive') { self._mediaRecorder.stop(); } if (self._userCallbacks && typeof self._userCallbacks.onErrorRecording === 'function') { self._fireCallback('onerror', error); } }; self._mediaRecorder.onstop = function() { if (self._customMimeType) { self._stopAudioProcess(); } else { self._createBlob(self._recordedChunks); } }; self._mediaRecorder.start(self.timeslice); self._fireCallback('onstart'); }; /** * Stop to recording a stream. * @return {Blob} Blob of recorded chuncks. */ QBMediaRecorder.prototype.stop = function() { var mediaRecorder = this._mediaRecorder, mediaRecorderState = mediaRecorder && mediaRecorder.state ? mediaRecorder.state : 'inactive'; if(mediaRecorder && (mediaRecorderState === 'recording' || mediaRecorderState === 'paused')){ mediaRecorder.stop(); } else { console.warn(ERRORS.actionFailed); } }; /** * Pause to recording a stream. * @returns {void} */ QBMediaRecorder.prototype.pause = function() { var self = this; if(self._mediaRecorder && self._mediaRecorder.state === 'recording') { self._mediaRecorder.pause(); } else { console.warn(ERRORS.actionFailed); } }; /** * Resume to recording a stream. * @returns {void} */ QBMediaRecorder.prototype.resume = function() { var self = this; if(self._mediaRecorder && self._mediaRecorder.state === 'paused') { self._mediaRecorder.resume(); } else { console.warn(ERRORS.actionFailed); } }; /** * Change a recorded stream. * @param {MediaStream} stream - Stream object representing a flux of audio- or video-related data. * @returns {void} */ QBMediaRecorder.prototype.change = function(stream) { var self = this; if(!stream) { throw new Error(ERRORS.requreArgument); } self._keepRecording = true; // don't stop a record self.stop(); self._stream = null; self._mediaRecorder = null; // TODO stream.clone self._stream = stream; if (self._customMimeType) { self._setCustomRecorder(); } else { self._setMediaRecorder(); } self._setEvents(); }; /** * Create a file from blob and download as the file. Its method will fire 'stop' if recording in progress. * @param {Strint} [fileName=Date.now()] - Name of file. * @param {Blob} [blob] - You can set blob which you get from the method `stop` or don't set anything and we will get recorded chuncks. * @returns {void} * * @example * var rec = new qbRecorder(); * rec.start(stream); * // ... * rec.download(false); // Set false, name will be generated based on Date.now() */ QBMediaRecorder.prototype.download = function(fileName, blob) { var self = this; var mediaRecorderState = this.getState(); if(mediaRecorderState === QBMediaRecorder._STATES[1] || mediaRecorderState === QBMediaRecorder._STATES[2]) { this._mediaRecorder.stop(); } var url = URL.createObjectURL(blob || self._getBlobRecorded()), a = document.createElement('a'); a.style.display = 'none'; a.href = url; a.download = (fileName || Date.now()) + '.' + self._getExtension(); document.body.appendChild(a); // Start dowloading a.click(); // Remove link setTimeout(function() { document.body.removeChild(a); window.URL.revokeObjectURL(url); }, 100); }; QBMediaRecorder.prototype._createBlob = function(chunks) { var self = this; var blob = new Blob(chunks, { 'type' : self.mimeType }); self.recordedBlobs.push(blob); if(!self._keepRecording) { if(self.recordedBlobs.length > 1) { self._fireCallback('onstop', blob); } else { self._fireCallback('onstop', self.recordedBlobs[0]); } } self._keepRecording = false; }; /** * Create a Blob from recorded chunks. * @access private * @param {Object} [data] - Recorded data. * @return {Object} - Blob of recorded media or what you set in data */ QBMediaRecorder.prototype._getBlobRecorded = function(data) { var self = this, chunks = data || self._recordedChunks; if(!chunks.length) { console.warn(ERRORS.no_recorded_chunks); return false; } return new Blob(chunks, { 'type' : self.mimeType }); }; /** * Return a extension of a file. Based on available mimeType. * @access private * @return {String} For example, 'webm' / 'mp4' / 'ogg' */ QBMediaRecorder.prototype._getExtension = function() { var self = this; var endTypeMedia = self.mimeType.indexOf('/'), extension = self.mimeType.substring(endTypeMedia + 1), startCodecsInfo = extension.indexOf(';'); if(startCodecsInfo !== -1) { extension = extension.substring(0, startCodecsInfo); } return extension; }; QBMediaRecorder.prototype._startAudioProcess = function() { if(!QBMediaRecorder.isAudioContext()) { throw new Error(ERRORS.unsupport); } var self = this, audioContext, audioInput, recorder, volume; self._closeAudioProcess(); audioContext = window.AudioContext || window.webkitAudioContext; self._audioContext = new audioContext; volume = self._audioContext.createGain(); audioInput = self._audioContext.createMediaStreamSource(self._stream); recorder = self._audioContext.createScriptProcessor(self.BUFFER_SIZE, self.INPUT_CHANNELS, self.OUTPUT_CHANNELS); audioInput.connect(volume); self._postMessageToWorker({ cmd: 'init', mimeType: self.mimeType, sampleRate: audioInput.context.sampleRate }); recorder.onaudioprocess = function(e) { if (self._mediaRecorder.state === QBMediaRecorder._STATES[1]) { self._postMessageToWorker({ cmd: 'record', bufferChunk: e.inputBuffer.getChannelData(0), bufferSize: self.BUFFER_SIZE }); } }; volume.connect(recorder); recorder.connect(self._audioContext.destination); }; QBMediaRecorder.prototype._closeAudioProcess = function() { var self = this; if (self._audioContext) { self._audioContext.close() .then(function() { self._audioContext = null; self._postMessageToWorker({cmd: 'init', mimeType: ''}); }); } }; QBMediaRecorder.prototype._stopAudioProcess = function() { this._postMessageToWorker({cmd: 'finish'}); }; QBMediaRecorder.prototype._postMessageToWorker = function(data) { if (this._worker) { this._worker.postMessage(data); } }; module.exports = QBMediaRecorder;