diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3fb9d16 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +# Node +node_modules + +# bower +bower_components diff --git a/.jshintrc b/.jshintrc new file mode 100644 index 0000000..227290f --- /dev/null +++ b/.jshintrc @@ -0,0 +1,26 @@ +{ + "browser": true, + "camelcase": true, + "curly": true, + "devel": true, + "eqeqeq": true, + "forin": false, + "globalstrict": true, + "quotmark": "single", + "undef": true, + "globals": { + "MediaStreamRecorder": true, + "ObjectStore": true, + "StereoAudioRecorder": true, + "CanvasRecorder": true, + "WhammyRecorder": true, + "Whammy": true, + "GifRecorder": true, + "AudioContext": true, + "URL": true, + "IsChrome": true, + "bytesToSize": true, + "FileReaderSync": true, + "postMessage": true + } +} diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..fe416cc --- /dev/null +++ b/.npmignore @@ -0,0 +1,3 @@ +node_modules +lib-cov +npm-debug.log \ No newline at end of file diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..c1cc706 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,10 @@ +language: node_js +node_js: + - "0.11" +install: npm install +before_script: + - npm install grunt-cli + - npm install grunt + - grunt +matrix: + fast_finish: true diff --git a/AudioStreamRecorder/FlashAudioRecorder.js b/AudioStreamRecorder/FlashAudioRecorder.js index e0d022a..3a4d7b6 100644 --- a/AudioStreamRecorder/FlashAudioRecorder.js +++ b/AudioStreamRecorder/FlashAudioRecorder.js @@ -6,251 +6,244 @@ // FlashAudioRecorder.js // Based on recorder.js - https://github.com/jwagener/recorder.js -function FlashAudioRecorder(o) -{ - if (o == null) { - o = {}; - } - var self = this, - baseUrl = getBaseUrl(), - defaults = { - swfObjectPath: baseUrl+'lib/recorder.js/recorder.swf', - jsLibPath: baseUrl+'lib/recorder.js/recorder.js', - encoderPath: baseUrl+'lib/wavencoder/wavencoder.js', - flashContainer: null, - dataType:'url', // url | blob | raw | dataUri | false - uploadParams:{ - url:'', - audioParam:"", - params:{} - }, - ondataavailable: null, - onstop: function(e) { - - }, - onstart:function(e) { - - }, - onFlashSecurity: function(e) { - var flashContainer = Recorder.options.flashContainer; - flashContainer.style.left = ((window.innerWidth || document.body.offsetWidth) / 2) - 115 + "px"; - flashContainer.style.top = ((window.innerHeight || document.body.offsetHeight) / 2) - 70 + "px"; - }, - onready: function(e) { - - }, - onerror: function(e) { - - } - }, - _initialized = false, - _startRequest = false, - options = extend(defaults,o); - - include(options.jsLibPath,init); - - function init() { - if (!_initialized) - { - Recorder._defaultOnShowFlash = true; - - Recorder.initialize({ - swfSrc: options.swfObjectPath, - flashContainer: options.flashContainer, - onFlashSecurity: function(e) { - self.onFlashSecurity(e); - }, - initialized: function(e) { - self.onready(); - - _initialized = true; - - if (_startRequest) { - start(); - } - } - }); - } - - } - function initEncoder() { - WavEncoder.defaults = { - numChannels: 1, // mono - sampleRateHz: 44100, // 44100 Hz - bytesPerSample: 2, // 16 bit - clip: true - }; - } - function start(interval) { - _startRequest = true; - if (_initialized) - { - if (self.state != 'inactive') { - var error ={ - message: 'The object is in an invalid state', - code:DOMException.INVALID_STATE_ERR - } - self.onerror(error); - throw error; - } - self.state = 'recording'; - - _startRequest = false; - Recorder.record({ - start: function(e) { - self.onstart(e); - } - }); - } - } - function stop() { - if (self.state == 'inactive') { - var error ={ - message: 'The object is in an invalid state', - code:DOMException.INVALID_STATE_ERR - } - self.onerror(error); - throw error; - } - self.state = 'inactive'; - _startRequest = false; - Recorder.stop(); - self.onstop(); - if (typeof self.ondataavailable == 'function') { - - if (self.dataType == 'url') - { - upload(); - } - else if(self.dataType != false) - { - handleBinaryData(); - } - } - } - function handleBinaryData() { - Recorder.getAudioData(function(data){ - if (self.dataType == 'raw') { - return self.ondataavailable({ - data: data, - dataType: self.dataType - }); - }; - - include(options.encoderPath,function(){ - initEncoder(); - var datauri = WavEncoder.encode(data); - if (self.dataType == 'datauri') { - return self.ondataavailable({ - data: datauri, - dataType: self.dataType - }); - }; - var audioBlob = new Blob([datauri], { - type: self.mimeType - }); - if (self.dataType == 'blob') { - return self.ondataavailable({ - data: audioBlob, - dataType: self.dataType - }); - }; - }); - }); - - - } - function upload(params) { - if (params == null) - params = self.uploadParams; - - params.success = function(msg) { - self.ondataavailable({ - data:msg, - dataType: 'url' - }); - }; - params.error = function(msg) { - self.onerror({ - msg:msg - }); - } - Recorder.upload(params); - } - // get script folder - function getBaseUrl() { - var scripts = document.head.getElementsByTagName("script"); - var loc = scripts[scripts.length-1].src; - return loc.substring(0, loc.lastIndexOf('/'))+'/'; - } - // extending user options - function extend(o1,o2) { - var obj = {}; - for(var i in o1) - { - if (o2[i] != null) { - if (typeof o2[i] == "object") { - obj[i] = extend(o2[i],{}); - } - else { - obj[i] = o2[i]; - } - } - else { - if (typeof o2[i] == "object") { - obj[i] = extend(o1[i],{}); - } - else { - obj[i] = o1[i]; - } - } - } - return obj; - } - function include(src,callback) { - var scripts = document.getElementsByTagName('script'), - found = false; - - for (var i = 0,len = scripts.length;i < len; i++) - { - if (scripts[i].getAttribute('src') == src) { - found = true; - } - } - if (found) { - if (typeof callback == 'function') { - callback(); - } - } - else - { - var js = document.createElement("script"); - - js.type = "text/javascript"; - js.src = src; - if (typeof callback == 'function') { - js.onload = callback; - } - - document.body.appendChild(js); - } - - } - - this.ondataavailable = options.ondataavailable; - this.onstop = options.onstop; - this.onstart = options.onstart; - this.onFlashSecurity = options.onFlashSecurity; - this.onerror = options.onerror; - this.onready = options.onready; - this.state = 'inactive'; - this.mimeType = 'audio/wav'; - - this.uploadParams = options.uploadParams; - this.dataType = options.dataType; - - this.start = start; - this.upload = upload; - this.stop = stop; - this.baseUrl = baseUrl; -} \ No newline at end of file +function FlashAudioRecorder(o) { + if (o == null) { + o = {}; + } + var self = this, + baseUrl = getBaseUrl(), + defaults = { + swfObjectPath: baseUrl + 'lib/recorder.js/recorder.swf', + jsLibPath: baseUrl + 'lib/recorder.js/recorder.js', + encoderPath: baseUrl + 'lib/wavencoder/wavencoder.js', + flashContainer: null, + dataType: 'url', // url | blob | raw | dataUri | false + uploadParams: { + url: '', + audioParam: "", + params: {} + }, + ondataavailable: null, + onstop: function(e) { + + }, + onstart: function(e) { + + }, + onFlashSecurity: function(e) { + var flashContainer = Recorder.options.flashContainer; + flashContainer.style.left = ((window.innerWidth || document.body.offsetWidth) / 2) - 115 + "px"; + flashContainer.style.top = ((window.innerHeight || document.body.offsetHeight) / 2) - 70 + "px"; + }, + onready: function(e) { + + }, + onerror: function(e) { + + } + }, + _initialized = false, + _startRequest = false, + options = extend(defaults, o); + + include(options.jsLibPath, init); + + function init() { + if (!_initialized) { + Recorder._defaultOnShowFlash = true; + + Recorder.initialize({ + swfSrc: options.swfObjectPath, + flashContainer: options.flashContainer, + onFlashSecurity: function(e) { + self.onFlashSecurity(e); + }, + initialized: function(e) { + self.onready(); + + _initialized = true; + + if (_startRequest) { + start(); + } + } + }); + } + + } + + function initEncoder() { + WavEncoder.defaults = { + numChannels: 1, // mono + sampleRateHz: 44100, // 44100 Hz + bytesPerSample: 2, // 16 bit + clip: true + }; + } + + function start(interval) { + _startRequest = true; + if (_initialized) { + if (self.state != 'inactive') { + var error = { + message: 'The object is in an invalid state', + code: DOMException.INVALID_STATE_ERR + } + self.onerror(error); + throw error; + } + self.state = 'recording'; + + _startRequest = false; + Recorder.record({ + start: function(e) { + self.onstart(e); + } + }); + } + } + + function stop() { + if (self.state == 'inactive') { + var error = { + message: 'The object is in an invalid state', + code: DOMException.INVALID_STATE_ERR + } + self.onerror(error); + throw error; + } + self.state = 'inactive'; + _startRequest = false; + Recorder.stop(); + self.onstop(); + if (typeof self.ondataavailable == 'function') { + + if (self.dataType == 'url') { + upload(); + } else if (self.dataType != false) { + handleBinaryData(); + } + } + } + + function handleBinaryData() { + Recorder.getAudioData(function(data) { + if (self.dataType == 'raw') { + return self.ondataavailable({ + data: data, + dataType: self.dataType + }); + }; + + include(options.encoderPath, function() { + initEncoder(); + var datauri = WavEncoder.encode(data); + if (self.dataType == 'datauri') { + return self.ondataavailable({ + data: datauri, + dataType: self.dataType + }); + }; + var audioBlob = new Blob([datauri], { + type: self.mimeType + }); + if (self.dataType == 'blob') { + return self.ondataavailable({ + data: audioBlob, + dataType: self.dataType + }); + }; + }); + }); + + + } + + function upload(params) { + if (params == null) + params = self.uploadParams; + + params.success = function(msg) { + self.ondataavailable({ + data: msg, + dataType: 'url' + }); + }; + params.error = function(msg) { + self.onerror({ + msg: msg + }); + } + Recorder.upload(params); + } + // get script folder + function getBaseUrl() { + var scripts = document.head.getElementsByTagName("script"); + var loc = scripts[scripts.length - 1].src; + return loc.substring(0, loc.lastIndexOf('/')) + '/'; + } + // extending user options + function extend(o1, o2) { + var obj = {}; + for (var i in o1) { + if (o2[i] != null) { + if (typeof o2[i] == "object") { + obj[i] = extend(o2[i], {}); + } else { + obj[i] = o2[i]; + } + } else { + if (typeof o2[i] == "object") { + obj[i] = extend(o1[i], {}); + } else { + obj[i] = o1[i]; + } + } + } + return obj; + } + + function include(src, callback) { + var scripts = document.getElementsByTagName('script'), + found = false; + + for (var i = 0, len = scripts.length; i < len; i++) { + if (scripts[i].getAttribute('src') == src) { + found = true; + } + } + if (found) { + if (typeof callback == 'function') { + callback(); + } + } else { + var js = document.createElement("script"); + + js.type = "text/javascript"; + js.src = src; + if (typeof callback == 'function') { + js.onload = callback; + } + + document.body.appendChild(js); + } + + } + + this.ondataavailable = options.ondataavailable; + this.onstop = options.onstop; + this.onstart = options.onstart; + this.onFlashSecurity = options.onFlashSecurity; + this.onerror = options.onerror; + this.onready = options.onready; + this.state = 'inactive'; + this.mimeType = 'audio/wav'; + + this.uploadParams = options.uploadParams; + this.dataType = options.dataType; + + this.start = start; + this.upload = upload; + this.stop = stop; + this.baseUrl = baseUrl; +} diff --git a/AudioStreamRecorder/MediaRecorder.js b/AudioStreamRecorder/MediaRecorderWrapper.js similarity index 70% rename from AudioStreamRecorder/MediaRecorder.js rename to AudioStreamRecorder/MediaRecorderWrapper.js index be74af4..08be01b 100644 --- a/AudioStreamRecorder/MediaRecorder.js +++ b/AudioStreamRecorder/MediaRecorderWrapper.js @@ -1,22 +1,22 @@ -// ================ +// ================== // 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. -*/ + * 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) { // if user chosen only audio option; and he tried to pass MediaStream with // both audio and video tracks; // using a dirty workaround to generate audio-only stream so that we can get audio/ogg output. - if (this.type == 'audio' && mediaStream.getVideoTracks && mediaStream.getVideoTracks().length && !navigator.mozGetUserMedia) { + if (this.type === 'audio' && mediaStream.getVideoTracks && mediaStream.getVideoTracks().length && !navigator.mozGetUserMedia) { var context = new AudioContext(); var mediaStreamSource = context.createMediaStreamSource(mediaStream); @@ -36,14 +36,21 @@ function MediaRecorderWrapper(mediaStream) { isStopRecording = false; function startRecording() { - if (isStopRecording) return; + if (isStopRecording) { + return; + } + + if (isPaused) { + setTimeout(startRecording, 500); + return; + } mediaRecorder = new MediaRecorder(mediaStream); mediaRecorder.ondataavailable = function(e) { console.log('ondataavailable', e.data.type, e.data.size, e.data); - // mediaRecorder.state == 'recording' means that media recorder is associated with "session" - // mediaRecorder.state == 'stopped' means that media recorder is detached from the "session" ... in this case; "session" will also be deleted. + // mediaRecorder.state === 'recording' means that media recorder is associated with "session" + // mediaRecorder.state === 'stopped' means that media recorder is detached from the "session" ... in this case; "session" will also be deleted. if (!e.data.size) { console.warn('Recording of', e.data.type, 'failed.'); @@ -119,6 +126,32 @@ function MediaRecorderWrapper(mediaStream) { } }; + var isPaused = false; + + this.pause = function() { + if (!mediaRecorder) { + return; + } + + isPaused = true; + + if (mediaRecorder.state === 'recording') { + mediaRecorder.pause(); + } + }; + + this.resume = function() { + if (!mediaRecorder) { + return; + } + + isPaused = false; + + if (mediaRecorder.state === 'paused') { + mediaRecorder.resume(); + } + }; + this.ondataavailable = this.onstop = function() {}; // Reference to itself diff --git a/AudioStreamRecorder/StereoAudioRecorder.js b/AudioStreamRecorder/StereoAudioRecorder.js index c8991e8..492af87 100644 --- a/AudioStreamRecorder/StereoAudioRecorder.js +++ b/AudioStreamRecorder/StereoAudioRecorder.js @@ -1,218 +1,47 @@ // ====================== // StereoAudioRecorder.js -// source code from: http://typedarray.org/wp-content/projects/WebAudioRecorder/script.js +function StereoAudioRecorder(mediaStream) { + // void start(optional long timeSlice) + // timestamp to fire "ondataavailable" + this.start = function(timeSlice) { + timeSlice = timeSlice || 1000; -function StereoAudioRecorder(mediaStream, root) { + mediaRecorder = new StereoAudioRecorderHelper(mediaStream, this); - // variables - var deviceSampleRate = 44100; // range: 22050 to 96000 - - // check device sample rate - if(window.AudioContext){ - deviceSampleRate = (new window.AudioContext()).sampleRate; - } + mediaRecorder.record(); - var leftchannel = []; - var rightchannel = []; - var scriptprocessornode; - var recording = false; - var recordingLength = 0; - var volume; - var audioInput; - var sampleRate = root.sampleRate || deviceSampleRate; - var audioContext; - var context; - - var numChannels = root.audioChannels || 2; - - this.record = function() { - recording = true; - // reset the buffers for the new recording - leftchannel.length = rightchannel.length = 0; - recordingLength = 0; - }; - - this.requestData = function() { - if (recordingLength == 0) { - requestDataInvoked = false; - return; - } - - requestDataInvoked = true; - // clone stuff - var internal_leftchannel = leftchannel.slice(0); - var internal_rightchannel = rightchannel.slice(0); - var internal_recordingLength = recordingLength; - - // reset the buffers for the new recording - leftchannel.length = rightchannel.length = []; - recordingLength = 0; - requestDataInvoked = false; - - // we flat the left and right channels down - var leftBuffer = mergeBuffers(internal_leftchannel, internal_recordingLength); - var rightBuffer = mergeBuffers(internal_leftchannel, internal_recordingLength); - - // we interleave both channels together - if (numChannels === 2) { - var interleaved = interleave(leftBuffer, rightBuffer); - } else { - var interleaved = leftBuffer; - } - - // we create our wav file - var buffer = new ArrayBuffer(44 + interleaved.length * 2); - var view = new 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, numChannels, true); - view.setUint32(24, sampleRate, true); - view.setUint32(28, sampleRate * 4, true); - view.setUint16(32, numChannels * 2, 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; - var volume = 1; - for (var i = 0; i < lng; i++) { - view.setInt16(index, interleaved[i] * (0x7FFF * volume), true); - index += 2; - } - - // our final binary blob - var blob = new Blob([view], { - type: 'audio/wav' - }); - - console.debug('audio recorded blob size:', bytesToSize(blob.size)); - - root.ondataavailable(blob); + timeout = setInterval(function() { + mediaRecorder.requestData(); + }, timeSlice); }; this.stop = function() { - // we stop recording - recording = false; - this.requestData(); - - audioInput.disconnect(); + if (mediaRecorder) { + mediaRecorder.stop(); + clearTimeout(timeout); + } }; - function interleave(leftChannel, rightChannel) { - var length = leftChannel.length + rightChannel.length; - var result = new Float32Array(length); - - var inputIndex = 0; - - for (var index = 0; index < length;) { - result[index++] = leftChannel[inputIndex]; - result[index++] = rightChannel[inputIndex]; - inputIndex++; + this.pause = function() { + if (!mediaRecorder) { + return; } - return result; - } - function mergeBuffers(channelBuffer, recordingLength) { - var result = new Float32Array(recordingLength); - 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; - } + mediaRecorder.pause(); + }; - function writeUTFBytes(view, offset, string) { - var lng = string.length; - for (var i = 0; i < lng; i++) { - view.setUint8(offset + i, string.charCodeAt(i)); + this.resume = function() { + if (!mediaRecorder) { + return; } - } - - // creates the audio context - - // creates the audio context - var audioContext = ObjectStore.AudioContext; - - if (!ObjectStore.AudioContextConstructor) - ObjectStore.AudioContextConstructor = new audioContext(); - - var context = ObjectStore.AudioContextConstructor; - - // creates a gain node - ObjectStore.VolumeGainNode = context.createGain(); - - var volume = ObjectStore.VolumeGainNode; - - // creates an audio node from the microphone incoming stream - ObjectStore.AudioInput = context.createMediaStreamSource(mediaStream); - - // creates an audio node from the microphone incoming stream - var audioInput = ObjectStore.AudioInput; - - // 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 - Legal values are 256, 512, 1024, 2048, 4096, 8192, and 16384.*/ - var bufferSize = root.bufferSize || 2048; - if (root.bufferSize == 0) bufferSize = 0; - - if (context.createJavaScriptNode) { - scriptprocessornode = context.createJavaScriptNode(bufferSize, numChannels, numChannels); - } else if (context.createScriptProcessor) { - scriptprocessornode = context.createScriptProcessor(bufferSize, numChannels, numChannels); - } else { - throw 'WebAudio API has no support on this browser.'; - } - - bufferSize = scriptprocessornode.bufferSize; - - console.debug('using audio buffer-size:', bufferSize); - - var requestDataInvoked = false; - - // sometimes "scriptprocessornode" disconnects from he destination-node - // and there is no exception thrown in this case. - // and obviously no further "ondataavailable" events will be emitted. - // below global-scope variable is added to debug such unexpected but "rare" cases. - window.scriptprocessornode = scriptprocessornode; - - if (numChannels == 1) { - console.debug('All right-channels are skipped.'); - } - - // http://webaudio.github.io/web-audio-api/#the-scriptprocessornode-interface - scriptprocessornode.onaudioprocess = function(e) { - if (!recording || requestDataInvoked) return; - - var left = e.inputBuffer.getChannelData(0); - leftchannel.push(new Float32Array(left)); - - if (numChannels == 2) { - var right = e.inputBuffer.getChannelData(1); - rightchannel.push(new Float32Array(right)); - } - recordingLength += bufferSize; + mediaRecorder.resume(); }; - volume.connect(scriptprocessornode); - scriptprocessornode.connect(context.destination); + this.ondataavailable = function() {}; + + // Reference to "StereoAudioRecorder" object + var mediaRecorder; + var timeout; } diff --git a/AudioStreamRecorder/StereoAudioRecorderHelper.js b/AudioStreamRecorder/StereoAudioRecorderHelper.js new file mode 100644 index 0000000..166629d --- /dev/null +++ b/AudioStreamRecorder/StereoAudioRecorderHelper.js @@ -0,0 +1,230 @@ +// ============================ +// StereoAudioRecorderHelper.js + +// source code from: http://typedarray.org/wp-content/projects/WebAudioRecorder/script.js + +function StereoAudioRecorderHelper(mediaStream, root) { + + // variables + var deviceSampleRate = 44100; // range: 22050 to 96000 + + if (!ObjectStore.AudioContextConstructor) { + ObjectStore.AudioContextConstructor = new ObjectStore.AudioContext(); + } + + // check device sample rate + deviceSampleRate = ObjectStore.AudioContextConstructor.sampleRate; + + var leftchannel = []; + var rightchannel = []; + var scriptprocessornode; + var recording = false; + var recordingLength = 0; + var volume; + var audioInput; + var sampleRate = root.sampleRate || deviceSampleRate; + var context; + + var numChannels = root.audioChannels || 2; + + this.record = function() { + recording = true; + // reset the buffers for the new recording + leftchannel.length = rightchannel.length = 0; + recordingLength = 0; + }; + + this.requestData = function() { + if (isPaused) { + return; + } + + if (recordingLength === 0) { + requestDataInvoked = false; + return; + } + + requestDataInvoked = true; + // clone stuff + var internalLeftChannel = leftchannel.slice(0); + var internalRightChannel = rightchannel.slice(0); + var internalRecordingLength = recordingLength; + + // reset the buffers for the new recording + leftchannel.length = rightchannel.length = []; + recordingLength = 0; + requestDataInvoked = false; + + // we flat the left and right channels down + var leftBuffer = mergeBuffers(internalLeftChannel, internalRecordingLength); + var rightBuffer = mergeBuffers(internalLeftChannel, internalRecordingLength); + + // we interleave both channels together + if (numChannels === 2) { + var interleaved = interleave(leftBuffer, rightBuffer); + } else { + var interleaved = leftBuffer; + } + + // we create our wav file + var buffer = new ArrayBuffer(44 + interleaved.length * 2); + var view = new 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, numChannels, true); + view.setUint32(24, sampleRate, true); + view.setUint32(28, sampleRate * 4, true); + view.setUint16(32, numChannels * 2, 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; + var volume = 1; + for (var i = 0; i < lng; i++) { + view.setInt16(index, interleaved[i] * (0x7FFF * volume), true); + index += 2; + } + + // our final binary blob + var blob = new Blob([view], { + type: 'audio/wav' + }); + + console.debug('audio recorded blob size:', bytesToSize(blob.size)); + + root.ondataavailable(blob); + }; + + this.stop = function() { + // we stop recording + recording = false; + this.requestData(); + + audioInput.disconnect(); + }; + + function interleave(leftChannel, rightChannel) { + var length = leftChannel.length + rightChannel.length; + var result = new 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, recordingLength) { + var result = new Float32Array(recordingLength); + 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 + var context = ObjectStore.AudioContextConstructor; + + // creates a gain node + ObjectStore.VolumeGainNode = context.createGain(); + + var volume = ObjectStore.VolumeGainNode; + + // creates an audio node from the microphone incoming stream + ObjectStore.AudioInput = context.createMediaStreamSource(mediaStream); + + // creates an audio node from the microphone incoming stream + var audioInput = ObjectStore.AudioInput; + + // 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 + Legal values are 256, 512, 1024, 2048, 4096, 8192, and 16384.*/ + var bufferSize = root.bufferSize || 2048; + if (root.bufferSize === 0) { + bufferSize = 0; + } + + if (context.createJavaScriptNode) { + scriptprocessornode = context.createJavaScriptNode(bufferSize, numChannels, numChannels); + } else if (context.createScriptProcessor) { + scriptprocessornode = context.createScriptProcessor(bufferSize, numChannels, numChannels); + } else { + throw 'WebAudio API has no support on this browser.'; + } + + bufferSize = scriptprocessornode.bufferSize; + + console.debug('using audio buffer-size:', bufferSize); + + var requestDataInvoked = false; + + // sometimes "scriptprocessornode" disconnects from he destination-node + // and there is no exception thrown in this case. + // and obviously no further "ondataavailable" events will be emitted. + // below global-scope variable is added to debug such unexpected but "rare" cases. + window.scriptprocessornode = scriptprocessornode; + + if (numChannels === 1) { + console.debug('All right-channels are skipped.'); + } + + var isPaused = false; + + this.pause = function() { + isPaused = true; + }; + + this.resume = function() { + isPaused = false; + }; + + // http://webaudio.github.io/web-audio-api/#the-scriptprocessornode-interface + scriptprocessornode.onaudioprocess = function(e) { + if (!recording || requestDataInvoked || isPaused) { + return; + } + + var left = e.inputBuffer.getChannelData(0); + leftchannel.push(new Float32Array(left)); + + if (numChannels === 2) { + var right = e.inputBuffer.getChannelData(1); + rightchannel.push(new Float32Array(right)); + } + recordingLength += bufferSize; + }; + + volume.connect(scriptprocessornode); + scriptprocessornode.connect(context.destination); +} diff --git a/AudioStreamRecorder/StereoRecorder.js b/AudioStreamRecorder/StereoRecorder.js deleted file mode 100644 index 760699c..0000000 --- a/AudioStreamRecorder/StereoRecorder.js +++ /dev/null @@ -1,31 +0,0 @@ -// ================= -// StereoRecorder.js - -function StereoRecorder(mediaStream) { - // void start(optional long timeSlice) - // timestamp to fire "ondataavailable" - this.start = function(timeSlice) { - timeSlice = timeSlice || 1000; - - mediaRecorder = new StereoAudioRecorder(mediaStream, this); - - mediaRecorder.record(); - - timeout = setInterval(function() { - mediaRecorder.requestData(); - }, timeSlice); - }; - - this.stop = function() { - if (mediaRecorder) { - mediaRecorder.stop(); - clearTimeout(timeout); - } - }; - - this.ondataavailable = function() {}; - - // Reference to "StereoAudioRecorder" object - var mediaRecorder; - var timeout; -} diff --git a/Gruntfile.js b/Gruntfile.js new file mode 100644 index 0000000..4432686 --- /dev/null +++ b/Gruntfile.js @@ -0,0 +1,183 @@ +'use strict'; + +module.exports = function(grunt) { + require('load-grunt-tasks')(grunt, { + pattern: 'grunt-*', + config: 'package.json', + scope: 'devDependencies' + }); + + // configure project + grunt.initConfig({ + // make node configurations available + pkg: grunt.file.readJSON('package.json'), + concat: { + options: { + stripBanners: true, + separator: '\n' + }, + dist: { + src: [ + 'common/head.js', + 'common/MediaStreamRecorder.js', + 'common/MultiStreamRecorder.js', + 'common/Cross-Browser-Declarations.js', + 'common/ObjectStore.js', + 'AudioStreamRecorder/MediaRecorderWrapper.js', + 'AudioStreamRecorder/StereoAudioRecorder.js', + 'AudioStreamRecorder/StereoAudioRecorderHelper.js', + 'VideoStreamRecorder/WhammyRecorder.js', + 'VideoStreamRecorder/WhammyRecorderHelper.js', + 'VideoStreamRecorder/GifRecorder.js', + 'VideoStreamRecorder/lib/whammy.js' + ], + dest: 'MediaStreamRecorder.js' + }, + }, + htmlhint: { + html1: { + src: [ + './demos/*.html' + ], + options: { + 'tag-pair': true + } + } + }, + jshint: { + options: { + + globals: { + webkitIndexedDB: true, + mozIndexedDB: true, + OIndexedDB: true, + msIndexedDB: true, + indexedDB: true, + FileReaderSync: true, + postMessage: true, + Whammy: true, + WhammyRecorder: true, + MediaStreamRecorder: true, + StereoAudioRecorder: true, + URL: true, + webkitURL: true, + DiskStorage: true, + requestAnimationFrame: true, + cancelAnimationFrame: true, + webkitRequestAnimationFrame: true, + webkitCancelAnimationFrame: true, + mozRequestAnimationFrame: true, + mozCancelAnimationFrame: true, + MediaStream: true, + webkitMediaStream: true, + html2canvas: true, + GifRecorder: true, + GIFEncoder: true, + MediaRecorder: true, + webkitAudioContext: true, + mozAudioContext: true, + AudioContext: true, + JSON: true, + typeof: true, + define: true + }, + browser: true, + browserify: true, + node: true, + camelcase: true, + curly: true, + devel: true, + eqeqeq: true, + forin: false, + globalstrict: true, + quotmark: true, + undef: true, + //es5: true, + funcscope: true, + shadow: true, //----should be false? + typed: true, + worker: true + }, + files: ['MediaStreamRecorder.js'], + ignore_warning: { + options: { + '-W015': true + } + } + }, + uglify: { + options: { + mangle: false + }, + my_target: { + files: { + 'MediaStreamRecorder.min.js': ['MediaStreamRecorder.js'] + } + } + }, + jsbeautifier: { + files: [ + './AudioStreamRecorder/*.js', + './VideoStreamRecorder/*.js', + './common/*.js', + 'Gruntfile.js' + ], + options: { + js: { + braceStyle: "collapse", + breakChainedMethods: false, + e4x: false, + evalCode: false, + indentChar: " ", + indentLevel: 0, + indentSize: 4, + indentWithTabs: false, + jslintHappy: false, + keepArrayIndentation: false, + keepFunctionIndentation: false, + maxPreserveNewlines: 10, + preserveNewlines: true, + spaceBeforeConditional: true, + spaceInParen: false, + unescapeStrings: false, + wrapLineLength: 0 + }, + html: { + braceStyle: "collapse", + indentChar: " ", + indentScripts: "keep", + indentSize: 4, + maxPreserveNewlines: 10, + preserveNewlines: true, + unformatted: ["a", "sub", "sup", "b", "i", "u"], + wrapLineLength: 0 + }, + css: { + indentChar: " ", + indentSize: 4 + } + } + }, + bump: { + options: { + files: ['package.json', 'bower.json'], + updateConfigs: [], + commit: true, + commitMessage: 'v%VERSION%', + commitFiles: ['package.json', 'bower.json'], + createTag: true, + tagName: '%VERSION%', + tagMessage: '%VERSION%', + push: false, + pushTo: 'upstream', + gitDescribeOptions: '--tags --always --abbrev=1 --dirty=-d' + } + } + }); + + // enable plugins + + // set default tasks to run when grunt is called without parameters + // http://gruntjs.com/api/grunt.task + grunt.registerTask('default', ['concat', 'jsbeautifier', 'htmlhint', 'jshint', 'uglify']); +}; diff --git a/How-to-Link-Specific-Files.md b/How-to-Link-Specific-Files.md deleted file mode 100644 index cc6f611..0000000 --- a/How-to-Link-Specific-Files.md +++ /dev/null @@ -1,88 +0,0 @@ -## [MediaStreamRecorder.js](https://github.com/streamproc/MediaStreamRecorder) - [Demos](https://www.webrtc-experiment.com/msr/) - [![npm](https://img.shields.io/npm/v/msr.svg)](https://npmjs.org/package/msr) [![downloads](https://img.shields.io/npm/dm/msr.svg)](https://npmjs.org/package/msr) - -You can link specific files: - -To link specific files, you must [download](https://github.com/streamproc/MediaStreamRecorder) this ZIP: - -* https://github.com/streamproc/MediaStreamRecorder/archive/master.zip - -```html - -``` - -`data-require`: Comma separated modules names. Supported values are `StereoRecorder,MediaRecorder,WhammyRecorder,GifRecorder`. - -```html -// to record audio only on chrome -data-require="StereoRecorder" - -// to record audio only on firefox -data-require="MediaRecorder" - -// to record audio both on chrome and firefox -data-require="MediaRecorder,StereoRecorder" - -// to record only video (both on chrome and firefox) -data-require="MediaRecorder,WhammyRecorder" - -// to record multiple streams in chrome -data-require="StereoAudioRecorder,WhammyRecorder,MultiStreamRecorder" - -// to record only gif -data-require="GifRecorder" - -// to record everything -data-require="StereoRecorder,MediaRecorder,WhammyRecorder,GifRecorder,MultiStreamRecorder" -``` - -`data-scripts-dir="/"`: Location of the directory where all required script files resides. - -```html -// root-directory -data-scripts-dir="/" - -// sub/nested directory -data-scripts-dir="../subdir/" - -// same directory where HTML-file is placed -data-scripts-dir="../" - -// you can use absolute-URIs -data-scripts-dir="//cdn.webrtc-experiment.com/msr/" -``` - -You can manually link the files as well; use `data-manual=true`: - -```html - - - - - - - - - - - - - - - - - - - - - - - - -``` - -## License - -[MediaStreamRecorder.js](https://github.com/streamproc/MediaStreamRecorder) library is released under [MIT licence](https://www.webrtc-experiment.com/licence/). diff --git a/MediaStreamRecorder-v1.2.js b/MediaStreamRecorder-v1.2.js deleted file mode 100644 index 7f222c0..0000000 --- a/MediaStreamRecorder-v1.2.js +++ /dev/null @@ -1,123 +0,0 @@ -// Muaz Khan - www.MuazKhan.com -// MIT License - www.webrtc-experiment.com/licence -// Documentation - github.com/streamproc/MediaStreamRecorder - -// ______________________ -// MediaStreamRecorder.js - -function MediaStreamRecorder(mediaStream) { - if (!mediaStream) throw 'MediaStream is mandatory.'; - - // void start(optional long timeSlice) - // timestamp to fire "ondataavailable" - this.start = function(timeSlice) { - // 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.MediaRecorderWrapper; - - // video recorder (in WebM format) - if (this.mimeType.indexOf('video') != -1) { - Recorder = IsChrome ? window.WhammyRecorder : window.MediaRecorderWrapper; - } - - // video recorder (in GIF format) - if (this.mimeType === 'image/gif') Recorder = window.GifRecorder; - - mediaRecorder = new Recorder(mediaStream); - mediaRecorder.ondataavailable = this.ondataavailable; - 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); - }; - - // Reference to "MediaRecorder.js" - var mediaRecorder; -} - -// below scripts are used to auto-load required files. - -function loadScript(src, onload) { - var root = window.MediaStreamRecorderScriptsDir; - - var script = document.createElement('script'); - script.src = root + src; - script.onload = onload || function() {}; - document.documentElement.appendChild(script); -} - -var scripts = document.getElementsByTagName('script'); -var scriptTag = scripts[scripts.length - 1]; - -if (!scriptTag.getAttribute('data-manual')) { - var dataScriptsDir = scriptTag.getAttribute('data-scripts-dir'); - if (dataScriptsDir) { - var selfDir = dataScriptsDir.indexOf('//') != -1 ? '' : scriptTag.src.replace('//', '-----').split('/')[0].replace('-----', '//'); - window.MediaStreamRecorderScriptsDir = selfDir + dataScriptsDir; - } else { - window.MediaStreamRecorderScriptsDir = scriptTag.src.replace(scriptTag.src.split('/').pop(), ''); - } - - var requiredScripts = scriptTag.getAttribute('data-require'); - if (!requiredScripts) { - // for old users - requiredScripts = 'StereoRecorder,MediaRecorder,WhammyRecorder,GifRecorder'; - } - - // cross-browser getUserMedia/AudioContext declarations - loadScript('common/Cross-Browser-Declarations.js'); - - requiredScripts.trim().toLowerCase().split(',').forEach(function(script) { - if (script == 'stereorecorder') { - // stores AudioContext-level objects in memory for re-usability purposes - loadScript('common/ObjectStore.js'); - - // both these files are used to support audio recording in chrome - loadScript('AudioStreamRecorder/StereoRecorder.js'); - loadScript('AudioStreamRecorder/StereoAudioRecorder.js'); - } - - if (script == 'mediarecorder') { - // this one uses MediaRecorder draft for voice & video recording (works only in Firefox) - loadScript('AudioStreamRecorder/MediaRecorder.js'); - } - - if (script == 'whammyrecorder') { - // these files are supporting video-recording in chrome (webm) - loadScript('VideoStreamRecorder/WhammyRecorder.js'); - loadScript('VideoStreamRecorder/WhammyRecorderHelper.js'); - loadScript('VideoStreamRecorder/lib/whammy.js'); - } - - if (script == 'gifrecorder') { - // these files are used to support gif-recording in both chrome & firefox - loadScript('VideoStreamRecorder/GifRecorder.js'); - loadScript('VideoStreamRecorder/lib/gif-encoder.js'); - } - - if(script == 'multistreamrecorder') { - loadScript('MultiStreamRecorder.js'); - } - }); -} diff --git a/MediaStreamRecorder-standalone.js b/MediaStreamRecorder.js similarity index 57% rename from MediaStreamRecorder-standalone.js rename to MediaStreamRecorder.js index ef2e6da..cb96f99 100644 --- a/MediaStreamRecorder-standalone.js +++ b/MediaStreamRecorder.js @@ -1,30 +1,72 @@ +// Last time updated at September 19, 2015 + +// 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 + +// updates? +/* +-. this.recorderType = StereoAudioRecorder; +*/ + +//------------------------------------ + +// 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 -// Documentation - github.com/streamproc/MediaStreamRecorder +// MIT License - www.WebRTC-Experiment.com/licence +//------------------------------------ + +'use strict'; // ______________________ // MediaStreamRecorder.js function MediaStreamRecorder(mediaStream) { - if (!mediaStream) throw 'MediaStream is mandatory.'; + if (!mediaStream) { + throw 'MediaStream is mandatory.'; + } // void start(optional long timeSlice) // timestamp to fire "ondataavailable" this.start = function(timeSlice) { // 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.MediaRecorderWrapper; + var Recorder = IsChrome || IsEdge || IsOpera ? window.StereoAudioRecorder || IsEdge || IsOpera : window.MediaRecorderWrapper; // video recorder (in WebM format) - if (this.mimeType.indexOf('video') != -1) { - Recorder = IsChrome ? window.WhammyRecorder : window.MediaRecorderWrapper; + if (this.mimeType.indexOf('video') !== -1) { + Recorder = IsChrome || IsEdge || IsOpera ? window.WhammyRecorder : window.MediaRecorderWrapper; } // video recorder (in GIF format) - if (this.mimeType === 'image/gif') Recorder = window.GifRecorder; + if (this.mimeType === 'image/gif') { + Recorder = window.GifRecorder; + } + + // allows forcing StereoAudioRecorder.js on Edge/Firefox + if (this.recorderType) { + Recorder = this.recorderType; + } mediaRecorder = new Recorder(mediaStream); - mediaRecorder.ondataavailable = this.ondataavailable; + mediaRecorder.blobs = []; + + var self = this; + mediaRecorder.ondataavailable = function(data) { + mediaRecorder.blobs.push(data); + self.ondataavailable(data); + }; mediaRecorder.onstop = this.onstop; mediaRecorder.onStartedDrawingNonBlankFrames = this.onStartedDrawingNonBlankFrames; @@ -36,12 +78,17 @@ function MediaStreamRecorder(mediaStream) { this.onStartedDrawingNonBlankFrames = function() {}; this.clearOldRecordedFrames = function() { - if (!mediaRecorder) return; + if (!mediaRecorder) { + return; + } + mediaRecorder.clearOldRecordedFrames(); }; this.stop = function() { - if (mediaRecorder) mediaRecorder.stop(); + if (mediaRecorder) { + mediaRecorder.stop(); + } }; this.ondataavailable = function(blob) { @@ -52,48 +99,211 @@ function MediaStreamRecorder(mediaStream) { console.warn('stopped..', error); }; + this.save = function(file, fileName) { + if (!file) { + if (!mediaRecorder) { + return; + } + + var bigBlob = new Blob(mediaRecorder.blobs, { + type: mediaRecorder.blobs[0].type || this.mimeType + }); + + invokeSaveAsDialog(bigBlob); + 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); + }; + + this.recorderType = null; // StereoAudioRecorder || WhammyRecorder || MediaRecorderWrapper || GifRecorder + // Reference to "MediaRecorder.js" var mediaRecorder; } -// below scripts are used to auto-load required files. +// ______________________ +// MultiStreamRecorder.js + +function MultiStreamRecorder(mediaStream) { + if (!mediaStream) { + throw 'MediaStream is mandatory.'; + } -function loadScript(src, onload) { - var root = window.MediaStreamRecorderScriptsDir; + var self = this; + var isFirefox = !!navigator.mozGetUserMedia; - var script = document.createElement('script'); - script.src = root + src; - script.onload = onload || function() {}; - document.documentElement.appendChild(script); -} + this.stream = mediaStream; -// Muaz Khan - www.MuazKhan.com -// MIT License - www.webrtc-experiment.com/licence -// Documentation - github.com/streamproc/MediaStreamRecorder + // void start(optional long timeSlice) + // timestamp to fire "ondataavailable" + this.start = function(timeSlice) { + audioRecorder = new MediaStreamRecorder(mediaStream); + videoRecorder = new MediaStreamRecorder(mediaStream); + + audioRecorder.mimeType = 'audio/ogg'; + videoRecorder.mimeType = 'video/webm'; + + for (var prop in this) { + if (typeof this[prop] !== 'function') { + audioRecorder[prop] = videoRecorder[prop] = this[prop]; + } + } + + audioRecorder.ondataavailable = function(blob) { + if (!audioVideoBlobs[recordingInterval]) { + audioVideoBlobs[recordingInterval] = {}; + } + + audioVideoBlobs[recordingInterval].audio = blob; + + if (audioVideoBlobs[recordingInterval].video && !audioVideoBlobs[recordingInterval].onDataAvailableEventFired) { + audioVideoBlobs[recordingInterval].onDataAvailableEventFired = true; + fireOnDataAvailableEvent(audioVideoBlobs[recordingInterval]); + } + }; + + videoRecorder.ondataavailable = function(blob) { + if (isFirefox) { + return self.ondataavailable({ + video: blob, + audio: blob + }); + } + + if (!audioVideoBlobs[recordingInterval]) { + audioVideoBlobs[recordingInterval] = {}; + } + + audioVideoBlobs[recordingInterval].video = blob; + + if (audioVideoBlobs[recordingInterval].audio && !audioVideoBlobs[recordingInterval].onDataAvailableEventFired) { + audioVideoBlobs[recordingInterval].onDataAvailableEventFired = true; + fireOnDataAvailableEvent(audioVideoBlobs[recordingInterval]); + } + }; + + function fireOnDataAvailableEvent(blobs) { + recordingInterval++; + self.ondataavailable(blobs); + } + + videoRecorder.onstop = audioRecorder.onstop = function(error) { + self.onstop(error); + }; + + if (!isFirefox) { + // to make sure both audio/video are synced. + videoRecorder.onStartedDrawingNonBlankFrames = function() { + videoRecorder.clearOldRecordedFrames(); + audioRecorder.start(timeSlice); + }; + videoRecorder.start(timeSlice); + } else { + videoRecorder.start(timeSlice); + } + }; + + this.stop = function() { + if (audioRecorder) { + audioRecorder.stop(); + } + if (videoRecorder) { + videoRecorder.stop(); + } + }; + + this.ondataavailable = function(blob) { + console.log('ondataavailable..', blob); + }; + + this.onstop = function(error) { + console.warn('stopped..', error); + }; + + this.pause = function() { + if (audioRecorder) { + audioRecorder.pause(); + } + if (videoRecorder) { + videoRecorder.pause(); + } + }; + + this.resume = function() { + if (audioRecorder) { + audioRecorder.resume(); + } + if (videoRecorder) { + videoRecorder.resume(); + } + }; + + var audioRecorder; + var videoRecorder; + + var audioVideoBlobs = {}; + var recordingInterval = 0; +} // _____________________________ // Cross-Browser-Declarations.js -// animation-frame used in WebM recording -if (!window.requestAnimationFrame) { - requestAnimationFrame = window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame; -} +// WebAudio API representer +if (typeof AudioContext !== 'undefined') { + if (typeof webkitAudioContext !== 'undefined') { + /*global AudioContext:true*/ + var AudioContext = webkitAudioContext; + } -if (!window.cancelAnimationFrame) { - cancelAnimationFrame = window.webkitCancelAnimationFrame || window.mozCancelAnimationFrame; + if (typeof mozAudioContext !== 'undefined') { + /*global AudioContext:true*/ + var AudioContext = mozAudioContext; + } } -// WebAudio API representer -if (!window.AudioContext) { - window.AudioContext = window.webkitAudioContext || window.mozAudioContext; +if (typeof URL !== 'undefined' && typeof webkitURL !== 'undefined') { + /*global URL:true*/ + var URL = webkitURL; } -URL = window.URL || window.webkitURL; -navigator.getUserMedia = navigator.webkitGetUserMedia || navigator.mozGetUserMedia; +var IsEdge = navigator.userAgent.indexOf('Edge') !== -1 && (!!navigator.msSaveBlob || !!navigator.msSaveOrOpenBlob); +var IsOpera = !!window.opera || navigator.userAgent.indexOf('OPR/') !== -1; +var IsChrome = !IsEdge && !IsEdge && !!navigator.webkitGetUserMedia; -if (window.webkitMediaStream) window.MediaStream = window.webkitMediaStream; +if (typeof navigator !== 'undefined') { + if (typeof navigator.webkitGetUserMedia !== 'undefined') { + navigator.getUserMedia = navigator.webkitGetUserMedia; + } + + if (typeof navigator.mozGetUserMedia !== 'undefined') { + navigator.getUserMedia = navigator.mozGetUserMedia; + } +} else { + /*global navigator:true */ + var navigator = { + getUserMedia: {} + }; +} -IsChrome = !!navigator.webkitGetUserMedia; +if (typeof webkitMediaStream !== 'undefined') { + var MediaStream = webkitMediaStream; +} // Merge all other data-types except "function" @@ -110,22 +320,98 @@ function mergeProps(mergein, mergeto) { function reformatProps(obj) { var output = {}; for (var o in obj) { - if (o.indexOf('-') != -1) { + if (o.indexOf('-') !== -1) { var splitted = o.split('-'); var name = splitted[0] + splitted[1].split('')[0].toUpperCase() + splitted[1].substr(1); output[name] = obj[o]; - } else output[o] = obj[o]; + } else { + output[o] = obj[o]; + } } return output; } +// "dropFirstFrame" has been added by Graham Roth +// https://github.com/gsroth + +function dropFirstFrame(arr) { + arr.shift(); + return arr; +} + +function invokeSaveAsDialog(file, fileName) { + if (!file) { + throw 'Blob object is required.'; + } + + if (!file.type) { + file.type = 'video/webm'; + } + + var fileExtension = file.type.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: window.AudioContext || window.webkitAudioContext +}; + // ______________ (used to handle stuff like http://goo.gl/xmE5eg) issue #129 // ObjectStore.js var ObjectStore = { AudioContext: window.AudioContext || window.webkitAudioContext }; -// ================ +// ================== // MediaRecorder.js /** @@ -143,7 +429,7 @@ function MediaRecorderWrapper(mediaStream) { // if user chosen only audio option; and he tried to pass MediaStream with // both audio and video tracks; // using a dirty workaround to generate audio-only stream so that we can get audio/ogg output. - if (this.type == 'audio' && mediaStream.getVideoTracks && mediaStream.getVideoTracks().length && !navigator.mozGetUserMedia) { + if (this.type === 'audio' && mediaStream.getVideoTracks && mediaStream.getVideoTracks().length && !navigator.mozGetUserMedia) { var context = new AudioContext(); var mediaStreamSource = context.createMediaStreamSource(mediaStream); @@ -163,14 +449,21 @@ function MediaRecorderWrapper(mediaStream) { isStopRecording = false; function startRecording() { - if (isStopRecording) return; + if (isStopRecording) { + return; + } + + if (isPaused) { + setTimeout(startRecording, 500); + return; + } mediaRecorder = new MediaRecorder(mediaStream); mediaRecorder.ondataavailable = function(e) { console.log('ondataavailable', e.data.type, e.data.size, e.data); - // mediaRecorder.state == 'recording' means that media recorder is associated with "session" - // mediaRecorder.state == 'stopped' means that media recorder is detached from the "session" ... in this case; "session" will also be deleted. + // mediaRecorder.state === 'recording' means that media recorder is associated with "session" + // mediaRecorder.state === 'stopped' means that media recorder is detached from the "session" ... in this case; "session" will also be deleted. if (!e.data.size) { console.warn('Recording of', e.data.type, 'failed.'); @@ -246,6 +539,32 @@ function MediaRecorderWrapper(mediaStream) { } }; + var isPaused = false; + + this.pause = function() { + if (!mediaRecorder) { + return; + } + + isPaused = true; + + if (mediaRecorder.state === 'recording') { + mediaRecorder.pause(); + } + }; + + this.resume = function() { + if (!mediaRecorder) { + return; + } + + isPaused = false; + + if (mediaRecorder.state === 'paused') { + mediaRecorder.resume(); + } + }; + this.ondataavailable = this.onstop = function() {}; // Reference to itself @@ -259,16 +578,16 @@ function MediaRecorderWrapper(mediaStream) { var mediaRecorder; } -// ================= -// StereoRecorder.js +// ====================== +// StereoAudioRecorder.js -function StereoRecorder(mediaStream) { +function StereoAudioRecorder(mediaStream) { // void start(optional long timeSlice) // timestamp to fire "ondataavailable" this.start = function(timeSlice) { timeSlice = timeSlice || 1000; - mediaRecorder = new StereoAudioRecorder(mediaStream, this); + mediaRecorder = new StereoAudioRecorderHelper(mediaStream, this); mediaRecorder.record(); @@ -284,6 +603,22 @@ function StereoRecorder(mediaStream) { } }; + this.pause = function() { + if (!mediaRecorder) { + return; + } + + mediaRecorder.pause(); + }; + + this.resume = function() { + if (!mediaRecorder) { + return; + } + + mediaRecorder.resume(); + }; + this.ondataavailable = function() {}; // Reference to "StereoAudioRecorder" object @@ -291,21 +626,23 @@ function StereoRecorder(mediaStream) { var timeout; } -// ====================== -// StereoAudioRecorder.js +// ============================ +// StereoAudioRecorderHelper.js // source code from: http://typedarray.org/wp-content/projects/WebAudioRecorder/script.js -function StereoAudioRecorder(mediaStream, root) { +function StereoAudioRecorderHelper(mediaStream, root) { // variables var deviceSampleRate = 44100; // range: 22050 to 96000 - - // check device sample rate - if(window.AudioContext){ - deviceSampleRate = (new window.AudioContext()).sampleRate; + + if (!ObjectStore.AudioContextConstructor) { + ObjectStore.AudioContextConstructor = new ObjectStore.AudioContext(); } + // check device sample rate + deviceSampleRate = ObjectStore.AudioContextConstructor.sampleRate; + var leftchannel = []; var rightchannel = []; var scriptprocessornode; @@ -314,7 +651,6 @@ function StereoAudioRecorder(mediaStream, root) { var volume; var audioInput; var sampleRate = root.sampleRate || deviceSampleRate; - var audioContext; var context; var numChannels = root.audioChannels || 2; @@ -327,16 +663,20 @@ function StereoAudioRecorder(mediaStream, root) { }; this.requestData = function() { - if (recordingLength == 0) { + if (isPaused) { + return; + } + + if (recordingLength === 0) { requestDataInvoked = false; return; } requestDataInvoked = true; // clone stuff - var internal_leftchannel = leftchannel.slice(0); - var internal_rightchannel = rightchannel.slice(0); - var internal_recordingLength = recordingLength; + var internalLeftChannel = leftchannel.slice(0); + var internalRightChannel = rightchannel.slice(0); + var internalRecordingLength = recordingLength; // reset the buffers for the new recording leftchannel.length = rightchannel.length = []; @@ -344,8 +684,8 @@ function StereoAudioRecorder(mediaStream, root) { requestDataInvoked = false; // we flat the left and right channels down - var leftBuffer = mergeBuffers(internal_leftchannel, internal_recordingLength); - var rightBuffer = mergeBuffers(internal_leftchannel, internal_recordingLength); + var leftBuffer = mergeBuffers(internalLeftChannel, internalRecordingLength); + var rightBuffer = mergeBuffers(internalLeftChannel, internalRecordingLength); // we interleave both channels together if (numChannels === 2) { @@ -437,13 +777,6 @@ function StereoAudioRecorder(mediaStream, root) { } // creates the audio context - - // creates the audio context - var audioContext = ObjectStore.AudioContext; - - if (!ObjectStore.AudioContextConstructor) - ObjectStore.AudioContextConstructor = new audioContext(); - var context = ObjectStore.AudioContextConstructor; // creates a gain node @@ -466,7 +799,9 @@ function StereoAudioRecorder(mediaStream, root) { Higher values will be necessary to avoid audio breakup and glitches Legal values are 256, 512, 1024, 2048, 4096, 8192, and 16384.*/ var bufferSize = root.bufferSize || 2048; - if (root.bufferSize == 0) bufferSize = 0; + if (root.bufferSize === 0) { + bufferSize = 0; + } if (context.createJavaScriptNode) { scriptprocessornode = context.createJavaScriptNode(bufferSize, numChannels, numChannels); @@ -488,18 +823,30 @@ function StereoAudioRecorder(mediaStream, root) { // below global-scope variable is added to debug such unexpected but "rare" cases. window.scriptprocessornode = scriptprocessornode; - if (numChannels == 1) { + if (numChannels === 1) { console.debug('All right-channels are skipped.'); } + var isPaused = false; + + this.pause = function() { + isPaused = true; + }; + + this.resume = function() { + isPaused = false; + }; + // http://webaudio.github.io/web-audio-api/#the-scriptprocessornode-interface scriptprocessornode.onaudioprocess = function(e) { - if (!recording || requestDataInvoked) return; + if (!recording || requestDataInvoked || isPaused) { + return; + } var left = e.inputBuffer.getChannelData(0); leftchannel.push(new Float32Array(left)); - if (numChannels == 2) { + if (numChannels === 2) { var right = e.inputBuffer.getChannelData(1); rightchannel.push(new Float32Array(right)); } @@ -510,21 +857,89 @@ function StereoAudioRecorder(mediaStream, root) { scriptprocessornode.connect(context.destination); } -// ======================= -// WhammyRecorderHelper.js +// =================== +// WhammyRecorder.js -function WhammyRecorderHelper(mediaStream, root) { - this.record = function(timeSlice) { - if (!this.width) this.width = 320; - if (!this.height) this.height = 240; +function WhammyRecorder(mediaStream) { + // void start(optional long timeSlice) + // timestamp to fire "ondataavailable" + this.start = function(timeSlice) { + timeSlice = timeSlice || 1000; - if (this.video && this.video instanceof HTMLVideoElement) { - if (!this.width) this.width = video.videoWidth || video.clientWidth || 320; - if (!this.height) this.height = video.videoHeight || video.clientHeight || 240; + mediaRecorder = new WhammyRecorderHelper(mediaStream, this); + + for (var prop in this) { + if (typeof this[prop] !== 'function') { + mediaRecorder[prop] = this[prop]; + } } - if (!this.video) { - this.video = { + mediaRecorder.record(); + + timeout = setInterval(function() { + mediaRecorder.requestData(); + }, timeSlice); + }; + + this.stop = function() { + if (mediaRecorder) { + mediaRecorder.stop(); + clearTimeout(timeout); + } + }; + + this.clearOldRecordedFrames = function() { + if (mediaRecorder) { + mediaRecorder.clearOldRecordedFrames(); + } + }; + + this.pause = function() { + if (!mediaRecorder) { + return; + } + + mediaRecorder.pause(); + }; + + this.resume = function() { + if (!mediaRecorder) { + return; + } + + mediaRecorder.resume(); + }; + + this.ondataavailable = function() {}; + + // Reference to "WhammyRecorder" object + var mediaRecorder; + var timeout; +} + +// ========================== +// WhammyRecorderHelper.js + +function WhammyRecorderHelper(mediaStream, root) { + this.record = function(timeSlice) { + if (!this.width) { + this.width = 320; + } + if (!this.height) { + this.height = 240; + } + + if (this.video && this.video instanceof HTMLVideoElement) { + if (!this.width) { + this.width = video.videoWidth || video.clientWidth || 320; + } + if (!this.height) { + this.height = video.videoHeight || video.clientHeight || 240; + } + } + + if (!this.video) { + this.video = { width: this.width, height: this.height }; @@ -542,6 +957,7 @@ function WhammyRecorderHelper(mediaStream, root) { // setting defaults if (this.video && this.video instanceof HTMLVideoElement) { + this.isHTMLObject = true; video = this.video.cloneNode(); } else { video = document.createElement('video'); @@ -564,55 +980,77 @@ function WhammyRecorderHelper(mediaStream, root) { }; this.clearOldRecordedFrames = function() { - frames = []; + whammy.frames = []; }; var requestDataInvoked = false; this.requestData = function() { - if (!frames.length) { + if (isPaused) { + return; + } + + if (!whammy.frames.length) { requestDataInvoked = false; return; } requestDataInvoked = true; // clone stuff - var internal_frames = frames.slice(0); + var internalFrames = whammy.frames.slice(0); // reset the frames for the new recording - frames = []; - whammy.frames = dropBlackFrames(internal_frames, -1); + whammy.frames = dropBlackFrames(internalFrames, -1); - var WebM_Blob = whammy.compile(); - root.ondataavailable(WebM_Blob); + whammy.compile(function(whammyBlob) { + root.ondataavailable(whammyBlob); + console.debug('video recorded blob size:', bytesToSize(whammyBlob.size)); + }); - console.debug('video recorded blob size:', bytesToSize(WebM_Blob.size)); + whammy.frames = []; requestDataInvoked = false; }; - var frames = []; - var isOnStartedDrawingNonBlankFramesInvoked = false; function drawFrames() { - if (isStopDrawing) return; + if (isPaused) { + lastTime = new Date().getTime(); + setTimeout(drawFrames, 500); + return; + } - if (requestDataInvoked) return setTimeout(drawFrames, 100); + if (isStopDrawing) { + return; + } + + if (requestDataInvoked) { + return setTimeout(drawFrames, 100); + } var duration = new Date().getTime() - lastTime; - if (!duration) return drawFrames(); + if (!duration) { + return drawFrames(); + } // via webrtc-experiment#206, by Jack i.e. @Seymourr lastTime = new Date().getTime(); + if (!self.isHTMLObject && video.paused) { + video.play(); // Android + } + context.drawImage(video, 0, 0, canvas.width, canvas.height); - !isStopDrawing && frames.push({ - duration: duration, - image: canvas.toDataURL('image/webp') - }); - if (!isOnStartedDrawingNonBlankFramesInvoked && !isBlankFrame(frames[frames.length - 1])) { + if (!isStopDrawing) { + whammy.frames.push({ + duration: duration, + image: canvas.toDataURL('image/webp') + }); + } + + if (!isOnStartedDrawingNonBlankFramesInvoked && !isBlankFrame(whammy.frames[whammy.frames.length - 1])) { isOnStartedDrawingNonBlankFramesInvoked = true; root.onStartedDrawingNonBlankFrames(); } @@ -764,416 +1202,23 @@ function WhammyRecorderHelper(mediaStream, root) { return resultFrames; } -} - -// ================= -// WhammyRecorder.js - -function WhammyRecorder(mediaStream) { - // void start(optional long timeSlice) - // timestamp to fire "ondataavailable" - this.start = function(timeSlice) { - timeSlice = timeSlice || 1000; - mediaRecorder = new WhammyRecorderHelper(mediaStream, this); - - for (var prop in this) { - if (typeof this[prop] !== 'function') { - mediaRecorder[prop] = this[prop]; - } - } + var isPaused = false; - mediaRecorder.record(); - - timeout = setInterval(function() { - mediaRecorder.requestData(); - }, timeSlice); + this.pause = function() { + isPaused = true; }; - this.stop = function() { - if (mediaRecorder) { - mediaRecorder.stop(); - clearTimeout(timeout); - } + this.resume = function() { + isPaused = false; }; - - this.clearOldRecordedFrames = function() { - if (mediaRecorder) { - mediaRecorder.clearOldRecordedFrames(); - } - }; - - this.ondataavailable = function() {}; - - // Reference to "WhammyRecorder" object - var mediaRecorder; - var timeout; } - -// Muaz Khan - https://github.com/muaz-khan -// neizerth - https://github.com/neizerth -// MIT License - https://www.webrtc-experiment.com/licence/ -// Documentation - https://github.com/streamproc/MediaStreamRecorder - -// Note: -// ========================================================== -// whammy.js is an "external library" -// and has its own copyrights. Taken from "Whammy" project. - - -// 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? - -var Whammy = (function() { - - function toWebM(frames) { - var info = checkFrames(frames); - - var CLUSTER_MAX_DURATION = 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": 0x63c5 // 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 < CLUSTER_MAX_DURATION); - - var clusterCounter = 0; - var cluster = { - "id": 0x1f43b675, // Cluster - "data": [{ - "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 - }; - })) - }; //Add cluster to segment - EBML[1].data.push(cluster); - clusterTimecode += clusterDuration; - } - - return generateEBML(EBML); - } - - // sums the lengths of all the frames and gets the duration - - function checkFrames(frames) { - if (!frames[0]) { - console.warn('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 size_str = len.toString(2); - var padded = (new Array((zeroes * 7 + 7 + 1) - size_str.length)).join('0') + size_str; - 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 toBinStr_old(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 generateEBML_old(json) { - var ebml = ''; - for (var i = 0; i < json.length; i++) { - var data = json[i].data; - if (typeof data == 'object') data = generateEBML_old(data); - if (typeof data == 'number') data = toBinStr_old(data.toString(2)); - - var len = data.length; - var zeroes = Math.ceil(Math.ceil(Math.log(len) / Math.log(2)) / 8); - var size_str = len.toString(2); - var padded = (new Array((zeroes * 7 + 7 + 1) - size_str.length)).join('0') + size_str; - var size = (new Array(zeroes)).join('0') + '1' + padded; - - ebml += toBinStr_old(json[i].id.toString(2)) + toBinStr_old(size) + data; - - } - return ebml; - } - - 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 frame_start = 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(frame_start + 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 parseRIFF(string) { - var offset = 0; - var chunks = {}; - - while (offset < string.length) { - var id = string.substr(offset, 4); - var len = 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); - 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(''); - } - - // a more abstract-ish API - - function WhammyVideo(duration) { - this.frames = []; - this.duration = duration || 1; - this.quality = 100; - } - - 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 - }); - }; - WhammyVideo.prototype.compile = function() { - return new toWebM(this.frames.map(function(frame) { - var webp = parseWebP(parseRIFF(atob(frame.image.slice(23)))); - webp.duration = frame.duration; - return webp; - })); - }; - return { - Video: WhammyVideo, - toWebM: toWebM - }; -})(); - -// Muaz Khan - https://github.com/muaz-khan -// neizerth - https://github.com/neizerth -// MIT License - https://www.webrtc-experiment.com/licence/ -// Documentation - https://github.com/streamproc/MediaStreamRecorder -// ========================================================== +// -------------- // GifRecorder.js function GifRecorder(mediaStream) { - if (!window.GIFEncoder) { + if (typeof GIFEncoder === 'undefined') { throw 'Please link: https://cdn.webrtc-experiment.com/gif-recorder.js'; } @@ -1218,6 +1263,11 @@ function GifRecorder(mediaStream) { startTime = Date.now(); function drawVideoFrame(time) { + if (isPaused) { + setTimeout(drawVideoFrame, 500, time); + return; + } + lastAnimationFrame = requestAnimationFrame(drawVideoFrame); if (typeof lastFrameTime === undefined) { @@ -1225,7 +1275,13 @@ function GifRecorder(mediaStream) { } // ~10 fps - if (time - lastFrameTime < 90) return; + if (time - lastFrameTime < 90) { + return; + } + + if (video.paused) { + video.play(); // Android + } context.drawImage(video, 0, 0, imageWidth, imageHeight); @@ -1252,7 +1308,7 @@ function GifRecorder(mediaStream) { // todo: find a way to clear old recorded blobs gifEncoder.stream().bin = []; - }; + } this.stop = function() { if (lastAnimationFrame) { @@ -1262,6 +1318,16 @@ function GifRecorder(mediaStream) { } }; + var isPaused = false; + + this.pause = function() { + isPaused = true; + }; + + this.resume = function() { + isPaused = false; + }; + this.ondataavailable = function() {}; this.onstop = function() {}; @@ -1284,112 +1350,433 @@ function GifRecorder(mediaStream) { var timeout; } -// ______________________ -// MultiStreamRecorder.js +// https://github.com/antimatter15/whammy/blob/master/LICENSE +// _________ +// Whammy.js -function MultiStreamRecorder(mediaStream) { - if (!mediaStream) throw 'MediaStream is mandatory.'; +// 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? - var self = this; - var isFirefox = !!navigator.mozGetUserMedia; +/** + * 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. + * @typedef Whammy + * @class + * @example + * var recorder = new Whammy().Video(15); + * recorder.add(context || canvas || dataURL); + * var output = recorder.compile(); + */ - this.stream = mediaStream; +var Whammy = (function() { + // a more abstract-ish API - // void start(optional long timeSlice) - // timestamp to fire "ondataavailable" - this.start = function(timeSlice) { - audioRecorder = new MediaStreamRecorder(mediaStream); - videoRecorder = new MediaStreamRecorder(mediaStream); + function WhammyVideo(duration) { + this.frames = []; + this.duration = duration || 1; + this.quality = 0.8; + } - audioRecorder.mimeType = 'audio/ogg'; - videoRecorder.mimeType = 'video/webm'; + /** + * 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; + } - for (var prop in this) { - if (typeof this[prop] !== 'function') { - audioRecorder[prop] = videoRecorder[prop] = this[prop]; - } + if ('toDataURL' in frame) { + frame = frame.toDataURL('image/webp', this.quality); } - audioRecorder.ondataavailable = function(blob) { - if (!audioVideoBlobs[recordingInterval]) { - audioVideoBlobs[recordingInterval] = {}; + 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 (e) {' + _function.name + '(e.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 []; } - audioVideoBlobs[recordingInterval].audio = blob; + var clusterMaxDuration = 30000; - if (audioVideoBlobs[recordingInterval].video && !audioVideoBlobs[recordingInterval].onDataAvailableEventFired) { - audioVideoBlobs[recordingInterval].onDataAvailableEventFired = true; - fireOnDataAvailableEvent(audioVideoBlobs[recordingInterval]); + 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; } - }; - videoRecorder.ondataavailable = function(blob) { - if (isFirefox) { - return self.ondataavailable({ - video: blob, - audio: blob + 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; } - if (!audioVideoBlobs[recordingInterval]) { - audioVideoBlobs[recordingInterval] = {}; + 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 + }; + } - audioVideoBlobs[recordingInterval].video = blob; + function numToBuffer(num) { + var parts = []; + while (num > 0) { + parts.push(num & 0xff); + num = num >> 8; + } + return new Uint8Array(parts.reverse()); + } - if (audioVideoBlobs[recordingInterval].audio && !audioVideoBlobs[recordingInterval].onDataAvailableEventFired) { - audioVideoBlobs[recordingInterval].onDataAvailableEventFired = true; - fireOnDataAvailableEvent(audioVideoBlobs[recordingInterval]); + 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 fireOnDataAvailableEvent(blobs) { - recordingInterval++; - self.ondataavailable(blobs); + 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' + }); } - videoRecorder.onstop = audioRecorder.onstop = function(error) { - self.onstop(error); - }; + 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; + } - if (!isFirefox) { - // to make sure both audio/video are synced. - videoRecorder.onStartedDrawingNonBlankFrames = function() { - videoRecorder.clearOldRecordedFrames(); - audioRecorder.start(timeSlice); - }; - videoRecorder.start(timeSlice); - } else { - videoRecorder.start(timeSlice); + 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; } - }; - this.stop = function() { - if (audioRecorder) audioRecorder.stop(); - if (videoRecorder) videoRecorder.stop(); - }; + function parseWebP(riff) { + var VP8 = riff.RIFF[0].WEBP[0]; - this.ondataavailable = function(blob) { - console.log('ondataavailable..', blob); - }; + 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); + } - this.onstop = function(error) { - console.warn('stopped..', error); - }; + 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 + }; + } - var audioRecorder; - var videoRecorder; + 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); + } - var audioVideoBlobs = {}; - var recordingInterval = 0; -} + 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 bytesToSize(bytes) { - var k = 1000; - var sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']; - if (bytes === 0) { - return '0 Bytes'; + 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); } - var i = parseInt(Math.floor(Math.log(bytes) / Math.log(k)), 10); - return (bytes / Math.pow(k, i)).toPrecision(3) + ' ' + sizes[i]; -} + + /** + * 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 + }; +})(); diff --git a/MediaStreamRecorder.min.js b/MediaStreamRecorder.min.js new file mode 100644 index 0000000..de37ad4 --- /dev/null +++ b/MediaStreamRecorder.min.js @@ -0,0 +1 @@ +"use strict";function MediaStreamRecorder(mediaStream){if(!mediaStream)throw"MediaStream is mandatory.";this.start=function(timeSlice){var Recorder=IsChrome||IsEdge||IsOpera?window.StereoAudioRecorder||IsEdge||IsOpera:window.MediaRecorderWrapper;-1!==this.mimeType.indexOf("video")&&(Recorder=IsChrome||IsEdge||IsOpera?window.WhammyRecorder:window.MediaRecorderWrapper),"image/gif"===this.mimeType&&(Recorder=window.GifRecorder),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,mediaRecorder=mergeProps(mediaRecorder,this),mediaRecorder.start(timeSlice)},this.onStartedDrawingNonBlankFrames=function(){},this.clearOldRecordedFrames=function(){mediaRecorder&&mediaRecorder.clearOldRecordedFrames()},this.stop=function(){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;var bigBlob=new Blob(mediaRecorder.blobs,{type:mediaRecorder.blobs[0].type||this.mimeType});return void invokeSaveAsDialog(bigBlob)}invokeSaveAsDialog(file,fileName)},this.pause=function(){mediaRecorder&&(mediaRecorder.pause(),console.log("Paused recording.",this.mimeType||mediaRecorder.mimeType))},this.resume=function(){mediaRecorder&&(mediaRecorder.resume(),console.log("Resumed recording.",this.mimeType||mediaRecorder.mimeType))},this.recorderType=null;var mediaRecorder}function MultiStreamRecorder(mediaStream){if(!mediaStream)throw"MediaStream is mandatory.";var self=this,isFirefox=!!navigator.mozGetUserMedia;this.stream=mediaStream,this.start=function(timeSlice){function fireOnDataAvailableEvent(blobs){recordingInterval++,self.ondataavailable(blobs)}audioRecorder=new MediaStreamRecorder(mediaStream),videoRecorder=new MediaStreamRecorder(mediaStream),audioRecorder.mimeType="audio/ogg",videoRecorder.mimeType="video/webm";for(var prop in this)"function"!=typeof this[prop]&&(audioRecorder[prop]=videoRecorder[prop]=this[prop]);audioRecorder.ondataavailable=function(blob){audioVideoBlobs[recordingInterval]||(audioVideoBlobs[recordingInterval]={}),audioVideoBlobs[recordingInterval].audio=blob,audioVideoBlobs[recordingInterval].video&&!audioVideoBlobs[recordingInterval].onDataAvailableEventFired&&(audioVideoBlobs[recordingInterval].onDataAvailableEventFired=!0,fireOnDataAvailableEvent(audioVideoBlobs[recordingInterval]))},videoRecorder.ondataavailable=function(blob){return isFirefox?self.ondataavailable({video:blob,audio:blob}):(audioVideoBlobs[recordingInterval]||(audioVideoBlobs[recordingInterval]={}),audioVideoBlobs[recordingInterval].video=blob,void(audioVideoBlobs[recordingInterval].audio&&!audioVideoBlobs[recordingInterval].onDataAvailableEventFired&&(audioVideoBlobs[recordingInterval].onDataAvailableEventFired=!0,fireOnDataAvailableEvent(audioVideoBlobs[recordingInterval]))))},videoRecorder.onstop=audioRecorder.onstop=function(error){self.onstop(error)},isFirefox?videoRecorder.start(timeSlice):(videoRecorder.onStartedDrawingNonBlankFrames=function(){videoRecorder.clearOldRecordedFrames(),audioRecorder.start(timeSlice)},videoRecorder.start(timeSlice))},this.stop=function(){audioRecorder&&audioRecorder.stop(),videoRecorder&&videoRecorder.stop()},this.ondataavailable=function(blob){console.log("ondataavailable..",blob)},this.onstop=function(error){console.warn("stopped..",error)},this.pause=function(){audioRecorder&&audioRecorder.pause(),videoRecorder&&videoRecorder.pause()},this.resume=function(){audioRecorder&&audioRecorder.resume(),videoRecorder&&videoRecorder.resume()};var audioRecorder,videoRecorder,audioVideoBlobs={},recordingInterval=0}function mergeProps(mergein,mergeto){mergeto=reformatProps(mergeto);for(var t in mergeto)"function"!=typeof mergeto[t]&&(mergein[t]=mergeto[t]);return mergein}function reformatProps(obj){var output={};for(var o in obj)if(-1!==o.indexOf("-")){var splitted=o.split("-"),name=splitted[0]+splitted[1].split("")[0].toUpperCase()+splitted[1].substr(1);output[name]=obj[o]}else output[o]=obj[o];return output}function dropFirstFrame(arr){return arr.shift(),arr}function invokeSaveAsDialog(file,fileName){if(!file)throw"Blob object is required.";file.type||(file.type="video/webm");var fileExtension=file.type.split("/")[1];if(fileName&&-1!==fileName.indexOf(".")){var splitted=fileName.split(".");fileName=splitted[0],fileExtension=splitted[1]}var fileFullName=(fileName||Math.round(9999999999*Math.random())+888888888)+"."+fileExtension;if("undefined"!=typeof navigator.msSaveOrOpenBlob)return navigator.msSaveOrOpenBlob(file,fileFullName);if("undefined"!=typeof navigator.msSaveBlob)return navigator.msSaveBlob(file,fileFullName);var hyperlink=document.createElement("a");hyperlink.href=URL.createObjectURL(file),hyperlink.target="_blank",hyperlink.download=fileFullName,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:!0,cancelable:!0});hyperlink.dispatchEvent(evt),navigator.mozGetUserMedia||URL.revokeObjectURL(hyperlink.href)}function bytesToSize(bytes){var k=1e3,sizes=["Bytes","KB","MB","GB","TB"];if(0===bytes)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]}function MediaRecorderWrapper(mediaStream){if("audio"===this.type&&mediaStream.getVideoTracks&&mediaStream.getVideoTracks().length&&!navigator.mozGetUserMedia){var context=new AudioContext,mediaStreamSource=context.createMediaStreamSource(mediaStream),destination=context.createMediaStreamDestination();mediaStreamSource.connect(destination),mediaStream=destination.stream}this.start=function(mTimeSlice){function startRecording(){if(!isStopRecording){if(isPaused)return void setTimeout(startRecording,500);mediaRecorder=new MediaRecorder(mediaStream),mediaRecorder.ondataavailable=function(e){if(console.log("ondataavailable",e.data.type,e.data.size,e.data),!e.data.size)return void console.warn("Recording of",e.data.type,"failed.");var blob=new window.Blob([e.data],{type:e.data.type||self.mimeType||"audio/ogg"});self.ondataavailable(blob)},mediaRecorder.onstop=function(error){},mediaRecorder.onerror=function(error){console.error(error),self.start(mTimeSlice)},mediaRecorder.onwarning=function(warning){console.warn(warning)},mediaRecorder.start(0),setTimeout(function(){mediaRecorder.stop(),startRecording()},mTimeSlice)}}mTimeSlice=mTimeSlice||1e3,isStopRecording=!1,startRecording()};var isStopRecording=!1;this.stop=function(){isStopRecording=!0,self.onstop&&self.onstop({})};var isPaused=!1;this.pause=function(){mediaRecorder&&(isPaused=!0,"recording"===mediaRecorder.state&&mediaRecorder.pause())},this.resume=function(){mediaRecorder&&(isPaused=!1,"paused"===mediaRecorder.state&&mediaRecorder.resume())},this.ondataavailable=this.onstop=function(){};var self=this;!self.mimeType&&mediaStream.getAudioTracks&&(self.mimeType=mediaStream.getAudioTracks().length&&mediaStream.getVideoTracks().length?"video/webm":"audio/ogg");var mediaRecorder}function StereoAudioRecorder(mediaStream){this.start=function(timeSlice){timeSlice=timeSlice||1e3,mediaRecorder=new StereoAudioRecorderHelper(mediaStream,this),mediaRecorder.record(),timeout=setInterval(function(){mediaRecorder.requestData()},timeSlice)},this.stop=function(){mediaRecorder&&(mediaRecorder.stop(),clearTimeout(timeout))},this.pause=function(){mediaRecorder&&mediaRecorder.pause()},this.resume=function(){mediaRecorder&&mediaRecorder.resume()},this.ondataavailable=function(){};var mediaRecorder,timeout}function StereoAudioRecorderHelper(mediaStream,root){function interleave(leftChannel,rightChannel){for(var length=leftChannel.length+rightChannel.length,result=new Float32Array(length),inputIndex=0,index=0;length>index;)result[index++]=leftChannel[inputIndex],result[index++]=rightChannel[inputIndex],inputIndex++;return result}function mergeBuffers(channelBuffer,recordingLength){for(var result=new Float32Array(recordingLength),offset=0,lng=channelBuffer.length,i=0;lng>i;i++){var buffer=channelBuffer[i];result.set(buffer,offset),offset+=buffer.length}return result}function writeUTFBytes(view,offset,string){for(var lng=string.length,i=0;lng>i;i++)view.setUint8(offset+i,string.charCodeAt(i))}var deviceSampleRate=44100;ObjectStore.AudioContextConstructor||(ObjectStore.AudioContextConstructor=new ObjectStore.AudioContext),deviceSampleRate=ObjectStore.AudioContextConstructor.sampleRate;var scriptprocessornode,volume,audioInput,context,leftchannel=[],rightchannel=[],recording=!1,recordingLength=0,sampleRate=root.sampleRate||deviceSampleRate,numChannels=root.audioChannels||2;this.record=function(){recording=!0,leftchannel.length=rightchannel.length=0,recordingLength=0},this.requestData=function(){if(!isPaused){if(0===recordingLength)return void(requestDataInvoked=!1);requestDataInvoked=!0;var internalLeftChannel=leftchannel.slice(0),internalRecordingLength=(rightchannel.slice(0),recordingLength);leftchannel.length=rightchannel.length=[],recordingLength=0,requestDataInvoked=!1;var leftBuffer=mergeBuffers(internalLeftChannel,internalRecordingLength),rightBuffer=mergeBuffers(internalLeftChannel,internalRecordingLength);if(2===numChannels)var interleaved=interleave(leftBuffer,rightBuffer);else var interleaved=leftBuffer;var buffer=new ArrayBuffer(44+2*interleaved.length),view=new DataView(buffer);writeUTFBytes(view,0,"RIFF"),view.setUint32(4,44+2*interleaved.length,!0),writeUTFBytes(view,8,"WAVE"),writeUTFBytes(view,12,"fmt "),view.setUint32(16,16,!0),view.setUint16(20,1,!0),view.setUint16(22,numChannels,!0),view.setUint32(24,sampleRate,!0),view.setUint32(28,4*sampleRate,!0),view.setUint16(32,2*numChannels,!0),view.setUint16(34,16,!0),writeUTFBytes(view,36,"data"),view.setUint32(40,2*interleaved.length,!0);for(var lng=interleaved.length,index=44,volume=1,i=0;lng>i;i++)view.setInt16(index,32767*interleaved[i]*volume,!0),index+=2;var blob=new Blob([view],{type:"audio/wav"});console.debug("audio recorded blob size:",bytesToSize(blob.size)),root.ondataavailable(blob)}},this.stop=function(){recording=!1,this.requestData(),audioInput.disconnect()};var context=ObjectStore.AudioContextConstructor;ObjectStore.VolumeGainNode=context.createGain();var volume=ObjectStore.VolumeGainNode;ObjectStore.AudioInput=context.createMediaStreamSource(mediaStream);var audioInput=ObjectStore.AudioInput;audioInput.connect(volume);var bufferSize=root.bufferSize||2048;if(0===root.bufferSize&&(bufferSize=0),context.createJavaScriptNode)scriptprocessornode=context.createJavaScriptNode(bufferSize,numChannels,numChannels);else{if(!context.createScriptProcessor)throw"WebAudio API has no support on this browser.";scriptprocessornode=context.createScriptProcessor(bufferSize,numChannels,numChannels)}bufferSize=scriptprocessornode.bufferSize,console.debug("using audio buffer-size:",bufferSize);var requestDataInvoked=!1;window.scriptprocessornode=scriptprocessornode,1===numChannels&&console.debug("All right-channels are skipped.");var isPaused=!1;this.pause=function(){isPaused=!0},this.resume=function(){isPaused=!1},scriptprocessornode.onaudioprocess=function(e){if(recording&&!requestDataInvoked&&!isPaused){var left=e.inputBuffer.getChannelData(0);if(leftchannel.push(new Float32Array(left)),2===numChannels){var right=e.inputBuffer.getChannelData(1);rightchannel.push(new Float32Array(right))}recordingLength+=bufferSize}},volume.connect(scriptprocessornode),scriptprocessornode.connect(context.destination)}function WhammyRecorder(mediaStream){this.start=function(timeSlice){timeSlice=timeSlice||1e3,mediaRecorder=new WhammyRecorderHelper(mediaStream,this);for(var prop in this)"function"!=typeof this[prop]&&(mediaRecorder[prop]=this[prop]);mediaRecorder.record(),timeout=setInterval(function(){mediaRecorder.requestData()},timeSlice)},this.stop=function(){mediaRecorder&&(mediaRecorder.stop(),clearTimeout(timeout))},this.clearOldRecordedFrames=function(){mediaRecorder&&mediaRecorder.clearOldRecordedFrames()},this.pause=function(){mediaRecorder&&mediaRecorder.pause()},this.resume=function(){mediaRecorder&&mediaRecorder.resume()},this.ondataavailable=function(){};var mediaRecorder,timeout}function WhammyRecorderHelper(mediaStream,root){function drawFrames(){if(isPaused)return lastTime=(new Date).getTime(),void setTimeout(drawFrames,500);if(!isStopDrawing){if(requestDataInvoked)return setTimeout(drawFrames,100);var duration=(new Date).getTime()-lastTime;if(!duration)return drawFrames();lastTime=(new Date).getTime(),!self.isHTMLObject&&video.paused&&video.play(),context.drawImage(video,0,0,canvas.width,canvas.height),isStopDrawing||whammy.frames.push({duration:duration,image:canvas.toDataURL("image/webp")}),isOnStartedDrawingNonBlankFramesInvoked||isBlankFrame(whammy.frames[whammy.frames.length-1])||(isOnStartedDrawingNonBlankFramesInvoked=!0,root.onStartedDrawingNonBlankFrames()),setTimeout(drawFrames,10)}}function isBlankFrame(frame,_pixTolerance,_frameTolerance){var localCanvas=document.createElement("canvas");localCanvas.width=canvas.width,localCanvas.height=canvas.height;var matchPixCount,endPixCheck,maxPixCount,context2d=localCanvas.getContext("2d"),sampleColor={r:0,g:0,b:0},maxColorDifference=Math.sqrt(Math.pow(255,2)+Math.pow(255,2)+Math.pow(255,2)),pixTolerance=_pixTolerance&&_pixTolerance>=0&&1>=_pixTolerance?_pixTolerance:0,frameTolerance=_frameTolerance&&_frameTolerance>=0&&1>=_frameTolerance?_frameTolerance:0,image=new Image;image.src=frame.image,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;endPixCheck>pix;pix+=4){var currentColor={r:imageData.data[pix],g:imageData.data[pix+1],b:imageData.data[pix+2]},colorDifference=Math.sqrt(Math.pow(currentColor.r-sampleColor.r,2)+Math.pow(currentColor.g-sampleColor.g,2)+Math.pow(currentColor.b-sampleColor.b,2));maxColorDifference*pixTolerance>=colorDifference&&matchPixCount++}return maxPixCount*frameTolerance>=maxPixCount-matchPixCount?!1:!0}function dropBlackFrames(_frames,_framesToCheck,_pixTolerance,_frameTolerance){var localCanvas=document.createElement("canvas");localCanvas.width=canvas.width,localCanvas.height=canvas.height;for(var context2d=localCanvas.getContext("2d"),resultFrames=[],checkUntilNotBlack=-1===_framesToCheck,endCheckFrame=_framesToCheck&&_framesToCheck>0&&_framesToCheck<=_frames.length?_framesToCheck:_frames.length,sampleColor={r:0,g:0,b:0},maxColorDifference=Math.sqrt(Math.pow(255,2)+Math.pow(255,2)+Math.pow(255,2)),pixTolerance=_pixTolerance&&_pixTolerance>=0&&1>=_pixTolerance?_pixTolerance:0,frameTolerance=_frameTolerance&&_frameTolerance>=0&&1>=_frameTolerance?_frameTolerance:0,doNotCheckNext=!1,f=0;endCheckFrame>f;f++){var matchPixCount,endPixCheck,maxPixCount;if(!doNotCheckNext){var image=new Image;image.src=_frames[f].image,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;endPixCheck>pix;pix+=4){var currentColor={r:imageData.data[pix],g:imageData.data[pix+1],b:imageData.data[pix+2]},colorDifference=Math.sqrt(Math.pow(currentColor.r-sampleColor.r,2)+Math.pow(currentColor.g-sampleColor.g,2)+Math.pow(currentColor.b-sampleColor.b,2));maxColorDifference*pixTolerance>=colorDifference&&matchPixCount++}}!doNotCheckNext&&maxPixCount*frameTolerance>=maxPixCount-matchPixCount||(checkUntilNotBlack&&(doNotCheckNext=!0),resultFrames.push(_frames[f]))}return resultFrames=resultFrames.concat(_frames.slice(endCheckFrame)),resultFrames.length<=0&&resultFrames.push(_frames[_frames.length-1]),resultFrames}this.record=function(timeSlice){this.width||(this.width=320),this.height||(this.height=240),this.video&&this.video instanceof HTMLVideoElement&&(this.width||(this.width=video.videoWidth||video.clientWidth||320),this.height||(this.height=video.videoHeight||video.clientHeight||240)),this.video||(this.video={width:this.width,height:this.height}),this.canvas&&this.canvas.width&&this.canvas.height||(this.canvas={width:this.width,height:this.height}),canvas.width=this.canvas.width,canvas.height=this.canvas.height,this.video&&this.video instanceof HTMLVideoElement?(this.isHTMLObject=!0,video=this.video.cloneNode()):(video=document.createElement("video"),video.src=URL.createObjectURL(mediaStream),video.width=this.video.width,video.height=this.video.height),video.muted=!0,video.play(),lastTime=(new Date).getTime(),whammy=new Whammy.Video,console.log("canvas resolutions",canvas.width,"*",canvas.height),console.log("video width/height",video.width||canvas.width,"*",video.height||canvas.height),drawFrames()},this.clearOldRecordedFrames=function(){whammy.frames=[]};var requestDataInvoked=!1;this.requestData=function(){if(!isPaused){if(!whammy.frames.length)return void(requestDataInvoked=!1);requestDataInvoked=!0;var internalFrames=whammy.frames.slice(0);whammy.frames=dropBlackFrames(internalFrames,-1),whammy.compile(function(whammyBlob){root.ondataavailable(whammyBlob),console.debug("video recorded blob size:",bytesToSize(whammyBlob.size))}),whammy.frames=[],requestDataInvoked=!1}};var isOnStartedDrawingNonBlankFramesInvoked=!1,isStopDrawing=!1;this.stop=function(){isStopDrawing=!0,this.requestData()};var video,lastTime,whammy,canvas=document.createElement("canvas"),context=canvas.getContext("2d"),self=this,isPaused=!1;this.pause=function(){isPaused=!0},this.resume=function(){isPaused=!1}}function GifRecorder(mediaStream){function doneRecording(){endTime=Date.now();var gifBlob=new Blob([new Uint8Array(gifEncoder.stream().bin)],{type:"image/gif"});self.ondataavailable(gifBlob),gifEncoder.stream().bin=[]}if("undefined"==typeof GIFEncoder)throw"Please link: https://cdn.webrtc-experiment.com/gif-recorder.js";this.start=function(timeSlice){function drawVideoFrame(time){return isPaused?void setTimeout(drawVideoFrame,500,time):(lastAnimationFrame=requestAnimationFrame(drawVideoFrame),void 0===typeof lastFrameTime&&(lastFrameTime=time),void(90>time-lastFrameTime||(video.paused&&video.play(),context.drawImage(video,0,0,imageWidth,imageHeight),gifEncoder.addFrame(context),lastFrameTime=time)))}timeSlice=timeSlice||1e3;var imageWidth=this.videoWidth||320,imageHeight=this.videoHeight||240;canvas.width=video.width=imageWidth,canvas.height=video.height=imageHeight,gifEncoder=new GIFEncoder,gifEncoder.setRepeat(0),gifEncoder.setDelay(this.frameRate||200),gifEncoder.setQuality(this.quality||1),gifEncoder.start(),startTime=Date.now(),lastAnimationFrame=requestAnimationFrame(drawVideoFrame),timeout=setTimeout(doneRecording,timeSlice)},this.stop=function(){lastAnimationFrame&&(cancelAnimationFrame(lastAnimationFrame),clearTimeout(timeout),doneRecording())};var isPaused=!1;this.pause=function(){isPaused=!0},this.resume=function(){isPaused=!1},this.ondataavailable=function(){},this.onstop=function(){};var self=this,canvas=document.createElement("canvas"),context=canvas.getContext("2d"),video=document.createElement("video");video.muted=!0,video.autoplay=!0,video.src=URL.createObjectURL(mediaStream),video.play();var startTime,endTime,lastFrameTime,gifEncoder,timeout,lastAnimationFrame=null}if("undefined"!=typeof AudioContext){if("undefined"!=typeof webkitAudioContext)var AudioContext=webkitAudioContext;if("undefined"!=typeof mozAudioContext)var AudioContext=mozAudioContext}if("undefined"!=typeof URL&&"undefined"!=typeof webkitURL)var URL=webkitURL;var IsEdge=!(-1===navigator.userAgent.indexOf("Edge")||!navigator.msSaveBlob&&!navigator.msSaveOrOpenBlob),IsOpera=!!window.opera||-1!==navigator.userAgent.indexOf("OPR/"),IsChrome=!IsEdge&&!IsEdge&&!!navigator.webkitGetUserMedia;if("undefined"!=typeof navigator)"undefined"!=typeof navigator.webkitGetUserMedia&&(navigator.getUserMedia=navigator.webkitGetUserMedia),"undefined"!=typeof navigator.mozGetUserMedia&&(navigator.getUserMedia=navigator.mozGetUserMedia);else var navigator={getUserMedia:{}};if("undefined"!=typeof webkitMediaStream)var MediaStream=webkitMediaStream;var ObjectStore={AudioContext:window.AudioContext||window.webkitAudioContext},ObjectStore={AudioContext:window.AudioContext||window.webkitAudioContext},Whammy=function(){function WhammyVideo(duration){this.frames=[],this.duration=duration||1,this.quality=.8}function processInWebWorker(_function){var blob=URL.createObjectURL(new Blob([_function.toString(),"this.onmessage = function (e) {"+_function.name+"(e.data);}"],{type:"application/javascript"})),worker=new Worker(blob);return URL.revokeObjectURL(blob),worker}function whammyInWebWorker(frames){function ArrayToWebM(frames){var info=checkFrames(frames);if(!info)return[];for(var clusterMaxDuration=3e4,EBML=[{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:doubleToString(info.duration),id:17545}]},{id:374648427,data:[{id:174,data:[{data:1,id:215},{data:1,id:29637},{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:info.width,id:176},{data:info.height,id:186}]}]}]}]}],frameNumber=0,clusterTimecode=0;frameNumberclusterDuration);var clusterCounter=0,cluster={id:524531317,data:getClusterData(clusterTimecode,clusterCounter,clusterFrames)};EBML[1].data.push(cluster),clusterTimecode+=clusterDuration}return generateEBML(EBML)}function getClusterData(clusterTimecode,clusterCounter,clusterFrames){return[{data:clusterTimecode,id:231}].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)});return clusterCounter+=webp.duration,{data:block,id:163}}))}function checkFrames(frames){if(!frames[0])return void postMessage({error:"Something went wrong. Maybe WebP format is not supported in the current browser."});for(var width=frames[0].width,height=frames[0].height,duration=frames[0].duration,i=1;i0;)parts.push(255&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=[],pad=bits.length%8?new Array(9-bits.length%8).join("0"):"";bits=pad+bits;for(var i=0;i127)throw"TrackNumber > 127 not supported";var out=[128|data.trackNum,data.timecode>>8,255&data.timecode,flags].map(function(e){return String.fromCharCode(e)}).join("")+data.frame;return out}function parseWebP(riff){for(var VP8=riff.RIFF[0].WEBP[0],frameStart=VP8.indexOf("*"),i=0,c=[];4>i;i++)c[i]=VP8.charCodeAt(frameStart+3+i);var width,height,tmp;return tmp=c[1]<<8|c[0],width=16383&tmp,tmp=c[3]<<8|c[2],height=16383&tmp,{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){for(var offset=0,chunks={};offset -``` -## Otherwise, you can link specific files: + -* https://github.com/streamproc/MediaStreamRecorder/blob/master/How-to-Link-Specific-Files.md +https://cdn.rawgit.com/streamproc/MediaStreamRecorder/master/MediaStreamRecorder.js +``` ## Record audio+video in Firefox in single WebM @@ -175,6 +177,28 @@ function onMediaError(e) { mediaRecorder.stop(); ``` +## How to pause recordings? + +```javascript +mediaRecorder.pause(); +``` + +## How to resume recordings? + +```javascript +mediaRecorder.resume(); +``` + +## How to save recordings? + +```javascript +// invoke save-as dialog for all recorded blobs +mediaRecorder.save(); + +// or pass external blob/file +mediaRecorder.save(YourExternalBlob, 'FileName.webm'); +``` + ## How to upload recorded files using PHP? **PHP code:** @@ -225,7 +249,31 @@ function xhr(url, data, callback) { # API Documentation -## audioChannels +## `recorderType` + +You can force StereoAudioRecorder or WhammyRecorder or similar records on Firefox or Edge; even on Chrome and Opera. + +All browsers will be using your specified recorder: + +```javascript +// force WebAudio API on all browsers +// it allows you record remote audio-streams in Firefox +// it also works in Microsoft Edge +mediaRecorder.type = StereoAudioRecorder; + +// force webp based webm encoder on all browsers +mediaRecorder.type = WhammyRecorder; + +// force MediaRecorder API on all browsers +// Chrome is going to implement MediaRecorder API soon; +// so this property allows you force MediaRecorder in Chrome. +mediaRecorder.type = MediaRecorderWrapper; + +// force GifRecorder in all browsers. Both WhammyRecorder and MediaRecorder API will be ignored. +mediaRecorder.type = GifRecorder; +``` + +## `audioChannels` It is an integer value that accepts either 1 or 2. "1" means record only left-channel and skip right-one. The default value is "2". @@ -233,7 +281,7 @@ It is an integer value that accepts either 1 or 2. "1" means record only left-ch mediaRecorder.audioChannels = 1; ``` -## bufferSize +## `bufferSize` You can set following audio-bufferSize values: 0, 256, 512, 1024, 2048, 4096, 8192, and 16384. "0" means: let chrome decide the device's default bufferSize. Default value is "2048". @@ -241,7 +289,7 @@ You can set following audio-bufferSize values: 0, 256, 512, 1024, 2048, 4096, 81 mediaRecorder.bufferSize = 0; ``` -## sampleRate +## `sampleRate` Default "sampleRate" value is "44100". Currently you can't modify sample-rate in windows that's why this property isn't yet exposed to public API. @@ -252,7 +300,7 @@ It accepts values only in range: 22050 to 96000 mediaRecorder.sampleRate = 96000; ``` -## video +## `video` It is recommended to pass your HTMLVideoElement to get most accurate result. @@ -266,6 +314,42 @@ videoRecorder.onStartedDrawingNonBlankFrames = function() { }; ``` +## `stop` + +This method allows you stop recording. + +```javascript +mediaRecorder.stop(); +``` + +## `pause` + +This method allows you pause recording. + +```javascript +mediaRecorder.pause(); +``` + +## `resume` + +This method allows you resume recording. + +```javascript +mediaRecorder.resume(); +``` + +## `save` + +This method allows you save recording to disk (via save-as dialog). + +```javascript +// invoke save-as dialog for all recorded blobs +mediaRecorder.save(); + +// or pass external blob/file +mediaRecorder.save(YourExternalBlob, 'FileName.webm'); +``` + ## canvas Using this property, you can pass video resolutions: @@ -359,6 +443,7 @@ gifRecorder.mimeType = 'image/gif'; | Google Chrome | [Stable](https://www.google.com/intl/en_uk/chrome/browser/) / [Canary](https://www.google.com/intl/en/chrome/browser/canary.html) / [Beta](https://www.google.com/intl/en/chrome/browser/beta.html) / [Dev](https://www.google.com/intl/en/chrome/browser/index.html?extra=devchannel#eula) | | Opera | [Stable](http://www.opera.com/) / [NEXT](http://www.opera.com/computer/next) | | Android | [Chrome](https://play.google.com/store/apps/details?id=com.chrome.beta&hl=en) / [Firefox](https://play.google.com/store/apps/details?id=org.mozilla.firefox) / [Opera](https://play.google.com/store/apps/details?id=com.opera.browser) | +| Microsoft Edge | [Normal Build](https://www.microsoft.com/en-us/windows/microsoft-edge) | ## Contributors diff --git a/VideoStreamRecorder/GifRecorder.js b/VideoStreamRecorder/GifRecorder.js index 16ca130..beaa9f6 100644 --- a/VideoStreamRecorder/GifRecorder.js +++ b/VideoStreamRecorder/GifRecorder.js @@ -1,11 +1,11 @@ -// Muaz Khan - https://github.com/muaz-khan -// neizerth - https://github.com/neizerth -// MIT License - https://www.webrtc-experiment.com/licence/ -// Documentation - https://github.com/streamproc/MediaStreamRecorder -// ========================================================== +// -------------- // GifRecorder.js function GifRecorder(mediaStream) { + if (typeof GIFEncoder === 'undefined') { + throw 'Please link: https://cdn.webrtc-experiment.com/gif-recorder.js'; + } + // void start(optional long timeSlice) // timestamp to fire "ondataavailable" this.start = function(timeSlice) { @@ -47,6 +47,11 @@ function GifRecorder(mediaStream) { startTime = Date.now(); function drawVideoFrame(time) { + if (isPaused) { + setTimeout(drawVideoFrame, 500, time); + return; + } + lastAnimationFrame = requestAnimationFrame(drawVideoFrame); if (typeof lastFrameTime === undefined) { @@ -54,7 +59,13 @@ function GifRecorder(mediaStream) { } // ~10 fps - if (time - lastFrameTime < 90) return; + if (time - lastFrameTime < 90) { + return; + } + + if (video.paused) { + video.play(); // Android + } context.drawImage(video, 0, 0, imageWidth, imageHeight); @@ -81,7 +92,7 @@ function GifRecorder(mediaStream) { // todo: find a way to clear old recorded blobs gifEncoder.stream().bin = []; - }; + } this.stop = function() { if (lastAnimationFrame) { @@ -91,6 +102,16 @@ function GifRecorder(mediaStream) { } }; + var isPaused = false; + + this.pause = function() { + isPaused = true; + }; + + this.resume = function() { + isPaused = false; + }; + this.ondataavailable = function() {}; this.onstop = function() {}; diff --git a/VideoStreamRecorder/WhammyRecorder.js b/VideoStreamRecorder/WhammyRecorder.js index 0cdd5ec..5f01989 100644 --- a/VideoStreamRecorder/WhammyRecorder.js +++ b/VideoStreamRecorder/WhammyRecorder.js @@ -1,4 +1,4 @@ -// ================= +// =================== // WhammyRecorder.js function WhammyRecorder(mediaStream) { @@ -35,6 +35,22 @@ function WhammyRecorder(mediaStream) { } }; + this.pause = function() { + if (!mediaRecorder) { + return; + } + + mediaRecorder.pause(); + }; + + this.resume = function() { + if (!mediaRecorder) { + return; + } + + mediaRecorder.resume(); + }; + this.ondataavailable = function() {}; // Reference to "WhammyRecorder" object diff --git a/VideoStreamRecorder/WhammyRecorderHelper.js b/VideoStreamRecorder/WhammyRecorderHelper.js index f269aef..9882044 100644 --- a/VideoStreamRecorder/WhammyRecorderHelper.js +++ b/VideoStreamRecorder/WhammyRecorderHelper.js @@ -1,14 +1,22 @@ -// ======================= +// ========================== // WhammyRecorderHelper.js function WhammyRecorderHelper(mediaStream, root) { this.record = function(timeSlice) { - if (!this.width) this.width = 320; - if (!this.height) this.height = 240; + if (!this.width) { + this.width = 320; + } + if (!this.height) { + this.height = 240; + } if (this.video && this.video instanceof HTMLVideoElement) { - if (!this.width) this.width = video.videoWidth || video.clientWidth || 320; - if (!this.height) this.height = video.videoHeight || video.clientHeight || 240; + if (!this.width) { + this.width = video.videoWidth || video.clientWidth || 320; + } + if (!this.height) { + this.height = video.videoHeight || video.clientHeight || 240; + } } if (!this.video) { @@ -30,6 +38,7 @@ function WhammyRecorderHelper(mediaStream, root) { // setting defaults if (this.video && this.video instanceof HTMLVideoElement) { + this.isHTMLObject = true; video = this.video.cloneNode(); } else { video = document.createElement('video'); @@ -52,55 +61,77 @@ function WhammyRecorderHelper(mediaStream, root) { }; this.clearOldRecordedFrames = function() { - frames = []; + whammy.frames = []; }; var requestDataInvoked = false; this.requestData = function() { - if (!frames.length) { + if (isPaused) { + return; + } + + if (!whammy.frames.length) { requestDataInvoked = false; return; } requestDataInvoked = true; // clone stuff - var internal_frames = frames.slice(0); + var internalFrames = whammy.frames.slice(0); // reset the frames for the new recording - frames = []; - whammy.frames = dropBlackFrames(internal_frames, -1); + whammy.frames = dropBlackFrames(internalFrames, -1); - var WebM_Blob = whammy.compile(); - root.ondataavailable(WebM_Blob); + whammy.compile(function(whammyBlob) { + root.ondataavailable(whammyBlob); + console.debug('video recorded blob size:', bytesToSize(whammyBlob.size)); + }); - console.debug('video recorded blob size:', bytesToSize(WebM_Blob.size)); + whammy.frames = []; requestDataInvoked = false; }; - var frames = []; - var isOnStartedDrawingNonBlankFramesInvoked = false; function drawFrames() { - if (isStopDrawing) return; + if (isPaused) { + lastTime = new Date().getTime(); + setTimeout(drawFrames, 500); + return; + } + + if (isStopDrawing) { + return; + } - if (requestDataInvoked) return setTimeout(drawFrames, 100); + if (requestDataInvoked) { + return setTimeout(drawFrames, 100); + } var duration = new Date().getTime() - lastTime; - if (!duration) return drawFrames(); + if (!duration) { + return drawFrames(); + } // via webrtc-experiment#206, by Jack i.e. @Seymourr lastTime = new Date().getTime(); + if (!self.isHTMLObject && video.paused) { + video.play(); // Android + } + context.drawImage(video, 0, 0, canvas.width, canvas.height); - !isStopDrawing && frames.push({ - duration: duration, - image: canvas.toDataURL('image/webp') - }); - if (!isOnStartedDrawingNonBlankFramesInvoked && !isBlankFrame(frames[frames.length - 1])) { + if (!isStopDrawing) { + whammy.frames.push({ + duration: duration, + image: canvas.toDataURL('image/webp') + }); + } + + if (!isOnStartedDrawingNonBlankFramesInvoked && !isBlankFrame(whammy.frames[whammy.frames.length - 1])) { isOnStartedDrawingNonBlankFramesInvoked = true; root.onStartedDrawingNonBlankFrames(); } @@ -252,4 +283,14 @@ function WhammyRecorderHelper(mediaStream, root) { return resultFrames; } + + var isPaused = false; + + this.pause = function() { + isPaused = true; + }; + + this.resume = function() { + isPaused = false; + }; } diff --git a/VideoStreamRecorder/lib/whammy.js b/VideoStreamRecorder/lib/whammy.js index e70a1e7..2c7b112 100644 --- a/VideoStreamRecorder/lib/whammy.js +++ b/VideoStreamRecorder/lib/whammy.js @@ -1,353 +1,430 @@ -// Muaz Khan - https://github.com/muaz-khan -// neizerth - https://github.com/neizerth -// MIT License - https://www.webrtc-experiment.com/licence/ -// Documentation - https://github.com/streamproc/MediaStreamRecorder - -// Note: -// ========================================================== -// whammy.js is an "external library" -// and has its own copyrights. Taken from "Whammy" project. - - // 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. + * @typedef Whammy + * @class + * @example + * var recorder = new Whammy().Video(15); + * recorder.add(context || canvas || dataURL); + * var output = recorder.compile(); + */ + var Whammy = (function() { + // a more abstract-ish API - function toWebM(frames) { - var info = checkFrames(frames); + function WhammyVideo(duration) { + this.frames = []; + this.duration = duration || 1; + this.quality = 0.8; + } - var CLUSTER_MAX_DURATION = 30000; + /** + * 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; + } - 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 + 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 (e) {' + _function.name + '(e.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": "whammy", - "id": 0x4d80 // MuxingApp + 'data': 4, + 'id': 0x42f2 // EBMLMaxIDLength }, { - "data": "whammy", - "id": 0x5741 // WritingApp + 'data': 8, + 'id': 0x42f3 // EBMLMaxSizeLength }, { - "data": doubleToString(info.duration), - "id": 0x4489 // Duration + 'data': 'webm', + 'id': 0x4282 // DocType + }, { + 'data': 2, + 'id': 0x4287 // DocTypeVersion + }, { + 'data': 2, + 'id': 0x4285 // DocTypeReadVersion }] }, { - "id": 0x1654ae6b, // Tracks - "data": [{ - "id": 0xae, // TrackEntry - "data": [{ - "data": 1, - "id": 0xd7 // TrackNumber - }, { - "data": 1, - "id": 0x63c5 // TrackUID - }, { - "data": 0, - "id": 0x9c // FlagLacing + 'id': 0x18538067, // Segment + 'data': [{ + 'id': 0x1549a966, // Info + 'data': [{ + 'data': 1e6, //do things in millisecs (num of nanosecs for duration scale) + 'id': 0x2ad7b1 // TimecodeScale }, { - "data": "und", - "id": 0x22b59c // Language + 'data': 'whammy', + 'id': 0x4d80 // MuxingApp }, { - "data": "V_VP8", - "id": 0x86 // CodecID + 'data': 'whammy', + 'id': 0x5741 // WritingApp }, { - "data": "VP8", - "id": 0x258688 // CodecName - }, { - "data": 1, - "id": 0x83 // TrackType - }, { - "id": 0xe0, // Video - "data": [{ - "data": info.width, - "id": 0xb0 // PixelWidth + '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": info.height, - "id": 0xba // PixelHeight + '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 < CLUSTER_MAX_DURATION); - - var clusterCounter = 0; - var cluster = { - "id": 0x1f43b675, // Cluster - "data": [{ - "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 - }; - })) - }; //Add cluster to segment - EBML[1].data.push(cluster); - clusterTimecode += clusterDuration; + }]; + + //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); } - 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 + // sums the lengths of all the frames and gets the duration - function checkFrames(frames) { - if (!frames[0]) { - console.warn('Something went wrong. Maybe WebP format is not supported in the current browser.'); - return; - } + 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; + 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; + for (var i = 1; i < frames.length; i++) { + duration += frames[i].duration; + } + return { + duration: duration, + width: width, + height: height + }; } - return { - duration: duration, - width: width, - height: height - }; - } - function numToBuffer(num) { - var parts = []; - while (num > 0) { - parts.push(num & 0xff); - num = num >> 8; + function numToBuffer(num) { + var parts = []; + while (num > 0) { + parts.push(num & 0xff); + num = num >> 8; + } + return new Uint8Array(parts.reverse()); } - 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)); + function strToBuffer(str) { + return new Uint8Array(str.split('').map(function(e) { + return e.charCodeAt(0); + })); } - 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 size_str = len.toString(2); - var padded = (new Array((zeroes * 7 + 7 + 1) - size_str.length)).join('0') + size_str; - var size = (new Array(zeroes)).join('0') + '1' + padded; - - ebml.push(numToBuffer(json[i].id)); - ebml.push(bitsToBuffer(size)); - ebml.push(data); + 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); } - return new Blob(ebml, { - type: "video/webm" - }); - } + function generateEBML(json) { + var ebml = []; + for (var i = 0; i < json.length; i++) { + var data = json[i].data; - function toBinStr_old(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; - } + if (typeof data === 'object') { + data = generateEBML(data); + } - function generateEBML_old(json) { - var ebml = ''; - for (var i = 0; i < json.length; i++) { - var data = json[i].data; - if (typeof data == 'object') data = generateEBML_old(data); - if (typeof data == 'number') data = toBinStr_old(data.toString(2)); + if (typeof data === 'number') { + data = bitsToBuffer(data.toString(2)); + } - var len = data.length; - var zeroes = Math.ceil(Math.ceil(Math.log(len) / Math.log(2)) / 8); - var size_str = len.toString(2); - var padded = (new Array((zeroes * 7 + 7 + 1) - size_str.length)).join('0') + size_str; - var size = (new Array(zeroes)).join('0') + '1' + padded; + if (typeof data === 'string') { + data = strToBuffer(data); + } - ebml += toBinStr_old(json[i].id.toString(2)) + toBinStr_old(size) + 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; - } - return ebml; - } + ebml.push(numToBuffer(json[i].id)); + ebml.push(bitsToBuffer(size)); + ebml.push(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"; + return new Blob(ebml, { + type: 'video/webm' + }); } - 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 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 parseWebP(riff) { - var VP8 = riff.RIFF[0].WEBP[0]; + function makeSimpleBlock(data) { + var flags = 0; - var frame_start = 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(frame_start + 3 + i); + if (data.keyframe) { + flags |= 128; + } - var width, height, tmp; + if (data.invisible) { + flags |= 8; + } - //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 - }; - } + if (data.lacing) { + flags |= (data.lacing << 1); + } - function parseRIFF(string) { - var offset = 0; - var chunks = {}; + if (data.discardable) { + flags |= 1; + } - while (offset < string.length) { - var id = string.substr(offset, 4); - var len = 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); - 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); + 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; } - return chunks; - } - function doubleToString(num) { - return [].slice.call( - new Uint8Array((new Float64Array([num])).buffer), 0).map(function(e) { - return String.fromCharCode(e); - }).reverse().join(''); - } + function parseWebP(riff) { + var VP8 = riff.RIFF[0].WEBP[0]; - // a more abstract-ish API + 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); + } - function WhammyVideo(duration) { - this.frames = []; - this.duration = duration || 1; - this.quality = 100; - } + 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 + }; + } - WhammyVideo.prototype.add = function(frame, duration) { - if ('canvas' in frame) { //CanvasRenderingContext2D - frame = frame.canvas; + 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); } - if ('toDataURL' in frame) { - frame = frame.toDataURL('image/webp', this.quality); + 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; } - if (!(/^data:image\/webp;base64,/ig).test(frame)) { - throw "Input must be formatted properly as a base64 encoded DataURI of type image/webp"; + function doubleToString(num) { + return [].slice.call( + new Uint8Array((new Float64Array([num])).buffer), 0).map(function(e) { + return String.fromCharCode(e); + }).reverse().join(''); } - this.frames.push({ - image: frame, - duration: duration || this.duration - }); - }; - WhammyVideo.prototype.compile = function() { - return new toWebM(this.frames.map(function(frame) { + + 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 { - Video: WhammyVideo, - toWebM: toWebM + /** + * 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 }; })(); diff --git a/bug.md b/bug.md deleted file mode 100644 index 0639f42..0000000 --- a/bug.md +++ /dev/null @@ -1,28 +0,0 @@ -## List of all bugs in MediaStreamRecorder.js - -It is a list of all bugs need to be fixed. - -= - -##### AudioStreamRecorder/MediaRecorder.js `Line 27` - -1. It is not a valid behavior -2. Mozilla Firefox Nightly crashes -3. Redundant memory usage - -= - -##### VideoStreamRecorder/GifRecorder.js `Line 79` - -1. Must be able to clear old recorded GIFs -2. Both WebM and Gif recorders must work fine on Firefox - -`context.drawImage` seems throwing error on Firefox: `NS_ERROR_NOT_AVAILABLE: Component is not available`. - -Also, `uncaught exception: Input must be formatted properly as a base64 encoded DataURI of type image/webp`. - -= - -##### License - -[MediaStreamRecorder.js](https://github.com/streamproc/MediaStreamRecorder) library is released under [MIT licence](https://www.webrtc-experiment.com/licence/) . Copyright (c) 2013 [Muaz Khan](https://github.com/muaz-khan) and [neizerth](https://github.com/neizerth). diff --git a/common/Cross-Browser-Declarations.js b/common/Cross-Browser-Declarations.js index 0f985ee..5e6eb56 100644 --- a/common/Cross-Browser-Declarations.js +++ b/common/Cross-Browser-Declarations.js @@ -1,30 +1,46 @@ -// Muaz Khan - www.MuazKhan.com -// MIT License - www.webrtc-experiment.com/licence -// Documentation - github.com/streamproc/MediaStreamRecorder - // _____________________________ // Cross-Browser-Declarations.js -// animation-frame used in WebM recording -if (!window.requestAnimationFrame) { - requestAnimationFrame = window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame; -} +// WebAudio API representer +if (typeof AudioContext !== 'undefined') { + if (typeof webkitAudioContext !== 'undefined') { + /*global AudioContext:true*/ + var AudioContext = webkitAudioContext; + } -if (!window.cancelAnimationFrame) { - cancelAnimationFrame = window.webkitCancelAnimationFrame || window.mozCancelAnimationFrame; + if (typeof mozAudioContext !== 'undefined') { + /*global AudioContext:true*/ + var AudioContext = mozAudioContext; + } } -// WebAudio API representer -if (!window.AudioContext) { - window.AudioContext = window.webkitAudioContext || window.mozAudioContext; +if (typeof URL !== 'undefined' && typeof webkitURL !== 'undefined') { + /*global URL:true*/ + var URL = webkitURL; } -URL = window.URL || window.webkitURL; -navigator.getUserMedia = navigator.webkitGetUserMedia || navigator.mozGetUserMedia; +var IsEdge = navigator.userAgent.indexOf('Edge') !== -1 && (!!navigator.msSaveBlob || !!navigator.msSaveOrOpenBlob); +var IsOpera = !!window.opera || navigator.userAgent.indexOf('OPR/') !== -1; +var IsChrome = !IsEdge && !IsEdge && !!navigator.webkitGetUserMedia; + +if (typeof navigator !== 'undefined') { + if (typeof navigator.webkitGetUserMedia !== 'undefined') { + navigator.getUserMedia = navigator.webkitGetUserMedia; + } -if (window.webkitMediaStream) window.MediaStream = window.webkitMediaStream; + if (typeof navigator.mozGetUserMedia !== 'undefined') { + navigator.getUserMedia = navigator.mozGetUserMedia; + } +} else { + /*global navigator:true */ + var navigator = { + getUserMedia: {} + }; +} -IsChrome = !!navigator.webkitGetUserMedia; +if (typeof webkitMediaStream !== 'undefined') { + var MediaStream = webkitMediaStream; +} // Merge all other data-types except "function" @@ -41,11 +57,13 @@ function mergeProps(mergein, mergeto) { function reformatProps(obj) { var output = {}; for (var o in obj) { - if (o.indexOf('-') != -1) { + if (o.indexOf('-') !== -1) { var splitted = o.split('-'); var name = splitted[0] + splitted[1].split('')[0].toUpperCase() + splitted[1].substr(1); output[name] = obj[o]; - } else output[o] = obj[o]; + } else { + output[o] = obj[o]; + } } return output; } @@ -58,6 +76,56 @@ function dropFirstFrame(arr) { return arr; } +function invokeSaveAsDialog(file, fileName) { + if (!file) { + throw 'Blob object is required.'; + } + + if (!file.type) { + file.type = 'video/webm'; + } + + var fileExtension = file.type.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']; @@ -67,3 +135,9 @@ function bytesToSize(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: window.AudioContext || window.webkitAudioContext +}; diff --git a/common/MediaStreamRecorder.js b/common/MediaStreamRecorder.js new file mode 100644 index 0000000..e160a61 --- /dev/null +++ b/common/MediaStreamRecorder.js @@ -0,0 +1,107 @@ +// ______________________ +// MediaStreamRecorder.js + +function MediaStreamRecorder(mediaStream) { + if (!mediaStream) { + throw 'MediaStream is mandatory.'; + } + + // void start(optional long timeSlice) + // timestamp to fire "ondataavailable" + this.start = function(timeSlice) { + // 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 || IsEdge || IsOpera ? window.StereoAudioRecorder || IsEdge || IsOpera : window.MediaRecorderWrapper; + + // video recorder (in WebM format) + if (this.mimeType.indexOf('video') !== -1) { + Recorder = IsChrome || IsEdge || IsOpera ? window.WhammyRecorder : window.MediaRecorderWrapper; + } + + // video recorder (in GIF format) + if (this.mimeType === 'image/gif') { + Recorder = window.GifRecorder; + } + + // 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; + } + + var bigBlob = new Blob(mediaRecorder.blobs, { + type: mediaRecorder.blobs[0].type || this.mimeType + }); + + invokeSaveAsDialog(bigBlob); + 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); + }; + + this.recorderType = null; // StereoAudioRecorder || WhammyRecorder || MediaRecorderWrapper || GifRecorder + + // Reference to "MediaRecorder.js" + var mediaRecorder; +} diff --git a/MultiStreamRecorder.js b/common/MultiStreamRecorder.js similarity index 83% rename from MultiStreamRecorder.js rename to common/MultiStreamRecorder.js index f5d4eca..233d7f1 100644 --- a/MultiStreamRecorder.js +++ b/common/MultiStreamRecorder.js @@ -1,12 +1,10 @@ -// Muaz Khan - www.MuazKhan.com -// MIT License - www.webrtc-experiment.com/licence -// Documentation - github.com/streamproc/MediaStreamRecorder - // ______________________ // MultiStreamRecorder.js function MultiStreamRecorder(mediaStream) { - if (!mediaStream) throw 'MediaStream is mandatory.'; + if (!mediaStream) { + throw 'MediaStream is mandatory.'; + } var self = this; var isFirefox = !!navigator.mozGetUserMedia; @@ -83,8 +81,12 @@ function MultiStreamRecorder(mediaStream) { }; this.stop = function() { - if (audioRecorder) audioRecorder.stop(); - if (videoRecorder) videoRecorder.stop(); + if (audioRecorder) { + audioRecorder.stop(); + } + if (videoRecorder) { + videoRecorder.stop(); + } }; this.ondataavailable = function(blob) { @@ -95,6 +97,24 @@ function MultiStreamRecorder(mediaStream) { console.warn('stopped..', error); }; + this.pause = function() { + if (audioRecorder) { + audioRecorder.pause(); + } + if (videoRecorder) { + videoRecorder.pause(); + } + }; + + this.resume = function() { + if (audioRecorder) { + audioRecorder.resume(); + } + if (videoRecorder) { + videoRecorder.resume(); + } + }; + var audioRecorder; var videoRecorder; diff --git a/common/OpentTokStreamRecorder.js b/common/OpentTokStreamRecorder.js new file mode 100644 index 0000000..7a25e6c --- /dev/null +++ b/common/OpentTokStreamRecorder.js @@ -0,0 +1,333 @@ +// muazkh - github.com/muaz-khan +// @neizerth - github.com/neizerth +// MIT License - https://webrtc-experiment.appspot.com/licence/ +// Documentation - https://github.com/streamproc/MediaStreamRecorder +// ========================================================== +// OpentTokStreamRecorder.js + +function OpenTokStreamRecorder(o) { + var defaults = { + webrtc: false, + apiKey: null, + remoteApiKey: null, + sessionId: '', + token: null, + remoteToken: null, + remoteArchive: null, + saveAterStop: true, + container: null, + debug: false, + pollWhileProcessing: true, + style: null, // null | 'blank' | object (see http://www.tokbox.com/opentok/docs/js/reference/Recorder.html) + loadingDuration: 2000, // dirty hack while opentox doesnt support onready event + onerror: function() { + + }, + onstart: function() { + + }, + onstop: function() { + + }, + ondataavailable: function() { + + }, + onready: function() { + + }, + onsavecomplete: function() { + + } + }, + remoteDefaults = { + url: window.location, + method: 'GET', + data: {}, + success: function() {}, + error: function() {} + }, + baseUrl = getBaseUrl(), + self = this, + _initialized = false, + _startRequest = false, + options = extend(defaults, o), + recorder; + + include('https://swww.tokbox.com/v1.1/js/TB.min.js', init); + + function init() { + if (options.remoteApiKey != null) { + getUrlData(options.remoteApiKey, function(key) { + options.apiKey = key; + createRecorder(); + }); + } + if (options.remoteToken != null) { + getUrlData(options.remoteToken, function(token) { + options.token = token; + createRecorder(); + }); + } + createRecorder(); + } + + function start() { + _startRequest = true; + if (_initialized) { + _startRequest = false; + if (self.state != 'inactive') { + var obj = { + message: 'The object is in an invalid state', + code: DOMException.INVALID_STATE_ERR + } + self.onerror(obj); + throw obj; + } + self.state = 'recording'; + + recorder.startRecording(); + } + } + + function stop() { + if (self.state == 'inactive') { + var obj = { + message: 'The object is in an invalid state', + code: DOMException.INVALID_STATE_ERR + } + self.onerror(obj); + throw obj; + } + self.state = 'inactive'; + + recorder.stopRecording(); + + if (options.saveAterStop) { + save(); + }; + } + + function save() { + recorder.saveArchive(); + } + + function createRecorder() { + if (options.apiKey != null && options.token != null && !_initialized) { + _initialized = true; + if (options.debug) { + TB.setLogLevel(TB.DEBUG); // Prints out logging messages in console + } + var recorderManager = TB.initRecorderManager(options.apiKey), + recDiv = document.createElement('div'), + container = o.container == null ? document.body : o.container; + + recDiv.setAttribute('id', 'recorderElement'); + container.appendChild(recDiv); + recorder = recorderManager.displayRecorder(options.token, recDiv.id); + recorder.addEventListener('recordingStarted', self.onstart); + recorder.addEventListener('recordingStopped', self.onstop); + recorder.addEventListener('archiveSaved', onArchiveSaved); + + if (options.style != null) { + var style = {}; + if (options.style == 'blank') { + style = { + buttonDisplayMode: 'off', + showControlBar: false, + showMicButton: false, + showRecordButton: false, + showRecordCounter: false, + showRecordStopButton: false, + showReRecordButton: false, + showPlayButton: false, + showPlayCounter: false, + showPlayStopButton: false, + showSaveButton: false, + showSettingsButton: false + }; + } + recorder.setStyle(style); + } + + if (_startRequest) { + setTimeout(function() { + _initialized = true; + start(); + }, options.loadingDuration); // i have no idea how to handle ready event + } else { + _initialized = true; + } + self.onready(); + + } + } + + function onArchiveSaved(e) { + self.onsavecomplete(); + + if (options.remoteArchive != null) { + var params = {}, + url; + + if (typeof options.remoteArchive == 'function') { + params.url = options.remoteArchive(e); + } else if (typeof options.remoteArchive == 'string') { + params.url = options.remoteArchive; + } else if (typeof options.remoteArchive == 'object') { + params = options.remoteArchive; + } + + getVideoURL(params); + + } else { + self.ondataavailable({ + data: e, + dataType: 'raw' + }); + } + + } + + function getVideoURL(params) { + getUrlData(params, + function(answer) { + if (answer == 'not_ready' && self.pollWhileProcessing) { + getVideoURL(params) + } else { + self.ondataavailable({ + data: answer, + dataType: 'url' + }); + } + } + ); + } + + function getUrlData(params, callback) { + if (typeof params == 'string') { + params = { + url: params + } + } + var options = extend(remoteDefaults, params); + + options.success = callback; + options.error = self.onerror; + + if (window.jQuery != null) { + jQuery.ajax(options); + } else { + include(baseUrl + 'lib/AjaxRequest/AjaxRequest.js', function() { + switch (options.method) { + case 'GET': + AjaxRequest.get({ + url: options.url, + parameters: options.data, + onSuccess: function(obj) { + options.success(obj.responseText) + }, + onError: options.error + }) + break; + case 'POST': + AjaxRequest.post({ + url: options.url, + parameters: options.data, + onSuccess: function(obj) { + options.success(obj.responseText) + }, + onError: options.error + }) + break; + } + }); + } + } + + function getBaseUrl() { + var scripts = document.head.getElementsByTagName("script"); + var loc = scripts[scripts.length - 1].src; + return loc.substring(0, loc.lastIndexOf('/')) + '/'; + } + // extending user options + function extend(o1, o2) { + var obj = {}; + for (var i in o1) { + if (o2[i] != null) { + if (typeof o2[i] == "object") { + obj[i] = extend(o2[i], {}); + } else { + obj[i] = o2[i]; + } + } else { + if (typeof o2[i] == "object") { + obj[i] = extend(o1[i], {}); + } else { + obj[i] = o1[i]; + } + } + } + return obj; + } + + function include(src, callback) { + var scripts = document.getElementsByTagName('script'), + runCallback = false; + + for (var i = 0, len = scripts.length; i < len; i++) { + if (scripts[i].getAttribute('src') == src) { + runCallback = true; + if (scripts[i].getAttribute('loading') == 'loading') { + runCallback = false; + if (typeof callback == 'function') { + var onload = scripts[i].onload; + scripts[i].onload = function() { + callback(); + if (typeof onload == 'function') { + onload(); + } + } + } + } + } + } + + if (runCallback) { + if (typeof callback == 'function') { + callback(); + } + } else { + var js = document.createElement("script"); + + js.type = "text/javascript"; + js.src = src; + js.setAttribute('loading', 'loading'); + if (typeof callback == 'function') { + js.onload = function() { + callback() + js.removeAttribute('loading'); + } + } else { + js.onload = function() { + js.removeAttribute('loading'); + } + } + + document.body.appendChild(js); + } + } + this.baseUrl = baseUrl; + this.onstart = options.onstart; + this.onstop = options.onstop; + this.onerror = options.onerror; + this.onready = options.onready; + this.onsavecomplete = options.onsavecomplete; + this.start = start; + this.stop = stop; + this.save = save; + this.state = 'inactive'; + this.ondataavailable = options.ondataavailable; + this.getInterface = function() { + return recorder + }; +} diff --git a/common/head.js b/common/head.js new file mode 100644 index 0000000..0af4701 --- /dev/null +++ b/common/head.js @@ -0,0 +1,30 @@ +// Last time updated at September 19, 2015 + +// 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 + +// updates? +/* +-. this.recorderType = StereoAudioRecorder; +*/ + +//------------------------------------ + +// 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 +//------------------------------------ + +'use strict'; diff --git a/demos/MultiStreamRecorder.html b/demos/MultiStreamRecorder.html index abaf3e9..03e657f 100644 --- a/demos/MultiStreamRecorder.html +++ b/demos/MultiStreamRecorder.html @@ -7,182 +7,239 @@ --> - - MultiStreamRecorder.js & MediaStreamRecorder - - - - - - - - + + + +

