diff --git a/lib/account.js b/lib/account.js index f330742..8bfebb4 100644 --- a/lib/account.js +++ b/lib/account.js @@ -1,165 +1,165 @@ 'use strict'; import debug from 'debug'; import md5 from 'blueimp-md5'; import transform from 'sdp-transform'; import utils from './utils'; import { EventEmitter } from 'events'; import { Call } from './call'; import { ConferenceCall } from './conference'; const DEBUG = debug('sylkrtc:Account'); class Account extends EventEmitter { constructor(options, connection) { if (options.account.indexOf('@') === -1) { throw new Error('Invalid account id specified'); } super(); const id = options.account; const [username, domain] = id.split('@'); this._id = id; this._displayName = options.displayName; this._password = md5(username + ':' + (options.realm || domain)+ ':' + options.password); this._connection = connection; this._registrationState = null; this._calls = new Map(); this._confCalls = new Map(); } get id() { return this._id; } get password() { return this._password; } get displayName() { return this._displayName; } get registrationState() { return this._registrationState; } register() { const req = { sylkrtc: 'account-register', account: this._id }; this._sendRequest(req, (error) => { if (error) { DEBUG('Register error: %s', error); const oldState = this._registrationState; const newState = 'failed'; const data = {reason: error.toString()}; this._registrationState = newState; this.emit('registrationStateChanged', oldState, newState, data); } }); } unregister() { const req = { sylkrtc: 'account-unregister', account: this._id, }; this._sendRequest(req, (error) => { if (error) { DEBUG('Unregister error: %s', error); } const oldState = this._registrationState; const newState = null; this._registrationState = newState; this.emit('registrationStateChanged', oldState, newState, {}); }); } call(uri, options={}) { const callObj = new Call(this); callObj._initOutgoing(uri, options); this._calls.set(callObj.id, callObj); this.emit('outgoingCall', callObj); return callObj; } joinConference(uri, options={}) { const confCall = new ConferenceCall(this); confCall._initialize(uri, options); this._confCalls.set(confCall.id, confCall); this.emit('conferenceCall', confCall); return confCall; } setDeviceToken(oldToken, newToken) { DEBUG('Setting device token: %s -> %s', oldToken, newToken); const req = { sylkrtc: 'account-devicetoken', account: this._id, old_token: oldToken, new_token: newToken }; this._sendRequest(req, (error) => { if (error) { DEBUG('Error setting device token: %s', error); } }); } // Private API _handleEvent(message) { DEBUG('Received account event: %s', message.event); const data = {}; switch (message.event) { - case 'registration_state': + case 'registration-state': const oldState = this._registrationState; - const newState = message.data.state; + const newState = message.state; this._registrationState = newState; if (newState === 'failed') { - data.reason = message.data.reason; + data.reason = message.reason; } this.emit('registrationStateChanged', oldState, newState, data); break; - case 'incoming_session': + case 'incoming-session': let call = new Call(this); - call._initIncoming(message.session, message.data.originator, message.data.sdp); + call._initIncoming(message.session, message.originator, message.sdp); this._calls.set(call.id, call); // see what media types are offered const mediaTypes = { audio: false, video: false }; - const parsedSdp = transform.parse(message.data.sdp); + const parsedSdp = transform.parse(message.sdp); for (let media of parsedSdp.media) { if (media.type === 'audio' && media.port !== 0 && media.direction === 'sendrecv') { mediaTypes.audio = true; } else if (media.type === 'video' && media.port !== 0 && media.direction === 'sendrecv') { mediaTypes.video = true; } } - DEBUG('Incoming call from %s with media types: %o', message.data.originator.uri, mediaTypes); + DEBUG('Incoming call from %s with media types: %o', message.originator.uri, mediaTypes); this.emit('incomingCall', call, mediaTypes); break; - case 'missed_session': - data.originator = new utils.Identity(message.data.originator.uri, message.data.originator.display_name); + case 'missed-session': + data.originator = new utils.Identity(message.originator.uri, message.originator.display_name); this.emit('missedCall', data); break; - case 'conference_invite': - data.originator = new utils.Identity(message.data.originator.uri, message.data.originator.display_name); - data.room = message.data.room; + case 'conference-invite': + data.originator = new utils.Identity(message.originator.uri, message.originator.display_name); + data.room = message.room; this.emit('conferenceInvite', data); break; default: break; } } _sendRequest(req, cb) { this._connection._sendRequest(req, cb); } } export { Account }; diff --git a/lib/call.js b/lib/call.js index 1d704b2..977dda0 100644 --- a/lib/call.js +++ b/lib/call.js @@ -1,360 +1,360 @@ 'use strict'; import debug from 'debug'; import uuidv4 from 'uuid/v4'; import utils from './utils'; import { EventEmitter } from 'events'; const DEBUG = debug('sylkrtc:Call'); class Call extends EventEmitter { constructor(account) { super(); this._account = account; this._id = null; this._direction = null; this._pc = null; this._state = null; this._terminated = false; this._incomingSdp = null; this._localIdentity = new utils.Identity(account.id, account.displayName); this._remoteIdentity = null; this._dtmfSender = null; // bind some handlers to this instance this._onDtmf = this._onDtmf.bind(this); } get account() { return this._account; } get id() { return this._id; } get direction() { return this._direction; } get state() { return this._state; } get localIdentity() { return this._localIdentity; } get remoteIdentity() { return this._remoteIdentity; } getLocalStreams() { if (this._pc !== null) { return this._pc.getLocalStreams(); } else { return []; } } getRemoteStreams() { if (this._pc !== null) { return this._pc.getRemoteStreams(); } else { return []; } } answer(options = {}) { if (this._state !== 'incoming') { throw new Error('Call is not in the incoming state: ' + this._state); } if (!options.localStream) { throw new Error('Missing localStream'); } const pcConfig = options.pcConfig || {iceServers:[]}; const answerOptions = options.answerOptions; // Create the RTCPeerConnection this._initRTCPeerConnection(pcConfig); this._pc.addStream(options.localStream); this.emit('localStreamAdded', options.localStream); this._pc.setRemoteDescription( new RTCSessionDescription({type: 'offer', sdp: this._incomingSdp}), // success () => { utils.createLocalSdp(this._pc, 'answer', answerOptions) .then((sdp) => { DEBUG('Local SDP: %s', sdp); this._sendAnswer(sdp); }) .catch((reason) => { DEBUG(reason); this.terminate(); }); }, // failure (error) => { DEBUG('Error setting remote description: %s', error); this.terminate(); } ); } terminate() { if (this._terminated) { return; } DEBUG('Terminating call'); this._sendTerminate(); } sendDtmf(tones, duration=100, interToneGap=70) { DEBUG('sendDtmf()'); if (this._dtmfSender === null) { if (this._pc !== null) { let track = null; try { track = this._pc.getLocalStreams()[0].getAudioTracks()[0]; } catch (e) { // ignore } if (track !== null) { DEBUG('Creating DTMF sender'); this._dtmfSender = this._pc.createDTMFSender(track); if (this._dtmfSender) { this._dtmfSender.addEventListener('tonechange', this._onDtmf); } } } } if (this._dtmfSender) { DEBUG('Sending DTMF tones'); this._dtmfSender.insertDTMF(tones, duration, interToneGap); } } // Private API _initOutgoing(uri, options={}) { if (uri.indexOf('@') === -1) { throw new Error('Invalid URI'); } if (!options.localStream) { throw new Error('Missing localStream'); } this._id = uuidv4(); this._direction = 'outgoing'; this._remoteIdentity = new utils.Identity(uri); const pcConfig = options.pcConfig || {iceServers:[]}; const offerOptions = options.offerOptions; // Create the RTCPeerConnection this._initRTCPeerConnection(pcConfig); this._pc.addStream(options.localStream); this.emit('localStreamAdded', options.localStream); utils.createLocalSdp(this._pc, 'offer', offerOptions) .then((sdp) => { DEBUG('Local SDP: %s', sdp); this._sendCall(uri, sdp); }) .catch((reason) => { DEBUG(reason); this._localTerminate(reason); }); } _initIncoming(id, caller, sdp) { this._id = id; this._remoteIdentity = new utils.Identity(caller.uri, caller.display_name); this._incomingSdp = sdp; this._direction = 'incoming'; this._state = 'incoming'; DEBUG('Remote SDP: %s', sdp); } _handleEvent(message) { DEBUG('Call event: %o', message); switch (message.event) { case 'state': const oldState = this._state; - const newState = message.data.state; + const newState = message.state; this._state = newState; const data = {}; if (newState === 'accepted' && this._direction === 'outgoing') { - const sdp = utils.mungeSdp(message.data.sdp); + const sdp = utils.mungeSdp(message.sdp); DEBUG('Remote SDP: %s', sdp); this._pc.setRemoteDescription( new RTCSessionDescription({type: 'answer', sdp: sdp}), // success () => { // fake 'established' state change this._state = 'established'; this.emit('stateChanged', oldState, 'established', {}); }, // failure (error) => { DEBUG('Error accepting call: %s', error); this.terminate(); } ); DEBUG('Call accepted'); this.emit('stateChanged', oldState, newState, data); } else if (newState === 'established' && this._direction === 'outgoing') { // TODO: remove this this._state = oldState; } else { if (newState === 'terminated') { - data.reason = message.data.reason; + data.reason = message.reason; this._terminated = true; this._account._calls.delete(this.id); this._closeRTCPeerConnection(); } this.emit('stateChanged', oldState, newState, data); } break; default: break; } } _initRTCPeerConnection(pcConfig) { if (this._pc !== null) { throw new Error('RTCPeerConnection already initialized'); } this._pc = new RTCPeerConnection(pcConfig); this._pc.addEventListener('addstream', (event) => { DEBUG('Stream added'); this.emit('streamAdded', event.stream); }); this._pc.addEventListener('icecandidate', (event) => { if (event.candidate !== null) { DEBUG('New ICE candidate %o', event.candidate); } else { DEBUG('ICE candidate gathering finished'); } this._sendTrickle(event.candidate); }); } _sendRequest(req, cb) { this._account._sendRequest(req, cb); } _sendCall(uri, sdp) { const req = { sylkrtc: 'session-create', account: this.account.id, session: this.id, uri: uri, sdp: sdp }; this._sendRequest(req, (error) => { if (error) { DEBUG('Call error: %s', error); this._localTerminate(error); } }); } _sendTerminate() { const req = { sylkrtc: 'session-terminate', session: this.id }; this._sendRequest(req, (error) => { if (error) { DEBUG('Error terminating call: %s', error); this._localTerminate(error); } }); setTimeout(() => { if (!this._terminated) { DEBUG('Timeout terminating call'); this._localTerminate('200 OK'); } this._terminated = true; }, 150); } _sendTrickle(candidate) { const req = { sylkrtc: 'session-trickle', session: this.id, candidates: candidate !== null ? [candidate] : [], }; this._sendRequest(req, (error) => { if (error) { DEBUG('Trickle error: %s', error); this._localTerminate(error); } }); } _sendAnswer(sdp) { const req = { sylkrtc: 'session-answer', session: this.id, sdp: sdp }; this._sendRequest(req, (error) => { if (error) { DEBUG('Answer error: %s', error); this.terminate(); } }); } _closeRTCPeerConnection() { DEBUG('Closing RTCPeerConnection'); if (this._pc !== null) { for (let stream of this._pc.getLocalStreams()) { utils.closeMediaStream(stream); } for (let stream of this._pc.getRemoteStreams()) { utils.closeMediaStream(stream); } this._pc.close(); this._pc = null; if (this._dtmfSender !== null) { this._dtmfSender.removeEventListener('tonechange', this._onDtmf); this._dtmfSender = null; } } } _localTerminate(error) { if (this._terminated) { return; } DEBUG('Local terminate'); this._account._calls.delete(this.id); this._terminated = true; const oldState = this._state; const newState = 'terminated'; const data = { reason: error.toString() }; this._closeRTCPeerConnection(); this.emit('stateChanged', oldState, newState, data); } _onDtmf(event) { DEBUG('Sent DTMF tone %s', event.tone); this.emit('dtmfToneSent', event.tone); } } export { Call }; diff --git a/lib/conference.js b/lib/conference.js index 7424733..e079873 100644 --- a/lib/conference.js +++ b/lib/conference.js @@ -1,642 +1,618 @@ 'use strict'; import debug from 'debug'; import uuidv4 from 'uuid/v4'; import utils from './utils'; import { EventEmitter } from 'events'; const DEBUG = debug('sylkrtc:Conference'); class Participant extends EventEmitter { constructor(publisherId, identity, conference) { super(); this._id = uuidv4(); this._publisherId = publisherId; this._identity = identity; this._conference = conference; this._state = null; this._pc = null; this._videoSubscriptionPaused = false; this._audioSubscriptionPaused = false; this._videoPublishingPaused = false; this._audioPublishingPaused = false; } get id() { return this._id; } get publisherId() { return this._publisherId; } get identity() { return this._identity; } get conference() { return this._conference; } get videoPaused() { return this._videoSubscriptionPaused; } get state() { return this._state; } get streams() { if (this._pc !== null) { return this._pc.getRemoteStreams(); } else { return []; } } attach() { if (this._state !== null) { return; } this._setState('progress'); this._sendAttach(); } detach() { if (this._state !== null) { this._sendDetach(); } } pauseVideo() { this._sendUpdate({video: false}); this._videoSubscriptionPaused = true; } resumeVideo() { this._sendUpdate({video: true}); this._videoSubscriptionPaused = false; } _setState(newState) { const oldState = this._state; this._state = newState; DEBUG(`Participant ${this.id} state change: ${oldState} -> ${newState}`); this.emit('stateChanged', oldState, newState); } _handleOffer(offerSdp) { DEBUG('Handling SDP for participant offer: %s', offerSdp); // Create the RTCPeerConnection const pcConfig = this.conference._pcConfig; const pc = new RTCPeerConnection(pcConfig); pc.addEventListener('addstream', (event) => { DEBUG('Stream added'); this.emit('streamAdded', event.stream); }); pc.addEventListener('icecandidate', (event) => { if (event.candidate !== null) { DEBUG('New ICE candidate %o', event.candidate); } else { DEBUG('ICE candidate gathering finished'); } this._sendTrickle(event.candidate); }); this._pc = pc; // no need for a local stream since we are only going to receive media here pc.setRemoteDescription( new RTCSessionDescription({type: 'offer', sdp: offerSdp}), // success () => { utils.createLocalSdp(pc, 'answer') .then((sdp) => { DEBUG('Local SDP: %s', sdp); this._sendAnswer(sdp); }) .catch((reason) => { DEBUG(reason); this._close(); }); }, // failure (error) => { DEBUG('Error setting remote description: %s', error); this._close(); } ); } _sendAttach() { const req = { - sylkrtc: 'videoroom-ctl', + sylkrtc: 'videoroom-feed-attach', session: this.conference.id, - option: 'feed-attach', - feed_attach: { - session: this.id, - publisher: this._publisherId - } + publisher: this._publisherId, + feed: this.id }; DEBUG('Sending request: %o', req); this.conference._sendRequest(req, (error) => { if (error) { DEBUG('Error attaching to participant %s: %s', this._publisherId, error); } }); } _sendDetach() { const req = { - sylkrtc: 'videoroom-ctl', + sylkrtc: 'videoroom-feed-detach', session: this.conference.id, - option: 'feed-detach', - feed_detach: { - session: this.id - } + feed: this.id }; DEBUG('Sending request: %o', req); this.conference._sendRequest(req, (error) => { if (error) { DEBUG('Error detaching to participant %s: %s', this._publisherId, error); } this._close(); }); } _sendTrickle(candidate) { const req = { - sylkrtc: 'videoroom-ctl', - session: this.conference.id, - option: 'trickle', - trickle: { - session: this.id, - candidates: candidate !== null ? [candidate] : [] - } + sylkrtc: 'videoroom-session-trickle', + session: this.id, + candidates: candidate !== null ? [candidate] : [] }; this.conference._sendRequest(req, (error) => { if (error) { DEBUG('Trickle error: %s', error); this._close(); } }); } _sendAnswer(sdp) { const req = { - sylkrtc: 'videoroom-ctl', + sylkrtc: 'videoroom-feed-answer', session: this.conference.id, - option: 'feed-answer', - feed_answer: { - session: this.id, - sdp: sdp - } + feed: this.id, + sdp: sdp }; + DEBUG('Sending request: %o', req); this.conference._sendRequest(req, (error) => { if (error) { DEBUG('Answer error: %s', error); this._close(); } }); } _sendUpdate(options = {}) { const req = { - sylkrtc: 'videoroom-ctl', + sylkrtc: 'videoroom-session-update', session: this.id, - option: 'update', - update: {} + options: options }; - - req.update = Object.assign({}, options); DEBUG('Sending update participant request %o', req); this.conference._sendRequest(req, (error) => { if (error) { DEBUG('Answer error: %s', error); } }); } _close() { DEBUG('Closing Participant RTCPeerConnection'); if (this._pc !== null) { for (let stream of this._pc.getLocalStreams()) { utils.closeMediaStream(stream); } for (let stream of this._pc.getRemoteStreams()) { utils.closeMediaStream(stream); } this._pc.close(); this._pc = null; this._setState(null); } } } class ConferenceCall extends EventEmitter { constructor(account) { super(); this._account = account; this._id = null; this._pc = null; this._participants = new Map(); this._terminated = false; this._state = null; this._localIdentity = new utils.Identity(account.id, account.displayName); this._remoteIdentity = null; this._activeParticpants = []; this._pcConfig = null; // saved on initialize, used later for subscriptions } get account() { return this._account; } get id() { return this._id; } get direction() { // make this object API compatible with `Call` return 'outgoing'; } get state() { return this._state; } get localIdentity() { return this._localIdentity; } get remoteIdentity() { return this._remoteIdentity; } get participants() { return Array.from(new Set(this._participants.values())); } get activeParticipants() { return this._activeParticpants; } getLocalStreams() { if (this._pc !== null) { return this._pc.getLocalStreams(); } else { return []; } } getRemoteStreams() { let streams = []; for (let participant of new Set(this._participants.values())) { streams = streams.concat(participant.streams); } return streams; } scaleLocalTrack(oldTrack, divider) { DEBUG('Scaling track by %d', divider); let sender; for (sender of this._pc.getSenders()) { if (sender.track === oldTrack) { DEBUG('Found sender to modify track %o', sender); break; } } sender.setParameters({encodings: [{scaleResolutionDownBy: divider}]}) .then(() => { DEBUG("Scale set to %o", divider); DEBUG('Active encodings %o', sender.getParameters().encodings); }) .catch((error) => { DEBUG('Error %o', error) }); } configureRoom(ps, cb=null) { if (!Array.isArray(ps)) { return; } this._sendConfigureRoom(ps, cb); } terminate() { if (this._terminated) { return; } DEBUG('Terminating conference'); this._sendTerminate(); } inviteParticipants(ps) { if (this._terminated) { return; } if (!Array.isArray(ps) || ps.length === 0) { return; } DEBUG('Inviting participants: %o', ps); const req = { - sylkrtc: 'videoroom-ctl', + sylkrtc: 'videoroom-invite', session: this.id, - option: 'invite-participants', - invite_participants: { - participants: ps - } + participants: ps }; this._sendRequest(req, null); } // Private API _initialize(uri, options={}) { if (this._id !== null) { throw new Error('Already initialized'); } if (uri.indexOf('@') === -1) { throw new Error('Invalid URI'); } if (!options.localStream) { throw new Error('Missing localStream'); } this._id = uuidv4(); this._remoteIdentity = new utils.Identity(uri); options = Object.assign({}, options); const pcConfig = options.pcConfig || {iceServers:[]}; this._pcConfig = pcConfig; this._initialParticipants = options.initialParticipants || []; const offerOptions = options.offerOptions || {}; // only send audio / video through the publisher connection offerOptions.offerToReceiveAudio = false; offerOptions.offerToReceiveVideo = false; delete offerOptions.mandatory; // Create the RTCPeerConnection this._pc = new RTCPeerConnection(pcConfig); this._pc.addEventListener('icecandidate', (event) => { if (event.candidate !== null) { DEBUG('New ICE candidate %o', event.candidate); } else { DEBUG('ICE candidate gathering finished'); } this._sendTrickle(event.candidate); }); this._pc.addStream(options.localStream); this.emit('localStreamAdded', options.localStream); DEBUG('Offer options: %o', offerOptions); utils.createLocalSdp(this._pc, 'offer', offerOptions) .then((sdp) => { DEBUG('Local SDP: %s', sdp); this._sendJoin(sdp); }) .catch((reason) => { this._localTerminate(reason); }); } _handleEvent(message) { DEBUG('Conference event: %o', message); switch (message.event) { - case 'state': + case 'session-state': const oldState = this._state; - const newState = message.data.state; + const newState = message.state; this._state = newState; let data = {}; let participant; if (newState === 'accepted') { - let sdp = utils.mungeSdp(message.data.sdp); + let sdp = utils.mungeSdp(message.sdp); DEBUG('Remote SDP: %s', sdp); this._pc.setRemoteDescription( new RTCSessionDescription({type: 'answer', sdp: sdp}), // success () => { DEBUG('Conference accepted'); this.emit('stateChanged', oldState, newState, data); if (this._initialParticipants.length > 0 ) { setTimeout(() => { this.inviteParticipants(this._initialParticipants); }, 50); } }, // failure (error) => { DEBUG('Error processing conference accept: %s', error); this.terminate(); } ); } else { if (newState === 'terminated') { - data.reason = message.data.reason; + data.reason = message.reason; this._terminated = true; this._close(); } this.emit('stateChanged', oldState, newState, data); } break; - case 'initial_publishers': + case 'initial-publishers': // this comes between 'accepted' and 'established' states - for (let p of message.data.publishers) { + for (let p of message.publishers) { participant = new Participant(p.id, new utils.Identity(p.uri, p.display_name), this); this._participants.set(participant.id, participant); this._participants.set(p.id, participant); } break; - case 'publishers_joined': - for (let p of message.data.publishers) { + case 'publishers-joined': + for (let p of message.publishers) { DEBUG('Participant joined: %o', p); participant = new Participant(p.id, new utils.Identity(p.uri, p.display_name), this); this._participants.set(participant.id, participant); this._participants.set(p.id, participant); this.emit('participantJoined', participant); } break; - case 'publishers_left': - for (let pId of message.data.publishers) { + case 'publishers-left': + for (let pId of message.publishers) { participant = this._participants.get(pId); if (participant) { this._participants.delete(participant.id); this._participants.delete(pId); this.emit('participantLeft', participant); } } break; - case 'feed_attached': - participant = this._participants.get(message.data.subscription); + case 'feed-attached': + participant = this._participants.get(message.feed); if (participant) { - participant._handleOffer(message.data.sdp); + participant._handleOffer(message.sdp); } break; - case 'feed_established': - participant = this._participants.get(message.data.subscription); + case 'feed-established': + participant = this._participants.get(message.feed); if (participant) { participant._setState('established'); } break; - case 'configure-room': + case 'configure': let activeParticipants = []; let originator; const mappedOriginator = this._participants.get(message.originator); if (mappedOriginator) { originator = mappedOriginator.identity; } else if (message.originator === this.id) { originator = this.localIdentity; } else if (message.originator === 'videoroom'){ originator = message.originator; } for (let pId of message.active_participants) { participant = this._participants.get(pId); if (participant) { activeParticipants.push(participant); } else if (pId === this.id) { activeParticipants.push({ id: this.id, publisherId: this.id, identity: this.localIdentity, streams: this.getLocalStreams() }) } } this._activeParticpants = activeParticipants; let roomConfig = {originator: originator, activeParticipants: this._activeParticpants} this.emit('roomConfigured', roomConfig); break; default: break; } } _sendConfigureRoom(ps, cb = null) { const req = { - sylkrtc: 'videoroom-ctl', + sylkrtc: 'videoroom-configure', session: this.id, - option: 'configure-room', - configure_room: { - active_participants: ps - } + active_participants: ps }; this._sendRequest(req, (error) => { if (error) { DEBUG('Error configuring room: %s', error); if (cb) { cb(error); } } else { DEBUG('Configure room send: %o', ps); } }); } _sendJoin(sdp) { const req = { sylkrtc: 'videoroom-join', account: this.account.id, session: this.id, uri: this.remoteIdentity.uri, sdp: sdp }; DEBUG('Sending request: %o', req); this._sendRequest(req, (error) => { if (error) { this._localTerminate(error); } }); } _sendTerminate() { const req = { - sylkrtc: 'videoroom-terminate', + sylkrtc: 'videoroom-leave', session: this.id }; this._sendRequest(req, (error) => { if (error) { DEBUG('Error terminating conference: %s', error); this._localTerminate(error); } }); setTimeout(() => { if (!this._terminated) { DEBUG('Timeout terminating call'); this._localTerminate(''); } this._terminated = true; }, 150); } _sendTrickle(candidate) { const req = { - sylkrtc: 'videoroom-ctl', + sylkrtc: 'videoroom-session-trickle', session: this.id, - option: 'trickle', - trickle: { - candidates: candidate !== null ? [candidate] : [] - } + candidates: candidate !== null ? [candidate] : [] }; this._sendRequest(req, (error) => { if (error) { DEBUG('Trickle error: %s', error); this._localTerminate(error); } }); } _sendRequest(req, cb) { this._account._sendRequest(req, cb); } _close() { DEBUG('Closing RTCPeerConnection'); if (this._pc !== null) { for (let stream of this._pc.getLocalStreams()) { utils.closeMediaStream(stream); } for (let stream of this._pc.getRemoteStreams()) { utils.closeMediaStream(stream); } this._pc.close(); this._pc = null; } const participants = this.participants; this._participants = []; for (let p of participants) { p._close(); } } _localTerminate(reason) { if (this._terminated) { return; } DEBUG(`Local terminate, reason: ${reason}`); this._account._confCalls.delete(this.id); this._terminated = true; const oldState = this._state; const newState = 'terminated'; const data = { reason: reason.toString() }; this._close(); this.emit('stateChanged', oldState, newState, data); } } export { ConferenceCall }; diff --git a/lib/connection.js b/lib/connection.js index 03177b2..de4700c 100644 --- a/lib/connection.js +++ b/lib/connection.js @@ -1,323 +1,317 @@ 'use strict'; import bowser from 'bowser'; import debug from 'debug'; import uuidv4 from 'uuid/v4'; import { EventEmitter } from 'events'; import { setImmediate } from 'timers'; import { w3cwebsocket as W3CWebSocket } from 'websocket'; import { Account } from './account'; -const SYLKRTC_PROTO = 'sylkRTC-1'; +const SYLKRTC_PROTO = 'sylkRTC-2'; const DEBUG = debug('sylkrtc:Connection'); const MSECS = 1000; const INITIAL_DELAY = 0.5 * MSECS; const MAX_DELAY = 64 * MSECS; // compute a string for our well-known platforms let platform; if (bowser.iphone) { platform = 'iPhone'; } else if (bowser.ipad) { platform = 'iPad'; } else if (bowser.ipod) { platform = 'iPod'; } else if (bowser.android) { platform = 'Android'; } else if (bowser.mac) { platform = 'macOS'; } else if (bowser.linux) { platform = 'Linux'; } else if (bowser.windows) { platform = 'Windows'; } else { platform = 'Unknown'; } if (bowser.osversion) { platform = `${platform} ${bowser.osversion}`; } let browser = bowser.name; if (bowser.version) { browser = `${browser} ${bowser.version}`; } const USER_AGENT = `SylkRTC (${browser} on ${platform})`; 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'); } const 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); const req = { sylkrtc: 'account-add', account: acc.id, password: acc.password, display_name: acc.displayName, user_agent: USER_AGENT }; this._sendRequest(req, (error) => { if (error) { DEBUG('add_account error: %s', error); this._accounts.delete(acc.id); } if (cb) { cb(error, error ? null : 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); const req = { sylkrtc: 'account-remove', account: acc.id }; this._sendRequest(req, (error) => { if (error) { DEBUG('remove_account error: %s', error); } if (cb) { cb(error); } }); } 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) { const 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 = uuidv4(); 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) { const 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') { + if (message.sylkrtc === 'ready-event') { DEBUG('Received event: \"%s\"', message.event); - switch (message.event) { - case 'ready': - this._setState('ready'); - break; - default: - break; - } - } else if (message.sylkrtc === 'account_event') { + this._setState('ready'); + } 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') { + } 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 === 'videoroom_event') { + } else if (message.sylkrtc === 'videoroom-event') { const confId = message.session; for (let acc of this._accounts.values()) { let confCall = acc._confCalls.get(confId); if (confCall) { confCall._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 };