'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.6e+6); } if (config.initCallback) { config.initCallback(); // old code } }; /** * @property {Array} timestamps - Array of time stamps * @memberof MediaStreamRecorder * @example * console.log(recorder.timestamps); */ this.timestamps = []; function updateTimeStamp() { self.timestamps.push(new Date().getTime()); if (typeof config.onTimeStamp === 'function') { config.onTimeStamp(self.timestamps[self.timestamps.length - 1], self.timestamps); } } function getMimeType(secondObject) { if (mediaRecorder && mediaRecorder.mimeType) { return mediaRecorder.mimeType; } return secondObject.mimeType || 'video/webm'; } /** * This method stops recording MediaStream. * @param {function} callback - Callback function, that is used to pass recorded blob back to the callee. * @method * @memberof MediaStreamRecorder * @example * recorder.stop(function(blob) { * video.src = URL.createObjectURL(blob); * }); */ this.stop = function(callback) { callback = callback || function() {}; self.manuallyStopped = true; // used inside the mediaRecorder.onerror if (!mediaRecorder) { return; } this.recordingCallback = callback; if (mediaRecorder.state === 'recording') { mediaRecorder.stop(); } if (typeof config.timeSlice === 'number') { setTimeout(function() { self.blob = new Blob(arrayOfBlobs, { type: getMimeType(config) }); self.recordingCallback(self.blob); }, 100); } }; /** * This method pauses the recording process. * @method * @memberof MediaStreamRecorder * @example * recorder.pause(); */ this.pause = function() { if (!mediaRecorder) { return; } if (mediaRecorder.state === 'recording') { mediaRecorder.pause(); } }; /** * This method resumes the recording process. * @method * @memberof MediaStreamRecorder * @example * recorder.resume(); */ this.resume = function() { if (!mediaRecorder) { return; } if (mediaRecorder.state === 'paused') { mediaRecorder.resume(); } }; /** * This method resets currently recorded data. * @method * @memberof MediaStreamRecorder * @example * recorder.clearRecordedData(); */ this.clearRecordedData = function() { if (mediaRecorder && mediaRecorder.state === 'recording') { self.stop(clearRecordedDataCB); } clearRecordedDataCB(); }; function clearRecordedDataCB() { arrayOfBlobs = []; mediaRecorder = null; self.timestamps = []; } // Reference to "MediaRecorder" object var mediaRecorder; /** * Access to native MediaRecorder API * @method * @memberof MediaStreamRecorder * @instance * @example * var internal = recorder.getInternalRecorder(); * internal.ondataavailable = function() {}; // override * internal.stream, internal.onpause, internal.onstop, etc. * @returns {Object} Returns internal recording object. */ this.getInternalRecorder = function() { return mediaRecorder; }; function isMediaStreamActive() { if ('active' in mediaStream) { if (!mediaStream.active) { return false; } } else if ('ended' in mediaStream) { // old hack if (mediaStream.ended) { return false; } } return true; } /** * @property {Blob} blob - Recorded data as "Blob" object. * @memberof MediaStreamRecorder * @example * recorder.stop(function() { * var blob = recorder.blob; * }); */ this.blob = null; /** * Get MediaRecorder readonly state. * @method * @memberof MediaStreamRecorder * @example * var state = recorder.getState(); * @returns {String} Returns recording state. */ this.getState = function() { if (!mediaRecorder) { return 'inactive'; } return mediaRecorder.state || 'inactive'; }; // list of all recording states var allStates = []; /** * Get MediaRecorder all recording states. * @method * @memberof MediaStreamRecorder * @example * var state = recorder.getAllStates(); * @returns {Array} Returns all recording states */ this.getAllStates = function() { return allStates; }; // if any Track within the MediaStream is muted or not enabled at any time, // the browser will only record black frames // or silence since that is the content produced by the Track // so we need to stopRecording as soon as any single track ends. if (typeof config.checkForInactiveTracks === 'undefined') { config.checkForInactiveTracks = false; // disable to minimize CPU usage } var self = this; // this method checks if media stream is stopped // or if any track is ended. (function looper() { if (!mediaRecorder || config.checkForInactiveTracks === false) { return; } if (isMediaStreamActive() === false) { if (!config.disableLogs) { console.log('MediaStream seems stopped.'); } self.stop(); return; } setTimeout(looper, 1000); // check every second })(); // for debugging this.name = 'MediaStreamRecorder'; this.toString = function() { return this.name; }; } if (typeof RecordRTC !== 'undefined') { RecordRTC.MediaStreamRecorder = MediaStreamRecorder; } // source code from: http://typedarray.org/wp-content/projects/WebAudioRecorder/script.js // https://github.com/mattdiamond/Recorderjs#license-mit // ______________________ // StereoAudioRecorder.js /** * StereoAudioRecorder is a standalone class used by {@link RecordRTC} to bring "stereo" audio-recording in chrome. * @summary JavaScript standalone object for stereo audio recording. * @license {@link https://github.com/muaz-khan/RecordRTC/blob/master/LICENSE|MIT} * @author {@link https://MuazKhan.com|Muaz Khan} * @typedef StereoAudioRecorder * @class * @example * var recorder = new StereoAudioRecorder(MediaStream, { * sampleRate: 44100, * bufferSize: 4096 * }); * recorder.record(); * recorder.stop(function(blob) { * video.src = URL.createObjectURL(blob); * }); * @see {@link https://github.com/muaz-khan/RecordRTC|RecordRTC Source Code} * @param {MediaStream} mediaStream - MediaStream object fetched using getUserMedia API or generated using captureStreamUntilEnded or WebAudio API. * @param {object} config - {sampleRate: 44100, bufferSize: 4096, numberOfAudioChannels: 1, etc.} */ function StereoAudioRecorder(mediaStream, config) { if (!getTracks(mediaStream, 'audio').length) { throw 'Your stream has no audio tracks.'; } config = config || {}; var self = this; // variables var leftchannel = []; var rightchannel = []; var recording = false; var recordingLength = 0; var jsAudioNode; var numberOfAudioChannels = 2; /** * Set sample rates such as 8K or 16K. Reference: http://stackoverflow.com/a/28977136/552182 * @property {number} desiredSampRate - Desired Bits per sample * 1000 * @memberof StereoAudioRecorder * @instance * @example * var recorder = StereoAudioRecorder(mediaStream, { * desiredSampRate: 16 * 1000 // bits-per-sample * 1000 * }); */ var desiredSampRate = config.desiredSampRate; // backward compatibility if (config.leftChannel === true) { numberOfAudioChannels = 1; } if (config.numberOfAudioChannels === 1) { numberOfAudioChannels = 1; } if (!numberOfAudioChannels || numberOfAudioChannels < 1) { numberOfAudioChannels = 2; } if (!config.disableLogs) { console.log('StereoAudioRecorder is set to record number of channels: ' + numberOfAudioChannels); } // if any Track within the MediaStream is muted or not enabled at any time, // the browser will only record black frames // or silence since that is the content produced by the Track // so we need to stopRecording as soon as any single track ends. if (typeof config.checkForInactiveTracks === 'undefined') { config.checkForInactiveTracks = true; } function isMediaStreamActive() { if (config.checkForInactiveTracks === false) { // always return "true" return true; } if ('active' in mediaStream) { if (!mediaStream.active) { return false; } } else if ('ended' in mediaStream) { // old hack if (mediaStream.ended) { return false; } } return true; } /** * This method records MediaStream. * @method * @memberof StereoAudioRecorder * @example * recorder.record(); */ this.record = function() { if (isMediaStreamActive() === false) { throw 'Please make sure MediaStream is active.'; } resetVariables(); isAudioProcessStarted = isPaused = false; recording = true; if (typeof config.timeSlice !== 'undefined') { looper(); } }; function mergeLeftRightBuffers(config, callback) { function mergeAudioBuffers(config, cb) { var numberOfAudioChannels = config.numberOfAudioChannels; // todo: "slice(0)" --- is it causes loop? Should be removed? var leftBuffers = config.leftBuffers.slice(0); var rightBuffers = config.rightBuffers.slice(0); var sampleRate = config.sampleRate; var internalInterleavedLength = config.internalInterleavedLength; var desiredSampRate = config.desiredSampRate; if (numberOfAudioChannels === 2) { leftBuffers = mergeBuffers(leftBuffers, internalInterleavedLength); rightBuffers = mergeBuffers(rightBuffers, internalInterleavedLength); if (desiredSampRate) { leftBuffers = interpolateArray(leftBuffers, desiredSampRate, sampleRate); rightBuffers = interpolateArray(rightBuffers, desiredSampRate, sampleRate); } } if (numberOfAudioChannels === 1) { leftBuffers = mergeBuffers(leftBuffers, internalInterleavedLength); if (desiredSampRate) { leftBuffers = interpolateArray(leftBuffers, desiredSampRate, sampleRate); } } // set sample rate as desired sample rate if (desiredSampRate) { sampleRate = desiredSampRate; } // for changing the sampling rate, reference: // http://stackoverflow.com/a/28977136/552182 function interpolateArray(data, newSampleRate, oldSampleRate) { var fitCount = Math.round(data.length * (newSampleRate / oldSampleRate)); var newData = []; var springFactor = Number((data.length - 1) / (fitCount - 1)); newData[0] = data[0]; for (var i = 1; i < fitCount - 1; i++) { var tmp = i * springFactor; var before = Number(Math.floor(tmp)).toFixed(); var after = Number(Math.ceil(tmp)).toFixed(); var atPoint = tmp - before; newData[i] = linearInterpolate(data[before], data[after], atPoint); } newData[fitCount - 1] = data[data.length - 1]; return newData; } function linearInterpolate(before, after, atPoint) { return before + (after - before) * atPoint; } function mergeBuffers(channelBuffer, rLength) { var result = new Float64Array(rLength); var offset = 0; var lng = channelBuffer.length; for (var i = 0; i < lng; i++) { var buffer = channelBuffer[i]; result.set(buffer, offset); offset += buffer.length; } return result; } function interleave(leftChannel, rightChannel) { var length = leftChannel.length + rightChannel.length; var result = new Float64Array(length); var inputIndex = 0; for (var index = 0; index < length;) { result[index++] = leftChannel[inputIndex]; result[index++] = rightChannel[inputIndex]; inputIndex++; } return result; } function writeUTFBytes(view, offset, string) { var lng = string.length; for (var i = 0; i < lng; i++) { view.setUint8(offset + i, string.charCodeAt(i)); } } // interleave both channels together var interleaved; if (numberOfAudioChannels === 2) { interleaved = interleave(leftBuffers, rightBuffers); } if (numberOfAudioChannels === 1) { interleaved = leftBuffers; } var interleavedLength = interleaved.length; // create wav file var resultingBufferLength = 44 + interleavedLength * 2; var buffer = new ArrayBuffer(resultingBufferLength); var view = new DataView(buffer); // RIFF chunk descriptor/identifier writeUTFBytes(view, 0, 'RIFF'); // RIFF chunk length // changed "44" to "36" via #401 view.setUint32(4, 36 + interleavedLength * 2, true); // RIFF type writeUTFBytes(view, 8, 'WAVE'); // format chunk identifier // FMT sub-chunk writeUTFBytes(view, 12, 'fmt '); // format chunk length view.setUint32(16, 16, true); // sample format (raw) view.setUint16(20, 1, true); // stereo (2 channels) view.setUint16(22, numberOfAudioChannels, true); // sample rate view.setUint32(24, sampleRate, true); // byte rate (sample rate * block align) view.setUint32(28, sampleRate * numberOfAudioChannels * 2, true); // block align (channel count * bytes per sample) view.setUint16(32, numberOfAudioChannels * 2, true); // bits per sample view.setUint16(34, 16, true); // data sub-chunk // data chunk identifier writeUTFBytes(view, 36, 'data'); // data chunk length view.setUint32(40, interleavedLength * 2, true); // write the PCM samples var lng = interleavedLength; var index = 44; var volume = 1; for (var i = 0; i < lng; i++) { view.setInt16(index, interleaved[i] * (0x7FFF * volume), true); index += 2; } if (cb) { return cb({ buffer: buffer, view: view }); } postMessage({ buffer: buffer, view: view }); } if (config.noWorker) { mergeAudioBuffers(config, function(data) { callback(data.buffer, data.view); }); return; } var webWorker = processInWebWorker(mergeAudioBuffers); webWorker.onmessage = function(event) { callback(event.data.buffer, event.data.view); // release memory URL.revokeObjectURL(webWorker.workerURL); // kill webworker (or Chrome will kill your page after ~25 calls) webWorker.terminate(); }; webWorker.postMessage(config); } function processInWebWorker(_function) { var workerURL = URL.createObjectURL(new Blob([_function.toString(), ';this.onmessage = function (eee) {' + _function.name + '(eee.data);}' ], { type: 'application/javascript' })); var worker = new Worker(workerURL); worker.workerURL = workerURL; return worker; } /** * This method stops recording MediaStream. * @param {function} callback - Callback function, that is used to pass recorded blob back to the callee. * @method * @memberof StereoAudioRecorder * @example * recorder.stop(function(blob) { * video.src = URL.createObjectURL(blob); * }); */ this.stop = function(callback) { callback = callback || function() {}; // stop recording recording = false; mergeLeftRightBuffers({ desiredSampRate: desiredSampRate, sampleRate: sampleRate, numberOfAudioChannels: numberOfAudioChannels, internalInterleavedLength: recordingLength, leftBuffers: leftchannel, rightBuffers: numberOfAudioChannels === 1 ? [] : rightchannel, noWorker: config.noWorker }, function(buffer, view) { /** * @property {Blob} blob - The recorded blob object. * @memberof StereoAudioRecorder * @example * recorder.stop(function(){ * var blob = recorder.blob; * }); */ self.blob = new Blob([view], { type: 'audio/wav' }); /** * @property {ArrayBuffer} buffer - The recorded buffer object. * @memberof StereoAudioRecorder * @example * recorder.stop(function(){ * var buffer = recorder.buffer; * }); */ self.buffer = new ArrayBuffer(view.buffer.byteLength); /** * @property {DataView} view - The recorded data-view object. * @memberof StereoAudioRecorder * @example * recorder.stop(function(){ * var view = recorder.view; * }); */ self.view = view; self.sampleRate = desiredSampRate || sampleRate; self.bufferSize = bufferSize; // recorded audio length self.length = recordingLength; isAudioProcessStarted = false; if (callback) { callback(self.blob); } }); }; if (typeof RecordRTC.Storage === 'undefined') { RecordRTC.Storage = { AudioContextConstructor: null, AudioContext: window.AudioContext || window.webkitAudioContext }; } if (!RecordRTC.Storage.AudioContextConstructor || RecordRTC.Storage.AudioContextConstructor.state === 'closed') { RecordRTC.Storage.AudioContextConstructor = new RecordRTC.Storage.AudioContext(); } var context = RecordRTC.Storage.AudioContextConstructor; // creates an audio node from the microphone incoming stream var audioInput = context.createMediaStreamSource(mediaStream); var legalBufferValues = [0, 256, 512, 1024, 2048, 4096, 8192, 16384]; /** * From the spec: This value controls how frequently the audioprocess event is * dispatched and how many sample-frames need to be processed each call. * Lower values for buffer size will result in a lower (better) latency. * Higher values will be necessary to avoid audio breakup and glitches * The size of the buffer (in sample-frames) which needs to * be processed each time onprocessaudio is called. * Legal values are (256, 512, 1024, 2048, 4096, 8192, 16384). * @property {number} bufferSize - Buffer-size for how frequently the audioprocess event is dispatched. * @memberof StereoAudioRecorder * @example * recorder = new StereoAudioRecorder(mediaStream, { * bufferSize: 4096 * }); */ // "0" means, let chrome decide the most accurate buffer-size for current platform. var bufferSize = typeof config.bufferSize === 'undefined' ? 4096 : config.bufferSize; if (legalBufferValues.indexOf(bufferSize) === -1) { if (!config.disableLogs) { console.log('Legal values for buffer-size are ' + JSON.stringify(legalBufferValues, null, '\t')); } } if (context.createJavaScriptNode) { jsAudioNode = context.createJavaScriptNode(bufferSize, numberOfAudioChannels, numberOfAudioChannels); } else if (context.createScriptProcessor) { jsAudioNode = context.createScriptProcessor(bufferSize, numberOfAudioChannels, numberOfAudioChannels); } else { throw 'WebAudio API has no support on this browser.'; } // connect the stream to the script processor audioInput.connect(jsAudioNode); if (!config.bufferSize) { bufferSize = jsAudioNode.bufferSize; // device buffer-size } /** * The sample rate (in sample-frames per second) at which the * AudioContext handles audio. It is assumed that all AudioNodes * in the context run at this rate. In making this assumption, * sample-rate converters or "varispeed" processors are not supported * in real-time processing. * The sampleRate parameter describes the sample-rate of the * linear PCM audio data in the buffer in sample-frames per second. * An implementation must support sample-rates in at least * the range 22050 to 96000. * @property {number} sampleRate - Buffer-size for how frequently the audioprocess event is dispatched. * @memberof StereoAudioRecorder * @example * recorder = new StereoAudioRecorder(mediaStream, { * sampleRate: 44100 * }); */ var sampleRate = typeof config.sampleRate !== 'undefined' ? config.sampleRate : context.sampleRate || 44100; if (sampleRate < 22050 || sampleRate > 96000) { // Ref: http://stackoverflow.com/a/26303918/552182 if (!config.disableLogs) { console.log('sample-rate must be under range 22050 and 96000.'); } } if (!config.disableLogs) { if (config.desiredSampRate) { console.log('Desired sample-rate: ' + config.desiredSampRate); } } var isPaused = false; /** * This method pauses the recording process. * @method * @memberof StereoAudioRecorder * @example * recorder.pause(); */ this.pause = function() { isPaused = true; }; /** * This method resumes the recording process. * @method * @memberof StereoAudioRecorder * @example * recorder.resume(); */ this.resume = function() { if (isMediaStreamActive() === false) { throw 'Please make sure MediaStream is active.'; } if (!recording) { if (!config.disableLogs) { console.log('Seems recording has been restarted.'); } this.record(); return; } isPaused = false; }; /** * This method resets currently recorded data. * @method * @memberof StereoAudioRecorder * @example * recorder.clearRecordedData(); */ this.clearRecordedData = function() { config.checkForInactiveTracks = false; if (recording) { this.stop(clearRecordedDataCB); } clearRecordedDataCB(); }; function resetVariables() { leftchannel = []; rightchannel = []; recordingLength = 0; isAudioProcessStarted = false; recording = false; isPaused = false; context = null; self.leftchannel = leftchannel; self.rightchannel = rightchannel; self.numberOfAudioChannels = numberOfAudioChannels; self.desiredSampRate = desiredSampRate; self.sampleRate = sampleRate; self.recordingLength = recordingLength; intervalsBasedBuffers = { left: [], right: [], recordingLength: 0 }; } function clearRecordedDataCB() { if (jsAudioNode) { jsAudioNode.onaudioprocess = null; jsAudioNode.disconnect(); jsAudioNode = null; } if (audioInput) { audioInput.disconnect(); audioInput = null; } resetVariables(); } // for debugging this.name = 'StereoAudioRecorder'; this.toString = function() { return this.name; }; var isAudioProcessStarted = false; function onAudioProcessDataAvailable(e) { if (isPaused) { return; } if (isMediaStreamActive() === false) { if (!config.disableLogs) { console.log('MediaStream seems stopped.'); } jsAudioNode.disconnect(); recording = false; } if (!recording) { if (audioInput) { audioInput.disconnect(); audioInput = null; } return; } /** * This method is called on "onaudioprocess" event's first invocation. * @method {function} onAudioProcessStarted * @memberof StereoAudioRecorder * @example * recorder.onAudioProcessStarted: function() { }; */ if (!isAudioProcessStarted) { isAudioProcessStarted = true; if (config.onAudioProcessStarted) { config.onAudioProcessStarted(); } if (config.initCallback) { config.initCallback(); } } var left = e.inputBuffer.getChannelData(0); // we clone the samples var chLeft = new Float32Array(left); leftchannel.push(chLeft); if (numberOfAudioChannels === 2) { var right = e.inputBuffer.getChannelData(1); var chRight = new Float32Array(right); rightchannel.push(chRight); } recordingLength += bufferSize; // export raw PCM self.recordingLength = recordingLength; if (typeof config.timeSlice !== 'undefined') { intervalsBasedBuffers.recordingLength += bufferSize; intervalsBasedBuffers.left.push(chLeft); if (numberOfAudioChannels === 2) { intervalsBasedBuffers.right.push(chRight); } } } jsAudioNode.onaudioprocess = onAudioProcessDataAvailable; // to prevent self audio to be connected with speakers if (context.createMediaStreamDestination) { jsAudioNode.connect(context.createMediaStreamDestination()); } else { jsAudioNode.connect(context.destination); } // export raw PCM this.leftchannel = leftchannel; this.rightchannel = rightchannel; this.numberOfAudioChannels = numberOfAudioChannels; this.desiredSampRate = desiredSampRate; this.sampleRate = sampleRate; self.recordingLength = recordingLength; // helper for intervals based blobs var intervalsBasedBuffers = { left: [], right: [], recordingLength: 0 }; // this looper is used to support intervals based blobs (via timeSlice+ondataavailable) function looper() { if (!recording || typeof config.ondataavailable !== 'function' || typeof config.timeSlice === 'undefined') { return; } if (intervalsBasedBuffers.left.length) { mergeLeftRightBuffers({ desiredSampRate: desiredSampRate, sampleRate: sampleRate, numberOfAudioChannels: numberOfAudioChannels, internalInterleavedLength: intervalsBasedBuffers.recordingLength, leftBuffers: intervalsBasedBuffers.left, rightBuffers: numberOfAudioChannels === 1 ? [] : intervalsBasedBuffers.right }, function(buffer, view) { var blob = new Blob([view], { type: 'audio/wav' }); config.ondataavailable(blob); setTimeout(looper, config.timeSlice); }); intervalsBasedBuffers = { left: [], right: [], recordingLength: 0 }; } else { setTimeout(looper, config.timeSlice); } } } if (typeof RecordRTC !== 'undefined') { RecordRTC.StereoAudioRecorder = StereoAudioRecorder; } // _________________ // CanvasRecorder.js /** * CanvasRecorder is a standalone class used by {@link RecordRTC} to bring HTML5-Canvas recording into video WebM. It uses HTML2Canvas library and runs top over {@link Whammy}. * @summary HTML2Canvas recording into video WebM. * @license {@link https://github.com/muaz-khan/RecordRTC/blob/master/LICENSE|MIT} * @author {@link https://MuazKhan.com|Muaz Khan} * @typedef CanvasRecorder * @class * @example * var recorder = new CanvasRecorder(htmlElement, { disableLogs: true, useWhammyRecorder: true }); * recorder.record(); * recorder.stop(function(blob) { * video.src = URL.createObjectURL(blob); * }); * @see {@link https://github.com/muaz-khan/RecordRTC|RecordRTC Source Code} * @param {HTMLElement} htmlElement - querySelector/getElementById/getElementsByTagName[0]/etc. * @param {object} config - {disableLogs:true, initCallback: function} */ function CanvasRecorder(htmlElement, config) { if (typeof html2canvas === 'undefined') { throw 'Please link: https://www.webrtc-experiment.com/screenshot.js'; } config = config || {}; if (!config.frameInterval) { config.frameInterval = 10; } // via DetectRTC.js var isCanvasSupportsStreamCapturing = false; ['captureStream', 'mozCaptureStream', 'webkitCaptureStream'].forEach(function(item) { if (item in document.createElement('canvas')) { isCanvasSupportsStreamCapturing = true; } }); var _isChrome = (!!window.webkitRTCPeerConnection || !!window.webkitGetUserMedia) && !!window.chrome; var chromeVersion = 50; var matchArray = navigator.userAgent.match(/Chrom(e|ium)\/([0-9]+)\./); if (_isChrome && matchArray && matchArray[2]) { chromeVersion = parseInt(matchArray[2], 10); } if (_isChrome && chromeVersion < 52) { isCanvasSupportsStreamCapturing = false; } if (config.useWhammyRecorder) { isCanvasSupportsStreamCapturing = false; } var globalCanvas, mediaStreamRecorder; if (isCanvasSupportsStreamCapturing) { if (!config.disableLogs) { console.log('Your browser supports both MediRecorder API and canvas.captureStream!'); } if (htmlElement instanceof HTMLCanvasElement) { globalCanvas = htmlElement; } else if (htmlElement instanceof CanvasRenderingContext2D) { globalCanvas = htmlElement.canvas; } else { throw 'Please pass either HTMLCanvasElement or CanvasRenderingContext2D.'; } } else if (!!navigator.mozGetUserMedia) { if (!config.disableLogs) { console.error('Canvas recording is NOT supported in Firefox.'); } } var isRecording; /** * This method records Canvas. * @method * @memberof CanvasRecorder * @example * recorder.record(); */ this.record = function() { isRecording = true; if (isCanvasSupportsStreamCapturing && !config.useWhammyRecorder) { // CanvasCaptureMediaStream var canvasMediaStream; if ('captureStream' in globalCanvas) { canvasMediaStream = globalCanvas.captureStream(25); // 25 FPS } else if ('mozCaptureStream' in globalCanvas) { canvasMediaStream = globalCanvas.mozCaptureStream(25); } else if ('webkitCaptureStream' in globalCanvas) { canvasMediaStream = globalCanvas.webkitCaptureStream(25); } try { var mdStream = new MediaStream(); mdStream.addTrack(getTracks(canvasMediaStream, 'video')[0]); canvasMediaStream = mdStream; } catch (e) {} if (!canvasMediaStream) { throw 'captureStream API are NOT available.'; } // Note: Jan 18, 2016 status is that, // Firefox MediaRecorder API can't record CanvasCaptureMediaStream object. mediaStreamRecorder = new MediaStreamRecorder(canvasMediaStream, { mimeType: config.mimeType || 'video/webm' }); mediaStreamRecorder.record(); } else { whammy.frames = []; lastTime = new Date().getTime(); drawCanvasFrame(); } if (config.initCallback) { config.initCallback(); } }; this.getWebPImages = function(callback) { if (htmlElement.nodeName.toLowerCase() !== 'canvas') { callback(); return; } var framesLength = whammy.frames.length; whammy.frames.forEach(function(frame, idx) { var framesRemaining = framesLength - idx; if (!config.disableLogs) { console.log(framesRemaining + '/' + framesLength + ' frames remaining'); } if (config.onEncodingCallback) { config.onEncodingCallback(framesRemaining, framesLength); } var webp = frame.image.toDataURL('image/webp', 1); whammy.frames[idx].image = webp; }); if (!config.disableLogs) { console.log('Generating WebM'); } callback(); }; /** * This method stops recording Canvas. * @param {function} callback - Callback function, that is used to pass recorded blob back to the callee. * @method * @memberof CanvasRecorder * @example * recorder.stop(function(blob) { * video.src = URL.createObjectURL(blob); * }); */ this.stop = function(callback) { isRecording = false; var that = this; if (isCanvasSupportsStreamCapturing && mediaStreamRecorder) { mediaStreamRecorder.stop(callback); return; } this.getWebPImages(function() { /** * @property {Blob} blob - Recorded frames in video/webm blob. * @memberof CanvasRecorder * @example * recorder.stop(function() { * var blob = recorder.blob; * }); */ whammy.compile(function(blob) { if (!config.disableLogs) { console.log('Recording finished!'); } that.blob = blob; if (that.blob.forEach) { that.blob = new Blob([], { type: 'video/webm' }); } if (callback) { callback(that.blob); } whammy.frames = []; }); }); }; var isPausedRecording = false; /** * This method pauses the recording process. * @method * @memberof CanvasRecorder * @example * recorder.pause(); */ this.pause = function() { isPausedRecording = true; if (mediaStreamRecorder instanceof MediaStreamRecorder) { mediaStreamRecorder.pause(); return; } }; /** * This method resumes the recording process. * @method * @memberof CanvasRecorder * @example * recorder.resume(); */ this.resume = function() { isPausedRecording = false; if (mediaStreamRecorder instanceof MediaStreamRecorder) { mediaStreamRecorder.resume(); return; } if (!isRecording) { this.record(); } }; /** * This method resets currently recorded data. * @method * @memberof CanvasRecorder * @example * recorder.clearRecordedData(); */ this.clearRecordedData = function() { if (isRecording) { this.stop(clearRecordedDataCB); } clearRecordedDataCB(); }; function clearRecordedDataCB() { whammy.frames = []; isRecording = false; isPausedRecording = false; } // for debugging this.name = 'CanvasRecorder'; this.toString = function() { return this.name; }; function cloneCanvas() { //create a new canvas var newCanvas = document.createElement('canvas'); var context = newCanvas.getContext('2d'); //set dimensions newCanvas.width = htmlElement.width; newCanvas.height = htmlElement.height; //apply the old canvas to the new one context.drawImage(htmlElement, 0, 0); //return the new canvas return newCanvas; } function drawCanvasFrame() { if (isPausedRecording) { lastTime = new Date().getTime(); return setTimeout(drawCanvasFrame, 500); } if (htmlElement.nodeName.toLowerCase() === 'canvas') { var duration = new Date().getTime() - lastTime; // via #206, by Jack i.e. @Seymourr lastTime = new Date().getTime(); whammy.frames.push({ image: cloneCanvas(), duration: duration }); if (isRecording) { setTimeout(drawCanvasFrame, config.frameInterval); } return; } html2canvas(htmlElement, { grabMouse: typeof config.showMousePointer === 'undefined' || config.showMousePointer, onrendered: function(canvas) { var duration = new Date().getTime() - lastTime; if (!duration) { return setTimeout(drawCanvasFrame, config.frameInterval); } // via #206, by Jack i.e. @Seymourr lastTime = new Date().getTime(); whammy.frames.push({ image: canvas.toDataURL('image/webp', 1), duration: duration }); if (isRecording) { setTimeout(drawCanvasFrame, config.frameInterval); } } }); } var lastTime = new Date().getTime(); var whammy = new Whammy.Video(100); } if (typeof RecordRTC !== 'undefined') { RecordRTC.CanvasRecorder = CanvasRecorder; } // _________________ // WhammyRecorder.js /** * WhammyRecorder is a standalone class used by {@link RecordRTC} to bring video recording in Chrome. It runs top over {@link Whammy}. * @summary Video recording feature in Chrome. * @license {@link https://github.com/muaz-khan/RecordRTC/blob/master/LICENSE|MIT} * @author {@link https://MuazKhan.com|Muaz Khan} * @typedef WhammyRecorder * @class * @example * var recorder = new WhammyRecorder(mediaStream); * recorder.record(); * recorder.stop(function(blob) { * video.src = URL.createObjectURL(blob); * }); * @see {@link https://github.com/muaz-khan/RecordRTC|RecordRTC Source Code} * @param {MediaStream} mediaStream - MediaStream object fetched using getUserMedia API or generated using captureStreamUntilEnded or WebAudio API. * @param {object} config - {disableLogs: true, initCallback: function, video: HTMLVideoElement, etc.} */ function WhammyRecorder(mediaStream, config) { config = config || {}; if (!config.frameInterval) { config.frameInterval = 10; } if (!config.disableLogs) { console.log('Using frames-interval:', config.frameInterval); } /** * This method records video. * @method * @memberof WhammyRecorder * @example * recorder.record(); */ this.record = function() { if (!config.width) { config.width = 320; } if (!config.height) { config.height = 240; } if (!config.video) { config.video = { width: config.width, height: config.height }; } if (!config.canvas) { config.canvas = { width: config.width, height: config.height }; } canvas.width = config.canvas.width || 320; canvas.height = config.canvas.height || 240; context = canvas.getContext('2d'); // setting defaults if (config.video && config.video instanceof HTMLVideoElement) { video = config.video.cloneNode(); if (config.initCallback) { config.initCallback(); } } else { video = document.createElement('video'); setSrcObject(mediaStream, video); video.onloadedmetadata = function() { // "onloadedmetadata" may NOT work in FF? if (config.initCallback) { config.initCallback(); } }; video.width = config.video.width; video.height = config.video.height; } video.muted = true; video.play(); lastTime = new Date().getTime(); whammy = new Whammy.Video(); if (!config.disableLogs) { console.log('canvas resolutions', canvas.width, '*', canvas.height); console.log('video width/height', video.width || canvas.width, '*', video.height || canvas.height); } drawFrames(config.frameInterval); }; /** * Draw and push frames to Whammy * @param {integer} frameInterval - set minimum interval (in milliseconds) between each time we push a frame to Whammy */ function drawFrames(frameInterval) { frameInterval = typeof frameInterval !== 'undefined' ? frameInterval : 10; var duration = new Date().getTime() - lastTime; if (!duration) { return setTimeout(drawFrames, frameInterval, frameInterval); } if (isPausedRecording) { lastTime = new Date().getTime(); return setTimeout(drawFrames, 100); } // via #206, by Jack i.e. @Seymourr lastTime = new Date().getTime(); if (video.paused) { // via: https://github.com/muaz-khan/WebRTC-Experiment/pull/316 // Tweak for Android Chrome video.play(); } context.drawImage(video, 0, 0, canvas.width, canvas.height); whammy.frames.push({ duration: duration, image: canvas.toDataURL('image/webp') }); if (!isStopDrawing) { setTimeout(drawFrames, frameInterval, frameInterval); } } function asyncLoop(o) { var i = -1, length = o.length; (function loop() { i++; if (i === length) { o.callback(); return; } // "setTimeout" added by Jim McLeod setTimeout(function() { o.functionToLoop(loop, i); }, 1); })(); } /** * remove black frames from the beginning to the specified frame * @param {Array} _frames - array of frames to be checked * @param {number} _framesToCheck - number of frame until check will be executed (-1 - will drop all frames until frame not matched will be found) * @param {number} _pixTolerance - 0 - very strict (only black pixel color) ; 1 - all * @param {number} _frameTolerance - 0 - very strict (only black frame color) ; 1 - all * @returns {Array} - array of frames */ // pull#293 by @volodalexey function dropBlackFrames(_frames, _framesToCheck, _pixTolerance, _frameTolerance, callback) { var localCanvas = document.createElement('canvas'); localCanvas.width = canvas.width; localCanvas.height = canvas.height; var context2d = localCanvas.getContext('2d'); var resultFrames = []; var checkUntilNotBlack = _framesToCheck === -1; var endCheckFrame = (_framesToCheck && _framesToCheck > 0 && _framesToCheck <= _frames.length) ? _framesToCheck : _frames.length; var sampleColor = { r: 0, g: 0, b: 0 }; var maxColorDifference = Math.sqrt( Math.pow(255, 2) + Math.pow(255, 2) + Math.pow(255, 2) ); var pixTolerance = _pixTolerance && _pixTolerance >= 0 && _pixTolerance <= 1 ? _pixTolerance : 0; var frameTolerance = _frameTolerance && _frameTolerance >= 0 && _frameTolerance <= 1 ? _frameTolerance : 0; var doNotCheckNext = false; asyncLoop({ length: endCheckFrame, functionToLoop: function(loop, f) { var matchPixCount, endPixCheck, maxPixCount; var finishImage = function() { if (!doNotCheckNext && maxPixCount - matchPixCount <= maxPixCount * frameTolerance) { // console.log('removed black frame : ' + f + ' ; frame duration ' + _frames[f].duration); } else { // console.log('frame is passed : ' + f); if (checkUntilNotBlack) { doNotCheckNext = true; } resultFrames.push(_frames[f]); } loop(); }; if (!doNotCheckNext) { var image = new Image(); image.onload = function() { context2d.drawImage(image, 0, 0, canvas.width, canvas.height); var imageData = context2d.getImageData(0, 0, canvas.width, canvas.height); matchPixCount = 0; endPixCheck = imageData.data.length; maxPixCount = imageData.data.length / 4; for (var pix = 0; pix < endPixCheck; pix += 4) { var currentColor = { r: imageData.data[pix], g: imageData.data[pix + 1], b: imageData.data[pix + 2] }; var colorDifference = Math.sqrt( Math.pow(currentColor.r - sampleColor.r, 2) + Math.pow(currentColor.g - sampleColor.g, 2) + Math.pow(currentColor.b - sampleColor.b, 2) ); // difference in color it is difference in color vectors (r1,g1,b1) <=> (r2,g2,b2) if (colorDifference <= maxColorDifference * pixTolerance) { matchPixCount++; } } finishImage(); }; image.src = _frames[f].image; } else { finishImage(); } }, callback: function() { resultFrames = resultFrames.concat(_frames.slice(endCheckFrame)); if (resultFrames.length <= 0) { // at least one last frame should be available for next manipulation // if total duration of all frames will be < 1000 than ffmpeg doesn't work well... resultFrames.push(_frames[_frames.length - 1]); } callback(resultFrames); } }); } var isStopDrawing = false; /** * This method stops recording video. * @param {function} callback - Callback function, that is used to pass recorded blob back to the callee. * @method * @memberof WhammyRecorder * @example * recorder.stop(function(blob) { * video.src = URL.createObjectURL(blob); * }); */ this.stop = function(callback) { callback = callback || function() {}; isStopDrawing = true; var _this = this; // analyse of all frames takes some time! setTimeout(function() { // e.g. dropBlackFrames(frames, 10, 1, 1) - will cut all 10 frames // e.g. dropBlackFrames(frames, 10, 0.5, 0.5) - will analyse 10 frames // e.g. dropBlackFrames(frames, 10) === dropBlackFrames(frames, 10, 0, 0) - will analyse 10 frames with strict black color dropBlackFrames(whammy.frames, -1, null, null, function(frames) { whammy.frames = frames; // to display advertisement images! if (config.advertisement && config.advertisement.length) { whammy.frames = config.advertisement.concat(whammy.frames); } /** * @property {Blob} blob - Recorded frames in video/webm blob. * @memberof WhammyRecorder * @example * recorder.stop(function() { * var blob = recorder.blob; * }); */ whammy.compile(function(blob) { _this.blob = blob; if (_this.blob.forEach) { _this.blob = new Blob([], { type: 'video/webm' }); } if (callback) { callback(_this.blob); } }); }); }, 10); }; var isPausedRecording = false; /** * This method pauses the recording process. * @method * @memberof WhammyRecorder * @example * recorder.pause(); */ this.pause = function() { isPausedRecording = true; }; /** * This method resumes the recording process. * @method * @memberof WhammyRecorder * @example * recorder.resume(); */ this.resume = function() { isPausedRecording = false; if (isStopDrawing) { this.record(); } }; /** * This method resets currently recorded data. * @method * @memberof WhammyRecorder * @example * recorder.clearRecordedData(); */ this.clearRecordedData = function() { if (!isStopDrawing) { this.stop(clearRecordedDataCB); } clearRecordedDataCB(); }; function clearRecordedDataCB() { whammy.frames = []; isStopDrawing = true; isPausedRecording = false; } // for debugging this.name = 'WhammyRecorder'; this.toString = function() { return this.name; }; var canvas = document.createElement('canvas'); var context = canvas.getContext('2d'); var video; var lastTime; var whammy; } if (typeof RecordRTC !== 'undefined') { RecordRTC.WhammyRecorder = WhammyRecorder; } // https://github.com/antimatter15/whammy/blob/master/LICENSE // _________ // Whammy.js // todo: Firefox now supports webp for webm containers! // their MediaRecorder implementation works well! // should we provide an option to record via Whammy.js or MediaRecorder API is a better solution? /** * Whammy is a standalone class used by {@link RecordRTC} to bring video recording in Chrome. It is written by {@link https://github.com/antimatter15|antimatter15} * @summary A real time javascript webm encoder based on a canvas hack. * @license {@link https://github.com/muaz-khan/RecordRTC/blob/master/LICENSE|MIT} * @author {@link https://MuazKhan.com|Muaz Khan} * @typedef Whammy * @class * @example * var recorder = new Whammy().Video(15); * recorder.add(context || canvas || dataURL); * var output = recorder.compile(); * @see {@link https://github.com/muaz-khan/RecordRTC|RecordRTC Source Code} */ var Whammy = (function() { // a more abstract-ish API function WhammyVideo(duration) { this.frames = []; this.duration = duration || 1; this.quality = 0.8; } /** * Pass Canvas or Context or image/webp(string) to {@link Whammy} encoder. * @method * @memberof Whammy * @example * recorder = new Whammy().Video(0.8, 100); * recorder.add(canvas || context || 'image/webp'); * @param {string} frame - Canvas || Context || image/webp * @param {number} duration - Stick a duration (in milliseconds) */ WhammyVideo.prototype.add = function(frame, duration) { if ('canvas' in frame) { //CanvasRenderingContext2D frame = frame.canvas; } if ('toDataURL' in frame) { frame = frame.toDataURL('image/webp', this.quality); } if (!(/^data:image\/webp;base64,/ig).test(frame)) { throw 'Input must be formatted properly as a base64 encoded DataURI of type image/webp'; } this.frames.push({ image: frame, duration: duration || this.duration }); }; function processInWebWorker(_function) { var blob = URL.createObjectURL(new Blob([_function.toString(), 'this.onmessage = function (eee) {' + _function.name + '(eee.data);}' ], { type: 'application/javascript' })); var worker = new Worker(blob); URL.revokeObjectURL(blob); return worker; } function whammyInWebWorker(frames) { function ArrayToWebM(frames) { var info = checkFrames(frames); if (!info) { return []; } var clusterMaxDuration = 30000; var EBML = [{ 'id': 0x1a45dfa3, // EBML 'data': [{ 'data': 1, 'id': 0x4286 // EBMLVersion }, { 'data': 1, 'id': 0x42f7 // EBMLReadVersion }, { 'data': 4, 'id': 0x42f2 // EBMLMaxIDLength }, { 'data': 8, 'id': 0x42f3 // EBMLMaxSizeLength }, { 'data': 'webm', 'id': 0x4282 // DocType }, { 'data': 2, 'id': 0x4287 // DocTypeVersion }, { 'data': 2, 'id': 0x4285 // DocTypeReadVersion }] }, { 'id': 0x18538067, // Segment 'data': [{ 'id': 0x1549a966, // Info 'data': [{ 'data': 1e6, //do things in millisecs (num of nanosecs for duration scale) 'id': 0x2ad7b1 // TimecodeScale }, { 'data': 'whammy', 'id': 0x4d80 // MuxingApp }, { 'data': 'whammy', 'id': 0x5741 // WritingApp }, { 'data': doubleToString(info.duration), 'id': 0x4489 // Duration }] }, { 'id': 0x1654ae6b, // Tracks 'data': [{ 'id': 0xae, // TrackEntry 'data': [{ 'data': 1, 'id': 0xd7 // TrackNumber }, { 'data': 1, 'id': 0x73c5 // TrackUID }, { 'data': 0, 'id': 0x9c // FlagLacing }, { 'data': 'und', 'id': 0x22b59c // Language }, { 'data': 'V_VP8', 'id': 0x86 // CodecID }, { 'data': 'VP8', 'id': 0x258688 // CodecName }, { 'data': 1, 'id': 0x83 // TrackType }, { 'id': 0xe0, // Video 'data': [{ 'data': info.width, 'id': 0xb0 // PixelWidth }, { 'data': info.height, 'id': 0xba // PixelHeight }] }] }] }] }]; //Generate clusters (max duration) var frameNumber = 0; var clusterTimecode = 0; while (frameNumber < frames.length) { var clusterFrames = []; var clusterDuration = 0; do { clusterFrames.push(frames[frameNumber]); clusterDuration += frames[frameNumber].duration; frameNumber++; } while (frameNumber < frames.length && clusterDuration < clusterMaxDuration); var clusterCounter = 0; var cluster = { 'id': 0x1f43b675, // Cluster 'data': getClusterData(clusterTimecode, clusterCounter, clusterFrames) }; //Add cluster to segment EBML[1].data.push(cluster); clusterTimecode += clusterDuration; } return generateEBML(EBML); } function getClusterData(clusterTimecode, clusterCounter, clusterFrames) { return [{ 'data': clusterTimecode, 'id': 0xe7 // Timecode }].concat(clusterFrames.map(function(webp) { var block = makeSimpleBlock({ discardable: 0, frame: webp.data.slice(4), invisible: 0, keyframe: 1, lacing: 0, trackNum: 1, timecode: Math.round(clusterCounter) }); clusterCounter += webp.duration; return { data: block, id: 0xa3 }; })); } // sums the lengths of all the frames and gets the duration function checkFrames(frames) { if (!frames[0]) { postMessage({ error: 'Something went wrong. Maybe WebP format is not supported in the current browser.' }); return; } var width = frames[0].width, height = frames[0].height, duration = frames[0].duration; for (var i = 1; i < frames.length; i++) { duration += frames[i].duration; } return { duration: duration, width: width, height: height }; } function numToBuffer(num) { var parts = []; while (num > 0) { parts.push(num & 0xff); num = num >> 8; } return new Uint8Array(parts.reverse()); } function strToBuffer(str) { return new Uint8Array(str.split('').map(function(e) { return e.charCodeAt(0); })); } function bitsToBuffer(bits) { var data = []; var pad = (bits.length % 8) ? (new Array(1 + 8 - (bits.length % 8))).join('0') : ''; bits = pad + bits; for (var i = 0; i < bits.length; i += 8) { data.push(parseInt(bits.substr(i, 8), 2)); } return new Uint8Array(data); } function generateEBML(json) { var ebml = []; for (var i = 0; i < json.length; i++) { var data = json[i].data; if (typeof data === 'object') { data = generateEBML(data); } if (typeof data === 'number') { data = bitsToBuffer(data.toString(2)); } if (typeof data === 'string') { data = strToBuffer(data); } var len = data.size || data.byteLength || data.length; var zeroes = Math.ceil(Math.ceil(Math.log(len) / Math.log(2)) / 8); var sizeToString = len.toString(2); var padded = (new Array((zeroes * 7 + 7 + 1) - sizeToString.length)).join('0') + sizeToString; var size = (new Array(zeroes)).join('0') + '1' + padded; ebml.push(numToBuffer(json[i].id)); ebml.push(bitsToBuffer(size)); ebml.push(data); } return new Blob(ebml, { type: 'video/webm' }); } function toBinStrOld(bits) { var data = ''; var pad = (bits.length % 8) ? (new Array(1 + 8 - (bits.length % 8))).join('0') : ''; bits = pad + bits; for (var i = 0; i < bits.length; i += 8) { data += String.fromCharCode(parseInt(bits.substr(i, 8), 2)); } return data; } function makeSimpleBlock(data) { var flags = 0; if (data.keyframe) { flags |= 128; } if (data.invisible) { flags |= 8; } if (data.lacing) { flags |= (data.lacing << 1); } if (data.discardable) { flags |= 1; } if (data.trackNum > 127) { throw 'TrackNumber > 127 not supported'; } var out = [data.trackNum | 0x80, data.timecode >> 8, data.timecode & 0xff, flags].map(function(e) { return String.fromCharCode(e); }).join('') + data.frame; return out; } function parseWebP(riff) { var VP8 = riff.RIFF[0].WEBP[0]; var frameStart = VP8.indexOf('\x9d\x01\x2a'); // A VP8 keyframe starts with the 0x9d012a header for (var i = 0, c = []; i < 4; i++) { c[i] = VP8.charCodeAt(frameStart + 3 + i); } var width, height, tmp; //the code below is literally copied verbatim from the bitstream spec tmp = (c[1] << 8) | c[0]; width = tmp & 0x3FFF; tmp = (c[3] << 8) | c[2]; height = tmp & 0x3FFF; return { width: width, height: height, data: VP8, riff: riff }; } function getStrLength(string, offset) { return parseInt(string.substr(offset + 4, 4).split('').map(function(i) { var unpadded = i.charCodeAt(0).toString(2); return (new Array(8 - unpadded.length + 1)).join('0') + unpadded; }).join(''), 2); } function parseRIFF(string) { var offset = 0; var chunks = {}; while (offset < string.length) { var id = string.substr(offset, 4); var len = getStrLength(string, offset); var data = string.substr(offset + 4 + 4, len); offset += 4 + 4 + len; chunks[id] = chunks[id] || []; if (id === 'RIFF' || id === 'LIST') { chunks[id].push(parseRIFF(data)); } else { chunks[id].push(data); } } return chunks; } function doubleToString(num) { return [].slice.call( new Uint8Array((new Float64Array([num])).buffer), 0).map(function(e) { return String.fromCharCode(e); }).reverse().join(''); } var webm = new ArrayToWebM(frames.map(function(frame) { var webp = parseWebP(parseRIFF(atob(frame.image.slice(23)))); webp.duration = frame.duration; return webp; })); postMessage(webm); } /** * Encodes frames in WebM container. It uses WebWorkinvoke to invoke 'ArrayToWebM' method. * @param {function} callback - Callback function, that is used to pass recorded blob back to the callee. * @method * @memberof Whammy * @example * recorder = new Whammy().Video(0.8, 100); * recorder.compile(function(blob) { * // blob.size - blob.type * }); */ WhammyVideo.prototype.compile = function(callback) { var webWorker = processInWebWorker(whammyInWebWorker); webWorker.onmessage = function(event) { if (event.data.error) { console.error(event.data.error); return; } callback(event.data); }; webWorker.postMessage(this.frames); }; return { /** * A more abstract-ish API. * @method * @memberof Whammy * @example * recorder = new Whammy().Video(0.8, 100); * @param {?number} speed - 0.8 * @param {?number} quality - 100 */ Video: WhammyVideo }; })(); if (typeof RecordRTC !== 'undefined') { RecordRTC.Whammy = Whammy; } // ______________ (indexed-db) // DiskStorage.js /** * DiskStorage is a standalone object used by {@link RecordRTC} to store recorded blobs in IndexedDB storage. * @summary Writing blobs into IndexedDB. * @license {@link https://github.com/muaz-khan/RecordRTC/blob/master/LICENSE|MIT} * @author {@link https://MuazKhan.com|Muaz Khan} * @example * DiskStorage.Store({ * audioBlob: yourAudioBlob, * videoBlob: yourVideoBlob, * gifBlob : yourGifBlob * }); * DiskStorage.Fetch(function(dataURL, type) { * if(type === 'audioBlob') { } * if(type === 'videoBlob') { } * if(type === 'gifBlob') { } * }); * // DiskStorage.dataStoreName = 'recordRTC'; * // DiskStorage.onError = function(error) { }; * @property {function} init - This method must be called once to initialize IndexedDB ObjectStore. Though, it is auto-used internally. * @property {function} Fetch - This method fetches stored blobs from IndexedDB. * @property {function} Store - This method stores blobs in IndexedDB. * @property {function} onError - This function is invoked for any known/unknown error. * @property {string} dataStoreName - Name of the ObjectStore created in IndexedDB storage. * @see {@link https://github.com/muaz-khan/RecordRTC|RecordRTC Source Code} */ var DiskStorage = { /** * This method must be called once to initialize IndexedDB ObjectStore. Though, it is auto-used internally. * @method * @memberof DiskStorage * @internal * @example * DiskStorage.init(); */ init: function() { var self = this; if (typeof indexedDB === 'undefined' || typeof indexedDB.open === 'undefined') { console.error('IndexedDB API are not available in this browser.'); return; } var dbVersion = 1; var dbName = this.dbName || location.href.replace(/\/|:|#|%|\.|\[|\]/g, ''), db; var request = indexedDB.open(dbName, dbVersion); function createObjectStore(dataBase) { dataBase.createObjectStore(self.dataStoreName); } function putInDB() { var transaction = db.transaction([self.dataStoreName], 'readwrite'); if (self.videoBlob) { transaction.objectStore(self.dataStoreName).put(self.videoBlob, 'videoBlob'); } if (self.gifBlob) { transaction.objectStore(self.dataStoreName).put(self.gifBlob, 'gifBlob'); } if (self.audioBlob) { transaction.objectStore(self.dataStoreName).put(self.audioBlob, 'audioBlob'); } function getFromStore(portionName) { transaction.objectStore(self.dataStoreName).get(portionName).onsuccess = function(event) { if (self.callback) { self.callback(event.target.result, portionName); } }; } getFromStore('audioBlob'); getFromStore('videoBlob'); getFromStore('gifBlob'); } request.onerror = self.onError; request.onsuccess = function() { db = request.result; db.onerror = self.onError; if (db.setVersion) { if (db.version !== dbVersion) { var setVersion = db.setVersion(dbVersion); setVersion.onsuccess = function() { createObjectStore(db); putInDB(); }; } else { putInDB(); } } else { putInDB(); } }; request.onupgradeneeded = function(event) { createObjectStore(event.target.result); }; }, /** * This method fetches stored blobs from IndexedDB. * @method * @memberof DiskStorage * @internal * @example * DiskStorage.Fetch(function(dataURL, type) { * if(type === 'audioBlob') { } * if(type === 'videoBlob') { } * if(type === 'gifBlob') { } * }); */ Fetch: function(callback) { this.callback = callback; this.init(); return this; }, /** * This method stores blobs in IndexedDB. * @method * @memberof DiskStorage * @internal * @example * DiskStorage.Store({ * audioBlob: yourAudioBlob, * videoBlob: yourVideoBlob, * gifBlob : yourGifBlob * }); */ Store: function(config) { this.audioBlob = config.audioBlob; this.videoBlob = config.videoBlob; this.gifBlob = config.gifBlob; this.init(); return this; }, /** * This function is invoked for any known/unknown error. * @method * @memberof DiskStorage * @internal * @example * DiskStorage.onError = function(error){ * alerot( JSON.stringify(error) ); * }; */ onError: function(error) { console.error(JSON.stringify(error, null, '\t')); }, /** * @property {string} dataStoreName - Name of the ObjectStore created in IndexedDB storage. * @memberof DiskStorage * @internal * @example * DiskStorage.dataStoreName = 'recordRTC'; */ dataStoreName: 'recordRTC', dbName: null }; if (typeof RecordRTC !== 'undefined') { RecordRTC.DiskStorage = DiskStorage; } // ______________ // GifRecorder.js /** * GifRecorder is standalone calss used by {@link RecordRTC} to record video or canvas into animated gif. * @license {@link https://github.com/muaz-khan/RecordRTC/blob/master/LICENSE|MIT} * @author {@link https://MuazKhan.com|Muaz Khan} * @typedef GifRecorder * @class * @example * var recorder = new GifRecorder(mediaStream || canvas || context, { onGifPreview: function, onGifRecordingStarted: function, width: 1280, height: 720, frameRate: 200, quality: 10 }); * recorder.record(); * recorder.stop(function(blob) { * img.src = URL.createObjectURL(blob); * }); * @see {@link https://github.com/muaz-khan/RecordRTC|RecordRTC Source Code} * @param {MediaStream} mediaStream - MediaStream object or HTMLCanvasElement or CanvasRenderingContext2D. * @param {object} config - {disableLogs:true, initCallback: function, width: 320, height: 240, frameRate: 200, quality: 10} */ function GifRecorder(mediaStream, config) { if (typeof GIFEncoder === 'undefined') { var script = document.createElement('script'); script.src = 'https://www.webrtc-experiment.com/gif-recorder.js'; (document.body || document.documentElement).appendChild(script); } config = config || {}; var isHTMLObject = mediaStream instanceof CanvasRenderingContext2D || mediaStream instanceof HTMLCanvasElement; /** * This method records MediaStream. * @method * @memberof GifRecorder * @example * recorder.record(); */ this.record = function() { if (typeof GIFEncoder === 'undefined') { setTimeout(self.record, 1000); return; } if (!isLoadedMetaData) { setTimeout(self.record, 1000); return; } if (!isHTMLObject) { if (!config.width) { config.width = video.offsetWidth || 320; } if (!config.height) { config.height = video.offsetHeight || 240; } if (!config.video) { config.video = { width: config.width, height: config.height }; } if (!config.canvas) { config.canvas = { width: config.width, height: config.height }; } canvas.width = config.canvas.width || 320; canvas.height = config.canvas.height || 240; video.width = config.video.width || 320; video.height = config.video.height || 240; } // external library to record as GIF images gifEncoder = new GIFEncoder(); // void setRepeat(int iter) // Sets the number of times the set of GIF frames should be played. // Default is 1; 0 means play indefinitely. gifEncoder.setRepeat(0); // void setFrameRate(Number fps) // Sets frame rate in frames per second. // Equivalent to setDelay(1000/fps). // Using "setDelay" instead of "setFrameRate" gifEncoder.setDelay(config.frameRate || 200); // void setQuality(int quality) // Sets quality of color quantization (conversion of images to the // maximum 256 colors allowed by the GIF specification). // Lower values (minimum = 1) produce better colors, // but slow processing significantly. 10 is the default, // and produces good color mapping at reasonable speeds. // Values greater than 20 do not yield significant improvements in speed. gifEncoder.setQuality(config.quality || 10); // Boolean start() // This writes the GIF Header and returns false if it fails. gifEncoder.start(); if (typeof config.onGifRecordingStarted === 'function') { config.onGifRecordingStarted(); } startTime = Date.now(); function drawVideoFrame(time) { if (self.clearedRecordedData === true) { return; } if (isPausedRecording) { return setTimeout(function() { drawVideoFrame(time); }, 100); } lastAnimationFrame = requestAnimationFrame(drawVideoFrame); if (typeof lastFrameTime === undefined) { lastFrameTime = time; } // ~10 fps if (time - lastFrameTime < 90) { return; } if (!isHTMLObject && video.paused) { // via: https://github.com/muaz-khan/WebRTC-Experiment/pull/316 // Tweak for Android Chrome video.play(); } if (!isHTMLObject) { context.drawImage(video, 0, 0, canvas.width, canvas.height); } if (config.onGifPreview) { config.onGifPreview(canvas.toDataURL('image/png')); } gifEncoder.addFrame(context); lastFrameTime = time; } lastAnimationFrame = requestAnimationFrame(drawVideoFrame); if (config.initCallback) { config.initCallback(); } }; /** * This method stops recording MediaStream. * @param {function} callback - Callback function, that is used to pass recorded blob back to the callee. * @method * @memberof GifRecorder * @example * recorder.stop(function(blob) { * img.src = URL.createObjectURL(blob); * }); */ this.stop = function(callback) { callback = callback || function() {}; if (lastAnimationFrame) { cancelAnimationFrame(lastAnimationFrame); } endTime = Date.now(); /** * @property {Blob} blob - The recorded blob object. * @memberof GifRecorder * @example * recorder.stop(function(){ * var blob = recorder.blob; * }); */ this.blob = new Blob([new Uint8Array(gifEncoder.stream().bin)], { type: 'image/gif' }); callback(this.blob); // bug: find a way to clear old recorded blobs gifEncoder.stream().bin = []; }; var isPausedRecording = false; /** * This method pauses the recording process. * @method * @memberof GifRecorder * @example * recorder.pause(); */ this.pause = function() { isPausedRecording = true; }; /** * This method resumes the recording process. * @method * @memberof GifRecorder * @example * recorder.resume(); */ this.resume = function() { isPausedRecording = false; }; /** * This method resets currently recorded data. * @method * @memberof GifRecorder * @example * recorder.clearRecordedData(); */ this.clearRecordedData = function() { self.clearedRecordedData = true; clearRecordedDataCB(); }; function clearRecordedDataCB() { if (gifEncoder) { gifEncoder.stream().bin = []; } } // for debugging this.name = 'GifRecorder'; this.toString = function() { return this.name; }; var canvas = document.createElement('canvas'); var context = canvas.getContext('2d'); if (isHTMLObject) { if (mediaStream instanceof CanvasRenderingContext2D) { context = mediaStream; canvas = context.canvas; } else if (mediaStream instanceof HTMLCanvasElement) { context = mediaStream.getContext('2d'); canvas = mediaStream; } } var isLoadedMetaData = true; if (!isHTMLObject) { var video = document.createElement('video'); video.muted = true; video.autoplay = true; video.playsInline = true; isLoadedMetaData = false; video.onloadedmetadata = function() { isLoadedMetaData = true; }; setSrcObject(mediaStream, video); video.play(); } var lastAnimationFrame = null; var startTime, endTime, lastFrameTime; var gifEncoder; var self = this; } if (typeof RecordRTC !== 'undefined') { RecordRTC.GifRecorder = GifRecorder; } // Last time updated: 2019-06-21 4:09:42 AM UTC // ________________________ // MultiStreamsMixer v1.2.2 // Open-Sourced: https://github.com/muaz-khan/MultiStreamsMixer // -------------------------------------------------- // Muaz Khan - www.MuazKhan.com // MIT License - www.WebRTC-Experiment.com/licence // -------------------------------------------------- function MultiStreamsMixer(arrayOfMediaStreams, elementClass) { var browserFakeUserAgent = 'Fake/5.0 (FakeOS) AppleWebKit/123 (KHTML, like Gecko) Fake/12.3.4567.89 Fake/123.45'; (function(that) { if (typeof RecordRTC !== 'undefined') { return; } if (!that) { return; } if (typeof window !== 'undefined') { return; } if (typeof global === 'undefined') { return; } global.navigator = { userAgent: browserFakeUserAgent, getUserMedia: function() {} }; if (!global.console) { global.console = {}; } if (typeof global.console.log === 'undefined' || typeof global.console.error === 'undefined') { global.console.error = global.console.log = global.console.log || function() { console.log(arguments); }; } if (typeof document === 'undefined') { /*global document:true */ that.document = { documentElement: { appendChild: function() { return ''; } } }; document.createElement = document.captureStream = document.mozCaptureStream = function() { var obj = { getContext: function() { return obj; }, play: function() {}, pause: function() {}, drawImage: function() {}, toDataURL: function() { return ''; }, style: {} }; return obj; }; that.HTMLVideoElement = function() {}; } if (typeof location === 'undefined') { /*global location:true */ that.location = { protocol: 'file:', href: '', hash: '' }; } if (typeof screen === 'undefined') { /*global screen:true */ that.screen = { width: 0, height: 0 }; } if (typeof URL === 'undefined') { /*global screen:true */ that.URL = { createObjectURL: function() { return ''; }, revokeObjectURL: function() { return ''; } }; } /*global window:true */ that.window = global; })(typeof global !== 'undefined' ? global : null); // requires: chrome://flags/#enable-experimental-web-platform-features elementClass = elementClass || 'multi-streams-mixer'; var videos = []; var isStopDrawingFrames = false; var canvas = document.createElement('canvas'); var context = canvas.getContext('2d'); canvas.style.opacity = 0; canvas.style.position = 'absolute'; canvas.style.zIndex = -1; canvas.style.top = '-1000em'; canvas.style.left = '-1000em'; canvas.className = elementClass; (document.body || document.documentElement).appendChild(canvas); this.disableLogs = false; this.frameInterval = 10; this.width = 360; this.height = 240; // use gain node to prevent echo this.useGainNode = true; var self = this; // _____________________________ // Cross-Browser-Declarations.js // WebAudio API representer var AudioContext = window.AudioContext; if (typeof AudioContext === 'undefined') { if (typeof webkitAudioContext !== 'undefined') { /*global AudioContext:true */ AudioContext = webkitAudioContext; } if (typeof mozAudioContext !== 'undefined') { /*global AudioContext:true */ AudioContext = mozAudioContext; } } /*jshint -W079 */ var URL = window.URL; if (typeof URL === 'undefined' && typeof webkitURL !== 'undefined') { /*global URL:true */ URL = webkitURL; } if (typeof navigator !== 'undefined' && typeof navigator.getUserMedia === 'undefined') { // maybe window.navigator? if (typeof navigator.webkitGetUserMedia !== 'undefined') { navigator.getUserMedia = navigator.webkitGetUserMedia; } if (typeof navigator.mozGetUserMedia !== 'undefined') { navigator.getUserMedia = navigator.mozGetUserMedia; } } var MediaStream = window.MediaStream; if (typeof MediaStream === 'undefined' && typeof webkitMediaStream !== 'undefined') { MediaStream = webkitMediaStream; } /*global MediaStream:true */ if (typeof MediaStream !== 'undefined') { // override "stop" method for all browsers if (typeof MediaStream.prototype.stop === 'undefined') { MediaStream.prototype.stop = function() { this.getTracks().forEach(function(track) { track.stop(); }); }; } } var Storage = {}; if (typeof AudioContext !== 'undefined') { Storage.AudioContext = AudioContext; } else if (typeof webkitAudioContext !== 'undefined') { Storage.AudioContext = webkitAudioContext; } function setSrcObject(stream, element) { if ('srcObject' in element) { element.srcObject = stream; } else if ('mozSrcObject' in element) { element.mozSrcObject = stream; } else { element.srcObject = stream; } } this.startDrawingFrames = function() { drawVideosToCanvas(); }; function drawVideosToCanvas() { if (isStopDrawingFrames) { return; } var videosLength = videos.length; var fullcanvas = false; var remaining = []; videos.forEach(function(video) { if (!video.stream) { video.stream = {}; } if (video.stream.fullcanvas) { fullcanvas = video; } else { // todo: video.stream.active or video.stream.live to fix blank frames issues? remaining.push(video); } }); if (fullcanvas) { canvas.width = fullcanvas.stream.width; canvas.height = fullcanvas.stream.height; } else if (remaining.length) { canvas.width = videosLength > 1 ? remaining[0].width * 2 : remaining[0].width; var height = 1; if (videosLength === 3 || videosLength === 4) { height = 2; } if (videosLength === 5 || videosLength === 6) { height = 3; } if (videosLength === 7 || videosLength === 8) { height = 4; } if (videosLength === 9 || videosLength === 10) { height = 5; } canvas.height = remaining[0].height * height; } else { canvas.width = self.width || 360; canvas.height = self.height || 240; } if (fullcanvas && fullcanvas instanceof HTMLVideoElement) { drawImage(fullcanvas); } remaining.forEach(function(video, idx) { drawImage(video, idx); }); setTimeout(drawVideosToCanvas, self.frameInterval); } function drawImage(video, idx) { if (isStopDrawingFrames) { return; } var x = 0; var y = 0; var width = video.width; var height = video.height; if (idx === 1) { x = video.width; } if (idx === 2) { y = video.height; } if (idx === 3) { x = video.width; y = video.height; } if (idx === 4) { y = video.height * 2; } if (idx === 5) { x = video.width; y = video.height * 2; } if (idx === 6) { y = video.height * 3; } if (idx === 7) { x = video.width; y = video.height * 3; } if (typeof video.stream.left !== 'undefined') { x = video.stream.left; } if (typeof video.stream.top !== 'undefined') { y = video.stream.top; } if (typeof video.stream.width !== 'undefined') { width = video.stream.width; } if (typeof video.stream.height !== 'undefined') { height = video.stream.height; } context.drawImage(video, x, y, width, height); if (typeof video.stream.onRender === 'function') { video.stream.onRender(context, x, y, width, height, idx); } } function getMixedStream() { isStopDrawingFrames = false; var mixedVideoStream = getMixedVideoStream(); var mixedAudioStream = getMixedAudioStream(); if (mixedAudioStream) { mixedAudioStream.getTracks().filter(function(t) { return t.kind === 'audio'; }).forEach(function(track) { mixedVideoStream.addTrack(track); }); } var fullcanvas; arrayOfMediaStreams.forEach(function(stream) { if (stream.fullcanvas) { fullcanvas = true; } }); // mixedVideoStream.prototype.appendStreams = appendStreams; // mixedVideoStream.prototype.resetVideoStreams = resetVideoStreams; // mixedVideoStream.prototype.clearRecordedData = clearRecordedData; return mixedVideoStream; } function getMixedVideoStream() { resetVideoStreams(); var capturedStream; if ('captureStream' in canvas) { capturedStream = canvas.captureStream(); } else if ('mozCaptureStream' in canvas) { capturedStream = canvas.mozCaptureStream(); } else if (!self.disableLogs) { console.error('Upgrade to latest Chrome or otherwise enable this flag: chrome://flags/#enable-experimental-web-platform-features'); } var videoStream = new MediaStream(); capturedStream.getTracks().filter(function(t) { return t.kind === 'video'; }).forEach(function(track) { videoStream.addTrack(track); }); canvas.stream = videoStream; return videoStream; } function getMixedAudioStream() { // via: @pehrsons if (!Storage.AudioContextConstructor) { Storage.AudioContextConstructor = new Storage.AudioContext(); } self.audioContext = Storage.AudioContextConstructor; self.audioSources = []; if (self.useGainNode === true) { self.gainNode = self.audioContext.createGain(); self.gainNode.connect(self.audioContext.destination); self.gainNode.gain.value = 0; // don't hear self } var audioTracksLength = 0; arrayOfMediaStreams.forEach(function(stream) { if (!stream.getTracks().filter(function(t) { return t.kind === 'audio'; }).length) { return; } audioTracksLength++; var audioSource = self.audioContext.createMediaStreamSource(stream); if (self.useGainNode === true) { audioSource.connect(self.gainNode); } self.audioSources.push(audioSource); }); if (!audioTracksLength) { // because "self.audioContext" is not initialized // that's why we've to ignore rest of the code return; } self.audioDestination = self.audioContext.createMediaStreamDestination(); self.audioSources.forEach(function(audioSource) { audioSource.connect(self.audioDestination); }); return self.audioDestination.stream; } function getVideo(stream) { var video = document.createElement('video'); setSrcObject(stream, video); video.className = elementClass; video.muted = true; video.volume = 0; video.width = stream.width || self.width || 360; video.height = stream.height || self.height || 240; video.play(); return video; } this.appendStreams = function(streams) { if (!streams) { throw 'First parameter is required.'; } if (!(streams instanceof Array)) { streams = [streams]; } streams.forEach(function(stream) { var newStream = new MediaStream(); if (stream.getTracks().filter(function(t) { return t.kind === 'video'; }).length) { var video = getVideo(stream); video.stream = stream; videos.push(video); newStream.addTrack(stream.getTracks().filter(function(t) { return t.kind === 'video'; })[0]); } if (stream.getTracks().filter(function(t) { return t.kind === 'audio'; }).length) { var audioSource = self.audioContext.createMediaStreamSource(stream); self.audioDestination = self.audioContext.createMediaStreamDestination(); audioSource.connect(self.audioDestination); newStream.addTrack(self.audioDestination.stream.getTracks().filter(function(t) { return t.kind === 'audio'; })[0]); } arrayOfMediaStreams.push(newStream); }); }; this.releaseStreams = function() { videos = []; isStopDrawingFrames = true; if (self.gainNode) { self.gainNode.disconnect(); self.gainNode = null; } if (self.audioSources.length) { self.audioSources.forEach(function(source) { source.disconnect(); }); self.audioSources = []; } if (self.audioDestination) { self.audioDestination.disconnect(); self.audioDestination = null; } if (self.audioContext) { self.audioContext.close(); } self.audioContext = null; context.clearRect(0, 0, canvas.width, canvas.height); if (canvas.stream) { canvas.stream.stop(); canvas.stream = null; } }; this.resetVideoStreams = function(streams) { if (streams && !(streams instanceof Array)) { streams = [streams]; } resetVideoStreams(streams); }; function resetVideoStreams(streams) { videos = []; streams = streams || arrayOfMediaStreams; // via: @adrian-ber streams.forEach(function(stream) { if (!stream.getTracks().filter(function(t) { return t.kind === 'video'; }).length) { return; } var video = getVideo(stream); video.stream = stream; videos.push(video); }); } // for debugging this.name = 'MultiStreamsMixer'; this.toString = function() { return this.name; }; this.getMixedStream = getMixedStream; } if (typeof RecordRTC === 'undefined') { if (typeof module !== 'undefined' /* && !!module.exports*/ ) { module.exports = MultiStreamsMixer; } if (typeof define === 'function' && define.amd) { define('MultiStreamsMixer', [], function() { return MultiStreamsMixer; }); } } // ______________________ // MultiStreamRecorder.js /* * Video conference recording, using captureStream API along with WebAudio and Canvas2D API. */ /** * MultiStreamRecorder can record multiple videos in single container. * @summary Multi-videos recorder. * @license {@link https://github.com/muaz-khan/RecordRTC/blob/master/LICENSE|MIT} * @author {@link https://MuazKhan.com|Muaz Khan} * @typedef MultiStreamRecorder * @class * @example * var options = { * mimeType: 'video/webm' * } * var recorder = new MultiStreamRecorder(ArrayOfMediaStreams, options); * recorder.record(); * recorder.stop(function(blob) { * video.src = URL.createObjectURL(blob); * * // or * var blob = recorder.blob; * }); * @see {@link https://github.com/muaz-khan/RecordRTC|RecordRTC Source Code} * @param {MediaStreams} mediaStreams - Array of MediaStreams. * @param {object} config - {disableLogs:true, frameInterval: 1, mimeType: "video/webm"} */ function MultiStreamRecorder(arrayOfMediaStreams, options) { arrayOfMediaStreams = arrayOfMediaStreams || []; var self = this; var mixer; var mediaRecorder; options = options || { elementClass: 'multi-streams-mixer', mimeType: 'video/webm', video: { width: 360, height: 240 } }; if (!options.frameInterval) { options.frameInterval = 10; } if (!options.video) { options.video = {}; } if (!options.video.width) { options.video.width = 360; } if (!options.video.height) { options.video.height = 240; } /** * This method records all MediaStreams. * @method * @memberof MultiStreamRecorder * @example * recorder.record(); */ this.record = function() { // github/muaz-khan/MultiStreamsMixer mixer = new MultiStreamsMixer(arrayOfMediaStreams, options.elementClass || 'multi-streams-mixer'); if (getAllVideoTracks().length) { mixer.frameInterval = options.frameInterval || 10; mixer.width = options.video.width || 360; mixer.height = options.video.height || 240; mixer.startDrawingFrames(); } if (options.previewStream && typeof options.previewStream === 'function') { options.previewStream(mixer.getMixedStream()); } // record using MediaRecorder API mediaRecorder = new MediaStreamRecorder(mixer.getMixedStream(), options); mediaRecorder.record(); }; function getAllVideoTracks() { var tracks = []; arrayOfMediaStreams.forEach(function(stream) { getTracks(stream, 'video').forEach(function(track) { tracks.push(track); }); }); return tracks; } /** * This method stops recording MediaStream. * @param {function} callback - Callback function, that is used to pass recorded blob back to the callee. * @method * @memberof MultiStreamRecorder * @example * recorder.stop(function(blob) { * video.src = URL.createObjectURL(blob); * }); */ this.stop = function(callback) { if (!mediaRecorder) { return; } mediaRecorder.stop(function(blob) { self.blob = blob; callback(blob); self.clearRecordedData(); }); }; /** * This method pauses the recording process. * @method * @memberof MultiStreamRecorder * @example * recorder.pause(); */ this.pause = function() { if (mediaRecorder) { mediaRecorder.pause(); } }; /** * This method resumes the recording process. * @method * @memberof MultiStreamRecorder * @example * recorder.resume(); */ this.resume = function() { if (mediaRecorder) { mediaRecorder.resume(); } }; /** * This method resets currently recorded data. * @method * @memberof MultiStreamRecorder * @example * recorder.clearRecordedData(); */ this.clearRecordedData = function() { if (mediaRecorder) { mediaRecorder.clearRecordedData(); mediaRecorder = null; } if (mixer) { mixer.releaseStreams(); mixer = null; } }; /** * Add extra media-streams to existing recordings. * @method * @memberof MultiStreamRecorder * @param {MediaStreams} mediaStreams - Array of MediaStreams * @example * recorder.addStreams([newAudioStream, newVideoStream]); */ this.addStreams = function(streams) { if (!streams) { throw 'First parameter is required.'; } if (!(streams instanceof Array)) { streams = [streams]; } arrayOfMediaStreams.concat(streams); if (!mediaRecorder || !mixer) { return; } mixer.appendStreams(streams); if (options.previewStream && typeof options.previewStream === 'function') { options.previewStream(mixer.getMixedStream()); } }; /** * Reset videos during live recording. Replace old videos e.g. replace cameras with full-screen. * @method * @memberof MultiStreamRecorder * @param {MediaStreams} mediaStreams - Array of MediaStreams * @example * recorder.resetVideoStreams([newVideo1, newVideo2]); */ this.resetVideoStreams = function(streams) { if (!mixer) { return; } if (streams && !(streams instanceof Array)) { streams = [streams]; } mixer.resetVideoStreams(streams); }; /** * Returns MultiStreamsMixer * @method * @memberof MultiStreamRecorder * @example * let mixer = recorder.getMixer(); * mixer.appendStreams([newStream]); */ this.getMixer = function() { return mixer; }; // for debugging this.name = 'MultiStreamRecorder'; this.toString = function() { return this.name; }; } if (typeof RecordRTC !== 'undefined') { RecordRTC.MultiStreamRecorder = MultiStreamRecorder; } // _____________________ // RecordRTC.promises.js /** * RecordRTCPromisesHandler adds promises support in {@link RecordRTC}. Try a {@link https://github.com/muaz-khan/RecordRTC/blob/master/simple-demos/RecordRTCPromisesHandler.html|demo here} * @summary Promises for {@link RecordRTC} * @license {@link https://github.com/muaz-khan/RecordRTC/blob/master/LICENSE|MIT} * @author {@link https://MuazKhan.com|Muaz Khan} * @typedef RecordRTCPromisesHandler * @class * @example * var recorder = new RecordRTCPromisesHandler(mediaStream, options); * recorder.startRecording() * .then(successCB) * .catch(errorCB); * // Note: You can access all RecordRTC API using "recorder.recordRTC" e.g. * recorder.recordRTC.onStateChanged = function(state) {}; * recorder.recordRTC.setRecordingDuration(5000); * @see {@link https://github.com/muaz-khan/RecordRTC|RecordRTC Source Code} * @param {MediaStream} mediaStream - Single media-stream object, array of media-streams, html-canvas-element, etc. * @param {object} config - {type:"video", recorderType: MediaStreamRecorder, disableLogs: true, numberOfAudioChannels: 1, bufferSize: 0, sampleRate: 0, video: HTMLVideoElement, etc.} * @throws Will throw an error if "new" keyword is not used to initiate "RecordRTCPromisesHandler". Also throws error if first argument "MediaStream" is missing. * @requires {@link RecordRTC} */ function RecordRTCPromisesHandler(mediaStream, options) { if (!this) { throw 'Use "new RecordRTCPromisesHandler()"'; } if (typeof mediaStream === 'undefined') { throw 'First argument "MediaStream" is required.'; } var self = this; /** * @property {Blob} blob - Access/reach the native {@link RecordRTC} object. * @memberof RecordRTCPromisesHandler * @example * let internal = recorder.recordRTC.getInternalRecorder(); * alert(internal instanceof MediaStreamRecorder); * recorder.recordRTC.onStateChanged = function(state) {}; */ self.recordRTC = new RecordRTC(mediaStream, options); /** * This method records MediaStream. * @method * @memberof RecordRTCPromisesHandler * @example * recorder.startRecording() * .then(successCB) * .catch(errorCB); */ this.startRecording = function() { return new Promise(function(resolve, reject) { try { self.recordRTC.startRecording(); resolve(); } catch (e) { reject(e); } }); }; /** * This method stops the recording. * @method * @memberof RecordRTCPromisesHandler * @example * recorder.stopRecording().then(function() { * var blob = recorder.getBlob(); * }).catch(errorCB); */ this.stopRecording = function() { return new Promise(function(resolve, reject) { try { self.recordRTC.stopRecording(function(url) { self.blob = self.recordRTC.getBlob(); if (!self.blob || !self.blob.size) { reject('Empty blob.', self.blob); return; } resolve(url); }); } catch (e) { reject(e); } }); }; /** * This method pauses the recording. You can resume recording using "resumeRecording" method. * @method * @memberof RecordRTCPromisesHandler * @example * recorder.pauseRecording() * .then(successCB) * .catch(errorCB); */ this.pauseRecording = function() { return new Promise(function(resolve, reject) { try { self.recordRTC.pauseRecording(); resolve(); } catch (e) { reject(e); } }); }; /** * This method resumes the recording. * @method * @memberof RecordRTCPromisesHandler * @example * recorder.resumeRecording() * .then(successCB) * .catch(errorCB); */ this.resumeRecording = function() { return new Promise(function(resolve, reject) { try { self.recordRTC.resumeRecording(); resolve(); } catch (e) { reject(e); } }); }; /** * This method returns data-url for the recorded blob. * @method * @memberof RecordRTCPromisesHandler * @example * recorder.stopRecording().then(function() { * recorder.getDataURL().then(function(dataURL) { * window.open(dataURL); * }).catch(errorCB);; * }).catch(errorCB); */ this.getDataURL = function(callback) { return new Promise(function(resolve, reject) { try { self.recordRTC.getDataURL(function(dataURL) { resolve(dataURL); }); } catch (e) { reject(e); } }); }; /** * This method returns the recorded blob. * @method * @memberof RecordRTCPromisesHandler * @example * recorder.stopRecording().then(function() { * recorder.getBlob().then(function(blob) {}) * }).catch(errorCB); */ this.getBlob = function() { return new Promise(function(resolve, reject) { try { resolve(self.recordRTC.getBlob()); } catch (e) { reject(e); } }); }; /** * This method returns the internal recording object. * @method * @memberof RecordRTCPromisesHandler * @example * let internalRecorder = await recorder.getInternalRecorder(); * if(internalRecorder instanceof MultiStreamRecorder) { * internalRecorder.addStreams([newAudioStream]); * internalRecorder.resetVideoStreams([screenStream]); * } * @returns {Object} */ this.getInternalRecorder = function() { return new Promise(function(resolve, reject) { try { resolve(self.recordRTC.getInternalRecorder()); } catch (e) { reject(e); } }); }; /** * This method resets the recorder. So that you can reuse single recorder instance many times. * @method * @memberof RecordRTCPromisesHandler * @example * await recorder.reset(); * recorder.startRecording(); // record again */ this.reset = function() { return new Promise(function(resolve, reject) { try { resolve(self.recordRTC.reset()); } catch (e) { reject(e); } }); }; /** * Destroy RecordRTC instance. Clear all recorders and objects. * @method * @memberof RecordRTCPromisesHandler * @example * recorder.destroy().then(successCB).catch(errorCB); */ this.destroy = function() { return new Promise(function(resolve, reject) { try { resolve(self.recordRTC.destroy()); } catch (e) { reject(e); } }); }; /** * Get recorder's readonly state. * @method * @memberof RecordRTCPromisesHandler * @example * let state = await recorder.getState(); * // or * recorder.getState().then(state => { console.log(state); }) * @returns {String} Returns recording state. */ this.getState = function() { return new Promise(function(resolve, reject) { try { resolve(self.recordRTC.getState()); } catch (e) { reject(e); } }); }; /** * @property {Blob} blob - Recorded data as "Blob" object. * @memberof RecordRTCPromisesHandler * @example * await recorder.stopRecording(); * let blob = recorder.getBlob(); // or "recorder.recordRTC.blob" * invokeSaveAsDialog(blob); */ this.blob = null; /** * RecordRTC version number * @property {String} version - Release version number. * @memberof RecordRTCPromisesHandler * @static * @readonly * @example * alert(recorder.version); */ this.version = '5.6.2'; } if (typeof RecordRTC !== 'undefined') { RecordRTC.RecordRTCPromisesHandler = RecordRTCPromisesHandler; } // ______________________ // WebAssemblyRecorder.js /** * WebAssemblyRecorder lets you create webm videos in JavaScript via WebAssembly. The library consumes raw RGBA32 buffers (4 bytes per pixel) and turns them into a webm video with the given framerate and quality. This makes it compatible out-of-the-box with ImageData from a CANVAS. With realtime mode you can also use webm-wasm for streaming webm videos. * @summary Video recording feature in Chrome, Firefox and maybe Edge. * @license {@link https://github.com/muaz-khan/RecordRTC/blob/master/LICENSE|MIT} * @author {@link https://MuazKhan.com|Muaz Khan} * @typedef WebAssemblyRecorder * @class * @example * var recorder = new WebAssemblyRecorder(mediaStream); * recorder.record(); * recorder.stop(function(blob) { * video.src = URL.createObjectURL(blob); * }); * @see {@link https://github.com/muaz-khan/RecordRTC|RecordRTC Source Code} * @param {MediaStream} mediaStream - MediaStream object fetched using getUserMedia API or generated using captureStreamUntilEnded or WebAudio API. * @param {object} config - {webAssemblyPath:'webm-wasm.wasm',workerPath: 'webm-worker.js', frameRate: 30, width: 1920, height: 1080, bitrate: 1024, realtime: true} */ function WebAssemblyRecorder(stream, config) { // based on: github.com/GoogleChromeLabs/webm-wasm if (typeof ReadableStream === 'undefined' || typeof WritableStream === 'undefined') { // because it fixes readable/writable streams issues console.error('Following polyfill is strongly recommended: https://unpkg.com/@mattiasbuelens/web-streams-polyfill/dist/polyfill.min.js'); } config = config || {}; config.width = config.width || 640; config.height = config.height || 480; config.frameRate = config.frameRate || 30; config.bitrate = config.bitrate || 1200; config.realtime = config.realtime || true; function createBufferURL(buffer, type) { return URL.createObjectURL(new Blob([buffer], { type: type || '' })); } var finished; function cameraStream() { return new ReadableStream({ start: function(controller) { var cvs = document.createElement('canvas'); var video = document.createElement('video'); var first = true; video.srcObject = stream; video.muted = true; video.height = config.height; video.width = config.width; video.volume = 0; video.onplaying = function() { cvs.width = config.width; cvs.height = config.height; var ctx = cvs.getContext('2d'); var frameTimeout = 1000 / config.frameRate; var cameraTimer = setInterval(function f() { if (finished) { clearInterval(cameraTimer); controller.close(); } if (first) { first = false; if (config.onVideoProcessStarted) { config.onVideoProcessStarted(); } } ctx.drawImage(video, 0, 0); if (controller._controlledReadableStream.state !== 'closed') { try { controller.enqueue( ctx.getImageData(0, 0, config.width, config.height) ); } catch (e) {} } }, frameTimeout); }; video.play(); } }); } var worker; function startRecording(stream, buffer) { if (!config.workerPath && !buffer) { finished = false; // is it safe to use @latest ? fetch( 'https://unpkg.com/webm-wasm@latest/dist/webm-worker.js' ).then(function(r) { r.arrayBuffer().then(function(buffer) { startRecording(stream, buffer); }); }); return; } if (!config.workerPath && buffer instanceof ArrayBuffer) { var blob = new Blob([buffer], { type: 'text/javascript' }); config.workerPath = URL.createObjectURL(blob); } if (!config.workerPath) { console.error('workerPath parameter is missing.'); } worker = new Worker(config.workerPath); worker.postMessage(config.webAssemblyPath || 'https://unpkg.com/webm-wasm@latest/dist/webm-wasm.wasm'); worker.addEventListener('message', function(event) { if (event.data === 'READY') { worker.postMessage({ width: config.width, height: config.height, bitrate: config.bitrate || 1200, timebaseDen: config.frameRate || 30, realtime: config.realtime }); cameraStream().pipeTo(new WritableStream({ write: function(image) { if (finished) { console.error('Got image, but recorder is finished!'); return; } worker.postMessage(image.data.buffer, [image.data.buffer]); } })); } else if (!!event.data) { if (!isPaused) { arrayOfBuffers.push(event.data); } } }); } /** * This method records video. * @method * @memberof WebAssemblyRecorder * @example * recorder.record(); */ this.record = function() { arrayOfBuffers = []; isPaused = false; this.blob = null; startRecording(stream); if (typeof config.initCallback === 'function') { config.initCallback(); } }; var isPaused; /** * This method pauses the recording process. * @method * @memberof WebAssemblyRecorder * @example * recorder.pause(); */ this.pause = function() { isPaused = true; }; /** * This method resumes the recording process. * @method * @memberof WebAssemblyRecorder * @example * recorder.resume(); */ this.resume = function() { isPaused = false; }; function terminate(callback) { if (!worker) { if (callback) { callback(); } return; } // Wait for null event data to indicate that the encoding is complete worker.addEventListener('message', function(event) { if (event.data === null) { worker.terminate(); worker = null; if (callback) { callback(); } } }); worker.postMessage(null); } var arrayOfBuffers = []; /** * This method stops recording video. * @param {function} callback - Callback function, that is used to pass recorded blob back to the callee. * @method * @memberof WebAssemblyRecorder * @example * recorder.stop(function(blob) { * video.src = URL.createObjectURL(blob); * }); */ this.stop = function(callback) { finished = true; var recorder = this; terminate(function() { recorder.blob = new Blob(arrayOfBuffers, { type: 'video/webm' }); callback(recorder.blob); }); }; // for debugging this.name = 'WebAssemblyRecorder'; this.toString = function() { return this.name; }; /** * This method resets currently recorded data. * @method * @memberof WebAssemblyRecorder * @example * recorder.clearRecordedData(); */ this.clearRecordedData = function() { arrayOfBuffers = []; isPaused = false; this.blob = null; // todo: if recording-ON then STOP it first }; /** * @property {Blob} blob - The recorded blob object. * @memberof WebAssemblyRecorder * @example * recorder.stop(function(){ * var blob = recorder.blob; * }); */ this.blob = null; } if (typeof RecordRTC !== 'undefined') { RecordRTC.WebAssemblyRecorder = WebAssemblyRecorder; }