+ MultiStreamRecorder.js & MediaStreamRecorder +

+ +

+ Getting both audio/video blobs in single "ondataavailable" event using MediaStreamRecorder +

+ +
+ + ms + + + + + + +
+ +
+ + - -
+            // get blob after specific time interval
+            multiStreamRecorder.start(timeInterval);
+
+            document.querySelector('#stop-recording').disabled = false;
+            document.querySelector('#pause-recording').disabled = false;
+        }, false);
+
+        video.play();
+
+        container.appendChild(video);
+        container.appendChild(document.createElement('hr'));
+    }
+
+    function onMediaError(e) {
+        console.error('media error', e);
+    }
+
+    var container = document.getElementById('container');
+    var index = 1;
+
+    // below function via: http://goo.gl/B3ae8c
+    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];
+    }
+
+    // below function via: http://goo.gl/6QNDcI
+    function getTimeLength(milliseconds) {
+        var data = new Date(milliseconds);
+        return data.getUTCHours() + " hours, " + data.getUTCMinutes() + " minutes and " + data.getUTCSeconds() + " second(s)";
+    }
+
+    window.onbeforeunload = function() {
+        document.querySelector('#start-recording').disabled = false;
+    };
+    
+
+
 // cdn.webrtc-experiment.com/MediaStreamRecorder.js
 var mediaConstraints = {
     audio: true,
@@ -204,7 +261,9 @@ 

console.error('media error', e); }

