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
JavaScript
// ______________________
// 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;
}