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.

606 lines (528 loc) 19.6 kB
// ______________________ // MediaStreamRecorder.js /** * MediaStreamRecorder is an abstraction layer for {@link https://w3c.github.io/mediacapture-record/MediaRecorder.html|MediaRecorder API}. It is used by {@link RecordRTC} to record MediaStream(s) in both Chrome and Firefox. * @summary Runs top over {@link https://w3c.github.io/mediacapture-record/MediaRecorder.html|MediaRecorder API}. * @license {@link https://github.com/muaz-khan/RecordRTC/blob/master/LICENSE|MIT} * @author {@link https://github.com/muaz-khan|Muaz Khan} * @typedef MediaStreamRecorder * @class * @example * var config = { * mimeType: 'video/webm', // vp8, vp9, h264, mkv, opus/vorbis * audioBitsPerSecond : 256 * 8 * 1024, * videoBitsPerSecond : 256 * 8 * 1024, * bitsPerSecond: 256 * 8 * 1024, // if this is provided, skip above two * checkForInactiveTracks: true, * timeSlice: 1000, // concatenate intervals based blobs * ondataavailable: function() {} // get intervals based blobs * } * var recorder = new MediaStreamRecorder(mediaStream, config); * recorder.record(); * recorder.stop(function(blob) { * video.src = URL.createObjectURL(blob); * * // or * var blob = recorder.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 - {disableLogs:true, initCallback: function, mimeType: "video/webm", timeSlice: 1000} * @throws Will throw an error if first argument "MediaStream" is missing. Also throws error if "MediaRecorder API" are not supported by the browser. */ function MediaStreamRecorder(mediaStream, config, db) { var self = this; if (typeof mediaStream === "undefined") { throw "First argument 'MediaStream' is required."; } if (typeof MediaRecorder === "undefined") { throw "Your browser does not support the Media Recorder API. Please try other modules e.g. WhammyRecorder or StereoAudioRecorder."; } config = config || { // bitsPerSecond: 256 * 8 * 1024, mimeType: "video/webm", }; if (config.type === "audio") { if ( getTracks(mediaStream, "video").length && getTracks(mediaStream, "audio").length ) { var stream; if (!!navigator.mozGetUserMedia) { stream = new MediaStream(); stream.addTrack(getTracks(mediaStream, "audio")[0]); } else { // webkitMediaStream stream = new MediaStream(getTracks(mediaStream, "audio")); } mediaStream = stream; } if ( !config.mimeType || config.mimeType.toString().toLowerCase().indexOf("audio") === -1 ) { config.mimeType = isChrome ? "audio/webm" : "audio/ogg"; } if ( config.mimeType && config.mimeType.toString().toLowerCase() !== "audio/ogg" && !!navigator.mozGetUserMedia ) { // forcing better codecs on Firefox (via #166) config.mimeType = "audio/ogg"; } } // var arrayOfBlobs = []; /** * This method returns array of blobs. Use only with "timeSlice". Its useful to preview recording anytime, without using the "stop" method. * @method * @memberof MediaStreamRecorder * @example * var arrayOfBlobs = recorder.getArrayOfBlobs(); * @returns {Array} Returns array of recorded blobs. */ this.getArrayOfBlobs = function(blobProcessor) { db.blobs.toArray().then(function(existinBlobs) { var mergedBlobs = []; for (var index = 0; index < existinBlobs.length; index++) { mergedBlobs.push(existinBlobs[index].current); } var existing = new Blob(mergedBlobs, { type: "video/mp4", }); blobProcessor(existing); }); }; /** * This method records MediaStream. * @method * @memberof MediaStreamRecorder * @example * recorder.record(); */ this.record = function() { // set defaults self.blob = null; self.clearRecordedData(); self.timestamps = []; allStates = []; // arrayOfBlobs = []; var recorderHints = config; if (!config.disableLogs) { console.log( "Passing following config over MediaRecorder API.", recorderHints ); } if (mediaRecorder) { // mandatory to make sure Firefox doesn't fails to record streams 3-4 times without reloading the page. mediaRecorder = null; } if (isChrome && !isMediaRecorderCompatible()) { // to support video-only recording on stable recorderHints = "video/vp8"; } if ( typeof MediaRecorder.isTypeSupported === "function" && recorderHints.mimeType ) { if (!MediaRecorder.isTypeSupported(recorderHints.mimeType)) { if (!config.disableLogs) { console.warn( "MediaRecorder API seems unable to record mimeType:", recorderHints.mimeType ); } recorderHints.mimeType = config.type === "audio" ? "audio/webm" : "video/webm"; } } // using MediaRecorder API here try { mediaRecorder = new MediaRecorder(mediaStream, recorderHints); // reset config.mimeType = recorderHints.mimeType; } catch (e) { // chrome-based fallback mediaRecorder = new MediaRecorder(mediaStream); } // old hack? if ( recorderHints.mimeType && !MediaRecorder.isTypeSupported && "canRecordMimeType" in mediaRecorder && mediaRecorder.canRecordMimeType(recorderHints.mimeType) === false ) { if (!config.disableLogs) { console.warn( "MediaRecorder API seems unable to record mimeType:", recorderHints.mimeType ); } } // Dispatching OnDataAvailable Handler mediaRecorder.ondataavailable = function(e) { console.log("[ABM] ondataavailable", e.data.size, e.data.type); if (e.data) { allStates.push("ondataavailable: " + bytesToSize(e.data.size)); } if (typeof config.timeSlice === "number") { if (e.data && e.data.size) { console.log("[ABM] Writing chunk ", e.data.size); // arrayOfBlobs.push(e.data); updateRecordingChunk(e.data); updateTimeStamp(); if (typeof config.ondataavailable === "function") { // intervals based blobs var blob = config.getNativeBlob ? e.data : new Blob([e.data], { type: getMimeType(recorderHints), }); config.ondataavailable(blob); } } return; } if (!e.data || !e.data.size || e.data.size < 100 || self.blob) { // make sure that stopRecording always getting fired // even if there is invalid data if (self.recordingCallback) { self.recordingCallback( new Blob([], { type: getMimeType(recorderHints), }) ); self.recordingCallback = null; } return; } self.blob = config.getNativeBlob ? e.data : new Blob([e.data], { type: getMimeType(recorderHints), }); if (self.recordingCallback) { self.recordingCallback(self.blob); self.recordingCallback = null; } }; mediaRecorder.onstart = function() { allStates.push("started"); }; mediaRecorder.onpause = function() { allStates.push("paused"); }; mediaRecorder.onresume = function() { allStates.push("resumed"); }; mediaRecorder.onstop = function() { allStates.push("stopped"); }; mediaRecorder.onerror = function(error) { if (!error) { return; } if (!error.name) { error.name = "UnknownError"; } allStates.push("error: " + error); if (!config.disableLogs) { // via: https://w3c.github.io/mediacapture-record/MediaRecorder.html#exception-summary if ( error.name.toString().toLowerCase().indexOf("invalidstate") !== -1 ) { console.error( "The MediaRecorder is not in a state in which the proposed operation is allowed to be executed.", error ); } else if ( error.name.toString().toLowerCase().indexOf("notsupported") !== -1 ) { console.error( "MIME type (", recorderHints.mimeType, ") is not supported.", error ); } else if ( error.name.toString().toLowerCase().indexOf("security") !== -1 ) { console.error("MediaRecorder security error", error); } // older code below else if (error.name === "OutOfMemory") { console.error( "The UA has exhaused the available memory. User agents SHOULD provide as much additional information as possible in the message attribute.", error ); } else if (error.name === "IllegalStreamModification") { console.error( "A modification to the stream has occurred that makes it impossible to continue recording. An example would be the addition of a Track while recording is occurring. User agents SHOULD provide as much additional information as possible in the message attribute.", error ); } else if (error.name === "OtherRecordingError") { console.error( "Used for an fatal error other than those listed above. User agents SHOULD provide as much additional information as possible in the message attribute.", error ); } else if (error.name === "GenericError") { console.error( "The UA cannot provide the codec or recording option that has been requested.", error ); } else { console.error("MediaRecorder Error", error); } } (function(looper) { if ( !self.manuallyStopped && mediaRecorder && mediaRecorder.state === "inactive" ) { delete config.timeslice; // 10 minutes, enough? mediaRecorder.start(10 * 60 * 1000); return; } setTimeout(looper, 1000); })(); if ( mediaRecorder.state !== "inactive" && mediaRecorder.state !== "stopped" ) { mediaRecorder.stop(); } }; if (typeof config.timeSlice === "number") { updateTimeStamp(); mediaRecorder.start(config.timeSlice); } else { // default is 24 hours; enough? (thanks https://github.com/slidevjs/slidev/pull/488) // use config => {timeSlice: 1000} otherwise mediaRecorder.start(24 * 60 * 60 * 1000); } if (config.initCallback) { config.initCallback(); // old code } }; /** * @property {Array} timestamps - Array of time stamps * @memberof MediaStreamRecorder * @example * console.log(recorder.timestamps); */ this.timestamps = []; function updateRecordingChunk(data) { db.blobs .put({ current: data, created_on: new Date(), }) .then(function(updated) { console.log("[ABM]", "Content updated successfully"); }) .catch(function(error) { console.error("[ABM]", "Could not save content in DB....", error); }); } function updateTimeStamp() { self.timestamps.push(new Date().getTime()); if (typeof config.onTimeStamp === "function") { config.onTimeStamp( self.timestamps[self.timestamps.length - 1], self.timestamps ); } } function getMimeType(secondObject) { if (mediaRecorder && mediaRecorder.mimeType) { return mediaRecorder.mimeType; } return secondObject.mimeType || "video/webm"; } /** * This method stops recording MediaStream. * @param {function} callback - Callback function, that is used to pass recorded blob back to the callee. * @method * @memberof MediaStreamRecorder * @example * recorder.stop(function(blob) { * video.src = URL.createObjectURL(blob); * }); */ this.stop = function(callback) { callback = callback || function() {}; self.manuallyStopped = true; // used inside the mediaRecorder.onerror if (!mediaRecorder) { return; } this.recordingCallback = callback; if (mediaRecorder.state === "recording") { mediaRecorder.stop(); } if (typeof config.timeSlice === "number") { console.info("[ABM] Timeslicing function..."); setTimeout(function() { console.info( "[ABM] Running the timeslicing loop...", mediaRecorder.state ); // self.blob = new Blob(arrayOfBlobs, { // type: getMimeType(config), // }); // self.blob = indexDBClient.getData(); self.getArrayOfBlobs(function(existing) { self.blob = existing; self.recordingCallback(self.blob); }); }, 100); } }; /** * This method pauses the recording process. * @method * @memberof MediaStreamRecorder * @example * recorder.pause(); */ this.pause = function() { if (!mediaRecorder) { return; } if (mediaRecorder.state === "recording") { mediaRecorder.pause(); } }; /** * This method resumes the recording process. * @method * @memberof MediaStreamRecorder * @example * recorder.resume(); */ this.resume = function() { if (!mediaRecorder) { return; } if (mediaRecorder.state === "paused") { mediaRecorder.resume(); } }; /** * This method resets currently recorded data. * @method * @memberof MediaStreamRecorder * @example * recorder.clearRecordedData(); */ this.clearRecordedData = function() { if (mediaRecorder && mediaRecorder.state === "recording") { self.stop(clearRecordedDataCB); } clearRecordedDataCB(); }; function clearRecordedDataCB() { // arrayOfBlobs = []; //indexDBClient.reset(); mediaRecorder = null; self.timestamps = []; } // Reference to "MediaRecorder" object var mediaRecorder; /** * Access to native MediaRecorder API * @method * @memberof MediaStreamRecorder * @instance * @example * var internal = recorder.getInternalRecorder(); * internal.ondataavailable = function() {}; // override * internal.stream, internal.onpause, internal.onstop, etc. * @returns {Object} Returns internal recording object. */ this.getInternalRecorder = function() { return mediaRecorder; }; 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; } /** * @property {Blob} blob - Recorded data as "Blob" object. * @memberof MediaStreamRecorder * @example * recorder.stop(function() { * var blob = recorder.blob; * }); */ this.blob = null; /** * Get MediaRecorder readonly state. * @method * @memberof MediaStreamRecorder * @example * var state = recorder.getState(); * @returns {String} Returns recording state. */ this.getState = function() { if (!mediaRecorder) { return "inactive"; } return mediaRecorder.state || "inactive"; }; // list of all recording states var allStates = []; /** * Get MediaRecorder all recording states. * @method * @memberof MediaStreamRecorder * @example * var state = recorder.getAllStates(); * @returns {Array} Returns all recording states */ this.getAllStates = function() { return allStates; }; // if any Track within the MediaStream is muted or not enabled at any time, // the browser will only record black frames // or silence since that is the content produced by the Track // so we need to stopRecording as soon as any single track ends. if (typeof config.checkForInactiveTracks === "undefined") { config.checkForInactiveTracks = false; // disable to minimize CPU usage } var self = this; // this method checks if media stream is stopped // or if any track is ended. (function looper() { if (!mediaRecorder || config.checkForInactiveTracks === false) { return; } if (isMediaStreamActive() === false) { if (!config.disableLogs) { console.log("MediaStream seems stopped."); } self.stop(); return; } setTimeout(looper, 1000); // check every second })(); // for debugging this.name = "MediaStreamRecorder"; this.toString = function() { return this.name; }; } if (typeof RecordRTC !== "undefined") { RecordRTC.MediaStreamRecorder = MediaStreamRecorder; }