;(function() { "use strict"; var objectTypes = { 'function': true, 'object': true }; function checkGlobal(value) { return (value && value.Object === Object) ? value : null; } /** Built-in method references without a dependency on `root`. */ var freeParseFloat = parseFloat, freeParseInt = parseInt; /** Detect free variable `exports`. */ var freeExports = (objectTypes[typeof exports] && exports && !exports.nodeType) ? exports : undefined; /** Detect free variable `module`. */ var freeModule = (objectTypes[typeof module] && module && !module.nodeType) ? module : undefined; /** Detect the popular CommonJS extension `module.exports`. */ var moduleExports = (freeModule && freeModule.exports === freeExports) ? freeExports : undefined; /** Detect free variable `global` from Node.js. */ var freeGlobal = checkGlobal(freeExports && freeModule && typeof global == 'object' && global); /** Detect free variable `self`. */ var freeSelf = checkGlobal(objectTypes[typeof self] && self); /** Detect free variable `window`. */ var freeWindow = checkGlobal(objectTypes[typeof window] && window); /** Detect `this` as the global object. */ var thisGlobal = checkGlobal(objectTypes[typeof this] && this); /** * Used as a reference to the global object. * * The `this` value is used if it's the global object to avoid Greasemonkey's * restricted `window` object, otherwise the `window` object is used. */ var root = freeGlobal || ((freeWindow !== (thisGlobal && thisGlobal.window)) && freeWindow) || freeSelf || thisGlobal || Function('return this')(); if( !('gc' in window ) ) { window.gc = function(){} } if (!HTMLCanvasElement.prototype.toBlob) { Object.defineProperty(HTMLCanvasElement.prototype, 'toBlob', { value: function (callback, type, quality) { var binStr = atob( this.toDataURL(type, quality).split(',')[1] ), len = binStr.length, arr = new Uint8Array(len); for (var i=0; i 0 && ( this.frames / this.settings.framerate ) >= this.settings.autoSaveTime ) { this.save( function( blob ) { this.filename = this.baseFilename + '-part-' + pad( this.part ); download( blob, this.filename + this.extension, this.mimeType ); var count = this.count; this.dispose(); this.count = count+1; this.part++; this.filename = this.baseFilename + '-part-' + pad( this.part ); this.frames = 0; this.step(); }.bind( this ) ) } else { this.count++; this.frames++; this.step(); } }.bind( this ); fileReader.readAsArrayBuffer(blob); } CCTarEncoder.prototype.save = function( callback ) { callback( this.tape.save() ); } CCTarEncoder.prototype.dispose = function() { this.tape = new Tar(); this.count = 0; } function CCPNGEncoder( settings ) { CCTarEncoder.call( this, settings ); this.type = 'image/png'; this.fileExtension = '.png'; } CCPNGEncoder.prototype = Object.create( CCTarEncoder.prototype ); CCPNGEncoder.prototype.add = function( canvas ) { canvas.toBlob( function( blob ) { CCTarEncoder.prototype.add.call( this, blob ); }.bind( this ), this.type ) } function CCJPEGEncoder( settings ) { CCTarEncoder.call( this, settings ); this.type = 'image/jpeg'; this.fileExtension = '.jpg'; this.quality = ( settings.quality / 100 ) || .8; } CCJPEGEncoder.prototype = Object.create( CCTarEncoder.prototype ); CCJPEGEncoder.prototype.add = function( canvas ) { canvas.toBlob( function( blob ) { CCTarEncoder.prototype.add.call( this, blob ); }.bind( this ), this.type, this.quality ) } /* WebM Encoder */ function CCWebMEncoder( settings ) { var canvas = document.createElement( 'canvas' ); if( canvas.toDataURL( 'image/webp' ).substr(5,10) !== 'image/webp' ){ console.log( "WebP not supported - try another export format" ) } CCFrameEncoder.call( this, settings ); this.quality = ( settings.quality / 100 ) || .8; this.extension = '.webm' this.mimeType = 'video/webm' this.baseFilename = this.filename; this.framerate = settings.framerate; this.frames = 0; this.part = 1; this.videoWriter = new WebMWriter({ quality: this.quality, fileWriter: null, fd: null, frameRate: this.framerate }); } CCWebMEncoder.prototype = Object.create( CCFrameEncoder.prototype ); CCWebMEncoder.prototype.start = function( canvas ) { this.dispose(); } CCWebMEncoder.prototype.add = function( canvas ) { this.videoWriter.addFrame(canvas); if( this.settings.autoSaveTime > 0 && ( this.frames / this.settings.framerate ) >= this.settings.autoSaveTime ) { this.save( function( blob ) { this.filename = this.baseFilename + '-part-' + pad( this.part ); download( blob, this.filename + this.extension, this.mimeType ); this.dispose(); this.part++; this.filename = this.baseFilename + '-part-' + pad( this.part ); this.step(); }.bind( this ) ) } else { this.frames++; this.step(); } } CCWebMEncoder.prototype.save = function( callback ) { this.videoWriter.complete().then(callback); } CCWebMEncoder.prototype.dispose = function( canvas ) { this.frames = 0; this.videoWriter = new WebMWriter({ quality: this.quality, fileWriter: null, fd: null, frameRate: this.framerate }); } function CCFFMpegServerEncoder( settings ) { CCFrameEncoder.call( this, settings ); settings.quality = ( settings.quality / 100 ) || .8; this.encoder = new FFMpegServer.Video( settings ); this.encoder.on( 'process', function() { this.emit( 'process' ) }.bind( this ) ); this.encoder.on('finished', function( url, size ) { var cb = this.callback; if ( cb ) { this.callback = undefined; cb( url, size ); } }.bind( this ) ); this.encoder.on( 'progress', function( progress ) { if ( this.settings.onProgress ) { this.settings.onProgress( progress ) } }.bind( this ) ); this.encoder.on( 'error', function( data ) { alert(JSON.stringify(data, null, 2)); }.bind( this ) ); } CCFFMpegServerEncoder.prototype = Object.create( CCFrameEncoder.prototype ); CCFFMpegServerEncoder.prototype.start = function() { this.encoder.start( this.settings ); }; CCFFMpegServerEncoder.prototype.add = function( canvas ) { this.encoder.add( canvas ); } CCFFMpegServerEncoder.prototype.save = function( callback ) { this.callback = callback; this.encoder.end(); } CCFFMpegServerEncoder.prototype.safeToProceed = function() { return this.encoder.safeToProceed(); }; /* HTMLCanvasElement.captureStream() */ function CCStreamEncoder( settings ) { CCFrameEncoder.call( this, settings ); this.framerate = this.settings.framerate; this.type = 'video/webm'; this.extension = '.webm'; this.stream = null; this.mediaRecorder = null; this.chunks = []; } CCStreamEncoder.prototype = Object.create( CCFrameEncoder.prototype ); CCStreamEncoder.prototype.add = function( canvas ) { if( !this.stream ) { this.stream = canvas.captureStream( this.framerate ); this.mediaRecorder = new MediaRecorder( this.stream ); this.mediaRecorder.start(); this.mediaRecorder.ondataavailable = function(e) { this.chunks.push(e.data); }.bind( this ); } this.step(); } CCStreamEncoder.prototype.save = function( callback ) { this.mediaRecorder.onstop = function( e ) { var blob = new Blob( this.chunks, { 'type' : 'video/webm' }); this.chunks = []; callback( blob ); }.bind( this ); this.mediaRecorder.stop(); } /*function CCGIFEncoder( settings ) { CCFrameEncoder.call( this ); settings.quality = settings.quality || 6; this.settings = settings; this.encoder = new GIFEncoder(); this.encoder.setRepeat( 1 ); this.encoder.setDelay( settings.step ); this.encoder.setQuality( 6 ); this.encoder.setTransparent( null ); this.encoder.setSize( 150, 150 ); this.canvas = document.createElement( 'canvas' ); this.ctx = this.canvas.getContext( '2d' ); } CCGIFEncoder.prototype = Object.create( CCFrameEncoder ); CCGIFEncoder.prototype.start = function() { this.encoder.start(); } CCGIFEncoder.prototype.add = function( canvas ) { this.canvas.width = canvas.width; this.canvas.height = canvas.height; this.ctx.drawImage( canvas, 0, 0 ); this.encoder.addFrame( this.ctx ); this.encoder.setSize( canvas.width, canvas.height ); var readBuffer = new Uint8Array(canvas.width * canvas.height * 4); var context = canvas.getContext( 'webgl' ); context.readPixels(0, 0, canvas.width, canvas.height, context.RGBA, context.UNSIGNED_BYTE, readBuffer); this.encoder.addFrame( readBuffer, true ); } CCGIFEncoder.prototype.stop = function() { this.encoder.finish(); } CCGIFEncoder.prototype.save = function( callback ) { var binary_gif = this.encoder.stream().getData(); var data_url = 'data:image/gif;base64,'+encode64(binary_gif); window.location = data_url; return; var blob = new Blob( [ binary_gif ], { type: "octet/stream" } ); var url = window.URL.createObjectURL( blob ); callback( url ); }*/ function CCGIFEncoder( settings ) { CCFrameEncoder.call( this, settings ); settings.quality = 31 - ( ( settings.quality * 30 / 100 ) || 10 ); settings.workers = settings.workers || 4; this.extension = '.gif' this.mimeType = 'image/gif' this.canvas = document.createElement( 'canvas' ); this.ctx = this.canvas.getContext( '2d' ); this.sizeSet = false; this.encoder = new GIF({ workers: settings.workers, quality: settings.quality, workerScript: settings.workersPath + 'gif.worker.js' } ); this.encoder.on( 'progress', function( progress ) { if ( this.settings.onProgress ) { this.settings.onProgress( progress ) } }.bind( this ) ); this.encoder.on('finished', function( blob ) { var cb = this.callback; if ( cb ) { this.callback = undefined; cb( blob ); } }.bind( this ) ); } CCGIFEncoder.prototype = Object.create( CCFrameEncoder.prototype ); CCGIFEncoder.prototype.add = function( canvas ) { if( !this.sizeSet ) { this.encoder.setOption( 'width',canvas.width ); this.encoder.setOption( 'height',canvas.height ); this.sizeSet = true; } this.canvas.width = canvas.width; this.canvas.height = canvas.height; this.ctx.drawImage( canvas, 0, 0 ); this.encoder.addFrame( this.ctx, { copy: true, delay: this.settings.step } ); this.step(); /*this.encoder.setSize( canvas.width, canvas.height ); var readBuffer = new Uint8Array(canvas.width * canvas.height * 4); var context = canvas.getContext( 'webgl' ); context.readPixels(0, 0, canvas.width, canvas.height, context.RGBA, context.UNSIGNED_BYTE, readBuffer); this.encoder.addFrame( readBuffer, true );*/ } CCGIFEncoder.prototype.save = function( callback ) { this.callback = callback; this.encoder.render(); } function CCapture( settings ) { var _settings = settings || {}, _date = new Date(), _verbose, _display, _time, _startTime, _performanceTime, _performanceStartTime, _step, _encoder, _timeouts = [], _intervals = [], _frameCount = 0, _intermediateFrameCount = 0, _lastFrame = null, _requestAnimationFrameCallbacks = [], _capturing = false, _handlers = {}; _settings.framerate = _settings.framerate || 60; _settings.motionBlurFrames = 2 * ( _settings.motionBlurFrames || 1 ); _verbose = _settings.verbose || false; _display = _settings.display || false; _settings.step = 1000.0 / _settings.framerate ; _settings.timeLimit = _settings.timeLimit || 0; _settings.frameLimit = _settings.frameLimit || 0; _settings.startTime = _settings.startTime || 0; var _timeDisplay = document.createElement( 'div' ); _timeDisplay.style.position = 'absolute'; _timeDisplay.style.left = _timeDisplay.style.top = 0 _timeDisplay.style.backgroundColor = 'black'; _timeDisplay.style.fontFamily = 'monospace' _timeDisplay.style.fontSize = '11px' _timeDisplay.style.padding = '5px' _timeDisplay.style.color = 'red'; _timeDisplay.style.zIndex = 100000 if( _settings.display ) document.body.appendChild( _timeDisplay ); var canvasMotionBlur = document.createElement( 'canvas' ); var ctxMotionBlur = canvasMotionBlur.getContext( '2d' ); var bufferMotionBlur; var imageData; _log( 'Step is set to ' + _settings.step + 'ms' ); var _encoders = { gif: CCGIFEncoder, webm: CCWebMEncoder, ffmpegserver: CCFFMpegServerEncoder, png: CCPNGEncoder, jpg: CCJPEGEncoder, 'webm-mediarecorder': CCStreamEncoder }; var ctor = _encoders[ _settings.format ]; if ( !ctor ) { throw "Error: Incorrect or missing format: Valid formats are " + Object.keys(_encoders).join(", "); } _encoder = new ctor( _settings ); _encoder.step = _step _encoder.on('process', _process); _encoder.on('progress', _progress); if ("performance" in window == false) { window.performance = {}; } Date.now = (Date.now || function () { // thanks IE8 return new Date().getTime(); }); if ("now" in window.performance == false){ var nowOffset = Date.now(); if (performance.timing && performance.timing.navigationStart){ nowOffset = performance.timing.navigationStart } window.performance.now = function now(){ return Date.now() - nowOffset; } } var _oldSetTimeout = window.setTimeout, _oldSetInterval = window.setInterval, _oldClearInterval = window.clearInterval, _oldClearTimeout = window.clearTimeout, _oldRequestAnimationFrame = window.requestAnimationFrame, _oldNow = window.Date.now, _oldPerformanceNow = window.performance.now, _oldGetTime = window.Date.prototype.getTime; // Date.prototype._oldGetTime = Date.prototype.getTime; var media = []; function _init() { _log( 'Capturer start' ); _startTime = window.Date.now(); _time = _startTime + _settings.startTime; _performanceStartTime = window.performance.now(); _performanceTime = _performanceStartTime + _settings.startTime; window.Date.prototype.getTime = function(){ return _time; }; window.Date.now = function() { return _time; }; window.setTimeout = function( callback, time ) { var t = { callback: callback, time: time, triggerTime: _time + time }; _timeouts.push( t ); _log( 'Timeout set to ' + t.time ); return t; }; window.clearTimeout = function( id ) { for( var j = 0; j < _timeouts.length; j++ ) { if( _timeouts[ j ] == id ) { _timeouts.splice( j, 1 ); _log( 'Timeout cleared' ); continue; } } }; window.setInterval = function( callback, time ) { var t = { callback: callback, time: time, triggerTime: _time + time }; _intervals.push( t ); _log( 'Interval set to ' + t.time ); return t; }; window.clearInterval = function( id ) { _log( 'clear Interval' ); return null; }; window.requestAnimationFrame = function( callback ) { _requestAnimationFrameCallbacks.push( callback ); }; window.performance.now = function(){ return _performanceTime; }; function hookCurrentTime() {  if( !this._hooked ) { this._hooked = true; this._hookedTime = this.currentTime || 0; this.pause(); media.push( this ); } return this._hookedTime + _settings.startTime; }; try { Object.defineProperty( HTMLVideoElement.prototype, 'currentTime', { get: hookCurrentTime } ) Object.defineProperty( HTMLAudioElement.prototype, 'currentTime', { get: hookCurrentTime } ) } catch (err) { _log(err); } } function _start() { _init(); _encoder.start(); _capturing = true; } function _stop() { _capturing = false; _encoder.stop(); _destroy(); } function _call( fn, p ) { _oldSetTimeout( fn, 0, p ); } function _step() { //_oldRequestAnimationFrame( _process ); _call( _process ); } function _destroy() { _log( 'Capturer stop' ); window.setTimeout = _oldSetTimeout; window.setInterval = _oldSetInterval; window.clearInterval = _oldClearInterval; window.clearTimeout = _oldClearTimeout; window.requestAnimationFrame = _oldRequestAnimationFrame; window.Date.prototype.getTime = _oldGetTime; window.Date.now = _oldNow; window.performance.now = _oldPerformanceNow; } function _updateTime() { var seconds = _frameCount / _settings.framerate; if( ( _settings.frameLimit && _frameCount >= _settings.frameLimit ) || ( _settings.timeLimit && seconds >= _settings.timeLimit ) ) { _stop(); _save(); } var d = new Date( null ); d.setSeconds( seconds ); if( _settings.motionBlurFrames > 2 ) { _timeDisplay.textContent = 'CCapture ' + _settings.format + ' | ' + _frameCount + ' frames (' + _intermediateFrameCount + ' inter) | ' + d.toISOString().substr( 11, 8 ); } else { _timeDisplay.textContent = 'CCapture ' + _settings.format + ' | ' + _frameCount + ' frames | ' + d.toISOString().substr( 11, 8 ); } } function _checkFrame( canvas ) { if( canvasMotionBlur.width !== canvas.width || canvasMotionBlur.height !== canvas.height ) { canvasMotionBlur.width = canvas.width; canvasMotionBlur.height = canvas.height; bufferMotionBlur = new Uint16Array( canvasMotionBlur.height * canvasMotionBlur.width * 4 ); ctxMotionBlur.fillStyle = '#0' ctxMotionBlur.fillRect( 0, 0, canvasMotionBlur.width, canvasMotionBlur.height ); } } function _blendFrame( canvas ) { //_log( 'Intermediate Frame: ' + _intermediateFrameCount ); ctxMotionBlur.drawImage( canvas, 0, 0 ); imageData = ctxMotionBlur.getImageData( 0, 0, canvasMotionBlur.width, canvasMotionBlur.height ); for( var j = 0; j < bufferMotionBlur.length; j+= 4 ) { bufferMotionBlur[ j ] += imageData.data[ j ]; bufferMotionBlur[ j + 1 ] += imageData.data[ j + 1 ]; bufferMotionBlur[ j + 2 ] += imageData.data[ j + 2 ]; } _intermediateFrameCount++; } function _saveFrame(){ var data = imageData.data; for( var j = 0; j < bufferMotionBlur.length; j+= 4 ) { data[ j ] = bufferMotionBlur[ j ] * 2 / _settings.motionBlurFrames; data[ j + 1 ] = bufferMotionBlur[ j + 1 ] * 2 / _settings.motionBlurFrames; data[ j + 2 ] = bufferMotionBlur[ j + 2 ] * 2 / _settings.motionBlurFrames; } ctxMotionBlur.putImageData( imageData, 0, 0 ); _encoder.add( canvasMotionBlur ); _frameCount++; _intermediateFrameCount = 0; _log( 'Full MB Frame! ' + _frameCount + ' ' + _time ); for( var j = 0; j < bufferMotionBlur.length; j+= 4 ) { bufferMotionBlur[ j ] = 0; bufferMotionBlur[ j + 1 ] = 0; bufferMotionBlur[ j + 2 ] = 0; } gc(); } function _capture( canvas ) { if( _capturing ) { if( _settings.motionBlurFrames > 2 ) { _checkFrame( canvas ); _blendFrame( canvas ); if( _intermediateFrameCount >= .5 * _settings.motionBlurFrames ) { _saveFrame(); } else { _step(); } } else { _encoder.add( canvas ); _frameCount++; _log( 'Full Frame! ' + _frameCount ); } } } function _process() { var step = 1000 / _settings.framerate; var dt = ( _frameCount + _intermediateFrameCount / _settings.motionBlurFrames ) * step; _time = _startTime + dt; _performanceTime = _performanceStartTime + dt; media.forEach( function( v ) { v._hookedTime = dt / 1000; } ); _updateTime(); _log( 'Frame: ' + _frameCount + ' ' + _intermediateFrameCount ); for( var j = 0; j < _timeouts.length; j++ ) { if( _time >= _timeouts[ j ].triggerTime ) { _call( _timeouts[ j ].callback ) //console.log( 'timeout!' ); _timeouts.splice( j, 1 ); continue; } } for( var j = 0; j < _intervals.length; j++ ) { if( _time >= _intervals[ j ].triggerTime ) { _call( _intervals[ j ].callback ); _intervals[ j ].triggerTime += _intervals[ j ].time; //console.log( 'interval!' ); continue; } } _requestAnimationFrameCallbacks.forEach( function( cb ) { _call( cb, _time - g_startTime ); } ); _requestAnimationFrameCallbacks = []; } function _save( callback ) { if( !callback ) { callback = function( blob ) { download( blob, _encoder.filename + _encoder.extension, _encoder.mimeType ); return false; } } _encoder.save( callback ); } function _log( message ) { if( _verbose ) console.log( message ); } function _on( event, handler ) { _handlers[event] = handler; } function _emit( event ) { var handler = _handlers[event]; if ( handler ) { handler.apply( null, Array.prototype.slice.call( arguments, 1 ) ); } } function _progress( progress ) { _emit( 'progress', progress ); } return { start: _start, capture: _capture, stop: _stop, save: _save, on: _on } } (freeWindow || freeSelf || {}).CCapture = CCapture; // Some AMD build optimizers like r.js check for condition patterns like the following: if (typeof define == 'function' && typeof define.amd == 'object' && define.amd) { // Define as an anonymous module so, through path mapping, it can be // referenced as the "underscore" module. define(function() { return CCapture; }); } // Check for `exports` after `define` in case a build optimizer adds an `exports` object. else if (freeExports && freeModule) { // Export for Node.js. if (moduleExports) { (freeModule.exports = CCapture).CCapture = CCapture; } // Export for CommonJS support. freeExports.CCapture = CCapture; } else { // Export to the global object. root.CCapture = CCapture; } }());