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.

363 lines (310 loc) 12.2 kB
// _________________ // WhammyRecorder.js /** * WhammyRecorder is a standalone class used by {@link RecordRTC} to bring video recording in Chrome. It runs top over {@link Whammy}. * @summary Video recording feature in Chrome. * @license {@link https://github.com/muaz-khan/RecordRTC#license|MIT} * @author {@link http://www.MuazKhan.com|Muaz Khan} * @typedef WhammyRecorder * @class * @example * var recorder = new WhammyRecorder(mediaStream); * recorder.record(); * recorder.stop(function(blob) { * video.src = URL.createObjectURL(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, video: HTMLVideoElement, etc.} */ function WhammyRecorder(mediaStream, config) { config = config || {}; if (!config.frameInterval) { config.frameInterval = 10; } if (!config.disableLogs) { console.log('Using frames-interval:', config.frameInterval); } /** * This method records video. * @method * @memberof WhammyRecorder * @example * recorder.record(); */ this.record = function() { if (!config.width) { config.width = 320; } if (!config.height) { config.height = 240; } if (!config.video) { config.video = { width: config.width, height: config.height }; } if (!config.canvas) { config.canvas = { width: config.width, height: config.height }; } canvas.width = config.canvas.width; canvas.height = config.canvas.height; context = canvas.getContext('2d'); // setting defaults if (config.video && config.video instanceof HTMLVideoElement) { video = config.video.cloneNode(); if (config.initCallback) { config.initCallback(); } } else { video = document.createElement('video'); if (typeof video.srcObject !== 'undefined') { video.srcObject = mediaStream; } else { video.src = URL.createObjectURL(mediaStream); } video.onloadedmetadata = function() { // "onloadedmetadata" may NOT work in FF? if (config.initCallback) { config.initCallback(); } }; video.width = config.video.width; video.height = config.video.height; } video.muted = true; video.play(); lastTime = new Date().getTime(); whammy = new Whammy.Video(); if (!config.disableLogs) { console.log('canvas resolutions', canvas.width, '*', canvas.height); console.log('video width/height', video.width || canvas.width, '*', video.height || canvas.height); } drawFrames(config.frameInterval); }; /** * Draw and push frames to Whammy * @param {integer} frameInterval - set minimum interval (in milliseconds) between each time we push a frame to Whammy */ function drawFrames(frameInterval) { frameInterval = typeof frameInterval !== 'undefined' ? frameInterval : 10; var duration = new Date().getTime() - lastTime; if (!duration) { return setTimeout(drawFrames, frameInterval, frameInterval); } if (isPausedRecording) { lastTime = new Date().getTime(); return setTimeout(drawFrames, 100); } // via #206, by Jack i.e. @Seymourr lastTime = new Date().getTime(); if (video.paused) { // via: https://github.com/muaz-khan/WebRTC-Experiment/pull/316 // Tweak for Android Chrome video.play(); } context.drawImage(video, 0, 0, canvas.width, canvas.height); whammy.frames.push({ duration: duration, image: canvas.toDataURL('image/webp') }); if (!isStopDrawing) { setTimeout(drawFrames, frameInterval, frameInterval); } } function asyncLoop(o) { var i = -1, length = o.length; var loop = function() { i++; if (i === length) { o.callback(); return; } o.functionToLoop(loop, i); }; loop(); //init } /** * remove black frames from the beginning to the specified frame * @param {Array} _frames - array of frames to be checked * @param {number} _framesToCheck - number of frame until check will be executed (-1 - will drop all frames until frame not matched will be found) * @param {number} _pixTolerance - 0 - very strict (only black pixel color) ; 1 - all * @param {number} _frameTolerance - 0 - very strict (only black frame color) ; 1 - all * @returns {Array} - array of frames */ // pull#293 by @volodalexey function dropBlackFrames(_frames, _framesToCheck, _pixTolerance, _frameTolerance, callback) { var localCanvas = document.createElement('canvas'); localCanvas.width = canvas.width; localCanvas.height = canvas.height; var context2d = localCanvas.getContext('2d'); var resultFrames = []; var checkUntilNotBlack = _framesToCheck === -1; var endCheckFrame = (_framesToCheck && _framesToCheck > 0 && _framesToCheck <= _frames.length) ? _framesToCheck : _frames.length; var sampleColor = { r: 0, g: 0, b: 0 }; var maxColorDifference = Math.sqrt( Math.pow(255, 2) + Math.pow(255, 2) + Math.pow(255, 2) ); var pixTolerance = _pixTolerance && _pixTolerance >= 0 && _pixTolerance <= 1 ? _pixTolerance : 0; var frameTolerance = _frameTolerance && _frameTolerance >= 0 && _frameTolerance <= 1 ? _frameTolerance : 0; var doNotCheckNext = false; asyncLoop({ length: endCheckFrame, functionToLoop: function(loop, f) { var matchPixCount, endPixCheck, maxPixCount; var finishImage = function() { if (!doNotCheckNext && maxPixCount - matchPixCount <= maxPixCount * frameTolerance) { // console.log('removed black frame : ' + f + ' ; frame duration ' + _frames[f].duration); } else { // console.log('frame is passed : ' + f); if (checkUntilNotBlack) { doNotCheckNext = true; } resultFrames.push(_frames[f]); } loop(); }; if (!doNotCheckNext) { var image = new Image(); image.onload = function() { context2d.drawImage(image, 0, 0, canvas.width, canvas.height); var imageData = context2d.getImageData(0, 0, canvas.width, canvas.height); matchPixCount = 0; endPixCheck = imageData.data.length; maxPixCount = imageData.data.length / 4; for (var pix = 0; pix < endPixCheck; pix += 4) { var currentColor = { r: imageData.data[pix], g: imageData.data[pix + 1], b: imageData.data[pix + 2] }; var colorDifference = Math.sqrt( Math.pow(currentColor.r - sampleColor.r, 2) + Math.pow(currentColor.g - sampleColor.g, 2) + Math.pow(currentColor.b - sampleColor.b, 2) ); // difference in color it is difference in color vectors (r1,g1,b1) <=> (r2,g2,b2) if (colorDifference <= maxColorDifference * pixTolerance) { matchPixCount++; } } finishImage(); }; image.src = _frames[f].image; } else { finishImage(); } }, callback: function() { resultFrames = resultFrames.concat(_frames.slice(endCheckFrame)); if (resultFrames.length <= 0) { // at least one last frame should be available for next manipulation // if total duration of all frames will be < 1000 than ffmpeg doesn't work well... resultFrames.push(_frames[_frames.length - 1]); } callback(resultFrames); } }); } var isStopDrawing = false; /** * This method stops recording video. * @param {function} callback - Callback function, that is used to pass recorded blob back to the callee. * @method * @memberof WhammyRecorder * @example * recorder.stop(function(blob) { * video.src = URL.createObjectURL(blob); * }); */ this.stop = function(callback) { isStopDrawing = true; var _this = this; // analyse of all frames takes some time! setTimeout(function() { // e.g. dropBlackFrames(frames, 10, 1, 1) - will cut all 10 frames // e.g. dropBlackFrames(frames, 10, 0.5, 0.5) - will analyse 10 frames // e.g. dropBlackFrames(frames, 10) === dropBlackFrames(frames, 10, 0, 0) - will analyse 10 frames with strict black color dropBlackFrames(whammy.frames, -1, null, null, function(frames) { whammy.frames = frames; // to display advertisement images! if (config.advertisement && config.advertisement.length) { whammy.frames = config.advertisement.concat(whammy.frames); } /** * @property {Blob} blob - Recorded frames in video/webm blob. * @memberof WhammyRecorder * @example * recorder.stop(function() { * var blob = recorder.blob; * }); */ whammy.compile(function(blob) { _this.blob = blob; if (_this.blob.forEach) { _this.blob = new Blob([], { type: 'video/webm' }); } if (callback) { callback(_this.blob); } }); }); }, 10); }; var isPausedRecording = false; /** * This method pauses the recording process. * @method * @memberof WhammyRecorder * @example * recorder.pause(); */ this.pause = function() { isPausedRecording = true; }; /** * This method resumes the recording process. * @method * @memberof WhammyRecorder * @example * recorder.resume(); */ this.resume = function() { isPausedRecording = false; if (isStopDrawing) { this.record(); } }; /** * This method resets currently recorded data. * @method * @memberof WhammyRecorder * @example * recorder.clearRecordedData(); */ this.clearRecordedData = function() { this.pause(); whammy.frames = []; }; var canvas = document.createElement('canvas'); var context = canvas.getContext('2d'); var video; var lastTime; var whammy; } if (typeof RecordRTC !== 'undefined') { RecordRTC.WhammyRecorder = WhammyRecorder; }