@lox-audioserver/node-airplay-sender 0.4.4 → 0.4.5

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.
@@ -108,12 +108,10 @@ function AirTunesDevice(host, audioOut, options, mode = 0, txt = '') {
108
108
  if (!host) {
109
109
  throw new Error('host is mandatory');
110
110
  }
111
- const resendBufferSize = Number(options?.resendBufferSize ?? config_1.default.resend_buffer_size ?? 1000);
112
- this.audioPacketHistory = new BufferWithNames(Number.isFinite(resendBufferSize) && resendBufferSize > 0 ? resendBufferSize : 1000);
111
+ this.audioPacketHistory = new BufferWithNames(100);
113
112
  this.udpServers = new udpServers_1.default();
114
113
  this.audioOut = audioOut;
115
114
  this.type = 'airtunes';
116
- this.deviceMagic = Number(options?.deviceMagic ?? config_1.default.device_magic);
117
115
  this.options = options;
118
116
  this.log = options?.log;
119
117
  this.logLine = (...args) => logLine(this, ...args);
@@ -133,7 +131,6 @@ function AirTunesDevice(host, audioOut, options, mode = 0, txt = '') {
133
131
  this.airplay2 = options?.airplay2 ?? false;
134
132
  this.txt = Array.isArray(txt) ? txt : txt ? [String(txt)] : [];
135
133
  this.borkedshp = false;
136
- this.transient = false;
137
134
  if (this.airplay2 && this.mode === 0) {
138
135
  this.mode = 2;
139
136
  }
@@ -156,8 +153,7 @@ function AirTunesDevice(host, audioOut, options, mode = 0, txt = '') {
156
153
  }
157
154
  this.needPassword = false;
158
155
  this.needPin = false;
159
- const transientOption = options?.transient;
160
- this.transient = typeof transientOption === 'boolean' ? transientOption : false;
156
+ this.transient = false;
161
157
  let d = this.txt.filter((u) => String(u).startsWith('features='));
162
158
  if (d.length === 0)
163
159
  d = this.txt.filter((u) => String(u).startsWith('ft='));
@@ -167,7 +163,7 @@ function AirTunesDevice(host, audioOut, options, mode = 0, txt = '') {
167
163
  ...(features_set.length > 1 ? parseInt(features_set[1], 10).toString(2).split('') : []),
168
164
  ];
169
165
  if (this.features.length > 0) {
170
- this.transient = this.features[this.features.length - 1 - 48] === '1';
166
+ this.transient = (this.features[this.features.length - 1 - 48] == '1');
171
167
  }
172
168
  if (this.statusflags.length) {
173
169
  let PasswordRequired = (this.statusflags[this.statusflags.length - 1 - 7] == '1');
@@ -228,14 +224,6 @@ function AirTunesDevice(host, audioOut, options, mode = 0, txt = '') {
228
224
  this.audioCallback = null;
229
225
  this.encoder = [];
230
226
  this.credentials = null;
231
- this.onUnderrun = () => {
232
- this.encoder = [];
233
- this.logLine?.('underrun_encoder_reset');
234
- if (this.credentials) {
235
- this.credentials.encryptCount = 0;
236
- this.credentials.decryptCount = 0;
237
- }
238
- };
239
227
  // this.func = `
240
228
  // const {Worker, isMainThread, parentPort, workerData} = require('node:worker_threads');
241
229
  // var { WebSocketServer } = require('ws');
@@ -309,7 +297,8 @@ AirTunesDevice.prototype.doHandshake = function () {
309
297
  this.rtsp.on('ready', () => {
310
298
  this.status = 'playing';
311
299
  this.emit('status', 'playing');
312
- this.relayAudio();
300
+ if (this.airplay2)
301
+ this.relayAudio();
313
302
  });
314
303
  this.rtsp.on('need_password', () => {
315
304
  this.emit('status', 'need_password');
@@ -337,17 +326,10 @@ AirTunesDevice.prototype.doHandshake = function () {
337
326
  this.rtsp.startHandshake(this.udpServers, this.host, this.port);
338
327
  };
339
328
  AirTunesDevice.prototype.relayAudio = function () {
340
- if (this.audioCallback) {
341
- return;
342
- }
329
+ this.status = 'ready';
330
+ this.emit('status', 'ready');
343
331
  this.audioCallback = (packet) => {
344
- const airTunes = makeAirTunesPacket(packet, this.encoder, this.requireEncryption, this.alacEncoding, this.credentials, this.inputCodec, this.deviceMagic);
345
- try {
346
- this.audioPacketHistory?.add(packet.seq, airTunes);
347
- }
348
- catch {
349
- /* ignore history errors */
350
- }
332
+ const airTunes = makeAirTunesPacket(packet, this.encoder, this.requireEncryption, this.alacEncoding, this.credentials, this.inputCodec);
351
333
  // if (self.credentials) {
352
334
  // airTunes = self.credentials.encrypt(airTunes)
353
335
  // }
@@ -383,24 +365,10 @@ AirTunesDevice.prototype.relayAudio = function () {
383
365
  // self.sendAirTunesPacket(airTunes);}}
384
366
  // catch (_){}
385
367
  // });
386
- this.udpServers.on('resendRequested', (missedSeq, count) => {
387
- for (let i = 0; i < count; i += 1) {
388
- const seq = missedSeq + i;
389
- try {
390
- const pkt = this.audioPacketHistory?.getLatestNamed(seq);
391
- if (pkt && this.audioSocket) {
392
- this.audioSocket.send(pkt, 0, pkt.length, this.serverPort, this.host);
393
- }
394
- }
395
- catch {
396
- // ignore resend errors
397
- }
398
- }
399
- });
400
368
  this.audioOut.on('packet', this.audioCallback);
401
369
  };
402
- AirTunesDevice.prototype.onSyncNeeded = function (seq, tsOffsetFrames = 0, rtcp) {
403
- this.udpServers.sendControlSync(seq, this, tsOffsetFrames, rtcp, { ssrc: this.deviceMagic }, { ntp: rtcp?.xr?.ntp, ssrc: this.deviceMagic });
370
+ AirTunesDevice.prototype.onSyncNeeded = function (seq) {
371
+ this.udpServers.sendControlSync(seq, this);
404
372
  //if ( this.airplay2)this.rtsp.sendControlSync(seq, this, this.rtsp);
405
373
  };
406
374
  AirTunesDevice.prototype.cleanup = function () {
@@ -413,7 +381,6 @@ AirTunesDevice.prototype.cleanup = function () {
413
381
  this.audioOut.removeListener('packet', this.audioCallback);
414
382
  this.audioCallback = null;
415
383
  }
416
- this.encoder = [];
417
384
  this.udpServers.close();
418
385
  this.removeAllListeners();
419
386
  this.rtsp = null;
@@ -423,11 +390,8 @@ AirTunesDevice.prototype.reportStatus = function () {
423
390
  };
424
391
  AirTunesDevice.prototype.stop = function (cb) {
425
392
  try {
426
- if (!this.rtsp) {
427
- if (cb)
428
- cb();
393
+ if (!this.rtsp)
429
394
  return;
430
- }
431
395
  this.rtsp.once('end', function () {
432
396
  if (cb)
433
397
  cb();
@@ -462,8 +426,11 @@ AirTunesDevice.prototype.setPasscode = function (password) {
462
426
  return;
463
427
  this.rtsp.setPasscode(password);
464
428
  };
429
+ AirTunesDevice.prototype.requireEncryption = function () {
430
+ return Boolean(this.requireEncryption);
431
+ };
465
432
  exports.default = AirTunesDevice;
466
- function makeAirTunesPacket(packet, encoder, requireEncryption, alacEncoding = true, credentials = null, inputCodec = 'pcm', deviceMagic = config_1.default.device_magic) {
433
+ function makeAirTunesPacket(packet, encoder, requireEncryption, alacEncoding = true, credentials = null, inputCodec = 'pcm') {
467
434
  const useAlacInput = inputCodec === 'alac';
468
435
  var alac = useAlacInput
469
436
  ? packet.pcm
@@ -471,7 +438,7 @@ function makeAirTunesPacket(packet, encoder, requireEncryption, alacEncoding = t
471
438
  ? (0, alac_1.encodePcmToAlac)(packet.pcm)
472
439
  : pcmParse(packet.pcm);
473
440
  var airTunes = Buffer.alloc(alac.length + RTP_HEADER_SIZE);
474
- var header = makeRTPHeader(packet, deviceMagic);
441
+ var header = makeRTPHeader(packet);
475
442
  if (requireEncryption) {
476
443
  alac = encryptAES(alac, alac.length);
477
444
  }
@@ -521,7 +488,7 @@ function encryptAES(alacData, alacSize) {
521
488
  }
522
489
  return Buffer.concat([result, alacData.slice(end_of_encoded_data)]);
523
490
  }
524
- function makeRTPHeader(packet, deviceMagic) {
491
+ function makeRTPHeader(packet) {
525
492
  var header = Buffer.alloc(RTP_HEADER_SIZE);
526
493
  if (packet.seq === 0)
527
494
  header.writeUInt16BE(0x80e0, 0);
@@ -529,6 +496,6 @@ function makeRTPHeader(packet, deviceMagic) {
529
496
  header.writeUInt16BE(0x8060, 0);
530
497
  header.writeUInt16BE(nu.low16(packet.seq), 2);
531
498
  header.writeUInt32BE(packet.timestamp, 4);
532
- header.writeUInt32BE(deviceMagic, 8);
499
+ header.writeUInt32BE(config_1.default.device_magic, 8);
533
500
  return header;
534
501
  }
@@ -20,11 +20,11 @@ class Devices extends node_events_1.EventEmitter {
20
20
  }
21
21
  /** Wire device sync events from AudioOut into individual devices. */
22
22
  init() {
23
- this.audioOut.on('need_sync', (payload) => {
23
+ this.audioOut.on('need_sync', (seq) => {
24
24
  this.forEach((dev) => {
25
25
  try {
26
26
  if (dev.onSyncNeeded && dev.controlPort) {
27
- dev.onSyncNeeded(payload.seq, payload.tsOffsetFrames ?? 0, payload.rtcp);
27
+ dev.onSyncNeeded(seq);
28
28
  }
29
29
  }
30
30
  catch {
@@ -32,18 +32,6 @@ class Devices extends node_events_1.EventEmitter {
32
32
  }
33
33
  });
34
34
  });
35
- this.audioOut.on('underrun', () => {
36
- this.forEach((dev) => {
37
- try {
38
- if (dev.onUnderrun) {
39
- dev.onUnderrun();
40
- }
41
- }
42
- catch {
43
- /* ignore */
44
- }
45
- });
46
- });
47
35
  }
48
36
  /** Iterate over live devices. */
49
37
  forEach(it) {
@@ -40,8 +40,6 @@ const node_stream_1 = require("node:stream");
40
40
  const devices_1 = __importDefault(require("./devices"));
41
41
  const config_1 = __importStar(require("../utils/config"));
42
42
  const circularBuffer_1 = __importDefault(require("../utils/circularBuffer"));
43
- const ntp_1 = require("../utils/ntp");
44
- const numUtil_1 = require("../utils/numUtil");
45
43
  const audioOut_1 = __importDefault(require("./audioOut"));
46
44
  /**
47
45
  * High-level RAOP/AirPlay sender that wires together devices, buffering, and output.
@@ -50,33 +48,27 @@ const audioOut_1 = __importDefault(require("./audioOut"));
50
48
  class AirTunes extends node_stream_1.Duplex {
51
49
  devices;
52
50
  circularBuffer;
53
- deviceMagic;
54
51
  /**
55
- * @param options.packetSize Override packet size; defaults to config.
56
- * @param options.startTimeMs Optional unix ms to align playback start.
57
- * @param options.startTimeNtp Optional NTP timestamp (uint64) to align playback start.
58
- */
52
+ * @param options.packetSize Override packet size; defaults to config.
53
+ * @param options.startTimeMs Optional unix ms to align playback start.
54
+ */
59
55
  constructor(options = {}) {
60
56
  super({ readableObjectMode: false, writableObjectMode: false });
61
57
  if (options.config) {
62
58
  (0, config_1.applyConfig)(options.config);
63
59
  }
64
- // Randomize per-session device magic (SSRC equivalent) to align with reference behavior.
65
- const deviceMagic = typeof options.deviceMagic === 'number' ? options.deviceMagic : (0, numUtil_1.randomInt)(9);
66
60
  const audioOut = new audioOut_1.default();
67
- this.deviceMagic = deviceMagic;
68
61
  this.devices = new devices_1.default(audioOut);
69
62
  this.devices.init();
70
63
  this.devices.on('status', (key, status, desc) => {
71
64
  this.emit('device', key, status, desc);
72
65
  });
73
66
  const packetSize = options.packetSize ?? config_1.default.packet_size;
74
- const startTimeNtp = options.startTimeNtp ? (0, ntp_1.toNtpTimestamp)(options.startTimeNtp) : undefined;
75
67
  this.circularBuffer = new circularBuffer_1.default(config_1.default.packets_in_buffer, packetSize);
76
68
  this.circularBuffer.on('status', (status) => {
77
69
  this.emit('buffer', status);
78
70
  });
79
- audioOut.init(this.devices, this.circularBuffer, options.startTimeMs, startTimeNtp, deviceMagic, options.underrunMuteMs);
71
+ audioOut.init(this.devices, this.circularBuffer, options.startTimeMs);
80
72
  audioOut.on('metrics', (metrics) => {
81
73
  this.emit('metrics', metrics);
82
74
  });
@@ -89,7 +81,7 @@ class AirTunes extends node_stream_1.Duplex {
89
81
  }
90
82
  /** Register an AirTunes (RAOP) device and start streaming to it. */
91
83
  add(host, options, mode = 0, txt = '') {
92
- return this.devices.add('airtunes', host, { ...options, deviceMagic: this.deviceMagic, resendBufferSize: options.resendBufferSize }, mode, txt);
84
+ return this.devices.add('airtunes', host, options, mode, txt);
93
85
  }
94
86
  /** Register a CoreAudio output (legacy shim). */
95
87
  addCoreAudio(options) {
@@ -1,7 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.Client = Client;
4
- // @ts-nocheck
5
4
  var net = require('net'), nodeCrypto = require('crypto'), events = require('events'), util = require('util'), fs = require('fs'), dgram = require('dgram');
6
5
  const ntp = require('../utils/ntp').default ?? require('../utils/ntp');
7
6
  const config = require('../utils/config').default ?? require('../utils/config');
@@ -93,6 +92,7 @@ function Client(volume, password, audioOut, options) {
93
92
  this.activeRemote = nu.randomInt(9).toString().toUpperCase();
94
93
  this.dacpId = "04F8191D99BEC6E9";
95
94
  this.session = null;
95
+ this.readySent = false;
96
96
  this.timeout = null;
97
97
  this.volume = volume;
98
98
  this.progress = 0;
@@ -152,7 +152,6 @@ function Client(volume, password, audioOut, options) {
152
152
  this.homekitver = this.transient ? "4" : "3";
153
153
  this.metadataReady = false;
154
154
  this.connectAttempts = 0;
155
- this.lastBackoff = 0;
156
155
  }
157
156
  util.inherits(Client, events.EventEmitter);
158
157
  exports.default = { Client };
@@ -222,8 +221,7 @@ Client.prototype.startHandshake = function (udpServers, host, port) {
222
221
  const baseBackOff = Math.min(config.rtsp_retry_base_ms * Math.pow(2, nextAttempt - 1), config.rtsp_retry_max_ms);
223
222
  const jitter = Math.random() * config.rtsp_retry_jitter_ms;
224
223
  const backOff = baseBackOff + jitter;
225
- this.lastBackoff = backOff;
226
- if (this.debug || config.debug_dump)
224
+ if (this.debug)
227
225
  this.logLine?.('rtsp_retry', { attempt: nextAttempt, backOff, code: err?.code });
228
226
  setTimeout(() => {
229
227
  this.startTimeout();
@@ -242,14 +240,8 @@ Client.prototype.startHandshake = function (udpServers, host, port) {
242
240
  this.cleanup('rtsp_socket', err?.code);
243
241
  });
244
242
  this.socket.on('end', () => {
245
- if (this.debug) {
246
- this.logLine?.('rtsp_end', {
247
- lastStatus: rtsp_methods[this.status + 1] ?? this.status,
248
- cseq: this.cseq,
249
- });
250
- }
251
- this.connectAttempts = 0;
252
- this.lastBackoff = 0;
243
+ if (this.debug)
244
+ this.logLine?.('block2');
253
245
  this.cleanup('disconnected');
254
246
  });
255
247
  };
@@ -289,13 +281,6 @@ Client.prototype.setVolume = function (volume, callback) {
289
281
  this.sendNextRequest();
290
282
  };
291
283
  Client.prototype.setProgress = function (progress, duration, callback) {
292
- if (this.airplay2) {
293
- this.progress = progress;
294
- this.duration = duration;
295
- if (typeof callback === 'function')
296
- callback();
297
- return;
298
- }
299
284
  if (this.status !== PLAYING)
300
285
  return;
301
286
  let normProgress = progress;
@@ -341,12 +326,6 @@ Client.prototype.sendHeartBeat = function (callback) {
341
326
  this.sendNextRequest();
342
327
  };
343
328
  Client.prototype.setTrackInfo = function (name, artist, album, callback) {
344
- if (this.airplay2) {
345
- this.trackInfo = { name, artist, album };
346
- if (typeof callback === 'function')
347
- callback();
348
- return;
349
- }
350
329
  if (this.status !== PLAYING)
351
330
  return;
352
331
  if (name != this.trackInfo?.name || artist != this.trackInfo?.artist || album != this.trackInfo?.album) {
@@ -424,7 +403,6 @@ Client.prototype.cleanup = function (type, msg) {
424
403
  this.seed = null;
425
404
  this.credentials = null;
426
405
  // this.password = null;
427
- this.connectAttempts = 0;
428
406
  this.removeAllListeners();
429
407
  if (this.timeout) {
430
408
  clearTimeout(this.timeout);
@@ -840,9 +818,6 @@ Client.prototype.sendNextRequest = async function (di) {
840
818
  request += 'Connection: keep-alive\r\n';
841
819
  }
842
820
  request += 'Apple-Challenge: SdX9kFJVxgKVMFof/Znj4Q\r\n\r\n';
843
- this.socket.write(Buffer.from(request, 'utf-8'));
844
- request = '';
845
- this.status = PLAYING;
846
821
  break;
847
822
  case OPTIONS2:
848
823
  request = '';
@@ -1089,10 +1064,6 @@ Client.prototype.sendNextRequest = async function (di) {
1089
1064
  //this.logLine?.(request);
1090
1065
  break;
1091
1066
  case SETPROGRESS:
1092
- if (this.airplay2) {
1093
- this.status = PLAYING;
1094
- break;
1095
- }
1096
1067
  function hms(seconds) {
1097
1068
  const h = Math.floor(seconds / 3600);
1098
1069
  const m = Math.floor((seconds % 3600) / 60);
@@ -1113,10 +1084,6 @@ Client.prototype.sendNextRequest = async function (di) {
1113
1084
  //this.logLine?.(request);
1114
1085
  break;
1115
1086
  case SETDAAP:
1116
- if (this.airplay2) {
1117
- this.status = PLAYING;
1118
- break;
1119
- }
1120
1087
  let daapenc = true;
1121
1088
  //daapenc = true
1122
1089
  var name = this.daapEncode('minm', this.trackInfo.name, daapenc);
@@ -1165,10 +1132,6 @@ Client.prototype.sendNextRequest = async function (di) {
1165
1132
  return;
1166
1133
  }
1167
1134
  this.startTimeout();
1168
- if (config.debug_dump) {
1169
- // eslint-disable-next-line no-console
1170
- console.debug('rtsp_req', { status: rtsp_methods[this.status] ?? this.status, len: request.length });
1171
- }
1172
1135
  if (this.encryptedChannel && this.credentials) {
1173
1136
  this.socket.write(this.credentials.encrypt(Buffer.concat([Buffer.from(request, 'utf-8')])));
1174
1137
  }
@@ -1225,10 +1188,6 @@ function parseAuthenticate(auth, field) {
1225
1188
  Client.prototype.processData = function (blob, rawData) {
1226
1189
  this.logLine?.('Receiving request:', this.hostip, rtsp_methods[this.status + 1]);
1227
1190
  var response = parseResponse2(blob, this), headers = response.headers || {};
1228
- if (config.debug_dump) {
1229
- // eslint-disable-next-line no-console
1230
- console.debug('rtsp_res', { code: response.code, status: rtsp_methods[this.status + 1], len: rawData.length });
1231
- }
1232
1191
  if (this.debug) {
1233
1192
  try {
1234
1193
  if ((rawData.toString()).includes("bplist00")) {
@@ -1469,9 +1428,7 @@ Client.prototype.processData = function (blob, rawData) {
1469
1428
  return;
1470
1429
  }
1471
1430
  // this.logLine?.("DEBUG: Device Proof=" + this.deviceProof.toString('hex'));
1472
- if (!this.transient) {
1473
- this.srp.checkM2(this.deviceProof);
1474
- }
1431
+ this.srp.checkM2(this.deviceProof);
1475
1432
  if (this.transient == true) {
1476
1433
  this.credentials = new Credentials("sdsds", "", "", "", this.seed);
1477
1434
  this.credentials.writeKey = enc.HKDF("sha512", Buffer.from("Control-Salt"), this.srp.computeK(), Buffer.from("Control-Write-Encryption-Key"), 32);
@@ -1660,11 +1617,18 @@ Client.prototype.processData = function (blob, rawData) {
1660
1617
  case RECORD:
1661
1618
  this.metadataReady = true;
1662
1619
  this.emit('pair_success');
1663
- if (!this.airplay2) {
1620
+ if (this.airplay2) {
1621
+ // AirPlay2 may not send FLUSH after SETPROGRESS; ensure session exists and start relay once.
1622
+ this.session = this.session ?? "1";
1623
+ if (!this.readySent) {
1624
+ this.readySent = true;
1625
+ this.emit('ready');
1626
+ }
1627
+ }
1628
+ else {
1664
1629
  this.session = this.session ?? "1";
1665
1630
  this.emit('ready');
1666
1631
  }
1667
- ;
1668
1632
  this.status = SETVOLUME;
1669
1633
  break;
1670
1634
  case SETVOLUME:
@@ -1685,7 +1649,7 @@ Client.prototype.processData = function (blob, rawData) {
1685
1649
  }
1686
1650
  break;
1687
1651
  case SETPROGRESS:
1688
- // After reporting progress, stay in PLAYING; avoid forcing FLUSH on every update.
1652
+ // Keep PLAYING to avoid forcing FLUSH on every progress update.
1689
1653
  this.status = PLAYING;
1690
1654
  break;
1691
1655
  case SETDAAP:
@@ -133,112 +133,21 @@ class UDPServers extends node_events_1.EventEmitter {
133
133
  /**
134
134
  * Send an RTCP sync packet to a receiver to align playback.
135
135
  */
136
- sendControlSync(seq, dev, tsOffsetFrames = 0, sr, rr, xr) {
136
+ sendControlSync(seq, dev) {
137
137
  if (this.status !== BOUND || !this.control.socket)
138
138
  return;
139
139
  const packet = Buffer.alloc(20);
140
140
  packet.writeUInt16BE(0x80d4, 0);
141
141
  packet.writeUInt16BE(0x0007, 2);
142
- packet.writeUInt32BE((0, numUtil_1.low32)((seq + tsOffsetFrames) * config_1.default.frames_per_packet), 4);
142
+ packet.writeUInt32BE((0, numUtil_1.low32)(seq * config_1.default.frames_per_packet), 4);
143
143
  const ntpTime = ntp_1.default.timestamp();
144
144
  ntpTime.copy(packet, 8);
145
- packet.writeUInt32BE((0, numUtil_1.low32)((seq + tsOffsetFrames) * config_1.default.frames_per_packet + config_1.default.sampling_rate * 2), 16);
145
+ packet.writeUInt32BE((0, numUtil_1.low32)(seq * config_1.default.frames_per_packet + config_1.default.sampling_rate * 2), 16);
146
146
  const delay = Math.max(0, config_1.default.control_sync_base_delay_ms +
147
147
  Math.random() * config_1.default.control_sync_jitter_ms);
148
148
  setTimeout(() => {
149
- if (config_1.default.debug_dump) {
150
- // eslint-disable-next-line no-console
151
- console.debug('rtcp_sync', { seq, tsOffsetFrames, host: dev.host, port: dev.controlPort });
152
- }
153
149
  this.control.socket?.send(packet, 0, packet.length, dev.controlPort, dev.host);
154
- if (sr && config_1.default.send_rtcp_sr) {
155
- const srPacket = this.buildSenderReport(sr);
156
- if (config_1.default.debug_dump) {
157
- // eslint-disable-next-line no-console
158
- console.debug('rtcp_sr', { ssrc: config_1.default.device_magic, rtp: sr.rtpTimestamp, packets: sr.packetCount });
159
- }
160
- this.control.socket?.send(srPacket, 0, srPacket.length, dev.controlPort, dev.host);
161
- }
162
- if (config_1.default.send_rtcp_rr) {
163
- const rrPacket = this.buildReceiverReport(rr);
164
- if (config_1.default.debug_dump) {
165
- // eslint-disable-next-line no-console
166
- console.debug('rtcp_rr', { ssrc: config_1.default.device_magic });
167
- }
168
- this.control.socket?.send(rrPacket, 0, rrPacket.length, dev.controlPort, dev.host);
169
- }
170
- if (xr && config_1.default.send_rtcp_xr) {
171
- const xrPacket = this.buildExtendedReport(xr);
172
- if (config_1.default.debug_dump) {
173
- // eslint-disable-next-line no-console
174
- console.debug('rtcp_xr', { ssrc: config_1.default.device_magic });
175
- }
176
- this.control.socket?.send(xrPacket, 0, xrPacket.length, dev.controlPort, dev.host);
177
- }
178
150
  }, delay);
179
151
  }
180
- buildSenderReport(counters) {
181
- const sr = Buffer.alloc(28);
182
- // V=2, P=0, RC=0
183
- sr.writeUInt8(0x80, 0);
184
- // PT=200 (SR)
185
- sr.writeUInt8(200, 1);
186
- // length in 32-bit words minus 1 -> 6 words (28 bytes) => 6
187
- sr.writeUInt16BE(6, 2);
188
- // SSRC
189
- sr.writeUInt32BE(config_1.default.device_magic, 4);
190
- // NTP timestamp
191
- counters.ntp.copy(sr, 8, 0, 8);
192
- // RTP timestamp
193
- sr.writeUInt32BE((0, numUtil_1.low32)(counters.rtpTimestamp), 16);
194
- // packet count
195
- sr.writeUInt32BE((0, numUtil_1.low32)(counters.packetCount), 20);
196
- // octet count
197
- sr.writeUInt32BE((0, numUtil_1.low32)(counters.octetCount), 24);
198
- return sr;
199
- }
200
- buildReceiverReport(rr) {
201
- const packet = Buffer.alloc(8);
202
- // V=2, P=0, RC=0
203
- packet.writeUInt8(0x80, 0);
204
- // PT=201 (RR)
205
- packet.writeUInt8(201, 1);
206
- // length = 1 (8 bytes / 4 - 1)
207
- packet.writeUInt16BE(1, 2);
208
- // SSRC
209
- packet.writeUInt32BE(rr?.ssrc ?? config_1.default.device_magic, 4);
210
- return packet;
211
- }
212
- buildExtendedReport(xr) {
213
- // XR with RRT (Receiver Reference Time) and optional DLRR.
214
- const rrtBlock = Buffer.alloc(12);
215
- // BT=4 (RRT), reserved+block length=2 (8 octets following header)
216
- rrtBlock.writeUInt8(4, 0);
217
- rrtBlock.writeUInt8(0, 1);
218
- rrtBlock.writeUInt16BE(2, 2);
219
- (xr?.ntp ?? ntp_1.default.timestamp()).copy(rrtBlock, 4, 0, 8);
220
- let dlrrBlock = null;
221
- if (typeof xr?.lastRr === 'number' || typeof xr?.delaySinceLastRr === 'number') {
222
- dlrrBlock = Buffer.alloc(12);
223
- // BT=5 (DLRR)
224
- dlrrBlock.writeUInt8(5, 0);
225
- dlrrBlock.writeUInt8(0, 1);
226
- dlrrBlock.writeUInt16BE(3, 2); // block length (3 words = 12 bytes following header)
227
- dlrrBlock.writeUInt32BE(xr?.lastRr ?? 0, 4);
228
- dlrrBlock.writeUInt32BE(xr?.delaySinceLastRr ?? 0, 8);
229
- }
230
- const blocks = dlrrBlock ? Buffer.concat([rrtBlock, dlrrBlock]) : rrtBlock;
231
- const packet = Buffer.alloc(8 + blocks.length);
232
- // V=2, P=0, RC=0
233
- packet.writeUInt8(0x80, 0);
234
- // PT=207 (XR)
235
- packet.writeUInt8(207, 1);
236
- // length in 32-bit words minus 1
237
- packet.writeUInt16BE(packet.length / 4 - 1, 2);
238
- // SSRC
239
- packet.writeUInt32BE(xr?.ssrc ?? config_1.default.device_magic, 4);
240
- blocks.copy(packet, 8);
241
- return packet;
242
- }
243
152
  }
244
153
  exports.default = UDPServers;
@@ -90,21 +90,11 @@ class Credentials {
90
90
  }
91
91
  return result;
92
92
  }
93
- encryptAudio(message, aad, seq) {
94
- const nonce = Buffer.alloc(12, 0);
95
- nonce.writeUInt16BE(seq & 0xffff, 4);
96
- const [cipher, tag] = encryption_1.default.encryptAndSeal(message, aad, nonce, this.writeKey);
97
- // Reference appends auth tag and nonce (without leading zeros)
98
- return Buffer.concat([cipher, tag, nonce.slice(4)]);
99
- }
100
- rotateKeys() {
101
- const info = Buffer.from('AirPlay:Audio', 'utf-8'); // reference-style info
102
- const salt = Buffer.alloc(16, 0); // deterministic salt like reference
103
- const newKey = encryption_1.default.HKDF('sha256', salt, this.encryptionKey, info, 32);
104
- this.writeKey = newKey;
105
- this.readKey = newKey;
106
- this.encryptCount = 0;
107
- this.decryptCount = 0;
93
+ encryptAudio(message, aad, nonce) {
94
+ return Buffer.concat([
95
+ Buffer.concat(encryption_1.default.encryptAndSeal(message, aad, struct.pack("Q", nonce), this.writeKey)),
96
+ Buffer.from(struct.pack("Q", nonce)),
97
+ ]);
108
98
  }
109
99
  }
110
100
  exports.Credentials = Credentials;
package/dist/esm/index.js CHANGED
@@ -42,7 +42,6 @@ const node_events_1 = require("node:events");
42
42
  // Airtunes implementation (node_airtunes2 port) in src/core.
43
43
  const index_1 = __importDefault(require("./core/index"));
44
44
  const config_1 = __importStar(require("./utils/config"));
45
- const ntp_1 = require("./utils/ntp");
46
45
  class LoxAirplaySender extends node_events_1.EventEmitter {
47
46
  airtunes = null;
48
47
  deviceKey = null;
@@ -74,10 +73,6 @@ class LoxAirplaySender extends node_events_1.EventEmitter {
74
73
  this.airtunes = new index_1.default({
75
74
  packetSize: config_1.default.packet_size,
76
75
  startTimeMs: options.startTimeMs,
77
- startTimeNtp: options.startTimeNtp ? (0, ntp_1.toNtpTimestamp)(options.startTimeNtp) : undefined,
78
- deviceMagic: options.deviceMagic,
79
- resendBufferSize: options.resendBufferSize,
80
- underrunMuteMs: options.underrunMuteMs,
81
76
  config: options.config,
82
77
  });
83
78
  this.airtunes.on('device', (key, status, desc) => {
@@ -24,7 +24,6 @@ class CircularBuffer extends node_events_1.EventEmitter {
24
24
  buffers = [];
25
25
  currentSize = 0;
26
26
  status = WAITING;
27
- hadUnderrun = false;
28
27
  constructor(packetsInBuffer, packetSize) {
29
28
  super();
30
29
  this.packetPool = new packetPool_1.default(packetSize);
@@ -64,10 +63,6 @@ class CircularBuffer extends node_events_1.EventEmitter {
64
63
  this.status !== ENDED &&
65
64
  (this.status === FILLING || this.currentSize < this.packetSize)) {
66
65
  packet.pcm.fill(0);
67
- if (!this.hadUnderrun) {
68
- this.hadUnderrun = true;
69
- this.emit('underrun');
70
- }
71
66
  if (this.status !== FILLING && this.status !== WAITING) {
72
67
  this.status = FILLING;
73
68
  this.emit('status', 'buffering');
@@ -97,9 +92,6 @@ class CircularBuffer extends node_events_1.EventEmitter {
97
92
  }
98
93
  }
99
94
  this.currentSize -= this.packetSize;
100
- if (this.hadUnderrun && this.currentSize >= this.packetSize) {
101
- this.hadUnderrun = false;
102
- }
103
95
  if (this.status === ENDING && this.currentSize <= 0) {
104
96
  this.status = ENDED;
105
97
  this.currentSize = 0;
@@ -17,9 +17,9 @@ exports.config = {
17
17
  coreaudio_check_period: 2000, // CoreAudio buffer level check period
18
18
  coreaudio_preload: 352 * 2 * 2 * 50, // ~0.5s of silence pushed to CoreAudio to avoid draining AudioQueue
19
19
  sampling_rate: 44100, // fixed by AirTunes v2
20
- sync_period: 0, // UDP sync packets are sent to all AirTunes devices regularly (0 = derive from sample rate)
20
+ sync_period: 126, // UDP sync packets are sent to all AirTunes devices regularly
21
21
  stream_latency: 200, // audio UDP packets are flushed in bursts periodically
22
- rtsp_timeout: 2147483647, // RTSP servers are considered gone if no reply is received before the timeout (legacy default)
22
+ rtsp_timeout: 15000, // RTSP servers are considered gone if no reply is received before the timeout
23
23
  rtsp_heartbeat: 15000, // some RTSP (like HomePod) servers requires heartbeat.
24
24
  rtsp_retry_attempts: 3,
25
25
  rtsp_retry_base_ms: 300,
@@ -27,17 +27,7 @@ exports.config = {
27
27
  rtsp_retry_jitter_ms: 150,
28
28
  control_sync_base_delay_ms: 2,
29
29
  control_sync_jitter_ms: 3,
30
- use_monotonic_clock: true,
31
- jump_forward_enabled: false,
32
- jump_forward_threshold_ms: 180,
33
- jump_forward_lead_ms: 220,
34
30
  device_magic: (0, numUtil_1.randomInt)(9),
35
- resend_buffer_size: 1000,
36
- send_rtcp_sr: true,
37
- send_rtcp_rr: true,
38
- send_rtcp_xr: false,
39
- underrun_mute_ms: 50,
40
- debug_dump: false,
41
31
  ntp_epoch: 0x83aa7e80,
42
32
  iv_base64: 'ePRBLI0XN5ArFaaz7ncNZw',
43
33
  rsa_aeskey_base64: 'VjVbxWcmYgbBbhwBNlCh3K0CMNtWoB844BuiHGUJT51zQS7SDpMnlbBIobsKbfEJ3SCgWHRXjYWf7VQWRYtEcfx7ejA8xDIk5PSBYTvXP5dU2QoGrSBv0leDS6uxlEWuxBq3lIxCxpWO2YswHYKJBt06Uz9P2Fq2hDUwl3qOQ8oXb0OateTKtfXEwHJMprkhsJsGDrIc5W5NJFMAo6zCiM9bGSDeH2nvTlyW6bfI/Q0v0cDGUNeY3ut6fsoafRkfpCwYId+bg3diJh+uzw5htHDyZ2sN+BFYHzEfo8iv4KDxzeya9llqg6fRNQ8d5YjpvTnoeEQ9ye9ivjkBjcAfVw',