UNPKG

@vikasietum_tecknology/record-rtc

Version:

record-rtc is a library based on recordrtc library. In this forked version of the original library we have optimized the memory management. The video recording is stored in IndexDB in chunks.

911 lines (785 loc) 27.5 kB
// ____________ // RecordRTC.js /** * {@link https://github.com/muaz-khan/RecordRTC|RecordRTC} is a WebRTC JavaScript library for audio/video as well as screen activity recording. It supports Chrome, Firefox, Opera, Android, and Microsoft Edge. Platforms: Linux, Mac and Windows. * @summary Record audio, video or screen inside the browser. * @license {@link https://github.com/muaz-khan/RecordRTC/blob/master/LICENSE|MIT} * @author {@link https://MuazKhan.com|Muaz Khan} * @typedef RecordRTC * @class * @example * var recorder = RecordRTC(mediaStream or [arrayOfMediaStream], { * type: 'video', // audio or video or gif or canvas * recorderType: MediaStreamRecorder || CanvasRecorder || StereoAudioRecorder || Etc * }); * recorder.startRecording(); * @see For further information: * @see {@link https://github.com/muaz-khan/RecordRTC|RecordRTC Source Code} * @param {MediaStream} mediaStream - Single media-stream object, array of media-streams, html-canvas-element, etc. * @param {object} config - {type:"video", recorderType: MediaStreamRecorder, disableLogs: true, numberOfAudioChannels: 1, bufferSize: 0, sampleRate: 0, desiredSampRate: 16000, video: HTMLVideoElement, etc.} * @example * { * type: "video", * recorderType: MediaStreamRecorder, * disableLogs: true, * numberOfAudioChannels: 1, * bufferSize: 0, * sampleRate: 0, * desiredSampRate: 16000, * video: HTMLVideoElement, * } */ function RecordRTC(mediaStream, config, db) { if (!config.idb) { throw new Error("Please provide the IDB config."); } else if (!config.idb.version) { throw new Error("Please provide IDB store version"); } // var db = new Dexie(config.idb.storeName); // db.version(config.idb.version).stores({ // recordings: config.idb.schema, // }); db.version(config.idb.version).stores({ blobs: "++id,current", }); if (!mediaStream) { throw "First parameter is required."; } config = config || { type: "video", }; config = new RecordRTCConfiguration(mediaStream, config); // a reference to user's recordRTC object var self = this; function startRecording(config2) { if (!config.disableLogs) { console.debug("RecordRTC version: ", self.version); } clearExistingBlobs(); if (!!config2) { // allow users to set options using startRecording method // config2 is similar to main "config" object (second parameter over RecordRTC constructor) config = new RecordRTCConfiguration(mediaStream, config2); } if (!config.disableLogs) { console.debug("started recording " + config.type + " stream."); } if (mediaRecorder) { mediaRecorder.clearRecordedData(); mediaRecorder.record(); setState("recording"); if (self.recordingDuration) { handleRecordingDuration(); } return self; } initRecorder(function() { if (self.recordingDuration) { handleRecordingDuration(); } }); return self; } function initRecorder(initCallback) { if (initCallback) { config.initCallback = function() { initCallback(); initCallback = config.initCallback = null; // recorder.initRecorder should be call-backed once. }; } var Recorder = new GetRecorderType(mediaStream, config); mediaRecorder = new Recorder(mediaStream, config, db); mediaRecorder.record(); setState("recording"); if (!config.disableLogs) { console.debug( "Initialized recorderType:", mediaRecorder.constructor.name, "for output-type:", config.type ); } } function stopRecording(recordId, callback, dbSaveCallback) { callback = callback || function() {}; if (!mediaRecorder) { warningLog(); return; } if (self.state === "paused") { self.resumeRecording(); setTimeout(function() { stopRecording(callback); }, 1); return; } if (self.state !== "recording" && !config.disableLogs) { console.warn( "Recording state should be: 'recording', however current state is: ", self.state ); } if (!config.disableLogs) { console.debug("Stopped recording " + config.type + " stream."); } if (config.type !== "gif") { mediaRecorder.stop(_callback); } else { mediaRecorder.stop(); _callback(); } setState("stopped"); function _callback(__blob) { if (!mediaRecorder) { if (typeof callback.call === "function") { callback.call(self, ""); } else { callback(""); } return; } Object.keys(mediaRecorder).forEach(function(key) { if (typeof mediaRecorder[key] === "function") { return; } self[key] = mediaRecorder[key]; }); var blob = mediaRecorder.blob; if (!blob) { if (__blob) { mediaRecorder.blob = blob = __blob; } else { throw "Recording failed."; } } if (blob && !config.disableLogs) { console.debug(blob.type, "->", bytesToSize(blob.size)); } if (callback) { var url; try { url = URL.createObjectURL(blob); } catch (e) {} if (typeof callback.call === "function") { callback.call(self, url); } else { callback(url); } } console.debug("[ABM]", "Moving content to recordings"); moveToRecordings(recordId, blob, dbSaveCallback); if (!config.autoWriteToDisk) { return; } getDataURL(function(dataURL) { var parameter = {}; parameter[config.type + "Blob"] = dataURL; DiskStorage.Store(parameter); }); } } function pauseRecording() { if (!mediaRecorder) { warningLog(); return; } if (self.state !== "recording") { if (!config.disableLogs) { console.warn( "Unable to pause the recording. Recording state: ", self.state ); } return; } setState("paused"); mediaRecorder.pause(); if (!config.disableLogs) { console.debug("Paused recording."); } } function resumeRecording() { if (!mediaRecorder) { warningLog(); return; } if (self.state !== "paused") { if (!config.disableLogs) { console.warn( "Unable to resume the recording. Recording state: ", self.state ); } return; } setState("recording"); // not all libs have this method yet mediaRecorder.resume(); if (!config.disableLogs) { console.debug("Resumed recording."); } } function readFile(_blob) { postMessage(new FileReaderSync().readAsDataURL(_blob)); } function getMetadata(activitySessionId) { return db.recordings .where("activitySessionId") .equals(activitySessionId) .toArray(); } function moveToRecordings(recordId, blob, dbSaveCallback) { db.recordings .update(recordId, { filename: config.filename, content: blob, }) .then(function(response) { console.debug("[ABM]", "Moved to recordings"); db.blobs .clear() .then(function() { console.debug("[ABM]", "Blobs table cleared"); blob = null; }) .catch(function(err) { console.error("[ABM]", "Could not clear blobs", err); }); if (dbSaveCallback) { console.debug("[ABM]", "Calling dbSaveCallback..."); dbSaveCallback(); } }) .catch(function(err) { console.error("[ABM]", "Could not move the file contents", err); }); } function getDataURL(callback, _mediaRecorder) { if (!callback) { throw "Pass a callback function over getDataURL."; } var blob = _mediaRecorder ? _mediaRecorder.blob : (mediaRecorder || {}).blob; if (!blob) { if (!config.disableLogs) { console.warn("Blob encoder did not finish its job yet."); } setTimeout(function() { getDataURL(callback, _mediaRecorder); }, 1000); return; } if (typeof Worker !== "undefined" && !navigator.mozGetUserMedia) { var webWorker = processInWebWorker(readFile); webWorker.onmessage = function(event) { callback(event.data); }; webWorker.postMessage(blob); } else { var reader = new FileReader(); reader.readAsDataURL(blob); reader.onload = function(event) { callback(event.target.result); }; } function processInWebWorker(_function) { try { var blob = URL.createObjectURL( new Blob( [ _function.toString(), "this.onmessage = function (eee) {" + _function.name + "(eee.data);}", ], { type: "application/javascript", } ) ); var worker = new Worker(blob); URL.revokeObjectURL(blob); return worker; } catch (e) {} } } function handleRecordingDuration(counter) { counter = counter || 0; if (self.state === "paused") { setTimeout(function() { handleRecordingDuration(counter); }, 1000); return; } if (self.state === "stopped") { return; } if (counter >= self.recordingDuration) { stopRecording(self.onRecordingStopped); return; } counter += 1000; // 1-second setTimeout(function() { handleRecordingDuration(counter); }, 1000); } function setState(state) { if (!self) { return; } self.state = state; if (typeof self.onStateChanged.call === "function") { self.onStateChanged.call(self, state); } else { self.onStateChanged(state); } } function clearExistingBlobs() { db.blobs.clear() .then(function() { console.debug("[ABM]", "Blobs table cleared"); }).catch(function(err) { console.error("[ABM]", "Could not clear blobs", err); }); } var WARNING = "It seems that recorder is destroyed or 'startRecording' is not invoked for " + config.type + " recorder."; function warningLog() { if (config.disableLogs === true) { return; } console.warn(WARNING); } var mediaRecorder; var returnObject = { /** * This method starts the recording. * @method * @memberof RecordRTC * @instance * @example * var recorder = RecordRTC(mediaStream, { * type: 'video' * }); * recorder.startRecording(); */ startRecording: startRecording, /** * This method stops the recording. It is strongly recommended to get "blob" or "URI" inside the callback to make sure all recorders finished their job. * @param {function} callback - Callback to get the recorded blob. * @method * @memberof RecordRTC * @instance * @example * recorder.stopRecording(function() { * // use either "this" or "recorder" object; both are identical * video.src = this.toURL(); * var blob = this.getBlob(); * }); */ stopRecording: stopRecording, /** * This method pauses the recording. You can resume recording using "resumeRecording" method. * @method * @memberof RecordRTC * @instance * @todo Firefox is unable to pause the recording. Fix it. * @example * recorder.pauseRecording(); // pause the recording * recorder.resumeRecording(); // resume again */ pauseRecording: pauseRecording, /** * This method resumes the recording. * @method * @memberof RecordRTC * @instance * @example * recorder.pauseRecording(); // first of all, pause the recording * recorder.resumeRecording(); // now resume it */ resumeRecording: resumeRecording, /** * This method initializes the recording. * @method * @memberof RecordRTC * @instance * @todo This method should be deprecated. * @example * recorder.initRecorder(); */ initRecorder: initRecorder, /** * Ask RecordRTC to auto-stop the recording after 5 minutes. * @method * @memberof RecordRTC * @instance * @example * var fiveMinutes = 5 * 1000 * 60; * recorder.setRecordingDuration(fiveMinutes, function() { * var blob = this.getBlob(); * video.src = this.toURL(); * }); * * // or otherwise * recorder.setRecordingDuration(fiveMinutes).onRecordingStopped(function() { * var blob = this.getBlob(); * video.src = this.toURL(); * }); */ setRecordingDuration: function(recordingDuration, callback) { if (typeof recordingDuration === "undefined") { throw "recordingDuration is required."; } if (typeof recordingDuration !== "number") { throw "recordingDuration must be a number."; } self.recordingDuration = recordingDuration; self.onRecordingStopped = callback || function() {}; return { onRecordingStopped: function(callback) { self.onRecordingStopped = callback; }, }; }, /** * This method can be used to clear/reset all the recorded data. * @method * @memberof RecordRTC * @instance * @todo Figure out the difference between "reset" and "clearRecordedData" methods. * @example * recorder.clearRecordedData(); */ clearRecordedData: function() { if (!mediaRecorder) { warningLog(); return; } mediaRecorder.clearRecordedData(); if (!config.disableLogs) { console.debug("Cleared old recorded data."); } }, /** * Get the recorded blob. Use this method inside the "stopRecording" callback. * @method * @memberof RecordRTC * @instance * @example * recorder.stopRecording(function() { * var blob = this.getBlob(); * * var file = new File([blob], 'filename.webm', { * type: 'video/webm' * }); * * var formData = new FormData(); * formData.append('file', file); // upload "File" object rather than a "Blob" * uploadToServer(formData); * }); * @returns {Blob} Returns recorded data as "Blob" object. */ getBlob: function() { if (!mediaRecorder) { warningLog(); return; } return mediaRecorder.blob; }, /** * Get data-URI instead of Blob. * @param {function} callback - Callback to get the Data-URI. * @method * @memberof RecordRTC * @instance * @example * recorder.stopRecording(function() { * recorder.getDataURL(function(dataURI) { * video.src = dataURI; * }); * }); */ getDataURL: getDataURL, /** * Get virtual/temporary URL. Usage of this URL is limited to current tab. * @method * @memberof RecordRTC * @instance * @example * recorder.stopRecording(function() { * video.src = this.toURL(); * }); * @returns {String} Returns a virtual/temporary URL for the recorded "Blob". */ toURL: function() { if (!mediaRecorder) { warningLog(); return; } return URL.createObjectURL(mediaRecorder.blob); }, /** * Get internal recording object (i.e. internal module) e.g. MutliStreamRecorder, MediaStreamRecorder, StereoAudioRecorder or WhammyRecorder etc. * @method * @memberof RecordRTC * @instance * @example * var internalRecorder = recorder.getInternalRecorder(); * if(internalRecorder instanceof MultiStreamRecorder) { * internalRecorder.addStreams([newAudioStream]); * internalRecorder.resetVideoStreams([screenStream]); * } * @returns {Object} Returns internal recording object. */ getInternalRecorder: function() { return mediaRecorder; }, /** * Invoke save-as dialog to save the recorded blob into your disk. * @param {string} fileName - Set your own file name. * @method * @memberof RecordRTC * @instance * @example * recorder.stopRecording(function() { * this.save('file-name'); * * // or manually: * invokeSaveAsDialog(this.getBlob(), 'filename.webm'); * }); */ save: function(fileName) { if (!mediaRecorder) { warningLog(); return; } invokeSaveAsDialog(mediaRecorder.blob, fileName); }, /** * This method gets a blob from indexed-DB storage. * @param {function} callback - Callback to get the recorded blob. * @method * @memberof RecordRTC * @instance * @example * recorder.getFromDisk(function(dataURL) { * video.src = dataURL; * }); */ getFromDisk: function(callback) { if (!mediaRecorder) { warningLog(); return; } RecordRTC.getFromDisk(config.type, callback); }, /** * This method appends an array of webp images to the recorded video-blob. It takes an "array" object. * @type {Array.<Array>} * @param {Array} arrayOfWebPImages - Array of webp images. * @method * @memberof RecordRTC * @instance * @todo This method should be deprecated. * @example * var arrayOfWebPImages = []; * arrayOfWebPImages.push({ * duration: index, * image: 'data:image/webp;base64,...' * }); * recorder.setAdvertisementArray(arrayOfWebPImages); */ setAdvertisementArray: function(arrayOfWebPImages) { config.advertisement = []; var length = arrayOfWebPImages.length; for (var i = 0; i < length; i++) { config.advertisement.push({ duration: i, image: arrayOfWebPImages[i], }); } }, /** * It is equivalent to <code class="str">"recorder.getBlob()"</code> method. Usage of "getBlob" is recommended, though. * @property {Blob} blob - Recorded Blob can be accessed using this property. * @memberof RecordRTC * @instance * @readonly * @example * recorder.stopRecording(function() { * var blob = this.blob; * * // below one is recommended * var blob = this.getBlob(); * }); */ blob: null, /** * This works only with {recorderType:StereoAudioRecorder}. Use this property on "stopRecording" to verify the encoder's sample-rates. * @property {number} bufferSize - Buffer-size used to encode the WAV container * @memberof RecordRTC * @instance * @readonly * @example * recorder.stopRecording(function() { * alert('Recorder used this buffer-size: ' + this.bufferSize); * }); */ bufferSize: 0, /** * This works only with {recorderType:StereoAudioRecorder}. Use this property on "stopRecording" to verify the encoder's sample-rates. * @property {number} sampleRate - Sample-rates used to encode the WAV container * @memberof RecordRTC * @instance * @readonly * @example * recorder.stopRecording(function() { * alert('Recorder used these sample-rates: ' + this.sampleRate); * }); */ sampleRate: 0, /** * {recorderType:StereoAudioRecorder} returns ArrayBuffer object. * @property {ArrayBuffer} buffer - Audio ArrayBuffer, supported only in Chrome. * @memberof RecordRTC * @instance * @readonly * @example * recorder.stopRecording(function() { * var arrayBuffer = this.buffer; * alert(arrayBuffer.byteLength); * }); */ buffer: null, /** * This method resets the recorder. So that you can reuse single recorder instance many times. * @method * @memberof RecordRTC * @instance * @example * recorder.reset(); * recorder.startRecording(); */ reset: function() { if (self.state === "recording" && !config.disableLogs) { console.warn("Stop an active recorder."); } if ( mediaRecorder && typeof mediaRecorder.clearRecordedData === "function" ) { mediaRecorder.clearRecordedData(); } mediaRecorder = null; setState("inactive"); self.blob = null; }, /** * This method is called whenever recorder's state changes. Use this as an "event". * @property {String} state - A recorder's state can be: recording, paused, stopped or inactive. * @method * @memberof RecordRTC * @instance * @example * recorder.onStateChanged = function(state) { * console.debug('Recorder state: ', state); * }; */ onStateChanged: function(state) { if (!config.disableLogs) { console.debug("Recorder state changed:", state); } }, /** * A recorder can have inactive, recording, paused or stopped states. * @property {String} state - A recorder's state can be: recording, paused, stopped or inactive. * @memberof RecordRTC * @static * @readonly * @example * // this looper function will keep you updated about the recorder's states. * (function looper() { * document.querySelector('h1').innerHTML = 'Recorder\'s state is: ' + recorder.state; * if(recorder.state === 'stopped') return; // ignore+stop * setTimeout(looper, 1000); // update after every 3-seconds * })(); * recorder.startRecording(); */ state: "inactive", /** * Get recorder's readonly state. * @method * @memberof RecordRTC * @example * var state = recorder.getState(); * @returns {String} Returns recording state. */ getState: function() { return self.state; }, /** * Destroy RecordRTC instance. Clear all recorders and objects. * @method * @memberof RecordRTC * @example * recorder.destroy(); */ destroy: function() { var disableLogsCache = config.disableLogs; config = { disableLogs: true, }; self.reset(); setState("destroyed"); returnObject = self = null; if (Storage.AudioContextConstructor) { Storage.AudioContextConstructor.close(); Storage.AudioContextConstructor = null; } config.disableLogs = disableLogsCache; if (!config.disableLogs) { console.debug("RecordRTC is destroyed."); } }, /** * RecordRTC version number * @property {String} version - Release version number. * @memberof RecordRTC * @static * @readonly * @example * alert(recorder.version); */ version: "@@version", /** * The method returns the metadata about the recording using the activitySessionId. * @property {String} activitySessionId - The activity session unique identifier * @method * @memberof RecordRTC * @instance */ getMetadata: getMetadata, }; if (!this) { self = returnObject; return returnObject; } // if someone wants to use RecordRTC with the "new" keyword. for (var prop in returnObject) { this[prop] = returnObject[prop]; } self = this; return returnObject; } RecordRTC.version = "@@version"; if (typeof module !== "undefined" /* && !!module.exports*/ ) { module.exports = RecordRTC; } if (typeof define === "function" && define.amd) { define("RecordRTC", [], function() { return RecordRTC; }); }