diff --git "a/src/app/static/assets/js/RecordRTC.js" "b/src/app/static/assets/js/RecordRTC.js" new file mode 100644--- /dev/null +++ "b/src/app/static/assets/js/RecordRTC.js" @@ -0,0 +1,6750 @@ +"use strict"; + +// Last time updated: 2021-03-09 3:20:22 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 60 minutes; enough? + // use config => {timeSlice: 1000} otherwise + + mediaRecorder.start(3.6e6); + } + + 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,/gi.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; +}