@babblevoice/projectrtp 2.5.26 → 2.5.29

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.23_beta4 . --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
@@ -64,7 +64,7 @@ class rtpnodeconnection {
64
64
  this._processmessage( modifiedmsg )
65
65
  } )
66
66
  } catch( e ) {
67
- console.error( "Unhandled exception in babble-rtp", e )
67
+ console.error( "Unhandled exception in projectrtp", e )
68
68
  }
69
69
  } )
70
70
  }
@@ -109,16 +109,48 @@ class rtpnodeconnection {
109
109
  /**
110
110
  * Send a message back to the main server, include stats to help with load balancing.
111
111
  * @param { object } msg
112
- * @returns { void }
112
+ * @param { function } [ cb ]
113
+ * @returns { Promise | undefined }
113
114
  */
114
115
  send( msg, cb = undefined ) {
116
+
117
+ let retval
118
+ if( !cb ) retval = new Promise( ( r ) => cb = r )
115
119
  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)
120
+ if( this._destroying ) {
121
+ return
122
+ }
123
+ modifiedmsg.status = this.parent.prtp.stats()
124
+ modifiedmsg.status.instance = instance
125
+
126
+ delete modifiedmsg.forcelocal
127
+ delete modifiedmsg.em
128
+ delete modifiedmsg.openchannel
129
+ delete modifiedmsg.close
130
+ delete modifiedmsg.mix
131
+ delete modifiedmsg.unmix
132
+ delete modifiedmsg.echo
133
+ delete modifiedmsg.play
134
+ delete modifiedmsg.record
135
+ delete modifiedmsg.direction
136
+ delete modifiedmsg.dtmf
137
+ delete modifiedmsg.remote
138
+
139
+ this.connection.write( message.createmessage( modifiedmsg ), () => {
140
+ cb( modifiedmsg )
141
+ } )
121
142
  } )
143
+
144
+ return retval
145
+ }
146
+
147
+ #cleanup() {
148
+ this.connectionlength -= 1
149
+
150
+ if( 0 == this.connectionlength && "listen" == this.mode ) {
151
+ this.parent.connections.delete( this.connectionid )
152
+ this.connection.destroy()
153
+ }
122
154
  }
123
155
 
124
156
 
@@ -131,25 +163,61 @@ class rtpnodeconnection {
131
163
  this.connectionlength += 1
132
164
  msg.forcelocal = true
133
165
 
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()
166
+ const timerid = setTimeout( () => {
167
+ console.trace( "Timeout opening channel", msg )
168
+ process.exit(1)
169
+ }, 4000 )
170
+
171
+ let chan
172
+ try {
173
+ chan = await this.parent.prtp.openchannel( msg, ( cookie ) => {
174
+
175
+ if( !chan ) {
176
+ console.error( "Error opening channel - message before open", msg, cookie )
177
+ return
178
+ }
179
+
180
+ // TODO: we might want to ensure the actual event has been written
181
+ // to the server before cleaning up the channel on our side?
182
+ const tosend = { ...{ "id": chan.id, "uuid": chan.uuid }, ...cookie }
183
+
184
+ this.send( tosend,
185
+ () => {
186
+ if ( "close" === cookie.action ) {
187
+ if( !chan ) {
188
+ const closebforeopen = { error: "unknown error opening channel", cookie, msg }
189
+ this.send( closebforeopen, () => {
190
+ this.#cleanup()
191
+ } )
192
+ return
193
+ }
194
+ channels.delete( chan.uuid )
195
+ this.#cleanup()
146
196
  }
147
- }
148
- } )
149
- } )
197
+ } )
198
+ } )
199
+ } catch( e ) {
200
+ console.error( "Error opening channel", e )
201
+ const opentosend = { error: "unknown error opening channel", msg }
202
+ await this.send( opentosend )
203
+ this.#cleanup()
204
+ return false
205
+ }
206
+
207
+ if( !chan ) {
208
+ console.trace( "Error opening channel", msg )
209
+ const opentosend = { error: "unknown error opening channel", msg }
210
+ await this.send( opentosend )
211
+ this.#cleanup()
212
+ return false
213
+ }
214
+
150
215
  channels.set( chan.uuid, chan )
