@babblevoice/babble-drachtio-callmanager 1.4.3 → 1.5.0

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/lib/call.js CHANGED
@@ -90,6 +90,7 @@ class call {
90
90
  /**
91
91
  @typedef { Object } callstate
92
92
  @property { boolean } trying
93
+ @property { boolean } early
93
94
  @property { boolean } ringing
94
95
  @property { boolean } established
95
96
  @property { boolean } canceled
@@ -103,6 +104,7 @@ class call {
103
104
  /** @member { callstate } */
104
105
  this.state = {
105
106
  "trying": false,
107
+ "early": false,
106
108
  "ringing": false,
107
109
  "established": false,
108
110
  "canceled": false,
@@ -694,6 +696,12 @@ class call {
694
696
  Emitted when a call is ringing
695
697
  @event call.ringing
696
698
  @type {call}
699
+ */
700
+
701
+ /**
702
+ Emitted when a call is receives early
703
+ @event call.early
704
+ @type {call}
697
705
  */
698
706
 
699
707
  /**
@@ -812,6 +820,33 @@ class call {
812
820
  callmanager.options.em.emit( "call.ringing", this )
813
821
  }
814
822
 
823
+ /**
824
+ Called from newuac when we receive a 183
825
+ @private
826
+ */
827
+ async _onearly() {
828
+
829
+ if( this.state.established ) return
830
+ if( !this._res || !this._res.msg || !this._res.msg.body ) return
831
+
832
+ /* we have this._res */
833
+ this.sdp.remote = sdpgen.create( this._res.msg.body )
834
+ await this.answer( { "early": true } )
835
+
836
+ if( !this.parent || this.parent.state.established ) return
837
+
838
+ await this.parent.answer( { "early": true } )
839
+ await this.channels.audio.mix( this.parent.channels.audio )
840
+
841
+ this.parent._res.send( 183, {
842
+ headers: {
843
+ "User-Agent": "project",
844
+ "Supported": "replaces"
845
+ },
846
+ "body": this.sdp.local.toString()
847
+ } )
848
+ }
849
+
815
850
  /**
816
851
  Called from newuac when we are answered and we have a dialog,
817
852
  this = child call (the new call - the bleg)
@@ -1303,80 +1338,96 @@ class call {
1303
1338
  }
1304
1339
 
1305
1340
  /**
1306
- Answer this (inbound) call and store a channel which can be used.
1307
- @return {Promise} Returns a promise which resolves if the call is answered, otherwise rejects the promise.
1308
- This framework will catch and cleanup this call if this is rejected.
1341
+ * Answer this (inbound) call and store a channel which can be used. This framework will catch and cleanup this call if this is rejected.
1342
+ * @param { object } options
1343
+ * @param { boolean } options.early - don't answer the channel (establish) but establish early media (respond to 183).
1344
+ *
1345
+ * @return {Promise} Returns a promise which resolves if the call is answered, otherwise rejects the promise.
1309
1346
  */
1310
1347
  async answer( options = {} ) {
1311
1348
 
1312
1349
  if( this.canceled || this.established ) return
1313
1350
 
1314
1351
  options = { ...callmanager.options, ...this.options, ...options }
1315
- this.sdp.remote = sdpgen.create( this._req.msg.body )
1316
-
1317
- /* options.preferedcodecs may have been narrowed down so we still check callmanager as well */
1318
- this.selectedcodec = this.sdp.remote.intersection( options.preferedcodecs, true )
1319
- if( false === this.selectedcodec ) {
1320
- this.selectedcodec = this.sdp.remote.intersection( callmanager.options.preferedcodecs, true )
1321
- }
1322
1352
 
1323
- let remoteaudio = this.sdp.remote.getaudio()
1324
- if( !remoteaudio ) return
1353
+ let channeldef
1354
+ if( this._req.msg && this._req.msg.body ) {
1355
+ this.sdp.remote = sdpgen.create( this._req.msg.body )
1356
+ /* options.preferedcodecs may have been narrowed down so we still check callmanager as well */
1357
+ this.selectedcodec = this.sdp.remote.intersection( options.preferedcodecs, true )
1358
+ if( false === this.selectedcodec ) {
1359
+ this.selectedcodec = this.sdp.remote.intersection( callmanager.options.preferedcodecs, true )
1360
+ }
1361
+ let remoteaudio = this.sdp.remote.getaudio()
1362
+ if( !remoteaudio ) return
1325
1363
 
1326
- this.sdp.remote.select( this.selectedcodec )
1327
-
1328
- await call._parsesdpcandidates( remoteaudio, this.sdp.remote.sdp )
1364
+ this.sdp.remote.select( this.selectedcodec )
1365
+
1366
+ await call._parsesdpcandidates( remoteaudio, this.sdp.remote.sdp )
1329
1367
 
1330
- let channeldef = call._createchannelremotedef( remoteaudio.address, remoteaudio.port, remoteaudio.audio.payloads[ 0 ] )
1368
+ channeldef = call._createchannelremotedef( remoteaudio.address, remoteaudio.port, remoteaudio.audio.payloads[ 0 ] )
1331
1369
 
1332
- let iswebrtc = this._iswebrtc
1370
+ if( this._iswebrtc ) {
1371
+ channeldef.remote.dtls = {
1372
+ "fingerprint": this.sdp.remote.sdp.media[ 0 ].fingerprint,
1373
+ "mode": this.sdp.remote.sdp.media[ 0 ].setup==="passive"?"active":"passive" /* prefer passive for us */
1374
+ }
1333
1375
 
1334
- if( iswebrtc ) {
1335
- channeldef.remote.dtls = {
1336
- "fingerprint": this.sdp.remote.sdp.media[ 0 ].fingerprint,
1337
- "mode": this.sdp.remote.sdp.media[ 0 ].setup==="passive"?"active":"passive" /* prefer passive for us */
1376
+ channeldef.remote.icepwd = this.sdp.remote.sdp.media[ 0 ].icePwd
1338
1377
  }
1339
-
1340
- channeldef.remote.icepwd = this.sdp.remote.sdp.media[ 0 ].icePwd
1341
1378
  }
1342
1379
 
1343
- let ch = await projectrtp.openchannel( channeldef, this._handlechannelevents.bind( this ) )
1344
- this.channels.audio = ch
1345
- this.sdp.local = sdpgen.create()
1346
- .addcodecs( this.selectedcodec )
1347
- .setconnectionaddress( ch.local.address )
1348
- .setaudioport( ch.local.port )
1380
+ /*
1381
+ We might have already opened our audio when we received 183 (early).
1382
+ */
1383
+ if( this.channels.audio ) {
1384
+ this.channels.audio.remote( channeldef.remote )
1385
+ } else {
1386
+ let ch = await projectrtp.openchannel( channeldef, this._handlechannelevents.bind( this ) )
1387
+ this.channels.audio = ch
1388
+ this.sdp.local = sdpgen.create()
1389
+ .addcodecs( this.selectedcodec )
1390
+ .setconnectionaddress( ch.local.address )
1391
+ .setaudioport( ch.local.port )
1392
+
1393
+ if( callmanager.options.rfc2833 ) {
1394
+ this.sdp.local.addcodecs( "2833" )
1395
+ }
1349
1396
 
1350
- if( this.canceled ) return
1397
+ if( this._iswebrtc ) {
1398
+ this.sdp.local.addssrc( ch.local.ssrc )
1399
+ .secure( ch.local.dtls.fingerprint, channeldef.remote.dtls.mode )
1400
+ .addicecandidates( ch.local.address, ch.local.port, ch.local.icepwd )
1401
+ .rtcpmux()
1402
+ }
1351
1403
 
1352
- if( true === callmanager.options.rfc2833 ) {
1353
- this.sdp.local.addcodecs( "2833" )
1404
+ if( this.canceled ) return
1354
1405
  }
1355
1406
 
1356
- if( iswebrtc ) {
1357
- this.sdp.local.addssrc( ch.local.ssrc )
1358
- .secure( ch.local.dtls.fingerprint, channeldef.remote.dtls.mode )
1359
- .addicecandidates( ch.local.address, ch.local.port, ch.local.icepwd )
1360
- .rtcpmux()
1361
- }
1407
+ if( options.early ) {
1408
+ this.state.early = true
1409
+ this._em.emit( "call.early", this )
1410
+ callmanager.options.em.emit( "call.early", this )
1362
1411
 
1363
- let dialog = await callmanager.options.srf.createUAS( this._req, this._res, {
1364
- localSdp: this.sdp.local.toString(),
1365
- headers: {
1366
- "User-Agent": "project",
1367
- "Supported": "replaces"
1368
- }
1369
- } )
1412
+ } else {
1413
+ let dialog = await callmanager.options.srf.createUAS( this._req, this._res, {
1414
+ localSdp: this.sdp.local.toString(),
1415
+ headers: {
1416
+ "User-Agent": "project",
1417
+ "Supported": "replaces"
1418
+ }
1419
+ } )
1370
1420
 
1371
- this.established = true
1372
- this._dialog = dialog
1373
- this.sip.tags.local = dialog.sip.localTag
1374
- callstore.set( this )
1421
+ this.established = true
1422
+ this._dialog = dialog
1423
+ this.sip.tags.local = dialog.sip.localTag
1424
+ callstore.set( this )
1375
1425
 
1376
- this._addevents( this._dialog )
1426
+ this._addevents( this._dialog )
1427
+ this._em.emit( "call.answered", this )
1428
+ callmanager.options.em.emit( "call.answered", this )
1377
1429
 
1378
- this._em.emit( "call.answered", this )
1379
- callmanager.options.em.emit( "call.answered", this )
1430
+ }
1380
1431
  }
1381
1432
 
1382
1433
  /**
@@ -1940,7 +1991,7 @@ class call {
1940
1991
  let target = a_1.sdp.remote.getaudio()
1941
1992
  if( target ) {
1942
1993
  /* we should always get in here */
1943
- a_1.channels.audio.remote( call._createchannelremotedef( target.address, target.port, a_1.selectedcodec ) )
1994
+ a_1.channels.audio.remote( call._createchannelremotedef( target.address, target.port, a_1.selectedcodec ).remote )
1944
1995
 
1945
1996
  /* there might be situations where mix is not the correct thing - perhaps a pop push application? */
1946
1997
  a_1.channels.audio.mix( c_1.channels.audio )
@@ -2472,10 +2523,12 @@ class call {
2472
2523
  if( callbacks && callbacks.early ) callbacks.early( newcall )
2473
2524
  callmanager.options.em.emit( "call.new", newcall )
2474
2525
  },
2475
- cbProvisional: ( res ) => {
2526
+ cbProvisional: async ( res ) => {
2476
2527
  newcall._res = res
2477
2528
  if( 180 === res.status ) {
2478
2529
  newcall._onring()
2530
+ } else if( 183 === res.status ) {
2531
+ await newcall._onearly()
2479
2532
  }
2480
2533
 
2481
2534
  if( newcall.canceled ) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@babblevoice/babble-drachtio-callmanager",
3
- "version": "1.4.3",
3
+ "version": "1.5.0",
4
4
  "description": "Call processing to create a PBX",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -0,0 +1,102 @@
1
+
2
+
3
+ const { v4: uuidv4 } = require( "uuid" )
4
+ const expect = require( "chai" ).expect
5
+ const callmanager = require( "../../index.js" )
6
+ const call = require( "../../lib/call.js" )
7
+ const srf = require( "../mock/srf.js" )
8
+
9
+ const projectrtpmessage = require( "@babblevoice/projectrtp/lib/message.js" )
10
+ const net = require( "net" )
11
+
12
+ const clearcallmanager = require( "../../lib/callmanager.js" )._clear
13
+
14
+ describe( "call early", function() {
15
+
16
+ afterEach( function() {
17
+ clearcallmanager()
18
+ } )
19
+
20
+ beforeEach( function() {
21
+ clearcallmanager()
22
+ } )
23
+
24
+
25
+ it( `Create call and send 183 - early`, async () => {
26
+ let srfscenario = new srf.srfscenario()
27
+
28
+ let rtpserver = callmanager.projectrtp.proxy.listen()
29
+
30
+ let connection = net.createConnection( 9002, "127.0.0.1" )
31
+ .on( "error", ( e ) => {
32
+ console.error( e )
33
+ } )
34
+
35
+ connection.on( "connect", () => {
36
+ /* announce our node */
37
+ connection.write( projectrtpmessage.createmessage( {"status":{"channel":{"available":5000,"current":0},"workercount":12,"instance":"ca0ef6a9-9174-444d-bdeb-4c9eb54d4566"}} ) )
38
+ } )
39
+
40
+ let msgid
41
+ let mixing
42
+ let messagestate = projectrtpmessage.newstate()
43
+ connection.on( "data", ( data ) => {
44
+ projectrtpmessage.parsemessage( messagestate, data, ( msg ) => {
45
+ if( "open" === msg.channel ) {
46
+ msgid = msg.id
47
+ setTimeout( () => connection.write( projectrtpmessage.createmessage( {"local":{"port":10008,"dtls":{"fingerprint":"Some fingerprint","enabled":false},"address":"192.168.0.141"},"id": msg.id, "uuid": uuidv4(),"action":"open","status":{"channel":{"available":4995,"current":5},"workercount":12,"instance":"ca0ef6a9-9174-444d-bdeb-4c9eb54d4566"}} ) ), 2 )
48
+ } else if ( "close" === msg.channel ) {
49
+ connection.write( projectrtpmessage.createmessage( {"id": msg.id,"uuid":msg.uuid,"action":"close","reason":"requested","stats":{"in":{"mos":4.5,"count":586,"dropped":0,"skip":0},"out":{"count":303,"skip":0},"tick":{"meanus":124,"maxus":508,"count":597}}} ) )
50
+ } else if ( "mix" === msg.channel ) {
51
+ mixing = true
52
+ }
53
+ } )
54
+ } )
55
+
56
+ /* ensure we are connected */
57
+ await new Promise( ( r ) => setTimeout( () => r(), 100 ) )
58
+
59
+ srfscenario.oncreateUAC( async ( contact, options, callbacks ) => {
60
+ callbacks.cbProvisional( {
61
+ "status": 183,
62
+ "msg": {
63
+ "body": `v=0
64
+ o=- 1608235282228 0 IN IP4 127.0.0.1
65
+ s=
66
+ c=IN IP4 192.168.0.141
67
+ t=0 0
68
+ m=audio 20000 RTP/AVP 0 101
69
+ a=rtpmap:101 telephone-event/8000
70
+ a=fmtp:101 0-16
71
+ a=sendrecv`.replace(/(\r\n|\n|\r)/gm, "\r\n")
72
+ }
73
+ } )
74
+
75
+ await new Promise( ( r ) => setTimeout( () => r(), 100 ) )
76
+ throw { "status": 503 }
77
+ } )
78
+
79
+ let msgsent
80
+ let call = await new Promise( ( resolve ) => {
81
+ srfscenario.oncall( async ( call ) => { resolve( call ) } )
82
+ srfscenario.inbound()
83
+ srfscenario.res.onsend( (c) => {
84
+ msgsent = c
85
+ })
86
+ } )
87
+
88
+
89
+ let newcall = await call.newuac( { "contact": "callto" } )
90
+
91
+ await call._onhangup( "wire" )
92
+
93
+ expect( newcall.state.early ).to.be.true
94
+ expect( call.state.early ).to.be.true
95
+ expect( mixing ).to.be.true
96
+ expect( msgsent ).to.equal( 183 )
97
+
98
+ connection.destroy()
99
+ rtpserver.destroy()
100
+ } )
101
+
102
+ } )
package/test/mock/srf.js CHANGED
@@ -165,6 +165,11 @@ class res {
165
165
  this.callbacks = {
166
166
  "onsend": false
167
167
  }
168
+
169
+ this.msg = {
170
+ "body": possiblesdp[ sdpid % possiblesdp.length ],
171
+ method: "INVITE"
172
+ }
168
173
  }
169
174
 
170
175
  /* returns undefined - https://drachtio.org/api#sip-request */