UNPKG

webrtc

Version:

WebRTC abstraction for making it simple to manage multiple peer connections of various types.

265 lines (237 loc) 9.59 kB
var util = require('util'); var webrtc = 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 Peer(options) { var self = 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('offer', function (offer) { self.send('offer', offer); }); this.pc.on('answer', function (offer) { self.send('answer', offer); }); 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); }); } // call emitter constructor WildEmitter.call(this); 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') { // workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=1064247 message.payload.sdp = message.payload.sdp.replace('a=fmtp:0 profile-level-id=0x42e00c;packetization-mode=1\r\n', ''); this.pc.handleOffer(message.payload, function (err) { if (err) { return; } // auto-accept self.pc.answer(self.receiveMedia, function (err, sessionDescription) { //self.send('answer', sessionDescription); }); }); } else if (message.type === 'answer') { 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}); } }; // 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: webrtc.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 (!webrtc.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) { 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; // FIXME: addEventListener('ended', ...) would be nicer // but does not work in firefox this.stream.onended = function () { self.end(); }; this.parent.emit('peerStreamAdded', this); } }; Peer.prototype.handleStreamRemoved = function () { this.parent.peers.splice(this.parent.peers.indexOf(this), 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;