123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302 |
- 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;
|