@babblevoice/babble-drachtio-callmanager 2.0.2 → 2.0.3
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/.eslintrc.js +67 -0
- package/index.js +3 -4
- package/lib/call.js +5 -25
- package/lib/callmanager.js +3 -3
- package/lib/sdp.js +78 -46
- package/package.json +7 -3
- package/test/interface/call.js +130 -130
- package/test/interface/callmanager.js +12 -12
- package/test/interface/callsdp.js +6 -6
- package/test/interface/early.js +72 -73
- package/test/interface/events.js +10 -10
- package/test/interface/hold.js +15 -15
- package/test/interface/latecall.js +8 -8
- package/test/interface/sdp.js +321 -47
- package/test/interface/xfer.js +47 -56
- package/test/interface/serverconnect.js +0 -79
package/.eslintrc.js
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
module.exports = {
|
|
2
|
+
"env": {
|
|
3
|
+
"browser": false,
|
|
4
|
+
"node": true,
|
|
5
|
+
"mocha": true,
|
|
6
|
+
"commonjs": true,
|
|
7
|
+
"es2021": true
|
|
8
|
+
},
|
|
9
|
+
"extends": [ "plugin:promise/recommended", "eslint:recommended" ],
|
|
10
|
+
"overrides": [
|
|
11
|
+
],
|
|
12
|
+
"parserOptions": {
|
|
13
|
+
"ecmaVersion": "latest"
|
|
14
|
+
},
|
|
15
|
+
"ignorePatterns": [ "out" ],
|
|
16
|
+
"rules": {
|
|
17
|
+
"require-atomic-updates": [
|
|
18
|
+
"error"
|
|
19
|
+
],
|
|
20
|
+
"no-invalid-this": [
|
|
21
|
+
"error"
|
|
22
|
+
],
|
|
23
|
+
"no-useless-call": [
|
|
24
|
+
"error"
|
|
25
|
+
],
|
|
26
|
+
"no-useless-return": [
|
|
27
|
+
"error"
|
|
28
|
+
],
|
|
29
|
+
"no-var": [
|
|
30
|
+
"error"
|
|
31
|
+
],
|
|
32
|
+
"prefer-const": [
|
|
33
|
+
"error"
|
|
34
|
+
],
|
|
35
|
+
"complexity": [
|
|
36
|
+
"error",
|
|
37
|
+
10
|
|
38
|
+
],
|
|
39
|
+
"max-depth": [
|
|
40
|
+
"error",
|
|
41
|
+
5
|
|
42
|
+
],
|
|
43
|
+
"no-eval": [
|
|
44
|
+
"error"
|
|
45
|
+
],
|
|
46
|
+
"indent": [
|
|
47
|
+
"error",
|
|
48
|
+
2
|
|
49
|
+
],
|
|
50
|
+
"linebreak-style": [
|
|
51
|
+
"error",
|
|
52
|
+
"unix"
|
|
53
|
+
],
|
|
54
|
+
"quotes": [
|
|
55
|
+
"error",
|
|
56
|
+
"double"
|
|
57
|
+
],
|
|
58
|
+
"semi": [
|
|
59
|
+
"error",
|
|
60
|
+
"never"
|
|
61
|
+
],
|
|
62
|
+
"yoda": [
|
|
63
|
+
"error",
|
|
64
|
+
"always"
|
|
65
|
+
]
|
|
66
|
+
}
|
|
67
|
+
}
|
package/index.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
|
|
2
|
-
const assert = require( "assert" )
|
|
3
2
|
const callmanager = require( "./lib/callmanager.js" )
|
|
4
3
|
const store = require( "./lib/store.js" )
|
|
5
4
|
|
|
@@ -9,7 +8,7 @@ const default_options = {
|
|
|
9
8
|
"preferedcodecs": "g722 ilbc pcmu pcma",
|
|
10
9
|
//"transcode": true, - this never made it into the software - TODO
|
|
11
10
|
"uactimeout": 30000, /* timeout when calling a client */
|
|
12
|
-
"seexpire": 120000, /* session expires timeout */
|
|
11
|
+
"seexpire": 120000, /* session expires timeout mS */
|
|
13
12
|
"rfc2833": true, /* Enable RFC 2833 - DTMF */
|
|
14
13
|
"late": false, /* Late negotiation */
|
|
15
14
|
"registrar": false, /* our registrar object or falsey */
|
|
@@ -21,7 +20,7 @@ const default_options = {
|
|
|
21
20
|
@returns { callmanager }
|
|
22
21
|
*/
|
|
23
22
|
module.exports.callmanager = ( options ) => {
|
|
24
|
-
|
|
23
|
+
const ouroptions = { ...default_options, ...options }
|
|
25
24
|
return callmanager.callmanager( ouroptions )
|
|
26
25
|
}
|
|
27
26
|
|
|
@@ -43,4 +42,4 @@ module.exports.store = store
|
|
|
43
42
|
/**
|
|
44
43
|
* Call
|
|
45
44
|
*/
|
|
46
|
-
|
|
45
|
+
module.exports.call = callmanager.call
|
package/lib/call.js
CHANGED
|
@@ -66,17 +66,13 @@ const inboundsiperros = {
|
|
|
66
66
|
404: hangupcodes.UNALLOCATED_NUMBER
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
-
|
|
69
|
+
let callmanager = {
|
|
70
70
|
"options": {}
|
|
71
71
|
}
|
|
72
72
|
|
|
73
73
|
/** @class */
|
|
74
74
|
class call {
|
|
75
75
|
|
|
76
|
-
#state = {
|
|
77
|
-
"waitingforevent": false
|
|
78
|
-
}
|
|
79
|
-
|
|
80
76
|
/**
|
|
81
77
|
Construct our call object with all defaults, including a default UUID.
|
|
82
78
|
@constructs call
|
|
@@ -1459,7 +1455,7 @@ class call {
|
|
|
1459
1455
|
/**
|
|
1460
1456
|
Private helper function to add events to our RTP channel.
|
|
1461
1457
|
@param {object} e - the rtp event
|
|
1462
|
-
@
|
|
1458
|
+
@private
|
|
1463
1459
|
*/
|
|
1464
1460
|
_handlechannelevents( e ) {
|
|
1465
1461
|
|
|
@@ -1489,16 +1485,6 @@ class call {
|
|
|
1489
1485
|
this._tevent( e.event )
|
|
1490
1486
|
}
|
|
1491
1487
|
|
|
1492
|
-
if( !this._promises.resolve.channelevent ) {
|
|
1493
|
-
this.#state.waitingforevent = e
|
|
1494
|
-
return
|
|
1495
|
-
}
|
|
1496
|
-
|
|
1497
|
-
this.#matchandthrow( e )
|
|
1498
|
-
|
|
1499
|
-
}
|
|
1500
|
-
|
|
1501
|
-
#matchandthrow( e ) {
|
|
1502
1488
|
if( this._eventconstraints ) {
|
|
1503
1489
|
let constraintkeys = Object.keys( this._eventconstraints )
|
|
1504
1490
|
for( const k of constraintkeys ) {
|
|
@@ -1506,11 +1492,11 @@ class call {
|
|
|
1506
1492
|
if( "object" === typeof this._eventconstraints[ k ] ) {
|
|
1507
1493
|
/* regex */
|
|
1508
1494
|
if( !e[ k ].match( this._eventconstraints[ k ] ) ) {
|
|
1509
|
-
return
|
|
1495
|
+
return
|
|
1510
1496
|
}
|
|
1511
1497
|
} else if( this._eventconstraints[ k ] != e[ k ] ) {
|
|
1512
1498
|
/* not a match */
|
|
1513
|
-
return
|
|
1499
|
+
return
|
|
1514
1500
|
}
|
|
1515
1501
|
}
|
|
1516
1502
|
}
|
|
@@ -1527,7 +1513,7 @@ class call {
|
|
|
1527
1513
|
this._promises.resolve.channelevent = false
|
|
1528
1514
|
this._promises.promise.channelevent = false
|
|
1529
1515
|
if( r ) r( e )
|
|
1530
|
-
|
|
1516
|
+
|
|
1531
1517
|
}
|
|
1532
1518
|
|
|
1533
1519
|
/**
|
|
@@ -1540,7 +1526,6 @@ class call {
|
|
|
1540
1526
|
A telephone event will resolve this promise as we typically need speech to be interupted
|
|
1541
1527
|
by the user. Note, peeking a telephone-event (i.e. DTMF) will not clear it like waitfortelevents will.
|
|
1542
1528
|
@param { regex } constraints - event to filter for from our RTP server - excluding DTMF events - these will always return
|
|
1543
|
-
@returns { Promise< object > }
|
|
1544
1529
|
*/
|
|
1545
1530
|
waitforanyevent( constraints, timeout = 500 ) {
|
|
1546
1531
|
|
|
@@ -1560,11 +1545,6 @@ class call {
|
|
|
1560
1545
|
if( r ) r( "timeout" )
|
|
1561
1546
|
}, timeout * 1000 )
|
|
1562
1547
|
|
|
1563
|
-
if( this.#state.waitingforevent ) {
|
|
1564
|
-
this.#matchandthrow( this.#state.waitingforevent )
|
|
1565
|
-
}
|
|
1566
|
-
this.#state.waitingforevent = false
|
|
1567
|
-
|
|
1568
1548
|
return this._promises.promise.channelevent
|
|
1569
1549
|
}
|
|
1570
1550
|
|
package/lib/callmanager.js
CHANGED
|
@@ -37,9 +37,9 @@ class callmanager {
|
|
|
37
37
|
@private
|
|
38
38
|
*/
|
|
39
39
|
async _oninvite( req, res, next ) {
|
|
40
|
-
if( req.msg.method
|
|
40
|
+
if( "INVITE" !== req.msg.method ) return next()
|
|
41
41
|
|
|
42
|
-
|
|
42
|
+
const calldesc = {
|
|
43
43
|
"callid": req.getParsedHeader( "call-id" ),
|
|
44
44
|
"tags": {
|
|
45
45
|
"remote": req.getParsedHeader( "from" ).params.tag,
|
|
@@ -97,7 +97,7 @@ Is configured to use drachtio and registers emitter for presence.
|
|
|
97
97
|
*/
|
|
98
98
|
let cm = false
|
|
99
99
|
module.exports.callmanager = ( options ) => {
|
|
100
|
-
if(
|
|
100
|
+
if( false !== cm ) return cm
|
|
101
101
|
|
|
102
102
|
assert( undefined !== options.srf )
|
|
103
103
|
cm = new callmanager( options )
|
package/lib/sdp.js
CHANGED
|
@@ -6,7 +6,7 @@ const crypto = require( "crypto" )
|
|
|
6
6
|
/*
|
|
7
7
|
An SDP Generator.
|
|
8
8
|
*/
|
|
9
|
-
|
|
9
|
+
let sessionidcounter = Math.floor( Math.random() * 100000 )
|
|
10
10
|
|
|
11
11
|
const codecconv = {
|
|
12
12
|
"0": "pcmu",
|
|
@@ -34,27 +34,27 @@ const codecdefs = {
|
|
|
34
34
|
"rtp": {
|
|
35
35
|
"pcmu": {
|
|
36
36
|
payload: 0,
|
|
37
|
-
codec:
|
|
37
|
+
codec: "PCMU",
|
|
38
38
|
rate: 8000
|
|
39
39
|
},
|
|
40
40
|
"pcma": {
|
|
41
41
|
payload: 8,
|
|
42
|
-
codec:
|
|
42
|
+
codec: "PCMA",
|
|
43
43
|
rate: 8000
|
|
44
44
|
},
|
|
45
45
|
"g722": {
|
|
46
46
|
payload: 9,
|
|
47
|
-
codec:
|
|
47
|
+
codec: "G722",
|
|
48
48
|
rate: 8000
|
|
49
49
|
},
|
|
50
50
|
"ilbc": {
|
|
51
51
|
payload: 97,
|
|
52
|
-
codec:
|
|
52
|
+
codec: "ilbc",
|
|
53
53
|
rate: 8000
|
|
54
54
|
},
|
|
55
55
|
"2833": {
|
|
56
56
|
payload: 101,
|
|
57
|
-
codec:
|
|
57
|
+
codec: "telephone-event/8000"
|
|
58
58
|
}
|
|
59
59
|
},
|
|
60
60
|
"fmtp": {
|
|
@@ -80,6 +80,44 @@ const defaultaudiomedia = {
|
|
|
80
80
|
"direction": "sendrecv"
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
+
/**
|
|
84
|
+
* Takes a mixed input and outputs an array in the form [ "pcmu", "pcma" ]
|
|
85
|
+
* @param { string | array<string|number> } codecarray
|
|
86
|
+
* @return { array< string >}
|
|
87
|
+
*/
|
|
88
|
+
function alltocodecname( codecarray ) {
|
|
89
|
+
|
|
90
|
+
/* check and convert to array */
|
|
91
|
+
if ( "string" === typeof codecarray ) {
|
|
92
|
+
codecarray = codecarray.split( /[ ,]+/ )
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/* convert to payloads */
|
|
96
|
+
const retval = []
|
|
97
|
+
for( const oin of codecarray ) {
|
|
98
|
+
if( undefined != codecrconv[ oin ] ) {
|
|
99
|
+
retval.push( oin )
|
|
100
|
+
} else if( codecconv[ oin ] ) {
|
|
101
|
+
retval.push( codecconv[ oin ] )
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return retval
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
*
|
|
110
|
+
* @param { object } audio
|
|
111
|
+
* @param { number } pt
|
|
112
|
+
* @returns { string }
|
|
113
|
+
*/
|
|
114
|
+
function getconfigforpt( audio, pt ) {
|
|
115
|
+
for( const fmtp of audio.fmtp ) {
|
|
116
|
+
if( pt == fmtp.payload ) return fmtp.config
|
|
117
|
+
}
|
|
118
|
+
return ""
|
|
119
|
+
}
|
|
120
|
+
|
|
83
121
|
class sdp {
|
|
84
122
|
|
|
85
123
|
constructor( sdp ) {
|
|
@@ -90,14 +128,14 @@ class sdp {
|
|
|
90
128
|
this.sdp = {
|
|
91
129
|
version: 0,
|
|
92
130
|
origin: {
|
|
93
|
-
username:
|
|
131
|
+
username: "-",
|
|
94
132
|
sessionId: sessionidcounter,
|
|
95
133
|
sessionVersion: 0,
|
|
96
|
-
netType:
|
|
134
|
+
netType: "IN",
|
|
97
135
|
ipVer: 4,
|
|
98
136
|
address: "127.0.0.1"
|
|
99
137
|
},
|
|
100
|
-
name:
|
|
138
|
+
name: "project",
|
|
101
139
|
timing: {
|
|
102
140
|
start: 0,
|
|
103
141
|
stop: 0
|
|
@@ -130,7 +168,7 @@ class sdp {
|
|
|
130
168
|
this.sdp.media.forEach( ( media, i, a ) => {
|
|
131
169
|
|
|
132
170
|
if ( "audio" === media.type ) {
|
|
133
|
-
if ( typeof media.payloads
|
|
171
|
+
if ( "string" === typeof media.payloads ) {
|
|
134
172
|
media.payloads = media.payloads.split( /[ ,]+/ )
|
|
135
173
|
}
|
|
136
174
|
|
|
@@ -149,7 +187,7 @@ class sdp {
|
|
|
149
187
|
Ideally we replicate the object required for target in our RTP service.
|
|
150
188
|
*/
|
|
151
189
|
getaudio() {
|
|
152
|
-
|
|
190
|
+
const m = this.sdp.media.find( mo => "audio" === mo.type )
|
|
153
191
|
|
|
154
192
|
if ( m ) {
|
|
155
193
|
|
|
@@ -270,11 +308,11 @@ class sdp {
|
|
|
270
308
|
}
|
|
271
309
|
|
|
272
310
|
codecarr.forEach( codec => {
|
|
273
|
-
|
|
274
|
-
|
|
311
|
+
const codecn = codecrconv[ codec ]
|
|
312
|
+
const def = codecdefs.rtp[ codec ]
|
|
275
313
|
if ( undefined !== def ) {
|
|
276
314
|
/* suported audio */
|
|
277
|
-
|
|
315
|
+
const m = this.getmedia( codecdefs.type[ codec ] )
|
|
278
316
|
|
|
279
317
|
/* Don't allow duplicates */
|
|
280
318
|
if( m.payloads.includes( codecn ) ) return
|
|
@@ -303,7 +341,7 @@ class sdp {
|
|
|
303
341
|
}
|
|
304
342
|
|
|
305
343
|
for( const m of this.sdp.media ) {
|
|
306
|
-
|
|
344
|
+
const mmsid = crypto.randomBytes( 16 ).toString( "hex" )
|
|
307
345
|
m.ssrcs = [
|
|
308
346
|
{ "id": ssrc, "attribute": "cname", "value": crypto.randomBytes( 16 ).toString( "hex" ) },
|
|
309
347
|
{ "id": ssrc, "attribute": "msid", "value": this.sdp.msidSemantic.token + " " + mmsid },
|
|
@@ -351,7 +389,7 @@ class sdp {
|
|
|
351
389
|
"port": port,
|
|
352
390
|
"type": "host",
|
|
353
391
|
"generation": 0
|
|
354
|
-
|
|
392
|
+
}
|
|
355
393
|
]
|
|
356
394
|
|
|
357
395
|
m.iceUfrag = crypto.randomBytes( 8 ).toString( "hex" )
|
|
@@ -361,7 +399,7 @@ class sdp {
|
|
|
361
399
|
return this
|
|
362
400
|
}
|
|
363
401
|
|
|
364
|
-
rtcpmux(
|
|
402
|
+
rtcpmux() {
|
|
365
403
|
for( const m of this.sdp.media ) {
|
|
366
404
|
m.rtcpMux = "rtcp-mux"
|
|
367
405
|
}
|
|
@@ -395,43 +433,37 @@ class sdp {
|
|
|
395
433
|
If first ony, it only returns the first match
|
|
396
434
|
*/
|
|
397
435
|
intersection( other, firstonly = false ) {
|
|
398
|
-
|
|
399
|
-
|
|
436
|
+
|
|
437
|
+
/* ensure other side is on the format [ "pcma", "pcmu" ] */
|
|
438
|
+
other = alltocodecname( other )
|
|
439
|
+
|
|
440
|
+
let audio = []
|
|
441
|
+
for( const m of this.sdp.media ) {
|
|
442
|
+
if( "audio" == m.type ) audio = m
|
|
400
443
|
}
|
|
401
444
|
|
|
402
|
-
/*
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
445
|
+
/* work out an array of codecs on this side in the format of [ "pcma", "pcmu" ] */
|
|
446
|
+
const ourcodecs = []
|
|
447
|
+
for( const pt of audio.payloads ) {
|
|
448
|
+
if( undefined === codecconv[ pt ] ) continue
|
|
449
|
+
if( 97 == pt ) {
|
|
450
|
+
if( -1 == getconfigforpt( audio, 97 ).indexOf( "mode=30" ) )
|
|
451
|
+
ourcodecs.push( codecconv[ pt ] )
|
|
452
|
+
} else {
|
|
453
|
+
ourcodecs.push( codecconv[ pt ] )
|
|
406
454
|
}
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
/* Does it exist in payloads and fmtp where required */
|
|
410
|
-
let retval = []
|
|
411
|
-
this.sdp.media.forEach( m => {
|
|
412
|
-
retval = retval.concat( other.filter( pl => {
|
|
413
|
-
if ( m.payloads.includes( pl ) ) {
|
|
414
|
-
let codecname = codecconv[ pl ]
|
|
415
|
-
if ( undefined === codecdefs.fmtp[ codecname ] ) return true
|
|
455
|
+
}
|
|
416
456
|
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
}
|
|
420
|
-
return false
|
|
421
|
-
} ) )
|
|
422
|
-
} )
|
|
457
|
+
/* intersection */
|
|
458
|
+
let retval = other.filter( value => ourcodecs.includes( value ) )
|
|
423
459
|
|
|
424
|
-
|
|
460
|
+
/* If fisrt only - i.e. select codec */
|
|
461
|
+
if ( firstonly && 0 < retval.length ) {
|
|
425
462
|
retval = [ retval[ 0 ] ]
|
|
426
463
|
this.select( retval[ 0 ] )
|
|
427
464
|
}
|
|
428
465
|
|
|
429
|
-
|
|
430
|
-
retval.forEach( ( codec, i, a ) => {
|
|
431
|
-
if ( undefined != codecconv[ codec ] ) a[ i ] = codecconv[ codec ]
|
|
432
|
-
} )
|
|
433
|
-
|
|
434
|
-
let full = retval.join( " " )
|
|
466
|
+
const full = retval.join( " " )
|
|
435
467
|
if( !full ) return false
|
|
436
468
|
|
|
437
469
|
return full
|
|
@@ -440,7 +472,7 @@ class sdp {
|
|
|
440
472
|
toString() {
|
|
441
473
|
|
|
442
474
|
/* We need to convert payloads back to string to stop a , being added */
|
|
443
|
-
|
|
475
|
+
const co = Object.assign( this.sdp )
|
|
444
476
|
|
|
445
477
|
co.media.forEach( ( media, i, a ) => {
|
|
446
478
|
if( Array.isArray( media.payloads ) ) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@babblevoice/babble-drachtio-callmanager",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.3",
|
|
4
4
|
"description": "Call processing to create a PBX",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -21,14 +21,18 @@
|
|
|
21
21
|
"license": "MIT",
|
|
22
22
|
"dependencies": {
|
|
23
23
|
"@babblevoice/babble-drachtio-auth": "^1.0.2",
|
|
24
|
-
"chai": "^4.3.4",
|
|
25
24
|
"drachtio-srf": "^4.4.47",
|
|
26
|
-
"mocha": "^9.2.2",
|
|
27
25
|
"uuid": "^8.3.2"
|
|
28
26
|
},
|
|
29
27
|
"optionalDependencies": {
|
|
30
28
|
"@babblevoice/projectrtp": "^2.3.7"
|
|
31
29
|
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"chai": "^4.3.4",
|
|
32
|
+
"eslint": "^8.42.0",
|
|
33
|
+
"eslint-plugin-promise": "^6.1.1",
|
|
34
|
+
"mocha": "^9.2.2"
|
|
35
|
+
},
|
|
32
36
|
"bugs": {
|
|
33
37
|
"url": "https://github.com/tinpotnick/babble-drachtio-callmanager/issues"
|
|
34
38
|
},
|