You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					
					
						
							302 lines
						
					
					
						
							11 KiB
						
					
					
				
			
		
		
	
	
							302 lines
						
					
					
						
							11 KiB
						
					
					
				var util = require('util');
 | 
						|
var webrtcSupport = require('webrtcsupport');
 | 
						|
var PeerConnection = require('rtcpeerconnection');
 | 
						|
var WildEmitter = require('wildemitter');
 | 
						|
var FileTransfer = require('filetransfer');
 | 
						|
 | 
						|
// the inband-v1 protocol is sending metadata inband in a serialized JSON object
 | 
						|
// followed by the actual data. Receiver closes the datachannel upon completion
 | 
						|
var INBAND_FILETRANSFER_V1 = 'https://simplewebrtc.com/protocol/filetransfer#inband-v1';
 | 
						|
 | 
						|
function isAllTracksEnded(stream) {
 | 
						|
    var isAllTracksEnded = true;
 | 
						|
    stream.getTracks().forEach(function (t) {
 | 
						|
        isAllTracksEnded = t.readyState === 'ended' && isAllTracksEnded;
 | 
						|
    });
 | 
						|
    return isAllTracksEnded;
 | 
						|
}
 | 
						|
 | 
						|
function Peer(options) {
 | 
						|
    var self = this;
 | 
						|
 | 
						|
    // call emitter constructor
 | 
						|
    WildEmitter.call(this);
 | 
						|
 | 
						|
    this.id = options.id;
 | 
						|
    this.parent = options.parent;
 | 
						|
    this.type = options.type || 'video';
 | 
						|
    this.oneway = options.oneway || false;
 | 
						|
    this.sharemyscreen = options.sharemyscreen || false;
 | 
						|
    this.browserPrefix = options.prefix;
 | 
						|
    this.stream = options.stream;
 | 
						|
    this.enableDataChannels = options.enableDataChannels === undefined ? this.parent.config.enableDataChannels : options.enableDataChannels;
 | 
						|
    this.receiveMedia = options.receiveMedia || this.parent.config.receiveMedia;
 | 
						|
    this.channels = {};
 | 
						|
    this.sid = options.sid || Date.now().toString();
 | 
						|
    // Create an RTCPeerConnection via the polyfill
 | 
						|
    this.pc = new PeerConnection(this.parent.config.peerConnectionConfig, this.parent.config.peerConnectionConstraints);
 | 
						|
    this.pc.on('ice', this.onIceCandidate.bind(this));
 | 
						|
    this.pc.on('endOfCandidates', function (event) {
 | 
						|
        self.send('endOfCandidates', event);
 | 
						|
    });
 | 
						|
    this.pc.on('offer', function (offer) {
 | 
						|
        if (self.parent.config.nick) offer.nick = self.parent.config.nick;
 | 
						|
        self.send('offer', offer);
 | 
						|
    });
 | 
						|
    this.pc.on('answer', function (answer) {
 | 
						|
        if (self.parent.config.nick) answer.nick = self.parent.config.nick;
 | 
						|
        self.send('answer', answer);
 | 
						|
    });
 | 
						|
    this.pc.on('addStream', this.handleRemoteStreamAdded.bind(this));
 | 
						|
    this.pc.on('addChannel', this.handleDataChannelAdded.bind(this));
 | 
						|
    this.pc.on('removeStream', this.handleStreamRemoved.bind(this));
 | 
						|
    // Just fire negotiation needed events for now
 | 
						|
    // When browser re-negotiation handling seems to work
 | 
						|
    // we can use this as the trigger for starting the offer/answer process
 | 
						|
    // automatically. We'll just leave it be for now while this stabalizes.
 | 
						|
    this.pc.on('negotiationNeeded', this.emit.bind(this, 'negotiationNeeded'));
 | 
						|
    this.pc.on('iceConnectionStateChange', this.emit.bind(this, 'iceConnectionStateChange'));
 | 
						|
    this.pc.on('iceConnectionStateChange', function () {
 | 
						|
        switch (self.pc.iceConnectionState) {
 | 
						|
        case 'failed':
 | 
						|
            // currently, in chrome only the initiator goes to failed
 | 
						|
            // so we need to signal this to the peer
 | 
						|
            if (self.pc.pc.peerconnection.localDescription.type === 'offer') {
 | 
						|
                self.parent.emit('iceFailed', self);
 | 
						|
                self.send('connectivityError');
 | 
						|
            }
 | 
						|
            break;
 | 
						|
        }
 | 
						|
    });
 | 
						|
    this.pc.on('signalingStateChange', this.emit.bind(this, 'signalingStateChange'));
 | 
						|
    this.logger = this.parent.logger;
 | 
						|
 | 
						|
    // handle screensharing/broadcast mode
 | 
						|
    if (options.type === 'screen') {
 | 
						|
        if (this.parent.localScreen && this.sharemyscreen) {
 | 
						|
            this.logger.log('adding local screen stream to peer connection');
 | 
						|
            this.pc.addStream(this.parent.localScreen);
 | 
						|
            this.broadcaster = options.broadcaster;
 | 
						|
        }
 | 
						|
    } else {
 | 
						|
        this.parent.localStreams.forEach(function (stream) {
 | 
						|
            self.pc.addStream(stream);
 | 
						|
        });
 | 
						|
    }
 | 
						|
 | 
						|
    this.on('channelOpen', function (channel) {
 | 
						|
        if (channel.protocol === INBAND_FILETRANSFER_V1) {
 | 
						|
            channel.onmessage = function (event) {
 | 
						|
                var metadata = JSON.parse(event.data);
 | 
						|
                var receiver = new FileTransfer.Receiver();
 | 
						|
                receiver.receive(metadata, channel);
 | 
						|
                self.emit('fileTransfer', metadata, receiver);
 | 
						|
                receiver.on('receivedFile', function (file, metadata) {
 | 
						|
                    receiver.channel.close();
 | 
						|
                });
 | 
						|
            };
 | 
						|
        }
 | 
						|
    });
 | 
						|
 | 
						|
    // proxy events to parent
 | 
						|
    this.on('*', function () {
 | 
						|
        self.parent.emit.apply(self.parent, arguments);
 | 
						|
    });
 | 
						|
}
 | 
						|
 | 
						|
