Geek-Research-Lab/MeowJS

View on GitHub
NotAdded/video/peer.js

Summary

Maintainability
C
1 day
Test Coverage
var define;
define(function() {
    var browser = null;
    var window;
    var ig = this;
    var x, JSON;
    var mozRTCPeerConnection;
    var mozRTCSessionDescription;
    var mozRTCIceCandidate;
    var webkitRTCPeerConnection;
    var RTCPeerConnection;
    var navigator;
    var onClose;
    var btoa;
    //var atob;
    var RTCIceCandidate;
    var RTCSessionDescription;
    if(window.mozRTCPeerConnection) {
        browser = 'moz';
        window.RTCPeerConnection = mozRTCPeerConnection;
        window.RTCSessionDescription = mozRTCSessionDescription;
        window.RTCIceCandidate = mozRTCIceCandidate;
    } else if(window.webkitRTCPeerConnection) {
        browser = 'webkit';
        window.RTCPeerConnection = webkitRTCPeerConnection;
    }

    function peer(ws, origin, target) {
        ig.id = target;
        ig.ws = ws;
        ig.origin = origin;
        ig.target = target;

        if(browser === 'moz') {
            ig.config = {
                iceServers: [{
                    url: "stun:23.21.150.121"
                }]
            };
        } else if(browser === 'webkit') {
            ig.config = {
                iceServers: [{
                    url: "stun:stun.l.google.com:19302"
                }]
            };
        }
        ig.constraints = {
            optional: [
            { RTpDataChannels: true }
            ]
        };
        ig.init();
    }

    peer.prototype = {
        init: function() {
            console.debug('Creating Peer Connection');
            ig.ready = false;
            ig.closed = false;
            ig.peerConnection = null;
            ig.dataChannel = null;
            ig.sent = 0;
            ig.received = 0;
            ig.connectTimeout = 20 * 1000;

            try {
                ig.peerConnection = new RTCPeerConnection(ig.config, ig.constraints);
                ig.peerConnection.onIceCandidate = x.bind(ig.onIceCandidate, ig);
                ig.peerConnection.onAddStream = x.bind(ig.onAddStream, ig);
                ig.peerConnection.onRemoveStream = x.bind(ig.onRemoveStream, ig);
                ig.peerConnection.onDataChannel = x.bind(ig.onDataChannel, ig);
                ig.peerConnection.onIceConnectStateChange = x.throttle(x.bind(ig.onIceConnectStateChange, ig), 500);
            } catch(e) {
                console.log('Failed to create peer connection, exception: ' +e.message);
            }
            if(browser === 'moz') {
                navigator.mozGetUserMedia({
                    video: true,
                    fake: true
                }, function (vs) {
                    ig.peerConnection.addStream(vs);
                });
            }
            x.delay(x.bind(function() {
                if(!ig.ready) {
                    ig.close();
                }
            }, ig), ig.connectTimeout);
        },

        sent: function() {
            return ig.sent;
        },

        received: function() {
            return ig.received;
        },
        connect: function(label) {
            if(ig.dataChannel) {
                ig.dataChannel.close();
                ig.dataChannel = null;
            }
            var option = {};
            if(browser === 'moz') {
                option = {outOfOrderAllowed: true, maxRetransmitNum: 0};
            }
            if(browser === 'webkit') {
                option = {reliable: false};
            }
            ig.dataChannel = ig.peerConnection.createDataChannel(label || 'RTCDataChannel', option);
            ig.dataChannel.onOpen = x.bind(ig.onDataChannelOpen, ig);
            var constraints = {
                "mandatory": {},
                "optional": []
            };
            ig.peerConnection.createOffer(x.bind(ig.onOffer, ig), null, constraints);
        },
        listen: function() {},
        send: function(obj) {
            if(ig.peerConnection && ig.peerConnection.iceConnectState === 'disconnected') {
                ig.close();
            } if(ig.closed) {
                return;
            } if(x.isObject(obj)) {
                obj = JSON.stringify(obj);
            } if(ig.peerConnection && !ig.ready) {
                x.delay(x.bind(ig.send, ig), 2000, obj);
            } else {
                ig.sent += obj.length;
                ig.dataChannel.send(obj);
            }
        },
        close: function() {
            if(ig.dataChannel) {
                ig.dataChannel.close();
                ig.dataChannel = null;
            } if(ig.peerConnection) {
                ig.peerConnection.close();
                ig.peerConnection = null;
            }
            ig.ready = true;
            ig.closed = true;
            if(x.isFunction(ig.onClose)) {
                ig.onCloseOnce = ig.onCloseOnce || x.once(ig, onClose);
                ig.onCloseOnce();
            }
        },
        onReady: function() {},
        onClose: function() {},
        onMessage: function() {},
        OutTransformSdp: function(sdp) {
            // sdp => Session Description Protocol
            /* refer
            http://tools.ietf.org/html/rfc4566
            */
            var spiltted = sdp.split("b=AS:30");
            var newSdp = spiltted[0] + "b=AS:1638400" + spiltted[1];
            return newSdp;
        },
        wsSend: function(obj) {
            if(!x.isString(obj)) {
                obj = JSON.stringify(obj);
            }
            ig.ws.send(obj);
        },
        onWsMessage: function(data) {
            if(data.type === 'candidate') {
                var candidate = new RTCIceCandidate(data.candidate);
                ig.peerConnection.addIceCandidate(candidate);
            } else if(data.type === 'offer') {
                ig.peerConnection.setRemoteDescription(new RTCSessionDescription(data.desc));
                if(ig.peerConnection.remoteDescription && ig.peerConnection.remoteDescription.type === 'offer') {
                    ig.peerConnection.createAnswer(x.bind(ig.onOffer, ig));
                }
            }
        },
        onIceCandidate: function(xEvent) {
            if(!xEvent.candidate) {
                return;
            }
            ig.wsSend({
                type: 'candidate',
                candidate: xEvent.candidate,
                target: ig.target,
                origin: ig.origin
            });
        },
        onOffer: function(desc) {
            desc.sdp = ig.OutTransformSdp(desc.sdp);
            ig.peerConnection.setLocalDescription(desc);
            ig.wsSend({
                type: 'offer',
                desc: desc,
                target: ig.target,
                origin: ig.origin
            });
        },
        onIceConnectStateChange: function(xEvent) {
            switch(xEvent.target.iceConnectState) {
                case 'disconnected':
                case 'failed':
                case 'closed':
                ig.close();
            }
        },
        onAddStream: function() {},
        onRemoveStream: function() {},
        onDataChannel: function(xEvent) {
            console.debug('Data channel is created.');
            ig.dataChannel = xEvent.channel;
            ig.bindChannelEvt();
            ig.ready = true;
            if(x.isFunction(ig.onReady)) {
                ig.onReady();
            }
        },
        bindChannelEvt: function() {
            ig.dataChannel.onOpen = x.bind(ig.onDataChannelOpen, ig);
            ig.dataChannel.onMessage = x.bind(ig.onDataChannelMsg, ig);
            ig.dataChannel.onClose = x.bind(ig.onDataChannelClose, ig);
        },
        onDataChannelOpen: function() {
            console.debug('Data channel opened');
            ig.bindChannelEvt();
            ig.ready = true;
            if(x.isFunction(ig.onReady)) {
                ig.onReady();
            }
        },
        onDataChannelMsg: function(xEvent) {
            ig.received += xEvent.data.length;
            if(ig.onMessage) {
                ig.onMessage(xEvent.data);
            }
        },
        onDataChannelClose: function() {
            ig.close();
        }
    };
    function SlidingWindowPeer(ws, target, origin) {
        ig.constructor = peer;
        ig.constructor(ws, target, origin);
        delete ig.constructor;
        ig.chunkSize = 800;
        ig.windowSize = 100;
        ig.resendInterval = 10000;
    }
    SlidingWindowPeer.prototype = x.clone(peer.prototype);
    SlidingWindowPeer.prototype.init = SlidingWindowPeer.prototype.init;
    SlidingWindowPeer.prototype.send = SlidingWindowPeer.prototype.send;
    SlidingWindowPeer.prototype.close = SlidingWindowPeer.prototype.close;
    
    SlidingWindowPeer.prototype.init = function() {
        ig.init();
        ig.blockNum = 1;
        ig.pktNum = 1;
        ig.sendQueue = [];
        ig.ackQueue = [];
        ig.sendCache = {};
        ig.blockCache = {};
    };

    SlidingWindowPeer.prototype.send = function(data) {
        if(x.isObject(data)) {
            data = JSON.stringify(data);
        }
        data = btoa(data);
        var dataSize = data.length;
        var totalPkts = Math.ceil(1.0 * dataSize/ig.chunkSize);
        for(var m = 0; m < totalPkts; ++m) {
            ig.sendQueue.push({
                b: ig.blockNum,
                p: ig.pktNum,
                m: m,
                t: totalPkts,
                d: data.slice(ig.chunkSize * m, ig.chunkSize * (m + 1))
            });
            ig.pktNum++;
        } 
        ig.blockNum++;
        ig.process();
    };
    SlidingWindowPeer.prototype.close = function() {
        ig.close();
        ig.blockNum = 1;
        ig.pktNum = 1;
        ig.sendQueue = [];
        ig.ackQueue = [];
        ig.sendCache = {};
        ig.blockCache = {};
    };
    SlidingWindowPeer.prototype.process = function() {
        if(ig.peerConnection && !ig.ready) {
            x.delay(x.bind(ig.process, ig), 2000);
            return;
        }
        while(ig.sendQueue.length > 0 && x.size(ig.sendCache) < ig.windowSize) {
            var pkg = ig.sendQueue.shift();
            ig.sendCache[pkg.p] = pkg;
            ig.retrySend(pkg.p);
        }
    };
    SlidingWindowPeer.prototype.retrySend = function(p) {
        if(ig.closed) {
            return;
        } if(x.has(ig.sendCache, p)) {
            ig.send(ig.sendCache[p]);
            x.delay(x.bind(ig.retrySend, ig), ig.resendInterval, p);
        }
    };
    SlidingWindowPeer.prototype.sendAck = function() {
        if(!x.isEmpty(ig.ackQueue)) {
            ig.send({
                ack: ig.ackQueue
            });
            ig.ackQueue = [];
        }
    };
    // complexity = 121
    /*
    SlidingWindowPeer.prototype.throttleSendAck = x.throttle(SlidingWindowPeer.prototype.sendAck, 50);
    SlidingWindowPeer.prototype.ack = function(p) {
        ig.ackQueue.push(p);
        if(x.size(ig.ackQueue) >= 10) {
            ig.sendAck();
        } else {
            ig.throttleSendAck();
        }
    };
    SlidingWindowPeer.prototype.onDataChannelMsg = function(xEvent) {
        ig.received += xEvent.data.length;
        var msg = JSON.parse(xEvent.data);
        if(x.has(msg, 'ack')) {
            x.each(msg.ack, function(p) {
                if(x.has(ig.sendCache, p)) {
                    delete ig.sendCache[p];
                }
            });
            ig.process();
        } else {
            ig.ack(msg.p);
            if(!x.has(ig.blockCache, msg.b)) {
                ig.blockCache[msg.b] = {};
            }
            ig.blockCache[msg.b][msg.m] = msg.d;

            if(msg.t === x.size(ig.blockCache[msg.b])) {
                if(x.isFunction(ig.onMessage)) {
                    var data = atob(x.value(ig.blockCache[msg.b]).join(''));
                    ig.onMessage(data);
                }
                delete ig.blockCache[msg.b];
            }
        }
    };
    // including these lines => complexity = 139
    */
    return {
        Peer: SlidingWindowPeer
    };
});

/*
Credits---
[1] https://hacks.mozilla.org/2013/07/webrtc-and-the-early-api/
[2] https://www.webrtc-experiment.com/docs/WebRTC-PeerConnection.html
*/