; (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 < len; i++) { arr[i] = binStr.charCodeAt(i); } callback(new Blob([arr], { type: type || 'image/png' })); } }); } // @license http://opensource.org/licenses/MIT // copyright Paul Irish 2015 // Date.now() is supported everywhere except IE8. For IE8 we use the Date.now polyfill // github.com/Financial-Times/polyfill-service/blob/master/polyfills/Date.now/polyfill.js // as Safari 6 doesn't have support for NavigationTiming, we use a Date.now() timestamp for relative values // if you want values similar to what you'd get with real perf.now, place this towards the head of the page // but in reality, you're just getting the delta between now() calls, so it's not terribly important where it's placed (function() { 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; } } })(); function pad(n) { return String("0000000" + n).slice(-7); } // https://developer.mozilla.org/en-US/Add-ons/Code_snippets/Timers var g_startTime = window.Date.now(); function guid() { function s4() { return Math.floor((1 + Math.random()) * 0x10000).toString(16).substring(1); } return s4() + s4() + '-' + s4() + '-' + s4() + '-' + s4() + '-' + s4() + s4() + s4(); } function CCFrameEncoder(settings) { var _handlers = {}; this.settings = settings; this.on = function(event, handler) { _handlers[event] = handler; }; this.emit = function(event) { var handler = _handlers[event]; if (handler) { handler.apply(null, Array.prototype.slice.call(arguments, 1)); } }; this.filename = settings.name || guid(); this.extension = ''; this.mimeType = ''; } CCFrameEncoder.prototype.start = function() {}; CCFrameEncoder.prototype.stop = function() {}; CCFrameEncoder.prototype.add = function() {}; CCFrameEncoder.prototype.save = function() {}; CCFrameEncoder.prototype.dispose = function() {}; CCFrameEncoder.prototype.safeToProceed = function() { return true; }; CCFrameEncoder.prototype.step = function() { console.log('Step not set!') } function CCTarEncoder(settings) { CCFrameEncoder.call(this, settings); this.extension = '.tar' this.mimeType = 'application/x-tar' this.fileExtension = ''; this.tape = null this.baseFilename = 'j360'; this.count = 0; } var globalChunkCount = 0; var globalPartCount = 0; CCTarEncoder.prototype = Object.create(CCFrameEncoder.prototype); CCTarEncoder.prototype.start = function() { this.dispose(); }; CCTarEncoder.prototype.add = function(blob) { var fileReader = new FileReader(); fileReader.onload = function() { this.tape.append(pad(globalChunkCount) + this.fileExtension, new Uint8Array(fileReader.result)); if (this.settings.autoSaveTime > 0 && (this.count / this.settings.framerate) >= this.settings.autoSaveTime) { this.save(function(blob) { this.filename = this.baseFilename + '_part_' + pad(globalPartCount); download(blob, this.filename + this.extension, this.mimeType); globalPartCount++; globalChunkCount++; this.dispose(); this.step(); }.bind(this)) } else { globalChunkCount++; this.count++; 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 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) } function CC360Encoder(settings) { CCTarEncoder.call(this, settings); // this.type = 'image/png'; // this.fileExtension = '.png'; this.type = 'image/jpeg'; this.fileExtension = '.jpg'; this.quality = (settings.quality / 100) || 1; } CC360Encoder.prototype = Object.create(CCTarEncoder.prototype); CC360Encoder.prototype.add = function(canvas) { equiManaged.preBlob(equiManaged.cubeCamera); equiManaged.canvas.toBlob(function(blob) { CCTarEncoder.prototype.add.call(this, blob); }.bind(this), this.type, this.quality) } 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) } /* 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); settings.quality = (settings.quality / 100) || .8; this.extension = '.webm' this.mimeType = 'video/webm' this.baseFilename = this.filename; this.frames = []; this.part = 1; } CCWebMEncoder.prototype = Object.create(CCFrameEncoder.prototype); CCWebMEncoder.prototype.start = function(canvas) { this.dispose(); } CCWebMEncoder.prototype.add = function(canvas) { this.frames.push(canvas.toDataURL('image/webp', this.quality)); if (this.settings.autoSaveTime > 0 && (this.frames.length / 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.step(); } } CCWebMEncoder.prototype.save = function(callback) { if (!this.frames.length) return; var webm = Whammy.fromImageArray(this.frames, this.settings.framerate) var blob = new Blob([webm], { type: "octet/stream" }); callback(blob); } CCWebMEncoder.prototype.dispose = function(canvas) { this.frames = []; } 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 || 8; 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 || 25; _settings.motionBlurFrames = 2 * (_settings.motionBlurFrames || 1); _verbose = _settings.verbose || false; _display = _settings.display || true; _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 = '16px' _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, threesixty: CC360Encoder, 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, _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.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; } }());