@babblevoice/projectrtp 2.5.28 → 2.5.30

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/README.md CHANGED
@@ -60,7 +60,7 @@ If you wish to build outsode of a Docker image, there are npm target scripts for
60
60
 
61
61
  ```bash
62
62
  docker buildx prune
63
- docker buildx build --platform linux/amd64,linux/arm64 -t tinpotnick/projectrtp:2.5.27 . --push
63
+ docker buildx build --platform linux/amd64,linux/arm64 -t tinpotnick/projectrtp:2.5.29 . --push
64
64
  ```
65
65
 
66
66
  ## Example scripts
package/index.js CHANGED
@@ -31,13 +31,13 @@ function gencerts() {
31
31
  const combined = keypath + "dtls-srtp.pem"
32
32
 
33
33
  const openssl = spawnSync( "openssl", [ "genrsa", "-out", serverkey, "4096" ] )
34
- if( 0 !== openssl.status ) throw "Failed to genrsa: " + openssl.status
34
+ if( 0 !== openssl.status ) throw new Error( "Failed to genrsa: " + openssl.status )
35
35
 
36
36
  const request = spawnSync( "openssl", [ "req", "-new", "-key", serverkey , "-out", servercsr, "-subj", "/C=GB/CN=projectrtp" ] )
37
- if( 0 !== request.status ) throw "Failed to generate csr: " + request.status
37
+ if( 0 !== request.status ) throw new Error( "Failed to generate csr: " + request.status )
38
38
 
39
39
  const sign = spawnSync( "openssl", [ "x509", "-req", "-in", servercsr, "-signkey", serverkey, "-out", servercert ] )
40
- if( 0 !== sign.status ) throw "Failed to sign key: " + sign.status
40
+ if( 0 !== sign.status ) throw new Error( "Failed to sign key: " + sign.status )
41
41
 
42
42
  const serverkeydata = fs.readFileSync( serverkey )
43
43
  const servercertdata = fs.readFileSync( servercert )
@@ -407,9 +407,9 @@ class projectrtp {
407
407
  run( params ) {
408
408
 
409
409
  if( "win32" == process.platform && "x64" == process.arch ) {
410
- throw "Platform not currently supported"
410
+ throw new Error( "Platform not currently supported" )
411
411
  } else if( "win32" == process.platform && "ia32" == process.arch ) {
412
- throw "Platform not currently supported"
412
+ throw new Error( "Platform not currently supported" )
413
413
  }
414
414
 
415
415
  if( actualprojectrtp ) return
@@ -445,7 +445,6 @@ class projectrtp {
445
445
  if( !params.forcelocal && server.interface.get() ) {
446
446
  return server.interface.get().openchannel( params, cb )
447
447
  } else {
448
- /* use local */
449
448
  const chan = actualprojectrtp.openchannel( params, ( d ) => {
450
449
  try{
451
450
  if( chan.em ) {
@@ -471,7 +470,7 @@ class projectrtp {
471
470
 
472
471
  /* ensure we are identicle to the node version of this object */
473
472
  chan.openchannel = this.openchannel.bind( this )
474
-
473
+
475
474
  return chan
476
475
  }
477
476
  }
package/lib/message.js CHANGED
@@ -75,7 +75,7 @@ function trimstateandcallback( state, cb ) {
75
75
  if( e instanceof SyntaxError ) {
76
76
  return
77
77
  }
78
- throw e
78
+ throw new Error( e )
79
79
  }
80
80
  }
81
81
  }
package/lib/node.js CHANGED
@@ -15,7 +15,12 @@ const channelmap = {
15
15
  }
16
16
  },
17
17
  "unmix": ( chan ) => chan.unmix(),
18
- "dtmf": ( chan, msg ) => chan.dtmf( msg.digits ),
18
+ "dtmf": ( chan, msg ) => {
19
+ if( !msg.digits ) return
20
+ if( "string" !== typeof msg.digits ) return
21
+ if( 10 < msg.digits.length ) return
22
+ chan.dtmf( msg.digits )
23
+ },
19
24
  "echo": ( chan ) => chan.echo(),
20
25
  "play": ( chan, msg ) => chan.play( msg.soup ),
21
26
  "record": ( chan, msg ) => chan.record( msg.options ),
@@ -64,7 +69,7 @@ class rtpnodeconnection {
64
69
  this._processmessage( modifiedmsg )
65
70
  } )
66
71
  } catch( e ) {
67
- console.error( "Unhandled exception in babble-rtp", e )
72
+ console.error( "Unhandled exception in projectrtp", e )
68
73
  }
69
74
  } )
70
75
  }