- - ← WebRTC Experiments - - + + MediaStreamRecorder Demos + + + + \ No newline at end of file diff --git a/demos/audio-recorder.html b/demos/audio-recorder.html index 0465074..389bb53 100644 --- a/demos/audio-recorder.html +++ b/demos/audio-recorder.html @@ -7,147 +7,221 @@ --> - - WebRTC Audio Recording using MediaStreamRecorder - - - - - - - - - - -

- WebRTC Audio Recording using MediaStreamRecorder -

- -
- - ms - - - - - - -
- -
-
- - -
+
+
+    WebRTC Audio Recording using MediaStreamRecorder
+
+    
+    
+    
+
+    
+
+
+
+    

+ WebRTC Audio Recording using MediaStreamRecorder +

+ +
+ + ms + + + + + + + + + +

+ + + +
+ +
+ + + +
 // cdn.webrtc-experiment.com/MediaStreamRecorder.js
+
 var mediaConstraints = {
-    audio: true
+    audio: !!navigator.mozGetUserMedia, // if firefox (don't use for chrome)
+    video: true  // if firefox or chrome
 };
 
 navigator.getUserMedia(mediaConstraints, onMediaSuccess, onMediaError);
 
 function onMediaSuccess(stream) {
     var mediaRecorder = new MediaStreamRecorder(stream);
-    mediaRecorder.mimeType = 'audio/ogg';
+    mediaRecorder.mimeType = 'video/webm';
     mediaRecorder.ondataavailable = function (blob) {
         // POST/PUT "Blob" using FormData/XHR2
         var blobURL = URL.createObjectURL(blob);
-        document.write('<a href="' + blobURL + '">' + blobURL + '</a>');
+        document.write('' + blobURL + '');
     };
     mediaRecorder.start(3000);
 }
