diff --git a/.DS_Store b/.DS_Store index 7e39e58..96830b5 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/mocaprender/render.html b/mocaprender/render.html index fdd840e..03f5f52 100644 --- a/mocaprender/render.html +++ b/mocaprender/render.html @@ -182,6 +182,33 @@ +
+ 正在录制,按R键停止 +
+ + + + + + diff --git a/mocaprender/script.js b/mocaprender/script.js index 67b586a..b8a7908 100644 --- a/mocaprender/script.js +++ b/mocaprender/script.js @@ -99,6 +99,8 @@ statsContainer.appendChild(stats2.dom); // Main Render Loop const clock = new THREE.Clock(); +var isRecordingStarted = false; + function animate() { requestAnimationFrame(animate); @@ -109,6 +111,11 @@ function animate() { currentVrm.update(clock.getDelta()); } renderer.render(scene, orbitCamera); + + if(isRecordingStarted)html2canvas(elementToRecord).then(function (canvas) { + context.clearRect(0, 0, canvas2d.width, canvas2d.height); + context.drawImage(canvas, 0, 0, canvas2d.width, canvas2d.height); + }); } animate(); @@ -774,6 +781,14 @@ document.addEventListener("keydown", (event) => { case "ArrowDown": positionOffset.y -= step; break; + case "r": + if (isRecordingStarted) { + stopRecording(); + document.getElementById("recording").style.display = "none"; + } else { + startRecording(); + document.getElementById("recording").style.display = ""; + } } }); @@ -797,3 +812,49 @@ contentDom.ondrop = (e) => { contentDom.style.backgroundPosition = "center"; contentDom.style.backgroundRepeat = "no-repeat"; }; + +var elementToRecord = contentDom; +var canvas2d = document.getElementById("background-canvas"); +var context = canvas2d.getContext("2d"); + +canvas2d.width = elementToRecord.clientWidth; +canvas2d.height = elementToRecord.clientHeight; + +var recorder = new RecordRTC(canvas2d, { + type: "canvas", +}); + +function startRecording() { + this.disabled = true; + + isRecordingStarted = true; + + recorder.startRecording(); +} + +function stopRecording() { + this.disabled = true; + + recorder.stopRecording(function () { + isRecordingStarted = false; + + var blob = recorder.getBlob(); + + const link = document.createElement("a"); + link.href = URL.createObjectURL(blob); + link.download = "video.webm"; + + link.dispatchEvent( + new MouseEvent("click", { + bubbles: true, + cancelable: true, + view: window, + }) + ); + + setTimeout(() => { + window.URL.revokeObjectURL(blob); + link.remove(); + }, 100); + }); +} diff --git a/render/render.html b/render/render.html index 2b9ff6b..06cc880 100644 --- a/render/render.html +++ b/render/render.html @@ -58,11 +58,15 @@ overflow: hidden; position: fixed; " - > + >
- Render - Mocap + Render + Mocap
+
+ 初始化动作捕捉引擎… -
- 初始化动作捕捉引擎… -
+
+ +
+ 正在录制,按R键停止 +
+ + + + diff --git a/render/render.js b/render/render.js index 43b37a3..682701f 100644 --- a/render/render.js +++ b/render/render.js @@ -20,7 +20,6 @@ document.body.setAttribute( globalSettings.ui.themeColor ); - // import Helper Functions from Kalidokit const remap = Kalidokit.Utils.remap; const clamp = Kalidokit.Utils.clamp; @@ -31,7 +30,6 @@ let currentVrm; var mocapData = null; - // renderer const renderer = new THREE.WebGLRenderer({ alpha: true, @@ -97,7 +95,6 @@ var modelPath = modelObj.path; // Main Render Loop const clock = new THREE.Clock(); - var fileType = modelPath .substring(modelPath.lastIndexOf(".") + 1) .toLowerCase(); @@ -203,7 +200,6 @@ loader.load( (error) => console.error(error) ); - // Animate Rotation Helper function const rigRotation = ( name, @@ -404,7 +400,7 @@ const animateVRM = (vrm, results) => { if (!vrm && !skeletonHelper) { return; } - if(!results) return; + if (!results) return; // Take the results from `Holistic` and animate character based on its Face, Pose, and Hand Keypoints. let riggedPose = results.riggedPose, riggedLeftHand = results.riggedLeftHand, @@ -412,7 +408,7 @@ const animateVRM = (vrm, results) => { riggedFace = results.riggedFace; // Animate Face - if (riggedFace ) { + if (riggedFace) { rigRotation("Neck", riggedFace.head, 0.7); if (fileType == "vrm") rigFace(structuredClone(riggedFace)); } @@ -446,7 +442,7 @@ const animateVRM = (vrm, results) => { } // Animate Hands - if (riggedLeftHand && fileType == "vrm") { + if (riggedLeftHand && fileType == "vrm") { rigRotation("LeftHand", { // Combine pose rotation Z and hand rotation X Y z: riggedPose.LeftHand.z, @@ -523,9 +519,10 @@ const animateVRM = (vrm, results) => { ); rigRotation("RightLittleDistal", riggedRightHand.RightLittleDistal); } - }; +var isRecordingStarted = false; + function animate() { requestAnimationFrame(animate); @@ -538,21 +535,25 @@ function animate() { currentVrm.update(clock.getDelta()); } renderer.render(scene, orbitCamera); + + if(isRecordingStarted)html2canvas(elementToRecord).then(function (canvas) { + context.clearRect(0, 0, canvas2d.width, canvas2d.height); + context.drawImage(canvas, 0, 0, canvas2d.width, canvas2d.height); + }); } animate(); var isStart = false; -window.onMocapData = (data)=>{ - if(!isStart){ - document.getElementById('loading').remove() - isStart = true +window.onMocapData = (data) => { + if (!isStart) { + document.getElementById("loading").remove(); + isStart = true; } - console.log("sendRenderDataForward") stats2.update(); // Animate model mocapData = data; -} +}; var app = new Vue({ el: "#controller", @@ -600,10 +601,17 @@ document.addEventListener("keydown", (event) => { case "ArrowDown": positionOffset.y -= step; break; + case "r": + if(isRecordingStarted){ + stopRecording() + document.getElementById("recording").style.display = "none"; + }else{ + startRecording() + document.getElementById("recording").style.display = "" + } } }); - if (localStorage.getItem("useCamera") !== "camera") { document.querySelector("#model").style.transform = "scale(-1, 1)"; } @@ -627,4 +635,50 @@ contentDom.ondrop = (e) => { contentDom.style.backgroundSize = "cover"; contentDom.style.backgroundPosition = "center"; contentDom.style.backgroundRepeat = "no-repeat"; +}; + +var elementToRecord = contentDom; +var canvas2d = document.getElementById("background-canvas"); +var context = canvas2d.getContext("2d"); + +canvas2d.width = elementToRecord.clientWidth; +canvas2d.height = elementToRecord.clientHeight; + +var recorder = new RecordRTC(canvas2d, { + type: "canvas" +}); + +function startRecording() { + this.disabled = true; + + isRecordingStarted = true; + + recorder.startRecording(); +}; + +function stopRecording() { + this.disabled = true; + + recorder.stopRecording(function () { + isRecordingStarted = false; + + var blob = recorder.getBlob(); + + const link = document.createElement("a"); + link.href = URL.createObjectURL(blob); + link.download = "video.webm"; + + link.dispatchEvent( + new MouseEvent("click", { + bubbles: true, + cancelable: true, + view: window, + }) + ); + + setTimeout(() => { + window.URL.revokeObjectURL(blob); + link.remove(); + }, 100); + }); }; \ No newline at end of file diff --git a/utils/RecordRTC.js b/utils/RecordRTC.js new file mode 100644 index 0000000..179e1df --- /dev/null +++ b/utils/RecordRTC.js @@ -0,0 +1,6211 @@ +'use strict'; + +// Last time updated: 2022-04-05 11:18:05 AM UTC + +// ________________ +// RecordRTC v5.6.2 + +// Open-Sourced: https://github.com/muaz-khan/RecordRTC + +// -------------------------------------------------- +// Muaz Khan - www.MuazKhan.com +// MIT License - www.WebRTC-Experiment.com/licence +// -------------------------------------------------- + +// ____________ +// RecordRTC.js + +/** + * {@link https://github.com/muaz-khan/RecordRTC|RecordRTC} is a WebRTC JavaScript library for audio/video as well as screen activity recording. It supports Chrome, Firefox, Opera, Android, and Microsoft Edge. Platforms: Linux, Mac and Windows. + * @summary Record audio, video or screen inside the browser. + * @license {@link https://github.com/muaz-khan/RecordRTC/blob/master/LICENSE|MIT} + * @author {@link https://MuazKhan.com|Muaz Khan} + * @typedef RecordRTC + * @class + * @example + * var recorder = RecordRTC(mediaStream or [arrayOfMediaStream], { + * type: 'video', // audio or video or gif or canvas + * recorderType: MediaStreamRecorder || CanvasRecorder || StereoAudioRecorder || Etc + * }); + * recorder.startRecording(); + * @see For further information: + * @see {@link https://github.com/muaz-khan/RecordRTC|RecordRTC Source Code} + * @param {MediaStream} mediaStream - Single media-stream object, array of media-streams, html-canvas-element, etc. + * @param {object} config - {type:"video", recorderType: MediaStreamRecorder, disableLogs: true, numberOfAudioChannels: 1, bufferSize: 0, sampleRate: 0, desiredSampRate: 16000, video: HTMLVideoElement, etc.} + */ + +function RecordRTC(mediaStream, config) { + if (!mediaStream) { + throw 'First parameter is required.'; + } + + config = config || { + type: 'video' + }; + + config = new RecordRTCConfiguration(mediaStream, config); + + // a reference to user's recordRTC object + var self = this; + + function startRecording(config2) { + if (!config.disableLogs) { + console.log('RecordRTC version: ', self.version); + } + + if (!!config2) { + // allow users to set options using startRecording method + // config2 is similar to main "config" object (second parameter over RecordRTC constructor) + config = new RecordRTCConfiguration(mediaStream, config2); + } + + if (!config.disableLogs) { + console.log('started recording ' + config.type + ' stream.'); + } + + if (mediaRecorder) { + mediaRecorder.clearRecordedData(); + mediaRecorder.record(); + + setState('recording'); + + if (self.recordingDuration) { + handleRecordingDuration(); + } + return self; + } + + initRecorder(function() { + if (self.recordingDuration) { + handleRecordingDuration(); + } + }); + + return self; + } + + function initRecorder(initCallback) { + if (initCallback) { + config.initCallback = function() { + initCallback(); + initCallback = config.initCallback = null; // recorder.initRecorder should be call-backed once. + }; + } + + var Recorder = new GetRecorderType(mediaStream, config); + + mediaRecorder = new Recorder(mediaStream, config); + mediaRecorder.record(); + + setState('recording'); + + if (!config.disableLogs) { + console.log('Initialized recorderType:', mediaRecorder.constructor.name, 'for output-type:', config.type); + } + } + + function stopRecording(callback) { + callback = callback || function() {}; + + if (!mediaRecorder) { + warningLog(); + return; + } + + if (self.state === 'paused') { + self.resumeRecording(); + + setTimeout(function() { + stopRecording(callback); + }, 1); + return; + } + + if (self.state !== 'recording' && !config.disableLogs) { + console.warn('Recording state should be: "recording", however current state is: ', self.state); + } + + if (!config.disableLogs) { + console.log('Stopped recording ' + config.type + ' stream.'); + } + + if (config.type !== 'gif') { + mediaRecorder.stop(_callback); + } else { + mediaRecorder.stop(); + _callback(); + } + + setState('stopped'); + + function _callback(__blob) { + if (!mediaRecorder) { + if (typeof callback.call === 'function') { + callback.call(self, ''); + } else { + callback(''); + } + return; + } + + Object.keys(mediaRecorder).forEach(function(key) { + if (typeof mediaRecorder[key] === 'function') { + return; + } + + self[key] = mediaRecorder[key]; + }); + + var blob = mediaRecorder.blob; + + if (!blob) { + if (__blob) { + mediaRecorder.blob = blob = __blob; + } else { + throw 'Recording failed.'; + } + } + + if (blob && !config.disableLogs) { + console.log(blob.type, '->', bytesToSize(blob.size)); + } + + if (callback) { + var url; + + try { + url = URL.createObjectURL(blob); + } catch (e) {} + + if (typeof callback.call === 'function') { + callback.call(self, url); + } else { + callback(url); + } + } + + if (!config.autoWriteToDisk) { + return; + } + + getDataURL(function(dataURL) { + var parameter = {}; + parameter[config.type + 'Blob'] = dataURL; + DiskStorage.Store(parameter); + }); + } + } + + function pauseRecording() { + if (!mediaRecorder) { + warningLog(); + return; + } + + if (self.state !== 'recording') { + if (!config.disableLogs) { + console.warn('Unable to pause the recording. Recording state: ', self.state); + } + return; + } + + setState('paused'); + + mediaRecorder.pause(); + + if (!config.disableLogs) { + console.log('Paused recording.'); + } + } + + function resumeRecording() { + if (!mediaRecorder) { + warningLog(); + return; + } + + if (self.state !== 'paused') { + if (!config.disableLogs) { + console.warn('Unable to resume the recording. Recording state: ', self.state); + } + return; + } + + setState('recording'); + + // not all libs have this method yet + mediaRecorder.resume(); + + if (!config.disableLogs) { + console.log('Resumed recording.'); + } + } + + function readFile(_blob) { + postMessage(new FileReaderSync().readAsDataURL(_blob)); + } + + function getDataURL(callback, _mediaRecorder) { + if (!callback) { + throw 'Pass a callback function over getDataURL.'; + } + + var blob = _mediaRecorder ? _mediaRecorder.blob : (mediaRecorder || {}).blob; + + if (!blob) { + if (!config.disableLogs) { + console.warn('Blob encoder did not finish its job yet.'); + } + + setTimeout(function() { + getDataURL(callback, _mediaRecorder); + }, 1000); + return; + } + + if (typeof Worker !== 'undefined' && !navigator.mozGetUserMedia) { + var webWorker = processInWebWorker(readFile); + + webWorker.onmessage = function(event) { + callback(event.data); + }; + + webWorker.postMessage(blob); + } else { + var reader = new FileReader(); + reader.readAsDataURL(blob); + reader.onload = function(event) { + callback(event.target.result); + }; + } + + function processInWebWorker(_function) { + try { + var blob = URL.createObjectURL(new Blob([_function.toString(), + 'this.onmessage = function (eee) {' + _function.name + '(eee.data);}' + ], { + type: 'application/javascript' + })); + + var worker = new Worker(blob); + URL.revokeObjectURL(blob); + return worker; + } catch (e) {} + } + } + + function handleRecordingDuration(counter) { + counter = counter || 0; + + if (self.state === 'paused') { + setTimeout(function() { + handleRecordingDuration(counter); + }, 1000); + return; + } + + if (self.state === 'stopped') { + return; + } + + if (counter >= self.recordingDuration) { + stopRecording(self.onRecordingStopped); + return; + } + + counter += 1000; // 1-second + + setTimeout(function() { + handleRecordingDuration(counter); + }, 1000); + } + + function setState(state) { + if (!self) { + return; + } + + self.state = state; + + if (typeof self.onStateChanged.call === 'function') { + self.onStateChanged.call(self, state); + } else { + self.onStateChanged(state); + } + } + + var WARNING = 'It seems that recorder is destroyed or "startRecording" is not invoked for ' + config.type + ' recorder.'; + + function warningLog() { + if (config.disableLogs === true) { + return; + } + + console.warn(WARNING); + } + + var mediaRecorder; + + var returnObject = { + /** + * This method starts the recording. + * @method + * @memberof RecordRTC + * @instance + * @example + * var recorder = RecordRTC(mediaStream, { + * type: 'video' + * }); + * recorder.startRecording(); + */ + startRecording: startRecording, + + /** + * This method stops the recording. It is strongly recommended to get "blob" or "URI" inside the callback to make sure all recorders finished their job. + * @param {function} callback - Callback to get the recorded blob. + * @method + * @memberof RecordRTC + * @instance + * @example + * recorder.stopRecording(function() { + * // use either "this" or "recorder" object; both are identical + * video.src = this.toURL(); + * var blob = this.getBlob(); + * }); + */ + stopRecording: stopRecording, + + /** + * This method pauses the recording. You can resume recording using "resumeRecording" method. + * @method + * @memberof RecordRTC + * @instance + * @todo Firefox is unable to pause the recording. Fix it. + * @example + * recorder.pauseRecording(); // pause the recording + * recorder.resumeRecording(); // resume again + */ + pauseRecording: pauseRecording, + + /** + * This method resumes the recording. + * @method + * @memberof RecordRTC + * @instance + * @example + * recorder.pauseRecording(); // first of all, pause the recording + * recorder.resumeRecording(); // now resume it + */ + resumeRecording: resumeRecording, + + /** + * This method initializes the recording. + * @method + * @memberof RecordRTC + * @instance + * @todo This method should be deprecated. + * @example + * recorder.initRecorder(); + */ + initRecorder: initRecorder, + + /** + * Ask RecordRTC to auto-stop the recording after 5 minutes. + * @method + * @memberof RecordRTC + * @instance + * @example + * var fiveMinutes = 5 * 1000 * 60; + * recorder.setRecordingDuration(fiveMinutes, function() { + * var blob = this.getBlob(); + * video.src = this.toURL(); + * }); + * + * // or otherwise + * recorder.setRecordingDuration(fiveMinutes).onRecordingStopped(function() { + * var blob = this.getBlob(); + * video.src = this.toURL(); + * }); + */ + setRecordingDuration: function(recordingDuration, callback) { + if (typeof recordingDuration === 'undefined') { + throw 'recordingDuration is required.'; + } + + if (typeof recordingDuration !== 'number') { + throw 'recordingDuration must be a number.'; + } + + self.recordingDuration = recordingDuration; + self.onRecordingStopped = callback || function() {}; + + return { + onRecordingStopped: function(callback) { + self.onRecordingStopped = callback; + } + }; + }, + + /** + * This method can be used to clear/reset all the recorded data. + * @method + * @memberof RecordRTC + * @instance + * @todo Figure out the difference between "reset" and "clearRecordedData" methods. + * @example + * recorder.clearRecordedData(); + */ + clearRecordedData: function() { + if (!mediaRecorder) { + warningLog(); + return; + } + + mediaRecorder.clearRecordedData(); + + if (!config.disableLogs) { + console.log('Cleared old recorded data.'); + } + }, + + /** + * Get the recorded blob. Use this method inside the "stopRecording" callback. + * @method + * @memberof RecordRTC + * @instance + * @example + * recorder.stopRecording(function() { + * var blob = this.getBlob(); + * + * var file = new File([blob], 'filename.webm', { + * type: 'video/webm' + * }); + * + * var formData = new FormData(); + * formData.append('file', file); // upload "File" object rather than a "Blob" + * uploadToServer(formData); + * }); + * @returns {Blob} Returns recorded data as "Blob" object. + */ + getBlob: function() { + if (!mediaRecorder) { + warningLog(); + return; + } + + return mediaRecorder.blob; + }, + + /** + * Get data-URI instead of Blob. + * @param {function} callback - Callback to get the Data-URI. + * @method + * @memberof RecordRTC + * @instance + * @example + * recorder.stopRecording(function() { + * recorder.getDataURL(function(dataURI) { + * video.src = dataURI; + * }); + * }); + */ + getDataURL: getDataURL, + + /** + * Get virtual/temporary URL. Usage of this URL is limited to current tab. + * @method + * @memberof RecordRTC + * @instance + * @example + * recorder.stopRecording(function() { + * video.src = this.toURL(); + * }); + * @returns {String} Returns a virtual/temporary URL for the recorded "Blob". + */ + toURL: function() { + if (!mediaRecorder) { + warningLog(); + return; + } + + return URL.createObjectURL(mediaRecorder.blob); + }, + + /** + * Get internal recording object (i.e. internal module) e.g. MutliStreamRecorder, MediaStreamRecorder, StereoAudioRecorder or WhammyRecorder etc. + * @method + * @memberof RecordRTC + * @instance + * @example + * var internalRecorder = recorder.getInternalRecorder(); + * if(internalRecorder instanceof MultiStreamRecorder) { + * internalRecorder.addStreams([newAudioStream]); + * internalRecorder.resetVideoStreams([screenStream]); + * } + * @returns {Object} Returns internal recording object. + */ + getInternalRecorder: function() { + return mediaRecorder; + }, + + /** + * Invoke save-as dialog to save the recorded blob into your disk. + * @param {string} fileName - Set your own file name. + * @method + * @memberof RecordRTC + * @instance + * @example + * recorder.stopRecording(function() { + * this.save('file-name'); + * + * // or manually: + * invokeSaveAsDialog(this.getBlob(), 'filename.webm'); + * }); + */ + save: function(fileName) { + if (!mediaRecorder) { + warningLog(); + return; + } + + invokeSaveAsDialog(mediaRecorder.blob, fileName); + }, + + /** + * This method gets a blob from indexed-DB storage. + * @param {function} callback - Callback to get the recorded blob. + * @method + * @memberof RecordRTC + * @instance + * @example + * recorder.getFromDisk(function(dataURL) { + * video.src = dataURL; + * }); + */ + getFromDisk: function(callback) { + if (!mediaRecorder) { + warningLog(); + return; + } + + RecordRTC.getFromDisk(config.type, callback); + }, + + /** + * This method appends an array of webp images to the recorded video-blob. It takes an "array" object. + * @type {Array.} + * @param {Array} arrayOfWebPImages - Array of webp images. + * @method + * @memberof RecordRTC + * @instance + * @todo This method should be deprecated. + * @example + * var arrayOfWebPImages = []; + * arrayOfWebPImages.push({ + * duration: index, + * image: 'data:image/webp;base64,...' + * }); + * recorder.setAdvertisementArray(arrayOfWebPImages); + */ + setAdvertisementArray: function(arrayOfWebPImages) { + config.advertisement = []; + + var length = arrayOfWebPImages.length; + for (var i = 0; i < length; i++) { + config.advertisement.push({ + duration: i, + image: arrayOfWebPImages[i] + }); + } + }, + + /** + * It is equivalent to "recorder.getBlob()" method. Usage of "getBlob" is recommended, though. + * @property {Blob} blob - Recorded Blob can be accessed using this property. + * @memberof RecordRTC + * @instance + * @readonly + * @example + * recorder.stopRecording(function() { + * var blob = this.blob; + * + * // below one is recommended + * var blob = this.getBlob(); + * }); + */ + blob: null, + + /** + * This works only with {recorderType:StereoAudioRecorder}. Use this property on "stopRecording" to verify the encoder's sample-rates. + * @property {number} bufferSize - Buffer-size used to encode the WAV container + * @memberof RecordRTC + * @instance + * @readonly + * @example + * recorder.stopRecording(function() { + * alert('Recorder used this buffer-size: ' + this.bufferSize); + * }); + */ + bufferSize: 0, + + /** + * This works only with {recorderType:StereoAudioRecorder}. Use this property on "stopRecording" to verify the encoder's sample-rates. + * @property {number} sampleRate - Sample-rates used to encode the WAV container + * @memberof RecordRTC + * @instance + * @readonly + * @example + * recorder.stopRecording(function() { + * alert('Recorder used these sample-rates: ' + this.sampleRate); + * }); + */ + sampleRate: 0, + + /** + * {recorderType:StereoAudioRecorder} returns ArrayBuffer object. + * @property {ArrayBuffer} buffer - Audio ArrayBuffer, supported only in Chrome. + * @memberof RecordRTC + * @instance + * @readonly + * @example + * recorder.stopRecording(function() { + * var arrayBuffer = this.buffer; + * alert(arrayBuffer.byteLength); + * }); + */ + buffer: null, + + /** + * This method resets the recorder. So that you can reuse single recorder instance many times. + * @method + * @memberof RecordRTC + * @instance + * @example + * recorder.reset(); + * recorder.startRecording(); + */ + reset: function() { + if (self.state === 'recording' && !config.disableLogs) { + console.warn('Stop an active recorder.'); + } + + if (mediaRecorder && typeof mediaRecorder.clearRecordedData === 'function') { + mediaRecorder.clearRecordedData(); + } + mediaRecorder = null; + setState('inactive'); + self.blob = null; + }, + + /** + * This method is called whenever recorder's state changes. Use this as an "event". + * @property {String} state - A recorder's state can be: recording, paused, stopped or inactive. + * @method + * @memberof RecordRTC + * @instance + * @example + * recorder.onStateChanged = function(state) { + * console.log('Recorder state: ', state); + * }; + */ + onStateChanged: function(state) { + if (!config.disableLogs) { + console.log('Recorder state changed:', state); + } + }, + + /** + * A recorder can have inactive, recording, paused or stopped states. + * @property {String} state - A recorder's state can be: recording, paused, stopped or inactive. + * @memberof RecordRTC + * @static + * @readonly + * @example + * // this looper function will keep you updated about the recorder's states. + * (function looper() { + * document.querySelector('h1').innerHTML = 'Recorder\'s state is: ' + recorder.state; + * if(recorder.state === 'stopped') return; // ignore+stop + * setTimeout(looper, 1000); // update after every 3-seconds + * })(); + * recorder.startRecording(); + */ + state: 'inactive', + + /** + * Get recorder's readonly state. + * @method + * @memberof RecordRTC + * @example + * var state = recorder.getState(); + * @returns {String} Returns recording state. + */ + getState: function() { + return self.state; + }, + + /** + * Destroy RecordRTC instance. Clear all recorders and objects. + * @method + * @memberof RecordRTC + * @example + * recorder.destroy(); + */ + destroy: function() { + var disableLogsCache = config.disableLogs; + + config = { + disableLogs: true + }; + self.reset(); + setState('destroyed'); + returnObject = self = null; + + if (Storage.AudioContextConstructor) { + Storage.AudioContextConstructor.close(); + Storage.AudioContextConstructor = null; + } + + config.disableLogs = disableLogsCache; + + if (!config.disableLogs) { + console.log('RecordRTC is destroyed.'); + } + }, + + /** + * RecordRTC version number + * @property {String} version - Release version number. + * @memberof RecordRTC + * @static + * @readonly + * @example + * alert(recorder.version); + */ + version: '5.6.2' + }; + + if (!this) { + self = returnObject; + return returnObject; + } + + // if someone wants to use RecordRTC with the "new" keyword. + for (var prop in returnObject) { + this[prop] = returnObject[prop]; + } + + self = this; + + return returnObject; +} + +RecordRTC.version = '5.6.2'; + +if (typeof module !== 'undefined' /* && !!module.exports*/ ) { + module.exports = RecordRTC; +} + +if (typeof define === 'function' && define.amd) { + define('RecordRTC', [], function() { + return RecordRTC; + }); +} + +RecordRTC.getFromDisk = function(type, callback) { + if (!callback) { + throw 'callback is mandatory.'; + } + + console.log('Getting recorded ' + (type === 'all' ? 'blobs' : type + ' blob ') + ' from disk!'); + DiskStorage.Fetch(function(dataURL, _type) { + if (type !== 'all' && _type === type + 'Blob' && callback) { + callback(dataURL); + } + + if (type === 'all' && callback) { + callback(dataURL, _type.replace('Blob', '')); + } + }); +}; + +/** + * This method can be used to store recorded blobs into IndexedDB storage. + * @param {object} options - {audio: Blob, video: Blob, gif: Blob} + * @method + * @memberof RecordRTC + * @example + * RecordRTC.writeToDisk({ + * audio: audioBlob, + * video: videoBlob, + * gif : gifBlob + * }); + */ +RecordRTC.writeToDisk = function(options) { + console.log('Writing recorded blob(s) to disk!'); + options = options || {}; + if (options.audio && options.video && options.gif) { + options.audio.getDataURL(function(audioDataURL) { + options.video.getDataURL(function(videoDataURL) { + options.gif.getDataURL(function(gifDataURL) { + DiskStorage.Store({ + audioBlob: audioDataURL, + videoBlob: videoDataURL, + gifBlob: gifDataURL + }); + }); + }); + }); + } else if (options.audio && options.video) { + options.audio.getDataURL(function(audioDataURL) { + options.video.getDataURL(function(videoDataURL) { + DiskStorage.Store({ + audioBlob: audioDataURL, + videoBlob: videoDataURL + }); + }); + }); + } else if (options.audio && options.gif) { + options.audio.getDataURL(function(audioDataURL) { + options.gif.getDataURL(function(gifDataURL) { + DiskStorage.Store({ + audioBlob: audioDataURL, + gifBlob: gifDataURL + }); + }); + }); + } else if (options.video && options.gif) { + options.video.getDataURL(function(videoDataURL) { + options.gif.getDataURL(function(gifDataURL) { + DiskStorage.Store({ + videoBlob: videoDataURL, + gifBlob: gifDataURL + }); + }); + }); + } else if (options.audio) { + options.audio.getDataURL(function(audioDataURL) { + DiskStorage.Store({ + audioBlob: audioDataURL + }); + }); + } else if (options.video) { + options.video.getDataURL(function(videoDataURL) { + DiskStorage.Store({ + videoBlob: videoDataURL + }); + }); + } else if (options.gif) { + options.gif.getDataURL(function(gifDataURL) { + DiskStorage.Store({ + gifBlob: gifDataURL + }); + }); + } +}; + +// __________________________ +// RecordRTC-Configuration.js + +/** + * {@link RecordRTCConfiguration} is an inner/private helper for {@link RecordRTC}. + * @summary It configures the 2nd parameter passed over {@link RecordRTC} and returns a valid "config" object. + * @license {@link https://github.com/muaz-khan/RecordRTC/blob/master/LICENSE|MIT} + * @author {@link https://MuazKhan.com|Muaz Khan} + * @typedef RecordRTCConfiguration + * @class + * @example + * var options = RecordRTCConfiguration(mediaStream, options); + * @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 - {type:"video", disableLogs: true, numberOfAudioChannels: 1, bufferSize: 0, sampleRate: 0, video: HTMLVideoElement, getNativeBlob:true, etc.} + */ + +function RecordRTCConfiguration(mediaStream, config) { + if (!config.recorderType && !config.type) { + if (!!config.audio && !!config.video) { + config.type = 'video'; + } else if (!!config.audio && !config.video) { + config.type = 'audio'; + } + } + + if (config.recorderType && !config.type) { + if (config.recorderType === WhammyRecorder || config.recorderType === CanvasRecorder || (typeof WebAssemblyRecorder !== 'undefined' && config.recorderType === WebAssemblyRecorder)) { + config.type = 'video'; + } else if (config.recorderType === GifRecorder) { + config.type = 'gif'; + } else if (config.recorderType === StereoAudioRecorder) { + config.type = 'audio'; + } else if (config.recorderType === MediaStreamRecorder) { + if (getTracks(mediaStream, 'audio').length && getTracks(mediaStream, 'video').length) { + config.type = 'video'; + } else if (!getTracks(mediaStream, 'audio').length && getTracks(mediaStream, 'video').length) { + config.type = 'video'; + } else if (getTracks(mediaStream, 'audio').length && !getTracks(mediaStream, 'video').length) { + config.type = 'audio'; + } else { + // config.type = 'UnKnown'; + } + } + } + + if (typeof MediaStreamRecorder !== 'undefined' && typeof MediaRecorder !== 'undefined' && 'requestData' in MediaRecorder.prototype) { + if (!config.mimeType) { + config.mimeType = 'video/webm'; + } + + if (!config.type) { + config.type = config.mimeType.split('/')[0]; + } + + if (!config.bitsPerSecond) { + // config.bitsPerSecond = 128000; + } + } + + // consider default type=audio + if (!config.type) { + if (config.mimeType) { + config.type = config.mimeType.split('/')[0]; + } + if (!config.type) { + config.type = 'audio'; + } + } + + return config; +} + +// __________________ +// GetRecorderType.js + +/** + * {@link GetRecorderType} is an inner/private helper for {@link RecordRTC}. + * @summary It returns best recorder-type available for your browser. + * @license {@link https://github.com/muaz-khan/RecordRTC/blob/master/LICENSE|MIT} + * @author {@link https://MuazKhan.com|Muaz Khan} + * @typedef GetRecorderType + * @class + * @example + * var RecorderType = GetRecorderType(options); + * var recorder = new RecorderType(options); + * @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 - {type:"video", disableLogs: true, numberOfAudioChannels: 1, bufferSize: 0, sampleRate: 0, video: HTMLVideoElement, etc.} + */ + +function GetRecorderType(mediaStream, config) { + var recorder; + + // StereoAudioRecorder can work with all three: Edge, Firefox and Chrome + // todo: detect if it is Edge, then auto use: StereoAudioRecorder + if (isChrome || isEdge || isOpera) { + // Media Stream Recording API has not been implemented in chrome yet; + // That's why using WebAudio API to record stereo audio in WAV format + recorder = StereoAudioRecorder; + } + + if (typeof MediaRecorder !== 'undefined' && 'requestData' in MediaRecorder.prototype && !isChrome) { + recorder = MediaStreamRecorder; + } + + // video recorder (in WebM format) + if (config.type === 'video' && (isChrome || isOpera)) { + recorder = WhammyRecorder; + + if (typeof WebAssemblyRecorder !== 'undefined' && typeof ReadableStream !== 'undefined') { + recorder = WebAssemblyRecorder; + } + } + + // video recorder (in Gif format) + if (config.type === 'gif') { + recorder = GifRecorder; + } + + // html2canvas recording! + if (config.type === 'canvas') { + recorder = CanvasRecorder; + } + + if (isMediaRecorderCompatible() && recorder !== CanvasRecorder && recorder !== GifRecorder && typeof MediaRecorder !== 'undefined' && 'requestData' in MediaRecorder.prototype) { + if (getTracks(mediaStream, 'video').length || getTracks(mediaStream, 'audio').length) { + // audio-only recording + if (config.type === 'audio') { + if (typeof MediaRecorder.isTypeSupported === 'function' && MediaRecorder.isTypeSupported('audio/webm')) { + recorder = MediaStreamRecorder; + } + // else recorder = StereoAudioRecorder; + } else { + // video or screen tracks + if (typeof MediaRecorder.isTypeSupported === 'function' && MediaRecorder.isTypeSupported('video/webm')) { + recorder = MediaStreamRecorder; + } + } + } + } + + if (mediaStream instanceof Array && mediaStream.length) { + recorder = MultiStreamRecorder; + } + + if (config.recorderType) { + recorder = config.recorderType; + } + + if (!config.disableLogs && !!recorder && !!recorder.name) { + console.log('Using recorderType:', recorder.name || recorder.constructor.name); + } + + if (!recorder && isSafari) { + recorder = MediaStreamRecorder; + } + + return recorder; +} + +// _____________ +// MRecordRTC.js + +/** + * MRecordRTC runs on top of {@link RecordRTC} to bring multiple recordings in a single place, by providing simple API. + * @summary MRecordRTC stands for "Multiple-RecordRTC". + * @license {@link https://github.com/muaz-khan/RecordRTC/blob/master/LICENSE|MIT} + * @author {@link https://MuazKhan.com|Muaz Khan} + * @typedef MRecordRTC + * @class + * @example + * var recorder = new MRecordRTC(); + * recorder.addStream(MediaStream); + * recorder.mediaType = { + * audio: true, // or StereoAudioRecorder or MediaStreamRecorder + * video: true, // or WhammyRecorder or MediaStreamRecorder or WebAssemblyRecorder or CanvasRecorder + * gif: true // or GifRecorder + * }; + * // mimeType is optional and should be set only in advance cases. + * recorder.mimeType = { + * audio: 'audio/wav', + * video: 'video/webm', + * gif: 'image/gif' + * }; + * recorder.startRecording(); + * @see For further information: + * @see {@link https://github.com/muaz-khan/RecordRTC/tree/master/MRecordRTC|MRecordRTC Source Code} + * @param {MediaStream} mediaStream - MediaStream object fetched using getUserMedia API or generated using captureStreamUntilEnded or WebAudio API. + * @requires {@link RecordRTC} + */ + +function MRecordRTC(mediaStream) { + + /** + * This method attaches MediaStream object to {@link MRecordRTC}. + * @param {MediaStream} mediaStream - A MediaStream object, either fetched using getUserMedia API, or generated using captureStreamUntilEnded or WebAudio API. + * @method + * @memberof MRecordRTC + * @example + * recorder.addStream(MediaStream); + */ + this.addStream = function(_mediaStream) { + if (_mediaStream) { + mediaStream = _mediaStream; + } + }; + + /** + * This property can be used to set the recording type e.g. audio, or video, or gif, or canvas. + * @property {object} mediaType - {audio: true, video: true, gif: true} + * @memberof MRecordRTC + * @example + * var recorder = new MRecordRTC(); + * recorder.mediaType = { + * audio: true, // TRUE or StereoAudioRecorder or MediaStreamRecorder + * video: true, // TRUE or WhammyRecorder or MediaStreamRecorder or WebAssemblyRecorder or CanvasRecorder + * gif : true // TRUE or GifRecorder + * }; + */ + this.mediaType = { + audio: true, + video: true + }; + + /** + * This method starts recording. + * @method + * @memberof MRecordRTC + * @example + * recorder.startRecording(); + */ + this.startRecording = function() { + var mediaType = this.mediaType; + var recorderType; + var mimeType = this.mimeType || { + audio: null, + video: null, + gif: null + }; + + if (typeof mediaType.audio !== 'function' && isMediaRecorderCompatible() && !getTracks(mediaStream, 'audio').length) { + mediaType.audio = false; + } + + if (typeof mediaType.video !== 'function' && isMediaRecorderCompatible() && !getTracks(mediaStream, 'video').length) { + mediaType.video = false; + } + + if (typeof mediaType.gif !== 'function' && isMediaRecorderCompatible() && !getTracks(mediaStream, 'video').length) { + mediaType.gif = false; + } + + if (!mediaType.audio && !mediaType.video && !mediaType.gif) { + throw 'MediaStream must have either audio or video tracks.'; + } + + if (!!mediaType.audio) { + recorderType = null; + if (typeof mediaType.audio === 'function') { + recorderType = mediaType.audio; + } + + this.audioRecorder = new RecordRTC(mediaStream, { + type: 'audio', + bufferSize: this.bufferSize, + sampleRate: this.sampleRate, + numberOfAudioChannels: this.numberOfAudioChannels || 2, + disableLogs: this.disableLogs, + recorderType: recorderType, + mimeType: mimeType.audio, + timeSlice: this.timeSlice, + onTimeStamp: this.onTimeStamp + }); + + if (!mediaType.video) { + this.audioRecorder.startRecording(); + } + } + + if (!!mediaType.video) { + recorderType = null; + if (typeof mediaType.video === 'function') { + recorderType = mediaType.video; + } + + var newStream = mediaStream; + + if (isMediaRecorderCompatible() && !!mediaType.audio && typeof mediaType.audio === 'function') { + var videoTrack = getTracks(mediaStream, 'video')[0]; + + if (isFirefox) { + newStream = new MediaStream(); + newStream.addTrack(videoTrack); + + if (recorderType && recorderType === WhammyRecorder) { + // Firefox does NOT supports webp-encoding yet + // But Firefox do supports WebAssemblyRecorder + recorderType = MediaStreamRecorder; + } + } else { + newStream = new MediaStream(); + newStream.addTrack(videoTrack); + } + } + + this.videoRecorder = new RecordRTC(newStream, { + type: 'video', + video: this.video, + canvas: this.canvas, + frameInterval: this.frameInterval || 10, + disableLogs: this.disableLogs, + recorderType: recorderType, + mimeType: mimeType.video, + timeSlice: this.timeSlice, + onTimeStamp: this.onTimeStamp, + workerPath: this.workerPath, + webAssemblyPath: this.webAssemblyPath, + frameRate: this.frameRate, // used by WebAssemblyRecorder; values: usually 30; accepts any. + bitrate: this.bitrate // used by WebAssemblyRecorder; values: 0 to 1000+ + }); + + if (!mediaType.audio) { + this.videoRecorder.startRecording(); + } + } + + if (!!mediaType.audio && !!mediaType.video) { + var self = this; + + var isSingleRecorder = isMediaRecorderCompatible() === true; + + if (mediaType.audio instanceof StereoAudioRecorder && !!mediaType.video) { + isSingleRecorder = false; + } else if (mediaType.audio !== true && mediaType.video !== true && mediaType.audio !== mediaType.video) { + isSingleRecorder = false; + } + + if (isSingleRecorder === true) { + self.audioRecorder = null; + self.videoRecorder.startRecording(); + } else { + self.videoRecorder.initRecorder(function() { + self.audioRecorder.initRecorder(function() { + // Both recorders are ready to record things accurately + self.videoRecorder.startRecording(); + self.audioRecorder.startRecording(); + }); + }); + } + } + + if (!!mediaType.gif) { + recorderType = null; + if (typeof mediaType.gif === 'function') { + recorderType = mediaType.gif; + } + this.gifRecorder = new RecordRTC(mediaStream, { + type: 'gif', + frameRate: this.frameRate || 200, + quality: this.quality || 10, + disableLogs: this.disableLogs, + recorderType: recorderType, + mimeType: mimeType.gif + }); + this.gifRecorder.startRecording(); + } + }; + + /** + * This method stops recording. + * @param {function} callback - Callback function is invoked when all encoders finished their jobs. + * @method + * @memberof MRecordRTC + * @example + * recorder.stopRecording(function(recording){ + * var audioBlob = recording.audio; + * var videoBlob = recording.video; + * var gifBlob = recording.gif; + * }); + */ + this.stopRecording = function(callback) { + callback = callback || function() {}; + + if (this.audioRecorder) { + this.audioRecorder.stopRecording(function(blobURL) { + callback(blobURL, 'audio'); + }); + } + + if (this.videoRecorder) { + this.videoRecorder.stopRecording(function(blobURL) { + callback(blobURL, 'video'); + }); + } + + if (this.gifRecorder) { + this.gifRecorder.stopRecording(function(blobURL) { + callback(blobURL, 'gif'); + }); + } + }; + + /** + * This method pauses recording. + * @method + * @memberof MRecordRTC + * @example + * recorder.pauseRecording(); + */ + this.pauseRecording = function() { + if (this.audioRecorder) { + this.audioRecorder.pauseRecording(); + } + + if (this.videoRecorder) { + this.videoRecorder.pauseRecording(); + } + + if (this.gifRecorder) { + this.gifRecorder.pauseRecording(); + } + }; + + /** + * This method resumes recording. + * @method + * @memberof MRecordRTC + * @example + * recorder.resumeRecording(); + */ + this.resumeRecording = function() { + if (this.audioRecorder) { + this.audioRecorder.resumeRecording(); + } + + if (this.videoRecorder) { + this.videoRecorder.resumeRecording(); + } + + if (this.gifRecorder) { + this.gifRecorder.resumeRecording(); + } + }; + + /** + * This method can be used to manually get all recorded blobs. + * @param {function} callback - All recorded blobs are passed back to the "callback" function. + * @method + * @memberof MRecordRTC + * @example + * recorder.getBlob(function(recording){ + * var audioBlob = recording.audio; + * var videoBlob = recording.video; + * var gifBlob = recording.gif; + * }); + * // or + * var audioBlob = recorder.getBlob().audio; + * var videoBlob = recorder.getBlob().video; + */ + this.getBlob = function(callback) { + var output = {}; + + if (this.audioRecorder) { + output.audio = this.audioRecorder.getBlob(); + } + + if (this.videoRecorder) { + output.video = this.videoRecorder.getBlob(); + } + + if (this.gifRecorder) { + output.gif = this.gifRecorder.getBlob(); + } + + if (callback) { + callback(output); + } + + return output; + }; + + /** + * Destroy all recorder instances. + * @method + * @memberof MRecordRTC + * @example + * recorder.destroy(); + */ + this.destroy = function() { + if (this.audioRecorder) { + this.audioRecorder.destroy(); + this.audioRecorder = null; + } + + if (this.videoRecorder) { + this.videoRecorder.destroy(); + this.videoRecorder = null; + } + + if (this.gifRecorder) { + this.gifRecorder.destroy(); + this.gifRecorder = null; + } + }; + + /** + * This method can be used to manually get all recorded blobs' DataURLs. + * @param {function} callback - All recorded blobs' DataURLs are passed back to the "callback" function. + * @method + * @memberof MRecordRTC + * @example + * recorder.getDataURL(function(recording){ + * var audioDataURL = recording.audio; + * var videoDataURL = recording.video; + * var gifDataURL = recording.gif; + * }); + */ + this.getDataURL = function(callback) { + this.getBlob(function(blob) { + if (blob.audio && blob.video) { + getDataURL(blob.audio, function(_audioDataURL) { + getDataURL(blob.video, function(_videoDataURL) { + callback({ + audio: _audioDataURL, + video: _videoDataURL + }); + }); + }); + } else if (blob.audio) { + getDataURL(blob.audio, function(_audioDataURL) { + callback({ + audio: _audioDataURL + }); + }); + } else if (blob.video) { + getDataURL(blob.video, function(_videoDataURL) { + callback({ + video: _videoDataURL + }); + }); + } + }); + + function getDataURL(blob, callback00) { + if (typeof Worker !== 'undefined') { + var webWorker = processInWebWorker(function readFile(_blob) { + postMessage(new FileReaderSync().readAsDataURL(_blob)); + }); + + webWorker.onmessage = function(event) { + callback00(event.data); + }; + + webWorker.postMessage(blob); + } else { + var reader = new FileReader(); + reader.readAsDataURL(blob); + reader.onload = function(event) { + callback00(event.target.result); + }; + } + } + + function processInWebWorker(_function) { + var blob = URL.createObjectURL(new Blob([_function.toString(), + 'this.onmessage = function (eee) {' + _function.name + '(eee.data);}' + ], { + type: 'application/javascript' + })); + + var worker = new Worker(blob); + var url; + if (typeof URL !== 'undefined') { + url = URL; + } else if (typeof webkitURL !== 'undefined') { + url = webkitURL; + } else { + throw 'Neither URL nor webkitURL detected.'; + } + url.revokeObjectURL(blob); + return worker; + } + }; + + /** + * This method can be used to ask {@link MRecordRTC} to write all recorded blobs into IndexedDB storage. + * @method + * @memberof MRecordRTC + * @example + * recorder.writeToDisk(); + */ + this.writeToDisk = function() { + RecordRTC.writeToDisk({ + audio: this.audioRecorder, + video: this.videoRecorder, + gif: this.gifRecorder + }); + }; + + /** + * This method can be used to invoke a save-as dialog for all recorded blobs. + * @param {object} args - {audio: 'audio-name', video: 'video-name', gif: 'gif-name'} + * @method + * @memberof MRecordRTC + * @example + * recorder.save({ + * audio: 'audio-file-name', + * video: 'video-file-name', + * gif : 'gif-file-name' + * }); + */ + this.save = function(args) { + args = args || { + audio: true, + video: true, + gif: true + }; + + if (!!args.audio && this.audioRecorder) { + this.audioRecorder.save(typeof args.audio === 'string' ? args.audio : ''); + } + + if (!!args.video && this.videoRecorder) { + this.videoRecorder.save(typeof args.video === 'string' ? args.video : ''); + } + if (!!args.gif && this.gifRecorder) { + this.gifRecorder.save(typeof args.gif === 'string' ? args.gif : ''); + } + }; +} + +/** + * This method can be used to get all recorded blobs from IndexedDB storage. + * @param {string} type - 'all' or 'audio' or 'video' or 'gif' + * @param {function} callback - Callback function to get all stored blobs. + * @method + * @memberof MRecordRTC + * @example + * MRecordRTC.getFromDisk('all', function(dataURL, type){ + * if(type === 'audio') { } + * if(type === 'video') { } + * if(type === 'gif') { } + * }); + */ +MRecordRTC.getFromDisk = RecordRTC.getFromDisk; + +/** + * This method can be used to store recorded blobs into IndexedDB storage. + * @param {object} options - {audio: Blob, video: Blob, gif: Blob} + * @method + * @memberof MRecordRTC + * @example + * MRecordRTC.writeToDisk({ + * audio: audioBlob, + * video: videoBlob, + * gif : gifBlob + * }); + */ +MRecordRTC.writeToDisk = RecordRTC.writeToDisk; + +if (typeof RecordRTC !== 'undefined') { + RecordRTC.MRecordRTC = MRecordRTC; +} + +var browserFakeUserAgent = 'Fake/5.0 (FakeOS) AppleWebKit/123 (KHTML, like Gecko) Fake/12.3.4567.89 Fake/123.45'; + +(function(that) { + if (!that) { + return; + } + + if (typeof window !== 'undefined') { + return; + } + + if (typeof global === 'undefined') { + return; + } + + global.navigator = { + userAgent: browserFakeUserAgent, + getUserMedia: function() {} + }; + + if (!global.console) { + global.console = {}; + } + + if (typeof global.console.log === 'undefined' || typeof global.console.error === 'undefined') { + global.console.error = global.console.log = global.console.log || function() { + console.log(arguments); + }; + } + + if (typeof document === 'undefined') { + /*global document:true */ + that.document = { + documentElement: { + appendChild: function() { + return ''; + } + } + }; + + document.createElement = document.captureStream = document.mozCaptureStream = function() { + var obj = { + getContext: function() { + return obj; + }, + play: function() {}, + pause: function() {}, + drawImage: function() {}, + toDataURL: function() { + return ''; + }, + style: {} + }; + return obj; + }; + + that.HTMLVideoElement = function() {}; + } + + if (typeof location === 'undefined') { + /*global location:true */ + that.location = { + protocol: 'file:', + href: '', + hash: '' + }; + } + + if (typeof screen === 'undefined') { + /*global screen:true */ + that.screen = { + width: 0, + height: 0 + }; + } + + if (typeof URL === 'undefined') { + /*global screen:true */ + that.URL = { + createObjectURL: function() { + return ''; + }, + revokeObjectURL: function() { + return ''; + } + }; + } + + /*global window:true */ + that.window = global; +})(typeof global !== 'undefined' ? global : null); + +// _____________________________ +// Cross-Browser-Declarations.js + +// animation-frame used in WebM recording + +/*jshint -W079 */ +var requestAnimationFrame = window.requestAnimationFrame; +if (typeof requestAnimationFrame === 'undefined') { + if (typeof webkitRequestAnimationFrame !== 'undefined') { + /*global requestAnimationFrame:true */ + requestAnimationFrame = webkitRequestAnimationFrame; + } else if (typeof mozRequestAnimationFrame !== 'undefined') { + /*global requestAnimationFrame:true */ + requestAnimationFrame = mozRequestAnimationFrame; + } else if (typeof msRequestAnimationFrame !== 'undefined') { + /*global requestAnimationFrame:true */ + requestAnimationFrame = msRequestAnimationFrame; + } else if (typeof requestAnimationFrame === 'undefined') { + // via: https://gist.github.com/paulirish/1579671 + var lastTime = 0; + + /*global requestAnimationFrame:true */ + requestAnimationFrame = function(callback, element) { + var currTime = new Date().getTime(); + var timeToCall = Math.max(0, 16 - (currTime - lastTime)); + var id = setTimeout(function() { + callback(currTime + timeToCall); + }, timeToCall); + lastTime = currTime + timeToCall; + return id; + }; + } +} + +/*jshint -W079 */ +var cancelAnimationFrame = window.cancelAnimationFrame; +if (typeof cancelAnimationFrame === 'undefined') { + if (typeof webkitCancelAnimationFrame !== 'undefined') { + /*global cancelAnimationFrame:true */ + cancelAnimationFrame = webkitCancelAnimationFrame; + } else if (typeof mozCancelAnimationFrame !== 'undefined') { + /*global cancelAnimationFrame:true */ + cancelAnimationFrame = mozCancelAnimationFrame; + } else if (typeof msCancelAnimationFrame !== 'undefined') { + /*global cancelAnimationFrame:true */ + cancelAnimationFrame = msCancelAnimationFrame; + } else if (typeof cancelAnimationFrame === 'undefined') { + /*global cancelAnimationFrame:true */ + cancelAnimationFrame = function(id) { + clearTimeout(id); + }; + } +} + +// WebAudio API representer +var AudioContext = window.AudioContext; + +if (typeof AudioContext === 'undefined') { + if (typeof webkitAudioContext !== 'undefined') { + /*global AudioContext:true */ + AudioContext = webkitAudioContext; + } + + if (typeof mozAudioContext !== 'undefined') { + /*global AudioContext:true */ + AudioContext = mozAudioContext; + } +} + +/*jshint -W079 */ +var URL = window.URL; + +if (typeof URL === 'undefined' && typeof webkitURL !== 'undefined') { + /*global URL:true */ + URL = webkitURL; +} + +if (typeof navigator !== 'undefined' && typeof navigator.getUserMedia === 'undefined') { // maybe window.navigator? + if (typeof navigator.webkitGetUserMedia !== 'undefined') { + navigator.getUserMedia = navigator.webkitGetUserMedia; + } + + if (typeof navigator.mozGetUserMedia !== 'undefined') { + navigator.getUserMedia = navigator.mozGetUserMedia; + } +} + +var isEdge = navigator.userAgent.indexOf('Edge') !== -1 && (!!navigator.msSaveBlob || !!navigator.msSaveOrOpenBlob); +var isOpera = !!window.opera || navigator.userAgent.indexOf('OPR/') !== -1; +var isFirefox = navigator.userAgent.toLowerCase().indexOf('firefox') > -1 && ('netscape' in window) && / rv:/.test(navigator.userAgent); +var isChrome = (!isOpera && !isEdge && !!navigator.webkitGetUserMedia) || isElectron() || navigator.userAgent.toLowerCase().indexOf('chrome/') !== -1; + +var isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent); + +if (isSafari && !isChrome && navigator.userAgent.indexOf('CriOS') !== -1) { + isSafari = false; + isChrome = true; +} + +var MediaStream = window.MediaStream; + +if (typeof MediaStream === 'undefined' && typeof webkitMediaStream !== 'undefined') { + MediaStream = webkitMediaStream; +} + +/*global MediaStream:true */ +if (typeof MediaStream !== 'undefined') { + // override "stop" method for all browsers + if (typeof MediaStream.prototype.stop === 'undefined') { + MediaStream.prototype.stop = function() { + this.getTracks().forEach(function(track) { + track.stop(); + }); + }; + } +} + +// below function via: http://goo.gl/B3ae8c +/** + * Return human-readable file size. + * @param {number} bytes - Pass bytes and get formatted string. + * @returns {string} - formatted string + * @example + * bytesToSize(1024*1024*5) === '5 GB' + * @see {@link https://github.com/muaz-khan/RecordRTC|RecordRTC Source Code} + */ +function bytesToSize(bytes) { + var k = 1000; + var sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']; + if (bytes === 0) { + return '0 Bytes'; + } + var i = parseInt(Math.floor(Math.log(bytes) / Math.log(k)), 10); + return (bytes / Math.pow(k, i)).toPrecision(3) + ' ' + sizes[i]; +} + +/** + * @param {Blob} file - File or Blob object. This parameter is required. + * @param {string} fileName - Optional file name e.g. "Recorded-Video.webm" + * @example + * invokeSaveAsDialog(blob or file, [optional] fileName); + * @see {@link https://github.com/muaz-khan/RecordRTC|RecordRTC Source Code} + */ +function invokeSaveAsDialog(file, fileName) { + if (!file) { + throw 'Blob object is required.'; + } + + if (!file.type) { + try { + file.type = 'video/webm'; + } catch (e) {} + } + + var fileExtension = (file.type || 'video/webm').split('/')[1]; + if (fileExtension.indexOf(';') !== -1) { + // extended mimetype, e.g. 'video/webm;codecs=vp8,opus' + fileExtension = fileExtension.split(';')[0]; + } + if (fileName && fileName.indexOf('.') !== -1) { + var splitted = fileName.split('.'); + fileName = splitted[0]; + fileExtension = splitted[1]; + } + + var fileFullName = (fileName || (Math.round(Math.random() * 9999999999) + 888888888)) + '.' + fileExtension; + + if (typeof navigator.msSaveOrOpenBlob !== 'undefined') { + return navigator.msSaveOrOpenBlob(file, fileFullName); + } else if (typeof navigator.msSaveBlob !== 'undefined') { + return navigator.msSaveBlob(file, fileFullName); + } + + var hyperlink = document.createElement('a'); + hyperlink.href = URL.createObjectURL(file); + hyperlink.download = fileFullName; + + hyperlink.style = 'display:none;opacity:0;color:transparent;'; + (document.body || document.documentElement).appendChild(hyperlink); + + if (typeof hyperlink.click === 'function') { + hyperlink.click(); + } else { + hyperlink.target = '_blank'; + hyperlink.dispatchEvent(new MouseEvent('click', { + view: window, + bubbles: true, + cancelable: true + })); + } + + URL.revokeObjectURL(hyperlink.href); +} + +/** + * from: https://github.com/cheton/is-electron/blob/master/index.js + **/ +function isElectron() { + // Renderer process + if (typeof window !== 'undefined' && typeof window.process === 'object' && window.process.type === 'renderer') { + return true; + } + + // Main process + if (typeof process !== 'undefined' && typeof process.versions === 'object' && !!process.versions.electron) { + return true; + } + + // Detect the user agent when the `nodeIntegration` option is set to true + if (typeof navigator === 'object' && typeof navigator.userAgent === 'string' && navigator.userAgent.indexOf('Electron') >= 0) { + return true; + } + + return false; +} + +function getTracks(stream, kind) { + if (!stream || !stream.getTracks) { + return []; + } + + return stream.getTracks().filter(function(t) { + return t.kind === (kind || 'audio'); + }); +} + +function setSrcObject(stream, element) { + if ('srcObject' in element) { + element.srcObject = stream; + } else if ('mozSrcObject' in element) { + element.mozSrcObject = stream; + } else { + element.srcObject = stream; + } +} + +/** + * @param {Blob} file - File or Blob object. + * @param {function} callback - Callback function. + * @example + * getSeekableBlob(blob or file, callback); + * @see {@link https://github.com/muaz-khan/RecordRTC|RecordRTC Source Code} + */ +function getSeekableBlob(inputBlob, callback) { + // EBML.js copyrights goes to: https://github.com/legokichi/ts-ebml + if (typeof EBML === 'undefined') { + throw new Error('Please link: https://www.webrtc-experiment.com/EBML.js'); + } + + var reader = new EBML.Reader(); + var decoder = new EBML.Decoder(); + var tools = EBML.tools; + + var fileReader = new FileReader(); + fileReader.onload = function(e) { + var ebmlElms = decoder.decode(this.result); + ebmlElms.forEach(function(element) { + reader.read(element); + }); + reader.stop(); + var refinedMetadataBuf = tools.makeMetadataSeekable(reader.metadatas, reader.duration, reader.cues); + var body = this.result.slice(reader.metadataSize); + var newBlob = new Blob([refinedMetadataBuf, body], { + type: 'video/webm' + }); + + callback(newBlob); + }; + fileReader.readAsArrayBuffer(inputBlob); +} + +if (typeof RecordRTC !== 'undefined') { + RecordRTC.invokeSaveAsDialog = invokeSaveAsDialog; + RecordRTC.getTracks = getTracks; + RecordRTC.getSeekableBlob = getSeekableBlob; + RecordRTC.bytesToSize = bytesToSize; + RecordRTC.isElectron = isElectron; +} + +// __________ (used to handle stuff like http://goo.gl/xmE5eg) issue #129 +// Storage.js + +/** + * Storage is a standalone object used by {@link RecordRTC} to store reusable objects e.g. "new AudioContext". + * @license {@link https://github.com/muaz-khan/RecordRTC/blob/master/LICENSE|MIT} + * @author {@link https://MuazKhan.com|Muaz Khan} + * @example + * Storage.AudioContext === webkitAudioContext + * @property {webkitAudioContext} AudioContext - Keeps a reference to AudioContext object. + * @see {@link https://github.com/muaz-khan/RecordRTC|RecordRTC Source Code} + */ + +var Storage = {}; + +if (typeof AudioContext !== 'undefined') { + Storage.AudioContext = AudioContext; +} else if (typeof webkitAudioContext !== 'undefined') { + Storage.AudioContext = webkitAudioContext; +} + +if (typeof RecordRTC !== 'undefined') { + RecordRTC.Storage = Storage; +} + +function isMediaRecorderCompatible() { + if (isFirefox || isSafari || isEdge) { + return true; + } + + var nVer = navigator.appVersion; + var nAgt = navigator.userAgent; + var fullVersion = '' + parseFloat(navigator.appVersion); + var majorVersion = parseInt(navigator.appVersion, 10); + var nameOffset, verOffset, ix; + + if (isChrome || isOpera) { + verOffset = nAgt.indexOf('Chrome'); + fullVersion = nAgt.substring(verOffset + 7); + } + + // trim the fullVersion string at semicolon/space if present + if ((ix = fullVersion.indexOf(';')) !== -1) { + fullVersion = fullVersion.substring(0, ix); + } + + if ((ix = fullVersion.indexOf(' ')) !== -1) { + fullVersion = fullVersion.substring(0, ix); + } + + majorVersion = parseInt('' + fullVersion, 10); + + if (isNaN(majorVersion)) { + fullVersion = '' + parseFloat(navigator.appVersion); + majorVersion = parseInt(navigator.appVersion, 10); + } + + return majorVersion >= 49; +} + +// ______________________ +// MediaStreamRecorder.js + +/** + * MediaStreamRecorder is an abstraction layer for {@link https://w3c.github.io/mediacapture-record/MediaRecorder.html|MediaRecorder API}. It is used by {@link RecordRTC} to record MediaStream(s) in both Chrome and Firefox. + * @summary Runs top over {@link https://w3c.github.io/mediacapture-record/MediaRecorder.html|MediaRecorder API}. + * @license {@link https://github.com/muaz-khan/RecordRTC/blob/master/LICENSE|MIT} + * @author {@link https://github.com/muaz-khan|Muaz Khan} + * @typedef MediaStreamRecorder + * @class + * @example + * var config = { + * mimeType: 'video/webm', // vp8, vp9, h264, mkv, opus/vorbis + * audioBitsPerSecond : 256 * 8 * 1024, + * videoBitsPerSecond : 256 * 8 * 1024, + * bitsPerSecond: 256 * 8 * 1024, // if this is provided, skip above two + * checkForInactiveTracks: true, + * timeSlice: 1000, // concatenate intervals based blobs + * ondataavailable: function() {} // get intervals based blobs + * } + * var recorder = new MediaStreamRecorder(mediaStream, config); + * 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", timeSlice: 1000} + * @throws Will throw an error if first argument "MediaStream" is missing. Also throws error if "MediaRecorder API" are not supported by the browser. + */ + +function MediaStreamRecorder(mediaStream, config) { + var self = this; + + if (typeof mediaStream === 'undefined') { + throw 'First argument "MediaStream" is required.'; + } + + if (typeof MediaRecorder === 'undefined') { + throw 'Your browser does not support the Media Recorder API. Please try other modules e.g. WhammyRecorder or StereoAudioRecorder.'; + } + + config = config || { + // bitsPerSecond: 256 * 8 * 1024, + mimeType: 'video/webm' + }; + + if (config.type === 'audio') { + if (getTracks(mediaStream, 'video').length && getTracks(mediaStream, 'audio').length) { + var stream; + if (!!navigator.mozGetUserMedia) { + stream = new MediaStream(); + stream.addTrack(getTracks(mediaStream, 'audio')[0]); + } else { + // webkitMediaStream + stream = new MediaStream(getTracks(mediaStream, 'audio')); + } + mediaStream = stream; + } + + if (!config.mimeType || config.mimeType.toString().toLowerCase().indexOf('audio') === -1) { + config.mimeType = isChrome ? 'audio/webm' : 'audio/ogg'; + } + + if (config.mimeType && config.mimeType.toString().toLowerCase() !== 'audio/ogg' && !!navigator.mozGetUserMedia) { + // forcing better codecs on Firefox (via #166) + config.mimeType = 'audio/ogg'; + } + } + + var arrayOfBlobs = []; + + /** + * This method returns array of blobs. Use only with "timeSlice". Its useful to preview recording anytime, without using the "stop" method. + * @method + * @memberof MediaStreamRecorder + * @example + * var arrayOfBlobs = recorder.getArrayOfBlobs(); + * @returns {Array} Returns array of recorded blobs. + */ + this.getArrayOfBlobs = function() { + return arrayOfBlobs; + }; + + /** + * This method records MediaStream. + * @method + * @memberof MediaStreamRecorder + * @example + * recorder.record(); + */ + this.record = function() { + // set defaults + self.blob = null; + self.clearRecordedData(); + self.timestamps = []; + allStates = []; + arrayOfBlobs = []; + + 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'; + } + + if (typeof MediaRecorder.isTypeSupported === 'function' && recorderHints.mimeType) { + if (!MediaRecorder.isTypeSupported(recorderHints.mimeType)) { + if (!config.disableLogs) { + console.warn('MediaRecorder API seems unable to record mimeType:', recorderHints.mimeType); + } + + recorderHints.mimeType = config.type === 'audio' ? 'audio/webm' : 'video/webm'; + } + } + + // using MediaRecorder API here + try { + mediaRecorder = new MediaRecorder(mediaStream, recorderHints); + + // reset + config.mimeType = recorderHints.mimeType; + } catch (e) { + // chrome-based fallback + mediaRecorder = new MediaRecorder(mediaStream); + } + + // old hack? + if (recorderHints.mimeType && !MediaRecorder.isTypeSupported && 'canRecordMimeType' in mediaRecorder && mediaRecorder.canRecordMimeType(recorderHints.mimeType) === false) { + if (!config.disableLogs) { + console.warn('MediaRecorder API seems unable to record mimeType:', recorderHints.mimeType); + } + } + + // Dispatching OnDataAvailable Handler + mediaRecorder.ondataavailable = function(e) { + if (e.data) { + allStates.push('ondataavailable: ' + bytesToSize(e.data.size)); + } + + if (typeof config.timeSlice === 'number') { + if (e.data && e.data.size) { + arrayOfBlobs.push(e.data); + updateTimeStamp(); + + if (typeof config.ondataavailable === 'function') { + // intervals based blobs + var blob = config.getNativeBlob ? e.data : new Blob([e.data], { + type: getMimeType(recorderHints) + }); + config.ondataavailable(blob); + } + } + return; + } + + if (!e.data || !e.data.size || e.data.size < 100 || self.blob) { + // make sure that stopRecording always getting fired + // even if there is invalid data + if (self.recordingCallback) { + self.recordingCallback(new Blob([], { + type: getMimeType(recorderHints) + })); + self.recordingCallback = null; + } + return; + } + + self.blob = config.getNativeBlob ? e.data : new Blob([e.data], { + type: getMimeType(recorderHints) + }); + + if (self.recordingCallback) { + self.recordingCallback(self.blob); + self.recordingCallback = null; + } + }; + + mediaRecorder.onstart = function() { + allStates.push('started'); + }; + + mediaRecorder.onpause = function() { + allStates.push('paused'); + }; + + mediaRecorder.onresume = function() { + allStates.push('resumed'); + }; + + mediaRecorder.onstop = function() { + allStates.push('stopped'); + }; + + mediaRecorder.onerror = function(error) { + if (!error) { + return; + } + + if (!error.name) { + error.name = 'UnknownError'; + } + + allStates.push('error: ' + error); + + if (!config.disableLogs) { + // via: https://w3c.github.io/mediacapture-record/MediaRecorder.html#exception-summary + if (error.name.toString().toLowerCase().indexOf('invalidstate') !== -1) { + console.error('The MediaRecorder is not in a state in which the proposed operation is allowed to be executed.', error); + } else if (error.name.toString().toLowerCase().indexOf('notsupported') !== -1) { + console.error('MIME type (', recorderHints.mimeType, ') is not supported.', error); + } else if (error.name.toString().toLowerCase().indexOf('security') !== -1) { + console.error('MediaRecorder security error', error); + } + + // older code below + 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.', error); + } 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.', error); + } 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.', error); + } 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); + } + } + + (function(looper) { + if (!self.manuallyStopped && mediaRecorder && mediaRecorder.state === 'inactive') { + delete config.timeslice; + + // 10 minutes, enough? + mediaRecorder.start(10 * 60 * 1000); + return; + } + + setTimeout(looper, 1000); + })(); + + if (mediaRecorder.state !== 'inactive' && mediaRecorder.state !== 'stopped') { + mediaRecorder.stop(); + } + }; + + if (typeof config.timeSlice === 'number') { + updateTimeStamp(); + mediaRecorder.start(config.timeSlice); + } else { + // default is 24 hours; enough? (thanks https://github.com/slidevjs/slidev/pull/488) + // use config => {timeSlice: 1000} otherwise + + mediaRecorder.start(24 * 60 * 60 * 1000); + } + + if (config.initCallback) { + config.initCallback(); // old code + } + }; + + /** + * @property {Array} timestamps - Array of time stamps + * @memberof MediaStreamRecorder + * @example + * console.log(recorder.timestamps); + */ + this.timestamps = []; + + function updateTimeStamp() { + self.timestamps.push(new Date().getTime()); + + if (typeof config.onTimeStamp === 'function') { + config.onTimeStamp(self.timestamps[self.timestamps.length - 1], self.timestamps); + } + } + + function getMimeType(secondObject) { + if (mediaRecorder && mediaRecorder.mimeType) { + return mediaRecorder.mimeType; + } + + return secondObject.mimeType || 'video/webm'; + } + + /** + * 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) { + callback = callback || function() {}; + + self.manuallyStopped = true; // used inside the mediaRecorder.onerror + + if (!mediaRecorder) { + return; + } + + this.recordingCallback = callback; + + if (mediaRecorder.state === 'recording') { + mediaRecorder.stop(); + } + + if (typeof config.timeSlice === 'number') { + setTimeout(function() { + self.blob = new Blob(arrayOfBlobs, { + type: getMimeType(config) + }); + + self.recordingCallback(self.blob); + }, 100); + } + }; + + /** + * 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 (!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 && mediaRecorder.state === 'recording') { + self.stop(clearRecordedDataCB); + } + + clearRecordedDataCB(); + }; + + function clearRecordedDataCB() { + arrayOfBlobs = []; + mediaRecorder = null; + self.timestamps = []; + } + + // Reference to "MediaRecorder" object + var mediaRecorder; + + /** + * Access to native MediaRecorder API + * @method + * @memberof MediaStreamRecorder + * @instance + * @example + * var internal = recorder.getInternalRecorder(); + * internal.ondataavailable = function() {}; // override + * internal.stream, internal.onpause, internal.onstop, etc. + * @returns {Object} Returns internal recording object. + */ + this.getInternalRecorder = function() { + return 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; + } + + /** + * @property {Blob} blob - Recorded data as "Blob" object. + * @memberof MediaStreamRecorder + * @example + * recorder.stop(function() { + * var blob = recorder.blob; + * }); + */ + this.blob = null; + + + /** + * Get MediaRecorder readonly state. + * @method + * @memberof MediaStreamRecorder + * @example + * var state = recorder.getState(); + * @returns {String} Returns recording state. + */ + this.getState = function() { + if (!mediaRecorder) { + return 'inactive'; + } + + return mediaRecorder.state || 'inactive'; + }; + + // list of all recording states + var allStates = []; + + /** + * Get MediaRecorder all recording states. + * @method + * @memberof MediaStreamRecorder + * @example + * var state = recorder.getAllStates(); + * @returns {Array} Returns all recording states + */ + this.getAllStates = function() { + return allStates; + }; + + // if any Track within the MediaStream is muted or not enabled at any time, + // the browser will only record black frames + // or silence since that is the content produced by the Track + // so we need to stopRecording as soon as any single track ends. + if (typeof config.checkForInactiveTracks === 'undefined') { + config.checkForInactiveTracks = false; // disable to minimize CPU usage + } + + var self = this; + + // this method checks if media stream is stopped + // or if any track is ended. + (function looper() { + if (!mediaRecorder || config.checkForInactiveTracks === false) { + return; + } + + if (isMediaStreamActive() === false) { + if (!config.disableLogs) { + console.log('MediaStream seems stopped.'); + } + self.stop(); + return; + } + + setTimeout(looper, 1000); // check every second + })(); + + // for debugging + this.name = 'MediaStreamRecorder'; + this.toString = function() { + return this.name; + }; +} + +if (typeof RecordRTC !== 'undefined') { + RecordRTC.MediaStreamRecorder = MediaStreamRecorder; +} + +// source code from: http://typedarray.org/wp-content/projects/WebAudioRecorder/script.js +// https://github.com/mattdiamond/Recorderjs#license-mit +// ______________________ +// StereoAudioRecorder.js + +/** + * StereoAudioRecorder is a standalone class used by {@link RecordRTC} to bring "stereo" audio-recording in chrome. + * @summary JavaScript standalone object for stereo audio recording. + * @license {@link https://github.com/muaz-khan/RecordRTC/blob/master/LICENSE|MIT} + * @author {@link https://MuazKhan.com|Muaz Khan} + * @typedef StereoAudioRecorder + * @class + * @example + * var recorder = new StereoAudioRecorder(MediaStream, { + * sampleRate: 44100, + * bufferSize: 4096 + * }); + * 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 - {sampleRate: 44100, bufferSize: 4096, numberOfAudioChannels: 1, etc.} + */ + +function StereoAudioRecorder(mediaStream, config) { + if (!getTracks(mediaStream, 'audio').length) { + throw 'Your stream has no audio tracks.'; + } + + config = config || {}; + + var self = this; + + // variables + var leftchannel = []; + var rightchannel = []; + var recording = false; + var recordingLength = 0; + var jsAudioNode; + + var numberOfAudioChannels = 2; + + /** + * Set sample rates such as 8K or 16K. Reference: http://stackoverflow.com/a/28977136/552182 + * @property {number} desiredSampRate - Desired Bits per sample * 1000 + * @memberof StereoAudioRecorder + * @instance + * @example + * var recorder = StereoAudioRecorder(mediaStream, { + * desiredSampRate: 16 * 1000 // bits-per-sample * 1000 + * }); + */ + var desiredSampRate = config.desiredSampRate; + + // backward compatibility + if (config.leftChannel === true) { + numberOfAudioChannels = 1; + } + + if (config.numberOfAudioChannels === 1) { + numberOfAudioChannels = 1; + } + + if (!numberOfAudioChannels || numberOfAudioChannels < 1) { + numberOfAudioChannels = 2; + } + + if (!config.disableLogs) { + console.log('StereoAudioRecorder is set to record number of channels: ' + numberOfAudioChannels); + } + + // if any Track within the MediaStream is muted or not enabled at any time, + // the browser will only record black frames + // or silence since that is the content produced by the Track + // so we need to stopRecording as soon as any single track ends. + if (typeof config.checkForInactiveTracks === 'undefined') { + config.checkForInactiveTracks = true; + } + + function isMediaStreamActive() { + if (config.checkForInactiveTracks === false) { + // always return "true" + return true; + } + + if ('active' in mediaStream) { + if (!mediaStream.active) { + return false; + } + } else if ('ended' in mediaStream) { // old hack + if (mediaStream.ended) { + return false; + } + } + return true; + } + + /** + * This method records MediaStream. + * @method + * @memberof StereoAudioRecorder + * @example + * recorder.record(); + */ + this.record = function() { + if (isMediaStreamActive() === false) { + throw 'Please make sure MediaStream is active.'; + } + + resetVariables(); + + isAudioProcessStarted = isPaused = false; + recording = true; + + if (typeof config.timeSlice !== 'undefined') { + looper(); + } + }; + + function mergeLeftRightBuffers(config, callback) { + function mergeAudioBuffers(config, cb) { + var numberOfAudioChannels = config.numberOfAudioChannels; + + // todo: "slice(0)" --- is it causes loop? Should be removed? + var leftBuffers = config.leftBuffers.slice(0); + var rightBuffers = config.rightBuffers.slice(0); + var sampleRate = config.sampleRate; + var internalInterleavedLength = config.internalInterleavedLength; + var desiredSampRate = config.desiredSampRate; + + if (numberOfAudioChannels === 2) { + leftBuffers = mergeBuffers(leftBuffers, internalInterleavedLength); + rightBuffers = mergeBuffers(rightBuffers, internalInterleavedLength); + + if (desiredSampRate) { + leftBuffers = interpolateArray(leftBuffers, desiredSampRate, sampleRate); + rightBuffers = interpolateArray(rightBuffers, desiredSampRate, sampleRate); + } + } + + if (numberOfAudioChannels === 1) { + leftBuffers = mergeBuffers(leftBuffers, internalInterleavedLength); + + if (desiredSampRate) { + leftBuffers = interpolateArray(leftBuffers, desiredSampRate, sampleRate); + } + } + + // set sample rate as desired sample rate + if (desiredSampRate) { + sampleRate = desiredSampRate; + } + + // for changing the sampling rate, reference: + // http://stackoverflow.com/a/28977136/552182 + function interpolateArray(data, newSampleRate, oldSampleRate) { + var fitCount = Math.round(data.length * (newSampleRate / oldSampleRate)); + var newData = []; + var springFactor = Number((data.length - 1) / (fitCount - 1)); + newData[0] = data[0]; + for (var i = 1; i < fitCount - 1; i++) { + var tmp = i * springFactor; + var before = Number(Math.floor(tmp)).toFixed(); + var after = Number(Math.ceil(tmp)).toFixed(); + var atPoint = tmp - before; + newData[i] = linearInterpolate(data[before], data[after], atPoint); + } + newData[fitCount - 1] = data[data.length - 1]; + return newData; + } + + function linearInterpolate(before, after, atPoint) { + return before + (after - before) * atPoint; + } + + function mergeBuffers(channelBuffer, rLength) { + var result = new Float64Array(rLength); + var offset = 0; + var lng = channelBuffer.length; + + for (var i = 0; i < lng; i++) { + var buffer = channelBuffer[i]; + result.set(buffer, offset); + offset += buffer.length; + } + + return result; + } + + function interleave(leftChannel, rightChannel) { + var length = leftChannel.length + rightChannel.length; + + var result = new Float64Array(length); + + var inputIndex = 0; + + for (var index = 0; index < length;) { + result[index++] = leftChannel[inputIndex]; + result[index++] = rightChannel[inputIndex]; + inputIndex++; + } + return result; + } + + function writeUTFBytes(view, offset, string) { + var lng = string.length; + for (var i = 0; i < lng; i++) { + view.setUint8(offset + i, string.charCodeAt(i)); + } + } + + // interleave both channels together + var interleaved; + + if (numberOfAudioChannels === 2) { + interleaved = interleave(leftBuffers, rightBuffers); + } + + if (numberOfAudioChannels === 1) { + interleaved = leftBuffers; + } + + var interleavedLength = interleaved.length; + + // create wav file + var resultingBufferLength = 44 + interleavedLength * 2; + + var buffer = new ArrayBuffer(resultingBufferLength); + + var view = new DataView(buffer); + + // RIFF chunk descriptor/identifier + writeUTFBytes(view, 0, 'RIFF'); + + // RIFF chunk length + // changed "44" to "36" via #401 + view.setUint32(4, 36 + interleavedLength * 2, true); + + // RIFF type + writeUTFBytes(view, 8, 'WAVE'); + + // format chunk identifier + // FMT sub-chunk + writeUTFBytes(view, 12, 'fmt '); + + // format chunk length + view.setUint32(16, 16, true); + + // sample format (raw) + view.setUint16(20, 1, true); + + // stereo (2 channels) + view.setUint16(22, numberOfAudioChannels, true); + + // sample rate + view.setUint32(24, sampleRate, true); + + // byte rate (sample rate * block align) + view.setUint32(28, sampleRate * numberOfAudioChannels * 2, true); + + // block align (channel count * bytes per sample) + view.setUint16(32, numberOfAudioChannels * 2, true); + + // bits per sample + view.setUint16(34, 16, true); + + // data sub-chunk + // data chunk identifier + writeUTFBytes(view, 36, 'data'); + + // data chunk length + view.setUint32(40, interleavedLength * 2, true); + + // write the PCM samples + var lng = interleavedLength; + var index = 44; + var volume = 1; + for (var i = 0; i < lng; i++) { + view.setInt16(index, interleaved[i] * (0x7FFF * volume), true); + index += 2; + } + + if (cb) { + return cb({ + buffer: buffer, + view: view + }); + } + + postMessage({ + buffer: buffer, + view: view + }); + } + + if (config.noWorker) { + mergeAudioBuffers(config, function(data) { + callback(data.buffer, data.view); + }); + return; + } + + + var webWorker = processInWebWorker(mergeAudioBuffers); + + webWorker.onmessage = function(event) { + callback(event.data.buffer, event.data.view); + + // release memory + URL.revokeObjectURL(webWorker.workerURL); + + // kill webworker (or Chrome will kill your page after ~25 calls) + webWorker.terminate(); + }; + + webWorker.postMessage(config); + } + + function processInWebWorker(_function) { + var workerURL = URL.createObjectURL(new Blob([_function.toString(), + ';this.onmessage = function (eee) {' + _function.name + '(eee.data);}' + ], { + type: 'application/javascript' + })); + + var worker = new Worker(workerURL); + worker.workerURL = workerURL; + return worker; + } + + /** + * This method stops recording MediaStream. + * @param {function} callback - Callback function, that is used to pass recorded blob back to the callee. + * @method + * @memberof StereoAudioRecorder + * @example + * recorder.stop(function(blob) { + * video.src = URL.createObjectURL(blob); + * }); + */ + this.stop = function(callback) { + callback = callback || function() {}; + + // stop recording + recording = false; + + mergeLeftRightBuffers({ + desiredSampRate: desiredSampRate, + sampleRate: sampleRate, + numberOfAudioChannels: numberOfAudioChannels, + internalInterleavedLength: recordingLength, + leftBuffers: leftchannel, + rightBuffers: numberOfAudioChannels === 1 ? [] : rightchannel, + noWorker: config.noWorker + }, function(buffer, view) { + /** + * @property {Blob} blob - The recorded blob object. + * @memberof StereoAudioRecorder + * @example + * recorder.stop(function(){ + * var blob = recorder.blob; + * }); + */ + self.blob = new Blob([view], { + type: 'audio/wav' + }); + + /** + * @property {ArrayBuffer} buffer - The recorded buffer object. + * @memberof StereoAudioRecorder + * @example + * recorder.stop(function(){ + * var buffer = recorder.buffer; + * }); + */ + self.buffer = new ArrayBuffer(view.buffer.byteLength); + + /** + * @property {DataView} view - The recorded data-view object. + * @memberof StereoAudioRecorder + * @example + * recorder.stop(function(){ + * var view = recorder.view; + * }); + */ + self.view = view; + + self.sampleRate = desiredSampRate || sampleRate; + self.bufferSize = bufferSize; + + // recorded audio length + self.length = recordingLength; + + isAudioProcessStarted = false; + + if (callback) { + callback(self.blob); + } + }); + }; + + if (typeof RecordRTC.Storage === 'undefined') { + RecordRTC.Storage = { + AudioContextConstructor: null, + AudioContext: window.AudioContext || window.webkitAudioContext + }; + } + + if (!RecordRTC.Storage.AudioContextConstructor || RecordRTC.Storage.AudioContextConstructor.state === 'closed') { + RecordRTC.Storage.AudioContextConstructor = new RecordRTC.Storage.AudioContext(); + } + + var context = RecordRTC.Storage.AudioContextConstructor; + + // creates an audio node from the microphone incoming stream + var audioInput = context.createMediaStreamSource(mediaStream); + + var legalBufferValues = [0, 256, 512, 1024, 2048, 4096, 8192, 16384]; + + /** + * From the spec: This value controls how frequently the audioprocess event is + * dispatched and how many sample-frames need to be processed each call. + * Lower values for buffer size will result in a lower (better) latency. + * Higher values will be necessary to avoid audio breakup and glitches + * The size of the buffer (in sample-frames) which needs to + * be processed each time onprocessaudio is called. + * Legal values are (256, 512, 1024, 2048, 4096, 8192, 16384). + * @property {number} bufferSize - Buffer-size for how frequently the audioprocess event is dispatched. + * @memberof StereoAudioRecorder + * @example + * recorder = new StereoAudioRecorder(mediaStream, { + * bufferSize: 4096 + * }); + */ + + // "0" means, let chrome decide the most accurate buffer-size for current platform. + var bufferSize = typeof config.bufferSize === 'undefined' ? 4096 : config.bufferSize; + + if (legalBufferValues.indexOf(bufferSize) === -1) { + if (!config.disableLogs) { + console.log('Legal values for buffer-size are ' + JSON.stringify(legalBufferValues, null, '\t')); + } + } + + if (context.createJavaScriptNode) { + jsAudioNode = context.createJavaScriptNode(bufferSize, numberOfAudioChannels, numberOfAudioChannels); + } else if (context.createScriptProcessor) { + jsAudioNode = context.createScriptProcessor(bufferSize, numberOfAudioChannels, numberOfAudioChannels); + } else { + throw 'WebAudio API has no support on this browser.'; + } + + // connect the stream to the script processor + audioInput.connect(jsAudioNode); + + if (!config.bufferSize) { + bufferSize = jsAudioNode.bufferSize; // device buffer-size + } + + /** + * The sample rate (in sample-frames per second) at which the + * AudioContext handles audio. It is assumed that all AudioNodes + * in the context run at this rate. In making this assumption, + * sample-rate converters or "varispeed" processors are not supported + * in real-time processing. + * The sampleRate parameter describes the sample-rate of the + * linear PCM audio data in the buffer in sample-frames per second. + * An implementation must support sample-rates in at least + * the range 22050 to 96000. + * @property {number} sampleRate - Buffer-size for how frequently the audioprocess event is dispatched. + * @memberof StereoAudioRecorder + * @example + * recorder = new StereoAudioRecorder(mediaStream, { + * sampleRate: 44100 + * }); + */ + var sampleRate = typeof config.sampleRate !== 'undefined' ? config.sampleRate : context.sampleRate || 44100; + + if (sampleRate < 22050 || sampleRate > 96000) { + // Ref: http://stackoverflow.com/a/26303918/552182 + if (!config.disableLogs) { + console.log('sample-rate must be under range 22050 and 96000.'); + } + } + + if (!config.disableLogs) { + if (config.desiredSampRate) { + console.log('Desired sample-rate: ' + config.desiredSampRate); + } + } + + var isPaused = false; + /** + * This method pauses the recording process. + * @method + * @memberof StereoAudioRecorder + * @example + * recorder.pause(); + */ + this.pause = function() { + isPaused = true; + }; + + /** + * This method resumes the recording process. + * @method + * @memberof StereoAudioRecorder + * @example + * recorder.resume(); + */ + this.resume = function() { + if (isMediaStreamActive() === false) { + throw 'Please make sure MediaStream is active.'; + } + + if (!recording) { + if (!config.disableLogs) { + console.log('Seems recording has been restarted.'); + } + this.record(); + return; + } + + isPaused = false; + }; + + /** + * This method resets currently recorded data. + * @method + * @memberof StereoAudioRecorder + * @example + * recorder.clearRecordedData(); + */ + this.clearRecordedData = function() { + config.checkForInactiveTracks = false; + + if (recording) { + this.stop(clearRecordedDataCB); + } + + clearRecordedDataCB(); + }; + + function resetVariables() { + leftchannel = []; + rightchannel = []; + recordingLength = 0; + isAudioProcessStarted = false; + recording = false; + isPaused = false; + context = null; + + self.leftchannel = leftchannel; + self.rightchannel = rightchannel; + self.numberOfAudioChannels = numberOfAudioChannels; + self.desiredSampRate = desiredSampRate; + self.sampleRate = sampleRate; + self.recordingLength = recordingLength; + + intervalsBasedBuffers = { + left: [], + right: [], + recordingLength: 0 + }; + } + + function clearRecordedDataCB() { + if (jsAudioNode) { + jsAudioNode.onaudioprocess = null; + jsAudioNode.disconnect(); + jsAudioNode = null; + } + + if (audioInput) { + audioInput.disconnect(); + audioInput = null; + } + + resetVariables(); + } + + // for debugging + this.name = 'StereoAudioRecorder'; + this.toString = function() { + return this.name; + }; + + var isAudioProcessStarted = false; + + function onAudioProcessDataAvailable(e) { + if (isPaused) { + return; + } + + if (isMediaStreamActive() === false) { + if (!config.disableLogs) { + console.log('MediaStream seems stopped.'); + } + jsAudioNode.disconnect(); + recording = false; + } + + if (!recording) { + if (audioInput) { + audioInput.disconnect(); + audioInput = null; + } + return; + } + + /** + * This method is called on "onaudioprocess" event's first invocation. + * @method {function} onAudioProcessStarted + * @memberof StereoAudioRecorder + * @example + * recorder.onAudioProcessStarted: function() { }; + */ + if (!isAudioProcessStarted) { + isAudioProcessStarted = true; + if (config.onAudioProcessStarted) { + config.onAudioProcessStarted(); + } + + if (config.initCallback) { + config.initCallback(); + } + } + + var left = e.inputBuffer.getChannelData(0); + + // we clone the samples + var chLeft = new Float32Array(left); + leftchannel.push(chLeft); + + if (numberOfAudioChannels === 2) { + var right = e.inputBuffer.getChannelData(1); + var chRight = new Float32Array(right); + rightchannel.push(chRight); + } + + recordingLength += bufferSize; + + // export raw PCM + self.recordingLength = recordingLength; + + if (typeof config.timeSlice !== 'undefined') { + intervalsBasedBuffers.recordingLength += bufferSize; + intervalsBasedBuffers.left.push(chLeft); + + if (numberOfAudioChannels === 2) { + intervalsBasedBuffers.right.push(chRight); + } + } + } + + jsAudioNode.onaudioprocess = onAudioProcessDataAvailable; + + // to prevent self audio to be connected with speakers + if (context.createMediaStreamDestination) { + jsAudioNode.connect(context.createMediaStreamDestination()); + } else { + jsAudioNode.connect(context.destination); + } + + // export raw PCM + this.leftchannel = leftchannel; + this.rightchannel = rightchannel; + this.numberOfAudioChannels = numberOfAudioChannels; + this.desiredSampRate = desiredSampRate; + this.sampleRate = sampleRate; + self.recordingLength = recordingLength; + + // helper for intervals based blobs + var intervalsBasedBuffers = { + left: [], + right: [], + recordingLength: 0 + }; + + // this looper is used to support intervals based blobs (via timeSlice+ondataavailable) + function looper() { + if (!recording || typeof config.ondataavailable !== 'function' || typeof config.timeSlice === 'undefined') { + return; + } + + if (intervalsBasedBuffers.left.length) { + mergeLeftRightBuffers({ + desiredSampRate: desiredSampRate, + sampleRate: sampleRate, + numberOfAudioChannels: numberOfAudioChannels, + internalInterleavedLength: intervalsBasedBuffers.recordingLength, + leftBuffers: intervalsBasedBuffers.left, + rightBuffers: numberOfAudioChannels === 1 ? [] : intervalsBasedBuffers.right + }, function(buffer, view) { + var blob = new Blob([view], { + type: 'audio/wav' + }); + config.ondataavailable(blob); + + setTimeout(looper, config.timeSlice); + }); + + intervalsBasedBuffers = { + left: [], + right: [], + recordingLength: 0 + }; + } else { + setTimeout(looper, config.timeSlice); + } + } +} + +if (typeof RecordRTC !== 'undefined') { + RecordRTC.StereoAudioRecorder = StereoAudioRecorder; +} + +// _________________ +// CanvasRecorder.js + +/** + * CanvasRecorder is a standalone class used by {@link RecordRTC} to bring HTML5-Canvas recording into video WebM. It uses HTML2Canvas library and runs top over {@link Whammy}. + * @summary HTML2Canvas recording into video WebM. + * @license {@link https://github.com/muaz-khan/RecordRTC/blob/master/LICENSE|MIT} + * @author {@link https://MuazKhan.com|Muaz Khan} + * @typedef CanvasRecorder + * @class + * @example + * var recorder = new CanvasRecorder(htmlElement, { disableLogs: true, useWhammyRecorder: true }); + * recorder.record(); + * recorder.stop(function(blob) { + * video.src = URL.createObjectURL(blob); + * }); + * @see {@link https://github.com/muaz-khan/RecordRTC|RecordRTC Source Code} + * @param {HTMLElement} htmlElement - querySelector/getElementById/getElementsByTagName[0]/etc. + * @param {object} config - {disableLogs:true, initCallback: function} + */ + +function CanvasRecorder(htmlElement, config) { + if (typeof html2canvas === 'undefined') { + throw 'Please link: https://www.webrtc-experiment.com/screenshot.js'; + } + + config = config || {}; + if (!config.frameInterval) { + config.frameInterval = 10; + } + + // via DetectRTC.js + var isCanvasSupportsStreamCapturing = false; + ['captureStream', 'mozCaptureStream', 'webkitCaptureStream'].forEach(function(item) { + if (item in document.createElement('canvas')) { + isCanvasSupportsStreamCapturing = true; + } + }); + + var _isChrome = (!!window.webkitRTCPeerConnection || !!window.webkitGetUserMedia) && !!window.chrome; + + var chromeVersion = 50; + var matchArray = navigator.userAgent.match(/Chrom(e|ium)\/([0-9]+)\./); + if (_isChrome && matchArray && matchArray[2]) { + chromeVersion = parseInt(matchArray[2], 10); + } + + if (_isChrome && chromeVersion < 52) { + isCanvasSupportsStreamCapturing = false; + } + + if (config.useWhammyRecorder) { + isCanvasSupportsStreamCapturing = false; + } + + var globalCanvas, mediaStreamRecorder; + + if (isCanvasSupportsStreamCapturing) { + if (!config.disableLogs) { + console.log('Your browser supports both MediRecorder API and canvas.captureStream!'); + } + + if (htmlElement instanceof HTMLCanvasElement) { + globalCanvas = htmlElement; + } else if (htmlElement instanceof CanvasRenderingContext2D) { + globalCanvas = htmlElement.canvas; + } else { + throw 'Please pass either HTMLCanvasElement or CanvasRenderingContext2D.'; + } + } else if (!!navigator.mozGetUserMedia) { + if (!config.disableLogs) { + console.error('Canvas recording is NOT supported in Firefox.'); + } + } + + var isRecording; + + /** + * This method records Canvas. + * @method + * @memberof CanvasRecorder + * @example + * recorder.record(); + */ + this.record = function() { + isRecording = true; + + if (isCanvasSupportsStreamCapturing && !config.useWhammyRecorder) { + // CanvasCaptureMediaStream + var canvasMediaStream; + if ('captureStream' in globalCanvas) { + canvasMediaStream = globalCanvas.captureStream(25); // 25 FPS + } else if ('mozCaptureStream' in globalCanvas) { + canvasMediaStream = globalCanvas.mozCaptureStream(25); + } else if ('webkitCaptureStream' in globalCanvas) { + canvasMediaStream = globalCanvas.webkitCaptureStream(25); + } + + try { + var mdStream = new MediaStream(); + mdStream.addTrack(getTracks(canvasMediaStream, 'video')[0]); + canvasMediaStream = mdStream; + } catch (e) {} + + if (!canvasMediaStream) { + throw 'captureStream API are NOT available.'; + } + + // Note: Jan 18, 2016 status is that, + // Firefox MediaRecorder API can't record CanvasCaptureMediaStream object. + mediaStreamRecorder = new MediaStreamRecorder(canvasMediaStream, { + mimeType: config.mimeType || 'video/webm' + }); + mediaStreamRecorder.record(); + } else { + whammy.frames = []; + lastTime = new Date().getTime(); + drawCanvasFrame(); + } + + if (config.initCallback) { + config.initCallback(); + } + }; + + this.getWebPImages = function(callback) { + if (htmlElement.nodeName.toLowerCase() !== 'canvas') { + callback(); + return; + } + + var framesLength = whammy.frames.length; + whammy.frames.forEach(function(frame, idx) { + var framesRemaining = framesLength - idx; + if (!config.disableLogs) { + console.log(framesRemaining + '/' + framesLength + ' frames remaining'); + } + + if (config.onEncodingCallback) { + config.onEncodingCallback(framesRemaining, framesLength); + } + + var webp = frame.image.toDataURL('image/webp', 1); + whammy.frames[idx].image = webp; + }); + + if (!config.disableLogs) { + console.log('Generating WebM'); + } + + callback(); + }; + + /** + * This method stops recording Canvas. + * @param {function} callback - Callback function, that is used to pass recorded blob back to the callee. + * @method + * @memberof CanvasRecorder + * @example + * recorder.stop(function(blob) { + * video.src = URL.createObjectURL(blob); + * }); + */ + this.stop = function(callback) { + isRecording = false; + + var that = this; + + if (isCanvasSupportsStreamCapturing && mediaStreamRecorder) { + mediaStreamRecorder.stop(callback); + return; + } + + this.getWebPImages(function() { + /** + * @property {Blob} blob - Recorded frames in video/webm blob. + * @memberof CanvasRecorder + * @example + * recorder.stop(function() { + * var blob = recorder.blob; + * }); + */ + whammy.compile(function(blob) { + if (!config.disableLogs) { + console.log('Recording finished!'); + } + + that.blob = blob; + + if (that.blob.forEach) { + that.blob = new Blob([], { + type: 'video/webm' + }); + } + + if (callback) { + callback(that.blob); + } + + whammy.frames = []; + }); + }); + }; + + var isPausedRecording = false; + + /** + * This method pauses the recording process. + * @method + * @memberof CanvasRecorder + * @example + * recorder.pause(); + */ + this.pause = function() { + isPausedRecording = true; + + if (mediaStreamRecorder instanceof MediaStreamRecorder) { + mediaStreamRecorder.pause(); + return; + } + }; + + /** + * This method resumes the recording process. + * @method + * @memberof CanvasRecorder + * @example + * recorder.resume(); + */ + this.resume = function() { + isPausedRecording = false; + + if (mediaStreamRecorder instanceof MediaStreamRecorder) { + mediaStreamRecorder.resume(); + return; + } + + if (!isRecording) { + this.record(); + } + }; + + /** + * This method resets currently recorded data. + * @method + * @memberof CanvasRecorder + * @example + * recorder.clearRecordedData(); + */ + this.clearRecordedData = function() { + if (isRecording) { + this.stop(clearRecordedDataCB); + } + clearRecordedDataCB(); + }; + + function clearRecordedDataCB() { + whammy.frames = []; + isRecording = false; + isPausedRecording = false; + } + + // for debugging + this.name = 'CanvasRecorder'; + this.toString = function() { + return this.name; + }; + + function cloneCanvas() { + //create a new canvas + var newCanvas = document.createElement('canvas'); + var context = newCanvas.getContext('2d'); + + //set dimensions + newCanvas.width = htmlElement.width; + newCanvas.height = htmlElement.height; + + //apply the old canvas to the new one + context.drawImage(htmlElement, 0, 0); + + //return the new canvas + return newCanvas; + } + + function drawCanvasFrame() { + if (isPausedRecording) { + lastTime = new Date().getTime(); + return setTimeout(drawCanvasFrame, 500); + } + + if (htmlElement.nodeName.toLowerCase() === 'canvas') { + var duration = new Date().getTime() - lastTime; + // via #206, by Jack i.e. @Seymourr + lastTime = new Date().getTime(); + + whammy.frames.push({ + image: cloneCanvas(), + duration: duration + }); + + if (isRecording) { + setTimeout(drawCanvasFrame, config.frameInterval); + } + return; + } + + html2canvas(htmlElement, { + grabMouse: typeof config.showMousePointer === 'undefined' || config.showMousePointer, + onrendered: function(canvas) { + var duration = new Date().getTime() - lastTime; + if (!duration) { + return setTimeout(drawCanvasFrame, config.frameInterval); + } + + // via #206, by Jack i.e. @Seymourr + lastTime = new Date().getTime(); + + whammy.frames.push({ + image: canvas.toDataURL('image/webp', 1), + duration: duration + }); + + if (isRecording) { + setTimeout(drawCanvasFrame, config.frameInterval); + } + } + }); + } + + var lastTime = new Date().getTime(); + + var whammy = new Whammy.Video(100); +} + +if (typeof RecordRTC !== 'undefined') { + RecordRTC.CanvasRecorder = CanvasRecorder; +} + +// _________________ +// 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/blob/master/LICENSE|MIT} + * @author {@link https://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 || 320; + canvas.height = config.canvas.height || 240; + + 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'); + + setSrcObject(mediaStream, video); + + 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; + + (function loop() { + i++; + if (i === length) { + o.callback(); + return; + } + + // "setTimeout" added by Jim McLeod + setTimeout(function() { + o.functionToLoop(loop, i); + }, 1); + })(); + } + + + /** + * 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) { + callback = callback || function() {}; + + 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() { + if (!isStopDrawing) { + this.stop(clearRecordedDataCB); + } + clearRecordedDataCB(); + }; + + function clearRecordedDataCB() { + whammy.frames = []; + isStopDrawing = true; + isPausedRecording = false; + } + + // for debugging + this.name = 'WhammyRecorder'; + this.toString = function() { + return this.name; + }; + + var canvas = document.createElement('canvas'); + var context = canvas.getContext('2d'); + + var video; + var lastTime; + var whammy; +} + +if (typeof RecordRTC !== 'undefined') { + RecordRTC.WhammyRecorder = WhammyRecorder; +} + +// https://github.com/antimatter15/whammy/blob/master/LICENSE +// _________ +// Whammy.js + +// todo: Firefox now supports webp for webm containers! +// their MediaRecorder implementation works well! +// should we provide an option to record via Whammy.js or MediaRecorder API is a better solution? + +/** + * Whammy is a standalone class used by {@link RecordRTC} to bring video recording in Chrome. It is written by {@link https://github.com/antimatter15|antimatter15} + * @summary A real time javascript webm encoder based on a canvas hack. + * @license {@link https://github.com/muaz-khan/RecordRTC/blob/master/LICENSE|MIT} + * @author {@link https://MuazKhan.com|Muaz Khan} + * @typedef Whammy + * @class + * @example + * var recorder = new Whammy().Video(15); + * recorder.add(context || canvas || dataURL); + * var output = recorder.compile(); + * @see {@link https://github.com/muaz-khan/RecordRTC|RecordRTC Source Code} + */ + +var Whammy = (function() { + // a more abstract-ish API + + function WhammyVideo(duration) { + this.frames = []; + this.duration = duration || 1; + this.quality = 0.8; + } + + /** + * Pass Canvas or Context or image/webp(string) to {@link Whammy} encoder. + * @method + * @memberof Whammy + * @example + * recorder = new Whammy().Video(0.8, 100); + * recorder.add(canvas || context || 'image/webp'); + * @param {string} frame - Canvas || Context || image/webp + * @param {number} duration - Stick a duration (in milliseconds) + */ + WhammyVideo.prototype.add = function(frame, duration) { + if ('canvas' in frame) { //CanvasRenderingContext2D + frame = frame.canvas; + } + + if ('toDataURL' in frame) { + frame = frame.toDataURL('image/webp', this.quality); + } + + if (!(/^data:image\/webp;base64,/ig).test(frame)) { + throw 'Input must be formatted properly as a base64 encoded DataURI of type image/webp'; + } + this.frames.push({ + image: frame, + duration: duration || this.duration + }); + }; + + function processInWebWorker(_function) { + var blob = URL.createObjectURL(new Blob([_function.toString(), + 'this.onmessage = function (eee) {' + _function.name + '(eee.data);}' + ], { + type: 'application/javascript' + })); + + var worker = new Worker(blob); + URL.revokeObjectURL(blob); + return worker; + } + + function whammyInWebWorker(frames) { + function ArrayToWebM(frames) { + var info = checkFrames(frames); + if (!info) { + return []; + } + + var clusterMaxDuration = 30000; + + var EBML = [{ + 'id': 0x1a45dfa3, // EBML + 'data': [{ + 'data': 1, + 'id': 0x4286 // EBMLVersion + }, { + 'data': 1, + 'id': 0x42f7 // EBMLReadVersion + }, { + 'data': 4, + 'id': 0x42f2 // EBMLMaxIDLength + }, { + 'data': 8, + 'id': 0x42f3 // EBMLMaxSizeLength + }, { + 'data': 'webm', + 'id': 0x4282 // DocType + }, { + 'data': 2, + 'id': 0x4287 // DocTypeVersion + }, { + 'data': 2, + 'id': 0x4285 // DocTypeReadVersion + }] + }, { + 'id': 0x18538067, // Segment + 'data': [{ + 'id': 0x1549a966, // Info + 'data': [{ + 'data': 1e6, //do things in millisecs (num of nanosecs for duration scale) + 'id': 0x2ad7b1 // TimecodeScale + }, { + 'data': 'whammy', + 'id': 0x4d80 // MuxingApp + }, { + 'data': 'whammy', + 'id': 0x5741 // WritingApp + }, { + 'data': doubleToString(info.duration), + 'id': 0x4489 // Duration + }] + }, { + 'id': 0x1654ae6b, // Tracks + 'data': [{ + 'id': 0xae, // TrackEntry + 'data': [{ + 'data': 1, + 'id': 0xd7 // TrackNumber + }, { + 'data': 1, + 'id': 0x73c5 // TrackUID + }, { + 'data': 0, + 'id': 0x9c // FlagLacing + }, { + 'data': 'und', + 'id': 0x22b59c // Language + }, { + 'data': 'V_VP8', + 'id': 0x86 // CodecID + }, { + 'data': 'VP8', + 'id': 0x258688 // CodecName + }, { + 'data': 1, + 'id': 0x83 // TrackType + }, { + 'id': 0xe0, // Video + 'data': [{ + 'data': info.width, + 'id': 0xb0 // PixelWidth + }, { + 'data': info.height, + 'id': 0xba // PixelHeight + }] + }] + }] + }] + }]; + + //Generate clusters (max duration) + var frameNumber = 0; + var clusterTimecode = 0; + while (frameNumber < frames.length) { + + var clusterFrames = []; + var clusterDuration = 0; + do { + clusterFrames.push(frames[frameNumber]); + clusterDuration += frames[frameNumber].duration; + frameNumber++; + } while (frameNumber < frames.length && clusterDuration < clusterMaxDuration); + + var clusterCounter = 0; + var cluster = { + 'id': 0x1f43b675, // Cluster + 'data': getClusterData(clusterTimecode, clusterCounter, clusterFrames) + }; //Add cluster to segment + EBML[1].data.push(cluster); + clusterTimecode += clusterDuration; + } + + return generateEBML(EBML); + } + + function getClusterData(clusterTimecode, clusterCounter, clusterFrames) { + return [{ + 'data': clusterTimecode, + 'id': 0xe7 // Timecode + }].concat(clusterFrames.map(function(webp) { + var block = makeSimpleBlock({ + discardable: 0, + frame: webp.data.slice(4), + invisible: 0, + keyframe: 1, + lacing: 0, + trackNum: 1, + timecode: Math.round(clusterCounter) + }); + clusterCounter += webp.duration; + return { + data: block, + id: 0xa3 + }; + })); + } + + // sums the lengths of all the frames and gets the duration + + function checkFrames(frames) { + if (!frames[0]) { + postMessage({ + error: 'Something went wrong. Maybe WebP format is not supported in the current browser.' + }); + return; + } + + var width = frames[0].width, + height = frames[0].height, + duration = frames[0].duration; + + for (var i = 1; i < frames.length; i++) { + duration += frames[i].duration; + } + return { + duration: duration, + width: width, + height: height + }; + } + + function numToBuffer(num) { + var parts = []; + while (num > 0) { + parts.push(num & 0xff); + num = num >> 8; + } + return new Uint8Array(parts.reverse()); + } + + function strToBuffer(str) { + return new Uint8Array(str.split('').map(function(e) { + return e.charCodeAt(0); + })); + } + + function bitsToBuffer(bits) { + var data = []; + var pad = (bits.length % 8) ? (new Array(1 + 8 - (bits.length % 8))).join('0') : ''; + bits = pad + bits; + for (var i = 0; i < bits.length; i += 8) { + data.push(parseInt(bits.substr(i, 8), 2)); + } + return new Uint8Array(data); + } + + function generateEBML(json) { + var ebml = []; + for (var i = 0; i < json.length; i++) { + var data = json[i].data; + + if (typeof data === 'object') { + data = generateEBML(data); + } + + if (typeof data === 'number') { + data = bitsToBuffer(data.toString(2)); + } + + if (typeof data === 'string') { + data = strToBuffer(data); + } + + var len = data.size || data.byteLength || data.length; + var zeroes = Math.ceil(Math.ceil(Math.log(len) / Math.log(2)) / 8); + var sizeToString = len.toString(2); + var padded = (new Array((zeroes * 7 + 7 + 1) - sizeToString.length)).join('0') + sizeToString; + var size = (new Array(zeroes)).join('0') + '1' + padded; + + ebml.push(numToBuffer(json[i].id)); + ebml.push(bitsToBuffer(size)); + ebml.push(data); + } + + return new Blob(ebml, { + type: 'video/webm' + }); + } + + function toBinStrOld(bits) { + var data = ''; + var pad = (bits.length % 8) ? (new Array(1 + 8 - (bits.length % 8))).join('0') : ''; + bits = pad + bits; + for (var i = 0; i < bits.length; i += 8) { + data += String.fromCharCode(parseInt(bits.substr(i, 8), 2)); + } + return data; + } + + function makeSimpleBlock(data) { + var flags = 0; + + if (data.keyframe) { + flags |= 128; + } + + if (data.invisible) { + flags |= 8; + } + + if (data.lacing) { + flags |= (data.lacing << 1); + } + + if (data.discardable) { + flags |= 1; + } + + if (data.trackNum > 127) { + throw 'TrackNumber > 127 not supported'; + } + + var out = [data.trackNum | 0x80, data.timecode >> 8, data.timecode & 0xff, flags].map(function(e) { + return String.fromCharCode(e); + }).join('') + data.frame; + + return out; + } + + function parseWebP(riff) { + var VP8 = riff.RIFF[0].WEBP[0]; + + var frameStart = VP8.indexOf('\x9d\x01\x2a'); // A VP8 keyframe starts with the 0x9d012a header + for (var i = 0, c = []; i < 4; i++) { + c[i] = VP8.charCodeAt(frameStart + 3 + i); + } + + var width, height, tmp; + + //the code below is literally copied verbatim from the bitstream spec + tmp = (c[1] << 8) | c[0]; + width = tmp & 0x3FFF; + tmp = (c[3] << 8) | c[2]; + height = tmp & 0x3FFF; + return { + width: width, + height: height, + data: VP8, + riff: riff + }; + } + + function getStrLength(string, offset) { + return parseInt(string.substr(offset + 4, 4).split('').map(function(i) { + var unpadded = i.charCodeAt(0).toString(2); + return (new Array(8 - unpadded.length + 1)).join('0') + unpadded; + }).join(''), 2); + } + + function parseRIFF(string) { + var offset = 0; + var chunks = {}; + + while (offset < string.length) { + var id = string.substr(offset, 4); + var len = getStrLength(string, offset); + var data = string.substr(offset + 4 + 4, len); + offset += 4 + 4 + len; + chunks[id] = chunks[id] || []; + + if (id === 'RIFF' || id === 'LIST') { + chunks[id].push(parseRIFF(data)); + } else { + chunks[id].push(data); + } + } + return chunks; + } + + function doubleToString(num) { + return [].slice.call( + new Uint8Array((new Float64Array([num])).buffer), 0).map(function(e) { + return String.fromCharCode(e); + }).reverse().join(''); + } + + var webm = new ArrayToWebM(frames.map(function(frame) { + var webp = parseWebP(parseRIFF(atob(frame.image.slice(23)))); + webp.duration = frame.duration; + return webp; + })); + + postMessage(webm); + } + + /** + * Encodes frames in WebM container. It uses WebWorkinvoke to invoke 'ArrayToWebM' method. + * @param {function} callback - Callback function, that is used to pass recorded blob back to the callee. + * @method + * @memberof Whammy + * @example + * recorder = new Whammy().Video(0.8, 100); + * recorder.compile(function(blob) { + * // blob.size - blob.type + * }); + */ + WhammyVideo.prototype.compile = function(callback) { + var webWorker = processInWebWorker(whammyInWebWorker); + + webWorker.onmessage = function(event) { + if (event.data.error) { + console.error(event.data.error); + return; + } + callback(event.data); + }; + + webWorker.postMessage(this.frames); + }; + + return { + /** + * A more abstract-ish API. + * @method + * @memberof Whammy + * @example + * recorder = new Whammy().Video(0.8, 100); + * @param {?number} speed - 0.8 + * @param {?number} quality - 100 + */ + Video: WhammyVideo + }; +})(); + +if (typeof RecordRTC !== 'undefined') { + RecordRTC.Whammy = Whammy; +} + +// ______________ (indexed-db) +// DiskStorage.js + +/** + * DiskStorage is a standalone object used by {@link RecordRTC} to store recorded blobs in IndexedDB storage. + * @summary Writing blobs into IndexedDB. + * @license {@link https://github.com/muaz-khan/RecordRTC/blob/master/LICENSE|MIT} + * @author {@link https://MuazKhan.com|Muaz Khan} + * @example + * DiskStorage.Store({ + * audioBlob: yourAudioBlob, + * videoBlob: yourVideoBlob, + * gifBlob : yourGifBlob + * }); + * DiskStorage.Fetch(function(dataURL, type) { + * if(type === 'audioBlob') { } + * if(type === 'videoBlob') { } + * if(type === 'gifBlob') { } + * }); + * // DiskStorage.dataStoreName = 'recordRTC'; + * // DiskStorage.onError = function(error) { }; + * @property {function} init - This method must be called once to initialize IndexedDB ObjectStore. Though, it is auto-used internally. + * @property {function} Fetch - This method fetches stored blobs from IndexedDB. + * @property {function} Store - This method stores blobs in IndexedDB. + * @property {function} onError - This function is invoked for any known/unknown error. + * @property {string} dataStoreName - Name of the ObjectStore created in IndexedDB storage. + * @see {@link https://github.com/muaz-khan/RecordRTC|RecordRTC Source Code} + */ + + +var DiskStorage = { + /** + * This method must be called once to initialize IndexedDB ObjectStore. Though, it is auto-used internally. + * @method + * @memberof DiskStorage + * @internal + * @example + * DiskStorage.init(); + */ + init: function() { + var self = this; + + if (typeof indexedDB === 'undefined' || typeof indexedDB.open === 'undefined') { + console.error('IndexedDB API are not available in this browser.'); + return; + } + + var dbVersion = 1; + var dbName = this.dbName || location.href.replace(/\/|:|#|%|\.|\[|\]/g, ''), + db; + var request = indexedDB.open(dbName, dbVersion); + + function createObjectStore(dataBase) { + dataBase.createObjectStore(self.dataStoreName); + } + + function putInDB() { + var transaction = db.transaction([self.dataStoreName], 'readwrite'); + + if (self.videoBlob) { + transaction.objectStore(self.dataStoreName).put(self.videoBlob, 'videoBlob'); + } + + if (self.gifBlob) { + transaction.objectStore(self.dataStoreName).put(self.gifBlob, 'gifBlob'); + } + + if (self.audioBlob) { + transaction.objectStore(self.dataStoreName).put(self.audioBlob, 'audioBlob'); + } + + function getFromStore(portionName) { + transaction.objectStore(self.dataStoreName).get(portionName).onsuccess = function(event) { + if (self.callback) { + self.callback(event.target.result, portionName); + } + }; + } + + getFromStore('audioBlob'); + getFromStore('videoBlob'); + getFromStore('gifBlob'); + } + + request.onerror = self.onError; + + request.onsuccess = function() { + db = request.result; + db.onerror = self.onError; + + if (db.setVersion) { + if (db.version !== dbVersion) { + var setVersion = db.setVersion(dbVersion); + setVersion.onsuccess = function() { + createObjectStore(db); + putInDB(); + }; + } else { + putInDB(); + } + } else { + putInDB(); + } + }; + request.onupgradeneeded = function(event) { + createObjectStore(event.target.result); + }; + }, + /** + * This method fetches stored blobs from IndexedDB. + * @method + * @memberof DiskStorage + * @internal + * @example + * DiskStorage.Fetch(function(dataURL, type) { + * if(type === 'audioBlob') { } + * if(type === 'videoBlob') { } + * if(type === 'gifBlob') { } + * }); + */ + Fetch: function(callback) { + this.callback = callback; + this.init(); + + return this; + }, + /** + * This method stores blobs in IndexedDB. + * @method + * @memberof DiskStorage + * @internal + * @example + * DiskStorage.Store({ + * audioBlob: yourAudioBlob, + * videoBlob: yourVideoBlob, + * gifBlob : yourGifBlob + * }); + */ + Store: function(config) { + this.audioBlob = config.audioBlob; + this.videoBlob = config.videoBlob; + this.gifBlob = config.gifBlob; + + this.init(); + + return this; + }, + /** + * This function is invoked for any known/unknown error. + * @method + * @memberof DiskStorage + * @internal + * @example + * DiskStorage.onError = function(error){ + * alerot( JSON.stringify(error) ); + * }; + */ + onError: function(error) { + console.error(JSON.stringify(error, null, '\t')); + }, + + /** + * @property {string} dataStoreName - Name of the ObjectStore created in IndexedDB storage. + * @memberof DiskStorage + * @internal + * @example + * DiskStorage.dataStoreName = 'recordRTC'; + */ + dataStoreName: 'recordRTC', + dbName: null +}; + +if (typeof RecordRTC !== 'undefined') { + RecordRTC.DiskStorage = DiskStorage; +} + +// ______________ +// GifRecorder.js + +/** + * GifRecorder is standalone calss used by {@link RecordRTC} to record video or canvas into animated gif. + * @license {@link https://github.com/muaz-khan/RecordRTC/blob/master/LICENSE|MIT} + * @author {@link https://MuazKhan.com|Muaz Khan} + * @typedef GifRecorder + * @class + * @example + * var recorder = new GifRecorder(mediaStream || canvas || context, { onGifPreview: function, onGifRecordingStarted: function, width: 1280, height: 720, frameRate: 200, quality: 10 }); + * recorder.record(); + * recorder.stop(function(blob) { + * img.src = URL.createObjectURL(blob); + * }); + * @see {@link https://github.com/muaz-khan/RecordRTC|RecordRTC Source Code} + * @param {MediaStream} mediaStream - MediaStream object or HTMLCanvasElement or CanvasRenderingContext2D. + * @param {object} config - {disableLogs:true, initCallback: function, width: 320, height: 240, frameRate: 200, quality: 10} + */ + +function GifRecorder(mediaStream, config) { + if (typeof GIFEncoder === 'undefined') { + var script = document.createElement('script'); + script.src = 'https://www.webrtc-experiment.com/gif-recorder.js'; + (document.body || document.documentElement).appendChild(script); + } + + config = config || {}; + + var isHTMLObject = mediaStream instanceof CanvasRenderingContext2D || mediaStream instanceof HTMLCanvasElement; + + /** + * This method records MediaStream. + * @method + * @memberof GifRecorder + * @example + * recorder.record(); + */ + this.record = function() { + if (typeof GIFEncoder === 'undefined') { + setTimeout(self.record, 1000); + return; + } + + if (!isLoadedMetaData) { + setTimeout(self.record, 1000); + return; + } + + if (!isHTMLObject) { + if (!config.width) { + config.width = video.offsetWidth || 320; + } + + if (!config.height) { + config.height = video.offsetHeight || 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 || 320; + canvas.height = config.canvas.height || 240; + + video.width = config.video.width || 320; + video.height = config.video.height || 240; + } + + // external library to record as GIF images + gifEncoder = new GIFEncoder(); + + // void setRepeat(int iter) + // Sets the number of times the set of GIF frames should be played. + // Default is 1; 0 means play indefinitely. + gifEncoder.setRepeat(0); + + // void setFrameRate(Number fps) + // Sets frame rate in frames per second. + // Equivalent to setDelay(1000/fps). + // Using "setDelay" instead of "setFrameRate" + gifEncoder.setDelay(config.frameRate || 200); + + // void setQuality(int quality) + // Sets quality of color quantization (conversion of images to the + // maximum 256 colors allowed by the GIF specification). + // Lower values (minimum = 1) produce better colors, + // but slow processing significantly. 10 is the default, + // and produces good color mapping at reasonable speeds. + // Values greater than 20 do not yield significant improvements in speed. + gifEncoder.setQuality(config.quality || 10); + + // Boolean start() + // This writes the GIF Header and returns false if it fails. + gifEncoder.start(); + + if (typeof config.onGifRecordingStarted === 'function') { + config.onGifRecordingStarted(); + } + + startTime = Date.now(); + + function drawVideoFrame(time) { + if (self.clearedRecordedData === true) { + return; + } + + if (isPausedRecording) { + return setTimeout(function() { + drawVideoFrame(time); + }, 100); + } + + lastAnimationFrame = requestAnimationFrame(drawVideoFrame); + + if (typeof lastFrameTime === undefined) { + lastFrameTime = time; + } + + // ~10 fps + if (time - lastFrameTime < 90) { + return; + } + + if (!isHTMLObject && video.paused) { + // via: https://github.com/muaz-khan/WebRTC-Experiment/pull/316 + // Tweak for Android Chrome + video.play(); + } + + if (!isHTMLObject) { + context.drawImage(video, 0, 0, canvas.width, canvas.height); + } + + if (config.onGifPreview) { + config.onGifPreview(canvas.toDataURL('image/png')); + } + + gifEncoder.addFrame(context); + lastFrameTime = time; + } + + lastAnimationFrame = requestAnimationFrame(drawVideoFrame); + + 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 GifRecorder + * @example + * recorder.stop(function(blob) { + * img.src = URL.createObjectURL(blob); + * }); + */ + this.stop = function(callback) { + callback = callback || function() {}; + + if (lastAnimationFrame) { + cancelAnimationFrame(lastAnimationFrame); + } + + endTime = Date.now(); + + /** + * @property {Blob} blob - The recorded blob object. + * @memberof GifRecorder + * @example + * recorder.stop(function(){ + * var blob = recorder.blob; + * }); + */ + this.blob = new Blob([new Uint8Array(gifEncoder.stream().bin)], { + type: 'image/gif' + }); + + callback(this.blob); + + // bug: find a way to clear old recorded blobs + gifEncoder.stream().bin = []; + }; + + var isPausedRecording = false; + + /** + * This method pauses the recording process. + * @method + * @memberof GifRecorder + * @example + * recorder.pause(); + */ + this.pause = function() { + isPausedRecording = true; + }; + + /** + * This method resumes the recording process. + * @method + * @memberof GifRecorder + * @example + * recorder.resume(); + */ + this.resume = function() { + isPausedRecording = false; + }; + + /** + * This method resets currently recorded data. + * @method + * @memberof GifRecorder + * @example + * recorder.clearRecordedData(); + */ + this.clearRecordedData = function() { + self.clearedRecordedData = true; + clearRecordedDataCB(); + }; + + function clearRecordedDataCB() { + if (gifEncoder) { + gifEncoder.stream().bin = []; + } + } + + // for debugging + this.name = 'GifRecorder'; + this.toString = function() { + return this.name; + }; + + var canvas = document.createElement('canvas'); + var context = canvas.getContext('2d'); + + if (isHTMLObject) { + if (mediaStream instanceof CanvasRenderingContext2D) { + context = mediaStream; + canvas = context.canvas; + } else if (mediaStream instanceof HTMLCanvasElement) { + context = mediaStream.getContext('2d'); + canvas = mediaStream; + } + } + + var isLoadedMetaData = true; + + if (!isHTMLObject) { + var video = document.createElement('video'); + video.muted = true; + video.autoplay = true; + video.playsInline = true; + + isLoadedMetaData = false; + video.onloadedmetadata = function() { + isLoadedMetaData = true; + }; + + setSrcObject(mediaStream, video); + + video.play(); + } + + var lastAnimationFrame = null; + var startTime, endTime, lastFrameTime; + + var gifEncoder; + + var self = this; +} + +if (typeof RecordRTC !== 'undefined') { + RecordRTC.GifRecorder = GifRecorder; +} + +// Last time updated: 2019-06-21 4:09:42 AM UTC + +// ________________________ +// MultiStreamsMixer v1.2.2 + +// Open-Sourced: https://github.com/muaz-khan/MultiStreamsMixer + +// -------------------------------------------------- +// Muaz Khan - www.MuazKhan.com +// MIT License - www.WebRTC-Experiment.com/licence +// -------------------------------------------------- + +function MultiStreamsMixer(arrayOfMediaStreams, elementClass) { + + var browserFakeUserAgent = 'Fake/5.0 (FakeOS) AppleWebKit/123 (KHTML, like Gecko) Fake/12.3.4567.89 Fake/123.45'; + + (function(that) { + if (typeof RecordRTC !== 'undefined') { + return; + } + + if (!that) { + return; + } + + if (typeof window !== 'undefined') { + return; + } + + if (typeof global === 'undefined') { + return; + } + + global.navigator = { + userAgent: browserFakeUserAgent, + getUserMedia: function() {} + }; + + if (!global.console) { + global.console = {}; + } + + if (typeof global.console.log === 'undefined' || typeof global.console.error === 'undefined') { + global.console.error = global.console.log = global.console.log || function() { + console.log(arguments); + }; + } + + if (typeof document === 'undefined') { + /*global document:true */ + that.document = { + documentElement: { + appendChild: function() { + return ''; + } + } + }; + + document.createElement = document.captureStream = document.mozCaptureStream = function() { + var obj = { + getContext: function() { + return obj; + }, + play: function() {}, + pause: function() {}, + drawImage: function() {}, + toDataURL: function() { + return ''; + }, + style: {} + }; + return obj; + }; + + that.HTMLVideoElement = function() {}; + } + + if (typeof location === 'undefined') { + /*global location:true */ + that.location = { + protocol: 'file:', + href: '', + hash: '' + }; + } + + if (typeof screen === 'undefined') { + /*global screen:true */ + that.screen = { + width: 0, + height: 0 + }; + } + + if (typeof URL === 'undefined') { + /*global screen:true */ + that.URL = { + createObjectURL: function() { + return ''; + }, + revokeObjectURL: function() { + return ''; + } + }; + } + + /*global window:true */ + that.window = global; + })(typeof global !== 'undefined' ? global : null); + + // requires: chrome://flags/#enable-experimental-web-platform-features + + elementClass = elementClass || 'multi-streams-mixer'; + + var videos = []; + var isStopDrawingFrames = false; + + var canvas = document.createElement('canvas'); + var context = canvas.getContext('2d'); + canvas.style.opacity = 0; + canvas.style.position = 'absolute'; + canvas.style.zIndex = -1; + canvas.style.top = '-1000em'; + canvas.style.left = '-1000em'; + canvas.className = elementClass; + (document.body || document.documentElement).appendChild(canvas); + + this.disableLogs = false; + this.frameInterval = 10; + + this.width = 360; + this.height = 240; + + // use gain node to prevent echo + this.useGainNode = true; + + var self = this; + + // _____________________________ + // Cross-Browser-Declarations.js + + // WebAudio API representer + var AudioContext = window.AudioContext; + + if (typeof AudioContext === 'undefined') { + if (typeof webkitAudioContext !== 'undefined') { + /*global AudioContext:true */ + AudioContext = webkitAudioContext; + } + + if (typeof mozAudioContext !== 'undefined') { + /*global AudioContext:true */ + AudioContext = mozAudioContext; + } + } + + /*jshint -W079 */ + var URL = window.URL; + + if (typeof URL === 'undefined' && typeof webkitURL !== 'undefined') { + /*global URL:true */ + URL = webkitURL; + } + + if (typeof navigator !== 'undefined' && typeof navigator.getUserMedia === 'undefined') { // maybe window.navigator? + if (typeof navigator.webkitGetUserMedia !== 'undefined') { + navigator.getUserMedia = navigator.webkitGetUserMedia; + } + + if (typeof navigator.mozGetUserMedia !== 'undefined') { + navigator.getUserMedia = navigator.mozGetUserMedia; + } + } + + var MediaStream = window.MediaStream; + + if (typeof MediaStream === 'undefined' && typeof webkitMediaStream !== 'undefined') { + MediaStream = webkitMediaStream; + } + + /*global MediaStream:true */ + if (typeof MediaStream !== 'undefined') { + // override "stop" method for all browsers + if (typeof MediaStream.prototype.stop === 'undefined') { + MediaStream.prototype.stop = function() { + this.getTracks().forEach(function(track) { + track.stop(); + }); + }; + } + } + + var Storage = {}; + + if (typeof AudioContext !== 'undefined') { + Storage.AudioContext = AudioContext; + } else if (typeof webkitAudioContext !== 'undefined') { + Storage.AudioContext = webkitAudioContext; + } + + function setSrcObject(stream, element) { + if ('srcObject' in element) { + element.srcObject = stream; + } else if ('mozSrcObject' in element) { + element.mozSrcObject = stream; + } else { + element.srcObject = stream; + } + } + + this.startDrawingFrames = function() { + drawVideosToCanvas(); + }; + + function drawVideosToCanvas() { + if (isStopDrawingFrames) { + return; + } + + var videosLength = videos.length; + + var fullcanvas = false; + var remaining = []; + videos.forEach(function(video) { + if (!video.stream) { + video.stream = {}; + } + + if (video.stream.fullcanvas) { + fullcanvas = video; + } else { + // todo: video.stream.active or video.stream.live to fix blank frames issues? + remaining.push(video); + } + }); + + if (fullcanvas) { + canvas.width = fullcanvas.stream.width; + canvas.height = fullcanvas.stream.height; + } else if (remaining.length) { + canvas.width = videosLength > 1 ? remaining[0].width * 2 : remaining[0].width; + + var height = 1; + if (videosLength === 3 || videosLength === 4) { + height = 2; + } + if (videosLength === 5 || videosLength === 6) { + height = 3; + } + if (videosLength === 7 || videosLength === 8) { + height = 4; + } + if (videosLength === 9 || videosLength === 10) { + height = 5; + } + canvas.height = remaining[0].height * height; + } else { + canvas.width = self.width || 360; + canvas.height = self.height || 240; + } + + if (fullcanvas && fullcanvas instanceof HTMLVideoElement) { + drawImage(fullcanvas); + } + + remaining.forEach(function(video, idx) { + drawImage(video, idx); + }); + + setTimeout(drawVideosToCanvas, self.frameInterval); + } + + function drawImage(video, idx) { + if (isStopDrawingFrames) { + return; + } + + var x = 0; + var y = 0; + var width = video.width; + var height = video.height; + + if (idx === 1) { + x = video.width; + } + + if (idx === 2) { + y = video.height; + } + + if (idx === 3) { + x = video.width; + y = video.height; + } + + if (idx === 4) { + y = video.height * 2; + } + + if (idx === 5) { + x = video.width; + y = video.height * 2; + } + + if (idx === 6) { + y = video.height * 3; + } + + if (idx === 7) { + x = video.width; + y = video.height * 3; + } + + if (typeof video.stream.left !== 'undefined') { + x = video.stream.left; + } + + if (typeof video.stream.top !== 'undefined') { + y = video.stream.top; + } + + if (typeof video.stream.width !== 'undefined') { + width = video.stream.width; + } + + if (typeof video.stream.height !== 'undefined') { + height = video.stream.height; + } + + context.drawImage(video, x, y, width, height); + + if (typeof video.stream.onRender === 'function') { + video.stream.onRender(context, x, y, width, height, idx); + } + } + + function getMixedStream() { + isStopDrawingFrames = false; + var mixedVideoStream = getMixedVideoStream(); + + var mixedAudioStream = getMixedAudioStream(); + if (mixedAudioStream) { + mixedAudioStream.getTracks().filter(function(t) { + return t.kind === 'audio'; + }).forEach(function(track) { + mixedVideoStream.addTrack(track); + }); + } + + var fullcanvas; + arrayOfMediaStreams.forEach(function(stream) { + if (stream.fullcanvas) { + fullcanvas = true; + } + }); + + // mixedVideoStream.prototype.appendStreams = appendStreams; + // mixedVideoStream.prototype.resetVideoStreams = resetVideoStreams; + // mixedVideoStream.prototype.clearRecordedData = clearRecordedData; + + return mixedVideoStream; + } + + function getMixedVideoStream() { + resetVideoStreams(); + + var capturedStream; + + if ('captureStream' in canvas) { + capturedStream = canvas.captureStream(); + } else if ('mozCaptureStream' in canvas) { + capturedStream = canvas.mozCaptureStream(); + } else if (!self.disableLogs) { + console.error('Upgrade to latest Chrome or otherwise enable this flag: chrome://flags/#enable-experimental-web-platform-features'); + } + + var videoStream = new MediaStream(); + + capturedStream.getTracks().filter(function(t) { + return t.kind === 'video'; + }).forEach(function(track) { + videoStream.addTrack(track); + }); + + canvas.stream = videoStream; + + return videoStream; + } + + function getMixedAudioStream() { + // via: @pehrsons + if (!Storage.AudioContextConstructor) { + Storage.AudioContextConstructor = new Storage.AudioContext(); + } + + self.audioContext = Storage.AudioContextConstructor; + + self.audioSources = []; + + if (self.useGainNode === true) { + self.gainNode = self.audioContext.createGain(); + self.gainNode.connect(self.audioContext.destination); + self.gainNode.gain.value = 0; // don't hear self + } + + var audioTracksLength = 0; + arrayOfMediaStreams.forEach(function(stream) { + if (!stream.getTracks().filter(function(t) { + return t.kind === 'audio'; + }).length) { + return; + } + + audioTracksLength++; + + var audioSource = self.audioContext.createMediaStreamSource(stream); + + if (self.useGainNode === true) { + audioSource.connect(self.gainNode); + } + + self.audioSources.push(audioSource); + }); + + if (!audioTracksLength) { + // because "self.audioContext" is not initialized + // that's why we've to ignore rest of the code + return; + } + + self.audioDestination = self.audioContext.createMediaStreamDestination(); + self.audioSources.forEach(function(audioSource) { + audioSource.connect(self.audioDestination); + }); + return self.audioDestination.stream; + } + + function getVideo(stream) { + var video = document.createElement('video'); + + setSrcObject(stream, video); + + video.className = elementClass; + + video.muted = true; + video.volume = 0; + + video.width = stream.width || self.width || 360; + video.height = stream.height || self.height || 240; + + video.play(); + + return video; + } + + this.appendStreams = function(streams) { + if (!streams) { + throw 'First parameter is required.'; + } + + if (!(streams instanceof Array)) { + streams = [streams]; + } + + streams.forEach(function(stream) { + var newStream = new MediaStream(); + + if (stream.getTracks().filter(function(t) { + return t.kind === 'video'; + }).length) { + var video = getVideo(stream); + video.stream = stream; + videos.push(video); + + newStream.addTrack(stream.getTracks().filter(function(t) { + return t.kind === 'video'; + })[0]); + } + + if (stream.getTracks().filter(function(t) { + return t.kind === 'audio'; + }).length) { + var audioSource = self.audioContext.createMediaStreamSource(stream); + self.audioDestination = self.audioContext.createMediaStreamDestination(); + audioSource.connect(self.audioDestination); + + newStream.addTrack(self.audioDestination.stream.getTracks().filter(function(t) { + return t.kind === 'audio'; + })[0]); + } + + arrayOfMediaStreams.push(newStream); + }); + }; + + this.releaseStreams = function() { + videos = []; + isStopDrawingFrames = true; + + if (self.gainNode) { + self.gainNode.disconnect(); + self.gainNode = null; + } + + if (self.audioSources.length) { + self.audioSources.forEach(function(source) { + source.disconnect(); + }); + self.audioSources = []; + } + + if (self.audioDestination) { + self.audioDestination.disconnect(); + self.audioDestination = null; + } + + if (self.audioContext) { + self.audioContext.close(); + } + + self.audioContext = null; + + context.clearRect(0, 0, canvas.width, canvas.height); + + if (canvas.stream) { + canvas.stream.stop(); + canvas.stream = null; + } + }; + + this.resetVideoStreams = function(streams) { + if (streams && !(streams instanceof Array)) { + streams = [streams]; + } + + resetVideoStreams(streams); + }; + + function resetVideoStreams(streams) { + videos = []; + streams = streams || arrayOfMediaStreams; + + // via: @adrian-ber + streams.forEach(function(stream) { + if (!stream.getTracks().filter(function(t) { + return t.kind === 'video'; + }).length) { + return; + } + + var video = getVideo(stream); + video.stream = stream; + videos.push(video); + }); + } + + // for debugging + this.name = 'MultiStreamsMixer'; + this.toString = function() { + return this.name; + }; + + this.getMixedStream = getMixedStream; + +} + +if (typeof RecordRTC === 'undefined') { + if (typeof module !== 'undefined' /* && !!module.exports*/ ) { + module.exports = MultiStreamsMixer; + } + + if (typeof define === 'function' && define.amd) { + define('MultiStreamsMixer', [], function() { + return MultiStreamsMixer; + }); + } +} + +// ______________________ +// MultiStreamRecorder.js + +/* + * Video conference recording, using captureStream API along with WebAudio and Canvas2D API. + */ + +/** + * MultiStreamRecorder can record multiple videos in single container. + * @summary Multi-videos recorder. + * @license {@link https://github.com/muaz-khan/RecordRTC/blob/master/LICENSE|MIT} + * @author {@link https://MuazKhan.com|Muaz Khan} + * @typedef MultiStreamRecorder + * @class + * @example + * var options = { + * mimeType: 'video/webm' + * } + * var recorder = new MultiStreamRecorder(ArrayOfMediaStreams, 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 {MediaStreams} mediaStreams - Array of MediaStreams. + * @param {object} config - {disableLogs:true, frameInterval: 1, mimeType: "video/webm"} + */ + +function MultiStreamRecorder(arrayOfMediaStreams, options) { + arrayOfMediaStreams = arrayOfMediaStreams || []; + var self = this; + + var mixer; + var mediaRecorder; + + options = options || { + elementClass: 'multi-streams-mixer', + mimeType: 'video/webm', + video: { + width: 360, + height: 240 + } + }; + + if (!options.frameInterval) { + options.frameInterval = 10; + } + + if (!options.video) { + options.video = {}; + } + + if (!options.video.width) { + options.video.width = 360; + } + + if (!options.video.height) { + options.video.height = 240; + } + + /** + * This method records all MediaStreams. + * @method + * @memberof MultiStreamRecorder + * @example + * recorder.record(); + */ + this.record = function() { + // github/muaz-khan/MultiStreamsMixer + mixer = new MultiStreamsMixer(arrayOfMediaStreams, options.elementClass || 'multi-streams-mixer'); + + if (getAllVideoTracks().length) { + mixer.frameInterval = options.frameInterval || 10; + mixer.width = options.video.width || 360; + mixer.height = options.video.height || 240; + mixer.startDrawingFrames(); + } + + if (options.previewStream && typeof options.previewStream === 'function') { + options.previewStream(mixer.getMixedStream()); + } + + // record using MediaRecorder API + mediaRecorder = new MediaStreamRecorder(mixer.getMixedStream(), options); + mediaRecorder.record(); + }; + + function getAllVideoTracks() { + var tracks = []; + arrayOfMediaStreams.forEach(function(stream) { + getTracks(stream, 'video').forEach(function(track) { + tracks.push(track); + }); + }); + return tracks; + } + + /** + * This method stops recording MediaStream. + * @param {function} callback - Callback function, that is used to pass recorded blob back to the callee. + * @method + * @memberof MultiStreamRecorder + * @example + * recorder.stop(function(blob) { + * video.src = URL.createObjectURL(blob); + * }); + */ + this.stop = function(callback) { + if (!mediaRecorder) { + return; + } + + mediaRecorder.stop(function(blob) { + self.blob = blob; + + callback(blob); + + self.clearRecordedData(); + }); + }; + + /** + * This method pauses the recording process. + * @method + * @memberof MultiStreamRecorder + * @example + * recorder.pause(); + */ + this.pause = function() { + if (mediaRecorder) { + mediaRecorder.pause(); + } + }; + + /** + * This method resumes the recording process. + * @method + * @memberof MultiStreamRecorder + * @example + * recorder.resume(); + */ + this.resume = function() { + if (mediaRecorder) { + mediaRecorder.resume(); + } + }; + + /** + * This method resets currently recorded data. + * @method + * @memberof MultiStreamRecorder + * @example + * recorder.clearRecordedData(); + */ + this.clearRecordedData = function() { + if (mediaRecorder) { + mediaRecorder.clearRecordedData(); + mediaRecorder = null; + } + + if (mixer) { + mixer.releaseStreams(); + mixer = null; + } + }; + + /** + * Add extra media-streams to existing recordings. + * @method + * @memberof MultiStreamRecorder + * @param {MediaStreams} mediaStreams - Array of MediaStreams + * @example + * recorder.addStreams([newAudioStream, newVideoStream]); + */ + this.addStreams = function(streams) { + if (!streams) { + throw 'First parameter is required.'; + } + + if (!(streams instanceof Array)) { + streams = [streams]; + } + + arrayOfMediaStreams.concat(streams); + + if (!mediaRecorder || !mixer) { + return; + } + + mixer.appendStreams(streams); + + if (options.previewStream && typeof options.previewStream === 'function') { + options.previewStream(mixer.getMixedStream()); + } + }; + + /** + * Reset videos during live recording. Replace old videos e.g. replace cameras with full-screen. + * @method + * @memberof MultiStreamRecorder + * @param {MediaStreams} mediaStreams - Array of MediaStreams + * @example + * recorder.resetVideoStreams([newVideo1, newVideo2]); + */ + this.resetVideoStreams = function(streams) { + if (!mixer) { + return; + } + + if (streams && !(streams instanceof Array)) { + streams = [streams]; + } + + mixer.resetVideoStreams(streams); + }; + + /** + * Returns MultiStreamsMixer + * @method + * @memberof MultiStreamRecorder + * @example + * let mixer = recorder.getMixer(); + * mixer.appendStreams([newStream]); + */ + this.getMixer = function() { + return mixer; + }; + + // for debugging + this.name = 'MultiStreamRecorder'; + this.toString = function() { + return this.name; + }; +} + +if (typeof RecordRTC !== 'undefined') { + RecordRTC.MultiStreamRecorder = MultiStreamRecorder; +} + +// _____________________ +// RecordRTC.promises.js + +/** + * RecordRTCPromisesHandler adds promises support in {@link RecordRTC}. Try a {@link https://github.com/muaz-khan/RecordRTC/blob/master/simple-demos/RecordRTCPromisesHandler.html|demo here} + * @summary Promises for {@link RecordRTC} + * @license {@link https://github.com/muaz-khan/RecordRTC/blob/master/LICENSE|MIT} + * @author {@link https://MuazKhan.com|Muaz Khan} + * @typedef RecordRTCPromisesHandler + * @class + * @example + * var recorder = new RecordRTCPromisesHandler(mediaStream, options); + * recorder.startRecording() + * .then(successCB) + * .catch(errorCB); + * // Note: You can access all RecordRTC API using "recorder.recordRTC" e.g. + * recorder.recordRTC.onStateChanged = function(state) {}; + * recorder.recordRTC.setRecordingDuration(5000); + * @see {@link https://github.com/muaz-khan/RecordRTC|RecordRTC Source Code} + * @param {MediaStream} mediaStream - Single media-stream object, array of media-streams, html-canvas-element, etc. + * @param {object} config - {type:"video", recorderType: MediaStreamRecorder, disableLogs: true, numberOfAudioChannels: 1, bufferSize: 0, sampleRate: 0, video: HTMLVideoElement, etc.} + * @throws Will throw an error if "new" keyword is not used to initiate "RecordRTCPromisesHandler". Also throws error if first argument "MediaStream" is missing. + * @requires {@link RecordRTC} + */ + +function RecordRTCPromisesHandler(mediaStream, options) { + if (!this) { + throw 'Use "new RecordRTCPromisesHandler()"'; + } + + if (typeof mediaStream === 'undefined') { + throw 'First argument "MediaStream" is required.'; + } + + var self = this; + + /** + * @property {Blob} blob - Access/reach the native {@link RecordRTC} object. + * @memberof RecordRTCPromisesHandler + * @example + * let internal = recorder.recordRTC.getInternalRecorder(); + * alert(internal instanceof MediaStreamRecorder); + * recorder.recordRTC.onStateChanged = function(state) {}; + */ + self.recordRTC = new RecordRTC(mediaStream, options); + + /** + * This method records MediaStream. + * @method + * @memberof RecordRTCPromisesHandler + * @example + * recorder.startRecording() + * .then(successCB) + * .catch(errorCB); + */ + this.startRecording = function() { + return new Promise(function(resolve, reject) { + try { + self.recordRTC.startRecording(); + resolve(); + } catch (e) { + reject(e); + } + }); + }; + + /** + * This method stops the recording. + * @method + * @memberof RecordRTCPromisesHandler + * @example + * recorder.stopRecording().then(function() { + * var blob = recorder.getBlob(); + * }).catch(errorCB); + */ + this.stopRecording = function() { + return new Promise(function(resolve, reject) { + try { + self.recordRTC.stopRecording(function(url) { + self.blob = self.recordRTC.getBlob(); + + if (!self.blob || !self.blob.size) { + reject('Empty blob.', self.blob); + return; + } + + resolve(url); + }); + } catch (e) { + reject(e); + } + }); + }; + + /** + * This method pauses the recording. You can resume recording using "resumeRecording" method. + * @method + * @memberof RecordRTCPromisesHandler + * @example + * recorder.pauseRecording() + * .then(successCB) + * .catch(errorCB); + */ + this.pauseRecording = function() { + return new Promise(function(resolve, reject) { + try { + self.recordRTC.pauseRecording(); + resolve(); + } catch (e) { + reject(e); + } + }); + }; + + /** + * This method resumes the recording. + * @method + * @memberof RecordRTCPromisesHandler + * @example + * recorder.resumeRecording() + * .then(successCB) + * .catch(errorCB); + */ + this.resumeRecording = function() { + return new Promise(function(resolve, reject) { + try { + self.recordRTC.resumeRecording(); + resolve(); + } catch (e) { + reject(e); + } + }); + }; + + /** + * This method returns data-url for the recorded blob. + * @method + * @memberof RecordRTCPromisesHandler + * @example + * recorder.stopRecording().then(function() { + * recorder.getDataURL().then(function(dataURL) { + * window.open(dataURL); + * }).catch(errorCB);; + * }).catch(errorCB); + */ + this.getDataURL = function(callback) { + return new Promise(function(resolve, reject) { + try { + self.recordRTC.getDataURL(function(dataURL) { + resolve(dataURL); + }); + } catch (e) { + reject(e); + } + }); + }; + + /** + * This method returns the recorded blob. + * @method + * @memberof RecordRTCPromisesHandler + * @example + * recorder.stopRecording().then(function() { + * recorder.getBlob().then(function(blob) {}) + * }).catch(errorCB); + */ + this.getBlob = function() { + return new Promise(function(resolve, reject) { + try { + resolve(self.recordRTC.getBlob()); + } catch (e) { + reject(e); + } + }); + }; + + /** + * This method returns the internal recording object. + * @method + * @memberof RecordRTCPromisesHandler + * @example + * let internalRecorder = await recorder.getInternalRecorder(); + * if(internalRecorder instanceof MultiStreamRecorder) { + * internalRecorder.addStreams([newAudioStream]); + * internalRecorder.resetVideoStreams([screenStream]); + * } + * @returns {Object} + */ + this.getInternalRecorder = function() { + return new Promise(function(resolve, reject) { + try { + resolve(self.recordRTC.getInternalRecorder()); + } catch (e) { + reject(e); + } + }); + }; + + /** + * This method resets the recorder. So that you can reuse single recorder instance many times. + * @method + * @memberof RecordRTCPromisesHandler + * @example + * await recorder.reset(); + * recorder.startRecording(); // record again + */ + this.reset = function() { + return new Promise(function(resolve, reject) { + try { + resolve(self.recordRTC.reset()); + } catch (e) { + reject(e); + } + }); + }; + + /** + * Destroy RecordRTC instance. Clear all recorders and objects. + * @method + * @memberof RecordRTCPromisesHandler + * @example + * recorder.destroy().then(successCB).catch(errorCB); + */ + this.destroy = function() { + return new Promise(function(resolve, reject) { + try { + resolve(self.recordRTC.destroy()); + } catch (e) { + reject(e); + } + }); + }; + + /** + * Get recorder's readonly state. + * @method + * @memberof RecordRTCPromisesHandler + * @example + * let state = await recorder.getState(); + * // or + * recorder.getState().then(state => { console.log(state); }) + * @returns {String} Returns recording state. + */ + this.getState = function() { + return new Promise(function(resolve, reject) { + try { + resolve(self.recordRTC.getState()); + } catch (e) { + reject(e); + } + }); + }; + + /** + * @property {Blob} blob - Recorded data as "Blob" object. + * @memberof RecordRTCPromisesHandler + * @example + * await recorder.stopRecording(); + * let blob = recorder.getBlob(); // or "recorder.recordRTC.blob" + * invokeSaveAsDialog(blob); + */ + this.blob = null; + + /** + * RecordRTC version number + * @property {String} version - Release version number. + * @memberof RecordRTCPromisesHandler + * @static + * @readonly + * @example + * alert(recorder.version); + */ + this.version = '5.6.2'; +} + +if (typeof RecordRTC !== 'undefined') { + RecordRTC.RecordRTCPromisesHandler = RecordRTCPromisesHandler; +} + +// ______________________ +// WebAssemblyRecorder.js + +/** + * WebAssemblyRecorder lets you create webm videos in JavaScript via WebAssembly. The library consumes raw RGBA32 buffers (4 bytes per pixel) and turns them into a webm video with the given framerate and quality. This makes it compatible out-of-the-box with ImageData from a CANVAS. With realtime mode you can also use webm-wasm for streaming webm videos. + * @summary Video recording feature in Chrome, Firefox and maybe Edge. + * @license {@link https://github.com/muaz-khan/RecordRTC/blob/master/LICENSE|MIT} + * @author {@link https://MuazKhan.com|Muaz Khan} + * @typedef WebAssemblyRecorder + * @class + * @example + * var recorder = new WebAssemblyRecorder(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 - {webAssemblyPath:'webm-wasm.wasm',workerPath: 'webm-worker.js', frameRate: 30, width: 1920, height: 1080, bitrate: 1024, realtime: true} + */ +function WebAssemblyRecorder(stream, config) { + // based on: github.com/GoogleChromeLabs/webm-wasm + + if (typeof ReadableStream === 'undefined' || typeof WritableStream === 'undefined') { + // because it fixes readable/writable streams issues + console.error('Following polyfill is strongly recommended: https://unpkg.com/@mattiasbuelens/web-streams-polyfill/dist/polyfill.min.js'); + } + + config = config || {}; + + config.width = config.width || 640; + config.height = config.height || 480; + config.frameRate = config.frameRate || 30; + config.bitrate = config.bitrate || 1200; + config.realtime = config.realtime || true; + + function createBufferURL(buffer, type) { + return URL.createObjectURL(new Blob([buffer], { + type: type || '' + })); + } + + var finished; + + function cameraStream() { + return new ReadableStream({ + start: function(controller) { + var cvs = document.createElement('canvas'); + var video = document.createElement('video'); + var first = true; + video.srcObject = stream; + video.muted = true; + video.height = config.height; + video.width = config.width; + video.volume = 0; + video.onplaying = function() { + cvs.width = config.width; + cvs.height = config.height; + var ctx = cvs.getContext('2d'); + var frameTimeout = 1000 / config.frameRate; + var cameraTimer = setInterval(function f() { + if (finished) { + clearInterval(cameraTimer); + controller.close(); + } + + if (first) { + first = false; + if (config.onVideoProcessStarted) { + config.onVideoProcessStarted(); + } + } + + ctx.drawImage(video, 0, 0); + if (controller._controlledReadableStream.state !== 'closed') { + try { + controller.enqueue( + ctx.getImageData(0, 0, config.width, config.height) + ); + } catch (e) {} + } + }, frameTimeout); + }; + video.play(); + } + }); + } + + var worker; + + function startRecording(stream, buffer) { + if (!config.workerPath && !buffer) { + finished = false; + + // is it safe to use @latest ? + + fetch( + 'https://unpkg.com/webm-wasm@latest/dist/webm-worker.js' + ).then(function(r) { + r.arrayBuffer().then(function(buffer) { + startRecording(stream, buffer); + }); + }); + return; + } + + if (!config.workerPath && buffer instanceof ArrayBuffer) { + var blob = new Blob([buffer], { + type: 'text/javascript' + }); + config.workerPath = URL.createObjectURL(blob); + } + + if (!config.workerPath) { + console.error('workerPath parameter is missing.'); + } + + worker = new Worker(config.workerPath); + + worker.postMessage(config.webAssemblyPath || 'https://unpkg.com/webm-wasm@latest/dist/webm-wasm.wasm'); + worker.addEventListener('message', function(event) { + if (event.data === 'READY') { + worker.postMessage({ + width: config.width, + height: config.height, + bitrate: config.bitrate || 1200, + timebaseDen: config.frameRate || 30, + realtime: config.realtime + }); + + cameraStream().pipeTo(new WritableStream({ + write: function(image) { + if (finished) { + console.error('Got image, but recorder is finished!'); + return; + } + + worker.postMessage(image.data.buffer, [image.data.buffer]); + } + })); + } else if (!!event.data) { + if (!isPaused) { + arrayOfBuffers.push(event.data); + } + } + }); + } + + /** + * This method records video. + * @method + * @memberof WebAssemblyRecorder + * @example + * recorder.record(); + */ + this.record = function() { + arrayOfBuffers = []; + isPaused = false; + this.blob = null; + startRecording(stream); + + if (typeof config.initCallback === 'function') { + config.initCallback(); + } + }; + + var isPaused; + + /** + * This method pauses the recording process. + * @method + * @memberof WebAssemblyRecorder + * @example + * recorder.pause(); + */ + this.pause = function() { + isPaused = true; + }; + + /** + * This method resumes the recording process. + * @method + * @memberof WebAssemblyRecorder + * @example + * recorder.resume(); + */ + this.resume = function() { + isPaused = false; + }; + + function terminate(callback) { + if (!worker) { + if (callback) { + callback(); + } + + return; + } + + // Wait for null event data to indicate that the encoding is complete + worker.addEventListener('message', function(event) { + if (event.data === null) { + worker.terminate(); + worker = null; + + if (callback) { + callback(); + } + } + }); + + worker.postMessage(null); + } + + var arrayOfBuffers = []; + + /** + * This method stops recording video. + * @param {function} callback - Callback function, that is used to pass recorded blob back to the callee. + * @method + * @memberof WebAssemblyRecorder + * @example + * recorder.stop(function(blob) { + * video.src = URL.createObjectURL(blob); + * }); + */ + this.stop = function(callback) { + finished = true; + + var recorder = this; + + terminate(function() { + recorder.blob = new Blob(arrayOfBuffers, { + type: 'video/webm' + }); + + callback(recorder.blob); + }); + }; + + // for debugging + this.name = 'WebAssemblyRecorder'; + this.toString = function() { + return this.name; + }; + + /** + * This method resets currently recorded data. + * @method + * @memberof WebAssemblyRecorder + * @example + * recorder.clearRecordedData(); + */ + this.clearRecordedData = function() { + arrayOfBuffers = []; + isPaused = false; + this.blob = null; + + // todo: if recording-ON then STOP it first + }; + + /** + * @property {Blob} blob - The recorded blob object. + * @memberof WebAssemblyRecorder + * @example + * recorder.stop(function(){ + * var blob = recorder.blob; + * }); + */ + this.blob = null; +} + +if (typeof RecordRTC !== 'undefined') { + RecordRTC.WebAssemblyRecorder = WebAssemblyRecorder; +}