util.inherits(Peer, WildEmitter);
 | 
						|
 | 
						|
Peer.prototype.handleMessage = function (message) {
 | 
						|
    var self = this;
 | 
						|
 | 
						|
    this.logger.log('getting', message.type, message);
 | 
						|
 | 
						|
    if (message.prefix) this.browserPrefix = message.prefix;
 | 
						|
 | 
						|
    if (message.type === 'offer') {
 | 
						|
        if (!this.nick) this.nick = message.payload.nick;
 | 
						|
        delete message.payload.nick;
 | 
						|
        this.pc.handleOffer(message.payload, function (err) {
 | 
						|
            if (err) {
 | 
						|
                return;
 | 
						|
            }
 | 
						|
            // auto-accept
 | 
						|
            self.pc.answer(function (err, sessionDescription) {
 | 
						|
                //self.send('answer', sessionDescription);
 | 
						|
            });
 | 
						|
        });
 | 
						|
    } else if (message.type === 'answer') {
 | 
						|
        if (!this.nick) this.nick = message.payload.nick;
 | 
						|
        delete message.payload.nick;
 | 
						|
        this.pc.handleAnswer(message.payload);
 | 
						|
    } else if (message.type === 'candidate') {
 | 
						|
        this.pc.processIce(message.payload);
 | 
						|
    } else if (message.type === 'connectivityError') {
 | 
						|
        this.parent.emit('connectivityError', self);
 | 
						|
    } else if (message.type === 'mute') {
 | 
						|
        this.parent.emit('mute', {id: message.from, name: message.payload.name});
 | 
						|
    } else if (message.type === 'unmute') {
 | 
						|
        this.parent.emit('unmute', {id: message.from, name: message.payload.name});
 | 
						|
    } else if (message.type === 'endOfCandidates') {
 | 
						|
        // Edge requires an end-of-candidates. Since only Edge will have mLines or tracks on the
 | 
						|
        // shim this will only be called in Edge.
 | 
						|
        var mLines = this.pc.pc.peerconnection.transceivers || [];
 | 
						|
        mLines.forEach(function (mLine) {
 | 
						|
            if (mLine.iceTransport) {
 | 
						|
                mLine.iceTransport.addRemoteCandidate({});
 | 
						|
            }
 | 
						|
        });
 | 
						|
    }
 | 
						|
};
 | 
						|
 | 
						|
