diff --git a/lib/call.js b/lib/call.js index 77d757b..cec5744 100644 --- a/lib/call.js +++ b/lib/call.js @@ -1,463 +1,343 @@ 'use strict'; import debug from 'debug'; import uuid from 'node-uuid'; import rtcninja from 'rtcninja'; -import transform from 'sdp-transform'; +import utils from './utils'; import { EventEmitter } from 'events'; const DEBUG = debug('sylkrtc:Call'); class Identity { constructor(uri, displayName='') { this._uri = uri; this._displayName = displayName; } get uri() { return this._uri; } get displayName() { return this._displayName; } toString() { if (!this._displayName) { return this._uri; } else { return `${this._displayName} <${this._uri}>`; } } } 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 Identity(account.id, account.displayName); this._remoteIdentity = null; } 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); } - const self = this; const pcConfig = options.pcConfig || {iceServers:[]}; const mediaConstraints = options.mediaConstraints || {audio: true, video: true}; const answerOptions = options.answerOptions; const localStream = options.localStream || null; // Create the RTCPeerConnection this._initRTCPeerConnection(pcConfig); - if (localStream !== null) { - // Use the provided stream - userMediaSucceeded(localStream); - } else { - // Get the user media - rtcninja.getUserMedia( - mediaConstraints, - userMediaSucceeded, - userMediaFailed - ); - } - - function userMediaSucceeded(stream) { - // adding a local stream doesn't trigger the 'onaddstream' callback - self._pc.addStream(stream); - self.emit('localStreamAdded', stream); - - self._pc.setRemoteDescription( - new rtcninja.RTCSessionDescription({type: 'offer', sdp: self._incomingSdp}), - // success - function() { - self._createLocalSDP( - 'answer', - answerOptions, - // success - function(sdp) { - DEBUG('Local SDP: %s', sdp); - self._sendAnswer(sdp); - }, - // failure - function(error) { - DEBUG('Error creating local SDP: %s', error); - self.terminate(); - } - ); - }, - // failure - function(error) { - DEBUG('Error setting remote description: %s', error); - self.terminate(); - } - ); - } - - function userMediaFailed(error) { - DEBUG('Error getting user media: %s', error); - self.terminate(); - } + utils.getUserMedia(mediaConstraints, localStream) + .then((stream) => { + this._pc.addStream(stream); + this.emit('localStreamAdded', stream); + this._pc.setRemoteDescription( + new rtcninja.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(); + } + ); + }) + .catch(function(reason) { + DEBUG(reason); + this.terminate(); + }); } terminate() { if (this._terminated) { return; } DEBUG('Terminating call'); this._sendTerminate(); } // Private API _initOutgoing(uri, options={}) { if (uri.indexOf('@') === -1) { throw new Error('Invalid URI'); } this._id = uuid.v4(); this._direction = 'outgoing'; this._remoteIdentity = new Identity(uri); - const self = this; const pcConfig = options.pcConfig || {iceServers:[]}; const mediaConstraints = options.mediaConstraints || {audio: true, video: true}; const offerOptions = options.offerOptions; const localStream = options.localStream || null; // Create the RTCPeerConnection this._initRTCPeerConnection(pcConfig); - if (localStream !== null) { - // Use the provided stream - userMediaSucceeded(localStream); - } else { - // Get the user media - rtcninja.getUserMedia( - mediaConstraints, - userMediaSucceeded, - userMediaFailed - ); - } - - function userMediaSucceeded(stream) { - // adding a local stream doesn't trigger the 'onaddstream' callback - self._pc.addStream(stream); - self.emit('localStreamAdded', stream); - - self._createLocalSDP( - 'offer', - offerOptions, - // success - function(sdp) { - DEBUG('Local SDP: %s', sdp); - self._sendCall(uri, sdp); - }, - // failure - function(error) { - DEBUG('Error creating local SDP: %s', error); - self._localTerminate(error); - } - ); - } - - function userMediaFailed(error) { - DEBUG('Error getting user media: %s', error); - self._localTerminate(error); - } + utils.getUserMedia(mediaConstraints, localStream) + .then((stream) => { + this._pc.addStream(stream); + this.emit('localStreamAdded', stream); + utils.createLocalSdp(this._pc, 'offer', offerOptions) + .then((sdp) => { + DEBUG('Local SDP: %s', sdp); + this._sendCall(uri, sdp); + }) + .catch((reason) => { + DEBUG(reason); + this._localTerminate(reason); + }); + }) + .catch(function(reason) { + DEBUG(reason); + this._localTerminate(reason); + }); } _initIncoming(id, caller, sdp) { this._id = id; this._remoteIdentity = new Identity(caller.uri, caller.display_name); this._incomingSdp = sdp; this._direction = 'incoming'; this._state = 'incoming'; } _handleEvent(message) { DEBUG('Call event: %o', message); switch (message.event) { case 'state': const oldState = this._state; const newState = message.data.state; this._state = newState; let data = {}; if (newState === 'accepted' && this._direction === 'outgoing') { - const self = this; this._pc.setRemoteDescription( new rtcninja.RTCSessionDescription({type: 'answer', sdp: message.data.sdp}), // success - function() { + () => { DEBUG('Call accepted'); - self.emit('stateChanged', oldState, newState, data); + this.emit('stateChanged', oldState, newState, data); }, // failure - function(error) { + (error) => { DEBUG('Error accepting call: %s', error); - self.terminate(); + this.terminate(); } ); } else { if (newState === 'terminated') { data.reason = message.data.reason; this._terminated = true; this._closeRTCPeerConnection(); } this.emit('stateChanged', oldState, newState, data); } break; default: break; } } _initRTCPeerConnection(pcConfig) { if (this._pc !== null) { throw new Error('RTCPeerConnection already initialized'); } - const self = this; this._pc = new rtcninja.RTCPeerConnection(pcConfig); - this._pc.onaddstream = function(event, stream) { + this._pc.onaddstream = (event, stream) => { DEBUG('Stream added'); - self.emit('streamAdded', stream); + this.emit('streamAdded', stream); }; - this._pc.onicecandidate = function(event) { - let candidate = null; + this._pc.onicecandidate = (event) => { if (event.candidate !== null) { - candidate = { - 'candidate': event.candidate.candidate, - 'sdpMid': event.candidate.sdpMid, - 'sdpMLineIndex': event.candidate.sdpMLineIndex - }; - DEBUG('New ICE candidate %o', candidate); + DEBUG('New ICE candidate %o', event.candidate); + } else { + DEBUG('ICE candidate gathering finished'); } - self._sendTrickle(candidate); + this._sendTrickle(event.candidate); }; } - _createLocalSDP(type, options, onSuccess, onFailure) { - const self = this; - - if (type === 'offer') { - this._pc.createOffer( - // success - createSucceeded, - // failure - failure, - // options - options - ); - } else if (type === 'answer') { - this._pc.createAnswer( - // success - createSucceeded, - // failure - failure, - // options - options - ); - } else { - throw new Error('type must be "offer" or "answer", but "' +type+ '" was given'); - } - - function createSucceeded(desc) { - self._pc.setLocalDescription( - desc, - // success - function() { - onSuccess(self._fixLocalSdp(self._pc.localDescription.sdp)); - }, - // failure - failure - ); - } - - function failure(error) { - onFailure(error); - } - } - - _fixLocalSdp(sdp) { - let parsedSdp = transform.parse(sdp); - let h264payload = null; - let hasProfileLevelId = false; - - for (let media of parsedSdp.media) { - if (media.type === 'video') { - for (let rtp of media.rtp) { - if (rtp.codec === 'H264') { - h264payload = rtp.payload; - break; - } - } - if (h264payload !== null) { - for (let fmtp of media.fmtp) { - if (fmtp.payload === h264payload && fmtp.config.indexOf('profile-level-id') !== -1) { - hasProfileLevelId = true; - break - } - } - if (!hasProfileLevelId) { - media.fmtp.push({payload: h264payload, config: 'profile-level-id=42e01f;packetization-mode=1;level-asymmetry-allowed=1'}); - } - break; - } - } - } - return transform.write(parsedSdp); - } - _sendRequest(req, cb) { this._account._sendRequest(req, cb); } _sendCall(uri, sdp) { let req = { sylkrtc: 'session-create', account: this.account.id, session: this.id, uri: uri, sdp: sdp }; - const self = this; - this._sendRequest(req, function(error) { + this._sendRequest(req, (error) => { if (error) { DEBUG('Call error: %s', error); - self._localTerminate(error); + this._localTerminate(error); } }); } _sendTerminate() { let req = { sylkrtc: 'session-terminate', session: this.id }; this._sendRequest(req, (error) => { if (error) { DEBUG('Error terminating call: %s', error); this._localTerminate(error); } this._terminated = true; }); setTimeout(() => { if (!this._terminated) { DEBUG('Timeout terminating call'); this._localTerminate('200 OK'); } this._terminated = true; }, 750); } _sendTrickle(candidate) { let req = { sylkrtc: 'session-trickle', session: this.id, candidates: candidate !== null ? [candidate] : [], }; this._sendRequest(req, null); } _sendAnswer(sdp) { let req = { sylkrtc: 'session-answer', session: this.id, sdp: sdp }; - const self = this; - this._sendRequest(req, function(error) { + this._sendRequest(req, (error) => { if (error) { DEBUG('Answer error: %s', error); - self.terminate(); + this.terminate(); } }); } _closeRTCPeerConnection() { DEBUG('Closing RTCPeerConnection'); if (this._pc !== null) { for (let stream of this._pc.getLocalStreams()) { rtcninja.closeMediaStream(stream); } for (let stream of this._pc.getRemoteStreams()) { rtcninja.closeMediaStream(stream); } this._pc.close(); this._pc = 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'; let data = { reason: error.toString() }; this._closeRTCPeerConnection(); this.emit('stateChanged', oldState, newState, data); } } export { Call, Identity }; diff --git a/lib/utils.js b/lib/utils.js new file mode 100644 index 0000000..7e4f68e --- /dev/null +++ b/lib/utils.js @@ -0,0 +1,99 @@ +'use strict'; + +import rtcninja from 'rtcninja'; +import transform from 'sdp-transform'; + + +function getUserMedia(mediaConstraints, localStream=null) { + if (localStream !== null) { + return Promise.resolve(localStream); + } + + let p = new Promise(function(resolve, reject) { + rtcninja.getUserMedia( + mediaConstraints, + //success + function(stream) { + resolve(stream); + }, + // failure + function(error) { + reject('Error getting user media: %s', error); + } + ); + }); + + return p; +} + + +function createLocalSdp(pc, type, options) { + if (type !== 'offer' && type !== 'answer') { + return Promise.reject('type must be "offer" or "answer", but "' +type+ '" was given'); + } + let p = new Promise(function(resolve, reject) { + let createFunc; + if (type === 'offer' ) { + createFunc = pc.createOffer; + } else { + createFunc = pc.createAnswer; + } + createFunc.call( + pc, + // success + function(desc) { + pc.setLocalDescription( + desc, + // success + function() { + resolve(fixupSdp(pc.localDescription.sdp)); + }, + // failure + function(error) { + reject('Error setting local description: ' + error); + } + ); + }, + // failure + function(error) { + reject('Error creating local SDP: ' + error); + }, + options + ); + }); + return p; +} + + +function fixupSdp(sdp) { + let parsedSdp = transform.parse(sdp); + let h264payload = null; + let hasProfileLevelId = false; + + for (let media of parsedSdp.media) { + if (media.type === 'video') { + for (let rtp of media.rtp) { + if (rtp.codec === 'H264') { + h264payload = rtp.payload; + break; + } + } + if (h264payload !== null) { + for (let fmtp of media.fmtp) { + if (fmtp.payload === h264payload && fmtp.config.indexOf('profile-level-id') !== -1) { + hasProfileLevelId = true; + break; + } + } + if (!hasProfileLevelId) { + media.fmtp.push({payload: h264payload, config: 'profile-level-id=42e01f;packetization-mode=1;level-asymmetry-allowed=1'}); + } + break; + } + } + } + return transform.write(parsedSdp); +} + + +export default { getUserMedia, createLocalSdp };