├── .gitignore ├── LICENSE ├── README.md ├── client ├── index.html ├── scripts │ ├── index.js │ ├── recorder.js │ └── socket.io-1.2.0.js └── workers │ ├── EmsArgs.js │ ├── EmsWorkerProxy.js │ ├── OpusEncoder.js │ ├── opusenc.data.js │ ├── opusenc.js │ └── recorderWorker.js ├── package.json └── server └── app.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | bin 3 | **/*.log 4 | uploads -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014-15 Ban Mido 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### Note 2 | 3 | For those looking for pure client based solution, not streaming packets to server, take a look at [this demo](https://github.com/Mido22/MediaRecorder-sample), it works in chrome v47+ and firefox v25+, the code is quite simple, my advice to you is use HTML5's MediaRecorder directly and no use some third-party wrapper library unless it is absolutely needed. 4 | 5 | --- 6 | 7 | # Opus Audio Recorder 8 | 9 | 10 | If support for MediaRecorder API is detected( in case of Firefox), my recorder is just a wrapper for it, for other case, I am using [Rillke](https://github.com/Rillke)'s [opus library](https://github.com/Rillke/opusenc.js) for compressing wav audio into ogg file on client side. 11 | 12 | You can just host the files in `client` folder in any server, if you just want to record audio from microphone in one shot and save as `ogg` file, but if you do not want to run out of browser memory( while recording long podcasts and such,) you can keep sending the data as chunks to the server, at desired time intervals and server would join all of them into single file( `ffmpeg` needed for browsers other than firefox) and provide you the final link. 13 | 14 | 15 | ### Things needed: 16 | * node (for server) 17 | * ffmpeg (for media manipulation, OPTIONAL) 18 | 19 | 20 | ### set-up: 21 | * npm install 22 | * `ffmpeg` must be pre-installed and must be part of path 23 | 24 | to start the application, just type `npm start` in the project root folder. 25 | 26 | 27 | ### Sources: 28 | * [Opus](https://github.com/Rillke/opusenc.js) 29 | * [Opus( older one)](https://github.com/kazuki/opus.js-sample) 30 | * [Wav Recorder](https://github.com/mattdiamond/Recorderjs) 31 | 32 | 33 | License 34 | ------- 35 | 36 | See file LICENSE for further information. 37 | 38 | 39 | Author 40 | ------ 41 | 42 | Ban Mido 43 | 44 | -------------------------------------------------------------------------------- /client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Opus Recorder 7 | 14 | 15 | 16 |
17 |
18 |

Javascript Opus Recorder

19 |
20 | 21 |

make sure you enable permissions for recording audio.

22 |
23 | 24 | 25 |
26 |
27 |
auto-upload :
28 |
upload every( in seconds):
29 |
30 |
31 |
32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /client/scripts/index.js: -------------------------------------------------------------------------------- 1 | navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia || navigator.msGetUserMedia; 2 | 3 | var recorder 4 | , input 5 | , start 6 | , stop 7 | , recordingslist 8 | , socket 9 | , autoUpload = document.getElementById('autoUpload') 10 | , intervalTime = document.getElementById('intervalTime') 11 | ; 12 | 13 | socket = io(); 14 | socket.on('link', onServerFileReady); 15 | function onServerFileReady(data){ 16 | //if(self.uid!== data.uid) return; 17 | data.path = data.path.replace('\\', '/'); 18 | var url = location.protocol + '//' + location.host + '/'+data.path; 19 | data.url = url; 20 | tmp =url.split('/'); 21 | data.name = tmp[tmp.length-1]; 22 | addFileLink(data); 23 | } 24 | 25 | document.addEventListener("DOMContentLoaded", function() { 26 | socket = io(); 27 | start = document.getElementById('start'); 28 | stop = document.getElementById('stop'); 29 | recordingslist = document.getElementById('recordingslist'); 30 | audio_context = new AudioContext; 31 | navigator.getUserMedia({audio: true}, function(stream) { 32 | window.stream = stream; 33 | start.removeAttribute('disabled'); 34 | }, function(e){ console.log('error occoured= '+e)}); 35 | 36 | start.setAttribute('disabled',true); 37 | stop.setAttribute('disabled',true); 38 | start.onclick = startRecording; 39 | stop.onclick = stopRecording; 40 | }); 41 | 42 | function startRecording() { 43 | recorder = new OpusRecorder(window.stream, { 44 | autoUpload: autoUpload.checked, 45 | intervalTime: Math.round(intervalTime.value * 1000) 46 | }, socket); 47 | recorder.start(); 48 | start.setAttribute('disabled',true); 49 | stop.removeAttribute('disabled'); 50 | } 51 | 52 | function stopRecording() { 53 | recorder.stop(addFileLink); 54 | start.removeAttribute('disabled'); 55 | stop.setAttribute('disabled',true); 56 | } 57 | 58 | function addFileLink(data) { 59 | 60 | var url = data.url 61 | , li = document.createElement('li') 62 | , au = document.createElement('audio') 63 | , hf = document.createElement('a') 64 | ; 65 | if(document.querySelector('li.'+data.uid)) return; 66 | li.className += " ."+data.uid; 67 | au.controls = true; 68 | au.src = url; 69 | hf.href = url; 70 | hf.download = data.name || 'download'; 71 | hf.innerHTML = hf.download; 72 | li.appendChild(au); 73 | li.appendChild(hf); 74 | recordingslist.appendChild(li); 75 | } 76 | 77 | -------------------------------------------------------------------------------- /client/scripts/recorder.js: -------------------------------------------------------------------------------- 1 | (function(window){ 2 | 3 | window.AudioContext = window.AudioContext || window.webkitAudioContext; 4 | window.URL = window.URL || window.webkitURL; 5 | window.audioContext = new AudioContext(); 6 | window.OpusRecorder = OpusRecorder; 7 | var WORKER_PATH = 'workers/recorderWorker.js', 8 | OPUS_WORKER_PATH = 'workers/EmsWorkerProxy.js', 9 | commonCallback; 10 | 11 | function OggShimRecorder(stream, cfg){ 12 | 13 | 14 | var config = cfg || {} 15 | , self = this 16 | , bufferLen = 16384 17 | , intervalTime = config.intervalTime || 60000 18 | , autoUpload = !!config.autoUpload 19 | , recording = false 20 | , worker = new Worker(config.workerPath || WORKER_PATH) 21 | , oggWorker = new Worker(config.opusWorkerPath || OPUS_WORKER_PATH) 22 | , source = window.audioContext.createMediaStreamSource(stream) 23 | , ctx = source.context 24 | , sampleRate = ctx.sampleRate 25 | , node = ctx.createScriptProcessor(bufferLen, 2, 2) 26 | , callback = config.callback 27 | , vInterval 28 | , recorderType = 'OggShim' 29 | ; 30 | source.connect(node); 31 | node.connect(ctx.destination); 32 | 33 | //using the below variables to keep track of chucks sent to worker for conversion, we are gonna convert chunks sequencially as when done in parallel, sometimes the (last) smaller chuck finishes first sometimes and previous few chucks are missed. 34 | var encodingInProgess = false, 35 | waitQueue = []; // for holding wav to ogg conversion worker call data( used for ensuring sequencing) 36 | 37 | 38 | node.onaudioprocess = function(e){ 39 | if (!recording) return; 40 | worker.postMessage({ 41 | command: 'record', 42 | buffer: [e.inputBuffer.getChannelData(0), e.inputBuffer.getChannelData(1)] 43 | }); 44 | }; 45 | 46 | worker.postMessage({ 47 | command: 'init', 48 | config: { 49 | sampleRate: sampleRate 50 | } 51 | }); 52 | 53 | worker.onmessage = function(e){ 54 | var data = e.data; 55 | delete data.command; 56 | blobToArrayBuffer(data.blob, function(buffer){ 57 | oggWorker.postMessage({ 58 | command: 'encode', 59 | args: ['in', 'out'], 60 | outData: {out: {MIME: 'audio/ogg'}}, 61 | fileData: {in: new Uint8Array(buffer)} 62 | }); 63 | oggWorker.onmessage = function(e) { 64 | var next; 65 | if(e.data && e.data.reply){ 66 | if(e.data.reply === 'done'){ 67 | data.blob = e.data.values.out.blob; 68 | callback(data); 69 | if(waitQueue.length){ 70 | next = waitQueue.shift(); 71 | worker.postMessage(next); 72 | }else{ 73 | encodingInProgess = false; 74 | } 75 | } 76 | } 77 | }; 78 | }); 79 | }; 80 | 81 | this.start = function(){ 82 | recording = true; 83 | if(autoUpload) vInterval = setInterval(getBlob, intervalTime); 84 | }; 85 | 86 | this.stop = function(cb){ 87 | callback = cb || callback; 88 | if(vInterval) clearInterval(vInterval); 89 | getBlob(true); 90 | recording = false; 91 | }; 92 | 93 | function getBlob(last){ 94 | 95 | var msg = { 96 | command: 'export', 97 | autoUpload: autoUpload, 98 | stop: last, 99 | recorderType: recorderType 100 | }; 101 | if(encodingInProgess){ 102 | waitQueue.push(msg); 103 | }else{ 104 | encodingInProgess = true; 105 | worker.postMessage(msg); 106 | } 107 | } 108 | 109 | }; 110 | 111 | function FoxRecorder(stream, cfg){ 112 | 113 | console.log('using native MediaRecorder for recording...' ); 114 | var mediaRecorder = new MediaRecorder(stream); 115 | var config = cfg || {} 116 | , self = this 117 | , intervalTime = config.intervalTime || 60000 118 | , autoUpload = !!config.autoUpload 119 | , callback = config.callback 120 | , chunksRequested = 0, chunkId = 0 // for identifying the last data chunk 121 | , vInterval 122 | , stopped 123 | , callback = config.callback 124 | , recorderType = 'Fox' 125 | ; 126 | 127 | this.start = function(){ 128 | mediaRecorder.start(); 129 | if(autoUpload) vInterval = setInterval(requestData, intervalTime); 130 | }; 131 | 132 | this.stop = function(cb){ 133 | callback = cb || callback; 134 | if(vInterval) clearInterval(vInterval); 135 | mediaRecorder.stop(); 136 | chunksRequested++; 137 | stopped = true; // can also be checked as mediarecorder.state === 'inactive' 138 | }; 139 | 140 | function requestData(){ 141 | mediaRecorder.requestData(); 142 | chunksRequested++; 143 | } 144 | 145 | mediaRecorder.ondataavailable = function(e){ 146 | chunkId++; 147 | callback({ 148 | autoUpload: autoUpload, 149 | stop: (chunkId >= chunksRequested) && stopped, 150 | type: 'ogg', 151 | blob: e.data, 152 | recorderType: recorderType 153 | }); 154 | } 155 | }; 156 | 157 | function OpusRecorder(stream, cfg, socket){ 158 | 159 | var config = cfg || {} 160 | , self = this 161 | , callback = config.callback 162 | , recorder 163 | , uid = genRandom() 164 | ; 165 | 166 | config.intervalTime = config.intervalTime || 60000; 167 | config.autoUpload = !!config.autoUpload; 168 | config.type = 'ogg'; 169 | config.callback = onBlobData; 170 | if(callback) commonCallback = callback; 171 | 172 | this.start = function(){ 173 | recorder = (window.MediaRecorder)? new FoxRecorder(stream, config) : new OggShimRecorder(stream, config); 174 | recorder.start(); 175 | }; 176 | 177 | this.stop = function(cb){ 178 | if(cb) callback = commonCallback = cb; 179 | recorder.stop(); 180 | }; 181 | 182 | function onBlobData(data){ 183 | data.uid= uid; 184 | if(!autoUpload){ 185 | data.name = [data.uid, data.type || 'ogg'].join('.'); 186 | data.url = window.URL.createObjectURL(data.blob); 187 | callback(data); 188 | }else{ 189 | socket.emit('save', data); 190 | } 191 | } 192 | 193 | }; 194 | 195 | function genRandom(){ 196 | return ('Rec:'+(new Date()).toTimeString().slice(0, 8) + ':' + Math.round(Math.random()*1000)).replace(/:/g,'_'); 197 | } 198 | 199 | function blobToArrayBuffer(blob, cb){ 200 | var fileReader = new FileReader(); 201 | fileReader.onload = function() { 202 | cb(this.result); 203 | }; 204 | fileReader.readAsArrayBuffer(blob); 205 | } 206 | 207 | })(window); 208 | 209 | 210 | 211 | -------------------------------------------------------------------------------- /client/scripts/socket.io-1.2.0.js: -------------------------------------------------------------------------------- 1 | !function(e){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=e();else if("function"==typeof define&&define.amd)define([],e);else{var f;"undefined"!=typeof window?f=window:"undefined"!=typeof global?f=global:"undefined"!=typeof self&&(f=self),f.io=e()}}(function(){var define,module,exports;return function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);throw new Error("Cannot find module '"+o+"'")}var f=n[o]={exports:{}};t[o][0].call(f.exports,function(e){var n=t[o][1][e];return s(n?n:e)},f,f.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o0&&!this.encoding){var pack=this.packetBuffer.shift();this.packet(pack)}};Manager.prototype.cleanup=function(){var sub;while(sub=this.subs.shift())sub.destroy();this.packetBuffer=[];this.encoding=false;this.decoder.destroy()};Manager.prototype.close=Manager.prototype.disconnect=function(){this.skipReconnect=true;this.readyState="closed";this.engine&&this.engine.close()};Manager.prototype.onclose=function(reason){debug("close");this.cleanup();this.readyState="closed";this.emit("close",reason);if(this._reconnection&&!this.skipReconnect){this.reconnect()}};Manager.prototype.reconnect=function(){if(this.reconnecting||this.skipReconnect)return this;var self=this;this.attempts++;if(this.attempts>this._reconnectionAttempts){debug("reconnect failed");this.emitAll("reconnect_failed");this.reconnecting=false}else{var delay=this.attempts*this.reconnectionDelay();delay=Math.min(delay,this.reconnectionDelayMax());debug("will wait %dms before reconnect attempt",delay);this.reconnecting=true;var timer=setTimeout(function(){if(self.skipReconnect)return;debug("attempting reconnect");self.emitAll("reconnect_attempt",self.attempts);self.emitAll("reconnecting",self.attempts);if(self.skipReconnect)return;self.open(function(err){if(err){debug("reconnect attempt error");self.reconnecting=false;self.reconnect();self.emitAll("reconnect_error",err.data)}else{debug("reconnect success");self.onreconnect()}})},delay);this.subs.push({destroy:function(){clearTimeout(timer)}})}};Manager.prototype.onreconnect=function(){var attempt=this.attempts;this.attempts=0;this.reconnecting=false;this.emitAll("reconnect",attempt)}},{"./on":4,"./socket":5,"./url":6,"component-bind":7,"component-emitter":8,debug:9,"engine.io-client":10,indexof:36,"object-component":37,"socket.io-parser":40}],4:[function(_dereq_,module,exports){module.exports=on;function on(obj,ev,fn){obj.on(ev,fn);return{destroy:function(){obj.removeListener(ev,fn)}}}},{}],5:[function(_dereq_,module,exports){var parser=_dereq_("socket.io-parser");var Emitter=_dereq_("component-emitter");var toArray=_dereq_("to-array");var on=_dereq_("./on");var bind=_dereq_("component-bind");var debug=_dereq_("debug")("socket.io-client:socket");var hasBin=_dereq_("has-binary");module.exports=exports=Socket;var events={connect:1,connect_error:1,connect_timeout:1,disconnect:1,error:1,reconnect:1,reconnect_attempt:1,reconnect_failed:1,reconnect_error:1,reconnecting:1};var emit=Emitter.prototype.emit;function Socket(io,nsp){this.io=io;this.nsp=nsp;this.json=this;this.ids=0;this.acks={};if(this.io.autoConnect)this.open();this.receiveBuffer=[];this.sendBuffer=[];this.connected=false;this.disconnected=true}Emitter(Socket.prototype);Socket.prototype.subEvents=function(){if(this.subs)return;var io=this.io;this.subs=[on(io,"open",bind(this,"onopen")),on(io,"packet",bind(this,"onpacket")),on(io,"close",bind(this,"onclose"))]};Socket.prototype.open=Socket.prototype.connect=function(){if(this.connected)return this;this.subEvents();this.io.open();if("open"==this.io.readyState)this.onopen();return this};Socket.prototype.send=function(){var args=toArray(arguments);args.unshift("message");this.emit.apply(this,args);return this};Socket.prototype.emit=function(ev){if(events.hasOwnProperty(ev)){emit.apply(this,arguments);return this}var args=toArray(arguments);var parserType=parser.EVENT;if(hasBin(args)){parserType=parser.BINARY_EVENT}var packet={type:parserType,data:args};if("function"==typeof args[args.length-1]){debug("emitting packet with ack id %d",this.ids);this.acks[this.ids]=args.pop();packet.id=this.ids++}if(this.connected){this.packet(packet)}else{this.sendBuffer.push(packet)}return this};Socket.prototype.packet=function(packet){packet.nsp=this.nsp;this.io.packet(packet)};Socket.prototype.onopen=function(){debug("transport is open - connecting");if("/"!=this.nsp){this.packet({type:parser.CONNECT})}};Socket.prototype.onclose=function(reason){debug("close (%s)",reason);this.connected=false;this.disconnected=true;this.emit("disconnect",reason)};Socket.prototype.onpacket=function(packet){if(packet.nsp!=this.nsp)return;switch(packet.type){case parser.CONNECT:this.onconnect();break;case parser.EVENT:this.onevent(packet);break;case parser.BINARY_EVENT:this.onevent(packet);break;case parser.ACK:this.onack(packet);break;case parser.BINARY_ACK:this.onack(packet);break;case parser.DISCONNECT:this.ondisconnect();break;case parser.ERROR:this.emit("error",packet.data);break}};Socket.prototype.onevent=function(packet){var args=packet.data||[];debug("emitting event %j",args);if(null!=packet.id){debug("attaching ack callback to event");args.push(this.ack(packet.id))}if(this.connected){emit.apply(this,args)}else{this.receiveBuffer.push(args)}};Socket.prototype.ack=function(id){var self=this;var sent=false;return function(){if(sent)return;sent=true;var args=toArray(arguments);debug("sending ack %j",args);var type=hasBin(args)?parser.BINARY_ACK:parser.ACK;self.packet({type:type,id:id,data:args})}};Socket.prototype.onack=function(packet){debug("calling ack %s with %j",packet.id,packet.data);var fn=this.acks[packet.id];fn.apply(this,packet.data);delete this.acks[packet.id]};Socket.prototype.onconnect=function(){this.connected=true;this.disconnected=false;this.emit("connect");this.emitBuffered()};Socket.prototype.emitBuffered=function(){var i;for(i=0;i=hour)return(ms/hour).toFixed(1)+"h";if(ms>=min)return(ms/min).toFixed(1)+"m";if(ms>=sec)return(ms/sec|0)+"s";return ms+"ms"};debug.enabled=function(name){for(var i=0,len=debug.skips.length;i';iframe=document.createElement(html)}catch(e){iframe=document.createElement("iframe");iframe.name=self.iframeId;iframe.src="javascript:0"}iframe.id=self.iframeId;self.form.appendChild(iframe);self.iframe=iframe}initIframe();data=data.replace(rEscapedNewline,"\\\n");this.area.value=data.replace(rNewline,"\\n");try{this.form.submit()}catch(e){}if(this.iframe.attachEvent){this.iframe.onreadystatechange=function(){if(self.iframe.readyState=="complete"){complete()}}}else{this.iframe.onload=complete}}}).call(this,typeof self!=="undefined"?self:typeof window!=="undefined"?window:{})},{"./polling":17,"component-inherit":20}],16:[function(_dereq_,module,exports){(function(global){var XMLHttpRequest=_dereq_("xmlhttprequest");var Polling=_dereq_("./polling");var Emitter=_dereq_("component-emitter");var inherit=_dereq_("component-inherit");var debug=_dereq_("debug")("engine.io-client:polling-xhr");module.exports=XHR;module.exports.Request=Request;function empty(){}function XHR(opts){Polling.call(this,opts);if(global.location){var isSSL="https:"==location.protocol;var port=location.port;if(!port){port=isSSL?443:80}this.xd=opts.hostname!=global.location.hostname||port!=opts.port;this.xs=opts.secure!=isSSL}}inherit(XHR,Polling);XHR.prototype.supportsBinary=true;XHR.prototype.request=function(opts){opts=opts||{};opts.uri=this.uri();opts.xd=this.xd;opts.xs=this.xs;opts.agent=this.agent||false;opts.supportsBinary=this.supportsBinary;opts.enablesXDR=this.enablesXDR;return new Request(opts)};XHR.prototype.doWrite=function(data,fn){var isBinary=typeof data!=="string"&&data!==undefined;var req=this.request({method:"POST",data:data,isBinary:isBinary});var self=this;req.on("success",fn);req.on("error",function(err){self.onError("xhr post error",err)});this.sendXhr=req};XHR.prototype.doPoll=function(){debug("xhr poll");var req=this.request();var self=this;req.on("data",function(data){self.onData(data)});req.on("error",function(err){self.onError("xhr poll error",err)});this.pollXhr=req};function Request(opts){this.method=opts.method||"GET";this.uri=opts.uri;this.xd=!!opts.xd;this.xs=!!opts.xs;this.async=false!==opts.async;this.data=undefined!=opts.data?opts.data:null;this.agent=opts.agent;this.isBinary=opts.isBinary;this.supportsBinary=opts.supportsBinary;this.enablesXDR=opts.enablesXDR;this.create()}Emitter(Request.prototype);Request.prototype.create=function(){var xhr=this.xhr=new XMLHttpRequest({agent:this.agent,xdomain:this.xd,xscheme:this.xs,enablesXDR:this.enablesXDR});var self=this;try{debug("xhr open %s: %s",this.method,this.uri);xhr.open(this.method,this.uri,this.async);if(this.supportsBinary){xhr.responseType="arraybuffer"}if("POST"==this.method){try{if(this.isBinary){xhr.setRequestHeader("Content-type","application/octet-stream")}else{xhr.setRequestHeader("Content-type","text/plain;charset=UTF-8")}}catch(e){}}if("withCredentials"in xhr){xhr.withCredentials=true}if(this.hasXDR()){xhr.onload=function(){self.onLoad()};xhr.onerror=function(){self.onError(xhr.responseText)}}else{xhr.onreadystatechange=function(){if(4!=xhr.readyState)return;if(200==xhr.status||1223==xhr.status){self.onLoad()}else{setTimeout(function(){self.onError(xhr.status)},0)}}}debug("xhr data %s",this.data);xhr.send(this.data)}catch(e){setTimeout(function(){self.onError(e)},0);return}if(global.document){this.index=Request.requestsCount++;Request.requests[this.index]=this}};Request.prototype.onSuccess=function(){this.emit("success");this.cleanup()};Request.prototype.onData=function(data){this.emit("data",data);this.onSuccess()};Request.prototype.onError=function(err){this.emit("error",err);this.cleanup()};Request.prototype.cleanup=function(){if("undefined"==typeof this.xhr||null===this.xhr){return}if(this.hasXDR()){this.xhr.onload=this.xhr.onerror=empty}else{this.xhr.onreadystatechange=empty}try{this.xhr.abort()}catch(e){}if(global.document){delete Request.requests[this.index]}this.xhr=null};Request.prototype.onLoad=function(){var data;try{var contentType;try{contentType=this.xhr.getResponseHeader("Content-Type").split(";")[0]}catch(e){}if(contentType==="application/octet-stream"){data=this.xhr.response}else{if(!this.supportsBinary){data=this.xhr.responseText}else{data="ok"}}}catch(e){this.onError(e)}if(null!=data){this.onData(data)}};Request.prototype.hasXDR=function(){return"undefined"!==typeof global.XDomainRequest&&!this.xs&&this.enablesXDR};Request.prototype.abort=function(){this.cleanup()};if(global.document){Request.requestsCount=0;Request.requests={};if(global.attachEvent){global.attachEvent("onunload",unloadHandler)}else if(global.addEventListener){global.addEventListener("beforeunload",unloadHandler)}}function unloadHandler(){for(var i in Request.requests){if(Request.requests.hasOwnProperty(i)){Request.requests[i].abort()}}}}).call(this,typeof self!=="undefined"?self:typeof window!=="undefined"?window:{})},{"./polling":17,"component-emitter":8,"component-inherit":20,debug:9,xmlhttprequest:19}],17:[function(_dereq_,module,exports){var Transport=_dereq_("../transport");var parseqs=_dereq_("parseqs");var parser=_dereq_("engine.io-parser");var inherit=_dereq_("component-inherit");var debug=_dereq_("debug")("engine.io-client:polling");module.exports=Polling;var hasXHR2=function(){var XMLHttpRequest=_dereq_("xmlhttprequest");var xhr=new XMLHttpRequest({xdomain:false});return null!=xhr.responseType}();function Polling(opts){var forceBase64=opts&&opts.forceBase64;if(!hasXHR2||forceBase64){this.supportsBinary=false}Transport.call(this,opts)}inherit(Polling,Transport);Polling.prototype.name="polling";Polling.prototype.doOpen=function(){this.poll()};Polling.prototype.pause=function(onPause){var pending=0;var self=this;this.readyState="pausing";function pause(){debug("paused");self.readyState="paused";onPause()}if(this.polling||!this.writable){var total=0;if(this.polling){debug("we are currently polling - waiting to pause");total++;this.once("pollComplete",function(){debug("pre-pause polling complete");--total||pause()})}if(!this.writable){debug("we are currently writing - waiting to pause");total++;this.once("drain",function(){debug("pre-pause writing complete");--total||pause()})}}else{pause()}};Polling.prototype.poll=function(){debug("polling");this.polling=true;this.doPoll();this.emit("poll")};Polling.prototype.onData=function(data){var self=this;debug("polling got data %s",data);var callback=function(packet,index,total){if("opening"==self.readyState){self.onOpen()}if("close"==packet.type){self.onClose();return false}self.onPacket(packet)};parser.decodePayload(data,this.socket.binaryType,callback);if("closed"!=this.readyState){this.polling=false;this.emit("pollComplete");if("open"==this.readyState){this.poll()}else{debug('ignoring poll - transport state "%s"',this.readyState)}}};Polling.prototype.doClose=function(){var self=this;function close(){debug("writing close packet");self.write([{type:"close"}])}if("open"==this.readyState){debug("transport open - closing");close()}else{debug("transport not open - deferring close");this.once("open",close)}};Polling.prototype.write=function(packets){var self=this;this.writable=false;var callbackfn=function(){self.writable=true;self.emit("drain")};var self=this;parser.encodePayload(packets,this.supportsBinary,function(data){self.doWrite(data,callbackfn)})};Polling.prototype.uri=function(){var query=this.query||{};var schema=this.secure?"https":"http";var port="";if(false!==this.timestampRequests){query[this.timestampParam]=+new Date+"-"+Transport.timestamps++}if(!this.supportsBinary&&!query.sid){query.b64=1}query=parseqs.encode(query);if(this.port&&("https"==schema&&this.port!=443||"http"==schema&&this.port!=80)){port=":"+this.port}if(query.length){query="?"+query}return schema+"://"+this.hostname+port+this.path+query}},{"../transport":13,"component-inherit":20,debug:9,"engine.io-parser":21,parseqs:29,xmlhttprequest:19}],18:[function(_dereq_,module,exports){var Transport=_dereq_("../transport");var parser=_dereq_("engine.io-parser");var parseqs=_dereq_("parseqs");var inherit=_dereq_("component-inherit");var debug=_dereq_("debug")("engine.io-client:websocket");var WebSocket=_dereq_("ws");module.exports=WS;function WS(opts){var forceBase64=opts&&opts.forceBase64;if(forceBase64){this.supportsBinary=false}Transport.call(this,opts)}inherit(WS,Transport);WS.prototype.name="websocket";WS.prototype.supportsBinary=true;WS.prototype.doOpen=function(){if(!this.check()){return}var self=this;var uri=this.uri();var protocols=void 0;var opts={agent:this.agent};this.ws=new WebSocket(uri,protocols,opts);if(this.ws.binaryType===undefined){this.supportsBinary=false}this.ws.binaryType="arraybuffer";this.addEventListeners()};WS.prototype.addEventListeners=function(){var self=this;this.ws.onopen=function(){self.onOpen()};this.ws.onclose=function(){self.onClose()};this.ws.onmessage=function(ev){self.onData(ev.data)};this.ws.onerror=function(e){self.onError("websocket error",e)}};if("undefined"!=typeof navigator&&/iPad|iPhone|iPod/i.test(navigator.userAgent)){WS.prototype.onData=function(data){var self=this;setTimeout(function(){Transport.prototype.onData.call(self,data)},0)}}WS.prototype.write=function(packets){var self=this;this.writable=false;for(var i=0,l=packets.length;i1){return{type:packetslist[type],data:data.substring(1)}}else{return{type:packetslist[type]}}}var asArray=new Uint8Array(data);var type=asArray[0];var rest=sliceBuffer(data,1);if(Blob&&binaryType==="blob"){rest=new Blob([rest])}return{type:packetslist[type],data:rest}};exports.decodeBase64Packet=function(msg,binaryType){var type=packetslist[msg.charAt(0)];if(!global.ArrayBuffer){return{type:type,data:{base64:true,data:msg.substr(1)}}}var data=base64encoder.decode(msg.substr(1));if(binaryType==="blob"&&Blob){data=new Blob([data])}return{type:type,data:data}};exports.encodePayload=function(packets,supportsBinary,callback){if(typeof supportsBinary=="function"){callback=supportsBinary;supportsBinary=null}if(supportsBinary){if(Blob&&!isAndroid){return exports.encodePayloadAsBlob(packets,callback)}return exports.encodePayloadAsArrayBuffer(packets,callback)}if(!packets.length){return callback("0:")}function setLengthHeader(message){return message.length+":"+message}function encodeOne(packet,doneCallback){exports.encodePacket(packet,supportsBinary,true,function(message){doneCallback(null,setLengthHeader(message))})}map(packets,encodeOne,function(err,results){return callback(results.join(""))})};function map(ary,each,done){var result=new Array(ary.length);var next=after(ary.length,done);var eachWithIndex=function(i,el,cb){each(el,function(error,msg){result[i]=msg;cb(error,result)})};for(var i=0;i0){var tailArray=new Uint8Array(bufferTail);var isString=tailArray[0]===0;var msgLength="";for(var i=1;;i++){if(tailArray[i]==255)break;if(msgLength.length>310){numberTooLong=true;break}msgLength+=tailArray[i]}if(numberTooLong)return callback(err,0,1);bufferTail=sliceBuffer(bufferTail,2+msgLength.length);msgLength=parseInt(msgLength);var msg=sliceBuffer(bufferTail,0,msgLength);if(isString){try{msg=String.fromCharCode.apply(null,new Uint8Array(msg))}catch(e){var typed=new Uint8Array(msg);msg="";for(var i=0;ibytes){end=bytes}if(start>=bytes||start>=end||bytes===0){return new ArrayBuffer(0)}var abv=new Uint8Array(arraybuffer);var result=new Uint8Array(end-start);for(var i=start,ii=0;i>2];base64+=chars[(bytes[i]&3)<<4|bytes[i+1]>>4];base64+=chars[(bytes[i+1]&15)<<2|bytes[i+2]>>6];base64+=chars[bytes[i+2]&63]}if(len%3===2){base64=base64.substring(0,base64.length-1)+"="}else if(len%3===1){base64=base64.substring(0,base64.length-2)+"=="}return base64};exports.decode=function(base64){var bufferLength=base64.length*.75,len=base64.length,i,p=0,encoded1,encoded2,encoded3,encoded4;if(base64[base64.length-1]==="="){bufferLength--;if(base64[base64.length-2]==="="){bufferLength--}}var arraybuffer=new ArrayBuffer(bufferLength),bytes=new Uint8Array(arraybuffer);for(i=0;i>4;bytes[p++]=(encoded2&15)<<4|encoded3>>2;bytes[p++]=(encoded3&3)<<6|encoded4&63}return arraybuffer}})("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/")},{}],26:[function(_dereq_,module,exports){(function(global){var BlobBuilder=global.BlobBuilder||global.WebKitBlobBuilder||global.MSBlobBuilder||global.MozBlobBuilder;var blobSupported=function(){try{var b=new Blob(["hi"]);return b.size==2}catch(e){return false}}();var blobBuilderSupported=BlobBuilder&&BlobBuilder.prototype.append&&BlobBuilder.prototype.getBlob;function BlobBuilderConstructor(ary,options){options=options||{};var bb=new BlobBuilder;for(var i=0;i=55296&&value<=56319&&counter65535){value-=65536;output+=stringFromCharCode(value>>>10&1023|55296);value=56320|value&1023}output+=stringFromCharCode(value)}return output}function createByte(codePoint,shift){return stringFromCharCode(codePoint>>shift&63|128)}function encodeCodePoint(codePoint){if((codePoint&4294967168)==0){return stringFromCharCode(codePoint)}var symbol="";if((codePoint&4294965248)==0){symbol=stringFromCharCode(codePoint>>6&31|192)}else if((codePoint&4294901760)==0){symbol=stringFromCharCode(codePoint>>12&15|224);symbol+=createByte(codePoint,6)}else if((codePoint&4292870144)==0){symbol=stringFromCharCode(codePoint>>18&7|240);symbol+=createByte(codePoint,12);symbol+=createByte(codePoint,6)}symbol+=stringFromCharCode(codePoint&63|128);return symbol}function utf8encode(string){var codePoints=ucs2decode(string);var length=codePoints.length;var index=-1;var codePoint;var byteString="";while(++index=byteCount){throw Error("Invalid byte index")}var continuationByte=byteArray[byteIndex]&255;byteIndex++;if((continuationByte&192)==128){return continuationByte&63}throw Error("Invalid continuation byte")}function decodeSymbol(){var byte1;var byte2;var byte3;var byte4;var codePoint;if(byteIndex>byteCount){throw Error("Invalid byte index")}if(byteIndex==byteCount){return false}byte1=byteArray[byteIndex]&255;byteIndex++;if((byte1&128)==0){return byte1}if((byte1&224)==192){var byte2=readContinuationByte();codePoint=(byte1&31)<<6|byte2;if(codePoint>=128){return codePoint}else{throw Error("Invalid continuation byte")}}if((byte1&240)==224){byte2=readContinuationByte();byte3=readContinuationByte();codePoint=(byte1&15)<<12|byte2<<6|byte3;if(codePoint>=2048){return codePoint}else{throw Error("Invalid continuation byte")}}if((byte1&248)==240){byte2=readContinuationByte();byte3=readContinuationByte();byte4=readContinuationByte();codePoint=(byte1&15)<<18|byte2<<12|byte3<<6|byte4;if(codePoint>=65536&&codePoint<=1114111){return codePoint}}throw Error("Invalid UTF-8 detected")}var byteArray;var byteCount;var byteIndex;function utf8decode(byteString){byteArray=ucs2decode(byteString);byteCount=byteArray.length;byteIndex=0;var codePoints=[];var tmp;while((tmp=decodeSymbol())!==false){codePoints.push(tmp)}return ucs2encode(codePoints)}var utf8={version:"2.0.0",encode:utf8encode,decode:utf8decode};if(typeof define=="function"&&typeof define.amd=="object"&&define.amd){define(function(){return utf8})}else if(freeExports&&!freeExports.nodeType){if(freeModule){freeModule.exports=utf8}else{var object={};var hasOwnProperty=object.hasOwnProperty;for(var key in utf8){hasOwnProperty.call(utf8,key)&&(freeExports[key]=utf8[key])}}}else{root.utf8=utf8}})(this)}).call(this,typeof self!=="undefined"?self:typeof window!=="undefined"?window:{})},{}],28:[function(_dereq_,module,exports){(function(global){var rvalidchars=/^[\],:{}\s]*$/;var rvalidescape=/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g;var rvalidtokens=/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g;var rvalidbraces=/(?:^|:|,)(?:\s*\[)+/g;var rtrimLeft=/^\s+/;var rtrimRight=/\s+$/;module.exports=function parsejson(data){if("string"!=typeof data||!data){return null}data=data.replace(rtrimLeft,"").replace(rtrimRight,"");if(global.JSON&&JSON.parse){return JSON.parse(data)}if(rvalidchars.test(data.replace(rvalidescape,"@").replace(rvalidtokens,"]").replace(rvalidbraces,""))){return new Function("return "+data)()}}}).call(this,typeof self!=="undefined"?self:typeof window!=="undefined"?window:{})},{}],29:[function(_dereq_,module,exports){exports.encode=function(obj){var str="";for(var i in obj){if(obj.hasOwnProperty(i)){if(str.length)str+="&";str+=encodeURIComponent(i)+"="+encodeURIComponent(obj[i])}}return str};exports.decode=function(qs){var qry={};var pairs=qs.split("&");for(var i=0,l=pairs.length;i1)))/4)-floor((year-1901+month)/100)+floor((year-1601+month)/400)}}if(!(isProperty={}.hasOwnProperty)){isProperty=function(property){var members={},constructor;if((members.__proto__=null,members.__proto__={toString:1},members).toString!=getClass){isProperty=function(property){var original=this.__proto__,result=property in(this.__proto__=null,this);this.__proto__=original;return result}}else{constructor=members.constructor;isProperty=function(property){var parent=(this.constructor||constructor).prototype;return property in this&&!(property in parent&&this[property]===parent[property])}}members=null;return isProperty.call(this,property)}}var PrimitiveTypes={"boolean":1,number:1,string:1,undefined:1};var isHostType=function(object,property){var type=typeof object[property];return type=="object"?!!object[property]:!PrimitiveTypes[type]};forEach=function(object,callback){var size=0,Properties,members,property;(Properties=function(){this.valueOf=0}).prototype.valueOf=0;members=new Properties;for(property in members){if(isProperty.call(members,property)){size++}}Properties=members=null;if(!size){members=["valueOf","toString","toLocaleString","propertyIsEnumerable","isPrototypeOf","hasOwnProperty","constructor"];forEach=function(object,callback){var isFunction=getClass.call(object)==functionClass,property,length;var hasProperty=!isFunction&&typeof object.constructor!="function"&&isHostType(object,"hasOwnProperty")?object.hasOwnProperty:isProperty;for(property in object){if(!(isFunction&&property=="prototype")&&hasProperty.call(object,property)){callback(property)}}for(length=members.length;property=members[--length];hasProperty.call(object,property)&&callback(property));}}else if(size==2){forEach=function(object,callback){var members={},isFunction=getClass.call(object)==functionClass,property;for(property in object){if(!(isFunction&&property=="prototype")&&!isProperty.call(members,property)&&(members[property]=1)&&isProperty.call(object,property)){callback(property)}}}}else{forEach=function(object,callback){var isFunction=getClass.call(object)==functionClass,property,isConstructor;for(property in object){if(!(isFunction&&property=="prototype")&&isProperty.call(object,property)&&!(isConstructor=property==="constructor")){callback(property)}}if(isConstructor||isProperty.call(object,property="constructor")){callback(property)}}}return forEach(object,callback)};if(!has("json-stringify")){var Escapes={92:"\\\\",34:'\\"',8:"\\b",12:"\\f",10:"\\n",13:"\\r",9:"\\t"};var leadingZeroes="000000";var toPaddedString=function(width,value){return(leadingZeroes+(value||0)).slice(-width)};var unicodePrefix="\\u00";var quote=function(value){var result='"',index=0,length=value.length,isLarge=length>10&&charIndexBuggy,symbols;if(isLarge){symbols=value.split("")}for(;index-1/0&&value<1/0){if(getDay){date=floor(value/864e5);for(year=floor(date/365.2425)+1970-1;getDay(year+1,0)<=date;year++);for(month=floor((date-getDay(year,0))/30.42);getDay(year,month+1)<=date;month++);date=1+date-getDay(year,month);time=(value%864e5+864e5)%864e5;hours=floor(time/36e5)%24;minutes=floor(time/6e4)%60;seconds=floor(time/1e3)%60;milliseconds=time%1e3}else{year=value.getUTCFullYear();month=value.getUTCMonth();date=value.getUTCDate();hours=value.getUTCHours();minutes=value.getUTCMinutes();seconds=value.getUTCSeconds();milliseconds=value.getUTCMilliseconds()}value=(year<=0||year>=1e4?(year<0?"-":"+")+toPaddedString(6,year<0?-year:year):toPaddedString(4,year))+"-"+toPaddedString(2,month+1)+"-"+toPaddedString(2,date)+"T"+toPaddedString(2,hours)+":"+toPaddedString(2,minutes)+":"+toPaddedString(2,seconds)+"."+toPaddedString(3,milliseconds)+"Z"}else{value=null}}else if(typeof value.toJSON=="function"&&(className!=numberClass&&className!=stringClass&&className!=arrayClass||isProperty.call(value,"toJSON"))){value=value.toJSON(property)}}if(callback){value=callback.call(object,property,value)}if(value===null){return"null"}className=getClass.call(value);if(className==booleanClass){return""+value}else if(className==numberClass){return value>-1/0&&value<1/0?""+value:"null"}else if(className==stringClass){return quote(""+value)}if(typeof value=="object"){for(length=stack.length;length--;){if(stack[length]===value){throw TypeError()}}stack.push(value);results=[];prefix=indentation;indentation+=whitespace;if(className==arrayClass){for(index=0,length=value.length;index0){for(whitespace="",width>10&&(width=10);whitespace.length=48&&charCode<=57||charCode>=97&&charCode<=102||charCode>=65&&charCode<=70)){abort()}}value+=fromCharCode("0x"+source.slice(begin,Index));break;default:abort()}}else{if(charCode==34){break}charCode=source.charCodeAt(Index);begin=Index;while(charCode>=32&&charCode!=92&&charCode!=34){charCode=source.charCodeAt(++Index)}value+=source.slice(begin,Index)}}if(source.charCodeAt(Index)==34){Index++;return value}abort();default:begin=Index;if(charCode==45){isSigned=true;charCode=source.charCodeAt(++Index)}if(charCode>=48&&charCode<=57){if(charCode==48&&(charCode=source.charCodeAt(Index+1),charCode>=48&&charCode<=57)){abort()}isSigned=false;for(;Index=48&&charCode<=57);Index++);if(source.charCodeAt(Index)==46){position=++Index;for(;position=48&&charCode<=57);position++);if(position==Index){abort()}Index=position}charCode=source.charCodeAt(Index);if(charCode==101||charCode==69){charCode=source.charCodeAt(++Index);if(charCode==43||charCode==45){Index++}for(position=Index;position=48&&charCode<=57);position++);if(position==Index){abort()}Index=position}return+source.slice(begin,Index)}if(isSigned){abort()}if(source.slice(Index,Index+4)=="true"){Index+=4;return true}else if(source.slice(Index,Index+5)=="false"){Index+=5;return false}else if(source.slice(Index,Index+4)=="null"){Index+=4;return null}abort()}}return"$"};var get=function(value){var results,hasMembers;if(value=="$"){abort()}if(typeof value=="string"){if((charIndexBuggy?value.charAt(0):value[0])=="@"){return value.slice(1)}if(value=="["){results=[];for(;;hasMembers||(hasMembers=true)){value=lex();if(value=="]"){break}if(hasMembers){if(value==","){value=lex();if(value=="]"){abort()}}else{abort()}}if(value==","){abort()}results.push(get(value))}return results}else if(value=="{"){results={};for(;;hasMembers||(hasMembers=true)){value=lex();if(value=="}"){break}if(hasMembers){if(value==","){value=lex();if(value=="}"){abort()}}else{abort()}}if(value==","||typeof value!="string"||(charIndexBuggy?value.charAt(0):value[0])!="@"||lex()!=":"){abort()}results[value.slice(1)]=get(lex())}return results}abort()}return value};var update=function(source,property,callback){var element=walk(source,property,callback);if(element===undef){delete source[property]}else{source[property]=element}};var walk=function(source,property,callback){var value=source[property],length;if(typeof value=="object"&&value){if(getClass.call(value)==arrayClass){for(length=value.length;length--;){update(value,length,callback)}}else{forEach(value,function(property){update(value,property,callback)})}}return callback.call(source,property,value)};JSON3.parse=function(source,callback){var result,value;Index=0;Source=""+source;result=get(lex());if(lex()!="$"){abort()}Index=Source=null;return callback&&getClass.call(callback)==functionClass?walk((value={},value[""]=result,value),"",callback):result}}}if(isLoader){define(function(){return JSON3})}})(this)},{}],44:[function(_dereq_,module,exports){module.exports=toArray;function toArray(list,index){var array=[];index=index||0;for(var i=index||0;i@wikipedia.de 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy 5 | * of this software and associated documentation files (the "Software"), to 6 | * deal in the Software without restriction, including without limitation the 7 | * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 8 | * sell copies of the Software, and to permit persons to whom the Software is 9 | * furnished to do so, subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in 12 | * all copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 19 | * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 20 | * IN THE SOFTWARE. 21 | */ 22 | 23 | /*global self: false, Module:false */ 24 | /*jslint vars: false, white: false */ 25 | /*jshint onevar: false, white: false, laxbreak: true */ 26 | ( function( global ) { 27 | 'use strict'; 28 | 29 | /** 30 | * Emscripten C function arguments utitlites 31 | * @class EmsArgs 32 | * @singleton 33 | */ 34 | global.EmsArgs = { 35 | /** 36 | * Read a Blob as ArrayBuffer and invoke the supplied callback when completed 37 | * 38 | * @param {Blob} blob Blob to be read 39 | * @param {Function} cb Callback invoked upon completion of the 40 | * operation 41 | * @param {ArrayBuffer} cb.data ArrayBuffer containing a copy of the Blob data 42 | */ 43 | readBlobAsArrayBuffer: function readBlobAsArrayBuffer( blob, cb ) { 44 | var frs, fr; 45 | 46 | if ( global.FileReaderSync ) { 47 | frs = new FileReaderSync(); 48 | // Ensure ASYNC callback 49 | // TODO: Investigate why application crashes (exit(1)) 50 | // when using synchroneous callback here 51 | setTimeout( function() { 52 | cb( frs.readAsArrayBuffer( blob ) ); 53 | }, 5 ); 54 | return; 55 | } 56 | 57 | fr = new FileReader(); 58 | fr.addEventListener( 'loadend', function() { 59 | cb( fr.result ); 60 | } ); 61 | fr.readAsArrayBuffer( blob ); 62 | }, 63 | 64 | 65 | /** 66 | * @param {string} s String to be UTF-8 encoded as buffer 67 | * @param {Function} cb Callback when string has been converted 68 | * @param {number} cb.sPointer Pointer to the string 69 | */ 70 | getBufferFor: function( s, cb ) { 71 | var b = new Blob( [ s + '\0' ] ); 72 | global.EmsArgs.readBlobAsArrayBuffer( b, function( res ) { 73 | var arg = new Uint8Array( res ); 74 | var dataPtr = Module._malloc( arg.length * arg.BYTES_PER_ELEMENT ); 75 | 76 | // Copy data to Emscripten heap 77 | var dataHeap = new Uint8Array( Module.HEAPU8.buffer, dataPtr, arg.length * arg.BYTES_PER_ELEMENT ); 78 | dataHeap.set( arg ); 79 | cb( dataPtr ); 80 | } ); 81 | }, 82 | 83 | /** 84 | * @param {Array} args Arguments to be passed to the C function. 85 | * @param {Function} cb Callback when arguments were converted 86 | * @param {number} cb.argsPointer A pointer to the the Array of Pointers of arguments 87 | * in the Emscripten Heap (**argv) 88 | */ 89 | cArgsPointer: function( args, cb ) { 90 | var pointers = new Uint32Array( args.length ), 91 | nextArg, processedArg; 92 | 93 | args = args.slice( 0 ); 94 | 95 | processedArg = function( sPointer ) { 96 | pointers[ pointers.length - args.length - 1 ] = sPointer; 97 | nextArg(); 98 | }; 99 | 100 | nextArg = function() { 101 | if ( args.length ) { 102 | global.EmsArgs.getBufferFor( args.shift(), processedArg ); 103 | } else { 104 | var nPointerBytes = pointers.length * pointers.BYTES_PER_ELEMENT; 105 | var pointerPtr = Module._malloc( nPointerBytes ); 106 | var pointerHeap = new Uint8Array( Module.HEAPU8.buffer, pointerPtr, nPointerBytes ); 107 | pointerHeap.set( new Uint8Array( pointers.buffer ) ); 108 | cb( pointerHeap ); 109 | } 110 | }; 111 | nextArg(); 112 | } 113 | }; 114 | 115 | }( self ) ); 116 | -------------------------------------------------------------------------------- /client/workers/EmsWorkerProxy.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright © 2014 Rainer Rillke @wikipedia.de 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy 5 | * of this software and associated documentation files (the "Software"), to 6 | * deal in the Software without restriction, including without limitation the 7 | * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 8 | * sell copies of the Software, and to permit persons to whom the Software is 9 | * furnished to do so, subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in 12 | * all copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 19 | * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 20 | * IN THE SOFTWARE. 21 | */ 22 | 23 | /*global self: false, OpusEncoder: false, importScripts: false */ 24 | /*jslint vars: false, white: false */ 25 | /*jshint onevar: false, white: false, laxbreak: true */ 26 | ( function( global ) { 27 | 'use strict'; 28 | 29 | /** 30 | * Worker proxy implementing communication between worker and website 31 | * @class EmsWorkerProxy 32 | * @singleton 33 | */ 34 | global.EmsWorkerProxy = { 35 | init: function() { 36 | global.onmessage = function( e ) { 37 | switch ( e.data.command ) { 38 | case 'ping': 39 | global.postMessage( { reply: 'pong' } ); 40 | break; 41 | case 'encode': 42 | if ( !global.OpusEncoder ) { 43 | importScripts( 'OpusEncoder.js' ); 44 | } 45 | OpusEncoder.encode( e.data ); 46 | break; 47 | case 'prefetch': 48 | if ( !global.OpusEncoder ) { 49 | importScripts( 'OpusEncoder.js' ); 50 | OpusEncoder.prefetch( e.data ); 51 | } 52 | break; 53 | } 54 | }; 55 | } 56 | }; 57 | global.EmsWorkerProxy.init(); 58 | 59 | }( self ) ); 60 | -------------------------------------------------------------------------------- /client/workers/OpusEncoder.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright © 2014 Rainer Rillke @wikipedia.de 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy 5 | * of this software and associated documentation files (the "Software"), to 6 | * deal in the Software without restriction, including without limitation the 7 | * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 8 | * sell copies of the Software, and to permit persons to whom the Software is 9 | * furnished to do so, subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in 12 | * all copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 19 | * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 20 | * IN THE SOFTWARE. 21 | */ 22 | 23 | /*global self: false, Runtime: false, OpusEncoder: false, Module: false, FS: false, EmsArgs: false, console: false */ 24 | /*jslint vars: false, white: false */ 25 | /*jshint onevar: false, white: false, laxbreak: true, worker: true, strict: false */ 26 | 27 | ( function( global ) { 28 | 'use strict'; 29 | var MainScriptLoader, 30 | downloadCompleted, 31 | downloadError; 32 | 33 | /** 34 | * Manages encoding and progress notifications 35 | * @class OpusEncoder 36 | * @singleton 37 | */ 38 | global.OpusEncoder = { 39 | encode: function( data ) { 40 | OpusEncoder.setUpLogging( data ); 41 | OpusEncoder.monitorWWDownload(); 42 | MainScriptLoader.downloadAndExecute( data, function() { 43 | // Before execute main script ... 44 | if ( !global.EmsArgs ){ 45 | importScripts( 'EmsArgs.js' ); 46 | } 47 | OpusEncoder.setUpModule( data ); 48 | }, function() { 49 | // After the main script was executed ... 50 | MainScriptLoader.whenInitialized( function() { 51 | OpusEncoder._encode( data ); 52 | } ); 53 | } ); 54 | }, 55 | 56 | prefetch: function( data ) { 57 | if ( global.Module && global.EmsArgs && global.Runtime ) { 58 | return; 59 | } 60 | OpusEncoder.setUpLogging( data ); 61 | MainScriptLoader.xhrload( data ); 62 | importScripts( 'EmsArgs.js' ); 63 | }, 64 | 65 | setUpLogging: function( data ) { 66 | if ( !global.console ) global.console = {}; 67 | console.log = function() { 68 | ( data.log || OpusEncoder.log )( 69 | Array.prototype.slice.call( arguments ) 70 | ); 71 | }; 72 | 73 | console.error = function() { 74 | ( data.err || OpusEncoder.err )( 75 | Array.prototype.slice.call( arguments ) 76 | ); 77 | }; 78 | }, 79 | 80 | monitorWWDownload: function() { 81 | var lastLog = 0; 82 | 83 | MainScriptLoader.onDownloadComplete = function() { 84 | console.log( 'Worker downloaded successfully.' ); 85 | }; 86 | 87 | MainScriptLoader.onDownloadProgress = function( loaded, total ) { 88 | var now = Date.now(), 89 | diff = now - lastLog; 90 | 91 | if ( diff > 450 ) { 92 | lastLog = now; 93 | console.log( 'Downloading Opus Encoder code ... ' + Math.round( 100 * loaded / total, 2) + '%' ); 94 | } 95 | }; 96 | MainScriptLoader.onDownloadError = function( err ) { 97 | console.log( 'Failed to download worker utilizing XHR.\n' + err + '\nTrying importScripts() ...' ); 98 | }; 99 | }, 100 | 101 | setUpModule: function( data ) { 102 | /*jshint forin:false */ 103 | var totalFileLength = 0, 104 | filename, memRequired; 105 | 106 | for ( filename in data.fileData ) { 107 | if ( !data.fileData.hasOwnProperty( filename ) ) { 108 | return; 109 | } 110 | var fileData = data.fileData[filename]; 111 | 112 | totalFileLength += fileData.length; 113 | } 114 | 115 | memRequired = totalFileLength * 2 + 0x1000000; 116 | // Currently "The asm.js rules specify that the heap size must be 117 | // a multiple of 16MB or a power of two. Minimum heap size is 64KB" 118 | // If we don't correct it here asm will dump errors on us while adjusting 119 | // the number but we would ignore following the error and shut down the 120 | // worker due to this error 121 | memRequired = memRequired - ( memRequired % 0x1000000 ) + 0x1000000; 122 | 123 | global.Module = { 124 | TOTAL_MEMORY: memRequired, 125 | _main: MainScriptLoader.initialized, 126 | noExitRuntime: true, 127 | preRun: OpusEncoder.setUpFilesystem, 128 | printErr: console.error.bind( console ), 129 | monitorRunDependencies: function( runDeps ) { 130 | console.log( 'Loading run dependencies. Outstanding: ' + runDeps ); 131 | }, 132 | locateFile: function( memFile ) { 133 | return memFile.replace( /^opusenc\.(html|js)\.mem$/, 'opusenc.data.js' ); 134 | } 135 | }; 136 | }, 137 | 138 | setUpFilesystem: function() { 139 | var infoBuff = '', 140 | errBuff = '', 141 | lastInfoFlush = Date.now(), 142 | lastErrFlush = Date.now(), 143 | infoTimeout, errTimeout, flushInfo, flushErr; 144 | 145 | OpusEncoder.flushInfo = flushInfo = function() { 146 | clearTimeout( infoTimeout ); 147 | lastInfoFlush = Date.now(); 148 | if ( infoBuff.replace( /\s*/g, '' ) ) { 149 | console.log( infoBuff ); 150 | infoBuff = ''; 151 | } 152 | }; 153 | OpusEncoder.flushErr = flushErr = function() { 154 | clearTimeout( errTimeout ); 155 | lastErrFlush = Date.now(); 156 | if ( errBuff.replace( /\s*/g, '' ) ) { 157 | console.log( errBuff ); 158 | errBuff = ''; 159 | } 160 | }; 161 | 162 | FS.init( global.prompt || function() { 163 | console.log( 'Input requested from within web worker. Returning empty string.' ); 164 | return ''; 165 | }, function( infoChar ) { 166 | infoBuff += String.fromCharCode( infoChar ); 167 | clearTimeout( infoTimeout ); 168 | infoTimeout = setTimeout( flushInfo, 5 ); 169 | if ( lastInfoFlush + 700 < Date.now() ) { 170 | flushInfo(); 171 | } 172 | }, function( errChar ) { 173 | errBuff += String.fromCharCode( errChar ); 174 | clearTimeout( errTimeout ); 175 | errTimeout = setTimeout( flushErr, 5 ); 176 | if ( lastErrFlush + 700 < Date.now() ) { 177 | flushErr(); 178 | } 179 | } ); 180 | }, 181 | 182 | done: function( args ) { 183 | global.postMessage( { 184 | reply: 'done', 185 | values: args 186 | } ); 187 | }, 188 | 189 | progress: function( args ) { 190 | global.postMessage( { 191 | reply: 'progress', 192 | values: args 193 | } ); 194 | }, 195 | 196 | log: function( args ) { 197 | global.postMessage( { 198 | reply: 'log', 199 | values: args 200 | } ); 201 | }, 202 | 203 | err: function( args ) { 204 | global.postMessage( { 205 | reply: 'err', 206 | values: args 207 | } ); 208 | }, 209 | 210 | _encode: function( data ) { 211 | /*jshint forin:false */ 212 | var fPointer; 213 | 214 | // Get a pointer for the callback function 215 | fPointer = Runtime.addFunction( function( encoded, total, seconds ) { 216 | var filename, fileContent, b; 217 | 218 | // We *know* that writing to to stdin and/or stderr completed 219 | OpusEncoder.flushInfo(); 220 | OpusEncoder.flushErr(); 221 | 222 | if ( encoded === total && encoded === 100 && seconds === -1 ) { 223 | // Read output files 224 | for ( filename in data.outData ) { 225 | if ( !data.outData.hasOwnProperty( filename ) ) { 226 | return; 227 | } 228 | fileContent = FS.readFile( filename, { 229 | encoding: 'binary' 230 | } ); 231 | b = new Blob( 232 | [fileContent], 233 | {type: data.outData[filename].MIME} 234 | ); 235 | data.outData[filename].blob = b; 236 | } 237 | (data.done || OpusEncoder.done)( data.outData ); 238 | } else { 239 | (data.progress || OpusEncoder.progress)( 240 | Array.prototype.slice.call( arguments ) 241 | ); 242 | } 243 | } ); 244 | 245 | // Set module arguments (command line arguments) 246 | var args = data.args, 247 | argsCloned = args.slice( 0 ); 248 | 249 | args.unshift( 'opusenc.js' ); 250 | Module['arguments'] = argsCloned; 251 | 252 | // Create all neccessary files in MEMFS or whatever 253 | // the mounted file system is 254 | var filename; 255 | 256 | for ( filename in data.fileData ) { 257 | if ( !data.fileData.hasOwnProperty( filename ) ) { 258 | return; 259 | } 260 | var fileData = data.fileData[filename], 261 | stream = FS.open( filename, 'w+' ); 262 | 263 | FS.write( stream, fileData, 0, fileData.length ); 264 | FS.close( stream ); 265 | } 266 | 267 | // Create output files 268 | for ( filename in data.outData ) { 269 | if ( !data.outData.hasOwnProperty( filename ) ) { 270 | return; 271 | } 272 | FS.close( FS.open( filename, 'w+' ) ); 273 | } 274 | 275 | // Prepare C function to be called 276 | var encode_buffer = Module.cwrap( 'encode_buffer', 'number', ['number', 'number', 'number'] ); 277 | 278 | // Copy command line args to Emscripten Heap and get a pointer to them 279 | EmsArgs.cArgsPointer( args, function( pointerHeap ) { 280 | try { 281 | global.Module.noExitRuntime = false; 282 | encode_buffer( args.length, pointerHeap.byteOffset, fPointer ); 283 | } catch ( ex ) { 284 | console.error( ex.message || ex ); 285 | } 286 | } ); 287 | } 288 | }; 289 | 290 | /** 291 | * Downloads main script 292 | * @class MainScriptLoader 293 | * @singleton 294 | * @private 295 | */ 296 | MainScriptLoader = { 297 | name: 'opusenc.js', 298 | text: null, 299 | status: 'idle', 300 | xhrload: function( data, complete, err ) { 301 | var xhrfailed = function( errMsg ) { 302 | if ( MainScriptLoader.status !== 'loading' ) { 303 | return; 304 | } 305 | MainScriptLoader.status = 'xhrfailed'; 306 | MainScriptLoader.onDownloadError( errMsg ); 307 | if ( err ) err(); 308 | }; 309 | 310 | if ( global.__debug ) { 311 | MainScriptLoader.status = 'loading'; 312 | return xhrfailed( 'Debug modus enabled.' ); 313 | } 314 | 315 | var xhr = new XMLHttpRequest(); 316 | xhr.onreadystatechange = function() { 317 | if ( xhr.readyState === xhr.DONE ) { 318 | if ( xhr.status === 200 || xhr.status === 0 && location.protocol === 'file:' ) { 319 | MainScriptLoader.text = xhr.responseText; 320 | MainScriptLoader.status = 'loaded'; 321 | MainScriptLoader.onDownloadComplete(); 322 | if ( complete ) complete(); 323 | if ( downloadCompleted ) downloadCompleted(); 324 | } else { 325 | xhrfailed( 'Server status ' + xhr.status ); 326 | } 327 | } 328 | }; 329 | xhr.onprogress = function( e ) { 330 | if ( e.lengthComputable ) { 331 | MainScriptLoader.onDownloadProgress( e.loaded, e.total ); 332 | } 333 | }; 334 | xhr.onerror = function() { 335 | xhrfailed( 'There was an error with the request.' ); 336 | }; 337 | xhr.ontimeout = function() { 338 | xhrfailed( 'Request timed out.' ); 339 | }; 340 | 341 | try { 342 | MainScriptLoader.status = 'loading'; 343 | xhr.open( 'GET', MainScriptLoader.name ); 344 | xhr.send( null ); 345 | } catch ( ex ) { 346 | xhrfailed( ex.message || ex ); 347 | } 348 | }, 349 | onDownloadProgress: function( /* loaded, total */ ) {}, 350 | onDownloadComplete: function() {}, 351 | onDownloadError: function( /* description */ ) {}, 352 | downloadAndExecute: function( data, beforeExecution, afterExecution ) { 353 | switch ( MainScriptLoader.status ) { 354 | case 'idle': 355 | MainScriptLoader.xhrload( data, function() { 356 | beforeExecution(); 357 | MainScriptLoader.execute(); 358 | afterExecution(); 359 | }, function() { 360 | beforeExecution(); 361 | importScripts( MainScriptLoader.name ); 362 | afterExecution(); 363 | } ); 364 | break; 365 | case 'xhrfailed': 366 | beforeExecution(); 367 | importScripts( MainScriptLoader.name ); 368 | afterExecution(); 369 | break; 370 | case 'loaded': 371 | beforeExecution(); 372 | MainScriptLoader.execute(); 373 | afterExecution(); 374 | break; 375 | case 'loading': 376 | downloadCompleted = function() { 377 | downloadCompleted = null; 378 | downloadError = null; 379 | beforeExecution(); 380 | MainScriptLoader.execute(); 381 | afterExecution(); 382 | }; 383 | downloadError = function() { 384 | beforeExecution(); 385 | importScripts( MainScriptLoader.name ); 386 | afterExecution(); 387 | }; 388 | break; 389 | } 390 | }, 391 | execute: function() { 392 | if ( !MainScriptLoader.text ) { 393 | throw new Error( 'Main script text must be loaded before!' ); 394 | } 395 | global.callEval( MainScriptLoader.text ); 396 | }, 397 | queue: [], 398 | isInitialized: false, 399 | whenInitialized: function( cb ) { 400 | if ( MainScriptLoader.isInitialized ) { 401 | cb(); 402 | } else { 403 | MainScriptLoader.queue.push( cb ); 404 | } 405 | }, 406 | initialized: function() { 407 | MainScriptLoader.isInitialized = true; 408 | while ( MainScriptLoader.queue.length ) { 409 | MainScriptLoader.queue.shift()(); 410 | } 411 | } 412 | }; 413 | 414 | }( self ) ); 415 | 416 | ( function( global ) { 417 | /* jshint evil: true */ 418 | global.callEval = function ( s ) { 419 | var Module = global.Module, 420 | ret = eval( s ); 421 | 422 | global.FS = FS; 423 | global.Module = Module; 424 | global.Runtime = Runtime; 425 | return ret; 426 | }; 427 | }( self ) ); -------------------------------------------------------------------------------- /client/workers/opusenc.data.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mido22/recordOpus/f7ae9e22b0d8157aa136f2df23aeea2882efd42f/client/workers/opusenc.data.js -------------------------------------------------------------------------------- /client/workers/recorderWorker.js: -------------------------------------------------------------------------------- 1 | var recLength 2 | , recBuffersL 3 | , recBuffersR 4 | , sampleRate 5 | , numChannels 6 | , encodingInProgress 7 | , recBuffersLTemp = [] 8 | , recBuffersRTemp = [] 9 | , recLengthTemp = 0 10 | , self = this 11 | ; 12 | 13 | this.onmessage = function(e){ 14 | switch(e.data.command){ 15 | case 'init': 16 | init(e.data.config); 17 | break; 18 | case 'record': 19 | record(e.data.buffer); 20 | break; 21 | case 'export': 22 | exportFile(e.data); 23 | break; 24 | } 25 | }; 26 | 27 | function init(config){ 28 | sampleRate = config.sampleRate; 29 | numChannels = config.numChannels || 2; 30 | clear(); 31 | } 32 | 33 | function record(inputBuffer){ 34 | if(encodingInProgress){ 35 | recBuffersLTemp.push(inputBuffer[0]); 36 | recBuffersRTemp.push(inputBuffer[1]); 37 | recLengthTemp += inputBuffer[0].length; 38 | }else{ 39 | recBuffersL.push(inputBuffer[0]); 40 | recBuffersR.push(inputBuffer[1]); 41 | recLength += inputBuffer[0].length; 42 | bufferLength=inputBuffer[0].length; 43 | } 44 | } 45 | 46 | function exportFile(data){ 47 | data = data || {}; 48 | encodingInProgress=true; 49 | var buffers = [mergeBuffers(recBuffersL, recLength), 50 | mergeBuffers(recBuffersR, recLength)] 51 | , interleaved; 52 | if (numChannels === 2){ 53 | interleaved = interleave(buffers[0], buffers[1]); 54 | } else { 55 | interleaved = buffers[0]; 56 | } 57 | data.blob = new Blob([encodeWAV(interleaved)], { type: 'audio/wav' }); 58 | this.postMessage(data); 59 | clear(); 60 | } 61 | 62 | function clear(){ 63 | recLength = recLengthTemp; 64 | recBuffersL = recBuffersLTemp; 65 | recBuffersR = recBuffersRTemp; 66 | recLengthTemp = 0; 67 | recBuffersLTemp = []; 68 | recBuffersRTemp = []; 69 | encodingInProgress=false; 70 | } 71 | 72 | function mergeBuffers(recBuffers, recLength){ 73 | var result = new Float32Array(recLength); 74 | var offset = 0; 75 | for (var i = 0; i < recBuffers.length; i++){ 76 | result.set(recBuffers[i], offset); 77 | offset += recBuffers[i].length; 78 | } 79 | return result; 80 | } 81 | 82 | function interleave(inputL, inputR){ 83 | var length = inputL.length + inputR.length; 84 | var result = new Float32Array(length); 85 | 86 | var index = 0, 87 | inputIndex = 0; 88 | 89 | while (index < length){ 90 | result[index++] = inputL[inputIndex]; 91 | result[index++] = inputR[inputIndex]; 92 | inputIndex++; 93 | } 94 | return result; 95 | } 96 | 97 | function floatTo16BitPCM(output, offset, input){ 98 | for (var i = 0; i < input.length; i++, offset+=2){ 99 | var s = Math.max(-1, Math.min(1, input[i])); 100 | output.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7FFF, true); 101 | } 102 | } 103 | 104 | function writeString(view, offset, string){ 105 | for (var i = 0; i < string.length; i++){ 106 | view.setUint8(offset + i, string.charCodeAt(i)); 107 | } 108 | } 109 | 110 | function encodeWAV(samples){ 111 | var buffer = new ArrayBuffer(44 + samples.length * 2); 112 | var view = new DataView(buffer); 113 | 114 | /* RIFF identifier */ 115 | writeString(view, 0, 'RIFF'); 116 | /* RIFF chunk length */ 117 | view.setUint32(4, 36 + samples.length * 2, true); 118 | /* RIFF type */ 119 | writeString(view, 8, 'WAVE'); 120 | /* format chunk identifier */ 121 | writeString(view, 12, 'fmt '); 122 | /* format chunk length */ 123 | view.setUint32(16, 16, true); 124 | /* sample format (raw) */ 125 | view.setUint16(20, 1, true); 126 | /* channel count */ 127 | view.setUint16(22, numChannels, true); 128 | /* sample rate */ 129 | view.setUint32(24, sampleRate, true); 130 | /* byte rate (sample rate * block align) */ 131 | view.setUint32(28, sampleRate * 4, true); 132 | /* block align (channel count * bytes per sample) */ 133 | view.setUint16(32, numChannels * 2, true); 134 | /* bits per sample */ 135 | view.setUint16(34, 16, true); 136 | /* data chunk identifier */ 137 | writeString(view, 36, 'data'); 138 | /* data chunk length */ 139 | view.setUint32(40, samples.length * 2, true); 140 | 141 | floatTo16BitPCM(view, 44, samples); 142 | 143 | return view; 144 | } 145 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "recordOpus", 3 | "version": "1.0.1", 4 | "description": "audio recorder using opus encoding", 5 | "scripts": { 6 | "start": "node server/app.js", 7 | "test": "echo \"One day I would write them, but not today.\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/Mido22/recordOpus.git" 12 | }, 13 | "keywords": [ 14 | "opus", 15 | "ogg", 16 | "getUserMedia", 17 | "audio-recorder", 18 | "nodejs", 19 | "javascript" 20 | ], 21 | "author": "Ban Mido", 22 | "license": "MIT", 23 | "bugs": { 24 | "url": "https://github.com/Mido22/recordOpus/issues" 25 | }, 26 | "homepage": "https://github.com/Mido22/recordOpus", 27 | "dependencies": { 28 | "express": "4.10.2", 29 | "fluent-ffmpeg": "^2.0.0-rc3", 30 | "mkdirp": "^0.5.0", 31 | "open": "0.0.5", 32 | "rimraf": "^2.3.2", 33 | "socket.io": "1.2.0" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /server/app.js: -------------------------------------------------------------------------------- 1 | var app = require('express')() 2 | , TMP_PATH = 'uploads' 3 | , open = require("open") 4 | , ffmpeg = require('fluent-ffmpeg') 5 | , rm = require('rimraf') 6 | , fs = require('fs') 7 | , mkdirp = require('mkdirp') 8 | , path = require('path') 9 | , noop = function(){} 10 | ; 11 | app.use("/", require('express').static(__dirname.replace('server', 'client'))); 12 | app.use("/workers", require('express').static(__dirname.replace('server', 'client/workers'))); 13 | app.use("/uploads", require('express').static(__dirname.replace('server', TMP_PATH))); 14 | 15 | var http = require('http').Server(app); 16 | http.listen(80, function(){ 17 | console.log('listening on 80'); 18 | open("http://localhost:80"); 19 | }); 20 | 21 | // for socket communication. 22 | var io = require('socket.io')(http); 23 | io.on('connection', function(socket){ 24 | socket.on('save', function(data){ 25 | saveAudio(data, function(response){ 26 | socket.emit('link', response); 27 | }); 28 | }); 29 | }); 30 | 31 | //function listening to save request from client, callback is called once the response is ready 32 | function saveAudio(data, callback){ 33 | if(data.recorderType === 'Fox' || !data.autoUpload){ 34 | saveFox(data, callback); 35 | }else{ 36 | saveOpusShim(data, callback); 37 | } 38 | } 39 | 40 | // function in charge of saving file for Firefox native recorder[ we just append to file, not create new file for each chunck of audio recieved] 41 | function saveFox(data, callback){ 42 | data.path = path.join(TMP_PATH, data.uid + '.ogg'); 43 | fs.appendFile( data.path, data.blob, function(err){ 44 | if(err){ 45 | console.log(err); 46 | return; 47 | } 48 | if(data.autoUpload && !data.stop) return; 49 | returnLink(data, data.path, callback); 50 | }); 51 | } 52 | 53 | // function in charge of saving file for OggShimRecorder 54 | function saveOpusShim(data, callback){ 55 | data.path = TMP_PATH+'/'+data.uid; 56 | mkDir(data.path, function(){ 57 | var fileName = 'audio_' + Math.random() + '.ogg', 58 | p = path.resolve(data.path); 59 | fs.appendFile(path.join(p,'files.txt'), "file '"+path.join(p,fileName)+"'\n", function(err){ 60 | if(err){ 61 | console.log(err); 62 | return; 63 | } 64 | fs.appendFile( path.join(p,fileName), data.blob, function(err){ 65 | if(err){ 66 | console.log(err); 67 | return; 68 | } 69 | if(data.stop){ 70 | var outFile = path.join(TMP_PATH, data.uid + '.ogg'), 71 | inFile = path.join(p,'files.txt'); 72 | concat(inFile, outFile, function(err, filepath){ 73 | 74 | rm(data.path, function(err){ 75 | if(err){ 76 | console.log('error while removing dir',err); 77 | } 78 | }); 79 | returnLink(data, filepath, callback); 80 | }); 81 | } 82 | }); 83 | }); 84 | }); 85 | } 86 | 87 | // common method for responding once the file is successfully saved in server. 88 | function returnLink(data, filepath, callback){ 89 | data.path = filepath; 90 | delete data.blob; 91 | callback(data); 92 | } 93 | 94 | // for concating all audio chunks into single file. 95 | function concat(inFile, outFile, callback){ 96 | try{ 97 | ffmpeg().input(inFile) 98 | .inputOptions('-f', 'concat') 99 | .output(outFile) 100 | .on('error', function(err) { 101 | console.log('err:', err); 102 | callback(err); 103 | }).on('end', function() { 104 | callback(null, outFile); 105 | }).run(); 106 | }catch(e){ 107 | console.log('err:', e); 108 | callback(e); 109 | } 110 | } 111 | 112 | // for creating a directory at the given path if not present already. 113 | function mkDir(dirPath, cb){ 114 | fs.exists(dirPath, function(exists){ 115 | if(!exists){ 116 | mkdirp(dirPath, '0755', function(err){ 117 | if(err) console.log( 'error creating folder'); 118 | else cb(); 119 | }); 120 | }else{ 121 | cb(); 122 | } 123 | }); 124 | } 125 | 126 | mkDir(TMP_PATH, noop); --------------------------------------------------------------------------------