diff --git a/examples/basic/stream.html b/examples/basic/stream.html new file mode 100644 index 00000000..ae86f548 --- /dev/null +++ b/examples/basic/stream.html @@ -0,0 +1,53 @@ +<!DOCTYPE html> + +<center> + <img id="img" width="100%"><br> + Stream <select id="select"><option id="from">from</option></select> + add <input id="pass" placeholder="password" type="password"> + resolution <input id="res" value="240" type="number" step="16"> +</center> +<video id="video" width="100%" controls autoplay style="display: none;"></video> +<canvas id="canvas" width="0" style="display: none;"></canvas> + +<script src="../jquery.js"></script> +<script src="../../../gun/gun.js"></script> +<script src="../../../gun/sea.js"></script> +<script src="../../../gun/lib/webrtc.js"></script> + +<script>;(async function(){ +gun = Gun(location.origin + '/gun'); //gun = GUN(); + +stream = canvas.getContext('2d'), stream.from = navigator.mediaDevices; + +(await stream.from.enumerateDevices()).forEach((device,i) => { + if('videoinput' !== device.kind){ return } + var opt = $(from).clone().prependTo('select').get(0); + $(opt).text(opt.id = device.label || 'Camera '+i); + opt.value = device.deviceId; +}); + +$('select').on('change', async eve => { $(from).text('Off'); // update label + if('Off' == select.value){ return video.srcObject = null } + video.srcObject = await stream.from.getUserMedia({ audio: false, + video: (select.value && {deviceId: {exact: select.value}}) || {facingMode: "environment"} + }); +}); + +setInterval(async tmp => { + if(!video.srcObject){ return } + var size = parseInt(res.value); + stream.drawImage(video, 0,0, + canvas.width = size || video.videoWidth * 0.1, + canvas.height = (size * (video.videoHeight/video.videoWidth)) || video.videoHeight * 0.1 + ); + var b64 = canvas.toDataURL('image/jpeg'); + if(pass.value){ b64 = await SEA.encrypt(b64, pass.value) } + gun.get('test').get('video').put(b64); +}, 99); + +gun.get('test').get('video').on(async data => { + if(pass.value){ data = await SEA.decrypt(data, pass.value) } + img.src = data; +}); + +}());</script> \ No newline at end of file diff --git a/gun.js b/gun.js index 705dcf4d..f1bd67d5 100644 --- a/gun.js +++ b/gun.js @@ -370,6 +370,7 @@ } ctx.stun++; // TODO: 'forget' feature in SEA tied to this, bad approach, but hacked in for now. Any changes here must update there. var aid = msg['#']+ctx.all++, id = {toString: function(){ return aid }, _: ctx}; id.toJSON = id.toString; // this *trick* makes it compatible between old & new versions. + root.dup.track(id)['#'] = msg['#']; // fixes new OK acks for RPC like RTC. DBG && (DBG.ph = DBG.ph || +new Date); root.on('put', {'#': id, '@': msg['@'], put: {'#': soul, '.': key, ':': val, '>': state}, _: ctx}); } @@ -399,13 +400,18 @@ } function ack(msg){ // aggregate ACKs. var id = msg['@'] || '', ctx; - if(!(ctx = id._)){ return } + if(!(ctx = id._)){ + var dup = (dup = msg.$) && (dup = dup._) && (dup = dup.root) && (dup = dup.dup); + if(!(dup = dup.check(id))){ return } + msg['@'] = dup['#'] || msg['@']; + return; + } ctx.acks = (ctx.acks||0) + 1; if(ctx.err = msg.err){ msg['@'] = ctx['#']; fire(ctx); // TODO: BUG? How it skips/stops propagation of msg if any 1 item is error, this would assume a whole batch/resync has same malicious intent. } - if(!ctx.stop && !ctx.crack){ ctx.crack = ctx.match && ctx.match.push(function(){back(ctx)}) } // handle synchronous acks + if(!ctx.stop && !ctx.crack){ ctx.crack = ctx.match && ctx.match.push(function(){back(ctx)}) } // handle synchronous acks. NOTE: If a storage peer ACKs synchronously then the PUT loop has not even counted up how many items need to be processed, so ctx.STOP flags this and adds only 1 callback to the end of the PUT loop. back(ctx); } function back(ctx){ @@ -1092,7 +1098,7 @@ setTimeout.each(Object.keys(stun = stun.add||''), function(cb){ if(cb = stun[cb]){cb()} }); // resume the stunned reads // Any perf reasons to CPU schedule this .keys( ? }).hatch = tmp; // this is not official yet ^ //console.log(1, "PUT", as.run, as.graph); - (as.via._).on('out', {put: as.out = as.graph, opt: as.opt, '#': ask, _: tmp}); + (as.via._).on('out', {put: as.out = as.graph, ok: as.ok || as.opt, opt: as.opt, '#': ask, _: tmp}); }; ran.end = function(stun,root){ stun.end = noop; // like with the earlier id, cheaper to make this flag a function so below callbacks do not have to do an extra type check. if(stun.the.to === stun && stun === stun.the.last){ delete root.stun } diff --git a/lib/webrtc.js b/lib/webrtc.js index abc887cb..07bb5d71 100644 --- a/lib/webrtc.js +++ b/lib/webrtc.js @@ -33,30 +33,38 @@ // The above change corrects at least firefox RTC Peer handler where it **throws** on over 6 ice servers, and updates url: to urls: removing deprecation warning opt.rtc.dataChannel = opt.rtc.dataChannel || {ordered: false, maxRetransmits: 2}; opt.rtc.sdp = opt.rtc.sdp || {mandatory: {OfferToReceiveAudio: false, OfferToReceiveVideo: false}}; + opt.rtc.max = opt.rtc.max || 55; // is this a magic number? + opt.rtc.room = opt.rtc.room || Gun.window && (location.hash.slice(1) || location.pathname.slice(1)); opt.announce = function(to){ - root.on('out', {rtc: {id: opt.pid, to:to}}); // announce ourself + opt.rtc.start = +new Date; // handle room logic: + root.$.get('/RTC/'+opt.rtc.room+'<?99').get('+').put(opt.pid, function(ack){ + if(!ack.ok || !ack.ok.rtc){ return } + open(ack); + }, {acks: opt.rtc.max}).on(function(last,key, msg){ + if(last === opt.pid || opt.rtc.start > msg.put['>']){ return } + open({'#': ''+msg['#'], ok: {rtc: {id: last}}}); + }); }; + var mesh = opt.mesh = opt.mesh || Gun.Mesh(root); root.on('create', function(at){ this.to.next(at); setTimeout(opt.announce, 1); }); - root.on('in', function(msg){ - if(msg.rtc){ open(msg) } - this.to.next(msg); - }); function open(msg){ - var rtc = msg.rtc, peer, tmp; - if(!rtc || !rtc.id){ return } - delete opt.announce[rtc.id]; /// remove after connect + if(this && this.off){ this.off() } // Ignore this, because of ask / ack. + if(!msg.ok){ return } + var rtc = msg.ok.rtc, peer, tmp; + if(!rtc || !rtc.id || rtc.id === opt.pid){ return } + //console.log("webrtc:", rtc); if(tmp = rtc.answer){ if(!(peer = opt.peers[rtc.id] || open[rtc.id]) || peer.remoteSet){ return } - tmp.sdp = tmp.sdp.replace(/\\r\\n/g, '\r\n') + tmp.sdp = tmp.sdp.replace(/\\r\\n/g, '\r\n'); return peer.setRemoteDescription(peer.remoteSet = new opt.RTCSessionDescription(tmp)); } if(tmp = rtc.candidate){ - peer = opt.peers[rtc.id] || open[rtc.id] || open({rtc: {id: rtc.id}}); + peer = opt.peers[rtc.id] || open[rtc.id] || open({ok: {rtc: {id: rtc.id}}}); return peer.addIceCandidate(new opt.RTCIceCandidate(tmp)); } //if(opt.peers[rtc.id]){ return } @@ -64,23 +72,23 @@ (peer = new opt.RTCPeerConnection(opt.rtc)).id = rtc.id; var wire = peer.wire = peer.createDataChannel('dc', opt.rtc.dataChannel); open[rtc.id] = peer; - wire.onclose = function(){ - delete open[rtc.id]; - mesh.bye(peer); - //reconnect(peer); - }; - wire.onerror = function(err){}; + wire.to = setTimeout(function(){delete open[rtc.id]},1000*60); + wire.onclose = function(){ mesh.bye(peer) }; + wire.onerror = function(err){ }; wire.onopen = function(e){ - //delete open[rtc.id]; + delete open[rtc.id]; mesh.hi(peer); + //clearTimeout(wire.to); + //delete open[rtc.id]; } wire.onmessage = function(msg){ if(!msg){ return } + console.log(opt.pid, "HEARD FROM WEBRTC:"); mesh.hear(msg.data || msg, peer); }; peer.onicecandidate = function(e){ // source: EasyRTC! if(!e.candidate){ return } - root.on('out', {'@': msg['#'], rtc: {candidate: e.candidate, id: opt.pid}}); + root.on('out', {'@': msg['#'], ok: {rtc: {candidate: e.candidate, id: opt.pid}}}); } peer.ondatachannel = function(e){ var rc = e.channel; @@ -93,16 +101,16 @@ peer.setRemoteDescription(new opt.RTCSessionDescription(tmp)); peer.createAnswer(function(answer){ peer.setLocalDescription(answer); - root.on('out', {'@': msg['#'], rtc: {answer: answer, id: opt.pid}}); + root.on('out', {'@': msg['#'], ok: {rtc: {answer: answer, id: opt.pid}}}); }, function(){}, opt.rtc.sdp); return; } peer.createOffer(function(offer){ peer.setLocalDescription(offer); - root.on('out', {'@': msg['#'], rtc: {offer: offer, id: opt.pid}}); + root.on('out', {'@': msg['#'], '#': root.ask(open), ok: {rtc: {offer: offer, id: opt.pid}}}); }, function(){}, opt.rtc.sdp); return peer; } }); var noop = function(){}; -}()); +}()); \ No newline at end of file diff --git a/test/common.js b/test/common.js index a6b1392d..79356785 100644 --- a/test/common.js +++ b/test/common.js @@ -4045,6 +4045,76 @@ describe('Gun', function(){ nopasstun(0, gunB); nopasstun(done, gunC); }, 100); + }); + + it('ack aggregation bypass', function(done){ + var alice = GUN({localStorage: false}); + var bob = GUN({localStorage: false}); + var carl = GUN({localStorage: false}); + + var adam = alice.back('opt.mesh'); + var asay = adam.say; + + var bdam = bob.back('opt.mesh'); + var bsay = bdam.say; + + var cdam = carl.back('opt.mesh'); + var csay = cdam.say; + + //console.only.i = 1; + adam.say = function(raw, peer){ + console.only(2, 'adam says:', raw); + console.only(1, '...'); + bdam.hear((raw.length && raw) || JSON.stringify(raw), {}); + asay(raw, peer); + } + bdam.say = function(raw, peer){ + console.only(7, "bob the relay is like YO", raw); + adam.hear((raw.length && raw) || JSON.stringify(raw), {}); + cdam.hear((raw.length && raw) || JSON.stringify(raw), {}); + bsay(raw, peer); + } + cdam.say = function(raw, peer){ + console.only(4, "carl speaks out:", raw); + console.only(3, "..."); + bdam.hear((raw.length && raw) || JSON.stringify(raw), {}); + csay(raw, peer); + } + + carl.on('put', async function(msg){ + this.to.next(msg); + + var tmp = msg.put; + + //if(Math.random() > 0.5){ return; } + //console.log(msg.put); + + //localStorage[tmp['#']+tmp['.']] = tmp[':']; + + setTimeout(function(){ + carl.on('out', {'@': msg['#']+'', ok: {BANANA: 9}}); + }, 10); + }); + + alice.on('get', function(msg){ setTimeout(function(){ Gun.on.get.ack(msg); },9) }) + + + setTimeout(async function(){ + var pair = await SEA.pair(); + var user = alice.user(); + setTimeout(function(){ + var c = 0; + //alice.on('auth', function(){ + alice.get('test').put({a: 1, b: 2, c: 3}, function(ack){ + //console.log("my data got saved?", ack); + + if(ack.ok.BANANA && ++c === c){ + done(); + } + }, {acks: 99}); + //}); user.auth(pair); + },10); + }, 100); }); /*it.only('Make sure circular contexts are not copied', function(done){