151
- this.send( { ...chan, ...{ "action": "open" } } )
152
216
 
217
+ clearTimeout( timerid )
218
+
219
+ const opentosend = { ...chan, ...{ "action": "open" } }
220
+ this.send( opentosend )
153
221
  return true
154
222
  }
155
223
 
package/lib/server.js CHANGED
@@ -4,9 +4,10 @@ const net = require( "net" )
4
4
  const { v4: uuidv4 } = require( "uuid" )
5
5
 
6
6
  const EventEmitter = require( "events" )
7
-
8
7
  const message = require( "./message" )
9
8
 
9
+ const opentimeout = 5000
10
+
10
11
  /**
11
12
  * Node host address and port object
12
13
  * @typedef { object } nodehost
@@ -105,12 +106,36 @@ class channel {
105
106
  */
106
107
  openresolve
107
108
 
109
+ /**
110
+ * @private
111
+ * @type { function }
112
+ */
113
+ closeresolve
114
+
108
115
  /**
109
116
  * @private
110
117
  * @type { function }
111
118
  */
112
119
  openreject
113
120
 
121
+ /**
122
+ * @private
123
+ * @type { function }
124
+ */
125
+ closereject
126
+
127
+ /**
128
+ * @private
129
+ * @type { NodeJS.Timeout }
130
+ */
131
+ opentimer
132
+
133
+ /**
134
+ * @private
135
+ * @type { NodeJS.Timeout }
136
+ */
137
+ closetimer
138
+
114
139
  /**
115
140
  * Array of messages sent to and from the remote end.
116
141
  * @type { Array< any > }
@@ -152,6 +177,8 @@ class channel {
152
177
  */
153
178
  _bridge
154
179
 
180
+ #connected = false
181
+
155
182
  /**
156
183
  *
157
184
  */
@@ -206,14 +233,15 @@ class channel {
206
233
  }
207
234
 
208
235
  /**
209
- * Creates a new channel which connects to a remote rtp node.
236
+ * Connects to a RTP server then creates channel
210
237
  * @param { object } options
211
238
  * @param { channelcallback } cb
212
239
  * @param { function } openresolve
240
+ * @param { function } openreject
213
241
  * @returns { Promise< channel > } - resolves when the connection is connected
214
242
  * @internal
215
243
  */
216
- static _createforconnect( options, cb, openresolve /* openreject do something with me */ ) {
244
+ static _createforconnect( options, cb, openresolve, openreject ) {
217
245
 
218
246
  const newchannel = new channel( options.id )
219
247
  if( cb ) newchannel.em.on( "all", cb )
@@ -228,14 +256,29 @@ class channel {
228
256
  "type": nodeconnectiontype.connect
229
257
  }
230
258
 
259
+ newchannel.openresolve = openresolve
260
+ newchannel.openreject = openreject
261
+
231
262
  newchannel.connection.sock.setKeepAlive( true )
232
263
  newchannel.channels = [ newchannel ]
233
264
 
234
265
  newchannel.connection.sock.on( "connect", () => {
235
266
  newchannel._write( options )
267
+ newchannel.#connected = true
236
268
  if ( connectresolve ) connectresolve( newchannel )
237
269
  } )
238
270
 
271
+ newchannel.opentimer = setTimeout( () => {
272
+ if( newchannel.openreject ) {
273
+ let addmesg = ""
274
+ if( newchannel.#connected ) addmesg = " - post connect"
275
+ newchannel.openreject( new Error( "timed out waiting for open" + addmesg ) )
276
+ delete newchannel.openresolve
277
+ delete newchannel.openreject
278
+ delete newchannel.opentimer
279
+ }
280
+ }, opentimeout )
281
+
239
282
  const state = message.newstate()
240
283
  newchannel.connection.sock.on( "data", ( data ) => {
241
284
  message.parsemessage( state, data, ( /** @type { object } */ receivedmsg ) => {
@@ -248,8 +291,6 @@ class channel {
248
291
  if( receivedmsg && receivedmsg.status && receivedmsg.status.instance ) {
249
292
  newchannel.connection.instance = receivedmsg.status.instance
250
293
  }
251
-
252
- if ( openresolve ) openresolve( newchannel )
253
294
  } )
254
295
  } )
255
296
 
@@ -259,6 +300,16 @@ class channel {
259
300
  } )
260
301
 
261
302
  newchannel.connection.sock.on( "close", () => {
303
+ newchannel.channels.forEach( chnl => {
304
+ channels.delete( chnl.id )
305
+ if( chnl.openreject ) {
306
+ chnl.openreject( new Error( "Connection closed whilst waiting for open" ) )
307
+ clearTimeout( chnl.opentimer )
308
+ delete chnl.openresolve
309
+ delete chnl.openreject
310
+ delete chnl.opentimer
311
+ }
312
+ } )
262
313
  } )
263
314
  } )
264
315
  }