@@ -109,16 +114,48 @@ class rtpnodeconnection {
109
114
  /**
110
115
  * Send a message back to the main server, include stats to help with load balancing.
111
116
  * @param { object } msg
112
- * @returns { void }
117
+ * @param { function } [ cb ]
118
+ * @returns { Promise | undefined }
113
119
  */
114
120
  send( msg, cb = undefined ) {
121
+
122
+ let retval
123
+ if( !cb ) retval = new Promise( ( r ) => cb = r )
115
124
  this.parent._post( msg, ( modifiedmsg ) => {
116
- if( this._destroying ) return
117
- msg.status = this.parent.prtp.stats()
118
- msg.status.instance = instance
119
- this.connection.write( message.createmessage( modifiedmsg ) )
120
- if (cb !== undefined) cb(msg)
125
+ if( this._destroying ) {
126
+ return
127
+ }
128
+ modifiedmsg.status = this.parent.prtp.stats()
129
+ modifiedmsg.status.instance = instance
130
+
131
+ delete modifiedmsg.forcelocal
132
+ delete modifiedmsg.em
133
+ delete modifiedmsg.openchannel
134
+ delete modifiedmsg.close
135
+ delete modifiedmsg.mix
136
+ delete modifiedmsg.unmix
137
+ delete modifiedmsg.echo
138
+ delete modifiedmsg.play
139
+ delete modifiedmsg.record
140
+ delete modifiedmsg.direction
141
+ delete modifiedmsg.dtmf
142
+ delete modifiedmsg.remote
143
+
144
+ this.connection.write( message.createmessage( modifiedmsg ), () => {
145
+ cb( modifiedmsg )
146
+ } )
121
147
  } )
148
+
149
+ return retval
150
+ }
151
+
152
+ #cleanup() {
153
+ this.connectionlength -= 1
154
+
155
+ if( 0 == this.connectionlength && "listen" == this.mode ) {
156
+ this.parent.connections.delete( this.connectionid )
157
+ this.connection.destroy()
158
+ }
122
159
  }
123
160
 
124
161
 
