@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.
|
@@ -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
|
|
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
|
|
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),
|
|
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
|
|
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
|
|
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),
|
|
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,
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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,
|
|
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,
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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