// Muaz Khan - https://github.com/muaz-khan // MIT License - https://www.webrtc-experiment.com/licence/ // Documentation - https://github.com/muaz-khan/WebRTC-Experiment/tree/master/RecordRTC // ========================================================== // RecordRTC.js function RecordRTC(mediaStream, config) { config = config || { }; if (!mediaStream) throw 'MediaStream is mandatory.'; if (!config.type) config.type = 'audio'; function startRecording() { console.debug('started recording stream.'); // Media Stream Recording API has not been implemented in chrome yet; // That's why using WebAudio API to record stereo audio in WAV format var Recorder = IsChrome ? window.StereoRecorder : window.MediaStreamRecorder; // video recorder (in WebM format) if (config.type == 'video') Recorder = window.WhammyRecorder; // video recorder (in Gif format) if (config.type == 'gif') Recorder = window.GifRecorder; mediaRecorder = new Recorder(mediaStream); // Merge all data-types except "function" mediaRecorder = mergeProps(mediaRecorder, config); mediaRecorder.record(); } function stopRecording(callback) { console.warn('stopped recording stream.'); mediaRecorder.stop(); if (callback && mediaRecorder) { var url = URL.createObjectURL(mediaRecorder.recordedBlob); callback(url); } } var mediaRecorder; return { startRecording: startRecording, stopRecording: stopRecording, getBlob: function() { if (!mediaRecorder) return console.warn('RecordRTC is idle.'); return mediaRecorder.recordedBlob; }, getDataURL: function(callback) { if (!mediaRecorder) return console.warn('RecordRTC is idle.'); var reader = new window.FileReader(); reader.readAsDataURL(mediaRecorder.recordedBlob); reader.onload = function(event) { if (callback) callback(event.target.result); }; }, toURL: function() { if (!mediaRecorder) return console.warn('RecordRTC is idle.'); return URL.createObjectURL(mediaRecorder.recordedBlob); }, save: function() { if (!mediaRecorder) return console.warn('RecordRTC is idle.'); console.log('saving recorded stream to disk!'); this.getDataURL(function(dataURL) { var hyperlink = document.createElement('a'); hyperlink.href = dataURL; hyperlink.target = '_blank'; hyperlink.download = (Math.round(Math.random() * 9999999999) + 888888888) + '.' + mediaRecorder.recordedBlob.type.split('/')[1]; var evt = new window.MouseEvent('click', { view: window, bubbles: true, cancelable: true }); hyperlink.dispatchEvent(evt); (window.URL || window.webkitURL).revokeObjectURL(hyperlink.href); }); } }; } // ========================== // Cross-Browser Declarations // animation-frame used in WebM recording requestAnimationFrame = window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame; cancelAnimationFrame = window.webkitCancelAnimationFrame || window.mozCancelAnimationFrame; // WebAudio API representer AudioContext = window.webkitAudioContext || window.mozAudioContext; URL = window.URL || window.webkitURL; navigator.getUserMedia = navigator.webkitGetUserMedia || navigator.mozGetUserMedia; if (window.webkitMediaStream) window.MediaStream = window.webkitMediaStream; IsChrome = !!navigator.webkitGetUserMedia; // 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; } // Muaz Khan - https://github.com/muaz-khan // ======================================== MediaStreamRecorder.js // encoder only support 48k/16k mono audio channel function MediaStreamRecorder(mediaStream) { var self = this; this.record = function() { // http://dxr.mozilla.org/mozilla-central/source/content/media/MediaRecorder.cpp // https://wiki.mozilla.org/Gecko:MediaRecorder mediaRecorder = new window.MediaRecorder(mediaStream); mediaRecorder.ondataavailable = function(e) { self.recordedBlob = new window.Blob([self.recordedBlob, e.data], { type: 'audio/ogg' }); }; mediaRecorder.start(0); }; this.stop = function() { if (mediaRecorder.state == 'recording') { mediaRecorder.requestData(); mediaRecorder.stop(); } }; // Reference to "MediaRecorder" object var mediaRecorder; } // Muaz Khan - https://github.com/muaz-khan // ======================================== StereoRecorder.js function StereoRecorder(mediaStream) { this.record = function() { mediaRecorder = new StereoAudioRecorder(mediaStream); mediaRecorder.record(); }; this.stop = function() { if (mediaRecorder) mediaRecorder.stop(); this.recordedBlob = mediaRecorder.recordedBlob; }; // Reference to "StereoAudioRecorder" object var mediaRecorder; } // source code from: http://typedarray.org/wp-content/projects/WebAudioRecorder/script.js function StereoAudioRecorder(mediaStream) { // variables var leftchannel = []; var rightchannel = []; var recorder; var recording = false; var recordingLength = 0; var volume; var audioInput; var sampleRate = 44100; var audioContext; var context; this.record = function() { recording = true; // reset the buffers for the new recording leftchannel.length = rightchannel.length = 0; recordingLength = 0; }; this.stop = function() { // we stop recording recording = false; // we flat the left and right channels down var leftBuffer = mergeBuffers(leftchannel, recordingLength); var rightBuffer = mergeBuffers(rightchannel, recordingLength); // we interleave both channels together var interleaved = interleave(leftBuffer, rightBuffer); // we create our wav file var buffer = new window.ArrayBuffer(44 + interleaved.length * 2); var view = new window.DataView(buffer); // RIFF chunk descriptor writeUTFBytes(view, 0, 'RIFF'); view.setUint32(4, 44 + interleaved.length * 2, true); writeUTFBytes(view, 8, 'WAVE'); // FMT sub-chunk writeUTFBytes(view, 12, 'fmt '); view.setUint32(16, 16, true); view.setUint16(20, 1, true); // stereo (2 channels) view.setUint16(22, 2, true); view.setUint32(24, sampleRate, true); view.setUint32(28, sampleRate * 4, true); view.setUint16(32, 4, true); view.setUint16(34, 16, true); // data sub-chunk writeUTFBytes(view, 36, 'data'); view.setUint32(40, interleaved.length * 2, true); // write the PCM samples var lng = interleaved.length; var index = 44; volume = 1; for (var i = 0; i < lng; i++) { view.setInt16(index, interleaved[i] * (0x7FFF * volume), true); index += 2; } // final binary blob this.recordedBlob = new window.Blob([view], { type: 'audio/wav' }); }; function interleave(leftChannel, rightChannel) { var length = leftChannel.length + rightChannel.length; var result = new window.Float32Array(length); var inputIndex = 0; for (var index = 0; index < length;) { result[index++] = leftChannel[inputIndex]; result[index++] = rightChannel[inputIndex]; inputIndex++; } return result; } function mergeBuffers(channelBuffer, rLength) { var result = new window.Float32Array(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 writeUTFBytes(view, offset, string) { var lng = string.length; for (var i = 0; i < lng; i++) { view.setUint8(offset + i, string.charCodeAt(i)); } } // creates the audio context audioContext = window.AudioContext || window.webkitAudioContext; context = new audioContext(); // creates a gain node volume = context.createGain(); // creates an audio node from the microphone incoming stream audioInput = context.createMediaStreamSource(mediaStream); // connect the stream to the gain node audioInput.connect(volume); /* 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 */ var bufferSize = 2048; recorder = context.createJavaScriptNode(bufferSize, 2, 2); recorder.onaudioprocess = function(e) { if (!recording) return; var left = e.inputBuffer.getChannelData(0); var right = e.inputBuffer.getChannelData(1); // we clone the samples leftchannel.push(new window.Float32Array(left)); rightchannel.push(new window.Float32Array(right)); recordingLength += bufferSize; }; // we connect the recorder volume.connect(recorder); recorder.connect(context.destination); } // Muaz Khan - https://github.com/muaz-khan // ======================================== WhammyRecorder.js function WhammyRecorder(mediaStream) { this.record = function() { var imageWidth = this.width || 320; var imageHeight = this.height || 240; canvas.width = video.width = imageWidth; canvas.height = video.height = imageHeight; startTime = Date.now(); function drawVideoFrame(time) { lastAnimationFrame = requestAnimationFrame(drawVideoFrame); if (typeof lastFrameTime === undefined) { lastFrameTime = time; } // ~10 fps if (time - lastFrameTime < 90) return; context.drawImage(video, 0, 0, imageWidth, imageHeight); // whammy.add(canvas, time - lastFrameTime); whammy.add(canvas); // console.log('Recording...' + Math.round((Date.now() - startTime) / 1000) + 's'); // console.log("fps: ", 1000 / (time - lastFrameTime)); lastFrameTime = time; } lastAnimationFrame = requestAnimationFrame(drawVideoFrame); }; this.stop = function() { if (lastAnimationFrame) cancelAnimationFrame(lastAnimationFrame); endTime = Date.now(); console.log('frames captured: ' + whammy.frames.length + ' => ' + ((endTime - startTime) / 1000) + 's video'); this.recordedBlob = whammy.compile(); whammy.frames = []; }; var canvas = document.createElement('canvas'); var context = canvas.getContext('2d'); var video = document.createElement('video'); video.muted = true; video.autoplay = true; video.src = URL.createObjectURL(mediaStream); video.play(); var lastAnimationFrame = null; var startTime, endTime, lastFrameTime; var whammy = new window.Whammy.Video(10, 0.6); } // Muaz Khan - https://github.com/muaz-khan // ======================================== GifRecorder.js function GifRecorder(mediaStream) { this.record = function() { var imageWidth = this.width || 320; var imageHeight = this.height || 240; canvas.width = video.width = imageWidth; canvas.height = video.height = imageHeight; // external library to record as GIF images gifEncoder = new window.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(this.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(this.quality || 10); // Boolean start() // This writes the GIF Header and returns false if it fails. gifEncoder.start(); startTime = Date.now(); function drawVideoFrame(time) { lastAnimationFrame = requestAnimationFrame(drawVideoFrame); if (typeof lastFrameTime === undefined) { lastFrameTime = time; } // ~10 fps if (time - lastFrameTime < 90) return; context.drawImage(video, 0, 0, imageWidth, imageHeight); gifEncoder.addFrame(context); // console.log('Recording...' + Math.round((Date.now() - startTime) / 1000) + 's'); // console.log("fps: ", 1000 / (time - lastFrameTime)); lastFrameTime = time; } lastAnimationFrame = requestAnimationFrame(drawVideoFrame); }; this.stop = function() { if (lastAnimationFrame) cancelAnimationFrame(lastAnimationFrame); endTime = Date.now(); this.recordedBlob = new window.Blob([new window.Uint8Array(gifEncoder.stream().bin)], { type: 'image/gif' }); // bug: find a way to clear old recorded blobs gifEncoder.stream().bin = []; }; var canvas = document.createElement('canvas'); var context = canvas.getContext('2d'); var video = document.createElement('video'); video.muted = true; video.autoplay = true; video.src = URL.createObjectURL(mediaStream); video.play(); var lastAnimationFrame = null; var startTime, endTime, lastFrameTime; var gifEncoder; } // Muaz Khan - https://github.com/muaz-khan // ======================================== whammy.js // whammy.js is an "external library" // and has its own copyrights. Taken from "Whammy" project. var Whammy=function(){function g(a){for(var b=a[0].width,e=a[0].height,c=a[0].duration,d=1;da[d].duration)throw"Frame "+(d+1)+" has a weird duration";c+=a[d].duration}var f=0,a=[{id:440786851,data:[{data:1,id:17030},{data:1,id:17143},{data:4,id:17138},{data:8,id:17139},{data:"webm",id:17026},{data:2,id:17031},{data:2,id:17029}]},{id:408125543,data:[{id:357149030, data:[{data:1E6,id:2807729},{data:"whammy",id:19840},{data:"whammy",id:22337},{data:[].slice.call(new Uint8Array((new Float64Array([c])).buffer),0).map(function(a){return String.fromCharCode(a)}).reverse().join(""),id:17545}]},{id:374648427,data:[{id:174,data:[{data:1,id:215},{data:1,id:25541},{data:0,id:156},{data:"und",id:2274716},{data:"V_VP8",id:134},{data:"VP8",id:2459272},{data:1,id:131},{id:224,data:[{data:b,id:176},{data:e,id:186}]}]}]},{id:524531317,data:[{data:0,id:231}].concat(a.map(function(a){var b; b=a.data.slice(4);var c=Math.round(f);b=[129,c>>8,c&255,128].map(function(a){return String.fromCharCode(a)}).join("")+b;f+=a.duration;return{data:b,id:163}}))}]}];return j(a)}function m(a){for(var b=[];0>=8;return new Uint8Array(b.reverse())}function k(a){for(var b=[],a=(a.length%8?Array(9-a.length%8).join("0"):"")+a,e=0;ec;c++)d[c]=b.charCodeAt(e+3+c);c=d[1]<< 8|d[0];e=c&16383;c=d[3]<<8|d[2];return{width:e,height:c&16383,data:b,riff:a}}function h(a){for(var b=0,e={};b>2,h=(s&3)<<4|i>>4,e=(i&15)<<2|r>>6,t=r&63,isNaN(i)?e=t=64:isNaN(r)&&(t=64),o=o+u.charAt(c)+u.charAt(h)+u.charAt(e)+u.charAt(t);return o}LZWEncoder=function(){var c={},it=-1,st,ht,rt,l,w,et,ut=12,ct=5003,t,ft=ut,o,ot=1<=254&&k(t)},at=function(n){tt(a),s=f+2,h=!0,e(f,n)},tt=function(n){for(var t=0;t=0){rt=g-c,c==0&&(rt=1);do if((c-=rt)<0&&(c+=g),u[c]==w){l=y[c];continue n}while(u[c]>=0)}e(l,i),l=nt,s0&&(n.writeByte(r),n.writeBytes(g,0,r),r=0)},b=function(n){return(1<0?i|=r<=8;)nt(i&255,u),i>>=8,n-=8;if((s>o||h)&&(h?(o=b(t=v),h=!1):(++t,o=t==ft?ot:b(t))),r==p){while(n>0)nt(i&255,u),i>>=8,n-=8;k(u)}};return lt.apply(this,arguments),c},NeuQuant=function(){var c={},t=256,tt=499,nt=491,rt=487,it=503,g=3*it,b=t-1,r=4,pt=100,ft=16,y=1<>a,dt=y<>3,l=6,ti=1<>1,i=o+1;i>1,i=o+1;i<256;i++)f[i]=b},vt=function(){var t,u,k,b,p,c,n,s,o,y,ut,a,f,ft;for(i>l,n<=1&&(n=0),t=0;t=ft&&(f-=i),t++,y==0&&(y=1),t%y==0)for(s-=s/et,c-=c/kt,n=c>>l,n<=1&&(n=0),u=0;u=0;)c=h?c=t:(c++,e<0&&(e=-e),o=s[0]-i,o<0&&(o=-o),e+=o,e=0&&(s=n[l],e=r-s[1],e>=h?l=-1:(l--,e<0&&(e=-e),o=s[0]-i,o<0&&(o=-o),e+=o,e>=r,n[i][1]>>=r,n[i][2]>>=r,n[i][3]=i},lt=function(i,r,f,e,o){var a,y,l,c,h,p,s;for(l=r-i,l<-1&&(l=-1),c=r+i,c>t&&(c=t),a=r+1,y=r-1,p=1;al;){if(h=v[p++],al){s=n[y--];try{s[0]-=h*(s[0]-f)/u,s[1]-=h*(s[1]-e)/u,s[2]-=h*(s[2]-o)/u}catch(w){}}}},at=function(t,i,r,u,f){var o=n[i];o[0]-=t*(o[0]-r)/e,o[1]-=t*(o[1]-u)/e,o[2]-=t*(o[2]-f)/e},yt=function(i,u,f){var h,c,e,b,d,l,k,v,w,y;for(v=2147483647,w=v,l=-1,k=l,h=0;h>ft-r),b>a,s[h]-=d,o[h]+=d<=0&&(y=n)},dt=t.setRepeat=function(n){n>=0&&(k=n)},bt=t.setTransparent=function(n){v=n},kt=t.addFrame=function(t,i){if(t==null||!f||n==null){throw new Error("Please call start method before calling addFrame");return!1}var r=!0;try{i?a=t:(a=t.getImageData(0,0,t.canvas.width,t.canvas.height).data,ft||et(t.canvas.width,t.canvas.height)),ct(),ht(),e&&(vt(),tt(),k>=0&<()),st(),ot(),e||tt(),at(),e=!1}catch(u){r=!1}return r},ui=t.finish=function(){if(!f)return!1;var t=!0;f=!1;try{n.writeByte(59)}catch(i){t=!1}return t},nt=function(){g=0,a=null,i=null,l=null,r=null,b=!1,e=!0},fi=t.setFrameRate=function(n){n!=15&&(d=Math.round(100/n))},ri=t.setQuality=function(n){n<1&&(n=1),it=n},et=t.setSize=function et(n,t){(!f||e)&&(o=n,s=t,o<1&&(o=320),s<1&&(s=240),ft=!0)},ti=t.start=function(){nt();var t=!0;b=!1,n=new h;try{n.writeUTFBytes("GIF89a")}catch(i){t=!1}return f=t},ii=t.cont=function(){nt();var t=!0;return b=!1,n=new h,f=t},ht=function(){var e=i.length,o=e/3,f,n,t,u;for(l=[],f=new NeuQuant(i,e,it),r=f.process(),n=0,t=0;t>16,v=(n&65280)>>8,a=n&255,s=0,h=16777216,l=r.length;for(t=0;t=0&&(t=y&7),t<<=2,n.writeByte(0|t|0|i),u(d),n.writeByte(g),n.writeByte(0)},ot=function(){n.writeByte(44),u(0),u(0),u(o),u(s),e?n.writeByte(0):n.writeByte(128|p)},vt=function(){u(o),u(s),n.writeByte(240|p),n.writeByte(0),n.writeByte(0)},lt=function(){n.writeByte(33),n.writeByte(255),n.writeByte(11),n.writeUTFBytes("NETSCAPE2.0"),n.writeByte(3),n.writeByte(1),u(k),n.writeByte(0)},tt=function(){var i,t;for(n.writeBytes(r),i=768-r.length,t=0;t>8&255)},at=function(){var t=new LZWEncoder(o,s,l,rt);t.encode(n)},wt=t.stream=function(){return n},pt=t.setProperties=function(n,t){f=n,e=t};return t}