// Last time updated: 2017-04-12 11:03:58 AM UTC // links: // Open-Sourced: https://github.com/streamproc/MediaStreamRecorder // https://cdn.WebRTC-Experiment.com/MediaStreamRecorder.js // https://www.WebRTC-Experiment.com/MediaStreamRecorder.js // npm install msr //------------------------------------ // Browsers Support:: // Chrome (all versions) [ audio/video separately ] // Firefox ( >= 29 ) [ audio/video in single webm/mp4 container or only audio in ogg ] // Opera (all versions) [ same as chrome ] // Android (Chrome) [ only video ] // Android (Opera) [ only video ] // Android (Firefox) [ only video ] // Microsoft Edge (Only Audio & Gif) //------------------------------------ // Muaz Khan - www.MuazKhan.com // MIT License - www.WebRTC-Experiment.com/licence //------------------------------------ // ______________________ // MediaStreamRecorder.js function MediaStreamRecorder(mediaStream) { if (!mediaStream) { throw 'MediaStream is mandatory.'; } // void start(optional long timeSlice) // timestamp to fire "ondataavailable" this.start = function(timeSlice) { var Recorder; if (typeof MediaRecorder !== 'undefined') { Recorder = MediaRecorderWrapper; } else if (IsChrome || IsOpera || IsEdge) { if (this.mimeType.indexOf('video') !== -1) { Recorder = WhammyRecorder; } else if (this.mimeType.indexOf('audio') !== -1) { Recorder = StereoAudioRecorder; } } // video recorder (in GIF format) if (this.mimeType === 'image/gif') { Recorder = GifRecorder; } // audio/wav is supported only via StereoAudioRecorder // audio/pcm (int16) is supported only via StereoAudioRecorder if (this.mimeType === 'audio/wav' || this.mimeType === 'audio/pcm') { Recorder = StereoAudioRecorder; } // allows forcing StereoAudioRecorder.js on Edge/Firefox if (this.recorderType) { Recorder = this.recorderType; } mediaRecorder = new Recorder(mediaStream); mediaRecorder.blobs = []; var self = this; mediaRecorder.ondataavailable = function(data) { mediaRecorder.blobs.push(data); self.ondataavailable(data); }; mediaRecorder.onstop = this.onstop; mediaRecorder.onStartedDrawingNonBlankFrames = this.onStartedDrawingNonBlankFrames; // Merge all data-types except "function" mediaRecorder = mergeProps(mediaRecorder, this); mediaRecorder.start(timeSlice); }; this.onStartedDrawingNonBlankFrames = function() {}; this.clearOldRecordedFrames = function() { if (!mediaRecorder) { return; } mediaRecorder.clearOldRecordedFrames(); }; this.stop = function() { if (mediaRecorder) { mediaRecorder.stop(); } }; this.ondataavailable = function(blob) { console.log('ondataavailable..', blob); }; this.onstop = function(error) { console.warn('stopped..', error); }; this.save = function(file, fileName) { if (!file) { if (!mediaRecorder) { return; } ConcatenateBlobs(mediaRecorder.blobs, mediaRecorder.blobs[0].type, function(concatenatedBlob) { invokeSaveAsDialog(concatenatedBlob); }); return; } invokeSaveAsDialog(file, fileName); }; this.pause = function() { if (!mediaRecorder) { return; } mediaRecorder.pause(); console.log('Paused recording.', this.mimeType || mediaRecorder.mimeType); }; this.resume = function() { if (!mediaRecorder) { return; } mediaRecorder.resume(); console.log('Resumed recording.', this.mimeType || mediaRecorder.mimeType); }; // StereoAudioRecorder || WhammyRecorder || MediaRecorderWrapper || GifRecorder this.recorderType = null; // video/webm or audio/webm or audio/ogg or audio/wav this.mimeType = 'video/webm'; // logs are enabled by default this.disableLogs = false; // Reference to "MediaRecorder.js" var mediaRecorder; } // ______________________ // MultiStreamRecorder.js function MultiStreamRecorder(arrayOfMediaStreams) { if (arrayOfMediaStreams instanceof MediaStream) { arrayOfMediaStreams = [arrayOfMediaStreams]; } var self = this; if (!this.mimeType) { this.mimeType = 'video/webm'; } if (!this.frameInterval) { this.frameInterval = 10; } if (!this.video) { this.video = {}; } if (!this.video.width) { this.video.width = 360; } if (!this.video.height) { this.video.height = 240; } this.start = function(timeSlice) { isStoppedRecording = false; var mixedVideoStream = getMixedVideoStream(); var mixedAudioStream = getMixedAudioStream(); if (mixedAudioStream) { mixedAudioStream.getAudioTracks().forEach(function(track) { mixedVideoStream.addTrack(track); }); } if (self.previewStream && typeof self.previewStream === 'function') { self.previewStream(mixedVideoStream); } mediaRecorder = new MediaStreamRecorder(mixedVideoStream); for (var prop in self) { if (typeof self[prop] !== 'function') { mediaRecorder[prop] = self[prop]; } } mediaRecorder.ondataavailable = function(blob) { self.ondataavailable(blob); }; drawVideosToCanvas(); mediaRecorder.start(timeSlice); }; this.stop = function(callback) { isStoppedRecording = true; if (!mediaRecorder) { return; } mediaRecorder.stop(function(blob) { callback(blob); }); }; function getMixedAudioStream() { // via: @pehrsons self.audioContext = new AudioContext(); var audioSources = []; var audioTracksLength = 0; arrayOfMediaStreams.forEach(function(stream) { if (!stream.getAudioTracks().length) { return; } audioTracksLength++; audioSources.push(self.audioContext.createMediaStreamSource(stream)); }); if (!audioTracksLength) { return; } self.audioDestination = self.audioContext.createMediaStreamDestination(); audioSources.forEach(function(audioSource) { audioSource.connect(self.audioDestination); }); return self.audioDestination.stream; } var videos = []; var mediaRecorder; function getMixedVideoStream() { // via: @adrian-ber arrayOfMediaStreams.forEach(function(stream) { if (!stream.getVideoTracks().length) { return; } var video = getVideo(stream); video.width = self.video.width; video.height = self.video.height; videos.push(video); }); 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(); // via #126 capturedStream.getVideoTracks().forEach(function(track) { videoStream.addTrack(track); }); return videoStream; } function getVideo(stream) { var video = document.createElement('video'); video.src = URL.createObjectURL(stream); video.play(); return video; } var isStoppedRecording = false; function drawVideosToCanvas() { if (isStoppedRecording) { return; } var videosLength = videos.length; canvas.width = videosLength > 1 ? videos[0].width * 2 : videos[0].width; canvas.height = videosLength > 2 ? videos[0].height * 2 : videos[0].height; videos.forEach(function(video, idx) { if (videosLength === 1) { context.drawImage(video, 0, 0, video.width, video.height); return; } if (videosLength === 2) { var x = 0; var y = 0; if (idx === 1) { x = video.width; } context.drawImage(video, x, y, video.width, video.height); return; } if (videosLength === 3) { var x = 0; var y = 0; if (idx === 1) { x = video.width; } if (idx === 2) { y = video.height; } context.drawImage(video, x, y, video.width, video.height); return; } if (videosLength === 4) { var x = 0; var y = 0; if (idx === 1) { x = video.width; } if (idx === 2) { y = video.height; } if (idx === 3) { x = video.width; y = video.height; } context.drawImage(video, x, y, video.width, video.height); return; } }); setTimeout(drawVideosToCanvas, self.frameInterval); } var canvas = document.createElement('canvas'); var context = canvas.getContext('2d'); canvas.style = 'opacity:0;position:absolute;z-index:-1;top: -100000000;left:-1000000000;'; (document.body || document.documentElement).appendChild(canvas); this.pause = function() { if (mediaRecorder) { mediaRecorder.pause(); } }; this.resume = function() { if (mediaRecorder) { mediaRecorder.resume(); } }; this.clearRecordedData = function() { videos = []; context.clearRect(0, 0, canvas.width, canvas.height); isStoppedRecording = false; mediaRecorder = null; if (mediaRecorder) { mediaRecorder.clearRecordedData(); } }; this.addStream = function(stream) { if (stream instanceof Array && stream.length) { stream.forEach(this.addStream); return; } arrayOfMediaStreams.push(stream); if (!mediaRecorder) { return; } if (stream.getVideoTracks().length) { var video = getVideo(stream); video.width = self.video.width; video.height = self.video.height; videos.push(video); } if (stream.getAudioTracks().length && self.audioContext) { var audioSource = self.audioContext.createMediaStreamSource(stream); audioSource.connect(self.audioDestination); } }; this.ondataavailable = function(blob) { if (self.disableLogs) { return; } console.log('ondataavailable', blob); }; } if (typeof MediaStreamRecorder !== 'undefined') { MediaStreamRecorder.MultiStreamRecorder = MultiStreamRecorder; } // _____________________________ // Cross-Browser-Declarations.js var browserFakeUserAgent = 'Fake/5.0 (FakeOS) AppleWebKit/123 (KHTML, like Gecko) Fake/12.3.4567.89 Fake/123.45'; (function(that) { if (typeof window !== 'undefined') { return; } if (typeof window === 'undefined' && typeof global !== 'undefined') { global.navigator = { userAgent: browserFakeUserAgent, getUserMedia: function() {} }; /*global window:true */ that.window = global; } else if (typeof window === 'undefined') { // window = this; } if (typeof document === 'undefined') { /*global document:true */ that.document = {}; document.createElement = document.captureStream = document.mozCaptureStream = function() { return {}; }; } 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 }; } })(typeof global !== 'undefined' ? global : window); // 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; } } if (typeof window === 'undefined') { /*jshint -W020 */ window = {}; } // 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') { if (typeof navigator.webkitGetUserMedia !== 'undefined') { navigator.getUserMedia = navigator.webkitGetUserMedia; } if (typeof navigator.mozGetUserMedia !== 'undefined') { navigator.getUserMedia = navigator.mozGetUserMedia; } } else { navigator = { getUserMedia: function() {}, userAgent: browserFakeUserAgent }; } var IsEdge = navigator.userAgent.indexOf('Edge') !== -1 && (!!navigator.msSaveBlob || !!navigator.msSaveOrOpenBlob); var IsOpera = false; if (typeof opera !== 'undefined' && navigator.userAgent && navigator.userAgent.indexOf('OPR/') !== -1) { IsOpera = true; } var IsChrome = !IsEdge && !IsEdge && !!navigator.webkitGetUserMedia; var MediaStream = window.MediaStream; if (typeof MediaStream === 'undefined' && typeof webkitMediaStream !== 'undefined') { MediaStream = webkitMediaStream; } /*global MediaStream:true */ if (typeof MediaStream !== 'undefined') { if (!('getVideoTracks' in MediaStream.prototype)) { MediaStream.prototype.getVideoTracks = function() { if (!this.getTracks) { return []; } var tracks = []; this.getTracks.forEach(function(track) { if (track.kind.toString().indexOf('video') !== -1) { tracks.push(track); } }); return tracks; }; MediaStream.prototype.getAudioTracks = function() { if (!this.getTracks) { return []; } var tracks = []; this.getTracks.forEach(function(track) { if (track.kind.toString().indexOf('audio') !== -1) { tracks.push(track); } }); return tracks; }; } if (!('stop' in MediaStream.prototype)) { MediaStream.prototype.stop = function() { this.getAudioTracks().forEach(function(track) { if (!!track.stop) { track.stop(); } }); this.getVideoTracks().forEach(function(track) { if (!!track.stop) { track.stop(); } }); }; } } if (typeof location !== 'undefined') { if (location.href.indexOf('file:') === 0) { console.error('Please load this HTML file on HTTP or HTTPS.'); } } // Merge all other data-types except "function" function mergeProps(mergein, mergeto) { for (var t in mergeto) { if (typeof mergeto[t] !== 'function') { mergein[t] = mergeto[t]; } } return mergein; } // "dropFirstFrame" has been added by Graham Roth // https://github.com/gsroth function dropFirstFrame(arr) { arr.shift(); return arr; } /** * @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 (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.target = '_blank'; hyperlink.download = fileFullName; if (!!navigator.mozGetUserMedia) { hyperlink.onclick = function() { (document.body || document.documentElement).removeChild(hyperlink); }; (document.body || document.documentElement).appendChild(hyperlink); } var evt = new MouseEvent('click', { view: window, bubbles: true, cancelable: true }); hyperlink.dispatchEvent(evt); if (!navigator.mozGetUserMedia) { URL.revokeObjectURL(hyperlink.href); } } 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]; } // ______________ (used to handle stuff like http://goo.gl/xmE5eg) issue #129 // ObjectStore.js var ObjectStore = { AudioContext: AudioContext }; function isMediaRecorderCompatible() { var isOpera = !!window.opera || navigator.userAgent.indexOf(' OPR/') >= 0; var isChrome = !!window.chrome && !isOpera; var isFirefox = typeof window.InstallTrigger !== 'undefined'; if (isFirefox) { return true; } if (!isChrome) { return false; } 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) { 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; } // ______________ (used to handle stuff like http://goo.gl/xmE5eg) issue #129 // ObjectStore.js var ObjectStore = { AudioContext: window.AudioContext || window.webkitAudioContext }; // ================== // MediaRecorder.js /** * Implementation of https://dvcs.w3.org/hg/dap/raw-file/default/media-stream-capture/MediaRecorder.html * The MediaRecorder accepts a mediaStream as input source passed from UA. When recorder starts, * a MediaEncoder will be created and accept the mediaStream as input source. * Encoder will get the raw data by track data changes, encode it by selected MIME Type, then store the encoded in EncodedBufferCache object. * The encoded data will be extracted on every timeslice passed from Start function call or by RequestData function. * Thread model: * When the recorder starts, it creates a "Media Encoder" thread to read data from MediaEncoder object and store buffer in EncodedBufferCache object. * Also extract the encoded data and create blobs on every timeslice passed from start function or RequestData function called by UA. */ function MediaRecorderWrapper(mediaStream) { var self = this; /** * This method records MediaStream. * @method * @memberof MediaStreamRecorder * @example * recorder.record(); */ this.start = function(timeSlice, __disableLogs) { if (!self.mimeType) { self.mimeType = 'video/webm'; } if (self.mimeType.indexOf('audio') !== -1) { if (mediaStream.getVideoTracks().length && mediaStream.getAudioTracks().length) { var stream; if (!!navigator.mozGetUserMedia) { stream = new MediaStream(); stream.addTrack(mediaStream.getAudioTracks()[0]); } else { // webkitMediaStream stream = new MediaStream(mediaStream.getAudioTracks()); } mediaStream = stream; } } if (self.mimeType.indexOf('audio') !== -1) { self.mimeType = IsChrome ? 'audio/webm' : 'audio/ogg'; } self.dontFireOnDataAvailableEvent = false; var recorderHints = { mimeType: self.mimeType }; if (!self.disableLogs && !__disableLogs) { console.log('Passing following params 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'; } // http://dxr.mozilla.org/mozilla-central/source/content/media/MediaRecorder.cpp // https://wiki.mozilla.org/Gecko:MediaRecorder // https://dvcs.w3.org/hg/dap/raw-file/default/media-stream-capture/MediaRecorder.html // starting a recording session; which will initiate "Reading Thread" // "Reading Thread" are used to prevent main-thread blocking scenarios try { mediaRecorder = new MediaRecorder(mediaStream, recorderHints); } catch (e) { // if someone passed NON_supported mimeType // or if Firefox on Android mediaRecorder = new MediaRecorder(mediaStream); } if ('canRecordMimeType' in mediaRecorder && mediaRecorder.canRecordMimeType(self.mimeType) === false) { if (!self.disableLogs) { console.warn('MediaRecorder API seems unable to record mimeType:', self.mimeType); } } // i.e. stop recording when