Page MenuHomePhabricator

No OneTemporary

diff --git a/README.md b/README.md
index 5de9553..96a8e35 100644
--- a/README.md
+++ b/README.md
@@ -1,254 +1,258 @@
# sylkrtc.js
JavaScript library implementing the API for communicating with [SylkServer's](http://sylkserver.com) WebRTC gateway application.
## Building
Grab the source code using Darcs or Git and install the dependencies:
cd sylkrtc
./configure
Build the development release (not minified):
make
Build a minified version:
make min
## Development
Auto-building the library as changes are made:
make watch
### Debugging
sylkrtc uses the [debug](https://github.com/visionmedia/debug) library for easy debugging. By default debugging is disabled. In order to enable sylkrtc debug type the following in the browser JavaScript console:
sylkrtc.debug.enable('sylkrtc*');
Then refresh the page.
## API
The entrypoint to the library is the `sylkrtc` object. Several objects (`Connection`, `Account` and `Call`) inherit from Node's `EventEmitter` class, you may want to check [its documentation](https://nodejs.org/api/events.html).
### sylkrtc
The main entrypoint to the library. It exposes the main function to connect to SylkServer and some utility functions for general use.
#### sylkrtc.createConnection(options={})
Creates a `sylkrtc` connection towards a SylkServer instance. The only supported option (at the moment) is "server", which should point to the WebSocket endpoint of the WebRTC gateway application. Example: `wss://1.2.3.4:8088/webrtcgateway/ws`.
It returns a `Connection` object.
Example:
let connection = sylkrtc.createConnection({server: 'wss://1.2.3.4:8088/webrtcgateway/ws'});
#### sylkrtc.debug
[debug](https://github.com/visionmedia/debug) module, exposed.
Used for debugging, with the 'sylkrtc' prefix.
#### sylkrtc.rtcninja
[rtcninja](https://github.com/eface2face/rtcninja.js) module, exposed.
Used for accessing WebRTC APIs and dealing with platform differences.
#### sylkrtc.closeMediaStream(stream)
Helper function to close the given `stream`. When a local media stream is closed the camera is stopped in case it was active, for example.
Note: when a `Call` is terminated all streams will be automatically closed.
### Connection
Object representing the interaction with SylkServer. Multiple connections can be created with `sylkrtc.createConnection`, but typically only one is needed. Reconnecting in case the connection is interrupted is taken care of automatically.
Events emitted:
* **stateChanged**: indicates the WebSocket connection state has changed. Two arguments are provided: `oldState` and `newState`, the old connection state and the new connection state, respectively. Possible state values are: null, connecting, connected, ready, disconnected and closed. If the connection is involuntarily interrupted the state will transition to disconnected and the connection will be retried. Once the closed state is set, as a result of the user calling Connection.close(), the connection can no longer be used or reconnected.
#### Connection.addAccount(options={}, cb=null)
Configures an `Account` to be used through `sylkrtc`. 2 options are required: *account* (the account ID) and *password*. The account won't be registered, it will just be created. Optionally *realm* can be passed, which will be used instead of the domain for the HA1
calculation.
The *password* won't be stored or transmitted as given, the HA1 hash (as used in [Digest access authentication](https://en.wikipedia.org/wiki/Digest_access_authentication)) is created and used instead.
The `cb` argument is a callback which will be called with an error and the account object
itself.
Example:
connection.addAccount({account: saghul@sip2sip.info, password: 1234}, function(error, account) {
if (error) {
console.log('Error adding account!' + account);
} else {
console.log('Account added!');
}
});
#### Connection.removeAccount(account, cb=null)
Removes the given account. The callback will be called once the operation completes (it
cannot fail).
Example:
connection.removeAccount(account, function() {
console('Account removed!');
});
+#### Connection.reconnect()
+
+Starts reconnecting immediately if the state was 'disconnected';
+
#### Connection.close()
Close the connection with SylkServer. All accounts will be unbound.
#### Connection.state
Getter property returning the current connection state.
### Account
Object representing a SIP account which will be used for making / receiving calls.
Events emitted:
* **registrationStateChanged**: indicates the SIP registration state has changed. Three arguments are provided: `oldState`, `newState` and `data`. `oldState` and `newState` represent the old registration state and the new registration state, respectively, and `data` is a generic per-state data object. Possible states:
* null: registration hasn't started or it has ended
* registering: registration is in progress
* registered
* failed: registration failed, the `data` object will contain a 'reason' property.
* **outgoingCall**: emitted when an outgoing call is made. A single argument is provided: the `Call` object.
* **incomingCall**: emitted when an incoming call is received. Two arguments are provided: the `Call` object and a `mediaTypes`
object, which has 2 boolean properties: `audio` and `video`, indicating if those media types were present in the initial SDP.
* **missedCall**: emitted when an incoming call is missed. A `data` object is provided, which contains an `originator` attribute,
which is an `Identity` object.
#### Account.register()
Start the SIP registration process for the account. Progress will be reported via the
*registrationStateChanged* event.
Note: it's not necessary to be registered to make an outgoing call.
#### Account.unregister()
Unregister the account. Progress will be reported via the
*registrationStateChanged* event.
#### Account.call(uri, options={})
Start an outgoing call. Supported options:
* pcConfig: configuration options for `RTCPeerConnection`. [Reference](http://w3c.github.io/webrtc-pc/#configuration).
* mediaConstraints: constraints to be used when getting the local user media. [Reference](http://www.w3.org/TR/mediacapture-streams/#mediastreamconstraints).
* offerOptions: `RTCOfferOptions`. [Reference](http://w3c.github.io/webrtc-pc/#idl-def-RTCOfferOptions).
* localStream: if specified, it will be used by sylkrtc instead of using `getUserMedia`.
Example:
let call = account.call('3333@sip2sip.info', {mediaConstraints: {audio: true, video: false}});
#### Account.id
Getter property returning the account ID.
#### Account.password
Getter property returning the HA1 password for the account.
#### Account.registrationState
getter property returning the current registration state.
### Call
Object representing a audio/video call. Signalling is done using SIP underneath.
Events emitted:
* **localStreamAdded**: emitted when the local stream is added to the call. A single argument is provided: the stream itself.
* **streamAdded**: emitted when a remote stream is added to the call. A single argument is provided: the stream itself.
* **stateChanged**: indicates the call state has changed. Three arguments are provided: `oldState`, `newState` and `data`. `oldState` and `newState` indicate the previous and current state respectively, and `data` is a generic per-state data object. Possible states:
* terminated: the call has ended (the `data` object contains a `reason` attribute)
* accepted: the call has been accepted (either locally or remotely)
* incoming: initial state for incoming calls
* progress: initial state for outgoing calls
* established: call media has been established
#### Call.answer(options={})
Answer an incoming call. Supported options:
* pcConfig: configuration options for `RTCPeerConnection`. [Reference](http://w3c.github.io/webrtc-pc/#configuration).
* mediaConstraints: constraints to be used when getting the local user media. [Reference](http://www.w3.org/TR/mediacapture-streams/#mediastreamconstraints).
* answerOptions: `RTCAnswerOptions`. [Reference](http://w3c.github.io/webrtc-pc/#idl-def-RTCAnswerOptions).
* localStream: if specified, it will be used by sylkrtc instead of using `getUserMedia`.
#### Call.terminate()
End the call.
#### Call.getLocalStreams()
Returns an array of *local* `RTCMediaStream` objects.
#### Call.getRemoteStreams()
Returns an array of *remote* `RTCMediaStream` objects.
#### Call.account
Getter property which returns the `Account` object associated with this call.
#### Call.id
Getter property which returns the ID for this call. Note: this is not related to the SIP Call-ID header.
#### Call.direction
Getter property which returns the call direction: "incoming" or "outgoing". Note: this is not related to the SDP "a=" direction attribute.
#### Call.state
Getter property which returns the call state.
#### Call.localIdentity
Getter property which returns the local identity. (See the `Identity` object).
#### Call.remoteIdentity
Getter property which returns the remote identity. (See the `Identity` object).
### Identity
Object representing the identity of the caller / callee.
#### Identity.uri
SIP URI, without the 'sip:' prefix.
#### Identity.displayName
Display name assiciated with the identity. Set to '' if absent.
#### Identity.toString()
Function returning a string representation of the identity. It can take 2 forms
depending on the availability of the display name: 'bob@biloxi.com' or
'Bob <bob@biloxi.com>'.
## License
MIT. See the `LICENSE` file in this directory.
## Credits
Special thanks to [NLnet](http://nlnet.nl) for sponsoring most of the efforts behind this project.
diff --git a/lib/connection.js b/lib/connection.js
index 261d892..5820597 100644
--- a/lib/connection.js
+++ b/lib/connection.js
@@ -1,273 +1,283 @@
'use strict';
import debug from 'debug';
import uuid from 'node-uuid';
import { EventEmitter } from 'events';
import { setImmediate } from 'timers';
import { w3cwebsocket as W3CWebSocket } from 'websocket';
import { Account } from './account';
const SYLKRTC_PROTO = 'sylkRTC-1';
const DEBUG = debug('sylkrtc:Connection');
const MSECS = 1000;
const INITIAL_DELAY = 0.5 * MSECS;
const MAX_DELAY = 64 * MSECS;
class Connection extends EventEmitter {
constructor(options = {}) {
if (!options.server) {
throw new Error('\"server\" must be specified');
}
super();
this._wsUri = options.server;
this._sock = null;
this._state = null;
this._closed = false;
this._timer = null;
this._delay = INITIAL_DELAY;
this._accounts = new Map();
this._requests = new Map();
}
get state() {
return this._state;
}
close() {
if (this._closed) {
return;
}
this._closed = true;
if (this._timer) {
clearTimeout(this._timer);
this._timer = null;
}
if (this._sock) {
this._sock.close();
this._sock = null;
} else {
setImmediate(() => {
this._setState('closed');
});
}
}
addAccount(options = {}, cb = null) {
if (typeof options.account !== 'string' || typeof options.password !== 'string') {
throw new Error('Invalid options, \"account\" and \"password\" must be supplied');
}
if (this._accounts.has(options.account)) {
throw new Error('Account already added');
}
let acc = new Account(options, this);
// add it early to the set so we don't add it more than once, ever
this._accounts.set(acc.id, acc);
let req = {
sylkrtc: 'account-add',
account: acc.id,
password: acc.password
};
this._sendRequest(req, (error) => {
if (error) {
DEBUG('add_account error: %s', error);
this._accounts.delete(acc.id);
acc = null;
}
if (cb) {
cb(error, acc);
}
});
}
removeAccount(account, cb=null) {
const acc = this._accounts.get(account.id);
if (account !== acc) {
throw new Error('Unknown account');
}
// delete the account from the mapping, regardless of the result
this._accounts.delete(account.id);
let req = {
sylkrtc: 'account-remove',
account: acc.id
};
this._sendRequest(req, (error) => {
if (error) {
DEBUG('remove_account error: %s', error);
}
if (cb) {
cb();
}
});
}
+ reconnect() {
+ if (this._state === 'disconnected') {
+ clearTimeout(this._timer);
+ this._delay = INITIAL_DELAY;
+ this._timer = setTimeout(() => {
+ this._connect();
+ }, this._delay);
+ }
+ }
+
// Private API
_initialize() {
if (this._sock !== null) {
throw new Error('WebSocket already initialized');
}
if (this._timer !== null) {
throw new Error('Initialize is in progress');
}
DEBUG('Initializing');
if (process.browser) {
window.addEventListener('beforeunload', () => {
if (this._sock !== null) {
let noop = function() {};
this._sock.onerror = noop;
this._sock.onmessage = noop;
this._sock.onclose = noop;
this._sock.close();
}
});
}
this._timer = setTimeout(() => {
this._connect();
}, this._delay);
}
_connect() {
DEBUG('WebSocket connecting');
this._setState('connecting');
this._sock = new W3CWebSocket(this._wsUri, SYLKRTC_PROTO);
this._sock.onopen = () => {
DEBUG('WebSocket connection open');
this._onOpen();
};
this._sock.onerror = () => {
DEBUG('WebSocket connection got error');
};
this._sock.onclose = (event) => {
DEBUG('WebSocket connection closed: %d: (reason=\"%s\", clean=%s)', event.code, event.reason, event.wasClean);
this._onClose();
};
this._sock.onmessage = (event) => {
DEBUG('WebSocket received message: %o', event);
this._onMessage(event);
};
}
_sendRequest(req, cb) {
const transaction = uuid.v4();
req.transaction = transaction;
if (this._state !== 'ready') {
setImmediate(() => {
cb(new Error('Connection is not ready'));
});
return;
}
this._requests.set(transaction, {req: req, cb: cb});
this._sock.send(JSON.stringify(req));
}
_setState(newState) {
DEBUG('Set state: %s -> %s', this._state, newState);
const oldState = this._state;
this._state = newState;
this.emit('stateChanged', oldState, newState);
}
// WebSocket callbacks
_onOpen() {
clearTimeout(this._timer);
this._timer = null;
this._delay = INITIAL_DELAY;
this._setState('connected');
}
_onClose() {
this._sock = null;
if (this._timer) {
clearTimeout(this._timer);
this._timer = null;
}
// remove all accounts, the server no longer has them anyway
this._accounts.clear();
this._setState('disconnected');
if (!this._closed) {
this._delay = this._delay * 2;
if (this._delay > MAX_DELAY) {
this._delay = INITIAL_DELAY;
}
DEBUG('Retrying connection in %s seconds', this._delay / MSECS);
this._timer = setTimeout(() => {
this._connect();
}, this._delay);
} else {
this._setState('closed');
}
}
_onMessage(event) {
let message = JSON.parse(event.data);
if (typeof message.sylkrtc === 'undefined') {
DEBUG('Unrecognized message received');
return;
}
DEBUG('Received \"%s\" message: %o', message.sylkrtc, message);
if (message.sylkrtc === 'event') {
DEBUG('Received event: \"%s\"', message.event);
switch (message.event) {
case 'ready':
this._setState('ready');
break;
default:
break;
}
} else if (message.sylkrtc === 'account_event') {
let acc = this._accounts.get(message.account);
if (!acc) {
DEBUG('Account %s not found', message.account);
return;
}
acc._handleEvent(message);
} else if (message.sylkrtc === 'session_event') {
const sessionId = message.session;
for (let acc of this._accounts.values()) {
let call = acc._calls.get(sessionId);
if (call) {
call._handleEvent(message);
break;
}
}
} else if (message.sylkrtc === 'ack' || message.sylkrtc === 'error') {
const transaction = message.transaction;
const data = this._requests.get(transaction);
if (!data) {
DEBUG('Could not find transaction %s', transaction);
return;
}
this._requests.delete(transaction);
DEBUG('Received \"%s\" for request: %o', message.sylkrtc, data.req);
if (data.cb) {
if (message.sylkrtc === 'ack') {
data.cb(null);
} else {
data.cb(new Error(message.error));
}
}
}
}
}
export { Connection };

File Metadata

Mime Type
text/x-diff
Expires
Sat, Nov 23, 2:04 PM (1 d, 4 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
3409198
Default Alt Text
(18 KB)

Event Timeline