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