UNPKG

socket-signaler-client

Version:

Client for SocketIO WebRTC streaming webcam chatrooms

459 lines (365 loc) 15 kB
/* globals define, module, require */ /** * Socket Signaler Client * Version: 0.0.1 * GitHub: https://github.com/mcmouse/socketio-signaler-client **/ (function (root, factory) { 'use strict'; if (typeof define === 'function' && define.amd) { define(['https://cdnjs.cloudflare.com/ajax/libs/EventEmitter/4.2.11/EventEmitter.min.js', 'https://cdn.socket.io/socket.io-1.3.3.js'], function (EventEmitter, io) { return factory(window, EventEmitter, io); }); } else if (typeof exports === 'object') { module.exports = factory(window, require('wolfy87-eventemitter'), require('socket.io-client')); } else { root.PeerConnectionClient = factory(window, root.EventEmitter, root.io); } }(this, function factory(window, EventEmitter, io) { 'use strict'; function PeerConnectionClient(options) { this.peerConnections = []; if (navigator.mozGetUserMedia) { //Set constraints to properly negotiate connection this.constraints = { offerToReceiveAudio: true, offerToReceiveVideo: true }; } else { //Set constraints to properly negotiate connection this.constraints = { mandatory: { OfferToReceiveAudio: true, OfferToReceiveVideo: true }, }; } //Declare our public STUN server this.iceServers = { 'iceServers': [{ 'url': 'stun:stun.services.mozilla.com' }, { 'url': 'stun:stun.l.google.com:19302' }] }; //Set up our prefixed defaults this.setupRTCObjects = function () { //PeerConnection this.PeerConnection = window.RTCPeerConnection || window.mozRTCPeerConnection || window.webkitRTCPeerConnection; //SessionDescription this.SessionDescription = window.RTCSessionDescription || window.mozRTCSessionDescription || window.webkitRTCSessionDescription; //GetUserMedia navigator.getUserMedia = (navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia || navigator.msGetUserMedia); //RTCIceCandidate this.RTCIceCandidate = window.RTCIceCandidate || window.mozRTCIceCandidate; }; //Generate our option defaults this.generateDefaults = function (options) { options = options || {}; options.server = options.server || 'http://' + window.location.host + '/'; options.room = options.room || 'default'; options.debug = options.debug || false; return options; }; //Set up our event handlers this.bindEvents = function () { this.socket.on('initialized', this.getPeerList.bind(this)); this.socket.on('list', this.generateConnections.bind(this)); this.socket.on('offer', this.createAnswer.bind(this)); this.socket.on('answer', this.handleAnswer.bind(this)); this.socket.on('icecandidate', this.receiveIceCandidate.bind(this)); this.socket.on('newconnection', this.addPeer.bind(this)); this.socket.on('disconnect', this.disconnectConnection.bind(this)); this.socket.on('peerconnected', this.peerConnected.bind(this)); this.socket.on('streamremoved', this.removeRemoteStream.bind(this)); }; //Request our list of peers - all other users connected to the socket.io room this.getPeerList = function () { this.socket.emit('list'); }; //Generate our PeerConnections for each user received by getPeerList this.generateConnections = function (peers) { if (options.debug) console.log('Received list, creating connection(s) for ' + peers.length + ' peers'); //Create a new connection for each peer peers.forEach(this.addPeer.bind(this)); }; //Add a peer to our connections, initializing the PeerConnection object //and sending an offer if we are currently broadcasting this.addPeer = function (id, suppress) { if (!this.hasPeer(id)) { console.log('Adding peer: ' + id); //Create new peer var peer = { connection: new this.PeerConnection(this.iceServers), id: id, }; //Bind events peer = this.bindConnectionEvents(peer, suppress); //Add peer to array of connections this.peerConnections.push(peer); //If we are currently broadcasting, broadcast our stream to the newly added peer if (this.localStream) { this.addStreamToPeer(this.localStream, peer); } return peer; } }; //Bind our PeerConnection events //Not using "onremovestream" because it seems to be in a buggy state - //returning nonexistant remote connections on renegotiation this.bindConnectionEvents = function (peer, suppress) { if (options.debug) console.log('Binding connection events for peer: ' + peer.id); //Send ice candidate to peer peer.connection.onicecandidate = function (event) { if (options.debug && options.debug === 'verbose') console.log('Sending ICE candidate to ' + peer.id); if (options.debug && options.debug === 'verbose') console.log('Full event ', event); if (options.debug && options.debug === 'verbose') console.log('Stringified event ', JSON.stringify(event)); console.log(Object.keys(event)); if (event.candidate) { this.socket.emit('icecandidate', { target: peer.id, candidate: event.candidate }); } }.bind(this); //When connection succeeds after stream is added, proxy event peer.connection.onaddstream = function (event) { //Checking suppress because if a connection is renegotiated and the remote maintains a //stream, onaddstream will be fired twice if (options.debug) console.log('Remote stream added from ' + peer.id); if (!suppress) { this.emit('remoteStreamAdded', event.stream, peer.id); } else { if (options.debug) console.log('onaddstream event suppressed'); } }.bind(this); //Untested peer.connection.ondatachannel = function (event) { if (options.debug) console.log('Data channel added from ' + peer.id); this.emit('dataChannelAdded', event.channel, peer.id); }.bind(this); //Don't add unnecessary ICE candidates. //On Ice error, close the connection peer.connection.oniceconnectionstatechange = function () { switch (peer.connection.iceConnectionState) { case 'disconnected': case 'failed': this.logError('iceConnectionState is disconnected, closing connections to ' + peer.id); peer.connection.close(); break; case 'completed': peer.connection.onicecandidate = function () {}; break; } }.bind(this); return peer; }; //Create our offer and set our local session description this.createOffer = function (peer) { if (options.debug) console.log('Creating offer for: ' + peer.id); //Create our offer peer.connection.createOffer(function (offer) { //Set local description from offer peer.connection.setLocalDescription(new this.SessionDescription(offer), //Send offer this.sendOffer({ target: peer.id, offer: offer }), this.logError); //Pass constraints }.bind(this), this.logError, this.constraints); }; //Send offer to server this.sendOffer = function (offer) { if (options.debug) console.log('Sending offer to ' + offer.target); this.socket.emit('offer', offer); }; //Set remote and local session description this.createAnswer = function (data) { //Retreive peer from this.peerConnections and set offer var peer = this.getPeer(data.sender), offer = data.offer; if (options.debug) console.log('Creating answer for ' + peer.id); //Set remote description from offer peer.connection.setRemoteDescription(new this.SessionDescription(offer), function () { //Callback after remote description set peer.connection.createAnswer(function (answer) { //Callback after answer created to set the local description peer.connection.setLocalDescription(new this.SessionDescription(answer), //Send answer back to peer this.sendAnswer({ target: peer.id, answer: answer }), this.logError); //Set constraints for answer }.bind(this), this.logError, this.constraints); //Error logging for setRemoteDescription }.bind(this), this.logError); }; //Send answer to server this.sendAnswer = function (answer) { if (options.debug) console.log('Sent answer to peer: ' + answer.target); this.socket.emit('answer', answer); }; //Handle the answer received from peer this.handleAnswer = function (data) { var peer = this.getPeer(data.sender), answer = data.answer; if (options.debug) console.log('Handling answer from: ' + peer.id); //Set remote description peer.connection.setRemoteDescription(new this.SessionDescription(answer), //Trigger peer connected and emit to let peer know that we're good function () { this.peerConnected(peer.id); this.socket.emit('peerconnected', peer.id); }.bind(this), this.logError); }; //Event fired when our peer is connected this.peerConnected = function (id) { this.emit('peerconnected', id); if (options.debug) console.log('Signaling with peer: ' + id); }; //Disconnect a peer this.disconnectConnection = function (id) { //Get our peer var peer = this.getPeer(id); //Close connection peer.connection.close(); //Proxy events out this.emit('peerDisconnected', id); this.emit('remoteStreamRemoved', id); if (options.debug) console.log('Disconnected with peer: ' + id); //Remove peer from internal array this.peerConnections.forEach(function (peer, index, peers) { if (peer.id === id) { peers.splice(index, 1); } }); }; //Find a peer by ID this.getPeer = function (id) { var foundPeer; //Match based on ID this.peerConnections.forEach(function (peer) { if (peer.id === id) { foundPeer = peer; } }); //Create peer if it doesn't exist if (!foundPeer) { foundPeer = this.addPeer(id); } return foundPeer; }; //Check if a peer exists based on ID this.hasPeer = function (id) { var foundPeer = false; this.peerConnections.forEach(function (peer) { if (peer.id === id) { foundPeer = true; } }); return foundPeer; }; //Log errors this.logError = function (error) { if (options.debug) console.error(error); else console.error('There was an error with the signaller'); }; //Add stream to all connections this.addStream = function (stream) { this.peerConnections.forEach(function (peer) { this.addStreamToPeer(stream, peer); }.bind(this)); }; //Add stream to single connection this.addStreamToPeer = function (stream, peer) { if (options.debug) console.log('Stream added to ' + peer.id); peer.connection.addStream(stream); //Need to renegotiate after a stream is added this.createOffer(peer); }; //Add local webcam and/or microphone stream //Toggle video with opts.video and audio with opts.audio this.addLocalStream = function (opts) { opts = opts || {}; //Request access navigator.getUserMedia({ audio: opts.audio || true, video: opts.video || true, //Add stream to PeerConnection }, function (stream) { //Reference to our stream this.localStream = stream; //Add stream to all peers this.addStream(stream); //Proxy stream added event this.emit('localStreamAdded', stream); if (options.debug) console.log('Local stream added'); }.bind(this), this.logError); }; //Remove stream from all connections this.removeLocalStream = function () { //Stop recording this.localStream.stop(); //Remove our local stream entirely this.localStream = undefined; this.peerConnections.forEach(function (peer) { if (options.debug) console.log('Local stream removed from ' + peer.id); this.socket.emit('streamremoved', peer.id); //Demolish stream this.regenStream(peer, true); }.bind(this)); //Pass events through this.emit('localStreamRemoved'); }; //Remove stream this.removeRemoteStream = function (id) { this.emit('remoteStreamRemoved', id); if (options.debug) console.log('Removing stream from: ' + id); //Need to regenerate stream to deal with "phantom" remote MediaStream bug(?) this.regenStream(this.getPeer(id)); }; //Regenerate a stream by re-creating its PeerConnection and re-binding all events this.regenStream = function (peer, suppress) { if (options.debug) console.log('Regenerating PeerConnection for ' + peer.id); //If we are regenerating because we're removing a local connection and there is a remote stream suppress = suppress && peer.connection.getRemoteStreams().length; //Replace PeerConnection with new connection peer.connection = new this.PeerConnection(this.iceServers); //Bind connection events this.bindConnectionEvents(peer, suppress); //Add local stream if necessary if (this.localStream) { this.addStreamToPeer(this.localStream, peer); } }; //Process ice candidate this.receiveIceCandidate = function (data) { if (options.debug && options.debug === 'verbose') console.log('Received candidate', data.candidate); //This is horrible, but that's the way the data is packaged if (data.candidate.candidate) { //Unpackage data var peer = this.getPeer(data.sender), candidate = data.candidate.candidate, line = data.candidate.sdpMLineIndex; if (options.debug && options.debug === 'verbose') console.log('Added ICE candidate from ' + peer.id); //Suppress error message if we're debugging after unnecessary ice candidates are sent - causes like 30 errors if you pause during connection peer.connection.addIceCandidate(new this.RTCIceCandidate({ candidate: candidate, sdpMLineIndex: line, })); } }; //Get our prefixed RTC objects this.setupRTCObjects(); //Options defaults options = this.generateDefaults(options); //Set up our socket connection this.socket = io(options.server + options.room); //Bind our events this.bindEvents(); } PeerConnectionClient.prototype = new EventEmitter(); return PeerConnectionClient; }));