nexmo-stitch
Advanced tools
Comparing version
@@ -25,2 +25,4 @@ # Changelog | ||
- Provide `minified`/`uglify` version of conversationClient.js in `dist/conversationClient.min.js` | ||
### Fixes | ||
@@ -32,3 +34,8 @@ | ||
- Call status now does not go to a previous status | ||
- Switch to websocket protocol to fix disconnection issues | ||
--- | ||
## 2.0.5 | ||
@@ -39,4 +46,4 @@ | ||
- Now you can listen to errors thrown at the application level: | ||
- Let you listen to general errors | ||
- Let you listen to specific errors | ||
- Lets you listen to general errors | ||
- Lets you listen to specific errors | ||
for example, combined with `application.updateToken`, you can request to update the token. | ||
@@ -43,0 +50,0 @@ |
@@ -86,7 +86,14 @@ /* | ||
params.members.map((m) => { | ||
const member = new Member(this, m); | ||
if (m.user_id === this.application.me.id) { | ||
this.me = member; | ||
if (this.members[m.member_id]) { | ||
this.members[m.member_id]._normalise(m); | ||
if (m.user_id === this.application.me.id && m.state !== "LEFT") { | ||
this.me = this.members[m.member_id]; | ||
} | ||
} else { | ||
const member = new Member(this, m); | ||
if (m.user_id === this.application.me.id && m.state !== "LEFT") { | ||
this.me = member; | ||
} | ||
this.members[member.id] = member; | ||
} | ||
this.members[member.id] = member; | ||
}); | ||
@@ -144,3 +151,3 @@ break; | ||
const request_body = {}; | ||
const _normalizeParams = (params) => { | ||
const _createDefaultValues = (params) => { | ||
if (params) { | ||
@@ -160,6 +167,7 @@ if (params.user_id) { | ||
} | ||
return request_body; | ||
}; | ||
return new Promise((resolve, reject) => { | ||
_normalizeParams(params); | ||
const request_body = _createDefaultValues(params); | ||
this.application.session.sendRequest({ | ||
@@ -166,0 +174,0 @@ type: 'conversation:join', |
@@ -28,2 +28,3 @@ /* | ||
* @property {Call.MEMBER_CALL_STATES} MEMBER_CALL_STATES - the available member call states | ||
* @property {Call.STATUS_PERMITTED_FLOW} STATUS_PERMITTED_FLOW - the permitted call status transition map, describes the "from" and allowed "to" transitions | ||
* @emits Application#member:call | ||
@@ -81,3 +82,2 @@ * @emits Call#member:call:state | ||
/** | ||
@@ -109,2 +109,3 @@ * Enum for Call status. | ||
}; | ||
/** | ||
@@ -117,3 +118,3 @@ * Enum for Call direction. | ||
this.CALL_DIRECTION = { | ||
/** The Call started form another end */ | ||
/** The Call started from another end */ | ||
INBOUND: 'inbound', | ||
@@ -124,2 +125,3 @@ /** The Call started from this client */ | ||
Object.freeze(this.CALL_DIRECTION); | ||
/** | ||
@@ -142,3 +144,38 @@ * Enum for Call Member states. | ||
Object.freeze(this.MEMBER_CALL_STATES); | ||
this.status = this.CALL_STATUS.STARTED; | ||
/** | ||
* Enum for the permitted call status transition. | ||
* @readonly | ||
* @alias Call.STATUS_PERMITTED_FLOW | ||
* @enum {Map<string, Array<Call.CALL_STATUS>} | ||
*/ | ||
this.STATUS_PERMITTED_FLOW = new Map([ | ||
/** Permitted transition array from STARTED */ | ||
["STARTED", [ | ||
this.CALL_STATUS.RINGING, | ||
this.CALL_STATUS.ANSWERED, | ||
this.CALL_STATUS.FAILED, | ||
this.CALL_STATUS.TIMEOUT, | ||
this.CALL_STATUS.UNANSWERED, | ||
this.CALL_STATUS.REJECTED, | ||
this.CALL_STATUS.BUSY | ||
]], | ||
/** Permitted transition array from RINGING */ | ||
["RINGING", [ | ||
this.CALL_STATUS.ANSWERED, | ||
this.CALL_STATUS.FAILED, | ||
this.CALL_STATUS.TIMEOUT, | ||
this.CALL_STATUS.UNANSWERED, | ||
this.CALL_STATUS.REJECTED, | ||
this.CALL_STATUS.BUSY | ||
]], | ||
/** Permitted transition set from ANSWERED */ | ||
["ANSWERED", [ | ||
this.CALL_STATUS.COMPLETED, | ||
this.CALL_STATUS.FAILED | ||
]] | ||
]); | ||
Object.freeze(this.STATUS_PERMITTED_FLOW); | ||
this.status = null; | ||
this.direction = this.CALL_DIRECTION.INBOUND; | ||
@@ -193,2 +230,29 @@ this._setupConversationObject(conversation); | ||
/** | ||
* Validate the current call status transition | ||
* If a transition is not defined, return false | ||
* @param {string} status the status to validate | ||
* @returns {boolean} false if the transition is not permitted | ||
* @private | ||
*/ | ||
_isValidStatusTransition(status) { | ||
if (!status) { | ||
throw new NexmoClientError(`Provide the status to validate the transition from '${this.status}'`); | ||
} | ||
// if the call object is just initialised allow any state | ||
if (!this.status) { | ||
return true; | ||
} | ||
const current_status = this.status.toUpperCase(); | ||
if (!this.STATUS_PERMITTED_FLOW.has(current_status)) { | ||
return false; | ||
} | ||
if (this.status === status) { | ||
return false; | ||
} | ||
return (this.STATUS_PERMITTED_FLOW.get(current_status) | ||
.indexOf(status) !== -1) | ||
} | ||
/** | ||
* Go through the members of the conversation and if .me is the only one (JOINED or INVITED) | ||
@@ -258,5 +322,2 @@ * call call.hangUp(). | ||
} | ||
} else { | ||
this._setStatusAndEmit(this.CALL_STATUS.REJECTED); | ||
return Promise.resolve(); | ||
} | ||
@@ -322,3 +383,3 @@ } else { | ||
if (!conversation.me) { | ||
this.log.debug("missing own member object"); | ||
this.log.debug("missing own member object iin conversation"); | ||
} else { | ||
@@ -349,2 +410,6 @@ this.to = Object.assign({}, conversation.members); | ||
_setStatusAndEmit(status) { | ||
if (!this._isValidStatusTransition(status)) { | ||
this.log.warn(`status attempt switch from ${this.status} to ${status}`); | ||
return; | ||
} | ||
this.status = status; | ||
@@ -351,0 +416,0 @@ this.application.emit("call:status:changed", this); |
@@ -1,3 +0,11 @@ | ||
const LISTENER_GROUP = 'Stitch-errors'; | ||
/* | ||
* Nexmo Stitch SDK | ||
* Errors Emitter | ||
* | ||
* Copyright (c) Nexmo Inc. 2018 | ||
*/ | ||
const logger = require('loglevel'); | ||
const NexmoClientError = require('../conversationClientError').NexmoClientError; | ||
/** | ||
@@ -7,24 +15,44 @@ * Class that can emit errors via any emitter passed to it. | ||
* @param {Emitter} emitter - Any event emitter that implements "emit" and "releaseGroup". Basically object that is mixed with Wildemitter. | ||
* @property {string} LISTENER_GROUP='Stitch-errors' - the group this emitter will register | ||
* @emits Emitter#NXM-errors | ||
* @private | ||
*/ | ||
class ErrorsEmitter { | ||
constructor(emitter) { | ||
this.log = logger.getLogger(this.constructor.name); | ||
if (!emitter) { | ||
throw new NexmoClientError('no emitter object passed for the Error Emitter'); | ||
} | ||
this.emitter = emitter; | ||
this.LISTENER_GROUP = 'Stitch-errors'; | ||
} | ||
emitResponseIfError(response) { | ||
if (this._isTypeError(response.type)) { | ||
this.emitter.emit(response.type, LISTENER_GROUP, response); | ||
/** | ||
* Detect if the param.type includes error and emit that payload in the LISTENER_GROUP | ||
* @param param - the payload to forward in the LISTENER_GROUP | ||
* @param param.type - the type of the event to check if it's an error | ||
*/ | ||
emitResponseIfError(param) { | ||
if (this._isTypeError(param.type)) { | ||
return this.emitter.emit(param.type, this.LISTENER_GROUP, param); | ||
} | ||
return; | ||
} | ||
/** | ||
* Release Group on the registered emitter (using the namespace LISTENER_GROUP that is set) | ||
*/ | ||
cleanup() { | ||
this.emitter.releaseGroup(LISTENER_GROUP); | ||
return this.emitter.releaseGroup(this.LISTENER_GROUP); | ||
} | ||
_isTypeError(type) { | ||
return type.indexOf('error') !== -1; | ||
/** | ||
* Returns true if the param includes 'error' | ||
* @param {string} type - the error type to check | ||
*/ | ||
_isTypeError(param) { | ||
return param.indexOf('error') !== -1; | ||
} | ||
} | ||
module.exports = ErrorsEmitter; | ||
module.exports = ErrorsEmitter; |
@@ -64,2 +64,3 @@ /* | ||
this.parentConversation.rtcObjects = {}; | ||
this.rtcPromises = []; | ||
this.streamIndex = 0; | ||
@@ -327,8 +328,2 @@ this.rtcstats_enabled = (this.application.session.config && this.application.session.config.rtcstats_enabled); | ||
this.streamIndex++; | ||
this.parentConversation.me.emit("media:stream:on", { | ||
type, | ||
name: name, | ||
index: index, | ||
localStream | ||
}); | ||
const p = new Promise((resolve, reject) => { | ||
@@ -390,2 +385,9 @@ pc.createOffer() | ||
}); | ||
this.rtcPromises.push(p); | ||
this.parentConversation.me.emit("media:stream:on", { | ||
type, | ||
name: name, | ||
index: index, | ||
localStream | ||
}); | ||
const promisesArray = []; | ||
@@ -806,21 +808,36 @@ pc.onicecandidate = (event) => { | ||
/** | ||
* Mute our member | ||
* | ||
* @param {number} [streamIndex] stream id to set | ||
* @param {boolean} [mute] true for mute, false for unmute | ||
* @param {boolean} [audio=true] true for audio stream | ||
* @param {boolean} [video=false] true for video stream | ||
* @example <caption>Mute audio stream</caption> | ||
* media.mute(true, true, false) | ||
* @example <caption>Mute audio and video streams</caption> | ||
* media.mute(true, true, true) | ||
* @example <caption>Mute only video</caption> | ||
* media.mute(true, false, true) | ||
* @private | ||
*/ | ||
mute(mute, audio = true, video = false, streamIndex) { | ||
_setMediaTracksAndMute(rtc_id, tracks, mute, mediaType, mediaSuccess) { | ||
this._enableMediaTracks(tracks, !mute); | ||
return new Promise((resolve, reject) => { | ||
this._sendMuteRequest(rtc_id, mediaType, (response) => { | ||
if (response.type === mediaSuccess) { | ||
resolve(response.body); | ||
} else { | ||
this._enableMediaTracks(tracks, mute); | ||
reject(new NexmoApiError(response)); | ||
} | ||
}) | ||
}) | ||
} | ||
let tracks = []; | ||
_waitForAllRtcObject() { | ||
return Promise.all(this.rtcPromises); | ||
} | ||
/** | ||
* Mute our member | ||
* | ||
* @param {number} [streamIndex] stream id to set | ||
* @param {boolean} [mute] true for mute, false for unmute | ||
* @param {boolean} [audio=true] true for audio stream | ||
* @param {boolean} [video=false] true for video stream | ||
* @example <caption>Mute audio stream</caption> | ||
* media.mute(true, true, false) | ||
* @example <caption>Mute audio and video streams</caption> | ||
* media.mute(true, true, true) | ||
* @example <caption>Mute only video</caption> | ||
* media.mute(true, false, true) | ||
* @private | ||
*/ | ||
mute(mute, audio = true, video = false, streamIndex) { | ||
const state = mute ? 'on' : 'off'; | ||
@@ -834,59 +851,39 @@ const audioType = 'audio:mute:' + state; | ||
const self = this; | ||
if (audio) { | ||
let rtcObjects = []; | ||
if (streamIndex !== undefined) { | ||
const rtcObject = Object.values(this.parentConversation.rtcObjects).find((rtcObj => rtcObj.streamIndex === streamIndex)); | ||
if (rtcObject) { | ||
rtcObjects.push(rtcObject); | ||
} | ||
} else { | ||
rtcObjects = rtcObjects.concat(Object.values(this.parentConversation.rtcObjects)); | ||
} | ||
rtcObjects.forEach((rtcObject) => { | ||
let audioPromise = new Promise((resolve, reject) => { | ||
tracks = tracks.concat(rtcObject.localStream.getAudioTracks()); | ||
this._sendMuteRequest(rtcObject.id, audioType, (response) => { | ||
if (response.type === audioSuccess) { | ||
resolve(response.body); | ||
} else { | ||
reject(new NexmoApiError(response)); | ||
const audioPromise = this._waitForAllRtcObject() | ||
.then(() => { | ||
const rtcObject = Object.values(this.parentConversation.rtcObjects).find((rtcObj => rtcObj.streamIndex === streamIndex)); | ||
if (rtcObject) { | ||
const audioTracks = rtcObject.localStream.getAudioTracks(); | ||
return this._setMediaTracksAndMute(rtcObject.id, audioTracks, mute, audioType, audioSuccess); | ||
} | ||
}) | ||
}); | ||
}); | ||
promises.push(audioPromise); | ||
}); | ||
} | ||
} | ||
if (this.application.activeStream && this.application.activeStream.rtc_id) { | ||
const rtc_id = this.application.activeStream.rtc_id; | ||
const audioTracks = this.parentConversation.localStream.getAudioTracks(); | ||
const audioPromise = this._setMediaTracksAndMute(rtc_id, audioTracks, mute, audioType, audioSuccess); | ||
promises.push(audioPromise); | ||
} | ||
if (video) { | ||
let rtcObjects = []; | ||
if (streamIndex !== undefined) { | ||
const rtcObject = Object.values(this.parentConversation.rtcObjects).find((rtcObj => rtcObj.streamIndex === streamIndex)); | ||
if (rtcObject) { | ||
rtcObjects.push(rtcObject); | ||
} | ||
} else { | ||
rtcObjects = rtcObjects.concat(Object.values(this.parentConversation.rtcObjects)); | ||
const audioPromise = this._waitForAllRtcObject() | ||
.then(() => { | ||
const rtcObject = Object.values(this.parentConversation.rtcObjects).find((rtcObj => rtcObj.streamIndex === streamIndex)); | ||
if (rtcObject) { | ||
const videoTracks = rtcObject.localStream.getVideoTracks(); | ||
return this._setMediaTracksAndMute(rtcObject.id, videoTracks, mute, videoType, videoSuccess); | ||
} | ||
}); | ||
promises.push(audioPromise); | ||
} | ||
rtcObjects.forEach((rtcObject) => { | ||
let videoPromise = new Promise((resolve, reject) => { | ||
tracks = tracks.concat(rtcObject.localStream.getVideoTracks()); | ||
this._sendMuteRequest(rtcObject.id, videoType, (response) => { | ||
if (response.type === videoSuccess) { | ||
resolve(response.body); | ||
} else { | ||
reject(new NexmoApiError(response)); | ||
} | ||
}) | ||
}); | ||
promises.push(videoPromise); | ||
}); | ||
} | ||
this._enableMediaTracks(tracks, !mute); | ||
return Promise.all(promises).catch(function(response) { | ||
self._enableMediaTracks(tracks, mute); | ||
throw response; | ||
}); | ||
return Promise.all(promises); | ||
} | ||
@@ -946,7 +943,7 @@ | ||
const media_div = document.createElement("div"); | ||
media.appendChild(source); | ||
media_div.appendChild(media); | ||
document.insertBefore(media_div); | ||
// Older browsers may not have srcObject | ||
@@ -959,3 +956,3 @@ if ("srcObject" in media) { | ||
} | ||
media.onloadedmetadata = (e) => { | ||
@@ -1072,2 +1069,5 @@ media.play(); | ||
const do_gatherDone = () => { | ||
if (!this.parentConversation.pc) { | ||
return; | ||
} | ||
const event_to_emit = { | ||
@@ -1074,0 +1074,0 @@ type: 'rtc:new', |
@@ -79,3 +79,3 @@ /* | ||
screenShareExtensionId: '', | ||
SDK_version: '2.1.0-beta.3', | ||
SDK_version: '2.1.0-beta.4', | ||
url: 'https://ws.nexmo.com', | ||
@@ -143,3 +143,4 @@ iceServers: [{ | ||
reconnection: config.reconnection, | ||
autoConnect: config.autoConnect | ||
autoConnect: config.autoConnect, | ||
transports: ['websocket'] | ||
}); | ||
@@ -507,3 +508,2 @@ | ||
* logout from the cloud. | ||
* | ||
*/ | ||
@@ -519,6 +519,3 @@ logout() { | ||
this.disconnect(); | ||
if (this.errorsEmitter) { | ||
this.errorsEmitter.cleanup(); | ||
delete this.errorsEmitter; | ||
} | ||
delete this.errorsEmitter; | ||
delete this.application; | ||
@@ -570,11 +567,11 @@ delete this.cache; | ||
_updateTokenInCache(token, username) { | ||
if (this.cache) { | ||
this.cache.updateToken({ | ||
token: token, | ||
username: username | ||
}); | ||
} | ||
} | ||
_updateTokenInCache(token, username) { | ||
if (this.cache) { | ||
this.cache.updateToken({ | ||
token: token, | ||
username: username | ||
}); | ||
} | ||
} | ||
} | ||
module.exports = ConversationClient; |
@@ -5,3 +5,3 @@ { | ||
"repository": "https://github.com/Nexmo/conversation-js-sdk", | ||
"version": "2.1.0-beta.3", | ||
"version": "2.1.0-beta.4", | ||
"keywords": [ | ||
@@ -48,2 +48,3 @@ "nexmo", | ||
"grunt-string-replace": "^1.3.1", | ||
"grunt-terser": "^0.1.0", | ||
"grunt-umd": "^3.0.0", | ||
@@ -50,0 +51,0 @@ "jsdoc": "latest", |
Sorry, the diff of this file is too big to display
780374
-5.75%33
3.13%22196
-6.46%