rtc-quickconnect
Version:
Create a WebRTC connection in record time
1,087 lines (863 loc) • 33.4 kB
JavaScript
/* jshint node: true */
/* global location */
;
var rtc = require('rtc-tools');
var mbus = require('mbus');
var detectPlugin = require('rtc-core/plugin');
var debug = rtc.logger('rtc-quickconnect');
var extend = require('cog/extend');
var sdpSupport = require('./lib/sdpsupport');
/**
# rtc-quickconnect
This is a high level helper module designed to help you get up
an running with WebRTC really, really quickly. By using this module you
are trading off some flexibility, so if you need a more flexible
configuration you should drill down into lower level components of the
[rtc.io](http://www.rtc.io) suite. In particular you should check out
[rtc](https://github.com/rtc-io/rtc).
## Example Usage
In the simplest case you simply call quickconnect with a single string
argument which tells quickconnect which server to use for signaling:
<<< examples/simple.js
<<< docs/events.md
<<< docs/examples.md
## Regarding Signalling and a Signalling Server
Signaling is an important part of setting up a WebRTC connection and for
our examples we use our own test instance of the
[rtc-switchboard](https://github.com/rtc-io/rtc-switchboard). For your
testing and development you are more than welcome to use this also, but
just be aware that we use this for our testing so it may go up and down
a little. If you need something more stable, why not consider deploying
an instance of the switchboard yourself - it's pretty easy :)
## Reference
```
quickconnect(signalhost, opts?) => rtc-sigaller instance (+ helpers)
```
### Valid Quick Connect Options
The options provided to the `rtc-quickconnect` module function influence the
behaviour of some of the underlying components used from the rtc.io suite.
Listed below are some of the commonly used options:
- `ns` (default: '')
An optional namespace for your signalling room. While quickconnect
will generate a unique hash for the room, this can be made to be more
unique by providing a namespace. Using a namespace means two demos
that have generated the same hash but use a different namespace will be
in different rooms.
- `room` (default: null) _added 0.6_
Rather than use the internal hash generation
(plus optional namespace) for room name generation, simply use this room
name instead. __NOTE:__ Use of the `room` option takes precendence over
`ns`.
- `debug` (default: false)
Write rtc.io suite debug output to the browser console.
- `expectedLocalStreams` (default: not specified) _added 3.0_
By providing a positive integer value for this option will mean that
the created quickconnect instance will wait until the specified number of
streams have been added to the quickconnect "template" before announcing
to the signaling server.
- `manualJoin` (default: `false`)
Set this value to `true` if you would prefer to call the `join` function
to connecting to the signalling server, rather than having that happen
automatically as soon as quickconnect is ready to.
#### Options for Peer Connection Creation
Options that are passed onto the
[rtc.createConnection](https://github.com/rtc-io/rtc#createconnectionopts-constraints)
function:
- `iceServers`
This provides a list of ice servers that can be used to help negotiate a
connection between peers.
#### Options for P2P negotiation
Under the hood, quickconnect uses the
[rtc/couple](https://github.com/rtc-io/rtc#rtccouple) logic, and the options
passed to quickconnect are also passed onto this function.
**/
module.exports = function (signalhost, opts) {
var hash = typeof location != 'undefined' && location.hash.slice(1);
var signaller = require('rtc-pluggable-signaller')(extend({
signaller: signalhost,
// use the primus endpoint as a fallback in case we are talking to an
// older switchboard instance
endpoints: ['/', '/primus']
}, opts));
var getPeerData = require('./lib/getpeerdata')(signaller.peers);
var generateIceServers = require('rtc-core/genice');
// init configurable vars
var ns = (opts || {}).ns || '';
var room = (opts || {}).room;
var debugging = (opts || {}).debug;
var allowJoin = !(opts || {}).manualJoin;
var profile = {};
var announced = false;
// Schemes allow customisation about how connections are made
// In particular, providing schemes allows providing different sets of ICE servers
// between peers
var schemes = require('./lib/schemes')(signaller, opts);
// collect the local streams
var localStreams = [];
// create the calls map
var calls = signaller.calls = require('./lib/calls')(signaller, opts);
// create the known data channels registry
var channels = {};
var pending = {};
// Reconnecting indicates peers that are in the process of reconnecting
var reconnecting = {};
// save the plugins passed to the signaller
var plugins = signaller.plugins = (opts || {}).plugins || [];
var plugin = detectPlugin(plugins);
var pluginReady;
// check how many local streams have been expected (default: 0)
var expectedLocalStreams = parseInt((opts || {}).expectedLocalStreams, 10) || 0;
var announceTimer = 0;
var updateTimer = 0;
var CLOSED_STATES = ['failed', 'closed'];
function checkReadyToAnnounce() {
clearTimeout(announceTimer);
// if we have already announced do nothing!
if (announced) {
return;
}
if (!allowJoin) {
return;
}
// if we have a plugin but it's not initialized we aren't ready
if (plugin && (!pluginReady)) {
return;
}
// if we are waiting for a set number of streams, then wait until we have
// the required number
if (expectedLocalStreams && localStreams.length < expectedLocalStreams) {
return;
}
// announce ourselves to our new friend
announceTimer = setTimeout(function () {
var data = extend({ room: room }, profile);
// announce and emit the local announce event
signaller.announce(data);
announced = true;
}, 0);
}
function connect(id, connectOpts) {
debug('connecting to ' + id);
if (!id) return debug('invalid target peer ID');
if (pending[id]) {
return debug('a connection is already pending for ' + id + ', as of ' + (Date.now() - pending[id]) + 'ms ago');
}
connectOpts = connectOpts || {};
var scheme = schemes.get(connectOpts.scheme, true);
var data = getPeerData(id);
var pc;
var monitor;
var call;
// if the room is not a match, abort
if (data.room !== room) {
return debug('mismatching room, expected: ' + room + ', got: ' + (data && data.room));
}
if (data.id !== id) {
return debug('mismatching ids, expected: ' + id + ', got: ' + data.id);
}
// Allow prevention of connections if required
if (scheme && typeof scheme.allowConnection === 'function' && !scheme.allowConnection(id, data)) {
signaller('peer:rejected', id, data, scheme);
return debug('peer connection was rejected for ' + id);
}
pending[id] = Date.now();
// end any call to this id so we know we are starting fresh
calls.end(id);
signaller('peer:prepare', id, data, scheme);
function clearPending(msg) {
debug('connection for ' + id + ' is no longer pending [' + (msg || 'no reason') + '], connect available again');
if (pending[id]) {
delete pending[id];
}
if (reconnecting[id]) {
delete reconnecting[id];
}
}
// Regenerate ICE servers (or use existing cached ICE)
generateIceServers(extend({ targetPeer: id }, opts, (scheme || {}).connection), function (err, iceServers) {
if (err) {
signaller('icegeneration:error', id, scheme && scheme.id, err);
} else {
signaller('peer:iceservers', id, scheme && scheme.id, iceServers || []);
}
// Generate the connection options for the provided options and default values
var connectionOptions = extend({
sdpSemantics: sdpSupport.detectTargetSemantics(signaller, data)
}, opts, { iceServers: iceServers });
// create a peer connection
// iceServers that have been created using genice taking precendence
pc = rtc.createConnection(connectionOptions, (opts || {}).constraints);
signaller('peer:connect', id, pc, data);
// add this connection to the calls list
call = calls.create(id, pc, data, connectionOptions);
// add the local streams/tracks
localStreams.forEach(function (stream) {
if (pc.addTrack){
stream.getAudioTracks().concat(stream.getVideoTracks()).forEach(
track => pc.addTrack(track, stream));
}
else {
pc.addStream(stream);
}
// Fire the couple event
signaller('peer:localMediaAdded', id, pc, data);
});
// add any necessary data channels
Object.keys(channels).forEach(function (label) {
addDataChannelToConnection(pc, id, label);
});
// Watch for any datachannel creation from in-band negotiated channels
pc.ondatachannel = function (evt) {
var channel = evt && evt.channel;
// if we have no channel, abort
if (!channel) {
return;
}
if (channels[channel.label] !== undefined) {
gotPeerChannel(channel, pc, getPeerData(id));
}
};
// couple the connections
debug('coupling ' + signaller.id + ' to ' + id);
monitor = rtc.couple(pc, id, signaller, extend({}, opts, {
logger: mbus('pc.' + id, signaller)
}));
// Apply the monitor to the call
call.monitor = monitor;
// once active, trigger the peer connect event
monitor.once('connected', function () {
clearPending('connected successfully');
calls.start(id, pc, data);
});
monitor.once('closed', function () {
clearPending('closed');
calls.end(id);
});
monitor.once('aborted', function () {
clearPending('aborted');
});
monitor.once('failed', function () {
clearPending('failed');
calls.fail(id);
});
// The following states are intermediate states based on the disconnection timer
monitor.once('failing', calls.failing.bind(null, id));
monitor.once('recovered', calls.recovered.bind(null, id));
// Fire the couple event
signaller('peer:couple', id, pc, data, monitor);
// if we are the master connnection, create the offer
// NOTE: this only really for the sake of politeness, as rtc couple
// implementation handles the slave attempting to create an offer
if (signaller.isMaster(id)) {
monitor.createOffer();
}
signaller('peer:prepared', id);
});
}
function getActiveCall(peerId) {
var call = calls.get(peerId);
if (!call) {
throw new Error('No active call for peer: ' + peerId);
}
return call;
}
/**
Adds a data channel to a peer connection, if required.
The channel will be added to the connection under the following situations:
1. This is the master peer in the connection to `targetPeer`
2. `opts.negotiated` is true
*/
function addDataChannelToConnection(pc, targetPeer, label) {
if (!pc || !targetPeer || !label) return;
var channelId = Object.keys(channels).indexOf(label);
if (channelId === -1) return console.warn('Channel ID not found');
var channelOpts = channels[label] || {};
var negotiated = !!channelOpts.negotiated;
// If negotiated, allow auto generation of the channel ID
if (negotiated) {
channelOpts = extend({ id: channelId }, channelOpts);
}
// If not negotiated, we have to be the master in order to create
if (!negotiated && !signaller.isMaster(targetPeer)) return;
var data = getPeerData(targetPeer);
gotPeerChannel(pc.createDataChannel(label, channelOpts), pc, data);
}
function gotPeerChannel(channel, pc, data) {
var channelMonitor;
var channelConnectionTimer;
function channelReady() {
var call = calls.get(data.id);
var args = [data.id, channel, data, pc];
// decouple the channel.onopen listener
debug('reporting channel "' + channel.label + '" ready, have call: ' + (!!call));
clearInterval(channelMonitor);
clearTimeout(channelConnectionTimer);
channel.onopen = null;
// save the channel
if (call) {
call.channels.set(channel.label, channel);
// Remove the channel from the call on close, and emit the required events
channel.onclose = function () {
debug('channel "' + channel.label + '" to ' + data.id + ' has closed');
var args = [data.id, channel, channel.label];
// decouple the events
channel.onopen = null;
// Remove the channel entry
call.channels.remove(channel.label);
// emit the plain channel:closed event
signaller.apply(signaller, ['channel:closed'].concat(args));
// emit the labelled version of the event
signaller.apply(signaller, ['channel:closed:' + channel.label].concat(args));
}
}
// trigger the %channel.label%:open event
debug('triggering channel:opened events for channel: ' + channel.label);
// emit the plain channel:opened event
signaller.apply(signaller, ['channel:opened'].concat(args));
// emit the channel:opened:%label% eve
signaller.apply(
signaller,
['channel:opened:' + channel.label].concat(args)
);
}
// If the channel has failed to create for some reason, recreate the channel
function recreateChannel() {
debug('recreating data channel: ' + channel.label);
// Clear timers
clearInterval(channelMonitor);
clearTimeout(channelConnectionTimer);
// Force the channel to close if it is in an open state
if (['connecting', 'open'].indexOf(channel.readyState) !== -1) {
channel.close();
}
// Recreate the channel using the cached options
signaller.createDataChannel(channel.label, channels[channel.label])
}
debug('channel ' + channel.label + ' discovered for peer: ' + data.id);
if (channel.readyState === 'open') {
return channelReady();
}
debug('channel not ready, current state = ' + channel.readyState);
channel.onopen = channelReady;
// monitor the channel open (don't trust the channel open event just yet)
channelMonitor = setInterval(function () {
debug('checking channel state, current state = ' + channel.readyState + ', connection state ' + pc.iceConnectionState);
if (channel.readyState === 'open') {
channelReady();
}
// If the underlying connection has failed/closed, or if the ready state of the channel has transitioned to a closure
// state, then terminate the monitor
else if (CLOSED_STATES.indexOf(pc.iceConnectionState) !== -1 || CLOSED_STATES.indexOf(channel.readyState) !== -1) {
debug('connection or channel has terminated, cancelling channel monitor');
clearInterval(channelMonitor);
clearTimeout(channelConnectionTimer);
}
// If the connection has connected, but the channel is stuck in the connecting state
// start a timer. If this expires, then we will attempt to created the data channel
else if (pc.iceConnectionState === 'connected' && channel.readyState === 'connecting' && !channelConnectionTimer) {
channelConnectionTimer = setTimeout(function () {
if (channel.readyState !== 'connecting') return;
var args = [data.id, channel, data, pc];
// emit the plain channel:failed event
signaller.apply(signaller, ['channel:failed'].concat(args));
// emit the channel:opened:%label% eve
signaller.apply(
signaller,
['channel:failed:' + channel.label].concat(args)
);
// Recreate the channel
return recreateChannel();
}, (opts || {}).channelTimeout || 2000);
}
}, 500);
}
function initPlugin() {
return plugin && plugin.init(opts, function (err) {
if (err) {
return console.error('Could not initialize plugin: ', err);
}
pluginReady = true;
checkReadyToAnnounce();
});
}
function handleLocalAnnounce(data) {
// if we send an announce with an updated room then update our local room name
if (data && typeof data.room != 'undefined') {
room = data.room;
}
}
function handlePeerFilter(id, data) {
// only connect with the peer if we are ready
data.allow = data.allow && (localStreams.length >= expectedLocalStreams);
}
function handlePeerUpdate(data) {
// Do not allow peer updates if we are not announced
if (!announced) return;
var id = data && data.id;
var activeCall = id && calls.get(id);
// if we have received an update for a peer that has no active calls,
// and is not currently in the process of setting up a call
// then pass this onto the announce handler
if (id && (!activeCall) && !pending[id] && !reconnecting[id]) {
debug('received peer update from peer ' + id + ', no active calls');
signaller('peer:autoreconnect', id);
return signaller.reconnectTo(id);
}
}
function handlePeerLeave(data) {
var id = data && data.id;
if (id) {
delete pending[id];
calls.end(id);
}
}
function handlePeerClose(id) {
if (!announced) return;
delete pending[id];
debug('call has from ' + signaller.id + ' to ' + id + ' has ended, reannouncing');
return signaller.profile();
}
// if the room is not defined, then generate the room name
if (!room) {
// if the hash is not assigned, then create a random hash value
if (typeof location != 'undefined' && (!hash)) {
hash = location.hash = '' + (Math.pow(2, 53) * Math.random());
}
room = ns + '#' + hash;
}
if (debugging) {
rtc.logger.enable.apply(rtc.logger, Array.isArray(debug) ? debugging : ['*']);
}
signaller.on('peer:announce', function (data) {
connect(data.id, { scheme: data.scheme });
});
signaller.on('peer:update', handlePeerUpdate);
signaller.on('message:reconnect', function (data, sender, message) {
debug('received reconnect message');
// Sender arguments are always last
if (!message) {
message = sender;
sender = data;
data = undefined;
}
if (!sender.id) return console.warn('Could not reconnect, no sender ID');
// Abort any current calls
calls.abort(sender.id);
delete reconnecting[sender.id];
signaller('peer:reconnecting', sender.id, data || {});
// Reapply sender information in case aborts caused it to be removed
signaller.peers.set(sender.id, sender);
connect(sender.id, data || {});
// If this is the master, echo the reconnection back to the peer instructing that
// the reconnection has been accepted and to connect
var isMaster = signaller.isMaster(sender.id);
if (isMaster) {
signaller.to(sender.id).send('/reconnect', data || {});
}
});
/**
### Quickconnect Broadcast and Data Channel Helper Functions
The following are functions that are patched into the `rtc-signaller`
instance that make working with and creating functional WebRTC applications
a lot simpler.
**/
/**
#### addStream
```
addStream(stream:MediaStream) => qc
```
Add the stream to active calls and also save the stream so that it
can be added to future calls.
**/
signaller.broadcast = signaller.addStream = function (stream) {
localStreams.push(stream);
// if we have any active calls, then add the stream
calls.keys().forEach(function (id) {
// get call obj
var call = calls.get(id);
// check addTrack or addStream
if (call.pc.addTrack) {
// Firefox + Chrome 64 and above
stream.getAudioTracks().concat(stream.getVideoTracks()).forEach(function (track) {
debug('addTrack trackId:',track.id, ',streamId:',stream.id);
call.pc.addTrack(track, stream);
});
} else {
// Upto chrome 63
call.pc.addStream(stream);
}
signaller('peer:localMediaAdded', id, call.pc);
});
checkReadyToAnnounce();
return signaller;
};
/**
#### addTrack
The `addTrack` function terminates do the pure WebRTC addTrack logic.
**/
signaller.addTrack = function (track, stream) {
if(localStreams.indexOf(stream) == -1){
localStreams.push(stream);
}
// if we have any active calls, then add the stream
calls.values().forEach(function (call) {
call.pc.addTrack(track, stream);
});
checkReadyToAnnounce();
return signaller;
};
/**
#### replaceTrack
The `replaceTrack` function call web WebRTC API for replaceTrack (sender)
replaceTrack(newTrack, oldTrackId)
**/
signaller.replaceTrack = function (track, trackId) {
// all tracks
var allTracks = [];
localStreams.forEach(s=>{
allTracks = allTracks.concat(s.getTracks());
});
// find existing track
var removingTrack = allTracks.find(t=>t.id === trackId);
if (removingTrack){
var replacingStream = localStreams.find(s=>
s.getTracks().find(t=>t === removingTrack));
// removing
replacingStream.removeTrack(removingTrack);
// add new track
replacingStream.addTrack(track);
}
else{
console.error('cannot find the track to be replaced:', trackId, track);
return;
}
// replace pc track
calls.values().forEach(function(call) {
var sender = call.pc.getSenders().find(function(s) {
return s.track.id === removingTrack.id;
});
if (sender){
// found the sender, then replace the track
sender.replaceTrack(track);
}
else{
console.error('cannot find sender:', trackId, track);
}
});
return signaller;
};
/**
#### endCall
The `endCall` function terminates the active call with the given ID.
If a call with the call ID does not exist it will do nothing.
**/
signaller.endCall = calls.end;
/**
#### endCalls()
The `endCalls` function terminates all the active calls that have been
created in this quickconnect instance. Calling `endCalls` does not
kill the connection with the signalling server.
**/
signaller.endCalls = function () {
calls.keys().forEach(calls.end);
};
/**
#### close()
The `close` function provides a convenient way of closing all associated
peer connections. This function simply uses the `endCalls` function and
the underlying `leave` function of the signaller to do a "full cleanup"
of all connections.
**/
signaller.close = function () {
// We are no longer announced
announced = false;
// Remove any pending update annoucements
if (updateTimer) clearTimeout(updateTimer);
// Cleanup
signaller.endCalls();
signaller.leave();
};
/**
#### createDataChannel(label, config)
Request that a data channel with the specified `label` is created on
the peer connection. When the data channel is open and available, an
event will be triggered using the label of the data channel.
For example, if a new data channel was requested using the following
call:
```js
var qc = quickconnect('https://switchboard.rtc.io/').createDataChannel('test');
```
Then when the data channel is ready for use, a `test:open` event would
be emitted by `qc`.
**/
signaller.createDataChannel = function (label, opts) {
// save the data channel opts in the local channels dictionary
channels[label] = opts || null;
// create a channel on all existing calls
calls.keys().forEach(function (peerId) {
var call = calls.get(peerId);
var dc;
// if we are the master connection, create the data channel
if (call && call.pc) {
var existingChannel = call.channels.get(label);
if (existingChannel && existingChannel.readyState !== 'closed') {
return debug('Attempted to create data channel "' + label + '" for a call to ' + peerId + ' but an open channel already exists');
}
// Add the datachannel (if required)
addDataChannelToConnection(call.pc, peerId, label);
}
});
return signaller;
};
/**
#### join()
The `join` function is used when `manualJoin` is set to true when creating
a quickconnect instance. Call the `join` function once you are ready to
join the signalling server and initiate connections with other people.
**/
signaller.join = function () {
allowJoin = true;
checkReadyToAnnounce();
};
/**
#### `get(name)`
The `get` function returns the property value for the specified property name.
**/
signaller.get = function (name) {
return profile[name];
};
/**
#### `getLocalStreams()`
Return a copy of the local streams that have currently been configured
**/
signaller.getLocalStreams = function () {
return [].concat(localStreams);
};
/**
#### reactive()
Flag that this session will be a reactive connection.
**/
signaller.reactive = function () {
// add the reactive flag
opts = opts || {};
opts.reactive = true;
// chain
return signaller;
};
/**
#### registerScheme
Registers a connection scheme for use, and check it for validity
**/
signaller.registerScheme = schemes.add;
/**
#### getSche,e
Returns the connection sheme given by ID
**/
signaller.getScheme = schemes.get;
/**
#### removeStream
```
removeStream(stream:MediaStream)
```
Remove the specified stream from both the local streams that are to
be connected to new peers, and also from any active calls.
**/
signaller.removeStream = function (stream) {
var localIndex = localStreams.indexOf(stream);
// remove the stream from any active calls
calls.values().forEach(function (call) {
// If `RTCPeerConnection.removeTrack` exists (Firefox), then use that
// as `RTCPeerConnection.removeStream` is not supported
if (call.pc.removeTrack) {
stream.getTracks().forEach(function (track) {
try {
call.pc.removeTrack(call.pc.getSenders().find(function (sender) { return sender.track == track; }));
} catch (e) {
// When using LocalMediaStreamTracks, this seems to throw an error due to
// LocalMediaStreamTrack not implementing the RTCRtpSender inteface.
// Without `removeStream` and with `removeTrack` not allowing for local stream
// removal, this needs some thought when dealing with FF renegotiation
console.error('Error removing media track', e);
}
});
}
// Otherwise we just use `RTCPeerConnection.removeStream`
else {
try {
call.pc.removeStream(stream);
} catch (e) {
console.error('Failed to remove media stream', e);
}
}
});
// remove the stream from the localStreams array
if (localIndex >= 0) {
localStreams.splice(localIndex, 1);
}
return signaller;
};
/**
#### requestChannel
```
requestChannel(targetId, label, callback)
```
This is a function that can be used to respond to remote peers supplying
a data channel as part of their configuration. As per the `receiveStream`
function this function will either fire the callback immediately if the
channel is already available, or once the channel has been discovered on
the call.
**/
signaller.requestChannel = function (targetId, label, callback) {
var call = getActiveCall(targetId);
var channel = call && call.channels.get(label);
// if we have then channel trigger the callback immediately
if (channel) {
callback(null, channel);
return signaller;
}
// if not, wait for it
signaller.once('channel:opened:' + label, function (id, dc) {
callback(null, dc);
});
return signaller;
};
/**
#### requestStream
```
requestStream(targetId, idx, callback)
```
Used to request a remote stream from a quickconnect instance. If the
stream is already available in the calls remote streams, then the callback
will be triggered immediately, otherwise this function will monitor
`stream:added` events and wait for a match.
In the case that an unknown target is requested, then an exception will
be thrown.
**/
signaller.requestStream = function (targetId, idx, callback) {
var call = getActiveCall(targetId);
var stream;
function waitForStream(peerId) {
if (peerId !== targetId) {
return;
}
// get the stream
stream = call.pc.getRemoteStreams()[idx];
// if we have the stream, then remove the listener and trigger the cb
if (stream) {
signaller.removeListener('stream:added', waitForStream);
callback(null, stream);
}
}
// look for the stream in the remote streams of the call
stream = call.pc.getRemoteStreams()[idx];
// if we found the stream then trigger the callback
if (stream) {
callback(null, stream);
return signaller;
}
// otherwise wait for the stream
signaller.on('stream:added', waitForStream);
return signaller;
};
/**
#### profile(data)
Update the profile data with the attached information, so when
the signaller announces it includes this data in addition to any
room and id information.
**/
signaller.profile = function (data) {
extend(profile, data || {});
// if we have already announced, then reannounce our profile to provide
// others a `peer:update` event
if (announced) {
clearTimeout(updateTimer);
updateTimer = setTimeout(function () {
// Check that our announced status hasn't changed
if (!announced) return;
debug('[' + signaller.id + '] reannouncing');
signaller.announce(profile);
}, (opts || {}).updateDelay || 1000);
}
return signaller;
};
/**
#### waitForCall
```
waitForCall(targetId, callback)
```
Wait for a call from the specified targetId. If the call is already
active the callback will be fired immediately, otherwise we will wait
for a `call:started` event that matches the requested `targetId`
**/
signaller.waitForCall = function (targetId, callback) {
var call = calls.get(targetId);
if (call && call.active) {
callback(null, call.pc);
return signaller;
}
signaller.on('call:started', function handleNewCall(id) {
if (id === targetId) {
signaller.removeListener('call:started', handleNewCall);
callback(null, calls.get(id).pc);
}
});
};
/**
Attempts to reconnect to a certain target peer. It will close any existing
call to that peer, and restart the connection process
**/
signaller.reconnectTo = function (id, reconnectOpts) {
if (!id) return;
signaller.to(id).send('/reconnect', reconnectOpts)
// If this is the master, connect, otherwise the master will send a /reconnect
// message back instructing the connection to start
var isMaster = signaller.isMaster(id);
if (isMaster) {
// Abort any current calls
signaller('log', 'aborting call');
// Preserve peer data
var peerData = signaller.peers.get(id);
try {
calls.abort(id);
} catch (e) {
signaller('log', e.message);
}
signaller('log', 'call aborted');
signaller('peer:reconnecting', id, reconnectOpts || {});
// Reapply peer data in case it was deleted during abort
signaller.peers.set(id, peerData);
return connect(id, reconnectOpts);
}
// Flag that we are waiting for the master to indicate the reconnection is a go
else {
reconnecting[id] = Date.now();
}
};
// if we have an expected number of local streams, then use a filter to
// check if we should respond
if (expectedLocalStreams) {
signaller.on('peer:filter', handlePeerFilter);
}
// respond to local announce messages
signaller.on('local:announce', handleLocalAnnounce);
// handle ping messages
signaller.on('message:ping', calls.ping);
// Handle when a remote peer leaves that the appropriate closing occurs this
// side as well
signaller.on('message:leave', handlePeerLeave);
// When a call:ended, we reannounce ourselves. This offers a degree of failure handling
// as if a call has dropped unexpectedly (ie. failure/unable to connect) the other peers
// connected to the signaller will attempt to reconnect
signaller.on('call:ended', handlePeerClose);
// if we plugin is active, then initialize it
if (plugin) {
initPlugin();
} else {
// Test if we are ready to announce
process.nextTick(function () {
checkReadyToAnnounce();
});
}
// pass the signaller on
return signaller;
};