@@ -279,11 +330,8 @@ class channel {
279
330
 
280
331
  const resolvepromise = new Promise( ( resolve, reject ) => {
281
332
 
282
- if ( 0 < nodes.size ) {
283
- const node = randomenode( options.nodeinstance )
284
- const request = JSON.parse( JSON.stringify( options ) )
285
- request.channel = "open"
286
- channel._createforlisten( request, node, cb, resolve, reject )
333
+ if( !this.connection ) {
334
+ reject( new Error( "No connection" ) )
287
335
  return
288
336
  }
289
337
 
@@ -294,7 +342,17 @@ class channel {
294
342
  newchannel.channels = this.channels
295
343
  newchannel.channels.push( newchannel )
296
344
  newchannel.openresolve = resolve
345
+ newchannel.openreject = reject
297
346
  newchannel._write( options )
347
+
348
+ newchannel.opentimer = setTimeout( () => {
349
+ if( newchannel.openreject ) {
350
+ newchannel.openreject( new Error( "timed out waiting for open" ) )
351
+ delete newchannel.openresolve
352
+ delete newchannel.openreject
353
+ delete newchannel.opentimer
354
+ }
355
+ }, opentimeout )
298
356
  } )
299
357
 
300
358
  return resolvepromise
@@ -314,12 +372,24 @@ class channel {
314
372
  }
315
373
 
316
374
  /**
317
- * @summary Close the channel
375
+ * Closes the channel. It might throw a timeout error if the channel is not closed within 5 seconds.
376
+ * @param { object } [ options ]
377
+ * @param { number } [ options.timeout ] - different timeout to wait
378
+ * @returns { Promise< object > }
318
379
  */
319
- close() {
380
+ close( options ) {
320
381
 
321
- /* any outstanding to remove where unmix was not called first? */
322
- //this._removebridge()
382
+ let timeout = opentimeout
383
+ if( options && options.timeout ) timeout = options.timeout
384
+
385
+ this.closetimer = setTimeout( () => {
386
+ if( this.closereject ) {
387
+ this.closereject( new Error( "timed out waiting for close" ) )
388
+ delete this.closereject
389
+ delete this.closeresolve
390
+ delete this.closetimer
391
+ }
392
+ }, timeout )
323
393
 
324
394
  /* close us */
325
395
  this._write( {
@@ -327,11 +397,18 @@ class channel {
327
397
  "uuid": this.uuid
328
398
  } )
329
399
 
330
- if( this.openresolve ) {
331
- this.openresolve()
400
+ if( this.openreject ) {
401
+ this.openreject( new Error( "Channel closed before open" ) )
402
+ clearTimeout( this.opentimer )
332
403
  delete this.openresolve
333
404
  delete this.openreject
405
+ delete this.opentimer
334
406
  }
407
+
408
+ return new Promise( ( resolve, reject ) => {
409
+ this.closeresolve = resolve
410
+ this.closereject = reject
411
+ } )
335
412
  }
336
413
 
337
414
  /**
@@ -354,142 +431,6 @@ class channel {
354
431
  return true
355
432
  }
356
433
 
357
- /**
358
- * Add _bridge object to both channel
359
- * @param { object } other - other channel
360
- * @returns { void }
361
- */
362
- _createbridges( other ) {
363
-
364
- if( !this._bridge && !other._bridge ) {
365
- this._bridge = other._bridge = {
366
- "bridges": [],
367
- "main": this.connection.instance
368
- }
369
- }
370
-
371
- if( !this._bridge && other._bridge ) this._bridge = other._bridge
372
- if( this._bridge && !other._bridge ) other._bridge = this._bridge
373
-
374
- if( this._bridge !== other._bridge )
375
- throw new Error( "some complex mixing occured and this function needs finishing" )
376
- }
377
-
378
- /**
379
- * Any channels which have been mixed we create an
380
- * object common to both channels so we can keep track of channels which
381
- * require tidyin up. We try to maintain a star shaped network.
382
- * In the example diagram, the out mix method mixes between phone1 and this
383
- * and phone2 and other.
384
- * Markdown (mermaid)
385
- graph TD
386
- this -->|rtp - our created bridge to link the 2 remote channels| other
387
- other --> this
388
-
389
- phone1 --> this
390
- this --> phone1
391
-
392
- phone2 --> other
393
- other --> phone2
394
- * @private
395
- * @param { object } other - other channel
396
- * @returns { Promise< boolean > }
397
- */
398
- async _addbridge( other ) {
399
-
400
- let leftid = this.connection.instance
401
- const rightid = other.connection.instance
402
-
403
- this._createbridges( other )
404
-
405
- /* nothing more to do if we are on the same node */
406
- if( rightid === leftid ) return false
407
-
408
- /* It might have changed now we have linked them up */
409
- leftid = this._bridge.main
410
-
411
- let found = false
412
- for( const bridge of this._bridge.bridges ) {
413
- if( ( bridge.left.connection.instance === leftid || bridge.right.connection.instance === leftid ) &&
414
- ( bridge.left.connection.instance === rightid || bridge.right.connection.instance === rightid ) ) {
415
- found = true
416
- bridge.channels.add( this )
417
- bridge.channels.add( other )
418
-
419
- this.mix( bridge.left )
420
- other.mix( bridge.right )
421
- }
422
- }
423
-
424
- if( found ) return
425
-
426
- const bridge = {
427
- /* TODO: this will need reworking with to support reverse connection types */
428
- "left": await serverinterface.get().openchannel( { "nodeinstance": leftid } ),
429
- "right": await serverinterface.get().openchannel( { "nodeinstance": rightid } ),
430
- "channels": new Set( [ this, other ] )
431
- }
432
-
433
- const addressleft = bridge.left.local.privateaddress || bridge.left.local.address
434
- const addressright = bridge.right.local.privateaddress || bridge.right.local.address
435
- const g722 = 9
436
-
437
- bridge.left.remote( { "address": addressright, "port": bridge.right.local.port, "codec": g722 } )
438
- bridge.right.remote( { "address": addressleft, "port": bridge.left.local.port, "codec": g722 } )
439
-
440
- this._bridge.bridges.push( bridge )
441
-
442
- this._write( {
443
- "channel": "mix",
444
- "other": {
445
- "id": bridge.left.id,
446
- "uuid": bridge.left.uuid
447
- }
448
- } )
449
-
450
- other._write( {
451
- "channel": "mix",
452
- "other": {
453
- "id": bridge.right.id,
454
- "uuid": bridge.right.uuid
455
- }
456
- } )
457
-
458
- return true
459
- }
460
-
461
- /**
462
- * We check if we are a remote node (i.e.. not the centre of the star)
463
- * then we close down the link between the two.
464
- * @private
465
- * @returns { boolean }
466
- */
467
- _removebridge() {
468
-
469
- /* no remote bridges have been created */
470
- if( !this._bridge || !this._bridge.main ) return
471
-
472
- const usid = this.connection.instance
473
-
474
- /* The centre of star cannot remove from the network until last man standing */
475
- if( usid == this._bridge.main ) return
476
-
477
- this._bridge.bridges = this._bridge.bridges.filter( ( bridge ) => {
478
- bridge.channels.delete( this )
479
-
480
- if( 2 > bridge.channels.size ) {
481
-
482
- bridge.left._write( { "channel": "unmix" } )
483
- bridge.left._write( { "channel": "unmix" } )
484
-
485
- bridge.left.close()
486
- bridge.right.close()
487
- return false /* remove */
488
- }
489
- return true /* keep */
490
- } )
491
- }
492
-
493
434
  /**
494
435
  @summary Removes us from an existing mix
495
436
  @returns { boolean }
@@ -592,6 +533,7 @@ class channel {
592
533
  }
593
534
 
594
535
  /**
536
+ * We have received teh open confirm message so we should action it
595
537
  * @private
596
538
  * @param { object } msg
597
539
  * @returns { boolean } - is further processing required
@@ -605,33 +547,66 @@ class channel {
605
547
 
606
548
  if( !this.openresolve ) return true
607
549
  this.openresolve( this )
550
+ clearTimeout( this.opentimer )
608
551
  delete this.openresolve
609
552
  delete this.openreject
553
+ delete this.opentimer
610
554
 
611
555
  return true
612
556
  }
613
557
 
614
558
  /**
615
- *
559
+ * We have received a close message so we should action it
560
+ * @param { object } msg
616
561
  * @returns { void }
617
562
  * @private
618
563
  */
619
- _runclose() {
620
- if( undefined !== this.openresolve ) {
621
- this.openresolve()
564
+ _runclose( msg ) {
565
+ if( this.openreject ) {
566
+ console.error( "Received close before open", msg )
567
+ this.openreject( new Error( "Received close before open" ) )
568
+ clearTimeout( this.opentimer )
622
569
  delete this.openresolve
623
570
  delete this.openreject
571
+ delete this.opentimer
572
+ }
573
+
574
+ if( this.closeresolve ) {
575
+ this.closeresolve( msg )
576
+ clearTimeout( this.closetimer )
577
+ delete this.closeresolve
578
+ delete this.closereject
579
+ delete this.closetimer
624
580
  }
625
581
 
582
+ this.#cleanupsharedchannels()
583
+ channels.delete( this.id )
584
+ }
585
+
586
+ #cleanupsharedchannels() {
626
587
  if( this.channels ) {
627
588
  // adjust with filter
628
589
  const index = this.channels.indexOf( this )
629
590
  if( -1 < index ) this.channels.splice( index, 1 )
630
- if( 0 === this.channels.length && this.connection.sock ) this.connection.sock.destroy()
591
+ if( 0 === this.channels.length && this.connection.sock ) {
592
+ this.connection.sock.destroy()
593
+ }
631
594
  }
595
+ }
632
596
 
597
+ #haserror( msg ) {
598
+ if( !msg.error ) return false
599
+ console.error( msg )
633
600
 
634
- channels.delete( this.id )
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
635
610
  }
636
611
 
637
612
  /**
@@ -643,11 +618,12 @@ class channel {
643
618
  msg.timestamp = ( new Date ).getTime()
644
619
  this.history.push( msg )
645
620
  if( this._runopen( msg ) ) return
621
+ if( this.#haserror( msg ) ) return
646
622
 
647
623
  this.em.emit( "all", msg )
648
624
  this.em.emit( msg.action, msg )
649
625
 
650
- if( "close" == msg.action ) this._runclose()
626
+ if( "close" == msg.action ) this._runclose( msg )
651
627
  }
652
628
 
653
629
  /**
@@ -765,16 +741,16 @@ class rtpserver {
765
741
 
766
742
  if( 0 === nodes.size && 0 === listeningnodes.length ) {
767
743
  console.error( "No available RTP nodes" )
768
- reject( "No available RTP nodes" )
744
+ reject( new Error( "No available RTP nodes" ) )
769
745
  }
770
746
 
771
747
  const request = JSON.parse( JSON.stringify( options ) )
772
748
  request.channel = "open"
773
749
 
774
750
  const node = randomenode( options.nodeinstance )
775
-
751
+
776
752
  if ( 0 < nodes.size ) channel._createforlisten( request, node, callback, resolve, reject )
777
- else channel._createforconnect( request, callback, resolve )
753
+ else channel._createforconnect( request, callback, resolve, reject )
778
754
 
779
755
  } )
780
756
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@babblevoice/projectrtp",
3
- "version": "2.5.26",
3
+ "version": "2.5.29",
4
4
  "description": "A scalable Node addon RTP server",
5
5
  "main": "index.js",
6
6
  "directories": {