// send via signalling channel
 | 
						|
Peer.prototype.send = function (messageType, payload) {
 | 
						|
    var message = {
 | 
						|
        to: this.id,
 | 
						|
        sid: this.sid,
 | 
						|
        broadcaster: this.broadcaster,
 | 
						|
        roomType: this.type,
 | 
						|
        type: messageType,
 | 
						|
        payload: payload,
 | 
						|
        prefix: webrtcSupport.prefix
 | 
						|
    };
 | 
						|
    this.logger.log('sending', messageType, message);
 | 
						|
    this.parent.emit('message', message);
 | 
						|
};
 | 
						|
 | 
						|
// send via data channel
 | 
						|
// returns true when message was sent and false if channel is not open
 | 
						|
Peer.prototype.sendDirectly = function (channel, messageType, payload) {
 | 
						|
    var message = {
 | 
						|
        type: messageType,
 | 
						|
        payload: payload
 | 
						|
    };
 | 
						|
    this.logger.log('sending via datachannel', channel, messageType, message);
 | 
						|
    var dc = this.getDataChannel(channel);
 | 
						|
    if (dc.readyState != 'open') return false;
 | 
						|
    dc.send(JSON.stringify(message));
 | 
						|
    return true;
 | 
						|
};
 | 
						|
 | 
						|
// Internal method registering handlers for a data channel and emitting events on the peer
 | 
						|
Peer.prototype._observeDataChannel = function (channel) {
 | 
						|
    var self = this;
 | 
						|
    channel.onclose = this.emit.bind(this, 'channelClose', channel);
 | 
						|
    channel.onerror = this.emit.bind(this, 'channelError', channel);
 | 
						|
    channel.onmessage = function (event) {
 | 
						|
        self.emit('channelMessage', self, channel.label, JSON.parse(event.data), channel, event);
 | 
						|
    };
 | 
						|
    channel.onopen = this.emit.bind(this, 'channelOpen', channel);
 | 
						|
};
 | 
						|
 | 
						|
// Fetch or create a data channel by the given name
 | 
						|
Peer.prototype.getDataChannel = function (name, opts) {
 | 
						|
    if (!webrtcSupport.supportDataChannel) return this.emit('error', new Error('createDataChannel not supported'));
 | 
						|
    var channel = this.channels[name];
 | 
						|
    opts || (opts = {});
 | 
						|
    if (channel) return channel;
 | 
						|
    // if we don't have one by this label, create it
 | 
						|
    channel = this.channels[name] = this.pc.createDataChannel(name, opts);
 | 
						|
    this._observeDataChannel(channel);
 | 
						|
    return channel;
 | 
						|
};
 | 
						|
 | 
						|
Peer.prototype.onIceCandidate = function (candidate) {
 | 
						|
    if (this.closed) return;
 | 
						|
    if (candidate) {
 | 
						|
        var pcConfig = this.parent.config.peerConnectionConfig;
 | 
						|
        if (webrtcSupport.prefix === 'moz' && pcConfig && pcConfig.iceTransports &&
 | 
						|
                candidate.candidate && candidate.candidate.candidate &&
 | 
						|
                candidate.candidate.candidate.indexOf(pcConfig.iceTransports) < 0) {
 | 
						|
            this.logger.log('Ignoring ice candidate not matching pcConfig iceTransports type: ', pcConfig.iceTransports);
 | 
						|
        } else {
 | 
						|
            this.send('candidate', candidate);
 | 
						|
        }
 | 
						|
    } else {
 | 
						|
        this.logger.log("End of candidates.");
 | 
						|
    }
 | 
						|
};
 | 
						|
 | 
						|
