@babblevoice/babble-drachtio-callmanager 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +179 -0
- package/index.js +45 -0
- package/jsdoc.conf.json +18 -0
- package/lib/call.js +2431 -0
- package/lib/callmanager.js +114 -0
- package/lib/sdp.js +452 -0
- package/lib/store.js +146 -0
- package/package.json +37 -0
- package/test/interface/call.js +1044 -0
- package/test/interface/callmanager.js +125 -0
- package/test/interface/callsdp.js +53 -0
- package/test/interface/events.js +87 -0
- package/test/interface/hold.js +174 -0
- package/test/interface/latecall.js +132 -0
- package/test/interface/sdp.js +296 -0
- package/test/interface/xfer.js +169 -0
- package/test/mock/srf.js +387 -0
- package/test/unit/store.js +153 -0
package/lib/call.js
ADDED
|
@@ -0,0 +1,2431 @@
|
|
|
1
|
+
|
|
2
|
+
const { v4: uuidv4 } = require( "uuid" )
|
|
3
|
+
const events = require( "events" )
|
|
4
|
+
|
|
5
|
+
const projectrtp = require( "projectrtp" ).projectrtp
|
|
6
|
+
|
|
7
|
+
const parseuri = require( "drachtio-srf" ).parseUri
|
|
8
|
+
const sdpgen = require( "./sdp.js" )
|
|
9
|
+
const callstore = require( "./store.js" )
|
|
10
|
+
|
|
11
|
+
const sipauth = require( "babble-drachtio-auth" )
|
|
12
|
+
|
|
13
|
+
/*
|
|
14
|
+
Enum for different reasons for hangup.
|
|
15
|
+
*/
|
|
16
|
+
const hangupcodes = {
|
|
17
|
+
/* Client error responses */
|
|
18
|
+
PAYMENT_REQUIRED: { "reason": "PAYMENT_REQUIRED", "sip": 402 },
|
|
19
|
+
FORBIDDEN: { "reason": "FORBIDDEN", "sip": 403 },
|
|
20
|
+
OUTGOING_CALL_BARRED: { "reason": "OUTGOING_CALL_BARRED", "sip": 403 },
|
|
21
|
+
INCOMING_CALL_BARRED: { "reason": "INCOMING_CALL_BARRED", "sip": 403 },
|
|
22
|
+
UNALLOCATED_NUMBER: { "reason": "UNALLOCATED_NUMBER", "sip": 404 },
|
|
23
|
+
NOT_ALLOWED: { "reason": "NOT_ALLOWED", "sip": 405 },
|
|
24
|
+
NOT_ACCEPTABLE: { "reason": "NOT_ACCEPTABLE", "sip": 406 },
|
|
25
|
+
PROXY_AUTHENTICATION: { "reason": "PROXY_AUTHENTICATION", "sip": 407 },
|
|
26
|
+
REQUEST_TIMEOUT: { "reason": "REQUEST_TIMEOUT", "sip": 408 },
|
|
27
|
+
USER_GONE: { "reason": "USER_GONE", "sip": 410 },
|
|
28
|
+
TEMPORARILY_UNAVAILABLE: { "reason": "TEMPORARILY_UNAVAILABLE", "sip": 480 },
|
|
29
|
+
CALL_DOES_NOT_EXIST: { "reason": "CALL_DOES_NOT_EXIST", "sip": 481 },
|
|
30
|
+
LOOP_DETECTED: { "reason": "LOOP_DETECTED", "sip": 482 },
|
|
31
|
+
TOO_MANY_HOPS: { "reason": "TOO_MANY_HOPS", "sip": 483 },
|
|
32
|
+
INVALID_NUMBER_FORMAT: { "reason": "INVALID_NUMBER_FORMAT", "sip": 484 },
|
|
33
|
+
AMBIGUOUS: { "reason": "AMBIGUOUS", "sip": 485 },
|
|
34
|
+
USER_BUSY: { "reason": "USER_BUSY", "sip": 486 },
|
|
35
|
+
NORMAL_CLEARING: { "reason": "NORMAL_CLEARING", "sip": 487 },
|
|
36
|
+
ORIGINATOR_CANCEL: { "reason": "ORIGINATOR_CANCEL", "sip": 487 },
|
|
37
|
+
USER_NOT_REGISTERED: { "reason": "USER_NOT_REGISTERED", "sip": 487 },
|
|
38
|
+
BLIND_TRANSFER: { "reason": "BLIND_TRANSFER", "sip": 487 },
|
|
39
|
+
ATTENDED_TRANSFER: { "reason": "ATTENDED_TRANSFER", "sip": 487 },
|
|
40
|
+
LOSE_RACE: { "reason": "LOSE_RACE", "sip": 487 },
|
|
41
|
+
PICKED_OFF: { "reason": "PICKED_OFF", "sip": 487 },
|
|
42
|
+
MANAGER_REQUEST: { "reason": "MANAGER_REQUEST", "sip": 487 },
|
|
43
|
+
REQUEST_TERMINATED: { "reason": "REQUEST_TERMINATED", "sip": 487 },
|
|
44
|
+
INCOMPATIBLE_DESTINATION: { "reason": "INCOMPATIBLE_DESTINATION", "sip": 488 },
|
|
45
|
+
/* Server error responses */
|
|
46
|
+
SERVER_ERROR: { "reason": "SERVER_ERROR", "sip": 500 },
|
|
47
|
+
FACILITY_REJECTED: { "reason": "FACILITY_REJECTED", "sip": 501 },
|
|
48
|
+
DESTINATION_OUT_OF_ORDER: { "reason": "DESTINATION_OUT_OF_ORDER", "sip": 502 },
|
|
49
|
+
SERVICE_UNAVAILABLE: { "reason": "SERVICE_UNAVAILABLE", "sip": 503 },
|
|
50
|
+
SERVER_TIMEOUT: { "reason": "SERVER_TIMEOUT", "sip": 504 },
|
|
51
|
+
MESSAGE_TOO_LARGE: { "reason": "MESSAGE_TOO_LARGE", "sip": 513 },
|
|
52
|
+
/* Global error responses */
|
|
53
|
+
BUSY_EVERYWHERE: { "reason": "BUSY_EVERYWHERE", "sip": 600 },
|
|
54
|
+
DECLINED: { "reason": "DECLINED", "sip": 603 },
|
|
55
|
+
DOES_NOT_EXIST_ANYWHERE: { "reason": "DOES_NOT_EXIST_ANYWHERE", "sip": 604 },
|
|
56
|
+
UNWANTED: { "reason": "UNWANTED", "sip": 607 },
|
|
57
|
+
REJECTED: { "reason": "REJECTED", "sip": 608 }
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/* Reverse codes - include inbound error codes.
|
|
61
|
+
If not in this list we return REQUEST_TERMINATED during creation */
|
|
62
|
+
const inboundsiperros = {
|
|
63
|
+
486: hangupcodes.USER_BUSY,
|
|
64
|
+
408: hangupcodes.REQUEST_TIMEOUT,
|
|
65
|
+
404: hangupcodes.UNALLOCATED_NUMBER
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
var callmanager
|
|
69
|
+
|
|
70
|
+
/** @class */
|
|
71
|
+
class call {
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
Construct our call object with all defaults, including a default UUID.
|
|
75
|
+
@constructs call
|
|
76
|
+
@hideconstructor
|
|
77
|
+
*/
|
|
78
|
+
constructor() {
|
|
79
|
+
this.uuid = uuidv4()
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
@enum {string} type "uas" | "uac"
|
|
83
|
+
@summary The type (uac or uas) from our perspective.
|
|
84
|
+
*/
|
|
85
|
+
this.type = "uac"
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
@typedef { Object } callstate
|
|
89
|
+
@property { boolean } trying
|
|
90
|
+
@property { boolean } ringing
|
|
91
|
+
@property { boolean } established
|
|
92
|
+
@property { boolean } canceled
|
|
93
|
+
@property { boolean } destroyed
|
|
94
|
+
@property { boolean } held
|
|
95
|
+
@property { boolean } authed
|
|
96
|
+
@property { boolean } cleaned
|
|
97
|
+
@property { boolean } refered
|
|
98
|
+
*/
|
|
99
|
+
|
|
100
|
+
/** @member { callstate } */
|
|
101
|
+
this.state = {
|
|
102
|
+
"trying": false,
|
|
103
|
+
"ringing": false,
|
|
104
|
+
"established": false,
|
|
105
|
+
"canceled": false,
|
|
106
|
+
"destroyed": false,
|
|
107
|
+
"held": false,
|
|
108
|
+
"authed": false,
|
|
109
|
+
"cleaned": false,
|
|
110
|
+
"refered": false
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
@member
|
|
115
|
+
@summary Channels which have been created
|
|
116
|
+
*/
|
|
117
|
+
this.channels = {
|
|
118
|
+
"audio": false,
|
|
119
|
+
"closed": {
|
|
120
|
+
"audio": []
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
@member
|
|
126
|
+
@summary Store our local and remote sdp objects
|
|
127
|
+
*/
|
|
128
|
+
this.sdp = {
|
|
129
|
+
"local": false,
|
|
130
|
+
"remote": false
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
@member
|
|
135
|
+
@summary UACs we create
|
|
136
|
+
*/
|
|
137
|
+
this.children = new Set()
|
|
138
|
+
/**
|
|
139
|
+
@member
|
|
140
|
+
@summary Who created us
|
|
141
|
+
*/
|
|
142
|
+
this.parent = false
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
@typedef {Object} epochs
|
|
146
|
+
@property {number} startat UNIX timestamp of when the call was started (created)
|
|
147
|
+
@property {number} answerat UNIX timestamp of when the call was answered
|
|
148
|
+
@property {number} endat UNIX timestamp of when the call ended
|
|
149
|
+
*/
|
|
150
|
+
|
|
151
|
+
/** @member {epochs} */
|
|
152
|
+
this.epochs = {
|
|
153
|
+
"startat": Math.floor( +new Date() / 1000 ),
|
|
154
|
+
"answerat": 0,
|
|
155
|
+
"endat": 0,
|
|
156
|
+
"mix": 0
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
@typedef {Object} sipdialog
|
|
161
|
+
@property {object} tags
|
|
162
|
+
@property {string} tags.local
|
|
163
|
+
@property {string} tags.remote
|
|
164
|
+
*/
|
|
165
|
+
/** @member {sipdialog} */
|
|
166
|
+
this.sip = {
|
|
167
|
+
"tags": {
|
|
168
|
+
"remote": "",
|
|
169
|
+
"local": ""
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
@typedef { Object } entity
|
|
175
|
+
@property { string } [ username ] username part
|
|
176
|
+
@property { string } [ realm ] realm (domain) part
|
|
177
|
+
@property { string } [ uri ] full uri
|
|
178
|
+
@property { string } [ display ] how the user should be displayed
|
|
179
|
+
*/
|
|
180
|
+
/**
|
|
181
|
+
For inbound calls - this is discovered by authentication. For outbound
|
|
182
|
+
this is requested by the caller - i.e. the destination is the registered user.
|
|
183
|
+
@member { _entity }
|
|
184
|
+
@private
|
|
185
|
+
*/
|
|
186
|
+
this._entity
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
Override caller id or name.
|
|
190
|
+
@member { _remote }
|
|
191
|
+
@private
|
|
192
|
+
*/
|
|
193
|
+
this._remote = {
|
|
194
|
+
"id": false,
|
|
195
|
+
"name": false
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* @member { object }
|
|
200
|
+
* @summary contains network information regarding call
|
|
201
|
+
*/
|
|
202
|
+
this.network = {
|
|
203
|
+
"remote": {
|
|
204
|
+
"address": "",
|
|
205
|
+
"port": 0,
|
|
206
|
+
"protocol": ""
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
@member {object}
|
|
212
|
+
@summary user definable object that allows other modules to store data in this call.
|
|
213
|
+
*/
|
|
214
|
+
this.vars = {}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
@member {string}
|
|
218
|
+
@private
|
|
219
|
+
*/
|
|
220
|
+
this._receivedtelevents = ""
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
@member {object}
|
|
224
|
+
@private
|
|
225
|
+
*/
|
|
226
|
+
this._promises = {
|
|
227
|
+
"resolve": {
|
|
228
|
+
"auth": false,
|
|
229
|
+
"hangup": false,
|
|
230
|
+
"events": false,
|
|
231
|
+
"channelevent": false
|
|
232
|
+
},
|
|
233
|
+
"reject": {
|
|
234
|
+
"auth": false
|
|
235
|
+
},
|
|
236
|
+
"promise": {
|
|
237
|
+
"hangup": false,
|
|
238
|
+
"events": false,
|
|
239
|
+
"channelevent": false
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
@member {object}
|
|
245
|
+
@private
|
|
246
|
+
*/
|
|
247
|
+
this._timers = {
|
|
248
|
+
"auth": false,
|
|
249
|
+
"newuac": false,
|
|
250
|
+
"events": false,
|
|
251
|
+
"seinterval": false,
|
|
252
|
+
"anyevent": false
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
@member {object}
|
|
257
|
+
@private
|
|
258
|
+
*/
|
|
259
|
+
this._em = new events.EventEmitter()
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
@member
|
|
263
|
+
@private
|
|
264
|
+
*/
|
|
265
|
+
this._auth = sipauth.create( callmanager.options.proxy )
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
Enable access for other modules.
|
|
269
|
+
*/
|
|
270
|
+
this.hangupcodes = hangupcodes
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
@typedef entity
|
|
275
|
+
@property { string } username
|
|
276
|
+
@property { string } realm
|
|
277
|
+
@property { string } uri
|
|
278
|
+
@property { string } display
|
|
279
|
+
@property { number } ccc - Current Call Count
|
|
280
|
+
*/
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
Returns the entity if known (i.e. outbound or inbound authed).
|
|
284
|
+
@returns { Promise< entity > }
|
|
285
|
+
*/
|
|
286
|
+
get entity() {
|
|
287
|
+
|
|
288
|
+
return ( async () => {
|
|
289
|
+
if( !this._entity ) return
|
|
290
|
+
|
|
291
|
+
if( !this._entity.username && this._entity.uri ) {
|
|
292
|
+
this._entity.username = this._entity.uri.split( "@" )[ 0 ]
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if( !this._entity.realm && this._entity.uri ) {
|
|
296
|
+
let uriparts = this._entity.uri.split( "@" )
|
|
297
|
+
if( uriparts.length > 1 )
|
|
298
|
+
this._entity.realm = uriparts[ 1 ]
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if( !this._entity.uri && this._entity.username && this._entity.realm ) {
|
|
302
|
+
this._entity.uri = this._entity.username + "@" + this._entity.realm
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
let entitycalls = await callstore.getbyentity( this._entity.uri )
|
|
306
|
+
|
|
307
|
+
return {
|
|
308
|
+
"username": this._entity.username,
|
|
309
|
+
"realm": this._entity.realm,
|
|
310
|
+
"uri": this._entity.uri,
|
|
311
|
+
"display": this._entity.display?this._entity.display:"",
|
|
312
|
+
"ccc": entitycalls.size
|
|
313
|
+
}
|
|
314
|
+
} )()
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
@typedef remoteid
|
|
319
|
+
@property { string } host
|
|
320
|
+
@property { string } user
|
|
321
|
+
@property { string } name
|
|
322
|
+
@property { string } uri
|
|
323
|
+
@property { boolean } privacy
|
|
324
|
+
@property { string } type - "callerid" | "calledid"
|
|
325
|
+
*/
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
@typedef callerid
|
|
329
|
+
@property { string } id
|
|
330
|
+
@property { string } name
|
|
331
|
+
*/
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Sets caller id name or id
|
|
335
|
+
* @param { callerid } rem
|
|
336
|
+
*/
|
|
337
|
+
set remote( rem ) {
|
|
338
|
+
|
|
339
|
+
for( const key in this._remote ) {
|
|
340
|
+
if( rem[ key ] in rem ) {
|
|
341
|
+
this._remote[ key ] = rem[ key ]
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
Returns the caller or called id, number, name and domains + privacy if set.
|
|
348
|
+
@returns { remoteid } remoteid
|
|
349
|
+
*/
|
|
350
|
+
get remote() {
|
|
351
|
+
|
|
352
|
+
switch( this.type ) {
|
|
353
|
+
case "uac": {
|
|
354
|
+
|
|
355
|
+
/* "Display Name" <sip:0123456789@bling.babblevoice.com>;party=calling;screen=yes;privacy=off */
|
|
356
|
+
if( this._entity ) {
|
|
357
|
+
return {
|
|
358
|
+
"name": this._entity.display,
|
|
359
|
+
"uri": this._entity.uri,
|
|
360
|
+
"user": this._entity.username,
|
|
361
|
+
"host": this._entity.realm,
|
|
362
|
+
"privacy": false,
|
|
363
|
+
"type": "calledid"
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
if( this.options && this.options.contact ) {
|
|
368
|
+
let parseduri = parseuri( this.options.contact )
|
|
369
|
+
return {
|
|
370
|
+
"name": "",
|
|
371
|
+
"uri": this.options.contact,
|
|
372
|
+
"user": parseduri.user,
|
|
373
|
+
"host": parseduri.host,
|
|
374
|
+
"privacy": false,
|
|
375
|
+
"type": "calledid"
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/* we shouldn't get here */
|
|
380
|
+
return {
|
|
381
|
+
"name": "",
|
|
382
|
+
"uri": "",
|
|
383
|
+
"user": "0000000000",
|
|
384
|
+
"host": "localhost.localdomain",
|
|
385
|
+
"privacy": false,
|
|
386
|
+
"type": "calledid"
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
default: {
|
|
390
|
+
/* uas - inbound */
|
|
391
|
+
let parsed
|
|
392
|
+
if( this._entity ) {
|
|
393
|
+
return {
|
|
394
|
+
"name": this._remote.name?this._remote.name:(this._entity.display),
|
|
395
|
+
"uri": this._entity.uri,
|
|
396
|
+
"user": this._remote.id?this._remote.id:(this._entity.username),
|
|
397
|
+
"host": this._entity.realm,
|
|
398
|
+
"privacy": false,
|
|
399
|
+
"type": "callerid"
|
|
400
|
+
}
|
|
401
|
+
} else if( this._req.has( "p-asserted-identity" ) ) {
|
|
402
|
+
parsed = this._req.getParsedHeader( "p-asserted-identity" )
|
|
403
|
+
} else if( this._req.has( "remote-party-id" ) ) {
|
|
404
|
+
parsed = this._req.getParsedHeader( "remote-party-id" )
|
|
405
|
+
} else {
|
|
406
|
+
parsed = this._req.getParsedHeader( "from" )
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
let parseduri = parseuri( parsed.uri )
|
|
410
|
+
if( !parsed ) parsed = {}
|
|
411
|
+
if( !parsed.params ) parsed.params = {}
|
|
412
|
+
|
|
413
|
+
return {
|
|
414
|
+
"name": this._remote.name?this._remote.name:( !parsed.name?"":parsed.name.replace( /['"]+/g, "" ) ),
|
|
415
|
+
"uri": parsed.uri,
|
|
416
|
+
"user": this._remote.id?this._remote.id:(parseduri.user),
|
|
417
|
+
"host": parseduri.host,
|
|
418
|
+
"privacy": parsed.params.privacy === "true",
|
|
419
|
+
"type": "callerid"
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
@typedef destination
|
|
427
|
+
@property { string } host
|
|
428
|
+
@property { string } user
|
|
429
|
+
*/
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
Return the destination of the call.
|
|
433
|
+
@return { destination } destination - parsed uri
|
|
434
|
+
*/
|
|
435
|
+
get destination() {
|
|
436
|
+
if( undefined !== this.referingtouri ) {
|
|
437
|
+
return parseuri( this.referingtouri )
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
if( "uac" == this.type ) {
|
|
441
|
+
return parseuri( this.sip.contact[ 0 ].uri )
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
return parseuri( this._req.msg.uri )
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/*
|
|
448
|
+
State functions
|
|
449
|
+
Get state as a string
|
|
450
|
+
According to state machine in RFC 4235, we send early if we have received a 1xx with tag
|
|
451
|
+
I am going to use 100 and 180 - which should be the same.
|
|
452
|
+
*/
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
hasmedia
|
|
456
|
+
@return {bool} - true if the call has media (i.e. is established on not held).
|
|
457
|
+
*/
|
|
458
|
+
get hasmedia() {
|
|
459
|
+
if( this.state.held ) return false
|
|
460
|
+
return true == this.state.established
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
trying
|
|
465
|
+
@return {bool} - true if the call has been trying.
|
|
466
|
+
*/
|
|
467
|
+
set trying( s ) {
|
|
468
|
+
if( this.state.trying != s ) {
|
|
469
|
+
this.state.trying = s
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/**
|
|
474
|
+
trying
|
|
475
|
+
@return {bool} - true if the call has been trying.
|
|
476
|
+
*/
|
|
477
|
+
get trying() {
|
|
478
|
+
return this.state.trying
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
ringing
|
|
483
|
+
@return {bool} - true if the call has been ringing.
|
|
484
|
+
*/
|
|
485
|
+
get ringing() {
|
|
486
|
+
return this.state.ringing
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* @param { boolean } r - the new state
|
|
491
|
+
*/
|
|
492
|
+
set ringing( r ) {
|
|
493
|
+
this.state.ringing = r
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
established - if the call isn't already established then set the answerat time.
|
|
498
|
+
@param {bool} s - true if the call has been established.
|
|
499
|
+
*/
|
|
500
|
+
set established( s ) {
|
|
501
|
+
if( this.state.established != s ) {
|
|
502
|
+
this.epochs.answerat = Math.floor( +new Date() / 1000 )
|
|
503
|
+
this.state.established = s
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
/**
|
|
508
|
+
established
|
|
509
|
+
@return {bool} - true if the call has been established.
|
|
510
|
+
*/
|
|
511
|
+
get established() {
|
|
512
|
+
return this.state.established
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
/**
|
|
516
|
+
@summary canceled - if the call isn't already canceled then set the endat time.
|
|
517
|
+
@type {boolean}
|
|
518
|
+
*/
|
|
519
|
+
set canceled( s ) {
|
|
520
|
+
if( this.state.canceled != s ) {
|
|
521
|
+
this.epochs.endat = Math.floor( +new Date() / 1000 )
|
|
522
|
+
this.state.canceled = s
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
/**
|
|
527
|
+
@summary is the call canceled
|
|
528
|
+
@return {boolean} - true if the call has been canceled.
|
|
529
|
+
*/
|
|
530
|
+
get canceled() {
|
|
531
|
+
return true == this.state.canceled
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
/**
|
|
535
|
+
destroyed - if the call isn't already desroyed then set the endat time.
|
|
536
|
+
@param {bool} s - true if the call has been destroyed.
|
|
537
|
+
*/
|
|
538
|
+
set destroyed( s ) {
|
|
539
|
+
if( this.state.destroyed != s ) {
|
|
540
|
+
this.epochs.endat = Math.floor( +new Date() / 1000 )
|
|
541
|
+
this.state.destroyed = s
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
/**
|
|
546
|
+
destroyed
|
|
547
|
+
@return {bool} - true if teh call has been destroyed.
|
|
548
|
+
*/
|
|
549
|
+
get destroyed() {
|
|
550
|
+
return true == this.state.destroyed
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
/**
|
|
554
|
+
@summary the current state of the call as a string: trying|proceeding|early|confirmed|terminated
|
|
555
|
+
@return {string}
|
|
556
|
+
*/
|
|
557
|
+
get statestr() {
|
|
558
|
+
if( this.state.established ) {
|
|
559
|
+
return "confirmed"
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
if( this.state.ringing ) {
|
|
563
|
+
return "early"
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
if( this.state.trying ) {
|
|
567
|
+
return "proceeding"
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
if( this.state.destroyed ) {
|
|
571
|
+
return "terminated"
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
return "trying"
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
/**
|
|
578
|
+
duration
|
|
579
|
+
@return {number} - the number of seconds between now (or endat if ended) and the time the call was started.
|
|
580
|
+
*/
|
|
581
|
+
get duration() {
|
|
582
|
+
if( 0 !== this.epochs.endat ) return parseInt( this.epochs.endat - this.epochs.startat )
|
|
583
|
+
return parseInt( Math.floor( +new Date() / 1000 ) - this.epochs.startat )
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
/**
|
|
587
|
+
Get the estrablished time.
|
|
588
|
+
@return {number} - the number of seconds between now (or endat if ended) and the time the call was answered.
|
|
589
|
+
*/
|
|
590
|
+
get billingduration() {
|
|
591
|
+
if( 0 === this.epochs.answerat ) return 0
|
|
592
|
+
if( 0 !== this.epochs.endat ) return parseInt( this.epochs.endat - this.epochs.answerat )
|
|
593
|
+
return parseInt( Math.floor( +new Date() / 1000 ) - this.epochs.answerat )
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
/**
|
|
597
|
+
Callback for events we pass back to inerested parties.
|
|
598
|
+
@callback call.event
|
|
599
|
+
@param {object} call - we pass *this back into the requester
|
|
600
|
+
*/
|
|
601
|
+
|
|
602
|
+
/**
|
|
603
|
+
Registers an event callback for this specific call. An event sink registered
|
|
604
|
+
on this member will receive events only for this call. We emit on call specific
|
|
605
|
+
emitter and a global emitter.
|
|
606
|
+
@param { string } ev - The contact string for registered or other sip contact
|
|
607
|
+
@param { call.event } cb
|
|
608
|
+
*/
|
|
609
|
+
on( ev, cb ) {
|
|
610
|
+
this._em.on( ev, cb )
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
/**
|
|
614
|
+
See event emitter once
|
|
615
|
+
@param { string } ev - The contact string for registered or other sip contact
|
|
616
|
+
@param { call.event } cb
|
|
617
|
+
*/
|
|
618
|
+
once( ev, cb ) {
|
|
619
|
+
this._em.once( ev, cb )
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
/**
|
|
623
|
+
See event emitter off
|
|
624
|
+
@param { string } ev - The contact string for registered or other sip contact
|
|
625
|
+
@param { call.event } cb
|
|
626
|
+
*/
|
|
627
|
+
off( ev, cb ) {
|
|
628
|
+
if( !cb ) {
|
|
629
|
+
this._em.removeAllListeners( ev )
|
|
630
|
+
return
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
this._em.off( ev, cb )
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
/**
|
|
637
|
+
See event emitter removeAllListeners
|
|
638
|
+
@param { string } ev - The contact string for registered or other sip contact
|
|
639
|
+
*/
|
|
640
|
+
removealllisteners( ev ) {
|
|
641
|
+
if( !ev ) {
|
|
642
|
+
let evnames = this._em.eventNames()
|
|
643
|
+
for( let evname of evnames ) {
|
|
644
|
+
this._em.removeAllListeners( evname )
|
|
645
|
+
}
|
|
646
|
+
} else {
|
|
647
|
+
this._em.removeAllListeners( ev )
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
/**
|
|
652
|
+
See event emitter setMaxListeners
|
|
653
|
+
@param { number } n
|
|
654
|
+
*/
|
|
655
|
+
setmaxlisteners( n ) {
|
|
656
|
+
this._em.setMaxListeners( n )
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
/**
|
|
660
|
+
Allows 3rd parties to emit events to listeners specific to this call.
|
|
661
|
+
@param { string } ev - event name
|
|
662
|
+
*/
|
|
663
|
+
emit( ev ) {
|
|
664
|
+
this._em.emit( ev, this )
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
/**
|
|
668
|
+
Call creation event.
|
|
669
|
+
@event call.new
|
|
670
|
+
@type {call}
|
|
671
|
+
*/
|
|
672
|
+
|
|
673
|
+
/**
|
|
674
|
+
Emitted when a call is ringing
|
|
675
|
+
@event call.ringing
|
|
676
|
+
@type {call}
|
|
677
|
+
*/
|
|
678
|
+
|
|
679
|
+
/**
|
|
680
|
+
Emitted when a call is answered
|
|
681
|
+
@event call.answered
|
|
682
|
+
@type {call}
|
|
683
|
+
*/
|
|
684
|
+
|
|
685
|
+
/**
|
|
686
|
+
Emitted when a call is mixed with another call (not after unhold as this has it's own event)
|
|
687
|
+
@event call.mix
|
|
688
|
+
@type {call}
|
|
689
|
+
*/
|
|
690
|
+
|
|
691
|
+
/**
|
|
692
|
+
Emitted when a call is authed
|
|
693
|
+
@event call.authed
|
|
694
|
+
@type {call}
|
|
695
|
+
*/
|
|
696
|
+
|
|
697
|
+
/**
|
|
698
|
+
Emitted when a call auth fails
|
|
699
|
+
@event call.authed.failed
|
|
700
|
+
@type {call}
|
|
701
|
+
*/
|
|
702
|
+
|
|
703
|
+
/**
|
|
704
|
+
Emitted when a call is placed on hold
|
|
705
|
+
@event call.hold
|
|
706
|
+
@type {call}
|
|
707
|
+
*/
|
|
708
|
+
|
|
709
|
+
/**
|
|
710
|
+
Emitted when a call is taken off hold
|
|
711
|
+
@event call.unhold
|
|
712
|
+
@type {call}
|
|
713
|
+
*/
|
|
714
|
+
|
|
715
|
+
/**
|
|
716
|
+
Emitted when a call is destroyed
|
|
717
|
+
@event call.destroyed
|
|
718
|
+
@type {call}
|
|
719
|
+
*/
|
|
720
|
+
|
|
721
|
+
/**
|
|
722
|
+
Emitted immediatly called after call.destroyed
|
|
723
|
+
@event call.reporting
|
|
724
|
+
@type {call}
|
|
725
|
+
*/
|
|
726
|
+
|
|
727
|
+
/**
|
|
728
|
+
Emitted immediatly before a call is picked
|
|
729
|
+
@event call.pick
|
|
730
|
+
@type {call}
|
|
731
|
+
*/
|
|
732
|
+
|
|
733
|
+
/**
|
|
734
|
+
Emits the event call.pick to allow other parts of dial plan to give up on further processing.
|
|
735
|
+
It wuld be normal to bridge this call to another after this call has been made.
|
|
736
|
+
*/
|
|
737
|
+
pick() {
|
|
738
|
+
this._em.emit( "call.pick", this )
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
/**
|
|
742
|
+
Delink calls logically - any calls which have parent or children they are all removed.
|
|
743
|
+
when the dialog is either answered (or doesn't answer for some reason).
|
|
744
|
+
The promise resolves to a new call is one is generated, or undefined if not.
|
|
745
|
+
*/
|
|
746
|
+
detach() {
|
|
747
|
+
if( this.parent ) {
|
|
748
|
+
this.parent.children.delete( this )
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
for( let child of this.children ) {
|
|
752
|
+
child.parent = false
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
this.parent = false
|
|
756
|
+
this.children.clear()
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
/**
|
|
760
|
+
Logically adopt a child call
|
|
761
|
+
@param { call } other
|
|
762
|
+
*/
|
|
763
|
+
adopt( other, mix ) {
|
|
764
|
+
other.parent = this
|
|
765
|
+
this.children.add( other )
|
|
766
|
+
|
|
767
|
+
if( mix ) {
|
|
768
|
+
this.channels.audio.mix( other.channels.audio )
|
|
769
|
+
|
|
770
|
+
this._em.emit( "call.mix", this )
|
|
771
|
+
callmanager.options.em.emit( "call.mix", this )
|
|
772
|
+
other._em.emit( "call.mix", other )
|
|
773
|
+
callmanager.options.em.emit( "call.mix", other )
|
|
774
|
+
|
|
775
|
+
this.epochs.mix = Math.floor( +new Date() / 1000 )
|
|
776
|
+
other.epochs.mix = Math.floor( +new Date() / 1000 )
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
/**
|
|
781
|
+
Called from newuac when we receive a 180
|
|
782
|
+
@private
|
|
783
|
+
*/
|
|
784
|
+
_onring() {
|
|
785
|
+
if( this.state.ringing ) return
|
|
786
|
+
this.state.ringing = true
|
|
787
|
+
if( false !== this.parent ) {
|
|
788
|
+
this.parent.ring()
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
this._em.emit( "call.ringing", this )
|
|
792
|
+
callmanager.options.em.emit( "call.ringing", this )
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
/**
|
|
796
|
+
Called from newuac when we are answered and we have a dialog,
|
|
797
|
+
this = child call (the new call - the bleg)
|
|
798
|
+
@private
|
|
799
|
+
*/
|
|
800
|
+
async _onanswer() {
|
|
801
|
+
|
|
802
|
+
let hangups = []
|
|
803
|
+
if( this.parent ) {
|
|
804
|
+
for( let child of this.parent.children ) {
|
|
805
|
+
if( child.uuid !== this.uuid ) {
|
|
806
|
+
child.detach()
|
|
807
|
+
/* do not await - we do not want to delay the winner in
|
|
808
|
+
connecting by waiting for the completion of the hangups */
|
|
809
|
+
hangups.push( child.hangup( hangupcodes.LOSE_RACE ) )
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
if( this.state.destroyed ) return this
|
|
815
|
+
callstore.set( this )
|
|
816
|
+
|
|
817
|
+
if( true === this.options.noAck ) {
|
|
818
|
+
await this._onlatebridge()
|
|
819
|
+
} else {
|
|
820
|
+
await this._onearlybridge()
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
this.established = true
|
|
824
|
+
this.sip.tags.remote = this._dialog.sip.remoteTag
|
|
825
|
+
|
|
826
|
+
let r = this._promises.resolve.newuac
|
|
827
|
+
this._promises.resolve.newuac = false
|
|
828
|
+
this._promises.reject.newuac = false
|
|
829
|
+
|
|
830
|
+
if( hangups.length > 0 ) {
|
|
831
|
+
await Promise.all( hangups )
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
if( r ) r( this )
|
|
835
|
+
return this
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
/**
|
|
839
|
+
On an early negotiation we have already sent our sdp without
|
|
840
|
+
knowing what the otherside is going to offer. We now have the
|
|
841
|
+
other sides SDP so we can work out the first common CODEC.
|
|
842
|
+
this = child call (the new call - the bleg)
|
|
843
|
+
@private
|
|
844
|
+
*/
|
|
845
|
+
async _onearlybridge() {
|
|
846
|
+
if( this.destroyed ) return
|
|
847
|
+
|
|
848
|
+
this._addevents( this._dialog )
|
|
849
|
+
|
|
850
|
+
this.sdp.remote = sdpgen.create( this._dialog.remote.sdp )
|
|
851
|
+
this.selectedcodec = this.sdp.remote.intersection( this.options.preferedcodecs, true )
|
|
852
|
+
if( "" == this.selectedcodec ) {
|
|
853
|
+
return this.hangup( hangupcodes.INCOMPATIBLE_DESTINATION )
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
let target = this.sdp.remote.getaudio()
|
|
857
|
+
if( !target ) return
|
|
858
|
+
|
|
859
|
+
if( this._iswebrtc ) {
|
|
860
|
+
let actpass = "act"
|
|
861
|
+
if( "act" == this.sdp.remote.sdp.media[ 0 ].setup ) actpass = "pass" /* act|pass|actpass */
|
|
862
|
+
|
|
863
|
+
channeldef.remote.dtls = {
|
|
864
|
+
"fingerprint": this.sdp.remote.sdp.media[ 0 ].fingerprint.hash, /* remote hash */
|
|
865
|
+
"setup": actpass /* "act"|"pass" */
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
this.channels.audio.remote( call._createchannelremotedef( target.address, target.port, target.audio.payloads[ 0 ] ) )
|
|
870
|
+
|
|
871
|
+
if( this.parent ) {
|
|
872
|
+
if( !this.parent.established ) {
|
|
873
|
+
await this.parent.answer( { "preferedcodecs": this.selectedcodec } )
|
|
874
|
+
.catch( ( err ) => {
|
|
875
|
+
console.error( err )
|
|
876
|
+
} )
|
|
877
|
+
|
|
878
|
+
if( !this.parent.established ) {
|
|
879
|
+
return this.hangup( hangupcodes.USER_GONE )
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
this.channels.audio.mix( this.parent.channels.audio )
|
|
884
|
+
|
|
885
|
+
this._em.emit( "call.mix", this )
|
|
886
|
+
callmanager.options.em.emit( "call.mix", this )
|
|
887
|
+
this.parent._em.emit( "call.mix", this.parent )
|
|
888
|
+
callmanager.options.em.emit( "call.mix", this.parent )
|
|
889
|
+
|
|
890
|
+
this.epochs.mix = Math.floor( +new Date() / 1000 )
|
|
891
|
+
if( this.parent ) this.parent.epochs.mix = Math.floor( +new Date() / 1000 )
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
return this
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
/**
|
|
898
|
+
Accept and bridge to calls with late negotiation.
|
|
899
|
+
this = child call (the new call - the bleg)
|
|
900
|
+
OR
|
|
901
|
+
this = standalone call - no other legs
|
|
902
|
+
@private
|
|
903
|
+
*/
|
|
904
|
+
async _onlatebridge() {
|
|
905
|
+
|
|
906
|
+
/* Calculate the best codec for both legs - find a common codec if possible
|
|
907
|
+
if not - transcode */
|
|
908
|
+
|
|
909
|
+
this.sdp.remote = sdpgen.create( this._req.msg.body )
|
|
910
|
+
|
|
911
|
+
let alegremotesdp
|
|
912
|
+
if( this.parent ) {
|
|
913
|
+
if( this.parent.established ) {
|
|
914
|
+
alegremotesdp = this.parent.sdp.remote
|
|
915
|
+
} else {
|
|
916
|
+
alegremotesdp = sdpgen.create( this.parent._req.msg.body )
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
this.parent.selectedcodec = this.selectedcodec = this.sdp.remote.intersection(
|
|
920
|
+
alegremotesdp.intersection( this.options.preferedcodecs ), true )
|
|
921
|
+
|
|
922
|
+
if( "" == this.selectedcodec ) {
|
|
923
|
+
/* Ok - transcode */
|
|
924
|
+
this.selectedcodec = this.sdp.remote.intersection( this.options.preferedcodecs, true )
|
|
925
|
+
this.parent.selectedcodec = this.options.preferedcodecs
|
|
926
|
+
if( "" == this.selectedcodec || "" == this.parent.selectedcodec ) {
|
|
927
|
+
return this.hangup( hangupcodes.INCOMPATIBLE_DESTINATION )
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
} else {
|
|
931
|
+
/* no parent - just pick our prefered codec */
|
|
932
|
+
this.selectedcodec = this.sdp.remote.intersection( this.options.preferedcodecs, true )
|
|
933
|
+
if( "" == this.selectedcodec ) {
|
|
934
|
+
return this.hangup( hangupcodes.INCOMPATIBLE_DESTINATION )
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
let target = this.sdp.remote.getaudio()
|
|
939
|
+
if( !target ) return
|
|
940
|
+
let channeldef = call._createchannelremotedef( target.address, target.port, target.audio.payloads[ 0 ] )
|
|
941
|
+
|
|
942
|
+
this.channels.audio = await projectrtp.openchannel( channeldef, this._handlechannelevents.bind( this ) )
|
|
943
|
+
|
|
944
|
+
this.sdp.local = sdpgen.create()
|
|
945
|
+
.addcodecs( this.selectedcodec )
|
|
946
|
+
.setconnectionaddress( this.channels.audio.local.address )
|
|
947
|
+
.setaudioport( this.channels.audio.local.port )
|
|
948
|
+
|
|
949
|
+
if( true === this.options.rfc2833 ) {
|
|
950
|
+
this.sdp.local.addcodecs( "2833" )
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
this._dialog = await this._dialog.ack( this.sdp.local.toString() )
|
|
954
|
+
this._addevents( this._dialog )
|
|
955
|
+
|
|
956
|
+
if( this.parent ) {
|
|
957
|
+
if( !this.parent.established ) {
|
|
958
|
+
await this.parent.answer( { "preferedcodecs": this.selectedcodec } )
|
|
959
|
+
.catch( ( err ) => {
|
|
960
|
+
console.error( err )
|
|
961
|
+
} )
|
|
962
|
+
|
|
963
|
+
if( !this.parent.established ) {
|
|
964
|
+
return this.hangup( hangupcodes.USER_GONE )
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
this.channels.audio.mix( this.parent.channels.audio )
|
|
969
|
+
|
|
970
|
+
this._em.emit( "call.mix", this )
|
|
971
|
+
callmanager.options.em.emit( "call.mix", this )
|
|
972
|
+
this.parent._em.emit( "call.mix", this.parent )
|
|
973
|
+
callmanager.options.em.emit( "call.mix", this.parent )
|
|
974
|
+
|
|
975
|
+
this.epochs.mix = Math.floor( +new Date() / 1000 )
|
|
976
|
+
if( this.parent ) this.parent.epochs.mix = Math.floor( +new Date() / 1000 )
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
return this
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
/**
|
|
983
|
+
Sometimes we don't care who if we are the parent or child - we just want the other party
|
|
984
|
+
@return {object|bool} returns call object or if none false
|
|
985
|
+
*/
|
|
986
|
+
get other() {
|
|
987
|
+
if( this.parent ) return this.parent
|
|
988
|
+
|
|
989
|
+
for( const child of this.children ) {
|
|
990
|
+
if( child.established ) {
|
|
991
|
+
return child
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
if( this.children.length > 0 ) return this.children[ 0 ]
|
|
996
|
+
|
|
997
|
+
return false
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
/**
|
|
1001
|
+
auth - returns promise. This will force a call to be authed by a client. If the call
|
|
1002
|
+
has been refered by another client that has been authed this call will assume that auth.
|
|
1003
|
+
@todo check refering call has been authed
|
|
1004
|
+
@return {Promise} Returns promise which resolves on success or rejects on failed auth. If not caught this framework will catch and cleanup.
|
|
1005
|
+
*/
|
|
1006
|
+
auth() {
|
|
1007
|
+
return new Promise( ( resolve, reject ) => {
|
|
1008
|
+
|
|
1009
|
+
if( undefined !== this.referingtouri ) {
|
|
1010
|
+
/* if we have been refered the call has been authed to proceed by the refering party */
|
|
1011
|
+
resolve()
|
|
1012
|
+
return
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
this._promises.resolve.auth = resolve
|
|
1016
|
+
this._promises.reject.auth = reject
|
|
1017
|
+
|
|
1018
|
+
this._timers.auth = setTimeout( () => {
|
|
1019
|
+
this._promises.reject.auth()
|
|
1020
|
+
this._promises.resolve.auth = false
|
|
1021
|
+
this._promises.reject.auth = false
|
|
1022
|
+
this._timers.auth = false
|
|
1023
|
+
|
|
1024
|
+
this.hangup( hangupcodes.REQUEST_TIMEOUT )
|
|
1025
|
+
|
|
1026
|
+
}, 50000 )
|
|
1027
|
+
|
|
1028
|
+
if( !this._auth.requestauth( this._req, this._res ) ) return this.hangup( hangupcodes.FORBIDDEN )
|
|
1029
|
+
} )
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
/**
|
|
1033
|
+
Called by us we handle the auth challenge in this function
|
|
1034
|
+
@private
|
|
1035
|
+
*/
|
|
1036
|
+
async _onauth( req, res ) {
|
|
1037
|
+
|
|
1038
|
+
/* have we got an auth responce */
|
|
1039
|
+
if( !this._auth.has( req ) ) return
|
|
1040
|
+
|
|
1041
|
+
this._req = req
|
|
1042
|
+
this._res = res
|
|
1043
|
+
|
|
1044
|
+
this._req.on( "cancel", () => this._oncanceled() )
|
|
1045
|
+
|
|
1046
|
+
let authorization = this._auth.parseauthheaders( this._req, this._res )
|
|
1047
|
+
|
|
1048
|
+
if( undefined === callmanager.options.userlookup ) {
|
|
1049
|
+
this._promises.reject.auth( "no userlookup function provided")
|
|
1050
|
+
this._promises.resolve.auth = false
|
|
1051
|
+
this._promises.reject.auth = false
|
|
1052
|
+
return
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
let user = await callmanager.options.userlookup( authorization.username, authorization.realm )
|
|
1056
|
+
|
|
1057
|
+
if( !user || !this._auth.verifyauth( this._req, authorization, user.secret ) ) {
|
|
1058
|
+
|
|
1059
|
+
if( this._auth.stale ) {
|
|
1060
|
+
return this._auth.requestauth( this._req, this._res )
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
this.hangup( hangupcodes.FORBIDDEN )
|
|
1064
|
+
|
|
1065
|
+
let r = this._promises.reject.auth
|
|
1066
|
+
this._promises.resolve.auth = false
|
|
1067
|
+
this._promises.reject.auth = false
|
|
1068
|
+
|
|
1069
|
+
if( this._timers.auth ) clearTimeout( this._timers.auth )
|
|
1070
|
+
this._timers.auth = false
|
|
1071
|
+
|
|
1072
|
+
if( r ) r()
|
|
1073
|
+
|
|
1074
|
+
this._em.emit( "call.authed.failed", this )
|
|
1075
|
+
callmanager.options.em.emit( "call.authed.failed", this )
|
|
1076
|
+
|
|
1077
|
+
return
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
if( this.destroyed ) return
|
|
1081
|
+
|
|
1082
|
+
if( this._timers.auth ) clearTimeout( this._timers.auth )
|
|
1083
|
+
this._timers.auth = false
|
|
1084
|
+
|
|
1085
|
+
this._entity = {
|
|
1086
|
+
"username": authorization.username,
|
|
1087
|
+
"realm": authorization.realm,
|
|
1088
|
+
"uri": authorization.username + "@" + authorization.realm,
|
|
1089
|
+
"display": !user.display?"":user.display
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
this.state.authed = true
|
|
1093
|
+
|
|
1094
|
+
callstore.set( this )
|
|
1095
|
+
|
|
1096
|
+
let r = this._promises.resolve.auth
|
|
1097
|
+
this._promises.resolve.auth = false
|
|
1098
|
+
this._promises.reject.auth = false
|
|
1099
|
+
this._timers.auth = false
|
|
1100
|
+
if( r ) r()
|
|
1101
|
+
|
|
1102
|
+
this._em.emit( "call.authed", this )
|
|
1103
|
+
callmanager.options.em.emit( "call.authed", this )
|
|
1104
|
+
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
/**
|
|
1108
|
+
Called by us to handle call cancelled
|
|
1109
|
+
@private
|
|
1110
|
+
*/
|
|
1111
|
+
_oncanceled( req, res ) {
|
|
1112
|
+
this.canceled = true
|
|
1113
|
+
|
|
1114
|
+
for( let child of this.children ) {
|
|
1115
|
+
child.hangup()
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
this._onhangup( "wire", hangupcodes.ORIGINATOR_CANCEL )
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
/**
|
|
1122
|
+
Called by us to handle DTMF events. If it finds a match it resolves the Promise created by waitfortelevents.
|
|
1123
|
+
@private
|
|
1124
|
+
*/
|
|
1125
|
+
_tevent( e ) {
|
|
1126
|
+
this._receivedtelevents += e
|
|
1127
|
+
|
|
1128
|
+
if( undefined !== this.eventmatch ) {
|
|
1129
|
+
let ourmatch = this._receivedtelevents.match( this.eventmatch )
|
|
1130
|
+
if( null !== ourmatch ) {
|
|
1131
|
+
|
|
1132
|
+
this._receivedtelevents = this._receivedtelevents.slice( ourmatch[ 0 ].length + ourmatch.index )
|
|
1133
|
+
|
|
1134
|
+
if( this._promises.resolve.events ) {
|
|
1135
|
+
let r = this._promises.resolve.events
|
|
1136
|
+
|
|
1137
|
+
this._promises.resolve.events = false
|
|
1138
|
+
this._promises.promise.events = false
|
|
1139
|
+
r( ourmatch[ 0 ] )
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
if( this._timers.events ) {
|
|
1143
|
+
clearTimeout( this._timers.events )
|
|
1144
|
+
this._timers.events = false
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
/**
|
|
1151
|
+
Called by our call plan to wait for events for auto attendant/IVR.
|
|
1152
|
+
@param {string} [match] - reg exp matching what is required from the user.
|
|
1153
|
+
@param {Int} [timeout] - time to wait before giving up.
|
|
1154
|
+
@return {Promise} - the promise either resolves to a string if it matches or undefined if it times out..
|
|
1155
|
+
*/
|
|
1156
|
+
waitfortelevents( match = /[0-9A-D\*#]/, timeout = 30000 ) {
|
|
1157
|
+
|
|
1158
|
+
if( this.destroyed ) throw "Call already destroyed"
|
|
1159
|
+
if( this._promises.promise.events ) return this._promises.promise.events
|
|
1160
|
+
|
|
1161
|
+
this._promises.promise.events = new Promise( ( resolve ) => {
|
|
1162
|
+
|
|
1163
|
+
this._timers.events = setTimeout( () => {
|
|
1164
|
+
|
|
1165
|
+
if( this._promises.resolve.events ) {
|
|
1166
|
+
this._promises.resolve.events()
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
this._promises.resolve.events = false
|
|
1170
|
+
this._promises.promise.events = false
|
|
1171
|
+
this._timers.events = false
|
|
1172
|
+
|
|
1173
|
+
}, timeout )
|
|
1174
|
+
|
|
1175
|
+
if( typeof match === "string" ){
|
|
1176
|
+
this.eventmatch = new RegExp( match )
|
|
1177
|
+
} else {
|
|
1178
|
+
this.eventmatch = match
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
this._promises.resolve.events = resolve
|
|
1182
|
+
|
|
1183
|
+
/* if we have something already in our buffer */
|
|
1184
|
+
this._tevent( "" )
|
|
1185
|
+
} )
|
|
1186
|
+
|
|
1187
|
+
return this._promises.promise.events
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
/**
|
|
1191
|
+
Clear our current buffer to ensure new input
|
|
1192
|
+
*/
|
|
1193
|
+
clearevents() {
|
|
1194
|
+
this._receivedtelevents = ""
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
/**
|
|
1198
|
+
If we are not ringing - send ringing to the other end.
|
|
1199
|
+
*/
|
|
1200
|
+
ring() {
|
|
1201
|
+
if( !this.ringing && "uas" === this.type ) {
|
|
1202
|
+
this.state.ringing = true
|
|
1203
|
+
this._res.send( 180, {
|
|
1204
|
+
headers: {
|
|
1205
|
+
"User-Agent": "project",
|
|
1206
|
+
"Supported": "replaces"
|
|
1207
|
+
}
|
|
1208
|
+
} )
|
|
1209
|
+
|
|
1210
|
+
this._em.emit( "call.ringing", this )
|
|
1211
|
+
callmanager.options.em.emit( "call.ringing", this )
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
/**
|
|
1216
|
+
Shortcut to hangup with the reason busy.
|
|
1217
|
+
*/
|
|
1218
|
+
busy() {
|
|
1219
|
+
this.hangup( hangupcodes.USER_BUSY )
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
/**
|
|
1223
|
+
* @private
|
|
1224
|
+
*/
|
|
1225
|
+
get _iswebrtc() {
|
|
1226
|
+
|
|
1227
|
+
/* Have we received remote SDP? */
|
|
1228
|
+
if( !this.sdp.remote ) {
|
|
1229
|
+
return this.sip &&
|
|
1230
|
+
this.sip.contact &&
|
|
1231
|
+
-1 !== this.sip.contact.indexOf( ";transport=ws" )
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
return this.sip &&
|
|
1235
|
+
this.sip.contact &&
|
|
1236
|
+
-1 !== this.sip.contact[ 0 ].uri.indexOf( ";transport=ws" ) &&
|
|
1237
|
+
this.sdp.remote.sdp.media[ 0 ] &&
|
|
1238
|
+
-1 !== this.sdp.remote.sdp.media[ 0 ].protocol.toLowerCase().indexOf( "savpf" ) /* 'UDP/TLS/RTP/SAVPF' */
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
/**
|
|
1242
|
+
Answer this (inbound) call and store a channel which can be used.
|
|
1243
|
+
@return {Promise} Returns a promise which resolves if the call is answered, otherwise rejects the promise.
|
|
1244
|
+
This framework will catch and cleanup this call if this is rejected.
|
|
1245
|
+
*/
|
|
1246
|
+
async answer( options = {} ) {
|
|
1247
|
+
|
|
1248
|
+
if( this.canceled || this.established ) return
|
|
1249
|
+
|
|
1250
|
+
options = { ...callmanager.options, ...this.options, ...options }
|
|
1251
|
+
this.sdp.remote = sdpgen.create( this._req.msg.body )
|
|
1252
|
+
|
|
1253
|
+
/* options.preferedcodecs may have been narrowed down so we still check callmanager as well */
|
|
1254
|
+
this.selectedcodec = this.sdp.remote.intersection( options.preferedcodecs, true )
|
|
1255
|
+
if( false === this.selectedcodec ) {
|
|
1256
|
+
this.selectedcodec = this.sdp.remote.intersection( callmanager.options.preferedcodecs, true )
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
let remoteaudio = this.sdp.remote.getaudio()
|
|
1260
|
+
if( !remoteaudio ) return
|
|
1261
|
+
|
|
1262
|
+
this.sdp.remote.select( this.selectedcodec )
|
|
1263
|
+
let channeldef = call._createchannelremotedef( remoteaudio.address, remoteaudio.port, remoteaudio.audio.payloads[ 0 ] )
|
|
1264
|
+
|
|
1265
|
+
let iswebrtc = this._iswebrtc
|
|
1266
|
+
|
|
1267
|
+
if( iswebrtc ) {
|
|
1268
|
+
channeldef.remote.dtls = {
|
|
1269
|
+
"fingerprint": this.sdp.remote.sdp.media[ 0 ].fingerprint,
|
|
1270
|
+
"mode": this.sdp.remote.sdp.media[ 0 ].setup==="passive"?"active":"passive" /* prefer passive for us */
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
channeldef.remote.icepwd = this.sdp.remote.sdp.media[ 0 ].icePwd
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
let ch = await projectrtp.openchannel( channeldef, this._handlechannelevents.bind( this ) )
|
|
1277
|
+
this.channels.audio = ch
|
|
1278
|
+
this.sdp.local = sdpgen.create()
|
|
1279
|
+
.addcodecs( this.selectedcodec )
|
|
1280
|
+
.setconnectionaddress( ch.local.address )
|
|
1281
|
+
.setaudioport( ch.local.port )
|
|
1282
|
+
|
|
1283
|
+
if( this.canceled ) return
|
|
1284
|
+
|
|
1285
|
+
if( true === callmanager.options.rfc2833 ) {
|
|
1286
|
+
this.sdp.local.addcodecs( "2833" )
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
if( iswebrtc ) {
|
|
1290
|
+
this.sdp.local.addssrc( ch.local.ssrc )
|
|
1291
|
+
.secure( ch.local.dtls.fingerprint, channeldef.remote.dtls.mode )
|
|
1292
|
+
.addicecandidates( ch.local.address, ch.local.port, ch.local.icepwd )
|
|
1293
|
+
.rtcpmux()
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
let dialog = await callmanager.options.srf.createUAS( this._req, this._res, {
|
|
1297
|
+
localSdp: this.sdp.local.toString(),
|
|
1298
|
+
headers: {
|
|
1299
|
+
"User-Agent": "project",
|
|
1300
|
+
"Supported": "replaces"
|
|
1301
|
+
}
|
|
1302
|
+
} )
|
|
1303
|
+
|
|
1304
|
+
this.established = true
|
|
1305
|
+
this._dialog = dialog
|
|
1306
|
+
this.sip.tags.local = dialog.sip.localTag
|
|
1307
|
+
callstore.set( this )
|
|
1308
|
+
|
|
1309
|
+
this._addevents( this._dialog )
|
|
1310
|
+
|
|
1311
|
+
this._em.emit( "call.answered", this )
|
|
1312
|
+
callmanager.options.em.emit( "call.answered", this )
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
/**
|
|
1316
|
+
Private helper function to add events to our RTP channel.
|
|
1317
|
+
@param {object} e - the rtp event
|
|
1318
|
+
@private
|
|
1319
|
+
*/
|
|
1320
|
+
_handlechannelevents( e ) {
|
|
1321
|
+
|
|
1322
|
+
try {
|
|
1323
|
+
this._em.emit( "channel", { "call": this, "event": e } )
|
|
1324
|
+
} catch ( e ) { console.error( e ) }
|
|
1325
|
+
|
|
1326
|
+
if( "close" === e.action && this.destroyed ) {
|
|
1327
|
+
/* keep a record */
|
|
1328
|
+
this.channels.closed.audio.push( e )
|
|
1329
|
+
|
|
1330
|
+
if( "requested" == e.reason ) {
|
|
1331
|
+
this._cleanup()
|
|
1332
|
+
return
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
this.channels.audio = false
|
|
1336
|
+
this.hangup() /* ? */
|
|
1337
|
+
return
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
if( "telephone-event" === e.action ) {
|
|
1341
|
+
this._tevent( e.event )
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
if( this._eventconstraints ) {
|
|
1345
|
+
let constraintkeys = Object.keys( this._eventconstraints )
|
|
1346
|
+
for( const k of constraintkeys ) {
|
|
1347
|
+
|
|
1348
|
+
if( "object" === typeof this._eventconstraints[ k ] ) {
|
|
1349
|
+
/* regex */
|
|
1350
|
+
if( !e[ k ].match( this._eventconstraints[ k ] ) ) {
|
|
1351
|
+
return
|
|
1352
|
+
}
|
|
1353
|
+
} else if( this._eventconstraints[ k ] != e[ k ] ) {
|
|
1354
|
+
/* not a match */
|
|
1355
|
+
return
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
/*
|
|
1361
|
+
We get here if the contraints match OR it is a tel event.
|
|
1362
|
+
A close event will be caught on call clean up.
|
|
1363
|
+
*/
|
|
1364
|
+
|
|
1365
|
+
if( this._timers.anyevent ) clearTimeout( this._timers.anyevent )
|
|
1366
|
+
this._timers.anyevent = false
|
|
1367
|
+
|
|
1368
|
+
let r = this._promises.resolve.channelevent
|
|
1369
|
+
this._promises.resolve.channelevent = false
|
|
1370
|
+
this._promises.promise.channelevent = false
|
|
1371
|
+
if( r ) r( e )
|
|
1372
|
+
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
/**
|
|
1376
|
+
Wait for any event of interest. DTMF or Audio (channel close, audio event etc).
|
|
1377
|
+
When this is extended to SIP DTMF this will be also included.
|
|
1378
|
+
|
|
1379
|
+
constraints will limit the promise firing to one which matches the event we expect.
|
|
1380
|
+
timeout will force a firing.
|
|
1381
|
+
|
|
1382
|
+
A telephone event will resolve this promise as we typically need speech to be interupted
|
|
1383
|
+
by the user. Note, peeking a telephone-event (i.e. DTMF) will not clear it like waitfortelevents will.
|
|
1384
|
+
@param { regex } constraints - event to filter for from our RTP server - excluding DTMF events - these will always return
|
|
1385
|
+
*/
|
|
1386
|
+
waitforanyevent( constraints, timeout = 500 ) {
|
|
1387
|
+
|
|
1388
|
+
if( this.destroyed ) throw "Call already destroyed"
|
|
1389
|
+
if ( this._promises.promise.channelevent ) return this._promises.promise.channelevent
|
|
1390
|
+
|
|
1391
|
+
this._eventconstraints = constraints
|
|
1392
|
+
|
|
1393
|
+
this._promises.promise.channelevent = new Promise( ( resolve ) => {
|
|
1394
|
+
this._promises.resolve.channelevent = resolve
|
|
1395
|
+
} )
|
|
1396
|
+
|
|
1397
|
+
this._timers.anyevent = setTimeout( () => {
|
|
1398
|
+
let r = this._promises.resolve.channelevent
|
|
1399
|
+
if( r ) r( "timeout" )
|
|
1400
|
+
}, timeout * 1000 )
|
|
1401
|
+
|
|
1402
|
+
return this._promises.promise.channelevent
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1405
|
+
/**
|
|
1406
|
+
* Place the call on hold. TODO.
|
|
1407
|
+
*/
|
|
1408
|
+
hold() {
|
|
1409
|
+
this._hold()
|
|
1410
|
+
if( this.state.held ) {
|
|
1411
|
+
this._dialog.modify( this.sdp.local.toString() )
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
/**
|
|
1416
|
+
* Take a call off hold.
|
|
1417
|
+
*/
|
|
1418
|
+
unhold() {
|
|
1419
|
+
this._unhold()
|
|
1420
|
+
if( !this.state.held ) {
|
|
1421
|
+
this._dialog.modify( this.sdp.local.toString() )
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
/**
|
|
1426
|
+
* If we have been placed on hold (and it has been neotiated) then configure audio to match.
|
|
1427
|
+
* @private
|
|
1428
|
+
*/
|
|
1429
|
+
_hold() {
|
|
1430
|
+
|
|
1431
|
+
if( this.state.held ) return
|
|
1432
|
+
this.state.held = true
|
|
1433
|
+
|
|
1434
|
+
this.channels.audio.direction( { "send": false, "recv": false } )
|
|
1435
|
+
this.sdp.local.setaudiodirection( "inactive" )
|
|
1436
|
+
|
|
1437
|
+
let other = this.other
|
|
1438
|
+
if( other ) {
|
|
1439
|
+
other.channels.audio.unmix()
|
|
1440
|
+
other.channels.audio.play( callmanager.options.moh )
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1443
|
+
this._em.emit( "call.hold", this )
|
|
1444
|
+
callmanager.options.em.emit( "call.hold", this )
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
/**
|
|
1448
|
+
Same as _hold.
|
|
1449
|
+
@private
|
|
1450
|
+
*/
|
|
1451
|
+
_unhold() {
|
|
1452
|
+
if( !this.state.held ) return
|
|
1453
|
+
this.state.held = false
|
|
1454
|
+
|
|
1455
|
+
this.channels.audio.direction( { "send": true, "recv": true } )
|
|
1456
|
+
this.sdp.local.setaudiodirection( "sendrecv" )
|
|
1457
|
+
|
|
1458
|
+
let other = this.other
|
|
1459
|
+
if( other ) {
|
|
1460
|
+
this.channels.audio.mix( other.channels.audio )
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1463
|
+
this._em.emit( "call.unhold", this )
|
|
1464
|
+
callmanager.options.em.emit( "call.unhold", this )
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
/**
|
|
1468
|
+
As part of the transfer flow a subscription is implied during a transfer which we must update the transferee.
|
|
1469
|
+
@private
|
|
1470
|
+
*/
|
|
1471
|
+
async _notifyreferfail() {
|
|
1472
|
+
let opts = {
|
|
1473
|
+
"method": "NOTIFY",
|
|
1474
|
+
"headers": {
|
|
1475
|
+
"Event": "refer;id=" + this.referreq.get( "cseq" ).match( /(\d+)/ )[ 0 ],
|
|
1476
|
+
"Subscription-State": "terminated;reason=error",
|
|
1477
|
+
"Content-Type": "message/sipfrag;version=2.0"
|
|
1478
|
+
},
|
|
1479
|
+
"body": "SIP/2.0 400 Ok\r\n"
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1482
|
+
await this._dialog.request( opts ).catch( ( e ) => {
|
|
1483
|
+
console.error( e )
|
|
1484
|
+
} )
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
/**
|
|
1488
|
+
As part of the transfer flow a subscription is implied during a transfer which we must update the transferee.
|
|
1489
|
+
@private
|
|
1490
|
+
*/
|
|
1491
|
+
async _notifyrefercomplete() {
|
|
1492
|
+
let opts = {
|
|
1493
|
+
"method": "NOTIFY",
|
|
1494
|
+
"headers": {
|
|
1495
|
+
"Event": "refer;id=" + this.referreq.get( "cseq" ).match( /(\d+)/ )[ 0 ],
|
|
1496
|
+
"Subscription-State": "terminated;reason=complete",
|
|
1497
|
+
"Content-Type": "message/sipfrag;version=2.0"
|
|
1498
|
+
},
|
|
1499
|
+
"body": "SIP/2.0 200 Ok\r\n"
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1502
|
+
await this._dialog.request( opts )
|
|
1503
|
+
.catch( ( e ) => {
|
|
1504
|
+
console.error( e )
|
|
1505
|
+
} )
|
|
1506
|
+
}
|
|
1507
|
+
|
|
1508
|
+
/**
|
|
1509
|
+
As part of the transfer flow a subscription is implied during a transfer which we must update the transferee.
|
|
1510
|
+
@private
|
|
1511
|
+
*/
|
|
1512
|
+
async _notifyreferstart() {
|
|
1513
|
+
let opts = {
|
|
1514
|
+
"method": "NOTIFY",
|
|
1515
|
+
"headers": {
|
|
1516
|
+
"Event": "refer;id=" + this.referreq.get( "cseq" ).match( /(\d+)/ )[ 0 ],
|
|
1517
|
+
"Subscription-State": "active;expires=60",
|
|
1518
|
+
"Content-Type": "message/sipfrag;version=2.0"
|
|
1519
|
+
},
|
|
1520
|
+
"body": "SIP/2.0 100 Trying\r\n"
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
await this._dialog.request( opts )
|
|
1524
|
+
.catch( ( e ) => {
|
|
1525
|
+
console.error( e )
|
|
1526
|
+
} )
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1529
|
+
/**
|
|
1530
|
+
Send out modified SDP to get the audio to the new location.
|
|
1531
|
+
@private
|
|
1532
|
+
*/
|
|
1533
|
+
async _modifyforxfer() {
|
|
1534
|
+
this.sdp.local.setaudiodirection( "sendrecv" )
|
|
1535
|
+
await this._dialog.modify( this.sdp.local.toString() )
|
|
1536
|
+
.catch( ( e ) => {
|
|
1537
|
+
console.error( e )
|
|
1538
|
+
} )
|
|
1539
|
+
}
|
|
1540
|
+
|
|
1541
|
+
/**
|
|
1542
|
+
Add events for the drachtio dialog object that this object requires.
|
|
1543
|
+
@private
|
|
1544
|
+
*/
|
|
1545
|
+
_addevents( dialog ) {
|
|
1546
|
+
|
|
1547
|
+
/* Drachtio doesn't appear to have finished SE support, i.e. it sends
|
|
1548
|
+
a regular INVITE when we set the Supported: timer and Session-Expires headers
|
|
1549
|
+
but it doesn't appear to indicate to us when it does fail. It most cases our
|
|
1550
|
+
RTP stall timer will kick in first, but if a call is placed on hold followed
|
|
1551
|
+
by AWOL... */
|
|
1552
|
+
this._timers.seinterval = setInterval( async () => {
|
|
1553
|
+
let opts = {
|
|
1554
|
+
"method": "INVITE",
|
|
1555
|
+
"body": this.sdp.local.toString()
|
|
1556
|
+
}
|
|
1557
|
+
|
|
1558
|
+
let res = await dialog.request( opts )
|
|
1559
|
+
.catch( ( e ) => {
|
|
1560
|
+
console.error( e )
|
|
1561
|
+
this.hangup( hangupcodes.USER_GONE )
|
|
1562
|
+
} )
|
|
1563
|
+
|
|
1564
|
+
if( !this.destroyed && 200 != res.msg.status ) {
|
|
1565
|
+
this.hangup( hangupcodes.USER_GONE )
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1568
|
+
}, callmanager.options.seexpire )
|
|
1569
|
+
|
|
1570
|
+
dialog.on( "destroy", async ( req ) => {
|
|
1571
|
+
await this._onhangup( "wire" )
|
|
1572
|
+
} )
|
|
1573
|
+
|
|
1574
|
+
dialog.on( "modify", ( req, res ) => {
|
|
1575
|
+
// The application must respond, using the res parameter provided.
|
|
1576
|
+
if( "INVITE" === req.msg.method ) {
|
|
1577
|
+
|
|
1578
|
+
let sdp = sdpgen.create( req.msg.body )
|
|
1579
|
+
let media = sdp.getmedia()
|
|
1580
|
+
|
|
1581
|
+
if( ( "inactive" === media.direction || "0.0.0.0" === sdp.sdp.connection.ip ) && !this.state.held ) {
|
|
1582
|
+
this._hold()
|
|
1583
|
+
res.send( 200, {
|
|
1584
|
+
"headers": {
|
|
1585
|
+
"Subject" : "Call on hold",
|
|
1586
|
+
"User-Agent": "project",
|
|
1587
|
+
"Allow": "INVITE, ACK, CANCEL, OPTIONS, BYE, REFER, NOTIFY",
|
|
1588
|
+
"Supported": "replaces"
|
|
1589
|
+
},
|
|
1590
|
+
"body": this.sdp.local.toString()
|
|
1591
|
+
} )
|
|
1592
|
+
} else if( "inactive" !== media.direction && "0.0.0.0" !== sdp.sdp.connection.ip && this.state.held ) {
|
|
1593
|
+
this._unhold()
|
|
1594
|
+
res.send( 200, {
|
|
1595
|
+
"headers": {
|
|
1596
|
+
"Subject" : "Call off hold",
|
|
1597
|
+
"User-Agent": "project"
|
|
1598
|
+
},
|
|
1599
|
+
"body": this.sdp.local.toString()
|
|
1600
|
+
} )
|
|
1601
|
+
} else {
|
|
1602
|
+
/* Unknown - but respond to keep the call going */
|
|
1603
|
+
res.send( 200, {
|
|
1604
|
+
"headers": {
|
|
1605
|
+
"Subject" : "Ok",
|
|
1606
|
+
"User-Agent": "project"
|
|
1607
|
+
},
|
|
1608
|
+
"body": this.sdp.local.toString()
|
|
1609
|
+
} )
|
|
1610
|
+
}
|
|
1611
|
+
}
|
|
1612
|
+
} )
|
|
1613
|
+
|
|
1614
|
+
dialog.on( "refer", async ( req, res ) => {
|
|
1615
|
+
/*
|
|
1616
|
+
We only support the xfer of 2 legged calls. The xfered call will pick up
|
|
1617
|
+
the auth from the transferee. For example, inbound anonymous call, gets handled
|
|
1618
|
+
by user 1. User 1 then refers - to internal extension, so this can has now been
|
|
1619
|
+
authed by user 1 - so has access to internal extenions.
|
|
1620
|
+
*/
|
|
1621
|
+
if( !this.other ) return res.send( 400, "1 legged calls" )
|
|
1622
|
+
|
|
1623
|
+
/* If the invite was authed - then we auth the refer */
|
|
1624
|
+
if( this.state.authed ) {
|
|
1625
|
+
if( !this._auth.has( req ) ) {
|
|
1626
|
+
return this._auth.requestauth( this._req, this._res )
|
|
1627
|
+
}
|
|
1628
|
+
await this._onauth( req )
|
|
1629
|
+
if( this.destroyed ) return
|
|
1630
|
+
}
|
|
1631
|
+
|
|
1632
|
+
if( !req.has( "refer-to" ) ) {
|
|
1633
|
+
res.send( 400, "Bad request - no refer-to" )
|
|
1634
|
+
return
|
|
1635
|
+
}
|
|
1636
|
+
|
|
1637
|
+
let referto = req.getParsedHeader( "refer-to" )
|
|
1638
|
+
let parsedrefuri = parseuri( referto.uri )
|
|
1639
|
+
|
|
1640
|
+
if( !parsedrefuri || !parsedrefuri.user ) {
|
|
1641
|
+
res.send( 400, "Bad request - no refer-to user" )
|
|
1642
|
+
return
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1645
|
+
if( !parsedrefuri.host ) {
|
|
1646
|
+
res.send( 400, "Bad request - no refer-to host" )
|
|
1647
|
+
return
|
|
1648
|
+
}
|
|
1649
|
+
|
|
1650
|
+
/*
|
|
1651
|
+
Example refer to fields
|
|
1652
|
+
Refer-To: sip:alice@atlanta.example.com
|
|
1653
|
+
|
|
1654
|
+
Refer-To: <sip:bob@biloxi.example.net?Accept-Contact=sip:bobsdesk.
|
|
1655
|
+
biloxi.example.net&Call-ID%3D55432%40alicepc.atlanta.example.com>
|
|
1656
|
+
|
|
1657
|
+
Refer-To: <sip:dave@denver.example.org?Replaces=12345%40192.168.118.3%3B
|
|
1658
|
+
to-tag%3D12345%3Bfrom-tag%3D5FFE-3994>
|
|
1659
|
+
|
|
1660
|
+
Refer-To: <sip:carol@cleveland.example.org;method=SUBSCRIBE>
|
|
1661
|
+
*/
|
|
1662
|
+
|
|
1663
|
+
this.referreq = req
|
|
1664
|
+
this.referres = res
|
|
1665
|
+
|
|
1666
|
+
/* getParsedHeader doesn't appear to parse tags in uri params */
|
|
1667
|
+
let replacesuri = decodeURIComponent( referto.uri )
|
|
1668
|
+
let replaces = replacesuri.match( /replaces=(.*?)(;|$)/i )
|
|
1669
|
+
|
|
1670
|
+
if( null !== replaces ) {
|
|
1671
|
+
return this._runattendedxfer( req, res, replaces, replacesuri )
|
|
1672
|
+
} else {
|
|
1673
|
+
this._runblindxfer( req, res, referto )
|
|
1674
|
+
}
|
|
1675
|
+
} )
|
|
1676
|
+
}
|
|
1677
|
+
|
|
1678
|
+
/**
|
|
1679
|
+
We have authed, parsed the headers and decided we have been asked to perform
|
|
1680
|
+
a blind xfer on the other leg.
|
|
1681
|
+
@private
|
|
1682
|
+
*/
|
|
1683
|
+
_runblindxfer( req, res, referto ) {
|
|
1684
|
+
let othercall = this.other
|
|
1685
|
+
if( !othercall ) return res.send( 400, "We have no-one to refer" )
|
|
1686
|
+
|
|
1687
|
+
othercall.state.refered = true
|
|
1688
|
+
|
|
1689
|
+
this.detach()
|
|
1690
|
+
res.send( 202 )
|
|
1691
|
+
this._notifyreferstart()
|
|
1692
|
+
|
|
1693
|
+
othercall.referingtouri = referto.uri
|
|
1694
|
+
callmanager.options.em.emit( "call.new", othercall )
|
|
1695
|
+
this._notifyrefercomplete()
|
|
1696
|
+
|
|
1697
|
+
/* As part of the call flow the client will send us a hangup next - so only set the cause */
|
|
1698
|
+
this._sethangupcause( "wire", hangupcodes.BLIND_TRANSFER )
|
|
1699
|
+
}
|
|
1700
|
+
|
|
1701
|
+
/**
|
|
1702
|
+
Attended xfers have 4 calls. 2 pairs.
|
|
1703
|
+
a_1 -> b_1
|
|
1704
|
+
b_2 -> c_1
|
|
1705
|
+
|
|
1706
|
+
b wants to connect a and c and then step out of the way
|
|
1707
|
+
where b_2 is the active call and b_1 is the one b placed on hold
|
|
1708
|
+
therefore b_2 = this
|
|
1709
|
+
|
|
1710
|
+
It is also pottential this could be requested
|
|
1711
|
+
a_1 -> b_1
|
|
1712
|
+
b_2 <- c_1
|
|
1713
|
+
|
|
1714
|
+
or
|
|
1715
|
+
|
|
1716
|
+
a_1 <- b_1
|
|
1717
|
+
b_2 -> c_1
|
|
1718
|
+
|
|
1719
|
+
or
|
|
1720
|
+
|
|
1721
|
+
a_1 <- b_1
|
|
1722
|
+
b_2 <- c_1
|
|
1723
|
+
|
|
1724
|
+
ends up with
|
|
1725
|
+
a_1 - c_1
|
|
1726
|
+
|
|
1727
|
+
An attended transfer could also go to an application - not other call:
|
|
1728
|
+
a_1 <- b_1
|
|
1729
|
+
b_2 - conference
|
|
1730
|
+
|
|
1731
|
+
ends up with
|
|
1732
|
+
a_1 - b_2 (rtp channel)
|
|
1733
|
+
|
|
1734
|
+
@private
|
|
1735
|
+
*/
|
|
1736
|
+
async _runattendedxfer( req, res, replaces, replacesuri ) {
|
|
1737
|
+
let totag = replacesuri.match( /to-tag=(.*?)(;|$)/i )
|
|
1738
|
+
let fromtag = replacesuri.match( /from-tag=(.*?)(;|$)/i )
|
|
1739
|
+
|
|
1740
|
+
if( replaces.length < 3 || totag.length < 3 || fromtag.length < 3 ) {
|
|
1741
|
+
res.send( 400, "Bad call reference for replaces" )
|
|
1742
|
+
return
|
|
1743
|
+
}
|
|
1744
|
+
|
|
1745
|
+
let searchfor = { "callid": replaces[ 1 ], "tags": { "local": totag[ 1 ], "remote": fromtag[ 1 ] } }
|
|
1746
|
+
let failed = false
|
|
1747
|
+
|
|
1748
|
+
let b_1 = await callstore.getbycallid( searchfor )
|
|
1749
|
+
.catch( ( e ) => {
|
|
1750
|
+
console.error( e )
|
|
1751
|
+
res.send( 400, e )
|
|
1752
|
+
failed = true
|
|
1753
|
+
} )
|
|
1754
|
+
if( failed || !b_1 ) return res.send( 400, "No call matches that call id" )
|
|
1755
|
+
if( !b_1.sdp.remote ) return res.send( 400, "No remote sdp negotiated (b_1)!" )
|
|
1756
|
+
let b_2 = this /* so we can follow the above terminology */
|
|
1757
|
+
|
|
1758
|
+
let c_1 = b_2.other
|
|
1759
|
+
if( !c_1 ) c_1 = b_2
|
|
1760
|
+
|
|
1761
|
+
let a_1 = b_1.other
|
|
1762
|
+
if( !a_1 ) return res.send( 400, "Can't attened xfer 1 legged calls" )
|
|
1763
|
+
if( !a_1.sdp.remote ) return res.send( 400, "No remote sdp negotiated (a_1)!" )
|
|
1764
|
+
|
|
1765
|
+
if( !a_1.channels.audio ) return res.send( 400, "No channel (a_1)" )
|
|
1766
|
+
if( !b_1.channels.audio ) return res.send( 400, "No channel (b_1)" )
|
|
1767
|
+
if( !b_2.channels.audio ) return res.send( 400, "No channel (b_2)" )
|
|
1768
|
+
if( !c_1.channels.audio ) return res.send( 400, "No channel (c_1)" )
|
|
1769
|
+
|
|
1770
|
+
b_1.detach()
|
|
1771
|
+
b_2.detach()
|
|
1772
|
+
|
|
1773
|
+
/* Swap channels and update */
|
|
1774
|
+
let a_1_audio = a_1.channels.audio
|
|
1775
|
+
let a_1_sdp = a_1.sdp.local
|
|
1776
|
+
|
|
1777
|
+
a_1.channels.audio = b_2.channels.audio
|
|
1778
|
+
a_1.sdp.local = b_2.sdp.local
|
|
1779
|
+
|
|
1780
|
+
a_1.sdp.local
|
|
1781
|
+
.clearcodecs()
|
|
1782
|
+
.addcodecs( a_1.selectedcodec )
|
|
1783
|
+
.select( a_1.selectedcodec )
|
|
1784
|
+
.setaudiodirection( "sendrecv" )
|
|
1785
|
+
|
|
1786
|
+
if( true === callmanager.options.rfc2833 ) {
|
|
1787
|
+
a_1.sdp.local.addcodecs( "2833" )
|
|
1788
|
+
}
|
|
1789
|
+
|
|
1790
|
+
/* Link logically */
|
|
1791
|
+
a_1.children.add( c_1 )
|
|
1792
|
+
c_1.parent = a_1
|
|
1793
|
+
|
|
1794
|
+
/* this one will be hung up soon anyway - so it to has to close the correct one */
|
|
1795
|
+
b_2.channels.audio = a_1_audio
|
|
1796
|
+
b_2.sdp.local = a_1_sdp
|
|
1797
|
+
|
|
1798
|
+
failed = false
|
|
1799
|
+
|
|
1800
|
+
await new Promise( ( resolve ) => {
|
|
1801
|
+
res.send( 202, "Refering", {}, ( err, response ) => {
|
|
1802
|
+
resolve()
|
|
1803
|
+
} )
|
|
1804
|
+
} )
|
|
1805
|
+
|
|
1806
|
+
failed = false
|
|
1807
|
+
await this._notifyreferstart()
|
|
1808
|
+
.catch( ( e ) => {
|
|
1809
|
+
console.error( e )
|
|
1810
|
+
this._notifyreferfail()
|
|
1811
|
+
failed = true
|
|
1812
|
+
} )
|
|
1813
|
+
|
|
1814
|
+
if( failed ) return
|
|
1815
|
+
|
|
1816
|
+
/* modify ports and renegotiate codecs */
|
|
1817
|
+
await a_1._modifyforxfer()
|
|
1818
|
+
|
|
1819
|
+
let target = a_1.sdp.remote.getaudio()
|
|
1820
|
+
if( target ) {
|
|
1821
|
+
/* we should always get in here */
|
|
1822
|
+
a_1.channels.audio.remote( call._createchannelremotedef( target.address, target.port, a_1.selectedcodec ) )
|
|
1823
|
+
|
|
1824
|
+
/* there might be situations where mix is not the correct thing - perhaps a pop push application? */
|
|
1825
|
+
a_1.channels.audio.mix( c_1.channels.audio )
|
|
1826
|
+
|
|
1827
|
+
/* Now inform our RTP server also - we might need to wait untl the target has completed so need a notify mechanism */
|
|
1828
|
+
a_1.channels.audio.direction( { "send": false, "recv": false } )
|
|
1829
|
+
|
|
1830
|
+
a_1._em.emit( "call.mix", a_1 )
|
|
1831
|
+
callmanager.options.em.emit( "call.mix", a_1 )
|
|
1832
|
+
}
|
|
1833
|
+
|
|
1834
|
+
this._notifyrefercomplete()
|
|
1835
|
+
|
|
1836
|
+
a_1.state.refered = true
|
|
1837
|
+
|
|
1838
|
+
this.hangup_cause = Object.assign( { "src": "wire" }, hangupcodes.ATTENDED_TRANSFER )
|
|
1839
|
+
b_1.hangup( hangupcodes.ATTENDED_TRANSFER )
|
|
1840
|
+
}
|
|
1841
|
+
|
|
1842
|
+
/**
|
|
1843
|
+
Helper function to create a channel target definition
|
|
1844
|
+
@private
|
|
1845
|
+
*/
|
|
1846
|
+
static _createchannelremotedef( address, port, codec ) {
|
|
1847
|
+
return {
|
|
1848
|
+
"remote": {
|
|
1849
|
+
"address": address,
|
|
1850
|
+
"port": port,
|
|
1851
|
+
"codec": codec
|
|
1852
|
+
/*
|
|
1853
|
+
dtls:{
|
|
1854
|
+
fingerprint: "",
|
|
1855
|
+
setup: ""
|
|
1856
|
+
}
|
|
1857
|
+
*/
|
|
1858
|
+
}
|
|
1859
|
+
}
|
|
1860
|
+
}
|
|
1861
|
+
|
|
1862
|
+
/**
|
|
1863
|
+
Sets our hangup cause correctly - if not already set.
|
|
1864
|
+
@private
|
|
1865
|
+
*/
|
|
1866
|
+
_sethangupcause( src, reason ) {
|
|
1867
|
+
if( !this.hangup_cause ) {
|
|
1868
|
+
if( reason ) {
|
|
1869
|
+
this.hangup_cause = reason
|
|
1870
|
+
} else {
|
|
1871
|
+
this.hangup_cause = hangupcodes.NORMAL_CLEARING
|
|
1872
|
+
if( "wire" === src && !this.state.established ) {
|
|
1873
|
+
this.hangup_cause = hangupcodes.ORIGINATOR_CANCEL
|
|
1874
|
+
}
|
|
1875
|
+
}
|
|
1876
|
+
/* make sure we don't copy src back into our table of causes */
|
|
1877
|
+
this.hangup_cause = Object.assign( { "src": src }, this.hangup_cause )
|
|
1878
|
+
}
|
|
1879
|
+
}
|
|
1880
|
+
|
|
1881
|
+
/**
|
|
1882
|
+
When our dialog has confirmed we have hung up
|
|
1883
|
+
@param {string} [us] - "us"|"wire"
|
|
1884
|
+
@param {object} reason - one of the reasons from the hangupcodes enum - only used if we havn't alread set our reason
|
|
1885
|
+
@private
|
|
1886
|
+
*/
|
|
1887
|
+
async _onhangup( src = "us", reason ) {
|
|
1888
|
+
|
|
1889
|
+
if( this.destroyed ) return
|
|
1890
|
+
this.destroyed = true
|
|
1891
|
+
|
|
1892
|
+
this._sethangupcause( src, reason )
|
|
1893
|
+
await callstore.delete( this )
|
|
1894
|
+
|
|
1895
|
+
let r = this._promises.resolve.hangup
|
|
1896
|
+
this._promises.promise.hangup = false
|
|
1897
|
+
this._promises.resolve.hangup = false
|
|
1898
|
+
try{
|
|
1899
|
+
if( r ) r()
|
|
1900
|
+
} catch( e ) {
|
|
1901
|
+
console.error( e )
|
|
1902
|
+
}
|
|
1903
|
+
|
|
1904
|
+
let hangups = []
|
|
1905
|
+
for( let child of this.children ) {
|
|
1906
|
+
hangups.push( child.hangup( this.hangup_cause ) )
|
|
1907
|
+
}
|
|
1908
|
+
|
|
1909
|
+
if( hangups.length > 0 ) {
|
|
1910
|
+
await Promise.all( hangups )
|
|
1911
|
+
}
|
|
1912
|
+
|
|
1913
|
+
this._em.emit( "call.destroyed", this )
|
|
1914
|
+
callmanager.options.em.emit( "call.destroyed", this )
|
|
1915
|
+
let audiochannel = this.channels.audio
|
|
1916
|
+
this.channels.audio = false
|
|
1917
|
+
if( audiochannel ) {
|
|
1918
|
+
audiochannel.close()
|
|
1919
|
+
this._timers.cleanup = setTimeout( () => {
|
|
1920
|
+
console.error( "Timeout waiting for channel close, cleaning up anyway" )
|
|
1921
|
+
this._cleanup()
|
|
1922
|
+
}, 60000 )
|
|
1923
|
+
} else {
|
|
1924
|
+
this._cleanup()
|
|
1925
|
+
}
|
|
1926
|
+
}
|
|
1927
|
+
|
|
1928
|
+
/**
|
|
1929
|
+
Use this as our destructor. This may get called more than once depending on what is going on
|
|
1930
|
+
@private
|
|
1931
|
+
*/
|
|
1932
|
+
_cleanup() {
|
|
1933
|
+
|
|
1934
|
+
if( this.state.cleaned ) return
|
|
1935
|
+
this.state.cleaned = true
|
|
1936
|
+
|
|
1937
|
+
/* Clean up promises (ensure they are resolved) and clear any timers */
|
|
1938
|
+
for ( const [ key, value ] of Object.entries( this._timers ) ) {
|
|
1939
|
+
if( value ) clearTimeout( value )
|
|
1940
|
+
this._timers[ key ] = false
|
|
1941
|
+
}
|
|
1942
|
+
|
|
1943
|
+
let authreject = this._promises.reject.auth
|
|
1944
|
+
this._promises.reject.auth = false
|
|
1945
|
+
this._promises.resolve.auth = false
|
|
1946
|
+
if( authreject ) authreject( this )
|
|
1947
|
+
|
|
1948
|
+
let resolves = []
|
|
1949
|
+
for ( const [ key, value ] of Object.entries( this._promises.resolve ) ) {
|
|
1950
|
+
if( value ) resolves.push( value )
|
|
1951
|
+
this._promises.resolve[ key ] = false
|
|
1952
|
+
}
|
|
1953
|
+
|
|
1954
|
+
resolves.forEach( r => r( this ) )
|
|
1955
|
+
|
|
1956
|
+
this.removealllisteners()
|
|
1957
|
+
|
|
1958
|
+
this._em.emit( "call.reporting", this )
|
|
1959
|
+
callmanager.options.em.emit( "call.reporting", this )
|
|
1960
|
+
}
|
|
1961
|
+
|
|
1962
|
+
/**
|
|
1963
|
+
Hangup the call with reason.
|
|
1964
|
+
@param {object} reason - one of the reasons from the hangupcodes enum
|
|
1965
|
+
*/
|
|
1966
|
+
async hangup( reason ) {
|
|
1967
|
+
|
|
1968
|
+
if( this.destroyed ) return
|
|
1969
|
+
|
|
1970
|
+
this._sethangupcause( "us", reason )
|
|
1971
|
+
await callstore.delete( this )
|
|
1972
|
+
|
|
1973
|
+
if( this.established ) {
|
|
1974
|
+
try {
|
|
1975
|
+
await this._dialog.destroy()
|
|
1976
|
+
} catch( e ) { console.error( e ) }
|
|
1977
|
+
|
|
1978
|
+
} else if( "uac" === this.type ) {
|
|
1979
|
+
try {
|
|
1980
|
+
this._req.cancel()
|
|
1981
|
+
} catch( e ) { console.error( e ) }
|
|
1982
|
+
|
|
1983
|
+
this.canceled = true
|
|
1984
|
+
|
|
1985
|
+
} else {
|
|
1986
|
+
try {
|
|
1987
|
+
this._res.send( this.hangup_cause.sip )
|
|
1988
|
+
} catch( e ) { console.error( e ) }
|
|
1989
|
+
}
|
|
1990
|
+
|
|
1991
|
+
await this._onhangup( "us", reason )
|
|
1992
|
+
}
|
|
1993
|
+
|
|
1994
|
+
async waitforhangup() {
|
|
1995
|
+
if( this.destroyed ) return
|
|
1996
|
+
|
|
1997
|
+
if( !this._promises.promise.hangup ) {
|
|
1998
|
+
this._promises.promise.hangup = new Promise( ( resolve ) => {
|
|
1999
|
+
this._promises.resolve.hangup = resolve
|
|
2000
|
+
} )
|
|
2001
|
+
}
|
|
2002
|
+
|
|
2003
|
+
await this._promises.resolve.hangup
|
|
2004
|
+
this._promises.promise.hangup = false
|
|
2005
|
+
this._promises.resolve.hangup = false
|
|
2006
|
+
|
|
2007
|
+
}
|
|
2008
|
+
|
|
2009
|
+
_requestauth() {
|
|
2010
|
+
this.send( 407 )
|
|
2011
|
+
}
|
|
2012
|
+
|
|
2013
|
+
/**
|
|
2014
|
+
Send an UPDATE. Use to updated called id, caller id, sdp etc. Send in dialog - TODO look how to send
|
|
2015
|
+
early as this is recomended in the RFC.
|
|
2016
|
+
@param { Object } options
|
|
2017
|
+
@param { remoteid } [ options.remote ] - if present update the remote called/caller id (display) - if not will get from other
|
|
2018
|
+
*/
|
|
2019
|
+
async update( options ) {
|
|
2020
|
+
|
|
2021
|
+
if( !this._dialog ) {
|
|
2022
|
+
console.error( "Early update not currently supported" )
|
|
2023
|
+
return false
|
|
2024
|
+
}
|
|
2025
|
+
|
|
2026
|
+
/* Check client supports update */
|
|
2027
|
+
if( !this._req ) return false
|
|
2028
|
+
if( !this._req.has( "Allow" ) ) return false
|
|
2029
|
+
let allow = this._req.get( "Allow" )
|
|
2030
|
+
if( !/\bupdate\b/i.test( allow ) ) return false
|
|
2031
|
+
|
|
2032
|
+
let requestoptions = {}
|
|
2033
|
+
requestoptions.method = "update"
|
|
2034
|
+
if( this.sdp.local ) {
|
|
2035
|
+
requestoptions.body = this.sdp.local.toString()
|
|
2036
|
+
}
|
|
2037
|
+
|
|
2038
|
+
requestoptions.headers = {}
|
|
2039
|
+
|
|
2040
|
+
let remoteidheader = "P-Preferred-Identity"
|
|
2041
|
+
let name = ""
|
|
2042
|
+
let user = "0000000000"
|
|
2043
|
+
let realm = "localhost.localdomain"
|
|
2044
|
+
|
|
2045
|
+
if( options && options.remote ) {
|
|
2046
|
+
name = options.remote.display.replace( /[^\w\-\s']+/g, "" ) /* only allow alpa num whitespace and ' */
|
|
2047
|
+
realm = options.remote.realm
|
|
2048
|
+
user = options.remote.username
|
|
2049
|
+
} else {
|
|
2050
|
+
let other = this.other
|
|
2051
|
+
if( other ) {
|
|
2052
|
+
let remote = other.remote
|
|
2053
|
+
name = remote.name.replace( /[^\w\-\s']+/g, "" ) /* only allow alpa num whitespace and ' */
|
|
2054
|
+
realm = remote.host
|
|
2055
|
+
user = remote.user
|
|
2056
|
+
}
|
|
2057
|
+
}
|
|
2058
|
+
|
|
2059
|
+
let remoteid = `"${name}" <sip:${user}@${realm}>`
|
|
2060
|
+
requestoptions.headers[ remoteidheader ] = remoteid
|
|
2061
|
+
|
|
2062
|
+
this._dialog.request( requestoptions )
|
|
2063
|
+
return true
|
|
2064
|
+
}
|
|
2065
|
+
|
|
2066
|
+
/**
|
|
2067
|
+
@callback earlycallback
|
|
2068
|
+
@param { call } call - our call object which is early
|
|
2069
|
+
*/
|
|
2070
|
+
|
|
2071
|
+
/**
|
|
2072
|
+
@callback confirmcallback
|
|
2073
|
+
@async
|
|
2074
|
+
@param { call } call - our call object which is early
|
|
2075
|
+
*/
|
|
2076
|
+
|
|
2077
|
+
/**
|
|
2078
|
+
@callback failcallback
|
|
2079
|
+
@param { call } call - our call object which is early
|
|
2080
|
+
*/
|
|
2081
|
+
|
|
2082
|
+
/**
|
|
2083
|
+
@summary Creates a new SIP dialog. Returns a promise which resolves
|
|
2084
|
+
when the dialog is either answered (or cancelled for some reason).
|
|
2085
|
+
The promise resolves to a new call is one is generated, or undefined if not.
|
|
2086
|
+
@param { Object } [ options ] - Options object. See default_options in index.js for more details.
|
|
2087
|
+
@param { string } [ options.contact ] - The contact string
|
|
2088
|
+
@param { boolean } [ options.orphan ] - If present and true then orphan the new call
|
|
2089
|
+
@param { string } [ options.auth.username ] - If SIP auth required username
|
|
2090
|
+
@param { string } [ options.auth.password ] - If SIP auth required password
|
|
2091
|
+
@param { object } [ options.headers ] - Object containing extra sip headers required.
|
|
2092
|
+
@param { object } [ options.uactimeout ] - override the deault timeout
|
|
2093
|
+
@param { boolean } [ options.late ] - late negotiation
|
|
2094
|
+
@param { entity } [ options.entity ] - used to store this call against and look up a contact string if not supplied.
|
|
2095
|
+
@param { string } [ options.entity.username ]
|
|
2096
|
+
@param { string } [ options.entity.realm ]
|
|
2097
|
+
@param { string } [ options.entity.uri ]
|
|
2098
|
+
@param { number } [ options.entity.max ] - if included no more than this number of calls for this entity (only if we look user up)
|
|
2099
|
+
@param { object } [ callbacks ]
|
|
2100
|
+
@param { earlycallback } [ callbacks.early ] - callback to provide a call object with early call (pre dialog)
|
|
2101
|
+
@param { confirmcallback } [ callbacks.confirm ] - called when a dialog is confirmed but before it is bridged with a parent - this provides an opportunity for another call to adopt this call
|
|
2102
|
+
@param { failcallback } [ callbacks.fail ] - Called when child is terminated
|
|
2103
|
+
@return { Promise< call | false > } - returns a promise which resolves to a new call object if a dialog has been confirmed. If none are confirmed ten return false. Each attempt is fed into callbacks.early.
|
|
2104
|
+
*/
|
|
2105
|
+
async newuac( options, callbacks = {} ) {
|
|
2106
|
+
|
|
2107
|
+
/* If max-forwards is not specified then we decrement the parent and pass on */
|
|
2108
|
+
if( !( "headers" in options ) ) options.headers = {}
|
|
2109
|
+
if( !options.headers[ Object.keys( options.headers ).find( key => key.toLowerCase() === "max-forwards" ) ] ) {
|
|
2110
|
+
if( !this._req.has( "Max-Forwards" ) ) {
|
|
2111
|
+
return false
|
|
2112
|
+
}
|
|
2113
|
+
|
|
2114
|
+
let maxforwards = parseInt( this._req.get( "Max-Forwards" ) )
|
|
2115
|
+
if( maxforwards <= 0 ) return false
|
|
2116
|
+
options.headers[ "Max-Forwards" ] = maxforwards - 1
|
|
2117
|
+
}
|
|
2118
|
+
|
|
2119
|
+
if( !options.orphan ) {
|
|
2120
|
+
options.parent = this
|
|
2121
|
+
}
|
|
2122
|
+
|
|
2123
|
+
return await call.newuac( options, callbacks )
|
|
2124
|
+
}
|
|
2125
|
+
|
|
2126
|
+
/**
|
|
2127
|
+
@summary Creates a new SIP dialog(s). Returns a promise which resolves
|
|
2128
|
+
when the dialog is either answered (or cancelled for some reason).
|
|
2129
|
+
The promise resolves to a new call is one is generated, or undefined if not.
|
|
2130
|
+
@param { object } [ options ] - Options object. See default_options in index.js for more details.
|
|
2131
|
+
@param { call } [ options.parent ] - the parent call object
|
|
2132
|
+
@param { string } [ options.contact ] - The contact string
|
|
2133
|
+
@param { string } [ options.auth.username ] - If SIP auth required username
|
|
2134
|
+
@param { string } [ options.auth.password ] - If SIP auth required password
|
|
2135
|
+
@param { object } [ options.headers ] - Object containing extra sip headers required.
|
|
2136
|
+
@param { object } [ options.uactimeout ] - override the deault timeout
|
|
2137
|
+
@param { boolean | number } [ options.autoanswer ] - if true add call-info to auto answer, if number delay to add
|
|
2138
|
+
@param { boolean } [ options.late ] - late negotiation
|
|
2139
|
+
@param { entity } [ options.entity ] - used to store this call against and look up a contact string if not supplied.
|
|
2140
|
+
@param { string } [ options.entity.username ]
|
|
2141
|
+
@param { string } [ options.entity.realm ]
|
|
2142
|
+
@param { string } [ options.entity.uri ]
|
|
2143
|
+
@param { number } [ options.entity.max ] - if included no more than this number of calls for this entity (only if we look user up)
|
|
2144
|
+
@param { object } [ callbacks ]
|
|
2145
|
+
@param { earlycallback } [ callbacks.early ] - callback to provide a call object with early call (pre dialog)
|
|
2146
|
+
@param { confirmcallback } [ callbacks.confirm ] - called when a dialog is confirmed but before it is bridged with a parent - this provides an opportunity for another call to adopt this call
|
|
2147
|
+
@param { failcallback } [ callbacks.fail ] - Called when child is terminated
|
|
2148
|
+
@return { Promise< call | false > } - returns a promise which resolves to a new call object if a dialog has been confirmed. If none are confirmed ten return false. Each attempt is fed into callbacks.early.
|
|
2149
|
+
*/
|
|
2150
|
+
static async newuac( options, callbacks = {} ) {
|
|
2151
|
+
|
|
2152
|
+
/* If we don't have a contact we need to look up the entity */
|
|
2153
|
+
if( undefined === options.contact ) {
|
|
2154
|
+
|
|
2155
|
+
/* We check call count early - so we can call multiple registrations */
|
|
2156
|
+
if( options.entity && options.entity.max ) {
|
|
2157
|
+
if( !options.entity.uri ) {
|
|
2158
|
+
options.entity.uri = options.entity.username + "@" + options.entity.realm
|
|
2159
|
+
}
|
|
2160
|
+
|
|
2161
|
+
let cs = await callstore.getbyentity( options.entity.uri )
|
|
2162
|
+
if( cs.size >= options.entity.max ) {
|
|
2163
|
+
return false
|
|
2164
|
+
}
|
|
2165
|
+
}
|
|
2166
|
+
|
|
2167
|
+
/* If we have an entity - we need to look them up */
|
|
2168
|
+
if( !callmanager.options.registrar ) return false
|
|
2169
|
+
if( !options.entity ) return false
|
|
2170
|
+
|
|
2171
|
+
let contactinfo = await callmanager.options.registrar.contacts( options.entity )
|
|
2172
|
+
if( !contactinfo || 0 == contactinfo.contacts.length ) {
|
|
2173
|
+
return false
|
|
2174
|
+
}
|
|
2175
|
+
|
|
2176
|
+
let othercalls = []
|
|
2177
|
+
let ourcallbacks = {}
|
|
2178
|
+
let failcount = 0
|
|
2179
|
+
|
|
2180
|
+
let waitonchildrenresolve
|
|
2181
|
+
let waitonchildrenpromise = new Promise( ( resolve ) => {
|
|
2182
|
+
waitonchildrenresolve = resolve
|
|
2183
|
+
} )
|
|
2184
|
+
|
|
2185
|
+
ourcallbacks.early = ( c ) => {
|
|
2186
|
+
othercalls.push( c )
|
|
2187
|
+
if( callbacks.early ) callbacks.early( c )
|
|
2188
|
+
}
|
|
2189
|
+
|
|
2190
|
+
ourcallbacks.fail = ( c ) => {
|
|
2191
|
+
failcount++
|
|
2192
|
+
if( failcount >= othercalls.length ) {
|
|
2193
|
+
/* we have no more to try */
|
|
2194
|
+
waitonchildrenresolve( c )
|
|
2195
|
+
}
|
|
2196
|
+
if( callbacks.fail ) callbacks.fail( c )
|
|
2197
|
+
}
|
|
2198
|
+
|
|
2199
|
+
ourcallbacks.confirm = ( c ) => {
|
|
2200
|
+
waitonchildrenresolve( c )
|
|
2201
|
+
if( callbacks.confirm ) callbacks.confirm( c )
|
|
2202
|
+
}
|
|
2203
|
+
|
|
2204
|
+
for( let contact of contactinfo.contacts ) {
|
|
2205
|
+
if( undefined === contact ) continue
|
|
2206
|
+
let newoptions = { ...options }
|
|
2207
|
+
|
|
2208
|
+
newoptions.contact = contact.contact
|
|
2209
|
+
call.newuac( newoptions, ourcallbacks )
|
|
2210
|
+
}
|
|
2211
|
+
|
|
2212
|
+
let child = await waitonchildrenpromise
|
|
2213
|
+
|
|
2214
|
+
if( child && !child.parent ) {
|
|
2215
|
+
/* we have to terminate other calls we generated as this
|
|
2216
|
+
will not happen in the call object without a parent */
|
|
2217
|
+
for( let other of othercalls ) {
|
|
2218
|
+
if( other.uuid !== child.uuid ) {
|
|
2219
|
+
other.detach()
|
|
2220
|
+
other.hangup( hangupcodes.LOSE_RACE )
|
|
2221
|
+
}
|
|
2222
|
+
}
|
|
2223
|
+
}
|
|
2224
|
+
return child
|
|
2225
|
+
}
|
|
2226
|
+
|
|
2227
|
+
let newcall = new call()
|
|
2228
|
+
newcall.type = "uac"
|
|
2229
|
+
|
|
2230
|
+
if( options.parent ) {
|
|
2231
|
+
options.parent.adopt( newcall )
|
|
2232
|
+
}
|
|
2233
|
+
|
|
2234
|
+
newcall.options = {
|
|
2235
|
+
headers: { ...options.headers }
|
|
2236
|
+
}
|
|
2237
|
+
|
|
2238
|
+
let remoteidheader = "P-Preferred-Identity"
|
|
2239
|
+
let name = ""
|
|
2240
|
+
let user = "0000000000"
|
|
2241
|
+
let realm = "localhost.localdomain"
|
|
2242
|
+
|
|
2243
|
+
if( options.parent ) {
|
|
2244
|
+
let remote = options.parent.remote
|
|
2245
|
+
if( remote ) {
|
|
2246
|
+
name = remote.name.replace( /[^\w\-\s']+/g, "" ) /* only allow alpa num whitespace and ' */
|
|
2247
|
+
realm = remote.host
|
|
2248
|
+
user = remote.user
|
|
2249
|
+
}
|
|
2250
|
+
}
|
|
2251
|
+
|
|
2252
|
+
let callerid = `"${name}" <sip:${user}@${realm}>`
|
|
2253
|
+
|
|
2254
|
+
newcall.options.headers[ remoteidheader ] = callerid
|
|
2255
|
+
|
|
2256
|
+
if( options.entity ) {
|
|
2257
|
+
newcall._entity = options.entity
|
|
2258
|
+
callstore.set( newcall )
|
|
2259
|
+
}
|
|
2260
|
+
|
|
2261
|
+
if( true === options.autoanswer ) {
|
|
2262
|
+
newcall.options.headers[ "Call-Info" ] = `<sip:${user}@${realm}>;answer-after=0`
|
|
2263
|
+
} else if ( "number" == typeof options.autoanswer ) {
|
|
2264
|
+
newcall.options.headers[ "Call-Info" ] = `<sip:${user}@${realm}>;answer-after=${options.autoanswer}`
|
|
2265
|
+
}
|
|
2266
|
+
|
|
2267
|
+
// Polycom
|
|
2268
|
+
// Alert-Info: <https://www.babblevoice.com/polycom/LoudRing.wav>
|
|
2269
|
+
// Vtech
|
|
2270
|
+
// Alert-Info: <http://www.babblevoice.com>;info=ringer2
|
|
2271
|
+
|
|
2272
|
+
|
|
2273
|
+
newcall.options = { ...callmanager.options, ...newcall.options, ...options }
|
|
2274
|
+
|
|
2275
|
+
newcall._timers.newuac = setTimeout( () => {
|
|
2276
|
+
newcall.hangup( hangupcodes.REQUEST_TIMEOUT )
|
|
2277
|
+
}, newcall.options.uactimeout )
|
|
2278
|
+
|
|
2279
|
+
if( newcall.options.late ) {
|
|
2280
|
+
newcall.options.noAck = true /* this is a MUST for late negotiation */
|
|
2281
|
+
} else {
|
|
2282
|
+
newcall.channels.audio = await projectrtp.openchannel( newcall._handlechannelevents.bind( newcall ) )
|
|
2283
|
+
|
|
2284
|
+
newcall.sdp.local = sdpgen.create().addcodecs( newcall.options.preferedcodecs )
|
|
2285
|
+
newcall.sdp.local.setaudioport( newcall.channels.audio.local.port )
|
|
2286
|
+
.setconnectionaddress( newcall.channels.audio.local.address )
|
|
2287
|
+
|
|
2288
|
+
/* DTLS is only supported ( outbound ) on websocket connections */
|
|
2289
|
+
if( -1 !== options.contact.toLocaleUpperCase().indexOf( "transport=ws" ) ) { /* ws or wss */
|
|
2290
|
+
/* We need to add
|
|
2291
|
+
msid-semantic
|
|
2292
|
+
m=audio 13412 UDP/TLS/RTP/SAVPF 9 126 13 (SAVPF)
|
|
2293
|
+
a=setup:act/pass
|
|
2294
|
+
a=fingerprint:sha-256
|
|
2295
|
+
a=ice-ufrag
|
|
2296
|
+
a=ice-pwd:
|
|
2297
|
+
a=candidate
|
|
2298
|
+
a=end-of-candidates
|
|
2299
|
+
|
|
2300
|
+
There is also a lot of
|
|
2301
|
+
a=ssrc:2328136401 <some other details>
|
|
2302
|
+
|
|
2303
|
+
TODO: add to our SDP newcall.channels.audio
|
|
2304
|
+
*/
|
|
2305
|
+
}
|
|
2306
|
+
|
|
2307
|
+
/* Create our SDP */
|
|
2308
|
+
newcall.options.localSdp = newcall.sdp.local.toString()
|
|
2309
|
+
}
|
|
2310
|
+
|
|
2311
|
+
let addressparts = parseuri( options.contact )
|
|
2312
|
+
if( addressparts ) {
|
|
2313
|
+
newcall.network.remote.address = addressparts.host
|
|
2314
|
+
if( addressparts.port ) newcall.network.remote.port = addressparts.port
|
|
2315
|
+
}
|
|
2316
|
+
|
|
2317
|
+
newcall._dialog = await callmanager.options.srf.createUAC( options.contact, newcall.options, {
|
|
2318
|
+
cbRequest: ( err, req ) => {
|
|
2319
|
+
|
|
2320
|
+
if( !req ) {
|
|
2321
|
+
newcall.state.destroyed = true
|
|
2322
|
+
console.error( "No req object??", err )
|
|
2323
|
+
return
|
|
2324
|
+
}
|
|
2325
|
+
|
|
2326
|
+
newcall._req = req
|
|
2327
|
+
newcall.state.trying = true
|
|
2328
|
+
|
|
2329
|
+
newcall.sip = {
|
|
2330
|
+
"callid": req.getParsedHeader( "call-id" ),
|
|
2331
|
+
"tags": {
|
|
2332
|
+
"local": req.getParsedHeader( "from" ).params.tag,
|
|
2333
|
+
"remote": ""
|
|
2334
|
+
},
|
|
2335
|
+
"contact": [ { "uri": options.contact } ]
|
|
2336
|
+
}
|
|
2337
|
+
|
|
2338
|
+
callstore.set( newcall )
|
|
2339
|
+
if( callbacks && callbacks.early ) callbacks.early( newcall )
|
|
2340
|
+
callmanager.options.em.emit( "call.new", newcall )
|
|
2341
|
+
},
|
|
2342
|
+
cbProvisional: ( res ) => {
|
|
2343
|
+
newcall._res = res
|
|
2344
|
+
if( 180 === res.status ) {
|
|
2345
|
+
newcall._onring()
|
|
2346
|
+
}
|
|
2347
|
+
|
|
2348
|
+
if( newcall.canceled ) {
|
|
2349
|
+
newcall.hangup()
|
|
2350
|
+
}
|
|
2351
|
+
}
|
|
2352
|
+
} ).catch( ( err ) => {
|
|
2353
|
+
if ( undefined !== err.status ) {
|
|
2354
|
+
let reason = hangupcodes.REQUEST_TERMINATED
|
|
2355
|
+
if( err.status in inboundsiperros ) reason = inboundsiperros[ err.status ]
|
|
2356
|
+
|
|
2357
|
+
if( newcall ) newcall._onhangup( "wire", reason )
|
|
2358
|
+
} else {
|
|
2359
|
+
console.error( err )
|
|
2360
|
+
}
|
|
2361
|
+
} )
|
|
2362
|
+
|
|
2363
|
+
if( newcall._timers.newuac ) clearTimeout( newcall._timers.newuac )
|
|
2364
|
+
newcall._timers.newuac = false
|
|
2365
|
+
|
|
2366
|
+
if( newcall.state.destroyed ) {
|
|
2367
|
+
|
|
2368
|
+
if( callbacks && callbacks.fail ) callbacks.fail( newcall )
|
|
2369
|
+
return newcall
|
|
2370
|
+
}
|
|
2371
|
+
|
|
2372
|
+
newcall.sdp.remote = sdpgen.create( newcall._dialog.remote.sdp )
|
|
2373
|
+
|
|
2374
|
+
if( callbacks.confirm ) await callbacks.confirm( newcall )
|
|
2375
|
+
return await newcall._onanswer()
|
|
2376
|
+
}
|
|
2377
|
+
|
|
2378
|
+
/**
|
|
2379
|
+
Create a new object when we receive an INVITE request.
|
|
2380
|
+
|
|
2381
|
+
@param { object } req - req object from drachtio
|
|
2382
|
+
@param { res } res - res object from drachtio
|
|
2383
|
+
@returns { call }
|
|
2384
|
+
*/
|
|
2385
|
+
static frominvite( req, res ) {
|
|
2386
|
+
let c = new call()
|
|
2387
|
+
|
|
2388
|
+
c.type = "uas"
|
|
2389
|
+
|
|
2390
|
+
/**
|
|
2391
|
+
@typedef { Object } source
|
|
2392
|
+
@property { string } address
|
|
2393
|
+
@property { number } port
|
|
2394
|
+
@property { string } protocol
|
|
2395
|
+
*/
|
|
2396
|
+
|
|
2397
|
+
c.network.remote.address = req.source_address
|
|
2398
|
+
c.network.remote.port = req.source_port
|
|
2399
|
+
c.network.remote.protocol = req.protocol
|
|
2400
|
+
|
|
2401
|
+
c.sip.callid = req.getParsedHeader( "call-id" )
|
|
2402
|
+
c.sip.contact = req.getParsedHeader( "contact" )
|
|
2403
|
+
c.sip.tags.remote = req.getParsedHeader( "from" ).params.tag
|
|
2404
|
+
|
|
2405
|
+
/**
|
|
2406
|
+
@member
|
|
2407
|
+
@private
|
|
2408
|
+
*/
|
|
2409
|
+
c._req = req
|
|
2410
|
+
c._req.on( "cancel", () => this._oncanceled() )
|
|
2411
|
+
/**
|
|
2412
|
+
@member
|
|
2413
|
+
@private
|
|
2414
|
+
*/
|
|
2415
|
+
c._res = res
|
|
2416
|
+
|
|
2417
|
+
callstore.set( c ).then( () => {
|
|
2418
|
+
callmanager.options.em.emit( "call.new", c )
|
|
2419
|
+
} )
|
|
2420
|
+
|
|
2421
|
+
return c
|
|
2422
|
+
|
|
2423
|
+
}
|
|
2424
|
+
|
|
2425
|
+
static hangupcodes = hangupcodes
|
|
2426
|
+
static setcallmanager( cm ) {
|
|
2427
|
+
callmanager = cm
|
|
2428
|
+
}
|
|
2429
|
+
}
|
|
2430
|
+
|
|
2431
|
+
module.exports = call
|