@lox-audioserver/node-airplay-sender 0.4.0 → 0.4.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.
@@ -65,8 +65,6 @@ type AirTunesDeviceInstance = EventEmitter & {
65
65
  relayAudio: () => void;
66
66
  cleanup: () => void;
67
67
  onUnderrun?: () => void;
68
- audioNonce: number;
69
- packetsSent: number;
70
68
  };
71
69
  /**
72
70
  * Construct a RAOP/AirPlay device handler.
@@ -133,6 +133,7 @@ function AirTunesDevice(host, audioOut, options, mode = 0, txt = '') {
133
133
  this.airplay2 = options?.airplay2 ?? false;
134
134
  this.txt = Array.isArray(txt) ? txt : txt ? [String(txt)] : [];
135
135
  this.borkedshp = false;
136
+ this.transient = false;
136
137
  if (this.airplay2 && this.mode === 0) {
137
138
  this.mode = 2;
138
139
  }
@@ -165,6 +166,9 @@ function AirTunesDevice(host, audioOut, options, mode = 0, txt = '') {
165
166
  ...(features_set.length > 0 ? parseInt(features_set[0], 10).toString(2).split('') : []),
166
167
  ...(features_set.length > 1 ? parseInt(features_set[1], 10).toString(2).split('') : []),
167
168
  ];
169
+ if (this.features.length > 0) {
170
+ this.transient = this.features[this.features.length - 1 - 48] === '1';
171
+ }
168
172
  if (this.statusflags.length) {
169
173
  let PasswordRequired = (this.statusflags[this.statusflags.length - 1 - 7] == '1');
170
174
  let PinRequired = (this.statusflags[this.statusflags.length - 1 - 3] == '1');
@@ -172,6 +176,10 @@ function AirTunesDevice(host, audioOut, options, mode = 0, txt = '') {
172
176
  // console.debug('needPss', PasswordRequired, PinRequired, OneTimePairingRequired);
173
177
  this.needPassword = PasswordRequired;
174
178
  this.needPin = (PinRequired || OneTimePairingRequired);
179
+ this.transient = !(PasswordRequired || PinRequired || OneTimePairingRequired);
180
+ }
181
+ if (this.airplay2 && this.statusflags.length === 0 && !this.needPassword && !this.needPin) {
182
+ this.transient = true;
175
183
  }
176
184
  // console.debug('transient', this.transient);
177
185
  // detect old shairports with broken text
@@ -220,17 +228,14 @@ function AirTunesDevice(host, audioOut, options, mode = 0, txt = '') {
220
228
  this.audioCallback = null;
221
229
  this.encoder = [];
222
230
  this.credentials = null;
223
- this.audioNonce = Math.floor(Math.random() * 0xffffffff);
224
231
  this.onUnderrun = () => {
225
232
  this.encoder = [];
226
233
  this.logLine?.('underrun_encoder_reset');
227
- this.audioNonce = Math.floor(Math.random() * 0xffffffff);
228
234
  if (this.credentials) {
229
235
  this.credentials.encryptCount = 0;
230
236
  this.credentials.decryptCount = 0;
231
237
  }
232
238
  };
233
- this.packetsSent = 0;
234
239
  // this.func = `
235
240
  // const {Worker, isMainThread, parentPort, workerData} = require('node:worker_threads');
236
241
  // var { WebSocketServer } = require('ws');
@@ -336,9 +341,7 @@ AirTunesDevice.prototype.relayAudio = function () {
336
341
  return;
337
342
  }
338
343
  this.audioCallback = (packet) => {
339
- const airTunes = makeAirTunesPacket(packet, this.encoder, this.requireEncryption, this.alacEncoding, this.credentials, this.inputCodec, this.deviceMagic, this.audioNonce);
340
- this.packetsSent += 1;
341
- this.audioNonce = (this.audioNonce + 1) >>> 0;
344
+ const airTunes = makeAirTunesPacket(packet, this.encoder, this.requireEncryption, this.alacEncoding, this.credentials, this.inputCodec, this.deviceMagic);
342
345
  try {
343
346
  this.audioPacketHistory?.add(packet.seq, airTunes);
344
347
  }
@@ -411,11 +414,6 @@ AirTunesDevice.prototype.cleanup = function () {
411
414
  this.audioCallback = null;
412
415
  }
413
416
  this.encoder = [];
414
- this.audioNonce = Math.floor(Math.random() * 0xffffffff);
415
- if (this.credentials) {
416
- this.credentials.rotateKeys?.();
417
- }
418
- this.packetsSent = 0;
419
417
  this.udpServers.close();
420
418
  this.removeAllListeners();
421
419
  this.rtsp = null;
@@ -465,7 +463,7 @@ AirTunesDevice.prototype.setPasscode = function (password) {
465
463
  this.rtsp.setPasscode(password);
466
464
  };
467
465
  exports.default = AirTunesDevice;
468
- function makeAirTunesPacket(packet, encoder, requireEncryption, alacEncoding = true, credentials = null, inputCodec = 'pcm', deviceMagic = config_1.default.device_magic, audioNonce = packet.seq) {
466
+ function makeAirTunesPacket(packet, encoder, requireEncryption, alacEncoding = true, credentials = null, inputCodec = 'pcm', deviceMagic = config_1.default.device_magic) {
469
467
  const useAlacInput = inputCodec === 'alac';
470
468
  var alac = useAlacInput
471
469
  ? packet.pcm
@@ -478,7 +476,7 @@ function makeAirTunesPacket(packet, encoder, requireEncryption, alacEncoding = t
478
476
  alac = encryptAES(alac, alac.length);
479
477
  }
480
478
  if (credentials) {
481
- let pcm = credentials.encryptAudio(alac, header.slice(4, 12), audioNonce);
479
+ let pcm = credentials.encryptAudio(alac, header.slice(4, 12), packet.seq);
482
480
  let airplay = Buffer.alloc(RTP_HEADER_SIZE + pcm.length);
483
481
  header.copy(airplay);
484
482
  pcm.copy(airplay, RTP_HEADER_SIZE);
@@ -133,6 +133,7 @@ function AirTunesDevice(host, audioOut, options, mode = 0, txt = '') {
133
133
  this.airplay2 = options?.airplay2 ?? false;
134
134
  this.txt = Array.isArray(txt) ? txt : txt ? [String(txt)] : [];
135
135
  this.borkedshp = false;
136
+ this.transient = false;
136
137
  if (this.airplay2 && this.mode === 0) {
137
138
  this.mode = 2;
138
139
  }
@@ -165,6 +166,9 @@ function AirTunesDevice(host, audioOut, options, mode = 0, txt = '') {
165
166
  ...(features_set.length > 0 ? parseInt(features_set[0], 10).toString(2).split('') : []),
166
167
  ...(features_set.length > 1 ? parseInt(features_set[1], 10).toString(2).split('') : []),
167
168
  ];
169
+ if (this.features.length > 0) {
170
+ this.transient = this.features[this.features.length - 1 - 48] === '1';
171
+ }
168
172
  if (this.statusflags.length) {
169
173
  let PasswordRequired = (this.statusflags[this.statusflags.length - 1 - 7] == '1');
170
174
  let PinRequired = (this.statusflags[this.statusflags.length - 1 - 3] == '1');
@@ -172,6 +176,10 @@ function AirTunesDevice(host, audioOut, options, mode = 0, txt = '') {
172
176
  // console.debug('needPss', PasswordRequired, PinRequired, OneTimePairingRequired);
173
177
  this.needPassword = PasswordRequired;
174
178
  this.needPin = (PinRequired || OneTimePairingRequired);
179
+ this.transient = !(PasswordRequired || PinRequired || OneTimePairingRequired);
180
+ }
181
+ if (this.airplay2 && this.statusflags.length === 0 && !this.needPassword && !this.needPin) {
182
+ this.transient = true;
175
183
  }
176
184
  // console.debug('transient', this.transient);
177
185
  // detect old shairports with broken text
@@ -220,17 +228,14 @@ function AirTunesDevice(host, audioOut, options, mode = 0, txt = '') {
220
228
  this.audioCallback = null;
221
229
  this.encoder = [];
222
230
  this.credentials = null;
223
- this.audioNonce = Math.floor(Math.random() * 0xffffffff);
224
231
  this.onUnderrun = () => {
225
232
  this.encoder = [];
226
233
  this.logLine?.('underrun_encoder_reset');
227
- this.audioNonce = Math.floor(Math.random() * 0xffffffff);
228
234
  if (this.credentials) {
229
235
  this.credentials.encryptCount = 0;
230
236
  this.credentials.decryptCount = 0;
231
237
  }
232
238
  };
233
- this.packetsSent = 0;
234
239
  // this.func = `
235
240
  // const {Worker, isMainThread, parentPort, workerData} = require('node:worker_threads');
236
241
  // var { WebSocketServer } = require('ws');
@@ -336,9 +341,7 @@ AirTunesDevice.prototype.relayAudio = function () {
336
341
  return;
337
342
  }
338
343
  this.audioCallback = (packet) => {
339
- const airTunes = makeAirTunesPacket(packet, this.encoder, this.requireEncryption, this.alacEncoding, this.credentials, this.inputCodec, this.deviceMagic, this.audioNonce);
340
- this.packetsSent += 1;
341
- this.audioNonce = (this.audioNonce + 1) >>> 0;
344
+ const airTunes = makeAirTunesPacket(packet, this.encoder, this.requireEncryption, this.alacEncoding, this.credentials, this.inputCodec, this.deviceMagic);
342
345
  try {
343
346
  this.audioPacketHistory?.add(packet.seq, airTunes);
344
347
  }
@@ -411,11 +414,6 @@ AirTunesDevice.prototype.cleanup = function () {
411
414
  this.audioCallback = null;
412
415
  }
413
416
  this.encoder = [];
414
- this.audioNonce = Math.floor(Math.random() * 0xffffffff);
415
- if (this.credentials) {
416
- this.credentials.rotateKeys?.();
417
- }
418
- this.packetsSent = 0;
419
417
  this.udpServers.close();
420
418
  this.removeAllListeners();
421
419
  this.rtsp = null;
@@ -465,7 +463,7 @@ AirTunesDevice.prototype.setPasscode = function (password) {
465
463
  this.rtsp.setPasscode(password);
466
464
  };
467
465
  exports.default = AirTunesDevice;
468
- function makeAirTunesPacket(packet, encoder, requireEncryption, alacEncoding = true, credentials = null, inputCodec = 'pcm', deviceMagic = config_1.default.device_magic, audioNonce = packet.seq) {
466
+ function makeAirTunesPacket(packet, encoder, requireEncryption, alacEncoding = true, credentials = null, inputCodec = 'pcm', deviceMagic = config_1.default.device_magic) {
469
467
  const useAlacInput = inputCodec === 'alac';
470
468
  var alac = useAlacInput
471
469
  ? packet.pcm
@@ -478,7 +476,7 @@ function makeAirTunesPacket(packet, encoder, requireEncryption, alacEncoding = t
478
476
  alac = encryptAES(alac, alac.length);
479
477
  }
480
478
  if (credentials) {
481
- let pcm = credentials.encryptAudio(alac, header.slice(4, 12), audioNonce);
479
+ let pcm = credentials.encryptAudio(alac, header.slice(4, 12), packet.seq);
482
480
  let airplay = Buffer.alloc(RTP_HEADER_SIZE + pcm.length);
483
481
  header.copy(airplay);
484
482
  pcm.copy(airplay, RTP_HEADER_SIZE);
@@ -90,11 +90,12 @@ class Credentials {
90
90
  }
91
91
  return result;
92
92
  }
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
- ]);
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)]);
98
99
  }
99
100
  rotateKeys() {
100
101
  const info = Buffer.from('AirPlay:Audio', 'utf-8'); // reference-style info
@@ -25,7 +25,7 @@ declare class Credentials {
25
25
  toString(): string;
26
26
  encrypt(message: Buffer): Buffer;
27
27
  decrypt(message: Buffer): Buffer;
28
- encryptAudio(message: Buffer, aad: Buffer | null, nonce: number): Buffer;
28
+ encryptAudio(message: Buffer, aad: Buffer | null, seq: number): Buffer;
29
29
  rotateKeys(): void;
30
30
  }
31
31
  export { Credentials };
@@ -90,11 +90,12 @@ class Credentials {
90
90
  }
91
91
  return result;
92
92
  }
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
- ]);
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)]);
98
99
  }
99
100
  rotateKeys() {
100
101
  const info = Buffer.from('AirPlay:Audio', 'utf-8'); // reference-style info
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lox-audioserver/node-airplay-sender",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "description": "AirPlay sender (RAOP/AirPlay 1; AirPlay 2 control/auth, best-effort audio)",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",