@@ -131,25 +168,61 @@ class rtpnodeconnection {
131
168
  this.connectionlength += 1
132
169
  msg.forcelocal = true
133
170
 
134
- const chan = await this.parent.prtp.openchannel( msg, ( cookie ) => {
135
- // TODO: we might want to ensure the actual event has been written
136
- // to the server before cleaning up the channel on our side?
137
- this.send( { ...{ "id": chan.id, "uuid": chan.uuid }, ...cookie },
138
- ( cookie ) => {
139
- if ( "close" === cookie.action ) {
140
- this.connectionlength -= 1
141
- channels.delete( chan.uuid )
142
-
143
- if( 0 == this.connectionlength && "listen" == this.mode ) {
144
- this.parent.connections.delete( this.connectionid )
145
- this.connection.destroy()
171
+ const timerid = setTimeout( () => {
172
+ console.trace( "Timeout opening channel", msg )
173
+ process.exit(1)
174
+ }, 4000 )
175
+
176
+ let chan
177
+ try {
178
+ chan = await this.parent.prtp.openchannel( msg, ( cookie ) => {
179
+
180
+ if( !chan ) {
181
+ console.error( "Error opening channel - message before open", msg, cookie )
182
+ return
183
+ }
184
+
185
+ // TODO: we might want to ensure the actual event has been written
186
+ // to the server before cleaning up the channel on our side?
187
+ const tosend = { ...{ "id": chan.id, "uuid": chan.uuid }, ...cookie }
188
+
189
+ this.send( tosend,
190
+ () => {
191
+ if ( "close" === cookie.action ) {
192
+ if( !chan ) {
193
+ const closebforeopen = { error: "unknown error opening channel", cookie, msg }
194
+ this.send( closebforeopen, () => {
195
+ this.#cleanup()
196
+ } )
197
+ return
198
+ }
199
+ channels.delete( chan.uuid )
200
+ this.#cleanup()
146
201
  }
147
- }
148
- } )
149
- } )
202
+ } )
203
+ } )
204
+ } catch( e ) {
205
+ console.error( "Error opening channel", e )
206
+ const opentosend = { error: "unknown error opening channel", msg }
207
+ await this.send( opentosend )
208
+ this.#cleanup()
209
+ return false
210
+ }
211
+
212
+ if( !chan ) {
213
+ console.trace( "Error opening channel", msg )
214
+ const opentosend = { error: "unknown error opening channel", msg }
215
+ await this.send( opentosend )
216
+ this.#cleanup()
217
+ return false
218
+ }
219
+
150
220
  channels.set( chan.uuid, chan )
151
- this.send( { ...chan, ...{ "action": "open" } } )
152
221
 
222
+ clearTimeout( timerid )
223
+
224
+ const opentosend = { ...chan, ...{ "action": "open" } }
225
+ this.send( opentosend )
153
226
  return true
154
227
  }
155
228
 
package/lib/server.js CHANGED
@@ -177,6 +177,8 @@ class channel {
177
177
  */
178
178
  _bridge
179
179
 
180
+ #connected = false
181
+
180
182
  /**
181
183
  *
182
184
  */
@@ -262,12 +264,15 @@ class channel {
262
264
 
263
265
  newchannel.connection.sock.on( "connect", () => {
264
266
  newchannel._write( options )
267
+ newchannel.#connected = true
265
268
  if ( connectresolve ) connectresolve( newchannel )
266
269
  } )
267
270
 
268
271
  newchannel.opentimer = setTimeout( () => {
269
272
  if( newchannel.openreject ) {
270
- newchannel.openreject()
273
+ let addmesg = ""
274
+ if( newchannel.#connected ) addmesg = " - post connect"
275
+ newchannel.openreject( new Error( "timed out waiting for open" + addmesg ) )
271
276
  delete newchannel.openresolve
272
277
  delete newchannel.openreject
273
278
  delete newchannel.opentimer
@@ -298,7 +303,7 @@ class channel {
298
303
  newchannel.channels.forEach( chnl => {
299
304
  channels.delete( chnl.id )
300
305
  if( chnl.openreject ) {
301
- chnl.openreject()
306
+ chnl.openreject( new Error( "Connection closed whilst waiting for open" ) )
302
307
  clearTimeout( chnl.opentimer )
303
308
  delete chnl.openresolve
304
309
  delete chnl.openreject
@@ -325,11 +330,8 @@ class channel {
325
330
 
326
331
  const resolvepromise = new Promise( ( resolve, reject ) => {
327
332
 
328
- if ( 0 < nodes.size ) {
329
- const node = randomenode( options.nodeinstance )
330
- const request = JSON.parse( JSON.stringify( options ) )
331
- request.channel = "open"
332
- channel._createforlisten( request, node, cb, resolve, reject )
333
+ if( !this.connection ) {
334
+ reject( new Error( "No connection" ) )
333
335
  return
334
336
  }
335
337
 
@@ -345,7 +347,7 @@ class channel {
345
347
 
346
348
  newchannel.opentimer = setTimeout( () => {
347
349
  if( newchannel.openreject ) {
348
- newchannel.openreject()
350
+ newchannel.openreject( new Error( "timed out waiting for open" ) )
349
351
  delete newchannel.openresolve
350
352
  delete newchannel.openreject
351
353
  delete newchannel.opentimer
@@ -382,7 +384,7 @@ class channel {
382
384
 
383
385
  this.closetimer = setTimeout( () => {
384
386
  if( this.closereject ) {
385
- this.closereject()
387
+ this.closereject( new Error( "timed out waiting for close" ) )
386
388
  delete this.closereject
387
389
  delete this.closeresolve
388
390
  delete this.closetimer
@@ -396,7 +398,7 @@ class channel {
396
398
  } )
397
399
 
398
400
  if( this.openreject ) {
399
- this.openreject()
401
+ this.openreject( new Error( "Channel closed before open" ) )
400
402
  clearTimeout( this.opentimer )
401
403
  delete this.openresolve
402
404
  delete this.openreject
@@ -561,7 +563,8 @@ class channel {
561
563
  */
562
564
  _runclose( msg ) {
563
565
  if( this.openreject ) {
564
- this.openreject()
566
+ console.error( "Received close before open", msg )
567
+ this.openreject( new Error( "Received close before open" ) )
565
568
  clearTimeout( this.opentimer )
566
569
  delete this.openresolve
567
570
  delete this.openreject
@@ -591,6 +594,21 @@ class channel {
591
594
  }
592
595
  }
593
596
 
597
+ #haserror( msg ) {
598
+ if( !msg.error ) return false
599
+ console.error( msg )
600
+
601
+ if( this.openreject ) {
602
+ this.openreject( new Error( msg.error ) )
603
+ clearTimeout( this.opentimer )
604
+ delete this.openresolve
605
+ delete this.openreject
606
+ delete this.opentimer
607
+ }
608
+
609
+ return true
610
+ }
611
+
594
612
  /**
595
613
  * Called by our socket to pass data back up the chain.
596
614
  * @ignore
@@ -600,6 +618,7 @@ class channel {
600
618
  msg.timestamp = ( new Date ).getTime()
601
619
  this.history.push( msg )
602
620
  if( this._runopen( msg ) ) return
621
+ if( this.#haserror( msg ) ) return
603
622
 
604
623
  this.em.emit( "all", msg )
605
624
  this.em.emit( msg.action, msg )
@@ -722,7 +741,7 @@ class rtpserver {
722
741
 
723
742
  if( 0 === nodes.size && 0 === listeningnodes.length ) {
724
743
  console.error( "No available RTP nodes" )
725
- reject( "No available RTP nodes" )
744
+ reject( new Error( "No available RTP nodes" ) )
726
745
  }
727
746
 
728
747
  const request = JSON.parse( JSON.stringify( options ) )
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@babblevoice/projectrtp",
3
- "version": "2.5.28",
3
+ "version": "2.5.30",
4
4
  "description": "A scalable Node addon RTP server",
5
5
  "main": "index.js",
6
6
  "directories": {
@@ -347,7 +347,8 @@ void projectrtpchannel::doopen( void ) {
347
347
  boost::asio::ip::udp::v4(), this->port ), ec );
348
348
 
349
349
  if( ec ) {
350
- this->badsocketopen( "failed to bind rtp socket" );
350
+ auto err = std::string( "failed to bind rtp socket: " ) + std::to_string( this->port ) + std::string( ": " ) + ec.message();
351
+ this->badsocketopen( err.c_str() );
351
352
  return;
352
353
  }
353
354
 
@@ -362,7 +363,8 @@ void projectrtpchannel::doopen( void ) {
362
363
  boost::asio::ip::udp::v4(), this->port + 1 ), ec );
363
364
 
364
365
  if( ec ) {
365
- this->badsocketopen( "failed to bind rtcp socket" );
366
+ auto err = std::string( "failed to bind rtcp socket: " ) + std::to_string( this->port + 1 ) + std::string( ": " ) + ec.message();
367
+ this->badsocketopen( err.c_str() );
366
368
  return;
367
369
  }
368
370
 
@@ -2366,10 +2368,12 @@ void getchannelstats( napi_env env, napi_value &result ) {
2366
2368
  void initrtpchannel( napi_env env, napi_value &result, int32_t startport, int32_t endport ) {
2367
2369
  napi_value ccreate;
2368
2370
 
2371
+ AQUIRESPINLOCK( availableportslock );
2369
2372
  while(!availableports.empty()) availableports.pop();
2370
2373
  for( int i = (int) startport; i < (int) endport; i = i + 2 ) {
2371
2374
  availableports.push( i );
2372
2375
  }
2376
+ RELEASESPINLOCK( availableportslock );
2373
2377
 
2374
2378
  if( napi_ok != napi_create_function( env, "exports", NAPI_AUTO_LENGTH, channelcreate, nullptr, &ccreate ) ) return;
2375
2379
  if( napi_ok != napi_set_named_property( env, result, "openchannel", ccreate ) ) return;
@@ -312,8 +312,8 @@ describe( "record", function() {
312
312
 
313
313
  expect( channel.record( {
314
314
  "file": "/tmp/ourpowerrecording.wav",
315
- "startabovepower": 250,
316
- "finishbelowpower": 200,
315
+ "startabovepower": 40,
316
+ "finishbelowpower": 80,
317
317
  "minduration": 2000,
318
318
  "maxduration": 15000,
319
319
  "poweraveragepackets": 20
@@ -382,8 +382,8 @@ describe( "record", function() {
382
382
 
383
383
  expect( channel.record( {
384
384
  "file": "/tmp/ourtimeoutpowerrecording.wav",
385
- "startabovepower": 250,
386
- "finishbelowpower": 200,
385
+ "startabovepower": 40,
386
+ "finishbelowpower": 80,
387
387
  "minduration": 200,
388
388
  "maxduration": 500,
389
389
  "poweraveragepackets": 50
@@ -449,8 +449,8 @@ describe( "record", function() {
449
449
 
450
450
  expect( channel.record( {
451
451
  "file": "/tmp/dualrecordingpower.wav",
452
- "startabovepower": 200,
453
- "finishbelowpower": 180,
452
+ "startabovepower": 40,
453
+ "finishbelowpower": 200,
454
454
  "minduration": 200,
455
455
  "maxduration": 3500,
456
456
  "poweraveragepackets": 10 /* faster response */
@@ -470,7 +470,7 @@ describe( "record", function() {
470
470
  server.close()
471
471
 
472
472
  let stats = fs.statSync( "/tmp/dualrecordingpower.wav" )
473
- expect( stats.size ).to.be.within( 40000, 52000 )
473
+ expect( stats.size ).to.be.within( 30000 , 40000 )
474
474
 
475
475
  stats = fs.statSync( "/tmp/dualrecording.wav" )
476
476
  expect( stats.size ).to.be.within( 110000, 190000 )
@@ -4,9 +4,76 @@
4
4
  const expect = require( "chai" ).expect
5
5
  const projectrtp = require( "../../index.js" ).projectrtp
6
6
 
7
+ const fs = require( "node:fs" ).promises
8
+ const dgram = require( "dgram" )
9
+ const rtp = require( "../util/rtp" )
10
+
7
11
  describe( "tonegen", function() {
12
+
13
+ const silentfilename = "/tmp/silent.wav"
14
+
15
+ this.afterEach( async () => {
16
+ try{
17
+ await fs.unlink( silentfilename )
18
+ } catch( e ) { /* silent */ }
19
+
20
+ } )
21
+
8
22
  it( "tone.generate exists", async function() {
9
23
  expect( projectrtp.tone.generate ).to.be.an( "function" )
10
24
  } )
25
+
26
+ it( "generate silence and ensure looped is silent", async function() {
27
+
28
+ this.timeout( 4000 )
29
+ this.slow( 3000 )
30
+
31
+ /* Trying to replicate a bug where we use the silence in a tone file to send silence */
32
+ projectrtp.tone.generate( "350+440*0.5:1000", silentfilename )
33
+ projectrtp.tone.generate( "400+450*0.5/0/400+450*0.5/0:400/200/400/2000", silentfilename )
34
+ projectrtp.tone.generate( "697+1209*0.5/0/697+1336*0.5/0/697+1477*0.5/0/697+1633*0.5/0:400/100", silentfilename )
35
+
36
+ const server = dgram.createSocket( "udp4" )
37
+
38
+ let done
39
+ const complete = new Promise( resolve => done = resolve )
40
+ let recvcount = 0
41
+ let allaregood = true
42
+ server.on( "message", function( dgm ) {
43
+ // check
44
+ const parsed = rtp.parsepk( dgm )
45
+
46
+ recvcount++
47
+
48
+ allaregood &&= Array.from( parsed.payload ).every( val => 255 === val )
49
+
50
+ if( 100 < recvcount ) channel.close()
51
+ } )
52
+
53
+ server.bind()
54
+ await new Promise( resolve => server.on( "listening", resolve() ) )
55
+
56
+ const ourport = server.address().port
57
+
58
+ const chandef = { "remote": { "address": "localhost", "port": ourport, "codec": 0 } }
59
+ const channel = await projectrtp.openchannel( chandef, function( d ) {
60
+ if( "close" === d.action ) {
61
+ server.close()
62
+ done()
63
+ }
64
+ } )
65
+
66
+ const prompt = { "wav": silentfilename, "start": 2100, "stop": 3100, "loop": 3 } //silent
67
+ const soup = {
68
+ "files": [ prompt ], "interupt": true
69
+ }
70
+
71
+ channel.play( soup )
72
+
73
+ await complete
74
+
75
+ expect( allaregood ).to.be.true
76
+
77
+ } )
11
78
  } )
12
79