lox-airplay-sender 0.1.0 → 0.2.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.
@@ -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,107 @@ 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
+ this.timeout = setTimeout(() => {
251
+ if (this.debug)
252
+ this.logLine?.('timeout');
253
+ this.cleanup('timeout');
233
254
  }, config.rtsp_timeout);
234
255
  };
235
256
  Client.prototype.clearTimeout = function () {
@@ -280,10 +301,13 @@ Client.prototype.setPasscode = async function (passcode) {
280
301
  this.sendNextRequest();
281
302
  };
282
303
  Client.prototype.startHeartBeat = function () {
283
- var self = this;
304
+ if (this.heartBeat) {
305
+ clearInterval(this.heartBeat);
306
+ this.heartBeat = null;
307
+ }
284
308
  if (config.rtsp_heartbeat > 0) {
285
- this.heartBeat = setInterval(function () {
286
- self.sendHeartBeat(function () {
309
+ this.heartBeat = setInterval(() => {
310
+ this.sendHeartBeat(() => {
287
311
  //this.logLine?.('HeartBeat sent!');
288
312
  });
289
313
  }, config.rtsp_heartbeat);
@@ -319,7 +343,6 @@ Client.prototype.setArtwork = function (art, contentType, callback) {
319
343
  contentType = null;
320
344
  }
321
345
  if (typeof art == 'string') {
322
- var self = this;
323
346
  if (contentType === null) {
324
347
  var ext = art.slice(-4);
325
348
  if (ext == ".jpg" || ext == "jpeg") {
@@ -332,14 +355,14 @@ Client.prototype.setArtwork = function (art, contentType, callback) {
332
355
  contentType = "image/gif";
333
356
  }
334
357
  else {
335
- return self.cleanup('unknown_art_file_ext');
358
+ return this.cleanup('unknown_art_file_ext');
336
359
  }
337
360
  }
338
- return fs.readFile(art, function (err, data) {
361
+ return fs.readFile(art, (err, data) => {
339
362
  if (err !== null) {
340
- return self.cleanup('invalid_art_file');
363
+ return this.cleanup('invalid_art_file');
341
364
  }
342
- self.setArtwork(data, contentType, callback);
365
+ this.setArtwork(data, contentType, callback);
343
366
  });
344
367
  }
345
368
  if (contentType === null)
@@ -388,6 +411,44 @@ Client.prototype.cleanup = function (type, msg) {
388
411
  this.socket.destroy();
389
412
  this.socket = null;
390
413
  }
414
+ if (this.eventsocket) {
415
+ try {
416
+ this.eventsocket.destroy?.();
417
+ this.eventsocket = null;
418
+ }
419
+ catch {
420
+ /* ignore */
421
+ }
422
+ }
423
+ if (this.controlsocket) {
424
+ try {
425
+ this.controlsocket.close();
426
+ }
427
+ catch {
428
+ /* ignore */
429
+ }
430
+ this.controlsocket = null;
431
+ }
432
+ if (this.timingsocket) {
433
+ try {
434
+ this.timingsocket.close();
435
+ }
436
+ catch {
437
+ /* ignore */
438
+ }
439
+ this.timingsocket = null;
440
+ }
441
+ const audioSocket = this.audioSocket;
442
+ if (audioSocket) {
443
+ try {
444
+ audioSocket.close?.();
445
+ audioSocket.destroy?.();
446
+ }
447
+ catch {
448
+ /* ignore */
449
+ }
450
+ this.audioSocket = null;
451
+ }
391
452
  };
392
453
  function parseResponse(blob) {
393
454
  var response = {}, lines = blob.split('\r\n');
@@ -531,10 +592,8 @@ Client.prototype.sendNextRequest = async function (di) {
531
592
  this.socket.write(Buffer.from(request, 'utf-8'));
532
593
  }
533
594
  else {
534
- this.logLine?.("pass", this.password);
535
595
  if (this.password) {
536
596
  this.status = this.airplay2 ? PAIR_SETUP_1 : PAIR_PIN_SETUP_1;
537
- this.logLine?.("pass2", this.password);
538
597
  this.sendNextRequest();
539
598
  }
540
599
  else {
@@ -591,8 +650,6 @@ Client.prototype.sendNextRequest = async function (di) {
591
650
  request += this.makeHead("POST", "/pair-verify", "", true);
592
651
  request += 'Content-Type: application/octet-stream\r\n';
593
652
  this.pair_verify_1_verifier = ATVAuthenticator.verifier(this.authSecret);
594
- if (this.debug)
595
- this.logLine?.(this.authSecret);
596
653
  request += 'Content-Length:' + Buffer.byteLength(this.pair_verify_1_verifier.verifierBody) + '\r\n\r\n';
597
654
  this.socket.write(Buffer.concat([Buffer.from(request, 'utf-8'), this.pair_verify_1_verifier.verifierBody]));
598
655
  request = '';
@@ -846,8 +903,7 @@ Client.prototype.sendNextRequest = async function (di) {
846
903
  catch (e) { }
847
904
  try {
848
905
  this.timingsocket = dgram.createSocket({ type: 'udp4', reuseAddr: true });
849
- var self = this;
850
- this.timingsocket.on('message', function (msg, rinfo) {
906
+ this.timingsocket.on('message', (msg, rinfo) => {
851
907
  // only listen and respond on own hosts
852
908
  // if (this.hosts.indexOf(rinfo.address) < 0) return;
853
909
  var ts1 = msg.readUInt32BE(24);
@@ -861,8 +917,8 @@ Client.prototype.sendNextRequest = async function (di) {
861
917
  var ntpTime = ntp.timestamp();
862
918
  ntpTime.copy(reply, 16);
863
919
  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);
920
+ this.timingsocket.send(reply, 0, reply.length, rinfo.port, rinfo.address);
921
+ this.logLine?.('timing socket pinged', rinfo.port, rinfo.address);
866
922
  });
867
923
  this.timingsocket.bind(this.timingPort, this.socket.address().address);
868
924
  }
@@ -916,9 +972,8 @@ Client.prototype.sendNextRequest = async function (di) {
916
972
  }] });
917
973
  request += 'Content-Length: ' + Buffer.byteLength(setap2) + '\r\n\r\n';
918
974
  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);
975
+ this.controlsocket.on('message', (msg) => {
976
+ this.logLine?.('controlsocket.data', msg);
922
977
  });
923
978
  this.controlsocket.bind(this.controlPort, this.socket.address().address);
924
979
  let s2ct = this.credentials.encrypt(Buffer.concat([Buffer.from(request, 'utf-8'), setap2]));
@@ -928,18 +983,29 @@ Client.prototype.sendNextRequest = async function (di) {
928
983
  case RECORD:
929
984
  //this.logLine?.(request);
930
985
  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
986
  this.event_credentials = new Credentials("sdsds", "", "", "", this.seed);
941
987
  this.event_credentials.writeKey = enc.HKDF("sha512", Buffer.from("Events-Salt"), this.srp.computeK(), Buffer.from("Events-Read-Encryption-Key"), 32);
942
988
  this.event_credentials.readKey = enc.HKDF("sha512", Buffer.from("Events-Salt"), this.srp.computeK(), Buffer.from("Events-Write-Encryption-Key"), 32);
989
+ this.eventsocket = net.connect(this.eventPort, this.hostip, async () => {
990
+ });
991
+ this.eventsocket.on('data', (data) => {
992
+ if (this.debug) {
993
+ this.logLine?.('eventsocket.data', data);
994
+ try {
995
+ const decrypted = this.event_credentials?.decrypt(data);
996
+ if (decrypted) {
997
+ this.logLine?.('eventsocket.data2', decrypted.toString());
998
+ }
999
+ }
1000
+ catch (err) {
1001
+ this.logLine?.('eventsocket.decrypt.error', err);
1002
+ }
1003
+ }
1004
+ });
1005
+ this.eventsocket.on('error', (err) => {
1006
+ if (this.debug)
1007
+ this.logLine?.('eventsocket.error', err);
1008
+ });
943
1009
  }
944
1010
  if (this.airplay2 != null && this.credentials != null) {
945
1011
  // this.controlsocket.close();
@@ -1117,19 +1183,19 @@ function parseAuthenticate(auth, field) {
1117
1183
  Client.prototype.processData = function (blob, rawData) {
1118
1184
  this.logLine?.('Receiving request:', this.hostip, rtsp_methods[this.status + 1]);
1119
1185
  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));
1186
+ if (this.debug) {
1187
+ try {
1188
+ if ((rawData.toString()).includes("bplist00")) {
1189
+ const buf = Buffer.from(rawData).slice(rawData.length - parseInt(headers['Content-Length']), rawData.length);
1190
+ const bplist = bplistParser.parseBuffer(buf);
1191
+ this.logLine?.("incoming-res:", JSON.stringify(bplist));
1126
1192
  }
1127
- catch (_) {
1128
- this.logLine?.("incoming-res: \r\n", rawData.toString());
1193
+ else {
1194
+ this.logLine?.("incoming-res:", { code: response.code, length: rawData.length });
1129
1195
  }
1130
1196
  }
1131
- else {
1132
- this.logLine?.("incoming-res: \r\n", rawData.toString());
1197
+ catch {
1198
+ this.logLine?.("incoming-res:", { code: response.code, length: rawData.length });
1133
1199
  }
1134
1200
  }
1135
1201
  if (this.status != OPTIONS && this.status != OPTIONS2 && this.mode == 0) {
@@ -1363,7 +1429,6 @@ Client.prototype.processData = function (blob, rawData) {
1363
1429
  this.credentials.writeKey = enc.HKDF("sha512", Buffer.from("Control-Salt"), this.srp.computeK(), Buffer.from("Control-Write-Encryption-Key"), 32);
1364
1430
  this.credentials.readKey = enc.HKDF("sha512", Buffer.from("Control-Salt"), this.srp.computeK(), Buffer.from("Control-Read-Encryption-Key"), 32);
1365
1431
  this.encryptedChannel = true;
1366
- this.logLine?.(this.srp.computeK());
1367
1432
  this.status = SETUP_AP2_1;
1368
1433
  }
1369
1434
  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,107 @@ 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
+ this.timeout = setTimeout(() => {
251
+ if (this.debug)
252
+ this.logLine?.('timeout');
253
+ this.cleanup('timeout');
233
254
  }, config.rtsp_timeout);
234
255
  };
235
256
  Client.prototype.clearTimeout = function () {
@@ -280,10 +301,13 @@ Client.prototype.setPasscode = async function (passcode) {
280
301
  this.sendNextRequest();
281
302
  };
282
303
  Client.prototype.startHeartBeat = function () {
283
- var self = this;
304
+ if (this.heartBeat) {
305
+ clearInterval(this.heartBeat);
306
+ this.heartBeat = null;
307
+ }
284
308
  if (config.rtsp_heartbeat > 0) {
285
- this.heartBeat = setInterval(function () {
286
- self.sendHeartBeat(function () {
309
+ this.heartBeat = setInterval(() => {
310
+ this.sendHeartBeat(() => {
287
311
  //this.logLine?.('HeartBeat sent!');
288
312
  });
289
313
  }, config.rtsp_heartbeat);
@@ -319,7 +343,6 @@ Client.prototype.setArtwork = function (art, contentType, callback) {
319
343
  contentType = null;
320
344
  }
321
345
  if (typeof art == 'string') {
322
- var self = this;
323
346
  if (contentType === null) {
324
347
  var ext = art.slice(-4);
325
348
  if (ext == ".jpg" || ext == "jpeg") {
@@ -332,14 +355,14 @@ Client.prototype.setArtwork = function (art, contentType, callback) {
332
355
  contentType = "image/gif";
333
356
  }
334
357
  else {
335
- return self.cleanup('unknown_art_file_ext');
358
+ return this.cleanup('unknown_art_file_ext');
336
359
  }
337
360
  }
338
- return fs.readFile(art, function (err, data) {
361
+ return fs.readFile(art, (err, data) => {
339
362
  if (err !== null) {
340
- return self.cleanup('invalid_art_file');
363
+ return this.cleanup('invalid_art_file');
341
364
  }
342
- self.setArtwork(data, contentType, callback);
365
+ this.setArtwork(data, contentType, callback);
343
366
  });
344
367
  }
345
368
  if (contentType === null)
@@ -388,6 +411,44 @@ Client.prototype.cleanup = function (type, msg) {
388
411
  this.socket.destroy();
389
412
  this.socket = null;
390
413
  }
414
+ if (this.eventsocket) {
415
+ try {
416
+ this.eventsocket.destroy?.();
417
+ this.eventsocket = null;
418
+ }
419
+ catch {
420
+ /* ignore */
421
+ }
422
+ }
423
+ if (this.controlsocket) {
424
+ try {
425
+ this.controlsocket.close();
426
+ }
427
+ catch {
428
+ /* ignore */
429
+ }
430
+ this.controlsocket = null;
431
+ }
432
+ if (this.timingsocket) {
433
+ try {
434
+ this.timingsocket.close();
435
+ }
436
+ catch {
437
+ /* ignore */
438
+ }
439
+ this.timingsocket = null;
440
+ }
441
+ const audioSocket = this.audioSocket;
442
+ if (audioSocket) {
443
+ try {
444
+ audioSocket.close?.();
445
+ audioSocket.destroy?.();
446
+ }
447
+ catch {
448
+ /* ignore */
449
+ }
450
+ this.audioSocket = null;
451
+ }
391
452
  };
392
453
  function parseResponse(blob) {
393
454
  var response = {}, lines = blob.split('\r\n');
@@ -531,10 +592,8 @@ Client.prototype.sendNextRequest = async function (di) {
531
592
  this.socket.write(Buffer.from(request, 'utf-8'));
532
593
  }
533
594
  else {
534
- this.logLine?.("pass", this.password);
535
595
  if (this.password) {
536
596
  this.status = this.airplay2 ? PAIR_SETUP_1 : PAIR_PIN_SETUP_1;
537
- this.logLine?.("pass2", this.password);
538
597
  this.sendNextRequest();
539
598
  }
540
599
  else {
@@ -591,8 +650,6 @@ Client.prototype.sendNextRequest = async function (di) {
591
650
  request += this.makeHead("POST", "/pair-verify", "", true);
592
651
  request += 'Content-Type: application/octet-stream\r\n';
593
652
  this.pair_verify_1_verifier = ATVAuthenticator.verifier(this.authSecret);
594
- if (this.debug)
595
- this.logLine?.(this.authSecret);
596
653
  request += 'Content-Length:' + Buffer.byteLength(this.pair_verify_1_verifier.verifierBody) + '\r\n\r\n';
597
654
  this.socket.write(Buffer.concat([Buffer.from(request, 'utf-8'), this.pair_verify_1_verifier.verifierBody]));
598
655
  request = '';
@@ -846,8 +903,7 @@ Client.prototype.sendNextRequest = async function (di) {
846
903
  catch (e) { }
847
904
  try {
848
905
  this.timingsocket = dgram.createSocket({ type: 'udp4', reuseAddr: true });
849
- var self = this;
850
- this.timingsocket.on('message', function (msg, rinfo) {
906
+ this.timingsocket.on('message', (msg, rinfo) => {
851
907
  // only listen and respond on own hosts
852
908
  // if (this.hosts.indexOf(rinfo.address) < 0) return;
853
909
  var ts1 = msg.readUInt32BE(24);
@@ -861,8 +917,8 @@ Client.prototype.sendNextRequest = async function (di) {
861
917
  var ntpTime = ntp.timestamp();
862
918
  ntpTime.copy(reply, 16);
863
919
  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);
920
+ this.timingsocket.send(reply, 0, reply.length, rinfo.port, rinfo.address);
921
+ this.logLine?.('timing socket pinged', rinfo.port, rinfo.address);
866
922
  });
867
923
  this.timingsocket.bind(this.timingPort, this.socket.address().address);
868
924
  }
@@ -916,9 +972,8 @@ Client.prototype.sendNextRequest = async function (di) {
916
972
  }] });
917
973
  request += 'Content-Length: ' + Buffer.byteLength(setap2) + '\r\n\r\n';
918
974
  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);
975
+ this.controlsocket.on('message', (msg) => {
976
+ this.logLine?.('controlsocket.data', msg);
922
977
  });
923
978
  this.controlsocket.bind(this.controlPort, this.socket.address().address);
924
979
  let s2ct = this.credentials.encrypt(Buffer.concat([Buffer.from(request, 'utf-8'), setap2]));
@@ -928,18 +983,29 @@ Client.prototype.sendNextRequest = async function (di) {
928
983
  case RECORD:
929
984
  //this.logLine?.(request);
930
985
  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
986
  this.event_credentials = new Credentials("sdsds", "", "", "", this.seed);
941
987
  this.event_credentials.writeKey = enc.HKDF("sha512", Buffer.from("Events-Salt"), this.srp.computeK(), Buffer.from("Events-Read-Encryption-Key"), 32);
942
988
  this.event_credentials.readKey = enc.HKDF("sha512", Buffer.from("Events-Salt"), this.srp.computeK(), Buffer.from("Events-Write-Encryption-Key"), 32);
989
+ this.eventsocket = net.connect(this.eventPort, this.hostip, async () => {
990
+ });
991
+ this.eventsocket.on('data', (data) => {
992
+ if (this.debug) {
993
+ this.logLine?.('eventsocket.data', data);
994
+ try {
995
+ const decrypted = this.event_credentials?.decrypt(data);
996
+ if (decrypted) {
997
+ this.logLine?.('eventsocket.data2', decrypted.toString());
998
+ }
999
+ }
1000
+ catch (err) {
1001
+ this.logLine?.('eventsocket.decrypt.error', err);
1002
+ }
1003
+ }
1004
+ });
1005
+ this.eventsocket.on('error', (err) => {
1006
+ if (this.debug)
1007
+ this.logLine?.('eventsocket.error', err);
1008
+ });
943
1009
  }
944
1010
  if (this.airplay2 != null && this.credentials != null) {
945
1011
  // this.controlsocket.close();
@@ -1117,19 +1183,19 @@ function parseAuthenticate(auth, field) {
1117
1183
  Client.prototype.processData = function (blob, rawData) {
1118
1184
  this.logLine?.('Receiving request:', this.hostip, rtsp_methods[this.status + 1]);
1119
1185
  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));
1186
+ if (this.debug) {
1187
+ try {
1188
+ if ((rawData.toString()).includes("bplist00")) {
1189
+ const buf = Buffer.from(rawData).slice(rawData.length - parseInt(headers['Content-Length']), rawData.length);
1190
+ const bplist = bplistParser.parseBuffer(buf);
1191
+ this.logLine?.("incoming-res:", JSON.stringify(bplist));
1126
1192
  }
1127
- catch (_) {
1128
- this.logLine?.("incoming-res: \r\n", rawData.toString());
1193
+ else {
1194
+ this.logLine?.("incoming-res:", { code: response.code, length: rawData.length });
1129
1195
  }
1130
1196
  }
1131
- else {
1132
- this.logLine?.("incoming-res: \r\n", rawData.toString());
1197
+ catch {
1198
+ this.logLine?.("incoming-res:", { code: response.code, length: rawData.length });
1133
1199
  }
1134
1200
  }
1135
1201
  if (this.status != OPTIONS && this.status != OPTIONS2 && this.mode == 0) {
@@ -1363,7 +1429,6 @@ Client.prototype.processData = function (blob, rawData) {
1363
1429
  this.credentials.writeKey = enc.HKDF("sha512", Buffer.from("Control-Salt"), this.srp.computeK(), Buffer.from("Control-Write-Encryption-Key"), 32);
1364
1430
  this.credentials.readKey = enc.HKDF("sha512", Buffer.from("Control-Salt"), this.srp.computeK(), Buffer.from("Control-Read-Encryption-Key"), 32);
1365
1431
  this.encryptedChannel = true;
1366
- this.logLine?.(this.srp.computeK());
1367
1432
  this.status = SETUP_AP2_1;
1368
1433
  }
1369
1434
  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.0",
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",