@@ -156,7 +230,8 @@ 

console.error('media error', e); }

- - ← WebRTC Experiments - - + + MediaStreamRecorder Demos + + + \ No newline at end of file diff --git a/demos/flash-audio-recorder-upload.html b/demos/flash-audio-recorder-upload.html index 9f66059..e2543af 100644 --- a/demos/flash-audio-recorder-upload.html +++ b/demos/flash-audio-recorder-upload.html @@ -1,40 +1,43 @@ - - Audio Recording using FlashAudioRecorder - - - -

- Audio Recording using FlashAudioRecorder -

- - + + + Audio Recording using FlashAudioRecorder + + + + +

+ Audio Recording using FlashAudioRecorder +

+ + + \ No newline at end of file diff --git a/demos/flash-audio-recorder.html b/demos/flash-audio-recorder.html index 3e2b4c2..9c4f7dc 100644 --- a/demos/flash-audio-recorder.html +++ b/demos/flash-audio-recorder.html @@ -1,34 +1,37 @@ - - Audio Recording using FlashAudioRecorder - - - -

- Audio Recording using FlashAudioRecorder -

- - + + + Audio Recording using FlashAudioRecorder + + + + +

+ Audio Recording using FlashAudioRecorder +

+ + + \ No newline at end of file diff --git a/demos/gif-recorder.html b/demos/gif-recorder.html index c2f273f..9124334 100644 --- a/demos/gif-recorder.html +++ b/demos/gif-recorder.html @@ -7,131 +7,207 @@ --> - - WebRTC Gif Recording using MediaStreamRecorder - - - - - - - -

