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.

321 lines (273 loc) 11.9 kB
// ______________________ // MediaStreamRecorder.js // todo: need to show alert boxes for incompatible cases // encoder only supports 48k/16k mono audio channel /* * Implementation of https://dvcs.w3.org/hg/dap/raw-file/default/media-stream-capture/MediaRecorder.html * The MediaRecorder accepts a mediaStream as input source passed from UA. When recorder starts, * a MediaEncoder will be created and accept the mediaStream as input source. * Encoder will get the raw data by track data changes, encode it by selected MIME Type, then store the encoded in EncodedBufferCache object. * The encoded data will be extracted on every timeslice passed from Start function call or by RequestData function. * Thread model: * When the recorder starts, it creates a "Media Encoder" thread to read data from MediaEncoder object and store buffer in EncodedBufferCache object. * Also extract the encoded data and create blobs on every timeslice passed from start function or RequestData function called by UA. */ /** * MediaStreamRecorder is an abstraction layer for "MediaRecorder API". It is used by {@link RecordRTC} to record MediaStream(s) in Firefox. * @summary Runs top over MediaRecorder API. * @license {@link https://github.com/muaz-khan/RecordRTC#license|MIT} * @author {@link http://www.MuazKhan.com|Muaz Khan} * @typedef MediaStreamRecorder * @class * @example * var options = { * mimeType: 'video/mp4', // audio/ogg or video/webm * audioBitsPerSecond : 256 * 8 * 1024, * videoBitsPerSecond : 256 * 8 * 1024, * bitsPerSecond: 256 * 8 * 1024, // if this is provided, skip above two * getNativeBlob: true // by default it is false * } * var recorder = new MediaStreamRecorder(MediaStream, options); * 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", onAudioProcessStarted: function} */ function MediaStreamRecorder(mediaStream, config) { var self = this; config = config || { // bitsPerSecond: 256 * 8 * 1024, mimeType: 'video/webm' }; if (config.type === 'audio') { if (mediaStream.getVideoTracks().length && mediaStream.getAudioTracks().length) { var stream; if (!!navigator.mozGetUserMedia) { stream = new MediaStream(); stream.addTrack(mediaStream.getAudioTracks()[0]); } else { // webkitMediaStream stream = new MediaStream(mediaStream.getAudioTracks()); } mediaStream = stream; } if (!config.mimeType || config.mimeType.indexOf('audio') === -1) { config.mimeType = isChrome ? 'audio/webm' : 'audio/ogg'; } } /** * This method records MediaStream. * @method * @memberof MediaStreamRecorder * @example * recorder.record(); */ this.record = function() { self.blob = null; 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'; } // http://dxr.mozilla.org/mozilla-central/source/content/media/MediaRecorder.cpp // https://wiki.mozilla.org/Gecko:MediaRecorder // https://dvcs.w3.org/hg/dap/raw-file/default/media-stream-capture/MediaRecorder.html // starting a recording session; which will initiate "Reading Thread" // "Reading Thread" are used to prevent main-thread blocking scenarios mediaRecorder = new MediaRecorder(mediaStream, recorderHints); if ('canRecordMimeType' in mediaRecorder && mediaRecorder.canRecordMimeType(config.mimeType) === false) { if (!config.disableLogs) { console.warn('MediaRecorder API seems unable to record mimeType:', config.mimeType); } } // i.e. stop recording when <video> is paused by the user; and auto restart recording // when video is resumed. E.g. yourStream.getVideoTracks()[0].muted = true; // it will auto-stop recording. mediaRecorder.ignoreMutedMedia = config.ignoreMutedMedia || false; // Dispatching OnDataAvailable Handler mediaRecorder.ondataavailable = function(e) { if (self.dontFireOnDataAvailableEvent) { return; } if (!e.data || !e.data.size || e.data.size < 100 || self.blob) { return; } /** * @property {Blob} blob - Recorded frames in video/webm blob. * @memberof MediaStreamRecorder * @example * recorder.stop(function() { * var blob = recorder.blob; * }); */ self.blob = config.getNativeBlob ? e.data : new Blob([e.data], { type: config.mimeType || 'video/webm' }); if (self.recordingCallback) { self.recordingCallback(self.blob); self.recordingCallback = null; } }; mediaRecorder.onerror = function(error) { if (!config.disableLogs) { if (error.name === 'InvalidState') { console.error('The MediaRecorder is not in a state in which the proposed operation is allowed to be executed.'); } 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.'); } 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.'); } 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.'); } 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); } } // When the stream is "ended" set recording to 'inactive' // and stop gathering data. Callers should not rely on // exactness of the timeSlice value, especially // if the timeSlice value is small. Callers should // consider timeSlice as a minimum value if (mediaRecorder.state !== 'inactive' && mediaRecorder.state !== 'stopped') { mediaRecorder.stop(); } }; // void start(optional long mTimeSlice) // The interval of passing encoded data from EncodedBufferCache to onDataAvailable // handler. "mTimeSlice < 0" means Session object does not push encoded data to // onDataAvailable, instead, it passive wait the client side pull encoded data // by calling requestData API. mediaRecorder.start(3.6e+6); // Start recording. If timeSlice has been provided, mediaRecorder will // raise a dataavailable event containing the Blob of collected data on every timeSlice milliseconds. // If timeSlice isn't provided, UA should call the RequestData to obtain the Blob data, also set the mTimeSlice to zero. if (config.onAudioProcessStarted) { config.onAudioProcessStarted(); } if (config.initCallback) { config.initCallback(); } }; /** * 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) { if (!mediaRecorder) { return; } this.recordingCallback = callback || function() {}; // mediaRecorder.state === 'recording' means that media recorder is associated with "session" // mediaRecorder.state === 'stopped' means that media recorder is detached from the "session" ... in this case; "session" will also be deleted. if (mediaRecorder.state === 'recording') { // "stop" method auto invokes "requestData"! mediaRecorder.requestData(); mediaRecorder.stop(); } }; /** * 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 (this.dontFireOnDataAvailableEvent) { this.dontFireOnDataAvailableEvent = false; var disableLogs = config.disableLogs; config.disableLogs = true; this.record(); config.disableLogs = disableLogs; return; } 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) { return; } this.pause(); this.dontFireOnDataAvailableEvent = true; this.stop(); }; // Reference to "MediaRecorder" object var 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; } var self = this; // this method checks if media stream is stopped // or any track is ended. (function looper() { if (!mediaRecorder) { return; } if (isMediaStreamActive() === false) { if (!config.disableLogs) { console.log('MediaStream seems stopped.'); } self.stop(); return; } setTimeout(looper, 1000); // check every second })(); } if (typeof RecordRTC !== 'undefined') { RecordRTC.MediaStreamRecorder = MediaStreamRecorder; }