UNPKG

12.7 kBJavaScriptView Raw
1var util = require('util');
2var webrtc = require('webrtcsupport');
3var PeerConnection = require('rtcpeerconnection');
4var WildEmitter = require('wildemitter');
5var mockconsole = require('mockconsole');
6var localMedia = require('localmedia');
7
8
9function WebRTC(opts) {
10 var self = this;
11 var options = opts || {};
12 var config = this.config = {
13 debug: false,
14 // makes the entire PC config overridable
15 peerConnectionConfig: {
16 iceServers: [{"url": "stun:stun.l.google.com:19302"}]
17 },
18 peerConnectionContraints: {
19 optional: [
20 {DtlsSrtpKeyAgreement: true}
21 ]
22 },
23 receiveMedia: {
24 mandatory: {
25 OfferToReceiveAudio: true,
26 OfferToReceiveVideo: true
27 }
28 },
29 enableDataChannels: true
30 };
31 var item;
32
33 // expose screensharing check
34 this.screenSharingSupport = webrtc.screenSharing;
35
36 // We also allow a 'logger' option. It can be any object that implements
37 // log, warn, and error methods.
38 // We log nothing by default, following "the rule of silence":
39 // http://www.linfo.org/rule_of_silence.html
40 this.logger = function () {
41 // we assume that if you're in debug mode and you didn't
42 // pass in a logger, you actually want to log as much as
43 // possible.
44 if (opts.debug) {
45 return opts.logger || console;
46 } else {
47 // or we'll use your logger which should have its own logic
48 // for output. Or we'll return the no-op.
49 return opts.logger || mockconsole;
50 }
51 }();
52
53 // set options
54 for (item in options) {
55 this.config[item] = options[item];
56 }
57
58 // check for support
59 if (!webrtc.support) {
60 this.logger.error('Your browser doesn\'t seem to support WebRTC');
61 }
62
63 // where we'll store our peer connections
64 this.peers = [];
65
66 // call localMedia constructor
67 localMedia.call(this, this.config);
68
69 this.on('speaking', function () {
70 if (!self.hardMuted) {
71 // FIXME: should use sendDirectlyToAll, but currently has different semantics wrt payload
72 self.peers.forEach(function (peer) {
73 if (peer.enableDataChannels) {
74 var dc = peer.getDataChannel('hark');
75 if (dc.readyState != 'open') return;
76 dc.send(JSON.stringify({type: 'speaking'}));
77 }
78 });
79 }
80 });
81 this.on('stoppedSpeaking', function () {
82 if (!self.hardMuted) {
83 // FIXME: should use sendDirectlyToAll, but currently has different semantics wrt payload
84 self.peers.forEach(function (peer) {
85 if (peer.enableDataChannels) {
86 var dc = peer.getDataChannel('hark');
87 if (dc.readyState != 'open') return;
88 dc.send(JSON.stringify({type: 'stoppedSpeaking'}));
89 }
90 });
91 }
92 });
93 this.on('volumeChange', function (volume, treshold) {
94 if (!self.hardMuted) {
95 // FIXME: should use sendDirectlyToAll, but currently has different semantics wrt payload
96 self.peers.forEach(function (peer) {
97 if (peer.enableDataChannels) {
98 var dc = peer.getDataChannel('hark');
99 if (dc.readyState != 'open') return;
100 dc.send(JSON.stringify({type: 'volume', volume: volume }));
101 }
102 });
103 }
104 });
105
106 // log events in debug mode
107 if (this.config.debug) {
108 this.on('*', function (event, val1, val2) {
109 var logger;
110 // if you didn't pass in a logger and you explicitly turning on debug
111 // we're just going to assume you're wanting log output with console
112 if (self.config.logger === mockconsole) {
113 logger = console;
114 } else {
115 logger = self.logger;
116 }
117 logger.log('event:', event, val1, val2);
118 });
119 }
120}
121
122util.inherits(WebRTC, localMedia);
123
124WebRTC.prototype.createPeer = function (opts) {
125 var peer;
126 opts.parent = this;
127 peer = new Peer(opts);
128 this.peers.push(peer);
129 return peer;
130};
131
132// removes peers
133WebRTC.prototype.removePeers = function (id, type) {
134 this.getPeers(id, type).forEach(function (peer) {
135 peer.end();
136 });
137};
138
139// fetches all Peer objects by session id and/or type
140WebRTC.prototype.getPeers = function (sessionId, type) {
141 return this.peers.filter(function (peer) {
142 return (!sessionId || peer.id === sessionId) && (!type || peer.type === type);
143 });
144};
145
146// sends message to all
147WebRTC.prototype.sendToAll = function (message, payload) {
148 this.peers.forEach(function (peer) {
149 peer.send(message, payload);
150 });
151};
152
153// sends message to all using a datachannel
154// only sends to anyone who has an open datachannel
155WebRTC.prototype.sendDirectlyToAll = function (channel, message, payload) {
156 this.peers.forEach(function (peer) {
157 if (peer.enableDataChannels) {
158 peer.sendDirectly(channel, message, payload);
159 }
160 });
161};
162
163function Peer(options) {
164 var self = this;
165
166 this.id = options.id;
167 this.parent = options.parent;
168 this.type = options.type || 'video';
169 this.oneway = options.oneway || false;
170 this.sharemyscreen = options.sharemyscreen || false;
171 this.browserPrefix = options.prefix;
172 this.stream = options.stream;
173 this.enableDataChannels = options.enableDataChannels === undefined ? this.parent.config.enableDataChannels : options.enableDataChannels;
174 this.receiveMedia = options.receiveMedia || this.parent.config.receiveMedia;
175 this.channels = {};
176 // Create an RTCPeerConnection via the polyfill
177 this.pc = new PeerConnection(this.parent.config.peerConnectionConfig, this.parent.config.peerConnectionContraints);
178 this.pc.on('ice', this.onIceCandidate.bind(this));
179 this.pc.on('addStream', this.handleRemoteStreamAdded.bind(this));
180 this.pc.on('addChannel', this.handleDataChannelAdded.bind(this));
181 this.pc.on('removeStream', this.handleStreamRemoved.bind(this));
182 // Just fire negotiation needed events for now
183 // When browser re-negotiation handling seems to work
184 // we can use this as the trigger for starting the offer/answer process
185 // automatically. We'll just leave it be for now while this stabalizes.
186 this.pc.on('negotiationNeeded', this.emit.bind(this, 'negotiationNeeded'));
187 this.pc.on('iceConnectionStateChange', this.emit.bind(this, 'iceConnectionStateChange'));
188 this.pc.on('iceConnectionStateChange', function () {
189 switch (self.pc.iceConnectionState) {
190 case 'failed':
191 // currently, in chrome only the initiator goes to failed
192 // so we need to signal this to the peer
193 if (self.pc.pc.peerconnection.localDescription.type === 'offer') {
194 self.parent.emit('iceFailed', self);
195 self.send('connectivityError');
196 }
197 break;
198 }
199 });
200 this.pc.on('signalingStateChange', this.emit.bind(this, 'signalingStateChange'));
201 this.logger = this.parent.logger;
202
203 // handle screensharing/broadcast mode
204 if (options.type === 'screen') {
205 if (this.parent.localScreen && this.sharemyscreen) {
206 this.logger.log('adding local screen stream to peer connection');
207 this.pc.addStream(this.parent.localScreen);
208 this.broadcaster = options.broadcaster;
209 }
210 } else {
211 this.parent.localStreams.forEach(function (stream) {
212 self.pc.addStream(stream);
213 });
214 }
215
216 // call emitter constructor
217 WildEmitter.call(this);
218
219 // proxy events to parent
220 this.on('*', function () {
221 self.parent.emit.apply(self.parent, arguments);
222 });
223}
224
225Peer.prototype = Object.create(WildEmitter.prototype, {
226 constructor: {
227 value: Peer
228 }
229});
230
231Peer.prototype.handleMessage = function (message) {
232 var self = this;
233
234 this.logger.log('getting', message.type, message);
235
236 if (message.prefix) this.browserPrefix = message.prefix;
237
238 if (message.type === 'offer') {
239 this.pc.handleOffer(message.payload, function (err) {
240 if (err) {
241 return;
242 }
243 // auto-accept
244 self.pc.answer(self.receiveMedia, function (err, sessionDescription) {
245 self.send('answer', sessionDescription);
246 });
247 });
248 } else if (message.type === 'answer') {
249 this.pc.handleAnswer(message.payload);
250 } else if (message.type === 'candidate') {
251 this.pc.processIce(message.payload);
252 } else if (message.type === 'connectivityError') {
253 this.parent.emit('connectivityError', self);
254 } else if (message.type === 'mute') {
255 this.parent.emit('mute', {id: message.from, name: message.payload.name});
256 } else if (message.type === 'unmute') {
257 this.parent.emit('unmute', {id: message.from, name: message.payload.name});
258 }
259};
260
261// send via signalling channel
262Peer.prototype.send = function (messageType, payload) {
263 var message = {
264 to: this.id,
265 broadcaster: this.broadcaster,
266 roomType: this.type,
267 type: messageType,
268 payload: payload,
269 prefix: webrtc.prefix
270 };
271 this.logger.log('sending', messageType, message);
272 this.parent.emit('message', message);
273};
274
275// send via data channel
276// returns true when message was sent and false if channel is not open
277Peer.prototype.sendDirectly = function (channel, messageType, payload) {
278 var message = {
279 type: messageType,
280 payload: payload
281 };
282 this.logger.log('sending via datachannel', channel, messageType, message);
283 var dc = this.getDataChannel(channel);
284 if (dc.readyState != 'open') return false;
285 dc.send(JSON.stringify(message));
286 return true;
287};
288
289// Internal method registering handlers for a data channel and emitting events on the peer
290Peer.prototype._observeDataChannel = function (channel) {
291 var self = this;
292 channel.onclose = this.emit.bind(this, 'channelClose', channel);
293 channel.onerror = this.emit.bind(this, 'channelError', channel);
294 channel.onmessage = function (event) {
295 self.emit('channelMessage', self, channel.label, JSON.parse(event.data), channel, event);
296 };
297 channel.onopen = this.emit.bind(this, 'channelOpen', channel);
298};
299
300// Fetch or create a data channel by the given name
301Peer.prototype.getDataChannel = function (name, opts) {
302 if (!webrtc.dataChannel) return this.emit('error', new Error('createDataChannel not supported'));
303 var channel = this.channels[name];
304 opts || (opts = {});
305 if (channel) return channel;
306 // if we don't have one by this label, create it
307 channel = this.channels[name] = this.pc.createDataChannel(name, opts);
308 this._observeDataChannel(channel);
309 return channel;
310};
311
312Peer.prototype.onIceCandidate = function (candidate) {
313 if (this.closed) return;
314 if (candidate) {
315 this.send('candidate', candidate);
316 } else {
317 this.logger.log("End of candidates.");
318 }
319};
320
321Peer.prototype.start = function () {
322 var self = this;
323
324 // well, the webrtc api requires that we either
325 // a) create a datachannel a priori
326 // b) do a renegotiation later to add the SCTP m-line
327 // Let's do (a) first...
328 if (this.enableDataChannels) {
329 this.getDataChannel('simplewebrtc');
330 }
331
332 this.pc.offer(this.receiveMedia, function (err, sessionDescription) {
333 self.send('offer', sessionDescription);
334 });
335};
336
337Peer.prototype.end = function () {
338 if (this.closed) return;
339 this.pc.close();
340 this.handleStreamRemoved();
341};
342
343Peer.prototype.handleRemoteStreamAdded = function (event) {
344 var self = this;
345 if (this.stream) {
346 this.logger.warn('Already have a remote stream');
347 } else {
348 this.stream = event.stream;
349 // FIXME: addEventListener('ended', ...) would be nicer
350 // but does not work in firefox
351 this.stream.onended = function () {
352 self.end();
353 };
354 this.parent.emit('peerStreamAdded', this);
355 }
356};
357
358Peer.prototype.handleStreamRemoved = function () {
359 this.parent.peers.splice(this.parent.peers.indexOf(this), 1);
360 this.closed = true;
361 this.parent.emit('peerStreamRemoved', this);
362};
363
364Peer.prototype.handleDataChannelAdded = function (channel) {
365 this.channels[channel.label] = channel;
366 this._observeDataChannel(channel);
367};
368
369module.exports = WebRTC;