- WebRTC Gif Recording using MediaStreamRecorder -

- -
- - - - - - - - - - - -
-
-
- - -
+
+
+    WebRTC Gif Recording using MediaStreamRecorder
+
+    
+    
+    
+    
+
+    
+
+
+
+    

+ WebRTC Gif Recording using MediaStreamRecorder +

+ +
+ + + + + + + + + +

+ + + + + + +
+
+
+ + +
 // cdn.webrtc-experiment.com/MediaStreamRecorder.js
+// cdn.webrtc-experiment.com/gif-recorder.js
 
 var mediaConstraints = {
     video: true
@@ -145,7 +221,7 @@ 

mediaRecorder.ondataavailable = function (blob) { // POST/PUT "Blob" using FormData/XHR2 var blobURL = URL.createObjectURL(blob); - document.write('<a href="' + blobURL + '">' + blobURL + '</a>'); + document.write('' + blobURL + ''); }; mediaRecorder.start(3000); } @@ -154,7 +230,8 @@

console.error('media error', e); }

- - ← WebRTC Experiments - - + + MediaStreamRecorder Demos + + + \ No newline at end of file diff --git a/demos/index.html b/demos/index.html new file mode 100644 index 0000000..531a91e --- /dev/null +++ b/demos/index.html @@ -0,0 +1,61 @@ +

MediaStreamRecorder.js Demos

+ +
    +
  1. + It is open-sourced here: https://github.com/streamproc/MediaStreamRecorder +
  2. +
  3. + Audio Recording +
  4. + +
  5. + Video Recording +
  6. + +
  7. + Audio + Video Recording +
  8. + +
  9. + Gif Recording +
  10. +
+ + diff --git a/demos/opentok-stream-recorder.html b/demos/opentok-stream-recorder.html index 352b385..48b6195 100644 --- a/demos/opentok-stream-recorder.html +++ b/demos/opentok-stream-recorder.html @@ -1,39 +1,42 @@ - - Stream Recording using OpenTokStreamRecorder - - - -

- Stream Recording using OpenTokStreamRecorder -

-

- You must run this app on your server. Our example will be soon. -

- - + + + Stream Recording using OpenTokStreamRecorder + + + + +

+ Stream Recording using OpenTokStreamRecorder +

+

+ You must run this app on your server. Our example will be soon. +

+ + + \ No newline at end of file diff --git a/demos/video-recorder.html b/demos/video-recorder.html index 4f91493..3ae137e 100644 --- a/demos/video-recorder.html +++ b/demos/video-recorder.html @@ -7,129 +7,214 @@ --> - - WebRTC Video Recording using MediaStreamRecorder - - - - - - -

- WebRTC Video Recording using MediaStreamRecorder -

- -
- - - - - - - - - - - -
-
-
- - -
+
+
+    WebRTC Video Recording using MediaStreamRecorder
+
+    
+    
+    
+
+    
+
+
+
+    

+ WebRTC Video Recording using MediaStreamRecorder +

+ +
+ + + + + + + + + + + +

+ + + + + + +
+
+
+ + +
 // cdn.webrtc-experiment.com/MediaStreamRecorder.js
 
 var mediaConstraints = {
@@ -145,7 +230,7 @@ 

mediaRecorder.ondataavailable = function (blob) { // POST/PUT "Blob" using FormData/XHR2 var blobURL = URL.createObjectURL(blob); - document.write('<a href="' + blobURL + '">' + blobURL + '</a>'); + document.write('' + blobURL + ''); }; mediaRecorder.start(3000); } @@ -154,7 +239,8 @@

console.error('media error', e); }

- - ← WebRTC Experiments - - + + MediaStreamRecorder Demos + + + \ No newline at end of file diff --git a/package.json b/package.json index ed6c597..92b4acf 100644 --- a/package.json +++ b/package.json @@ -59,5 +59,17 @@ "email": "muazkh@gmail.com" }, "homepage": "https://www.webrtc-experiment.com/msr/", - "_from": "msr@" + "_from": "msr@", + "devDependencies": { + "grunt": "latest", + "grunt-cli": "latest", + "load-grunt-tasks": "latest", + "grunt-contrib-concat": "latest", + "grunt-contrib-csslint": "latest", + "grunt-contrib-jshint": "latest", + "grunt-contrib-uglify": "latest", + "grunt-htmlhint": "latest", + "grunt-jsbeautifier": "latest", + "grunt-bump": "latest" + } } diff --git a/server.js b/server.js new file mode 100644 index 0000000..0c666ec --- /dev/null +++ b/server.js @@ -0,0 +1,80 @@ +// http://127.0.0.1:9001 +// http://localhost:9001 + +var server = require('http'), + url = require('url'), + path = require('path'), + fs = require('fs'); + +var isWin = !!process.platform.match(/^win/); + +function serverHandler(request, response) { + var uri = url.parse(request.url).pathname; + + if(isWin) { + filename = path.join(process.cwd() + '\\demos\\', uri); + } + else { + filename = path.join(process.cwd() + '/demos/', uri); + } + + fs.exists(filename, function(exists) { + if (!exists) { + response.writeHead(404, { + 'Content-Type': 'text/plain' + }); + response.write('404 Not Found: ' + filename + '\n'); + response.end(); + return; + } + + if (filename.indexOf('favicon.ico') !== -1) { + return; + } + + if (fs.statSync(filename).isDirectory() && !isWin) { + filename += '/index.html'; + } else if (fs.statSync(filename).isDirectory() && !!isWin) { + filename += '\\index.html'; + } + + fs.readFile(filename, 'binary', function(err, file) { + if (err) { + response.writeHead(500, { + 'Content-Type': 'text/plain' + }); + response.write(err + '\n'); + response.end(); + return; + } + + var contentType; + + if (filename.indexOf('.html') !== -1) { + contentType = 'text/html'; + } + + if (filename.indexOf('.js') !== -1) { + contentType = 'application/javascript'; + } + + if (contentType) { + response.writeHead(200, { + 'Content-Type': contentType + }); + } else response.writeHead(200); + + response.write(file, 'binary'); + response.end(); + }); + }); +} + +var app; + +app = server.createServer(serverHandler); + +app = app.listen(process.env.PORT || 9001, process.env.IP || "0.0.0.0", function() { + var addr = app.address(); + console.log("Server listening at", addr.address + ":" + addr.port); +});