Peer.prototype.start = function () {
 | 
						|
    var self = this;
 | 
						|
 | 
						|
    // well, the webrtc api requires that we either
 | 
						|
    // a) create a datachannel a priori
 | 
						|
    // b) do a renegotiation later to add the SCTP m-line
 | 
						|
    // Let's do (a) first...
 | 
						|
    if (this.enableDataChannels) {
 | 
						|
        this.getDataChannel('simplewebrtc');
 | 
						|
    }
 | 
						|
 | 
						|
    this.pc.offer(this.receiveMedia, function (err, sessionDescription) {
 | 
						|
        //self.send('offer', sessionDescription);
 | 
						|
    });
 | 
						|
};
 | 
						|
 | 
						|
Peer.prototype.icerestart = function () {
 | 
						|
    var constraints = this.receiveMedia;
 | 
						|
    constraints.mandatory.IceRestart = true;
 | 
						|
    this.pc.offer(constraints, function (err, success) { });
 | 
						|
};
 | 
						|
 | 
						|
Peer.prototype.end = function () {
 | 
						|
    if (this.closed) return;
 | 
						|
    this.pc.close();
 | 
						|
    this.handleStreamRemoved();
 | 
						|
};
 | 
						|
 | 
						|
Peer.prototype.handleRemoteStreamAdded = function (event) {
 | 
						|
    var self = this;
 | 
						|
    if (this.stream) {
 | 
						|
        this.logger.warn('Already have a remote stream');
 | 
						|
    } else {
 | 
						|
        this.stream = event.stream;
 | 
						|
 | 
						|
        this.stream.getTracks().forEach(function (track) {
 | 
						|
            track.addEventListener('ended', function () {
 | 
						|
                if (isAllTracksEnded(self.stream)) {
 | 
						|
                    self.end();
 | 
						|
                }
 | 
						|
            });
 | 
						|
        });
 | 
						|
 | 
						|
        this.parent.emit('peerStreamAdded', this);
 | 
						|
    }
 | 
						|
};
 | 
						|
 | 
						|
Peer.prototype.handleStreamRemoved = function () {
 | 
						|
    var peerIndex = this.parent.peers.indexOf(this);
 | 
						|
    if (peerIndex > -1) {
 | 
						|
        this.parent.peers.splice(peerIndex, 1);
 | 
						|
        this.closed = true;
 | 
						|
        this.parent.emit('peerStreamRemoved', this);
 | 
						|
    }
 | 
						|
};
 | 
						|
 | 
						|
Peer.prototype.handleDataChannelAdded = function (channel) {
 | 
						|
    this.channels[channel.label] = channel;
 | 
						|
    this._observeDataChannel(channel);
 | 
						|
};
 | 
						|
 | 
						|
Peer.prototype.sendFile = function (file) {
 | 
						|
    var sender = new FileTransfer.Sender();
 | 
						|
    var dc = this.getDataChannel('filetransfer' + (new Date()).getTime(), {
 | 
						|
        protocol: INBAND_FILETRANSFER_V1
 | 
						|
    });
 | 
						|
    // override onopen
 | 
						|
    dc.onopen = function () {
 | 
						|
        dc.send(JSON.stringify({
 | 
						|
            size: file.size,
 | 
						|
            name: file.name
 | 
						|
        }));
 | 
						|
        sender.send(file, dc);
 | 
						|
    };
 | 
						|
    // override onclose
 | 
						|
    dc.onclose = function () {
 | 
						|
        console.log('sender received transfer');
 | 
						|
        sender.emit('complete');
 | 
						|
    };
 | 
						|
    return sender;
 | 
						|
};
 | 
						|
 | 
						|
module.exports = Peer;
 | 
						|
 |