lox-airplay-sender 0.1.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/core/deviceAirtunes.js +37 -37
- package/dist/core/rtsp.js +175 -106
- package/dist/core/udpServers.js +5 -1
- package/dist/esm/core/deviceAirtunes.js +37 -37
- package/dist/esm/core/rtsp.js +175 -106
- package/dist/esm/core/udpServers.js +5 -1
- package/dist/esm/utils/config.js +7 -1
- package/dist/utils/config.d.ts +6 -0
- package/dist/utils/config.js +7 -1
- package/package.json +1 -1
|
@@ -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',
|
|
246
|
+
this.udpServers.on('ports', (err) => {
|
|
248
247
|
if (err) {
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
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
|
-
|
|
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
|
-
|
|
277
|
+
this.logLine?.(e);
|
|
280
278
|
}
|
|
281
279
|
}
|
|
282
280
|
if (!this.rtsp) {
|
|
283
281
|
return;
|
|
284
282
|
}
|
|
285
|
-
this.rtsp.on('config',
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
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
|
-
|
|
291
|
+
this.audioOut?.setLatencyFrames?.(this.audioLatency);
|
|
294
292
|
}
|
|
295
293
|
catch {
|
|
296
294
|
/* ignore */
|
|
297
295
|
}
|
|
298
296
|
});
|
|
299
|
-
this.rtsp.on('ready',
|
|
300
|
-
|
|
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',
|
|
303
|
-
|
|
303
|
+
this.rtsp.on('need_password', () => {
|
|
304
|
+
this.emit('status', 'need_password');
|
|
304
305
|
});
|
|
305
|
-
this.rtsp.on('pair_failed',
|
|
306
|
-
|
|
306
|
+
this.rtsp.on('pair_failed', () => {
|
|
307
|
+
this.emit('status', 'pair_failed');
|
|
307
308
|
});
|
|
308
|
-
this.rtsp.on('pair_success',
|
|
309
|
-
|
|
309
|
+
this.rtsp.on('pair_success', () => {
|
|
310
|
+
this.emit('status', 'pair_success');
|
|
310
311
|
});
|
|
311
|
-
this.rtsp.on('end',
|
|
312
|
-
|
|
313
|
-
|
|
312
|
+
this.rtsp.on('end', (err) => {
|
|
313
|
+
this.logLine?.(err);
|
|
314
|
+
this.cleanup();
|
|
314
315
|
if (err !== 'stopped')
|
|
315
|
-
|
|
316
|
+
this.emit(err);
|
|
316
317
|
});
|
|
317
318
|
}
|
|
318
319
|
catch (e) {
|
|
319
|
-
|
|
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 =
|
|
332
|
-
|
|
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 (
|
|
337
|
-
|
|
336
|
+
if (this.audioSocket == null) {
|
|
337
|
+
this.audioSocket = node_dgram_1.default.createSocket('udp4');
|
|
338
338
|
}
|
|
339
|
-
|
|
339
|
+
this.audioSocket.send(airTunes, 0, airTunes.length, this.serverPort, this.host);
|
|
340
340
|
};
|
|
341
341
|
// this.sendAirTunesPacket = function(airTunes) {
|
|
342
342
|
// try{
|
package/dist/core/rtsp.js
CHANGED
|
@@ -150,86 +150,111 @@ function Client(volume, password, audioOut, options) {
|
|
|
150
150
|
this.hostip = null;
|
|
151
151
|
this.homekitver = this.transient ? "4" : "3";
|
|
152
152
|
this.metadataReady = false;
|
|
153
|
+
this.connectAttempts = 0;
|
|
153
154
|
}
|
|
154
155
|
util.inherits(Client, events.EventEmitter);
|
|
155
156
|
exports.default = { Client };
|
|
156
157
|
Client.prototype.startHandshake = function (udpServers, host, port) {
|
|
157
|
-
var self = this;
|
|
158
158
|
this.startTimeout();
|
|
159
159
|
this.hostip = host;
|
|
160
160
|
this.controlPort = udpServers.control.port;
|
|
161
161
|
this.timingPort = udpServers.timing.port;
|
|
162
|
-
this.
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
if (
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
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
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
250
|
+
if (this.timeout) {
|
|
251
|
+
clearTimeout(this.timeout);
|
|
252
|
+
this.timeout = null;
|
|
253
|
+
}
|
|
254
|
+
this.timeout = setTimeout(() => {
|
|
255
|
+
if (this.debug)
|
|
256
|
+
this.logLine?.('timeout');
|
|
257
|
+
this.cleanup('timeout');
|
|
233
258
|
}, config.rtsp_timeout);
|
|
234
259
|
};
|
|
235
260
|
Client.prototype.clearTimeout = function () {
|
|
@@ -280,10 +305,13 @@ Client.prototype.setPasscode = async function (passcode) {
|
|
|
280
305
|
this.sendNextRequest();
|
|
281
306
|
};
|
|
282
307
|
Client.prototype.startHeartBeat = function () {
|
|
283
|
-
|
|
308
|
+
if (this.heartBeat) {
|
|
309
|
+
clearInterval(this.heartBeat);
|
|
310
|
+
this.heartBeat = null;
|
|
311
|
+
}
|
|
284
312
|
if (config.rtsp_heartbeat > 0) {
|
|
285
|
-
this.heartBeat = setInterval(
|
|
286
|
-
|
|
313
|
+
this.heartBeat = setInterval(() => {
|
|
314
|
+
this.sendHeartBeat(() => {
|
|
287
315
|
//this.logLine?.('HeartBeat sent!');
|
|
288
316
|
});
|
|
289
317
|
}, config.rtsp_heartbeat);
|
|
@@ -319,7 +347,6 @@ Client.prototype.setArtwork = function (art, contentType, callback) {
|
|
|
319
347
|
contentType = null;
|
|
320
348
|
}
|
|
321
349
|
if (typeof art == 'string') {
|
|
322
|
-
var self = this;
|
|
323
350
|
if (contentType === null) {
|
|
324
351
|
var ext = art.slice(-4);
|
|
325
352
|
if (ext == ".jpg" || ext == "jpeg") {
|
|
@@ -332,14 +359,14 @@ Client.prototype.setArtwork = function (art, contentType, callback) {
|
|
|
332
359
|
contentType = "image/gif";
|
|
333
360
|
}
|
|
334
361
|
else {
|
|
335
|
-
return
|
|
362
|
+
return this.cleanup('unknown_art_file_ext');
|
|
336
363
|
}
|
|
337
364
|
}
|
|
338
|
-
return fs.readFile(art,
|
|
365
|
+
return fs.readFile(art, (err, data) => {
|
|
339
366
|
if (err !== null) {
|
|
340
|
-
return
|
|
367
|
+
return this.cleanup('invalid_art_file');
|
|
341
368
|
}
|
|
342
|
-
|
|
369
|
+
this.setArtwork(data, contentType, callback);
|
|
343
370
|
});
|
|
344
371
|
}
|
|
345
372
|
if (contentType === null)
|
|
@@ -388,6 +415,44 @@ Client.prototype.cleanup = function (type, msg) {
|
|
|
388
415
|
this.socket.destroy();
|
|
389
416
|
this.socket = null;
|
|
390
417
|
}
|
|
418
|
+
if (this.eventsocket) {
|
|
419
|
+
try {
|
|
420
|
+
this.eventsocket.destroy?.();
|
|
421
|
+
this.eventsocket = null;
|
|
422
|
+
}
|
|
423
|
+
catch {
|
|
424
|
+
/* ignore */
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
if (this.controlsocket) {
|
|
428
|
+
try {
|
|
429
|
+
this.controlsocket.close();
|
|
430
|
+
}
|
|
431
|
+
catch {
|
|
432
|
+
/* ignore */
|
|
433
|
+
}
|
|
434
|
+
this.controlsocket = null;
|
|
435
|
+
}
|
|
436
|
+
if (this.timingsocket) {
|
|
437
|
+
try {
|
|
438
|
+
this.timingsocket.close();
|
|
439
|
+
}
|
|
440
|
+
catch {
|
|
441
|
+
/* ignore */
|
|
442
|
+
}
|
|
443
|
+
this.timingsocket = null;
|
|
444
|
+
}
|
|
445
|
+
const audioSocket = this.audioSocket;
|
|
446
|
+
if (audioSocket) {
|
|
447
|
+
try {
|
|
448
|
+
audioSocket.close?.();
|
|
449
|
+
audioSocket.destroy?.();
|
|
450
|
+
}
|
|
451
|
+
catch {
|
|
452
|
+
/* ignore */
|
|
453
|
+
}
|
|
454
|
+
this.audioSocket = null;
|
|
455
|
+
}
|
|
391
456
|
};
|
|
392
457
|
function parseResponse(blob) {
|
|
393
458
|
var response = {}, lines = blob.split('\r\n');
|
|
@@ -531,10 +596,8 @@ Client.prototype.sendNextRequest = async function (di) {
|
|
|
531
596
|
this.socket.write(Buffer.from(request, 'utf-8'));
|
|
532
597
|
}
|
|
533
598
|
else {
|
|
534
|
-
this.logLine?.("pass", this.password);
|
|
535
599
|
if (this.password) {
|
|
536
600
|
this.status = this.airplay2 ? PAIR_SETUP_1 : PAIR_PIN_SETUP_1;
|
|
537
|
-
this.logLine?.("pass2", this.password);
|
|
538
601
|
this.sendNextRequest();
|
|
539
602
|
}
|
|
540
603
|
else {
|
|
@@ -591,8 +654,6 @@ Client.prototype.sendNextRequest = async function (di) {
|
|
|
591
654
|
request += this.makeHead("POST", "/pair-verify", "", true);
|
|
592
655
|
request += 'Content-Type: application/octet-stream\r\n';
|
|
593
656
|
this.pair_verify_1_verifier = ATVAuthenticator.verifier(this.authSecret);
|
|
594
|
-
if (this.debug)
|
|
595
|
-
this.logLine?.(this.authSecret);
|
|
596
657
|
request += 'Content-Length:' + Buffer.byteLength(this.pair_verify_1_verifier.verifierBody) + '\r\n\r\n';
|
|
597
658
|
this.socket.write(Buffer.concat([Buffer.from(request, 'utf-8'), this.pair_verify_1_verifier.verifierBody]));
|
|
598
659
|
request = '';
|
|
@@ -846,8 +907,7 @@ Client.prototype.sendNextRequest = async function (di) {
|
|
|
846
907
|
catch (e) { }
|
|
847
908
|
try {
|
|
848
909
|
this.timingsocket = dgram.createSocket({ type: 'udp4', reuseAddr: true });
|
|
849
|
-
|
|
850
|
-
this.timingsocket.on('message', function (msg, rinfo) {
|
|
910
|
+
this.timingsocket.on('message', (msg, rinfo) => {
|
|
851
911
|
// only listen and respond on own hosts
|
|
852
912
|
// if (this.hosts.indexOf(rinfo.address) < 0) return;
|
|
853
913
|
var ts1 = msg.readUInt32BE(24);
|
|
@@ -861,8 +921,8 @@ Client.prototype.sendNextRequest = async function (di) {
|
|
|
861
921
|
var ntpTime = ntp.timestamp();
|
|
862
922
|
ntpTime.copy(reply, 16);
|
|
863
923
|
ntpTime.copy(reply, 24);
|
|
864
|
-
|
|
865
|
-
|
|
924
|
+
this.timingsocket.send(reply, 0, reply.length, rinfo.port, rinfo.address);
|
|
925
|
+
this.logLine?.('timing socket pinged', rinfo.port, rinfo.address);
|
|
866
926
|
});
|
|
867
927
|
this.timingsocket.bind(this.timingPort, this.socket.address().address);
|
|
868
928
|
}
|
|
@@ -916,9 +976,8 @@ Client.prototype.sendNextRequest = async function (di) {
|
|
|
916
976
|
}] });
|
|
917
977
|
request += 'Content-Length: ' + Buffer.byteLength(setap2) + '\r\n\r\n';
|
|
918
978
|
this.controlsocket = dgram.createSocket({ type: 'udp4', reuseAddr: true });
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
self.logLine?.('controlsocket.data', msg);
|
|
979
|
+
this.controlsocket.on('message', (msg) => {
|
|
980
|
+
this.logLine?.('controlsocket.data', msg);
|
|
922
981
|
});
|
|
923
982
|
this.controlsocket.bind(this.controlPort, this.socket.address().address);
|
|
924
983
|
let s2ct = this.credentials.encrypt(Buffer.concat([Buffer.from(request, 'utf-8'), setap2]));
|
|
@@ -928,18 +987,29 @@ Client.prototype.sendNextRequest = async function (di) {
|
|
|
928
987
|
case RECORD:
|
|
929
988
|
//this.logLine?.(request);
|
|
930
989
|
if (this.airplay2) {
|
|
931
|
-
this.eventsocket = net.connect(this.eventPort, this.hostip, async function () {
|
|
932
|
-
});
|
|
933
|
-
this.eventsocket.on('data', function (data) {
|
|
934
|
-
self.logLine?.('eventsocket.data', data);
|
|
935
|
-
self.logLine?.('eventsocket.data2', self.event_credentials.decrypt(data).toString());
|
|
936
|
-
});
|
|
937
|
-
this.eventsocket.on('error', function (err) {
|
|
938
|
-
self.logLine?.('eventsocket.error', err);
|
|
939
|
-
});
|
|
940
990
|
this.event_credentials = new Credentials("sdsds", "", "", "", this.seed);
|
|
941
991
|
this.event_credentials.writeKey = enc.HKDF("sha512", Buffer.from("Events-Salt"), this.srp.computeK(), Buffer.from("Events-Read-Encryption-Key"), 32);
|
|
942
992
|
this.event_credentials.readKey = enc.HKDF("sha512", Buffer.from("Events-Salt"), this.srp.computeK(), Buffer.from("Events-Write-Encryption-Key"), 32);
|
|
993
|
+
this.eventsocket = net.connect(this.eventPort, this.hostip, async () => {
|
|
994
|
+
});
|
|
995
|
+
this.eventsocket.on('data', (data) => {
|
|
996
|
+
if (this.debug) {
|
|
997
|
+
this.logLine?.('eventsocket.data', data);
|
|
998
|
+
try {
|
|
999
|
+
const decrypted = this.event_credentials?.decrypt(data);
|
|
1000
|
+
if (decrypted) {
|
|
1001
|
+
this.logLine?.('eventsocket.data2', decrypted.toString());
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
catch (err) {
|
|
1005
|
+
this.logLine?.('eventsocket.decrypt.error', err);
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
});
|
|
1009
|
+
this.eventsocket.on('error', (err) => {
|
|
1010
|
+
if (this.debug)
|
|
1011
|
+
this.logLine?.('eventsocket.error', err);
|
|
1012
|
+
});
|
|
943
1013
|
}
|
|
944
1014
|
if (this.airplay2 != null && this.credentials != null) {
|
|
945
1015
|
// this.controlsocket.close();
|
|
@@ -1117,19 +1187,19 @@ function parseAuthenticate(auth, field) {
|
|
|
1117
1187
|
Client.prototype.processData = function (blob, rawData) {
|
|
1118
1188
|
this.logLine?.('Receiving request:', this.hostip, rtsp_methods[this.status + 1]);
|
|
1119
1189
|
var response = parseResponse2(blob, this), headers = response.headers || {};
|
|
1120
|
-
if (this.debug
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
this.logLine?.("incoming-res:
|
|
1190
|
+
if (this.debug) {
|
|
1191
|
+
try {
|
|
1192
|
+
if ((rawData.toString()).includes("bplist00")) {
|
|
1193
|
+
const buf = Buffer.from(rawData).slice(rawData.length - parseInt(headers['Content-Length']), rawData.length);
|
|
1194
|
+
const bplist = bplistParser.parseBuffer(buf);
|
|
1195
|
+
this.logLine?.("incoming-res:", JSON.stringify(bplist));
|
|
1126
1196
|
}
|
|
1127
|
-
|
|
1128
|
-
this.logLine?.("incoming-res:
|
|
1197
|
+
else {
|
|
1198
|
+
this.logLine?.("incoming-res:", { code: response.code, length: rawData.length });
|
|
1129
1199
|
}
|
|
1130
1200
|
}
|
|
1131
|
-
|
|
1132
|
-
this.logLine?.("incoming-res:
|
|
1201
|
+
catch {
|
|
1202
|
+
this.logLine?.("incoming-res:", { code: response.code, length: rawData.length });
|
|
1133
1203
|
}
|
|
1134
1204
|
}
|
|
1135
1205
|
if (this.status != OPTIONS && this.status != OPTIONS2 && this.mode == 0) {
|
|
@@ -1363,7 +1433,6 @@ Client.prototype.processData = function (blob, rawData) {
|
|
|
1363
1433
|
this.credentials.writeKey = enc.HKDF("sha512", Buffer.from("Control-Salt"), this.srp.computeK(), Buffer.from("Control-Write-Encryption-Key"), 32);
|
|
1364
1434
|
this.credentials.readKey = enc.HKDF("sha512", Buffer.from("Control-Salt"), this.srp.computeK(), Buffer.from("Control-Read-Encryption-Key"), 32);
|
|
1365
1435
|
this.encryptedChannel = true;
|
|
1366
|
-
this.logLine?.(this.srp.computeK());
|
|
1367
1436
|
this.status = SETUP_AP2_1;
|
|
1368
1437
|
}
|
|
1369
1438
|
else {
|
package/dist/core/udpServers.js
CHANGED
|
@@ -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
|
-
|
|
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',
|
|
246
|
+
this.udpServers.on('ports', (err) => {
|
|
248
247
|
if (err) {
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
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
|
-
|
|
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
|
-
|
|
277
|
+
this.logLine?.(e);
|
|
280
278
|
}
|
|
281
279
|
}
|
|
282
280
|
if (!this.rtsp) {
|
|
283
281
|
return;
|
|
284
282
|
}
|
|
285
|
-
this.rtsp.on('config',
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
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
|
-
|
|
291
|
+
this.audioOut?.setLatencyFrames?.(this.audioLatency);
|
|
294
292
|
}
|
|
295
293
|
catch {
|
|
296
294
|
/* ignore */
|
|
297
295
|
}
|
|
298
296
|
});
|
|
299
|
-
this.rtsp.on('ready',
|
|
300
|
-
|
|
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',
|
|
303
|
-
|
|
303
|
+
this.rtsp.on('need_password', () => {
|
|
304
|
+
this.emit('status', 'need_password');
|
|
304
305
|
});
|
|
305
|
-
this.rtsp.on('pair_failed',
|
|
306
|
-
|
|
306
|
+
this.rtsp.on('pair_failed', () => {
|
|
307
|
+
this.emit('status', 'pair_failed');
|
|
307
308
|
});
|
|
308
|
-
this.rtsp.on('pair_success',
|
|
309
|
-
|
|
309
|
+
this.rtsp.on('pair_success', () => {
|
|
310
|
+
this.emit('status', 'pair_success');
|
|
310
311
|
});
|
|
311
|
-
this.rtsp.on('end',
|
|
312
|
-
|
|
313
|
-
|
|
312
|
+
this.rtsp.on('end', (err) => {
|
|
313
|
+
this.logLine?.(err);
|
|
314
|
+
this.cleanup();
|
|
314
315
|
if (err !== 'stopped')
|
|
315
|
-
|
|
316
|
+
this.emit(err);
|
|
316
317
|
});
|
|
317
318
|
}
|
|
318
319
|
catch (e) {
|
|
319
|
-
|
|
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 =
|
|
332
|
-
|
|
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 (
|
|
337
|
-
|
|
336
|
+
if (this.audioSocket == null) {
|
|
337
|
+
this.audioSocket = node_dgram_1.default.createSocket('udp4');
|
|
338
338
|
}
|
|
339
|
-
|
|
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/esm/core/rtsp.js
CHANGED
|
@@ -150,86 +150,111 @@ function Client(volume, password, audioOut, options) {
|
|
|
150
150
|
this.hostip = null;
|
|
151
151
|
this.homekitver = this.transient ? "4" : "3";
|
|
152
152
|
this.metadataReady = false;
|
|
153
|
+
this.connectAttempts = 0;
|
|
153
154
|
}
|
|
154
155
|
util.inherits(Client, events.EventEmitter);
|
|
155
156
|
exports.default = { Client };
|
|
156
157
|
Client.prototype.startHandshake = function (udpServers, host, port) {
|
|
157
|
-
var self = this;
|
|
158
158
|
this.startTimeout();
|
|
159
159
|
this.hostip = host;
|
|
160
160
|
this.controlPort = udpServers.control.port;
|
|
161
161
|
this.timingPort = udpServers.timing.port;
|
|
162
|
-
this.
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
if (
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
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
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
250
|
+
if (this.timeout) {
|
|
251
|
+
clearTimeout(this.timeout);
|
|
252
|
+
this.timeout = null;
|
|
253
|
+
}
|
|
254
|
+
this.timeout = setTimeout(() => {
|
|
255
|
+
if (this.debug)
|
|
256
|
+
this.logLine?.('timeout');
|
|
257
|
+
this.cleanup('timeout');
|
|
233
258
|
}, config.rtsp_timeout);
|
|
234
259
|
};
|
|
235
260
|
Client.prototype.clearTimeout = function () {
|
|
@@ -280,10 +305,13 @@ Client.prototype.setPasscode = async function (passcode) {
|
|
|
280
305
|
this.sendNextRequest();
|
|
281
306
|
};
|
|
282
307
|
Client.prototype.startHeartBeat = function () {
|
|
283
|
-
|
|
308
|
+
if (this.heartBeat) {
|
|
309
|
+
clearInterval(this.heartBeat);
|
|
310
|
+
this.heartBeat = null;
|
|
311
|
+
}
|
|
284
312
|
if (config.rtsp_heartbeat > 0) {
|
|
285
|
-
this.heartBeat = setInterval(
|
|
286
|
-
|
|
313
|
+
this.heartBeat = setInterval(() => {
|
|
314
|
+
this.sendHeartBeat(() => {
|
|
287
315
|
//this.logLine?.('HeartBeat sent!');
|
|
288
316
|
});
|
|
289
317
|
}, config.rtsp_heartbeat);
|
|
@@ -319,7 +347,6 @@ Client.prototype.setArtwork = function (art, contentType, callback) {
|
|
|
319
347
|
contentType = null;
|
|
320
348
|
}
|
|
321
349
|
if (typeof art == 'string') {
|
|
322
|
-
var self = this;
|
|
323
350
|
if (contentType === null) {
|
|
324
351
|
var ext = art.slice(-4);
|
|
325
352
|
if (ext == ".jpg" || ext == "jpeg") {
|
|
@@ -332,14 +359,14 @@ Client.prototype.setArtwork = function (art, contentType, callback) {
|
|
|
332
359
|
contentType = "image/gif";
|
|
333
360
|
}
|
|
334
361
|
else {
|
|
335
|
-
return
|
|
362
|
+
return this.cleanup('unknown_art_file_ext');
|
|
336
363
|
}
|
|
337
364
|
}
|
|
338
|
-
return fs.readFile(art,
|
|
365
|
+
return fs.readFile(art, (err, data) => {
|
|
339
366
|
if (err !== null) {
|
|
340
|
-
return
|
|
367
|
+
return this.cleanup('invalid_art_file');
|
|
341
368
|
}
|
|
342
|
-
|
|
369
|
+
this.setArtwork(data, contentType, callback);
|
|
343
370
|
});
|
|
344
371
|
}
|
|
345
372
|
if (contentType === null)
|
|
@@ -388,6 +415,44 @@ Client.prototype.cleanup = function (type, msg) {
|
|
|
388
415
|
this.socket.destroy();
|
|
389
416
|
this.socket = null;
|
|
390
417
|
}
|
|
418
|
+
if (this.eventsocket) {
|
|
419
|
+
try {
|
|
420
|
+
this.eventsocket.destroy?.();
|
|
421
|
+
this.eventsocket = null;
|
|
422
|
+
}
|
|
423
|
+
catch {
|
|
424
|
+
/* ignore */
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
if (this.controlsocket) {
|
|
428
|
+
try {
|
|
429
|
+
this.controlsocket.close();
|
|
430
|
+
}
|
|
431
|
+
catch {
|
|
432
|
+
/* ignore */
|
|
433
|
+
}
|
|
434
|
+
this.controlsocket = null;
|
|
435
|
+
}
|
|
436
|
+
if (this.timingsocket) {
|
|
437
|
+
try {
|
|
438
|
+
this.timingsocket.close();
|
|
439
|
+
}
|
|
440
|
+
catch {
|
|
441
|
+
/* ignore */
|
|
442
|
+
}
|
|
443
|
+
this.timingsocket = null;
|
|
444
|
+
}
|
|
445
|
+
const audioSocket = this.audioSocket;
|
|
446
|
+
if (audioSocket) {
|
|
447
|
+
try {
|
|
448
|
+
audioSocket.close?.();
|
|
449
|
+
audioSocket.destroy?.();
|
|
450
|
+
}
|
|
451
|
+
catch {
|
|
452
|
+
/* ignore */
|
|
453
|
+
}
|
|
454
|
+
this.audioSocket = null;
|
|
455
|
+
}
|
|
391
456
|
};
|
|
392
457
|
function parseResponse(blob) {
|
|
393
458
|
var response = {}, lines = blob.split('\r\n');
|
|
@@ -531,10 +596,8 @@ Client.prototype.sendNextRequest = async function (di) {
|
|
|
531
596
|
this.socket.write(Buffer.from(request, 'utf-8'));
|
|
532
597
|
}
|
|
533
598
|
else {
|
|
534
|
-
this.logLine?.("pass", this.password);
|
|
535
599
|
if (this.password) {
|
|
536
600
|
this.status = this.airplay2 ? PAIR_SETUP_1 : PAIR_PIN_SETUP_1;
|
|
537
|
-
this.logLine?.("pass2", this.password);
|
|
538
601
|
this.sendNextRequest();
|
|
539
602
|
}
|
|
540
603
|
else {
|
|
@@ -591,8 +654,6 @@ Client.prototype.sendNextRequest = async function (di) {
|
|
|
591
654
|
request += this.makeHead("POST", "/pair-verify", "", true);
|
|
592
655
|
request += 'Content-Type: application/octet-stream\r\n';
|
|
593
656
|
this.pair_verify_1_verifier = ATVAuthenticator.verifier(this.authSecret);
|
|
594
|
-
if (this.debug)
|
|
595
|
-
this.logLine?.(this.authSecret);
|
|
596
657
|
request += 'Content-Length:' + Buffer.byteLength(this.pair_verify_1_verifier.verifierBody) + '\r\n\r\n';
|
|
597
658
|
this.socket.write(Buffer.concat([Buffer.from(request, 'utf-8'), this.pair_verify_1_verifier.verifierBody]));
|
|
598
659
|
request = '';
|
|
@@ -846,8 +907,7 @@ Client.prototype.sendNextRequest = async function (di) {
|
|
|
846
907
|
catch (e) { }
|
|
847
908
|
try {
|
|
848
909
|
this.timingsocket = dgram.createSocket({ type: 'udp4', reuseAddr: true });
|
|
849
|
-
|
|
850
|
-
this.timingsocket.on('message', function (msg, rinfo) {
|
|
910
|
+
this.timingsocket.on('message', (msg, rinfo) => {
|
|
851
911
|
// only listen and respond on own hosts
|
|
852
912
|
// if (this.hosts.indexOf(rinfo.address) < 0) return;
|
|
853
913
|
var ts1 = msg.readUInt32BE(24);
|
|
@@ -861,8 +921,8 @@ Client.prototype.sendNextRequest = async function (di) {
|
|
|
861
921
|
var ntpTime = ntp.timestamp();
|
|
862
922
|
ntpTime.copy(reply, 16);
|
|
863
923
|
ntpTime.copy(reply, 24);
|
|
864
|
-
|
|
865
|
-
|
|
924
|
+
this.timingsocket.send(reply, 0, reply.length, rinfo.port, rinfo.address);
|
|
925
|
+
this.logLine?.('timing socket pinged', rinfo.port, rinfo.address);
|
|
866
926
|
});
|
|
867
927
|
this.timingsocket.bind(this.timingPort, this.socket.address().address);
|
|
868
928
|
}
|
|
@@ -916,9 +976,8 @@ Client.prototype.sendNextRequest = async function (di) {
|
|
|
916
976
|
}] });
|
|
917
977
|
request += 'Content-Length: ' + Buffer.byteLength(setap2) + '\r\n\r\n';
|
|
918
978
|
this.controlsocket = dgram.createSocket({ type: 'udp4', reuseAddr: true });
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
self.logLine?.('controlsocket.data', msg);
|
|
979
|
+
this.controlsocket.on('message', (msg) => {
|
|
980
|
+
this.logLine?.('controlsocket.data', msg);
|
|
922
981
|
});
|
|
923
982
|
this.controlsocket.bind(this.controlPort, this.socket.address().address);
|
|
924
983
|
let s2ct = this.credentials.encrypt(Buffer.concat([Buffer.from(request, 'utf-8'), setap2]));
|
|
@@ -928,18 +987,29 @@ Client.prototype.sendNextRequest = async function (di) {
|
|
|
928
987
|
case RECORD:
|
|
929
988
|
//this.logLine?.(request);
|
|
930
989
|
if (this.airplay2) {
|
|
931
|
-
this.eventsocket = net.connect(this.eventPort, this.hostip, async function () {
|
|
932
|
-
});
|
|
933
|
-
this.eventsocket.on('data', function (data) {
|
|
934
|
-
self.logLine?.('eventsocket.data', data);
|
|
935
|
-
self.logLine?.('eventsocket.data2', self.event_credentials.decrypt(data).toString());
|
|
936
|
-
});
|
|
937
|
-
this.eventsocket.on('error', function (err) {
|
|
938
|
-
self.logLine?.('eventsocket.error', err);
|
|
939
|
-
});
|
|
940
990
|
this.event_credentials = new Credentials("sdsds", "", "", "", this.seed);
|
|
941
991
|
this.event_credentials.writeKey = enc.HKDF("sha512", Buffer.from("Events-Salt"), this.srp.computeK(), Buffer.from("Events-Read-Encryption-Key"), 32);
|
|
942
992
|
this.event_credentials.readKey = enc.HKDF("sha512", Buffer.from("Events-Salt"), this.srp.computeK(), Buffer.from("Events-Write-Encryption-Key"), 32);
|
|
993
|
+
this.eventsocket = net.connect(this.eventPort, this.hostip, async () => {
|
|
994
|
+
});
|
|
995
|
+
this.eventsocket.on('data', (data) => {
|
|
996
|
+
if (this.debug) {
|
|
997
|
+
this.logLine?.('eventsocket.data', data);
|
|
998
|
+
try {
|
|
999
|
+
const decrypted = this.event_credentials?.decrypt(data);
|
|
1000
|
+
if (decrypted) {
|
|
1001
|
+
this.logLine?.('eventsocket.data2', decrypted.toString());
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
catch (err) {
|
|
1005
|
+
this.logLine?.('eventsocket.decrypt.error', err);
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
});
|
|
1009
|
+
this.eventsocket.on('error', (err) => {
|
|
1010
|
+
if (this.debug)
|
|
1011
|
+
this.logLine?.('eventsocket.error', err);
|
|
1012
|
+
});
|
|
943
1013
|
}
|
|
944
1014
|
if (this.airplay2 != null && this.credentials != null) {
|
|
945
1015
|
// this.controlsocket.close();
|
|
@@ -1117,19 +1187,19 @@ function parseAuthenticate(auth, field) {
|
|
|
1117
1187
|
Client.prototype.processData = function (blob, rawData) {
|
|
1118
1188
|
this.logLine?.('Receiving request:', this.hostip, rtsp_methods[this.status + 1]);
|
|
1119
1189
|
var response = parseResponse2(blob, this), headers = response.headers || {};
|
|
1120
|
-
if (this.debug
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
this.logLine?.("incoming-res:
|
|
1190
|
+
if (this.debug) {
|
|
1191
|
+
try {
|
|
1192
|
+
if ((rawData.toString()).includes("bplist00")) {
|
|
1193
|
+
const buf = Buffer.from(rawData).slice(rawData.length - parseInt(headers['Content-Length']), rawData.length);
|
|
1194
|
+
const bplist = bplistParser.parseBuffer(buf);
|
|
1195
|
+
this.logLine?.("incoming-res:", JSON.stringify(bplist));
|
|
1126
1196
|
}
|
|
1127
|
-
|
|
1128
|
-
this.logLine?.("incoming-res:
|
|
1197
|
+
else {
|
|
1198
|
+
this.logLine?.("incoming-res:", { code: response.code, length: rawData.length });
|
|
1129
1199
|
}
|
|
1130
1200
|
}
|
|
1131
|
-
|
|
1132
|
-
this.logLine?.("incoming-res:
|
|
1201
|
+
catch {
|
|
1202
|
+
this.logLine?.("incoming-res:", { code: response.code, length: rawData.length });
|
|
1133
1203
|
}
|
|
1134
1204
|
}
|
|
1135
1205
|
if (this.status != OPTIONS && this.status != OPTIONS2 && this.mode == 0) {
|
|
@@ -1363,7 +1433,6 @@ Client.prototype.processData = function (blob, rawData) {
|
|
|
1363
1433
|
this.credentials.writeKey = enc.HKDF("sha512", Buffer.from("Control-Salt"), this.srp.computeK(), Buffer.from("Control-Write-Encryption-Key"), 32);
|
|
1364
1434
|
this.credentials.readKey = enc.HKDF("sha512", Buffer.from("Control-Salt"), this.srp.computeK(), Buffer.from("Control-Read-Encryption-Key"), 32);
|
|
1365
1435
|
this.encryptedChannel = true;
|
|
1366
|
-
this.logLine?.(this.srp.computeK());
|
|
1367
1436
|
this.status = SETUP_AP2_1;
|
|
1368
1437
|
}
|
|
1369
1438
|
else {
|
|
@@ -143,7 +143,11 @@ class UDPServers extends node_events_1.EventEmitter {
|
|
|
143
143
|
const ntpTime = ntp_1.default.timestamp();
|
|
144
144
|
ntpTime.copy(packet, 8);
|
|
145
145
|
packet.writeUInt32BE((0, numUtil_1.low32)(seq * config_1.default.frames_per_packet + config_1.default.sampling_rate * 2), 16);
|
|
146
|
-
|
|
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;
|
package/dist/esm/utils/config.js
CHANGED
|
@@ -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:
|
|
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/dist/utils/config.d.ts
CHANGED
|
@@ -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;
|
package/dist/utils/config.js
CHANGED
|
@@ -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:
|
|
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',
|