lox-airplay-sender 0.1.0 → 0.2.1

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.
@@ -241,24 +241,22 @@ function AirTunesDevice(host, audioOut, options, mode = 0, txt = '') {
241
241
  }
242
242
  Object.setPrototypeOf(AirTunesDevice.prototype, node_events_1.EventEmitter.prototype);
243
243
  AirTunesDevice.prototype.start = function () {
244
- var self = this;
245
244
  this.audioSocket = node_dgram_1.default.createSocket('udp4');
246
245
  // Wait until timing and control ports are chosen. We need them in RTSP handshake.
247
- this.udpServers.on('ports', function (err) {
246
+ this.udpServers.on('ports', (err) => {
248
247
  if (err) {
249
- self.logLine?.(err.code);
250
- self.status = 'stopped';
251
- self.emit('status', 'stopped');
252
- self.logLine?.('port issues');
253
- self.emit('error', 'udp_ports', err.code);
248
+ this.logLine?.(err.code);
249
+ this.status = 'stopped';
250
+ this.emit('status', 'stopped');
251
+ this.logLine?.('port issues');
252
+ this.emit('error', 'udp_ports', err.code);
254
253
  return;
255
254
  }
256
- self.doHandshake();
255
+ this.doHandshake();
257
256
  });
258
257
  this.udpServers.bind(this.host);
259
258
  };
260
259
  AirTunesDevice.prototype.doHandshake = function () {
261
- var self = this;
262
260
  try {
263
261
  if (this.rtsp == null) {
264
262
  try {
@@ -276,47 +274,50 @@ AirTunesDevice.prototype.doHandshake = function () {
276
274
  });
277
275
  }
278
276
  catch (e) {
279
- self.logLine?.(e);
277
+ this.logLine?.(e);
280
278
  }
281
279
  }
282
280
  if (!this.rtsp) {
283
281
  return;
284
282
  }
285
- this.rtsp.on('config', function (setup) {
286
- self.audioLatency = setup.audioLatency;
287
- self.requireEncryption = setup.requireEncryption;
288
- self.serverPort = setup.server_port;
289
- self.controlPort = setup.control_port;
290
- self.timingPort = setup.timing_port;
291
- self.credentials = setup.credentials;
283
+ this.rtsp.on('config', (setup) => {
284
+ this.audioLatency = setup.audioLatency;
285
+ this.requireEncryption = setup.requireEncryption;
286
+ this.serverPort = setup.server_port;
287
+ this.controlPort = setup.control_port;
288
+ this.timingPort = setup.timing_port;
289
+ this.credentials = setup.credentials;
292
290
  try {
293
- self.audioOut?.setLatencyFrames?.(self.audioLatency);
291
+ this.audioOut?.setLatencyFrames?.(this.audioLatency);
294
292
  }
295
293
  catch {
296
294
  /* ignore */
297
295
  }
298
296
  });
299
- this.rtsp.on('ready', function () {
300
- self.relayAudio();
297
+ this.rtsp.on('ready', () => {
298
+ this.status = 'playing';
299
+ this.emit('status', 'playing');
300
+ if (this.airplay2)
301
+ this.relayAudio();
301
302
  });
302
- this.rtsp.on('need_password', function () {
303
- self.emit('status', 'need_password');
303
+ this.rtsp.on('need_password', () => {
304
+ this.emit('status', 'need_password');
304
305
  });
305
- this.rtsp.on('pair_failed', function () {
306
- self.emit('status', 'pair_failed');
306
+ this.rtsp.on('pair_failed', () => {
307
+ this.emit('status', 'pair_failed');
307
308
  });
308
- this.rtsp.on('pair_success', function () {
309
- self.emit('status', 'pair_success');
309
+ this.rtsp.on('pair_success', () => {
310
+ this.emit('status', 'pair_success');
310
311
  });
311
- this.rtsp.on('end', function (err) {
312
- self.logLine?.(err);
313
- self.cleanup();
312
+ this.rtsp.on('end', (err) => {
313
+ this.logLine?.(err);
314
+ this.cleanup();
314
315
  if (err !== 'stopped')
315
- self.emit(err);
316
+ this.emit(err);
316
317
  });
317
318
  }
318
319
  catch (e) {
319
- self.logLine?.(e);
320
+ this.logLine?.(e);
320
321
  }
321
322
  // console.log(this.udpServers, this.host,this.port)
322
323
  if (!this.rtsp) {
@@ -325,18 +326,17 @@ AirTunesDevice.prototype.doHandshake = function () {
325
326
  this.rtsp.startHandshake(this.udpServers, this.host, this.port);
326
327
  };
327
328
  AirTunesDevice.prototype.relayAudio = function () {
328
- var self = this;
329
329
  this.status = 'ready';
330
330
  this.emit('status', 'ready');
331
- this.audioCallback = function (packet) {
332
- var airTunes = makeAirTunesPacket(packet, self.encoder, self.requireEncryption, self.alacEncoding, self.credentials, self.inputCodec);
331
+ this.audioCallback = (packet) => {
332
+ const airTunes = makeAirTunesPacket(packet, this.encoder, this.requireEncryption, this.alacEncoding, this.credentials, this.inputCodec);
333
333
  // if (self.credentials) {
334
334
  // airTunes = self.credentials.encrypt(airTunes)
335
335
  // }
336
- if (self.audioSocket == null) {
337
- self.audioSocket = node_dgram_1.default.createSocket('udp4');
336
+ if (this.audioSocket == null) {
337
+ this.audioSocket = node_dgram_1.default.createSocket('udp4');
338
338
  }
339
- self.audioSocket.send(airTunes, 0, airTunes.length, self.serverPort, self.host);
339
+ this.audioSocket.send(airTunes, 0, airTunes.length, this.serverPort, this.host);
340
340
  };
341
341
  // this.sendAirTunesPacket = function(airTunes) {
342
342
  // try{
package/dist/core/rtsp.js CHANGED
@@ -150,86 +150,111 @@ function Client(volume, password, audioOut, options) {
150
150
  this.hostip = null;
151
151
  this.homekitver = this.transient ? "4" : "3";
152
152
  this.metadataReady = false;
153
+ this.connectAttempts = 0;
153
154
  }
154
155
  util.inherits(Client, events.EventEmitter);
155
156
  exports.default = { Client };
156
157
  Client.prototype.startHandshake = function (udpServers, host, port) {
157
- var self = this;
158
158
  this.startTimeout();
159
159
  this.hostip = host;
160
160
  this.controlPort = udpServers.control.port;
161
161
  this.timingPort = udpServers.timing.port;
162
- this.socket = net.connect(port, host, async function () {
163
- self.clearTimeout();
164
- if (self.needPassword || self.needPin) {
165
- self.status = PAIR_PIN_START;
166
- self.sendNextRequest();
167
- self.startHeartBeat();
168
- }
169
- else {
170
- if (self.mode != 2) {
171
- if (self.debug)
172
- self.logLine?.("AUTH_SETUP", "nah");
173
- self.status = OPTIONS;
174
- self.sendNextRequest();
175
- self.startHeartBeat();
162
+ this.connectAttempts = 0;
163
+ const connect = () => {
164
+ const attempt = this.connectAttempts ?? 0;
165
+ this.socket = net.connect(port, host, async () => {
166
+ this.clearTimeout();
167
+ this.connectAttempts = 0;
168
+ if (this.needPassword || this.needPin) {
169
+ this.status = PAIR_PIN_START;
170
+ this.sendNextRequest();
171
+ this.startHeartBeat();
176
172
  }
177
173
  else {
178
- self.status = AUTH_SETUP;
179
- if (self.debug)
180
- self.logLine?.("AUTH_SETUP", "yah");
181
- self.sendNextRequest();
182
- self.startHeartBeat();
174
+ if (this.mode != 2) {
175
+ if (this.debug)
176
+ this.logLine?.("AUTH_SETUP", "nah");
177
+ this.status = OPTIONS;
178
+ this.sendNextRequest();
179
+ this.startHeartBeat();
180
+ }
181
+ else {
182
+ this.status = AUTH_SETUP;
183
+ if (this.debug)
184
+ this.logLine?.("AUTH_SETUP", "yah");
185
+ this.sendNextRequest();
186
+ this.startHeartBeat();
187
+ }
183
188
  }
184
- }
185
- });
186
- var blob = '';
187
- this.socket.on('data', function (data) {
188
- if (self.encryptedChannel && self.credentials) {
189
- // if (self.debug != false) self.logLine?.("incoming", data)
190
- data = self.credentials.decrypt(data);
191
- }
192
- self.clearTimeout();
193
- /*
194
- * I wish I could use node's HTTP parser for this...
195
- * I assume that all responses have empty bodies.
196
- */
197
- var rawData = data;
198
- const dataStr = data.toString();
199
- blob += dataStr;
200
- var endIndex = blob.indexOf('\r\n\r\n');
201
- if (endIndex < 0) {
202
- return;
203
- }
204
- endIndex += 4;
205
- blob = blob.substring(0, endIndex);
206
- self.processData(blob, rawData);
207
- blob = dataStr.substring(endIndex);
208
- });
209
- this.socket.on('error', function (err) {
210
- self.socket = null;
211
- if (self.debug)
212
- self.logLine?.(err.code);
213
- if (err.code === 'ECONNREFUSED') {
214
- if (self.debug)
215
- self.logLine?.('block');
216
- self.cleanup('connection_refused');
217
- }
218
- else
219
- self.cleanup('rtsp_socket', err.code);
220
- });
221
- this.socket.on('end', function () {
222
- if (self.debug)
223
- self.logLine?.('block2');
224
- self.cleanup('disconnected');
225
- });
189
+ });
190
+ let blob = '';
191
+ this.socket.on('data', (data) => {
192
+ if (this.encryptedChannel && this.credentials) {
193
+ // if (this.debug != false) this.logLine?.("incoming", data)
194
+ data = this.credentials.decrypt(data);
195
+ }
196
+ this.clearTimeout();
197
+ /*
198
+ * I wish I could use node's HTTP parser for this...
199
+ * I assume that all responses have empty bodies.
200
+ */
201
+ const rawData = data;
202
+ const dataStr = data.toString();
203
+ blob += dataStr;
204
+ let endIndex = blob.indexOf('\r\n\r\n');
205
+ if (endIndex < 0) {
206
+ return;
207
+ }
208
+ endIndex += 4;
209
+ blob = blob.substring(0, endIndex);
210
+ this.processData(blob, rawData);
211
+ blob = dataStr.substring(endIndex);
212
+ });
213
+ this.socket.on('error', (err) => {
214
+ this.socket = null;
215
+ this.clearTimeout();
216
+ const nextAttempt = (this.connectAttempts ?? 0) + 1;
217
+ this.connectAttempts = nextAttempt;
218
+ const shouldRetry = nextAttempt <= config.rtsp_retry_attempts;
219
+ if (shouldRetry) {
220
+ const baseBackOff = Math.min(config.rtsp_retry_base_ms * Math.pow(2, nextAttempt - 1), config.rtsp_retry_max_ms);
221
+ const jitter = Math.random() * config.rtsp_retry_jitter_ms;
222
+ const backOff = baseBackOff + jitter;
223
+ if (this.debug)
224
+ this.logLine?.('rtsp_retry', { attempt: nextAttempt, backOff, code: err?.code });
225
+ setTimeout(() => {
226
+ this.startTimeout();
227
+ connect();
228
+ }, backOff);
229
+ return;
230
+ }
231
+ if (this.debug)
232
+ this.logLine?.(err?.code);
233
+ if (err?.code === 'ECONNREFUSED') {
234
+ if (this.debug)
235
+ this.logLine?.('block');
236
+ this.cleanup('connection_refused');
237
+ }
238
+ else
239
+ this.cleanup('rtsp_socket', err?.code);
240
+ });
241
+ this.socket.on('end', () => {
242
+ if (this.debug)
243
+ this.logLine?.('block2');
244
+ this.cleanup('disconnected');
245
+ });
246
+ };
247
+ connect();
226
248
  };
227
249
  Client.prototype.startTimeout = function () {
228
- var self = this;
229
- this.timeout = setTimeout(function () {
230
- if (self.debug)
231
- self.logLine?.('timeout');
232
- self.cleanup('timeout');
250
+ if (this.timeout) {
251
+ clearTimeout(this.timeout);
252
+ this.timeout = null;
253
+ }
254
+ this.timeout = setTimeout(() => {
255
+ if (this.debug)
256
+ this.logLine?.('timeout');
257
+ this.cleanup('timeout');
233
258
  }, config.rtsp_timeout);
234
259
  };
235
260
  Client.prototype.clearTimeout = function () {
@@ -280,10 +305,13 @@ Client.prototype.setPasscode = async function (passcode) {
280
305
  this.sendNextRequest();
281
306
  };
282
307
  Client.prototype.startHeartBeat = function () {
283
- var self = this;
308
+ if (this.heartBeat) {
309
+ clearInterval(this.heartBeat);
310
+ this.heartBeat = null;
311
+ }
284
312
  if (config.rtsp_heartbeat > 0) {
285
- this.heartBeat = setInterval(function () {
286
- self.sendHeartBeat(function () {
313
+ this.heartBeat = setInterval(() => {
314
+ this.sendHeartBeat(() => {
287
315
  //this.logLine?.('HeartBeat sent!');
288
316
  });
289
317
  }, config.rtsp_heartbeat);
@@ -319,7 +347,6 @@ Client.prototype.setArtwork = function (art, contentType, callback) {
319
347
  contentType = null;
320
348
  }
321
349
  if (typeof art == 'string') {
322
- var self = this;
323
350
  if (contentType === null) {
324
351
  var ext = art.slice(-4);
325
352
  if (ext == ".jpg" || ext == "jpeg") {
@@ -332,14 +359,14 @@ Client.prototype.setArtwork = function (art, contentType, callback) {
332
359
  contentType = "image/gif";
333
360
  }
334
361
  else {
335
- return self.cleanup('unknown_art_file_ext');
362
+ return this.cleanup('unknown_art_file_ext');
336
363
  }
337
364
  }
338
- return fs.readFile(art, function (err, data) {
365
+ return fs.readFile(art, (err, data) => {
339
366
  if (err !== null) {
340
- return self.cleanup('invalid_art_file');
367
+ return this.cleanup('invalid_art_file');
341
368
  }
342
- self.setArtwork(data, contentType, callback);
369
+ this.setArtwork(data, contentType, callback);
343
370
  });
344
371
  }
345
372
  if (contentType === null)
@@ -388,6 +415,44 @@ Client.prototype.cleanup = function (type, msg) {
388
415
  this.socket.destroy();
389
416
  this.socket = null;
390
417
  }
418
+ if (this.eventsocket) {
419
+ try {
420
+ this.eventsocket.destroy?.();
421
+ this.eventsocket = null;
422
+ }
423
+ catch {
424
+ /* ignore */
425
+ }
426
+ }
427
+ if (this.controlsocket) {
428
+ try {
429
+ this.controlsocket.close();
430
+ }
431
+ catch {
432
+ /* ignore */
433
+ }
434
+ this.controlsocket = null;
435
+ }
436
+ if (this.timingsocket) {
437
+ try {
438
+ this.timingsocket.close();
439
+ }
440
+ catch {
441
+ /* ignore */
442
+ }
443
+ this.timingsocket = null;
444
+ }
445
+ const audioSocket = this.audioSocket;
446
+ if (audioSocket) {
447
+ try {
448
+ audioSocket.close?.();
449
+ audioSocket.destroy?.();
450
+ }
451
+ catch {
452
+ /* ignore */
453
+ }
454
+ this.audioSocket = null;
455
+ }
391
456
  };
392
457
  function parseResponse(blob) {
393
458
  var response = {}, lines = blob.split('\r\n');
@@ -531,10 +596,8 @@ Client.prototype.sendNextRequest = async function (di) {
531
596
  this.socket.write(Buffer.from(request, 'utf-8'));
532
597
  }
533
598
  else {
534
- this.logLine?.("pass", this.password);
535
599
  if (this.password) {
536
600
  this.status = this.airplay2 ? PAIR_SETUP_1 : PAIR_PIN_SETUP_1;
537
- this.logLine?.("pass2", this.password);
538
601
  this.sendNextRequest();
539
602
  }
540
603
  else {
@@ -591,8 +654,6 @@ Client.prototype.sendNextRequest = async function (di) {
591
654
  request += this.makeHead("POST", "/pair-verify", "", true);
592
655
  request += 'Content-Type: application/octet-stream\r\n';
593
656
  this.pair_verify_1_verifier = ATVAuthenticator.verifier(this.authSecret);
594
- if (this.debug)
595
- this.logLine?.(this.authSecret);
596
657
  request += 'Content-Length:' + Buffer.byteLength(this.pair_verify_1_verifier.verifierBody) + '\r\n\r\n';
597
658
  this.socket.write(Buffer.concat([Buffer.from(request, 'utf-8'), this.pair_verify_1_verifier.verifierBody]));
598
659
  request = '';
@@ -846,8 +907,7 @@ Client.prototype.sendNextRequest = async function (di) {
846
907
  catch (e) { }
847
908
  try {
848
909
  this.timingsocket = dgram.createSocket({ type: 'udp4', reuseAddr: true });
849
- var self = this;
850
- this.timingsocket.on('message', function (msg, rinfo) {
910
+ this.timingsocket.on('message', (msg, rinfo) => {
851
911
  // only listen and respond on own hosts
852
912
  // if (this.hosts.indexOf(rinfo.address) < 0) return;
853
913
  var ts1 = msg.readUInt32BE(24);
@@ -861,8 +921,8 @@ Client.prototype.sendNextRequest = async function (di) {
861
921
  var ntpTime = ntp.timestamp();
862
922
  ntpTime.copy(reply, 16);
863
923
  ntpTime.copy(reply, 24);
864
- self.timingsocket.send(reply, 0, reply.length, rinfo.port, rinfo.address);
865
- self.logLine?.('timing socket pinged', rinfo.port, rinfo.address);
924
+ this.timingsocket.send(reply, 0, reply.length, rinfo.port, rinfo.address);
925
+ this.logLine?.('timing socket pinged', rinfo.port, rinfo.address);
866
926
  });
867
927
  this.timingsocket.bind(this.timingPort, this.socket.address().address);
868
928
  }
@@ -916,9 +976,8 @@ Client.prototype.sendNextRequest = async function (di) {
916
976
  }] });
917
977
  request += 'Content-Length: ' + Buffer.byteLength(setap2) + '\r\n\r\n';
918
978
  this.controlsocket = dgram.createSocket({ type: 'udp4', reuseAddr: true });
919
- var self = this;
920
- this.controlsocket.on('message', function (msg, rinfo) {
921
- self.logLine?.('controlsocket.data', msg);
979
+ this.controlsocket.on('message', (msg) => {
980
+ this.logLine?.('controlsocket.data', msg);
922
981
  });
923
982
  this.controlsocket.bind(this.controlPort, this.socket.address().address);
924
983
  let s2ct = this.credentials.encrypt(Buffer.concat([Buffer.from(request, 'utf-8'), setap2]));
@@ -928,18 +987,29 @@ Client.prototype.sendNextRequest = async function (di) {
928
987
  case RECORD:
929
988
  //this.logLine?.(request);
930
989
  if (this.airplay2) {
931
- this.eventsocket = net.connect(this.eventPort, this.hostip, async function () {
932
- });
933
- this.eventsocket.on('data', function (data) {
934
- self.logLine?.('eventsocket.data', data);
935
- self.logLine?.('eventsocket.data2', self.event_credentials.decrypt(data).toString());
936
- });
937
- this.eventsocket.on('error', function (err) {
938
- self.logLine?.('eventsocket.error', err);
939
- });
940
990
  this.event_credentials = new Credentials("sdsds", "", "", "", this.seed);
941
991
  this.event_credentials.writeKey = enc.HKDF("sha512", Buffer.from("Events-Salt"), this.srp.computeK(), Buffer.from("Events-Read-Encryption-Key"), 32);
942
992
  this.event_credentials.readKey = enc.HKDF("sha512", Buffer.from("Events-Salt"), this.srp.computeK(), Buffer.from("Events-Write-Encryption-Key"), 32);
993
+ this.eventsocket = net.connect(this.eventPort, this.hostip, async () => {
994
+ });
995
+ this.eventsocket.on('data', (data) => {
996
+ if (this.debug) {
997
+ this.logLine?.('eventsocket.data', data);
998
+ try {
999
+ const decrypted = this.event_credentials?.decrypt(data);
1000
+ if (decrypted) {
1001
+ this.logLine?.('eventsocket.data2', decrypted.toString());
1002
+ }
1003
+ }
1004
+ catch (err) {
1005
+ this.logLine?.('eventsocket.decrypt.error', err);
1006
+ }
1007
+ }
1008
+ });
1009
+ this.eventsocket.on('error', (err) => {
1010
+ if (this.debug)
1011
+ this.logLine?.('eventsocket.error', err);
1012
+ });
943
1013
  }
944
1014
  if (this.airplay2 != null && this.credentials != null) {
945
1015
  // this.controlsocket.close();
@@ -1117,19 +1187,19 @@ function parseAuthenticate(auth, field) {
1117
1187
  Client.prototype.processData = function (blob, rawData) {
1118
1188
  this.logLine?.('Receiving request:', this.hostip, rtsp_methods[this.status + 1]);
1119
1189
  var response = parseResponse2(blob, this), headers = response.headers || {};
1120
- if (this.debug != false) {
1121
- if ((rawData.toString()).includes("bplist00")) {
1122
- try {
1123
- let buf = Buffer.from(rawData).slice(rawData.length - parseInt(headers['Content-Length']), rawData.length);
1124
- let bplist = bplistParser.parseBuffer(buf);
1125
- this.logLine?.("incoming-res: \r\n", JSON.stringify(bplist));
1190
+ if (this.debug) {
1191
+ try {
1192
+ if ((rawData.toString()).includes("bplist00")) {
1193
+ const buf = Buffer.from(rawData).slice(rawData.length - parseInt(headers['Content-Length']), rawData.length);
1194
+ const bplist = bplistParser.parseBuffer(buf);
1195
+ this.logLine?.("incoming-res:", JSON.stringify(bplist));
1126
1196
  }
1127
- catch (_) {
1128
- this.logLine?.("incoming-res: \r\n", rawData.toString());
1197
+ else {
1198
+ this.logLine?.("incoming-res:", { code: response.code, length: rawData.length });
1129
1199
  }
1130
1200
  }
1131
- else {
1132
- this.logLine?.("incoming-res: \r\n", rawData.toString());
1201
+ catch {
1202
+ this.logLine?.("incoming-res:", { code: response.code, length: rawData.length });
1133
1203
  }
1134
1204
  }
1135
1205
  if (this.status != OPTIONS && this.status != OPTIONS2 && this.mode == 0) {
@@ -1363,7 +1433,6 @@ Client.prototype.processData = function (blob, rawData) {
1363
1433
  this.credentials.writeKey = enc.HKDF("sha512", Buffer.from("Control-Salt"), this.srp.computeK(), Buffer.from("Control-Write-Encryption-Key"), 32);
1364
1434
  this.credentials.readKey = enc.HKDF("sha512", Buffer.from("Control-Salt"), this.srp.computeK(), Buffer.from("Control-Read-Encryption-Key"), 32);
1365
1435
  this.encryptedChannel = true;
1366
- this.logLine?.(this.srp.computeK());
1367
1436
  this.status = SETUP_AP2_1;
1368
1437
  }
1369
1438
  else {
@@ -143,7 +143,11 @@ class UDPServers extends node_events_1.EventEmitter {
143
143
  const ntpTime = ntp_1.default.timestamp();
144
144
  ntpTime.copy(packet, 8);
145
145
  packet.writeUInt32BE((0, numUtil_1.low32)(seq * config_1.default.frames_per_packet + config_1.default.sampling_rate * 2), 16);
146
- this.control.socket.send(packet, 0, packet.length, dev.controlPort, dev.host);
146
+ const delay = Math.max(0, config_1.default.control_sync_base_delay_ms +
147
+ Math.random() * config_1.default.control_sync_jitter_ms);
148
+ setTimeout(() => {
149
+ this.control.socket?.send(packet, 0, packet.length, dev.controlPort, dev.host);
150
+ }, delay);
147
151
  }
148
152
  }
149
153
  exports.default = UDPServers;
@@ -241,24 +241,22 @@ function AirTunesDevice(host, audioOut, options, mode = 0, txt = '') {
241
241
  }
242
242
  Object.setPrototypeOf(AirTunesDevice.prototype, node_events_1.EventEmitter.prototype);
243
243
  AirTunesDevice.prototype.start = function () {
244
- var self = this;
245
244
  this.audioSocket = node_dgram_1.default.createSocket('udp4');
246
245
  // Wait until timing and control ports are chosen. We need them in RTSP handshake.
247
- this.udpServers.on('ports', function (err) {
246
+ this.udpServers.on('ports', (err) => {
248
247
  if (err) {
249
- self.logLine?.(err.code);
250
- self.status = 'stopped';
251
- self.emit('status', 'stopped');
252
- self.logLine?.('port issues');
253
- self.emit('error', 'udp_ports', err.code);
248
+ this.logLine?.(err.code);
249
+ this.status = 'stopped';
250
+ this.emit('status', 'stopped');
251
+ this.logLine?.('port issues');
252
+ this.emit('error', 'udp_ports', err.code);
254
253
  return;
255
254
  }
256
- self.doHandshake();
255
+ this.doHandshake();
257
256
  });
258
257
  this.udpServers.bind(this.host);
259
258
  };
260
259
  AirTunesDevice.prototype.doHandshake = function () {
261
- var self = this;
262
260
  try {
263
261
  if (this.rtsp == null) {
264
262
  try {
@@ -276,47 +274,50 @@ AirTunesDevice.prototype.doHandshake = function () {
276
274
  });
277
275
  }
278
276
  catch (e) {
279
- self.logLine?.(e);
277
+ this.logLine?.(e);
280
278
  }
281
279
  }
282
280
  if (!this.rtsp) {
283
281
  return;
284
282
  }
285
- this.rtsp.on('config', function (setup) {
286
- self.audioLatency = setup.audioLatency;
287
- self.requireEncryption = setup.requireEncryption;
288
- self.serverPort = setup.server_port;
289
- self.controlPort = setup.control_port;
290
- self.timingPort = setup.timing_port;
291
- self.credentials = setup.credentials;
283
+ this.rtsp.on('config', (setup) => {
284
+ this.audioLatency = setup.audioLatency;
285
+ this.requireEncryption = setup.requireEncryption;
286
+ this.serverPort = setup.server_port;
287
+ this.controlPort = setup.control_port;
288
+ this.timingPort = setup.timing_port;
289
+ this.credentials = setup.credentials;
292
290
  try {
293
- self.audioOut?.setLatencyFrames?.(self.audioLatency);
291
+ this.audioOut?.setLatencyFrames?.(this.audioLatency);
294
292
  }
295
293
  catch {
296
294
  /* ignore */
297
295
  }
298
296
  });
299
- this.rtsp.on('ready', function () {
300
- self.relayAudio();
297
+ this.rtsp.on('ready', () => {
298
+ this.status = 'playing';
299
+ this.emit('status', 'playing');
300
+ if (this.airplay2)
301
+ this.relayAudio();
301
302
  });
302
- this.rtsp.on('need_password', function () {
303
- self.emit('status', 'need_password');
303
+ this.rtsp.on('need_password', () => {
304
+ this.emit('status', 'need_password');
304
305
  });
305
- this.rtsp.on('pair_failed', function () {
306
- self.emit('status', 'pair_failed');
306
+ this.rtsp.on('pair_failed', () => {
307
+ this.emit('status', 'pair_failed');
307
308
  });
308
- this.rtsp.on('pair_success', function () {
309
- self.emit('status', 'pair_success');
309
+ this.rtsp.on('pair_success', () => {
310
+ this.emit('status', 'pair_success');
310
311
  });
311
- this.rtsp.on('end', function (err) {
312
- self.logLine?.(err);
313
- self.cleanup();
312
+ this.rtsp.on('end', (err) => {
313
+ this.logLine?.(err);
314
+ this.cleanup();
314
315
  if (err !== 'stopped')
315
- self.emit(err);
316
+ this.emit(err);
316
317
  });
317
318
  }
318
319
  catch (e) {
319
- self.logLine?.(e);
320
+ this.logLine?.(e);
320
321
  }
321
322
  // console.log(this.udpServers, this.host,this.port)
322
323
  if (!this.rtsp) {
@@ -325,18 +326,17 @@ AirTunesDevice.prototype.doHandshake = function () {
325
326
  this.rtsp.startHandshake(this.udpServers, this.host, this.port);
326
327
  };
327
328
  AirTunesDevice.prototype.relayAudio = function () {
328
- var self = this;
329
329
  this.status = 'ready';
330
330
  this.emit('status', 'ready');
331
- this.audioCallback = function (packet) {
332
- var airTunes = makeAirTunesPacket(packet, self.encoder, self.requireEncryption, self.alacEncoding, self.credentials, self.inputCodec);
331
+ this.audioCallback = (packet) => {
332
+ const airTunes = makeAirTunesPacket(packet, this.encoder, this.requireEncryption, this.alacEncoding, this.credentials, this.inputCodec);
333
333
  // if (self.credentials) {
334
334
  // airTunes = self.credentials.encrypt(airTunes)
335
335
  // }
336
- if (self.audioSocket == null) {
337
- self.audioSocket = node_dgram_1.default.createSocket('udp4');
336
+ if (this.audioSocket == null) {
337
+ this.audioSocket = node_dgram_1.default.createSocket('udp4');
338
338
  }
339
- self.audioSocket.send(airTunes, 0, airTunes.length, self.serverPort, self.host);
339
+ this.audioSocket.send(airTunes, 0, airTunes.length, this.serverPort, this.host);
340
340
  };
341
341
  // this.sendAirTunesPacket = function(airTunes) {
342
342
  // try{
@@ -150,86 +150,111 @@ function Client(volume, password, audioOut, options) {
150
150
  this.hostip = null;
151
151
  this.homekitver = this.transient ? "4" : "3";
152
152
  this.metadataReady = false;
153
+ this.connectAttempts = 0;
153
154
  }
154
155
  util.inherits(Client, events.EventEmitter);
155
156
  exports.default = { Client };
156
157
  Client.prototype.startHandshake = function (udpServers, host, port) {
157
- var self = this;
158
158
  this.startTimeout();
159
159
  this.hostip = host;
160
160
  this.controlPort = udpServers.control.port;
161
161
  this.timingPort = udpServers.timing.port;
162
- this.socket = net.connect(port, host, async function () {
163
- self.clearTimeout();
164
- if (self.needPassword || self.needPin) {
165
- self.status = PAIR_PIN_START;
166
- self.sendNextRequest();
167
- self.startHeartBeat();
168
- }
169
- else {
170
- if (self.mode != 2) {
171
- if (self.debug)
172
- self.logLine?.("AUTH_SETUP", "nah");
173
- self.status = OPTIONS;
174
- self.sendNextRequest();
175
- self.startHeartBeat();
162
+ this.connectAttempts = 0;
163
+ const connect = () => {
164
+ const attempt = this.connectAttempts ?? 0;
165
+ this.socket = net.connect(port, host, async () => {
166
+ this.clearTimeout();
167
+ this.connectAttempts = 0;
168
+ if (this.needPassword || this.needPin) {
169
+ this.status = PAIR_PIN_START;
170
+ this.sendNextRequest();
171
+ this.startHeartBeat();
176
172
  }
177
173
  else {
178
- self.status = AUTH_SETUP;
179
- if (self.debug)
180
- self.logLine?.("AUTH_SETUP", "yah");
181
- self.sendNextRequest();
182
- self.startHeartBeat();
174
+ if (this.mode != 2) {
175
+ if (this.debug)
176
+ this.logLine?.("AUTH_SETUP", "nah");
177
+ this.status = OPTIONS;
178
+ this.sendNextRequest();
179
+ this.startHeartBeat();
180
+ }
181
+ else {
182
+ this.status = AUTH_SETUP;
183
+ if (this.debug)
184
+ this.logLine?.("AUTH_SETUP", "yah");
185
+ this.sendNextRequest();
186
+ this.startHeartBeat();
187
+ }
183
188
  }
184
- }
185
- });
186
- var blob = '';
187
- this.socket.on('data', function (data) {
188
- if (self.encryptedChannel && self.credentials) {
189
- // if (self.debug != false) self.logLine?.("incoming", data)
190
- data = self.credentials.decrypt(data);
191
- }
192
- self.clearTimeout();
193
- /*
194
- * I wish I could use node's HTTP parser for this...
195
- * I assume that all responses have empty bodies.
196
- */
197
- var rawData = data;
198
- const dataStr = data.toString();
199
- blob += dataStr;
200
- var endIndex = blob.indexOf('\r\n\r\n');
201
- if (endIndex < 0) {
202
- return;
203
- }
204
- endIndex += 4;
205
- blob = blob.substring(0, endIndex);
206
- self.processData(blob, rawData);
207
- blob = dataStr.substring(endIndex);
208
- });
209
- this.socket.on('error', function (err) {
210
- self.socket = null;
211
- if (self.debug)
212
- self.logLine?.(err.code);
213
- if (err.code === 'ECONNREFUSED') {
214
- if (self.debug)
215
- self.logLine?.('block');
216
- self.cleanup('connection_refused');
217
- }
218
- else
219
- self.cleanup('rtsp_socket', err.code);
220
- });
221
- this.socket.on('end', function () {
222
- if (self.debug)
223
- self.logLine?.('block2');
224
- self.cleanup('disconnected');
225
- });
189
+ });
190
+ let blob = '';
191
+ this.socket.on('data', (data) => {
192
+ if (this.encryptedChannel && this.credentials) {
193
+ // if (this.debug != false) this.logLine?.("incoming", data)
194
+ data = this.credentials.decrypt(data);
195
+ }
196
+ this.clearTimeout();
197
+ /*
198
+ * I wish I could use node's HTTP parser for this...
199
+ * I assume that all responses have empty bodies.
200
+ */
201
+ const rawData = data;
202
+ const dataStr = data.toString();
203
+ blob += dataStr;
204
+ let endIndex = blob.indexOf('\r\n\r\n');
205
+ if (endIndex < 0) {
206
+ return;
207
+ }
208
+ endIndex += 4;
209
+ blob = blob.substring(0, endIndex);
210
+ this.processData(blob, rawData);
211
+ blob = dataStr.substring(endIndex);
212
+ });
213
+ this.socket.on('error', (err) => {
214
+ this.socket = null;
215
+ this.clearTimeout();
216
+ const nextAttempt = (this.connectAttempts ?? 0) + 1;
217
+ this.connectAttempts = nextAttempt;
218
+ const shouldRetry = nextAttempt <= config.rtsp_retry_attempts;
219
+ if (shouldRetry) {
220
+ const baseBackOff = Math.min(config.rtsp_retry_base_ms * Math.pow(2, nextAttempt - 1), config.rtsp_retry_max_ms);
221
+ const jitter = Math.random() * config.rtsp_retry_jitter_ms;
222
+ const backOff = baseBackOff + jitter;
223
+ if (this.debug)
224
+ this.logLine?.('rtsp_retry', { attempt: nextAttempt, backOff, code: err?.code });
225
+ setTimeout(() => {
226
+ this.startTimeout();
227
+ connect();
228
+ }, backOff);
229
+ return;
230
+ }
231
+ if (this.debug)
232
+ this.logLine?.(err?.code);
233
+ if (err?.code === 'ECONNREFUSED') {
234
+ if (this.debug)
235
+ this.logLine?.('block');
236
+ this.cleanup('connection_refused');
237
+ }
238
+ else
239
+ this.cleanup('rtsp_socket', err?.code);
240
+ });
241
+ this.socket.on('end', () => {
242
+ if (this.debug)
243
+ this.logLine?.('block2');
244
+ this.cleanup('disconnected');
245
+ });
246
+ };
247
+ connect();
226
248
  };
227
249
  Client.prototype.startTimeout = function () {
228
- var self = this;
229
- this.timeout = setTimeout(function () {
230
- if (self.debug)
231
- self.logLine?.('timeout');
232
- self.cleanup('timeout');
250
+ if (this.timeout) {
251
+ clearTimeout(this.timeout);
252
+ this.timeout = null;
253
+ }
254
+ this.timeout = setTimeout(() => {
255
+ if (this.debug)
256
+ this.logLine?.('timeout');
257
+ this.cleanup('timeout');
233
258
  }, config.rtsp_timeout);
234
259
  };
235
260
  Client.prototype.clearTimeout = function () {
@@ -280,10 +305,13 @@ Client.prototype.setPasscode = async function (passcode) {
280
305
  this.sendNextRequest();
281
306
  };
282
307
  Client.prototype.startHeartBeat = function () {
283
- var self = this;
308
+ if (this.heartBeat) {
309
+ clearInterval(this.heartBeat);
310
+ this.heartBeat = null;
311
+ }
284
312
  if (config.rtsp_heartbeat > 0) {
285
- this.heartBeat = setInterval(function () {
286
- self.sendHeartBeat(function () {
313
+ this.heartBeat = setInterval(() => {
314
+ this.sendHeartBeat(() => {
287
315
  //this.logLine?.('HeartBeat sent!');
288
316
  });
289
317
  }, config.rtsp_heartbeat);
@@ -319,7 +347,6 @@ Client.prototype.setArtwork = function (art, contentType, callback) {
319
347
  contentType = null;
320
348
  }
321
349
  if (typeof art == 'string') {
322
- var self = this;
323
350
  if (contentType === null) {
324
351
  var ext = art.slice(-4);
325
352
  if (ext == ".jpg" || ext == "jpeg") {
@@ -332,14 +359,14 @@ Client.prototype.setArtwork = function (art, contentType, callback) {
332
359
  contentType = "image/gif";
333
360
  }
334
361
  else {
335
- return self.cleanup('unknown_art_file_ext');
362
+ return this.cleanup('unknown_art_file_ext');
336
363
  }
337
364
  }
338
- return fs.readFile(art, function (err, data) {
365
+ return fs.readFile(art, (err, data) => {
339
366
  if (err !== null) {
340
- return self.cleanup('invalid_art_file');
367
+ return this.cleanup('invalid_art_file');
341
368
  }
342
- self.setArtwork(data, contentType, callback);
369
+ this.setArtwork(data, contentType, callback);
343
370
  });
344
371
  }
345
372
  if (contentType === null)
@@ -388,6 +415,44 @@ Client.prototype.cleanup = function (type, msg) {
388
415
  this.socket.destroy();
389
416
  this.socket = null;
390
417
  }
418
+ if (this.eventsocket) {
419
+ try {
420
+ this.eventsocket.destroy?.();
421
+ this.eventsocket = null;
422
+ }
423
+ catch {
424
+ /* ignore */
425
+ }
426
+ }
427
+ if (this.controlsocket) {
428
+ try {
429
+ this.controlsocket.close();
430
+ }
431
+ catch {
432
+ /* ignore */
433
+ }
434
+ this.controlsocket = null;
435
+ }
436
+ if (this.timingsocket) {
437
+ try {
438
+ this.timingsocket.close();
439
+ }
440
+ catch {
441
+ /* ignore */
442
+ }
443
+ this.timingsocket = null;
444
+ }
445
+ const audioSocket = this.audioSocket;
446
+ if (audioSocket) {
447
+ try {
448
+ audioSocket.close?.();
449
+ audioSocket.destroy?.();
450
+ }
451
+ catch {
452
+ /* ignore */
453
+ }
454
+ this.audioSocket = null;
455
+ }
391
456
  };
392
457
  function parseResponse(blob) {
393
458
  var response = {}, lines = blob.split('\r\n');
@@ -531,10 +596,8 @@ Client.prototype.sendNextRequest = async function (di) {
531
596
  this.socket.write(Buffer.from(request, 'utf-8'));
532
597
  }
533
598
  else {
534
- this.logLine?.("pass", this.password);
535
599
  if (this.password) {
536
600
  this.status = this.airplay2 ? PAIR_SETUP_1 : PAIR_PIN_SETUP_1;
537
- this.logLine?.("pass2", this.password);
538
601
  this.sendNextRequest();
539
602
  }
540
603
  else {
@@ -591,8 +654,6 @@ Client.prototype.sendNextRequest = async function (di) {
591
654
  request += this.makeHead("POST", "/pair-verify", "", true);
592
655
  request += 'Content-Type: application/octet-stream\r\n';
593
656
  this.pair_verify_1_verifier = ATVAuthenticator.verifier(this.authSecret);
594
- if (this.debug)
595
- this.logLine?.(this.authSecret);
596
657
  request += 'Content-Length:' + Buffer.byteLength(this.pair_verify_1_verifier.verifierBody) + '\r\n\r\n';
597
658
  this.socket.write(Buffer.concat([Buffer.from(request, 'utf-8'), this.pair_verify_1_verifier.verifierBody]));
598
659
  request = '';
@@ -846,8 +907,7 @@ Client.prototype.sendNextRequest = async function (di) {
846
907
  catch (e) { }
847
908
  try {
848
909
  this.timingsocket = dgram.createSocket({ type: 'udp4', reuseAddr: true });
849
- var self = this;
850
- this.timingsocket.on('message', function (msg, rinfo) {
910
+ this.timingsocket.on('message', (msg, rinfo) => {
851
911
  // only listen and respond on own hosts
852
912
  // if (this.hosts.indexOf(rinfo.address) < 0) return;
853
913
  var ts1 = msg.readUInt32BE(24);
@@ -861,8 +921,8 @@ Client.prototype.sendNextRequest = async function (di) {
861
921
  var ntpTime = ntp.timestamp();
862
922
  ntpTime.copy(reply, 16);
863
923
  ntpTime.copy(reply, 24);
864
- self.timingsocket.send(reply, 0, reply.length, rinfo.port, rinfo.address);
865
- self.logLine?.('timing socket pinged', rinfo.port, rinfo.address);
924
+ this.timingsocket.send(reply, 0, reply.length, rinfo.port, rinfo.address);
925
+ this.logLine?.('timing socket pinged', rinfo.port, rinfo.address);
866
926
  });
867
927
  this.timingsocket.bind(this.timingPort, this.socket.address().address);
868
928
  }
@@ -916,9 +976,8 @@ Client.prototype.sendNextRequest = async function (di) {
916
976
  }] });
917
977
  request += 'Content-Length: ' + Buffer.byteLength(setap2) + '\r\n\r\n';
918
978
  this.controlsocket = dgram.createSocket({ type: 'udp4', reuseAddr: true });
919
- var self = this;
920
- this.controlsocket.on('message', function (msg, rinfo) {
921
- self.logLine?.('controlsocket.data', msg);
979
+ this.controlsocket.on('message', (msg) => {
980
+ this.logLine?.('controlsocket.data', msg);
922
981
  });
923
982
  this.controlsocket.bind(this.controlPort, this.socket.address().address);
924
983
  let s2ct = this.credentials.encrypt(Buffer.concat([Buffer.from(request, 'utf-8'), setap2]));
@@ -928,18 +987,29 @@ Client.prototype.sendNextRequest = async function (di) {
928
987
  case RECORD:
929
988
  //this.logLine?.(request);
930
989
  if (this.airplay2) {
931
- this.eventsocket = net.connect(this.eventPort, this.hostip, async function () {
932
- });
933
- this.eventsocket.on('data', function (data) {
934
- self.logLine?.('eventsocket.data', data);
935
- self.logLine?.('eventsocket.data2', self.event_credentials.decrypt(data).toString());
936
- });
937
- this.eventsocket.on('error', function (err) {
938
- self.logLine?.('eventsocket.error', err);
939
- });
940
990
  this.event_credentials = new Credentials("sdsds", "", "", "", this.seed);
941
991
  this.event_credentials.writeKey = enc.HKDF("sha512", Buffer.from("Events-Salt"), this.srp.computeK(), Buffer.from("Events-Read-Encryption-Key"), 32);
942
992
  this.event_credentials.readKey = enc.HKDF("sha512", Buffer.from("Events-Salt"), this.srp.computeK(), Buffer.from("Events-Write-Encryption-Key"), 32);
993
+ this.eventsocket = net.connect(this.eventPort, this.hostip, async () => {
994
+ });
995
+ this.eventsocket.on('data', (data) => {
996
+ if (this.debug) {
997
+ this.logLine?.('eventsocket.data', data);
998
+ try {
999
+ const decrypted = this.event_credentials?.decrypt(data);
1000
+ if (decrypted) {
1001
+ this.logLine?.('eventsocket.data2', decrypted.toString());
1002
+ }
1003
+ }
1004
+ catch (err) {
1005
+ this.logLine?.('eventsocket.decrypt.error', err);
1006
+ }
1007
+ }
1008
+ });
1009
+ this.eventsocket.on('error', (err) => {
1010
+ if (this.debug)
1011
+ this.logLine?.('eventsocket.error', err);
1012
+ });
943
1013
  }
944
1014
  if (this.airplay2 != null && this.credentials != null) {
945
1015
  // this.controlsocket.close();
@@ -1117,19 +1187,19 @@ function parseAuthenticate(auth, field) {
1117
1187
  Client.prototype.processData = function (blob, rawData) {
1118
1188
  this.logLine?.('Receiving request:', this.hostip, rtsp_methods[this.status + 1]);
1119
1189
  var response = parseResponse2(blob, this), headers = response.headers || {};
1120
- if (this.debug != false) {
1121
- if ((rawData.toString()).includes("bplist00")) {
1122
- try {
1123
- let buf = Buffer.from(rawData).slice(rawData.length - parseInt(headers['Content-Length']), rawData.length);
1124
- let bplist = bplistParser.parseBuffer(buf);
1125
- this.logLine?.("incoming-res: \r\n", JSON.stringify(bplist));
1190
+ if (this.debug) {
1191
+ try {
1192
+ if ((rawData.toString()).includes("bplist00")) {
1193
+ const buf = Buffer.from(rawData).slice(rawData.length - parseInt(headers['Content-Length']), rawData.length);
1194
+ const bplist = bplistParser.parseBuffer(buf);
1195
+ this.logLine?.("incoming-res:", JSON.stringify(bplist));
1126
1196
  }
1127
- catch (_) {
1128
- this.logLine?.("incoming-res: \r\n", rawData.toString());
1197
+ else {
1198
+ this.logLine?.("incoming-res:", { code: response.code, length: rawData.length });
1129
1199
  }
1130
1200
  }
1131
- else {
1132
- this.logLine?.("incoming-res: \r\n", rawData.toString());
1201
+ catch {
1202
+ this.logLine?.("incoming-res:", { code: response.code, length: rawData.length });
1133
1203
  }
1134
1204
  }
1135
1205
  if (this.status != OPTIONS && this.status != OPTIONS2 && this.mode == 0) {
@@ -1363,7 +1433,6 @@ Client.prototype.processData = function (blob, rawData) {
1363
1433
  this.credentials.writeKey = enc.HKDF("sha512", Buffer.from("Control-Salt"), this.srp.computeK(), Buffer.from("Control-Write-Encryption-Key"), 32);
1364
1434
  this.credentials.readKey = enc.HKDF("sha512", Buffer.from("Control-Salt"), this.srp.computeK(), Buffer.from("Control-Read-Encryption-Key"), 32);
1365
1435
  this.encryptedChannel = true;
1366
- this.logLine?.(this.srp.computeK());
1367
1436
  this.status = SETUP_AP2_1;
1368
1437
  }
1369
1438
  else {
@@ -143,7 +143,11 @@ class UDPServers extends node_events_1.EventEmitter {
143
143
  const ntpTime = ntp_1.default.timestamp();
144
144
  ntpTime.copy(packet, 8);
145
145
  packet.writeUInt32BE((0, numUtil_1.low32)(seq * config_1.default.frames_per_packet + config_1.default.sampling_rate * 2), 16);
146
- this.control.socket.send(packet, 0, packet.length, dev.controlPort, dev.host);
146
+ const delay = Math.max(0, config_1.default.control_sync_base_delay_ms +
147
+ Math.random() * config_1.default.control_sync_jitter_ms);
148
+ setTimeout(() => {
149
+ this.control.socket?.send(packet, 0, packet.length, dev.controlPort, dev.host);
150
+ }, delay);
147
151
  }
148
152
  }
149
153
  exports.default = UDPServers;
@@ -18,8 +18,14 @@ exports.config = {
18
18
  sampling_rate: 44100, // fixed by AirTunes v2
19
19
  sync_period: 126, // UDP sync packets are sent to all AirTunes devices regularly
20
20
  stream_latency: 200, // audio UDP packets are flushed in bursts periodically
21
- rtsp_timeout: 2147483647, // RTSP servers are considered gone if no reply is received before the timeout
21
+ rtsp_timeout: 15000, // RTSP servers are considered gone if no reply is received before the timeout
22
22
  rtsp_heartbeat: 15000, // some RTSP (like HomePod) servers requires heartbeat.
23
+ rtsp_retry_attempts: 3,
24
+ rtsp_retry_base_ms: 300,
25
+ rtsp_retry_max_ms: 4000,
26
+ rtsp_retry_jitter_ms: 150,
27
+ control_sync_base_delay_ms: 2,
28
+ control_sync_jitter_ms: 3,
23
29
  device_magic: (0, numUtil_1.randomInt)(9),
24
30
  ntp_epoch: 0x83aa7e80,
25
31
  iv_base64: 'ePRBLI0XN5ArFaaz7ncNZw',
@@ -16,6 +16,12 @@ export interface AirplayConfig {
16
16
  stream_latency: number;
17
17
  rtsp_timeout: number;
18
18
  rtsp_heartbeat: number;
19
+ rtsp_retry_attempts: number;
20
+ rtsp_retry_base_ms: number;
21
+ rtsp_retry_max_ms: number;
22
+ rtsp_retry_jitter_ms: number;
23
+ control_sync_base_delay_ms: number;
24
+ control_sync_jitter_ms: number;
19
25
  device_magic: number;
20
26
  ntp_epoch: number;
21
27
  iv_base64: string;
@@ -18,8 +18,14 @@ exports.config = {
18
18
  sampling_rate: 44100, // fixed by AirTunes v2
19
19
  sync_period: 126, // UDP sync packets are sent to all AirTunes devices regularly
20
20
  stream_latency: 200, // audio UDP packets are flushed in bursts periodically
21
- rtsp_timeout: 2147483647, // RTSP servers are considered gone if no reply is received before the timeout
21
+ rtsp_timeout: 15000, // RTSP servers are considered gone if no reply is received before the timeout
22
22
  rtsp_heartbeat: 15000, // some RTSP (like HomePod) servers requires heartbeat.
23
+ rtsp_retry_attempts: 3,
24
+ rtsp_retry_base_ms: 300,
25
+ rtsp_retry_max_ms: 4000,
26
+ rtsp_retry_jitter_ms: 150,
27
+ control_sync_base_delay_ms: 2,
28
+ control_sync_jitter_ms: 3,
23
29
  device_magic: (0, numUtil_1.randomInt)(9),
24
30
  ntp_epoch: 0x83aa7e80,
25
31
  iv_base64: 'ePRBLI0XN5ArFaaz7ncNZw',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lox-airplay-sender",
3
- "version": "0.1.0",
3
+ "version": "0.2.1",
4
4
  "description": "AirPlay sender (RAOP/AirPlay 1 + AirPlay 2 auth flows) ",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",